Skip to content

Commit

Permalink
PIMS-545 Transfer Keycloak Users-Roles (#2166)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbarkowsky authored and TaylorFries committed Feb 13, 2024
1 parent a5d0c64 commit 59db332
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 0 deletions.
5 changes: 5 additions & 0 deletions tools/keycloakRoleMapping/.env-template
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CSS_API_CLIENT_ID= # Keycloak CSS API Service Account client_id
CSS_API_CLIENT_SECRET= # Keycloak CSS API Service Account client_secret

SSO_INTEGRATION_ID= # Current integration ID. Change between extract and inject commands.
SSO_ENVIRONMENT= # 'dev', 'test' or 'prod'. Default is 'dev'.
1 change: 1 addition & 0 deletions tools/keycloakRoleMapping/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
extractResults.json
24 changes: 24 additions & 0 deletions tools/keycloakRoleMapping/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Keycloak Roles Transfer Scripts

## Purpose

During the PIMS modernization project, the multiple Keycloak integrations were reworked into one singular one, and the names of roles were also changed from their original values.

These scripts were created to transfer the existing roles and re-map them to the new roles for each user.

## Instructions

### Setup

1. Node must be installed on your local system for this to work.
2. Use the command `npm i` from this directory to install the necessary dependencies.
3. Create a `.env` file using the `.env-template` file as an example. These keys should be available through the [Keycloak dashboard](https://bcgov.github.io/sso-requests).

### Commands

- `npm run extract`: Takes all users and roles from specified integration and saves a JSON file in this directory with their mappings.
- `npm run import`: Uses the JSON file saved in the extract command to transform and apply the old roles to new roles, applying them to relevant users.

### Notes

- Make sure to switch the integration information in your `.env` between extract and import commands.
20 changes: 20 additions & 0 deletions tools/keycloakRoleMapping/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "keycloak-role-mapping-tool",
"version": "1.0.0",
"description": "Used to extract roles from existing Keycloak integrations and remap them onto new ones.",
"scripts": {
"extract": "ts-node ./src/extractRoleMap.ts",
"import": "ts-node ./src/importRoleMap.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Dylan Barkowsky",
"license": "ISC",
"dependencies": {
"@bcgov/citz-imb-kc-css-api": "https://github.com/bcgov/citz-imb-kc-css-api/releases/download/v1.3.4/bcgov-citz-imb-kc-css-api-1.3.4.tgz"
},
"devDependencies": {
"@types/node": "20.11.16",
"ts-node": "10.9.2",
"typescript": "5.3.3"
}
}
71 changes: 71 additions & 0 deletions tools/keycloakRoleMapping/src/extractRoleMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { getRoles, getUsersWithRole } from '@bcgov/citz-imb-kc-css-api';
import fs from 'fs';

interface IRole {
name: string;
composite: boolean;
}

interface IRolesResponse {
data: IRole[];
}

interface IUser {
email: string;
firstName: string;
lastName: string;
username: string;
}

// Gets list of roles from integration
const getRoleList = async () => {
const result: IRolesResponse = await getRoles();
// Filter out base claims
return result.data.filter(role => role.composite);
}

// Create the user/roles object
const createUserRolesObject = async () => {
const roleUsers: Record<string, IUser[]> = {};

// Get roles
const roleList = await getRoleList();

// Get all users for each role
// Runs all calls in parallel
await Promise.all(roleList.map(async (role) => {
const users = await getUsersWithRole(role.name);
roleUsers[role.name] = users.data;
}))

// Convert data to an object of users with a list of all their current roles
const usersWithRoles: Record<string, string[]> = {};
Object.keys(roleUsers).forEach(role => {
// For each user with that role
roleUsers[role].forEach((user: IUser) => {
// Does this already exist in usersWithRoles?
if (usersWithRoles[user.username]) {
// Just add this role to the list
usersWithRoles[user.username].push(role);
} else {
usersWithRoles[user.username] = [role];
}
})
})
return usersWithRoles;
}

// Save the roles to a file
const saveResultToFile = async () => {
const result = await createUserRolesObject();
fs.writeFile('extractResults.json', JSON.stringify(result, null, 2), (err) => {
if (err) {
console.error('Error writing file', err);
} else {
console.log('Successfully wrote file: extractResults.json');
}
});
}

// Call the saving file function
saveResultToFile();
42 changes: 42 additions & 0 deletions tools/keycloakRoleMapping/src/importRoleMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { assignUserRoles } from '@bcgov/citz-imb-kc-css-api';
import fs from 'fs';
import data from '../extractResults.json';
import { getMappedRole } from './roleMappingConversion';

// Seemingly needed so it identifies keys as strings
const typedData = data as Record<string, string[]>;

const importRoles = async () => {
const usernames: string[] = Object.keys(typedData);

await Promise.all(usernames.map(async (username) => {
// Get old roles
const oldRoles = typedData[username];
// Map old roles to new roles
// and convert to Set to remove duplicates
const newRoles: Set<string> = new Set(oldRoles.map((role: string) => getMappedRole(role)))
try {
// If there is more than one new role, we have to choose the most permissive
// Logic should work if there's only one role as well.
switch (true) {
case newRoles.has('admin'):
await assignUserRoles(username, ['admin']);
break;
case newRoles.has('auditor'):
await assignUserRoles(username, ['auditor']);
break;
case newRoles.has('general user'):
await assignUserRoles(username, ['general user']);
break;
default:
break;
}

} catch (e) {
console.error(e);
}
}))
console.log('Finished successfully! Please check Keycloak integration to confirm.')
}

importRoles();
12 changes: 12 additions & 0 deletions tools/keycloakRoleMapping/src/roleMappingConversion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const getMappedRole = (oldRole: string) => {
switch (true) {
case ['Minister Assistant', 'Minister', 'Assistant Deputy', 'Executive Director', 'View Only Properties'].includes(oldRole):
return 'auditor';
case ['Manager', 'Agency Administrator', 'Real Estate Analyst', 'Real Estate Manager'].includes(oldRole):
return 'general user';
case ['System Administrator', 'SRES', 'SRES Financial Reporter', 'SRES Financial', 'SRES Financial Manager'].includes(oldRole):
return 'admin';
default:
throw new Error(`No mapped role found for original role: ${oldRole}`);
}
}
15 changes: 15 additions & 0 deletions tools/keycloakRoleMapping/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es2022",
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true
},
"include": ["./**/*"]
}

0 comments on commit 59db332

Please sign in to comment.