diff --git a/README.md b/README.md index fe74a78..749275d 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,7 @@ For Keycloak options, refer to the official [keycloak-connect](https://github.co | policyEnforcement | Sets the policy enforcement mode | no | PERMISSIVE | | tokenValidation | Sets the token validation method | no | ONLINE | | multiTenant | Sets the options for [multi-tenant configuration](#multi-tenant-options) | no | - | +| roleMerge | Sets the merge mode for @Role decorator | no | OVERRIDE | ### Multi Tenant Options | Option | Description | Required | Default | diff --git a/src/constants.ts b/src/constants.ts index 7a701c1..3719bf4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -68,3 +68,14 @@ export enum TokenValidation { */ NONE = 'none', } + +export enum RoleMerge { + /** + * Overrides roles if defined both controller and handlers, with controller taking over. + */ + OVERRIDE, + /** + * Merges all roles from both controller and handlers. + */ + ALL, +} diff --git a/src/guards/role.guard.ts b/src/guards/role.guard.ts index d21717f..454ca3d 100644 --- a/src/guards/role.guard.ts +++ b/src/guards/role.guard.ts @@ -12,6 +12,7 @@ import { KEYCLOAK_INSTANCE, KEYCLOAK_LOGGER, RoleMatchingMode, + RoleMerge, } from '../constants'; import { META_ROLES } from '../decorators/roles.decorator'; import { KeycloakConnectConfig } from '../interface/keycloak-connect-options.interface'; @@ -37,20 +38,46 @@ export class RoleGuard implements CanActivate { ) {} async canActivate(context: ExecutionContext): Promise { - const rolesMetaData = this.reflector.getAllAndOverride< - RoleDecoratorOptionsInterface - >(META_ROLES, [context.getClass(), context.getHandler()]); + const roleMerge = this.keycloakOpts.roleMerge + ? this.keycloakOpts.roleMerge + : RoleMerge.OVERRIDE; - if (!rolesMetaData || rolesMetaData.roles.length === 0) { - return true; + const rolesMetaDatas: RoleDecoratorOptionsInterface[] = []; + + if (roleMerge == RoleMerge.ALL) { + const mergedRoleMetaData = this.reflector.getAllAndMerge< + RoleDecoratorOptionsInterface[] + >(META_ROLES, [context.getClass(), context.getHandler()]); + + if (mergedRoleMetaData) { + rolesMetaDatas.push(...mergedRoleMetaData); + } + } else if (roleMerge == RoleMerge.OVERRIDE) { + const roleMetaData = this.reflector.getAllAndOverride< + RoleDecoratorOptionsInterface + >(META_ROLES, [context.getClass(), context.getHandler()]); + + if (roleMetaData) { + rolesMetaDatas.push(roleMetaData); + } + } else { + throw Error(`Unknown role merge: ${roleMerge}`); } - if (rolesMetaData && !rolesMetaData.mode) { - rolesMetaData.mode = RoleMatchingMode.ANY; + const combinedRoles = rolesMetaDatas.flatMap(x => x.roles); + + if (combinedRoles.length === 0) { + return true; } - const rolesStr = JSON.stringify(rolesMetaData.roles); - this.logger.verbose(`Roles: ${rolesStr}`); + // Use matching mode of first item + const roleMetaData = rolesMetaDatas[0]; + const roleMatchingMode = roleMetaData.mode + ? roleMetaData.mode + : RoleMatchingMode.ANY; + + this.logger.verbose(`Using matching mode: ${roleMatchingMode}`); + this.logger.verbose(`Roles: ${JSON.stringify(combinedRoles)}`); // Extract request const [request] = extractRequest(context); @@ -86,9 +113,9 @@ export class RoleGuard implements CanActivate { // For verbose logging, we store it instead of returning it immediately const granted = - rolesMetaData.mode === RoleMatchingMode.ANY - ? rolesMetaData.roles.some(r => accessToken.hasRole(r)) - : rolesMetaData.roles.every(r => accessToken.hasRole(r)); + roleMatchingMode === RoleMatchingMode.ANY + ? combinedRoles.some(r => accessToken.hasRole(r)) + : combinedRoles.every(r => accessToken.hasRole(r)); if (granted) { this.logger.verbose(`Resource granted due to role(s)`); diff --git a/src/interface/keycloak-connect-options.interface.ts b/src/interface/keycloak-connect-options.interface.ts index bdfddf7..cf3e94e 100644 --- a/src/interface/keycloak-connect-options.interface.ts +++ b/src/interface/keycloak-connect-options.interface.ts @@ -1,7 +1,11 @@ // The typings are a bit of a mess, I'm sure there's a better way to do this. import { LogLevel } from '@nestjs/common'; -import { PolicyEnforcementMode, TokenValidation } from '../constants'; +import { + PolicyEnforcementMode, + RoleMerge, + TokenValidation, +} from '../constants'; export type KeycloakConnectOptions = string | KeycloakConnectConfig; @@ -56,6 +60,11 @@ export interface NestKeycloakConfig { * Multi tenant options. */ multiTenant?: MultiTenantOptions; + + /** + * Role merging options. + */ + roleMerge?: RoleMerge; } /**