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

PIMS-2083 Remove Keycloak Roles Code #2676

Merged
merged 7 commits into from
Sep 19, 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
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
Loading