keycloak-api-manager 3.2.0 → 4.0.0

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 (45) hide show
  1. package/.env.example +27 -0
  2. package/Handlers/authenticationManagementHandler.js +1 -1
  3. package/Handlers/clientScopesHandler.js +5 -5
  4. package/Handlers/clientsHandler.js +241 -31
  5. package/Handlers/componentsHandler.js +2 -2
  6. package/Handlers/groupsHandler.js +18 -3
  7. package/Handlers/httpApiHelper.js +87 -0
  8. package/Handlers/identityProvidersHandler.js +3 -3
  9. package/Handlers/realmsHandler.js +26 -4
  10. package/Handlers/usersHandler.js +43 -10
  11. package/README.md +361 -26
  12. package/index.js +149 -29
  13. package/index.mjs +21 -0
  14. package/package.json +3 -2
  15. package/test/.mocharc.json +4 -0
  16. package/test/TESTING.md +327 -0
  17. package/test/config/CONFIGURATION.md +170 -0
  18. package/test/config/default.json +36 -0
  19. package/test/config/local.json.example +7 -0
  20. package/test/config/secrets.json.example +7 -0
  21. package/test/diagnostic-protocol-mappers.js +189 -0
  22. package/test/docker-keycloak/DEPLOYMENT_GUIDE.md +262 -0
  23. package/test/docker-keycloak/certs/.gitkeep +7 -0
  24. package/test/docker-keycloak/docker-compose-https.yml +50 -0
  25. package/test/docker-keycloak/docker-compose.yml +59 -0
  26. package/test/docker-keycloak/setup-keycloak.js +501 -0
  27. package/test/enableServerFeatures.js +315 -0
  28. package/test/helpers/config.js +218 -0
  29. package/test/helpers/docker-helpers.js +513 -0
  30. package/test/helpers/setup.js +186 -0
  31. package/test/package.json +18 -0
  32. package/test/setup.js +194 -0
  33. package/test/specs/authenticationManagement.test.js +224 -0
  34. package/test/specs/clientScopes.test.js +388 -0
  35. package/test/specs/clients.test.js +791 -0
  36. package/test/specs/components.test.js +151 -0
  37. package/test/specs/debugClientLibrary.test.js +88 -0
  38. package/test/specs/groups.test.js +362 -0
  39. package/test/specs/identityProviders.test.js +292 -0
  40. package/test/specs/realms.test.js +390 -0
  41. package/test/specs/roles.test.js +322 -0
  42. package/test/specs/users.test.js +445 -0
  43. package/test/testConfig.js +69 -0
  44. package/.idea/vcs.xml +0 -6
  45. package/.idea/workspace.xml +0 -94
@@ -0,0 +1,151 @@
1
+ const path = require('path');
2
+ const { expect } = require('chai');
3
+
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
+ const keycloakManager = require('keycloak-api-manager');
9
+
10
+ function shouldSkipFeature(err) {
11
+ if (!err || !err.message) {
12
+ return false;
13
+ }
14
+ const text = err.message.toLowerCase();
15
+ return (
16
+ text.includes('not supported') ||
17
+ text.includes('invalid') ||
18
+ text.includes('unknown_error') ||
19
+ text.includes('http 4')
20
+ );
21
+ }
22
+
23
+ describe('Components Handler', function () {
24
+ this.timeout(60000);
25
+
26
+ const keycloakConfig = (conf && conf.keycloak) || {};
27
+ const testRealm = `components-realm-${Date.now()}`;
28
+
29
+ let sampleComponent = null;
30
+ let createdComponentId = null;
31
+
32
+ before(async function () {
33
+ await keycloakManager.configure({
34
+ baseUrl: keycloakConfig.baseUrl,
35
+ realmName: keycloakConfig.realmName,
36
+ clientId: keycloakConfig.clientId,
37
+ clientSecret: keycloakConfig.clientSecret,
38
+ username: keycloakConfig.username,
39
+ password: keycloakConfig.password,
40
+ grantType: keycloakConfig.grantType,
41
+ tokenLifeSpan: keycloakConfig.tokenLifeSpan,
42
+ scope: keycloakConfig.scope,
43
+ });
44
+
45
+ await keycloakManager.realms.create({ realm: testRealm, enabled: true });
46
+ keycloakManager.setConfig({ realmName: testRealm });
47
+ });
48
+
49
+ after(async function () {
50
+ try {
51
+ keycloakManager.setConfig({ realmName: testRealm });
52
+ } catch (err) {
53
+ // best effort
54
+ }
55
+
56
+ try {
57
+ if (createdComponentId) {
58
+ await keycloakManager.components.del({ id: createdComponentId });
59
+ }
60
+ } catch (err) {
61
+ // best effort
62
+ }
63
+
64
+ try {
65
+ await keycloakManager.realms.del({ realm: testRealm });
66
+ } catch (err) {
67
+ // best effort
68
+ }
69
+
70
+ if (typeof keycloakManager.stop === 'function') {
71
+ keycloakManager.stop();
72
+ }
73
+ });
74
+
75
+ it('should list components and find one', async function () {
76
+ const components = await keycloakManager.components.find({});
77
+ expect(components).to.be.an('array');
78
+
79
+ if (!components.length) {
80
+ this.skip();
81
+ return;
82
+ }
83
+
84
+ sampleComponent = components[0];
85
+ const found = await keycloakManager.components.findOne({ id: sampleComponent.id });
86
+ expect(found).to.be.an('object');
87
+ expect(found.id).to.equal(sampleComponent.id);
88
+ });
89
+
90
+ it('should list sub-components when available', async function () {
91
+ if (!sampleComponent) {
92
+ const components = await keycloakManager.components.find({});
93
+ if (!components.length) {
94
+ this.skip();
95
+ return;
96
+ }
97
+ sampleComponent = components[0];
98
+ }
99
+
100
+ const sub = await keycloakManager.components.listSubComponents({
101
+ id: sampleComponent.id,
102
+ type: sampleComponent.providerType || 'org.keycloak.component.ComponentFactory',
103
+ });
104
+ expect(sub).to.be.an('array');
105
+ });
106
+
107
+ it('should create, update and delete a component when provider allows it', async function () {
108
+ const components = await keycloakManager.components.find({});
109
+ if (!components.length) {
110
+ this.skip();
111
+ return;
112
+ }
113
+
114
+ const base = components.find((item) => item.providerId && item.providerType) || components[0];
115
+
116
+ const payload = {
117
+ name: `component-copy-${Date.now()}`,
118
+ providerId: base.providerId,
119
+ providerType: base.providerType,
120
+ parentId: base.parentId,
121
+ config: base.config || {},
122
+ };
123
+
124
+ try {
125
+ const created = await keycloakManager.components.create(payload);
126
+ createdComponentId = created.id;
127
+ expect(createdComponentId).to.exist;
128
+
129
+ await keycloakManager.components.update(
130
+ { id: createdComponentId },
131
+ {
132
+ ...payload,
133
+ name: `${payload.name}-updated`,
134
+ }
135
+ );
136
+
137
+ const updated = await keycloakManager.components.findOne({ id: createdComponentId });
138
+ expect(updated).to.be.an('object');
139
+ expect(updated.name).to.equal(`${payload.name}-updated`);
140
+
141
+ await keycloakManager.components.del({ id: createdComponentId });
142
+ createdComponentId = null;
143
+ } catch (err) {
144
+ if (shouldSkipFeature(err)) {
145
+ this.skip();
146
+ return;
147
+ }
148
+ throw err;
149
+ }
150
+ });
151
+ });
@@ -0,0 +1,88 @@
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 } = require('../testConfig');
12
+
13
+ describe('Protocol Mappers - Debug Client Library', function () {
14
+ this.timeout(10000);
15
+
16
+ const keycloakConfig = (conf && conf.keycloak) || {};
17
+ let testClient = null;
18
+
19
+ before(async function () {
20
+ keycloakManager.setConfig({ realmName: TEST_REALM });
21
+
22
+ const clients = await keycloakManager.clients.find({ clientId: 'test-client' });
23
+ if (clients && clients.length > 0) {
24
+ testClient = clients[0];
25
+ console.log(`\nUsing client: ${testClient.clientId} (${testClient.id})`);
26
+ } else {
27
+ this.skip();
28
+ }
29
+ });
30
+
31
+ it('should log keycloak-admin-client requests', async function () {
32
+ if (!testClient) {
33
+ this.skip();
34
+ return;
35
+ }
36
+
37
+ // Hook into Node's http module to log requests
38
+ const originalRequest = http.request;
39
+ let lastRequest = null;
40
+
41
+ http.request = function(...args) {
42
+ const urlOrOptions = args[0];
43
+ lastRequest = {
44
+ method: args[1]?.method || (typeof args[0] === 'object' ? args[0].method : undefined),
45
+ url: typeof args[0] === 'string' ? args[0] : args[0]?.href,
46
+ options: typeof args[0] === 'object' ? args[0] : args[1],
47
+ };
48
+ console.log('\n📝 HTTP Request Logged:');
49
+ console.log('Method:', lastRequest.method);
50
+ console.log('URL:', lastRequest.url);
51
+ if (lastRequest.options) {
52
+ console.log('Headers:', JSON.stringify(lastRequest.options.headers, null, 2));
53
+ }
54
+ return originalRequest.apply(this, args);
55
+ };
56
+
57
+ try {
58
+ console.log('\n=== Attempting protocol mapper creation via library ===');
59
+
60
+ await keycloakManager.clients.addProtocolMapper(
61
+ { id: testClient.id },
62
+ {
63
+ name: `debug-mapper-${Date.now()}`,
64
+ protocol: 'openid-connect',
65
+ protocolMapper: 'oidc-usermodel-attribute-mapper',
66
+ consentRequired: false,
67
+ config: {
68
+ 'user.attribute': 'email',
69
+ 'claim.name': 'email_debug',
70
+ 'jsonType.label': 'String',
71
+ 'id.token.claim': 'true',
72
+ 'access.token.claim': 'true',
73
+ },
74
+ }
75
+ );
76
+
77
+ console.log('✅ SUCCESS');
78
+ } catch (err) {
79
+ console.log('❌ ERROR:', err.message);
80
+ if (lastRequest) {
81
+ console.log('\nLast request details:', JSON.stringify(lastRequest, null, 2));
82
+ }
83
+ } finally {
84
+ // Restore original
85
+ http.request = originalRequest;
86
+ }
87
+ });
88
+ });
@@ -0,0 +1,362 @@
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
+ describe('Groups Handler', function () {
93
+ this.timeout(60000);
94
+
95
+ const keycloakConfig = (conf && conf.keycloak) || {};
96
+ const parentGroupName = generateUniqueName('groups-parent');
97
+ const childGroupName = generateUniqueName('groups-child');
98
+ const testUserName = generateUniqueName('groups-user');
99
+ const testRealmRoleName = generateUniqueName('groups-realm-role');
100
+ const testClientId = generateUniqueName('groups-client');
101
+ const testClientRoleName = generateUniqueName('groups-client-role');
102
+
103
+ let adminToken = null;
104
+ let parentGroupId = null;
105
+ let childGroupId = null;
106
+ let userId = null;
107
+ let realmRole = null;
108
+ let client = null;
109
+ let clientRole = null;
110
+
111
+ before(async function () {
112
+ await keycloakManager.configure({
113
+ baseUrl: keycloakConfig.baseUrl,
114
+ realmName: keycloakConfig.realmName,
115
+ clientId: keycloakConfig.clientId,
116
+ clientSecret: keycloakConfig.clientSecret,
117
+ username: keycloakConfig.username,
118
+ password: keycloakConfig.password,
119
+ grantType: keycloakConfig.grantType,
120
+ tokenLifeSpan: keycloakConfig.tokenLifeSpan,
121
+ scope: keycloakConfig.scope,
122
+ });
123
+
124
+ adminToken = await getAdminToken(
125
+ keycloakConfig.baseUrl,
126
+ keycloakConfig.username,
127
+ keycloakConfig.password
128
+ );
129
+
130
+ // Use shared test realm (created by enableServerFeatures)
131
+ keycloakManager.setConfig({ realmName: TEST_REALM });
132
+
133
+ const createdParent = await keycloakManager.groups.create({ name: parentGroupName });
134
+ parentGroupId = createdParent.id;
135
+
136
+ const createdChild = await keycloakManager.groups.create({
137
+ name: childGroupName,
138
+ parentId: parentGroupId,
139
+ });
140
+ childGroupId = createdChild.id;
141
+
142
+ const createdUser = await keycloakManager.users.create({
143
+ username: testUserName,
144
+ enabled: true,
145
+ email: `${testUserName}@example.com`,
146
+ });
147
+ userId = createdUser.id;
148
+
149
+ await keycloakManager.users.addToGroup({ id: userId, groupId: parentGroupId });
150
+
151
+ await keycloakManager.roles.create({ name: testRealmRoleName });
152
+ realmRole = await keycloakManager.roles.findOneByName({ name: testRealmRoleName });
153
+
154
+ await keycloakManager.clients.create({
155
+ clientId: testClientId,
156
+ name: testClientId,
157
+ enabled: true,
158
+ publicClient: false,
159
+ protocol: 'openid-connect',
160
+ directAccessGrantsEnabled: true,
161
+ standardFlowEnabled: true,
162
+ });
163
+
164
+ const clients = await keycloakManager.clients.find({ clientId: testClientId });
165
+ client = clients[0];
166
+
167
+ await keycloakManager.clients.createRole({
168
+ id: client.id,
169
+ name: testClientRoleName,
170
+ description: 'Groups test client role',
171
+ });
172
+
173
+ clientRole = await keycloakManager.clients.findRole({
174
+ id: client.id,
175
+ roleName: testClientRoleName,
176
+ });
177
+ });
178
+
179
+ after(async function () {
180
+ try {
181
+ keycloakManager.setConfig({ realmName: TEST_REALM });
182
+ } catch (err) {
183
+ // best effort
184
+ }
185
+
186
+ try {
187
+ if (userId) {
188
+ await keycloakManager.users.del({ id: userId });
189
+ }
190
+ } catch (err) {
191
+ // best effort
192
+ }
193
+
194
+ try {
195
+ if (client) {
196
+ await keycloakManager.clients.del({ id: client.id });
197
+ }
198
+ } catch (err) {
199
+ // best effort
200
+ }
201
+
202
+ try {
203
+ if (realmRole) {
204
+ await keycloakManager.roles.delByName({ name: realmRole.name });
205
+ }
206
+ } catch (err) {
207
+ // best effort
208
+ }
209
+
210
+ try {
211
+ if (childGroupId) {
212
+ await keycloakManager.groups.del({ id: childGroupId });
213
+ }
214
+ } catch (err) {
215
+ // best effort
216
+ }
217
+
218
+ try {
219
+ if (parentGroupId) {
220
+ await keycloakManager.groups.del({ id: parentGroupId });
221
+ }
222
+ } catch (err) {
223
+ // best effort
224
+ }
225
+
226
+ // Don't delete shared test realm
227
+
228
+ if (typeof keycloakManager.stop === 'function') {
229
+ keycloakManager.stop();
230
+ }
231
+ });
232
+
233
+ it('should create, find, findOne, count and update groups', async function () {
234
+ const groups = await keycloakManager.groups.find({ search: parentGroupName });
235
+ expect(groups).to.be.an('array');
236
+ expect(groups.some((group) => group.id === parentGroupId)).to.equal(true);
237
+
238
+ const one = await keycloakManager.groups.findOne({ id: parentGroupId });
239
+ expect(one).to.be.an('object');
240
+ expect(one.id).to.equal(parentGroupId);
241
+
242
+ const count = await keycloakManager.groups.count({ search: parentGroupName });
243
+ expect(count).to.be.a('number');
244
+ expect(count).to.be.greaterThan(0);
245
+
246
+ const newName = `${parentGroupName}-updated`;
247
+ await keycloakManager.groups.update(
248
+ { id: parentGroupId },
249
+ { name: newName }
250
+ );
251
+
252
+ const updated = await keycloakManager.groups.findOne({ id: parentGroupId });
253
+ expect(updated.name).to.equal(newName);
254
+
255
+ const direct = await requestAdmin(
256
+ keycloakConfig.baseUrl,
257
+ adminToken,
258
+ `/admin/realms/${TEST_REALM}/groups/${parentGroupId}`
259
+ );
260
+ expect(direct.status).to.equal(200);
261
+ expect(direct.body.name).to.equal(newName);
262
+ });
263
+
264
+ it('should list subgroups and members', async function () {
265
+ const subGroups = await keycloakManager.groups.listSubGroups({
266
+ parentId: parentGroupId,
267
+ briefRepresentation: true,
268
+ });
269
+ expect(subGroups).to.be.an('array');
270
+ expect(subGroups.some((group) => group.id === childGroupId)).to.equal(true);
271
+
272
+ const directMembers = await requestAdmin(
273
+ keycloakConfig.baseUrl,
274
+ adminToken,
275
+ `/admin/realms/${TEST_REALM}/groups/${parentGroupId}/members`
276
+ );
277
+ expect(directMembers.status).to.equal(200);
278
+ expect(Array.isArray(directMembers.body)).to.equal(true);
279
+ expect(directMembers.body.some((member) => member.id === userId)).to.equal(true);
280
+ });
281
+
282
+ it('should manage realm role mappings for groups', async function () {
283
+ await keycloakManager.groups.addRealmRoleMappings({
284
+ id: parentGroupId,
285
+ roles: [{ id: realmRole.id, name: realmRole.name }],
286
+ });
287
+
288
+ const realmRoles = await keycloakManager.groups.listRealmRoleMappings({ id: parentGroupId });
289
+ expect(realmRoles).to.be.an('array');
290
+ expect(realmRoles.some((role) => role.id === realmRole.id)).to.equal(true);
291
+
292
+ const compositeRoles = await keycloakManager.groups.listCompositeRealmRoleMappings({ id: parentGroupId });
293
+ expect(compositeRoles).to.be.an('array');
294
+
295
+ const availableRoles = await keycloakManager.groups.listAvailableRealmRoleMappings({ id: parentGroupId });
296
+ expect(availableRoles).to.be.an('array');
297
+
298
+ const mappings = await keycloakManager.groups.listRoleMappings({ id: parentGroupId });
299
+ expect(mappings).to.be.an('object');
300
+
301
+ await keycloakManager.groups.delRealmRoleMappings({
302
+ id: parentGroupId,
303
+ roles: [{ id: realmRole.id, name: realmRole.name }],
304
+ });
305
+
306
+ const realmRolesAfter = await keycloakManager.groups.listRealmRoleMappings({ id: parentGroupId });
307
+ expect(realmRolesAfter.some((role) => role.id === realmRole.id)).to.equal(false);
308
+ });
309
+
310
+ it('should manage client role mappings for groups', async function () {
311
+ await keycloakManager.groups.addClientRoleMappings({
312
+ id: parentGroupId,
313
+ clientUniqueId: client.id,
314
+ roles: [{ id: clientRole.id, name: clientRole.name }],
315
+ });
316
+
317
+ const clientRoles = await keycloakManager.groups.listClientRoleMappings({
318
+ id: parentGroupId,
319
+ clientUniqueId: client.id,
320
+ });
321
+ expect(clientRoles).to.be.an('array');
322
+ expect(clientRoles.some((role) => role.id === clientRole.id)).to.equal(true);
323
+
324
+ const available = await keycloakManager.groups.listAvailableClientRoleMappings({
325
+ id: parentGroupId,
326
+ clientUniqueId: client.id,
327
+ });
328
+ expect(available).to.be.an('array');
329
+
330
+ const composite = await keycloakManager.groups.listCompositeClientRoleMappings({
331
+ id: parentGroupId,
332
+ clientUniqueId: client.id,
333
+ });
334
+ expect(composite).to.be.an('array');
335
+
336
+ await keycloakManager.groups.delClientRoleMappings({
337
+ id: parentGroupId,
338
+ clientUniqueId: client.id,
339
+ roles: [{ id: clientRole.id, name: clientRole.name }],
340
+ });
341
+
342
+ const rolesAfter = await keycloakManager.groups.listClientRoleMappings({
343
+ id: parentGroupId,
344
+ clientUniqueId: client.id,
345
+ });
346
+ expect(rolesAfter.some((role) => role.id === clientRole.id)).to.equal(false);
347
+ });
348
+
349
+ it('should delete groups and verify via admin API', async function () {
350
+ const toDelete = await keycloakManager.groups.create({ name: `groups-delete-${Date.now()}` });
351
+ expect(toDelete.id).to.exist;
352
+
353
+ await keycloakManager.groups.del({ id: toDelete.id });
354
+
355
+ const direct = await requestAdmin(
356
+ keycloakConfig.baseUrl,
357
+ adminToken,
358
+ `/admin/realms/${TEST_REALM}/groups/${toDelete.id}`
359
+ );
360
+ expect(direct.status).to.equal(404);
361
+ });
362
+ });