Aufsicht und Eingriff getrennt: neuer Haken "Audit-Betrieb"
Die DSGVO-Rolle trug audit:* komplett, also auch audit:admin. Ein DSGVO-Beauftragter konnte damit seal-backlog, rehash und cleanup - seine eigene Beweisgrundlage ersetzen. Wer das Protokoll beaufsichtigt, darf es nicht umschreiben koennen. Dieselbe Klasse wie R184-02: falsche Domaene, zu breit gebuendelt. Der naheliegende Fix waere falsch gewesen. audit:admin einfach aus der DSGVO-Rolle zu streichen haette es heimatlos gemacht: Die Admin-Rolle ist ausdruecklich ohne audit/gdpr gebaut, einzige verbleibende Quelle waere der Entwicklerzugriff - der alles gibt. Prod versiegeln haette dann Vollzugriff vorausgesetzt. Deshalb eine eigene versteckte Rolle "Audit-Betrieb" (audit:read + audit:admin), zugewiesen ueber eine Checkbox wie DSGVO/Entwickler. DSGVO behaelt audit:read + audit:export + gdpr:*. Fuer kein bestehendes Konto weitet sich etwas aus; es wird enger, und wer eingreifen koennen soll, bekommt es ausdruecklich. Keine zusaetzliche Rechte-Huerde davor, weil das am Henne-Ei-Problem scheitert: Nach der Aufteilung haelt zunaechst niemand audit:admin, koennte ihn also auch niemand vergeben. Stattdessen wird die Vergabe laut - CRITICAL im Protokoll und PERMISSION_CHANGED/CRITICAL im Alarmkanal, samt Kennzeichen, ob sich jemand den Haken selbst gesetzt hat. Nebenbei geschlossen: setUserGdprAccess() legte die DSGVO-Rolle im Notfallpfad mit audit:* komplett an - eine zweite Liste, die dasselbe bedeuten sollte und die Buendelung stillschweigend zurueckgebracht haette. ACHTUNG beim Deploy: Bestehende DSGVO-Konten verlieren audit:admin. Wer Prod versiegeln will, muss sich vorher "Audit-Betrieb" ankreuzen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -121,7 +121,7 @@ export default function UserList() {
|
||||
<td className="py-3 px-4">{user.email}</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{user.roles?.filter((role: any) => !['Developer', 'Kunde', 'DSGVO'].includes(role.name)).map((role: any) => (
|
||||
{user.roles?.filter((role: any) => !['Developer', 'Kunde', 'DSGVO', 'Audit-Betrieb'].includes(role.name)).map((role: any) => (
|
||||
<Badge key={role.id || role.name} variant="info">
|
||||
{role.name}
|
||||
</Badge>
|
||||
@@ -246,6 +246,7 @@ function UserModal({
|
||||
roleIds: [] as number[],
|
||||
isActive: true,
|
||||
hasDeveloperAccess: false,
|
||||
hasAuditOpsAccess: false,
|
||||
hasGdprAccess: false,
|
||||
isServiceAccount: false,
|
||||
whatsappNumber: '',
|
||||
@@ -264,9 +265,10 @@ function UserModal({
|
||||
currentPassword: '',
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
roleIds: user.roles?.filter((r: any) => !['Developer', 'Kunde', 'DSGVO'].includes(r.name)).map((r: any) => r.id) || [],
|
||||
roleIds: user.roles?.filter((r: any) => !['Developer', 'Kunde', 'DSGVO', 'Audit-Betrieb'].includes(r.name)).map((r: any) => r.id) || [],
|
||||
isActive: (user as any).isActive ?? true,
|
||||
hasDeveloperAccess: (user as any).hasDeveloperAccess ?? false,
|
||||
hasAuditOpsAccess: (user as any).hasAuditOpsAccess ?? false,
|
||||
hasGdprAccess: (user as any).hasGdprAccess ?? false,
|
||||
isServiceAccount: (user as any).isServiceAccount ?? false,
|
||||
whatsappNumber: (user as any).whatsappNumber || '',
|
||||
@@ -283,6 +285,7 @@ function UserModal({
|
||||
roleIds: [],
|
||||
isActive: true,
|
||||
hasDeveloperAccess: false,
|
||||
hasAuditOpsAccess: false,
|
||||
hasGdprAccess: false,
|
||||
isServiceAccount: false,
|
||||
whatsappNumber: '',
|
||||
@@ -326,6 +329,7 @@ function UserModal({
|
||||
roleIds: formData.roleIds,
|
||||
isActive: formData.isActive,
|
||||
hasDeveloperAccess: formData.hasDeveloperAccess,
|
||||
hasAuditOpsAccess: formData.hasAuditOpsAccess,
|
||||
hasGdprAccess: formData.hasGdprAccess,
|
||||
isServiceAccount: formData.isServiceAccount,
|
||||
// Das Kennzeichen senkt die Alarmstufe der Anmeldungen dieses Kontos.
|
||||
@@ -369,6 +373,7 @@ function UserModal({
|
||||
lastName: formData.lastName,
|
||||
roleIds: formData.roleIds,
|
||||
hasDeveloperAccess: formData.hasDeveloperAccess,
|
||||
hasAuditOpsAccess: formData.hasAuditOpsAccess,
|
||||
hasGdprAccess: formData.hasGdprAccess,
|
||||
isServiceAccount: formData.isServiceAccount,
|
||||
// Das Kennzeichen senkt die Alarmstufe der Anmeldungen dieses Kontos.
|
||||
@@ -489,7 +494,7 @@ function UserModal({
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Rollen</label>
|
||||
<div className="space-y-2">
|
||||
{roles.filter((role) => !['Developer', 'Kunde', 'DSGVO'].includes(role.name)).map((role) => (
|
||||
{roles.filter((role) => !['Developer', 'Kunde', 'DSGVO', 'Audit-Betrieb'].includes(role.name)).map((role) => (
|
||||
<label key={role.id} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -533,7 +538,28 @@ function UserModal({
|
||||
<Shield className="w-4 h-4 text-blue-600" />
|
||||
DSGVO-Zugriff
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">(Audit-Logs, Datenschutz)</span>
|
||||
<span className="text-sm text-gray-500">(Audit-Protokoll lesen, Datenschutz)</span>
|
||||
</label>
|
||||
<label className="flex items-start gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.hasAuditOpsAccess}
|
||||
onChange={(e) => setFormData({ ...formData, hasAuditOpsAccess: e.target.checked })}
|
||||
className="mt-1 rounded border-amber-300 text-amber-600 focus:ring-amber-500"
|
||||
/>
|
||||
<span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Shield className="w-4 h-4 text-amber-600" />
|
||||
Audit-Betrieb
|
||||
<span className="text-sm text-gray-500">(versiegeln, aufräumen, Aufbewahrung)</span>
|
||||
</span>
|
||||
<span className="block text-sm text-gray-500 mt-0.5">
|
||||
Erlaubt Eingriffe am Audit-Protokoll: Bestandssiegel setzen oder
|
||||
ersetzen, aufräumen, Aufbewahrungsfristen ändern. Bewusst getrennt
|
||||
vom DSGVO-Zugriff – wer das Protokoll beaufsichtigt, soll seine
|
||||
eigene Beweisgrundlage nicht ersetzen können.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
|
||||
@@ -1548,11 +1548,12 @@ export const userApi = {
|
||||
const res = await api.get<ApiResponse<User>>(`/users/${id}`);
|
||||
return res.data;
|
||||
},
|
||||
create: async (data: { email: string; password: string; firstName: string; lastName: string; roleIds: number[]; customerId?: number; hasDeveloperAccess?: boolean; hasGdprAccess?: boolean; isServiceAccount?: boolean; confirm?: string; whatsappNumber?: string; telegramUsername?: string; signalNumber?: string }) => {
|
||||
create: async (data: { email: string; password: string; firstName: string; lastName: string; roleIds: number[]; customerId?: number; hasDeveloperAccess?: boolean; hasGdprAccess?: boolean;
|
||||
hasAuditOpsAccess?: boolean; isServiceAccount?: boolean; confirm?: string; whatsappNumber?: string; telegramUsername?: string; signalNumber?: string }) => {
|
||||
const res = await api.post<ApiResponse<User>>('/users', data);
|
||||
return res.data;
|
||||
},
|
||||
update: async (id: number, data: Partial<User> & { password?: string; roleIds?: number[]; isServiceAccount?: boolean; confirm?: string }) => {
|
||||
update: async (id: number, data: Partial<User> & { password?: string; roleIds?: number[]; isServiceAccount?: boolean; hasGdprAccess?: boolean; hasDeveloperAccess?: boolean; hasAuditOpsAccess?: boolean; confirm?: string }) => {
|
||||
const res = await api.put<ApiResponse<User>>(`/users/${id}`, data);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user