-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PIMS-545 Transfer Keycloak Users-Roles (#2166)
- Loading branch information
1 parent
a5d0c64
commit 59db332
Showing
8 changed files
with
190 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
extractResults.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": ["./**/*"] | ||
} |