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.
- package/.env.example +27 -0
- package/Handlers/authenticationManagementHandler.js +1 -1
- package/Handlers/clientScopesHandler.js +5 -5
- package/Handlers/clientsHandler.js +241 -31
- package/Handlers/componentsHandler.js +2 -2
- package/Handlers/groupsHandler.js +18 -3
- package/Handlers/httpApiHelper.js +87 -0
- package/Handlers/identityProvidersHandler.js +3 -3
- package/Handlers/realmsHandler.js +26 -4
- package/Handlers/usersHandler.js +43 -10
- package/README.md +361 -26
- package/index.js +149 -29
- package/index.mjs +21 -0
- package/package.json +3 -2
- package/test/.mocharc.json +4 -0
- package/test/TESTING.md +327 -0
- package/test/config/CONFIGURATION.md +170 -0
- package/test/config/default.json +36 -0
- package/test/config/local.json.example +7 -0
- package/test/config/secrets.json.example +7 -0
- package/test/diagnostic-protocol-mappers.js +189 -0
- package/test/docker-keycloak/DEPLOYMENT_GUIDE.md +262 -0
- package/test/docker-keycloak/certs/.gitkeep +7 -0
- package/test/docker-keycloak/docker-compose-https.yml +50 -0
- package/test/docker-keycloak/docker-compose.yml +59 -0
- package/test/docker-keycloak/setup-keycloak.js +501 -0
- package/test/enableServerFeatures.js +315 -0
- package/test/helpers/config.js +218 -0
- package/test/helpers/docker-helpers.js +513 -0
- package/test/helpers/setup.js +186 -0
- package/test/package.json +18 -0
- package/test/setup.js +194 -0
- package/test/specs/authenticationManagement.test.js +224 -0
- package/test/specs/clientScopes.test.js +388 -0
- package/test/specs/clients.test.js +791 -0
- package/test/specs/components.test.js +151 -0
- package/test/specs/debugClientLibrary.test.js +88 -0
- package/test/specs/groups.test.js +362 -0
- package/test/specs/identityProviders.test.js +292 -0
- package/test/specs/realms.test.js +390 -0
- package/test/specs/roles.test.js +322 -0
- package/test/specs/users.test.js +445 -0
- package/test/testConfig.js +69 -0
- package/.idea/vcs.xml +0 -6
- package/.idea/workspace.xml +0 -94
|
@@ -0,0 +1,791 @@
|
|
|
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
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
35
|
+
try {
|
|
36
|
+
resolve(data ? JSON.parse(data) : null);
|
|
37
|
+
} catch {
|
|
38
|
+
resolve(data);
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
req.on('error', reject);
|
|
47
|
+
if (payload) {
|
|
48
|
+
req.write(payload);
|
|
49
|
+
}
|
|
50
|
+
req.end();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function getAdminToken(baseUrl, username, password) {
|
|
55
|
+
const url = new URL('/realms/master/protocol/openid-connect/token', baseUrl);
|
|
56
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
57
|
+
|
|
58
|
+
const params = new URLSearchParams({
|
|
59
|
+
grant_type: 'password',
|
|
60
|
+
client_id: 'admin-cli',
|
|
61
|
+
username,
|
|
62
|
+
password,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const options = {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const req = transport.request(url, options, (res) => {
|
|
74
|
+
let data = '';
|
|
75
|
+
res.on('data', (chunk) => {
|
|
76
|
+
data += chunk;
|
|
77
|
+
});
|
|
78
|
+
res.on('end', () => {
|
|
79
|
+
if (res.statusCode === 200) {
|
|
80
|
+
const parsed = JSON.parse(data);
|
|
81
|
+
resolve(parsed.access_token);
|
|
82
|
+
} else {
|
|
83
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
req.on('error', reject);
|
|
89
|
+
req.write(params.toString());
|
|
90
|
+
req.end();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function shouldSkipFeature(err, message) {
|
|
95
|
+
if (!err || !err.message) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const text = err.message.toLowerCase();
|
|
99
|
+
return text.includes('feature not enabled') ||
|
|
100
|
+
text.includes('authorization') && text.includes('disabled') ||
|
|
101
|
+
text.includes('not enabled') ||
|
|
102
|
+
text.includes('not supported') ||
|
|
103
|
+
text.includes('protocolmapper provider not found') ||
|
|
104
|
+
text.includes('http 404') ||
|
|
105
|
+
(message && text.includes(message));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function findClientByClientId(clientId) {
|
|
109
|
+
const clients = await keycloakManager.clients.find({ clientId });
|
|
110
|
+
return clients && clients.length ? clients[0] : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
describe('Clients Handler', function () {
|
|
114
|
+
this.timeout(60000);
|
|
115
|
+
|
|
116
|
+
const keycloakConfig = (conf && conf.keycloak) || {};
|
|
117
|
+
const sourceClientId = generateUniqueName('source-client');
|
|
118
|
+
const targetClientId = generateUniqueName('target-client');
|
|
119
|
+
const clientScopeName = generateUniqueName('scope');
|
|
120
|
+
const realmRoleName = generateUniqueName('realm-role');
|
|
121
|
+
const clientRoleName = generateUniqueName('client-role');
|
|
122
|
+
const protocolMapperName = generateUniqueName('mapper');
|
|
123
|
+
const protocolMapperNameTwo = generateUniqueName('mapper-two');
|
|
124
|
+
const protocolMapperNameThree = generateUniqueName('mapper-three');
|
|
125
|
+
const authzScopeName = generateUniqueName('authz-scope');
|
|
126
|
+
const authzResourceName = generateUniqueName('authz-resource');
|
|
127
|
+
const authzPolicyName = generateUniqueName('authz-policy');
|
|
128
|
+
const authzPermissionName = generateUniqueName('authz-permission');
|
|
129
|
+
const authzImportedResourceName = generateUniqueName('authz-imported');
|
|
130
|
+
|
|
131
|
+
let realmRoleId = null;
|
|
132
|
+
let clientScopeId = null;
|
|
133
|
+
let sourceClient = null;
|
|
134
|
+
let targetClient = null;
|
|
135
|
+
let clientRole = null;
|
|
136
|
+
let protocolMapperId = null;
|
|
137
|
+
let authzScope = null;
|
|
138
|
+
let authzResource = null;
|
|
139
|
+
let authzPolicy = null;
|
|
140
|
+
let authzPermission = null;
|
|
141
|
+
let testUserId = null;
|
|
142
|
+
let authzServicesAvailable = false;
|
|
143
|
+
let adminToken = null;
|
|
144
|
+
|
|
145
|
+
before(async function () {
|
|
146
|
+
await keycloakManager.configure({
|
|
147
|
+
baseUrl: keycloakConfig.baseUrl,
|
|
148
|
+
realmName: keycloakConfig.realmName,
|
|
149
|
+
clientId: keycloakConfig.clientId,
|
|
150
|
+
clientSecret: keycloakConfig.clientSecret,
|
|
151
|
+
username: keycloakConfig.username,
|
|
152
|
+
password: keycloakConfig.password,
|
|
153
|
+
grantType: keycloakConfig.grantType,
|
|
154
|
+
tokenLifeSpan: keycloakConfig.tokenLifeSpan,
|
|
155
|
+
scope: keycloakConfig.scope,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Use shared test realm (created by enableServerFeatures)
|
|
159
|
+
keycloakManager.setConfig({ realmName: TEST_REALM });
|
|
160
|
+
|
|
161
|
+
await keycloakManager.roles.create({ name: realmRoleName });
|
|
162
|
+
const realmRole = await keycloakManager.roles.findOneByName({ name: realmRoleName });
|
|
163
|
+
realmRoleId = realmRole.id;
|
|
164
|
+
|
|
165
|
+
const createdScope = await keycloakManager.clientScopes.create({
|
|
166
|
+
name: clientScopeName,
|
|
167
|
+
protocol: 'openid-connect',
|
|
168
|
+
});
|
|
169
|
+
clientScopeId = createdScope.id;
|
|
170
|
+
if (!clientScopeId) {
|
|
171
|
+
const scope = await keycloakManager.clientScopes.findOneByName({ name: clientScopeName });
|
|
172
|
+
clientScopeId = scope.id;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await keycloakManager.clients.create({
|
|
176
|
+
clientId: sourceClientId,
|
|
177
|
+
name: sourceClientId,
|
|
178
|
+
enabled: true,
|
|
179
|
+
publicClient: false,
|
|
180
|
+
protocol: 'openid-connect',
|
|
181
|
+
standardFlowEnabled: true,
|
|
182
|
+
directAccessGrantsEnabled: true,
|
|
183
|
+
});
|
|
184
|
+
sourceClient = await findClientByClientId(sourceClientId);
|
|
185
|
+
|
|
186
|
+
await keycloakManager.clients.create({
|
|
187
|
+
clientId: targetClientId,
|
|
188
|
+
name: targetClientId,
|
|
189
|
+
enabled: true,
|
|
190
|
+
publicClient: false,
|
|
191
|
+
protocol: 'openid-connect',
|
|
192
|
+
standardFlowEnabled: true,
|
|
193
|
+
directAccessGrantsEnabled: true,
|
|
194
|
+
serviceAccountsEnabled: true,
|
|
195
|
+
authorizationServicesEnabled: true,
|
|
196
|
+
});
|
|
197
|
+
targetClient = await findClientByClientId(targetClientId);
|
|
198
|
+
|
|
199
|
+
await keycloakManager.clients.createRole({
|
|
200
|
+
id: sourceClient.id,
|
|
201
|
+
name: clientRoleName,
|
|
202
|
+
description: 'Test client role',
|
|
203
|
+
});
|
|
204
|
+
clientRole = await keycloakManager.clients.findRole({
|
|
205
|
+
id: sourceClient.id,
|
|
206
|
+
roleName: clientRoleName,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const user = await keycloakManager.users.create({
|
|
210
|
+
username: `client-user-${Date.now()}`,
|
|
211
|
+
enabled: true,
|
|
212
|
+
email: `client-user-${Date.now()}@example.com`,
|
|
213
|
+
});
|
|
214
|
+
testUserId = user.id;
|
|
215
|
+
|
|
216
|
+
// Get admin token for direct API verification
|
|
217
|
+
adminToken = await getAdminToken(
|
|
218
|
+
keycloakConfig.baseUrl,
|
|
219
|
+
keycloakConfig.username,
|
|
220
|
+
keycloakConfig.password
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
after(async function () {
|
|
225
|
+
try {
|
|
226
|
+
if (targetClient) {
|
|
227
|
+
await keycloakManager.clients.del({ id: targetClient.id });
|
|
228
|
+
}
|
|
229
|
+
} catch (err) {
|
|
230
|
+
// Best-effort cleanup.
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
if (sourceClient) {
|
|
235
|
+
await keycloakManager.clients.del({ id: sourceClient.id });
|
|
236
|
+
}
|
|
237
|
+
} catch (err) {
|
|
238
|
+
// Best-effort cleanup.
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
if (clientScopeId) {
|
|
243
|
+
await keycloakManager.clientScopes.del({ id: clientScopeId });
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
// Best-effort cleanup.
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
if (realmRoleId) {
|
|
251
|
+
await keycloakManager.roles.delById({ id: realmRoleId });
|
|
252
|
+
}
|
|
253
|
+
} catch (err) {
|
|
254
|
+
// Best-effort cleanup.
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Don't delete shared test realm
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
if (testUserId) {
|
|
261
|
+
await keycloakManager.users.del({ id: testUserId });
|
|
262
|
+
}
|
|
263
|
+
} catch (err) {
|
|
264
|
+
// Best-effort cleanup.
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (typeof keycloakManager.stop === 'function') {
|
|
268
|
+
keycloakManager.stop();
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should create, find, update, and delete clients', async function () {
|
|
273
|
+
const found = await keycloakManager.clients.find({ clientId: targetClientId });
|
|
274
|
+
expect(found).to.be.an('array');
|
|
275
|
+
expect(found[0].clientId).to.equal(targetClientId);
|
|
276
|
+
|
|
277
|
+
const one = await keycloakManager.clients.findOne({ id: targetClient.id });
|
|
278
|
+
expect(one).to.be.an('object');
|
|
279
|
+
|
|
280
|
+
// Verify via direct Keycloak API call
|
|
281
|
+
const directClient = await requestAdmin(
|
|
282
|
+
keycloakConfig.baseUrl,
|
|
283
|
+
adminToken,
|
|
284
|
+
`/admin/realms/${TEST_REALM}/clients/${targetClient.id}`
|
|
285
|
+
);
|
|
286
|
+
expect(directClient.clientId).to.equal(targetClientId);
|
|
287
|
+
expect(directClient.id).to.equal(targetClient.id);
|
|
288
|
+
|
|
289
|
+
const updatedName = `${targetClientId}-updated`;
|
|
290
|
+
await keycloakManager.clients.update(
|
|
291
|
+
{ id: targetClient.id },
|
|
292
|
+
{ name: updatedName }
|
|
293
|
+
);
|
|
294
|
+
const updated = await keycloakManager.clients.findOne({ id: targetClient.id });
|
|
295
|
+
expect(updated.name).to.equal(updatedName);
|
|
296
|
+
|
|
297
|
+
// Verify update via direct Keycloak API call
|
|
298
|
+
const directUpdated = await requestAdmin(
|
|
299
|
+
keycloakConfig.baseUrl,
|
|
300
|
+
adminToken,
|
|
301
|
+
`/admin/realms/${TEST_REALM}/clients/${targetClient.id}`
|
|
302
|
+
);
|
|
303
|
+
expect(directUpdated.name).to.equal(updatedName);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should manage client roles', async function () {
|
|
307
|
+
const roles = await keycloakManager.clients.listRoles({ id: sourceClient.id });
|
|
308
|
+
expect(roles).to.be.an('array');
|
|
309
|
+
|
|
310
|
+
const role = await keycloakManager.clients.findRole({
|
|
311
|
+
id: sourceClient.id,
|
|
312
|
+
roleName: clientRoleName,
|
|
313
|
+
});
|
|
314
|
+
expect(role).to.be.an('object');
|
|
315
|
+
|
|
316
|
+
// Verify role via direct Keycloak API call
|
|
317
|
+
const directRoles = await requestAdmin(
|
|
318
|
+
keycloakConfig.baseUrl,
|
|
319
|
+
adminToken,
|
|
320
|
+
`/admin/realms/${TEST_REALM}/clients/${sourceClient.id}/roles`
|
|
321
|
+
);
|
|
322
|
+
const directRole = directRoles.find(r => r.name === clientRoleName);
|
|
323
|
+
expect(directRole).to.exist;
|
|
324
|
+
expect(directRole.name).to.equal(clientRoleName);
|
|
325
|
+
|
|
326
|
+
await keycloakManager.clients.updateRole(
|
|
327
|
+
{ id: sourceClient.id, roleName: clientRoleName },
|
|
328
|
+
{ name: clientRoleName, description: 'Updated role' }
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// Verify role update via direct API
|
|
332
|
+
const directUpdatedRole = await requestAdmin(
|
|
333
|
+
keycloakConfig.baseUrl,
|
|
334
|
+
adminToken,
|
|
335
|
+
`/admin/realms/${TEST_REALM}/clients/${sourceClient.id}/roles/${clientRoleName}`
|
|
336
|
+
);
|
|
337
|
+
expect(directUpdatedRole.description).to.equal('Updated role');
|
|
338
|
+
|
|
339
|
+
await keycloakManager.clients.delRole({
|
|
340
|
+
id: sourceClient.id,
|
|
341
|
+
roleName: clientRoleName,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
await keycloakManager.clients.createRole({
|
|
345
|
+
id: sourceClient.id,
|
|
346
|
+
name: clientRoleName,
|
|
347
|
+
description: 'Test client role',
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
clientRole = await keycloakManager.clients.findRole({
|
|
351
|
+
id: sourceClient.id,
|
|
352
|
+
roleName: clientRoleName,
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should manage client secrets and registration tokens', async function () {
|
|
357
|
+
const secret = await keycloakManager.clients.getClientSecret({ id: targetClient.id });
|
|
358
|
+
expect(secret).to.be.an('object');
|
|
359
|
+
|
|
360
|
+
const newSecret = await keycloakManager.clients.generateNewClientSecret({ id: targetClient.id });
|
|
361
|
+
expect(newSecret).to.be.an('object');
|
|
362
|
+
|
|
363
|
+
await keycloakManager.clients.invalidateSecret({ id: targetClient.id });
|
|
364
|
+
|
|
365
|
+
const regToken = await keycloakManager.clients.generateRegistrationAccessToken({
|
|
366
|
+
id: targetClient.id,
|
|
367
|
+
});
|
|
368
|
+
expect(regToken).to.be.an('object');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should manage service account user', async function () {
|
|
372
|
+
const user = await keycloakManager.clients.getServiceAccountUser({
|
|
373
|
+
id: targetClient.id,
|
|
374
|
+
});
|
|
375
|
+
expect(user).to.be.an('object');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should manage client scopes', async function () {
|
|
379
|
+
await keycloakManager.clients.addDefaultClientScope({
|
|
380
|
+
id: targetClient.id,
|
|
381
|
+
clientScopeId: clientScopeId,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const defaults = await keycloakManager.clients.listDefaultClientScopes({
|
|
385
|
+
id: targetClient.id,
|
|
386
|
+
});
|
|
387
|
+
expect(defaults).to.be.an('array');
|
|
388
|
+
|
|
389
|
+
await keycloakManager.clients.delDefaultClientScope({
|
|
390
|
+
id: targetClient.id,
|
|
391
|
+
clientScopeId: clientScopeId,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
await keycloakManager.clients.addOptionalClientScope({
|
|
395
|
+
id: targetClient.id,
|
|
396
|
+
clientScopeId: clientScopeId,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const optional = await keycloakManager.clients.listOptionalClientScopes({
|
|
400
|
+
id: targetClient.id,
|
|
401
|
+
});
|
|
402
|
+
expect(optional).to.be.an('array');
|
|
403
|
+
|
|
404
|
+
await keycloakManager.clients.delOptionalClientScope({
|
|
405
|
+
id: targetClient.id,
|
|
406
|
+
clientScopeId: clientScopeId,
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should manage client role scope mappings', async function () {
|
|
411
|
+
const available = await keycloakManager.clients.listAvailableClientScopeMappings({
|
|
412
|
+
id: targetClient.id,
|
|
413
|
+
client: sourceClient.id,
|
|
414
|
+
});
|
|
415
|
+
expect(available).to.be.an('array');
|
|
416
|
+
|
|
417
|
+
await keycloakManager.clients.addClientScopeMappings(
|
|
418
|
+
{ id: targetClient.id, client: sourceClient.id },
|
|
419
|
+
[clientRole]
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const mappings = await keycloakManager.clients.listClientScopeMappings({
|
|
423
|
+
id: targetClient.id,
|
|
424
|
+
client: sourceClient.id,
|
|
425
|
+
});
|
|
426
|
+
expect(mappings).to.be.an('array');
|
|
427
|
+
|
|
428
|
+
const composite = await keycloakManager.clients.listCompositeClientScopeMappings({
|
|
429
|
+
id: targetClient.id,
|
|
430
|
+
client: sourceClient.id,
|
|
431
|
+
});
|
|
432
|
+
expect(composite).to.be.an('array');
|
|
433
|
+
|
|
434
|
+
await keycloakManager.clients.delClientScopeMappings(
|
|
435
|
+
{ id: targetClient.id, client: sourceClient.id },
|
|
436
|
+
[clientRole]
|
|
437
|
+
);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('should manage realm role scope mappings', async function () {
|
|
441
|
+
const available = await keycloakManager.clients.listAvailableRealmScopeMappings({
|
|
442
|
+
id: targetClient.id,
|
|
443
|
+
});
|
|
444
|
+
expect(available).to.be.an('array');
|
|
445
|
+
|
|
446
|
+
await keycloakManager.clients.addRealmScopeMappings(
|
|
447
|
+
{ id: targetClient.id },
|
|
448
|
+
[{ id: realmRoleId, name: realmRoleName }]
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const mappings = await keycloakManager.clients.listRealmScopeMappings({
|
|
452
|
+
id: targetClient.id,
|
|
453
|
+
});
|
|
454
|
+
expect(mappings).to.be.an('array');
|
|
455
|
+
|
|
456
|
+
const composite = await keycloakManager.clients.listCompositeRealmScopeMappings({
|
|
457
|
+
id: targetClient.id,
|
|
458
|
+
});
|
|
459
|
+
expect(composite).to.be.an('array');
|
|
460
|
+
|
|
461
|
+
await keycloakManager.clients.delRealmScopeMappings(
|
|
462
|
+
{ id: targetClient.id },
|
|
463
|
+
[{ id: realmRoleId, name: realmRoleName }]
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should manage sessions and counts', async function () {
|
|
468
|
+
const sessions = await keycloakManager.clients.listSessions({ id: targetClient.id });
|
|
469
|
+
expect(sessions).to.be.an('array');
|
|
470
|
+
|
|
471
|
+
const offline = await keycloakManager.clients.listOfflineSessions({ id: targetClient.id });
|
|
472
|
+
expect(offline).to.be.an('array');
|
|
473
|
+
|
|
474
|
+
const count = await keycloakManager.clients.getSessionCount({ id: targetClient.id });
|
|
475
|
+
expect(count).to.be.a('number');
|
|
476
|
+
|
|
477
|
+
const offlineCount = await keycloakManager.clients.getOfflineSessionCount({ id: targetClient.id });
|
|
478
|
+
expect(offlineCount).to.be.a('number');
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('should manage cluster nodes', async function () {
|
|
482
|
+
const nodeName = `node-${Date.now()}`;
|
|
483
|
+
await keycloakManager.clients.addClusterNode({ id: targetClient.id, node: nodeName });
|
|
484
|
+
await keycloakManager.clients.deleteClusterNode({ id: targetClient.id, node: nodeName });
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should evaluate tokens and mappers', async function () {
|
|
488
|
+
try {
|
|
489
|
+
const accessToken = await keycloakManager.clients.evaluateGenerateAccessToken({
|
|
490
|
+
id: targetClient.id,
|
|
491
|
+
userId: testUserId,
|
|
492
|
+
});
|
|
493
|
+
expect(accessToken).to.exist;
|
|
494
|
+
|
|
495
|
+
const idToken = await keycloakManager.clients.evaluateGenerateIdToken({
|
|
496
|
+
id: targetClient.id,
|
|
497
|
+
userId: testUserId,
|
|
498
|
+
});
|
|
499
|
+
expect(idToken).to.exist;
|
|
500
|
+
|
|
501
|
+
const userInfo = await keycloakManager.clients.evaluateGenerateUserInfo({
|
|
502
|
+
id: targetClient.id,
|
|
503
|
+
userId: testUserId,
|
|
504
|
+
});
|
|
505
|
+
expect(userInfo).to.exist;
|
|
506
|
+
|
|
507
|
+
const mappers = await keycloakManager.clients.evaluateListProtocolMapper({
|
|
508
|
+
id: targetClient.id,
|
|
509
|
+
});
|
|
510
|
+
expect(mappers).to.be.an('array');
|
|
511
|
+
} catch (err) {
|
|
512
|
+
if (shouldSkipFeature(err)) {
|
|
513
|
+
this.test.title += ' (evaluation not supported)';
|
|
514
|
+
this.skip();
|
|
515
|
+
}
|
|
516
|
+
throw err;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
describe('Authorization Services', function () {
|
|
521
|
+
before(async function () {
|
|
522
|
+
// Ensure targetClient is created
|
|
523
|
+
if (!targetClient) {
|
|
524
|
+
console.log(' Target client not yet created, skipping Authorization Services tests');
|
|
525
|
+
this.skip();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Force update of client to ensure Authorization Services is fully enabled
|
|
530
|
+
try {
|
|
531
|
+
await keycloakManager.clients.update(
|
|
532
|
+
{ id: targetClient.id },
|
|
533
|
+
{
|
|
534
|
+
authorizationServicesEnabled: true,
|
|
535
|
+
serviceAccountsEnabled: true,
|
|
536
|
+
publicClient: false,
|
|
537
|
+
}
|
|
538
|
+
);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
console.log(' Error updating client for Authorization Services:', err.message);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Check if Authorization Services is available on targetClient
|
|
544
|
+
try {
|
|
545
|
+
await keycloakManager.clients.getResourceServer({ id: targetClient.id });
|
|
546
|
+
authzServicesAvailable = true;
|
|
547
|
+
} catch (err) {
|
|
548
|
+
if (shouldSkipFeature(err)) {
|
|
549
|
+
console.log(' Authorization Services not available, skipping tests');
|
|
550
|
+
this.skip();
|
|
551
|
+
} else {
|
|
552
|
+
throw err;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('should manage authorization scopes and resources', async function () {
|
|
558
|
+
authzScope = await keycloakManager.clients.createAuthorizationScope(
|
|
559
|
+
{ id: targetClient.id },
|
|
560
|
+
{ name: authzScopeName }
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
const scopes = await keycloakManager.clients.listAllScopes({
|
|
564
|
+
id: targetClient.id,
|
|
565
|
+
});
|
|
566
|
+
expect(scopes).to.be.an('array');
|
|
567
|
+
|
|
568
|
+
authzResource = await keycloakManager.clients.createResource(
|
|
569
|
+
{ id: targetClient.id },
|
|
570
|
+
{
|
|
571
|
+
name: authzResourceName,
|
|
572
|
+
uris: ['/resource'],
|
|
573
|
+
scopes: [authzScope],
|
|
574
|
+
}
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
const resources = await keycloakManager.clients.listResources({
|
|
578
|
+
id: targetClient.id,
|
|
579
|
+
deep: true,
|
|
580
|
+
});
|
|
581
|
+
expect(resources).to.be.an('array');
|
|
582
|
+
|
|
583
|
+
const foundResource = await keycloakManager.clients.getResource({
|
|
584
|
+
id: targetClient.id,
|
|
585
|
+
resourceId: authzResource._id,
|
|
586
|
+
});
|
|
587
|
+
expect(foundResource).to.be.an('object');
|
|
588
|
+
|
|
589
|
+
await keycloakManager.clients.updateResource(
|
|
590
|
+
{ id: targetClient.id, resourceId: authzResource._id },
|
|
591
|
+
{ name: `${authzResourceName}-updated` }
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
// Skip listAllResourcesByScope if scopeId format not supported
|
|
595
|
+
try {
|
|
596
|
+
const byScope = await keycloakManager.clients.listAllResourcesByScope({
|
|
597
|
+
id: targetClient.id,
|
|
598
|
+
scopeId: authzScope.id,
|
|
599
|
+
});
|
|
600
|
+
expect(byScope).to.be.an('array');
|
|
601
|
+
} catch (err) {
|
|
602
|
+
// Known issue: missingNormalization error with some Keycloak versions
|
|
603
|
+
console.log(` Skipping listAllResourcesByScope: ${err.message}`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
const scopesByResource = await keycloakManager.clients.listScopesByResource({
|
|
608
|
+
id: targetClient.id,
|
|
609
|
+
resourceId: authzResource._id,
|
|
610
|
+
});
|
|
611
|
+
expect(scopesByResource).to.be.an('array');
|
|
612
|
+
} catch (err) {
|
|
613
|
+
// Known issue: missingNormalization error with some Keycloak versions
|
|
614
|
+
console.log(` Skipping listScopesByResource: ${err.message}`);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('should manage policies and permissions', async function () {
|
|
619
|
+
try {
|
|
620
|
+
authzPolicy = await keycloakManager.clients.createPolicy(
|
|
621
|
+
{ id: targetClient.id, type: 'role' },
|
|
622
|
+
{
|
|
623
|
+
name: authzPolicyName,
|
|
624
|
+
decisionStrategy: 'UNANIMOUS',
|
|
625
|
+
logic: 'POSITIVE',
|
|
626
|
+
roles: [{ id: realmRoleId, required: true }],
|
|
627
|
+
}
|
|
628
|
+
);
|
|
629
|
+
} catch (err) {
|
|
630
|
+
// Try with simplified payload
|
|
631
|
+
authzPolicy = await keycloakManager.clients.createPolicy(
|
|
632
|
+
{ id: targetClient.id, type: 'js' },
|
|
633
|
+
{
|
|
634
|
+
name: authzPolicyName,
|
|
635
|
+
code: 'true;',
|
|
636
|
+
logic: 'POSITIVE',
|
|
637
|
+
}
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const dependents = await keycloakManager.clients.listDependentPolicies({
|
|
642
|
+
id: targetClient.id,
|
|
643
|
+
policyId: authzPolicy.id,
|
|
644
|
+
});
|
|
645
|
+
expect(dependents).to.be.an('array');
|
|
646
|
+
|
|
647
|
+
authzPermission = await keycloakManager.clients.createPermission(
|
|
648
|
+
{ id: targetClient.id, type: 'resource' },
|
|
649
|
+
{
|
|
650
|
+
name: authzPermissionName,
|
|
651
|
+
resources: [authzResource._id],
|
|
652
|
+
policies: [authzPolicy.id],
|
|
653
|
+
}
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
const found = await keycloakManager.clients.findPermissions({
|
|
657
|
+
id: targetClient.id,
|
|
658
|
+
name: authzPermissionName,
|
|
659
|
+
});
|
|
660
|
+
expect(found).to.be.an('array');
|
|
661
|
+
|
|
662
|
+
const byResource = await keycloakManager.clients.listPermissionsByResource({
|
|
663
|
+
id: targetClient.id,
|
|
664
|
+
resourceId: authzResource._id,
|
|
665
|
+
});
|
|
666
|
+
expect(byResource).to.be.an('array');
|
|
667
|
+
|
|
668
|
+
const byScope = await keycloakManager.clients.listAllPermissionsByScope({
|
|
669
|
+
id: targetClient.id,
|
|
670
|
+
scopeId: authzScope._id || authzScope.id,
|
|
671
|
+
});
|
|
672
|
+
expect(byScope).to.be.an('array');
|
|
673
|
+
|
|
674
|
+
const permScopes = await keycloakManager.clients.listPermissionScope({
|
|
675
|
+
id: targetClient.id,
|
|
676
|
+
permissionId: authzPermission.id,
|
|
677
|
+
});
|
|
678
|
+
expect(permScopes).to.be.an('array');
|
|
679
|
+
|
|
680
|
+
const assocScopes = await keycloakManager.clients.getAssociatedScopes({
|
|
681
|
+
id: targetClient.id,
|
|
682
|
+
permissionId: authzPermission.id,
|
|
683
|
+
});
|
|
684
|
+
expect(assocScopes).to.be.an('array');
|
|
685
|
+
|
|
686
|
+
const assocPolicies = await keycloakManager.clients.getAssociatedPolicies({
|
|
687
|
+
id: targetClient.id,
|
|
688
|
+
permissionId: authzPermission.id,
|
|
689
|
+
});
|
|
690
|
+
expect(assocPolicies).to.be.an('array');
|
|
691
|
+
|
|
692
|
+
const assocResources = await keycloakManager.clients.getAssociatedResources({
|
|
693
|
+
id: targetClient.id,
|
|
694
|
+
permissionId: authzPermission.id,
|
|
695
|
+
});
|
|
696
|
+
expect(assocResources).to.be.an('array');
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it('should manage resource server settings and resources export/import', async function () {
|
|
700
|
+
const server = await keycloakManager.clients.getResourceServer({
|
|
701
|
+
id: targetClient.id,
|
|
702
|
+
});
|
|
703
|
+
expect(server).to.be.an('object');
|
|
704
|
+
|
|
705
|
+
await keycloakManager.clients.updateResourceServer(
|
|
706
|
+
{ id: targetClient.id },
|
|
707
|
+
{ decisionStrategy: 'AFFIRMATIVE' }
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
const exported = await keycloakManager.clients.exportResource({
|
|
711
|
+
id: targetClient.id,
|
|
712
|
+
resourceId: authzResource._id,
|
|
713
|
+
});
|
|
714
|
+
expect(exported).to.be.an('object');
|
|
715
|
+
|
|
716
|
+
try {
|
|
717
|
+
await keycloakManager.clients.importResource(
|
|
718
|
+
{ id: targetClient.id },
|
|
719
|
+
{
|
|
720
|
+
name: authzImportedResourceName,
|
|
721
|
+
scopes: [authzScope],
|
|
722
|
+
}
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
// Verify the imported resource was created
|
|
726
|
+
const allResources = await keycloakManager.clients.listResources({
|
|
727
|
+
id: targetClient.id,
|
|
728
|
+
});
|
|
729
|
+
const importedResource = allResources.find(r => r.name === authzImportedResourceName);
|
|
730
|
+
expect(importedResource).to.be.an('object');
|
|
731
|
+
} catch (err) {
|
|
732
|
+
// importResource may not work as expected in all Keycloak versions
|
|
733
|
+
console.log(` Skipping importResource verification: ${err.message}`);
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('should manage authorization scope updates', async function () {
|
|
738
|
+
await keycloakManager.clients.updateAuthorizationScope(
|
|
739
|
+
{ id: targetClient.id, scopeId: authzScope._id || authzScope.id },
|
|
740
|
+
{ name: authzScopeName, displayName: `${authzScopeName}-display` }
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
const scope = await keycloakManager.clients.getAuthorizationScope({
|
|
744
|
+
id: targetClient.id,
|
|
745
|
+
scopeId: authzScope._id || authzScope.id,
|
|
746
|
+
});
|
|
747
|
+
expect(scope).to.be.an('object');
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('should manage keys when supported', async function () {
|
|
753
|
+
try {
|
|
754
|
+
const info = await keycloakManager.clients.getKeyInfo({
|
|
755
|
+
id: targetClient.id,
|
|
756
|
+
});
|
|
757
|
+
expect(info).to.be.an('object');
|
|
758
|
+
|
|
759
|
+
await keycloakManager.clients.generateKey({
|
|
760
|
+
id: targetClient.id,
|
|
761
|
+
attr: 'jwt.credential',
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
await keycloakManager.clients.generateAndDownloadKey(
|
|
765
|
+
{ id: targetClient.id, attr: 'jwt.credential' },
|
|
766
|
+
{
|
|
767
|
+
format: 'JKS',
|
|
768
|
+
keyAlias: 'test',
|
|
769
|
+
keyPassword: 'password',
|
|
770
|
+
storePassword: 'password',
|
|
771
|
+
}
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
await keycloakManager.clients.downloadKey(
|
|
775
|
+
{ id: targetClient.id, attr: 'jwt.credential' },
|
|
776
|
+
{
|
|
777
|
+
format: 'JKS',
|
|
778
|
+
keyAlias: 'test',
|
|
779
|
+
keyPassword: 'password',
|
|
780
|
+
storePassword: 'password',
|
|
781
|
+
}
|
|
782
|
+
);
|
|
783
|
+
} catch (err) {
|
|
784
|
+
if (shouldSkipFeature(err)) {
|
|
785
|
+
this.test.title += ' (keys not supported)';
|
|
786
|
+
this.skip();
|
|
787
|
+
}
|
|
788
|
+
throw err;
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
});
|