@owlmeans/iam 0.1.18-rc.2 → 0.1.18-rc.21
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 +2 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/iam/SKILL.md +193 -56
- package/build/consts.d.ts +91 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +97 -0
- package/build/consts.js.map +1 -1
- package/build/errors.d.ts +13 -0
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +17 -0
- package/build/errors.js.map +1 -1
- 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 +31 -0
- package/build/gate/param.d.ts.map +1 -0
- package/build/gate/param.js +99 -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/validate.d.ts +15 -0
- package/build/gate/validate.d.ts.map +1 -0
- package/build/gate/validate.js +107 -0
- package/build/gate/validate.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/permission.d.ts +21 -0
- package/build/permission.d.ts.map +1 -0
- package/build/permission.js +39 -0
- package/build/permission.js.map +1 -0
- package/build/types.d.ts +251 -6
- package/build/types.d.ts.map +1 -1
- package/package.json +7 -6
- package/src/consts.ts +105 -0
- package/src/errors.ts +19 -0
- package/src/gate/index.ts +3 -0
- package/src/gate/param.ts +122 -0
- package/src/gate/resolve.ts +114 -0
- package/src/gate/validate.ts +132 -0
- package/src/index.ts +2 -0
- package/src/permission.ts +48 -0
- package/src/types.ts +283 -6
- package/tests/gate-param.spec.ts +234 -0
package/build/types.d.ts
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
import type { InitializedService } from '@owlmeans/context';
|
|
2
2
|
import type { OidcProviderConfig } from '@owlmeans/oidc';
|
|
3
|
+
import type { GateParamSource, GateParamErrorCode, GateResolutionFailure, IamGrantMode, IamRemovalPolicy } from './consts.js';
|
|
4
|
+
/**
|
|
5
|
+
* The parts of a request a gate selector may read.
|
|
6
|
+
*
|
|
7
|
+
* Structural on purpose: `AbstractRequest` is assignable to it, so `@owlmeans/server-iam` passes its
|
|
8
|
+
* request straight through, while tooling outside the server stack can resolve a selector against a
|
|
9
|
+
* plain object without taking a dependency on `@owlmeans/entrypoint`.
|
|
10
|
+
*/
|
|
11
|
+
export interface GateRequestLike {
|
|
12
|
+
params?: unknown;
|
|
13
|
+
query?: unknown;
|
|
14
|
+
body?: unknown;
|
|
15
|
+
headers?: unknown;
|
|
16
|
+
auth?: unknown;
|
|
17
|
+
}
|
|
18
|
+
/** Where one gate param reads its resource id from. */
|
|
19
|
+
export interface GateResourceSelector {
|
|
20
|
+
/** The selector text exactly as written, for diagnostics. */
|
|
21
|
+
readonly selector: string;
|
|
22
|
+
/** Undefined for the bare form, which searches `sources` in order. */
|
|
23
|
+
readonly source?: GateParamSource;
|
|
24
|
+
/** Length 1 and UNSPLIT for the bare form — a bare key may legally contain dots. */
|
|
25
|
+
readonly path: string[];
|
|
26
|
+
/** The explicit source, or `DEFAULT_GATE_PARAM_SOURCES`. */
|
|
27
|
+
readonly sources: GateParamSource[];
|
|
28
|
+
}
|
|
29
|
+
export interface GateParamProblem {
|
|
30
|
+
code: GateParamErrorCode;
|
|
31
|
+
detail: string;
|
|
32
|
+
}
|
|
33
|
+
export interface ParsedGateParam {
|
|
34
|
+
permission: string;
|
|
35
|
+
/**
|
|
36
|
+
* @deprecated The bare-form flat key, kept so existing readers keep compiling. Read `resource`,
|
|
37
|
+
* which carries the source and the path for both forms.
|
|
38
|
+
*/
|
|
39
|
+
resourceParam?: string;
|
|
40
|
+
resource?: GateResourceSelector;
|
|
41
|
+
error?: GateParamProblem;
|
|
42
|
+
}
|
|
43
|
+
export interface GateResourceResolution {
|
|
44
|
+
id?: string;
|
|
45
|
+
from?: GateParamSource;
|
|
46
|
+
reason?: GateResolutionFailure;
|
|
47
|
+
}
|
|
48
|
+
/** What an entrypoint declares, so a selector can be checked against it before it is ever deployed. */
|
|
49
|
+
export interface GateParamAudit {
|
|
50
|
+
routePath?: string;
|
|
51
|
+
routeParams?: string[];
|
|
52
|
+
filter?: {
|
|
53
|
+
query?: object;
|
|
54
|
+
params?: object;
|
|
55
|
+
body?: object;
|
|
56
|
+
headers?: object;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export interface GateParamIssue {
|
|
60
|
+
param: string;
|
|
61
|
+
code: GateParamErrorCode;
|
|
62
|
+
detail: string;
|
|
63
|
+
}
|
|
64
|
+
/** A permission NAME taken apart. Never carries a selector — `@` is the gate's syntax alone. */
|
|
65
|
+
export interface ParsedPermissionName {
|
|
66
|
+
/** The whole name, exactly as it must be stored and granted. */
|
|
67
|
+
name: string;
|
|
68
|
+
/** What IAM stores as `resource`. The whole name when there is no action separator. */
|
|
69
|
+
resource: string;
|
|
70
|
+
/** What IAM stores as `action`. Absent for a bare, unsplittable legacy name. */
|
|
71
|
+
action?: string;
|
|
72
|
+
/** Set when the argument carried an `@` — which a NAME never legally does. */
|
|
73
|
+
problem?: GateParamProblem;
|
|
74
|
+
}
|
|
3
75
|
export interface IamClient {
|
|
4
76
|
id?: string;
|
|
5
77
|
clientId: string;
|
|
@@ -29,10 +101,32 @@ export interface IamClientOptions {
|
|
|
29
101
|
export interface IamPermissionArgs {
|
|
30
102
|
/** Action name. When absent the permission is unscoped (project-wide). */
|
|
31
103
|
permission?: string;
|
|
32
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* Declares that grants of this permission may be bound to specific resource ids.
|
|
106
|
+
*
|
|
107
|
+
* It does NOT mean grants must be: the same name is grantable in either form. A grant carrying no
|
|
108
|
+
* resource ids covers every resource, which is what `hasPermission` already implements.
|
|
109
|
+
*/
|
|
33
110
|
resourceScoped?: boolean;
|
|
34
111
|
/** Optional human-readable title for the permission definition. */
|
|
35
112
|
title?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Grouping tag — one of `IAM_AREAS`, or any string a deployment defines.
|
|
115
|
+
*
|
|
116
|
+
* Purely presentational: it groups the permission in an administrator's screen and is never read
|
|
117
|
+
* when a request is authorized. It is a TAG rather than a name segment precisely so an existing
|
|
118
|
+
* permission can be grouped without renaming it, which would orphan every grant already made.
|
|
119
|
+
*/
|
|
120
|
+
area?: string;
|
|
121
|
+
/**
|
|
122
|
+
* Marks a permission the PLATFORM owns rather than one the application declared for itself.
|
|
123
|
+
*
|
|
124
|
+
* The distinction is not cosmetic: an operator must not be able to revoke a platform marker from
|
|
125
|
+
* an undifferentiated list, and a repair that deletes "definitions no declaration names" must not
|
|
126
|
+
* reach one — a marker can be enforced by a runtime decoration rather than by a gate parameter,
|
|
127
|
+
* so its absence from the declarations is not evidence that it is unused.
|
|
128
|
+
*/
|
|
129
|
+
managed?: boolean;
|
|
36
130
|
}
|
|
37
131
|
export interface IamResourceSpec {
|
|
38
132
|
name: string;
|
|
@@ -40,16 +134,40 @@ export interface IamResourceSpec {
|
|
|
40
134
|
}
|
|
41
135
|
/** A permission declared for an entity's client (project). */
|
|
42
136
|
export interface IamPermissionDefinition {
|
|
43
|
-
/** Canonical name: "res--action" or bare "res" when unscoped. */
|
|
137
|
+
/** Canonical name: "res--action" or bare "res" when unscoped. NEVER carries an `@` selector. */
|
|
44
138
|
name: string;
|
|
45
139
|
resource: string;
|
|
46
140
|
action?: string;
|
|
47
141
|
resourceScoped?: boolean;
|
|
48
142
|
title?: string;
|
|
143
|
+
/** Grouping tag; presentational only. See `IamPermissionArgs.area`. */
|
|
144
|
+
area?: string;
|
|
145
|
+
/** Owned by the platform rather than declared by the application. See `IamPermissionArgs.managed`. */
|
|
146
|
+
managed?: boolean;
|
|
147
|
+
}
|
|
148
|
+
/** Narrows a definition listing. An `areas` entry of `null` matches definitions carrying no tag. */
|
|
149
|
+
export interface IamPermissionFilter {
|
|
150
|
+
areas?: (string | null)[];
|
|
151
|
+
managed?: boolean;
|
|
152
|
+
resourceScoped?: boolean;
|
|
49
153
|
}
|
|
50
154
|
export interface IamGrantArgs {
|
|
51
155
|
/** Resource ids for the resource-scoped grant form; omit for an unscoped (project-wide) grant. */
|
|
52
156
|
resources?: string[];
|
|
157
|
+
/**
|
|
158
|
+
* Which form to address. THE DEFAULTS ARE ASYMMETRIC ON PURPOSE, to preserve the semantics every
|
|
159
|
+
* existing caller already relies on:
|
|
160
|
+
*
|
|
161
|
+
* grantPermission — `resources != null` ? Resources : Blanket
|
|
162
|
+
* revokePermission — `resources != null` ? Resources : All
|
|
163
|
+
*
|
|
164
|
+
* Revoke defaults to `All` because a bare `revokePermission(...)` has always meant "remove it
|
|
165
|
+
* everywhere". Narrowing that to `Blanket` would leave the resource-scoped set standing for every
|
|
166
|
+
* existing caller — a live grant surviving a revoke, which is the failure direction nobody detects.
|
|
167
|
+
*
|
|
168
|
+
* Pass `mode` explicitly rather than depending on the default.
|
|
169
|
+
*/
|
|
170
|
+
mode?: IamGrantMode;
|
|
53
171
|
}
|
|
54
172
|
/** A permission granted to an end-user subject of an entity's client. */
|
|
55
173
|
export interface IamGrant {
|
|
@@ -60,6 +178,51 @@ export interface IamGrant {
|
|
|
60
178
|
permission: string;
|
|
61
179
|
/** Present only for resource-scoped grants. */
|
|
62
180
|
resources?: string[];
|
|
181
|
+
/** Which form this record is. Derived from `resources`, not new information. */
|
|
182
|
+
mode?: IamGrantMode;
|
|
183
|
+
}
|
|
184
|
+
/** Grants every definition a filter selects, plus any named outright. */
|
|
185
|
+
export interface IamGrantBundle {
|
|
186
|
+
filter?: IamPermissionFilter;
|
|
187
|
+
/** Extra names included regardless of the filter — a platform marker, for instance. */
|
|
188
|
+
permissions?: string[];
|
|
189
|
+
/** Defaults to `Blanket`: a tier of access is a role, not a resource ACL. */
|
|
190
|
+
mode?: IamGrantMode;
|
|
191
|
+
/** Only meaningful with `mode: Resources`. */
|
|
192
|
+
resources?: string[];
|
|
193
|
+
}
|
|
194
|
+
export interface IamPermissionDeleteArgs {
|
|
195
|
+
/** Defaults to `Cascade`. */
|
|
196
|
+
policy?: IamRemovalPolicy;
|
|
197
|
+
/** Required to delete a `managed` definition. Defaults to false. */
|
|
198
|
+
managed?: boolean;
|
|
199
|
+
}
|
|
200
|
+
export interface IamPermissionRemoval {
|
|
201
|
+
permission: string;
|
|
202
|
+
clientId: string;
|
|
203
|
+
/** The definition existed and was removed. False is a normal, non-error outcome. */
|
|
204
|
+
found: boolean;
|
|
205
|
+
/** Grants stripped as part of the removal. */
|
|
206
|
+
revoked: IamGrant[];
|
|
207
|
+
}
|
|
208
|
+
export interface IamNormalizeArgs {
|
|
209
|
+
/** Compute and report the plan without writing anything. */
|
|
210
|
+
dryRun?: boolean;
|
|
211
|
+
}
|
|
212
|
+
export interface IamNormalizationReport {
|
|
213
|
+
clientId: string;
|
|
214
|
+
renamed: {
|
|
215
|
+
from: string;
|
|
216
|
+
to: string;
|
|
217
|
+
grantsMoved: number;
|
|
218
|
+
}[];
|
|
219
|
+
merged: {
|
|
220
|
+
from: string;
|
|
221
|
+
into: string;
|
|
222
|
+
grantsMoved: number;
|
|
223
|
+
}[];
|
|
224
|
+
/** Well-formed definitions deliberately left alone. */
|
|
225
|
+
untouched: string[];
|
|
63
226
|
}
|
|
64
227
|
/** An end-user of an entity (customer-wide; shared across that entity's projects). */
|
|
65
228
|
export interface IamUser {
|
|
@@ -104,19 +267,87 @@ export interface IamService extends InitializedService {
|
|
|
104
267
|
* misconfiguration is loud instead of yielding a silently wrong issuer.
|
|
105
268
|
*/
|
|
106
269
|
getIssuerUrl: (entityId: string) => Promise<string>;
|
|
270
|
+
/**
|
|
271
|
+
* Find-or-create the entity's OIDC client.
|
|
272
|
+
*
|
|
273
|
+
* A client id is globally unique — a provider resolves it from the bare `client_id` a relying
|
|
274
|
+
* party sends, with no tenant context — so an implementation MUST refuse an existing record
|
|
275
|
+
* that belongs to a different entity (`IamClientError('client:entity-mismatch')`) rather than
|
|
276
|
+
* returning it. Silently adopting one hands the caller another tenant's secret and overwrites
|
|
277
|
+
* that tenant's redirect URIs.
|
|
278
|
+
*/
|
|
107
279
|
ensureClient: (entityId: string, clientId: string, options?: IamClientOptions) => Promise<IamClient>;
|
|
280
|
+
/**
|
|
281
|
+
* Reserve a client id for the entity without provisioning it, so a caller can find a free name
|
|
282
|
+
* before committing to it. Returns false when the id is already taken — by this entity or any
|
|
283
|
+
* other — and true when the reservation is now held.
|
|
284
|
+
*
|
|
285
|
+
* This exists because the id is minted from a name that may later be released (a project alias
|
|
286
|
+
* moves when its hostname is refused), so the naming index cannot answer whether an id is free.
|
|
287
|
+
* The registry is the only authority.
|
|
288
|
+
*/
|
|
289
|
+
claimClient: (entityId: string, clientId: string) => Promise<boolean>;
|
|
290
|
+
/**
|
|
291
|
+
* Release a client id and everything keyed by it. Called when a project or slot is deleted —
|
|
292
|
+
* without it a recreated project can inherit a stale registration.
|
|
293
|
+
*/
|
|
294
|
+
deleteClient: (entityId: string, clientId: string) => Promise<void>;
|
|
108
295
|
/**
|
|
109
296
|
* Ensures a permission/resource exists in the entity's client.
|
|
110
297
|
* Returns the canonical resource name (e.g. "res--action" or "res").
|
|
298
|
+
*
|
|
299
|
+
* MERGES into an existing definition rather than replacing it: `resource` and `action` are
|
|
300
|
+
* re-derived and overwrite, while `title`, `resourceScoped`, `area` and `managed` are set when
|
|
301
|
+
* provided — including when provided as `false` — and KEPT when omitted. Replacing wholesale made
|
|
302
|
+
* every flag last-write-wins, so a caller that happened not to pass one erased it.
|
|
303
|
+
*
|
|
304
|
+
* It NEVER rewrites the name. Callers round-trip a definition's `resource` + `action` back through
|
|
305
|
+
* this method to copy a definition set between clients, so a rename here would silently orphan
|
|
306
|
+
* every grant made against the old name.
|
|
307
|
+
*
|
|
308
|
+
* The `resource` argument must not carry a gate selector: `@` is the gate's syntax and is never
|
|
309
|
+
* part of a stored name. Implementations reject one rather than storing a key no gate looks up.
|
|
111
310
|
*/
|
|
112
311
|
ensurePermission: (entityId: string, clientId: string, resource?: string, args?: IamPermissionArgs) => Promise<string>;
|
|
312
|
+
/**
|
|
313
|
+
* Removes permission definitions, and by default every grant that referenced them.
|
|
314
|
+
*
|
|
315
|
+
* Takes NAMES: the definition record is keyed by name, and a malformed definition's stored
|
|
316
|
+
* `resource`/`action` cannot be trusted to recompose that key. Accepts an array so a repair costs
|
|
317
|
+
* one sweep of the entity's subjects rather than one per name.
|
|
318
|
+
*
|
|
319
|
+
* Idempotent — a name with no definition yields `{ found: false }` and writes nothing. Every other
|
|
320
|
+
* lifecycle operation here converges rather than erroring, and two repairs may run concurrently on
|
|
321
|
+
* one project.
|
|
322
|
+
*
|
|
323
|
+
* Implementations MUST revoke the grants before dropping the definition. There is no transaction
|
|
324
|
+
* across the two stores, and a crash between them must leave a definition with no grants — visible
|
|
325
|
+
* and re-runnable — rather than grants with no definition, which are invisible to any screen that
|
|
326
|
+
* renders the definition list yet still satisfy a gate.
|
|
327
|
+
*
|
|
328
|
+
* A `managed` definition is refused unless `args.managed` is explicitly true.
|
|
329
|
+
*/
|
|
330
|
+
deletePermission: (entityId: string, clientId: string, permission: string | string[], args?: IamPermissionDeleteArgs) => Promise<IamPermissionRemoval[]>;
|
|
331
|
+
/**
|
|
332
|
+
* Repairs definitions whose stored name carries a gate selector, moving grants with them.
|
|
333
|
+
*
|
|
334
|
+
* Strictly a rename and merge — never a delete, never an invention. Where the stripped name
|
|
335
|
+
* already exists the two are merged, because one entrypoint scoping a permission by `:id` and
|
|
336
|
+
* another by `:enquiryId` describe ONE permission. The target is marked resource-scoped: a leaked
|
|
337
|
+
* selector is positive evidence that it is, and that evidence is otherwise discarded.
|
|
338
|
+
*
|
|
339
|
+
* Grants move in the same write that renames the definition, which is why this exists as its own
|
|
340
|
+
* operation: reconstructing it as delete + re-declare + re-grant drops real access on any failure
|
|
341
|
+
* between the three steps.
|
|
342
|
+
*/
|
|
343
|
+
normalizePermissions: (entityId: string, clientId: string, args?: IamNormalizeArgs) => Promise<IamNormalizationReport>;
|
|
113
344
|
/**
|
|
114
345
|
* Idempotent: creates a resource, a client role for it, and assigns that role to
|
|
115
346
|
* the entity-owner role. Used by payment provisioning for account/project/wl resources.
|
|
116
347
|
*/
|
|
117
348
|
ensureResourceOwnership: (entityId: string, clientId: string, resource: IamResourceSpec) => Promise<void>;
|
|
118
|
-
/** Lists permission definitions registered for the entity's client. */
|
|
119
|
-
listPermissions: (entityId: string, clientId: string) => Promise<IamPermissionDefinition[]>;
|
|
349
|
+
/** Lists permission definitions registered for the entity's client, optionally narrowed. */
|
|
350
|
+
listPermissions: (entityId: string, clientId: string, filter?: IamPermissionFilter) => Promise<IamPermissionDefinition[]>;
|
|
120
351
|
/**
|
|
121
352
|
* Grants a permission to an end-user subject. With args.resources the grant is
|
|
122
353
|
* resource-scoped (bound to those resource ids); without it the grant is project-wide.
|
|
@@ -127,11 +358,25 @@ export interface IamService extends InitializedService {
|
|
|
127
358
|
* without it the whole grant is removed.
|
|
128
359
|
*/
|
|
129
360
|
revokePermission: (entityId: string, clientId: string, profileId: string, permission: string, args?: IamGrantArgs) => Promise<void>;
|
|
361
|
+
/**
|
|
362
|
+
* Grants every permission a bundle selects to each subject, idempotently.
|
|
363
|
+
*
|
|
364
|
+
* Exists as its own operation rather than a loop over `grantPermission` because a backend that
|
|
365
|
+
* loads-mutates-saves a subject per call turns N permissions across M subjects into N·M writes and
|
|
366
|
+
* N·M lost-update windows on the same records. Collapsing to one read and one write per subject is
|
|
367
|
+
* also what makes re-running it genuinely idempotent.
|
|
368
|
+
*
|
|
369
|
+
* Returns what each subject now holds, not what changed.
|
|
370
|
+
*/
|
|
371
|
+
grantBundle: (entityId: string, clientId: string, profileIds: string[], bundle: IamGrantBundle) => Promise<IamGrant[]>;
|
|
130
372
|
/** Lists grants for the entity's client, optionally for a single subject. */
|
|
131
373
|
listGrants: (entityId: string, clientId: string, profileId?: string) => Promise<IamGrant[]>;
|
|
132
374
|
/**
|
|
133
|
-
* Lists the entity's end-users.
|
|
134
|
-
*
|
|
375
|
+
* Lists the entity's end-users. End-users are customer-wide — shared across every project of
|
|
376
|
+
* the entity — so `clientId` scopes the reported `grantCount` to one project's client and does
|
|
377
|
+
* **not** filter the set: a user who has authenticated against a project but holds no grant
|
|
378
|
+
* there is still that project's user, and the screen that manages grants is exactly where they
|
|
379
|
+
* must be visible.
|
|
135
380
|
*/
|
|
136
381
|
listUsers: (entityId: string, clientId?: string) => Promise<IamUser[]>;
|
|
137
382
|
/** Loads a single end-user by subject id, or null when absent. */
|
package/build/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/iam",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -22,14 +22,15 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
26
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
27
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
28
|
-
"@owlmeans/oidc": "^0.1.18-rc.
|
|
25
|
+
"@owlmeans/auth": "^0.1.18-rc.14",
|
|
26
|
+
"@owlmeans/context": "^0.1.18-rc.13",
|
|
27
|
+
"@owlmeans/error": "^0.1.18-rc.13",
|
|
28
|
+
"@owlmeans/oidc": "^0.1.18-rc.26",
|
|
29
|
+
"@owlmeans/route": "^0.1.18-rc.14"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@owlmeans/dep-config": "workspace:*",
|
|
32
|
-
"@types/bun": "^1.
|
|
33
|
+
"@types/bun": "^1.4.0",
|
|
33
34
|
"nodemon": "^3.1.14",
|
|
34
35
|
"typescript": "^7.0.2"
|
|
35
36
|
},
|
package/src/consts.ts
CHANGED
|
@@ -4,3 +4,108 @@ export const IAM_MODE_KEYCLOAK = 'keycloak'
|
|
|
4
4
|
export const IAM_MODE_INTEGRATED = 'integrated'
|
|
5
5
|
|
|
6
6
|
export type IamMode = typeof IAM_MODE_KEYCLOAK | typeof IAM_MODE_INTEGRATED
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Gate-param syntax: `<permission>[@<selector>]`.
|
|
10
|
+
*
|
|
11
|
+
* The `@` belongs to the GATE and to nothing else. It says where a resource id is read from at
|
|
12
|
+
* request time; it is never part of a permission's stored NAME. A definition or a grant whose name
|
|
13
|
+
* carries one is a key nothing ever looks up, so every grant against it is a silent no-op.
|
|
14
|
+
*/
|
|
15
|
+
export const RESOURCE_PARAM_SEPARATOR = '@'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Separates an explicit source from its path: `@body:order.id`.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately `:` rather than a dot. Under a dotted-only form the meaning of a selector changes the
|
|
21
|
+
* moment a route gains a param named like a source keyword — `/report/:query` with `@query.id` flips
|
|
22
|
+
* from "the route param literally named `query.id`" to "the query string's `id`", redirecting an
|
|
23
|
+
* authorization lookup from a router-matched value to attacker-supplied input, caused by an
|
|
24
|
+
* unrelated route rename. `:` is decidable in one character, and no gate-param string written before
|
|
25
|
+
* this existed can contain one.
|
|
26
|
+
*/
|
|
27
|
+
export const RESOURCE_SOURCE_SEPARATOR = ':'
|
|
28
|
+
|
|
29
|
+
/** Separates path segments inside a qualified selector: `@body:order.id`. */
|
|
30
|
+
export const RESOURCE_PATH_SEPARATOR = '.'
|
|
31
|
+
|
|
32
|
+
/** Where a qualified selector may read a resource id from. */
|
|
33
|
+
export enum GateParamSource {
|
|
34
|
+
Params = 'params',
|
|
35
|
+
Query = 'query',
|
|
36
|
+
Body = 'body',
|
|
37
|
+
Headers = 'headers',
|
|
38
|
+
Auth = 'auth'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* What the bare form (`@enquiryId`) searches, in order.
|
|
43
|
+
*
|
|
44
|
+
* Params first, then query. The query fallback is legacy and deliberately kept: removing it would
|
|
45
|
+
* silently deny already-deployed applications whose selector happens to name a query key, and a
|
|
46
|
+
* silent denial inside a customer's running app is the worst available outcome.
|
|
47
|
+
*/
|
|
48
|
+
export const DEFAULT_GATE_PARAM_SOURCES: GateParamSource[] = [
|
|
49
|
+
GateParamSource.Params, GateParamSource.Query
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
/** Why a gate param is structurally wrong — a misconfiguration, not a failed authorization. */
|
|
53
|
+
export enum GateParamErrorCode {
|
|
54
|
+
UnknownSource = 'gate-param:unknown-source',
|
|
55
|
+
EmptySelector = 'gate-param:empty-selector',
|
|
56
|
+
EmptySegment = 'gate-param:empty-segment',
|
|
57
|
+
/** Named a request key the entrypoint's filter does not declare — AJV strips it before the gate. */
|
|
58
|
+
UnreachableKey = 'gate-param:unreachable-key',
|
|
59
|
+
/** Named a route param the entrypoint's own path does not carry. */
|
|
60
|
+
UnknownRouteParam = 'gate-param:unknown-route-param'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Why a selector produced no resource id. */
|
|
64
|
+
export enum GateResolutionFailure {
|
|
65
|
+
/** The source exists, the path does not. */
|
|
66
|
+
NotProvided = 'not-provided',
|
|
67
|
+
/** The request carries no such source at all. */
|
|
68
|
+
SourceMissing = 'source-missing',
|
|
69
|
+
/** Resolved to an object or a multi-element array — refused rather than stringified. */
|
|
70
|
+
NotScalar = 'not-scalar',
|
|
71
|
+
/** The selector itself did not parse. */
|
|
72
|
+
Malformed = 'malformed'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Which FORM of a grant an operation addresses.
|
|
77
|
+
*
|
|
78
|
+
* A resource-scoped permission is grantable two ways, under one name: `Blanket` (a PermissionSet
|
|
79
|
+
* carrying no `resources`, which `hasPermission` reads as covering every id) and `Resources` (an
|
|
80
|
+
* explicit id list). They are stored separately and must be revoked separately, or a UI offering
|
|
81
|
+
* both controls would have one silently wipe the other.
|
|
82
|
+
*/
|
|
83
|
+
export enum IamGrantMode {
|
|
84
|
+
Blanket = 'blanket',
|
|
85
|
+
Resources = 'resources',
|
|
86
|
+
/** Revoke-only: every form. Illegal on a grant. */
|
|
87
|
+
All = 'all'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** What to do about grants that still reference a definition being removed. */
|
|
91
|
+
export enum IamRemovalPolicy {
|
|
92
|
+
/** Strip the permission from every subject of the entity, then drop the definition. */
|
|
93
|
+
Cascade = 'cascade',
|
|
94
|
+
/** Refuse while any subject still holds it; the holders travel in the error. */
|
|
95
|
+
Refuse = 'refuse'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The grouping tiers a permission definition may be tagged with.
|
|
100
|
+
*
|
|
101
|
+
* A tag decides NOTHING at request time — `hasPermission` and every gate treat a permission name as
|
|
102
|
+
* one opaque string — and only groups the permission in an administrator's screen. `guest` is
|
|
103
|
+
* deliberately absent: a public surface declares no permissions, and adding an area later would
|
|
104
|
+
* re-classify names already tagged.
|
|
105
|
+
*/
|
|
106
|
+
export const IAM_AREAS = ['user', 'operator', 'admin'] as const
|
|
107
|
+
|
|
108
|
+
export type IamArea = typeof IAM_AREAS[number]
|
|
109
|
+
|
|
110
|
+
/** Separates the resource from the action in a permission name. TWO hyphens, never one. */
|
|
111
|
+
export const PERMISSION_ACTION_SEPARATOR = '--'
|
package/src/errors.ts
CHANGED
|
@@ -36,6 +36,24 @@ export class IamGrantError extends IamError {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* A refusal about a permission DEFINITION, as opposed to a grant.
|
|
41
|
+
*
|
|
42
|
+
* Separate from `IamGrantError`, which means a grant's subject is missing or its entity does not
|
|
43
|
+
* match — reusing it for a definition-level refusal is a category error the next reader has to unpick.
|
|
44
|
+
*
|
|
45
|
+
* Codes: `permission:held:<name>` (still granted to someone, under a policy that refuses),
|
|
46
|
+
* `permission:managed:<name>` (platform-owned, deleted only with an explicit second key).
|
|
47
|
+
*/
|
|
48
|
+
export class IamPermissionError extends IamError {
|
|
49
|
+
public static override typeName = `IamPermission${ResilientError.typeName}`
|
|
50
|
+
|
|
51
|
+
constructor(message = 'error') {
|
|
52
|
+
super(`permission:${message}`)
|
|
53
|
+
this.type = IamPermissionError.typeName
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
export class IamUnsupported extends IamError {
|
|
40
58
|
public static override typeName = `IamUnsupported${ResilientError.typeName}`
|
|
41
59
|
|
|
@@ -58,5 +76,6 @@ ResilientError.registerErrorClass(IamError)
|
|
|
58
76
|
ResilientError.registerErrorClass(IamClientError)
|
|
59
77
|
ResilientError.registerErrorClass(IamResourceError)
|
|
60
78
|
ResilientError.registerErrorClass(IamGrantError)
|
|
79
|
+
ResilientError.registerErrorClass(IamPermissionError)
|
|
61
80
|
ResilientError.registerErrorClass(IamUnsupported)
|
|
62
81
|
ResilientError.registerErrorClass(IamUserError)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RESOURCE_PARAM_SEPARATOR, RESOURCE_SOURCE_SEPARATOR, RESOURCE_PATH_SEPARATOR,
|
|
3
|
+
GateParamSource, GateParamErrorCode, DEFAULT_GATE_PARAM_SOURCES
|
|
4
|
+
} from '../consts.js'
|
|
5
|
+
import type { GateResourceSelector, GateParamProblem, ParsedGateParam } from '../types.js'
|
|
6
|
+
|
|
7
|
+
const SOURCES = Object.values(GateParamSource) as string[]
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read the `@…` half of a gate param.
|
|
11
|
+
*
|
|
12
|
+
* Two forms, and the difference between them is deliberate:
|
|
13
|
+
*
|
|
14
|
+
* `@enquiryId` — bare. A FLAT key, searched in `params` then `query`. It is never split on
|
|
15
|
+
* a dot, because a query key legally contains one and splitting it would
|
|
16
|
+
* change the meaning of selectors already deployed.
|
|
17
|
+
* `@body:order.id` — qualified. An explicit source and a nested path.
|
|
18
|
+
*
|
|
19
|
+
* The rule worth remembering: the bare form is lenient and flat, the qualified form is strict and
|
|
20
|
+
* nested.
|
|
21
|
+
*/
|
|
22
|
+
export const parseGateSelector = (selector: string): GateResourceSelector | GateParamProblem => {
|
|
23
|
+
if (selector === '') {
|
|
24
|
+
return { code: GateParamErrorCode.EmptySelector, detail: 'the selector after "@" is empty' }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const idx = selector.indexOf(RESOURCE_SOURCE_SEPARATOR)
|
|
28
|
+
if (idx < 0) {
|
|
29
|
+
return {
|
|
30
|
+
selector,
|
|
31
|
+
path: [selector],
|
|
32
|
+
sources: DEFAULT_GATE_PARAM_SOURCES
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const source = selector.slice(0, idx)
|
|
37
|
+
const rest = selector.slice(idx + 1)
|
|
38
|
+
|
|
39
|
+
if (!SOURCES.includes(source)) {
|
|
40
|
+
return {
|
|
41
|
+
code: GateParamErrorCode.UnknownSource,
|
|
42
|
+
detail: `"${source}" is not one of ${SOURCES.join(', ')}`
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (rest === '') {
|
|
47
|
+
return {
|
|
48
|
+
code: GateParamErrorCode.EmptySelector,
|
|
49
|
+
detail: `"${source}:" names no path`
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const path = rest.split(RESOURCE_PATH_SEPARATOR)
|
|
54
|
+
if (path.some(segment => segment === '')) {
|
|
55
|
+
return {
|
|
56
|
+
code: GateParamErrorCode.EmptySegment,
|
|
57
|
+
detail: `"${rest}" has an empty path segment`
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
selector,
|
|
63
|
+
source: source as GateParamSource,
|
|
64
|
+
path,
|
|
65
|
+
sources: [source as GateParamSource]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Split a gate param into the permission it checks and where the resource id comes from.
|
|
71
|
+
*
|
|
72
|
+
* Splits at the FIRST `@`. Everything before it is the permission name looked up in the subject's
|
|
73
|
+
* grants; everything after says where to read the resource id at request time. The suffix is the
|
|
74
|
+
* gate's syntax and nothing else's — a permission stored or granted under a name containing one is a
|
|
75
|
+
* key nothing ever looks up.
|
|
76
|
+
*/
|
|
77
|
+
export const parseGateParam = (param: string): ParsedGateParam => {
|
|
78
|
+
const idx = param.indexOf(RESOURCE_PARAM_SEPARATOR)
|
|
79
|
+
if (idx < 0) {
|
|
80
|
+
return { permission: param }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const permission = param.slice(0, idx)
|
|
84
|
+
const selector = param.slice(idx + 1)
|
|
85
|
+
const parsed = parseGateSelector(selector)
|
|
86
|
+
|
|
87
|
+
if ('code' in parsed) {
|
|
88
|
+
return { permission, resourceParam: selector, error: parsed }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
permission,
|
|
93
|
+
// Kept for readers written against the flat form. Only meaningful when the form IS flat.
|
|
94
|
+
...(parsed.source == null ? { resourceParam: selector } : {}),
|
|
95
|
+
resource: parsed
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Compose a gate param. The inverse of `parseGateParam` for every selector it accepts. */
|
|
100
|
+
export const formatGateParam = (
|
|
101
|
+
permission: string,
|
|
102
|
+
selector?: string | { source?: GateParamSource, path: string[] }
|
|
103
|
+
): string => {
|
|
104
|
+
if (selector == null) {
|
|
105
|
+
return permission
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof selector === 'string') {
|
|
109
|
+
return selector === '' ? permission : `${permission}${RESOURCE_PARAM_SEPARATOR}${selector}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (selector.path.length < 1) {
|
|
113
|
+
return permission
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const path = selector.path.join(RESOURCE_PATH_SEPARATOR)
|
|
117
|
+
const tail = selector.source == null
|
|
118
|
+
? path
|
|
119
|
+
: `${selector.source}${RESOURCE_SOURCE_SEPARATOR}${path}`
|
|
120
|
+
|
|
121
|
+
return `${permission}${RESOURCE_PARAM_SEPARATOR}${tail}`
|
|
122
|
+
}
|