Files
paperlessmanager/paperless-backend/src/auth/permissions.guard.spec.ts
T
bjoernpoettkerandClaude Opus 5.5 3f98dfd98f 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>
2026-11-04 10:46:43 +01:00

149 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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);
});
});
});