package security import ( "net/http" "github.com/missionworkforce/missionmatch/internal/platform/tenant" ) // RequireCapability gates a route on a capability. The role is read from the validated session in // context (set by the auth middleware), never from the request body or a header, so authorization // cannot be spoofed and is enforced at the API rather than by hiding a control in the UI. A caller // without the capability gets 403 before the handler runs. func RequireCapability(cap Capability) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p, ok := tenant.UserFromContext(r.Context()) if !ok || !Has(Role(p.Role), cap) { http.Error(w, "forbidden", http.StatusForbidden) return } next.ServeHTTP(w, r) }) } } // RequirePlatform gates a PLATFORM surface on the operator role, amendment 6 section 2 item 2. The // platform surfaces (Organizations, cross tenant Users, Curation, Federation, and platform Metrics) act // across every tenant at once, so they belong to the platform plane and to nothing else. // // It refuses with 404, not 403, and the distinction is the point. A 403 confirms the surface EXISTS and // tells a tenant's top seat that a cross tenant control plane is there to be found; it is also exactly how // this defect presented, a client shown platform shaped controls that refused her. A platform surface must // be indistinguishable from a path that was never built. This matches RequireTool, which refuses an // unentitled product the same way and for the same reason. // // The check is on the role in the validated session, never a header or a body, so it cannot be spoofed. // Superuser does NOT pass: amendment 6 narrowed the tenant top seat to within its tenant, and a platform // surface is not tenant scoped by definition. func RequirePlatform() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p, ok := tenant.UserFromContext(r.Context()) if !ok || Role(p.Role) != RoleOperator { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"error":{"code":"not_found","message":"not found"}}`)) return } next.ServeHTTP(w, r) }) } } // RequireRole gates a route on a minimum role in the hierarchy. func RequireRole(min Role) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p, ok := tenant.UserFromContext(r.Context()) if !ok || !AtLeast(Role(p.Role), min) { http.Error(w, "forbidden", http.StatusForbidden) return } next.ServeHTTP(w, r) }) } }