package server import ( "net/http" "github.com/go-chi/chi/v5" "git.wntrmute.dev/kyle/mcr/internal/auth" "git.wntrmute.dev/kyle/mcr/internal/policy" ) // PolicyEvaluator abstracts the policy engine for testability. type PolicyEvaluator interface { Evaluate(input policy.PolicyInput) (policy.Effect, *policy.Rule) } // AuditFunc is an optional callback for recording policy deny audit events. // It follows the same signature as db.WriteAuditEvent but without an error // return — audit failures should not block request processing. type AuditFunc func(eventType, actorID, repository, digest, ip string, details map[string]string) // RequirePolicy returns middleware that checks the policy engine for the // given action. Claims must already be in the context (set by RequireAuth). // The repository name is extracted from the chi "name" URL parameter; // global operations (catalog, version check) have an empty repository. func RequirePolicy(evaluator PolicyEvaluator, action policy.Action, auditFn AuditFunc) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { claims := auth.ClaimsFromContext(r.Context()) if claims == nil { writeOCIError(w, "UNAUTHORIZED", http.StatusUnauthorized, "authentication required") return } input := policy.PolicyInput{ Subject: claims.Subject, AccountType: claims.AccountType, Roles: claims.Roles, Action: action, Repository: chi.URLParam(r, "name"), } effect, _ := evaluator.Evaluate(input) if effect == policy.Deny { if auditFn != nil { auditFn("policy_deny", claims.Subject, input.Repository, "", r.RemoteAddr, map[string]string{ "action": string(action), }) } writeOCIError(w, "DENIED", http.StatusForbidden, "access denied by policy") return } next.ServeHTTP(w, r) }) } }