141 lines
3.6 KiB
TypeScript
141 lines
3.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
ExecutionContext,
|
|
ForbiddenException,
|
|
UnauthorizedException,
|
|
} from '@nestjs/common';
|
|
import { Reflector } from '@nestjs/core';
|
|
import { RequirePermissions } from '../../src/authorization/decorators/require-permissions.decorator';
|
|
import { PermissionsGuard } from '../../src/authorization/guards/permissions.guard';
|
|
import { Public } from '../../src/auth/decorators/public.decorator';
|
|
import type { AuthPrincipal } from '../../src/common/http/request-context';
|
|
|
|
@RequirePermissions('dashboard.read')
|
|
class ProtectedController {
|
|
@RequirePermissions('users.create')
|
|
createUser() {}
|
|
|
|
openToAuthenticatedUsers() {}
|
|
}
|
|
|
|
class UndecoratedController {
|
|
handle() {}
|
|
}
|
|
|
|
@RequirePermissions('dashboard.read')
|
|
class PublicController {
|
|
@Public()
|
|
handle() {}
|
|
}
|
|
|
|
function principal(permissions: string[]): AuthPrincipal {
|
|
return {
|
|
userId: 'b6f6b10e-434a-4d72-9ec9-3520212dd290',
|
|
username: 'admin',
|
|
sessionId: '1bab61ac-65d1-40e6-816b-63d90131a795',
|
|
firstName: 'Admin',
|
|
lastName: 'DH',
|
|
email: null,
|
|
mustChangePassword: false,
|
|
roles: ['admin'],
|
|
permissions,
|
|
transport: 'cookie',
|
|
};
|
|
}
|
|
|
|
function context(
|
|
controller: new () => object,
|
|
handlerName: string,
|
|
auth?: AuthPrincipal,
|
|
): ExecutionContext {
|
|
const handler = controller.prototype[
|
|
handlerName as keyof typeof controller.prototype
|
|
];
|
|
|
|
return {
|
|
getHandler: () => handler,
|
|
getClass: () => controller,
|
|
switchToHttp: () => ({
|
|
getRequest: () => ({ auth }),
|
|
}),
|
|
} as unknown as ExecutionContext;
|
|
}
|
|
|
|
const guard = new PermissionsGuard(new Reflector());
|
|
|
|
test('PermissionsGuard leaves handlers without requirements available', () => {
|
|
assert.equal(
|
|
guard.canActivate(context(UndecoratedController, 'handle')),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('PermissionsGuard gives explicit public metadata priority', () => {
|
|
assert.equal(guard.canActivate(context(PublicController, 'handle')), true);
|
|
});
|
|
|
|
test('PermissionsGuard returns 401 when required permissions have no principal', () => {
|
|
assert.throws(
|
|
() => guard.canActivate(context(ProtectedController, 'createUser')),
|
|
(error: unknown) =>
|
|
error instanceof UnauthorizedException && error.getStatus() === 401,
|
|
);
|
|
});
|
|
|
|
test('PermissionsGuard returns 403 when any required permission is missing', () => {
|
|
assert.throws(
|
|
() =>
|
|
guard.canActivate(
|
|
context(
|
|
ProtectedController,
|
|
'createUser',
|
|
principal(['dashboard.read']),
|
|
),
|
|
),
|
|
(error: unknown) =>
|
|
error instanceof ForbiddenException && error.getStatus() === 403,
|
|
);
|
|
});
|
|
|
|
test('PermissionsGuard requires changing a temporary password first', () => {
|
|
const auth = principal(['dashboard.read', 'users.create']);
|
|
auth.mustChangePassword = true;
|
|
|
|
assert.throws(
|
|
() =>
|
|
guard.canActivate(context(ProtectedController, 'createUser', auth)),
|
|
(error: unknown) =>
|
|
error instanceof ForbiddenException &&
|
|
error.getStatus() === 403 &&
|
|
(error.getResponse() as { code?: string }).code ===
|
|
'PASSWORD_CHANGE_REQUIRED',
|
|
);
|
|
});
|
|
|
|
test('PermissionsGuard authorizes when class and handler permissions are granted', () => {
|
|
assert.equal(
|
|
guard.canActivate(
|
|
context(
|
|
ProtectedController,
|
|
'createUser',
|
|
principal(['dashboard.read', 'users.create']),
|
|
),
|
|
),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('PermissionsGuard applies class requirements to every handler', () => {
|
|
assert.equal(
|
|
guard.canActivate(
|
|
context(
|
|
ProtectedController,
|
|
'openToAuthenticatedUsers',
|
|
principal(['dashboard.read']),
|
|
),
|
|
),
|
|
true,
|
|
);
|
|
});
|