rights system
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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 "{deleteGroupConfirm}" 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);
|
||||||
|
|||||||
Reference in New Issue
Block a user