rights system

This commit is contained in:
Matthias Hochmeister
2026-03-23 12:35:28 +01:00
parent 725d4d1729
commit 83b84664ce
2 changed files with 75 additions and 32 deletions

View File

@@ -144,20 +144,30 @@ class PermissionService {
// ── Admin methods ── // ── Admin methods ──
async getMatrix(): Promise<MatrixData> { async getMatrix(): Promise<MatrixData> {
const [fgResult, pResult, gpResult] = await Promise.all([ const [fgResult, pResult, gpResult, userGroupsResult] = await Promise.all([
pool.query('SELECT id, label, sort_order, maintenance FROM feature_groups ORDER BY sort_order'), pool.query('SELECT id, label, sort_order, maintenance FROM feature_groups ORDER BY sort_order'),
pool.query('SELECT id, feature_group_id, label, description, sort_order FROM permissions ORDER BY feature_group_id, sort_order'), pool.query('SELECT id, feature_group_id, label, description, sort_order FROM permissions ORDER BY feature_group_id, sort_order'),
pool.query('SELECT authentik_group, permission_id FROM group_permissions'), pool.query('SELECT authentik_group, permission_id FROM group_permissions'),
// Also include all dashboard_ groups from users table
pool.query(`SELECT DISTINCT g AS group_name FROM users, unnest(authentik_groups) AS g WHERE g LIKE 'dashboard_%' AND g != 'dashboard_admin'`),
]); ]);
const grants: Record<string, string[]> = {}; const grants: Record<string, string[]> = {};
const groupSet = new Set<string>(); const groupSet = new Set<string>();
// Add groups from group_permissions
for (const row of gpResult.rows) { for (const row of gpResult.rows) {
groupSet.add(row.authentik_group); groupSet.add(row.authentik_group);
if (!grants[row.authentik_group]) grants[row.authentik_group] = []; if (!grants[row.authentik_group]) grants[row.authentik_group] = [];
grants[row.authentik_group].push(row.permission_id); grants[row.authentik_group].push(row.permission_id);
} }
// Also add groups from users table (they may have no permissions yet)
for (const row of userGroupsResult.rows) {
groupSet.add(row.group_name);
if (!grants[row.group_name]) grants[row.group_name] = [];
}
const maintenance: Record<string, boolean> = {}; const maintenance: Record<string, boolean> = {};
for (const row of fgResult.rows) { for (const row of fgResult.rows) {
maintenance[row.id] = row.maintenance; maintenance[row.id] = row.maintenance;
@@ -180,13 +190,14 @@ class PermissionService {
} }
async getUnknownGroups(): Promise<string[]> { async getUnknownGroups(): Promise<string[]> {
// Groups from users table that are not yet in the permission matrix // Groups from users table that have zero permissions assigned
// (they appear in the matrix but admin should be notified)
const result = await pool.query(` const result = await pool.query(`
SELECT DISTINCT g AS group_name SELECT DISTINCT g AS group_name
FROM users, unnest(authentik_groups) AS g FROM users, unnest(authentik_groups) AS g
WHERE g LIKE 'dashboard_%' WHERE g LIKE 'dashboard_%'
AND g NOT IN (SELECT DISTINCT authentik_group FROM group_permissions)
AND g != 'dashboard_admin' AND g != 'dashboard_admin'
AND g NOT IN (SELECT DISTINCT authentik_group FROM group_permissions)
ORDER BY group_name ORDER BY group_name
`); `);
return result.rows.map((r: any) => r.group_name); return result.rows.map((r: any) => r.group_name);

View File

@@ -10,6 +10,11 @@ import {
Chip, Chip,
CircularProgress, CircularProgress,
Collapse, Collapse,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
FormControlLabel, FormControlLabel,
IconButton, IconButton,
Switch, Switch,
@@ -109,6 +114,7 @@ function PermissionMatrixTab() {
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({}); const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
const [showDepEditor, setShowDepEditor] = useState(false); const [showDepEditor, setShowDepEditor] = useState(false);
const [deleteGroupConfirm, setDeleteGroupConfirm] = useState<string | null>(null);
const toggleGroup = (groupId: string) => { const toggleGroup = (groupId: string) => {
setExpandedGroups(prev => ({ ...prev, [groupId]: !prev[groupId] })); setExpandedGroups(prev => ({ ...prev, [groupId]: !prev[groupId] }));
@@ -137,16 +143,6 @@ function PermissionMatrixTab() {
onError: () => showError('Fehler beim Speichern der Berechtigungen'), onError: () => showError('Fehler beim Speichern der Berechtigungen'),
}); });
const addGroupMutation = useMutation({
mutationFn: (groupName: string) => permissionsApi.setGroupPermissions(groupName, []),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-permission-matrix'] });
queryClient.invalidateQueries({ queryKey: ['admin-unknown-groups'] });
showSuccess('Gruppe hinzugefügt');
},
onError: () => showError('Fehler beim Hinzufügen der Gruppe'),
});
const deleteGroupMutation = useMutation({ const deleteGroupMutation = useMutation({
mutationFn: (groupName: string) => permissionsApi.deleteGroup(groupName), mutationFn: (groupName: string) => permissionsApi.deleteGroup(groupName),
onSuccess: () => { onSuccess: () => {
@@ -290,18 +286,10 @@ function PermissionMatrixTab() {
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Unknown Groups Alert */} {/* Unknown Groups Alert */}
{unknownGroups && unknownGroups.length > 0 && ( {unknownGroups && unknownGroups.length > 0 && (
<Alert severity="warning" sx={{ alignItems: 'center' }}> <Alert severity="info" sx={{ alignItems: 'center' }}>
<Typography variant="body2" sx={{ mb: 1 }}> <Typography variant="body2">
Folgende Gruppen wurden in der Benutzertabelle gefunden, sind aber noch nicht in der Berechtigungsmatrix: Folgende Gruppen haben noch keine Berechtigungen zugewiesen: <strong>{unknownGroups.join(', ')}</strong>
</Typography> </Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{unknownGroups.map(g => (
<Button key={g} variant="outlined" size="small" startIcon={<AddIcon />}
onClick={() => addGroupMutation.mutate(g)} disabled={addGroupMutation.isPending}>
{g} hinzufügen
</Button>
))}
</Box>
</Alert> </Alert>
)} )}
@@ -339,7 +327,7 @@ function PermissionMatrixTab() {
<DependencyEditor <DependencyEditor
groupHierarchy={groupHierarchy} groupHierarchy={groupHierarchy}
permissionDeps={permissionDeps} permissionDeps={permissionDeps}
allGroups={[...nonAdminGroups, ...groups.filter(g => g === 'dashboard_admin')]} allGroups={nonAdminGroups}
allPermissions={permissions} allPermissions={permissions}
onSave={(config) => depConfigMutation.mutate(config)} onSave={(config) => depConfigMutation.mutate(config)}
isSaving={depConfigMutation.isPending} isSaving={depConfigMutation.isPending}
@@ -376,11 +364,8 @@ function PermissionMatrixTab() {
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5 }}>
{g.replace('dashboard_', '')} {g.replace('dashboard_', '')}
<Tooltip title={`Gruppe "${g}" entfernen`} placement="top"> <Tooltip title={`Gruppe "${g}" entfernen`} placement="top">
<IconButton size="small" onClick={() => { <IconButton size="small" onClick={() => setDeleteGroupConfirm(g)}
if (window.confirm(`Gruppe "${g}" und alle zugehörigen Berechtigungen wirklich entfernen?`)) { sx={{ opacity: 0.4, '&:hover': { opacity: 1, color: 'error.main' } }}>
deleteGroupMutation.mutate(g);
}
}} sx={{ opacity: 0.4, '&:hover': { opacity: 1, color: 'error.main' } }}>
<DeleteIcon sx={{ fontSize: 14 }} /> <DeleteIcon sx={{ fontSize: 14 }} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -471,6 +456,33 @@ function PermissionMatrixTab() {
</TableContainer> </TableContainer>
</CardContent> </CardContent>
</Card> </Card>
{/* Delete Group Confirmation Dialog */}
<Dialog open={!!deleteGroupConfirm} onClose={() => setDeleteGroupConfirm(null)}>
<DialogTitle>Gruppe entfernen</DialogTitle>
<DialogContent>
<DialogContentText>
Soll die Gruppe &quot;{deleteGroupConfirm}&quot; und alle zugehörigen Berechtigungen
wirklich entfernt werden? Diese Aktion kann nicht rückgängig gemacht werden.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteGroupConfirm(null)}>Abbrechen</Button>
<Button
variant="contained"
color="error"
onClick={() => {
if (deleteGroupConfirm) {
deleteGroupMutation.mutate(deleteGroupConfirm);
setDeleteGroupConfirm(null);
}
}}
disabled={deleteGroupMutation.isPending}
>
{deleteGroupMutation.isPending ? <CircularProgress size={20} /> : 'Entfernen'}
</Button>
</DialogActions>
</Dialog>
</Box> </Box>
); );
} }
@@ -487,8 +499,28 @@ interface DependencyEditorProps {
} }
function DependencyEditor({ groupHierarchy, permissionDeps, allGroups, allPermissions, onSave, isSaving }: DependencyEditorProps) { function DependencyEditor({ groupHierarchy, permissionDeps, allGroups, allPermissions, onSave, isSaving }: DependencyEditorProps) {
const [editHierarchy, setEditHierarchy] = useState<Record<string, string[]>>(() => ({ ...groupHierarchy })); const groupSet = useMemo(() => new Set(allGroups), [allGroups]);
const [editDeps, setEditDeps] = useState<Record<string, string[]>>(() => ({ ...permissionDeps })); const permIdSet = useMemo(() => new Set(allPermissions.map(p => p.id)), [allPermissions]);
// Filter saved config to only include groups/permissions that actually exist
const [editHierarchy, setEditHierarchy] = useState<Record<string, string[]>>(() => {
const filtered: Record<string, string[]> = {};
for (const [g, inheritors] of Object.entries(groupHierarchy)) {
if (groupSet.has(g)) {
filtered[g] = inheritors.filter(i => groupSet.has(i));
}
}
return filtered;
});
const [editDeps, setEditDeps] = useState<Record<string, string[]>>(() => {
const filtered: Record<string, string[]> = {};
for (const [p, deps] of Object.entries(permissionDeps)) {
if (permIdSet.has(p)) {
filtered[p] = deps.filter(d => permIdSet.has(d));
}
}
return filtered;
});
const nonAdminGroups = allGroups.filter(g => g !== 'dashboard_admin'); const nonAdminGroups = allGroups.filter(g => g !== 'dashboard_admin');
const permOptions = allPermissions.map(p => p.id); const permOptions = allPermissions.map(p => p.id);