PermissionsGuard erlaubte bisher jede Route ohne @RequirePermission automatisch fuer jeden eingeloggten Nutzer (fail-open) und verliess sich allein auf test/authorization-coverage.e2e-spec.ts, um eine vergessene Decorator-Angabe vor dem Deploy zu erwischen - der Guard selbst hatte kein Default-Deny. Neuer Decorator @AnyAuthenticatedUser() macht das bisher stillschweigend erlaubte Verhalten explizit fuer die 12 Routen, die es tatsaechlich brauchen (ProfileController komplett, NotificationsController komplett, AuthController.me - alle bereits vorher auf der Allowlist des e2e-Tests). PermissionsGuard verweigert jetzt jede Route, die weder @RequirePermission noch @AnyAuthenticatedUser traegt - eine vergessene Decorator-Angabe schlaegt jetzt zur Laufzeit fehl (403), nicht mehr nur im Test. Der e2e-Test prueft jetzt denselben Decorator-Vertrag statt einer separat gepflegten Allowlist, kann also nicht mehr vom Guard-Verhalten abweichen. Neue Unit-Tests fuer PermissionsGuard decken beide Codepfade und das Fail-Closed-Verhalten ab. Co-Authored-By: Claude Sonnet 5 <[email protected]>
52 lines
2.2 KiB
TypeScript
52 lines
2.2 KiB
TypeScript
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
|
import { Reflector } from '@nestjs/core';
|
|
import { ANY_AUTHENTICATED_USER_KEY } from '../decorators/any-authenticated-user.decorator';
|
|
import { REQUIRE_PERMISSION_KEY } from '../decorators/require-permission.decorator';
|
|
import { PermissionKey } from '../permissions/permission-catalog';
|
|
import { PermissionResolverService } from '../permissions/permission-resolver.service';
|
|
import { AuthenticatedUser } from '../types/authenticated-user';
|
|
|
|
interface RequestWithUser {
|
|
user?: AuthenticatedUser;
|
|
}
|
|
|
|
/**
|
|
* Fails closed by decorator absence: a non-public route reaches this guard
|
|
* only after JwtAuthGuard confirms a valid, active session, and from here it
|
|
* is denied unless it carries either @RequirePermission (checked against the
|
|
* caller's live permission set) or the explicit @AnyAuthenticatedUser opt-out
|
|
* for routes that are meant to be reachable by any logged-in user regardless
|
|
* of role. A route with neither is a bug - forgetting to decorate a new
|
|
* route now fails shut at runtime instead of silently granting access to
|
|
* every authenticated user and relying solely on
|
|
* test/authorization-coverage.e2e-spec.ts to catch it before deploy.
|
|
*/
|
|
@Injectable()
|
|
export class PermissionsGuard implements CanActivate {
|
|
constructor(
|
|
private readonly reflector: Reflector,
|
|
private readonly permissionResolver: PermissionResolverService,
|
|
) {}
|
|
|
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
const requiredPermissions = this.reflector.getAllAndOverride<
|
|
PermissionKey[]
|
|
>(REQUIRE_PERMISSION_KEY, [context.getHandler(), context.getClass()]);
|
|
const request = context.switchToHttp().getRequest<RequestWithUser>();
|
|
const user = request.user;
|
|
|
|
if (requiredPermissions && requiredPermissions.length > 0) {
|
|
if (!user) {
|
|
return false;
|
|
}
|
|
return this.permissionResolver.hasAny(user.id, requiredPermissions);
|
|
}
|
|
|
|
const isAnyAuthenticatedUser = this.reflector.getAllAndOverride<boolean>(
|
|
ANY_AUTHENTICATED_USER_KEY,
|
|
[context.getHandler(), context.getClass()],
|
|
);
|
|
return Boolean(isAnyAuthenticatedUser && user);
|
|
}
|
|
}
|