'use client' /** * RBAC Management Page * * Features: * - Multi-tenant management * - Namespace-based isolation (CFO use case) * - Role management with permissions * - User-Role assignments with scope * - LLM access policies */ import { useState, useEffect, useCallback } from 'react' import { PagePurpose } from '@/components/common/PagePurpose' // Types interface Tenant { id: string name: string slug: string settings: Record max_users: number llm_quota_monthly: number status: string created_at: string } interface Namespace { id: string tenant_id: string name: string slug: string parent_namespace_id: string | null isolation_level: string data_classification: string metadata: Record created_at: string } interface Role { id: string tenant_id: string | null name: string description: string permissions: string[] is_system_role: boolean hierarchy_level: number created_at: string } interface UserRole { id: string user_id: string role_id: string role_name?: string tenant_id: string namespace_id: string | null namespace_name?: string granted_by: string expires_at: string | null created_at: string } interface LLMPolicy { id: string tenant_id: string namespace_id: string | null name: string allowed_data_categories: string[] blocked_data_categories: string[] require_pii_redaction: boolean pii_redaction_level: string allowed_models: string[] max_tokens_per_request: number max_requests_per_day: number created_at: string } // Tab configuration type TabId = 'tenants' | 'namespaces' | 'roles' | 'users' | 'policies' const TABS: { id: TabId; label: string; icon: string }[] = [ { id: 'tenants', label: 'Mandanten', icon: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4' }, { id: 'namespaces', label: 'Namespaces', icon: 'M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4' }, { id: 'roles', label: 'Rollen', icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z' }, { id: 'users', label: 'Benutzer-Rollen', icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z' }, { id: 'policies', label: 'LLM-Policies', icon: 'M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z' }, ] const STATUS_COLORS: Record = { active: 'bg-green-100 text-green-700', inactive: 'bg-slate-100 text-slate-500', suspended: 'bg-red-100 text-red-700', } const ISOLATION_COLORS: Record = { strict: 'bg-red-100 text-red-700', standard: 'bg-yellow-100 text-yellow-700', relaxed: 'bg-green-100 text-green-700', } const DATA_CLASSIFICATION_COLORS: Record = { restricted: 'bg-red-100 text-red-700', confidential: 'bg-orange-100 text-orange-700', internal: 'bg-yellow-100 text-yellow-700', public: 'bg-green-100 text-green-700', } // SDK requests are proxied through nginx on the same origin (no CORS issues) const SDK_BASE_URL = '' export default function RBACPage() { const [activeTab, setActiveTab] = useState('tenants') const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Data states const [tenants, setTenants] = useState([]) const [namespaces, setNamespaces] = useState([]) const [roles, setRoles] = useState([]) const [userRoles, setUserRoles] = useState([]) const [policies, setPolicies] = useState([]) // Filter states const [selectedTenantId, setSelectedTenantId] = useState('') const [searchTerm, setSearchTerm] = useState('') // Modal states const [showCreateModal, setShowCreateModal] = useState(false) const [editItem, setEditItem] = useState(null) // Load data based on active tab const loadData = useCallback(async () => { setLoading(true) setError(null) try { const headers: Record = { 'Content-Type': 'application/json', } if (selectedTenantId) { headers['X-Tenant-ID'] = selectedTenantId } switch (activeTab) { case 'tenants': { const res = await fetch(`${SDK_BASE_URL}/sdk/v1/tenants`, { headers }) if (res.ok) { const data = await res.json() setTenants(data.tenants || []) } break } case 'namespaces': { if (!selectedTenantId) { setNamespaces([]) break } const res = await fetch(`${SDK_BASE_URL}/sdk/v1/tenants/${selectedTenantId}/namespaces`, { headers }) if (res.ok) { const data = await res.json() setNamespaces(data.namespaces || []) } break } case 'roles': { const res = await fetch(`${SDK_BASE_URL}/sdk/v1/roles`, { headers }) if (res.ok) { const data = await res.json() setRoles(data.roles || []) } break } case 'users': { // This would need a user ID - for now show empty setUserRoles([]) break } case 'policies': { const res = await fetch(`${SDK_BASE_URL}/sdk/v1/llm/policies`, { headers }) if (res.ok) { const data = await res.json() setPolicies(data.policies || []) } break } } } catch (err) { console.error('Failed to load data:', err) setError('Verbindung zum AI Compliance SDK fehlgeschlagen. Stellen Sie sicher, dass der SDK-Service laeuft.') } finally { setLoading(false) } }, [activeTab, selectedTenantId]) useEffect(() => { loadData() }, [loadData]) // Load tenants on mount for the filter useEffect(() => { const loadTenants = async () => { try { const res = await fetch(`${SDK_BASE_URL}/sdk/v1/tenants`) if (res.ok) { const data = await res.json() setTenants(data.tenants || []) if (data.tenants?.length > 0 && !selectedTenantId) { setSelectedTenantId(data.tenants[0].id) } } } catch (err) { console.error('Failed to load tenants:', err) } } loadTenants() }, []) const handleCreate = async (data: any) => { try { const headers: Record = { 'Content-Type': 'application/json', } if (selectedTenantId) { headers['X-Tenant-ID'] = selectedTenantId } let endpoint = '' switch (activeTab) { case 'tenants': endpoint = `${SDK_BASE_URL}/sdk/v1/tenants` break case 'namespaces': endpoint = `${SDK_BASE_URL}/sdk/v1/tenants/${selectedTenantId}/namespaces` break case 'roles': endpoint = `${SDK_BASE_URL}/sdk/v1/roles` break case 'policies': endpoint = `${SDK_BASE_URL}/sdk/v1/llm/policies` break } const res = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify(data), }) if (res.ok) { setShowCreateModal(false) loadData() } else { const errData = await res.json() alert(`Fehler: ${errData.error || 'Unbekannter Fehler'}`) } } catch (err) { console.error('Create failed:', err) alert('Erstellen fehlgeschlagen') } } const filteredData = () => { const term = searchTerm.toLowerCase() switch (activeTab) { case 'tenants': return tenants.filter(t => t.name.toLowerCase().includes(term) || t.slug.toLowerCase().includes(term) ) case 'namespaces': return namespaces.filter(n => n.name.toLowerCase().includes(term) || n.slug.toLowerCase().includes(term) ) case 'roles': return roles.filter(r => r.name.toLowerCase().includes(term) || r.description?.toLowerCase().includes(term) ) case 'policies': return policies.filter(p => p.name.toLowerCase().includes(term) ) default: return [] } } // Statistics const stats = { tenants: tenants.length, namespaces: namespaces.length, roles: roles.length, systemRoles: roles.filter(r => r.is_system_role).length, policies: policies.length, activeUsers: userRoles.length, } return (
{/* Header */}

RBAC Management

Rollen, Berechtigungen & LLM-Zugriffskontrolle

{/* Page Purpose */} {/* Statistics */}

Mandanten

{stats.tenants}

Namespaces

{stats.namespaces}

Rollen

{stats.roles}

System-Rollen

{stats.systemRoles}

LLM-Policies

{stats.policies}

Zuweisungen

{stats.activeUsers}

{/* Tabs */}
{/* Search and Actions */}
setSearchTerm(e.target.value)} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500" />
{/* Content */}
{error && (
{error}
)} {loading ? (
) : ( <> {/* Tenants Tab */} {activeTab === 'tenants' && ( )} {/* Namespaces Tab */} {activeTab === 'namespaces' && ( )} {/* Roles Tab */} {activeTab === 'roles' && ( )} {/* User Roles Tab */} {activeTab === 'users' && ( )} {/* Policies Tab */} {activeTab === 'policies' && ( )} )}
{/* Create Modal */} {showCreateModal && ( setShowCreateModal(false)} onSave={handleCreate} /> )}
) } // Tenants Table Component function TenantsTable({ tenants, onEdit }: { tenants: Tenant[]; onEdit: (t: Tenant) => void }) { if (tenants.length === 0) { return (

Keine Mandanten vorhanden

) } return ( {tenants.map(tenant => ( ))}
Name Slug Status Max Users LLM Quota Aktionen
{tenant.name} {tenant.slug} {tenant.status} {tenant.max_users} {tenant.llm_quota_monthly?.toLocaleString()}
) } // Namespaces Table Component function NamespacesTable({ namespaces, onEdit }: { namespaces: Namespace[]; onEdit: (n: Namespace) => void }) { if (namespaces.length === 0) { return (

Keine Namespaces vorhanden. Waehlen Sie zuerst einen Mandanten.

) } return ( {namespaces.map(ns => ( ))}
Name Slug Isolation Klassifizierung Aktionen
{ns.name} {ns.slug} {ns.isolation_level} {ns.data_classification}
) } // Roles Table Component function RolesTable({ roles, onEdit }: { roles: Role[]; onEdit: (r: Role) => void }) { if (roles.length === 0) { return (

Keine Rollen vorhanden

) } return ( {roles.map(role => ( ))}
Name Beschreibung Typ Hierarchie Permissions Aktionen
{role.name} {role.description || '-'} {role.is_system_role ? 'System' : 'Custom'} {role.hierarchy_level}
{(role.permissions || []).slice(0, 3).map((p, i) => ( {p} ))} {(role.permissions || []).length > 3 && ( +{role.permissions.length - 3} )}
) } // User Roles Table Component function UserRolesTable({ userRoles, onEdit }: { userRoles: UserRole[]; onEdit: (ur: UserRole) => void }) { return (

Benutzer-Rollen Zuweisung

Waehlen Sie einen Benutzer aus, um dessen Rollen zu verwalten.

) } // Policies Table Component function PoliciesTable({ policies, onEdit }: { policies: LLMPolicy[]; onEdit: (p: LLMPolicy) => void }) { if (policies.length === 0) { return (

Keine LLM-Policies vorhanden

) } return ( {policies.map(policy => ( ))}
Name Erlaubte Kategorien Blockierte Kategorien PII Redaction Max Tokens Aktionen
{policy.name}
{(policy.allowed_data_categories || []).map((c, i) => ( {c} ))}
{(policy.blocked_data_categories || []).map((c, i) => ( {c} ))}
{policy.require_pii_redaction ? policy.pii_redaction_level : 'aus'} {policy.max_tokens_per_request?.toLocaleString()}
) } // Create Modal Component function CreateModal({ type, tenantId, onClose, onSave, }: { type: TabId tenantId: string onClose: () => void onSave: (data: any) => void }) { const [formData, setFormData] = useState({}) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() onSave(formData) } const renderForm = () => { switch (type) { case 'tenants': return ( <>
setFormData({ ...formData, name: e.target.value })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500" />
setFormData({ ...formData, slug: e.target.value })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500" />
setFormData({ ...formData, max_users: parseInt(e.target.value) })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500" />
setFormData({ ...formData, llm_quota_monthly: parseInt(e.target.value) })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500" />
) case 'namespaces': return ( <>
setFormData({ ...formData, name: e.target.value })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500" />
setFormData({ ...formData, slug: e.target.value })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500" />
) case 'roles': return ( <>
setFormData({ ...formData, name: e.target.value })} className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500" />