fix(security): Rechteprüfung deny by default, API-Keys nur auf freigegebenen Routen
- PermissionsGuard lehnt Routen ohne @RequirePermissions/@AllowAnyAuthenticated
ab. API-Keys umgehen keine Rechteprüfung mehr, sondern gelten nur auf
Routen mit @AllowApiKey (Paperless-Webhook, externer Upload, checksum,
Agent-Routen des Etikettendrucks).
- Rechte für 24 bisher offene Routen ergänzt, u.a. paperless/documents
(nur Admin), paperless/documents/:id und Kontonummern (Posteingang/
manuell), webhook/status (Admin). routen-rechte.spec.ts prüft alle Routen.
- Etiketten-Agent: printed/error nur für abgeholte, offene Jobs (bedingtes
Update, sonst 409) – ein gedruckter Job kann seine Nummer nicht mehr
freigeben. Feldwerte nur aus den Eingabefeldern der Vorlage ({number}
nicht vorgebbar) und URL-kodiert in GET-/Callback-URLs.
- PUT paperless/inbox/:id ignoriert Freigabe (CF15) und Zahlung (CF16)
ohne die jeweilige Rolle (Funktionstrennung).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
de9cb09bcd
commit
3f98dfd98f
21 files changed
+699
-51
No files matched your search
@@ -0,0 +1,148 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method -- Handler dienen nur als Metadaten-Schlüssel */
|
||||
import { ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { PermissionsGuard } from './permissions.guard';
|
||||
import {
|
||||
AllowAnyAuthenticated,
|
||||
AllowApiKey,
|
||||
RequirePermissions,
|
||||
} from './permissions.decorator';
|
||||
import { Public } from './public.decorator';
|
||||
import { Permission } from './permissions.enum';
|
||||
|
||||
class Routen {
|
||||
@RequirePermissions(Permission.VIEW_MAIL)
|
||||
mail() {}
|
||||
|
||||
@AllowAnyAuthenticated()
|
||||
eigeneEinstellungen() {}
|
||||
|
||||
ohneAngabe() {}
|
||||
|
||||
@Public()
|
||||
oeffentlich() {}
|
||||
|
||||
@AllowApiKey()
|
||||
@RequirePermissions(Permission.MANAGE_SETTINGS)
|
||||
agent() {}
|
||||
|
||||
@AllowApiKey()
|
||||
nurApiKey() {}
|
||||
}
|
||||
|
||||
@RequirePermissions(Permission.VIEW_INBOX)
|
||||
class KlasseMitRecht {
|
||||
route() {}
|
||||
}
|
||||
|
||||
function ctx(
|
||||
handler: (...args: unknown[]) => unknown,
|
||||
request: Record<string, unknown>,
|
||||
cls: object = Routen,
|
||||
): ExecutionContext {
|
||||
return {
|
||||
getHandler: () => handler,
|
||||
getClass: () => cls,
|
||||
switchToHttp: () => ({ getRequest: () => request }),
|
||||
} as unknown as ExecutionContext;
|
||||
}
|
||||
|
||||
const r = Routen.prototype;
|
||||
const nutzer = (...permissions: Permission[]) => ({
|
||||
method: 'GET',
|
||||
url: '/x',
|
||||
user: { permissions },
|
||||
});
|
||||
const apiKey = {
|
||||
method: 'GET',
|
||||
url: '/x',
|
||||
apiKeyMetadata: { id: '1', name: 'Agent' },
|
||||
};
|
||||
|
||||
describe('PermissionsGuard', () => {
|
||||
const guard = new PermissionsGuard(new Reflector());
|
||||
|
||||
it('erlaubt Benutzer mit passender Berechtigung', () => {
|
||||
expect(guard.canActivate(ctx(r.mail, nutzer(Permission.VIEW_MAIL)))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('verweigert Benutzer ohne passende Berechtigung', () => {
|
||||
expect(guard.canActivate(ctx(r.mail, nutzer(Permission.VIEW_INBOX)))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('erlaubt Admins (MANAGE_ALL) überall mit Rechteangabe', () => {
|
||||
expect(guard.canActivate(ctx(r.mail, nutzer(Permission.MANAGE_ALL)))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('übernimmt Rechte von der Klasse', () => {
|
||||
const k = KlasseMitRecht.prototype;
|
||||
expect(
|
||||
guard.canActivate(
|
||||
ctx(k.route, nutzer(Permission.VIEW_INBOX), KlasseMitRecht),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
guard.canActivate(
|
||||
ctx(k.route, nutzer(Permission.VIEW_MAIL), KlasseMitRecht),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('verweigert Routen ohne Rechteangabe (deny by default) – auch Admins', () => {
|
||||
expect(
|
||||
guard.canActivate(ctx(r.ohneAngabe, nutzer(Permission.MANAGE_ALL))),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('erlaubt @AllowAnyAuthenticated jedem angemeldeten Benutzer, auch ohne PM-Gruppe', () => {
|
||||
expect(guard.canActivate(ctx(r.eigeneEinstellungen, nutzer()))).toBe(true);
|
||||
expect(
|
||||
guard.canActivate(
|
||||
ctx(r.eigeneEinstellungen, { method: 'GET', url: '/x' }),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('lässt @Public-Routen immer durch', () => {
|
||||
expect(
|
||||
guard.canActivate(ctx(r.oeffentlich, { method: 'GET', url: '/x' })),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
describe('API-Keys', () => {
|
||||
it('gelten nur auf Routen mit @AllowApiKey', () => {
|
||||
expect(guard.canActivate(ctx(r.agent, apiKey))).toBe(true);
|
||||
expect(guard.canActivate(ctx(r.nurApiKey, apiKey))).toBe(true);
|
||||
});
|
||||
|
||||
it('umgehen keine Rechteprüfung mehr auf anderen Routen', () => {
|
||||
expect(() => guard.canActivate(ctx(r.mail, apiKey))).toThrow(
|
||||
ForbiddenException,
|
||||
);
|
||||
expect(() =>
|
||||
guard.canActivate(ctx(r.eigeneEinstellungen, apiKey)),
|
||||
).toThrow(ForbiddenException);
|
||||
expect(() => guard.canActivate(ctx(r.ohneAngabe, apiKey))).toThrow(
|
||||
ForbiddenException,
|
||||
);
|
||||
});
|
||||
|
||||
it('Benutzer brauchen auf @AllowApiKey-Routen weiterhin die Berechtigung', () => {
|
||||
expect(
|
||||
guard.canActivate(ctx(r.agent, nutzer(Permission.VIEW_SCANNER))),
|
||||
).toBe(false);
|
||||
expect(
|
||||
guard.canActivate(ctx(r.agent, nutzer(Permission.MANAGE_SETTINGS))),
|
||||
).toBe(true);
|
||||
expect(
|
||||
guard.canActivate(ctx(r.nurApiKey, nutzer(Permission.MANAGE_ALL))),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user