package rbac import ( "context" "errors" "strings" "github.com/google/uuid" ) var ( ErrTenantNotFound = errors.New("tenant not found") ErrNamespaceNotFound = errors.New("namespace not found") ErrRoleNotFound = errors.New("role not found") ErrPermissionDenied = errors.New("permission denied") ErrInvalidNamespace = errors.New("invalid namespace access") ErrDataCategoryBlocked = errors.New("data category blocked by policy") ErrLLMQuotaExceeded = errors.New("LLM quota exceeded") ErrModelNotAllowed = errors.New("model not allowed by policy") ) // Service provides RBAC business logic type Service struct { store *Store } // NewService creates a new RBAC service func NewService(store *Store) *Service { return &Service{store: store} } // GetEffectivePermissions computes all effective permissions for a user func (s *Service) GetEffectivePermissions(ctx context.Context, userID, tenantID uuid.UUID, namespaceID *uuid.UUID) (*EffectivePermissions, error) { // Get user roles userRoles, err := s.store.GetUserRolesForNamespace(ctx, userID, tenantID, namespaceID) if err != nil { return nil, err } // Aggregate permissions and roles permissionSet := make(map[string]bool) roleSet := make(map[string]bool) for _, ur := range userRoles { roleSet[ur.RoleName] = true for _, perm := range ur.RolePermissions { permissionSet[perm] = true } } // Convert to slices permissions := make([]string, 0, len(permissionSet)) for perm := range permissionSet { permissions = append(permissions, perm) } roles := make([]string, 0, len(roleSet)) for role := range roleSet { roles = append(roles, role) } // Get effective LLM policy llmPolicy, _ := s.store.GetEffectiveLLMPolicy(ctx, tenantID, namespaceID) // Get all accessible namespaces allUserRoles, err := s.store.GetUserRoles(ctx, userID, tenantID) if err != nil { return nil, err } // Build namespace access list namespaceAccessMap := make(map[uuid.UUID]*NamespaceAccess) for _, ur := range allUserRoles { var nsID uuid.UUID if ur.NamespaceID != nil { nsID = *ur.NamespaceID } else { continue // Tenant-wide roles don't have specific namespace } access, exists := namespaceAccessMap[nsID] if !exists { // Get namespace details ns, err := s.store.GetNamespace(ctx, nsID) if err != nil { continue } access = &NamespaceAccess{ NamespaceID: ns.ID, NamespaceName: ns.Name, NamespaceSlug: ns.Slug, DataClassification: ns.DataClassification, Roles: []string{}, Permissions: []string{}, } namespaceAccessMap[nsID] = access } access.Roles = append(access.Roles, ur.RoleName) access.Permissions = append(access.Permissions, ur.RolePermissions...) } namespaces := make([]NamespaceAccess, 0, len(namespaceAccessMap)) for _, access := range namespaceAccessMap { namespaces = append(namespaces, *access) } return &EffectivePermissions{ UserID: userID, TenantID: tenantID, NamespaceID: namespaceID, Permissions: permissions, Roles: roles, LLMPolicy: llmPolicy, Namespaces: namespaces, }, nil } // HasPermission checks if user has a specific permission func (s *Service) HasPermission(ctx context.Context, userID, tenantID uuid.UUID, namespaceID *uuid.UUID, permission string) (bool, error) { perms, err := s.GetEffectivePermissions(ctx, userID, tenantID, namespaceID) if err != nil { return false, err } return s.checkPermission(perms.Permissions, permission), nil } // checkPermission checks if a permission is granted given a list of permissions func (s *Service) checkPermission(permissions []string, required string) bool { for _, perm := range permissions { // Exact match if perm == required { return true } // Wildcard match (e.g., "compliance:*" matches "compliance:read") if strings.HasSuffix(perm, ":*") { prefix := strings.TrimSuffix(perm, "*") if strings.HasPrefix(required, prefix) { return true } } // Full wildcard (e.g., "compliance:*:read" matches "compliance:privacy:read") if strings.Contains(perm, ":*:") { parts := strings.Split(perm, ":*:") if len(parts) == 2 { if strings.HasPrefix(required, parts[0]+":") && strings.HasSuffix(required, ":"+parts[1]) { return true } } } // "own" namespace handling (e.g., "compliance:own:read" when in own namespace) // This requires context about whether the user is in their "own" namespace // For simplicity, we'll handle "own" as a prefix match for now if strings.Contains(perm, ":own:") { ownPerm := strings.Replace(perm, ":own:", ":", 1) if ownPerm == required || strings.HasPrefix(required, strings.TrimSuffix(ownPerm, "*")) { return true } } } return false } // HasAnyPermission checks if user has any of the specified permissions func (s *Service) HasAnyPermission(ctx context.Context, userID, tenantID uuid.UUID, namespaceID *uuid.UUID, permissions []string) (bool, error) { perms, err := s.GetEffectivePermissions(ctx, userID, tenantID, namespaceID) if err != nil { return false, err } for _, required := range permissions { if s.checkPermission(perms.Permissions, required) { return true, nil } } return false, nil } // HasAllPermissions checks if user has all specified permissions func (s *Service) HasAllPermissions(ctx context.Context, userID, tenantID uuid.UUID, namespaceID *uuid.UUID, permissions []string) (bool, error) { perms, err := s.GetEffectivePermissions(ctx, userID, tenantID, namespaceID) if err != nil { return false, err } for _, required := range permissions { if !s.checkPermission(perms.Permissions, required) { return false, nil } } return true, nil } // CanAccessNamespace checks if user can access a namespace func (s *Service) CanAccessNamespace(ctx context.Context, userID, tenantID, namespaceID uuid.UUID) (bool, error) { userRoles, err := s.store.GetUserRolesForNamespace(ctx, userID, tenantID, &namespaceID) if err != nil { return false, err } return len(userRoles) > 0, nil } // CanAccessDataCategory checks if user can access a data category via LLM func (s *Service) CanAccessDataCategory(ctx context.Context, userID, tenantID uuid.UUID, namespaceID *uuid.UUID, category string) (bool, string, error) { policy, err := s.store.GetEffectiveLLMPolicy(ctx, tenantID, namespaceID) if err != nil { return false, "", err } // No policy = allow all if policy == nil { return true, "", nil } // Check blocked categories for _, blocked := range policy.BlockedDataCategories { if blocked == category { return false, "data category is blocked by policy", nil } } // If allowed categories are specified, check if category is in the list if len(policy.AllowedDataCategories) > 0 { allowed := false for _, a := range policy.AllowedDataCategories { if a == category { allowed = true break } } if !allowed { return false, "data category is not in allowed list", nil } } return true, "", nil } // CanUseModel checks if a model is allowed by policy func (s *Service) CanUseModel(ctx context.Context, tenantID uuid.UUID, namespaceID *uuid.UUID, model string) (bool, error) { policy, err := s.store.GetEffectiveLLMPolicy(ctx, tenantID, namespaceID) if err != nil { return false, err } // No policy = allow all models if policy == nil || len(policy.AllowedModels) == 0 { return true, nil } // Check if model is in allowed list for _, allowed := range policy.AllowedModels { if allowed == model { return true, nil } // Partial match for model families (e.g., "qwen2.5" matches "qwen2.5:7b") if strings.HasPrefix(model, allowed+":") || strings.HasPrefix(model, allowed+"-") { return true, nil } } return false, nil } // GetPIIRedactionLevel returns the PII redaction level for a namespace func (s *Service) GetPIIRedactionLevel(ctx context.Context, tenantID uuid.UUID, namespaceID *uuid.UUID) (PIIRedactionLevel, bool, error) { policy, err := s.store.GetEffectiveLLMPolicy(ctx, tenantID, namespaceID) if err != nil { return PIIRedactionStrict, true, err } // No policy = use strict defaults if policy == nil { return PIIRedactionStrict, true, nil } return policy.PIIRedactionLevel, policy.RequirePIIRedaction, nil } // GetUserAccessibleNamespaces returns all namespaces a user can access func (s *Service) GetUserAccessibleNamespaces(ctx context.Context, userID, tenantID uuid.UUID) ([]*Namespace, error) { userRoles, err := s.store.GetUserRoles(ctx, userID, tenantID) if err != nil { return nil, err } // Get unique namespace IDs namespaceIDs := make(map[uuid.UUID]bool) hasTenantWideRole := false for _, ur := range userRoles { if ur.NamespaceID != nil { namespaceIDs[*ur.NamespaceID] = true } else { hasTenantWideRole = true } } // If user has tenant-wide role, return all namespaces if hasTenantWideRole { return s.store.ListNamespaces(ctx, tenantID) } // Otherwise, return only specific namespaces var namespaces []*Namespace for nsID := range namespaceIDs { ns, err := s.store.GetNamespace(ctx, nsID) if err != nil { continue } namespaces = append(namespaces, ns) } return namespaces, nil } // AssignRoleToUser assigns a role to a user func (s *Service) AssignRoleToUser(ctx context.Context, ur *UserRole, grantorUserID uuid.UUID) error { // Check if grantor has permission to assign roles hasPermission, err := s.HasAnyPermission(ctx, grantorUserID, ur.TenantID, ur.NamespaceID, []string{ "rbac:assign", "namespace:own:admin", PermissionComplianceAll, }) if err != nil { return err } if !hasPermission { return ErrPermissionDenied } ur.GrantedBy = grantorUserID return s.store.AssignRole(ctx, ur) } // RevokeRoleFromUser revokes a role from a user func (s *Service) RevokeRoleFromUser(ctx context.Context, userID, roleID, tenantID uuid.UUID, namespaceID *uuid.UUID, revokerUserID uuid.UUID) error { // Check if revoker has permission to revoke roles hasPermission, err := s.HasAnyPermission(ctx, revokerUserID, tenantID, namespaceID, []string{ "rbac:revoke", "namespace:own:admin", PermissionComplianceAll, }) if err != nil { return err } if !hasPermission { return ErrPermissionDenied } return s.store.RevokeRole(ctx, userID, roleID, tenantID, namespaceID) }