981 lines
34 KiB
Markdown
981 lines
34 KiB
Markdown
# 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`:
|
|
```typescript
|
|
<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
|
|
|
|
```typescript
|
|
// 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`:
|
|
|
|
```typescript
|
|
// 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`
|
|
|
|
```typescript
|
|
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`
|
|
|
|
```typescript
|
|
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
|
|
|
|
```css
|
|
/* 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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```python
|
|
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`:
|
|
|
|
```python
|
|
# 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`:
|
|
|
|
```python
|
|
# 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`:
|
|
|
|
```python
|
|
# 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`:
|
|
|
|
```typescript
|
|
// 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`:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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*
|