From 9317407369c72a43c5f2f0bdf8f006169669cdf8 Mon Sep 17 00:00:00 2001 From: Max Resnick Date: Fri, 11 Nov 2022 15:28:22 -0800 Subject: re-org --- cmd/main.go | 164 ++++++++++++++++++++++++++++++++++++++++++++++ internal/authz/handler.go | 62 ++++++++++++++++++ internal/authz/model.go | 62 ++++++++++++++++++ internal/git/handler.go | 27 ++++++++ main.go | 164 ---------------------------------------------- 5 files changed, 315 insertions(+), 164 deletions(-) create mode 100644 cmd/main.go create mode 100644 internal/authz/handler.go create mode 100644 internal/authz/model.go create mode 100644 internal/git/handler.go delete mode 100644 main.go diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..3f5cedf --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,164 @@ +package main + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/csv" + "flag" + "fmt" + "log" + "math/big" + "net/http" + "net/http/cgi" + "os" + + "github.com/casbin/casbin/v2" + "golang.org/x/crypto/bcrypt" +) + +var ( + reposDir = flag.String("r", "./repos", "Directory containing git repositories") + backendCommand = flag.String("c", "git http-backend", "CGI binary to execute") + addr = flag.String("l", ":8080", "Address/port to listen on") + newToken = flag.Bool("t", false, "make a new token") +) + +// LoadTokens load tokens from a csv into a map +func LoadTokens() (map[string]string, error) { + tokenMap := make(map[string]string) + // TODO this should be configurable + contents, err := os.Open("tokens.csv") + if err != nil { + fmt.Println("File reading error", err) + return tokenMap, err + } + defer contents.Close() + r := csv.NewReader(contents) + tokens, err := r.ReadAll() + if err != nil { + fmt.Println("File reading error", err) + return tokenMap, err + } + for _, acctToken := range tokens { + acct, hash := acctToken[0], acctToken[1] + tokenMap[acct] = hash + } + return tokenMap, err +} + +// NewToken generate a new token +func NewToken() (string, string, error) { + tokenBytes := make([]byte, 28) + for i := range tokenBytes { + max := big.NewInt(int64(255)) + randInt, err := rand.Int(rand.Reader, max) + if err != nil { + return "", "", err + } + tokenBytes[i] = uint8(randInt.Int64()) + } + hashBytes, err := bcrypt.GenerateFromPassword(tokenBytes, bcrypt.DefaultCost) + if err != nil { + return "", "", err + } + token := base64.URLEncoding.EncodeToString(tokenBytes) + hash := string(hashBytes) + return token, hash, nil +} + +// GitHttpBackendHandler a handler for git cgi +func GitHttpBackendHandler(reposDir, backendCommand string) http.Handler { + projectDirEnv := fmt.Sprintf("GIT_PROJECT_ROOT=%v", reposDir) + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + uid := ctx.Value("urn") + git := &cgi.Handler{ + Path: "/bin/sh", + Args: []string{"-c", backendCommand}, + Dir: ".", + Env: []string{ + projectDirEnv, + "GIT_HTTP_EXPORT_ALL=1", + fmt.Sprintf("REMOTE_USER=%s", uid), + }, + } + git.ServeHTTP(rw, req) + }) +} + +// Authentication middleware to enforce authentication of all requests. +func Authentication(authMap map[string]string, next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + u, p, ok := req.BasicAuth() + if !ok { + rw.Header().Set("WWW-Authenticate", `Basic realm="git"`) + http.Error(rw, "Authentication Required", 401) + return + } + urn := fmt.Sprintf("uid:%s", u) + hash, ok := authMap[urn] + if !ok { + http.Error(rw, "Bad Request", 400) + return + } + token, err := base64.URLEncoding.DecodeString(p) + if err != nil { + http.Error(rw, "Bad Request", 400) + return + } + if err := bcrypt.CompareHashAndPassword([]byte(hash), token); err != nil { + http.Error(rw, "Bad Request", 400) + return + } + ctx := context.WithValue(req.Context(), "urn", urn) + next.ServeHTTP(rw, req.WithContext(ctx)) + }) +} + +func Authorization(enf *casbin.Enforcer, next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + urn := ctx.Value("urn") + repo := req.URL.Path + action := req.Method + // defer req.Body.Close() + // body, _ := io.ReadAll(req.Body) + // log.Printf("%s", body) + ok, err := enf.Enforce(urn, repo, action) + if err != nil { + log.Printf("error running enforce %s", err) + http.Error(rw, "Bad Request", http.StatusBadRequest) + } + if !ok { + log.Printf("Access denied") + http.Error(rw, "Access denied", http.StatusUnauthorized) + } + log.Printf("Method %s Url %s", action, repo) + next.ServeHTTP(rw, req.WithContext(ctx)) + }) +} + +func main() { + // TODO this should be configurable + enf, _ := casbin.NewEnforcer("./auth_model.ini", "./policy.csv") + flag.Parse() + if *newToken { + token, hash, err := NewToken() + if err != nil { + log.Fatal(err) + } + fmt.Printf("token: %s\nhash: %s\n", token, hash) + return + } + tokens, err := LoadTokens() + if err != nil { + log.Fatal(err) + } + router := http.NewServeMux() + // TODO we don't want to use a global + // de-reference args + router.Handle("/", GitHttpBackendHandler(*reposDir, *backendCommand)) + mux := Authentication(tokens, Authorization(enf, router)) + log.Fatal(http.ListenAndServe(":8080", mux)) +} diff --git a/internal/authz/handler.go b/internal/authz/handler.go new file mode 100644 index 0000000..e47dd33 --- /dev/null +++ b/internal/authz/handler.go @@ -0,0 +1,62 @@ +package authz + +import ( + "context" + "encoding/base64" + "fmt" + "log" + "net/http" + + "github.com/casbin/casbin/v2" + "golang.org/x/crypto/bcrypt" +) + +// Authentication middleware to enforce authentication of all requests. +func Authentication(authMap TokenMap, next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + u, p, ok := req.BasicAuth() + if !ok { + rw.Header().Set("WWW-Authenticate", `Basic realm="git"`) + http.Error(rw, "Authentication Required", 401) + return + } + urn := fmt.Sprintf("uid:%s", u) + hash, ok := authMap[urn] + if !ok { + http.Error(rw, "Bad Request", 400) + return + } + token, err := base64.URLEncoding.DecodeString(p) + if err != nil { + http.Error(rw, "Bad Request", 400) + return + } + if err := bcrypt.CompareHashAndPassword([]byte(hash), token); err != nil { + http.Error(rw, "Bad Request", 400) + return + } + ctx := context.WithValue(req.Context(), "urn", urn) + next.ServeHTTP(rw, req.WithContext(ctx)) + }) +} + +// Authorization middleware to enforce authoirzation of all requests. +func Authorization(enf *casbin.Enforcer, next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + urn := ctx.Value("urn") + repo := req.URL.Path + action := req.Method + ok, err := enf.Enforce(urn, repo, action) + if err != nil { + log.Printf("error running enforce %s", err) + http.Error(rw, "Bad Request", http.StatusBadRequest) + } + if !ok { + log.Printf("Access denied") + http.Error(rw, "Access denied", http.StatusUnauthorized) + } + log.Printf("Method %s Url %s", action, repo) + next.ServeHTTP(rw, req.WithContext(ctx)) + }) +} diff --git a/internal/authz/model.go b/internal/authz/model.go new file mode 100644 index 0000000..cf9c952 --- /dev/null +++ b/internal/authz/model.go @@ -0,0 +1,62 @@ +package authz + +import ( + "crypto/rand" + "encoding/base64" + "encoding/csv" + "fmt" + "math/big" + "os" + + "golang.org/x/crypto/bcrypt" +) + +// NewTokenMap create a new token map +func NewTokenMap() TokenMap { + return TokenMap{} +} + +// TokenMap a map of username,hash +type TokenMap map[string]string + +// LoadTokens load tokens from a csv into a map +func (tm TokenMap) LoadTokensFromFile(path string) error { + // TODO this should be configurable + contents, err := os.Open(path) + if err != nil { + fmt.Println("File reading error", err) + return err + } + defer contents.Close() + r := csv.NewReader(contents) + tokens, err := r.ReadAll() + if err != nil { + fmt.Println("File reading error", err) + return err + } + for _, acctToken := range tokens { + acct, hash := acctToken[0], acctToken[1] + tm[acct] = hash + } + return err +} + +// GenerateNewToken generate a new token +func GenerateNewToken() (string, string, error) { + tokenBytes := make([]byte, 28) + for i := range tokenBytes { + max := big.NewInt(int64(255)) + randInt, err := rand.Int(rand.Reader, max) + if err != nil { + return "", "", err + } + tokenBytes[i] = uint8(randInt.Int64()) + } + hashBytes, err := bcrypt.GenerateFromPassword(tokenBytes, bcrypt.DefaultCost) + if err != nil { + return "", "", err + } + token := base64.URLEncoding.EncodeToString(tokenBytes) + hash := string(hashBytes) + return token, hash, nil +} diff --git a/internal/git/handler.go b/internal/git/handler.go new file mode 100644 index 0000000..5a3b3e6 --- /dev/null +++ b/internal/git/handler.go @@ -0,0 +1,27 @@ +package git + +import ( + "fmt" + "net/http" + "net/http/cgi" +) + +// GitHttpBackendHandler a handler for git cgi +func GitHttpBackendHandler(reposDir, backendCommand string) http.Handler { + projectDirEnv := fmt.Sprintf("GIT_PROJECT_ROOT=%v", reposDir) + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + uid := ctx.Value("urn") + gitBackendHandler := &cgi.Handler{ + Path: "/bin/sh", + Args: []string{"-c", backendCommand}, + Dir: ".", + Env: []string{ + projectDirEnv, + "GIT_HTTP_EXPORT_ALL=1", + fmt.Sprintf("REMOTE_USER=%s", uid), + }, + } + gitBackendHandler.ServeHTTP(rw, req) + }) +} diff --git a/main.go b/main.go deleted file mode 100644 index 3f5cedf..0000000 --- a/main.go +++ /dev/null @@ -1,164 +0,0 @@ -package main - -import ( - "context" - "crypto/rand" - "encoding/base64" - "encoding/csv" - "flag" - "fmt" - "log" - "math/big" - "net/http" - "net/http/cgi" - "os" - - "github.com/casbin/casbin/v2" - "golang.org/x/crypto/bcrypt" -) - -var ( - reposDir = flag.String("r", "./repos", "Directory containing git repositories") - backendCommand = flag.String("c", "git http-backend", "CGI binary to execute") - addr = flag.String("l", ":8080", "Address/port to listen on") - newToken = flag.Bool("t", false, "make a new token") -) - -// LoadTokens load tokens from a csv into a map -func LoadTokens() (map[string]string, error) { - tokenMap := make(map[string]string) - // TODO this should be configurable - contents, err := os.Open("tokens.csv") - if err != nil { - fmt.Println("File reading error", err) - return tokenMap, err - } - defer contents.Close() - r := csv.NewReader(contents) - tokens, err := r.ReadAll() - if err != nil { - fmt.Println("File reading error", err) - return tokenMap, err - } - for _, acctToken := range tokens { - acct, hash := acctToken[0], acctToken[1] - tokenMap[acct] = hash - } - return tokenMap, err -} - -// NewToken generate a new token -func NewToken() (string, string, error) { - tokenBytes := make([]byte, 28) - for i := range tokenBytes { - max := big.NewInt(int64(255)) - randInt, err := rand.Int(rand.Reader, max) - if err != nil { - return "", "", err - } - tokenBytes[i] = uint8(randInt.Int64()) - } - hashBytes, err := bcrypt.GenerateFromPassword(tokenBytes, bcrypt.DefaultCost) - if err != nil { - return "", "", err - } - token := base64.URLEncoding.EncodeToString(tokenBytes) - hash := string(hashBytes) - return token, hash, nil -} - -// GitHttpBackendHandler a handler for git cgi -func GitHttpBackendHandler(reposDir, backendCommand string) http.Handler { - projectDirEnv := fmt.Sprintf("GIT_PROJECT_ROOT=%v", reposDir) - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - ctx := req.Context() - uid := ctx.Value("urn") - git := &cgi.Handler{ - Path: "/bin/sh", - Args: []string{"-c", backendCommand}, - Dir: ".", - Env: []string{ - projectDirEnv, - "GIT_HTTP_EXPORT_ALL=1", - fmt.Sprintf("REMOTE_USER=%s", uid), - }, - } - git.ServeHTTP(rw, req) - }) -} - -// Authentication middleware to enforce authentication of all requests. -func Authentication(authMap map[string]string, next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - u, p, ok := req.BasicAuth() - if !ok { - rw.Header().Set("WWW-Authenticate", `Basic realm="git"`) - http.Error(rw, "Authentication Required", 401) - return - } - urn := fmt.Sprintf("uid:%s", u) - hash, ok := authMap[urn] - if !ok { - http.Error(rw, "Bad Request", 400) - return - } - token, err := base64.URLEncoding.DecodeString(p) - if err != nil { - http.Error(rw, "Bad Request", 400) - return - } - if err := bcrypt.CompareHashAndPassword([]byte(hash), token); err != nil { - http.Error(rw, "Bad Request", 400) - return - } - ctx := context.WithValue(req.Context(), "urn", urn) - next.ServeHTTP(rw, req.WithContext(ctx)) - }) -} - -func Authorization(enf *casbin.Enforcer, next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - ctx := req.Context() - urn := ctx.Value("urn") - repo := req.URL.Path - action := req.Method - // defer req.Body.Close() - // body, _ := io.ReadAll(req.Body) - // log.Printf("%s", body) - ok, err := enf.Enforce(urn, repo, action) - if err != nil { - log.Printf("error running enforce %s", err) - http.Error(rw, "Bad Request", http.StatusBadRequest) - } - if !ok { - log.Printf("Access denied") - http.Error(rw, "Access denied", http.StatusUnauthorized) - } - log.Printf("Method %s Url %s", action, repo) - next.ServeHTTP(rw, req.WithContext(ctx)) - }) -} - -func main() { - // TODO this should be configurable - enf, _ := casbin.NewEnforcer("./auth_model.ini", "./policy.csv") - flag.Parse() - if *newToken { - token, hash, err := NewToken() - if err != nil { - log.Fatal(err) - } - fmt.Printf("token: %s\nhash: %s\n", token, hash) - return - } - tokens, err := LoadTokens() - if err != nil { - log.Fatal(err) - } - router := http.NewServeMux() - // TODO we don't want to use a global - // de-reference args - router.Handle("/", GitHttpBackendHandler(*reposDir, *backendCommand)) - mux := Authentication(tokens, Authorization(enf, router)) - log.Fatal(http.ListenAndServe(":8080", mux)) -} -- cgit v1.2.3