strapi-plugin-keycloak-realm-users 1.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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +485 -0
  3. package/__tests__/constants.test.mjs +207 -0
  4. package/__tests__/mocks/strapi.mjs +182 -0
  5. package/__tests__/services/audit-log.test.mjs +283 -0
  6. package/__tests__/services/keycloak-client.test.mjs +651 -0
  7. package/__tests__/services/permission.test.mjs +374 -0
  8. package/__tests__/services/realm.test.mjs +415 -0
  9. package/__tests__/services/user.test.mjs +487 -0
  10. package/__tests__/utils/errors.test.mjs +109 -0
  11. package/admin/src/components/Initializer.jsx +14 -0
  12. package/admin/src/components/RealmBadge.jsx +17 -0
  13. package/admin/src/constants.js +14 -0
  14. package/admin/src/hooks/useAuditLogs.js +142 -0
  15. package/admin/src/hooks/useKeycloakRoles.js +182 -0
  16. package/admin/src/hooks/useKeycloakUsers.js +477 -0
  17. package/admin/src/hooks/useRealmAdmins.js +249 -0
  18. package/admin/src/hooks/useRealms.js +269 -0
  19. package/admin/src/index.js +46 -0
  20. package/admin/src/pages/App.jsx +21 -0
  21. package/admin/src/pages/AuditPage/index.jsx +213 -0
  22. package/admin/src/pages/RealmsPage/RealmEditPage.jsx +791 -0
  23. package/admin/src/pages/RealmsPage/RealmListPage.jsx +231 -0
  24. package/admin/src/pages/RealmsPage/index.jsx +7 -0
  25. package/admin/src/pages/UsersPage/UserEditPage.jsx +313 -0
  26. package/admin/src/pages/UsersPage/UserListPage.jsx +437 -0
  27. package/admin/src/pages/UsersPage/index.jsx +7 -0
  28. package/admin/src/pluginId.js +2 -0
  29. package/admin/src/translations/en.json +77 -0
  30. package/admin/src/translations/fr.json +77 -0
  31. package/babel.config.cjs +17 -0
  32. package/coverage/clover.xml +422 -0
  33. package/coverage/coverage-final.json +8 -0
  34. package/coverage/lcov-report/base.css +224 -0
  35. package/coverage/lcov-report/block-navigation.js +87 -0
  36. package/coverage/lcov-report/favicon.png +0 -0
  37. package/coverage/lcov-report/index.html +146 -0
  38. package/coverage/lcov-report/prettify.css +1 -0
  39. package/coverage/lcov-report/prettify.js +2 -0
  40. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  41. package/coverage/lcov-report/sorter.js +210 -0
  42. package/coverage/lcov-report/src/bootstrap.js.html +346 -0
  43. package/coverage/lcov-report/src/config/index.html +116 -0
  44. package/coverage/lcov-report/src/config/index.js.html +106 -0
  45. package/coverage/lcov-report/src/constants.js.html +850 -0
  46. package/coverage/lcov-report/src/content-types/audit-log/index.html +116 -0
  47. package/coverage/lcov-report/src/content-types/audit-log/index.js.html +94 -0
  48. package/coverage/lcov-report/src/content-types/index.html +116 -0
  49. package/coverage/lcov-report/src/content-types/index.js.html +112 -0
  50. package/coverage/lcov-report/src/content-types/realm-admin/index.html +116 -0
  51. package/coverage/lcov-report/src/content-types/realm-admin/index.js.html +94 -0
  52. package/coverage/lcov-report/src/content-types/realm-config/index.html +116 -0
  53. package/coverage/lcov-report/src/content-types/realm-config/index.js.html +94 -0
  54. package/coverage/lcov-report/src/controllers/audit.js.html +517 -0
  55. package/coverage/lcov-report/src/controllers/index.html +161 -0
  56. package/coverage/lcov-report/src/controllers/index.js.html +112 -0
  57. package/coverage/lcov-report/src/controllers/realm.js.html +1057 -0
  58. package/coverage/lcov-report/src/controllers/user.js.html +1324 -0
  59. package/coverage/lcov-report/src/destroy.js.html +100 -0
  60. package/coverage/lcov-report/src/index.html +116 -0
  61. package/coverage/lcov-report/src/policies/can-access-realm.js.html +163 -0
  62. package/coverage/lcov-report/src/policies/index.html +146 -0
  63. package/coverage/lcov-report/src/policies/index.js.html +106 -0
  64. package/coverage/lcov-report/src/policies/is-authenticated.js.html +100 -0
  65. package/coverage/lcov-report/src/register.js.html +106 -0
  66. package/coverage/lcov-report/src/routes/admin.js.html +844 -0
  67. package/coverage/lcov-report/src/routes/index.html +131 -0
  68. package/coverage/lcov-report/src/routes/index.js.html +109 -0
  69. package/coverage/lcov-report/src/services/audit-log.js.html +673 -0
  70. package/coverage/lcov-report/src/services/index.html +176 -0
  71. package/coverage/lcov-report/src/services/index.js.html +124 -0
  72. package/coverage/lcov-report/src/services/keycloak-client.js.html +2359 -0
  73. package/coverage/lcov-report/src/services/permission.js.html +955 -0
  74. package/coverage/lcov-report/src/services/realm.js.html +1207 -0
  75. package/coverage/lcov-report/src/services/user.js.html +1924 -0
  76. package/coverage/lcov-report/src/utils/errors.js.html +274 -0
  77. package/coverage/lcov-report/src/utils/index.html +116 -0
  78. package/coverage/lcov-report/src/utils/index.js.html +103 -0
  79. package/coverage/lcov.info +804 -0
  80. package/dist/_chunks/App-BaKrvCeS.mjs +1975 -0
  81. package/dist/_chunks/App-DO6syS77.js +1975 -0
  82. package/dist/_chunks/en-Li-XBDe9.mjs +72 -0
  83. package/dist/_chunks/en-aCyfgNfr.js +72 -0
  84. package/dist/_chunks/fr-Cj33Q8jI.js +72 -0
  85. package/dist/_chunks/fr-vLrXph-Z.mjs +72 -0
  86. package/dist/_chunks/index-DwDO4-0C.js +69 -0
  87. package/dist/_chunks/index-jTVd7LdQ.mjs +70 -0
  88. package/dist/admin/index.js +3 -0
  89. package/dist/admin/index.mjs +4 -0
  90. package/dist/server/index.js +3003 -0
  91. package/dist/server/index.mjs +3004 -0
  92. package/jest.config.cjs +50 -0
  93. package/package.json +55 -0
  94. package/server/src/bootstrap.js +87 -0
  95. package/server/src/config/index.js +7 -0
  96. package/server/src/constants.js +255 -0
  97. package/server/src/content-types/audit-log/index.js +3 -0
  98. package/server/src/content-types/audit-log/schema.json +61 -0
  99. package/server/src/content-types/index.js +9 -0
  100. package/server/src/content-types/realm-admin/index.js +3 -0
  101. package/server/src/content-types/realm-admin/schema.json +45 -0
  102. package/server/src/content-types/realm-config/index.js +3 -0
  103. package/server/src/content-types/realm-config/schema.json +56 -0
  104. package/server/src/controllers/audit.js +144 -0
  105. package/server/src/controllers/index.js +9 -0
  106. package/server/src/controllers/realm.js +324 -0
  107. package/server/src/controllers/user.js +413 -0
  108. package/server/src/destroy.js +5 -0
  109. package/server/src/index.js +21 -0
  110. package/server/src/policies/can-access-realm.js +26 -0
  111. package/server/src/policies/index.js +7 -0
  112. package/server/src/policies/is-authenticated.js +5 -0
  113. package/server/src/register.js +7 -0
  114. package/server/src/routes/admin.js +253 -0
  115. package/server/src/routes/index.js +8 -0
  116. package/server/src/services/audit-log.js +196 -0
  117. package/server/src/services/index.js +13 -0
  118. package/server/src/services/keycloak-client.js +758 -0
  119. package/server/src/services/permission.js +290 -0
  120. package/server/src/services/realm.js +374 -0
  121. package/server/src/services/user.js +613 -0
  122. package/server/src/utils/errors.js +63 -0
  123. package/server/src/utils/index.js +6 -0
@@ -0,0 +1,374 @@
1
+ /**
2
+ * @fileoverview Unit tests for the permission service.
3
+ * @module __tests__/services/permission
4
+ */
5
+
6
+ import permissionService from '../../server/src/services/permission.js';
7
+ import {
8
+ createMockStrapi,
9
+ createMockStrapiUser,
10
+ createMockRealmConfig,
11
+ createMockRealmAdmin,
12
+ } from '../mocks/strapi.mjs';
13
+ import { CONTENT_TYPES, DEFAULT_REALM_PERMISSIONS } from '../../server/src/constants.js';
14
+
15
+ describe('Permission Service', () => {
16
+ let strapi;
17
+ let service;
18
+
19
+ beforeEach(() => {
20
+ strapi = createMockStrapi();
21
+ service = permissionService({ strapi });
22
+ });
23
+
24
+ describe('isSuperAdmin', () => {
25
+ it('should return true for super admin users', () => {
26
+ const user = createMockStrapiUser({ isSuperAdmin: true });
27
+
28
+ expect(service.isSuperAdmin(user)).toBe(true);
29
+ });
30
+
31
+ it('should return false for regular users', () => {
32
+ const user = createMockStrapiUser({ isSuperAdmin: false });
33
+
34
+ expect(service.isSuperAdmin(user)).toBe(false);
35
+ });
36
+
37
+ it('should return false for null user', () => {
38
+ expect(service.isSuperAdmin(null)).toBe(false);
39
+ });
40
+
41
+ it('should return false for user without roles', () => {
42
+ const user = { id: 1, email: 'test@test.com' };
43
+
44
+ expect(service.isSuperAdmin(user)).toBe(false);
45
+ });
46
+
47
+ it('should return false for user with empty roles array', () => {
48
+ const user = createMockStrapiUser();
49
+ user.roles = [];
50
+
51
+ expect(service.isSuperAdmin(user)).toBe(false);
52
+ });
53
+ });
54
+
55
+ describe('getRealmAdminAssignment', () => {
56
+ it('should return assignment when found', async () => {
57
+ const assignment = createMockRealmAdmin();
58
+ strapi.documents.mockReturnValue({
59
+ findMany: jest.fn().mockResolvedValue([assignment]),
60
+ });
61
+
62
+ const result = await service.getRealmAdminAssignment(1, 'realm-doc-123');
63
+
64
+ expect(result).toEqual(assignment);
65
+ expect(strapi.documents).toHaveBeenCalledWith(CONTENT_TYPES.REALM_ADMIN);
66
+ });
67
+
68
+ it('should return null when no assignment exists', async () => {
69
+ strapi.documents.mockReturnValue({
70
+ findMany: jest.fn().mockResolvedValue([]),
71
+ });
72
+
73
+ const result = await service.getRealmAdminAssignment(1, 'realm-doc-123');
74
+
75
+ expect(result).toBeNull();
76
+ });
77
+
78
+ it('should query with correct filters', async () => {
79
+ const findManyMock = jest.fn().mockResolvedValue([]);
80
+ strapi.documents.mockReturnValue({ findMany: findManyMock });
81
+
82
+ await service.getRealmAdminAssignment(42, 'realm-xyz');
83
+
84
+ expect(findManyMock).toHaveBeenCalledWith({
85
+ filters: {
86
+ strapiUserId: 42,
87
+ realmConfig: { documentId: 'realm-xyz' },
88
+ },
89
+ populate: ['realmConfig'],
90
+ });
91
+ });
92
+ });
93
+
94
+ describe('canAccessRealm', () => {
95
+ it('should return true for super admin regardless of assignment', async () => {
96
+ const user = createMockStrapiUser({ isSuperAdmin: true });
97
+
98
+ const result = await service.canAccessRealm(user, 'any-realm', 'canDelete');
99
+
100
+ expect(result).toBe(true);
101
+ // Should not even query for assignments
102
+ expect(strapi.documents).not.toHaveBeenCalled();
103
+ });
104
+
105
+ it('should return true when user has the required permission', async () => {
106
+ const user = createMockStrapiUser();
107
+ const assignment = createMockRealmAdmin({
108
+ permissions: { canRead: true, canCreate: true },
109
+ });
110
+ strapi.documents.mockReturnValue({
111
+ findMany: jest.fn().mockResolvedValue([assignment]),
112
+ });
113
+
114
+ const result = await service.canAccessRealm(user, 'realm-123', 'canCreate');
115
+
116
+ expect(result).toBe(true);
117
+ });
118
+
119
+ it('should return false when user lacks the required permission', async () => {
120
+ const user = createMockStrapiUser();
121
+ const assignment = createMockRealmAdmin({
122
+ permissions: { canRead: true, canCreate: false },
123
+ });
124
+ strapi.documents.mockReturnValue({
125
+ findMany: jest.fn().mockResolvedValue([assignment]),
126
+ });
127
+
128
+ const result = await service.canAccessRealm(user, 'realm-123', 'canCreate');
129
+
130
+ expect(result).toBe(false);
131
+ });
132
+
133
+ it('should return false when user has no assignment', async () => {
134
+ const user = createMockStrapiUser();
135
+ strapi.documents.mockReturnValue({
136
+ findMany: jest.fn().mockResolvedValue([]),
137
+ });
138
+
139
+ const result = await service.canAccessRealm(user, 'realm-123', 'canRead');
140
+
141
+ expect(result).toBe(false);
142
+ });
143
+
144
+ it('should default to canRead permission', async () => {
145
+ const user = createMockStrapiUser();
146
+ const assignment = createMockRealmAdmin({
147
+ permissions: { canRead: true },
148
+ });
149
+ strapi.documents.mockReturnValue({
150
+ findMany: jest.fn().mockResolvedValue([assignment]),
151
+ });
152
+
153
+ const result = await service.canAccessRealm(user, 'realm-123');
154
+
155
+ expect(result).toBe(true);
156
+ });
157
+
158
+ it('should use default permissions when assignment has none', async () => {
159
+ const user = createMockStrapiUser();
160
+ const assignment = createMockRealmAdmin({ permissions: null });
161
+ strapi.documents.mockReturnValue({
162
+ findMany: jest.fn().mockResolvedValue([assignment]),
163
+ });
164
+
165
+ const result = await service.canAccessRealm(user, 'realm-123', 'canRead');
166
+
167
+ expect(result).toBe(DEFAULT_REALM_PERMISSIONS.canRead);
168
+ });
169
+ });
170
+
171
+ describe('getAccessibleRealms', () => {
172
+ it('should return all enabled realms with full permissions for super admin', async () => {
173
+ const user = createMockStrapiUser({ isSuperAdmin: true });
174
+ const realms = [
175
+ createMockRealmConfig({ name: 'realm-1' }),
176
+ createMockRealmConfig({ name: 'realm-2' }),
177
+ ];
178
+ strapi.documents.mockReturnValue({
179
+ findMany: jest.fn().mockResolvedValue(realms),
180
+ });
181
+
182
+ const result = await service.getAccessibleRealms(user);
183
+
184
+ expect(result).toHaveLength(2);
185
+ result.forEach((realm) => {
186
+ expect(realm.permissions).toEqual({
187
+ canRead: true,
188
+ canCreate: true,
189
+ canUpdate: true,
190
+ canDelete: true,
191
+ canManageRoles: true,
192
+ canResetPassword: true,
193
+ });
194
+ });
195
+ });
196
+
197
+ it('should return only assigned realms for regular users', async () => {
198
+ const user = createMockStrapiUser();
199
+ const assignments = [
200
+ createMockRealmAdmin({
201
+ realmConfig: createMockRealmConfig({ name: 'assigned-realm' }),
202
+ permissions: { canRead: true, canCreate: true },
203
+ }),
204
+ ];
205
+ strapi.documents.mockReturnValue({
206
+ findMany: jest.fn().mockResolvedValue(assignments),
207
+ });
208
+
209
+ const result = await service.getAccessibleRealms(user);
210
+
211
+ expect(result).toHaveLength(1);
212
+ expect(result[0].name).toBe('assigned-realm');
213
+ expect(result[0].permissions.canRead).toBe(true);
214
+ expect(result[0].permissions.canCreate).toBe(true);
215
+ });
216
+
217
+ it('should filter out assignments without realmConfig', async () => {
218
+ const user = createMockStrapiUser();
219
+ const assignments = [
220
+ createMockRealmAdmin({ realmConfig: null }),
221
+ createMockRealmAdmin({
222
+ realmConfig: createMockRealmConfig({ name: 'valid-realm' }),
223
+ }),
224
+ ];
225
+ strapi.documents.mockReturnValue({
226
+ findMany: jest.fn().mockResolvedValue(assignments),
227
+ });
228
+
229
+ const result = await service.getAccessibleRealms(user);
230
+
231
+ expect(result).toHaveLength(1);
232
+ expect(result[0].name).toBe('valid-realm');
233
+ });
234
+ });
235
+
236
+ describe('getRealmPermissions', () => {
237
+ it('should return full permissions for super admin', async () => {
238
+ const user = createMockStrapiUser({ isSuperAdmin: true });
239
+
240
+ const result = await service.getRealmPermissions(user, 'any-realm');
241
+
242
+ expect(result).toEqual({
243
+ canRead: true,
244
+ canCreate: true,
245
+ canUpdate: true,
246
+ canDelete: true,
247
+ canManageRoles: true,
248
+ canResetPassword: true,
249
+ });
250
+ });
251
+
252
+ it('should return assignment permissions for regular user', async () => {
253
+ const user = createMockStrapiUser();
254
+ const permissions = { canRead: true, canUpdate: true };
255
+ const assignment = createMockRealmAdmin({ permissions });
256
+ strapi.documents.mockReturnValue({
257
+ findMany: jest.fn().mockResolvedValue([assignment]),
258
+ });
259
+
260
+ const result = await service.getRealmPermissions(user, 'realm-123');
261
+
262
+ expect(result).toEqual(permissions);
263
+ });
264
+
265
+ it('should return null when user has no assignment', async () => {
266
+ const user = createMockStrapiUser();
267
+ strapi.documents.mockReturnValue({
268
+ findMany: jest.fn().mockResolvedValue([]),
269
+ });
270
+
271
+ const result = await service.getRealmPermissions(user, 'realm-123');
272
+
273
+ expect(result).toBeNull();
274
+ });
275
+ });
276
+
277
+ describe('assignUserToRealm', () => {
278
+ it('should create new assignment when none exists', async () => {
279
+ const createMock = jest.fn().mockResolvedValue({ documentId: 'new-assignment' });
280
+ strapi.documents.mockReturnValue({
281
+ findMany: jest.fn().mockResolvedValue([]),
282
+ create: createMock,
283
+ });
284
+
285
+ await service.assignUserToRealm(1, 'user@test.com', 'realm-123', { canCreate: true });
286
+
287
+ expect(createMock).toHaveBeenCalledWith({
288
+ data: {
289
+ strapiUserId: 1,
290
+ strapiUserEmail: 'user@test.com',
291
+ realmConfig: 'realm-123',
292
+ permissions: expect.objectContaining({ canCreate: true }),
293
+ },
294
+ });
295
+ });
296
+
297
+ it('should update existing assignment', async () => {
298
+ const existing = createMockRealmAdmin();
299
+ const updateMock = jest.fn().mockResolvedValue({ documentId: existing.documentId });
300
+ strapi.documents.mockReturnValue({
301
+ findMany: jest.fn().mockResolvedValue([existing]),
302
+ update: updateMock,
303
+ });
304
+
305
+ await service.assignUserToRealm(1, 'user@test.com', 'realm-123', { canDelete: true });
306
+
307
+ expect(updateMock).toHaveBeenCalledWith({
308
+ documentId: existing.documentId,
309
+ data: {
310
+ permissions: expect.objectContaining({ canDelete: true }),
311
+ },
312
+ });
313
+ });
314
+
315
+ it('should merge with default permissions', async () => {
316
+ const createMock = jest.fn().mockResolvedValue({ documentId: 'new' });
317
+ strapi.documents.mockReturnValue({
318
+ findMany: jest.fn().mockResolvedValue([]),
319
+ create: createMock,
320
+ });
321
+
322
+ await service.assignUserToRealm(1, 'user@test.com', 'realm-123', { canCreate: true });
323
+
324
+ const calledWith = createMock.mock.calls[0][0];
325
+ expect(calledWith.data.permissions.canRead).toBe(true); // from defaults
326
+ expect(calledWith.data.permissions.canCreate).toBe(true); // from override
327
+ });
328
+ });
329
+
330
+ describe('removeUserFromRealm', () => {
331
+ it('should delete assignment when it exists', async () => {
332
+ const assignment = createMockRealmAdmin();
333
+ const deleteMock = jest.fn().mockResolvedValue({});
334
+ strapi.documents.mockReturnValue({
335
+ findMany: jest.fn().mockResolvedValue([assignment]),
336
+ delete: deleteMock,
337
+ });
338
+
339
+ const result = await service.removeUserFromRealm(1, 'realm-123');
340
+
341
+ expect(result).toBe(true);
342
+ expect(deleteMock).toHaveBeenCalledWith({
343
+ documentId: assignment.documentId,
344
+ });
345
+ });
346
+
347
+ it('should return false when no assignment exists', async () => {
348
+ strapi.documents.mockReturnValue({
349
+ findMany: jest.fn().mockResolvedValue([]),
350
+ });
351
+
352
+ const result = await service.removeUserFromRealm(1, 'realm-123');
353
+
354
+ expect(result).toBe(false);
355
+ });
356
+ });
357
+
358
+ describe('getRealmAdmins', () => {
359
+ it('should return all admin assignments for a realm', async () => {
360
+ const assignments = [
361
+ createMockRealmAdmin({ strapiUserId: 1 }),
362
+ createMockRealmAdmin({ strapiUserId: 2 }),
363
+ ];
364
+ strapi.documents.mockReturnValue({
365
+ findMany: jest.fn().mockResolvedValue(assignments),
366
+ });
367
+
368
+ const result = await service.getRealmAdmins('realm-123');
369
+
370
+ expect(result).toHaveLength(2);
371
+ expect(strapi.documents).toHaveBeenCalledWith(CONTENT_TYPES.REALM_ADMIN);
372
+ });
373
+ });
374
+ });