This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/ai-compliance-sdk/internal/rbac/service.go
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

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)
}