Skip to content

Commit

Permalink
PIMS-2083 Remove Keycloak Roles Code (#2676)
Browse files Browse the repository at this point in the history
Co-authored-by: LawrenceLau2020 <68400651+LawrenceLau2020@users.noreply.github.com>
  • Loading branch information
dbarkowsky and LawrenceLau2020 authored Sep 19, 2024
1 parent 0464a7b commit 1d97341
Show file tree
Hide file tree
Showing 9 changed files with 3 additions and 610 deletions.
2 changes: 0 additions & 2 deletions express-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
"express-rate-limit": "7.4.0",
"morgan": "1.10.0",
"multer": "1.4.5-lts.1",
"node-cron": "3.0.3",
"node-sql-reader": "0.1.3",
"nunjucks": "3.2.4",
"pg": "8.12.0",
Expand All @@ -55,7 +54,6 @@
"@types/morgan": "1.9.9",
"@types/multer": "1.4.11",
"@types/node": "22.5.0",
"@types/node-cron": "3.0.11",
"@types/nunjucks": "3.2.6",
"@types/supertest": "6.0.2",
"@types/swagger-jsdoc": "6.0.4",
Expand Down
9 changes: 0 additions & 9 deletions express-api/src/middleware/keycloak/keycloakOptions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { SSOOptions, SSOUser } from '@bcgov/citz-imb-sso-express';
import logger from '@/utilities/winstonLogger';
import KeycloakService from '@/services/keycloak/keycloakService';
import { AppDataSource } from '@/appDataSource';
import { User } from '@/typeorm/Entities/User';

Expand All @@ -17,14 +16,6 @@ export const SSO_OPTIONS: SSOOptions = {
if (await users.exists({ where: { Username: user.preferred_username } })) {
await users.update({ Username: user.preferred_username }, { LastLogin: new Date() });
}
// Try to sync the user's roles from Keycloak
try {
await KeycloakService.syncKeycloakUser(user.preferred_username);
} catch (e) {
logger.warn(
`Could not sync roles for user ${user.preferred_username}. Error: ${(e as Error).message}`,
);
}
}
},
afterUserLogout: (user: SSOUser) => {
Expand Down
4 changes: 0 additions & 4 deletions express-api/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import app from '@/express';
import { AppDataSource } from '@/appDataSource';
import { Application } from 'express';
import { IncomingMessage, Server, ServerResponse } from 'http';
import cronSyncKeycloakRoles from '@/utilities/cronJobs/syncKeycloakRoles';

const { API_PORT } = constants;

Expand All @@ -27,9 +26,6 @@ const startApp = (app: Application) => {
.catch((err?: Error) => {
logger.error('Error during data source initialization. With error: ', err);
});

// Starting cron jobs
cronSyncKeycloakRoles();
});

return server;
Expand Down
264 changes: 2 additions & 262 deletions express-api/src/services/keycloak/keycloakService.ts
Original file line number Diff line number Diff line change
@@ -1,199 +1,6 @@
import { IKeycloakErrorResponse } from '@/services/keycloak/IKeycloakErrorResponse';
import { IKeycloakRole, IKeycloakRolesResponse } from '@/services/keycloak/IKeycloakRole';
import { IKeycloakUser, IKeycloakUsersResponse } from '@/services/keycloak/IKeycloakUser';
import {
keycloakRoleSchema,
keycloakUserRolesSchema,
keycloakUserSchema,
} from '@/services/keycloak/keycloakSchemas';
import logger from '@/utilities/winstonLogger';

import {
getRoles,
getRole,
updateRole,
createRole,
getIDIRUsers,
getBothBCeIDUser,
getUserRoles,
assignUserRoles,
unassignUserRole,
IDIRUserQuery,
} from '@bcgov/citz-imb-kc-css-api';
import rolesServices from '@/services/roles/rolesServices';
import { randomUUID } from 'crypto';
import { AppDataSource } from '@/appDataSource';
import { DeepPartial, In, Not } from 'typeorm';
import userServices from '@/services/users/usersServices';
import { Role } from '@/typeorm/Entities/Role';
import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode';
import { User } from '@/typeorm/Entities/User';

/**
* Synchronizes Keycloak roles with internal roles.
* Retrieves Keycloak roles, adds new roles to the internal system, updates existing roles, and deletes roles not present in Keycloak.
* @returns Returns the synchronized roles.
*/
const syncKeycloakRoles = async () => {
const systemUser = await userServices.getUsers({ username: 'system' });
if (systemUser?.length !== 1) {
throw new ErrorWithCode('System user was missing.', 500);
}
const systemId = systemUser[0].Id;
const roles = await KeycloakService.getKeycloakRoles();
for (const role of roles) {
const internalRole = await rolesServices.getRoles({ name: role.name });
if (internalRole.length == 0) {
const newRole: Role = {
Id: randomUUID(),
Name: role.name,
IsDisabled: false,
SortOrder: 0,
Description: undefined,
CreatedById: systemId,
CreatedBy: undefined,
CreatedOn: undefined,
UpdatedById: undefined,
UpdatedBy: undefined,
UpdatedOn: undefined,
Users: [],
};
await rolesServices.addRole(newRole);
} else {
const overwriteRole: DeepPartial<Role> = {
Id: internalRole[0].Id,
Name: role.name,
IsDisabled: false,
SortOrder: 0,
Description: undefined,
CreatedById: undefined,
CreatedOn: undefined,
UpdatedById: systemId,
UpdatedOn: new Date(),
};
await rolesServices.updateRole(overwriteRole);
}
}
//This deletion section is somewhat clunky. Could consider delete cascade on the schema to avoid some of this.
const internalRolesForDeletion = await AppDataSource.getRepository(Role).findBy({
Name: Not(In(roles.map((a) => a.name))),
});

if (internalRolesForDeletion.length) {
const roleIdsForDeletion = internalRolesForDeletion.map((role) => role.Id);
await AppDataSource.getRepository(User)
.createQueryBuilder()
.update(User)
.set({ RoleId: null })
.where('RoleId IN (:...ids)', { ids: roleIdsForDeletion })
.execute();
await AppDataSource.getRepository(Role).delete({
Id: In(roleIdsForDeletion),
});
}
return roles;
};

/**
* @description Fetch a list of groups from Keycloak and their associated role within PIMS
* @returns {IKeycloakRoles[]} A list of roles from Keycloak.
*/
const getKeycloakRoles = async () => {
try {
// Get roles available in Keycloak
const keycloakRoles: IKeycloakRolesResponse = await getRoles();
// Return the list of roles
return keycloakRoles.data;
} catch (e) {
throw new ErrorWithCode(
`Failed to update user's Keycloak roles. ${(e as IKeycloakErrorResponse).message}`,
500,
);
}
};

/**
* @description Get information on a single Keycloak role from the role name.
* @param {string} roleName String name of role in Keycloak
* @returns {IKeycloakRole} A single role object.
* @throws If the role does not exist in Keycloak.
*/
const getKeycloakRole = async (roleName: string) => {
// Get single role
const response: IKeycloakRole | IKeycloakErrorResponse = await getRole(roleName);
// Did the role exist? If not, it will be of type IKeycloakErrorResponse.
if (!keycloakRoleSchema.safeParse(response).success) {
const message = `keycloakService.getKeycloakRole: ${
(response as IKeycloakErrorResponse).message
}`;
logger.warn(message);
throw new Error(message);
}
// Return role info
return response;
};

/**
* @description Update a role that exists in Keycloak. Create it if it does not exist.
* @param {string} roleName String name of role in Keycloak
* @param {string} newRoleName The name to change the role name to.
* @returns {IKeycloakRole} The updated role information. Existing role info if cannot be updated.
* @throws {Error} If the newRoleName already exists.
*/
const updateKeycloakRole = async (roleName: string, newRoleName: string) => {
const roleWithNameAlready: IKeycloakRole = await getRole(newRoleName);
// If it already exists, log the error and return existing role
if (keycloakRoleSchema.safeParse(roleWithNameAlready).success) {
const message = `keycloakService.updateKeycloakRole: Role ${newRoleName} already exists`;
logger.warn(message);
throw new Error(message);
}
const response: IKeycloakRole = await getRole(roleName);
// Did the role to be changed exist? If not, it will be of type IKeycloakErrorResponse.
let role: IKeycloakRole;
if (keycloakRoleSchema.safeParse(response).success) {
// Already existed. Update the role.
role = await updateRole(roleName, newRoleName);
} else {
// Didn't exist already. Add the role.
role = await createRole(newRoleName);
}

// Return role info
return role;
};

/**
* @description Sync the given username string wtih keycloak
* @param {string} username String username to sync
* @returns A promise that resolves to the user object with associated Agency and Role.
* @throws {ErrorWithCode} If the username was not found.
*/
const syncKeycloakUser = async (username: string) => {
const users = await userServices.getUsers({ username: username });
if (users?.length !== 1) {
throw new ErrorWithCode('User was missing during keycloak role sync.', 500);
}
const user = users[0];
const kroles = await KeycloakService.getKeycloakUserRoles(user.Username);

if (kroles.length > 1) {
logger.warn(
`User ${user.Username} was assigned multiple roles in keycloak. This is not fully supported internally. A single role will be assigned arbitrarily.`,
);
}

const krole = kroles?.[0];
if (!krole) {
logger.warn(`User ${user.Username} has no roles in keycloak.`);
await userServices.updateUser({ Id: user.Id, RoleId: null });
return userServices.getUserById(user.Id);
}

const internalRole = await rolesServices.getRoleByName(krole.name);
await userServices.updateUser({ Id: user.Id, RoleId: internalRole.Id });
return userServices.getUserById(user.Id);
};
import { keycloakUserSchema } from '@/services/keycloak/keycloakSchemas';
import { getIDIRUsers, getBothBCeIDUser, IDIRUserQuery } from '@bcgov/citz-imb-kc-css-api';

/**
* @description Retrieves Keycloak users based on the provided filter.
Expand Down Expand Up @@ -230,76 +37,9 @@ const getKeycloakUser = async (guid: string) => {
}
};

/**
* @description Retrieves a Keycloak user's roles.
* @param {string} username The user's username.
* @returns {IKeycloakRole[]} A list of the user's roles.
* @throws If the user is not found.
*/
const getKeycloakUserRoles = async (username: string): Promise<IKeycloakRole[]> => {
const existingRolesResponse: IKeycloakRolesResponse | IKeycloakErrorResponse =
await getUserRoles(username);

if (!keycloakUserRolesSchema.safeParse(existingRolesResponse).success) {
const message = `keycloakService.getKeycloakUserRoles: ${(existingRolesResponse as IKeycloakErrorResponse).message}`;
logger.warn(message);
throw new Error(message);
}
// Ensure the response always returns an array of roles
return (existingRolesResponse as IKeycloakRolesResponse).data || [];
};

/**
* @description Updates a user's roles in Keycloak.
* @param {string} username The user's username.
* @param {string[]} roles A list of roles that the user should have.
* @returns {IKeycloakRole[]} A list of the updated Keycloak roles.
* @throws If the user does not exist.
*/
const updateKeycloakUserRoles = async (username: string, roles: string[]) => {
try {
const existingRolesResponse = await getKeycloakUserRoles(username);

// User is found in Keycloak.
const existingRoles: string[] = existingRolesResponse.map((role) => role.name);

// Find roles that are in Keycloak but are not in new user info.
const rolesToRemove = existingRoles.filter((existingRole) => !roles.includes(existingRole));
// Remove old roles
// No call to remove all as list, so have to loop.
rolesToRemove.forEach(async (role) => {
await unassignUserRole(username, role);
});

// Find new roles that aren't in Keycloak already.
const rolesToAdd = roles.filter((newRole) => !existingRoles.includes(newRole));
// Add new roles
const updatedRoles: IKeycloakRolesResponse = await assignUserRoles(username, rolesToAdd);

// Return updated list of roles
return updatedRoles.data;
} catch (e: unknown) {
const message = `keycloakService.updateKeycloakUserRoles: ${
(e as IKeycloakErrorResponse).message
}`;
logger.warn(message);
throw new ErrorWithCode(
`Failed to update user ${username}'s Keycloak roles. User's Keycloak account may not be active.`,
500,
);
}
};

const KeycloakService = {
getKeycloakUserRoles,
syncKeycloakRoles,
getKeycloakRole,
getKeycloakRoles,
updateKeycloakRole,
syncKeycloakUser,
getKeycloakUser,
getKeycloakUsers,
updateKeycloakUserRoles,
};

export default KeycloakService;
Loading

0 comments on commit 1d97341

Please sign in to comment.