97 lines
3.1 KiB
Go
97 lines
3.1 KiB
Go
|
|
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)
|
||
|
|
}
|
||
|
|
}
|