routing-controllers icon indicating copy to clipboard operation
routing-controllers copied to clipboard

Nested controllers with common prefix and authorization requirements

Open mpvosseller opened this issue 5 years ago • 3 comments

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 {
}

mpvosseller avatar Jan 22 '20 04:01 mpvosseller

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);
        }
    }
};

jotamorais avatar Jan 27 '20 16:01 jotamorais

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.

mpvosseller avatar Jan 28 '20 14:01 mpvosseller

Would love to see something like this as well.

Leafgard avatar Jan 12 '21 14:01 Leafgard