@vantageos/cloud-identity 0.9.1 → 0.11.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/CHANGELOG.md +110 -0
- package/README.md +254 -1
- package/dist/identity-refusal.d.ts +32 -0
- package/dist/identity-refusal.d.ts.map +1 -0
- package/dist/identity-refusal.js +28 -0
- package/dist/identity-refusal.js.map +1 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/namespace-write.d.ts +20 -0
- package/dist/namespace-write.d.ts.map +1 -0
- package/dist/namespace-write.js +40 -0
- package/dist/namespace-write.js.map +1 -0
- package/dist/person-principal.d.ts +7 -0
- package/dist/person-principal.d.ts.map +1 -1
- package/dist/person-principal.js +7 -0
- package/dist/person-principal.js.map +1 -1
- package/dist/principal-by-id.d.ts +206 -0
- package/dist/principal-by-id.d.ts.map +1 -0
- package/dist/principal-by-id.js +334 -0
- package/dist/principal-by-id.js.map +1 -0
- package/dist/role-policy.d.ts +72 -0
- package/dist/role-policy.d.ts.map +1 -0
- package/dist/role-policy.js +101 -0
- package/dist/role-policy.js.map +1 -0
- package/dist/scope-filter.d.ts +26 -0
- package/dist/scope-filter.d.ts.map +1 -1
- package/dist/scope-filter.js +29 -3
- package/dist/scope-filter.js.map +1 -1
- package/dist/secret-compare-sync.d.ts +29 -0
- package/dist/secret-compare-sync.d.ts.map +1 -0
- package/dist/secret-compare-sync.js +64 -0
- package/dist/secret-compare-sync.js.map +1 -0
- package/dist/tenant-membership.d.ts +85 -0
- package/dist/tenant-membership.d.ts.map +1 -0
- package/dist/tenant-membership.js +113 -0
- package/dist/tenant-membership.js.map +1 -0
- package/package.json +25 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role policy as DATA: a minimum-role check over a caller-supplied order, and a
|
|
3
|
+
* role-claim mapping with no package-chosen default.
|
|
4
|
+
*
|
|
5
|
+
* The package never knows what roles exist. "admin > editor > viewer" is one
|
|
6
|
+
* consumer's vocabulary, passed in at the call site; another consumer passes
|
|
7
|
+
* another. Every absence (no role, unknown role, empty order, no mapping, no
|
|
8
|
+
* fallback) is a refusal, never a default grant.
|
|
9
|
+
*
|
|
10
|
+
* Pure functions: no framework SDK, no I/O.
|
|
11
|
+
*/
|
|
12
|
+
import { type IdentityRefusal } from "./identity-refusal.js";
|
|
13
|
+
export type MinRoleInput = {
|
|
14
|
+
/** The verified role of the caller. null/undefined/"" is "no role". */
|
|
15
|
+
role: string | null | undefined;
|
|
16
|
+
/** The least-privileged role that is admitted. Must appear in `order`. */
|
|
17
|
+
minimum: string;
|
|
18
|
+
/**
|
|
19
|
+
* The caller's ordered role list, MOST privileged FIRST
|
|
20
|
+
* (e.g. `["admin", "editor", "viewer"]`). No empty list, no duplicates.
|
|
21
|
+
*/
|
|
22
|
+
order: readonly string[];
|
|
23
|
+
/** The consumer's own name for the entry point. Defaults to the function name. */
|
|
24
|
+
door?: string;
|
|
25
|
+
};
|
|
26
|
+
export type MinRoleResult = {
|
|
27
|
+
ok: true;
|
|
28
|
+
role: string;
|
|
29
|
+
} | {
|
|
30
|
+
ok: false;
|
|
31
|
+
refusal: IdentityRefusal;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Admit `role` when it is at or above `minimum` in `order`.
|
|
35
|
+
*
|
|
36
|
+
* Refuses: an invalid order (empty, duplicated, non-string entries), a
|
|
37
|
+
* `minimum` outside the order, a missing or unknown role, a role below the
|
|
38
|
+
* minimum. Matching is exact (no case-folding), and a role is only ever found
|
|
39
|
+
* by array position, so `"constructor"` or `"__proto__"` are unknown roles.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveMinRole(input: MinRoleInput): MinRoleResult;
|
|
42
|
+
/** Throwing form of {@link resolveMinRole}; returns the role, throws `IdentityRefusalError`. */
|
|
43
|
+
export declare function assertMinRole(input: MinRoleInput): string;
|
|
44
|
+
export type MapRoleClaimInput = {
|
|
45
|
+
/** The raw claim from the verified credential. Only a non-empty string counts. */
|
|
46
|
+
claim: unknown;
|
|
47
|
+
/** Claim value -> consumer role. Data supplied by the caller. */
|
|
48
|
+
mapping: Readonly<Record<string, string>>;
|
|
49
|
+
/**
|
|
50
|
+
* The role to use when the claim is absent or not in `mapping`. The package
|
|
51
|
+
* never picks one: omit it and an absent or unmapped claim is REFUSED.
|
|
52
|
+
* Supply the least-privileged role you have.
|
|
53
|
+
*/
|
|
54
|
+
fallback?: string;
|
|
55
|
+
door?: string;
|
|
56
|
+
};
|
|
57
|
+
export type MapRoleClaimResult = {
|
|
58
|
+
ok: true;
|
|
59
|
+
role: string;
|
|
60
|
+
source: "mapped" | "fallback";
|
|
61
|
+
} | {
|
|
62
|
+
ok: false;
|
|
63
|
+
refusal: IdentityRefusal;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Map a verified role claim to the consumer's role, with a least-privilege
|
|
67
|
+
* fallback that the CALLER must name. A claim that is not a string (array,
|
|
68
|
+
* object, number) is absent, never coerced. Lookup uses own keys only, so a
|
|
69
|
+
* claim of `"constructor"` is unmapped.
|
|
70
|
+
*/
|
|
71
|
+
export declare function mapRoleClaim(input: MapRoleClaimInput): MapRoleClaimResult;
|
|
72
|
+
//# sourceMappingURL=role-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role-policy.d.ts","sourceRoot":"","sources":["../src/role-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAS/B,MAAM,MAAM,YAAY,GAAG;IACzB,uEAAuE;IACvE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChC,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,CAAC;AAE5C;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,aAAa,CA2DjE;AAED,gGAAgG;AAChG,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAIzD;AAMD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,kFAAkF;IAClF,KAAK,EAAE,OAAO,CAAC;IACf,iEAAiE;IACjE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAA;CAAE,GACzD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,kBAAkB,CAgDzE"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role policy as DATA: a minimum-role check over a caller-supplied order, and a
|
|
3
|
+
* role-claim mapping with no package-chosen default.
|
|
4
|
+
*
|
|
5
|
+
* The package never knows what roles exist. "admin > editor > viewer" is one
|
|
6
|
+
* consumer's vocabulary, passed in at the call site; another consumer passes
|
|
7
|
+
* another. Every absence (no role, unknown role, empty order, no mapping, no
|
|
8
|
+
* fallback) is a refusal, never a default grant.
|
|
9
|
+
*
|
|
10
|
+
* Pure functions: no framework SDK, no I/O.
|
|
11
|
+
*/
|
|
12
|
+
import { IdentityRefusalError, refusal, } from "./identity-refusal.js";
|
|
13
|
+
const DOOR_MIN = "resolveMinRole";
|
|
14
|
+
const DOOR_MAP = "mapRoleClaim";
|
|
15
|
+
/**
|
|
16
|
+
* Admit `role` when it is at or above `minimum` in `order`.
|
|
17
|
+
*
|
|
18
|
+
* Refuses: an invalid order (empty, duplicated, non-string entries), a
|
|
19
|
+
* `minimum` outside the order, a missing or unknown role, a role below the
|
|
20
|
+
* minimum. Matching is exact (no case-folding), and a role is only ever found
|
|
21
|
+
* by array position, so `"constructor"` or `"__proto__"` are unknown roles.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveMinRole(input) {
|
|
24
|
+
const door = input.door ?? DOOR_MIN;
|
|
25
|
+
const { order, role, minimum } = input;
|
|
26
|
+
const orderValid = Array.isArray(order) &&
|
|
27
|
+
order.length > 0 &&
|
|
28
|
+
order.every((r) => typeof r === "string" && r.length > 0) &&
|
|
29
|
+
new Set(order).size === order.length;
|
|
30
|
+
if (!orderValid) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
refusal: refusal("RBAC_DENIED", "invalid-role-order", door, "The role order must be a non-empty list of distinct role names."),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const minIndex = order.indexOf(minimum);
|
|
37
|
+
if (minIndex === -1) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
refusal: refusal("RBAC_DENIED", "unknown-minimum-role", door, "The required minimum role is not part of the role order."),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const roleIndex = typeof role === "string" && role.length > 0 ? order.indexOf(role) : -1;
|
|
44
|
+
if (roleIndex === -1) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
refusal: refusal("RBAC_DENIED", "unknown-role", door, "The caller's role is absent or not part of the role order."),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (roleIndex > minIndex) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
refusal: refusal("RBAC_DENIED", "role-below-minimum", door, "The caller's role is below the required minimum."),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, role: order[roleIndex] };
|
|
57
|
+
}
|
|
58
|
+
/** Throwing form of {@link resolveMinRole}; returns the role, throws `IdentityRefusalError`. */
|
|
59
|
+
export function assertMinRole(input) {
|
|
60
|
+
const result = resolveMinRole(input);
|
|
61
|
+
if (!result.ok)
|
|
62
|
+
throw new IdentityRefusalError(result.refusal);
|
|
63
|
+
return result.role;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Map a verified role claim to the consumer's role, with a least-privilege
|
|
67
|
+
* fallback that the CALLER must name. A claim that is not a string (array,
|
|
68
|
+
* object, number) is absent, never coerced. Lookup uses own keys only, so a
|
|
69
|
+
* claim of `"constructor"` is unmapped.
|
|
70
|
+
*/
|
|
71
|
+
export function mapRoleClaim(input) {
|
|
72
|
+
const door = input.door ?? DOOR_MAP;
|
|
73
|
+
const { claim, mapping, fallback } = input;
|
|
74
|
+
if (mapping === null || typeof mapping !== "object" || Array.isArray(mapping)) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
refusal: refusal("RBAC_DENIED", "invalid-role-mapping", door, "The role mapping must be a plain object of claim to role."),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const present = typeof claim === "string" && claim.length > 0;
|
|
81
|
+
if (present && Object.hasOwn(mapping, claim)) {
|
|
82
|
+
const mapped = mapping[claim];
|
|
83
|
+
if (typeof mapped === "string" && mapped.length > 0) {
|
|
84
|
+
return { ok: true, role: mapped, source: "mapped" };
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
refusal: refusal("RBAC_DENIED", "invalid-role-mapping", door, "The role mapping holds a non-string or empty role."),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (typeof fallback === "string" && fallback.length > 0) {
|
|
92
|
+
return { ok: true, role: fallback, source: "fallback" };
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
refusal: refusal("RBAC_DENIED", present ? "role-claim-unmapped" : "role-claim-absent", door, present
|
|
97
|
+
? "The role claim is not in the mapping and no fallback was supplied."
|
|
98
|
+
: "No role claim was presented and no fallback was supplied."),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=role-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role-policy.js","sourceRoot":"","sources":["../src/role-policy.ts"],"names":[],"mappings":"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"}
|
package/dist/scope-filter.d.ts
CHANGED
|
@@ -61,6 +61,13 @@ import type { OAuthCtx } from "./types.js";
|
|
|
61
61
|
* and pass it BY NAME. It is never inferred from an omitted argument.
|
|
62
62
|
*/
|
|
63
63
|
export declare const LEGACY_WILDCARD_CTX: OAuthCtx;
|
|
64
|
+
/**
|
|
65
|
+
* THE path-boundary rule for namespaces: `namespace` is inside `prefix` when it
|
|
66
|
+
* equals it, or continues it after a '/'. `team/finance-archive` is NOT inside
|
|
67
|
+
* `team/finance`. One implementation, used by the read filter and by
|
|
68
|
+
* `assertNamespaceWrite`, so the two can never drift apart.
|
|
69
|
+
*/
|
|
70
|
+
export declare function namespaceMatchesPrefix(namespace: string, prefix: string): boolean;
|
|
64
71
|
/**
|
|
65
72
|
* Row shape accepted by the scope filter. All fields optional because real
|
|
66
73
|
* Convex documents from list_peers / list_messages / etc. don't all carry both.
|
|
@@ -112,6 +119,13 @@ export type GrantFieldDeclaration = readonly string[];
|
|
|
112
119
|
* Substring matches that don't fall on a '/' boundary are explicitly rejected
|
|
113
120
|
* (e.g. namespace="orchestrator/alphabet" does NOT match prefix
|
|
114
121
|
* "orchestrator/alpha"). This avoids the classic prefix-isolation bypass.
|
|
122
|
+
*
|
|
123
|
+
* @deprecated Since 0.11.0. A `createdBy` name matched against
|
|
124
|
+
* `fromAllowList` selects the row, which backend standard R-53 clause 1
|
|
125
|
+
* forbids: a name never selects or authorises a row. Use
|
|
126
|
+
* `resolveActingPrincipal` and `assertTargetBelongsTo`, which compare stored
|
|
127
|
+
* IDs. Behaviour unchanged; it stays exported until consumers migrate off it,
|
|
128
|
+
* and is removed in a major release.
|
|
115
129
|
*/
|
|
116
130
|
export declare function passesScopeFilter<T extends ScopeFilterable>(oauthCtx: OAuthCtx, row: T, grantFields?: GrantFieldDeclaration): boolean;
|
|
117
131
|
/**
|
|
@@ -119,6 +133,12 @@ export declare function passesScopeFilter<T extends ScopeFilterable>(oauthCtx: O
|
|
|
119
133
|
*
|
|
120
134
|
* `grantFields` — see `passesScopeFilter`. Defaults to `[]` (0.4.0-identical
|
|
121
135
|
* behaviour).
|
|
136
|
+
*
|
|
137
|
+
* @deprecated Since 0.11.0. It selects rows through `passesScopeFilter`,
|
|
138
|
+
* where a `createdBy` name selects the row, which backend standard R-53
|
|
139
|
+
* clause 1 forbids. Use `resolveActingPrincipal` and `assertTargetBelongsTo`,
|
|
140
|
+
* which compare stored IDs. Behaviour unchanged; it stays exported until
|
|
141
|
+
* consumers migrate off it, and is removed in a major release.
|
|
122
142
|
*/
|
|
123
143
|
export declare function scopeFilterList<T extends ScopeFilterable>(oauthCtx: OAuthCtx, rows: T[], grantFields?: GrantFieldDeclaration): T[];
|
|
124
144
|
/**
|
|
@@ -128,6 +148,12 @@ export declare function scopeFilterList<T extends ScopeFilterable>(oauthCtx: OAu
|
|
|
128
148
|
*
|
|
129
149
|
* `grantFields` — see `passesScopeFilter`. Defaults to `[]` (0.4.0-identical
|
|
130
150
|
* behaviour).
|
|
151
|
+
*
|
|
152
|
+
* @deprecated Since 0.11.0. It admits the row through `passesScopeFilter`,
|
|
153
|
+
* where a `createdBy` name selects the row, which backend standard R-53
|
|
154
|
+
* clause 1 forbids. Use `resolveActingPrincipal` and `assertTargetBelongsTo`,
|
|
155
|
+
* which compare stored IDs. Behaviour unchanged; it stays exported until
|
|
156
|
+
* consumers migrate off it, and is removed in a major release.
|
|
131
157
|
*/
|
|
132
158
|
export declare function scopeFilterGet<T extends ScopeFilterable>(oauthCtx: OAuthCtx, row: T | null | undefined, grantFields?: GrantFieldDeclaration): T | null;
|
|
133
159
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-filter.d.ts","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAKjC,CAAC;AAWF;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAIpD;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,qBAAqB,GAAG,SAAS,MAAM,EAAE,CAAC;AActD
|
|
1
|
+
{"version":3,"file":"scope-filter.d.ts","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAKjC,CAAC;AAWF;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAEjF;AAED;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAIpD;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,qBAAqB,GAAG,SAAS,MAAM,EAAE,CAAC;AActD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,eAAe,EACzD,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,CAAC,EACN,WAAW,GAAE,qBAAuC,GACnD,OAAO,CAeT;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,eAAe,EACvD,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,CAAC,EAAE,EACT,WAAW,GAAE,qBAAuC,GACnD,CAAC,EAAE,CAGL;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,eAAe,EACtD,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,EACzB,WAAW,GAAE,qBAAuC,GACnD,CAAC,GAAG,IAAI,CAIV;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAGtD"}
|
package/dist/scope-filter.js
CHANGED
|
@@ -71,6 +71,15 @@ function assertOauthCtx(oauthCtx) {
|
|
|
71
71
|
"if you deliberately want the pre-0.3.0 legacy-bearer wildcard behaviour.");
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* THE path-boundary rule for namespaces: `namespace` is inside `prefix` when it
|
|
76
|
+
* equals it, or continues it after a '/'. `team/finance-archive` is NOT inside
|
|
77
|
+
* `team/finance`. One implementation, used by the read filter and by
|
|
78
|
+
* `assertNamespaceWrite`, so the two can never drift apart.
|
|
79
|
+
*/
|
|
80
|
+
export function namespaceMatchesPrefix(namespace, prefix) {
|
|
81
|
+
return namespace === prefix || namespace.startsWith(`${prefix}/`);
|
|
82
|
+
}
|
|
74
83
|
/**
|
|
75
84
|
* Returns true when the scope profile grants full, wildcard access.
|
|
76
85
|
* Mirrors `isMasterScope` from the source module: `scope === "master"` OR
|
|
@@ -116,6 +125,13 @@ function grantFieldMatches(value, fromAllowList) {
|
|
|
116
125
|
* Substring matches that don't fall on a '/' boundary are explicitly rejected
|
|
117
126
|
* (e.g. namespace="orchestrator/alphabet" does NOT match prefix
|
|
118
127
|
* "orchestrator/alpha"). This avoids the classic prefix-isolation bypass.
|
|
128
|
+
*
|
|
129
|
+
* @deprecated Since 0.11.0. A `createdBy` name matched against
|
|
130
|
+
* `fromAllowList` selects the row, which backend standard R-53 clause 1
|
|
131
|
+
* forbids: a name never selects or authorises a row. Use
|
|
132
|
+
* `resolveActingPrincipal` and `assertTargetBelongsTo`, which compare stored
|
|
133
|
+
* IDs. Behaviour unchanged; it stays exported until consumers migrate off it,
|
|
134
|
+
* and is removed in a major release.
|
|
119
135
|
*/
|
|
120
136
|
export function passesScopeFilter(oauthCtx, row, grantFields = NO_GRANT_FIELDS) {
|
|
121
137
|
assertOauthCtx(oauthCtx);
|
|
@@ -126,9 +142,7 @@ export function passesScopeFilter(oauthCtx, row, grantFields = NO_GRANT_FIELDS)
|
|
|
126
142
|
return true;
|
|
127
143
|
if (namespace) {
|
|
128
144
|
for (const p of oauthCtx.namespaceReadPrefixes) {
|
|
129
|
-
if (namespace
|
|
130
|
-
return true;
|
|
131
|
-
if (namespace.startsWith(`${p}/`))
|
|
145
|
+
if (namespaceMatchesPrefix(namespace, p))
|
|
132
146
|
return true;
|
|
133
147
|
}
|
|
134
148
|
}
|
|
@@ -144,6 +158,12 @@ export function passesScopeFilter(oauthCtx, row, grantFields = NO_GRANT_FIELDS)
|
|
|
144
158
|
*
|
|
145
159
|
* `grantFields` — see `passesScopeFilter`. Defaults to `[]` (0.4.0-identical
|
|
146
160
|
* behaviour).
|
|
161
|
+
*
|
|
162
|
+
* @deprecated Since 0.11.0. It selects rows through `passesScopeFilter`,
|
|
163
|
+
* where a `createdBy` name selects the row, which backend standard R-53
|
|
164
|
+
* clause 1 forbids. Use `resolveActingPrincipal` and `assertTargetBelongsTo`,
|
|
165
|
+
* which compare stored IDs. Behaviour unchanged; it stays exported until
|
|
166
|
+
* consumers migrate off it, and is removed in a major release.
|
|
147
167
|
*/
|
|
148
168
|
export function scopeFilterList(oauthCtx, rows, grantFields = NO_GRANT_FIELDS) {
|
|
149
169
|
assertOauthCtx(oauthCtx);
|
|
@@ -156,6 +176,12 @@ export function scopeFilterList(oauthCtx, rows, grantFields = NO_GRANT_FIELDS) {
|
|
|
156
176
|
*
|
|
157
177
|
* `grantFields` — see `passesScopeFilter`. Defaults to `[]` (0.4.0-identical
|
|
158
178
|
* behaviour).
|
|
179
|
+
*
|
|
180
|
+
* @deprecated Since 0.11.0. It admits the row through `passesScopeFilter`,
|
|
181
|
+
* where a `createdBy` name selects the row, which backend standard R-53
|
|
182
|
+
* clause 1 forbids. Use `resolveActingPrincipal` and `assertTargetBelongsTo`,
|
|
183
|
+
* which compare stored IDs. Behaviour unchanged; it stays exported until
|
|
184
|
+
* consumers migrate off it, and is removed in a major release.
|
|
159
185
|
*/
|
|
160
186
|
export function scopeFilterGet(oauthCtx, row, grantFields = NO_GRANT_FIELDS) {
|
|
161
187
|
assertOauthCtx(oauthCtx);
|
package/dist/scope-filter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-filter.js","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAIH;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAa;IAC3C,KAAK,EAAE,QAAQ;IACf,aAAa,EAAE,CAAC,GAAG,CAAC;IACpB,qBAAqB,EAAE,EAAE;IACzB,sBAAsB,EAAE,EAAE;CAC3B,CAAC;AAEF,SAAS,cAAc,CAAC,QAAkB;IACxC,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,IAAI,SAAS,CACjB,2EAA2E;YACzE,0EAA0E,CAC7E,CAAC;IACJ,CAAC;AACH,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,GAAa;IACzC,cAAc,CAAC,GAAG,CAAC,CAAC;IACpB,IAAI,GAAG,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO,GAAG,CAAC,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AACzC,CAAC;AAeD,MAAM,eAAe,GAA0B,EAAE,CAAC;AAElD,SAAS,iBAAiB,CAAC,KAAc,EAAE,aAAuB;IAChE,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,IAAI,CACf,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,CACtE,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"scope-filter.js","sourceRoot":"","sources":["../src/scope-filter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous constant-time secret comparison.
|
|
3
|
+
*
|
|
4
|
+
* `timingSafeEqual` in `./crypto` is async (it uses Web Crypto for its dummy
|
|
5
|
+
* work) and takes bytes. A synchronous call site — a plain admin-secret gate,
|
|
6
|
+
* a framework hook that cannot await — needs a synchronous equivalent. This is
|
|
7
|
+
* pure JavaScript with no runtime dependency, so it also runs on any edge
|
|
8
|
+
* runtime. The async comparator is unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Constant time here means: the number of loop iterations depends only on the
|
|
11
|
+
* LONGER input's length, never on where the first difference is. JavaScript
|
|
12
|
+
* gives no hard timing guarantee; this removes the content-dependent early
|
|
13
|
+
* exit, which is the leak a naive `===` has.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Compare two strings (UTF-8) or byte arrays in constant time with respect to
|
|
17
|
+
* their content. Always walks the longer length; a length difference is folded
|
|
18
|
+
* into the result rather than returned early. Any other input type is `false`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function timingSafeEqualSync(a: string | Uint8Array, b: string | Uint8Array): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Assert a presented secret equals the configured one. Throws
|
|
23
|
+
* `IdentityRefusalError` (`CREDENTIAL_REFUSED`) on mismatch. An unset expected
|
|
24
|
+
* secret (undefined, null, empty) matches NOTHING, not even an empty
|
|
25
|
+
* presentation: a missing configuration must never open a door. The refusal
|
|
26
|
+
* carries neither value.
|
|
27
|
+
*/
|
|
28
|
+
export declare function assertSecretSync(presented: string | Uint8Array | null | undefined, expected: string | Uint8Array | null | undefined, door?: string): void;
|
|
29
|
+
//# sourceMappingURL=secret-compare-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-compare-sync.d.ts","sourceRoot":"","sources":["../src/secret-compare-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAYH;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,MAAM,GAAG,UAAU,EACtB,CAAC,EAAE,MAAM,GAAG,UAAU,GACrB,OAAO,CAcT;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,GAAG,SAAS,EACjD,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,GAAG,SAAS,EAChD,IAAI,SAAqB,GACxB,IAAI,CAyBN"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous constant-time secret comparison.
|
|
3
|
+
*
|
|
4
|
+
* `timingSafeEqual` in `./crypto` is async (it uses Web Crypto for its dummy
|
|
5
|
+
* work) and takes bytes. A synchronous call site — a plain admin-secret gate,
|
|
6
|
+
* a framework hook that cannot await — needs a synchronous equivalent. This is
|
|
7
|
+
* pure JavaScript with no runtime dependency, so it also runs on any edge
|
|
8
|
+
* runtime. The async comparator is unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Constant time here means: the number of loop iterations depends only on the
|
|
11
|
+
* LONGER input's length, never on where the first difference is. JavaScript
|
|
12
|
+
* gives no hard timing guarantee; this removes the content-dependent early
|
|
13
|
+
* exit, which is the leak a naive `===` has.
|
|
14
|
+
*/
|
|
15
|
+
import { IdentityRefusalError, refusal } from "./identity-refusal.js";
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
function toBytes(value) {
|
|
18
|
+
if (typeof value === "string")
|
|
19
|
+
return encoder.encode(value);
|
|
20
|
+
if (value instanceof Uint8Array)
|
|
21
|
+
return value;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Compare two strings (UTF-8) or byte arrays in constant time with respect to
|
|
26
|
+
* their content. Always walks the longer length; a length difference is folded
|
|
27
|
+
* into the result rather than returned early. Any other input type is `false`.
|
|
28
|
+
*/
|
|
29
|
+
export function timingSafeEqualSync(a, b) {
|
|
30
|
+
const x = toBytes(a);
|
|
31
|
+
const y = toBytes(b);
|
|
32
|
+
if (x === null || y === null)
|
|
33
|
+
return false;
|
|
34
|
+
const xl = x.length;
|
|
35
|
+
const yl = y.length;
|
|
36
|
+
const max = xl > yl ? xl : yl;
|
|
37
|
+
let diff = xl ^ yl;
|
|
38
|
+
for (let i = 0; i < max; i++) {
|
|
39
|
+
const xi = i < xl ? x[i] : 0;
|
|
40
|
+
const yi = i < yl ? y[i] : 0;
|
|
41
|
+
diff |= xi ^ yi;
|
|
42
|
+
}
|
|
43
|
+
return diff === 0;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Assert a presented secret equals the configured one. Throws
|
|
47
|
+
* `IdentityRefusalError` (`CREDENTIAL_REFUSED`) on mismatch. An unset expected
|
|
48
|
+
* secret (undefined, null, empty) matches NOTHING, not even an empty
|
|
49
|
+
* presentation: a missing configuration must never open a door. The refusal
|
|
50
|
+
* carries neither value.
|
|
51
|
+
*/
|
|
52
|
+
export function assertSecretSync(presented, expected, door = "assertSecretSync") {
|
|
53
|
+
const expectedBytes = toBytes(expected);
|
|
54
|
+
if (expectedBytes === null || expectedBytes.length === 0) {
|
|
55
|
+
throw new IdentityRefusalError(refusal("CREDENTIAL_REFUSED", "secret-not-configured", door, "No secret is configured for this entry point, so nothing can match."));
|
|
56
|
+
}
|
|
57
|
+
const presentedBytes = toBytes(presented) ?? new Uint8Array(0);
|
|
58
|
+
// Compare even when presented is empty so the work does not depend on it.
|
|
59
|
+
const equal = timingSafeEqualSync(presentedBytes, expectedBytes);
|
|
60
|
+
if (!equal || presentedBytes.length === 0) {
|
|
61
|
+
throw new IdentityRefusalError(refusal("CREDENTIAL_REFUSED", "secret-mismatch", door, "The presented secret does not match."));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=secret-compare-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-compare-sync.js","sourceRoot":"","sources":["../src/secret-compare-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAEtE,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;AAElC,SAAS,OAAO,CAAC,KAAc;IAC7B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5D,IAAI,KAAK,YAAY,UAAU;QAAE,OAAO,KAAK,CAAC;IAC9C,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,CAAsB,EACtB,CAAsB;IAEtB,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACrB,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACrB,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC3C,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC;IACpB,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC;IACpB,MAAM,GAAG,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9B,IAAI,IAAI,GAAG,EAAE,GAAG,EAAE,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAY,CAAC,CAAC,CAAC,CAAC,CAAC;QACzC,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAY,CAAC,CAAC,CAAC,CAAC,CAAC;QACzC,IAAI,IAAI,EAAE,GAAG,EAAE,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,CAAC;AACpB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,SAAiD,EACjD,QAAgD,EAChD,IAAI,GAAG,kBAAkB;IAEzB,MAAM,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxC,IAAI,aAAa,KAAK,IAAI,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,oBAAoB,CAC5B,OAAO,CACL,oBAAoB,EACpB,uBAAuB,EACvB,IAAI,EACJ,qEAAqE,CACtE,CACF,CAAC;IACJ,CAAC;IACD,MAAM,cAAc,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;IAC/D,0EAA0E;IAC1E,MAAM,KAAK,GAAG,mBAAmB,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC;IACjE,IAAI,CAAC,KAAK,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,oBAAoB,CAC5B,OAAO,CACL,oBAAoB,EACpB,iBAAiB,EACjB,IAAI,EACJ,sCAAsC,CACvC,CACF,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Membership resolution, row-in-tenant visibility and the org-admin proof.
|
|
3
|
+
*
|
|
4
|
+
* The package cannot query a datastore, so the membership read is an INJECTED
|
|
5
|
+
* async lookup. Everything that can be got wrong stays here: a throwing lookup,
|
|
6
|
+
* a miss, an inactive row, a row of the wrong shape and a row that names a
|
|
7
|
+
* different organisation are all refusals. Nothing in this module returns a
|
|
8
|
+
* grant on an error path.
|
|
9
|
+
*
|
|
10
|
+
* Identifiers are OPAQUE and compared byte-for-byte: no trimming, no case
|
|
11
|
+
* folding, and never a slug compared against an id (supply the same kind of
|
|
12
|
+
* identifier on both sides).
|
|
13
|
+
*/
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { type IdentityRefusal } from "./identity-refusal.js";
|
|
16
|
+
/**
|
|
17
|
+
* True only when the row is stamped with an organisation, the caller has one,
|
|
18
|
+
* and the two are strictly equal. An unstamped row is NEVER visible, including
|
|
19
|
+
* to a caller that also has no organisation (absence equals absence is not a
|
|
20
|
+
* match).
|
|
21
|
+
*/
|
|
22
|
+
export declare function isRowInTenant(input: {
|
|
23
|
+
rowOrgId: string | null | undefined;
|
|
24
|
+
callerOrgId: string | null | undefined;
|
|
25
|
+
}): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* What the consumer's lookup returns for (subject, organisation).
|
|
28
|
+
* `orgId`, when the row carries it, is re-checked here so a loose lookup (a
|
|
29
|
+
* prefix match, a wrong index) cannot become a membership in another org.
|
|
30
|
+
*/
|
|
31
|
+
export declare const membershipRecordSchema: z.ZodObject<{
|
|
32
|
+
active: z.ZodBoolean;
|
|
33
|
+
role: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
34
|
+
orgId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
35
|
+
}, z.core.$strip>;
|
|
36
|
+
export type MembershipRecord = z.infer<typeof membershipRecordSchema>;
|
|
37
|
+
export type ResolvedMembership = {
|
|
38
|
+
subject: string;
|
|
39
|
+
orgId: string;
|
|
40
|
+
/** The role the record carries, or null when it carries none. */
|
|
41
|
+
role: string | null;
|
|
42
|
+
};
|
|
43
|
+
export type ResolveMembershipInput = {
|
|
44
|
+
/** The verified subject. Never a client-supplied argument. */
|
|
45
|
+
subject: string;
|
|
46
|
+
/** The verified organisation id. */
|
|
47
|
+
orgId: string;
|
|
48
|
+
/** Indexed read of one membership. Return null/undefined on a miss. */
|
|
49
|
+
lookup: (subject: string, orgId: string) => Promise<MembershipRecord | null | undefined> | MembershipRecord | null | undefined;
|
|
50
|
+
door?: string;
|
|
51
|
+
};
|
|
52
|
+
export type ResolveMembershipResult = {
|
|
53
|
+
ok: true;
|
|
54
|
+
membership: ResolvedMembership;
|
|
55
|
+
} | {
|
|
56
|
+
ok: false;
|
|
57
|
+
refusal: IdentityRefusal;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the membership of a verified subject in a verified organisation via
|
|
61
|
+
* the injected `lookup`. Only `active === true` on a well-shaped record that
|
|
62
|
+
* names no other organisation is served. The lookup's own error text is never
|
|
63
|
+
* surfaced.
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveMembership(input: ResolveMembershipInput): Promise<ResolveMembershipResult>;
|
|
66
|
+
export type RequireOrgAdminInput = {
|
|
67
|
+
/** The organisation the credential was VERIFIED for (a verified claim). */
|
|
68
|
+
verifiedOrgId: string | null | undefined;
|
|
69
|
+
/** The organisation the action is aimed at. Must equal `verifiedOrgId`. */
|
|
70
|
+
targetOrgId: string | null | undefined;
|
|
71
|
+
/** The caller's verified role. */
|
|
72
|
+
role: string | null | undefined;
|
|
73
|
+
/** Roles that count as organisation admin: data, supplied by the consumer. */
|
|
74
|
+
adminRoles: readonly string[];
|
|
75
|
+
door?: string;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* An org-admin proof is bound to the caller's own verified organisation: the
|
|
79
|
+
* target must equal it, and the role must be one of `adminRoles`. Being an
|
|
80
|
+
* admin somewhere is not being an admin of the target. Throws
|
|
81
|
+
* `IdentityRefusalError`; returns nothing when served. An empty `adminRoles`
|
|
82
|
+
* admits nobody.
|
|
83
|
+
*/
|
|
84
|
+
export declare function requireOrgAdmin(input: RequireOrgAdminInput): void;
|
|
85
|
+
//# sourceMappingURL=tenant-membership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-membership.d.ts","sourceRoot":"","sources":["../src/tenant-membership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAU/B;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACpC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACxC,GAAG,OAAO,CAGV;AAMD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB;;;;iBAIjC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,MAAM,EAAE,CACN,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,KACV,OAAO,CAAC,gBAAgB,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,gBAAgB,GAAG,IAAI,GAAG,SAAS,CAAC;IACxF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAC/B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,kBAAkB,CAAA;CAAE,GAC5C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,CAAC;AAU5C;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,uBAAuB,CAAC,CA4ClC;AAMD,MAAM,MAAM,oBAAoB,GAAG;IACjC,2EAA2E;IAC3E,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACzC,2EAA2E;IAC3E,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACvC,kCAAkC;IAClC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChC,8EAA8E;IAC9E,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAmBjE"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Membership resolution, row-in-tenant visibility and the org-admin proof.
|
|
3
|
+
*
|
|
4
|
+
* The package cannot query a datastore, so the membership read is an INJECTED
|
|
5
|
+
* async lookup. Everything that can be got wrong stays here: a throwing lookup,
|
|
6
|
+
* a miss, an inactive row, a row of the wrong shape and a row that names a
|
|
7
|
+
* different organisation are all refusals. Nothing in this module returns a
|
|
8
|
+
* grant on an error path.
|
|
9
|
+
*
|
|
10
|
+
* Identifiers are OPAQUE and compared byte-for-byte: no trimming, no case
|
|
11
|
+
* folding, and never a slug compared against an id (supply the same kind of
|
|
12
|
+
* identifier on both sides).
|
|
13
|
+
*/
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { IdentityRefusalError, refusal, } from "./identity-refusal.js";
|
|
16
|
+
function nonEmpty(value) {
|
|
17
|
+
return typeof value === "string" && value.length > 0;
|
|
18
|
+
}
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Row in tenant
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/**
|
|
23
|
+
* True only when the row is stamped with an organisation, the caller has one,
|
|
24
|
+
* and the two are strictly equal. An unstamped row is NEVER visible, including
|
|
25
|
+
* to a caller that also has no organisation (absence equals absence is not a
|
|
26
|
+
* match).
|
|
27
|
+
*/
|
|
28
|
+
export function isRowInTenant(input) {
|
|
29
|
+
const { rowOrgId, callerOrgId } = input;
|
|
30
|
+
return nonEmpty(rowOrgId) && nonEmpty(callerOrgId) && rowOrgId === callerOrgId;
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Membership
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
/**
|
|
36
|
+
* What the consumer's lookup returns for (subject, organisation).
|
|
37
|
+
* `orgId`, when the row carries it, is re-checked here so a loose lookup (a
|
|
38
|
+
* prefix match, a wrong index) cannot become a membership in another org.
|
|
39
|
+
*/
|
|
40
|
+
export const membershipRecordSchema = z.object({
|
|
41
|
+
active: z.boolean(),
|
|
42
|
+
role: z.string().nullish(),
|
|
43
|
+
orgId: z.string().nullish(),
|
|
44
|
+
});
|
|
45
|
+
function deny(reason, door, detail) {
|
|
46
|
+
return { ok: false, refusal: refusal("RBAC_DENIED", reason, door, detail) };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the membership of a verified subject in a verified organisation via
|
|
50
|
+
* the injected `lookup`. Only `active === true` on a well-shaped record that
|
|
51
|
+
* names no other organisation is served. The lookup's own error text is never
|
|
52
|
+
* surfaced.
|
|
53
|
+
*/
|
|
54
|
+
export async function resolveMembership(input) {
|
|
55
|
+
const door = input.door ?? "resolveMembership";
|
|
56
|
+
if (typeof input?.lookup !== "function") {
|
|
57
|
+
// A programming error in the consumer, not a claim about any caller.
|
|
58
|
+
throw new TypeError("resolveMembership: lookup must be a function.");
|
|
59
|
+
}
|
|
60
|
+
const { subject, orgId, lookup } = input;
|
|
61
|
+
if (!nonEmpty(subject) || !nonEmpty(orgId)) {
|
|
62
|
+
return deny("membership-invalid-input", door, "A verified subject and a verified organisation are both required.");
|
|
63
|
+
}
|
|
64
|
+
let record;
|
|
65
|
+
try {
|
|
66
|
+
record = await lookup(subject, orgId);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return deny("membership-lookup-failed", door, "The membership could not be read, so none is granted.");
|
|
70
|
+
}
|
|
71
|
+
if (record === null || record === undefined) {
|
|
72
|
+
return deny("membership-not-found", door, "No membership for this subject in this organisation.");
|
|
73
|
+
}
|
|
74
|
+
const parsed = membershipRecordSchema.safeParse(record);
|
|
75
|
+
if (!parsed.success) {
|
|
76
|
+
return deny("membership-record-invalid", door, "The membership record is not well formed.");
|
|
77
|
+
}
|
|
78
|
+
const row = parsed.data;
|
|
79
|
+
if (nonEmpty(row.orgId) && row.orgId !== orgId) {
|
|
80
|
+
return deny("other-organisation", door, "The membership belongs to another organisation.");
|
|
81
|
+
}
|
|
82
|
+
if (row.active !== true) {
|
|
83
|
+
return deny("membership-inactive", door, "The membership is not active.");
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
membership: { subject, orgId, role: nonEmpty(row.role) ? row.role : null },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* An org-admin proof is bound to the caller's own verified organisation: the
|
|
92
|
+
* target must equal it, and the role must be one of `adminRoles`. Being an
|
|
93
|
+
* admin somewhere is not being an admin of the target. Throws
|
|
94
|
+
* `IdentityRefusalError`; returns nothing when served. An empty `adminRoles`
|
|
95
|
+
* admits nobody.
|
|
96
|
+
*/
|
|
97
|
+
export function requireOrgAdmin(input) {
|
|
98
|
+
const door = input.door ?? "requireOrgAdmin";
|
|
99
|
+
const { verifiedOrgId, targetOrgId, role, adminRoles } = input;
|
|
100
|
+
const fail = (reason, detail) => {
|
|
101
|
+
throw new IdentityRefusalError(refusal("RBAC_DENIED", reason, door, detail));
|
|
102
|
+
};
|
|
103
|
+
if (!nonEmpty(verifiedOrgId)) {
|
|
104
|
+
fail("no-verified-organisation", "The caller has no verified organisation.");
|
|
105
|
+
}
|
|
106
|
+
if (!nonEmpty(targetOrgId) || targetOrgId !== verifiedOrgId) {
|
|
107
|
+
fail("other-organisation", "The target organisation is not the caller's verified organisation.");
|
|
108
|
+
}
|
|
109
|
+
if (!nonEmpty(role) || !Array.isArray(adminRoles) || !adminRoles.includes(role)) {
|
|
110
|
+
fail("role-not-admin", "The caller's role is not an organisation-admin role.");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=tenant-membership.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-membership.js","sourceRoot":"","sources":["../src/tenant-membership.ts"],"names":[],"mappings":"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"}
|