Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace userAuthGroup with userRole, roles to claims in policy #1550

Merged
merged 5 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dops/src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from '../../guard/auth.guard';
import { RolesGuard } from '../../guard/roles.guard';
import { PermissionsGuard } from '../../guard/permissions.guard';
import { JwtServiceAccountStrategy } from './jwt-service-account.strategy';

@Module({
Expand All @@ -19,7 +19,7 @@ import { JwtServiceAccountStrategy } from './jwt-service-account.strategy';
},
{
provide: APP_GUARD,
useClass: RolesGuard,
useClass: PermissionsGuard,
},
],
exports: [AuthService],
Expand Down
8 changes: 8 additions & 0 deletions policy/src/decorator/permissions.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SetMetadata } from '@nestjs/common';
import { Claim } from '../enum/claims.enum';
import { IRole } from '../interface/role.interface';
import { IPermissions } from '../interface/permissions.interface';

export const PERMISSIONS_KEY = 'permissions';
export const Permissions = (...roles: Claim[] | IRole[] | IPermissions[]) =>
SetMetadata(PERMISSIONS_KEY, roles);
7 changes: 0 additions & 7 deletions policy/src/decorator/roles.decorator.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/**
* The roles of a user.
* The claim of a user.
*
* NOTE: The frontend has an identical copy of this. Any changes made here
* should be cascaded.
*/
export enum Role {
export enum Claim {
PUBLIC_AGENT = 'ORBC-PUBLIC-AGENT',
PUBLIC_ORG_ADMIN = 'ORBC-PUBLIC-ORG-ADMIN',
PUBLIC_USER_ADMIN = 'ORBC-PUBLIC-USER-ADMIN',
Expand Down
32 changes: 0 additions & 32 deletions policy/src/enum/user-auth-group.enum.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import { matchRoles } from '../helper/auth.helper';
import { IRole } from '../interface/role.interface';
import { IDP } from 'src/enum/idp.enum';
import { PERMISSIONS_KEY } from '../decorator/permissions.decorator';
import { IPermissions } from '../interface/permissions.interface';

@Injectable()
export class RolesGuard implements CanActivate {
export class PermissionsGuard implements CanActivate {
constructor(private reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const permissions = this.reflector.getAllAndOverride<Claim[] | IRole[]>(
PERMISSIONS_KEY,
[context.getHandler(), context.getClass()],
);
const permissions = this.reflector.getAllAndOverride<
Claim[] | IRole[] | IPermissions[]
>(PERMISSIONS_KEY, [context.getHandler(), context.getClass()]);
// Guard is invoked regardless of the decorator being actively called
if (!permissions) {
return true;
Expand Down
30 changes: 0 additions & 30 deletions policy/src/guard/roles.guard.ts

This file was deleted.

121 changes: 78 additions & 43 deletions policy/src/helper/auth.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@ import {
} from '@nestjs/common';
import { Directory } from '../enum/directory.enum';
import { IDP } from '../enum/idp.enum';
import { Role } from '../enum/roles.enum';
import { Claim } from '../enum/claims.enum';
import { IUserJWT } from '../interface/user-jwt.interface';
import {
UserAuthGroup,
ClientUserAuthGroup,
IDIRUserAuthGroup,
} from '../enum/user-auth-group.enum';
import { UserRole, ClientUserRole, IDIRUserRole } from '../enum/user-role.enum';
import { IRole } from '../interface/role.interface';
import { IPermissions } from '../interface/permissions.interface';

/**
* Determines the directory type based on the identity provider of the user.
Expand All @@ -33,13 +30,29 @@ export const getDirectory = (user: IUserJWT) => {
}
};

/**
* Type guard to check if an input object has the structure of an IPermissions.
*
* @param {IRole | Claim[]} permissions - The object to be checked.
* @returns {permissions is IPermissions} True if the object matches the IPermissions structure, false otherwise.
*/
const isIPermissions = (
permissions: IRole | Claim[] | IPermissions,
): permissions is IPermissions => {
const { allowedBCeIDRoles, allowedIdirRoles, claims } =
permissions as IPermissions;
return (
Boolean(allowedBCeIDRoles) || Boolean(allowedIdirRoles) || Boolean(claims)
);
};

/**
* Type guard to check if an input is an array of type Role.
*
* @param {Role[] | IRole[]} obj - The object to be checked.
* @returns {obj is Role[]} True if obj is an array of Role, false otherwise.
* @param {Claim[] | IRole[] | IPermissions[]} obj - The object to be checked.
* @returns {obj is Claim[]} True if obj is an array of Role, false otherwise.
*/
function isRoleArray(obj: Role[] | IRole[]): obj is Role[] {
function isClaimArray(obj: Claim[] | IRole[] | IPermissions[]): obj is Claim[] {
return Array.isArray(obj) && obj.every((item) => typeof item === 'string');
}

Expand All @@ -48,62 +61,84 @@ function isRoleArray(obj: Role[] | IRole[]): obj is Role[] {
*
* This method supports two kinds of inputs for role requirements:
* 1. Simple list of roles (Role[]): It checks if the user holds any role from the specified list. True indicates possession of a required role.
* 2. Complex role requirements (IRole[]): For each object defining roles with 'allOf', 'oneOf', and/or 'userAuthGroup', it evaluates:
* - If 'userAuthGroup' is defined, the user must belong to it.
* 2. Complex role requirements (IRole[]): For each object defining roles with 'allOf', 'oneOf', and/or 'userRole', it evaluates:
* - If 'userRole' is defined, the user must belong to it.
* - For 'allOf', the user must have all specified roles.
* - For 'oneOf', the user must have at least one of the specified roles.
* If any role object's criteria are met (considering 'userAuthGroup' if defined), it returns true.
* If any role object's criteria are met (considering 'userRole' if defined), it returns true.
* Throws an error if both 'allOf' and 'oneOf' are defined in a role object.
*
* @param {Role[] | IRole[]} roles - An array of roles or role requirement objects to be matched against the user's roles.
* @param {Role[]} userRoles - An array of roles assigned to the user.
* @param {UserAuthGroup} userAuthGroup - Optional. The user authorization group to which the user belongs.
* @param {Claim[] | IRole[] | IPermissions[]} permissions - An array of roles or role requirement objects to be matched against the user's roles.
* @param {Claim[]} userClaims - An array of roles assigned to the user.
* @param {UserRole} userRole - Optional. The user authorization group to which the user belongs.
* @returns {boolean} Returns true if the user meets any of the defined role criteria or belongs to the specified user authorization group; false otherwise.
*/
export const matchRoles = (
roles: Role[] | IRole[],
userRoles: Role[],
userAuthGroup?: UserAuthGroup,
permissions: Claim[] | IRole[] | IPermissions[],
userClaims: Claim[],
userRole?: UserRole,
) => {
if (isRoleArray(roles)) {
// Scenario: roles is a simple list of Role objects.
// This block checks if any of the roles assigned to the user (userRoles)
if (isIPermissions(permissions[0] as IPermissions)) {
const { allowedIdirRoles, allowedBCeIDRoles, claims } =
permissions[0] as IPermissions;
// If only claims is specified, return the value of that.
if (claims && !allowedBCeIDRoles && !allowedIdirRoles) {
return claims.some((claim) => userClaims.includes(claim));
}
let isAllowed: boolean;
const isIdir = userRole in IDIRUserRole;
if (isIdir) {
isAllowed = allowedIdirRoles?.includes(userRole as IDIRUserRole);
} else {
isAllowed = allowedBCeIDRoles?.includes(userRole as ClientUserRole);
}
// If claims is specified alongside the allowed roles, include
// its value in the output.
if (claims) {
isAllowed =
isAllowed && claims.some((claim) => userClaims.includes(claim));
}
return isAllowed;
}
if (isClaimArray(permissions)) {
// Scenario: claims is a simple list of Claim objects.
// This block checks if any of the claims assigned to the user (userRoles)
// matches at least one of the roles specified in the input list (roles).
// It returns true if there is a match, indicating the user has at least one of the required roles.
return roles?.some((role) => userRoles.includes(role));
return permissions?.some((claim) => userClaims.includes(claim));
} else {
// Scenario: roles is not a simple list, but an object or objects implementing IRole,
// Scenario: claims is not a simple list, but an object or objects implementing IRole,
// meaning complex role requirements can be specified.
// This block first checks for an invalid case where both 'allOf' and 'oneOf' are defined in a roleObject,
// then verifies if the user belongs to the specified 'userAuthGroup' if defined.
// then verifies if the user belongs to the specified 'userRole' if defined.
// Following, it checks two conditions for each role object:
// 1. allOf - every role listed must be included in userRoles.
// 2. oneOf - at least one of the roles listed must be included in userRoles.
// 1. allOf - every claim listed must be included in userClaims.
// 2. oneOf - at least one of the roles listed must be included in userClaims.
// It returns true if either condition is met for any role object, indicating the user meets the role requirements.
// An error is thrown if 'allOf' and 'oneOf' are both defined, as it's considered an invalid configuration.
return roles.some((roleObject) => {
return (permissions as IRole[]).some((roleObject) => {
if (roleObject.allOf?.length && roleObject.oneOf?.length) {
throw new InternalServerErrorException(
'Cannot define both allOf and oneOf at the same time!',
);
}

if (roleObject.userAuthGroup?.length) {
const userAuthGroupMatch = roleObject.userAuthGroup?.some(
(authGroup) => authGroup === userAuthGroup,
if (roleObject.userRole?.length) {
const userRoleMatch = roleObject.userRole?.some(
(role) => role === userRole,
);
if (!userAuthGroupMatch) {
if (!userRoleMatch) {
return false;
} else if (!roleObject.allOf?.length && !roleObject.oneOf?.length) {
return true;
}
}

const allOfMatch = roleObject.allOf?.every((role) =>
userRoles.includes(role),
const allOfMatch = roleObject.allOf?.every((claim) =>
userClaims.includes(claim),
);
const oneOfMatch = roleObject.oneOf?.some((role) =>
userRoles.includes(role),
const oneOfMatch = roleObject.oneOf?.some((claim) =>
userClaims.includes(claim),
);

return oneOfMatch || allOfMatch;
Expand Down Expand Up @@ -173,19 +208,19 @@ export const checkUserCompaniesContext = (
* 1. The user must have at least one of the specified roles.
* 2. If the user has at least one of the specified roles, they must also be associated with one of the specified companies.
*
* @param {Role[]} roles - An array of roles that the user is supposed to have.
* @param {Claim[]} roles - An array of roles that the user is supposed to have.
* @param {string} userGUID - The unique identifier for the user.
* @param {number[]} userCompanies - An array of company IDs that the user is supposed to be associated with.
* @param {IUserJWT} currentUser - The current user's information, including roles and identity provider.
* @throws {ForbiddenException} Throws ForbiddenException if the user does not meet the role or company association criteria.
*/
export const validateUserCompanyAndRoleContext = (
roles: Role[],
roles: Claim[],
userGUID: string,
userCompanies: number[],
currentUser: IUserJWT,
) => {
const rolesExists = matchRoles(roles, currentUser.roles);
const rolesExists = matchRoles(roles, currentUser.claims);
if (!rolesExists && userGUID) {
throw new ForbiddenException();
}
Expand All @@ -199,15 +234,15 @@ export const validateUserCompanyAndRoleContext = (
};

/**
* Determines if a specified UserAuthGroup value is present within a given enumeration object.
* Determines if a specified UserRole value is present within a given enumeration object.
*
* @param {UserAuthGroup} value - The UserAuthGroup value to be checked.
* @param {ReadonlyArray<ClientUserAuthGroup> | ReadonlyArray<IDIRUserAuthGroup>} enumObject - An array of UserAuthGroup values.
* @param {UserRole} value - The UserRole value to be checked.
* @param {ReadonlyArray<ClientUserRole> | ReadonlyArray<IDIRUserRole>} enumObject - An array of UserRole values.
* @returns {boolean} Returns true if the value is present in the enumObject, otherwise false.
*/
export const doesUserHaveAuthGroup = (
value: UserAuthGroup,
enumObject: readonly ClientUserAuthGroup[] | readonly IDIRUserAuthGroup[],
value: UserRole,
enumObject: readonly ClientUserRole[] | readonly IDIRUserRole[],
): boolean => {
// This function checks if the given value is present in the enumObject array
return Object.values(enumObject).includes(value);
Expand Down
31 changes: 31 additions & 0 deletions policy/src/interface/permissions.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Claim } from "../enum/claims.enum";
import { ClientUserRole, IDIRUserRole } from "../enum/user-role.enum";

/**
* The permission configuration for endpoints.
*/
export interface IPermissions {
/**
* The idir auth roles that are allowed to see the component.
*
* If the user has one of the specified auth groups,
* the component will render.
*/
allowedIdirRoles?: IDIRUserRole[];

/**
* The bceid auth roles that are allowed to see the component.
*
* If the user has one of the specified auth groups,
* the component will render.
*/
allowedBCeIDRoles?: ClientUserRole[];

/**
* The collection of individual security claims that may be used
* for additional consideration.
*
* If provided, the claim will be additionally checked on.
*/
claims?: Claim[];
}
Loading
Loading