package rbac import ( "context" "strings" "github.com/google/uuid" ) // PolicyEngine provides advanced policy evaluation type PolicyEngine struct { service *Service store *Store } // NewPolicyEngine creates a new policy engine func NewPolicyEngine(service *Service, store *Store) *PolicyEngine { return &PolicyEngine{ service: service, store: store, } } // LLMAccessRequest represents a request to access LLM functionality type LLMAccessRequest struct { UserID uuid.UUID TenantID uuid.UUID NamespaceID *uuid.UUID Model string DataCategories []string TokensRequested int Operation string // "query", "completion", "embedding", "analysis" } // LLMAccessResult represents the result of an LLM access check type LLMAccessResult struct { Allowed bool Reason string Policy *LLMPolicy RequirePIIRedaction bool PIIRedactionLevel PIIRedactionLevel MaxTokens int BlockedCategories []string } // EvaluateLLMAccess evaluates whether an LLM request should be allowed func (pe *PolicyEngine) EvaluateLLMAccess(ctx context.Context, req *LLMAccessRequest) (*LLMAccessResult, error) { result := &LLMAccessResult{ Allowed: false, RequirePIIRedaction: true, PIIRedactionLevel: PIIRedactionStrict, MaxTokens: 4000, BlockedCategories: []string{}, } // 1. Check base permission for LLM access hasPermission, err := pe.service.HasAnyPermission(ctx, req.UserID, req.TenantID, req.NamespaceID, []string{ PermissionLLMAll, PermissionLLMQuery, PermissionLLMOwnQuery, }) if err != nil { return result, err } if !hasPermission { result.Reason = "no LLM permission" return result, nil } // 2. Get effective policy policy, err := pe.store.GetEffectiveLLMPolicy(ctx, req.TenantID, req.NamespaceID) if err != nil { return result, err } result.Policy = policy // No policy = use defaults and allow if policy == nil { result.Allowed = true result.Reason = "no policy restrictions" return result, nil } // 3. Check model restrictions if len(policy.AllowedModels) > 0 { modelAllowed := false for _, allowed := range policy.AllowedModels { if allowed == req.Model || strings.HasPrefix(req.Model, allowed+":") || strings.HasPrefix(req.Model, allowed+"-") { modelAllowed = true break } } if !modelAllowed { result.Reason = "model not allowed: " + req.Model return result, nil } } // 4. Check data categories for _, category := range req.DataCategories { // Check if blocked for _, blocked := range policy.BlockedDataCategories { if blocked == category { result.BlockedCategories = append(result.BlockedCategories, category) } } // Check if allowed (if allowlist is defined) if len(policy.AllowedDataCategories) > 0 { allowed := false for _, a := range policy.AllowedDataCategories { if a == category { allowed = true break } } if !allowed { result.BlockedCategories = append(result.BlockedCategories, category) } } } if len(result.BlockedCategories) > 0 { result.Reason = "blocked data categories: " + strings.Join(result.BlockedCategories, ", ") return result, nil } // 5. Check token limits if req.TokensRequested > policy.MaxTokensPerRequest { result.Reason = "tokens requested exceeds limit" return result, nil } result.MaxTokens = policy.MaxTokensPerRequest // 6. Set PII redaction requirements result.RequirePIIRedaction = policy.RequirePIIRedaction result.PIIRedactionLevel = policy.PIIRedactionLevel // All checks passed result.Allowed = true result.Reason = "policy check passed" return result, nil } // NamespaceAccessRequest represents a request to access a namespace type NamespaceAccessRequest struct { UserID uuid.UUID TenantID uuid.UUID NamespaceID uuid.UUID Operation string // "read", "write", "admin" } // NamespaceAccessResult represents the result of a namespace access check type NamespaceAccessResult struct { Allowed bool Reason string DataClassification DataClassification IsolationLevel IsolationLevel Permissions []string } // EvaluateNamespaceAccess evaluates whether a namespace operation should be allowed func (pe *PolicyEngine) EvaluateNamespaceAccess(ctx context.Context, req *NamespaceAccessRequest) (*NamespaceAccessResult, error) { result := &NamespaceAccessResult{ Allowed: false, Permissions: []string{}, } // Get namespace details ns, err := pe.store.GetNamespace(ctx, req.NamespaceID) if err != nil { result.Reason = "namespace not found" return result, ErrNamespaceNotFound } result.DataClassification = ns.DataClassification result.IsolationLevel = ns.IsolationLevel // Check if user has any roles in this namespace userRoles, err := pe.store.GetUserRolesForNamespace(ctx, req.UserID, req.TenantID, &req.NamespaceID) if err != nil { return result, err } if len(userRoles) == 0 { // Check for tenant-wide roles tenantRoles, err := pe.store.GetUserRolesForNamespace(ctx, req.UserID, req.TenantID, nil) if err != nil { return result, err } if len(tenantRoles) == 0 { result.Reason = "no access to namespace" return result, nil } userRoles = tenantRoles } // Collect permissions permSet := make(map[string]bool) for _, ur := range userRoles { for _, perm := range ur.RolePermissions { permSet[perm] = true } } for perm := range permSet { result.Permissions = append(result.Permissions, perm) } // Check operation-specific permission var requiredPermission string switch req.Operation { case "read": requiredPermission = "namespace:read" case "write": requiredPermission = "namespace:write" case "admin": requiredPermission = "namespace:own:admin" default: requiredPermission = "namespace:read" } hasPermission := pe.service.checkPermission(result.Permissions, requiredPermission) if !hasPermission { // Check for broader permissions hasPermission = pe.service.checkPermission(result.Permissions, PermissionComplianceAll) || pe.service.checkPermission(result.Permissions, "namespace:*") } if !hasPermission { result.Reason = "insufficient permissions for operation: " + req.Operation return result, nil } result.Allowed = true result.Reason = "access granted" return result, nil } // DataAccessRequest represents a request to access data type DataAccessRequest struct { UserID uuid.UUID TenantID uuid.UUID NamespaceID *uuid.UUID ResourceType string // "control", "evidence", "audit", "policy" ResourceID *uuid.UUID Operation string // "read", "create", "update", "delete" DataCategories []string } // DataAccessResult represents the result of a data access check type DataAccessResult struct { Allowed bool Reason string RequireAuditLog bool AllowedCategories []string DeniedCategories []string } // EvaluateDataAccess evaluates whether a data operation should be allowed func (pe *PolicyEngine) EvaluateDataAccess(ctx context.Context, req *DataAccessRequest) (*DataAccessResult, error) { result := &DataAccessResult{ Allowed: false, RequireAuditLog: true, AllowedCategories: []string{}, DeniedCategories: []string{}, } // Build required permission based on resource type and operation requiredPermission := req.ResourceType + ":" + req.Operation // Check permission hasPermission, err := pe.service.HasAnyPermission(ctx, req.UserID, req.TenantID, req.NamespaceID, []string{ requiredPermission, req.ResourceType + ":*", PermissionComplianceAll, }) if err != nil { return result, err } if !hasPermission { result.Reason = "missing permission: " + requiredPermission return result, nil } // Check namespace access if namespace is specified if req.NamespaceID != nil { nsResult, err := pe.EvaluateNamespaceAccess(ctx, &NamespaceAccessRequest{ UserID: req.UserID, TenantID: req.TenantID, NamespaceID: *req.NamespaceID, Operation: req.Operation, }) if err != nil { return result, err } if !nsResult.Allowed { result.Reason = "namespace access denied: " + nsResult.Reason return result, nil } } // Check data categories if len(req.DataCategories) > 0 { policy, _ := pe.store.GetEffectiveLLMPolicy(ctx, req.TenantID, req.NamespaceID) if policy != nil { for _, category := range req.DataCategories { blocked := false // Check blocked list for _, b := range policy.BlockedDataCategories { if b == category { blocked = true break } } // Check allowed list if specified if !blocked && len(policy.AllowedDataCategories) > 0 { allowed := false for _, a := range policy.AllowedDataCategories { if a == category { allowed = true break } } if !allowed { blocked = true } } if blocked { result.DeniedCategories = append(result.DeniedCategories, category) } else { result.AllowedCategories = append(result.AllowedCategories, category) } } if len(result.DeniedCategories) > 0 { result.Reason = "blocked data categories: " + strings.Join(result.DeniedCategories, ", ") return result, nil } } } result.Allowed = true result.Reason = "access granted" return result, nil } // GetUserContext returns full context for a user including all permissions and policies func (pe *PolicyEngine) GetUserContext(ctx context.Context, userID, tenantID uuid.UUID) (*UserContext, error) { perms, err := pe.service.GetEffectivePermissions(ctx, userID, tenantID, nil) if err != nil { return nil, err } tenant, err := pe.store.GetTenant(ctx, tenantID) if err != nil { return nil, err } namespaces, err := pe.service.GetUserAccessibleNamespaces(ctx, userID, tenantID) if err != nil { return nil, err } return &UserContext{ UserID: userID, TenantID: tenantID, TenantName: tenant.Name, TenantSlug: tenant.Slug, Roles: perms.Roles, Permissions: perms.Permissions, Namespaces: namespaces, LLMPolicy: perms.LLMPolicy, }, nil } // UserContext represents complete context for a user type UserContext struct { UserID uuid.UUID `json:"user_id"` TenantID uuid.UUID `json:"tenant_id"` TenantName string `json:"tenant_name"` TenantSlug string `json:"tenant_slug"` Roles []string `json:"roles"` Permissions []string `json:"permissions"` Namespaces []*Namespace `json:"namespaces"` LLMPolicy *LLMPolicy `json:"llm_policy,omitempty"` }