@owlmeans/iam 0.1.18-rc.8 → 0.1.18
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/README.md +185 -15
- package/agent-meta/manifest.json +92 -4
- package/agent-meta/skills/iam/SKILL.md +91 -177
- package/agent-meta/skills/iam/parts/clients.md +93 -0
- package/agent-meta/skills/iam/parts/permissions.md +106 -0
- package/agent-meta/skills/iam/parts/service.md +115 -0
- package/agent-meta/skills/iam/parts/tenancy.md +72 -0
- package/build/consts.d.ts +170 -1
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +221 -0
- package/build/consts.js.map +1 -1
- package/build/consts.local.d.ts +58 -0
- package/build/consts.local.d.ts.map +1 -0
- package/build/consts.local.js +21 -0
- package/build/consts.local.js.map +1 -0
- package/build/errors.d.ts +26 -0
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +38 -0
- package/build/errors.js.map +1 -1
- package/build/facet.d.ts +10 -0
- package/build/facet.d.ts.map +1 -0
- package/build/facet.js +16 -0
- package/build/facet.js.map +1 -0
- package/build/gate/consts.local.d.ts +2 -0
- package/build/gate/consts.local.d.ts.map +1 -0
- package/build/gate/consts.local.js +3 -0
- package/build/gate/consts.local.js.map +1 -0
- package/build/gate/index.d.ts +4 -0
- package/build/gate/index.d.ts.map +1 -0
- package/build/gate/index.js +4 -0
- package/build/gate/index.js.map +1 -0
- package/build/gate/param.d.ts +6 -0
- package/build/gate/param.d.ts.map +1 -0
- package/build/gate/param.js +83 -0
- package/build/gate/param.js.map +1 -0
- package/build/gate/resolve.d.ts +10 -0
- package/build/gate/resolve.d.ts.map +1 -0
- package/build/gate/resolve.js +93 -0
- package/build/gate/resolve.js.map +1 -0
- package/build/gate/types.d.ts +109 -0
- package/build/gate/types.d.ts.map +1 -0
- package/build/gate/types.js +2 -0
- package/build/gate/types.js.map +1 -0
- package/build/gate/types.local.d.ts +5 -0
- package/build/gate/types.local.d.ts.map +1 -0
- package/build/gate/types.local.js +2 -0
- package/build/gate/types.local.js.map +1 -0
- package/build/gate/validate.d.ts +4 -0
- package/build/gate/validate.d.ts.map +1 -0
- package/build/gate/validate.js +100 -0
- package/build/gate/validate.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -0
- package/build/index.js.map +1 -1
- package/build/permission/types.d.ts +35 -0
- package/build/permission/types.d.ts.map +1 -0
- package/build/permission/types.js +2 -0
- package/build/permission/types.js.map +1 -0
- package/build/permission.d.ts +8 -0
- package/build/permission.d.ts.map +1 -0
- package/build/permission.js +34 -0
- package/build/permission.js.map +1 -0
- package/build/runtime/types.d.ts +90 -0
- package/build/runtime/types.d.ts.map +1 -0
- package/build/runtime/types.js +2 -0
- package/build/runtime/types.js.map +1 -0
- package/build/runtime.d.ts +75 -0
- package/build/runtime.d.ts.map +1 -0
- package/build/runtime.js +75 -0
- package/build/runtime.js.map +1 -0
- package/build/schemas.d.ts +13 -0
- package/build/schemas.d.ts.map +1 -0
- package/build/schemas.js +108 -0
- package/build/schemas.js.map +1 -0
- package/build/types.d.ts +429 -30
- package/build/types.d.ts.map +1 -1
- package/build/utils/types.d.ts +12 -0
- package/build/utils/types.d.ts.map +1 -0
- package/build/utils/types.js +2 -0
- package/build/utils/types.js.map +1 -0
- package/build/utils.d.ts +7 -6
- package/build/utils.d.ts.map +1 -1
- package/build/utils.js +10 -1
- package/build/utils.js.map +1 -1
- package/package.json +12 -7
- package/src/consts.local.ts +26 -0
- package/src/consts.ts +241 -1
- package/src/errors.ts +44 -0
- package/src/facet.ts +17 -0
- package/src/gate/consts.local.ts +3 -0
- package/src/gate/index.ts +3 -0
- package/src/gate/param.ts +108 -0
- package/src/gate/resolve.ts +114 -0
- package/src/gate/types.local.ts +4 -0
- package/src/gate/types.ts +117 -0
- package/src/gate/validate.ts +123 -0
- package/src/index.ts +9 -0
- package/src/permission/types.ts +33 -0
- package/src/permission.ts +48 -0
- package/src/runtime/types.ts +107 -0
- package/src/runtime.ts +142 -0
- package/src/schemas.ts +122 -0
- package/src/types.ts +492 -31
- package/src/utils/types.ts +11 -0
- package/src/utils.ts +12 -7
- package/tests/gate-param.spec.ts +233 -0
- package/tests/runtime.spec.ts +101 -0
- package/tests/tsconfig.json +12 -0
- package/tests/utils.spec.ts +47 -1
package/src/types.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import type { InitializedService } from '@owlmeans/context'
|
|
2
|
-
import type { OidcProviderConfig } from '@owlmeans/oidc'
|
|
2
|
+
import type { OidcOrganizationClaim, OidcPermissionSetClaim, OidcProviderConfig } from '@owlmeans/oidc'
|
|
3
|
+
import { type IamDefaultClass, type IamGrantMode, type IamGrantOrigin, type IamRemovalPolicy, IAM_AREAS, IAM_MODE_INTEGRATED, IAM_MODE_KEYCLOAK } from './consts.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A client's own configuration, stored with its registration.
|
|
7
|
+
*
|
|
8
|
+
* The two tenancy flags decide which organizations the client's subjects act in. With NEITHER, every
|
|
9
|
+
* subject acts in the client's owning organization (a single-tenant app). With either, every subject
|
|
10
|
+
* acts in organizations of its own, and the runtime IAM API lets their owners manage them — `users`
|
|
11
|
+
* opens the `user` area to those owners, `operators` the `operator` area. Anything else is the
|
|
12
|
+
* client's arbitrary configuration, carried through untouched.
|
|
13
|
+
*/
|
|
14
|
+
export interface IamClientConfig {
|
|
15
|
+
operators?: boolean
|
|
16
|
+
users?: boolean
|
|
17
|
+
[key: string]: unknown
|
|
18
|
+
}
|
|
3
19
|
|
|
4
20
|
export interface IamClient {
|
|
5
21
|
id?: string
|
|
@@ -8,8 +24,12 @@ export interface IamClient {
|
|
|
8
24
|
name?: string
|
|
9
25
|
/** The entity realm this client belongs to — replaces the old (client as any)._realm hack */
|
|
10
26
|
realm?: string
|
|
27
|
+
config?: IamClientConfig
|
|
11
28
|
}
|
|
12
29
|
|
|
30
|
+
/** A client as a reader that must never see its secret gets it. */
|
|
31
|
+
export interface IamClientInfo extends Omit<IamClient, 'secret'> {}
|
|
32
|
+
|
|
13
33
|
export interface IamCredentialsPair {
|
|
14
34
|
token: string
|
|
15
35
|
realm: string
|
|
@@ -28,15 +48,56 @@ export interface IamClientOptions {
|
|
|
28
48
|
* unusable client.
|
|
29
49
|
*/
|
|
30
50
|
redirectUris?: string[]
|
|
51
|
+
/**
|
|
52
|
+
* Merged SHALLOWLY into the stored configuration: a key passed is written (a flag passed as
|
|
53
|
+
* `false` included), a key omitted is kept. A backend with no tenancy refuses a tenancy flag.
|
|
54
|
+
*/
|
|
55
|
+
config?: IamClientConfig
|
|
31
56
|
}
|
|
32
57
|
|
|
33
58
|
export interface IamPermissionArgs {
|
|
34
59
|
/** Action name. When absent the permission is unscoped (project-wide). */
|
|
35
60
|
permission?: string
|
|
36
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Declares that grants of this permission may be bound to specific resource ids.
|
|
63
|
+
*
|
|
64
|
+
* It does NOT mean grants must be: the same name is grantable in either form. A grant carrying no
|
|
65
|
+
* resource ids covers every resource, which is what `hasPermission` already implements.
|
|
66
|
+
*/
|
|
37
67
|
resourceScoped?: boolean
|
|
68
|
+
/**
|
|
69
|
+
* Declares that grants of this permission are bound to ONE organization: stored on the row of that
|
|
70
|
+
* organization (or a group of it) and effective only while the subject acts in it.
|
|
71
|
+
*
|
|
72
|
+
* Turning it off while grants exist is refused — every bound grant would silently widen to
|
|
73
|
+
* every organization of its subject.
|
|
74
|
+
*/
|
|
75
|
+
entityScoped?: boolean
|
|
38
76
|
/** Optional human-readable title for the permission definition. */
|
|
39
77
|
title?: string
|
|
78
|
+
/**
|
|
79
|
+
* Grouping tag — one of `IAM_AREAS`, or any string a deployment defines.
|
|
80
|
+
*
|
|
81
|
+
* Purely presentational: it groups the permission in an administrator's screen and is never read
|
|
82
|
+
* when a request is authorized. It is a TAG rather than a name segment precisely so an existing
|
|
83
|
+
* permission can be grouped without renaming it, which would orphan every grant already made.
|
|
84
|
+
*/
|
|
85
|
+
area?: string
|
|
86
|
+
/**
|
|
87
|
+
* Marks a permission the PLATFORM owns rather than one the application declared for itself.
|
|
88
|
+
*
|
|
89
|
+
* The distinction is not cosmetic: an operator must not be able to revoke a platform marker from
|
|
90
|
+
* an undifferentiated list, and a repair that deletes "definitions no declaration names" must not
|
|
91
|
+
* reach one — a marker can be enforced by a runtime decoration rather than by a gate parameter,
|
|
92
|
+
* so its absence from the declarations is not evidence that it is unused.
|
|
93
|
+
*/
|
|
94
|
+
managed?: boolean
|
|
95
|
+
/**
|
|
96
|
+
* Who holds it without an explicit grant. `User` needs an unbound, unmanaged `user`-area
|
|
97
|
+
* definition; `Member` / `Owner` need an `entityScoped` one. `None` (or omitted on creation) is
|
|
98
|
+
* nobody.
|
|
99
|
+
*/
|
|
100
|
+
defaultClass?: IamDefaultClass
|
|
40
101
|
}
|
|
41
102
|
|
|
42
103
|
export interface IamResourceSpec {
|
|
@@ -46,33 +107,156 @@ export interface IamResourceSpec {
|
|
|
46
107
|
|
|
47
108
|
/** A permission declared for an entity's client (project). */
|
|
48
109
|
export interface IamPermissionDefinition {
|
|
49
|
-
/** Canonical name: "res--action" or bare "res" when unscoped. */
|
|
110
|
+
/** Canonical name: "res--action" or bare "res" when unscoped. NEVER carries an `@` selector. */
|
|
50
111
|
name: string
|
|
51
112
|
resource: string
|
|
52
113
|
action?: string
|
|
53
114
|
resourceScoped?: boolean
|
|
115
|
+
/** Grants are bound to one organization. See `IamPermissionArgs.entityScoped`. */
|
|
116
|
+
entityScoped?: boolean
|
|
54
117
|
title?: string
|
|
118
|
+
/** Grouping tag; presentational only. See `IamPermissionArgs.area`. */
|
|
119
|
+
area?: string
|
|
120
|
+
/** Owned by the platform rather than declared by the application. See `IamPermissionArgs.managed`. */
|
|
121
|
+
managed?: boolean
|
|
122
|
+
/** Who holds it by default. See `IamPermissionArgs.defaultClass`. */
|
|
123
|
+
defaultClass?: IamDefaultClass
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Narrows a definition listing. An `areas` entry of `null` matches definitions carrying no tag. */
|
|
127
|
+
export interface IamPermissionFilter {
|
|
128
|
+
areas?: (string | null)[]
|
|
129
|
+
managed?: boolean
|
|
130
|
+
resourceScoped?: boolean
|
|
131
|
+
entityScoped?: boolean
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A group, named the way it is on the wire: its organization's slug and its key in that organization. */
|
|
135
|
+
export interface IamGroupRef {
|
|
136
|
+
entitySlug: string
|
|
137
|
+
key: string
|
|
55
138
|
}
|
|
56
139
|
|
|
57
140
|
export interface IamGrantArgs {
|
|
58
141
|
/** Resource ids for the resource-scoped grant form; omit for an unscoped (project-wide) grant. */
|
|
59
142
|
resources?: string[]
|
|
143
|
+
/**
|
|
144
|
+
* Which form to address. THE DEFAULTS ARE ASYMMETRIC ON PURPOSE, to preserve the semantics every
|
|
145
|
+
* existing caller already relies on:
|
|
146
|
+
*
|
|
147
|
+
* grantPermission — `resources != null` ? Resources : Blanket
|
|
148
|
+
* revokePermission — `resources != null` ? Resources : All
|
|
149
|
+
*
|
|
150
|
+
* Revoke defaults to `All` because a bare `revokePermission(...)` has always meant "remove it
|
|
151
|
+
* everywhere". Narrowing that to `Blanket` would leave the resource-scoped set standing for every
|
|
152
|
+
* existing caller — a live grant surviving a revoke, which is the failure direction nobody detects.
|
|
153
|
+
*
|
|
154
|
+
* A `Blanket` or `All` revoke of a DECLARED definition RESTRICTS the subject: it writes the
|
|
155
|
+
* subject's own `false`, which cuts what it would inherit (class default, group, bundle).
|
|
156
|
+
* `Inherit` (revoke-only) drops that own value instead, grant or restriction.
|
|
157
|
+
*
|
|
158
|
+
* Pass `mode` explicitly rather than depending on the default.
|
|
159
|
+
*/
|
|
160
|
+
mode?: IamGrantMode
|
|
161
|
+
/**
|
|
162
|
+
* The organization a grant of an `entityScoped` definition is bound to — REQUIRED for one (it
|
|
163
|
+
* names the subject's row there, which must exist) and refused for any other definition.
|
|
164
|
+
*/
|
|
165
|
+
entitySlug?: string
|
|
60
166
|
}
|
|
61
167
|
|
|
62
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* A permission held by ONE subject — a person (`profileId`) or a group (`group`), never both — of an
|
|
170
|
+
* entity's client.
|
|
171
|
+
*/
|
|
63
172
|
export interface IamGrant {
|
|
64
|
-
/** Backend-specific subject id: integrated =
|
|
65
|
-
profileId
|
|
173
|
+
/** Backend-specific subject id: integrated = the pairwise `profileId`, keycloak = KC user id. */
|
|
174
|
+
profileId?: string
|
|
175
|
+
/** A group subject. */
|
|
176
|
+
group?: IamGroupRef
|
|
66
177
|
clientId: string
|
|
67
178
|
/** Canonical permission name. */
|
|
68
179
|
permission: string
|
|
69
180
|
/** Present only for resource-scoped grants. */
|
|
70
181
|
resources?: string[]
|
|
182
|
+
/** Which form this record is. Derived from `resources`, not new information. */
|
|
183
|
+
mode?: IamGrantMode
|
|
184
|
+
/** The organization the grant is bound to; absent for an unbound one. */
|
|
185
|
+
entitySlug?: string
|
|
186
|
+
/** Why the subject holds it. Listings report it; a write answers `Direct`. */
|
|
187
|
+
origin?: IamGrantOrigin
|
|
188
|
+
/** The group a `Group`-origin grant reaches the person through. */
|
|
189
|
+
through?: IamGroupRef
|
|
190
|
+
/**
|
|
191
|
+
* Present (`true`) only when a listing asked for restrictions (`IamGrantListArgs.restricted`): an
|
|
192
|
+
* inherited grant — by default class, group or bundle — that the subject's OWN restriction
|
|
193
|
+
* withholds. It is NOT held and never reaches a claim; a console shows it so the restriction can
|
|
194
|
+
* be lifted (`IamGrantMode.Inherit`).
|
|
195
|
+
*/
|
|
196
|
+
restricted?: boolean
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** What a grant listing adds to what is held. */
|
|
200
|
+
export interface IamGrantListArgs {
|
|
201
|
+
/** Also list the inherited grants the subject's own restriction withholds, as `restricted: true`. */
|
|
202
|
+
restricted?: boolean
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Grants every definition a filter selects, plus any named outright. */
|
|
206
|
+
export interface IamGrantBundle {
|
|
207
|
+
filter?: IamPermissionFilter
|
|
208
|
+
/** Extra names included regardless of the filter — a platform marker, for instance. */
|
|
209
|
+
permissions?: string[]
|
|
210
|
+
/** Defaults to `Blanket`: a tier of access is a role, not a resource ACL. */
|
|
211
|
+
mode?: IamGrantMode
|
|
212
|
+
/** Only meaningful with `mode: Resources`. */
|
|
213
|
+
resources?: string[]
|
|
214
|
+
/** The binding of the bundle's `entityScoped` definitions. See `IamGrantArgs.entitySlug`. */
|
|
215
|
+
entitySlug?: string
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface IamPermissionDeleteArgs {
|
|
219
|
+
/** Defaults to `Cascade`. */
|
|
220
|
+
policy?: IamRemovalPolicy
|
|
221
|
+
/** Required to delete a `managed` definition. Defaults to false. */
|
|
222
|
+
managed?: boolean
|
|
71
223
|
}
|
|
72
224
|
|
|
73
|
-
|
|
225
|
+
export interface IamPermissionRemoval {
|
|
226
|
+
permission: string
|
|
227
|
+
clientId: string
|
|
228
|
+
/** The definition existed and was removed. False is a normal, non-error outcome. */
|
|
229
|
+
found: boolean
|
|
230
|
+
/** Grants stripped as part of the removal. */
|
|
231
|
+
revoked: IamGrant[]
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface IamNormalizeArgs {
|
|
235
|
+
/** Compute and report the plan without writing anything. */
|
|
236
|
+
dryRun?: boolean
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface IamNormalizationReport {
|
|
240
|
+
clientId: string
|
|
241
|
+
renamed: { from: string, to: string, grantsMoved: number }[]
|
|
242
|
+
merged: { from: string, into: string, grantsMoved: number }[]
|
|
243
|
+
/** Well-formed definitions deliberately left alone. */
|
|
244
|
+
untouched: string[]
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** One organization a user of a client belongs to there. */
|
|
248
|
+
export interface IamMembershipInfo {
|
|
249
|
+
entitySlug: string
|
|
250
|
+
title?: string
|
|
251
|
+
owner: boolean
|
|
252
|
+
groups: string[]
|
|
253
|
+
/** The organization the user starts in. */
|
|
254
|
+
home?: boolean
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** A subject of one client (app): one account, seen through that app's rows. */
|
|
74
258
|
export interface IamUser {
|
|
75
|
-
/** Backend-specific subject id: integrated =
|
|
259
|
+
/** Backend-specific subject id: integrated = the pairwise `profileId`, keycloak = KC user id. */
|
|
76
260
|
profileId: string
|
|
77
261
|
/** Primary login identifier (email for the integrated OTP path). */
|
|
78
262
|
email?: string
|
|
@@ -82,9 +266,12 @@ export interface IamUser {
|
|
|
82
266
|
disabled?: boolean
|
|
83
267
|
/** Convenience count of permission grants the user holds (across clients, or for one client). */
|
|
84
268
|
grantCount?: number
|
|
269
|
+
/** The `entitySlug` of the organization the user starts in. */
|
|
270
|
+
home?: string
|
|
271
|
+
memberships?: IamMembershipInfo[]
|
|
85
272
|
}
|
|
86
273
|
|
|
87
|
-
/** Args to invite/create an end-user
|
|
274
|
+
/** Args to invite/create an end-user of a client. */
|
|
88
275
|
export interface IamUserInvite {
|
|
89
276
|
email: string
|
|
90
277
|
name?: string
|
|
@@ -92,13 +279,180 @@ export interface IamUserInvite {
|
|
|
92
279
|
role?: string
|
|
93
280
|
}
|
|
94
281
|
|
|
95
|
-
/** Args to update an existing end-user. */
|
|
282
|
+
/** Args to update an existing end-user. `disabled` is written on the user's primary row. */
|
|
96
283
|
export interface IamUserUpdate {
|
|
97
284
|
name?: string
|
|
98
285
|
role?: string
|
|
99
286
|
disabled?: boolean
|
|
100
287
|
}
|
|
101
288
|
|
|
289
|
+
/** An organization as a client's administration sees it. Named by slug only. */
|
|
290
|
+
export interface IamOrganization {
|
|
291
|
+
entitySlug: string
|
|
292
|
+
title?: string
|
|
293
|
+
/** How many subjects of the client are members. */
|
|
294
|
+
members?: number
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** `organizations.create`: the organization's title, and the subject who becomes its owner. */
|
|
298
|
+
export interface IamOrganizationArgs {
|
|
299
|
+
title?: string
|
|
300
|
+
/** A subject of the client — the creator, on the runtime path. */
|
|
301
|
+
profileId?: string
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface IamOrganizationUpdate {
|
|
305
|
+
title: string
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** A member of an organization, as one client sees it. */
|
|
309
|
+
export interface IamMember {
|
|
310
|
+
profileId: string
|
|
311
|
+
email?: string
|
|
312
|
+
name?: string
|
|
313
|
+
owner: boolean
|
|
314
|
+
/** Group keys of (this organization, this client). */
|
|
315
|
+
groups: string[]
|
|
316
|
+
/** Written by the staff synchronization; an operator cannot change it. */
|
|
317
|
+
managed?: boolean
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Find-or-create by e-mail: an invitation and a first sign-in converge on one account. */
|
|
321
|
+
export interface IamMemberInvite {
|
|
322
|
+
email: string
|
|
323
|
+
name?: string
|
|
324
|
+
owner?: boolean
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export interface IamMemberUpdate {
|
|
328
|
+
owner?: boolean
|
|
329
|
+
groups?: string[]
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** What a group grants its members: every definition the filter selects, plus names outright. */
|
|
333
|
+
export interface IamGroupBundle {
|
|
334
|
+
filter?: IamPermissionFilter
|
|
335
|
+
permissions?: string[]
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** A group of (organization, client) — kept inside the organization record. */
|
|
339
|
+
export interface IamGroup {
|
|
340
|
+
entitySlug: string
|
|
341
|
+
key: string
|
|
342
|
+
title?: string
|
|
343
|
+
/**
|
|
344
|
+
* Kept by the platform (the staff sync's `members`): its membership, bundles and existence are
|
|
345
|
+
* not an operator's to write. Its grants and restrictions are.
|
|
346
|
+
*/
|
|
347
|
+
managed?: boolean
|
|
348
|
+
members?: number
|
|
349
|
+
bundles: IamGroupBundle[]
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export interface IamGroupArgs {
|
|
353
|
+
title?: string
|
|
354
|
+
bundles?: IamGroupBundle[]
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** `syncStaff`: what the managed `members` group of the owning organization grants. */
|
|
358
|
+
export interface IamStaffSyncArgs {
|
|
359
|
+
bundles: IamGroupBundle[]
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** The subjects whose managed rows the sync created and removed. */
|
|
363
|
+
export interface IamStaffSyncReport {
|
|
364
|
+
added: string[]
|
|
365
|
+
removed: string[]
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Who a subject is to a client, as the provider builds its claims. */
|
|
369
|
+
export interface IamSubject {
|
|
370
|
+
/** The pairwise subject: `subjects.identify(clientId, accountId)`. */
|
|
371
|
+
profileId: string
|
|
372
|
+
email?: string
|
|
373
|
+
name?: string
|
|
374
|
+
organizations: OidcOrganizationClaim[]
|
|
375
|
+
/** The FULL claim sets: a set bound to an organization carries its `entitySlug`. */
|
|
376
|
+
permissions: OidcPermissionSetClaim[]
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export interface IamSignInArgs {
|
|
380
|
+
email: string
|
|
381
|
+
name?: string
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export interface IamSignInResult {
|
|
385
|
+
accountId: string
|
|
386
|
+
profileId: string
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The organizations a client's subjects act in. Every method takes `(entityId, clientId, …)`: the
|
|
391
|
+
* entity that OWNS the client, then the client — the organizations themselves are named by slug.
|
|
392
|
+
*/
|
|
393
|
+
export interface IamOrganizationFacet {
|
|
394
|
+
/** Every organization with a member of the client. */
|
|
395
|
+
list: (entityId: string, clientId: string) => Promise<IamOrganization[]>
|
|
396
|
+
get: (entityId: string, clientId: string, entitySlug: string) => Promise<IamOrganization | null>
|
|
397
|
+
create: (entityId: string, clientId: string, args?: IamOrganizationArgs) => Promise<IamOrganization>
|
|
398
|
+
update: (entityId: string, clientId: string, entitySlug: string, update: IamOrganizationUpdate) => Promise<IamOrganization>
|
|
399
|
+
members: (entityId: string, clientId: string, entitySlug: string) => Promise<IamMember[]>
|
|
400
|
+
/** Idempotent by e-mail. Refused past the membership cap. */
|
|
401
|
+
addMember: (entityId: string, clientId: string, entitySlug: string, invite: IamMemberInvite) => Promise<IamMember>
|
|
402
|
+
/** Refused when it would leave the organization without an owner. */
|
|
403
|
+
updateMember: (
|
|
404
|
+
entityId: string, clientId: string, entitySlug: string, profileId: string, update: IamMemberUpdate
|
|
405
|
+
) => Promise<IamMember>
|
|
406
|
+
/** Refused for the last owner. */
|
|
407
|
+
removeMember: (entityId: string, clientId: string, entitySlug: string, profileId: string) => Promise<void>
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Groups of (organization, client). Every method takes `(entityId, clientId, entitySlug, …)` and the
|
|
412
|
+
* group key after it where one is addressed; one client never reads or writes another client's group.
|
|
413
|
+
*/
|
|
414
|
+
export interface IamGroupFacet {
|
|
415
|
+
list: (entityId: string, clientId: string, entitySlug: string) => Promise<IamGroup[]>
|
|
416
|
+
ensure: (entityId: string, clientId: string, entitySlug: string, key: string, args?: IamGroupArgs) => Promise<IamGroup>
|
|
417
|
+
/** Also drops the key from every member row. Refused for a managed group. */
|
|
418
|
+
remove: (entityId: string, clientId: string, entitySlug: string, key: string) => Promise<void>
|
|
419
|
+
members: (entityId: string, clientId: string, entitySlug: string, key: string) => Promise<IamMember[]>
|
|
420
|
+
addMembers: (entityId: string, clientId: string, entitySlug: string, key: string, profileIds: string[]) => Promise<void>
|
|
421
|
+
removeMembers: (entityId: string, clientId: string, entitySlug: string, key: string, profileIds: string[]) => Promise<void>
|
|
422
|
+
/** The binding of a group grant is the group's organization; `args.entitySlug` is not taken. */
|
|
423
|
+
grant: (
|
|
424
|
+
entityId: string, clientId: string, entitySlug: string, key: string, permission: string,
|
|
425
|
+
args?: Omit<IamGrantArgs, 'entitySlug'>
|
|
426
|
+
) => Promise<IamGrant>
|
|
427
|
+
revoke: (
|
|
428
|
+
entityId: string, clientId: string, entitySlug: string, key: string, permission: string,
|
|
429
|
+
args?: Omit<IamGrantArgs, 'entitySlug'>
|
|
430
|
+
) => Promise<void>
|
|
431
|
+
/**
|
|
432
|
+
* What the group holds: its own grants (`Direct`) and the names its bundles select (`Bundle`),
|
|
433
|
+
* minus the names it restricts — which `args.restricted` lists too, as `restricted: true`.
|
|
434
|
+
*/
|
|
435
|
+
grants: (
|
|
436
|
+
entityId: string, clientId: string, entitySlug: string, key: string, args?: IamGrantListArgs
|
|
437
|
+
) => Promise<IamGrant[]>
|
|
438
|
+
setBundles: (
|
|
439
|
+
entityId: string, clientId: string, entitySlug: string, key: string, bundles: IamGroupBundle[]
|
|
440
|
+
) => Promise<IamGroup>
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The provider's half: who signs in to a client and what the claims say about them. Keyed by the
|
|
445
|
+
* ACCOUNT, which never leaves the provider; what a client sees is the pairwise subject.
|
|
446
|
+
*/
|
|
447
|
+
export interface IamSubjectFacet {
|
|
448
|
+
/** `null` = not admitted: no row of the client, or a disabled primary row. */
|
|
449
|
+
resolve: (clientId: string, accountId: string) => Promise<IamSubject | null>
|
|
450
|
+
/** Find-or-create the account by e-mail and its rows for the client. Race-safe. */
|
|
451
|
+
signIn: (clientId: string, args: IamSignInArgs) => Promise<IamSignInResult>
|
|
452
|
+
/** The pairwise `sub` of the account for the client — computed, the same on every call. */
|
|
453
|
+
identify: (clientId: string, accountId: string) => string
|
|
454
|
+
}
|
|
455
|
+
|
|
102
456
|
/** Unified IAM provider interface — all platform/agent code calls only this, never a backend directly */
|
|
103
457
|
export interface IamService extends InitializedService {
|
|
104
458
|
// --- Admin config (backend → OIDC RP config, payment provisioning) ---
|
|
@@ -132,6 +486,9 @@ export interface IamService extends InitializedService {
|
|
|
132
486
|
*/
|
|
133
487
|
ensureClient: (entityId: string, clientId: string, options?: IamClientOptions) => Promise<IamClient>
|
|
134
488
|
|
|
489
|
+
/** The entity's client without its secret, or null when there is none. Never provisions. */
|
|
490
|
+
getClient: (entityId: string, clientId: string) => Promise<IamClientInfo | null>
|
|
491
|
+
|
|
135
492
|
/**
|
|
136
493
|
* Reserve a client id for the entity without provisioning it, so a caller can find a free name
|
|
137
494
|
* before committing to it. Returns false when the id is already taken — by this entity or any
|
|
@@ -144,14 +501,29 @@ export interface IamService extends InitializedService {
|
|
|
144
501
|
claimClient: (entityId: string, clientId: string) => Promise<boolean>
|
|
145
502
|
|
|
146
503
|
/**
|
|
147
|
-
* Release a client id and everything keyed by it
|
|
148
|
-
* without it a recreated project can
|
|
504
|
+
* Release a client id and everything keyed by it — its subjects' rows, its groups and its
|
|
505
|
+
* definitions. Called when a project or slot is deleted; without it a recreated project can
|
|
506
|
+
* inherit a stale registration.
|
|
149
507
|
*/
|
|
150
508
|
deleteClient: (entityId: string, clientId: string) => Promise<void>
|
|
151
509
|
|
|
152
510
|
/**
|
|
153
511
|
* Ensures a permission/resource exists in the entity's client.
|
|
154
512
|
* Returns the canonical resource name (e.g. "res--action" or "res").
|
|
513
|
+
*
|
|
514
|
+
* MERGES into an existing definition rather than replacing it: `resource` and `action` are
|
|
515
|
+
* re-derived and overwrite, while `title`, `resourceScoped`, `entityScoped`, `area`, `managed` and
|
|
516
|
+
* `defaultClass` are set when provided — including when provided as `false` / `None` — and KEPT
|
|
517
|
+
* when omitted. Replacing wholesale made every flag last-write-wins, so a caller that happened not
|
|
518
|
+
* to pass one erased it.
|
|
519
|
+
*
|
|
520
|
+
* It NEVER rewrites the name. Callers round-trip a definition's `resource` + `action` back through
|
|
521
|
+
* this method to copy a definition set between clients, so a rename here would silently orphan
|
|
522
|
+
* every grant made against the old name.
|
|
523
|
+
*
|
|
524
|
+
* The `resource` argument must not carry a gate selector: `@` is the gate's syntax and is never
|
|
525
|
+
* part of a stored name. Implementations reject one rather than storing a key no gate looks up.
|
|
526
|
+
* A `defaultClass` the definition's flags do not admit is refused (`IamPermissionError`).
|
|
155
527
|
*/
|
|
156
528
|
ensurePermission: (
|
|
157
529
|
entityId: string,
|
|
@@ -160,6 +532,49 @@ export interface IamService extends InitializedService {
|
|
|
160
532
|
args?: IamPermissionArgs
|
|
161
533
|
) => Promise<string>
|
|
162
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Removes permission definitions, and by default every grant that referenced them.
|
|
537
|
+
*
|
|
538
|
+
* Takes NAMES: the definition record is keyed by name, and a malformed definition's stored
|
|
539
|
+
* `resource`/`action` cannot be trusted to recompose that key. Accepts an array so a repair costs
|
|
540
|
+
* one sweep of the entity's subjects rather than one per name.
|
|
541
|
+
*
|
|
542
|
+
* Idempotent — a name with no definition yields `{ found: false }` and writes nothing. Every other
|
|
543
|
+
* lifecycle operation here converges rather than erroring, and two repairs may run concurrently on
|
|
544
|
+
* one project.
|
|
545
|
+
*
|
|
546
|
+
* Implementations MUST revoke the grants before dropping the definition. There is no transaction
|
|
547
|
+
* across the two stores, and a crash between them must leave a definition with no grants — visible
|
|
548
|
+
* and re-runnable — rather than grants with no definition, which are invisible to any screen that
|
|
549
|
+
* renders the definition list yet still satisfy a gate.
|
|
550
|
+
*
|
|
551
|
+
* A `managed` definition is refused unless `args.managed` is explicitly true.
|
|
552
|
+
*/
|
|
553
|
+
deletePermission: (
|
|
554
|
+
entityId: string,
|
|
555
|
+
clientId: string,
|
|
556
|
+
permission: string | string[],
|
|
557
|
+
args?: IamPermissionDeleteArgs
|
|
558
|
+
) => Promise<IamPermissionRemoval[]>
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Repairs definitions whose stored name carries a gate selector, moving grants with them.
|
|
562
|
+
*
|
|
563
|
+
* Strictly a rename and merge — never a delete, never an invention. Where the stripped name
|
|
564
|
+
* already exists the two are merged, because one entrypoint scoping a permission by `:id` and
|
|
565
|
+
* another by `:enquiryId` describe ONE permission. The target is marked resource-scoped: a leaked
|
|
566
|
+
* selector is positive evidence that it is, and that evidence is otherwise discarded.
|
|
567
|
+
*
|
|
568
|
+
* Grants move in the same write that renames the definition, which is why this exists as its own
|
|
569
|
+
* operation: reconstructing it as delete + re-declare + re-grant drops real access on any failure
|
|
570
|
+
* between the three steps.
|
|
571
|
+
*/
|
|
572
|
+
normalizePermissions: (
|
|
573
|
+
entityId: string,
|
|
574
|
+
clientId: string,
|
|
575
|
+
args?: IamNormalizeArgs
|
|
576
|
+
) => Promise<IamNormalizationReport>
|
|
577
|
+
|
|
163
578
|
/**
|
|
164
579
|
* Idempotent: creates a resource, a client role for it, and assigns that role to
|
|
165
580
|
* the entity-owner role. Used by payment provisioning for account/project/wl resources.
|
|
@@ -172,12 +587,17 @@ export interface IamService extends InitializedService {
|
|
|
172
587
|
|
|
173
588
|
// --- Authorization (permission definitions & grants) ---
|
|
174
589
|
|
|
175
|
-
/** Lists permission definitions registered for the entity's client. */
|
|
176
|
-
listPermissions: (
|
|
590
|
+
/** Lists permission definitions registered for the entity's client, optionally narrowed. */
|
|
591
|
+
listPermissions: (
|
|
592
|
+
entityId: string,
|
|
593
|
+
clientId: string,
|
|
594
|
+
filter?: IamPermissionFilter
|
|
595
|
+
) => Promise<IamPermissionDefinition[]>
|
|
177
596
|
|
|
178
597
|
/**
|
|
179
598
|
* Grants a permission to an end-user subject. With args.resources the grant is
|
|
180
599
|
* resource-scoped (bound to those resource ids); without it the grant is project-wide.
|
|
600
|
+
* An `entityScoped` definition is granted on the subject's row of `args.entitySlug`.
|
|
181
601
|
*/
|
|
182
602
|
grantPermission: (
|
|
183
603
|
entityId: string,
|
|
@@ -189,7 +609,8 @@ export interface IamService extends InitializedService {
|
|
|
189
609
|
|
|
190
610
|
/**
|
|
191
611
|
* Revokes a grant. With args.resources only those resource ids are removed;
|
|
192
|
-
* without it the whole grant is removed.
|
|
612
|
+
* without it the whole grant is removed. A blanket revoke of a declared definition also
|
|
613
|
+
* restricts the subject from what it inherits; `IamGrantMode.Inherit` takes that back.
|
|
193
614
|
*/
|
|
194
615
|
revokePermission: (
|
|
195
616
|
entityId: string,
|
|
@@ -199,29 +620,69 @@ export interface IamService extends InitializedService {
|
|
|
199
620
|
args?: IamGrantArgs
|
|
200
621
|
) => Promise<void>
|
|
201
622
|
|
|
202
|
-
/**
|
|
203
|
-
|
|
623
|
+
/**
|
|
624
|
+
* Grants every permission a bundle selects to each subject, idempotently.
|
|
625
|
+
*
|
|
626
|
+
* Exists as its own operation rather than a loop over `grantPermission` because a backend that
|
|
627
|
+
* loads-mutates-saves a subject per call turns N permissions across M subjects into N·M writes and
|
|
628
|
+
* N·M lost-update windows on the same records. Collapsing to one read and one write per subject is
|
|
629
|
+
* also what makes re-running it genuinely idempotent.
|
|
630
|
+
*
|
|
631
|
+
* Returns what each subject now holds, not what changed.
|
|
632
|
+
*/
|
|
633
|
+
grantBundle: (
|
|
634
|
+
entityId: string,
|
|
635
|
+
clientId: string,
|
|
636
|
+
profileIds: string[],
|
|
637
|
+
bundle: IamGrantBundle
|
|
638
|
+
) => Promise<IamGrant[]>
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Lists grants for the entity's client, optionally for a single subject — what each holds
|
|
642
|
+
* directly, by its definition's default class and through its groups, `origin` telling which.
|
|
643
|
+
* Only what is HELD, unless `args.restricted` adds the inherited grants the subject's own
|
|
644
|
+
* restriction withholds (`restricted: true`).
|
|
645
|
+
*/
|
|
646
|
+
listGrants: (
|
|
647
|
+
entityId: string, clientId: string, profileId?: string, args?: IamGrantListArgs
|
|
648
|
+
) => Promise<IamGrant[]>
|
|
204
649
|
|
|
205
|
-
// --- End-user management (
|
|
650
|
+
// --- End-user management (per client: rows of one app) ---
|
|
206
651
|
|
|
207
652
|
/**
|
|
208
|
-
* Lists the
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* there is still that project's user, and the screen that manages grants is exactly where they
|
|
212
|
-
* must be visible.
|
|
653
|
+
* Lists the subjects of the client — every account holding a row of that app, in whichever
|
|
654
|
+
* organization. Without `clientId` it is a read-only aggregate over every client the entity
|
|
655
|
+
* owns, for a console; nothing is written through it.
|
|
213
656
|
*/
|
|
214
657
|
listUsers: (entityId: string, clientId?: string) => Promise<IamUser[]>
|
|
215
658
|
|
|
216
|
-
/** Loads a single
|
|
217
|
-
getUser: (entityId: string, profileId: string) => Promise<IamUser | null>
|
|
659
|
+
/** Loads a single subject of the client by its id, or null when absent. */
|
|
660
|
+
getUser: (entityId: string, clientId: string, profileId: string) => Promise<IamUser | null>
|
|
661
|
+
|
|
662
|
+
/** Creates (or resolves, idempotently by email) a subject of the client. */
|
|
663
|
+
inviteUser: (entityId: string, clientId: string, invite: IamUserInvite) => Promise<IamUser>
|
|
218
664
|
|
|
219
|
-
/**
|
|
220
|
-
|
|
665
|
+
/** Updates a subject's mutable fields; `disabled` is written on its primary row. */
|
|
666
|
+
updateUser: (entityId: string, clientId: string, profileId: string, update: IamUserUpdate) => Promise<IamUser>
|
|
221
667
|
|
|
222
|
-
/**
|
|
223
|
-
|
|
668
|
+
/** Deletes this account's rows of the client only — the account and its other apps stay. */
|
|
669
|
+
removeUser: (entityId: string, clientId: string, profileId: string) => Promise<void>
|
|
224
670
|
|
|
225
|
-
/**
|
|
226
|
-
|
|
671
|
+
/**
|
|
672
|
+
* Admits the owning organization's own staff to the client: keeps the managed `IAM_MEMBERS_GROUP`
|
|
673
|
+
* group there with `bundles`, a managed row per staff account, and removes the managed rows of
|
|
674
|
+
* people no longer on the staff.
|
|
675
|
+
*/
|
|
676
|
+
syncStaff: (entityId: string, clientId: string, args: IamStaffSyncArgs) => Promise<IamStaffSyncReport>
|
|
677
|
+
|
|
678
|
+
// --- Tenancy facets — a backend without them answers `unsupportedFacet(...)` ---
|
|
679
|
+
|
|
680
|
+
organizations: IamOrganizationFacet
|
|
681
|
+
groups: IamGroupFacet
|
|
682
|
+
subjects: IamSubjectFacet
|
|
227
683
|
}
|
|
684
|
+
|
|
685
|
+
// Kept as a type: a union of the mode constants' literal types.
|
|
686
|
+
export type IamMode = typeof IAM_MODE_KEYCLOAK | typeof IAM_MODE_INTEGRATED
|
|
687
|
+
|
|
688
|
+
export type IamArea = typeof IAM_AREAS[number]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface HasPermissionOptions {
|
|
2
|
+
/** Restrict the check to PermissionSets of this scope (the project's clientId). */
|
|
3
|
+
scope?: string
|
|
4
|
+
/** Resource-scoped check: the set must list this id — unless the set itself is unscoped. */
|
|
5
|
+
resourceId?: string
|
|
6
|
+
/**
|
|
7
|
+
* The organization the request acts in. A set bound to an organization (one carrying
|
|
8
|
+
* `entitySlug`) counts only when this names that same organization.
|
|
9
|
+
*/
|
|
10
|
+
entitySlug?: string
|
|
11
|
+
}
|