Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
361 lines
10 KiB
Go
361 lines
10 KiB
Go
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)
|
|
}
|