wiki/implementation/RBAC-AccessRules-Refactoring.md
2026-01-25 03:01:35 +01:00

34 KiB

RBAC Access Rules Editor - Refactoring Vorschlag

Übersicht

Dieses Dokument beschreibt die geplante Überarbeitung der RBAC Access Rules UI-Komponenten basierend auf drei Hauptanforderungen:

  1. System-Rollen bearbeitbar machen - AccessRules für System-Rollen editierbar
  2. Checkbox-basiertes UI - Kompakteres Layout mit Checkboxen statt Dropdowns
  3. Dot-Notation & Objekt-Katalog - Verfügbare Objekte als Dropdown auswählbar

1. System-Rollen mit bearbeitbaren AccessRules

Ist-Zustand

Aktuell in AdminMandateRolePermissionsPage.tsx:

<AccessRulesEditor
  roleId={role.id}
  roleName={role.roleLabel}
  readOnly={role.isSystemRole}  // ← System-Rollen sind komplett read-only
  ...
/>

Die readOnly-Prop wird basierend auf isSystemRole gesetzt, was dazu führt, dass AccessRules für System-Rollen weder angezeigt noch bearbeitet werden können.

Soll-Zustand

  • Rollen selbst (roleLabel, description) bleiben geschützt für System-Rollen
  • AccessRules sind für alle Rollen bearbeitbar (inkl. System-Rollen)
  • Zugriffskontrolle erfolgt über RBAC selbst (rbac.rules.manage Permission)

Implementierung

Option A: Separate Prop für Rollen-Protection

// AdminMandateRolePermissionsPage.tsx
<AccessRulesEditor
  roleId={role.id}
  roleName={role.roleLabel}
  readOnly={false}  // AccessRules immer bearbeitbar
  roleProtected={role.isSystemRole}  // Nur Rolle selbst geschützt
  apiBasePath="/api/rbac"
  mandateId={selectedMandateId}
/>

Option B: Immer bearbeitbar (empfohlen)

Entferne die readOnly-Logic basierend auf isSystemRole:

// AdminMandateRolePermissionsPage.tsx
<AccessRulesEditor
  roleId={role.id}
  roleName={role.roleLabel}
  readOnly={false}  // Alle AccessRules bearbeitbar
  apiBasePath="/api/rbac"
  mandateId={selectedMandateId}
/>

Die Zugriffskontrolle wird durch Backend-RBAC sichergestellt (nur SysAdmin kann AccessRules ändern).

Betroffene Dateien

Datei Änderung
frontend_nyla/src/pages/admin/AdminMandateRolePermissionsPage.tsx readOnly={false} setzen

2. Checkbox-basiertes Kompakt-Layout

Ist-Zustand

Aktuell werden AccessLevels (n/m/g/a) über Dropdown-Selects ausgewählt:

┌──────────────────────────────────────────────────────────────────┐
│ 📊 TrusteeContract                                          [🗑] │
├──────────────────────────────────────────────────────────────────┤
│   VIEW      READ       CREATE      UPDATE      DELETE            │
│   [✓]      [Gruppe▼]   [Eigene▼]   [Eigene▼]   [Keine▼]         │
└──────────────────────────────────────────────────────────────────┘

Probleme:

  • Dropdowns benötigen Klicks zum Öffnen
  • Nicht auf einen Blick erkennbar welche Berechtigungen gesetzt sind
  • Mehrere Regeln brauchen viel vertikalen Platz

Soll-Zustand

Kompaktes Checkbox-Grid in einer Zeile pro Objekt:

┌────────────────────────────────────────────────────────────────────────────┐
│ OBJEKT                  │ VIEW │     EIGENE     │    GRUPPE     │   ALLE   │
│                         │      │  C   R   U   D │ C   R   U   D │C  R  U  D│
├────────────────────────────────────────────────────────────────────────────┤
│ data.TrusteeContract    │ [✓]  │ [✓] [✓] [✓] [ ]│[ ] [✓] [ ] [ ]│[ ][ ][ ][ ]│
│ data.TrusteePosition    │ [✓]  │ [✓] [✓] [ ] [ ]│[✓] [✓] [✓] [ ]│[ ][ ][ ][ ]│
│ ui.feature.trustee.*    │ [✓]  │  -   -   -   - │ -   -   -   - │ -  -  -  - │
└────────────────────────────────────────────────────────────────────────────┘

Vorteile:

  • Alle Berechtigungen auf einen Blick sichtbar
  • Schnelle Toggle-Aktionen mit einzelnem Klick
  • Kompakter - mehr Regeln pro Bildschirm
  • Intuitive Matrix-Darstellung (bekannt von Unix-Permissions)

Neue Komponenten-Struktur

AccessRulesEditor/
├── AccessRulesEditor.tsx          # Haupt-Container
├── AccessRulesTable.tsx           # Tabellen-basierte Darstellung (NEU)
├── AccessRuleRow.tsx              # Eine Zeile = ein AccessRule (NEU)
├── AccessLevelCheckboxGroup.tsx   # Checkbox-Gruppe für m/g/a (NEU)
└── AccessRules.module.css         # Styles

Neue Komponente: AccessRulesTable.tsx

interface AccessRulesTableProps {
  rules: AccessRule[];
  context: RuleContext;
  readOnly?: boolean;
  onUpdate: (ruleId: string, updates: Partial<AccessRule>) => void;
  onDelete: (ruleId: string) => void;
}

const AccessRulesTable: React.FC<AccessRulesTableProps> = ({
  rules,
  context,
  readOnly,
  onUpdate,
  onDelete,
}) => {
  const isDataContext = context === 'DATA';
  
  return (
    <table className={styles.accessRulesTable}>
      <thead>
        <tr>
          <th className={styles.colObject}>Objekt (Dot-Notation)</th>
          <th className={styles.colView}>View</th>
          {isDataContext && (
            <>
              <th className={styles.colGroup} colSpan={4}>Eigene (m)</th>
              <th className={styles.colGroup} colSpan={4}>Gruppe (g)</th>
              <th className={styles.colGroup} colSpan={4}>Alle (a)</th>
            </>
          )}
          <th className={styles.colActions}></th>
        </tr>
        {isDataContext && (
          <tr className={styles.subHeader}>
            <th></th>
            <th></th>
            <th>C</th><th>R</th><th>U</th><th>D</th>
            <th>C</th><th>R</th><th>U</th><th>D</th>
            <th>C</th><th>R</th><th>U</th><th>D</th>
            <th></th>
          </tr>
        )}
      </thead>
      <tbody>
        {rules.map(rule => (
          <AccessRuleRow
            key={rule.id}
            rule={rule}
            isDataContext={isDataContext}
            readOnly={readOnly}
            onUpdate={onUpdate}
            onDelete={onDelete}
          />
        ))}
      </tbody>
    </table>
  );
};

Neue Komponente: AccessRuleRow.tsx

interface AccessRuleRowProps {
  rule: AccessRule;
  isDataContext: boolean;
  readOnly?: boolean;
  onUpdate: (ruleId: string, updates: Partial<AccessRule>) => void;
  onDelete: (ruleId: string) => void;
}

const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
  rule,
  isDataContext,
  readOnly,
  onUpdate,
  onDelete,
}) => {
  // Hilfsfunktion: Prüft ob Level mindestens X erreicht
  const hasLevel = (level: AccessLevel | null, minLevel: 'm' | 'g' | 'a'): boolean => {
    if (!level || level === 'n') return false;
    const hierarchy = ['n', 'm', 'g', 'a'];
    return hierarchy.indexOf(level) >= hierarchy.indexOf(minLevel);
  };

  // Hilfsfunktion: Setzt Level basierend auf Checkbox-Änderung
  const toggleLevel = (
    field: 'read' | 'create' | 'update' | 'delete',
    targetLevel: 'm' | 'g' | 'a',
    checked: boolean
  ) => {
    const currentLevel = rule[field] || 'n';
    let newLevel: AccessLevel;
    
    if (checked) {
      // Aktiviere mindestens dieses Level
      newLevel = targetLevel;
    } else {
      // Deaktiviere dieses Level, setze auf nächst-niedrigeres
      const hierarchy = ['n', 'm', 'g', 'a'];
      const targetIndex = hierarchy.indexOf(targetLevel);
      newLevel = hierarchy[targetIndex - 1] as AccessLevel || 'n';
    }
    
    onUpdate(rule.id, { [field]: newLevel });
  };

  return (
    <tr className={styles.ruleRow}>
      {/* Objekt-Name in Dot-Notation */}
      <td className={styles.objectCell}>
        <code>{rule.item || '(global)'}</code>
      </td>
      
      {/* View Checkbox */}
      <td className={styles.checkboxCell}>
        <input
          type="checkbox"
          checked={rule.view}
          onChange={(e) => onUpdate(rule.id, { view: e.target.checked })}
          disabled={readOnly}
        />
      </td>
      
      {/* CRUD Checkboxen für DATA-Kontext */}
      {isDataContext && (
        <>
          {/* Eigene (m) */}
          {['create', 'read', 'update', 'delete'].map(op => (
            <td key={`m-${op}`} className={styles.checkboxCell}>
              <input
                type="checkbox"
                checked={hasLevel(rule[op as keyof AccessRule] as AccessLevel, 'm')}
                onChange={(e) => toggleLevel(op as any, 'm', e.target.checked)}
                disabled={readOnly}
              />
            </td>
          ))}
          
          {/* Gruppe (g) */}
          {['create', 'read', 'update', 'delete'].map(op => (
            <td key={`g-${op}`} className={styles.checkboxCell}>
              <input
                type="checkbox"
                checked={hasLevel(rule[op as keyof AccessRule] as AccessLevel, 'g')}
                onChange={(e) => toggleLevel(op as any, 'g', e.target.checked)}
                disabled={readOnly}
              />
            </td>
          ))}
          
          {/* Alle (a) */}
          {['create', 'read', 'update', 'delete'].map(op => (
            <td key={`a-${op}`} className={styles.checkboxCell}>
              <input
                type="checkbox"
                checked={hasLevel(rule[op as keyof AccessRule] as AccessLevel, 'a')}
                onChange={(e) => toggleLevel(op as any, 'a', e.target.checked)}
                disabled={readOnly}
              />
            </td>
          ))}
        </>
      )}
      
      {/* Löschen-Button */}
      <td className={styles.actionsCell}>
        {!readOnly && (
          <button
            className={styles.deleteButton}
            onClick={() => onDelete(rule.id)}
            title="Regel löschen"
          >
            <FaTrash />
          </button>
        )}
      </td>
    </tr>
  );
};

CSS für Tabellen-Layout

/* AccessRules.module.css - Erweiterungen */

.accessRulesTable {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
}

.accessRulesTable th,
.accessRulesTable td {
  padding: 0.5rem 0.375rem;
  border-bottom: 1px solid var(--border-color);
  text-align: center;
}

.accessRulesTable th {
  background: var(--bg-secondary);
  font-weight: 600;
  font-size: 0.75rem;
  text-transform: uppercase;
  color: var(--text-secondary);
}

.colObject {
  text-align: left !important;
  min-width: 200px;
}

.colView {
  width: 50px;
}

.colGroup {
  border-left: 2px solid var(--border-color);
}

.subHeader th {
  font-size: 0.6875rem;
  padding: 0.25rem;
  background: var(--bg-tertiary);
}

.objectCell {
  text-align: left !important;
}

.objectCell code {
  font-family: 'Monaco', 'Menlo', monospace;
  font-size: 0.8125rem;
  background: var(--bg-tertiary);
  padding: 0.125rem 0.375rem;
  border-radius: 3px;
}

.checkboxCell {
  width: 32px;
}

.checkboxCell input[type="checkbox"] {
  width: 16px;
  height: 16px;
  cursor: pointer;
  accent-color: var(--primary-color);
}

.actionsCell {
  width: 40px;
}

.deleteButton {
  padding: 0.25rem;
  background: transparent;
  border: none;
  color: var(--text-tertiary);
  cursor: pointer;
  border-radius: 4px;
}

.deleteButton:hover {
  background: #fed7d7;
  color: #c53030;
}

Betroffene Dateien

Datei Änderung
frontend_nyla/src/components/AccessRules/AccessRulesTable.tsx NEU erstellen
frontend_nyla/src/components/AccessRules/AccessRuleRow.tsx NEU erstellen
frontend_nyla/src/components/AccessRules/AccessRulesEditor.tsx Integriere AccessRulesTable
frontend_nyla/src/components/AccessRules/AccessRules.module.css Tabellen-Styles hinzufügen
frontend_nyla/src/components/AccessRules/index.ts Exports aktualisieren

3. Dot-Notation & RBAC-Objekt-Katalog

Ist-Zustand

Aktuell wird das item-Feld als Freitext eingegeben:

// AddRuleForm.tsx (aktuell)
<input
  type="text"
  value={item}
  onChange={(e) => setItem(e.target.value)}
  placeholder="z.B. TrusteeContract oder TrusteeContract.salary"
/>

Probleme:

  • Benutzer muss gültige Objekt-Namen kennen
  • Keine Validierung gegen registrierte Objekte
  • Inkonsistente Schreibweisen möglich
  • Keine Übersicht der verfügbaren Objekte

Backend: RBAC Catalog Service

Der RbacCatalogService (gateway/modules/security/rbacCatalog.py) registriert bereits alle verfügbaren Objekte:

class RbacCatalogService:
    def __init__(self):
        self._uiObjects: Dict[str, Dict[str, Any]] = {}
        self._resourceObjects: Dict[str, Dict[str, Any]] = {}
        self._dataObjects: Dict[str, Dict[str, Any]] = {}  # ← NEU
        
    def getUiObjects(self, featureCode: Optional[str] = None) -> List[Dict[str, Any]]:
        # Gibt UI-Objekte zurück (z.B. "ui.feature.trustee.dashboard")
        
    def getResourceObjects(self, featureCode: Optional[str] = None) -> List[Dict[str, Any]]:
        # Gibt Resource-Objekte zurück (z.B. "resource.feature.trustee.documents.create")

Fehlt: DATA-Objekte werden nicht im Katalog registriert!

Soll-Zustand

  1. Backend: Neuer API-Endpoint zum Abrufen verfügbarer RBAC-Objekte
  2. Backend: DATA-Objekte (Tabellen/Entitäten) im Katalog registrieren
  3. Frontend: Dropdown zur Auswahl aus verfügbaren Objekten
  4. Frontend: Konsistente Dot-Notation für alle Objekte

Backend-Implementierung

1. DATA-Objekte im Katalog registrieren

Erweitere rbacCatalog.py:

# gateway/modules/security/rbacCatalog.py

class RbacCatalogService:
    def __init__(self):
        self._uiObjects: Dict[str, Dict[str, Any]] = {}
        self._resourceObjects: Dict[str, Dict[str, Any]] = {}
        self._dataObjects: Dict[str, Dict[str, Any]] = {}  # NEU
        
    def registerDataObject(
        self, 
        featureCode: str, 
        objectKey: str, 
        label: Dict[str, str], 
        meta: Optional[Dict[str, Any]] = None
    ) -> bool:
        """Register a DATA object (table/entity) for a feature."""
        try:
            self._dataObjects[objectKey] = {
                "objectKey": objectKey,
                "featureCode": featureCode,
                "label": label,
                "meta": meta or {},
                "type": "DATA"
            }
            return True
        except Exception as e:
            logger.error(f"Failed to register DATA object {objectKey}: {e}")
            return False
    
    def getDataObjects(self, featureCode: Optional[str] = None) -> List[Dict[str, Any]]:
        """Get all DATA objects, optionally filtered by feature."""
        if featureCode:
            return [obj for obj in self._dataObjects.values() 
                    if obj["featureCode"] == featureCode]
        return list(self._dataObjects.values())
    
    def getAllCatalogObjects(self, featureCode: Optional[str] = None) -> Dict[str, List[Dict[str, Any]]]:
        """Get all catalog objects grouped by type."""
        return {
            "DATA": self.getDataObjects(featureCode),
            "UI": self.getUiObjects(featureCode),
            "RESOURCE": self.getResourceObjects(featureCode)
        }

2. DATA-Objekte in Feature registrieren

Erweitere mainTrustee.py:

# gateway/modules/features/trustee/mainTrustee.py

# DATA Objects for RBAC catalog (Tabellen/Entitäten)
DATA_OBJECTS = [
    {
        "objectKey": "data.feature.trustee.TrusteeContract",
        "label": {"en": "Contract", "de": "Vertrag", "fr": "Contrat"},
        "meta": {"table": "TrusteeContract", "fields": ["id", "name", "salary", ...]}
    },
    {
        "objectKey": "data.feature.trustee.TrusteePosition",
        "label": {"en": "Position", "de": "Position", "fr": "Position"},
        "meta": {"table": "TrusteePosition", "fields": ["id", "label", ...]}
    },
    {
        "objectKey": "data.feature.trustee.TrusteeDocument",
        "label": {"en": "Document", "de": "Dokument", "fr": "Document"},
        "meta": {"table": "TrusteeDocument", "fields": ["id", "filename", ...]}
    },
]

def registerFeature(catalogService) -> bool:
    # ... bestehende UI/Resource Registrierung ...
    
    # NEU: DATA-Objekte registrieren
    for dataObj in DATA_OBJECTS:
        catalogService.registerDataObject(
            featureCode=FEATURE_CODE,
            objectKey=dataObj["objectKey"],
            label=dataObj["label"],
            meta=dataObj.get("meta")
        )

3. Neuer API-Endpoint für Katalog-Objekte

Erweitere routeAdminRbacRules.py:

# gateway/modules/routes/routeAdminRbacRules.py

from modules.security.rbacCatalog import getCatalogService

@router.get("/catalog/objects", response_model=Dict[str, Any])
@limiter.limit("60/minute")
async def get_catalog_objects(
    request: Request,
    context: Optional[str] = Query(None, description="Filter by context (DATA, UI, RESOURCE)"),
    featureCode: Optional[str] = Query(None, description="Filter by feature code"),
    currentUser: User = Depends(requireSysAdmin)
) -> Dict[str, Any]:
    """
    Get available RBAC catalog objects.
    Returns all registered DATA, UI and RESOURCE objects that can be used in AccessRules.
    
    Query Parameters:
    - context: Optional filter by context type (DATA, UI, RESOURCE)
    - featureCode: Optional filter by feature (e.g., "trustee")
    
    Returns:
    - Dictionary with objects grouped by context type, each with:
      - objectKey: Dot-notation identifier (e.g., "data.feature.trustee.TrusteeContract")
      - label: Multilingual label
      - featureCode: Owning feature
      - meta: Additional metadata
    
    Examples:
    - GET /api/rbac/catalog/objects                      → all objects
    - GET /api/rbac/catalog/objects?context=DATA         → only DATA objects
    - GET /api/rbac/catalog/objects?featureCode=trustee  → only trustee objects
    """
    try:
        catalog = getCatalogService()
        
        if context:
            # Einzelner Context
            try:
                accessContext = AccessRuleContext(context.upper())
            except ValueError:
                raise HTTPException(
                    status_code=400,
                    detail=f"Invalid context '{context}'. Must be one of: DATA, UI, RESOURCE"
                )
            
            if accessContext == AccessRuleContext.DATA:
                objects = catalog.getDataObjects(featureCode)
            elif accessContext == AccessRuleContext.UI:
                objects = catalog.getUiObjects(featureCode)
            else:
                objects = catalog.getResourceObjects(featureCode)
            
            return {context.upper(): objects}
        else:
            # Alle Contexts
            return catalog.getAllCatalogObjects(featureCode)
            
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error getting catalog objects: {str(e)}")
        raise HTTPException(
            status_code=500,
            detail=f"Failed to get catalog objects: {str(e)}"
        )

Frontend-Implementierung

1. Hook für Katalog-Objekte

Erstelle useCatalogObjects.ts:

// frontend_nyla/src/hooks/useCatalogObjects.ts

import { useState, useCallback } from 'react';
import api from '../api';
import { RuleContext } from './useAccessRules';

export interface CatalogObject {
  objectKey: string;
  featureCode: string;
  label: { [lang: string]: string };
  meta?: Record<string, any>;
  type: RuleContext;
}

interface CatalogObjects {
  DATA: CatalogObject[];
  UI: CatalogObject[];
  RESOURCE: CatalogObject[];
}

export function useCatalogObjects() {
  const [objects, setObjects] = useState<CatalogObjects>({ DATA: [], UI: [], RESOURCE: [] });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchObjects = useCallback(async (
    context?: RuleContext, 
    featureCode?: string
  ): Promise<CatalogObjects> => {
    setLoading(true);
    setError(null);
    
    try {
      const params = new URLSearchParams();
      if (context) params.append('context', context);
      if (featureCode) params.append('featureCode', featureCode);
      
      const url = `/api/rbac/catalog/objects${params.toString() ? `?${params}` : ''}`;
      const response = await api.get(url);
      
      const data = response.data as CatalogObjects;
      setObjects(data);
      return data;
    } catch (err: any) {
      const errorMsg = err.response?.data?.detail || err.message || 'Fehler beim Laden';
      setError(errorMsg);
      return { DATA: [], UI: [], RESOURCE: [] };
    } finally {
      setLoading(false);
    }
  }, []);

  const getObjectsByContext = useCallback((context: RuleContext): CatalogObject[] => {
    return objects[context] || [];
  }, [objects]);

  return {
    objects,
    loading,
    error,
    fetchObjects,
    getObjectsByContext,
  };
}

2. Objekt-Auswahl Dropdown

Aktualisiere AddRuleForm:

// AccessRulesEditor.tsx - AddRuleForm Komponente

interface AddRuleFormProps {
  context: RuleContext;
  availableObjects: CatalogObject[];  // NEU
  onAdd: (rule: AccessRuleCreate) => void;
  onCancel: () => void;
}

const AddRuleForm: React.FC<AddRuleFormProps> = ({ 
  context, 
  availableObjects,  // NEU
  onAdd, 
  onCancel 
}) => {
  const [item, setItem] = useState('');
  const [useCustom, setUseCustom] = useState(false);  // NEU: Toggle für Freitext
  // ... rest state
  
  // Gruppiere Objekte nach Feature
  const groupedObjects = useMemo(() => {
    const grouped: Record<string, CatalogObject[]> = {};
    availableObjects.forEach(obj => {
      if (!grouped[obj.featureCode]) {
        grouped[obj.featureCode] = [];
      }
      grouped[obj.featureCode].push(obj);
    });
    return grouped;
  }, [availableObjects]);
  
  // Aktuelle Sprache für Labels
  const lang = useLanguage(); // oder 'de' als Default
  
  return (
    <form className={styles.addRuleForm} onSubmit={handleSubmit}>
      <div className={styles.formGroup}>
        <label className={styles.formLabel}>
          Objekt auswählen
          <button 
            type="button" 
            className={styles.toggleCustom}
            onClick={() => setUseCustom(!useCustom)}
          >
            {useCustom ? '← Aus Katalog wählen' : 'Freie Eingabe →'}
          </button>
        </label>
        
        {useCustom ? (
          // Freitext-Eingabe (wie bisher)
          <input
            type="text"
            value={item}
            onChange={(e) => setItem(e.target.value)}
            placeholder={getPlaceholder()}
            className={styles.formInput}
          />
        ) : (
          // Dropdown mit verfügbaren Objekten
          <select
            value={item}
            onChange={(e) => setItem(e.target.value)}
            className={styles.formSelect}
          >
            <option value="">-- Global (alle Objekte) --</option>
            {Object.entries(groupedObjects).map(([feature, objs]) => (
              <optgroup key={feature} label={feature.toUpperCase()}>
                {objs.map(obj => (
                  <option key={obj.objectKey} value={obj.objectKey}>
                    {obj.objectKey} - {obj.label[lang] || obj.label.en}
                  </option>
                ))}
              </optgroup>
            ))}
          </select>
        )}
        
        <span className={styles.formHint}>
          Leer lassen für globale Regel. Längster Match gewinnt.
        </span>
      </div>
      
      {/* ... rest of form */}
    </form>
  );
};

3. Katalog-Objekte im Editor laden

// AccessRulesEditor.tsx

export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
  roleId,
  roleName,
  readOnly = false,
  apiBasePath = '/api/rbac',
  mandateId,
}) => {
  // ... bestehende hooks
  
  // NEU: Katalog-Objekte laden
  const { objects: catalogObjects, fetchObjects } = useCatalogObjects();
  
  useEffect(() => {
    fetchObjects(); // Alle Objekte laden beim Mount
  }, [fetchObjects]);
  
  // Objekte für aktuellen Tab filtern
  const currentContextObjects = useMemo(() => {
    return catalogObjects[activeTab] || [];
  }, [catalogObjects, activeTab]);
  
  return (
    <div className={styles.accessRulesEditor}>
      {/* ... header, tabs */}
      
      <div className={styles.tabContent}>
        {activeTab !== 'JSON' && (
          <RulesSection
            context={activeTab}
            rules={groupedRules[activeTab]}
            availableObjects={currentContextObjects}  // NEU
            readOnly={readOnly}
            onUpdate={handleUpdate}
            onDelete={handleDelete}
            onAdd={handleAdd}
          />
        )}
        {/* ... JSON tab */}
      </div>
      
      {/* ... action bar */}
    </div>
  );
};

Dot-Notation Schema

Konsistentes Namensschema für alle RBAC-Objekte:

<type>.<scope>.<feature>.<entity>[.<field>]

Beispiele:
├── DATA
│   ├── data.feature.trustee.TrusteeContract
│   ├── data.feature.trustee.TrusteeContract.salary    (Feld-Level)
│   ├── data.feature.trustee.TrusteePosition
│   ├── data.system.UserInDB                            (System-Entität)
│   └── data.system.Mandate
│
├── UI
│   ├── ui.feature.trustee.dashboard
│   ├── ui.feature.trustee.positions
│   ├── ui.admin.users                                  (Admin-Bereich)
│   ├── ui.playground.voice.settings                    (Playground)
│   └── ui.nav.trustee                                  (Navigation)
│
└── RESOURCE
    ├── resource.feature.trustee.documents.create
    ├── resource.feature.trustee.instance-roles.manage
    ├── resource.ai.model.anthropic                     (AI-Resources)
    └── resource.connector.sharepoint                   (Connectors)

Betroffene Dateien

Datei Änderung
Backend
gateway/modules/security/rbacCatalog.py registerDataObject(), getDataObjects(), getAllCatalogObjects()
gateway/modules/routes/routeAdminRbacRules.py Neuer Endpoint GET /api/rbac/catalog/objects
gateway/modules/features/trustee/mainTrustee.py DATA_OBJECTS Liste, Registrierung erweitern
gateway/modules/features/*/main*.py DATA_OBJECTS in allen Features
Frontend
frontend_nyla/src/hooks/useCatalogObjects.ts NEU erstellen
frontend_nyla/src/hooks/index.ts Export hinzufügen
frontend_nyla/src/components/AccessRules/AccessRulesEditor.tsx Katalog-Objekte integrieren

Zusammenfassung der Änderungen

Backend

Priorität Datei Beschreibung
1 rbacCatalog.py DATA-Objekte Registrierung
1 routeAdminRbacRules.py /catalog/objects Endpoint
2 mainTrustee.py DATA_OBJECTS definieren
2 Alle Feature main*.py DATA_OBJECTS in allen Features

Frontend

Priorität Datei Beschreibung
1 AdminMandateRolePermissionsPage.tsx readOnly={false} für System-Rollen
1 AccessRulesTable.tsx Neue Tabellen-Komponente
1 AccessRuleRow.tsx Zeilen-Komponente mit Checkboxen
2 useCatalogObjects.ts Hook für Katalog-Objekte
2 AccessRulesEditor.tsx Integration Katalog + Tabelle
2 AccessRules.module.css Styles für Tabelle

Migrations-Strategie

  1. Phase 1: Backend-Erweiterungen (Katalog + Endpoint)
  2. Phase 2: Frontend Checkbox-UI (ersetzt Dropdown)
  3. Phase 3: Katalog-Integration im Frontend
  4. Phase 4: System-Rollen bearbeitbar machen

Mockups

Kompakte Tabellen-Ansicht (Phase 2)

┌─────────────────────────────────────────────────────────────────────────────────┐
│ 📊 DATEN-REGELN                                                 [+ Neue Regel] │
├─────────────────────────────────────────────────────────────────────────────────┤
│                            │     │      EIGENE      │      GRUPPE     │  ALLE   │
│ OBJEKT                     │VIEW │  C    R    U   D │  C   R   U   D  │C  R  U D│
├─────────────────────────────────────────────────────────────────────────────────┤
│ data.feature.trustee.*     │ ✓  │  ✓   ✓   ✓   □  │  □   ✓   □   □  │□  □  □ □│ 🗑
│ data.feature.trustee.docs  │ ✓  │  ✓   ✓   ✓   ✓  │  ✓   ✓   ✓   ✓  │✓  ✓  ✓ ✓│ 🗑
└─────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────┐
│ 🖥 UI-REGELN                                                    [+ Neue Regel] │
├─────────────────────────────────────────────────────────────────────────────────┤
│ OBJEKT                                                          │ VIEW │       │
├─────────────────────────────────────────────────────────────────────────────────┤
│ ui.feature.trustee.dashboard                                    │  ✓   │   🗑  │
│ ui.feature.trustee.positions                                    │  ✓   │   🗑  │
│ ui.feature.trustee.documents                                    │  ✓   │   🗑  │
│ ui.admin.mandate-roles                                          │  □   │   🗑  │
└─────────────────────────────────────────────────────────────────────────────────┘

Objekt-Auswahl Dropdown (Phase 3)

┌─────────────────────────────────────────────────────────────────┐
│ Neue Regel hinzufügen                                           │
├─────────────────────────────────────────────────────────────────┤
│ Objekt auswählen:                          [Freie Eingabe →]   │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ -- Global (alle Objekte) --                               ▼│ │
│ ├─────────────────────────────────────────────────────────────┤ │
│ │ ▸ TRUSTEE                                                   │ │
│ │   data.feature.trustee.TrusteeContract - Vertrag           │ │
│ │   data.feature.trustee.TrusteePosition - Position          │ │
│ │   data.feature.trustee.TrusteeDocument - Dokument          │ │
│ │ ▸ SYSTEM                                                    │ │
│ │   data.system.UserInDB - Benutzer                          │ │
│ │   data.system.Mandate - Mandant                            │ │
│ └─────────────────────────────────────────────────────────────┘ │
│                                                                 │
│ [✓] Sichtbar (View)                                            │
│                                                                 │
│                                      [Abbrechen] [Hinzufügen]  │
└─────────────────────────────────────────────────────────────────┘

Offene Fragen

  1. Feature-Filter: Soll der Katalog nach aktiven Features des Mandanten gefiltert werden? Ja, der Katalog soll nach aktiven Features des Mandanten gefiltert werden.
  2. Wildcard-Patterns: Unterstützung für data.feature.trustee.* (alle Objekte eines Features)? Ja, die Unterstützung für data.feature.trustee.* (alle Objekte eines Features) soll implementiert werden.
  3. Feld-Level Permissions: Sollen einzelne Felder (z.B. TrusteeContract.salary) unterstützt werden? Ja, die Unterstützung für einzelne Felder (z.B. TrusteeContract.salary) soll implementiert werden.
  4. Vererbung: Sollen Berechtigungen von übergeordneten Objekten vererbt werden? Ja, die Vererbung von Berechtigungen von übergeordneten Objekten soll implementiert werden.

Erstellt: 2026-01-24 Status: Entwurf zur Diskussion