Initial commit: breakpilot-compliance - Compliance SDK Platform
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>
This commit is contained in:
360
ai-compliance-sdk/internal/rbac/service.go
Normal file
360
ai-compliance-sdk/internal/rbac/service.go
Normal file
@@ -0,0 +1,360 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user