package middleware import ( "net/http" "gateway/internal/library/actor" errs "gateway/internal/library/errors" "gateway/internal/library/errors/code" domperm "gateway/internal/model/permission/domain/usecase" "gateway/internal/response" "github.com/zeromicro/go-zero/core/logx" ) // CasbinRBACOptions tunes the enforcement middleware. type CasbinRBACOptions struct { // PlatformAdminRoleKey short-circuits enforcement when the actor's // auth context flags them as platform admin (handled upstream); the // value is the role key seeded for that role (e.g. // "platform_super_admin"). Empty disables the bypass. PlatformAdminRoleKey string // SkipPaths is an exact-match allowlist (e.g. /api/v1/health). // Useful for opting out specific routes when the middleware is // mounted on a wider group. SkipPaths map[string]struct{} } // CasbinRBACMiddleware enforces (tenant, uid, path, method) against // the Casbin policy loaded by the permission module. It MUST be // preceded by AuthJWTMiddleware in the chain so the actor is already // present on the request context (via library/actor). // // Mounted via @server(middleware: AuthJWT,CasbinRBAC). Missing actor // is treated as a hard 401 (the chain order guarantees AuthJWT runs // first); a successful actor with no matching policy returns 403 // (28505000 in Permission scope). // // File name follows goctl's stringx convention (`casbinrbac_middleware.go`) // so `make gen-api` sees it as already-generated and never overwrites // the implementation. type CasbinRBACMiddleware struct { rbac domperm.RBACUseCase opts CasbinRBACOptions } // NewCasbinRBACMiddleware wires the middleware with the permission // module's RBACUseCase. A nil rbac transparently allows requests // through (useful while the module is being rolled out). func NewCasbinRBACMiddleware(rbac domperm.RBACUseCase, opts CasbinRBACOptions) *CasbinRBACMiddleware { if opts.SkipPaths == nil { opts.SkipPaths = map[string]struct{}{} } return &CasbinRBACMiddleware{rbac: rbac, opts: opts} } // Handle implements the go-zero rest.Middleware signature. func (m *CasbinRBACMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { bld := errs.For(code.Permission) return func(w http.ResponseWriter, r *http.Request) { if m.rbac == nil { next(w, r) return } if _, ok := m.opts.SkipPaths[r.URL.Path]; ok { next(w, r) return } caller, err := actor.ActorFromContext(r.Context()) if err != nil { response.Write(r.Context(), w, nil, bld.AuthUnauthorized("missing actor for rbac check").WithCause(err)) return } result, err := m.rbac.Check(r.Context(), &domperm.CheckRequest{ TenantID: caller.TenantID, UID: caller.UID, Path: r.URL.Path, Method: r.Method, }) if err != nil { logx.WithContext(r.Context()).Errorf( "casbin: enforce error tenant=%s uid=%s path=%s method=%s: %v", caller.TenantID, caller.UID, r.URL.Path, r.Method, err) response.Write(r.Context(), w, nil, bld.SysInternal("casbin enforce failed").WithCause(err)) return } if !result.Allow { response.Write(r.Context(), w, nil, bld.AuthForbidden("rbac denied")) return } next(w, r) } }