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>
This commit is contained in:
459
ai-compliance-sdk/internal/rbac/middleware.go
Normal file
459
ai-compliance-sdk/internal/rbac/middleware.go
Normal file
@@ -0,0 +1,459 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Context keys for RBAC data
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
ContextKeyUserID contextKey = "user_id"
|
||||
ContextKeyTenantID contextKey = "tenant_id"
|
||||
ContextKeyNamespaceID contextKey = "namespace_id"
|
||||
ContextKeyPermissions contextKey = "permissions"
|
||||
ContextKeyRoles contextKey = "roles"
|
||||
ContextKeyUserContext contextKey = "user_context"
|
||||
)
|
||||
|
||||
// Middleware provides RBAC middleware for Gin
|
||||
type Middleware struct {
|
||||
service *Service
|
||||
policyEngine *PolicyEngine
|
||||
}
|
||||
|
||||
// NewMiddleware creates a new RBAC middleware
|
||||
func NewMiddleware(service *Service, policyEngine *PolicyEngine) *Middleware {
|
||||
return &Middleware{
|
||||
service: service,
|
||||
policyEngine: policyEngine,
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractUserContext extracts user context from headers/JWT and stores in context
|
||||
// This middleware should run after authentication
|
||||
func (m *Middleware) ExtractUserContext() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Extract user ID from header (set by auth middleware)
|
||||
userIDStr := c.GetHeader("X-User-ID")
|
||||
if userIDStr == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Extract tenant ID (from header or default)
|
||||
tenantIDStr := c.GetHeader("X-Tenant-ID")
|
||||
if tenantIDStr == "" {
|
||||
// Try to get from query param
|
||||
tenantIDStr = c.Query("tenant_id")
|
||||
}
|
||||
if tenantIDStr == "" {
|
||||
// Use default tenant slug
|
||||
tenantIDStr = c.GetHeader("X-Tenant-Slug")
|
||||
if tenantIDStr != "" {
|
||||
tenant, err := m.service.store.GetTenantBySlug(c.Request.Context(), tenantIDStr)
|
||||
if err == nil {
|
||||
tenantIDStr = tenant.ID.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tenantID uuid.UUID
|
||||
if tenantIDStr != "" {
|
||||
tenantID, _ = uuid.Parse(tenantIDStr)
|
||||
}
|
||||
|
||||
// Extract namespace ID (optional)
|
||||
var namespaceID *uuid.UUID
|
||||
namespaceIDStr := c.GetHeader("X-Namespace-ID")
|
||||
if namespaceIDStr == "" {
|
||||
namespaceIDStr = c.Query("namespace_id")
|
||||
}
|
||||
if namespaceIDStr != "" {
|
||||
if nsID, err := uuid.Parse(namespaceIDStr); err == nil {
|
||||
namespaceID = &nsID
|
||||
}
|
||||
}
|
||||
|
||||
// Store in context
|
||||
c.Set(string(ContextKeyUserID), userID)
|
||||
c.Set(string(ContextKeyTenantID), tenantID)
|
||||
if namespaceID != nil {
|
||||
c.Set(string(ContextKeyNamespaceID), *namespaceID)
|
||||
}
|
||||
|
||||
// Get effective permissions
|
||||
if tenantID != uuid.Nil {
|
||||
perms, err := m.service.GetEffectivePermissions(c.Request.Context(), userID, tenantID, namespaceID)
|
||||
if err == nil {
|
||||
c.Set(string(ContextKeyPermissions), perms.Permissions)
|
||||
c.Set(string(ContextKeyRoles), perms.Roles)
|
||||
}
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequirePermission requires the user to have a specific permission
|
||||
func (m *Middleware) RequirePermission(permission string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, tenantID, namespaceID := m.extractIDs(c)
|
||||
|
||||
if userID == uuid.Nil || tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
hasPermission, err := m.service.HasPermission(c.Request.Context(), userID, tenantID, namespaceID, permission)
|
||||
if err != nil || !hasPermission {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Insufficient permissions",
|
||||
"required": permission,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAnyPermission requires the user to have any of the specified permissions
|
||||
func (m *Middleware) RequireAnyPermission(permissions ...string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, tenantID, namespaceID := m.extractIDs(c)
|
||||
|
||||
if userID == uuid.Nil || tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
hasPermission, err := m.service.HasAnyPermission(c.Request.Context(), userID, tenantID, namespaceID, permissions)
|
||||
if err != nil || !hasPermission {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Insufficient permissions",
|
||||
"required": permissions,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAllPermissions requires the user to have all specified permissions
|
||||
func (m *Middleware) RequireAllPermissions(permissions ...string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, tenantID, namespaceID := m.extractIDs(c)
|
||||
|
||||
if userID == uuid.Nil || tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
hasPermission, err := m.service.HasAllPermissions(c.Request.Context(), userID, tenantID, namespaceID, permissions)
|
||||
if err != nil || !hasPermission {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Insufficient permissions - all required",
|
||||
"required": permissions,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireNamespaceAccess requires access to the specified namespace
|
||||
func (m *Middleware) RequireNamespaceAccess(operation string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, tenantID, namespaceID := m.extractIDs(c)
|
||||
|
||||
if userID == uuid.Nil || tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Get namespace ID from URL param if not in context
|
||||
if namespaceID == nil {
|
||||
nsIDStr := c.Param("namespace_id")
|
||||
if nsIDStr == "" {
|
||||
nsIDStr = c.Param("namespaceId")
|
||||
}
|
||||
if nsIDStr != "" {
|
||||
if nsID, err := uuid.Parse(nsIDStr); err == nil {
|
||||
namespaceID = &nsID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if namespaceID == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "bad_request",
|
||||
"message": "Namespace ID required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
result, err := m.policyEngine.EvaluateNamespaceAccess(c.Request.Context(), &NamespaceAccessRequest{
|
||||
UserID: userID,
|
||||
TenantID: tenantID,
|
||||
NamespaceID: *namespaceID,
|
||||
Operation: operation,
|
||||
})
|
||||
|
||||
if err != nil || !result.Allowed {
|
||||
reason := "access denied"
|
||||
if result != nil {
|
||||
reason = result.Reason
|
||||
}
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Namespace access denied",
|
||||
"reason": reason,
|
||||
"namespace": namespaceID.String(),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Store namespace access result in context
|
||||
c.Set("namespace_access", result)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireLLMAccess validates LLM access based on policy
|
||||
func (m *Middleware) RequireLLMAccess() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, tenantID, namespaceID := m.extractIDs(c)
|
||||
|
||||
if userID == uuid.Nil || tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Basic LLM permission check
|
||||
hasPermission, err := m.service.HasAnyPermission(c.Request.Context(), userID, tenantID, namespaceID, []string{
|
||||
PermissionLLMAll,
|
||||
PermissionLLMQuery,
|
||||
PermissionLLMOwnQuery,
|
||||
})
|
||||
|
||||
if err != nil || !hasPermission {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "LLM access denied",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRole requires the user to have a specific role
|
||||
func (m *Middleware) RequireRole(role string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
roles, exists := c.Get(string(ContextKeyRoles))
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
roleSlice, ok := roles.([]string)
|
||||
if !ok {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "internal_error",
|
||||
"message": "Invalid role data",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
hasRole := false
|
||||
for _, r := range roleSlice {
|
||||
if r == role {
|
||||
hasRole = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasRole {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Required role missing",
|
||||
"required": role,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// extractIDs extracts user, tenant, and namespace IDs from context
|
||||
func (m *Middleware) extractIDs(c *gin.Context) (uuid.UUID, uuid.UUID, *uuid.UUID) {
|
||||
var userID, tenantID uuid.UUID
|
||||
var namespaceID *uuid.UUID
|
||||
|
||||
if id, exists := c.Get(string(ContextKeyUserID)); exists {
|
||||
userID = id.(uuid.UUID)
|
||||
}
|
||||
if id, exists := c.Get(string(ContextKeyTenantID)); exists {
|
||||
tenantID = id.(uuid.UUID)
|
||||
}
|
||||
if id, exists := c.Get(string(ContextKeyNamespaceID)); exists {
|
||||
nsID := id.(uuid.UUID)
|
||||
namespaceID = &nsID
|
||||
}
|
||||
|
||||
return userID, tenantID, namespaceID
|
||||
}
|
||||
|
||||
// GetUserID retrieves user ID from Gin context
|
||||
func GetUserID(c *gin.Context) uuid.UUID {
|
||||
if id, exists := c.Get(string(ContextKeyUserID)); exists {
|
||||
return id.(uuid.UUID)
|
||||
}
|
||||
return uuid.Nil
|
||||
}
|
||||
|
||||
// GetTenantID retrieves tenant ID from Gin context
|
||||
func GetTenantID(c *gin.Context) uuid.UUID {
|
||||
if id, exists := c.Get(string(ContextKeyTenantID)); exists {
|
||||
return id.(uuid.UUID)
|
||||
}
|
||||
return uuid.Nil
|
||||
}
|
||||
|
||||
// GetNamespaceID retrieves namespace ID from Gin context
|
||||
func GetNamespaceID(c *gin.Context) *uuid.UUID {
|
||||
if id, exists := c.Get(string(ContextKeyNamespaceID)); exists {
|
||||
nsID := id.(uuid.UUID)
|
||||
return &nsID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPermissions retrieves permissions from Gin context
|
||||
func GetPermissions(c *gin.Context) []string {
|
||||
if perms, exists := c.Get(string(ContextKeyPermissions)); exists {
|
||||
return perms.([]string)
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// GetRoles retrieves roles from Gin context
|
||||
func GetRoles(c *gin.Context) []string {
|
||||
if roles, exists := c.Get(string(ContextKeyRoles)); exists {
|
||||
return roles.([]string)
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// ContextWithUserID adds user ID to context
|
||||
func ContextWithUserID(ctx context.Context, userID uuid.UUID) context.Context {
|
||||
return context.WithValue(ctx, ContextKeyUserID, userID)
|
||||
}
|
||||
|
||||
// ContextWithTenantID adds tenant ID to context
|
||||
func ContextWithTenantID(ctx context.Context, tenantID uuid.UUID) context.Context {
|
||||
return context.WithValue(ctx, ContextKeyTenantID, tenantID)
|
||||
}
|
||||
|
||||
// ContextWithNamespaceID adds namespace ID to context
|
||||
func ContextWithNamespaceID(ctx context.Context, namespaceID uuid.UUID) context.Context {
|
||||
return context.WithValue(ctx, ContextKeyNamespaceID, namespaceID)
|
||||
}
|
||||
|
||||
// UserIDFromContext retrieves user ID from standard context
|
||||
func UserIDFromContext(ctx context.Context) uuid.UUID {
|
||||
if id, ok := ctx.Value(ContextKeyUserID).(uuid.UUID); ok {
|
||||
return id
|
||||
}
|
||||
return uuid.Nil
|
||||
}
|
||||
|
||||
// TenantIDFromContext retrieves tenant ID from standard context
|
||||
func TenantIDFromContext(ctx context.Context) uuid.UUID {
|
||||
if id, ok := ctx.Value(ContextKeyTenantID).(uuid.UUID); ok {
|
||||
return id
|
||||
}
|
||||
return uuid.Nil
|
||||
}
|
||||
|
||||
// NamespaceIDFromContext retrieves namespace ID from standard context
|
||||
func NamespaceIDFromContext(ctx context.Context) *uuid.UUID {
|
||||
if id, ok := ctx.Value(ContextKeyNamespaceID).(uuid.UUID); ok {
|
||||
return &id
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasPermissionFromHeader checks permission from header-based context (for API keys)
|
||||
func (m *Middleware) HasPermissionFromHeader(c *gin.Context, permission string) bool {
|
||||
// Check X-API-Permissions header (set by API key auth)
|
||||
permsHeader := c.GetHeader("X-API-Permissions")
|
||||
if permsHeader != "" {
|
||||
perms := strings.Split(permsHeader, ",")
|
||||
for _, p := range perms {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == permission {
|
||||
return true
|
||||
}
|
||||
// Wildcard check
|
||||
if strings.HasSuffix(p, ":*") {
|
||||
prefix := strings.TrimSuffix(p, "*")
|
||||
if strings.HasPrefix(permission, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
197
ai-compliance-sdk/internal/rbac/models.go
Normal file
197
ai-compliance-sdk/internal/rbac/models.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// IsolationLevel defines namespace isolation strictness
|
||||
type IsolationLevel string
|
||||
|
||||
const (
|
||||
IsolationStrict IsolationLevel = "strict"
|
||||
IsolationShared IsolationLevel = "shared"
|
||||
IsolationPublic IsolationLevel = "public"
|
||||
)
|
||||
|
||||
// DataClassification defines data sensitivity levels
|
||||
type DataClassification string
|
||||
|
||||
const (
|
||||
ClassificationPublic DataClassification = "public"
|
||||
ClassificationInternal DataClassification = "internal"
|
||||
ClassificationConfidential DataClassification = "confidential"
|
||||
ClassificationRestricted DataClassification = "restricted"
|
||||
)
|
||||
|
||||
// TenantStatus defines tenant status
|
||||
type TenantStatus string
|
||||
|
||||
const (
|
||||
TenantStatusActive TenantStatus = "active"
|
||||
TenantStatusSuspended TenantStatus = "suspended"
|
||||
TenantStatusInactive TenantStatus = "inactive"
|
||||
)
|
||||
|
||||
// PIIRedactionLevel defines PII redaction strictness
|
||||
type PIIRedactionLevel string
|
||||
|
||||
const (
|
||||
PIIRedactionStrict PIIRedactionLevel = "strict"
|
||||
PIIRedactionModerate PIIRedactionLevel = "moderate"
|
||||
PIIRedactionMinimal PIIRedactionLevel = "minimal"
|
||||
PIIRedactionNone PIIRedactionLevel = "none"
|
||||
)
|
||||
|
||||
// Tenant represents a customer/organization (Mandant)
|
||||
type Tenant struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Slug string `json:"slug" db:"slug"`
|
||||
Settings map[string]any `json:"settings" db:"settings"`
|
||||
MaxUsers int `json:"max_users" db:"max_users"`
|
||||
LLMQuotaMonthly int `json:"llm_quota_monthly" db:"llm_quota_monthly"`
|
||||
Status TenantStatus `json:"status" db:"status"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// Namespace represents a department/division within a tenant (z.B. Finance, HR, IT)
|
||||
type Namespace struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Slug string `json:"slug" db:"slug"`
|
||||
ParentNamespaceID *uuid.UUID `json:"parent_namespace_id,omitempty" db:"parent_namespace_id"`
|
||||
IsolationLevel IsolationLevel `json:"isolation_level" db:"isolation_level"`
|
||||
DataClassification DataClassification `json:"data_classification" db:"data_classification"`
|
||||
Metadata map[string]any `json:"metadata,omitempty" db:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// Role defines a set of permissions
|
||||
type Role struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TenantID *uuid.UUID `json:"tenant_id,omitempty" db:"tenant_id"` // nil for system roles
|
||||
Name string `json:"name" db:"name"`
|
||||
Description string `json:"description,omitempty" db:"description"`
|
||||
Permissions []string `json:"permissions" db:"permissions"`
|
||||
IsSystemRole bool `json:"is_system_role" db:"is_system_role"`
|
||||
HierarchyLevel int `json:"hierarchy_level" db:"hierarchy_level"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// UserRole represents a user's role assignment with optional namespace scope
|
||||
type UserRole struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
RoleID uuid.UUID `json:"role_id" db:"role_id"`
|
||||
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
|
||||
NamespaceID *uuid.UUID `json:"namespace_id,omitempty" db:"namespace_id"` // nil = tenant-wide
|
||||
GrantedBy uuid.UUID `json:"granted_by" db:"granted_by"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty" db:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
|
||||
// Joined fields (populated by queries)
|
||||
RoleName string `json:"role_name,omitempty" db:"role_name"`
|
||||
RolePermissions []string `json:"role_permissions,omitempty" db:"role_permissions"`
|
||||
NamespaceName string `json:"namespace_name,omitempty" db:"namespace_name"`
|
||||
}
|
||||
|
||||
// LLMPolicy defines access controls for LLM operations
|
||||
type LLMPolicy struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
|
||||
NamespaceID *uuid.UUID `json:"namespace_id,omitempty" db:"namespace_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Description string `json:"description,omitempty" db:"description"`
|
||||
AllowedDataCategories []string `json:"allowed_data_categories" db:"allowed_data_categories"`
|
||||
BlockedDataCategories []string `json:"blocked_data_categories" db:"blocked_data_categories"`
|
||||
RequirePIIRedaction bool `json:"require_pii_redaction" db:"require_pii_redaction"`
|
||||
PIIRedactionLevel PIIRedactionLevel `json:"pii_redaction_level" db:"pii_redaction_level"`
|
||||
AllowedModels []string `json:"allowed_models" db:"allowed_models"`
|
||||
MaxTokensPerRequest int `json:"max_tokens_per_request" db:"max_tokens_per_request"`
|
||||
MaxRequestsPerDay int `json:"max_requests_per_day" db:"max_requests_per_day"`
|
||||
MaxRequestsPerHour int `json:"max_requests_per_hour" db:"max_requests_per_hour"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
Priority int `json:"priority" db:"priority"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// APIKey represents an API key for SDK access
|
||||
type APIKey struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
KeyHash string `json:"-" db:"key_hash"` // Never expose
|
||||
KeyPrefix string `json:"key_prefix" db:"key_prefix"`
|
||||
Permissions []string `json:"permissions" db:"permissions"`
|
||||
NamespaceRestrictions []uuid.UUID `json:"namespace_restrictions,omitempty" db:"namespace_restrictions"`
|
||||
RateLimitPerHour int `json:"rate_limit_per_hour" db:"rate_limit_per_hour"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty" db:"expires_at"`
|
||||
LastUsedAt *time.Time `json:"last_used_at,omitempty" db:"last_used_at"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
CreatedBy uuid.UUID `json:"created_by" db:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// EffectivePermissions represents a user's computed permissions
|
||||
type EffectivePermissions struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
NamespaceID *uuid.UUID `json:"namespace_id,omitempty"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Roles []string `json:"roles"`
|
||||
LLMPolicy *LLMPolicy `json:"llm_policy,omitempty"`
|
||||
Namespaces []NamespaceAccess `json:"namespaces,omitempty"`
|
||||
}
|
||||
|
||||
// NamespaceAccess represents a user's access to a namespace
|
||||
type NamespaceAccess struct {
|
||||
NamespaceID uuid.UUID `json:"namespace_id"`
|
||||
NamespaceName string `json:"namespace_name"`
|
||||
NamespaceSlug string `json:"namespace_slug"`
|
||||
DataClassification DataClassification `json:"data_classification"`
|
||||
Roles []string `json:"roles"`
|
||||
Permissions []string `json:"permissions"`
|
||||
}
|
||||
|
||||
// System role names (predefined)
|
||||
const (
|
||||
RoleComplianceExecutive = "compliance_executive"
|
||||
RoleComplianceOfficer = "compliance_officer"
|
||||
RoleDataProtectionOfficer = "data_protection_officer"
|
||||
RoleNamespaceAdmin = "namespace_admin"
|
||||
RoleAuditor = "auditor"
|
||||
RoleComplianceUser = "compliance_user"
|
||||
)
|
||||
|
||||
// Common permission patterns
|
||||
const (
|
||||
PermissionComplianceAll = "compliance:*"
|
||||
PermissionComplianceRead = "compliance:read"
|
||||
PermissionComplianceWrite = "compliance:write"
|
||||
PermissionComplianceOwnRead = "compliance:own:read"
|
||||
PermissionAuditAll = "audit:*"
|
||||
PermissionAuditRead = "audit:read"
|
||||
PermissionAuditLogRead = "audit:log:read"
|
||||
PermissionLLMAll = "llm:*"
|
||||
PermissionLLMQuery = "llm:query:execute"
|
||||
PermissionLLMOwnQuery = "llm:own:query"
|
||||
PermissionNamespaceRead = "namespace:read"
|
||||
PermissionNamespaceOwnAdmin = "namespace:own:admin"
|
||||
)
|
||||
|
||||
// Data categories for LLM access control
|
||||
const (
|
||||
DataCategorySalary = "salary"
|
||||
DataCategoryHealth = "health"
|
||||
DataCategoryPersonal = "personal"
|
||||
DataCategoryFinancial = "financial"
|
||||
DataCategoryLegal = "legal"
|
||||
DataCategoryHR = "hr"
|
||||
)
|
||||
395
ai-compliance-sdk/internal/rbac/policy_engine.go
Normal file
395
ai-compliance-sdk/internal/rbac/policy_engine.go
Normal file
@@ -0,0 +1,395 @@
|
||||
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"`
|
||||
}
|
||||
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)
|
||||
}
|
||||
651
ai-compliance-sdk/internal/rbac/store.go
Normal file
651
ai-compliance-sdk/internal/rbac/store.go
Normal file
@@ -0,0 +1,651 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Store provides database operations for RBAC entities
|
||||
type Store struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
// NewStore creates a new RBAC store
|
||||
func NewStore(pool *pgxpool.Pool) *Store {
|
||||
return &Store{pool: pool}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tenant Operations
|
||||
// ============================================================================
|
||||
|
||||
// CreateTenant creates a new tenant
|
||||
func (s *Store) CreateTenant(ctx context.Context, tenant *Tenant) error {
|
||||
tenant.ID = uuid.New()
|
||||
tenant.CreatedAt = time.Now().UTC()
|
||||
tenant.UpdatedAt = tenant.CreatedAt
|
||||
|
||||
if tenant.Status == "" {
|
||||
tenant.Status = TenantStatusActive
|
||||
}
|
||||
if tenant.Settings == nil {
|
||||
tenant.Settings = make(map[string]any)
|
||||
}
|
||||
|
||||
settingsJSON, err := json.Marshal(tenant.Settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal settings: %w", err)
|
||||
}
|
||||
|
||||
_, err = s.pool.Exec(ctx, `
|
||||
INSERT INTO compliance_tenants (id, name, slug, settings, max_users, llm_quota_monthly, status, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
`, tenant.ID, tenant.Name, tenant.Slug, settingsJSON, tenant.MaxUsers, tenant.LLMQuotaMonthly, tenant.Status, tenant.CreatedAt, tenant.UpdatedAt)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetTenant retrieves a tenant by ID
|
||||
func (s *Store) GetTenant(ctx context.Context, id uuid.UUID) (*Tenant, error) {
|
||||
var tenant Tenant
|
||||
var settingsJSON []byte
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT id, name, slug, settings, max_users, llm_quota_monthly, status, created_at, updated_at
|
||||
FROM compliance_tenants
|
||||
WHERE id = $1
|
||||
`, id).Scan(
|
||||
&tenant.ID, &tenant.Name, &tenant.Slug, &settingsJSON,
|
||||
&tenant.MaxUsers, &tenant.LLMQuotaMonthly, &tenant.Status,
|
||||
&tenant.CreatedAt, &tenant.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(settingsJSON, &tenant.Settings); err != nil {
|
||||
tenant.Settings = make(map[string]any)
|
||||
}
|
||||
|
||||
return &tenant, nil
|
||||
}
|
||||
|
||||
// GetTenantBySlug retrieves a tenant by slug
|
||||
func (s *Store) GetTenantBySlug(ctx context.Context, slug string) (*Tenant, error) {
|
||||
var tenant Tenant
|
||||
var settingsJSON []byte
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT id, name, slug, settings, max_users, llm_quota_monthly, status, created_at, updated_at
|
||||
FROM compliance_tenants
|
||||
WHERE slug = $1
|
||||
`, slug).Scan(
|
||||
&tenant.ID, &tenant.Name, &tenant.Slug, &settingsJSON,
|
||||
&tenant.MaxUsers, &tenant.LLMQuotaMonthly, &tenant.Status,
|
||||
&tenant.CreatedAt, &tenant.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(settingsJSON, &tenant.Settings); err != nil {
|
||||
tenant.Settings = make(map[string]any)
|
||||
}
|
||||
|
||||
return &tenant, nil
|
||||
}
|
||||
|
||||
// ListTenants lists all tenants
|
||||
func (s *Store) ListTenants(ctx context.Context) ([]*Tenant, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, name, slug, settings, max_users, llm_quota_monthly, status, created_at, updated_at
|
||||
FROM compliance_tenants
|
||||
ORDER BY name
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tenants []*Tenant
|
||||
for rows.Next() {
|
||||
var tenant Tenant
|
||||
var settingsJSON []byte
|
||||
|
||||
err := rows.Scan(
|
||||
&tenant.ID, &tenant.Name, &tenant.Slug, &settingsJSON,
|
||||
&tenant.MaxUsers, &tenant.LLMQuotaMonthly, &tenant.Status,
|
||||
&tenant.CreatedAt, &tenant.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(settingsJSON, &tenant.Settings); err != nil {
|
||||
tenant.Settings = make(map[string]any)
|
||||
}
|
||||
|
||||
tenants = append(tenants, &tenant)
|
||||
}
|
||||
|
||||
return tenants, nil
|
||||
}
|
||||
|
||||
// UpdateTenant updates a tenant
|
||||
func (s *Store) UpdateTenant(ctx context.Context, tenant *Tenant) error {
|
||||
tenant.UpdatedAt = time.Now().UTC()
|
||||
|
||||
settingsJSON, err := json.Marshal(tenant.Settings)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal settings: %w", err)
|
||||
}
|
||||
|
||||
_, err = s.pool.Exec(ctx, `
|
||||
UPDATE compliance_tenants
|
||||
SET name = $2, slug = $3, settings = $4, max_users = $5, llm_quota_monthly = $6, status = $7, updated_at = $8
|
||||
WHERE id = $1
|
||||
`, tenant.ID, tenant.Name, tenant.Slug, settingsJSON, tenant.MaxUsers, tenant.LLMQuotaMonthly, tenant.Status, tenant.UpdatedAt)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Namespace Operations
|
||||
// ============================================================================
|
||||
|
||||
// CreateNamespace creates a new namespace
|
||||
func (s *Store) CreateNamespace(ctx context.Context, ns *Namespace) error {
|
||||
ns.ID = uuid.New()
|
||||
ns.CreatedAt = time.Now().UTC()
|
||||
ns.UpdatedAt = ns.CreatedAt
|
||||
|
||||
if ns.IsolationLevel == "" {
|
||||
ns.IsolationLevel = IsolationStrict
|
||||
}
|
||||
if ns.DataClassification == "" {
|
||||
ns.DataClassification = ClassificationInternal
|
||||
}
|
||||
if ns.Metadata == nil {
|
||||
ns.Metadata = make(map[string]any)
|
||||
}
|
||||
|
||||
metadataJSON, err := json.Marshal(ns.Metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
_, err = s.pool.Exec(ctx, `
|
||||
INSERT INTO compliance_namespaces (id, tenant_id, name, slug, parent_namespace_id, isolation_level, data_classification, metadata, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
`, ns.ID, ns.TenantID, ns.Name, ns.Slug, ns.ParentNamespaceID, ns.IsolationLevel, ns.DataClassification, metadataJSON, ns.CreatedAt, ns.UpdatedAt)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetNamespace retrieves a namespace by ID
|
||||
func (s *Store) GetNamespace(ctx context.Context, id uuid.UUID) (*Namespace, error) {
|
||||
var ns Namespace
|
||||
var metadataJSON []byte
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, name, slug, parent_namespace_id, isolation_level, data_classification, metadata, created_at, updated_at
|
||||
FROM compliance_namespaces
|
||||
WHERE id = $1
|
||||
`, id).Scan(
|
||||
&ns.ID, &ns.TenantID, &ns.Name, &ns.Slug, &ns.ParentNamespaceID,
|
||||
&ns.IsolationLevel, &ns.DataClassification, &metadataJSON,
|
||||
&ns.CreatedAt, &ns.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(metadataJSON, &ns.Metadata); err != nil {
|
||||
ns.Metadata = make(map[string]any)
|
||||
}
|
||||
|
||||
return &ns, nil
|
||||
}
|
||||
|
||||
// GetNamespaceBySlug retrieves a namespace by tenant and slug
|
||||
func (s *Store) GetNamespaceBySlug(ctx context.Context, tenantID uuid.UUID, slug string) (*Namespace, error) {
|
||||
var ns Namespace
|
||||
var metadataJSON []byte
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, name, slug, parent_namespace_id, isolation_level, data_classification, metadata, created_at, updated_at
|
||||
FROM compliance_namespaces
|
||||
WHERE tenant_id = $1 AND slug = $2
|
||||
`, tenantID, slug).Scan(
|
||||
&ns.ID, &ns.TenantID, &ns.Name, &ns.Slug, &ns.ParentNamespaceID,
|
||||
&ns.IsolationLevel, &ns.DataClassification, &metadataJSON,
|
||||
&ns.CreatedAt, &ns.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(metadataJSON, &ns.Metadata); err != nil {
|
||||
ns.Metadata = make(map[string]any)
|
||||
}
|
||||
|
||||
return &ns, nil
|
||||
}
|
||||
|
||||
// ListNamespaces lists namespaces for a tenant
|
||||
func (s *Store) ListNamespaces(ctx context.Context, tenantID uuid.UUID) ([]*Namespace, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, tenant_id, name, slug, parent_namespace_id, isolation_level, data_classification, metadata, created_at, updated_at
|
||||
FROM compliance_namespaces
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY name
|
||||
`, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var namespaces []*Namespace
|
||||
for rows.Next() {
|
||||
var ns Namespace
|
||||
var metadataJSON []byte
|
||||
|
||||
err := rows.Scan(
|
||||
&ns.ID, &ns.TenantID, &ns.Name, &ns.Slug, &ns.ParentNamespaceID,
|
||||
&ns.IsolationLevel, &ns.DataClassification, &metadataJSON,
|
||||
&ns.CreatedAt, &ns.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(metadataJSON, &ns.Metadata); err != nil {
|
||||
ns.Metadata = make(map[string]any)
|
||||
}
|
||||
|
||||
namespaces = append(namespaces, &ns)
|
||||
}
|
||||
|
||||
return namespaces, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Role Operations
|
||||
// ============================================================================
|
||||
|
||||
// CreateRole creates a new role
|
||||
func (s *Store) CreateRole(ctx context.Context, role *Role) error {
|
||||
role.ID = uuid.New()
|
||||
role.CreatedAt = time.Now().UTC()
|
||||
role.UpdatedAt = role.CreatedAt
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO compliance_roles (id, tenant_id, name, description, permissions, is_system_role, hierarchy_level, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
`, role.ID, role.TenantID, role.Name, role.Description, role.Permissions, role.IsSystemRole, role.HierarchyLevel, role.CreatedAt, role.UpdatedAt)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetRole retrieves a role by ID
|
||||
func (s *Store) GetRole(ctx context.Context, id uuid.UUID) (*Role, error) {
|
||||
var role Role
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, name, description, permissions, is_system_role, hierarchy_level, created_at, updated_at
|
||||
FROM compliance_roles
|
||||
WHERE id = $1
|
||||
`, id).Scan(
|
||||
&role.ID, &role.TenantID, &role.Name, &role.Description,
|
||||
&role.Permissions, &role.IsSystemRole, &role.HierarchyLevel,
|
||||
&role.CreatedAt, &role.UpdatedAt,
|
||||
)
|
||||
|
||||
return &role, err
|
||||
}
|
||||
|
||||
// GetRoleByName retrieves a role by tenant and name
|
||||
func (s *Store) GetRoleByName(ctx context.Context, tenantID *uuid.UUID, name string) (*Role, error) {
|
||||
var role Role
|
||||
|
||||
query := `
|
||||
SELECT id, tenant_id, name, description, permissions, is_system_role, hierarchy_level, created_at, updated_at
|
||||
FROM compliance_roles
|
||||
WHERE name = $1 AND (tenant_id = $2 OR (tenant_id IS NULL AND is_system_role = TRUE))
|
||||
`
|
||||
|
||||
err := s.pool.QueryRow(ctx, query, name, tenantID).Scan(
|
||||
&role.ID, &role.TenantID, &role.Name, &role.Description,
|
||||
&role.Permissions, &role.IsSystemRole, &role.HierarchyLevel,
|
||||
&role.CreatedAt, &role.UpdatedAt,
|
||||
)
|
||||
|
||||
return &role, err
|
||||
}
|
||||
|
||||
// ListRoles lists roles for a tenant (including system roles)
|
||||
func (s *Store) ListRoles(ctx context.Context, tenantID *uuid.UUID) ([]*Role, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, tenant_id, name, description, permissions, is_system_role, hierarchy_level, created_at, updated_at
|
||||
FROM compliance_roles
|
||||
WHERE tenant_id = $1 OR is_system_role = TRUE
|
||||
ORDER BY hierarchy_level, name
|
||||
`, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var roles []*Role
|
||||
for rows.Next() {
|
||||
var role Role
|
||||
err := rows.Scan(
|
||||
&role.ID, &role.TenantID, &role.Name, &role.Description,
|
||||
&role.Permissions, &role.IsSystemRole, &role.HierarchyLevel,
|
||||
&role.CreatedAt, &role.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
roles = append(roles, &role)
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
// ListSystemRoles lists all system roles
|
||||
func (s *Store) ListSystemRoles(ctx context.Context) ([]*Role, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, tenant_id, name, description, permissions, is_system_role, hierarchy_level, created_at, updated_at
|
||||
FROM compliance_roles
|
||||
WHERE is_system_role = TRUE
|
||||
ORDER BY hierarchy_level, name
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var roles []*Role
|
||||
for rows.Next() {
|
||||
var role Role
|
||||
err := rows.Scan(
|
||||
&role.ID, &role.TenantID, &role.Name, &role.Description,
|
||||
&role.Permissions, &role.IsSystemRole, &role.HierarchyLevel,
|
||||
&role.CreatedAt, &role.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
roles = append(roles, &role)
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// User Role Operations
|
||||
// ============================================================================
|
||||
|
||||
// AssignRole assigns a role to a user
|
||||
func (s *Store) AssignRole(ctx context.Context, ur *UserRole) error {
|
||||
ur.ID = uuid.New()
|
||||
ur.CreatedAt = time.Now().UTC()
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO compliance_user_roles (id, user_id, role_id, tenant_id, namespace_id, granted_by, expires_at, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (user_id, role_id, tenant_id, namespace_id) DO UPDATE SET
|
||||
granted_by = EXCLUDED.granted_by,
|
||||
expires_at = EXCLUDED.expires_at
|
||||
`, ur.ID, ur.UserID, ur.RoleID, ur.TenantID, ur.NamespaceID, ur.GrantedBy, ur.ExpiresAt, ur.CreatedAt)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// RevokeRole revokes a role from a user
|
||||
func (s *Store) RevokeRole(ctx context.Context, userID, roleID, tenantID uuid.UUID, namespaceID *uuid.UUID) error {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
DELETE FROM compliance_user_roles
|
||||
WHERE user_id = $1 AND role_id = $2 AND tenant_id = $3 AND (namespace_id = $4 OR (namespace_id IS NULL AND $4 IS NULL))
|
||||
`, userID, roleID, tenantID, namespaceID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetUserRoles retrieves all roles for a user in a tenant
|
||||
func (s *Store) GetUserRoles(ctx context.Context, userID, tenantID uuid.UUID) ([]*UserRole, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT ur.id, ur.user_id, ur.role_id, ur.tenant_id, ur.namespace_id, ur.granted_by, ur.expires_at, ur.created_at,
|
||||
r.name as role_name, r.permissions as role_permissions,
|
||||
n.name as namespace_name
|
||||
FROM compliance_user_roles ur
|
||||
JOIN compliance_roles r ON ur.role_id = r.id
|
||||
LEFT JOIN compliance_namespaces n ON ur.namespace_id = n.id
|
||||
WHERE ur.user_id = $1 AND ur.tenant_id = $2
|
||||
AND (ur.expires_at IS NULL OR ur.expires_at > NOW())
|
||||
ORDER BY r.hierarchy_level, r.name
|
||||
`, userID, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var userRoles []*UserRole
|
||||
for rows.Next() {
|
||||
var ur UserRole
|
||||
var namespaceName *string
|
||||
|
||||
err := rows.Scan(
|
||||
&ur.ID, &ur.UserID, &ur.RoleID, &ur.TenantID, &ur.NamespaceID,
|
||||
&ur.GrantedBy, &ur.ExpiresAt, &ur.CreatedAt,
|
||||
&ur.RoleName, &ur.RolePermissions, &namespaceName,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if namespaceName != nil {
|
||||
ur.NamespaceName = *namespaceName
|
||||
}
|
||||
|
||||
userRoles = append(userRoles, &ur)
|
||||
}
|
||||
|
||||
return userRoles, nil
|
||||
}
|
||||
|
||||
// GetUserRolesForNamespace retrieves roles for a user in a specific namespace
|
||||
func (s *Store) GetUserRolesForNamespace(ctx context.Context, userID, tenantID uuid.UUID, namespaceID *uuid.UUID) ([]*UserRole, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT ur.id, ur.user_id, ur.role_id, ur.tenant_id, ur.namespace_id, ur.granted_by, ur.expires_at, ur.created_at,
|
||||
r.name as role_name, r.permissions as role_permissions
|
||||
FROM compliance_user_roles ur
|
||||
JOIN compliance_roles r ON ur.role_id = r.id
|
||||
WHERE ur.user_id = $1 AND ur.tenant_id = $2
|
||||
AND (ur.namespace_id = $3 OR ur.namespace_id IS NULL)
|
||||
AND (ur.expires_at IS NULL OR ur.expires_at > NOW())
|
||||
ORDER BY r.hierarchy_level, r.name
|
||||
`, userID, tenantID, namespaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var userRoles []*UserRole
|
||||
for rows.Next() {
|
||||
var ur UserRole
|
||||
err := rows.Scan(
|
||||
&ur.ID, &ur.UserID, &ur.RoleID, &ur.TenantID, &ur.NamespaceID,
|
||||
&ur.GrantedBy, &ur.ExpiresAt, &ur.CreatedAt,
|
||||
&ur.RoleName, &ur.RolePermissions,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
userRoles = append(userRoles, &ur)
|
||||
}
|
||||
|
||||
return userRoles, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LLM Policy Operations
|
||||
// ============================================================================
|
||||
|
||||
// CreateLLMPolicy creates a new LLM policy
|
||||
func (s *Store) CreateLLMPolicy(ctx context.Context, policy *LLMPolicy) error {
|
||||
policy.ID = uuid.New()
|
||||
policy.CreatedAt = time.Now().UTC()
|
||||
policy.UpdatedAt = policy.CreatedAt
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO compliance_llm_policies (
|
||||
id, tenant_id, namespace_id, name, description,
|
||||
allowed_data_categories, blocked_data_categories,
|
||||
require_pii_redaction, pii_redaction_level,
|
||||
allowed_models, max_tokens_per_request, max_requests_per_day, max_requests_per_hour,
|
||||
is_active, priority, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
||||
`,
|
||||
policy.ID, policy.TenantID, policy.NamespaceID, policy.Name, policy.Description,
|
||||
policy.AllowedDataCategories, policy.BlockedDataCategories,
|
||||
policy.RequirePIIRedaction, policy.PIIRedactionLevel,
|
||||
policy.AllowedModels, policy.MaxTokensPerRequest, policy.MaxRequestsPerDay, policy.MaxRequestsPerHour,
|
||||
policy.IsActive, policy.Priority, policy.CreatedAt, policy.UpdatedAt,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetLLMPolicy retrieves an LLM policy by ID
|
||||
func (s *Store) GetLLMPolicy(ctx context.Context, id uuid.UUID) (*LLMPolicy, error) {
|
||||
var policy LLMPolicy
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, namespace_id, name, description,
|
||||
allowed_data_categories, blocked_data_categories,
|
||||
require_pii_redaction, pii_redaction_level,
|
||||
allowed_models, max_tokens_per_request, max_requests_per_day, max_requests_per_hour,
|
||||
is_active, priority, created_at, updated_at
|
||||
FROM compliance_llm_policies
|
||||
WHERE id = $1
|
||||
`, id).Scan(
|
||||
&policy.ID, &policy.TenantID, &policy.NamespaceID, &policy.Name, &policy.Description,
|
||||
&policy.AllowedDataCategories, &policy.BlockedDataCategories,
|
||||
&policy.RequirePIIRedaction, &policy.PIIRedactionLevel,
|
||||
&policy.AllowedModels, &policy.MaxTokensPerRequest, &policy.MaxRequestsPerDay, &policy.MaxRequestsPerHour,
|
||||
&policy.IsActive, &policy.Priority, &policy.CreatedAt, &policy.UpdatedAt,
|
||||
)
|
||||
|
||||
return &policy, err
|
||||
}
|
||||
|
||||
// GetEffectiveLLMPolicy retrieves the effective LLM policy for a namespace
|
||||
func (s *Store) GetEffectiveLLMPolicy(ctx context.Context, tenantID uuid.UUID, namespaceID *uuid.UUID) (*LLMPolicy, error) {
|
||||
var policy LLMPolicy
|
||||
|
||||
// Get most specific active policy (namespace-specific or tenant-wide)
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, namespace_id, name, description,
|
||||
allowed_data_categories, blocked_data_categories,
|
||||
require_pii_redaction, pii_redaction_level,
|
||||
allowed_models, max_tokens_per_request, max_requests_per_day, max_requests_per_hour,
|
||||
is_active, priority, created_at, updated_at
|
||||
FROM compliance_llm_policies
|
||||
WHERE tenant_id = $1
|
||||
AND is_active = TRUE
|
||||
AND (namespace_id = $2 OR namespace_id IS NULL)
|
||||
ORDER BY
|
||||
CASE WHEN namespace_id = $2 THEN 0 ELSE 1 END,
|
||||
priority ASC
|
||||
LIMIT 1
|
||||
`, tenantID, namespaceID).Scan(
|
||||
&policy.ID, &policy.TenantID, &policy.NamespaceID, &policy.Name, &policy.Description,
|
||||
&policy.AllowedDataCategories, &policy.BlockedDataCategories,
|
||||
&policy.RequirePIIRedaction, &policy.PIIRedactionLevel,
|
||||
&policy.AllowedModels, &policy.MaxTokensPerRequest, &policy.MaxRequestsPerDay, &policy.MaxRequestsPerHour,
|
||||
&policy.IsActive, &policy.Priority, &policy.CreatedAt, &policy.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil // No policy = allow all
|
||||
}
|
||||
|
||||
return &policy, err
|
||||
}
|
||||
|
||||
// ListLLMPolicies lists LLM policies for a tenant
|
||||
func (s *Store) ListLLMPolicies(ctx context.Context, tenantID uuid.UUID) ([]*LLMPolicy, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, tenant_id, namespace_id, name, description,
|
||||
allowed_data_categories, blocked_data_categories,
|
||||
require_pii_redaction, pii_redaction_level,
|
||||
allowed_models, max_tokens_per_request, max_requests_per_day, max_requests_per_hour,
|
||||
is_active, priority, created_at, updated_at
|
||||
FROM compliance_llm_policies
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY priority, name
|
||||
`, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var policies []*LLMPolicy
|
||||
for rows.Next() {
|
||||
var policy LLMPolicy
|
||||
err := rows.Scan(
|
||||
&policy.ID, &policy.TenantID, &policy.NamespaceID, &policy.Name, &policy.Description,
|
||||
&policy.AllowedDataCategories, &policy.BlockedDataCategories,
|
||||
&policy.RequirePIIRedaction, &policy.PIIRedactionLevel,
|
||||
&policy.AllowedModels, &policy.MaxTokensPerRequest, &policy.MaxRequestsPerDay, &policy.MaxRequestsPerHour,
|
||||
&policy.IsActive, &policy.Priority, &policy.CreatedAt, &policy.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
policies = append(policies, &policy)
|
||||
}
|
||||
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
// UpdateLLMPolicy updates an LLM policy
|
||||
func (s *Store) UpdateLLMPolicy(ctx context.Context, policy *LLMPolicy) error {
|
||||
policy.UpdatedAt = time.Now().UTC()
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE compliance_llm_policies SET
|
||||
name = $2, description = $3,
|
||||
allowed_data_categories = $4, blocked_data_categories = $5,
|
||||
require_pii_redaction = $6, pii_redaction_level = $7,
|
||||
allowed_models = $8, max_tokens_per_request = $9, max_requests_per_day = $10, max_requests_per_hour = $11,
|
||||
is_active = $12, priority = $13, updated_at = $14
|
||||
WHERE id = $1
|
||||
`,
|
||||
policy.ID, policy.Name, policy.Description,
|
||||
policy.AllowedDataCategories, policy.BlockedDataCategories,
|
||||
policy.RequirePIIRedaction, policy.PIIRedactionLevel,
|
||||
policy.AllowedModels, policy.MaxTokensPerRequest, policy.MaxRequestsPerDay, policy.MaxRequestsPerHour,
|
||||
policy.IsActive, policy.Priority, policy.UpdatedAt,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteLLMPolicy deletes an LLM policy
|
||||
func (s *Store) DeleteLLMPolicy(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := s.pool.Exec(ctx, `DELETE FROM compliance_llm_policies WHERE id = $1`, id)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user