@elevasis/core 0.28.0 → 0.30.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/dist/auth/index.d.ts +5289 -0
- package/dist/auth/index.js +595 -0
- package/dist/index.d.ts +11 -11
- package/dist/knowledge/index.d.ts +1 -1
- package/dist/organization-model/index.d.ts +11 -11
- package/dist/test-utils/index.d.ts +24 -1
- package/package.json +7 -3
- package/src/__tests__/publish.test.ts +8 -7
- package/src/auth/__tests__/access-key-coverage.test.ts +42 -0
- package/src/auth/__tests__/access-key-scan.ts +117 -0
- package/src/auth/__tests__/access-keys.test.ts +81 -0
- package/src/auth/__tests__/access-model.test.ts +257 -0
- package/src/auth/__tests__/access-test-fixtures.ts +50 -0
- package/src/auth/__tests__/key-catalog-drift.test.ts +33 -0
- package/src/auth/__tests__/platform-admin-bypass-parity.test.ts +67 -0
- package/src/auth/access-keys.ts +173 -0
- package/src/auth/access-model.ts +185 -0
- package/src/auth/index.ts +6 -2
- package/src/auth/multi-tenancy/memberships/membership.ts +2 -4
- package/src/auth/multi-tenancy/permissions.ts +1 -1
- package/src/auth/multi-tenancy/types.ts +3 -12
- package/src/business/acquisition/api-schemas.test.ts +59 -8
- package/src/business/acquisition/api-schemas.ts +10 -5
- package/src/business/acquisition/build-templates.test.ts +187 -240
- package/src/business/acquisition/build-templates.ts +87 -98
- package/src/business/acquisition/types.ts +390 -389
- package/src/execution/engine/index.ts +6 -4
- package/src/execution/engine/tools/lead-service-types.ts +63 -34
- package/src/execution/engine/tools/platform/acquisition/types.ts +7 -8
- package/src/execution/engine/tools/registry.ts +6 -4
- package/src/execution/engine/tools/tool-maps.ts +23 -1
- package/src/organization-model/domains/prospecting.ts +2 -327
- package/src/organization-model/migration-helpers.ts +16 -12
- package/src/reference/_generated/contracts.md +352 -328
- package/src/reference/glossary.md +8 -6
- package/src/supabase/database.types.ts +13 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { AccessKeys, deriveAccessKeyCatalog } from '../access-keys'
|
|
3
|
+
import { checkAccess } from '../access-model'
|
|
4
|
+
import type { AccessContext } from '../access-model'
|
|
5
|
+
import type { OrganizationModel } from '../../organization-model/types'
|
|
6
|
+
|
|
7
|
+
const organizationModel = {
|
|
8
|
+
systems: {
|
|
9
|
+
sales: {
|
|
10
|
+
id: 'sales',
|
|
11
|
+
label: 'Sales',
|
|
12
|
+
lifecycle: 'active',
|
|
13
|
+
order: 10,
|
|
14
|
+
systems: {
|
|
15
|
+
crm: {
|
|
16
|
+
id: 'crm',
|
|
17
|
+
label: 'CRM',
|
|
18
|
+
lifecycle: 'active',
|
|
19
|
+
order: 10
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
operations: {
|
|
24
|
+
id: 'operations',
|
|
25
|
+
label: 'Operations',
|
|
26
|
+
lifecycle: 'beta',
|
|
27
|
+
order: 20
|
|
28
|
+
},
|
|
29
|
+
drafts: {
|
|
30
|
+
id: 'drafts',
|
|
31
|
+
label: 'Drafts',
|
|
32
|
+
lifecycle: 'draft',
|
|
33
|
+
order: 30
|
|
34
|
+
},
|
|
35
|
+
retired: {
|
|
36
|
+
id: 'retired',
|
|
37
|
+
label: 'Retired',
|
|
38
|
+
lifecycle: 'deprecated',
|
|
39
|
+
order: 40
|
|
40
|
+
},
|
|
41
|
+
missingLifecycle: {
|
|
42
|
+
id: 'missingLifecycle',
|
|
43
|
+
label: 'Missing Lifecycle',
|
|
44
|
+
order: 50
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} as OrganizationModel
|
|
48
|
+
|
|
49
|
+
const baseContext: AccessContext = {
|
|
50
|
+
organizationId: 'org-1',
|
|
51
|
+
organizationModel,
|
|
52
|
+
membership: {
|
|
53
|
+
id: 'membership-1',
|
|
54
|
+
organizationId: 'org-1',
|
|
55
|
+
effectivePermissions: ['sales.crm.manage']
|
|
56
|
+
},
|
|
57
|
+
profile: {
|
|
58
|
+
id: 'user-1',
|
|
59
|
+
isPlatformAdmin: false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function context(overrides: Partial<AccessContext> = {}): AccessContext {
|
|
64
|
+
return { ...baseContext, ...overrides }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('checkAccess', () => {
|
|
68
|
+
it('allows active system view access by lifecycle alone', () => {
|
|
69
|
+
expect(checkAccess('sales.crm', context())).toEqual({
|
|
70
|
+
allowed: true,
|
|
71
|
+
restrictedBy: null,
|
|
72
|
+
reason: 'allowed'
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('denies unknown keys through catalog validation', () => {
|
|
77
|
+
expect(checkAccess('unknown.system', context())).toEqual({
|
|
78
|
+
allowed: false,
|
|
79
|
+
restrictedBy: 'catalog',
|
|
80
|
+
reason: 'unknown-access-key'
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('denies missing membership before evaluating lifecycle or permissions', () => {
|
|
85
|
+
expect(checkAccess('sales.crm', context({ membership: null }))).toEqual({
|
|
86
|
+
allowed: false,
|
|
87
|
+
restrictedBy: 'membership',
|
|
88
|
+
reason: 'missing-membership'
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('denies organization mismatches', () => {
|
|
93
|
+
expect(
|
|
94
|
+
checkAccess(
|
|
95
|
+
'sales.crm',
|
|
96
|
+
context({
|
|
97
|
+
membership: { id: 'membership-2', organizationId: 'org-2', effectivePermissions: ['sales.crm.manage'] }
|
|
98
|
+
})
|
|
99
|
+
)
|
|
100
|
+
).toEqual({
|
|
101
|
+
allowed: false,
|
|
102
|
+
restrictedBy: 'membership',
|
|
103
|
+
reason: 'organization-mismatch'
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('denies draft, deprecated, archived, and missing lifecycle systems', () => {
|
|
108
|
+
const archivedModel = {
|
|
109
|
+
systems: {
|
|
110
|
+
archived: {
|
|
111
|
+
id: 'archived',
|
|
112
|
+
label: 'Archived',
|
|
113
|
+
lifecycle: 'archived',
|
|
114
|
+
order: 10
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} as OrganizationModel
|
|
118
|
+
|
|
119
|
+
expect(checkAccess('drafts', context())).toMatchObject({
|
|
120
|
+
allowed: false,
|
|
121
|
+
restrictedBy: 'system-lifecycle',
|
|
122
|
+
reason: 'system-not-active'
|
|
123
|
+
})
|
|
124
|
+
expect(checkAccess('retired', context())).toMatchObject({
|
|
125
|
+
allowed: false,
|
|
126
|
+
restrictedBy: 'system-lifecycle',
|
|
127
|
+
reason: 'system-not-active'
|
|
128
|
+
})
|
|
129
|
+
expect(checkAccess('missingLifecycle', context())).toMatchObject({
|
|
130
|
+
allowed: false,
|
|
131
|
+
restrictedBy: 'system-lifecycle',
|
|
132
|
+
reason: 'system-not-active'
|
|
133
|
+
})
|
|
134
|
+
expect(checkAccess('archived', context({ organizationModel: archivedModel }))).toMatchObject({
|
|
135
|
+
allowed: false,
|
|
136
|
+
restrictedBy: 'system-lifecycle',
|
|
137
|
+
reason: 'system-not-active'
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('allows beta systems only when beta access or development mode is enabled', () => {
|
|
142
|
+
expect(checkAccess('operations', context())).toEqual({
|
|
143
|
+
allowed: false,
|
|
144
|
+
restrictedBy: 'system-lifecycle',
|
|
145
|
+
reason: 'system-not-active'
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
expect(checkAccess('operations', context({ betaAccessEnabled: true }))).toEqual({
|
|
149
|
+
allowed: true,
|
|
150
|
+
restrictedBy: null,
|
|
151
|
+
reason: 'allowed'
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
expect(checkAccess('operations', context({ isDevelopment: true }))).toEqual({
|
|
155
|
+
allowed: true,
|
|
156
|
+
restrictedBy: null,
|
|
157
|
+
reason: 'allowed'
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('requires matching role permission for manage when permissions are supplied', () => {
|
|
162
|
+
expect(checkAccess({ systemPath: 'sales.crm', action: 'manage' }, context())).toEqual({
|
|
163
|
+
allowed: true,
|
|
164
|
+
restrictedBy: null,
|
|
165
|
+
reason: 'allowed'
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
expect(
|
|
169
|
+
checkAccess(
|
|
170
|
+
{ systemPath: 'sales.crm', action: 'manage' },
|
|
171
|
+
context({
|
|
172
|
+
membership: { id: 'membership-1', organizationId: 'org-1', effectivePermissions: [] }
|
|
173
|
+
})
|
|
174
|
+
)
|
|
175
|
+
).toEqual({
|
|
176
|
+
allowed: false,
|
|
177
|
+
restrictedBy: 'role-permission',
|
|
178
|
+
reason: 'role-permission-denied'
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('requires explicit role permissions for permission-only access keys', () => {
|
|
183
|
+
expect(
|
|
184
|
+
checkAccess(
|
|
185
|
+
AccessKeys.organizationManage,
|
|
186
|
+
context({
|
|
187
|
+
membership: { id: 'membership-1', organizationId: 'org-1', effectivePermissions: ['org.manage'] }
|
|
188
|
+
})
|
|
189
|
+
)
|
|
190
|
+
).toEqual({
|
|
191
|
+
allowed: true,
|
|
192
|
+
restrictedBy: null,
|
|
193
|
+
reason: 'allowed'
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
expect(checkAccess(AccessKeys.organizationManage, context())).toEqual({
|
|
197
|
+
allowed: false,
|
|
198
|
+
restrictedBy: 'role-permission',
|
|
199
|
+
reason: 'role-permission-denied'
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('checks diagnostic keys against the runtime diagnostic allowlist', () => {
|
|
204
|
+
expect(checkAccess(AccessKeys.operationsOverview, context())).toEqual({
|
|
205
|
+
allowed: false,
|
|
206
|
+
restrictedBy: 'diagnostic-allowlist',
|
|
207
|
+
reason: 'diagnostic-key-not-allowed'
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
expect(
|
|
211
|
+
checkAccess(
|
|
212
|
+
AccessKeys.operationsOverview,
|
|
213
|
+
context({ diagnosticAllowlist: [AccessKeys.operationsOverview.systemPath] })
|
|
214
|
+
)
|
|
215
|
+
).toEqual({
|
|
216
|
+
allowed: true,
|
|
217
|
+
restrictedBy: null,
|
|
218
|
+
reason: 'allowed'
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('keeps diagnostic catalog validation separate from runtime diagnostic allowlist', () => {
|
|
223
|
+
const accessCatalog = deriveAccessKeyCatalog(organizationModel, { diagnosticKeys: ['diagnostic.custom'] })
|
|
224
|
+
|
|
225
|
+
expect(
|
|
226
|
+
checkAccess('diagnostic.custom', context({ accessCatalog, diagnosticAllowlist: ['diagnostic.custom'] }))
|
|
227
|
+
).toEqual({
|
|
228
|
+
allowed: true,
|
|
229
|
+
restrictedBy: null,
|
|
230
|
+
reason: 'allowed'
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
expect(checkAccess('diagnostic.not-cataloged', context({ diagnosticAllowlist: ['diagnostic.not-cataloged'] }))).toEqual({
|
|
234
|
+
allowed: false,
|
|
235
|
+
restrictedBy: 'catalog',
|
|
236
|
+
reason: 'unknown-access-key'
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('centralizes platform-admin bypass with the platform-admin-bypass reason', () => {
|
|
241
|
+
expect(
|
|
242
|
+
checkAccess('unknown.system', context({ profile: { id: 'admin', isPlatformAdmin: true }, membership: null }))
|
|
243
|
+
).toEqual({
|
|
244
|
+
allowed: true,
|
|
245
|
+
restrictedBy: null,
|
|
246
|
+
reason: 'platform-admin-bypass'
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('denies platformAdmin special-case for non-platform admins', () => {
|
|
251
|
+
expect(checkAccess('platformAdmin', context())).toEqual({
|
|
252
|
+
allowed: false,
|
|
253
|
+
restrictedBy: 'role-permission',
|
|
254
|
+
reason: 'role-permission-denied'
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { OrganizationModel } from '../../organization-model/types'
|
|
2
|
+
|
|
3
|
+
export const accessTestOrganizationModel = {
|
|
4
|
+
systems: {
|
|
5
|
+
platform: {
|
|
6
|
+
id: 'platform',
|
|
7
|
+
label: 'Platform',
|
|
8
|
+
lifecycle: 'active',
|
|
9
|
+
order: 10
|
|
10
|
+
},
|
|
11
|
+
sales: {
|
|
12
|
+
id: 'sales',
|
|
13
|
+
label: 'Sales',
|
|
14
|
+
lifecycle: 'active',
|
|
15
|
+
order: 20,
|
|
16
|
+
systems: {
|
|
17
|
+
crm: {
|
|
18
|
+
id: 'crm',
|
|
19
|
+
label: 'CRM',
|
|
20
|
+
lifecycle: 'active',
|
|
21
|
+
order: 10
|
|
22
|
+
},
|
|
23
|
+
'lead-gen': {
|
|
24
|
+
id: 'lead-gen',
|
|
25
|
+
label: 'Lead Gen',
|
|
26
|
+
lifecycle: 'active',
|
|
27
|
+
order: 20
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
projects: {
|
|
32
|
+
id: 'projects',
|
|
33
|
+
label: 'Projects',
|
|
34
|
+
lifecycle: 'active',
|
|
35
|
+
order: 30
|
|
36
|
+
},
|
|
37
|
+
clients: {
|
|
38
|
+
id: 'clients',
|
|
39
|
+
label: 'Clients',
|
|
40
|
+
lifecycle: 'active',
|
|
41
|
+
order: 40
|
|
42
|
+
},
|
|
43
|
+
seo: {
|
|
44
|
+
id: 'seo',
|
|
45
|
+
label: 'SEO',
|
|
46
|
+
lifecycle: 'active',
|
|
47
|
+
order: 50
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} as unknown as OrganizationModel
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { AccessKeys, deriveAccessKeyCatalog, findAccessCatalogEntry, normalizeAccessKey } from '../access-keys'
|
|
4
|
+
import { accessTestOrganizationModel } from './access-test-fixtures'
|
|
5
|
+
import { scanAccessKeyUsages } from './access-key-scan'
|
|
6
|
+
|
|
7
|
+
describe('access key catalog drift', () => {
|
|
8
|
+
it('keeps runtime literal access keys in the derived catalog', () => {
|
|
9
|
+
const catalog = deriveAccessKeyCatalog(accessTestOrganizationModel)
|
|
10
|
+
const usages = scanAccessKeyUsages()
|
|
11
|
+
|
|
12
|
+
const unknownLiterals = usages.stringLiterals
|
|
13
|
+
.map((usage) => {
|
|
14
|
+
const key = normalizeAccessKey(usage.key)
|
|
15
|
+
return findAccessCatalogEntry(catalog, key) === undefined
|
|
16
|
+
? `${usage.file}: ${JSON.stringify(usage.key)}`
|
|
17
|
+
: null
|
|
18
|
+
})
|
|
19
|
+
.filter((usage): usage is string => usage !== null)
|
|
20
|
+
|
|
21
|
+
expect(unknownLiterals).toEqual([])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('keeps runtime AccessKeys references backed by exported symbols', () => {
|
|
25
|
+
const usages = scanAccessKeyUsages()
|
|
26
|
+
const exportedSymbols = new Set(Object.keys(AccessKeys))
|
|
27
|
+
const unknownSymbols = usages.accessKeySymbols
|
|
28
|
+
.filter((usage) => !exportedSymbols.has(usage.symbol))
|
|
29
|
+
.map((usage) => `${usage.file}: AccessKeys.${usage.symbol}`)
|
|
30
|
+
|
|
31
|
+
expect(unknownSymbols).toEqual([])
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { AccessKeys } from '../access-keys'
|
|
4
|
+
import { checkAccess, createAccessModel, type AccessContext } from '../access-model'
|
|
5
|
+
import { accessTestOrganizationModel } from './access-test-fixtures'
|
|
6
|
+
|
|
7
|
+
const bypassAnswer = {
|
|
8
|
+
allowed: true,
|
|
9
|
+
restrictedBy: null,
|
|
10
|
+
reason: 'platform-admin-bypass'
|
|
11
|
+
} as const
|
|
12
|
+
|
|
13
|
+
const baseContext: AccessContext = {
|
|
14
|
+
organizationId: 'org-1',
|
|
15
|
+
organizationModel: accessTestOrganizationModel,
|
|
16
|
+
membership: {
|
|
17
|
+
id: 'membership-1',
|
|
18
|
+
organizationId: 'org-1',
|
|
19
|
+
effectivePermissions: []
|
|
20
|
+
},
|
|
21
|
+
profile: {
|
|
22
|
+
id: 'user-1',
|
|
23
|
+
isPlatformAdmin: false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('platform admin bypass parity', () => {
|
|
28
|
+
it('short-circuits unknown keys before catalog and membership checks', () => {
|
|
29
|
+
expect(
|
|
30
|
+
checkAccess('unknown.system', {
|
|
31
|
+
...baseContext,
|
|
32
|
+
membership: null,
|
|
33
|
+
profile: { id: 'admin', isPlatformAdmin: true }
|
|
34
|
+
})
|
|
35
|
+
).toEqual(bypassAnswer)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('supports both camelCase and database-shaped platform-admin profile flags', () => {
|
|
39
|
+
expect(
|
|
40
|
+
checkAccess('unknown.system', {
|
|
41
|
+
...baseContext,
|
|
42
|
+
membership: null,
|
|
43
|
+
profile: { id: 'admin', is_platform_admin: true }
|
|
44
|
+
})
|
|
45
|
+
).toEqual(bypassAnswer)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('returns the same bypass answer through createAccessModel', () => {
|
|
49
|
+
const accessModel = createAccessModel(accessTestOrganizationModel)
|
|
50
|
+
|
|
51
|
+
expect(
|
|
52
|
+
accessModel.checkAccess('unknown.system', {
|
|
53
|
+
...baseContext,
|
|
54
|
+
membership: null,
|
|
55
|
+
profile: { id: 'admin', isPlatformAdmin: true }
|
|
56
|
+
})
|
|
57
|
+
).toEqual(bypassAnswer)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('does not grant platformAdmin access to non-platform admins', () => {
|
|
61
|
+
expect(checkAccess(AccessKeys.platformAdmin, baseContext)).toEqual({
|
|
62
|
+
allowed: false,
|
|
63
|
+
restrictedBy: 'role-permission',
|
|
64
|
+
reason: 'role-permission-denied'
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { listAllSystems } from '../organization-model/helpers'
|
|
3
|
+
import type { OrganizationModel } from '../organization-model/types'
|
|
4
|
+
import { PERMISSIONS, type PermissionKey } from './multi-tenancy/permissions'
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_ACCESS_ACTION = 'view' as const
|
|
7
|
+
export const PLATFORM_ADMIN_ACCESS_KEY = 'platform.admin' as const
|
|
8
|
+
export const PLATFORM_ADMIN_ACCESS_KEY_SHORTHAND = 'platformAdmin' as const
|
|
9
|
+
|
|
10
|
+
export const AccessActionSchema = z.enum(['view', 'manage'])
|
|
11
|
+
|
|
12
|
+
export const AccessKeyObjectSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
systemPath: z.string().trim().min(1),
|
|
15
|
+
action: AccessActionSchema.default(DEFAULT_ACCESS_ACTION)
|
|
16
|
+
})
|
|
17
|
+
.strict()
|
|
18
|
+
|
|
19
|
+
export const AccessKeyInputSchema = z.union([z.string().trim().min(1), AccessKeyObjectSchema])
|
|
20
|
+
export const NormalizedAccessKeySchema = AccessKeyObjectSchema
|
|
21
|
+
export const AccessKeySchema = AccessKeyInputSchema
|
|
22
|
+
|
|
23
|
+
export type AccessAction = z.infer<typeof AccessActionSchema>
|
|
24
|
+
export type AccessKeyObject = z.infer<typeof AccessKeyObjectSchema>
|
|
25
|
+
export type AccessKeyInput = z.input<typeof AccessKeyInputSchema>
|
|
26
|
+
export type NormalizedAccessKey = z.infer<typeof NormalizedAccessKeySchema>
|
|
27
|
+
export type AccessKey = NormalizedAccessKey
|
|
28
|
+
|
|
29
|
+
export type AccessCatalogEntrySource = 'om-system' | 'diagnostic' | 'platform' | 'permission'
|
|
30
|
+
|
|
31
|
+
export interface AccessCatalogEntry {
|
|
32
|
+
key: NormalizedAccessKey
|
|
33
|
+
source: AccessCatalogEntrySource
|
|
34
|
+
rolePermission?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AccessKeyCatalog {
|
|
38
|
+
bySystemPath: ReadonlyMap<string, readonly AccessCatalogEntry[]>
|
|
39
|
+
entries: readonly AccessCatalogEntry[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const DIAGNOSTIC_VIEW_ACCESS_KEYS = [
|
|
43
|
+
'diagnostic.operations.overview',
|
|
44
|
+
'diagnostic.operations.recent-executions',
|
|
45
|
+
'diagnostic.monitoring.execution-logs',
|
|
46
|
+
'diagnostic.monitoring.notifications'
|
|
47
|
+
] as const
|
|
48
|
+
|
|
49
|
+
export const AccessKeys = {
|
|
50
|
+
platformAdmin: { systemPath: PLATFORM_ADMIN_ACCESS_KEY, action: DEFAULT_ACCESS_ACTION },
|
|
51
|
+
organizationManage: { systemPath: 'permission.org', action: 'manage' },
|
|
52
|
+
membersManage: { systemPath: 'permission.members', action: 'manage' },
|
|
53
|
+
rolesManage: { systemPath: 'permission.roles', action: 'manage' },
|
|
54
|
+
secretsManage: { systemPath: 'permission.secrets', action: 'manage' },
|
|
55
|
+
operationsRead: { systemPath: 'permission.operations', action: DEFAULT_ACCESS_ACTION },
|
|
56
|
+
operationsManage: { systemPath: 'permission.operations', action: 'manage' },
|
|
57
|
+
acquisitionManage: { systemPath: 'permission.acquisition', action: 'manage' },
|
|
58
|
+
projectsManage: { systemPath: 'permission.projects', action: 'manage' },
|
|
59
|
+
clientsManage: { systemPath: 'permission.clients', action: 'manage' },
|
|
60
|
+
operationsOverview: { systemPath: 'diagnostic.operations.overview', action: DEFAULT_ACCESS_ACTION },
|
|
61
|
+
operationsRecentExecutions: { systemPath: 'diagnostic.operations.recent-executions', action: DEFAULT_ACCESS_ACTION },
|
|
62
|
+
monitoringExecutionLogs: { systemPath: 'diagnostic.monitoring.execution-logs', action: DEFAULT_ACCESS_ACTION },
|
|
63
|
+
monitoringNotifications: { systemPath: 'diagnostic.monitoring.notifications', action: DEFAULT_ACCESS_ACTION }
|
|
64
|
+
} as const satisfies Record<string, NormalizedAccessKey>
|
|
65
|
+
|
|
66
|
+
const PERMISSION_ACCESS_KEY_DEFINITIONS: readonly {
|
|
67
|
+
key: NormalizedAccessKey
|
|
68
|
+
rolePermission: PermissionKey
|
|
69
|
+
}[] = [
|
|
70
|
+
{ key: AccessKeys.organizationManage, rolePermission: PERMISSIONS.ORG_MANAGE },
|
|
71
|
+
{ key: AccessKeys.membersManage, rolePermission: PERMISSIONS.MEMBERS_MANAGE },
|
|
72
|
+
{ key: AccessKeys.rolesManage, rolePermission: PERMISSIONS.ROLES_MANAGE },
|
|
73
|
+
{ key: AccessKeys.secretsManage, rolePermission: PERMISSIONS.SECRETS_MANAGE },
|
|
74
|
+
{ key: AccessKeys.operationsRead, rolePermission: PERMISSIONS.OPERATIONS_READ },
|
|
75
|
+
{ key: AccessKeys.operationsManage, rolePermission: PERMISSIONS.OPERATIONS_MANAGE },
|
|
76
|
+
{ key: AccessKeys.acquisitionManage, rolePermission: PERMISSIONS.ACQUISITION_MANAGE },
|
|
77
|
+
{ key: AccessKeys.projectsManage, rolePermission: PERMISSIONS.PROJECTS_MANAGE },
|
|
78
|
+
{ key: AccessKeys.clientsManage, rolePermission: PERMISSIONS.CLIENTS_MANAGE }
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
export function normalizeAccessKey(input: AccessKeyInput): NormalizedAccessKey {
|
|
82
|
+
const parsed = AccessKeyInputSchema.parse(input)
|
|
83
|
+
const normalized =
|
|
84
|
+
typeof parsed === 'string'
|
|
85
|
+
? {
|
|
86
|
+
systemPath: parsed === PLATFORM_ADMIN_ACCESS_KEY_SHORTHAND ? PLATFORM_ADMIN_ACCESS_KEY : parsed,
|
|
87
|
+
action: DEFAULT_ACCESS_ACTION
|
|
88
|
+
}
|
|
89
|
+
: parsed
|
|
90
|
+
|
|
91
|
+
return NormalizedAccessKeySchema.parse(normalized)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function accessKeyToString(input: AccessKeyInput): string {
|
|
95
|
+
const key = normalizeAccessKey(input)
|
|
96
|
+
return `${key.systemPath}:${key.action}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function rolePermissionForAccessKey(key: NormalizedAccessKey): string | undefined {
|
|
100
|
+
if (key.action === DEFAULT_ACCESS_ACTION) return undefined
|
|
101
|
+
return `${key.systemPath}.${key.action}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface DeriveAccessKeyCatalogOptions {
|
|
105
|
+
diagnosticKeys?: readonly string[]
|
|
106
|
+
includeManageActions?: boolean
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function groupCatalogEntries(entries: readonly AccessCatalogEntry[]): ReadonlyMap<string, readonly AccessCatalogEntry[]> {
|
|
110
|
+
const grouped = new Map<string, AccessCatalogEntry[]>()
|
|
111
|
+
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const existing = grouped.get(entry.key.systemPath) ?? []
|
|
114
|
+
existing.push(entry)
|
|
115
|
+
grouped.set(entry.key.systemPath, existing)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return grouped
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildCatalogEntry(systemPath: string, action: AccessAction, source: AccessCatalogEntrySource): AccessCatalogEntry {
|
|
122
|
+
const key = normalizeAccessKey({ systemPath, action })
|
|
123
|
+
return {
|
|
124
|
+
key,
|
|
125
|
+
source,
|
|
126
|
+
rolePermission: rolePermissionForAccessKey(key)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function deriveAccessKeyCatalog(
|
|
131
|
+
organizationModel: OrganizationModel,
|
|
132
|
+
options: DeriveAccessKeyCatalogOptions = {}
|
|
133
|
+
): AccessKeyCatalog {
|
|
134
|
+
const { diagnosticKeys = DIAGNOSTIC_VIEW_ACCESS_KEYS, includeManageActions = true } = options
|
|
135
|
+
const entries: AccessCatalogEntry[] = [
|
|
136
|
+
buildCatalogEntry(PLATFORM_ADMIN_ACCESS_KEY, DEFAULT_ACCESS_ACTION, 'platform')
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
for (const { path } of listAllSystems(organizationModel)) {
|
|
140
|
+
entries.push(buildCatalogEntry(path, DEFAULT_ACCESS_ACTION, 'om-system'))
|
|
141
|
+
if (includeManageActions) {
|
|
142
|
+
entries.push(buildCatalogEntry(path, 'manage', 'om-system'))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const { key, rolePermission } of PERMISSION_ACCESS_KEY_DEFINITIONS) {
|
|
147
|
+
entries.push({
|
|
148
|
+
key,
|
|
149
|
+
source: 'permission',
|
|
150
|
+
rolePermission
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const systemPath of diagnosticKeys) {
|
|
155
|
+
entries.push(buildCatalogEntry(systemPath, DEFAULT_ACCESS_ACTION, 'diagnostic'))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
bySystemPath: groupCatalogEntries(entries),
|
|
160
|
+
entries
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function findAccessCatalogEntry(
|
|
165
|
+
catalog: AccessKeyCatalog,
|
|
166
|
+
key: NormalizedAccessKey
|
|
167
|
+
): AccessCatalogEntry | undefined {
|
|
168
|
+
return catalog.bySystemPath.get(key.systemPath)?.find((entry) => entry.key.action === key.action)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function listAccessKeys(catalog: AccessKeyCatalog): NormalizedAccessKey[] {
|
|
172
|
+
return catalog.entries.map((entry) => entry.key)
|
|
173
|
+
}
|