{"contents":"package githttp\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/bluesky-social/indigo/atproto/syntax\"\n\n\t\"tangled.org/core/rbac\"\n\t\"tangled.org/core/xrpc/serviceauth\"\n)\n\ntype contextKey string\n\nconst actorDidKey contextKey = \"actorDid\"\n\n// BasicToBearer converts git's Basic Auth credentials to Bearer auth.\n// Git credential helpers provide tokens as the password field in Basic Auth.\n// This middleware extracts the password and sets it as a Bearer token so that\n// the downstream VerifyServiceAuth middleware can validate it.\nfunc BasicToBearer() func(http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif _, password, ok := r.BasicAuth(); ok \u0026\u0026 password != \"\" {\n\t\t\t\tr.Header.Set(\"Authorization\", \"Bearer \"+password)\n\t\t\t}\n\t\t\tnext.ServeHTTP(w, r)\n\t\t})\n\t}\n}\n\n// ConditionalReceiveAuth applies auth validation only when the service query\n// parameter is git-receive-pack (push discovery). Clone/fetch discovery\n// (git-upload-pack) passes through without auth.\nfunc ConditionalReceiveAuth(sa *serviceauth.ServiceAuth, logger *slog.Logger) func(http.HandlerFunc) http.HandlerFunc {\n\treturn func(next http.HandlerFunc) http.HandlerFunc {\n\t\tauthed := gitAuthChallenge(sa.VerifyServiceAuth(next))\n\t\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.URL.Query().Get(\"service\") == \"git-receive-pack\" {\n\t\t\t\tauthed.ServeHTTP(w, r)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tnext.ServeHTTP(w, r)\n\t\t}\n\t}\n}\n\n// RequireAuth is middleware that validates Bearer JWT for write operations.\n// It extracts the actor DID and stores it in the request context.\nfunc RequireAuth(sa *serviceauth.ServiceAuth, logger *slog.Logger) func(http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn gitAuthChallenge(sa.VerifyServiceAuth(next))\n\t}\n}\n\n// gitAuthChallenge wraps an auth handler to return 401 + WWW-Authenticate\n// when no credentials are provided. Git credential helpers only activate\n// on 401 responses, not 403.\nfunc gitAuthChallenge(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tauth := r.Header.Get(\"Authorization\")\n\t\tif auth == \"\" {\n\t\t\tw.Header().Set(\"WWW-Authenticate\", `Basic realm=\"tangled\"`)\n\t\t\thttp.Error(w, \"authentication required\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\n// RequirePush is middleware that checks if the authenticated user has push permission.\nfunc RequirePush(enforcer *rbac.Enforcer, logger *slog.Logger) func(http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tactorDid, ok := r.Context().Value(serviceauth.ActorDid).(syntax.DID)\n\t\t\tif !ok {\n\t\t\t\trejectPush(w, \"authentication required\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdid := actorDid.String()\n\n\t\t\t// Store in our context key too for convenience\n\t\t\tr = r.WithContext(context.WithValue(r.Context(), actorDidKey, did))\n\n\t\t\t// Extract did/name from URL for permission check\n\t\t\t// URL pattern: /{did}/{name}/git-receive-pack\n\t\t\tparts := strings.Split(strings.Trim(r.URL.Path, \"/\"), \"/\")\n\t\t\tif len(parts) \u003c 3 {\n\t\t\t\trejectPush(w, \"invalid path\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\trepoDid := parts[0]\n\t\t\tname := parts[1]\n\t\t\trepoPath := repoDid + \"/\" + name\n\n\t\t\tok, err := enforcer.IsPushAllowed(did, rbac.ThisServer, repoPath)\n\t\t\tif err != nil || !ok {\n\t\t\t\tlogger.Warn(\"push denied\", \"actor\", did, \"repo\", repoPath, \"err\", err)\n\t\t\t\trejectPush(w, \"push access denied\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tnext.ServeHTTP(w, r)\n\t\t})\n\t}\n}\n","path":"githttp/auth.go","ref":"main"}