ef-keycloak-connect 1.8.2-RC2 → 1.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ {
2
+ }
package/README.md CHANGED
@@ -40,6 +40,7 @@ This adapter is extended from keycloak-connect and have functionalities of both
40
40
  - getRealmRoles
41
41
  - assignRoleToUser
42
42
  - authenticateFinesse
43
+ - createRealmAsTenant
43
44
 
44
45
  ```
45
46
  ### Example
@@ -98,7 +99,9 @@ Sample `config` is given below:
98
99
  "ef-server-url": "https://<cx instance fqdn>/unified-admin/",
99
100
  "FINESSE_USERNAME_ADMIN": "<cisco admin username>",
100
101
  "FINESSE_PASSWORD_ADMIN": "<cisco admin username>",
101
- "SYNC_AGENT_PASSWORD": "<qm sync agent/supervisor password>"
102
+ "SYNC_AGENT_PASSWORD": "<qm sync agent/supervisor password>",
103
+ "MASTER_USERNAME": "master_admin_username",
104
+ "MASTER_PASSWORD": "master_admin_password"
102
105
  }
103
106
  ```
104
107
  Here is the definition of each property defined in config file/object.
@@ -129,6 +132,8 @@ Here is the definition of each property defined in config file/object.
129
132
  - **FINESSE_PASSWORD_ADMIN:** Finesse Administrator Password of instance in use.
130
133
  - **FINESSE_URL**: Finesse Instance URL currently in use.
131
134
  - **SYNC_AGENT_PASSWORD**: Password for Users being synced to Keycloak/CX.
135
+ - **MASTER_USERNAME**: Keycloak master admin username.
136
+ - **MASTER_PASSWORD**: Keycloak master password username.
132
137
 
133
138
  For using keycloak-connect features:
134
139
  ```js
@@ -420,6 +425,16 @@ It takes a signle argument:
420
425
 
421
426
  - adminToken: Admin token is required to authorize the list of roles to return.
422
427
 
428
+ ### createRealmAsTenant(tenantName, realmFile, authzConfigFilePath)
429
+
430
+ This function creates a new Realm as a Teant in keycloak and also import its permission/policies based on configurations given in realm and authz files.
431
+
432
+ It takes 3 arguments:
433
+
434
+ - tenantName: Name of realm that will be created as tenant in keycloak.
435
+ - realmFile: Realm configurations given in a JSON format, against which tenant will be created in keycloak, this is the path of file.
436
+ - authzConfigFilePath: Authz configurations given in a JSON format, against which policies/permissions will be created in keycloak, this is the path of file.
437
+
423
438
  ### authenticateFinesse(username, password, finesseUrl, userRoles, finesseToken)
424
439
 
425
440
  This function sync finesse user in keycloak, it first authenticates user from finesse, then check for its existance in keycloak. If it exists in keycloak then generates an access_token along with role mapping and return it to user. If user doesn't exist then it creates a user, assign it roles and return the access_token along with role mapping for newly created user.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ef-keycloak-connect",
3
- "version": "1.8.2-RC2",
3
+ "version": "1.8.3",
4
4
  "description": "Node JS keycloak adapter for authentication and authorization.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -57,8 +57,11 @@ class CiscoSyncService {
57
57
 
58
58
  reject( {
59
59
 
60
- error_message: "Finesse Team Sync Error: Error occured while creating cx core team.",
61
- error_detail: error
60
+ error_message: `Finesse Team Sync Error: Error occured while creating cx core team. TEAM ID: ${ciscoTeam.id}, TEAM NAME: ${ciscoTeam.name}`,
61
+ error_detail: ( error?.status && error?.status == 409 ) ? {
62
+ status: 409,
63
+ reason: `CX Team ${ciscoTeam.name} already exists against this Cisco Team with different ID, Causing Conflict.`
64
+ } : error
62
65
  } );
63
66
  }
64
67
  } );
@@ -120,9 +123,6 @@ class CiscoSyncService {
120
123
  }
121
124
  } );
122
125
 
123
- //testing with only 2 teams for now
124
- //ciscoTeams = ciscoTeams.slice( 0, 2 );
125
-
126
126
  // Iterate over Cisco teams and sync them with CX
127
127
  for ( let ciscoTeam of ciscoTeams ) {
128
128
 
@@ -1442,8 +1442,6 @@ class CiscoSyncService {
1442
1442
  } );
1443
1443
  }
1444
1444
 
1445
- // Output the results
1446
-
1447
1445
  return {
1448
1446
  agentsToAdd: agentsToAdd,
1449
1447
  agentsToRemove: agentsToRemove,
@@ -1737,8 +1735,8 @@ class CiscoSyncService {
1737
1735
  }
1738
1736
 
1739
1737
  cxAgents[ ciscoTeamId ].push( {
1740
- id: keycloakSupervisor?.id,
1741
- username: keycloakSupervisor?.username
1738
+ id: keycloakAgent?.id,
1739
+ username: keycloakAgent?.username
1742
1740
  } );
1743
1741
  }
1744
1742
  } );
@@ -1,5 +1,6 @@
1
1
  let session = require( "express-session" );
2
2
  let Keycloak = require( "keycloak-connect" );
3
+ const fs = require( 'fs' );
3
4
  const Joi = require( "joi" );
4
5
  const qrcode = require( "qrcode" );
5
6
  const speakeasy = require( 'speakeasy' )
@@ -4276,6 +4277,382 @@ class KeycloakService extends Keycloak {
4276
4277
  } );
4277
4278
  }
4278
4279
 
4280
+ // !-------------- Multitenancy -----------------!
4281
+
4282
+ async createRealmAsTenant( tenantName, realmFile, authzConfigFilePath ) {
4283
+
4284
+ return new Promise( async ( resolve, reject ) => {
4285
+
4286
+ let realmImportSuccessful = false;
4287
+ let mainMessage = "";
4288
+ let realmConfigString = '';
4289
+ let realmData = '';
4290
+ let authzConfigString = '';
4291
+ let authzConfig = '';
4292
+
4293
+ let accessToken;
4294
+ let URL = keycloakConfig[ "auth-server-url" ] + "realms/master/protocol/openid-connect/token";
4295
+
4296
+ let config = {
4297
+ method: "post",
4298
+ url: URL,
4299
+ headers: {
4300
+ "Content-Type": "application/x-www-form-urlencoded",
4301
+ },
4302
+ data: {
4303
+ client_id: "admin-cli",
4304
+ grant_type: "password",
4305
+ username: keycloakConfig[ "MASTER_USERNAME" ],
4306
+ password: keycloakConfig[ "MASTER_PASSWORD" ]
4307
+ },
4308
+ };
4309
+
4310
+ try {
4311
+
4312
+ let adminAccessToken = await requestController.httpRequest( config, true );
4313
+
4314
+ accessToken = adminAccessToken.data.access_token;
4315
+
4316
+ let createRealmUrl = keycloakConfig[ "auth-server-url" ] + 'admin/realms';
4317
+
4318
+ // 1. Read the realm configuration JSON file
4319
+ console.log( `Reading realm configuration from: ${realmFile}` );
4320
+
4321
+ try {
4322
+
4323
+ realmConfigString = await fs.readFileSync( realmFile, 'utf-8' );
4324
+
4325
+ } catch ( fileReadError ) {
4326
+
4327
+ if ( fileReadError.code === 'ENOENT' ) {
4328
+
4329
+ reject( {
4330
+ "error_message": "Error occurred while reading Realm file during Tenant creation process.",
4331
+ "error_detail": {
4332
+ "status": 404,
4333
+ "reason": `Realm configuration file not found at provided path: ${realmFile} `
4334
+ }
4335
+ } );
4336
+ }
4337
+
4338
+ reject( {
4339
+ "error_message": "Error occurred while reading Realm file during Tenant creation process.",
4340
+ "error_detail": {
4341
+ "status": 500,
4342
+ "reason": `Error reading realm configuration file: ${fileReadError.message}. {file path: ${realmFile}} `
4343
+ }
4344
+ } )
4345
+
4346
+ }
4347
+
4348
+ if ( !realmConfigString || realmConfigString.trim() === '' ) {
4349
+
4350
+ reject( {
4351
+ "error_message": "Error occurred while reading Realm file during Tenant creation process.",
4352
+ "error_detail": {
4353
+ "status": 404,
4354
+ "reason": `Provided Realm configuration file to create Tenant is empty. Please provide valid realm file in JSON format. `
4355
+ }
4356
+ } );
4357
+
4358
+ }
4359
+
4360
+ try {
4361
+
4362
+ realmData = JSON.parse( realmConfigString );
4363
+
4364
+ } catch ( parseError ) {
4365
+
4366
+ if ( parseError instanceof SyntaxError ) {
4367
+
4368
+ reject( {
4369
+ "error_message": "Error occurred while parsing Realm file during Tenant creation process.",
4370
+ "error_detail": {
4371
+ "status": 400,
4372
+ "reason": `Invalid JSON in realm configuration file: ${parseError.message} `
4373
+ }
4374
+ } );
4375
+ }
4376
+
4377
+ reject( {
4378
+ "error_message": "Error occurred while parsing Realm file during Tenant creation process.",
4379
+ "error_detail": {
4380
+ "status": 500,
4381
+ "reason": `Error parsing realm configuration file: ${parseError.message}`
4382
+ }
4383
+ } );
4384
+ }
4385
+
4386
+
4387
+ realmData.id = tenantName;
4388
+ realmData.realm = tenantName;
4389
+
4390
+
4391
+ let config1 = {
4392
+
4393
+ method: "post",
4394
+ url: createRealmUrl,
4395
+ headers: {
4396
+ "Content-Type": "application/json",
4397
+ "Authorization": `Bearer ${accessToken}`
4398
+ },
4399
+ data: realmData
4400
+ };
4401
+
4402
+ try {
4403
+
4404
+ let realmCreation = await requestController.httpRequest( config1, false );
4405
+
4406
+ if ( realmCreation.status === 201 ) {
4407
+
4408
+ realmImportSuccessful = true;
4409
+ mainMessage = `Realm '${tenantName}' imported successfully!\n`;
4410
+
4411
+ }
4412
+
4413
+ // --- Authorization Settings Import (if applicable) ---
4414
+ if ( realmImportSuccessful && authzConfigFilePath ) {
4415
+
4416
+ let targetClientIdForAuthz = 'cim';
4417
+
4418
+ console.log( `Proceeding to import authorization settings for client '${targetClientIdForAuthz}' in realm '${tenantName}'.` );
4419
+ let clientUuid = '';
4420
+
4421
+ try {
4422
+
4423
+ // 4. Get the internal UUID of the target client
4424
+ console.log( `Fetching UUID for client '${targetClientIdForAuthz}' in realm '${tenantName}'...` );
4425
+ const getClientUrl = `${keycloakConfig[ "auth-server-url" ]}admin/realms/${tenantName}/clients`;
4426
+
4427
+ let config2 = {
4428
+
4429
+ method: "get",
4430
+ url: getClientUrl,
4431
+ headers: {
4432
+ "Content-Type": "application/json",
4433
+ "Authorization": `Bearer ${accessToken}`
4434
+ },
4435
+ params: {
4436
+ clientId: targetClientIdForAuthz,
4437
+ search: true
4438
+ }
4439
+ };
4440
+
4441
+ const clientSearchResponse = await requestController.httpRequest( config2, false );
4442
+
4443
+ if ( clientSearchResponse.data && clientSearchResponse.data.length > 0 ) {
4444
+
4445
+ // Filter to be sure, as Keycloak might return multiple if clientId is a substring without exact match flag
4446
+ const foundClient = clientSearchResponse?.data?.find( client => client?.clientId === targetClientIdForAuthz );
4447
+
4448
+ if ( foundClient ) {
4449
+
4450
+ clientUuid = foundClient.id;
4451
+ console.log( `Found client UUID: ${clientUuid}` );
4452
+ } else {
4453
+
4454
+ throw new Error( `Client with clientId '${targetClientIdForAuthz}' not found in realm '${tenantName}' after filtering.` );
4455
+ }
4456
+ } else {
4457
+ throw new Error( `Client with clientId '${targetClientIdForAuthz}' not found in realm '${tenantName}'. Response: ${JSON.stringify( clientSearchResponse.data )}` );
4458
+ }
4459
+
4460
+ // 5. Read the authorization settings JSON file
4461
+ console.log( `Reading authorization settings from: ${authzConfigFilePath}` );
4462
+
4463
+ try {
4464
+
4465
+ authzConfigString = await fs.readFileSync( authzConfigFilePath, 'utf-8' );
4466
+
4467
+ } catch ( fileReadError ) {
4468
+
4469
+ if ( fileReadError.code === 'ENOENT' ) {
4470
+
4471
+ reject( {
4472
+ "error_message": `Error occurred while reading Authz file while importing Permissions/Policies in ${targetClientIdForAuthz} during Tenant creation process.`,
4473
+ "error_detail": {
4474
+ "status": 404,
4475
+ "reason": `Authz file not found at provided path: ${authzConfigString}. `
4476
+ }
4477
+ } );
4478
+ }
4479
+
4480
+ reject( {
4481
+ "error_message": `Error occurred while reading Authz file while importing Permissions/Policies in ${targetClientIdForAuthz} during Tenant creation process.`,
4482
+ "error_detail": {
4483
+ "status": 500,
4484
+ "reason": `Error reading authz file: ${fileReadError.message}. {file path: ${authzConfigString}} `
4485
+ }
4486
+ } )
4487
+
4488
+ }
4489
+
4490
+ if ( !authzConfigString || authzConfigString.trim() === '' ) {
4491
+
4492
+ reject( {
4493
+ "error_message": `Error occurred while reading Authz file while importing Permissions/Policies in ${targetClientIdForAuthz} during Tenant creation process.`,
4494
+ "error_detail": {
4495
+ "status": 404,
4496
+ "reason": `Provided authz file to import Permissions/Policies is empty. Please provide valid authz file in JSON format. `
4497
+ }
4498
+ } );
4499
+
4500
+ }
4501
+
4502
+ try {
4503
+
4504
+ authzConfig = JSON.parse( authzConfigString );
4505
+
4506
+ } catch ( parseError ) {
4507
+
4508
+ if ( parseError instanceof SyntaxError ) {
4509
+
4510
+ reject( {
4511
+ "error_message": `Error occurred while parsing Authz file while importing Permissions/Policies in ${targetClientIdForAuthz} during Tenant creation process.`,
4512
+ "error_detail": {
4513
+ "status": 400,
4514
+ "reason": `Invalid JSON in authz configuration file: ${parseError.message} `
4515
+ }
4516
+ } );
4517
+ }
4518
+
4519
+ reject( {
4520
+ "error_message": `Error occurred while parsing Authz file while importing Permissions/Policies in ${targetClientIdForAuthz} during Tenant creation process.`,
4521
+ "error_detail": {
4522
+ "status": 500,
4523
+ "reason": `Error parsing JSON in authz configuration file: ${parseError.message}`
4524
+ }
4525
+ } );
4526
+ }
4527
+
4528
+
4529
+ // 6. Make the API call to Keycloak to import/update authorization settings
4530
+ console.log( `Importing authorization settings for client UUID '${clientUuid}'...` );
4531
+
4532
+ const importAuthzUrl = `${keycloakConfig[ "auth-server-url" ]}admin/realms/${tenantName}/clients/${clientUuid}/authz/resource-server/import`;
4533
+
4534
+ let config3 = {
4535
+
4536
+ method: "post",
4537
+ url: importAuthzUrl,
4538
+ headers: {
4539
+ "Content-Type": "application/json",
4540
+ "Authorization": `Bearer ${accessToken}`
4541
+ },
4542
+ data: authzConfig
4543
+ };
4544
+
4545
+ const authzResponse = await requestController.httpRequest( config3, false );
4546
+
4547
+ try {
4548
+
4549
+ // Keycloak typically returns 204 No Content for successful PUT on authz settings
4550
+ if ( authzResponse.status === 204 || authzResponse.status === 200 || authzResponse.status === 201 ) {
4551
+
4552
+ const authzSuccessMessage = ` Authorization settings for client '${targetClientIdForAuthz}' (UUID: ${clientUuid}) imported successfully into realm '${tenantName}'.`;
4553
+
4554
+ console.log( authzSuccessMessage );
4555
+ mainMessage += `${authzSuccessMessage}`;
4556
+
4557
+ resolve( {
4558
+
4559
+ status: 201,
4560
+ message: mainMessage
4561
+ } );
4562
+
4563
+ } else {
4564
+
4565
+ const authzWarningMessage = ` Authorization settings import for client '${targetClientIdForAuthz}' may have completed with status: ${authzResponse.status}. Response: ${JSON.stringify( authzResponse.data )}`;
4566
+ console.warn( authzWarningMessage );
4567
+ mainMessage += `${authzWarningMessage}`;
4568
+
4569
+ resolve( {
4570
+
4571
+ status: 201,
4572
+ message: mainMessage
4573
+ } );
4574
+ }
4575
+
4576
+ } catch ( er ) {
4577
+
4578
+ console.error( `Error importing authorization settings for client '${targetClientIdForAuthz}':` );
4579
+
4580
+ if ( er?.response ) {
4581
+
4582
+ console.error( 'Keycloak API Error Status (Authz Import):', er?.response?.status );
4583
+ console.error( 'Keycloak API Error Data (Authz Import):', JSON.stringify( er?.response?.data, null, 2 ) );
4584
+
4585
+ } else if ( er?.request ) {
4586
+
4587
+ console.error( 'No response received from Keycloak (Authz Import):', er.request );
4588
+
4589
+
4590
+ } else if ( er?.code === 'ENOENT' && er?.path === authzConfigFilePath ) {
4591
+
4592
+ console.error( 'Authorization settings file not found:', authzConfigFilePath );
4593
+
4594
+ } else {
4595
+
4596
+ console.error( 'Error during authz import request setup or client lookup:', er?.message );
4597
+ }
4598
+
4599
+ let error = await errorService.handleError( er );
4600
+
4601
+ reject( {
4602
+ error_message: "Realm Creation Error: Error while importing permissions/policies in newly created tenant in keycloak from authz file.",
4603
+ error_detail: error
4604
+ } );
4605
+
4606
+ }
4607
+
4608
+ } catch ( er ) {
4609
+
4610
+ let error = await errorService.handleError( er );
4611
+
4612
+ reject( {
4613
+
4614
+ error_message: "Realm Creation Error: Error occurred while fetching list of clients of newly created tenant",
4615
+ error_detail: error
4616
+ } );
4617
+
4618
+ }
4619
+ }
4620
+
4621
+ } catch ( er ) {
4622
+
4623
+ let error = await errorService.handleError( er );
4624
+
4625
+ reject( {
4626
+ error_message: "Realm Creation Error: Error while creating realm in keycloak from realm-file.",
4627
+ error_detail: error
4628
+ } );
4629
+
4630
+ }
4631
+
4632
+ } catch ( error ) {
4633
+
4634
+ if ( error.response ) {
4635
+
4636
+ if ( error.response.data.error_description == "Token is not active" ) {
4637
+ error.response.data.error_description = "Refresh Token Expired: The refresh token has expired. Please log in again.";
4638
+ }
4639
+
4640
+ reject( {
4641
+ status: error.response.status,
4642
+ message: `${error.response.data.error_description}`,
4643
+ } );
4644
+ } else {
4645
+
4646
+ reject( { message: error.message } );
4647
+ }
4648
+
4649
+ }
4650
+ } );
4651
+
4652
+ }
4653
+
4654
+ // !-------------- Multitenancy End -----------------!
4655
+
4279
4656
  //start
4280
4657
  startUserMonitoring = async ( { pollingInterval }, callback ) => {
4281
4658