Repository: bcgov/platform-services-registry
Pull request #5187: feat(4721): generate service account for new provision endpoint
Author: login: golebu2020
Created: 2025-03-13T18:59:51Z | Merged: 2025-03-14T21:13:00Z



Diff:
diff --git a/sandbox/docker-compose.yml b/sandbox/docker-compose.yml
index beddf027fc..e175fa2f44 100755
--- a/sandbox/docker-compose.yml
+++ b/sandbox/docker-compose.yml
@@ -37,6 +37,8 @@ services:
       context: ./
       dockerfile: ./keycloak-provision/Dockerfile
     environment:
+      PROVISION_SERVICE_ACCOUNT_ID: provision-service-account-id
+      PROVISION_SERVICE_ACCOUNT_SECRET: testsecret # pragma: allowlist secret
       KEYCLOAK_URL: http://keycloak:8080
       MASTER_ADMIN: admin
       MASTER_ADMIN_PASSWORD: password # pragma: allowlist secret
@@ -126,6 +128,8 @@ services:
       context: ./
       dockerfile: ./nats-provision/Dockerfile
     environment:
+      PROVISION_SERVICE_ACCOUNT_ID: provision-service-account-id
+      PROVISION_SERVICE_ACCOUNT_SECRET: testsecret # pragma: allowlist secret
       NATS_HOST: nats
       NATS_PORT: 4222
       APP_URL: http://${MACHINE_HOST_IP}:3000
@@ -133,6 +137,7 @@ services:
       PUBLIC_CLOUD_REALM_NAME: public-cloud
       PUBLIC_CLOUD_CLIENT_ID: roles
       PUBLIC_CLOUD_CLIENT_SECRET: testsecret # pragma: allowlist secret
+
     depends_on:
     - nats
 
diff --git a/sandbox/keycloak-provision/config.ts b/sandbox/keycloak-provision/config.ts
index b0eb938f3f..904c139d7e 100644
--- a/sandbox/keycloak-provision/config.ts
+++ b/sandbox/keycloak-provision/config.ts
@@ -11,3 +11,5 @@ export const ADMIN_CLIENT_SECRET = process.env.ADMIN_CLIENT_SECRET || '';
 export const PUBLIC_CLOUD_REALM_NAME = process.env.PUBLIC_CLOUD_REALM_NAME || '';
 export const PUBLIC_CLOUD_CLIENT_ID = process.env.PUBLIC_CLOUD_CLIENT_ID || '';
 export const PUBLIC_CLOUD_CLIENT_SECRET = process.env.PUBLIC_CLOUD_CLIENT_SECRET || '';
+export const PROVISION_SERVICE_ACCOUNT_ID = process.env.PROVISION_SERVICE_ACCOUNT_ID || '';
+export const PROVISION_SERVICE_ACCOUNT_SECRET = process.env.PROVISION_SERVICE_ACCOUNT_SECRET || '';
diff --git a/sandbox/keycloak-provision/main.ts b/sandbox/keycloak-provision/main.ts
index 32223f7226..8236babde2 100644
--- a/sandbox/keycloak-provision/main.ts
+++ b/sandbox/keycloak-provision/main.ts
@@ -18,6 +18,8 @@ import {
   PUBLIC_CLOUD_REALM_NAME,
   PUBLIC_CLOUD_CLIENT_ID,
   PUBLIC_CLOUD_CLIENT_SECRET,
+  PROVISION_SERVICE_ACCOUNT_ID,
+  PROVISION_SERVICE_ACCOUNT_SECRET,
 } from './config.js';
 
 const __filename = fileURLToPath(import.meta.url);
@@ -28,6 +30,26 @@ const msUsers: MsUser[] = JSON.parse(jsonData);
 
 const clientScope = 'https://graph.microsoft.com/.default';
 
+const ROLES = ['private-admin', 'public-admin'];
+
+function getMapperPayload(name: string, claimValue: string) {
+  const mapper = {
+    name,
+    protocol: 'openid-connect',
+    protocolMapper: 'oidc-hardcoded-claim-mapper',
+    config: {
+      'claim.name': name,
+      'claim.value': claimValue,
+      'jsonType.label': 'String',
+      'id.token.claim': 'true',
+      'access.token.claim': 'true',
+      'userinfo.token.claim': 'true',
+      'access.tokenResponse.claim': 'false',
+    },
+  };
+  return mapper;
+}
+
 async function main() {
   console.log('Starting Keycloak Provision...');
 
@@ -133,9 +155,31 @@ async function main() {
   await kc.upsertRealm(PUBLIC_CLOUD_REALM_NAME, { enabled: true });
   await kc.createRealmAdminServiceAccount(PUBLIC_CLOUD_REALM_NAME, PUBLIC_CLOUD_CLIENT_ID, PUBLIC_CLOUD_CLIENT_SECRET);
 
+  const provisionServiceAccount = await kc.createServiceAccount(
+    AUTH_REALM_NAME,
+    PROVISION_SERVICE_ACCOUNT_ID,
+    PROVISION_SERVICE_ACCOUNT_SECRET,
+  );
+
+  if (provisionServiceAccount?.id) {
+    const { id: provisionClientUid } = provisionServiceAccount;
+
+    await Promise.all([
+      kc.cli.clients.addProtocolMapper(
+        { realm: AUTH_REALM_NAME, id: provisionClientUid },
+        getMapperPayload('roles', ROLES.join(',')),
+      ),
+      kc.cli.clients.addProtocolMapper(
+        { realm: AUTH_REALM_NAME, id: provisionClientUid },
+        getMapperPayload('service_account_type', 'team'),
+      ),
+    ]);
+  }
+
   return {
     authRealm,
     authClient,
+    provisionServiceAccount,
   };
 }
 
diff --git a/sandbox/nats-provision/config.ts b/sandbox/nats-provision/config.ts
index 5228ec2e27..c2df1f28ec 100644
--- a/sandbox/nats-provision/config.ts
+++ b/sandbox/nats-provision/config.ts
@@ -5,3 +5,5 @@ export const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'http://localhost:8080';
 export const PUBLIC_CLOUD_REALM_NAME = process.env.PUBLIC_CLOUD_REALM_NAME || 'public-cloud';
 export const PUBLIC_CLOUD_CLIENT_ID = process.env.PUBLIC_CLOUD_CLIENT_ID || 'roles';
 export const PUBLIC_CLOUD_CLIENT_SECRET = process.env.PUBLIC_CLOUD_CLIENT_SECRET || 'testsecret';
+export const PROVISION_SERVICE_ACCOUNT_ID = process.env.PROVISION_SERVICE_ACCOUNT_ID || '';
+export const PROVISION_SERVICE_ACCOUNT_SECRET = process.env.PROVISION_SERVICE_ACCOUNT_SECRET || '';


Reviews:
user: login: junminahn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T20:05:10Z
user: login: golebu2020
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T20:09:31Z
user: login: junminahn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T21:10:22Z
user: login: junminahn
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T21:10:39Z

Review comments:
diff_hunk: @@ -28,6 +30,9 @@ const msUsers: MsUser[] = JSON.parse(jsonData);
 
 const clientScope = 'https://graph.microsoft.com/.default';
 
+const TEAM_SA_PREFIX = 'z_pltsvc-tsa-';
+const ROLES = ['admin', 'private-admin', 'public-admin'];
path: sandbox/keycloak-provision/main.ts
user: login: junminahn
body: `admin` role is not required
created_at: 2025-03-13T20:02:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,9 +138,81 @@ async function main() {
   await kc.upsertRealm(PUBLIC_CLOUD_REALM_NAME, { enabled: true });
   await kc.createRealmAdminServiceAccount(PUBLIC_CLOUD_REALM_NAME, PUBLIC_CLOUD_CLIENT_ID, PUBLIC_CLOUD_CLIENT_SECRET);
 
+  // // Creating service account for provision;
path: sandbox/keycloak-provision/main.ts
user: login: junminahn
body: please remove this comment
created_at: 2025-03-13T20:03:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,9 +138,81 @@ async function main() {
   await kc.upsertRealm(PUBLIC_CLOUD_REALM_NAME, { enabled: true });
   await kc.createRealmAdminServiceAccount(PUBLIC_CLOUD_REALM_NAME, PUBLIC_CLOUD_CLIENT_ID, PUBLIC_CLOUD_CLIENT_SECRET);
 
+  // // Creating service account for provision;
+
+  function generateRandomId(length: number) {
+    const buffer = crypto.randomBytes(Math.ceil(length / 2));
+    return buffer.toString('hex').slice(0, length);
+  }
+
+  function getMapperPayload(name: string, claimValue: string) {
+    const mapper = {
+      name,
+      protocol: 'openid-connect',
+      protocolMapper: 'oidc-hardcoded-claim-mapper',
+      config: {
+        'claim.name': name,
+        'claim.value': claimValue,
+        'jsonType.label': 'String',
+        'id.token.claim': 'true',
+        'access.token.claim': 'true',
+        'userinfo.token.claim': 'true',
+        'access.tokenResponse.claim': 'false',
+      },
+    };
+    return mapper;
+  }
+
+  async function createProvisionClient(kc: KcAdmin, realm: string, prefix: string, roles: any[]) {
path: sandbox/keycloak-provision/main.ts
user: login: junminahn
body: `roles: any[]`? can be specified
created_at: 2025-03-13T20:04:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,9 +138,81 @@ async function main() {
   await kc.upsertRealm(PUBLIC_CLOUD_REALM_NAME, { enabled: true });
   await kc.createRealmAdminServiceAccount(PUBLIC_CLOUD_REALM_NAME, PUBLIC_CLOUD_CLIENT_ID, PUBLIC_CLOUD_CLIENT_SECRET);
 
+  // // Creating service account for provision;
+
+  function generateRandomId(length: number) {
+    const buffer = crypto.randomBytes(Math.ceil(length / 2));
+    return buffer.toString('hex').slice(0, length);
+  }
+
+  function getMapperPayload(name: string, claimValue: string) {
+    const mapper = {
+      name,
+      protocol: 'openid-connect',
+      protocolMapper: 'oidc-hardcoded-claim-mapper',
+      config: {
+        'claim.name': name,
+        'claim.value': claimValue,
+        'jsonType.label': 'String',
+        'id.token.claim': 'true',
+        'access.token.claim': 'true',
+        'userinfo.token.claim': 'true',
+        'access.tokenResponse.claim': 'false',
+      },
+    };
+    return mapper;
+  }
+
+  async function createProvisionClient(kc: KcAdmin, realm: string, prefix: string, roles: any[]) {
+    const tsaId = generateRandomId(24);
+    const provisionClientId = `${prefix}${tsaId}`;
path: sandbox/keycloak-provision/main.ts
user: login: junminahn
body: the `client id` needs to be always the same or should be coming from environment variable.
think of why..
created_at: 2025-03-13T20:05:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,9 +138,81 @@ async function main() {
   await kc.upsertRealm(PUBLIC_CLOUD_REALM_NAME, { enabled: true });
   await kc.createRealmAdminServiceAccount(PUBLIC_CLOUD_REALM_NAME, PUBLIC_CLOUD_CLIENT_ID, PUBLIC_CLOUD_CLIENT_SECRET);
 
+  // // Creating service account for provision;
+
+  function generateRandomId(length: number) {
+    const buffer = crypto.randomBytes(Math.ceil(length / 2));
+    return buffer.toString('hex').slice(0, length);
+  }
+
+  function getMapperPayload(name: string, claimValue: string) {
+    const mapper = {
+      name,
+      protocol: 'openid-connect',
+      protocolMapper: 'oidc-hardcoded-claim-mapper',
+      config: {
+        'claim.name': name,
+        'claim.value': claimValue,
+        'jsonType.label': 'String',
+        'id.token.claim': 'true',
+        'access.token.claim': 'true',
+        'userinfo.token.claim': 'true',
+        'access.tokenResponse.claim': 'false',
+      },
+    };
+    return mapper;
+  }
+
+  async function createProvisionClient(kc: KcAdmin, realm: string, prefix: string, roles: any[]) {
+    const tsaId = generateRandomId(24);
+    const provisionClientId = `${prefix}${tsaId}`;
path: sandbox/keycloak-provision/main.ts
user: login: golebu2020
body: Yes, it should be the same I thought of it but wasn't sure. Since it will be used only once by the service. It will also avoid recreate more than one service accounts.
created_at: 2025-03-13T20:09:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,3 +11,5 @@ export const ADMIN_CLIENT_SECRET = process.env.ADMIN_CLIENT_SECRET || '';
 export const PUBLIC_CLOUD_REALM_NAME = process.env.PUBLIC_CLOUD_REALM_NAME || '';
 export const PUBLIC_CLOUD_CLIENT_ID = process.env.PUBLIC_CLOUD_CLIENT_ID || '';
 export const PUBLIC_CLOUD_CLIENT_SECRET = process.env.PUBLIC_CLOUD_CLIENT_SECRET || '';
+export const AUTH_RELM = process.env.AUTH_RELM || 'platform-services';
+export const FIXED_TSA_ID = process.env.FIXED_TSA_ID || 'dcd638006814e95f9972cd93';
path: sandbox/keycloak-provision/config.ts
user: login: junminahn
body: `export const PROVISION_SERVICE_ACCOUNT_ID = process.env.PROVISION_SERVICE_ACCOUNT_ID || 'provision-service-account-id';`
created_at: 2025-03-13T21:08:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,14 +11,17 @@ import {
   AUTH_REALM_NAME,
   AUTH_CLIENT_ID,
   AUTH_CLIENT_SECRET,
+  AUTH_RELM,
   GITOPS_CLIENT_ID,
   GITOPS_CLIENT_SECRET,
   ADMIN_CLIENT_ID,
   ADMIN_CLIENT_SECRET,
   PUBLIC_CLOUD_REALM_NAME,
   PUBLIC_CLOUD_CLIENT_ID,
   PUBLIC_CLOUD_CLIENT_SECRET,
+  FIXED_TSA_ID,
 } from './config.js';
+import * as crypto from 'crypto';
path: sandbox/keycloak-provision/main.ts
user: login: junminahn
body: please remove this line
created_at: 2025-03-13T21:08:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,9 +139,73 @@ async function main() {
   await kc.upsertRealm(PUBLIC_CLOUD_REALM_NAME, { enabled: true });
   await kc.createRealmAdminServiceAccount(PUBLIC_CLOUD_REALM_NAME, PUBLIC_CLOUD_CLIENT_ID, PUBLIC_CLOUD_CLIENT_SECRET);
 
+  function getMapperPayload(name: string, claimValue: string) {
+    const mapper = {
+      name,
+      protocol: 'openid-connect',
+      protocolMapper: 'oidc-hardcoded-claim-mapper',
+      config: {
+        'claim.name': name,
+        'claim.value': claimValue,
+        'jsonType.label': 'String',
+        'id.token.claim': 'true',
+        'access.token.claim': 'true',
+        'userinfo.token.claim': 'true',
+        'access.tokenResponse.claim': 'false',
+      },
+    };
+    return mapper;
+  }
+
+  async function createProvisionClient(kc: KcAdmin, realm: string, prefix: string, roles: string[]) {
+    const provisionClientId = `${prefix}${FIXED_TSA_ID}`;
path: sandbox/keycloak-provision/main.ts
user: login: junminahn
body: please remove this line
created_at: 2025-03-13T21:09:24Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-14T20:57:20Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=bcgov_platform-services-registry&pullRequest=5187) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=bcgov_platform-services-registry&pullRequest=5187&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=bcgov_platform-services-registry&pullRequest=5187&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=bcgov_platform-services-registry&pullRequest=5187&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=bcgov_platform-services-registry&pullRequest=5187&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=bcgov_platform-services-registry&pullRequest=5187&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=bcgov_platform-services-registry&pullRequest=5187)


author_association: NONE