Skip to content

Commit

Permalink
Add role merge option, closes #127
Browse files Browse the repository at this point in the history
  • Loading branch information
ferrerojosh committed Jul 13, 2022
1 parent c913c83 commit f540e44
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 13 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
11 changes: 11 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
51 changes: 39 additions & 12 deletions src/guards/role.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -37,20 +38,46 @@ export class RoleGuard implements CanActivate {
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
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);
Expand Down Expand Up @@ -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)`);
Expand Down
11 changes: 10 additions & 1 deletion src/interface/keycloak-connect-options.interface.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -56,6 +60,11 @@ export interface NestKeycloakConfig {
* Multi tenant options.
*/
multiTenant?: MultiTenantOptions;

/**
* Role merging options.
*/
roleMerge?: RoleMerge;
}

/**
Expand Down

0 comments on commit f540e44

Please sign in to comment.