routing-controllers
routing-controllers copied to clipboard
Nested controllers with common prefix and authorization requirements
Let's say I have a set of controllers (e.g. AdminUsersController
, AdminSettingsController
, etc..) that live under a shared path prefix (e.g. /api/admin/users
and /api/admin/settings
) and share an authorization requirement @Authorized('admin')
Is there a way to organize the controllers to share the @Authorized('admin')
authorization requirement without having to repeat it in each (as shown below)? My concern is accidentally forgetting to add the line when adding a new admin controller in the future.
If not this might be mitigated with some kind of formal nesting of controllers. I noticed that nesting controllers and shared prefixes were discussed previously in #207
@JsonController('/api/admin/users')
@Authorized('admin')
export class AdminUsersController {
}
@JsonController('/api/admin/settings')
@Authorized('admin')
export class AdminSettingsController {
}
Let's say I have a set of controllers (e.g.
AdminUsersController
,AdminSettingsController
, etc..) that live under a shared path prefix (e.g./api/admin/users
and/api/admin/settings
) and share an authorization requirement@Authorized('admin')
Is there a way to organize the controllers to share the
@Authorized('admin')
authorization requirement without having to repeat it in each (as shown below)? My concern is accidentally forgetting to add the line when adding a new admin controller in the future.If not this might be mitigated with some kind of formal nesting of controllers. I noticed that nesting controllers and shared prefixes were discussed previously in #207
@JsonController('/api/admin/users') @Authorized('admin') export class AdminUsersController { } @JsonController('/api/admin/settings') @Authorized('admin') export class AdminSettingsController { }
You can use the AuthorizationChecker
function within the RoutingControllersOptions
. Something like:
/**
* Routing Controllers configuration object
*/
private routingControllersOptions(): RoutingControllersOptions {
return {
defaults: {
//with this option, null will return 404 by default
nullResultCode: 404,
//with this option, void or Promise<void> will return 204 by default
undefinedResultCode: 204,
},
defaultErrorHandler: false,
classTransformer: true,
validation: true,
development: process.env.APP_ENV === 'development',
controllers: [`${this.config.get("srcPath")}/server/controllers/*{.ts,.js}`],
middlewares: [
CORSMiddleware,
multer,
ErrorHandler
],
routePrefix: "/api/v1",
authorizationChecker: async (action: Action, requiredRoles: string[]) => {
try {
let payload = this.decodeJWT(action);
if (isObject(payload) && payload.hasOwnProperty("sub")) {
const username = (payload as any).sub;
let authService = Container.get(AuthService);
const currentUser: CurrentUserDTO = await authService.GetUserAndRoles(username);
if (currentUser && currentUser.roles) {
// roles param can be a single role string (e.g. Role.User or 'User')
// or an array of roles (e.g. [Role.Admin, Role.User] or ['Admin', 'User'])
if (typeof requiredRoles === 'string') {
requiredRoles = [requiredRoles];
}
if (requiredRoles.length > 1) {
const roles = requiredRoles.filter((value, index, arr) => {
return value != 'USER'
})
return currentUser.roles.some(userRole => roles.indexOf(userRole.name) > -1);
}
}
return currentUser !== undefined && currentUser.roles !== undefined;
}
return false;
} catch (error) {
logger.error(error);
throw new UnauthorizedError(error);
}
}
};
Thanks @jotamorais. I am already using the AuthorizationChecker
.
My question is whether I can avoid having to repeat @Authorized('admin')
and /api/admin/
for each Admin API Controller I make.
e.g. As an example instead of this:
@JsonController('/api/admin/users')
@Authorized('admin')
export class AdminUsersController {
}
@JsonController('/api/admin/settings')
@Authorized('admin')
export class AdminSettingsController {
}
It might be nice if I could do something like:
@JsonController('/api/admin')
@Authorized('admin')
export class AdminController {
}
@ExtendController('AdminController') // inherits @Authorized('admin')
@JsonController('/users') // ends up at /api/admin/users
export class AdminUsersController {
}
@ExtendController('AdminController') // inherits @Authorized('admin')
@JsonController('/settings') // ends up at /api/admin/settings
export class AdminSettingsController {
}
Some kind of nesting could make it less likely that a new controller under /api/admin
is deployed without also protecting it with the admin
role. The trick is finding a nesting design that isn't over complicated. Not sure if there is already a pattern for this in routing-controllers.
Would love to see something like this as well.