keycloak-api-manager 4.0.1 → 5.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/Handlers/attackDetectionHandler.js +64 -0
- package/Handlers/clientPoliciesHandler.js +120 -0
- package/Handlers/groupsHandler.js +32 -0
- package/Handlers/organizationsHandler.js +243 -0
- package/Handlers/serverInfoHandler.js +36 -0
- package/Handlers/userProfileHandler.js +121 -0
- package/README.md +83 -7157
- package/docs/architecture.md +47 -0
- package/docs/deployment.md +32 -0
- package/docs/keycloak-setup.md +47 -0
- package/docs/test-configuration.md +43 -0
- package/docs/testing.md +60 -0
- package/index.js +156 -250
- package/package.json +28 -15
- package/test/.mocharc.json +2 -2
- package/test/config/secrets.json +12 -0
- package/test/docker-keycloak/certs/keycloak.crt +58 -0
- package/test/docker-keycloak/certs/keycloak.key +28 -0
- package/test/docker-keycloak/docker-compose-https.yml +2 -0
- package/test/docker-keycloak/docker-compose.yml +4 -4
- package/test/helpers/matrix.js +16 -0
- package/test/matrix/auth.json +27 -0
- package/test/matrix/clients.json +45 -0
- package/test/matrix/realms-components-idp.json +37 -0
- package/test/matrix/users-roles-groups.json +26 -0
- package/test/package-lock.json +3032 -0
- package/test/specs/attackDetection.test.js +102 -0
- package/test/specs/clientCredentials.test.js +3 -0
- package/test/specs/clientPolicies.test.js +162 -0
- package/test/specs/{debugClientLibrary.test.js → diagnostics/debugClientLibrary.test.js} +2 -2
- package/test/specs/groupPermissions.test.js +87 -0
- package/test/specs/matrix/matrix-auth.test.js +112 -0
- package/test/specs/matrix/matrix-clients.test.js +59 -0
- package/test/specs/matrix/matrix-realms-components-idp.test.js +111 -0
- package/test/specs/matrix/matrix-users-roles-groups.test.js +68 -0
- package/test/specs/organizations.test.js +183 -0
- package/test/specs/serverInfo.test.js +140 -0
- package/test/specs/userProfile.test.js +135 -0
- package/test/{enableServerFeatures.js → support/enableServerFeatures.js} +43 -26
- package/test/{setup.js → support/setup.js} +3 -3
- package/test/support/testConfig.js +69 -0
- package/test/testConfig.js +1 -69
- package/test-output.log +72 -0
- package/test/TESTING.md +0 -327
- package/test/config/CONFIGURATION.md +0 -170
- package/test/diagnostic-protocol-mappers.js +0 -189
- package/test/docker-keycloak/DEPLOYMENT_GUIDE.md +0 -262
- package/test/helpers/setup.js +0 -186
|
@@ -0,0 +1,68 @@
|
|
|
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 keycloakManager = require('keycloak-api-manager');
|
|
8
|
+
const { TEST_REALM } = require('../../testConfig');
|
|
9
|
+
const { loadMatrix, uniqueName } = require('../../helpers/matrix');
|
|
10
|
+
|
|
11
|
+
describe('Matrix - Users, Roles, Groups', function () {
|
|
12
|
+
this.timeout(30000);
|
|
13
|
+
|
|
14
|
+
const matrix = loadMatrix('users-roles-groups');
|
|
15
|
+
|
|
16
|
+
before(function () {
|
|
17
|
+
keycloakManager.setConfig({ realmName: TEST_REALM });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
matrix.cases.forEach((testCase) => {
|
|
21
|
+
it(`user-role-group case: ${testCase.name}`, async function () {
|
|
22
|
+
const roleName = uniqueName(`matrix-role-${testCase.name}`);
|
|
23
|
+
const groupName = uniqueName(`matrix-group-${testCase.name}`);
|
|
24
|
+
const username = uniqueName(`matrix-user-${testCase.name}`);
|
|
25
|
+
const email = `${username}@example.test`;
|
|
26
|
+
|
|
27
|
+
await keycloakManager.roles.create({
|
|
28
|
+
name: roleName,
|
|
29
|
+
description: `Role for ${testCase.name}`,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const group = await keycloakManager.groups.create({
|
|
33
|
+
name: groupName,
|
|
34
|
+
attributes: { description: ['Matrix group'] },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const user = await keycloakManager.users.create({
|
|
38
|
+
username,
|
|
39
|
+
email,
|
|
40
|
+
enabled: testCase.user.enabled,
|
|
41
|
+
emailVerified: testCase.user.emailVerified,
|
|
42
|
+
firstName: 'Matrix',
|
|
43
|
+
lastName: 'User',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await keycloakManager.users.addToGroup({
|
|
47
|
+
id: user.id,
|
|
48
|
+
groupId: group.id,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const role = await keycloakManager.roles.findOneByName({ name: roleName });
|
|
52
|
+
await keycloakManager.users.addRealmRoleMappings({
|
|
53
|
+
id: user.id,
|
|
54
|
+
roles: [role],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const userGroups = await keycloakManager.users.listGroups({ id: user.id });
|
|
58
|
+
expect(userGroups.map((g) => g.id)).to.include(group.id);
|
|
59
|
+
|
|
60
|
+
const roleMappings = await keycloakManager.users.listRealmRoleMappings({ id: user.id });
|
|
61
|
+
expect(roleMappings.map((r) => r.name)).to.include(roleName);
|
|
62
|
+
|
|
63
|
+
await keycloakManager.users.del({ id: user.id });
|
|
64
|
+
await keycloakManager.groups.del({ id: group.id });
|
|
65
|
+
await keycloakManager.roles.delByName({ name: roleName });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
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('../../index');
|
|
9
|
+
const { TEST_REALM, generateUniqueName } = require('../testConfig');
|
|
10
|
+
|
|
11
|
+
const config = {
|
|
12
|
+
baseUrl: conf.keycloak.baseUrl,
|
|
13
|
+
realmName: conf.keycloak.realmName,
|
|
14
|
+
clientId: conf.keycloak.clientId,
|
|
15
|
+
clientSecret: conf.keycloak.clientSecret,
|
|
16
|
+
grantType: conf.keycloak.grantType,
|
|
17
|
+
username: conf.keycloak.username,
|
|
18
|
+
password: conf.keycloak.password,
|
|
19
|
+
tokenLifeSpan: conf.keycloak.tokenLifeSpan
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Integration tests for Organizations Handler
|
|
24
|
+
* Tests organization CRUD, members, identity providers (Keycloak 25+)
|
|
25
|
+
*/
|
|
26
|
+
describe('Organizations Handler Tests', function () {
|
|
27
|
+
this.timeout(10000);
|
|
28
|
+
|
|
29
|
+
const testOrgData = {
|
|
30
|
+
name: `test-org-${Date.now()}`,
|
|
31
|
+
domains: [],
|
|
32
|
+
attributes: {
|
|
33
|
+
customAttr: ['value1']
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let createdOrgId;
|
|
38
|
+
let testUserId;
|
|
39
|
+
|
|
40
|
+
before(async function () {
|
|
41
|
+
await KeycloakManager.configure(config);
|
|
42
|
+
|
|
43
|
+
// Switch to test realm for operations
|
|
44
|
+
KeycloakManager.setConfig({ realmName: TEST_REALM });
|
|
45
|
+
|
|
46
|
+
// Create a test user for member operations in test realm
|
|
47
|
+
const userResult = await KeycloakManager.users.create({
|
|
48
|
+
username: `org-test-user-${Date.now()}`,
|
|
49
|
+
email: 'orguser@test.com',
|
|
50
|
+
enabled: true
|
|
51
|
+
});
|
|
52
|
+
testUserId = userResult.id;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
after(async function () {
|
|
56
|
+
// Clean up
|
|
57
|
+
if (createdOrgId) {
|
|
58
|
+
try {
|
|
59
|
+
await KeycloakManager.organizations.del({ id: createdOrgId });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
// Org might already be deleted
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (testUserId) {
|
|
65
|
+
try {
|
|
66
|
+
await KeycloakManager.users.del({ id: testUserId });
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// User might already be deleted
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
KeycloakManager.stop();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('Organization CRUD Operations', function () {
|
|
75
|
+
it('should create an organization', async function () {
|
|
76
|
+
const result = await KeycloakManager.organizations.create(testOrgData);
|
|
77
|
+
expect(result).to.have.property('id');
|
|
78
|
+
createdOrgId = result.id;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should find all organizations', async function () {
|
|
82
|
+
if (!createdOrgId) this.skip();
|
|
83
|
+
|
|
84
|
+
const orgs = await KeycloakManager.organizations.find({});
|
|
85
|
+
expect(orgs).to.be.an('array');
|
|
86
|
+
expect(orgs.length).to.be.greaterThan(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should find one organization by ID', async function () {
|
|
90
|
+
if (!createdOrgId) this.skip();
|
|
91
|
+
|
|
92
|
+
const org = await KeycloakManager.organizations.findOne({ id: createdOrgId });
|
|
93
|
+
expect(org).to.have.property('id', createdOrgId);
|
|
94
|
+
expect(org).to.have.property('name', testOrgData.name);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should update an organization', async function () {
|
|
98
|
+
if (!createdOrgId) this.skip();
|
|
99
|
+
|
|
100
|
+
const updatedData = {
|
|
101
|
+
attributes: {
|
|
102
|
+
customAttr: ['updatedValue']
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
await KeycloakManager.organizations.update({ id: createdOrgId }, updatedData);
|
|
107
|
+
|
|
108
|
+
const org = await KeycloakManager.organizations.findOne({ id: createdOrgId });
|
|
109
|
+
expect(org.attributes.customAttr).to.deep.equal(['updatedValue']);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('Organization Members', function () {
|
|
114
|
+
it('should add a member to organization', async function () {
|
|
115
|
+
if (!createdOrgId || !testUserId) this.skip();
|
|
116
|
+
|
|
117
|
+
await KeycloakManager.organizations.addMember({
|
|
118
|
+
id: createdOrgId,
|
|
119
|
+
userId: testUserId
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(true).to.be.true;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should list organization members', async function () {
|
|
126
|
+
if (!createdOrgId) this.skip();
|
|
127
|
+
|
|
128
|
+
const members = await KeycloakManager.organizations.listMembers({
|
|
129
|
+
id: createdOrgId
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(members).to.be.an('array');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should remove a member from organization', async function () {
|
|
136
|
+
if (!createdOrgId || !testUserId) this.skip();
|
|
137
|
+
|
|
138
|
+
await KeycloakManager.organizations.delMember({
|
|
139
|
+
id: createdOrgId,
|
|
140
|
+
userId: testUserId
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(true).to.be.true;
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('Organization Identity Providers', function () {
|
|
148
|
+
it('should list identity providers for organization', async function () {
|
|
149
|
+
if (!createdOrgId) this.skip();
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const idps = await KeycloakManager.organizations.listIdentityProviders({
|
|
153
|
+
id: createdOrgId
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(idps).to.be.an('array');
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// Method might not be available
|
|
159
|
+
if (error.response?.status === 404) {
|
|
160
|
+
this.skip();
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('Organization Deletion', function () {
|
|
168
|
+
it('should delete an organization', async function () {
|
|
169
|
+
if (!createdOrgId) this.skip();
|
|
170
|
+
|
|
171
|
+
await KeycloakManager.organizations.del({ id: createdOrgId });
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await KeycloakManager.organizations.findOne({ id: createdOrgId });
|
|
175
|
+
throw new Error('Organization should have been deleted');
|
|
176
|
+
} catch (error) {
|
|
177
|
+
expect(error.response?.status).to.equal(404);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
createdOrgId = null; // Prevent cleanup attempt
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
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('../../index');
|
|
9
|
+
const { TEST_REALM, generateUniqueName } = require('../testConfig');
|
|
10
|
+
|
|
11
|
+
const config = {
|
|
12
|
+
baseUrl: conf.keycloak.baseUrl,
|
|
13
|
+
realmName: conf.keycloak.realmName,
|
|
14
|
+
clientId: conf.keycloak.clientId,
|
|
15
|
+
clientSecret: conf.keycloak.clientSecret,
|
|
16
|
+
grantType: conf.keycloak.grantType,
|
|
17
|
+
username: conf.keycloak.username,
|
|
18
|
+
password: conf.keycloak.password,
|
|
19
|
+
tokenLifeSpan: conf.keycloak.tokenLifeSpan
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Integration tests for Server Info Handler
|
|
24
|
+
* Tests server information and metadata retrieval
|
|
25
|
+
*/
|
|
26
|
+
describe('Server Info Handler Tests', function () {
|
|
27
|
+
this.timeout(10000);
|
|
28
|
+
|
|
29
|
+
before(async function () {
|
|
30
|
+
await KeycloakManager.configure(config);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
after(async function () {
|
|
34
|
+
KeycloakManager.stop();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('Server Information', function () {
|
|
38
|
+
let serverInfo;
|
|
39
|
+
|
|
40
|
+
it('should get comprehensive server information', async function () {
|
|
41
|
+
serverInfo = await KeycloakManager.serverInfo.getInfo();
|
|
42
|
+
|
|
43
|
+
expect(serverInfo).to.exist;
|
|
44
|
+
expect(serverInfo).to.be.an('object');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should contain system information', async function () {
|
|
48
|
+
if (!serverInfo) this.skip();
|
|
49
|
+
|
|
50
|
+
expect(serverInfo).to.have.property('systemInfo');
|
|
51
|
+
expect(serverInfo.systemInfo).to.be.an('object');
|
|
52
|
+
|
|
53
|
+
// System info typically includes version, uptime, etc.
|
|
54
|
+
if (serverInfo.systemInfo.version) {
|
|
55
|
+
expect(serverInfo.systemInfo.version).to.be.a('string');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should contain memory information', async function () {
|
|
60
|
+
if (!serverInfo) this.skip();
|
|
61
|
+
|
|
62
|
+
expect(serverInfo).to.have.property('memoryInfo');
|
|
63
|
+
expect(serverInfo.memoryInfo).to.be.an('object');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should contain profile information', async function () {
|
|
67
|
+
if (!serverInfo) this.skip();
|
|
68
|
+
|
|
69
|
+
expect(serverInfo).to.have.property('profileInfo');
|
|
70
|
+
expect(serverInfo.profileInfo).to.be.an('object');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should contain available themes', async function () {
|
|
74
|
+
if (!serverInfo) this.skip();
|
|
75
|
+
|
|
76
|
+
expect(serverInfo).to.have.property('themes');
|
|
77
|
+
expect(serverInfo.themes).to.be.an('object');
|
|
78
|
+
|
|
79
|
+
// Themes should include login, account, admin, etc.
|
|
80
|
+
const themeTypes = Object.keys(serverInfo.themes);
|
|
81
|
+
expect(themeTypes.length).to.be.greaterThan(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should contain available providers', async function () {
|
|
85
|
+
if (!serverInfo) this.skip();
|
|
86
|
+
|
|
87
|
+
expect(serverInfo).to.have.property('providers');
|
|
88
|
+
expect(serverInfo.providers).to.be.an('object');
|
|
89
|
+
|
|
90
|
+
// Providers should include various SPIs
|
|
91
|
+
const providerTypes = Object.keys(serverInfo.providers);
|
|
92
|
+
expect(providerTypes.length).to.be.greaterThan(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should contain protocol mapper types', async function () {
|
|
96
|
+
if (!serverInfo) this.skip();
|
|
97
|
+
|
|
98
|
+
expect(serverInfo).to.have.property('protocolMapperTypes');
|
|
99
|
+
expect(serverInfo.protocolMapperTypes).to.be.an('object');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should contain component types', async function () {
|
|
103
|
+
if (!serverInfo) this.skip();
|
|
104
|
+
|
|
105
|
+
if (serverInfo.componentTypes) {
|
|
106
|
+
expect(serverInfo.componentTypes).to.be.an('object');
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should contain password policies', async function () {
|
|
111
|
+
if (!serverInfo) this.skip();
|
|
112
|
+
|
|
113
|
+
if (serverInfo.passwordPolicies) {
|
|
114
|
+
expect(serverInfo.passwordPolicies).to.be.an('array');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should contain enums', async function () {
|
|
119
|
+
if (!serverInfo) this.skip();
|
|
120
|
+
|
|
121
|
+
if (serverInfo.enums) {
|
|
122
|
+
expect(serverInfo.enums).to.be.an('object');
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should verify specific provider categories exist', async function () {
|
|
127
|
+
if (!serverInfo || !serverInfo.providers) this.skip();
|
|
128
|
+
|
|
129
|
+
const providers = serverInfo.providers;
|
|
130
|
+
|
|
131
|
+
// Check for common provider categories
|
|
132
|
+
// Note: These may vary by Keycloak version
|
|
133
|
+
const commonProviders = ['login-protocol', 'realm-cache', 'user-storage'];
|
|
134
|
+
const foundProviders = commonProviders.filter(p => providers[p]);
|
|
135
|
+
|
|
136
|
+
// At least some common providers should exist
|
|
137
|
+
expect(foundProviders.length).to.be.greaterThan(0);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
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('../../index');
|
|
9
|
+
const { TEST_REALM, generateUniqueName } = require('../testConfig');
|
|
10
|
+
|
|
11
|
+
const config = {
|
|
12
|
+
baseUrl: conf.keycloak.baseUrl,
|
|
13
|
+
realmName: conf.keycloak.realmName,
|
|
14
|
+
clientId: conf.keycloak.clientId,
|
|
15
|
+
clientSecret: conf.keycloak.clientSecret,
|
|
16
|
+
grantType: conf.keycloak.grantType,
|
|
17
|
+
username: conf.keycloak.username,
|
|
18
|
+
password: conf.keycloak.password,
|
|
19
|
+
tokenLifeSpan: conf.keycloak.tokenLifeSpan
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Integration tests for User Profile Handler
|
|
24
|
+
* Tests user profile configuration and metadata (Keycloak 15+)
|
|
25
|
+
*/
|
|
26
|
+
describe('User Profile Handler Tests', function () {
|
|
27
|
+
this.timeout(10000);
|
|
28
|
+
|
|
29
|
+
before(async function () {
|
|
30
|
+
await KeycloakManager.configure(config);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
after(async function () {
|
|
34
|
+
KeycloakManager.stop();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('User Profile Configuration', function () {
|
|
38
|
+
let originalConfig;
|
|
39
|
+
|
|
40
|
+
it('should get user profile configuration', async function () {
|
|
41
|
+
try {
|
|
42
|
+
const profileConfig = await KeycloakManager.userProfile.getConfiguration({});
|
|
43
|
+
|
|
44
|
+
expect(profileConfig).to.exist;
|
|
45
|
+
expect(profileConfig).to.have.property('attributes');
|
|
46
|
+
expect(profileConfig.attributes).to.be.an('array');
|
|
47
|
+
|
|
48
|
+
// Save original config for restoration
|
|
49
|
+
originalConfig = profileConfig;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// User Profile API only available in Keycloak 15+
|
|
52
|
+
if (error.response?.status === 404 || error.message?.includes('getProfile')) {
|
|
53
|
+
this.skip();
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should verify standard attributes exist in profile', async function () {
|
|
60
|
+
if (!originalConfig) this.skip();
|
|
61
|
+
|
|
62
|
+
const attributeNames = originalConfig.attributes.map(attr => attr.name);
|
|
63
|
+
|
|
64
|
+
// Standard attributes that should exist
|
|
65
|
+
expect(attributeNames).to.include('username');
|
|
66
|
+
expect(attributeNames).to.include('email');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should update user profile configuration', async function () {
|
|
70
|
+
if (!originalConfig) this.skip();
|
|
71
|
+
if (!originalConfig.attributes || !Array.isArray(originalConfig.attributes)) this.skip();
|
|
72
|
+
|
|
73
|
+
// Add a custom attribute to the configuration
|
|
74
|
+
const updatedConfig = JSON.parse(JSON.stringify(originalConfig));
|
|
75
|
+
|
|
76
|
+
// Check if test attribute already exists
|
|
77
|
+
const testAttrIndex = updatedConfig.attributes.findIndex(
|
|
78
|
+
attr => attr.name === 'testCustomAttribute'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (testAttrIndex === -1) {
|
|
82
|
+
// Add new test attribute
|
|
83
|
+
updatedConfig.attributes.push({
|
|
84
|
+
name: 'testCustomAttribute',
|
|
85
|
+
displayName: 'Test Custom Attribute',
|
|
86
|
+
validations: {},
|
|
87
|
+
permissions: {
|
|
88
|
+
view: ['admin', 'user'],
|
|
89
|
+
edit: ['admin']
|
|
90
|
+
},
|
|
91
|
+
multivalued: false
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await KeycloakManager.userProfile.updateConfiguration({}, updatedConfig);
|
|
97
|
+
|
|
98
|
+
// Verify the update
|
|
99
|
+
const verifyConfig = await KeycloakManager.userProfile.getConfiguration({});
|
|
100
|
+
const customAttrExists = verifyConfig.attributes.some(
|
|
101
|
+
attr => attr.name === 'testCustomAttribute'
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(customAttrExists).to.be.true;
|
|
105
|
+
|
|
106
|
+
// Restore original configuration
|
|
107
|
+
await KeycloakManager.userProfile.updateConfiguration({}, originalConfig);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// Restore on error
|
|
110
|
+
if (originalConfig) {
|
|
111
|
+
await KeycloakManager.userProfile.updateConfiguration({}, originalConfig);
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('User Profile Metadata', function () {
|
|
119
|
+
it('should get user profile metadata', async function () {
|
|
120
|
+
try {
|
|
121
|
+
const metadata = await KeycloakManager.userProfile.getMetadata({});
|
|
122
|
+
|
|
123
|
+
expect(metadata).to.exist;
|
|
124
|
+
// Metadata typically contains validators, attribute types, etc.
|
|
125
|
+
// Structure may vary by Keycloak version
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Metadata endpoint might not be available in all versions
|
|
128
|
+
if (error.response?.status === 404 || error.message?.includes('getProfileMetadata')) {
|
|
129
|
+
this.skip();
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -42,9 +42,9 @@ const path = require('path');
|
|
|
42
42
|
|
|
43
43
|
// Ensure NODE_ENV and config path are set before requiring testConfig
|
|
44
44
|
process.env.NODE_ENV = process.env.NODE_ENV || 'test';
|
|
45
|
-
process.env.PROPERTIES_PATH = path.join(__dirname, 'config');
|
|
45
|
+
process.env.PROPERTIES_PATH = path.join(__dirname, '../config');
|
|
46
46
|
|
|
47
|
-
const keycloakManager = require('
|
|
47
|
+
const keycloakManager = require('../../index');
|
|
48
48
|
const {
|
|
49
49
|
TEST_REALM,
|
|
50
50
|
TEST_CLIENT_ID,
|
|
@@ -103,11 +103,21 @@ async function enableServerFeatures() {
|
|
|
103
103
|
duplicateEmailsAllowed: false,
|
|
104
104
|
resetPasswordAllowed: true,
|
|
105
105
|
editUsernameAllowed: false,
|
|
106
|
-
bruteForceProtected: false
|
|
106
|
+
bruteForceProtected: false,
|
|
107
|
+
organizationsEnabled: true // Enable Organizations (requires 'organization' feature flag)
|
|
107
108
|
});
|
|
108
109
|
console.log(` ✓ Test realm created: ${TEST_REALM}`);
|
|
109
110
|
} else {
|
|
110
111
|
console.log(` ✓ Test realm already exists: ${TEST_REALM}`);
|
|
112
|
+
// Update existing realm to ensure all required settings are enabled
|
|
113
|
+
await keycloakManager.realms.update(
|
|
114
|
+
{ realm: TEST_REALM },
|
|
115
|
+
{
|
|
116
|
+
enabled: true,
|
|
117
|
+
bruteForceProtected: false,
|
|
118
|
+
organizationsEnabled: true
|
|
119
|
+
}
|
|
120
|
+
);
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
// Switch to test realm for all subsequent operations
|
|
@@ -201,56 +211,63 @@ async function enableServerFeatures() {
|
|
|
201
211
|
// 5. Create test group
|
|
202
212
|
console.log('\n5. Setting up test groups...');
|
|
203
213
|
const groups = await keycloakManager.groups.find();
|
|
214
|
+
let testGroupId;
|
|
204
215
|
if (!groups.some(g => g.name === TEST_GROUP_NAME)) {
|
|
205
|
-
await keycloakManager.groups.create({
|
|
216
|
+
const groupResult = await keycloakManager.groups.create({
|
|
206
217
|
name: TEST_GROUP_NAME,
|
|
207
218
|
attributes: {
|
|
208
219
|
description: ['Test group for API testing']
|
|
209
220
|
}
|
|
210
221
|
});
|
|
222
|
+
testGroupId = groupResult.id;
|
|
211
223
|
console.log(' ✓ Test group created');
|
|
212
224
|
} else {
|
|
225
|
+
testGroupId = groups.find(g => g.name === TEST_GROUP_NAME).id;
|
|
213
226
|
console.log(' ✓ Test group already exists');
|
|
214
227
|
}
|
|
215
228
|
|
|
216
229
|
// 6. Enable fine-grained admin permissions
|
|
217
230
|
console.log('\n6. Enabling fine-grained admin permissions...');
|
|
218
231
|
try {
|
|
219
|
-
|
|
232
|
+
// Enable users management permissions
|
|
233
|
+
const currentUserPerms = await keycloakManager.realms.getUsersManagementPermissions({
|
|
220
234
|
realm: TEST_REALM
|
|
221
235
|
});
|
|
222
236
|
|
|
223
|
-
if (!
|
|
237
|
+
if (!currentUserPerms.enabled) {
|
|
224
238
|
await keycloakManager.realms.updateUsersManagementPermissions({
|
|
225
239
|
realm: TEST_REALM,
|
|
226
240
|
enabled: true
|
|
227
241
|
});
|
|
228
|
-
console.log(' ✓
|
|
242
|
+
console.log(' ✓ Users fine-grained admin permissions enabled');
|
|
229
243
|
} else {
|
|
230
|
-
console.log(' ✓
|
|
244
|
+
console.log(' ✓ Users fine-grained admin permissions already enabled');
|
|
231
245
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
+
|
|
247
|
+
// Enable groups management permissions for the test group
|
|
248
|
+
if (testGroupId) {
|
|
249
|
+
try {
|
|
250
|
+
const groupPerms = await keycloakManager.groups.listPermissions({ id: testGroupId });
|
|
251
|
+
if (!groupPerms.enabled) {
|
|
252
|
+
await keycloakManager.groups.setPermissions(
|
|
253
|
+
{ id: testGroupId },
|
|
254
|
+
{ enabled: true }
|
|
255
|
+
);
|
|
256
|
+
console.log(' ✓ Groups fine-grained permissions enabled');
|
|
257
|
+
} else {
|
|
258
|
+
console.log(' ✓ Groups fine-grained permissions already enabled');
|
|
259
|
+
}
|
|
260
|
+
} catch (groupPermErr) {
|
|
261
|
+
console.log(` ⚠ Groups permissions: ${groupPermErr.message}`);
|
|
246
262
|
}
|
|
247
|
-
|
|
248
|
-
console.log(' ✓ Installation providers enabled');
|
|
263
|
+
}
|
|
249
264
|
} catch (err) {
|
|
250
|
-
console.log(` ⚠
|
|
265
|
+
console.log(` ⚠ Fine-grained permissions: ${err.message}`);
|
|
266
|
+
console.log(` ℹ This feature requires 'admin-fine-grained-authz' to be enabled`);
|
|
267
|
+
console.log(` in KC_FEATURES environment variable`);
|
|
251
268
|
}
|
|
252
269
|
|
|
253
|
-
// 7.
|
|
270
|
+
// 7. Realm features requiring server-side configuration
|
|
254
271
|
console.log('\n7. Enabling realm features...');
|
|
255
272
|
try {
|
|
256
273
|
// Note: Some features like installation providers cannot be enabled via API
|
|
@@ -31,7 +31,7 @@ const { exec } = require('child_process');
|
|
|
31
31
|
const path = require('path');
|
|
32
32
|
const fs = require('fs');
|
|
33
33
|
const os = require('os');
|
|
34
|
-
const keycloakManager = require('
|
|
34
|
+
const keycloakManager = require('../../index');
|
|
35
35
|
|
|
36
36
|
let sshTunnelProcess = null;
|
|
37
37
|
|
|
@@ -92,7 +92,7 @@ before(async function() {
|
|
|
92
92
|
console.log(`✓ SSH tunnel created: http://${sshTunnelUrl}\n`);
|
|
93
93
|
|
|
94
94
|
// Update config to use tunnel
|
|
95
|
-
const configPath = path.join(__dirname, 'config/local.json');
|
|
95
|
+
const configPath = path.join(__dirname, '../config/local.json');
|
|
96
96
|
let config = {};
|
|
97
97
|
|
|
98
98
|
if (fs.existsSync(configPath)) {
|
|
@@ -169,7 +169,7 @@ after(async function() {
|
|
|
169
169
|
|
|
170
170
|
// Clean up SSH tunnel config file if it was created
|
|
171
171
|
try {
|
|
172
|
-
const configPath = path.join(__dirname, 'config/local.json');
|
|
172
|
+
const configPath = path.join(__dirname, '../config/local.json');
|
|
173
173
|
if (fs.existsSync(configPath)) {
|
|
174
174
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
175
175
|
// Only delete local.json if it contains SSH tunnel config
|