2026-05-07 21:37:25 -06:00
|
|
|
package httpauth
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-18 14:14:04 -06:00
|
|
|
"encoding/json"
|
2026-05-07 21:37:25 -06:00
|
|
|
"net/http"
|
|
|
|
|
|
|
|
|
|
"code.nochebuena.dev/go/rbac"
|
2026-05-18 14:14:04 -06:00
|
|
|
"code.nochebuena.dev/go/xerrors"
|
2026-05-07 21:37:25 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// AuthzMiddleware reads the rbac.Identity from context (set by EnrichmentMiddleware)
|
|
|
|
|
// and gates the request against the required permission on resource.
|
|
|
|
|
// Uses rbac.PermissionProvider directly — no local redefinition of the interface.
|
|
|
|
|
// Returns 401 if no identity is in context.
|
|
|
|
|
// Returns 403 if the identity lacks the required permission or if the provider errors.
|
|
|
|
|
func AuthzMiddleware(provider rbac.PermissionProvider, resource string, required rbac.Permission) func(http.Handler) http.Handler {
|
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
identity, ok := rbac.FromContext(r.Context())
|
|
|
|
|
if !ok {
|
2026-05-18 14:14:04 -06:00
|
|
|
WriteJSONError(w, http.StatusUnauthorized, string(xerrors.ErrUnauthorized), "unauthorized")
|
2026-05-07 21:37:25 -06:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mask, err := provider.ResolveMask(r.Context(), identity.UID, resource)
|
|
|
|
|
if err != nil || !mask.Has(required) {
|
2026-05-18 14:14:04 -06:00
|
|
|
WriteJSONError(w, http.StatusForbidden, string(xerrors.ErrPermissionDenied), "forbidden")
|
2026-05-07 21:37:25 -06:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-18 14:14:04 -06:00
|
|
|
|
|
|
|
|
// WriteJSONError writes a structured JSON error body matching the httputil.Error format.
|
|
|
|
|
// Used by all httpauth-* provider packages to ensure a consistent error format across
|
|
|
|
|
// the full middleware stack.
|
|
|
|
|
func WriteJSONError(w http.ResponseWriter, status int, code, message string) {
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
w.WriteHeader(status)
|
|
|
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
|
|
|
"code": code,
|
|
|
|
|
"message": message,
|
|
|
|
|
})
|
|
|
|
|
}
|