@nu-art/permissions-backend 0.500.0 → 0.500.6

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 (88) hide show
  1. package/RequirePermission.d.ts +22 -10
  2. package/RequirePermission.js +24 -15
  3. package/_entity/access-group/ModuleBE_AccessGroupDB.d.ts +13 -0
  4. package/_entity/access-group/ModuleBE_AccessGroupDB.js +36 -0
  5. package/_entity/access-group/module-pack.d.ts +1 -0
  6. package/_entity/access-group/module-pack.js +3 -0
  7. package/_entity/permission-scope/ModuleBE_PermissionScopeDB.d.ts +6 -0
  8. package/_entity/permission-scope/ModuleBE_PermissionScopeDB.js +8 -0
  9. package/_entity/permission-scope/module-pack.d.ts +1 -0
  10. package/_entity/permission-scope/module-pack.js +3 -0
  11. package/_entity/user-permissions/ModuleBE_UserPermissionsAPI.d.ts +9 -0
  12. package/_entity/{permission-user/ModuleBE_PermissionUserAPI.js → user-permissions/ModuleBE_UserPermissionsAPI.js} +17 -16
  13. package/_entity/user-permissions/ModuleBE_UserPermissionsDB.d.ts +6 -0
  14. package/_entity/user-permissions/ModuleBE_UserPermissionsDB.js +8 -0
  15. package/_entity/user-permissions/module-pack.d.ts +2 -0
  16. package/_entity/user-permissions/module-pack.js +3 -0
  17. package/assertion-types.d.ts +9 -0
  18. package/consts.d.ts +7 -4
  19. package/consts.js +4 -2
  20. package/core/function-permission-registry.d.ts +5 -6
  21. package/core/function-permission-registry.js +10 -0
  22. package/core/module-pack.js +6 -7
  23. package/document-access-api.d.ts +6 -0
  24. package/document-access-api.js +49 -0
  25. package/document-access-enforcement.d.ts +9 -0
  26. package/document-access-enforcement.js +137 -0
  27. package/index.d.ts +12 -6
  28. package/index.js +12 -6
  29. package/modules/ModuleBE_Permissions.d.ts +63 -78
  30. package/modules/ModuleBE_Permissions.js +494 -441
  31. package/modules/ModuleBE_PermissionsAssert.d.ts +6 -54
  32. package/modules/ModuleBE_PermissionsAssert.js +60 -285
  33. package/package.json +14 -12
  34. package/PermissionKey_BE.d.ts +0 -16
  35. package/PermissionKey_BE.js +0 -59
  36. package/_entity/permission-access-level/ModuleBE_PermissionAccessLevelDB.d.ts +0 -13
  37. package/_entity/permission-access-level/ModuleBE_PermissionAccessLevelDB.js +0 -49
  38. package/_entity/permission-access-level/index.d.ts +0 -2
  39. package/_entity/permission-access-level/index.js +0 -2
  40. package/_entity/permission-access-level/module-pack.d.ts +0 -1
  41. package/_entity/permission-access-level/module-pack.js +0 -3
  42. package/_entity/permission-api/ModuleBE_PermissionAPIDB.d.ts +0 -10
  43. package/_entity/permission-api/ModuleBE_PermissionAPIDB.js +0 -62
  44. package/_entity/permission-api/index.d.ts +0 -2
  45. package/_entity/permission-api/index.js +0 -2
  46. package/_entity/permission-api/module-pack.d.ts +0 -1
  47. package/_entity/permission-api/module-pack.js +0 -3
  48. package/_entity/permission-domain/ModuleBE_PermissionDomainDB.d.ts +0 -9
  49. package/_entity/permission-domain/ModuleBE_PermissionDomainDB.js +0 -22
  50. package/_entity/permission-domain/index.d.ts +0 -2
  51. package/_entity/permission-domain/index.js +0 -2
  52. package/_entity/permission-domain/module-pack.d.ts +0 -1
  53. package/_entity/permission-domain/module-pack.js +0 -3
  54. package/_entity/permission-group/ModuleBE_PermissionGroupDB.d.ts +0 -12
  55. package/_entity/permission-group/ModuleBE_PermissionGroupDB.js +0 -65
  56. package/_entity/permission-group/index.d.ts +0 -2
  57. package/_entity/permission-group/index.js +0 -2
  58. package/_entity/permission-group/module-pack.d.ts +0 -1
  59. package/_entity/permission-group/module-pack.js +0 -3
  60. package/_entity/permission-project/ModuleBE_PermissionProjectDB.d.ts +0 -8
  61. package/_entity/permission-project/ModuleBE_PermissionProjectDB.js +0 -12
  62. package/_entity/permission-project/index.d.ts +0 -2
  63. package/_entity/permission-project/index.js +0 -2
  64. package/_entity/permission-project/module-pack.d.ts +0 -1
  65. package/_entity/permission-project/module-pack.js +0 -3
  66. package/_entity/permission-user/ModuleBE_PermissionUserAPI.d.ts +0 -9
  67. package/_entity/permission-user/ModuleBE_PermissionUserDB.d.ts +0 -34
  68. package/_entity/permission-user/ModuleBE_PermissionUserDB.js +0 -241
  69. package/_entity/permission-user/index.d.ts +0 -3
  70. package/_entity/permission-user/index.js +0 -3
  71. package/_entity/permission-user/module-pack.d.ts +0 -2
  72. package/_entity/permission-user/module-pack.js +0 -3
  73. package/_entity.d.ts +0 -12
  74. package/_entity.js +0 -18
  75. package/core/external-api-paths.d.ts +0 -13
  76. package/core/external-api-paths.js +0 -13
  77. package/core/utils.d.ts +0 -25
  78. package/core/utils.js +0 -85
  79. package/modules/consts.d.ts +0 -11
  80. package/modules/consts.js +0 -29
  81. package/modules/index.d.ts +0 -2
  82. package/modules/index.js +0 -20
  83. package/permissions-wire.d.ts +0 -46
  84. package/permissions-wire.js +0 -47
  85. package/permissions.d.ts +0 -22
  86. package/permissions.js +0 -152
  87. package/types.d.ts +0 -28
  88. /package/{types.js → assertion-types.js} +0 -0
@@ -1,458 +1,511 @@
1
- var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
2
- var useValue = arguments.length > 2;
3
- for (var i = 0; i < initializers.length; i++) {
4
- value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
5
- }
6
- return useValue ? value : void 0;
7
- };
8
- var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
9
- function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
10
- var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
11
- var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
12
- var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
13
- var _, done = false;
14
- for (var i = decorators.length - 1; i >= 0; i--) {
15
- var context = {};
16
- for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
17
- for (var p in contextIn.access) context.access[p] = contextIn.access[p];
18
- context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
19
- var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
20
- if (kind === "accessor") {
21
- if (result === void 0) continue;
22
- if (result === null || typeof result !== "object") throw new TypeError("Object expected");
23
- if (_ = accept(result.get)) descriptor.get = _;
24
- if (_ = accept(result.set)) descriptor.set = _;
25
- if (_ = accept(result.init)) initializers.unshift(_);
26
- }
27
- else if (_ = accept(result)) {
28
- if (kind === "field") initializers.unshift(_);
29
- else descriptor[key] = _;
30
- }
31
- }
32
- if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
- done = true;
34
- };
35
- import { _keys, arrayToMap, Dispatcher, filterInstances, flatArray, md5, Module, MUSTNeverHappenException, reduceToMap, } from '@nu-art/ts-common';
36
- import { ApiHandler, MemKey_ServerApi } from '@nu-art/http-server';
37
- import { RuntimeBE_Modules } from '@nu-art/db-api-backend';
38
- import { ApiDef_Permissions, DefaultAccessLevel_Admin, DefaultAccessLevel_Delete, DefaultAccessLevel_NoAccess, DefaultAccessLevel_Read, DefaultAccessLevel_Write, defaultLevelsRouteLookupWords, DuplicateDefaultAccessLevels, toPermissionDomainId, toPermissionGroupId, toPermissionProjectId, trimStartingForwardSlash, } from '@nu-art/permissions-shared';
39
- import { MemKey_AccountId, ModuleBE_SessionDB } from '@nu-art/user-account-backend';
40
- import { getRegisteredFunctionPermissions } from '../core/function-permission-registry.js';
41
- import { Domain_AccountManagement, Domain_Developer, Domain_PermissionsAssign, Domain_PermissionsDefine, PermissionsPackage_Developer, PermissionsPackage_Permissions } from '../permissions.js';
42
- import { ModuleBE_PermissionsAssert } from './ModuleBE_PermissionsAssert.js';
43
- import { getCreatePermissionKeyDefaults, getEnvConfigRef } from '../permissions-wire.js';
44
- import { ModuleBE_PermissionAccessLevelDB, ModuleBE_PermissionAPIDB, ModuleBE_PermissionDomainDB, ModuleBE_PermissionGroupDB, ModuleBE_PermissionProjectDB, ModuleBE_PermissionUserDB } from '../_entity.js';
45
- const dispatcher_collectPermissionsProjects = new Dispatcher('__collectPermissionsProjects');
46
- const GroupId_SuperAdmin = '8b54efda69b385a566735cca7be031d5';
47
- export const PermissionGroup_Permissions_SuperAdmin = {
48
- _id: toPermissionGroupId(GroupId_SuperAdmin),
49
- name: 'Super Admin',
50
- uiLabel: 'Super Admin',
51
- accessLevels: {
52
- [Domain_PermissionsDefine.namespace]: DefaultAccessLevel_Admin.name,
53
- [Domain_PermissionsAssign.namespace]: DefaultAccessLevel_Admin.name,
54
- [Domain_AccountManagement.namespace]: DefaultAccessLevel_Admin.name,
55
- [Domain_Developer.namespace]: DefaultAccessLevel_Admin.name,
56
- }
57
- };
58
- export const PermissionGroup_Permissions_Viewer = {
59
- _id: toPermissionGroupId('8c38d3bd2d76bbc37b5281f481c0bc1b'),
60
- name: 'Permissions Viewer',
61
- uiLabel: 'Permissions Viewer',
62
- accessLevels: {
63
- [Domain_AccountManagement.namespace]: DefaultAccessLevel_Read.name,
64
- [Domain_PermissionsDefine.namespace]: DefaultAccessLevel_Read.name,
65
- [Domain_PermissionsAssign.namespace]: DefaultAccessLevel_Read.name,
66
- }
67
- };
68
- export const PermissionGroup_Permissions_Editor = {
69
- _id: toPermissionGroupId('1524909cae174d0052b76a469b339218'),
70
- name: 'Permissions Editor',
71
- uiLabel: 'Permissions Editor',
72
- accessLevels: {
73
- [Domain_AccountManagement.namespace]: DefaultAccessLevel_Read.name,
74
- [Domain_PermissionsDefine.namespace]: DefaultAccessLevel_Read.name,
75
- [Domain_PermissionsAssign.namespace]: DefaultAccessLevel_Write.name,
76
- }
77
- };
78
- export const PermissionGroup_Account_Manager = {
79
- _id: toPermissionGroupId('6bb5feb12d0712ecee77f7f44188ec79'),
80
- name: 'Accounts Manager',
81
- uiLabel: 'Accounts Manager',
82
- accessLevels: {
83
- [Domain_AccountManagement.namespace]: DefaultAccessLevel_Write.name,
84
- }
85
- };
86
- export const PermissionGroup_Account_Admin = {
87
- _id: toPermissionGroupId('761a84bdde3f9be3fde9c50402a60401'),
88
- name: 'Accounts Admin',
89
- uiLabel: 'Accounts Admin',
90
- accessLevels: {
91
- [Domain_AccountManagement.namespace]: DefaultAccessLevel_Admin.name,
92
- }
93
- };
94
- export const PermissionGroup_Account_Viewer = {
95
- _id: toPermissionGroupId('7343853a980149ec94f967ac7ff4ccc3'),
96
- name: 'Accounts Viewer',
97
- uiLabel: 'Accounts Viewer',
98
- accessLevels: {
99
- [Domain_AccountManagement.namespace]: DefaultAccessLevel_Read.name,
100
- }
101
- };
102
- export const PermissionGroups_Permissions = [
103
- PermissionGroup_Permissions_SuperAdmin,
104
- PermissionGroup_Permissions_Viewer,
105
- PermissionGroup_Permissions_Editor,
106
- PermissionGroup_Account_Manager,
107
- PermissionGroup_Account_Admin,
108
- PermissionGroup_Account_Viewer,
109
- // {
110
- // _id: '60a417683e4016f4d933fee88953f0d5',
111
- // name: 'Permissions Read Self',
112
- // accessLevels: {
113
- // [Domain_PermissionsDefine.namespace]: PermissionsAccessLevel_ReadSelf.name,
114
- // [Domain_PermissionsAssign.namespace]: PermissionsAccessLevel_ReadSelf.name,
115
- // }
116
- // },
117
- ];
118
- export const PermissionProject_Permissions = {
119
- _id: toPermissionProjectId('f60db83936835e0be33e89caa365f0c3'),
120
- name: 'Permissions',
121
- packages: [PermissionsPackage_Permissions, PermissionsPackage_Developer],
122
- groups: PermissionGroups_Permissions
123
- };
124
- let ModuleBE_Permissions_Class = (() => {
125
- let _classSuper = Module;
126
- let _instanceExtraInitializers = [];
127
- let _toggleStrictMode_decorators;
128
- let _createProject_decorators;
129
- return class ModuleBE_Permissions_Class extends _classSuper {
130
- static {
131
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
132
- _toggleStrictMode_decorators = [ApiHandler(ApiDef_Permissions.toggleStrictMode)];
133
- _createProject_decorators = [ApiHandler(ApiDef_Permissions.createProject)];
134
- __esDecorate(this, null, _toggleStrictMode_decorators, { kind: "method", name: "toggleStrictMode", static: false, private: false, access: { has: obj => "toggleStrictMode" in obj, get: obj => obj.toggleStrictMode }, metadata: _metadata }, null, _instanceExtraInitializers);
135
- __esDecorate(this, null, _createProject_decorators, { kind: "method", name: "createProject", static: false, private: false, access: { has: obj => "createProject" in obj, get: obj => obj.createProject }, metadata: _metadata }, null, _instanceExtraInitializers);
136
- if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
137
- }
138
- init() {
139
- super.init();
140
- }
141
- async toggleStrictMode(_params) {
142
- const envConfigRef = getEnvConfigRef(ModuleBE_PermissionsAssert);
143
- if (!envConfigRef)
144
- return;
145
- MemKey_ServerApi.get().addPostCallAction(async () => {
146
- const currentConfig = await envConfigRef.get({});
147
- currentConfig.strictMode = !currentConfig.strictMode;
148
- await envConfigRef.set(currentConfig);
149
- });
150
- }
151
- async createProject(_params) {
152
- await this.__performProjectSetup().processor();
153
- }
154
- // __collectPermissionsProjects() {
155
- // return PermissionProject_Permissions;
156
- // }
157
- async __collectSessionData(data) {
158
- const permissionUserId = data.accountId;
159
- const permissionUser = await ModuleBE_PermissionUserDB.query.uniqueAssert(permissionUserId);
160
- const userGroups = filterInstances(await ModuleBE_PermissionGroupDB.query.all(permissionUser.groups.map(g => g.groupId)));
161
- const permissionMap = await this.getUserPermissionMap(userGroups);
162
- return {
163
- key: 'permissions', value: {
164
- domainToValueMap: permissionMap,
165
- roles: userGroups.map(group => ({ key: group.label, uiLabel: group.uiLabel })),
1
+ import { _keys, ApiException, batchActionParallel, Dispatcher, filterDuplicates, filterInstances, flatArray, Module } from '@nu-art/ts-common';
2
+ import { MemStorage } from '@nu-art/ts-common/mem-storage/MemStorage';
3
+ import { hashToUniqueId, stringToUniqueId } from '@nu-art/db-api-shared';
4
+ import { RuntimeBE_ModulesDB } from '@nu-art/db-api-backend';
5
+ import { AccessScope_Self, AllDocumentAccessKeys, getAllRegisteredScopes, getRegisteredGroupDefinitions, permissionScopeId } from '@nu-art/permissions-shared';
6
+ import { asSetupTaskKey } from '@nu-art/action-processor-backend';
7
+ import { getPermissionScopeValues } from '@nu-art/permissions-shared';
8
+ import { ModuleBE_PermissionScopeDB } from '../_entity/permission-scope/ModuleBE_PermissionScopeDB.js';
9
+ import { ModuleBE_UserPermissionsDB } from '../_entity/user-permissions/ModuleBE_UserPermissionsDB.js';
10
+ import { ModuleBE_AccessGroupDB } from '../_entity/access-group/ModuleBE_AccessGroupDB.js';
11
+ import { ModuleBE_Firebase } from '@nu-art/firebase-backend';
12
+ import { MemKey_ServiceAccountId, MemKey_UserAccessIds, MemKey_UserScopePermissions } from '../consts.js';
13
+ import { wireDocumentAccess } from '../document-access-enforcement.js';
14
+ import { ModuleBE_AccountDB } from '@nu-art/user-account-backend';
15
+ export const ServiceAccountId_Bootstrap = 'bootstrap-admin';
16
+ // --- Well-known group IDs ---
17
+ export const GroupId_AppDefault = hashToUniqueId('group/default');
18
+ export const GroupId_PermissionsAdmin = hashToUniqueId('group/permissions-admin');
19
+ const BootstrapSAGroupId = hashToUniqueId(ServiceAccountId_Bootstrap);
20
+ export const PermissionsInfraGroupIds = AllDocumentAccessKeys.reduce((ids, key) => {
21
+ ids[key] = hashToUniqueId(`permissions-infra:${key}`);
22
+ return ids;
23
+ }, {});
24
+ // --- Module ---
25
+ const dispatcher_resolveAdditionalGroupMemberships = new Dispatcher('__resolveAdditionalGroupMemberships');
26
+ export const SetupTaskKey_PermissionsGroups = asSetupTaskKey('permissions-groups');
27
+ class ModuleBE_Permissions_Class extends Module {
28
+ adminGrantFlagRef;
29
+ accessResolvers = new Map();
30
+ moduleScopeKeys = new Map();
31
+ constructor() {
32
+ super();
33
+ this.setDefaultConfig({
34
+ serviceAccounts: {
35
+ [ServiceAccountId_Bootstrap]: {
36
+ scopes: ['permissions-ui:view', 'access-group:create'],
37
+ enabled: true,
38
+ systemOnly: true,
166
39
  }
167
- };
168
- }
169
- getUserPermissionMap = (__runInitializers(this, _instanceExtraInitializers), async (userGroups) => {
170
- const permissionMap = {};
171
- const levelMaps = filterInstances(userGroups.map(i => i._levelsMap));
172
- levelMaps.forEach(levelMap => {
173
- _keys(levelMap).forEach(domainId => {
174
- if (!permissionMap[domainId])
175
- permissionMap[domainId] = 0;
176
- if (levelMap[domainId] > permissionMap[domainId])
177
- permissionMap[domainId] = levelMap[domainId];
178
- });
179
- });
180
- //All domains that are not defined for the user, are NoAccess by default.
181
- const allDomains = await ModuleBE_PermissionDomainDB.query.where({});
182
- allDomains.forEach(domain => {
183
- if (!permissionMap[domain._id])
184
- permissionMap[domain._id] = DefaultAccessLevel_NoAccess.value; //"fill in the gaps" - All domains that are not defined for the user, are NoAccess by default.
185
- });
186
- return permissionMap;
40
+ }
187
41
  });
188
- __performProjectSetup() {
42
+ }
43
+ permissionsAccessResolver = (item) => {
44
+ if (item._id === BootstrapSAGroupId)
189
45
  return {
190
- priority: 100,
191
- processor: async () => {
192
- const projects = dispatcher_collectPermissionsProjects.dispatchModule();
193
- projects.reduce((issues, project) => {
194
- return project.packages.reduce((issues, _package) => {
195
- return issues;
196
- }, issues);
197
- }, []);
198
- // Create All Projects
199
- await this.createPermissionProjects(projects);
200
- // Create domains/levels from function-permission registry (decorator-collected)
201
- if (projects.length > 0)
202
- await this.createDomainsAndLevelsFromFunctionPermissionRegistry(projects[0]._id);
203
- // Create all AppConfigs
204
- await this.createPermissionsKeys(projects);
205
- //Assign Super Admin if necessary
206
- await this.assignSuperAdmin();
46
+ __access: {
47
+ readers: [BootstrapSAGroupId],
48
+ writers: [BootstrapSAGroupId],
49
+ deleters: [],
50
+ owners: [],
207
51
  }
208
52
  };
209
- }
210
- /**
211
- * Creates domains and access levels from the function-permission registry (populated by @RequirePermission decorators).
212
- * New (scopeKey, value) pairs get domains/levels created; not assigned to anyone until explicitly assigned.
213
- */
214
- async createDomainsAndLevelsFromFunctionPermissionRegistry(projectId) {
215
- const defs = getRegisteredFunctionPermissions();
216
- if (defs.length === 0)
217
- return;
218
- this.logInfoBold('Creating domains/levels from function-permission registry');
219
- const _auditorId = MemKey_AccountId.get();
220
- const uniqueScopeKeys = [...new Set(defs.map(d => d.scopeKey))];
221
- const domainIdByScopeKey = {};
222
- for (const scopeKey of uniqueScopeKeys) {
223
- const domainId = toPermissionDomainId(md5(`function-permission-domain/${scopeKey}`));
224
- domainIdByScopeKey[scopeKey] = domainId;
225
- await ModuleBE_PermissionDomainDB.set.all([{
226
- _id: domainId,
227
- namespace: scopeKey,
228
- projectId,
229
- _auditorId
230
- }]);
231
- }
232
- const levelValueByName = {
233
- 'No-Access': DefaultAccessLevel_NoAccess.value,
234
- 'Read': DefaultAccessLevel_Read.value,
235
- 'Write': DefaultAccessLevel_Write.value,
236
- 'Delete': DefaultAccessLevel_Delete.value,
237
- 'Admin': DefaultAccessLevel_Admin.value
238
- };
239
- for (const def of defs) {
240
- const domainId = domainIdByScopeKey[def.scopeKey];
241
- const levelValue = levelValueByName[def.value] ?? 100;
242
- const levelId = md5(`${domainId}/${def.value}`);
243
- await ModuleBE_PermissionAccessLevelDB.set.all([{
244
- _id: levelId,
245
- domainId,
246
- name: def.value,
247
- value: levelValue,
248
- uiLabel: def.value,
249
- _auditorId
250
- }]);
251
- def.domainId = domainId;
252
- def.levelId = levelId;
253
- def.levelValue = levelValue;
53
+ return {
54
+ __access: {
55
+ readers: [GroupId_PermissionsAdmin],
56
+ writers: [GroupId_PermissionsAdmin],
57
+ deleters: [],
58
+ owners: [],
254
59
  }
255
- this.logInfoBold(`Created ${uniqueScopeKeys.length} domains and ${defs.length} access levels from function-permission registry`);
256
- }
257
- ;
258
- async createPermissionProjects(projects) {
259
- const map_nameToDBProject = await this.createProjects(projects);
260
- const map_nameToDbDomain = await this.createDomains(projects, map_nameToDBProject);
261
- const domainNameToLevelNameToDBAccessLevel = await this.createAccessLevels(projects, map_nameToDbDomain);
262
- await this.createGroups(projects, map_nameToDbDomain, domainNameToLevelNameToDBAccessLevel);
263
- await this.createApis(projects, domainNameToLevelNameToDBAccessLevel);
60
+ };
61
+ };
62
+ init() {
63
+ super.init();
64
+ this.adminGrantFlagRef = ModuleBE_Firebase.createModuleStateFirebaseRef(this, 'grantAdminOnLogin');
65
+ this.setAccessContextResolver(ModuleBE_AccessGroupDB, this.permissionsAccessResolver);
66
+ this.setAccessContextResolver(ModuleBE_PermissionScopeDB, this.permissionsAccessResolver);
67
+ this.setAccessContextResolver(ModuleBE_UserPermissionsDB, this.permissionsAccessResolver);
68
+ this.wireDocumentAccessToAllModules();
69
+ }
70
+ setAccessContextResolver(dbModule, resolver, scopeKeys) {
71
+ this.accessResolvers.set(dbModule.dbDef.dbKey, resolver);
72
+ if (scopeKeys)
73
+ this.moduleScopeKeys.set(dbModule.dbDef.dbKey, scopeKeys);
74
+ }
75
+ wireDocumentAccessToAllModules() {
76
+ for (const dbModule of RuntimeBE_ModulesDB()) {
77
+ const dbKey = dbModule.dbDef.dbKey;
78
+ wireDocumentAccess(dbModule, () => this.accessResolvers.get(dbKey), () => this.moduleScopeKeys.get(dbKey));
264
79
  }
265
- /**
266
- * Creates All the DB_PermissionProject
267
- *
268
- * @param projects - predefined permissions projects
269
- */
270
- async createProjects(projects) {
271
- this.logInfoBold('Creating Projects');
272
- const _auditorId = MemKey_AccountId.get();
273
- const preDBProjects = await ModuleBE_PermissionProjectDB.set.all(projects.map(project => ({
274
- _id: project._id,
275
- name: project.name,
276
- _auditorId
277
- })));
278
- const projectsMap_nameToDBProject = reduceToMap(preDBProjects, project => project.name, project => project);
279
- this.logInfoBold(`Created ${preDBProjects.length} Projects`);
280
- return projectsMap_nameToDBProject;
80
+ }
81
+ getAdminGrantFlagRef() {
82
+ return this.adminGrantFlagRef;
83
+ }
84
+ // --- Project setup ---
85
+ __performProjectSetup() {
86
+ return [{
87
+ key: SetupTaskKey_PermissionsGroups,
88
+ dependsOn: [],
89
+ processor: () => this.ensureDefinedGroups()
90
+ }];
91
+ }
92
+ async ensureDefinedGroups() {
93
+ await this.runAsServiceAccount(ServiceAccountId_Bootstrap, async () => {
94
+ this.logDebug('[FIRST_USER] bootstrap: starting ensureDefinedGroups');
95
+ await this.ensureBootstrapSAAccessGroup();
96
+ await this.ensureServiceAccountAccessGroups();
97
+ await this.ensurePermissionsInfraAccessGroups();
98
+ await this.ensureScopeEntities();
99
+ await this.ensureDefaultGroup();
100
+ await this.ensurePermissionsAdminGroup();
101
+ await this.ensureAppDefinedGroups();
102
+ await this.syncPersonalGroupsForExistingAccounts();
103
+ await this.recomputePermissionsForAllUsers();
104
+ this.logInfoBold('Recomputed UserPermissions for all users');
105
+ this.logDebug('[FIRST_USER] bootstrap: ensureDefinedGroups complete');
106
+ });
107
+ }
108
+ // --- Account lifecycle hooks ---
109
+ async __onUserLogin(account) {
110
+ await this.runAsServiceAccount(ServiceAccountId_Bootstrap, async () => {
111
+ await this.ensurePersonalAccessGroup(account);
112
+ await this.addToDefaultGroup(account);
113
+ await this.promoteIfNoAdmin(account);
114
+ await this.checkAdminGrantFlag(account);
115
+ await this.resolveAdditionalGroupMemberships(account, 'login');
116
+ await this.recomputePermissionsForUsers([account._id]);
117
+ });
118
+ }
119
+ async __onAccountDeleted(account) {
120
+ await this.runAsServiceAccount(ServiceAccountId_Bootstrap, async () => {
121
+ const personalGroupId = stringToUniqueId(account._id);
122
+ const personalGroup = await ModuleBE_AccessGroupDB.query.unique(personalGroupId);
123
+ if (personalGroup)
124
+ await ModuleBE_AccessGroupDB.delete.unique(personalGroupId);
125
+ const userPermId = stringToUniqueId(account._id);
126
+ const userPerm = await ModuleBE_UserPermissionsDB.query.unique(userPermId);
127
+ if (userPerm)
128
+ await ModuleBE_UserPermissionsDB.delete.unique(userPermId);
129
+ const allGroups = await ModuleBE_AccessGroupDB.query.where({});
130
+ const groupsContainingUser = allGroups.filter(g => g.members.includes(personalGroupId));
131
+ for (const group of groupsContainingUser) {
132
+ group.members = group.members.filter(m => m !== personalGroupId);
133
+ await ModuleBE_AccessGroupDB.set.item(group);
134
+ }
135
+ });
136
+ }
137
+ async ensurePersonalAccessGroup(account) {
138
+ const personalGroupId = stringToUniqueId(account._id);
139
+ const existing = await ModuleBE_AccessGroupDB.query.unique(personalGroupId);
140
+ this.logDebug(`[FIRST_USER] ensurePersonalAccessGroup: personalGroupId=${personalGroupId}, existing=${!!existing}`);
141
+ if (existing)
142
+ return;
143
+ await ModuleBE_AccessGroupDB.create.item({
144
+ _id: personalGroupId,
145
+ type: 'user',
146
+ key: AccessScope_Self,
147
+ label: `User (${account.email ?? account._id})`,
148
+ members: [],
149
+ });
150
+ const verify = await ModuleBE_AccessGroupDB.query.unique(personalGroupId);
151
+ this.logDebug(`[FIRST_USER] ensurePersonalAccessGroup: created, verified=${!!verify}`);
152
+ }
153
+ async addToDefaultGroup(account) {
154
+ const personalGroupId = stringToUniqueId(account._id);
155
+ const defaultGroup = await ModuleBE_AccessGroupDB.query.unique(GroupId_AppDefault);
156
+ this.logDebug(`[FIRST_USER] addToDefaultGroup: defaultGroup=${!!defaultGroup}, members=${defaultGroup?.members?.length}`);
157
+ if (!defaultGroup)
158
+ return;
159
+ if (defaultGroup.members.includes(personalGroupId))
160
+ return;
161
+ defaultGroup.members.push(personalGroupId);
162
+ await ModuleBE_AccessGroupDB.set.item(defaultGroup);
163
+ this.logDebug(`[FIRST_USER] addToDefaultGroup: user added, members now=${defaultGroup.members.length}`);
164
+ }
165
+ async promoteIfNoAdmin(account) {
166
+ const adminGroup = await ModuleBE_AccessGroupDB.query.unique(GroupId_PermissionsAdmin);
167
+ this.logDebug(`[FIRST_USER] promoteIfNoAdmin: adminGroup=${!!adminGroup}, members=${JSON.stringify(adminGroup?.members)}`);
168
+ if (!adminGroup) {
169
+ this.logDebug('[FIRST_USER] promoteIfNoAdmin: NO admin group found — returning');
170
+ return;
281
171
  }
282
- /**
283
- * Creates All the DB_PermissionDomains
284
- *
285
- * @param projects - predefined permissions projects
286
- * @param map_nameToDBProject
287
- */
288
- async createDomains(projects, map_nameToDBProject) {
289
- this.logInfoBold('Creating Domains');
290
- const _auditorId = MemKey_AccountId.get();
291
- const domainsToUpsert = flatArray(projects.map(project => project.packages.map(_package => _package.domains.map(domain => ({
292
- _id: domain._id,
293
- namespace: domain.namespace,
294
- projectId: map_nameToDBProject[project.name]._id,
295
- _auditorId
296
- })))));
297
- const dbDomain = await ModuleBE_PermissionDomainDB.set.all(domainsToUpsert);
298
- const domainsMap_nameToDbDomain = reduceToMap(dbDomain, domain => domain.namespace, domain => domain);
299
- this.logInfoBold(`Created ${dbDomain.length} Domains`);
300
- return domainsMap_nameToDbDomain;
172
+ const hasRealMembers = adminGroup.members.some(m => m !== BootstrapSAGroupId);
173
+ if (hasRealMembers) {
174
+ this.logDebug(`[FIRST_USER] promoteIfNoAdmin: admin already has non-SA members — returning`);
175
+ return;
301
176
  }
302
- /**
303
- * Creates All the DB_PermissionAccessLevel
304
- *
305
- * @param projects - predefined permissions projects
306
- * @param map_nameToDbDomain
307
- */
308
- async createAccessLevels(projects, map_nameToDbDomain) {
309
- this.logInfoBold('Creating Access Levels');
310
- const _auditorId = MemKey_AccountId.get();
311
- const levelsToUpsert = flatArray(projects.map(project => project.packages.map(_package => _package.domains.map(domain => {
312
- let levels = domain.levels ?? DuplicateDefaultAccessLevels(domain._id);
313
- return levels.map(level => {
314
- return {
315
- _id: level._id,
316
- domainId: map_nameToDbDomain[domain.namespace]._id,
317
- value: level.value,
318
- name: level.name,
319
- uiLabel: level.name,
320
- _auditorId
321
- };
322
- });
323
- }))));
324
- const dbLevels = await ModuleBE_PermissionAccessLevelDB.set.all(levelsToUpsert);
325
- const domainNameToLevelNameToDBAccessLevel = reduceToMap(dbLevels, level => level.domainId, (level, index, map) => {
326
- const domainLevels = map[level.domainId] || (map[level.domainId] = {});
327
- domainLevels[level.name] = level;
328
- return domainLevels;
329
- });
330
- this.logInfoBold(`Created ${dbLevels.length} Access Levels`);
331
- return domainNameToLevelNameToDBAccessLevel;
177
+ const personalGroupId = stringToUniqueId(account._id);
178
+ if (adminGroup.members.includes(personalGroupId))
179
+ return;
180
+ adminGroup.members.push(personalGroupId);
181
+ await ModuleBE_AccessGroupDB.set.item(adminGroup);
182
+ this.logDebug(`[FIRST_USER] promoteIfNoAdmin: promoted ${personalGroupId} to admin`);
183
+ }
184
+ async checkAdminGrantFlag(account) {
185
+ const flagValue = await this.adminGrantFlagRef.get(false);
186
+ if (!flagValue)
187
+ return;
188
+ const personalGroupId = stringToUniqueId(account._id);
189
+ const adminGroup = await ModuleBE_AccessGroupDB.query.unique(GroupId_PermissionsAdmin);
190
+ if (!adminGroup)
191
+ return;
192
+ if (adminGroup.members.includes(personalGroupId)) {
193
+ await this.adminGrantFlagRef.set(false);
194
+ return;
332
195
  }
333
- /**
334
- * Creates All the DB_PermissionGroup
335
- *
336
- * @param projects - predefined permissions projects
337
- * @param map_nameToDbDomain
338
- * @param domainNameToLevelNameToDBAccessLevel
339
- */
340
- async createGroups(projects, map_nameToDbDomain, domainNameToLevelNameToDBAccessLevel) {
341
- this.logInfoBold('Creating Groups');
342
- const _auditorId = MemKey_AccountId.get();
343
- const groupsToUpsert = flatArray(projects.map(project => {
344
- const groupsDef = flatArray([...project.packages.map(p => p.groups || []), ...project.groups || []]);
345
- return (groupsDef).map(group => {
346
- return {
347
- projectId: project._id,
348
- _id: group._id,
349
- _auditorId,
350
- label: group.name,
351
- uiLabel: group.name,
352
- accessLevelIds: _keys(group.accessLevels)
353
- .map(key => {
354
- const domainsMapNameToDbDomainElement = map_nameToDbDomain[key];
355
- if (!domainsMapNameToDbDomainElement)
356
- throw new MUSTNeverHappenException(`bah for key ${key}`);
357
- return domainNameToLevelNameToDBAccessLevel[domainsMapNameToDbDomainElement._id][group.accessLevels[key]]._id;
358
- })
359
- };
360
- });
196
+ adminGroup.members.push(personalGroupId);
197
+ await ModuleBE_AccessGroupDB.set.item(adminGroup);
198
+ await this.adminGrantFlagRef.set(false);
199
+ this.logInfo(`Granted Permissions Admin to user ${account._id} via RTDB flag (one-shot)`);
200
+ }
201
+ async resolveAdditionalGroupMemberships(account, context) {
202
+ const results = await dispatcher_resolveAdditionalGroupMemberships.dispatchModuleAsync(account._id, context);
203
+ const additionalGroupIds = filterDuplicates(flatArray(results));
204
+ if (additionalGroupIds.length === 0)
205
+ return;
206
+ const personalGroupId = stringToUniqueId(account._id);
207
+ const typedGroupIds = additionalGroupIds.map(id => stringToUniqueId(id));
208
+ const groups = filterInstances(await ModuleBE_AccessGroupDB.query.all(typedGroupIds));
209
+ const modifiedGroups = groups.filter(group => !group.members.includes(personalGroupId));
210
+ if (modifiedGroups.length === 0)
211
+ return;
212
+ modifiedGroups.forEach(group => group.members.push(personalGroupId));
213
+ await ModuleBE_AccessGroupDB.set.all(modifiedGroups);
214
+ modifiedGroups.forEach(group => this.logInfo(`Added user ${account._id} to group '${group.label}'`));
215
+ }
216
+ // --- Permission recomputation (materialized DB_UserPermissions) ---
217
+ async recomputePermissionsForUsers(accountIds) {
218
+ if (!accountIds.length)
219
+ return;
220
+ const allGroups = await ModuleBE_AccessGroupDB.query.where({});
221
+ this.logDebug(`[FIRST_USER] recomputePermissionsForUsers: accountIds=${JSON.stringify(accountIds)}, allGroups=${allGroups.length}`);
222
+ const entitiesToUpsert = await Promise.all(accountIds.map(async (accountId) => {
223
+ const personalGroupId = stringToUniqueId(accountId);
224
+ const { scopeEntries, accessIds } = await this.materializeFromGroups(personalGroupId, allGroups);
225
+ this.logDebug(`[FIRST_USER] materialized for ${accountId}: scopes=${scopeEntries.length}, accessIds=${JSON.stringify(accessIds)}`);
226
+ return {
227
+ _id: stringToUniqueId(accountId),
228
+ scopeEntries,
229
+ accessIds,
230
+ };
231
+ }));
232
+ if (entitiesToUpsert.length > 0)
233
+ await ModuleBE_UserPermissionsDB.set.all(entitiesToUpsert);
234
+ }
235
+ async recomputePermissionsForAllUsers() {
236
+ const allGroups = await ModuleBE_AccessGroupDB.query.where({});
237
+ const userGroups = allGroups.filter(g => g.type === 'user');
238
+ if (!userGroups.length)
239
+ return;
240
+ await batchActionParallel(userGroups, 50, async (batch) => {
241
+ const entitiesToUpsert = await Promise.all(batch.map(async (pg) => {
242
+ const { scopeEntries, accessIds } = await this.materializeFromGroups(pg._id, allGroups);
243
+ return {
244
+ _id: stringToUniqueId(pg._id),
245
+ scopeEntries,
246
+ accessIds,
247
+ };
361
248
  }));
362
- const dbGroups = await ModuleBE_PermissionGroupDB.set.all(groupsToUpsert);
363
- this.logInfoBold(`Created ${dbGroups.length} Groups`);
249
+ await ModuleBE_UserPermissionsDB.set.all(entitiesToUpsert);
250
+ });
251
+ }
252
+ async materializeFromGroups(personalGroupId, allGroups) {
253
+ const personalGroup = allGroups.find(g => g._id === personalGroupId);
254
+ const accessIds = {
255
+ [AccessScope_Self]: [personalGroupId],
256
+ };
257
+ if (!personalGroup)
258
+ return { scopeEntries: [], accessIds };
259
+ const reachableGroups = this.walkGroupGraphUp(personalGroupId, allGroups);
260
+ const allScopeIds = [];
261
+ if (personalGroup.scopeEntries?.length)
262
+ allScopeIds.push(...personalGroup.scopeEntries);
263
+ for (const group of reachableGroups) {
264
+ if (!accessIds[group.key])
265
+ accessIds[group.key] = [];
266
+ accessIds[group.key] = filterDuplicates([...accessIds[group.key], group._id]);
267
+ if (group.scopeEntries?.length)
268
+ allScopeIds.push(...group.scopeEntries);
364
269
  }
365
- /**
366
- * Creates All the DB_PermissionApi (path-based).
367
- * @deprecated API collection deprecated; use function-based permissions and @RequirePermission. Domains/levels from function-permission registry instead.
368
- * @param projects - predefined permissions projects
369
- * @param domainNameToLevelNameToDBAccessLevel
370
- */
371
- async createApis(projects, domainNameToLevelNameToDBAccessLevel) {
372
- this.logInfoBold('Creating APIs');
373
- const _auditorId = MemKey_AccountId.get();
374
- const apisToUpsert = flatArray(projects.map(project => {
375
- return project.packages.map(_package => _package.domains.map(domain => {
376
- const apis = [];
377
- apis.push(...(domain.customApis || []).map(api => ({
378
- projectId: project._id,
379
- path: trimStartingForwardSlash(api.path),
380
- _auditorId,
381
- accessLevelIds: [domainNameToLevelNameToDBAccessLevel[api.domainId ?? domain._id][api.accessLevel]._id]
382
- })));
383
- const apiModules = RuntimeBE_Modules();
384
- const apiModulesMap = arrayToMap(apiModules, m => m.dbModule.dbDef.dbKey);
385
- this.logDebug(_keys(apiModulesMap));
386
- const crudEndpoints = ['query', 'queryUnique', 'upsert', 'upsertAll', 'deleteUnique', 'deleteQuery', 'deleteAll'];
387
- const _apis = (domain.dbNames || []).map(dbName => {
388
- const apiModule = apiModulesMap[dbName];
389
- if (!apiModule)
390
- throw new MUSTNeverHappenException(`Could not find api module with dbName: ${dbName}`);
391
- const def = apiModule.crudApiDef;
392
- return filterInstances(crudEndpoints.map(key => {
393
- const endpoint = def[key];
394
- if (!endpoint?.path)
395
- return undefined;
396
- const accessLevelNameToAssign = defaultLevelsRouteLookupWords[endpoint.path.substring(endpoint.path.lastIndexOf('/') + 1)];
397
- const accessLevel = domainNameToLevelNameToDBAccessLevel[domain._id][accessLevelNameToAssign];
398
- if (!accessLevel)
399
- return undefined;
400
- return {
401
- projectId: project._id,
402
- path: endpoint.path,
403
- _auditorId,
404
- accessLevelIds: [accessLevel._id]
405
- };
406
- }));
407
- });
408
- apis.push(...flatArray(_apis));
409
- return apis;
410
- }));
411
- }));
412
- const dbApis = await ModuleBE_PermissionAPIDB.set.all(apisToUpsert);
413
- this.logInfoBold(`Created ${dbApis.length} APIs`);
270
+ const dedupedScopeIds = filterDuplicates(allScopeIds);
271
+ const scopeEntries = dedupedScopeIds.length > 0
272
+ ? await this.resolveScopeIdsToStrings(dedupedScopeIds)
273
+ : [];
274
+ return { scopeEntries, accessIds };
275
+ }
276
+ // --- Access group change handler ---
277
+ async __onAccessGroupChanged(changedGroupIds) {
278
+ await this.rematerializeForGroups(changedGroupIds);
279
+ }
280
+ async rematerializeForGroups(changedGroupIds) {
281
+ const allGroups = await ModuleBE_AccessGroupDB.query.where({});
282
+ const userGroups = allGroups.filter(g => g.type === 'user');
283
+ const affectedAccountIds = [];
284
+ for (const personalGroup of userGroups) {
285
+ const reachable = this.walkGroupGraphUp(personalGroup._id, allGroups);
286
+ const isAffected = reachable.some(g => changedGroupIds.includes(g._id));
287
+ if (isAffected)
288
+ affectedAccountIds.push(personalGroup._id);
414
289
  }
415
- /**
416
- * Creates permission keys associated with the given projects.
417
- *
418
- * @param projects - An array of projects.
419
- */
420
- async createPermissionsKeys(_projects) {
421
- this.logInfoBold('Creating App Config');
422
- const createDefaults = getCreatePermissionKeyDefaults();
423
- if (createDefaults) {
424
- try {
425
- await createDefaults(this);
426
- this.logInfoBold('Created Permission Key defaults.');
427
- }
428
- catch (e) {
429
- this.logErrorBold('Failed creating Permission Key defaults.', e);
430
- }
290
+ if (affectedAccountIds.length > 0)
291
+ await this.recomputePermissionsForUsers(affectedAccountIds);
292
+ }
293
+ walkGroupGraphUp(startGroupId, allGroups) {
294
+ const visited = new Set();
295
+ const queue = [startGroupId];
296
+ const result = [];
297
+ while (queue.length > 0) {
298
+ const currentId = queue.shift();
299
+ if (visited.has(currentId))
300
+ continue;
301
+ visited.add(currentId);
302
+ const parents = allGroups.filter(g => g.members.includes(currentId));
303
+ for (const parent of parents) {
304
+ result.push(parent);
305
+ queue.push(parent._id);
431
306
  }
432
- this.logInfoBold('Created App Config');
433
307
  }
434
- /**
435
- * If no "Super Admin" user is defined in the system!
436
- * The first user to press the create project button will become the "Super Admin" of the system
437
- *
438
- * If a "Super Admin" already exists in the system, a 403 will be thrown
439
- */
440
- assignSuperAdmin = async () => {
441
- this.logInfoBold('Assigning SuperAdmin permissions');
442
- const superAdminGroupId = toPermissionGroupId(GroupId_SuperAdmin);
443
- const existingSuperAdmin = (await ModuleBE_PermissionUserDB.query.custom({
444
- where: { __groupIds: { $ac: superAdminGroupId } },
445
- limit: 1
446
- }))[0];
447
- if (existingSuperAdmin)
448
- return;
449
- const accountId = MemKey_AccountId.get();
450
- const currentUser = await ModuleBE_PermissionUserDB.query.uniqueAssert(accountId);
451
- (currentUser.groups || (currentUser.groups = [])).push({ groupId: superAdminGroupId });
452
- await ModuleBE_PermissionUserDB.set.item(currentUser);
453
- await ModuleBE_SessionDB._session.rotate.reissue.bySession();
454
- this.logInfoBold('Assigned SuperAdmin permissions');
308
+ return result;
309
+ }
310
+ async resolveScopeIdsToStrings(scopeIds) {
311
+ const typedScopeIds = scopeIds.map(id => stringToUniqueId(id));
312
+ const scopeEntities = filterInstances(await ModuleBE_PermissionScopeDB.query.all(typedScopeIds));
313
+ return this.deduplicateScopeEntries(scopeEntities);
314
+ }
315
+ deduplicateScopeEntries(scopeEntities) {
316
+ const scopeMaxIdx = {};
317
+ for (const entity of scopeEntities) {
318
+ const scopeValues = getPermissionScopeValues(entity.key);
319
+ const valueIdx = scopeValues ? scopeValues.indexOf(entity.value) : -1;
320
+ const current = scopeMaxIdx[entity.key];
321
+ if (!current || valueIdx > current.idx)
322
+ scopeMaxIdx[entity.key] = { value: entity.value, idx: valueIdx };
323
+ }
324
+ return _keys(scopeMaxIdx).map(k => `${k}:${scopeMaxIdx[k].value}`);
325
+ }
326
+ // --- Service account elevation ---
327
+ async runAsServiceAccount(saId, action) {
328
+ const saConfig = this.config.serviceAccounts[saId];
329
+ if (!saConfig || !saConfig.enabled)
330
+ throw new ApiException(403, `Service account '${saId}' is not enabled`);
331
+ if (saConfig.systemOnly) {
332
+ const store = MemStorage.getStore();
333
+ if (store && MemKey_UserScopePermissions.peak() !== undefined)
334
+ throw new ApiException(403, `System-only service account '${saId}' cannot be used within a user context`);
335
+ }
336
+ const scopes = saId === ServiceAccountId_Bootstrap
337
+ ? this.resolveBootstrapScopes()
338
+ : saConfig.scopes;
339
+ const personalGroupId = hashToUniqueId(saId);
340
+ const accessIds = saId === ServiceAccountId_Bootstrap
341
+ ? this.resolveBootstrapAccessIds()
342
+ : await this.resolveSAAccessIds(personalGroupId);
343
+ const memStorage = new MemStorage();
344
+ return memStorage.init(async () => {
345
+ MemKey_ServiceAccountId.set(saId);
346
+ MemKey_UserScopePermissions.set(scopes);
347
+ MemKey_UserAccessIds.set(accessIds);
348
+ return action();
349
+ });
350
+ }
351
+ async resolveSAAccessIds(personalGroupId) {
352
+ return this.runAsServiceAccount(ServiceAccountId_Bootstrap, async () => {
353
+ const allGroups = await ModuleBE_AccessGroupDB.query.where({});
354
+ const { accessIds } = await this.materializeFromGroups(personalGroupId, allGroups);
355
+ return accessIds;
356
+ });
357
+ }
358
+ resolveBootstrapAccessIds() {
359
+ return {
360
+ [AccessScope_Self]: [BootstrapSAGroupId],
361
+ 'permissions-admin': [GroupId_PermissionsAdmin],
455
362
  };
456
- };
457
- })();
363
+ }
364
+ resolveBootstrapScopes() {
365
+ return [
366
+ 'permissions-ui:view',
367
+ 'access-group:create',
368
+ ];
369
+ }
370
+ // --- Bootstrap: ensure service account access group ---
371
+ async ensureBootstrapSAAccessGroup() {
372
+ const existing = await ModuleBE_AccessGroupDB.query.unique(BootstrapSAGroupId);
373
+ if (existing)
374
+ return;
375
+ await ModuleBE_AccessGroupDB.create.item({
376
+ _id: BootstrapSAGroupId,
377
+ type: 'service-account',
378
+ key: ServiceAccountId_Bootstrap,
379
+ label: 'Bootstrap Admin (SA)',
380
+ members: [],
381
+ });
382
+ this.logInfoBold('Created bootstrap service account access group');
383
+ }
384
+ async ensureServiceAccountAccessGroups() {
385
+ const saKeys = _keys(this.config.serviceAccounts).filter(saId => saId !== ServiceAccountId_Bootstrap);
386
+ if (saKeys.length === 0)
387
+ return;
388
+ const entries = saKeys.map(saId => ({
389
+ saId,
390
+ groupId: hashToUniqueId(saId),
391
+ }));
392
+ const existing = filterInstances(await ModuleBE_AccessGroupDB.query.all(entries.map(e => e.groupId)));
393
+ const existingIds = new Set(existing.map(g => g._id));
394
+ const toCreate = entries
395
+ .filter(e => !existingIds.has(e.groupId))
396
+ .map(e => ({
397
+ _id: e.groupId,
398
+ type: 'service-account',
399
+ key: e.saId,
400
+ label: `SA: ${e.saId}`,
401
+ members: [],
402
+ }));
403
+ if (toCreate.length === 0)
404
+ return;
405
+ await ModuleBE_AccessGroupDB.create.all(toCreate);
406
+ this.logInfoBold(`Created ${toCreate.length} service account access groups`);
407
+ }
408
+ // --- Bootstrap: ensure permissions infrastructure access groups ---
409
+ async ensurePermissionsInfraAccessGroups() {
410
+ const entries = AllDocumentAccessKeys.map(accessKey => ({
411
+ accessKey,
412
+ groupId: PermissionsInfraGroupIds[accessKey],
413
+ }));
414
+ const existing = filterInstances(await ModuleBE_AccessGroupDB.query.all(entries.map(e => e.groupId)));
415
+ const existingMap = new Map(existing.map(g => [g._id, g]));
416
+ const items = entries.map(e => {
417
+ const existingGroup = existingMap.get(e.groupId);
418
+ const members = filterDuplicates([GroupId_PermissionsAdmin, ...(existingGroup?.members ?? [])]);
419
+ return {
420
+ _id: e.groupId,
421
+ type: 'entity',
422
+ key: 'permissions',
423
+ label: `Permissions Infra ${e.accessKey}`,
424
+ members,
425
+ };
426
+ });
427
+ await ModuleBE_AccessGroupDB.set.all(items);
428
+ this.logInfoBold('Ensured permissions infrastructure access groups');
429
+ }
430
+ // --- Bootstrap: ensure scope entities ---
431
+ async ensureScopeEntities() {
432
+ const registeredScopes = getAllRegisteredScopes();
433
+ this.logDebug(`Registered scopes: ${registeredScopes.length} definitions`);
434
+ const scopeEntities = registeredScopes.flatMap(scope => scope.values.map(value => ({
435
+ _id: permissionScopeId(scope.key, value),
436
+ key: scope.key,
437
+ value,
438
+ })));
439
+ if (scopeEntities.length === 0)
440
+ return;
441
+ await ModuleBE_PermissionScopeDB.set.all(scopeEntities);
442
+ this.logInfoBold(`Ensured ${scopeEntities.length} scope entities`);
443
+ }
444
+ // --- Bootstrap: ensure default group ---
445
+ async ensureDefaultGroup() {
446
+ const existing = await ModuleBE_AccessGroupDB.query.unique(GroupId_AppDefault);
447
+ await ModuleBE_AccessGroupDB.set.all([{
448
+ _id: GroupId_AppDefault,
449
+ type: 'custom',
450
+ key: 'default',
451
+ label: 'Default',
452
+ members: existing?.members ?? [],
453
+ scopeEntries: [],
454
+ }]);
455
+ this.logInfoBold('Default group ensured');
456
+ }
457
+ // --- Bootstrap: permissions admin group ---
458
+ async ensurePermissionsAdminGroup() {
459
+ const scopeEntries = [
460
+ permissionScopeId('permissions-ui', 'view'),
461
+ permissionScopeId('access-group', 'create'),
462
+ ];
463
+ const existingAdmin = await ModuleBE_AccessGroupDB.query.unique(GroupId_PermissionsAdmin);
464
+ const adminMembers = filterDuplicates([BootstrapSAGroupId, ...(existingAdmin?.members ?? [])]);
465
+ this.logDebug(`[FIRST_USER] ensurePermissionsAdminGroup: id=${GroupId_PermissionsAdmin}, existing=${!!existingAdmin}, members=${JSON.stringify(adminMembers)}, scopes=${scopeEntries.length}`);
466
+ await ModuleBE_AccessGroupDB.set.all([{
467
+ _id: GroupId_PermissionsAdmin,
468
+ type: 'custom',
469
+ key: 'permissions-admin',
470
+ label: 'Permissions Admin',
471
+ members: adminMembers,
472
+ scopeEntries,
473
+ }]);
474
+ this.logInfoBold('Permissions Admin group upserted');
475
+ }
476
+ // --- Bootstrap: app-defined groups ---
477
+ async ensureAppDefinedGroups() {
478
+ const groupDefs = getRegisteredGroupDefinitions();
479
+ if (groupDefs.length === 0)
480
+ return;
481
+ const entries = groupDefs.map(def => ({
482
+ def,
483
+ groupId: hashToUniqueId(`group/${def.key}`),
484
+ }));
485
+ const existing = filterInstances(await ModuleBE_AccessGroupDB.query.all(entries.map(e => e.groupId)));
486
+ const existingMap = new Map(existing.map(g => [g._id, g]));
487
+ const items = entries.map(e => {
488
+ const existingGroup = existingMap.get(e.groupId);
489
+ const declaredMemberIds = (e.def.memberKeys ?? []).map(key => hashToUniqueId(`group/${key}`));
490
+ const mergedMembers = filterDuplicates([...declaredMemberIds, ...(existingGroup?.members ?? [])]);
491
+ return {
492
+ _id: e.groupId,
493
+ type: 'custom',
494
+ key: e.def.scopeKey ?? e.def.key,
495
+ label: e.def.label,
496
+ members: mergedMembers,
497
+ scopeEntries: filterDuplicates(e.def.scopes.map(({ scope, value }) => permissionScopeId(scope.key, value))),
498
+ };
499
+ });
500
+ await ModuleBE_AccessGroupDB.set.all(items);
501
+ this.logInfoBold(`Ensured ${groupDefs.length} application-defined groups: [${groupDefs.map(d => d.label).join(', ')}]`);
502
+ }
503
+ // --- Bootstrap: sync personal groups for existing accounts ---
504
+ async syncPersonalGroupsForExistingAccounts() {
505
+ const accounts = await ModuleBE_AccountDB.query.where({});
506
+ this.logDebug(`Found ${accounts.length} accounts for personal group sync`);
507
+ for (const account of accounts)
508
+ await this.ensurePersonalAccessGroup(account);
509
+ }
510
+ }
458
511
  export const ModuleBE_Permissions = new ModuleBE_Permissions_Class();