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,388 @@
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
+
12
+ function requestAdmin(baseUrl, token, apiPath, method = 'GET', body) {
13
+ const url = new URL(apiPath, baseUrl);
14
+ const transport = url.protocol === 'https:' ? https : http;
15
+ const payload = body ? JSON.stringify(body) : null;
16
+
17
+ const options = {
18
+ method,
19
+ headers: {
20
+ Accept: 'application/json',
21
+ Authorization: `Bearer ${token}`,
22
+ ...(payload ? { 'Content-Type': 'application/json' } : {}),
23
+ },
24
+ };
25
+
26
+ return new Promise((resolve, reject) => {
27
+ const req = transport.request(url, options, (res) => {
28
+ let data = '';
29
+ res.on('data', (chunk) => {
30
+ data += chunk;
31
+ });
32
+ res.on('end', () => {
33
+ let parsed = data;
34
+ try {
35
+ parsed = data ? JSON.parse(data) : null;
36
+ } catch (err) {
37
+ parsed = data;
38
+ }
39
+ resolve({ status: res.statusCode, body: parsed });
40
+ });
41
+ });
42
+
43
+ req.on('error', reject);
44
+ if (payload) {
45
+ req.write(payload);
46
+ }
47
+ req.end();
48
+ });
49
+ }
50
+
51
+ async function getAdminToken(baseUrl, username, password) {
52
+ const url = new URL('/realms/master/protocol/openid-connect/token', baseUrl);
53
+ const transport = url.protocol === 'https:' ? https : http;
54
+
55
+ const params = new URLSearchParams({
56
+ grant_type: 'password',
57
+ client_id: 'admin-cli',
58
+ username,
59
+ password,
60
+ });
61
+
62
+ const options = {
63
+ method: 'POST',
64
+ headers: {
65
+ 'Content-Type': 'application/x-www-form-urlencoded',
66
+ },
67
+ };
68
+
69
+ return new Promise((resolve, reject) => {
70
+ const req = transport.request(url, options, (res) => {
71
+ let data = '';
72
+ res.on('data', (chunk) => {
73
+ data += chunk;
74
+ });
75
+ res.on('end', () => {
76
+ if (res.statusCode === 200) {
77
+ const parsed = JSON.parse(data);
78
+ resolve(parsed.access_token);
79
+ } else {
80
+ reject(new Error(`HTTP ${res.statusCode}: ${data}`));
81
+ }
82
+ });
83
+ });
84
+
85
+ req.on('error', reject);
86
+ req.write(params.toString());
87
+ req.end();
88
+ });
89
+ }
90
+
91
+ function shouldSkipFeature(err) {
92
+ if (!err || !err.message) {
93
+ return false;
94
+ }
95
+ const text = err.message.toLowerCase();
96
+ return (
97
+ text.includes('protocolmapper provider not found') ||
98
+ text.includes('not supported') ||
99
+ text.includes('http 404') ||
100
+ text.includes('unknown_error')
101
+ );
102
+ }
103
+
104
+ describe('ClientScopes Handler', function () {
105
+ this.timeout(60000);
106
+
107
+ const keycloakConfig = (conf && conf.keycloak) || {};
108
+ const testRealm = `client-scopes-realm-${Date.now()}`;
109
+ const scopeName = `client-scope-${Date.now()}`;
110
+ const realmRoleName = `client-scope-realm-role-${Date.now()}`;
111
+ const clientId = `client-scope-client-${Date.now()}`;
112
+ const clientRoleName = `client-scope-client-role-${Date.now()}`;
113
+
114
+ let adminToken = null;
115
+ let scope = null;
116
+ let realmRole = null;
117
+ let client = null;
118
+ let clientRole = null;
119
+ let mapperId = null;
120
+
121
+ before(async function () {
122
+ await keycloakManager.configure({
123
+ baseUrl: keycloakConfig.baseUrl,
124
+ realmName: keycloakConfig.realmName,
125
+ clientId: keycloakConfig.clientId,
126
+ clientSecret: keycloakConfig.clientSecret,
127
+ username: keycloakConfig.username,
128
+ password: keycloakConfig.password,
129
+ grantType: keycloakConfig.grantType,
130
+ tokenLifeSpan: keycloakConfig.tokenLifeSpan,
131
+ scope: keycloakConfig.scope,
132
+ });
133
+
134
+ adminToken = await getAdminToken(
135
+ keycloakConfig.baseUrl,
136
+ keycloakConfig.username,
137
+ keycloakConfig.password
138
+ );
139
+
140
+ await keycloakManager.realms.create({ realm: testRealm, enabled: true });
141
+ keycloakManager.setConfig({ realmName: testRealm });
142
+
143
+ scope = await keycloakManager.clientScopes.create({
144
+ name: scopeName,
145
+ protocol: 'openid-connect',
146
+ description: 'test scope',
147
+ });
148
+
149
+ await keycloakManager.roles.create({ name: realmRoleName });
150
+ realmRole = await keycloakManager.roles.findOneByName({ name: realmRoleName });
151
+
152
+ await keycloakManager.clients.create({
153
+ clientId,
154
+ name: clientId,
155
+ enabled: true,
156
+ publicClient: false,
157
+ protocol: 'openid-connect',
158
+ directAccessGrantsEnabled: true,
159
+ standardFlowEnabled: true,
160
+ });
161
+
162
+ const clients = await keycloakManager.clients.find({ clientId });
163
+ client = clients[0];
164
+
165
+ await keycloakManager.clients.createRole({
166
+ id: client.id,
167
+ name: clientRoleName,
168
+ description: 'client scope role',
169
+ });
170
+
171
+ clientRole = await keycloakManager.clients.findRole({
172
+ id: client.id,
173
+ roleName: clientRoleName,
174
+ });
175
+ });
176
+
177
+ after(async function () {
178
+ try {
179
+ keycloakManager.setConfig({ realmName: testRealm });
180
+ } catch (err) {
181
+ // best effort
182
+ }
183
+
184
+ try {
185
+ if (scope) {
186
+ await keycloakManager.clientScopes.del({ id: scope.id });
187
+ }
188
+ } catch (err) {
189
+ // best effort
190
+ }
191
+
192
+ try {
193
+ if (client) {
194
+ await keycloakManager.clients.del({ id: client.id });
195
+ }
196
+ } catch (err) {
197
+ // best effort
198
+ }
199
+
200
+ try {
201
+ if (realmRole) {
202
+ await keycloakManager.roles.delByName({ name: realmRole.name });
203
+ }
204
+ } catch (err) {
205
+ // best effort
206
+ }
207
+
208
+ try {
209
+ await keycloakManager.realms.del({ realm: testRealm });
210
+ } catch (err) {
211
+ // best effort
212
+ }
213
+
214
+ if (typeof keycloakManager.stop === 'function') {
215
+ keycloakManager.stop();
216
+ }
217
+ });
218
+
219
+ it('should create, find, findOne, findOneByName, update and delete by name', async function () {
220
+ const list = await keycloakManager.clientScopes.find();
221
+ expect(list).to.be.an('array');
222
+ expect(list.some((item) => item.id === scope.id)).to.equal(true);
223
+
224
+ const one = await keycloakManager.clientScopes.findOne({ id: scope.id });
225
+ expect(one).to.be.an('object');
226
+ expect(one.name).to.equal(scopeName);
227
+
228
+ const byName = await keycloakManager.clientScopes.findOneByName({ name: scopeName });
229
+ expect(byName).to.be.an('object');
230
+ expect(byName.id).to.equal(scope.id);
231
+
232
+ const updatedDescription = `updated-${Date.now()}`;
233
+ await keycloakManager.clientScopes.update(
234
+ { id: scope.id },
235
+ { name: scopeName, protocol: 'openid-connect', description: updatedDescription }
236
+ );
237
+
238
+ const updated = await keycloakManager.clientScopes.findOne({ id: scope.id });
239
+ expect(updated.description).to.equal(updatedDescription);
240
+
241
+ const direct = await requestAdmin(
242
+ keycloakConfig.baseUrl,
243
+ adminToken,
244
+ `/admin/realms/${testRealm}/client-scopes/${scope.id}`
245
+ );
246
+ expect(direct.status).to.equal(200);
247
+ expect(direct.body.description).to.equal(updatedDescription);
248
+
249
+ const tempName = `client-scope-temp-${Date.now()}`;
250
+ await keycloakManager.clientScopes.create({ name: tempName, protocol: 'openid-connect' });
251
+ await keycloakManager.clientScopes.delByName({ name: tempName });
252
+
253
+ const deleted = await keycloakManager.clientScopes.findOneByName({ name: tempName });
254
+ expect(deleted).to.equal(undefined);
255
+ });
256
+
257
+ it('should manage default and optional client scopes', async function () {
258
+ await keycloakManager.clientScopes.addDefaultClientScope({ id: scope.id });
259
+ const defaults = await keycloakManager.clientScopes.listDefaultClientScopes();
260
+ expect(defaults).to.be.an('array');
261
+ expect(defaults.some((item) => item.id === scope.id)).to.equal(true);
262
+
263
+ await keycloakManager.clientScopes.delDefaultClientScope({ id: scope.id });
264
+ const defaultsAfter = await keycloakManager.clientScopes.listDefaultClientScopes();
265
+ expect(defaultsAfter.some((item) => item.id === scope.id)).to.equal(false);
266
+
267
+ await keycloakManager.clientScopes.addDefaultOptionalClientScope({ id: scope.id });
268
+ const optional = await keycloakManager.clientScopes.listDefaultOptionalClientScopes();
269
+ expect(optional).to.be.an('array');
270
+ expect(optional.some((item) => item.id === scope.id)).to.equal(true);
271
+
272
+ await keycloakManager.clientScopes.delDefaultOptionalClientScope({ id: scope.id });
273
+ const optionalAfter = await keycloakManager.clientScopes.listDefaultOptionalClientScopes();
274
+ expect(optionalAfter.some((item) => item.id === scope.id)).to.equal(false);
275
+ });
276
+
277
+ it('should manage protocol mappers when provider is available', async function () {
278
+ const mapper = {
279
+ name: `mapper-${Date.now()}`,
280
+ protocol: 'openid-connect',
281
+ protocolMapper: 'oidc-usermodel-property-mapper',
282
+ config: {
283
+ 'user.attribute': 'email',
284
+ 'claim.name': 'email',
285
+ 'jsonType.label': 'String',
286
+ 'id.token.claim': 'true',
287
+ 'access.token.claim': 'true',
288
+ },
289
+ };
290
+
291
+ try {
292
+ await keycloakManager.clientScopes.addProtocolMapper({ id: scope.id }, mapper);
293
+ const mappers = await keycloakManager.clientScopes.listProtocolMappers({ id: scope.id });
294
+ expect(mappers).to.be.an('array');
295
+
296
+ const foundByName = await keycloakManager.clientScopes.findProtocolMapperByName({
297
+ id: scope.id,
298
+ name: mapper.name,
299
+ });
300
+ expect(foundByName).to.be.an('object');
301
+ mapperId = foundByName.id;
302
+
303
+ const found = await keycloakManager.clientScopes.findProtocolMapper({
304
+ id: scope.id,
305
+ mapperId,
306
+ });
307
+ expect(found).to.be.an('object');
308
+
309
+ const byProtocol = await keycloakManager.clientScopes.findProtocolMappersByProtocol({
310
+ id: scope.id,
311
+ protocol: 'openid-connect',
312
+ });
313
+ expect(byProtocol).to.be.an('array');
314
+
315
+ await keycloakManager.clientScopes.updateProtocolMapper(
316
+ { id: scope.id, mapperId },
317
+ {
318
+ id: mapperId,
319
+ ...mapper,
320
+ name: `${mapper.name}-updated`,
321
+ }
322
+ );
323
+
324
+ await keycloakManager.clientScopes.delProtocolMapper({ id: scope.id, mapperId });
325
+ mapperId = null;
326
+ } catch (err) {
327
+ if (shouldSkipFeature(err)) {
328
+ this.skip();
329
+ return;
330
+ }
331
+ throw err;
332
+ }
333
+ });
334
+
335
+ it('should manage client and realm scope mappings', async function () {
336
+ await keycloakManager.clientScopes.addRealmScopeMappings(
337
+ { id: scope.id },
338
+ [{ id: realmRole.id, name: realmRole.name }]
339
+ );
340
+
341
+ const realmMappings = await keycloakManager.clientScopes.listRealmScopeMappings({ id: scope.id });
342
+ expect(realmMappings).to.be.an('array');
343
+ expect(realmMappings.some((item) => item.id === realmRole.id)).to.equal(true);
344
+
345
+ const availableRealm = await keycloakManager.clientScopes.listAvailableRealmScopeMappings({ id: scope.id });
346
+ expect(availableRealm).to.be.an('array');
347
+
348
+ const compositeRealm = await keycloakManager.clientScopes.listCompositeRealmScopeMappings({ id: scope.id });
349
+ expect(compositeRealm).to.be.an('array');
350
+
351
+ await keycloakManager.clientScopes.delRealmScopeMappings(
352
+ { id: scope.id },
353
+ [{ id: realmRole.id, name: realmRole.name }]
354
+ );
355
+
356
+ await keycloakManager.clientScopes.addClientScopeMappings(
357
+ { id: scope.id, client: client.id },
358
+ [{ id: clientRole.id, name: clientRole.name }]
359
+ );
360
+
361
+ const clientMappings = await keycloakManager.clientScopes.listClientScopeMappings({
362
+ id: scope.id,
363
+ client: client.id,
364
+ });
365
+ expect(clientMappings).to.be.an('array');
366
+ expect(clientMappings.some((item) => item.id === clientRole.id)).to.equal(true);
367
+
368
+ const availableClient = await keycloakManager.clientScopes.listAvailableClientScopeMappings({
369
+ id: scope.id,
370
+ client: client.id,
371
+ });
372
+ expect(availableClient).to.be.an('array');
373
+
374
+ const compositeClient = await keycloakManager.clientScopes.listCompositeClientScopeMappings({
375
+ id: scope.id,
376
+ client: client.id,
377
+ });
378
+ expect(compositeClient).to.be.an('array');
379
+
380
+ const mappings = await keycloakManager.clientScopes.listScopeMappings({ id: scope.id });
381
+ expect(mappings).to.be.an('object');
382
+
383
+ await keycloakManager.clientScopes.delClientScopeMappings(
384
+ { id: scope.id, client: client.id },
385
+ [{ id: clientRole.id, name: clientRole.name }]
386
+ );
387
+ });
388
+ });