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>
396 lines
10 KiB
Go
396 lines
10 KiB
Go
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"`
|
|
}
|