package rbac import ( "context" "encoding/json" "code.nochebuena.dev/einherjar/contracts/security" ) var _ security.PermissionProvider = (*claimsPermissionProvider)(nil) type claimsPermissionProvider struct { claimsKey string getClaims func(context.Context) map[string]any } // NewClaimsPermissionProvider returns a PermissionProvider that reads // pre-computed permission bitmasks from JWT claims stored in context. // // claimsKey is the top-level claim key whose value is a map[resource]int64. // Falls back to wildcard key "*" if the specific resource is absent. // Zero DB calls — fastest permission resolution for read-heavy APIs. // // getClaims is the function used to retrieve claims from context. // Pass authmw.GetClaims when using the authmw middleware chain. // // Flat claims format: claims["perms"]["orders"] = 7 // For multi-tenant permission isolation, use [NewCachedPermissionProvider] // wrapping a DB lookup — it automatically scopes cache keys per tenant. func NewClaimsPermissionProvider(claimsKey string, getClaims func(context.Context) map[string]any) security.PermissionProvider { return &claimsPermissionProvider{claimsKey: claimsKey, getClaims: getClaims} } func (p *claimsPermissionProvider) ResolveMask(ctx context.Context, _, resource string) (security.PermissionMask, error) { claims := p.getClaims(ctx) if claims == nil { return 0, nil } raw, ok := claims[p.claimsKey] if !ok { return 0, nil } resourceMap, ok := raw.(map[string]any) if !ok { return 0, nil } mask := extractMask(resourceMap, resource) if mask == 0 { mask = extractMask(resourceMap, "*") } return security.PermissionMask(mask), nil } func extractMask(m map[string]any, key string) int64 { v, ok := m[key] if !ok { return 0 } switch n := v.(type) { case int64: return n case float64: return int64(n) case json.Number: i, err := n.Int64() if err != nil { return 0 } return i } return 0 }