keycloak-api-manager 3.2.1 → 4.0.1

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.
Files changed (53) hide show
  1. package/.env.example +27 -0
  2. package/Handlers/clientsHandler.js +240 -30
  3. package/Handlers/groupsHandler.js +16 -1
  4. package/Handlers/httpApiHelper.js +87 -0
  5. package/Handlers/realmsHandler.js +26 -4
  6. package/Handlers/usersHandler.js +43 -10
  7. package/README.md +341 -10
  8. package/index.js +159 -29
  9. package/package.json +3 -14
  10. package/test/.mocharc.json +4 -0
  11. package/test/TESTING.md +327 -0
  12. package/test/config/CONFIGURATION.md +170 -0
  13. package/test/config/default.json +36 -0
  14. package/test/config/local.json.example +7 -0
  15. package/test/config/secrets.json.example +7 -0
  16. package/test/diagnostic-protocol-mappers.js +189 -0
  17. package/test/docker-keycloak/DEPLOYMENT_GUIDE.md +262 -0
  18. package/test/docker-keycloak/certs/.gitkeep +7 -0
  19. package/test/docker-keycloak/docker-compose-https.yml +50 -0
  20. package/test/docker-keycloak/docker-compose.yml +59 -0
  21. package/test/docker-keycloak/setup-keycloak.js +501 -0
  22. package/test/enableServerFeatures.js +315 -0
  23. package/test/helpers/config.js +218 -0
  24. package/test/helpers/docker-helpers.js +513 -0
  25. package/test/helpers/setup.js +186 -0
  26. package/test/package.json +18 -0
  27. package/test/setup.js +194 -0
  28. package/test/specs/authenticationManagement.test.js +224 -0
  29. package/test/specs/clientCredentials.test.js +76 -0
  30. package/test/specs/clientScopes.test.js +388 -0
  31. package/test/specs/clients.test.js +791 -0
  32. package/test/specs/components.test.js +151 -0
  33. package/test/specs/debugClientLibrary.test.js +88 -0
  34. package/test/specs/groups.test.js +362 -0
  35. package/test/specs/identityProviders.test.js +292 -0
  36. package/test/specs/realms.test.js +390 -0
  37. package/test/specs/roles.test.js +322 -0
  38. package/test/specs/users.test.js +445 -0
  39. package/test/testConfig.js +69 -0
  40. package/.mocharc.json +0 -7
  41. package/docker-compose.yml +0 -27
  42. package/test/authenticationManagement.test.js +0 -329
  43. package/test/clientScopes.test.js +0 -256
  44. package/test/clients.test.js +0 -284
  45. package/test/components.test.js +0 -122
  46. package/test/config.js +0 -137
  47. package/test/docker-helpers.js +0 -111
  48. package/test/groups.test.js +0 -284
  49. package/test/identityProviders.test.js +0 -197
  50. package/test/mocha.env.js +0 -55
  51. package/test/realms.test.js +0 -349
  52. package/test/roles.test.js +0 -215
  53. package/test/users.test.js +0 -405
package/.env.example ADDED
@@ -0,0 +1,27 @@
1
+ # Keycloak Container Configuration
2
+ # This file is auto-generated by setup-keycloak.js script
3
+ #
4
+ # For manual setup:
5
+ # 1. Copy this file to .env
6
+ # 2. Update the values as needed
7
+ # 3. Run: docker-compose config
8
+ # 4. Run: docker-compose up -d
9
+
10
+ # HTTPS Configuration (true/false)
11
+ KEYCLOAK_HTTPS=false
12
+
13
+ # Certificate directory (only needed if KEYCLOAK_HTTPS=true)
14
+ KEYCLOAK_CERT_PATH=./certs
15
+
16
+ # Scheme (http or https)
17
+ KEYCLOAK_SCHEME=http
18
+
19
+ # Hostname (localhost for local, full hostname for remote)
20
+ KEYCLOAK_HOSTNAME=localhost
21
+
22
+ # HTTPS port (only used if KEYCLOAK_HTTPS=true)
23
+ KEYCLOAK_HTTPS_PORT=8443
24
+
25
+ # Certificate file names (in KEYCLOAK_CERT_PATH)
26
+ KEYCLOAK_CERT_FILE=keycloak.crt
27
+ KEYCLOAK_KEY_FILE=keycloak.key
@@ -11,6 +11,52 @@ exports.setKcAdminClient=function(kcAdminClient){
11
11
  kcAdminClientHandler=kcAdminClient;
12
12
  }
13
13
 
14
+ /**
15
+ * Helper function to make direct HTTP calls to Keycloak Admin API.
16
+ * Used when @keycloak/keycloak-admin-client has bugs or inconsistencies.
17
+ *
18
+ * @param {string} path - API path relative to baseUrl (e.g., '/admin/realms/...')
19
+ * @param {string} method - HTTP method (GET, POST, PUT, DELETE)
20
+ * @param {Object} body - Request body for POST/PUT requests
21
+ * @returns {Promise<any>} Response data from Keycloak
22
+ */
23
+ async function directKeycloakApiCall(path, method = 'GET', body = null) {
24
+ const config = kcAdminClientHandler.baseUrl ?
25
+ { baseUrl: kcAdminClientHandler.baseUrl, realmName: kcAdminClientHandler.realmName } :
26
+ kcAdminClientHandler.getConfig();
27
+
28
+ const url = `${config.baseUrl}${path}`;
29
+ const token = kcAdminClientHandler.accessToken;
30
+
31
+ const options = {
32
+ method,
33
+ headers: {
34
+ 'Authorization': `Bearer ${token}`,
35
+ 'Content-Type': 'application/json',
36
+ 'Accept': 'application/json'
37
+ }
38
+ };
39
+
40
+ if (body && (method === 'POST' || method === 'PUT')) {
41
+ options.body = JSON.stringify(body);
42
+ }
43
+
44
+ const response = await fetch(url, options);
45
+
46
+ if (!response.ok) {
47
+ const errorText = await response.text();
48
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
49
+ }
50
+
51
+ const contentType = response.headers.get('content-type');
52
+ if (contentType && contentType.includes('application/json')) {
53
+ return await response.json();
54
+ }
55
+
56
+ return await response.text();
57
+ }
58
+
59
+
14
60
 
15
61
  /**
16
62
  * ***************************** - CREATE - *******************************
@@ -239,8 +285,19 @@ exports.invalidateSecret=function(filter){
239
285
  * - id: [required] The internal ID of the client (not clientId)
240
286
  *
241
287
  */
242
- exports.getInstallationProviders=function(filter){
243
- return (kcAdminClientHandler.clients.getInstallationProviders(filter));
288
+ exports.getInstallationProviders=async function(filter){
289
+ try {
290
+ const token = kcAdminClientHandler.accessToken;
291
+ const realmName = kcAdminClientHandler.realmName;
292
+ const path = `/admin/realms/${realmName}/clients/${filter.id}/installation/providers`;
293
+ const { makeRequest } = require('./httpApiHelper');
294
+ return await makeRequest(token, 'GET', path);
295
+ } catch (err) {
296
+ if (kcAdminClientHandler.clients && kcAdminClientHandler.clients.getInstallationProviders) {
297
+ return kcAdminClientHandler.clients.getInstallationProviders(filter);
298
+ }
299
+ throw err;
300
+ }
244
301
  }
245
302
 
246
303
 
@@ -254,8 +311,19 @@ exports.getInstallationProviders=function(filter){
254
311
  * - filter: JSON structure that defines the filter parameters:
255
312
  * - id: [required] The ID of the client (resource server) for which to list available policy providers.
256
313
  */
257
- exports.listPolicyProviders=function(filter){
258
- return (kcAdminClientHandler.clients.listPolicyProviders(filter));
314
+ exports.listPolicyProviders=async function(filter){
315
+ try {
316
+ const token = kcAdminClientHandler.accessToken;
317
+ const realmName = kcAdminClientHandler.realmName;
318
+ const path = `/admin/realms/${realmName}/clients/${filter.id}/authz/resource-server/policy-providers`;
319
+ const { makeRequest } = require('./httpApiHelper');
320
+ return await makeRequest(token, 'GET', path);
321
+ } catch (err) {
322
+ if (kcAdminClientHandler.clients && kcAdminClientHandler.clients.listPolicyProviders) {
323
+ return kcAdminClientHandler.clients.listPolicyProviders(filter);
324
+ }
325
+ throw err;
326
+ }
259
327
  }
260
328
 
261
329
 
@@ -415,8 +483,9 @@ exports.listAvailableClientScopeMappings=function(filter){
415
483
  * - name: [required] The role name
416
484
  * - {other RoleRepresentation fields}
417
485
  */
418
- exports.addClientScopeMappings=function(filter){
419
- return (kcAdminClientHandler.clients.addClientScopeMappings(filter));
486
+ exports.addClientScopeMappings=function(filter,roles){
487
+ const payload = roles || (filter && filter.roles);
488
+ return (kcAdminClientHandler.clients.addClientScopeMappings(filter,payload));
420
489
  }
421
490
 
422
491
 
@@ -462,8 +531,9 @@ exports.listCompositeClientScopeMappings=function(filter){
462
531
  * - name: [required] The role name
463
532
  * - {other RoleRepresentation fields}
464
533
  */
465
- exports.delClientScopeMappings=function(filter){
466
- return (kcAdminClientHandler.clients.delClientScopeMappings(filter));
534
+ exports.delClientScopeMappings=function(filter,roles){
535
+ const payload = roles || (filter && filter.roles);
536
+ return (kcAdminClientHandler.clients.delClientScopeMappings(filter,payload));
467
537
  }
468
538
 
469
539
 
@@ -585,12 +655,28 @@ exports.listOfflineSessions=function(filter){
585
655
  * ***************************** - getSessionCount - *******************************
586
656
  * The method retrieves the number of active user sessions for a given client.
587
657
  * This includes online sessions, not offline sessions (those are retrieved with listOfflineSessions).
658
+ *
588
659
  * @parameters:
589
660
  * - filter: JSON structure that defines the filter parameters:
590
661
  * - id: [required] The client ID whose session must be retrieved
662
+ *
663
+ * @returns {Promise<number>} A number representing the count of active sessions
664
+ *
665
+ * @note The return value is automatically normalized to a number. In some Keycloak versions,
666
+ * the API may return an object with a `count` property, but this method handles that internally
667
+ * and always returns the numeric count directly.
591
668
  */
592
669
  exports.getSessionCount=function(filter){
593
- return (kcAdminClientHandler.clients.getSessionCount(filter));
670
+ return kcAdminClientHandler.clients.getSessionCount(filter)
671
+ .then((result) => {
672
+ if (typeof result === 'number') {
673
+ return result;
674
+ }
675
+ if (result && typeof result.count === 'number') {
676
+ return result.count;
677
+ }
678
+ return result;
679
+ });
594
680
  }
595
681
 
596
682
 
@@ -602,12 +688,28 @@ exports.getSessionCount=function(filter){
602
688
  * The method retrieves the number of offline sessions associated with a given client.
603
689
  * Offline sessions represent sessions where the user has a valid offline token, typically used for long-lived access
604
690
  * without requiring active login.
691
+ *
605
692
  * @parameters:
606
693
  * - filter: JSON structure that defines the filter parameters:
607
694
  * - id: [required] The ID of the client for which you want to count offline sessions.
695
+ *
696
+ * @returns {Promise<number>} A number representing the count of offline sessions
697
+ *
698
+ * @note The return value is automatically normalized to a number. In some Keycloak versions,
699
+ * the API may return an object with a `count` property, but this method handles that internally
700
+ * and always returns the numeric count directly.
608
701
  */
609
702
  exports.getOfflineSessionCount=function(filter){
610
- return (kcAdminClientHandler.clients.getOfflineSessionCount(filter));
703
+ return kcAdminClientHandler.clients.getOfflineSessionCount(filter)
704
+ .then((result) => {
705
+ if (typeof result === 'number') {
706
+ return result;
707
+ }
708
+ if (result && typeof result.count === 'number') {
709
+ return result.count;
710
+ }
711
+ return result;
712
+ });
611
713
  }
612
714
 
613
715
 
@@ -693,8 +795,20 @@ exports.generateKey=function(filter){
693
795
  * - id: [required] The ID of the client whose key information should be retrieved
694
796
  * - attr: [optional] The name of the client attribute to get
695
797
  */
696
- exports.getKeyInfo=function(filter){
697
- return (kcAdminClientHandler.clients.getKeyInfo(filter));
798
+ exports.getKeyInfo=async function(filter){
799
+ try {
800
+ const token = kcAdminClientHandler.accessToken;
801
+ const realmName = kcAdminClientHandler.realmName;
802
+ const attr = filter.attr || 'jwt.credential';
803
+ const path = `/admin/realms/${realmName}/clients/${filter.id}/certificates/${attr}`;
804
+ const { makeRequest } = require('./httpApiHelper');
805
+ return await makeRequest(token, 'GET', path);
806
+ } catch (err) {
807
+ if (kcAdminClientHandler.clients && kcAdminClientHandler.clients.getKeyInfo) {
808
+ return kcAdminClientHandler.clients.getKeyInfo(filter);
809
+ }
810
+ throw err;
811
+ }
698
812
  }
699
813
 
700
814
 
@@ -800,13 +914,22 @@ exports.getAuthorizationScope=function(filter){
800
914
  * ***************************** - listAllResourcesByScope - *******************************
801
915
  * The method is used to retrieve all resources associated with a specific authorization scope for a given client.
802
916
  * This allows you to see which resources are governed by a particular scope in the client’s authorization settings.
917
+ *
803
918
  * @parameters:
804
919
  * - filter: JSON structure that defines the filter parameters:
805
920
  * - id: [required] The ID of the client to which the scope belongs
806
921
  * - scopeId [required] The ID of the authorization scope whose associated resources you want to list.
922
+ *
923
+ * @note This method uses a direct Keycloak Admin REST API call instead of
924
+ * @keycloak/keycloak-admin-client due to compatibility issues (missingNormalization)
925
+ * observed in some Keycloak/library combinations.
807
926
  */
808
- exports.listAllResourcesByScope=function(filter){
809
- return (kcAdminClientHandler.clients.listAllResourcesByScope(filter));
927
+ exports.listAllResourcesByScope=async function(filter){
928
+ const config = kcAdminClientHandler.getConfig ? kcAdminClientHandler.getConfig() :
929
+ { realmName: kcAdminClientHandler.realmName };
930
+ const realm = filter.realm || config.realmName;
931
+ const path = `/admin/realms/${realm}/clients/${filter.id}/authz/resource-server/scope/${filter.scopeId}/resources`;
932
+ return await directKeycloakApiCall(path, 'GET');
810
933
  }
811
934
 
812
935
 
@@ -847,13 +970,21 @@ exports.listPermissionScope=function(filter){
847
970
  * The method is used to import a resource into a client.
848
971
  * This is part of Keycloak’s Authorization Services (UMA 2.0) and allows you to programmatically define
849
972
  * resources that a client can protect with policies and permissions.
973
+ *
850
974
  * @parameters:
851
975
  * - filter: JSON structure that defines the filter parameters:
852
976
  * - id: [required] The ID of the client to which the resource should be imported
853
977
  * - resource [required] The resource representation object. This typically includes attributes like name, uris, type, scopes, and other Keycloak resource configuration options.
978
+ *
979
+ * @note This method uses a direct Keycloak Admin REST API call to avoid response
980
+ * inconsistencies seen with @keycloak/keycloak-admin-client in some environments.
854
981
  */
855
- exports.importResource=function(filter,resource){
856
- return (kcAdminClientHandler.clients.importResource(filter,resource));
982
+ exports.importResource=async function(filter,resource){
983
+ const config = kcAdminClientHandler.getConfig ? kcAdminClientHandler.getConfig() :
984
+ { realmName: kcAdminClientHandler.realmName };
985
+ const realm = filter.realm || config.realmName;
986
+ const path = `/admin/realms/${realm}/clients/${filter.id}/authz/resource-server/import`;
987
+ return await directKeycloakApiCall(path, 'POST', resource);
857
988
  }
858
989
 
859
990
 
@@ -1016,8 +1147,20 @@ exports.findPermissions=function(filter){
1016
1147
  * - status: JSON structure that defines the fine grain permission
1017
1148
  * - enabled: [required] Whether fine-grained permissions should be enabled or disabled.
1018
1149
  */
1019
- exports.updateFineGrainPermission=function(filter,status){
1020
- return (kcAdminClientHandler.clients.updateFineGrainPermission(filter,status));
1150
+ exports.updateFineGrainPermission=async function(filter,status){
1151
+ try {
1152
+ const token = kcAdminClientHandler.accessToken;
1153
+ const realmName = kcAdminClientHandler.realmName;
1154
+ const path = `/admin/realms/${realmName}/clients/${filter.id}/management/permissions`;
1155
+ const { makeRequest } = require('./httpApiHelper');
1156
+ return await makeRequest(token, 'PUT', path, status);
1157
+ } catch (err) {
1158
+ // Fallback to library implementation if HTTP call fails
1159
+ if (kcAdminClientHandler.clients && kcAdminClientHandler.clients.updateFineGrainPermission) {
1160
+ return kcAdminClientHandler.clients.updateFineGrainPermission(filter,status);
1161
+ }
1162
+ throw err;
1163
+ }
1021
1164
  }
1022
1165
 
1023
1166
 
@@ -1029,8 +1172,20 @@ exports.updateFineGrainPermission=function(filter,status){
1029
1172
  * - filter: JSON structure that defines the filter parameters:
1030
1173
  * - id: [required] The ID of the client (the resource server) where permissions are defined
1031
1174
  */
1032
- exports.listFineGrainPermissions=function(filter){
1033
- return (kcAdminClientHandler.clients.listFineGrainPermissions(filter));
1175
+ exports.listFineGrainPermissions=async function(filter){
1176
+ try {
1177
+ const token = kcAdminClientHandler.accessToken;
1178
+ const realmName = kcAdminClientHandler.realmName;
1179
+ const path = `/admin/realms/${realmName}/clients/${filter.id}/management/permissions`;
1180
+ const { makeRequest } = require('./httpApiHelper');
1181
+ return await makeRequest(token, 'GET', path);
1182
+ } catch (err) {
1183
+ // Fallback to library implementation if HTTP call fails
1184
+ if (kcAdminClientHandler.clients && kcAdminClientHandler.clients.listFineGrainPermissions) {
1185
+ return kcAdminClientHandler.clients.listFineGrainPermissions(filter);
1186
+ }
1187
+ throw err;
1188
+ }
1034
1189
  }
1035
1190
 
1036
1191
 
@@ -1083,13 +1238,22 @@ exports.getAssociatedResources=function(filter){
1083
1238
  * ***************************** - listScopesByResource - *******************************
1084
1239
  * The method is used to list all authorization scopes associated with a specific resource in a client’s resource server.
1085
1240
  * This allows administrators to understand which scopes are directly linked to a protected resource and therefore which permissions can be applied to it.
1241
+ *
1086
1242
  * @parameters:
1087
1243
  * - filter: JSON structure that defines the filter parameters:
1088
1244
  * - id: [required] The ID of the client (the resource server).
1089
1245
  * - resourceId: [required] The ID of the resource for which to list scopes.
1246
+ *
1247
+ * @note This method uses a direct Keycloak Admin REST API call instead of
1248
+ * @keycloak/keycloak-admin-client due to compatibility issues (missingNormalization)
1249
+ * observed in some Keycloak/library combinations.
1090
1250
  */
1091
- exports.listScopesByResource=function(filter){
1092
- return (kcAdminClientHandler.clients.listScopesByResource(filter));
1251
+ exports.listScopesByResource=async function(filter){
1252
+ const config = kcAdminClientHandler.getConfig ? kcAdminClientHandler.getConfig() :
1253
+ { realmName: kcAdminClientHandler.realmName };
1254
+ const realm = filter.realm || config.realmName;
1255
+ const path = `/admin/realms/${realm}/clients/${filter.id}/authz/resource-server/resource/${filter.resourceId}/scopes`;
1256
+ return await directKeycloakApiCall(path, 'GET');
1093
1257
  }
1094
1258
 
1095
1259
 
@@ -1263,8 +1427,19 @@ exports.evaluateListProtocolMapper=function(filter){
1263
1427
  * - {others}
1264
1428
  * - {others}
1265
1429
  */
1266
- exports.addProtocolMapper=function(filter,protocolMapperRepresentation){
1267
- return (kcAdminClientHandler.clients.addProtocolMapper(filter,protocolMapperRepresentation));
1430
+ exports.addProtocolMapper=async function(filter,protocolMapperRepresentation){
1431
+ try {
1432
+ const token = kcAdminClientHandler.accessToken;
1433
+ const realmName = kcAdminClientHandler.realmName;
1434
+ const path = `/admin/realms/${realmName}/clients/${filter.id}/protocol-mappers/models`;
1435
+ const { makeRequest } = require('./httpApiHelper');
1436
+ return await makeRequest(token, 'POST', path, protocolMapperRepresentation);
1437
+ } catch (err) {
1438
+ if (kcAdminClientHandler.clients && kcAdminClientHandler.clients.addProtocolMapper) {
1439
+ return kcAdminClientHandler.clients.addProtocolMapper(filter,protocolMapperRepresentation);
1440
+ }
1441
+ throw err;
1442
+ }
1268
1443
  }
1269
1444
 
1270
1445
 
@@ -1290,8 +1465,20 @@ exports.addProtocolMapper=function(filter,protocolMapperRepresentation){
1290
1465
  * - {other}
1291
1466
  * - {other}
1292
1467
  */
1293
- exports.updateProtocolMapper=function(filter,protocolMapperRepresentation){
1294
- return (kcAdminClientHandler.clients.updateProtocolMapper(filter,protocolMapperRepresentation));
1468
+ exports.updateProtocolMapper=async function(filter,protocolMapperRepresentation){
1469
+ try {
1470
+ const token = kcAdminClientHandler.accessToken;
1471
+ const realmName = kcAdminClientHandler.realmName;
1472
+ const path = `/admin/realms/${realmName}/clients/${filter.id}/protocol-mappers/models/${filter.mapperId}`;
1473
+ const { makeRequest } = require('./httpApiHelper');
1474
+ return await makeRequest(token, 'PUT', path, protocolMapperRepresentation);
1475
+ } catch (err) {
1476
+ // Fallback to library implementation if HTTP call fails
1477
+ if (kcAdminClientHandler.clients && kcAdminClientHandler.clients.updateProtocolMapper) {
1478
+ return kcAdminClientHandler.clients.updateProtocolMapper(filter,protocolMapperRepresentation);
1479
+ }
1480
+ throw err;
1481
+ }
1295
1482
  }
1296
1483
 
1297
1484
 
@@ -1322,8 +1509,19 @@ exports.updateProtocolMapper=function(filter,protocolMapperRepresentation){
1322
1509
  * - {other}
1323
1510
  * - {other}
1324
1511
  */
1325
- exports.addMultipleProtocolMappers=function(filter,protocolMapperRepresentation){
1326
- return (kcAdminClientHandler.clients.addMultipleProtocolMappers(filter,protocolMapperRepresentation));
1512
+ exports.addMultipleProtocolMappers=async function(filter,protocolMapperRepresentation){
1513
+ try {
1514
+ const token = kcAdminClientHandler.accessToken;
1515
+ const realmName = kcAdminClientHandler.realmName;
1516
+ const path = `/admin/realms/${realmName}/clients/${filter.id}/protocol-mappers/add-models`;
1517
+ const { makeRequest } = require('./httpApiHelper');
1518
+ return await makeRequest(token, 'POST', path, protocolMapperRepresentation);
1519
+ } catch (err) {
1520
+ if (kcAdminClientHandler.clients && kcAdminClientHandler.clients.addMultipleProtocolMappers) {
1521
+ return kcAdminClientHandler.clients.addMultipleProtocolMappers(filter,protocolMapperRepresentation);
1522
+ }
1523
+ throw err;
1524
+ }
1327
1525
  }
1328
1526
 
1329
1527
 
@@ -1405,7 +1603,19 @@ exports.listProtocolMappers=function(filter){
1405
1603
  * - id: [required] The internal client ID of the client
1406
1604
  * - mapperId: [required] The ID of the protocol mapper to delete
1407
1605
  */
1408
- exports.delProtocolMapper=function(filter){
1409
- return (kcAdminClientHandler.clients.delProtocolMapper(filter));
1606
+ exports.delProtocolMapper=async function(filter){
1607
+ try {
1608
+ const token = kcAdminClientHandler.accessToken;
1609
+ const realmName = kcAdminClientHandler.realmName;
1610
+ const path = `/admin/realms/${realmName}/clients/${filter.id}/protocol-mappers/models/${filter.mapperId}`;
1611
+ const { makeRequest } = require('./httpApiHelper');
1612
+ return await makeRequest(token, 'DELETE', path);
1613
+ } catch (err) {
1614
+ // Fallback to library implementation if HTTP call fails
1615
+ if (kcAdminClientHandler.clients && kcAdminClientHandler.clients.delProtocolMapper) {
1616
+ return kcAdminClientHandler.clients.delProtocolMapper(filter);
1617
+ }
1618
+ throw err;
1619
+ }
1410
1620
  }
1411
1621
 
@@ -26,6 +26,13 @@ exports.setKcAdminClient=function(kcAdminClient){
26
26
  * - {other [optional] group description fields}
27
27
  */
28
28
  exports.create=function(groupRepresentation){
29
+ if(groupRepresentation && groupRepresentation.parentId){
30
+ const { parentId, ...childGroupRepresentation } = groupRepresentation;
31
+ return (kcAdminClientHandler.groups.createChildGroup(
32
+ { id: parentId },
33
+ childGroupRepresentation
34
+ ));
35
+ }
29
36
  return (kcAdminClientHandler.groups.create(groupRepresentation));
30
37
  }
31
38
 
@@ -82,7 +89,15 @@ exports.del=function(filter){
82
89
  * - search: [optional] A text string to filter the group count by name
83
90
  */
84
91
  exports.count=function(filter){
85
- return (kcAdminClientHandler.groups.count(filter));
92
+ return (kcAdminClientHandler.groups.count(filter).then((response)=>{
93
+ if(typeof response === 'number'){
94
+ return response;
95
+ }
96
+ if(response && typeof response.count === 'number'){
97
+ return response.count;
98
+ }
99
+ return 0;
100
+ }));
86
101
  }
87
102
 
88
103
 
@@ -0,0 +1,87 @@
1
+ /**
2
+ * HTTP API Helper Module
3
+ *
4
+ * Provides direct HTTP calls to Keycloak Admin API
5
+ * Used when @keycloak/keycloak-admin-client has limitations
6
+ */
7
+
8
+ const http = require('http');
9
+ const https = require('https');
10
+ const { conf } = require('propertiesmanager');
11
+
12
+ /**
13
+ * Make a direct HTTP request to Keycloak API
14
+ * @param {string} token - Bearer token
15
+ * @param {string} method - HTTP method (GET, POST, PUT, DELETE)
16
+ * @param {string} path - API path (e.g., /admin/realms/realm-name/clients/id)
17
+ * @param {object} body - Request body (optional)
18
+ * @returns {Promise<object>} Response data
19
+ */
20
+ async function makeRequest(token, method, path, body = null) {
21
+ const keycloakConfig = (conf && conf.keycloak) || {};
22
+ const baseUrl = keycloakConfig.baseUrl;
23
+
24
+ if (!baseUrl) {
25
+ throw new Error('Keycloak baseUrl not configured');
26
+ }
27
+
28
+ const url = new URL(path, baseUrl);
29
+ const transport = url.protocol === 'https:' ? https : http;
30
+ const payload = body ? JSON.stringify(body) : null;
31
+
32
+ const options = {
33
+ method,
34
+ headers: {
35
+ 'Accept': 'application/json',
36
+ 'Authorization': `Bearer ${token}`,
37
+ ...(payload ? { 'Content-Type': 'application/json' } : {}),
38
+ },
39
+ };
40
+
41
+ return new Promise((resolve, reject) => {
42
+ const req = transport.request(url, options, (res) => {
43
+ let data = '';
44
+
45
+ res.on('data', (chunk) => {
46
+ data += chunk;
47
+ });
48
+
49
+ res.on('end', () => {
50
+ try {
51
+ if (res.statusCode >= 200 && res.statusCode < 300) {
52
+ const result = data ? JSON.parse(data) : null;
53
+ resolve(result);
54
+ } else {
55
+ let errorData = {};
56
+ try {
57
+ errorData = JSON.parse(data);
58
+ } catch {
59
+ errorData = { message: data };
60
+ }
61
+
62
+ const error = new Error(
63
+ errorData.error_description ||
64
+ errorData.message ||
65
+ `HTTP ${res.statusCode}`
66
+ );
67
+ error.statusCode = res.statusCode;
68
+ error.response = errorData;
69
+ reject(error);
70
+ }
71
+ } catch (parseErr) {
72
+ reject(parseErr);
73
+ }
74
+ });
75
+ });
76
+
77
+ req.on('error', reject);
78
+ if (payload) {
79
+ req.write(payload);
80
+ }
81
+ req.end();
82
+ });
83
+ }
84
+
85
+ module.exports = {
86
+ makeRequest,
87
+ };
@@ -362,8 +362,19 @@ exports.clearAdminEvents=function(realmFilter){
362
362
  * - realmFilter: is a JSON object that accepts filter parameters
363
363
  * - realm: [required] The name of the realm for which you want to retrieve the user management permission settings.
364
364
  */
365
- exports.getUsersManagementPermissions=function(realmFilter){
366
- return(kcAdminClientHandler.realms.getUsersManagementPermissions(realmFilter));
365
+ exports.getUsersManagementPermissions=async function(realmFilter){
366
+ try {
367
+ const token = kcAdminClientHandler.accessToken;
368
+ const realmName = realmFilter.realm;
369
+ const path = `/admin/realms/${realmName}/users-management-permissions`;
370
+ const { makeRequest } = require('./httpApiHelper');
371
+ return await makeRequest(token, 'GET', path);
372
+ } catch (err) {
373
+ if (kcAdminClientHandler.realms && kcAdminClientHandler.realms.getUsersManagementPermissions) {
374
+ return kcAdminClientHandler.realms.getUsersManagementPermissions(realmFilter);
375
+ }
376
+ throw err;
377
+ }
367
378
  }
368
379
 
369
380
 
@@ -379,8 +390,19 @@ exports.getUsersManagementPermissions=function(realmFilter){
379
390
  * - true: Activates fine-grained permissions for user management.
380
391
  * - false: Disables fine-grained permissions and falls back to standard admin roles.
381
392
  */
382
- exports.updateUsersManagementPermissions=function(updateParameters){
383
- return(kcAdminClientHandler.realms.updateUsersManagementPermissions(updateParameters));
393
+ exports.updateUsersManagementPermissions=async function(updateParameters){
394
+ try {
395
+ const token = kcAdminClientHandler.accessToken;
396
+ const realmName = updateParameters.realm;
397
+ const path = `/admin/realms/${realmName}/users-management-permissions`;
398
+ const { makeRequest } = require('./httpApiHelper');
399
+ return await makeRequest(token, 'PUT', path, { enabled: updateParameters.enabled });
400
+ } catch (err) {
401
+ if (kcAdminClientHandler.realms && kcAdminClientHandler.realms.updateUsersManagementPermissions) {
402
+ return kcAdminClientHandler.realms.updateUsersManagementPermissions(updateParameters);
403
+ }
404
+ throw err;
405
+ }
384
406
  }
385
407
 
386
408
  /**
@@ -198,7 +198,15 @@ exports.delFromGroup=function(parameters){
198
198
  * - search: [optional] a String containing group name such "cool-group",
199
199
  */
200
200
  exports.countGroups=function(filter){
201
- return (kcAdminClientHandler.users.countGroups(filter));
201
+ return (kcAdminClientHandler.users.countGroups(filter).then((response)=>{
202
+ if(typeof response === 'number'){
203
+ return response;
204
+ }
205
+ if(response && typeof response.count === 'number'){
206
+ return response.count;
207
+ }
208
+ return 0;
209
+ }));
202
210
  }
203
211
 
204
212
 
@@ -426,8 +434,21 @@ exports.listSessions=function(filter){
426
434
  * - id: [required] The ID of the user whose sessions will be listed
427
435
  * - clientId: [optional] The client ID whose sessions are being checked
428
436
  */
429
- exports.listOfflineSessions=function(filter){
430
- return (kcAdminClientHandler.users.listOfflineSessions(filter));
437
+ exports.listOfflineSessions=async function(filter){
438
+ const params = { ...filter };
439
+
440
+ if (params && params.clientId) {
441
+ try {
442
+ const clients = await kcAdminClientHandler.clients.find({ clientId: params.clientId });
443
+ if (Array.isArray(clients) && clients.length && clients[0].id) {
444
+ params.clientId = clients[0].id;
445
+ }
446
+ } catch (error) {
447
+ // Keep original clientId and let Keycloak validate it.
448
+ }
449
+ }
450
+
451
+ return (kcAdminClientHandler.users.listOfflineSessions(params));
431
452
  }
432
453
 
433
454
 
@@ -542,18 +563,30 @@ exports.delFromFederatedIdentity=function(options){
542
563
 
543
564
  /**
544
565
  * ***************************** - getUserStorageCredentialTypes - *******************************
545
- * For more details, see the keycloak-admin-client package in the Keycloak GitHub repository.
566
+ * Retrieves configured user-storage credential types for a specific user.
567
+ *
568
+ * @parameters:
569
+ * - filter is a JSON object that accepts this parameters:
570
+ * - id: [required] The unique ID of the user.
571
+ * - realm: [optional] The realm name.
546
572
  */
547
- exports.getUserStorageCredentialTypes=function(){
548
- return (kcAdminClientHandler.users.getUserStorageCredentialTypes());
573
+ exports.getUserStorageCredentialTypes=function(filter){
574
+ return (kcAdminClientHandler.users.getUserStorageCredentialTypes(filter));
549
575
  }
550
576
 
551
577
  /**
552
- * ***************************** - CREATE - *******************************
553
- * For more details, see the keycloak-admin-client package in the Keycloak GitHub repository.
578
+ * ***************************** - updateCredentialLabel - *******************************
579
+ * Updates the label of a specific credential for a user.
580
+ *
581
+ * @parameters:
582
+ * - filter is a JSON object that accepts this parameters:
583
+ * - id: [required] The unique ID of the user.
584
+ * - credentialId: [required] The unique ID of the credential.
585
+ * - realm: [optional] The realm name.
586
+ * - label: [required] String label to assign to the credential.
554
587
  */
555
- exports.updateCredentialLabel=function(){
556
- return (kcAdminClientHandler.users.updateCredentialLabel());
588
+ exports.updateCredentialLabel=function(filter,label){
589
+ return (kcAdminClientHandler.users.updateCredentialLabel(filter,label));
557
590
  }
558
591
 
559
592