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
@@ -0,0 +1,445 @@
1
+ const path = require('path');
2
+ const http = require('http');
3
+ const https = require('https');
4
+ const { expect } = require('chai');
5
+
6
+ process.env.NODE_ENV = process.env.NODE_ENV || 'test';
7
+ process.env.PROPERTIES_PATH = path.join(__dirname, '..', 'config');
8
+
9
+ const { conf } = require('propertiesmanager');
10
+ const keycloakManager = require('keycloak-api-manager');
11
+ const { TEST_REALM, generateUniqueName } = require('../testConfig');
12
+
13
+ function requestAdmin(baseUrl, token, apiPath, method = 'GET', body) {
14
+ const url = new URL(apiPath, baseUrl);
15
+ const transport = url.protocol === 'https:' ? https : http;
16
+ const payload = body ? JSON.stringify(body) : null;
17
+
18
+ const options = {
19
+ method,
20
+ headers: {
21
+ Accept: 'application/json',
22
+ Authorization: `Bearer ${token}`,
23
+ ...(payload ? { 'Content-Type': 'application/json' } : {}),
24
+ },
25
+ };
26
+
27
+ return new Promise((resolve, reject) => {
28
+ const req = transport.request(url, options, (res) => {
29
+ let data = '';
30
+ res.on('data', (chunk) => {
31
+ data += chunk;
32
+ });
33
+ res.on('end', () => {
34
+ let parsed = data;
35
+ try {
36
+ parsed = data ? JSON.parse(data) : null;
37
+ } catch (err) {
38
+ parsed = data;
39
+ }
40
+ resolve({ status: res.statusCode, body: parsed });
41
+ });
42
+ });
43
+
44
+ req.on('error', reject);
45
+ if (payload) {
46
+ req.write(payload);
47
+ }
48
+ req.end();
49
+ });
50
+ }
51
+
52
+ async function getAdminToken(baseUrl, username, password) {
53
+ const url = new URL('/realms/master/protocol/openid-connect/token', baseUrl);
54
+ const transport = url.protocol === 'https:' ? https : http;
55
+
56
+ const params = new URLSearchParams({
57
+ grant_type: 'password',
58
+ client_id: 'admin-cli',
59
+ username,
60
+ password,
61
+ });
62
+
63
+ const options = {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/x-www-form-urlencoded',
67
+ },
68
+ };
69
+
70
+ return new Promise((resolve, reject) => {
71
+ const req = transport.request(url, options, (res) => {
72
+ let data = '';
73
+ res.on('data', (chunk) => {
74
+ data += chunk;
75
+ });
76
+ res.on('end', () => {
77
+ if (res.statusCode === 200) {
78
+ const parsed = JSON.parse(data);
79
+ resolve(parsed.access_token);
80
+ } else {
81
+ reject(new Error(`HTTP ${res.statusCode}: ${data}`));
82
+ }
83
+ });
84
+ });
85
+
86
+ req.on('error', reject);
87
+ req.write(params.toString());
88
+ req.end();
89
+ });
90
+ }
91
+
92
+ function shouldSkipFeature(err) {
93
+ if (!err || !err.message) {
94
+ return false;
95
+ }
96
+ const text = err.message.toLowerCase();
97
+ return (
98
+ text.includes('feature not enabled') ||
99
+ text.includes('not enabled') ||
100
+ text.includes('not supported') ||
101
+ text.includes('http 404') ||
102
+ text.includes('unknown_error')
103
+ );
104
+ }
105
+
106
+ describe('Users Handler', function () {
107
+ this.timeout(60000);
108
+
109
+ const keycloakConfig = (conf && conf.keycloak) || {};
110
+ const testUserName = generateUniqueName('users-test');
111
+ const testEmail = `${testUserName}@example.com`;
112
+ const testPassword = 'UsersTest!123';
113
+ const testGroupName = generateUniqueName('users-group');
114
+ const testRealmRoleName = generateUniqueName('users-realm-role');
115
+ const testClientId = generateUniqueName('users-client');
116
+ const testClientRoleName = generateUniqueName('users-client-role');
117
+
118
+ let adminToken = null;
119
+ let userId = null;
120
+ let groupId = null;
121
+ let realmRole = null;
122
+ let client = null;
123
+ let clientRole = null;
124
+
125
+ before(async function () {
126
+ await keycloakManager.configure({
127
+ baseUrl: keycloakConfig.baseUrl,
128
+ realmName: keycloakConfig.realmName,
129
+ clientId: keycloakConfig.clientId,
130
+ clientSecret: keycloakConfig.clientSecret,
131
+ username: keycloakConfig.username,
132
+ password: keycloakConfig.password,
133
+ grantType: keycloakConfig.grantType,
134
+ tokenLifeSpan: keycloakConfig.tokenLifeSpan,
135
+ scope: keycloakConfig.scope,
136
+ });
137
+
138
+ adminToken = await getAdminToken(
139
+ keycloakConfig.baseUrl,
140
+ keycloakConfig.username,
141
+ keycloakConfig.password
142
+ );
143
+
144
+ // Use shared test realm (created by enableServerFeatures)
145
+ keycloakManager.setConfig({ realmName: TEST_REALM });
146
+
147
+ const createdUser = await keycloakManager.users.create({
148
+ username: testUserName,
149
+ email: testEmail,
150
+ enabled: true,
151
+ firstName: 'Users',
152
+ lastName: 'Test',
153
+ });
154
+ userId = createdUser.id;
155
+
156
+ await keycloakManager.users.resetPassword({
157
+ id: userId,
158
+ credential: {
159
+ type: 'password',
160
+ value: testPassword,
161
+ temporary: false,
162
+ },
163
+ });
164
+
165
+ const createdGroup = await keycloakManager.groups.create({
166
+ name: testGroupName,
167
+ });
168
+ groupId = createdGroup.id;
169
+
170
+ await keycloakManager.roles.create({ name: testRealmRoleName });
171
+ realmRole = await keycloakManager.roles.findOneByName({ name: testRealmRoleName });
172
+
173
+ await keycloakManager.clients.create({
174
+ clientId: testClientId,
175
+ name: testClientId,
176
+ enabled: true,
177
+ publicClient: false,
178
+ protocol: 'openid-connect',
179
+ directAccessGrantsEnabled: true,
180
+ standardFlowEnabled: true,
181
+ });
182
+
183
+ const clients = await keycloakManager.clients.find({ clientId: testClientId });
184
+ client = clients[0];
185
+
186
+ await keycloakManager.clients.createRole({
187
+ id: client.id,
188
+ name: testClientRoleName,
189
+ description: 'Users test client role',
190
+ });
191
+
192
+ clientRole = await keycloakManager.clients.findRole({
193
+ id: client.id,
194
+ roleName: testClientRoleName,
195
+ });
196
+ });
197
+
198
+ after(async function () {
199
+ try {
200
+ keycloakManager.setConfig({ realmName: TEST_REALM });
201
+ } catch (err) {
202
+ // Best-effort cleanup.
203
+ }
204
+
205
+ try {
206
+ if (userId) {
207
+ await keycloakManager.users.del({ id: userId });
208
+ }
209
+ } catch (err) {
210
+ // Best-effort cleanup.
211
+ }
212
+
213
+ try {
214
+ if (client) {
215
+ await keycloakManager.clients.del({ id: client.id });
216
+ }
217
+ } catch (err) {
218
+ // Best-effort cleanup.
219
+ }
220
+
221
+ try {
222
+ if (realmRole) {
223
+ await keycloakManager.roles.delByName({ name: realmRole.name });
224
+ }
225
+ } catch (err) {
226
+ // Best-effort cleanup.
227
+ }
228
+
229
+ try {
230
+ if (groupId) {
231
+ await keycloakManager.groups.del({ id: groupId });
232
+ }
233
+ } catch (err) {
234
+ // Best-effort cleanup.
235
+ }
236
+
237
+ // Don't delete shared test realm
238
+
239
+ if (typeof keycloakManager.stop === 'function') {
240
+ keycloakManager.stop();
241
+ }
242
+ });
243
+
244
+ it('should find, findOne, count and update users', async function () {
245
+ const users = await keycloakManager.users.find({ username: testUserName });
246
+ expect(users).to.be.an('array');
247
+ expect(users.some((item) => item.id === userId)).to.equal(true);
248
+
249
+ const one = await keycloakManager.users.findOne({ id: userId });
250
+ expect(one).to.be.an('object');
251
+ expect(one.username).to.equal(testUserName);
252
+
253
+ const count = await keycloakManager.users.count({ username: testUserName });
254
+ expect(count).to.be.a('number');
255
+ expect(count).to.be.greaterThan(0);
256
+
257
+ const newFirstName = `UsersUpdated-${Date.now()}`;
258
+ await keycloakManager.users.update(
259
+ { id: userId },
260
+ { firstName: newFirstName }
261
+ );
262
+
263
+ const updated = await keycloakManager.users.findOne({ id: userId });
264
+ expect(updated.firstName).to.equal(newFirstName);
265
+
266
+ const direct = await requestAdmin(
267
+ keycloakConfig.baseUrl,
268
+ adminToken,
269
+ `/admin/realms/${TEST_REALM}/users/${userId}`
270
+ );
271
+ expect(direct.status).to.equal(200);
272
+ expect(direct.body.firstName).to.equal(newFirstName);
273
+ });
274
+
275
+ it('should manage password and credentials', async function () {
276
+ const credentials = await keycloakManager.users.getCredentials({ id: userId });
277
+ expect(credentials).to.be.an('array');
278
+
279
+ const storageTypes = await keycloakManager.users.getUserStorageCredentialTypes({ id: userId });
280
+ expect(storageTypes).to.be.an('array');
281
+
282
+ if (!credentials.length) {
283
+ this.skip();
284
+ return;
285
+ }
286
+
287
+ const credential = credentials[0];
288
+ await keycloakManager.users.updateCredentialLabel(
289
+ { id: userId, credentialId: credential.id },
290
+ `label-${Date.now()}`
291
+ );
292
+ });
293
+
294
+ it('should manage group membership', async function () {
295
+ await keycloakManager.users.addToGroup({ id: userId, groupId });
296
+
297
+ const groups = await keycloakManager.users.listGroups({ id: userId });
298
+ expect(groups).to.be.an('array');
299
+ expect(groups.some((group) => group.id === groupId)).to.equal(true);
300
+
301
+ const count = await keycloakManager.users.countGroups({ id: userId });
302
+ expect(count).to.be.a('number');
303
+ expect(count).to.be.greaterThan(0);
304
+
305
+ const direct = await requestAdmin(
306
+ keycloakConfig.baseUrl,
307
+ adminToken,
308
+ `/admin/realms/${TEST_REALM}/users/${userId}/groups`
309
+ );
310
+ expect(direct.status).to.equal(200);
311
+ expect(Array.isArray(direct.body)).to.equal(true);
312
+
313
+ await keycloakManager.users.delFromGroup({ id: userId, groupId });
314
+ const groupsAfter = await keycloakManager.users.listGroups({ id: userId });
315
+ expect(groupsAfter.some((group) => group.id === groupId)).to.equal(false);
316
+ });
317
+
318
+ it('should manage realm role mappings', async function () {
319
+ await keycloakManager.users.addRealmRoleMappings({
320
+ id: userId,
321
+ roles: [{ id: realmRole.id, name: realmRole.name }],
322
+ });
323
+
324
+ const directRealmRoles = await keycloakManager.users.listRealmRoleMappings({ id: userId });
325
+ expect(directRealmRoles.some((role) => role.id === realmRole.id)).to.equal(true);
326
+
327
+ const compositeRealmRoles = await keycloakManager.users.listCompositeRealmRoleMappings({ id: userId });
328
+ expect(compositeRealmRoles).to.be.an('array');
329
+
330
+ const roleMappings = await keycloakManager.users.listRoleMappings({ id: userId });
331
+ expect(roleMappings).to.be.an('object');
332
+
333
+ const available = await keycloakManager.users.listAvailableRealmRoleMappings({ id: userId });
334
+ expect(available).to.be.an('array');
335
+
336
+ await keycloakManager.users.delRealmRoleMappings({
337
+ id: userId,
338
+ roles: [{ id: realmRole.id, name: realmRole.name }],
339
+ });
340
+
341
+ const afterDelete = await keycloakManager.users.listRealmRoleMappings({ id: userId });
342
+ expect(afterDelete.some((role) => role.id === realmRole.id)).to.equal(false);
343
+ });
344
+
345
+ it('should manage client role mappings', async function () {
346
+ await keycloakManager.users.addClientRoleMappings({
347
+ id: userId,
348
+ clientUniqueId: client.id,
349
+ roles: [{ id: clientRole.id, name: clientRole.name }],
350
+ });
351
+
352
+ const roles = await keycloakManager.users.listClientRoleMappings({
353
+ id: userId,
354
+ clientUniqueId: client.id,
355
+ });
356
+ expect(roles).to.be.an('array');
357
+ expect(roles.some((role) => role.id === clientRole.id)).to.equal(true);
358
+
359
+ const composite = await keycloakManager.users.listCompositeClientRoleMappings({
360
+ id: userId,
361
+ clientUniqueId: client.id,
362
+ });
363
+ expect(composite).to.be.an('array');
364
+
365
+ const available = await keycloakManager.users.listAvailableClientRoleMappings({
366
+ id: userId,
367
+ clientUniqueId: client.id,
368
+ });
369
+ expect(available).to.be.an('array');
370
+
371
+ await keycloakManager.users.delClientRoleMappings({
372
+ id: userId,
373
+ clientUniqueId: client.id,
374
+ roles: [{ id: clientRole.id, name: clientRole.name }],
375
+ });
376
+
377
+ const rolesAfter = await keycloakManager.users.listClientRoleMappings({
378
+ id: userId,
379
+ clientUniqueId: client.id,
380
+ });
381
+ expect(rolesAfter.some((role) => role.id === clientRole.id)).to.equal(false);
382
+ });
383
+
384
+ it('should list sessions and support logout', async function () {
385
+ const sessions = await keycloakManager.users.listSessions({ id: userId });
386
+ expect(sessions).to.be.an('array');
387
+
388
+ try {
389
+ const offlineSessions = await keycloakManager.users.listOfflineSessions({
390
+ id: userId,
391
+ clientId: testClientId,
392
+ });
393
+ expect(offlineSessions).to.be.an('array');
394
+ } catch (err) {
395
+ if (shouldSkipFeature(err)) {
396
+ this.skip();
397
+ return;
398
+ }
399
+ throw err;
400
+ }
401
+
402
+ await keycloakManager.users.logout({ id: userId });
403
+ });
404
+
405
+ it('should support impersonation when enabled', async function () {
406
+ try {
407
+ const response = await keycloakManager.users.impersonation({ id: userId });
408
+ expect(response).to.be.an('object');
409
+ } catch (err) {
410
+ if (shouldSkipFeature(err)) {
411
+ this.skip();
412
+ return;
413
+ }
414
+ throw err;
415
+ }
416
+ });
417
+
418
+ it('should list federated identities and handle optional provider linkage', async function () {
419
+ const identities = await keycloakManager.users.listFederatedIdentities({ id: userId });
420
+ expect(identities).to.be.an('array');
421
+
422
+ try {
423
+ await keycloakManager.users.addToFederatedIdentity({
424
+ id: userId,
425
+ federatedIdentityId: 'google',
426
+ federatedIdentity: {
427
+ identityProvider: 'google',
428
+ userId: `federated-${Date.now()}`,
429
+ userName: `federated-${Date.now()}`,
430
+ },
431
+ });
432
+
433
+ await keycloakManager.users.delFromFederatedIdentity({
434
+ id: userId,
435
+ federatedIdentityId: 'google',
436
+ });
437
+ } catch (err) {
438
+ if (shouldSkipFeature(err)) {
439
+ this.skip();
440
+ return;
441
+ }
442
+ throw err;
443
+ }
444
+ });
445
+ });
@@ -0,0 +1,69 @@
1
+ const path = require('path');
2
+
3
+ // Set up environment for propertiesmanager
4
+ process.env.NODE_ENV = process.env.NODE_ENV || 'test';
5
+ process.env.PROPERTIES_PATH = path.join(__dirname, 'config');
6
+
7
+ const { conf } = require('propertiesmanager');
8
+
9
+ /**
10
+ * Shared Test Configuration Module
11
+ *
12
+ * This module centralizes all test configuration using propertiesmanager.
13
+ * It loads configuration from test/config/ directory with the following merge order:
14
+ *
15
+ * 1. default.json - Base configuration for all environments
16
+ * 2. secrets.json - Sensitive credentials (gitignored)
17
+ * 3. local.json - Developer-specific overrides (gitignored, optional)
18
+ *
19
+ * Configuration Structure:
20
+ * - All files use environment wrappers: { "test": { ... } }
21
+ * - NODE_ENV determines which section to load (default: "test")
22
+ * - propertiesmanager automatically merges configs based on NODE_ENV
23
+ *
24
+ * Exports:
25
+ * - KEYCLOAK_CONFIG: Admin connection settings (baseUrl, username, password, etc.)
26
+ * - TEST_REALM: Name of the shared test realm
27
+ * - TEST_CLIENT_*: Client configuration for tests
28
+ * - TEST_USER_*: Test user credentials and details
29
+ * - TEST_ROLES: Array of role names to create
30
+ * - TEST_GROUP_NAME: Name of test group
31
+ * - TEST_CLIENT_SCOPE: Name of test client scope
32
+ * - generateUniqueName: Helper to create unique resource names
33
+ *
34
+ * Usage in tests:
35
+ * const { KEYCLOAK_CONFIG, TEST_REALM, TEST_USER_USERNAME } = require('./testConfig');
36
+ */
37
+
38
+ const realmConfig = conf?.realm || {};
39
+
40
+ module.exports = {
41
+ // Keycloak Admin Config (for admin connections)
42
+ KEYCLOAK_CONFIG: conf?.keycloak || {},
43
+
44
+ // Test Realm Configuration
45
+ TEST_REALM: realmConfig.name || 'keycloak-api-manager-test-realm',
46
+
47
+ // Test Client Configuration
48
+ TEST_CLIENT_ID: realmConfig.client?.clientId || 'test-client',
49
+ TEST_CLIENT_SECRET: realmConfig.client?.clientSecret || 'test-client-secret',
50
+
51
+ // Test User Configuration
52
+ TEST_USER_USERNAME: realmConfig.user?.username || 'test-user',
53
+ TEST_USER_PASSWORD: realmConfig.user?.password || 'test-password',
54
+ TEST_USER_EMAIL: realmConfig.user?.email || 'test-user@test.local',
55
+ TEST_USER_FIRSTNAME: realmConfig.user?.firstName || 'Test',
56
+ TEST_USER_LASTNAME: realmConfig.user?.lastName || 'User',
57
+
58
+ // Test Roles
59
+ TEST_ROLES: realmConfig.roles || ['test-role-1', 'test-role-2', 'test-admin-role'],
60
+
61
+ // Test Group
62
+ TEST_GROUP_NAME: realmConfig.group?.name || 'test-group',
63
+
64
+ // Test Client Scope
65
+ TEST_CLIENT_SCOPE: realmConfig.clientScope?.name || 'test-scope',
66
+
67
+ // Helper to generate unique names when needed
68
+ generateUniqueName: (prefix) => `${prefix}-${Date.now()}`
69
+ };
package/.mocharc.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "require": ["test/mocha.env.js"],
3
- "timeout": 15000,
4
- "spec": "test/**/*.test.js",
5
- "reporter": "spec",
6
- "parallel": false
7
- }
@@ -1,27 +0,0 @@
1
- version: '3.8'
2
-
3
- services:
4
- keycloak:
5
- image: keycloak/keycloak:latest
6
- container_name: keycloak-test
7
- ports:
8
- - "8080:8080"
9
- environment:
10
- KEYCLOAK_ADMIN: admin
11
- KEYCLOAK_ADMIN_PASSWORD: admin
12
- KC_DB: dev-mem
13
- KC_METRICS_ENABLED: 'false'
14
- KC_HEALTH_ENABLED: 'true'
15
- command:
16
- - start-dev
17
- healthcheck:
18
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/ready"]
19
- interval: 5s
20
- timeout: 5s
21
- retries: 12
22
- networks:
23
- - keycloak-network
24
-
25
- networks:
26
- keycloak-network:
27
- driver: bridge