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,315 @@
1
+ /**
2
+ * Enable Server Features - Shared Test Infrastructure Setup
3
+ *
4
+ * This script configures a shared Keycloak test realm with all necessary infrastructure
5
+ * before any tests run. It's called from setup.js in the global before() hook.
6
+ *
7
+ * Architecture:
8
+ * - Uses configuration from testConfig.js (loaded via propertiesmanager)
9
+ * - Creates infrastructure only if it doesn't already exist (idempotent)
10
+ * - All tests share this realm, avoiding repeated creation/deletion overhead
11
+ *
12
+ * Infrastructure Created:
13
+ * 1. Test Realm - Isolated environment for all tests
14
+ * 2. Test Client - Service account client for authentication tests
15
+ * 3. Test User - Standard user with credentials for user management tests
16
+ * 4. Test Roles - Realm roles for RBAC tests
17
+ * 5. Test Group - Group for group management tests
18
+ * 6. Fine-grained Permissions - Admin permissions if supported by server
19
+ * 7. Client Scope - Scope for protocol mapper and scope mapping tests
20
+ *
21
+ * Known Server-Side Limitations (Cannot be enabled via API):
22
+ * - Installation Providers: Requires server configuration, not API-configurable
23
+ * - Protocol Mappers: Requires protocol mapper providers to be installed/enabled at server level
24
+ * - Authorization Services: Some features require explicit server configuration
25
+ * - Consents Feature: Requires server-side configuration
26
+ *
27
+ * Tests that require these features will be marked as skipped with appropriate messages.
28
+ * These are not bugs - they are legitimate environmental constraints.
29
+ *
30
+ * Performance Impact:
31
+ * - Setup runs once: ~10-20 seconds
32
+ * - Per-test overhead saved: ~2-5 seconds per test
33
+ * - Total time saved: ~5-10 minutes for full suite (59 tests)
34
+ *
35
+ * Configuration Source:
36
+ * - test/config/default.json - Base config
37
+ * - test/config/secrets.json - Passwords
38
+ * - test/config/local.json - Optional developer overrides
39
+ */
40
+
41
+ const path = require('path');
42
+
43
+ // Ensure NODE_ENV and config path are set before requiring testConfig
44
+ process.env.NODE_ENV = process.env.NODE_ENV || 'test';
45
+ process.env.PROPERTIES_PATH = path.join(__dirname, 'config');
46
+
47
+ const keycloakManager = require('../index');
48
+ const {
49
+ TEST_REALM,
50
+ TEST_CLIENT_ID,
51
+ TEST_USER_USERNAME,
52
+ TEST_USER_PASSWORD,
53
+ TEST_USER_EMAIL,
54
+ TEST_USER_FIRSTNAME,
55
+ TEST_USER_LASTNAME,
56
+ TEST_ROLES,
57
+ TEST_GROUP_NAME,
58
+ TEST_CLIENT_SCOPE,
59
+ KEYCLOAK_CONFIG
60
+ } = require('./testConfig');
61
+
62
+ /**
63
+ * Main setup function - creates shared test infrastructure
64
+ * @returns {Promise<void>}
65
+ */
66
+ async function enableServerFeatures() {
67
+ const keycloakConfig = KEYCLOAK_CONFIG;
68
+
69
+ console.log('Configuring Keycloak Admin Client...');
70
+ await keycloakManager.configure({
71
+ baseUrl: keycloakConfig.baseUrl,
72
+ realmName: keycloakConfig.realmName,
73
+ clientId: keycloakConfig.clientId,
74
+ username: keycloakConfig.username,
75
+ password: keycloakConfig.password,
76
+ grantType: keycloakConfig.grantType
77
+ });
78
+
79
+ try {
80
+ // Step 1: Create or verify test realm
81
+ console.log(`\n1. Setting up test realm: ${TEST_REALM}`);
82
+ const existingRealms = await keycloakManager.realms.find();
83
+ const realmExists = existingRealms.some(r => r.realm === TEST_REALM);
84
+
85
+ if (!realmExists) {
86
+ await keycloakManager.realms.create({
87
+ realm: TEST_REALM,
88
+ enabled: true,
89
+ displayName: 'Keycloak API Manager Test Realm',
90
+ loginTheme: 'keycloak',
91
+ accountTheme: 'keycloak',
92
+ adminTheme: 'keycloak',
93
+ emailTheme: 'keycloak',
94
+ accessTokenLifespan: 3600,
95
+ ssoSessionIdleTimeout: 1800,
96
+ ssoSessionMaxLifespan: 36000,
97
+ offlineSessionIdleTimeout: 2592000,
98
+ registrationAllowed: false,
99
+ registrationEmailAsUsername: false,
100
+ rememberMe: true,
101
+ verifyEmail: false,
102
+ loginWithEmailAllowed: true,
103
+ duplicateEmailsAllowed: false,
104
+ resetPasswordAllowed: true,
105
+ editUsernameAllowed: false,
106
+ bruteForceProtected: false
107
+ });
108
+ console.log(` ✓ Test realm created: ${TEST_REALM}`);
109
+ } else {
110
+ console.log(` ✓ Test realm already exists: ${TEST_REALM}`);
111
+ }
112
+
113
+ // Switch to test realm for all subsequent operations
114
+ keycloakManager.setConfig({ realmName: TEST_REALM });
115
+
116
+ // 2. Create test client with all features enabled
117
+ console.log('\n2. Setting up test client...');
118
+ const clients = await keycloakManager.clients.find({ clientId: TEST_CLIENT_ID });
119
+ let testClient = clients.find(c => c.clientId === TEST_CLIENT_ID);
120
+
121
+ if (!testClient) {
122
+ const created = await keycloakManager.clients.create({
123
+ clientId: TEST_CLIENT_ID,
124
+ enabled: true,
125
+ protocol: 'openid-connect',
126
+ publicClient: false,
127
+ directAccessGrantsEnabled: true,
128
+ serviceAccountsEnabled: true,
129
+ authorizationServicesEnabled: true,
130
+ standardFlowEnabled: true,
131
+ implicitFlowEnabled: false,
132
+ bearerOnly: false,
133
+ consentRequired: false,
134
+ fullScopeAllowed: true,
135
+ redirectUris: ['http://localhost:*'],
136
+ webOrigins: ['*'],
137
+ attributes: {
138
+ 'client.secret.creation.time': Date.now().toString()
139
+ }
140
+ });
141
+ const allClients = await keycloakManager.clients.find({ clientId: TEST_CLIENT_ID });
142
+ testClient = allClients.find(c => c.clientId === TEST_CLIENT_ID);
143
+ console.log(` ✓ Test client created: ${TEST_CLIENT_ID}`);
144
+ } else {
145
+ // Update to ensure all features are enabled
146
+ await keycloakManager.clients.update({
147
+ id: testClient.id
148
+ }, {
149
+ authorizationServicesEnabled: true,
150
+ serviceAccountsEnabled: true,
151
+ publicClient: false,
152
+ directAccessGrantsEnabled: true,
153
+ standardFlowEnabled: true
154
+ });
155
+ console.log(` ✓ Test client updated: ${TEST_CLIENT_ID}`);
156
+ }
157
+
158
+ // 3. Create test user
159
+ console.log('\n3. Setting up test user...');
160
+ const users = await keycloakManager.users.find({ username: TEST_USER_USERNAME });
161
+ let testUser = users.find(u => u.username === TEST_USER_USERNAME);
162
+
163
+ if (!testUser) {
164
+ const created = await keycloakManager.users.create({
165
+ username: TEST_USER_USERNAME,
166
+ email: TEST_USER_EMAIL,
167
+ firstName: TEST_USER_FIRSTNAME,
168
+ lastName: TEST_USER_LASTNAME,
169
+ enabled: true,
170
+ emailVerified: true
171
+ });
172
+ await keycloakManager.users.resetPassword({
173
+ id: created.id,
174
+ credential: {
175
+ type: 'password',
176
+ value: TEST_USER_PASSWORD,
177
+ temporary: false
178
+ }
179
+ });
180
+ console.log(` ✓ Test user created: ${TEST_USER_USERNAME}`);
181
+ } else {
182
+ console.log(` ✓ Test user already exists: ${TEST_USER_USERNAME}`);
183
+ }
184
+
185
+ // 4. Create test roles
186
+ console.log('\n4. Setting up test roles...');
187
+ const existingRoles = await keycloakManager.roles.find();
188
+
189
+ for (const roleName of TEST_ROLES) {
190
+ if (!existingRoles.some(r => r.name === roleName)) {
191
+ await keycloakManager.roles.create({
192
+ name: roleName,
193
+ description: `${roleName} for testing`
194
+ });
195
+ console.log(` ✓ Role created: ${roleName}`);
196
+ } else {
197
+ console.log(` ✓ Role already exists: ${roleName}`);
198
+ }
199
+ }
200
+
201
+ // 5. Create test group
202
+ console.log('\n5. Setting up test groups...');
203
+ const groups = await keycloakManager.groups.find();
204
+ if (!groups.some(g => g.name === TEST_GROUP_NAME)) {
205
+ await keycloakManager.groups.create({
206
+ name: TEST_GROUP_NAME,
207
+ attributes: {
208
+ description: ['Test group for API testing']
209
+ }
210
+ });
211
+ console.log(' ✓ Test group created');
212
+ } else {
213
+ console.log(' ✓ Test group already exists');
214
+ }
215
+
216
+ // 6. Enable fine-grained admin permissions
217
+ console.log('\n6. Enabling fine-grained admin permissions...');
218
+ try {
219
+ const currentPerms = await keycloakManager.realms.getUsersManagementPermissions({
220
+ realm: TEST_REALM
221
+ });
222
+
223
+ if (!currentPerms.enabled) {
224
+ await keycloakManager.realms.updateUsersManagementPermissions({
225
+ realm: TEST_REALM,
226
+ enabled: true
227
+ });
228
+ console.log(' ✓ Fine-grained admin permissions enabled');
229
+ } else {
230
+ console.log(' ✓ Fine-grained admin permissions already enabled');
231
+ }
232
+ } catch (err) {
233
+ console.log(` ⚠ Fine-grained permissions: ${err.message}`);
234
+ console.log(` ℹ This is typically a server configuration setting that requires`);
235
+ console.log(` enabling "authorizationServicesEnabled" in realm settings`);
236
+ }
237
+
238
+ // 7. Update realm to enable protocol mappers and installation providers
239
+ console.log('\n7. Enabling realm features...');
240
+ try {
241
+ await keycloakManager.realms.update(
242
+ { realm: TEST_REALM },
243
+ {
244
+ installationProviders: ['docker-compose', 'docker', 'kubernetes', 'openshift'],
245
+ installationProvidersEnabled: true
246
+ }
247
+ );
248
+ console.log(' ✓ Installation providers enabled');
249
+ } catch (err) {
250
+ console.log(` ⚠ Installation providers: ${err.message}`);
251
+ }
252
+
253
+ // 7. Update realm to enable protocol mappers and installation providers
254
+ console.log('\n7. Enabling realm features...');
255
+ try {
256
+ // Note: Some features like installation providers cannot be enabled via API
257
+ // They require server-side configuration in keycloak configuration files
258
+ console.log(' ℹ Installation providers and protocol mappers require server configuration');
259
+ } catch (err) {
260
+ console.log(` ⚠ Realm features: ${err.message}`);
261
+ }
262
+
263
+ // 8. Create default client scopes
264
+ console.log('\n8. Setting up client scopes...');
265
+ const clientScopes = await keycloakManager.clientScopes.find();
266
+ if (!clientScopes.some(cs => cs.name === TEST_CLIENT_SCOPE)) {
267
+ await keycloakManager.clientScopes.create({
268
+ name: TEST_CLIENT_SCOPE,
269
+ description: 'Test client scope',
270
+ protocol: 'openid-connect'
271
+ });
272
+ console.log(' ✓ Test client scope created');
273
+ } else {
274
+ console.log(' ✓ Test client scope already exists');
275
+ }
276
+
277
+ // 9. Enable authorization services for the test client to support all permissions scenarios
278
+ console.log('\n9. Configuring fine-grained permissions for test client...');
279
+ try {
280
+ const clients = await keycloakManager.clients.find({ clientId: TEST_CLIENT_ID });
281
+ const client = clients.find(c => c.clientId === TEST_CLIENT_ID);
282
+
283
+ if (client) {
284
+ await keycloakManager.clients.updateFineGrainPermission(
285
+ { id: client.id },
286
+ { enabled: true }
287
+ );
288
+ console.log(' ✓ Fine-grained permissions enabled for test client');
289
+ }
290
+ } catch (err) {
291
+ console.log(` ⚠ Client fine-grained permissions: ${err.message}`);
292
+ }
293
+
294
+ console.log('\n✓ Keycloak server configuration complete!');
295
+ console.log(`\nTest realm: ${TEST_REALM}`);
296
+ console.log(`Test client: ${TEST_CLIENT_ID}`);
297
+ console.log(`Test user: ${TEST_USER_USERNAME}:${TEST_USER_PASSWORD}`);
298
+
299
+ } catch (error) {
300
+ console.error('\n✗ Error configuring Keycloak server:', error.message);
301
+ if (error.response?.data) {
302
+ console.error('Response data:', JSON.stringify(error.response.data, null, 2));
303
+ }
304
+ throw error;
305
+ } finally {
306
+ keycloakManager.stop();
307
+ }
308
+ }
309
+
310
+ module.exports = enableServerFeatures;
311
+
312
+ // Allow running standalone
313
+ if (require.main === module) {
314
+ enableServerFeatures();
315
+ }
@@ -0,0 +1,218 @@
1
+ const KcAdmClient = require('@keycloak/keycloak-admin-client').default;
2
+
3
+ /**
4
+ * Simple delay function
5
+ */
6
+ function delay(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+
10
+ /**
11
+ * PropertiesManager Configuration
12
+ *
13
+ * Structure:
14
+ * config/default.json - Base configuration (committed, safe values)
15
+ * config/local.json - Local overrides (git-ignored, auto-generated from Docker)
16
+ * config/secrets.json - Sensitive data (git-ignored, credentials)
17
+ *
18
+ * Priority (highest to lowest):
19
+ * 1. Environment variables (PM_KEYCLOAK_BASE_URL=...)
20
+ * 2. Command line (--keycloak.baseUrl=...)
21
+ * 3. config/secrets.json
22
+ * 4. config/local.json (auto-generated from Docker container)
23
+ * 5. config/default.json
24
+ *
25
+ * Note: local.json is automatically created from Docker container configuration
26
+ * when tests start up (see helpers/docker-helpers.js:updateConfigFromDocker)
27
+ */
28
+
29
+ let TEST_CONFIG = null;
30
+ let adminClient = null;
31
+ let propertiesLoaded = false;
32
+
33
+ /**
34
+ * Reset configuration cache (call when local.json is updated)
35
+ */
36
+ function resetConfig() {
37
+ TEST_CONFIG = null;
38
+ delete require.cache[require.resolve('propertiesmanager')];
39
+ propertiesLoaded = false;
40
+ }
41
+
42
+ /**
43
+ * Loads configuration from propertiesmanager
44
+ * Called lazily to ensure local.json is created from Docker first
45
+ */
46
+ function loadConfig() {
47
+ if (TEST_CONFIG) {
48
+ return TEST_CONFIG;
49
+ }
50
+
51
+ // Load propertiesmanager only when needed (after local.json is created)
52
+ // Using delete + require to force fresh load
53
+ if (propertiesLoaded) {
54
+ // Force reload by clearing cache
55
+ delete require.cache[require.resolve('propertiesmanager')];
56
+ }
57
+
58
+ const { conf } = require('propertiesmanager');
59
+ propertiesLoaded = true;
60
+
61
+ TEST_CONFIG = {
62
+ baseUrl: conf.keycloak?.baseUrl || 'http://localhost:8080',
63
+ realmName: conf.keycloak?.realm || 'master',
64
+ username: conf.keycloak?.adminUsername || 'admin',
65
+ password: conf.keycloak?.adminPassword || 'admin',
66
+ clientId: conf.keycloak?.clientId || 'admin-cli',
67
+ clientSecret: conf.keycloak?.clientSecret,
68
+ grantType: conf.keycloak?.grantType || 'password',
69
+ };
70
+
71
+ console.log('\n📍 Keycloak Configuration (from propertiesmanager):');
72
+ console.log(` Environment: ${process.env.NODE_ENV || 'test'}`);
73
+ console.log(` Base URL: ${TEST_CONFIG.baseUrl}`);
74
+ console.log(` Realm: ${TEST_CONFIG.realmName}`);
75
+ console.log(` ` + `Client ID: ${TEST_CONFIG.clientId}`);
76
+ console.log(` Grant Type: ${TEST_CONFIG.grantType}\n`);
77
+
78
+ return TEST_CONFIG;
79
+ }
80
+
81
+ /**
82
+ * Inizializza il client Keycloak admin
83
+ * Aspetta che Keycloak sia pronto prima di connettersi
84
+ */
85
+ async function initializeAdminClient() {
86
+ if (adminClient) {
87
+ return adminClient;
88
+ }
89
+
90
+ // Load configuration (after local.json has been created from Docker)
91
+ const config = loadConfig();
92
+
93
+ let retries = 30;
94
+ let lastError;
95
+
96
+ while (retries > 0) {
97
+ try {
98
+ adminClient = new KcAdmClient({
99
+ baseUrl: config.baseUrl,
100
+ realmName: config.realmName,
101
+ });
102
+
103
+ await adminClient.auth({
104
+ username: config.username,
105
+ password: config.password,
106
+ clientId: config.clientId,
107
+ grantType: config.grantType,
108
+ });
109
+
110
+ console.log('✓ Keycloak admin client initialized');
111
+ return adminClient;
112
+ } catch (err) {
113
+ lastError = err;
114
+ retries--;
115
+ if (retries > 0) {
116
+ console.log(`Waiting for Keycloak... (${retries} retries left)`);
117
+ await delay(2000);
118
+ } else {
119
+ console.error('OAuth2 Error Details:', {
120
+ message: err.message,
121
+ response: err.response?.data,
122
+ status: err.response?.status,
123
+ url: config.baseUrl
124
+ });
125
+ }
126
+ }
127
+ }
128
+
129
+ throw new Error(`Failed to connect to Keycloak after retries: ${lastError.message}`);
130
+ }
131
+
132
+ /**
133
+ * Crea il realm di test
134
+ */
135
+ async function setupTestRealm() {
136
+ const client = await initializeAdminClient();
137
+ const config = loadConfig();
138
+
139
+ // Switcha a master realm per creare il test realm
140
+ client.realmName = 'master';
141
+
142
+ try {
143
+ // Controlla se il realm esiste già
144
+ const realms = await client.realms.find();
145
+ const realmExists = realms.some((r) => r.realm === config.realmName);
146
+
147
+ if (!realmExists) {
148
+ await client.realms.create({
149
+ realm: config.realmName,
150
+ displayName: 'Test Realm',
151
+ enabled: true,
152
+ accessTokenLifespan: 3600,
153
+ refreshTokenMaxReuse: 0,
154
+ actionTokenGeneratedByAdminLifespan: 900,
155
+ actionTokenGeneratedByUserLifespan: 900,
156
+ });
157
+ console.log(`✓ Test realm '${config.realmName}' created`);
158
+ } else {
159
+ console.log(`✓ Test realm '${config.realmName}' already exists`);
160
+ }
161
+ } catch (err) {
162
+ if (err.response?.status === 409) {
163
+ console.log(`✓ Test realm '${config.realmName}' already exists`);
164
+ } else {
165
+ throw err;
166
+ }
167
+ }
168
+
169
+ // Switcha back al test realm
170
+ client.realmName = config.realmName;
171
+ }
172
+
173
+ /**
174
+ * Pulisce il realm di test
175
+ */
176
+ async function cleanupTestRealm() {
177
+ if (!adminClient) return;
178
+
179
+ const config = loadConfig();
180
+
181
+ try {
182
+ adminClient.realmName = 'master';
183
+ await adminClient.realms.del({ realm: config.realmName });
184
+ console.log(`✓ Test realm '${config.realmName}' deleted`);
185
+ } catch (err) {
186
+ if (err.response?.status !== 404) {
187
+ console.warn(`Warning: Failed to delete test realm: ${err.message}`);
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Ritorna il client admin configurato e autenticato
194
+ */
195
+ function getAdminClient() {
196
+ if (!adminClient) {
197
+ throw new Error('Admin client not initialized. Call initializeAdminClient() first');
198
+ }
199
+ return adminClient;
200
+ }
201
+
202
+ /**
203
+ * Reset del client (principalmente per i test)
204
+ */
205
+ function resetAdminClient() {
206
+ adminClient = null;
207
+ TEST_CONFIG = null; // Reset config too for fresh reload
208
+ }
209
+
210
+ module.exports = {
211
+ loadConfig,
212
+ initializeAdminClient,
213
+ setupTestRealm,
214
+ cleanupTestRealm,
215
+ getAdminClient,
216
+ resetAdminClient,
217
+ resetConfig,
218
+ };