chore: add test for admin controller permissions (#30316)

This commit is contained in:
bo0tzz
2026-07-28 15:44:51 +02:00
committed by GitHub
parent 4609adbb6d
commit b468459b1a
2 changed files with 82 additions and 8 deletions
+71
View File
@@ -0,0 +1,71 @@
import { RequestMethod } from '@nestjs/common';
import { METHOD_METADATA, PATH_METADATA } from '@nestjs/common/constants';
import { Reflector } from '@nestjs/core';
import { controllers } from 'src/controllers';
import { AuthenticatedOptions, getAuthenticatedOptions } from 'src/middleware/auth.guard';
const UNAUTHENTICATED_ADMIN_ROUTES = new Set([
'GET admin/maintenance/status',
'POST admin/maintenance/login',
'POST admin/database-backups/start-restore',
]);
const isAdminPermission = (permission: AuthenticatedOptions['permission']) =>
typeof permission === 'string' && permission.startsWith('admin');
const getRoutes = () => {
const reflector = new Reflector();
return controllers.flatMap((Controller) => {
const prefix = reflector.get<string>(PATH_METADATA, Controller);
return Object.getOwnPropertyNames(Controller.prototype).flatMap((name) => {
const handler = Object.getOwnPropertyDescriptor(Controller.prototype, name)?.value;
if (typeof handler !== 'function') {
return [];
}
const requestMethod = reflector.get<RequestMethod | undefined>(METHOD_METADATA, handler);
if (requestMethod === undefined) {
return [];
}
const method = RequestMethod[requestMethod];
const path = [prefix, reflector.get<string>(PATH_METADATA, handler)].filter((part) => part !== '/').join('/');
return {
id: `${method} ${path}`,
label: `${Controller.name}.${name} (${method} /${path})`,
path,
auth: getAuthenticatedOptions(reflector, handler),
};
});
});
};
describe('controllers', () => {
const routes = getRoutes();
const adminRoutes = routes.filter((route) => route.path === 'admin' || route.path.startsWith('admin/'));
it('should only allow non-admin access to bootstrap routes under admin/', () => {
const reachableByNonAdmins = adminRoutes.filter((route) => !route.auth?.admin).map((route) => route.id);
expect(new Set(reachableByNonAdmins)).toEqual(UNAUTHENTICATED_ADMIN_ROUTES);
});
it('should not authenticate the bootstrap routes under admin/', () => {
const authenticated = routes
.filter((route) => UNAUTHENTICATED_ADMIN_ROUTES.has(route.id) && route.auth !== undefined)
.map((route) => route.label);
expect(authenticated).toEqual([]);
});
it('should require admin access for routes with an admin permission', () => {
const offenders = routes
.filter((route) => isAdminPermission(route.auth?.permission) && !route.auth?.admin)
.map((route) => route.label);
expect(offenders).toEqual([]);
});
});
+11 -8
View File
@@ -17,7 +17,15 @@ import { getUserAgentDetails } from 'src/utils/request';
type AdminRoute = { admin?: true };
type SharedLinkRoute = { sharedLink?: true };
type AuthenticatedOptions = { permission?: Permission | false } & (AdminRoute | SharedLinkRoute);
export type AuthenticatedOptions = { permission?: Permission | false } & (AdminRoute | SharedLinkRoute);
type ReflectorTarget = Parameters<Reflector['get']>[1];
/** Resolves the `@Authenticated()` options of a route handler, with the defaults applied. */
export const getAuthenticatedOptions = (reflector: Reflector, target: ReflectorTarget) => {
const options = reflector.getAllAndOverride<AuthenticatedOptions | undefined>(MetadataKey.AuthRoute, [target]);
return options && { sharedLink: false, admin: false, ...options };
};
export const Authenticated = (options: AuthenticatedOptions = {}): MethodDecorator => {
const decorators: MethodDecorator[] = [
@@ -86,17 +94,12 @@ export class AuthGuard implements CanActivate {
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const targets = [context.getHandler()];
const options = this.reflector.getAllAndOverride<AuthenticatedOptions | undefined>(MetadataKey.AuthRoute, targets);
const options = getAuthenticatedOptions(this.reflector, context.getHandler());
if (!options) {
return true;
}
const {
admin: adminRoute,
sharedLink: sharedLinkRoute,
permission,
} = { sharedLink: false, admin: false, ...options };
const { admin: adminRoute, sharedLink: sharedLinkRoute, permission } = options;
const request = context.switchToHttp().getRequest<AuthRequest>();
request.user = await this.authService.authenticate({