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-545 Transfer Keycloak Users-Roles #2166

Merged
merged 7 commits into from
Feb 2, 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
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": ["./**/*"]
}
Loading