// authentication and authorization module package authz import ( "context" "encoding/hex" "log/slog" "net/http" "strings" "git.ofmax.li/go-git-server/internal/admin" "golang.org/x/crypto/bcrypt" ) // AuthzContextKey key used to store urn of user in context type AuthzContextKey string var ( AuthzUrnKey AuthzContextKey = "goGitAuthzUrn" ) func Authentication(authMap *SafeTokenMap, identityMap *IdentityMap, next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { slog.Info("access request recv") u, p, ok := req.BasicAuth() if !ok { u = "anon" ctx := context.WithValue(req.Context(), AuthzUrnKey, u) next.ServeHTTP(rw, req.WithContext(ctx)) return } // Look up the access ID from the provided username accessID, exists := identityMap.GetID(FriendlyName(u)) if !exists { slog.Info("failed access", "username", u) http.Error(rw, "Bad Request", http.StatusForbidden) return } hash, ok := authMap.Get(accessID) if !ok { slog.Info("failed access", "access_id", accessID) http.Error(rw, "Bad Request", http.StatusForbidden) return } token, err := hex.DecodeString(p) if err != nil { http.Error(rw, "Bad Request", http.StatusBadRequest) return } if err := bcrypt.CompareHashAndPassword([]byte(hash), token); err != nil { slog.Info("bad token for user", "access_id", accessID) http.Error(rw, "Bad Request", http.StatusForbidden) return } // Store the friendly name with appropriate prefix in context friendlyName, _ := identityMap.GetName(accessID) prefix := "uid:" // default to user if strings.HasPrefix(string(friendlyName), "bot+") { prefix = "aid:" } urn := prefix + string(friendlyName) ctx := context.WithValue(req.Context(), AuthzUrnKey, urn) slog.Info("access request granted", "urn", urn) next.ServeHTTP(rw, req.WithContext(ctx)) }) } // Authorization middleware to enforce authoirzation of all requests. func Authorization(adminSvc *admin.Servicer, next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { ctx := req.Context() urn, ok := ctx.Value(AuthzUrnKey).(string) if !ok || urn == "" { http.Error(rw, "Bad Request", http.StatusBadRequest) return } repo := req.URL.Path action := req.Method ok, err := adminSvc.Enforce(urn, repo, action) if err != nil { slog.Info("error running enforce", "error", err) http.Error(rw, "Bad Request", http.StatusBadRequest) return } if !ok && urn == "anon" { rw.Header().Set("WWW-Authenticate", `Basic realm="git"`) http.Error(rw, "Authentication Required", http.StatusUnauthorized) return } else if !ok { slog.Info("Not Authorized", "urn", urn, "repo", repo) http.Error(rw, "Access denied", http.StatusForbidden) return } slog.Debug("Access Attempt", "action", action, "repo", repo) next.ServeHTTP(rw, req.WithContext(ctx)) }) }