@apart-tech/intelligence-core 1.11.3 → 1.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/ability.d.ts +148 -0
- package/dist/auth/ability.d.ts.map +1 -0
- package/dist/auth/ability.js +285 -0
- package/dist/auth/ability.js.map +1 -0
- package/dist/auth/ability.test.d.ts +2 -0
- package/dist/auth/ability.test.d.ts.map +1 -0
- package/dist/auth/ability.test.js +680 -0
- package/dist/auth/ability.test.js.map +1 -0
- package/dist/auth/delegation-jwt.d.ts +167 -0
- package/dist/auth/delegation-jwt.d.ts.map +1 -0
- package/dist/auth/delegation-jwt.js +237 -0
- package/dist/auth/delegation-jwt.js.map +1 -0
- package/dist/auth/delegation-jwt.test.d.ts +2 -0
- package/dist/auth/delegation-jwt.test.d.ts.map +1 -0
- package/dist/auth/delegation-jwt.test.js +283 -0
- package/dist/auth/delegation-jwt.test.js.map +1 -0
- package/dist/auth/principal.d.ts +94 -0
- package/dist/auth/principal.d.ts.map +1 -0
- package/dist/auth/principal.js +33 -0
- package/dist/auth/principal.js.map +1 -0
- package/dist/config/config.test.d.ts +2 -0
- package/dist/config/config.test.d.ts.map +1 -0
- package/dist/config/config.test.js +57 -0
- package/dist/config/config.test.js.map +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +17 -0
- package/dist/config/index.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/__tests__/jwt.test.d.ts +2 -0
- package/dist/lib/__tests__/jwt.test.d.ts.map +1 -0
- package/dist/lib/__tests__/jwt.test.js +97 -0
- package/dist/lib/__tests__/jwt.test.js.map +1 -0
- package/dist/lib/jwt.d.ts +20 -0
- package/dist/lib/jwt.d.ts.map +1 -1
- package/dist/lib/jwt.js +56 -3
- package/dist/lib/jwt.js.map +1 -1
- package/dist/services/__tests__/delegation-cleanup-service.test.d.ts +2 -0
- package/dist/services/__tests__/delegation-cleanup-service.test.d.ts.map +1 -0
- package/dist/services/__tests__/delegation-cleanup-service.test.js +211 -0
- package/dist/services/__tests__/delegation-cleanup-service.test.js.map +1 -0
- package/dist/services/agent-run-service.d.ts +44 -7
- package/dist/services/agent-run-service.d.ts.map +1 -1
- package/dist/services/agent-run-service.js +14 -0
- package/dist/services/agent-run-service.js.map +1 -1
- package/dist/services/agent-schedule-service.d.ts +21 -0
- package/dist/services/agent-schedule-service.d.ts.map +1 -1
- package/dist/services/agent-schedule-service.js +12 -0
- package/dist/services/agent-schedule-service.js.map +1 -1
- package/dist/services/audit-event-service.d.ts +76 -0
- package/dist/services/audit-event-service.d.ts.map +1 -0
- package/dist/services/audit-event-service.js +48 -0
- package/dist/services/audit-event-service.js.map +1 -0
- package/dist/services/cleaning-service.d.ts.map +1 -1
- package/dist/services/cleaning-service.js +5 -1
- package/dist/services/cleaning-service.js.map +1 -1
- package/dist/services/delegation-cleanup-service.d.ts +133 -0
- package/dist/services/delegation-cleanup-service.d.ts.map +1 -0
- package/dist/services/delegation-cleanup-service.js +111 -0
- package/dist/services/delegation-cleanup-service.js.map +1 -0
- package/dist/services/edge-service.d.ts.map +1 -1
- package/dist/services/edge-service.js +3 -0
- package/dist/services/edge-service.js.map +1 -1
- package/dist/services/org-agent-type-service.d.ts +15 -0
- package/dist/services/org-agent-type-service.d.ts.map +1 -1
- package/dist/services/org-agent-type-service.js +2 -0
- package/dist/services/org-agent-type-service.js.map +1 -1
- package/dist/services/usage-service.d.ts +48 -0
- package/dist/services/usage-service.d.ts.map +1 -0
- package/dist/services/usage-service.js +116 -0
- package/dist/services/usage-service.js.map +1 -0
- package/dist/services/user-service.d.ts.map +1 -1
- package/dist/services/user-service.js +24 -6
- package/dist/services/user-service.js.map +1 -1
- package/dist/services/user-service.test.d.ts +2 -0
- package/dist/services/user-service.test.d.ts.map +1 -0
- package/dist/services/user-service.test.js +86 -0
- package/dist/services/user-service.test.js.map +1 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -2
- package/prisma/schema.prisma +158 -82
- package/dist/db/schema.d.ts +0 -507
- package/dist/db/schema.d.ts.map +0 -1
- package/dist/db/schema.js +0 -77
- package/dist/db/schema.js.map +0 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { type MongoAbility, type RawRuleOf } from "@casl/ability";
|
|
2
|
+
import type { Principal } from "./principal.js";
|
|
3
|
+
/**
|
|
4
|
+
* In-process authorization engine for Apart Intelligence (Phase 1b).
|
|
5
|
+
*
|
|
6
|
+
* Every permission check in the API service layer goes through
|
|
7
|
+
* `principal.can(action, subject)` via the `AppAbility` returned by
|
|
8
|
+
* `buildAbility(principal)`. CASL owns the rule engine; `buildAbility` owns
|
|
9
|
+
* the per-principal-type rule construction.
|
|
10
|
+
*
|
|
11
|
+
* The action/subject vocabulary is intentionally the minimal starter set —
|
|
12
|
+
* just enough to cover the five existing `requireRole` call sites in
|
|
13
|
+
* `members.ts` and `invites.ts`, plus the accept-invite flow for users
|
|
14
|
+
* without memberships. Phase 1e grows the vocabulary per-route as the
|
|
15
|
+
* broader route-handler sweep lands.
|
|
16
|
+
*
|
|
17
|
+
* **No field conditions in Phase 1b rules.** Fine-grained row scoping
|
|
18
|
+
* (e.g., "this user can only read their own membership") is not expressed
|
|
19
|
+
* in CASL rules. Tenant isolation at the query level — Prisma's org-id
|
|
20
|
+
* injection extension — already restricts every DB read to the active
|
|
21
|
+
* organization, so a `can('read', 'Membership')` check is implicitly
|
|
22
|
+
* scoped to the caller's org at the data layer. Route handlers that need
|
|
23
|
+
* self-ID checks (e.g., "this must be the caller's own user row") do them
|
|
24
|
+
* explicitly. This keeps Phase 1b's authorization surface narrow and the
|
|
25
|
+
* CASL type wiring simple; Phase 1e can add conditions back via the
|
|
26
|
+
* `subject()` runtime helper once the per-route refactor reveals the right
|
|
27
|
+
* shapes.
|
|
28
|
+
*
|
|
29
|
+
* **Do not** add an action or a subject to these types without also thinking
|
|
30
|
+
* through the role rules below. CASL rules that reference an unknown action
|
|
31
|
+
* or subject become dead code silently, which is the opposite of what
|
|
32
|
+
* authorization code should do.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Canonical action verbs. `manage` is CASL's wildcard matching any action.
|
|
36
|
+
*
|
|
37
|
+
* `bypass` was added in Phase 1e as a first-class action so that the
|
|
38
|
+
* `X-Pii-Bypass` header (Phase 1 Security Review finding M3) can be gated
|
|
39
|
+
* with the natural phrasing `can('bypass', 'Pii')`. It is granted
|
|
40
|
+
* implicitly by the `manage` wildcard, which is the correct CASL semantic
|
|
41
|
+
* and preserves the legacy-api-key-grants-everything invariant without
|
|
42
|
+
* an explicit rule. See decision `500bfa31` for the full rationale,
|
|
43
|
+
* including which alternatives were rejected.
|
|
44
|
+
*/
|
|
45
|
+
export type AppAction = "manage" | "create" | "read" | "update" | "delete" | "bypass";
|
|
46
|
+
/**
|
|
47
|
+
* Canonical subject names. `all` is CASL's wildcard matching any subject.
|
|
48
|
+
*
|
|
49
|
+
* Phase 1e added `OrgConfig`, `AgentSchedule`, and `Pii` to close the
|
|
50
|
+
* Phase 1 Security Review findings H6 (`/api/org/config` authz), H7
|
|
51
|
+
* (`/api/agent/schedules` authz), and M3 (`X-Pii-Bypass` gating). Any new
|
|
52
|
+
* subject added here **must also** be added to `CONCRETE_SUBJECTS` below
|
|
53
|
+
* or `intersect()` will silently drop any rule that targets it.
|
|
54
|
+
*/
|
|
55
|
+
export type AppSubject = "Organization" | "Membership" | "Invite" | "User" | "OrgConfig" | "AgentSchedule" | "Pii" | "Node" | "Search" | "Import" | "Domain" | "Workspace" | "AgentRun" | "OrgAgentConfig" | "OrgEmbedding" | "UserSecret" | "all";
|
|
56
|
+
/** The CASL ability type used everywhere in the API. */
|
|
57
|
+
export type AppAbility = MongoAbility<[AppAction, AppSubject]>;
|
|
58
|
+
/** A CASL raw rule typed for our ability — used when serializing a
|
|
59
|
+
* DelegatedAgent's captured ability snapshot. */
|
|
60
|
+
export type AppRawRule = RawRuleOf<AppAbility>;
|
|
61
|
+
/**
|
|
62
|
+
* Build a CASL `AppAbility` from a `Principal`. Pure function — no I/O, no
|
|
63
|
+
* DB reads. The principal must be fully constructed by the auth middleware
|
|
64
|
+
* before this is called; the result can be cached per-request.
|
|
65
|
+
*
|
|
66
|
+
* Rule semantics by principal type:
|
|
67
|
+
*
|
|
68
|
+
* - **UserPrincipal**: rules derive from the user's `role` on their active
|
|
69
|
+
* `organizationId`. `owner` grants `manage all` (including `bypass Pii`
|
|
70
|
+
* by wildcard). `admin` grants management of invites and agent schedules
|
|
71
|
+
* plus read of org/membership/OrgConfig/Pii. `member` grants read of
|
|
72
|
+
* org/membership/OrgConfig/AgentSchedule/Pii. `none` (no active org)
|
|
73
|
+
* grants read of self and create of memberships for invite acceptance.
|
|
74
|
+
* Scoping to the caller's org happens at the query layer via Prisma
|
|
75
|
+
* tenant isolation. Admins and members cannot bypass PII scrubbing —
|
|
76
|
+
* the `bypass` action is reserved for `manage`-grade principals only,
|
|
77
|
+
* per the Phase 1e decision `500bfa31`.
|
|
78
|
+
*
|
|
79
|
+
* - **OrgAgentPrincipal**: if `legacyApiKey` is true (the default for
|
|
80
|
+
* pre-Phase-1c API keys), grants `manage all` to match current behavior.
|
|
81
|
+
* Once Phase 1c binds keys to real `OrgAgentType` rows, this branch will
|
|
82
|
+
* read from `OrgAgentType.intrinsicPolicy` instead — the legacy path stays
|
|
83
|
+
* as a fallback until the backfill completes.
|
|
84
|
+
*
|
|
85
|
+
* - **DelegatedAgentPrincipal**: rehydrates CASL rules from
|
|
86
|
+
* `capturedAbility`. The snapshot was computed at agent spawn time as
|
|
87
|
+
* `intersect(userAbility, agentIntrinsicPolicy)` and serialized into
|
|
88
|
+
* `AgentRun.capturedAbility` (Phase 1c column). This function is a pure
|
|
89
|
+
* deserializer for that snapshot; it does not recompute anything.
|
|
90
|
+
*/
|
|
91
|
+
export declare function buildAbility(principal: Principal): AppAbility;
|
|
92
|
+
/**
|
|
93
|
+
* Thrown when `intersect()` is asked to combine rule sets whose shape is
|
|
94
|
+
* outside the starter CASL vocabulary — currently any rule that carries
|
|
95
|
+
* CASL `conditions` or `fields`. See `intersect` docstring.
|
|
96
|
+
*/
|
|
97
|
+
export declare class UnsupportedIntersectionError extends Error {
|
|
98
|
+
constructor(reason: string);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Compute the intersection of two raw CASL rule sets, producing a rule set
|
|
102
|
+
* that grants access only where BOTH inputs grant access.
|
|
103
|
+
*
|
|
104
|
+
* Used at agent spawn time to capture the effective authority of a
|
|
105
|
+
* `DelegatedAgentPrincipal`: the user's ability at spawn time, intersected
|
|
106
|
+
* with the agent's intrinsic policy, is the ceiling on what the agent can
|
|
107
|
+
* do for the life of the run. The result is persisted on
|
|
108
|
+
* `AgentRun.captured_ability` (the Phase 1c column) and rehydrated by
|
|
109
|
+
* `buildAbility(DelegatedAgentPrincipal)` on every callback from the
|
|
110
|
+
* sandbox. See the Phase 1d user story `ed8fcc68` for the full rationale.
|
|
111
|
+
*
|
|
112
|
+
* **Semantics.** The intersection is computed by building a CASL ability
|
|
113
|
+
* from each input and enumerating every concrete `(action, subject)` pair
|
|
114
|
+
* in the `AppAction × AppSubject` cross product. A rule is emitted for
|
|
115
|
+
* each pair where both abilities grant access. CASL's `manage` and `all`
|
|
116
|
+
* wildcards are handled implicitly because `.can("read", "Organization")`
|
|
117
|
+
* on a `manage all` ability returns true.
|
|
118
|
+
*
|
|
119
|
+
* The returned rules are NOT compressed — a full-overlap intersection
|
|
120
|
+
* yields `CONCRETE_ACTIONS.length × CONCRETE_SUBJECTS.length` concrete
|
|
121
|
+
* rules (currently 5 × 7 = 35) rather than
|
|
122
|
+
* `[{action:"manage", subject:"all"}]`. This is semantically equivalent
|
|
123
|
+
* but more verbose. If audit-log readability ever becomes a concern, a
|
|
124
|
+
* `compress()` helper can be added as a separate function; for now,
|
|
125
|
+
* verbosity is the price of simplicity.
|
|
126
|
+
*
|
|
127
|
+
* **Phase 1d limitation.** CASL conditions and field scopes are not
|
|
128
|
+
* supported on either side of the intersection. The current `AppAbility`
|
|
129
|
+
* vocabulary explicitly does not use them (see the docstring at the top
|
|
130
|
+
* of this file — "No field conditions in Phase 1b rules"), so this is
|
|
131
|
+
* the correct shape for the current starter vocabulary. If an input
|
|
132
|
+
* rule carries `conditions` or `fields`, this function throws
|
|
133
|
+
* `UnsupportedIntersectionError` rather than silently producing an
|
|
134
|
+
* over-permissive result. When a future phase grows the CASL vocabulary
|
|
135
|
+
* to use conditions, this function grows with it.
|
|
136
|
+
*
|
|
137
|
+
* **Edge cases.**
|
|
138
|
+
* - Empty ∩ X → empty (deny by default).
|
|
139
|
+
* - X ∩ empty → empty.
|
|
140
|
+
* - Disjoint inputs → empty.
|
|
141
|
+
* - Full overlap (both sides grant `manage all`) → all concrete rules
|
|
142
|
+
* in the cross product (5 × 7 = 35 today), which rehydrates to a
|
|
143
|
+
* functionally-manage-all ability.
|
|
144
|
+
* - One side is `manage all`, the other is a specific rule → the specific
|
|
145
|
+
* rule (expanded to concrete (action, subject) form).
|
|
146
|
+
*/
|
|
147
|
+
export declare function intersect(a: AppRawRule[], b: AppRawRule[]): AppRawRule[];
|
|
148
|
+
//# sourceMappingURL=ability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ability.d.ts","sourceRoot":"","sources":["../../src/auth/ability.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,SAAS,GACjB,QAAQ,GACR,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,QAAQ,CAAC;AAEb;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAClB,cAAc,GACd,YAAY,GACZ,QAAQ,GACR,MAAM,GACN,WAAW,GACX,eAAe,GACf,KAAK,GACL,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,WAAW,GACX,UAAU,GACV,gBAAgB,GAChB,cAAc,GACd,YAAY,GACZ,KAAK,CAAC;AAEV,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,YAAY,CAAC,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;AAE/D;kDACkD;AAClD,MAAM,MAAM,UAAU,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,SAAS,GAAG,UAAU,CA6G7D;AA2BD;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,KAAK;gBACzC,MAAM,EAAE,MAAM;CAI3B;AAyCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAgBxE"}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { AbilityBuilder, createMongoAbility, } from "@casl/ability";
|
|
2
|
+
/**
|
|
3
|
+
* Build a CASL `AppAbility` from a `Principal`. Pure function — no I/O, no
|
|
4
|
+
* DB reads. The principal must be fully constructed by the auth middleware
|
|
5
|
+
* before this is called; the result can be cached per-request.
|
|
6
|
+
*
|
|
7
|
+
* Rule semantics by principal type:
|
|
8
|
+
*
|
|
9
|
+
* - **UserPrincipal**: rules derive from the user's `role` on their active
|
|
10
|
+
* `organizationId`. `owner` grants `manage all` (including `bypass Pii`
|
|
11
|
+
* by wildcard). `admin` grants management of invites and agent schedules
|
|
12
|
+
* plus read of org/membership/OrgConfig/Pii. `member` grants read of
|
|
13
|
+
* org/membership/OrgConfig/AgentSchedule/Pii. `none` (no active org)
|
|
14
|
+
* grants read of self and create of memberships for invite acceptance.
|
|
15
|
+
* Scoping to the caller's org happens at the query layer via Prisma
|
|
16
|
+
* tenant isolation. Admins and members cannot bypass PII scrubbing —
|
|
17
|
+
* the `bypass` action is reserved for `manage`-grade principals only,
|
|
18
|
+
* per the Phase 1e decision `500bfa31`.
|
|
19
|
+
*
|
|
20
|
+
* - **OrgAgentPrincipal**: if `legacyApiKey` is true (the default for
|
|
21
|
+
* pre-Phase-1c API keys), grants `manage all` to match current behavior.
|
|
22
|
+
* Once Phase 1c binds keys to real `OrgAgentType` rows, this branch will
|
|
23
|
+
* read from `OrgAgentType.intrinsicPolicy` instead — the legacy path stays
|
|
24
|
+
* as a fallback until the backfill completes.
|
|
25
|
+
*
|
|
26
|
+
* - **DelegatedAgentPrincipal**: rehydrates CASL rules from
|
|
27
|
+
* `capturedAbility`. The snapshot was computed at agent spawn time as
|
|
28
|
+
* `intersect(userAbility, agentIntrinsicPolicy)` and serialized into
|
|
29
|
+
* `AgentRun.capturedAbility` (Phase 1c column). This function is a pure
|
|
30
|
+
* deserializer for that snapshot; it does not recompute anything.
|
|
31
|
+
*/
|
|
32
|
+
export function buildAbility(principal) {
|
|
33
|
+
const { can, build } = new AbilityBuilder(createMongoAbility);
|
|
34
|
+
switch (principal.type) {
|
|
35
|
+
case "user": {
|
|
36
|
+
switch (principal.role) {
|
|
37
|
+
case "owner":
|
|
38
|
+
// Full access — matches today's `requireRole("owner")` behavior.
|
|
39
|
+
can("manage", "all");
|
|
40
|
+
break;
|
|
41
|
+
case "admin":
|
|
42
|
+
// Admins manage the invite lifecycle and can read the org and
|
|
43
|
+
// members. They cannot touch memberships directly; role changes
|
|
44
|
+
// are owner-only. Phase 1e: admins can also manage agent
|
|
45
|
+
// schedules (scheduled delegated agents are an operational
|
|
46
|
+
// concern admins need to own), and can read org-level config
|
|
47
|
+
// surfaces without being able to change them.
|
|
48
|
+
// Phase 1b: admins get manage on the full knowledge-graph
|
|
49
|
+
// surface (Node, Import, Domain, Workspace, AgentRun) and
|
|
50
|
+
// org-level config (OrgAgentConfig, OrgEmbedding). They get
|
|
51
|
+
// read on Search (a read-only operation).
|
|
52
|
+
can("manage", "Invite");
|
|
53
|
+
can("manage", "AgentSchedule");
|
|
54
|
+
can("manage", "Node");
|
|
55
|
+
can("manage", "Import");
|
|
56
|
+
can("manage", "Domain");
|
|
57
|
+
can("manage", "Workspace");
|
|
58
|
+
can("manage", "AgentRun");
|
|
59
|
+
can("manage", "OrgAgentConfig");
|
|
60
|
+
can("manage", "OrgEmbedding");
|
|
61
|
+
can("manage", "UserSecret");
|
|
62
|
+
can("read", "Organization");
|
|
63
|
+
can("read", "Membership");
|
|
64
|
+
can("read", "OrgConfig");
|
|
65
|
+
can("read", "Pii");
|
|
66
|
+
can("read", "Search");
|
|
67
|
+
break;
|
|
68
|
+
case "member":
|
|
69
|
+
// Members can read their org and memberships and the org-level
|
|
70
|
+
// config surfaces (so that the UI can show them). They cannot
|
|
71
|
+
// create schedules in Phase 1e — CASL conditions for "own
|
|
72
|
+
// schedules" are not in the starter vocabulary, so member-
|
|
73
|
+
// created schedules would need handler-level self-ID
|
|
74
|
+
// enforcement that this phase does not introduce. Tenant
|
|
75
|
+
// isolation at the query layer scopes all reads to the
|
|
76
|
+
// active org.
|
|
77
|
+
// Phase 1b: members get full manage on Node (graph CRUD is
|
|
78
|
+
// the core work) and Workspace, create+read on AgentRun,
|
|
79
|
+
// and read on everything else.
|
|
80
|
+
can("manage", "Node");
|
|
81
|
+
can("manage", "Workspace");
|
|
82
|
+
can("manage", "UserSecret");
|
|
83
|
+
can("create", "AgentRun");
|
|
84
|
+
can("read", "AgentRun");
|
|
85
|
+
can("read", "Organization");
|
|
86
|
+
can("read", "Membership");
|
|
87
|
+
can("read", "OrgConfig");
|
|
88
|
+
can("read", "AgentSchedule");
|
|
89
|
+
can("read", "Pii");
|
|
90
|
+
can("read", "Search");
|
|
91
|
+
can("read", "Import");
|
|
92
|
+
can("read", "Domain");
|
|
93
|
+
can("read", "OrgAgentConfig");
|
|
94
|
+
can("read", "OrgEmbedding");
|
|
95
|
+
break;
|
|
96
|
+
case "none":
|
|
97
|
+
// No active org: the user is either pre-invite-acceptance or
|
|
98
|
+
// holds multiple memberships without choosing one. Allow reading
|
|
99
|
+
// their own user record (handler must verify self-ID) and
|
|
100
|
+
// creating a membership for the accept-invite flow.
|
|
101
|
+
// Phase 1b: UserSecret is self-service and not org-scoped.
|
|
102
|
+
can("read", "User");
|
|
103
|
+
can("create", "Membership");
|
|
104
|
+
can("manage", "UserSecret");
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case "org_agent": {
|
|
110
|
+
if (principal.legacyApiKey) {
|
|
111
|
+
// Pre-Phase-1c API key: match today's full-org-access behavior so
|
|
112
|
+
// nothing breaks during the migration. Phase 1c will bind keys to
|
|
113
|
+
// real OrgAgentType rows with intrinsicPolicy; this branch becomes
|
|
114
|
+
// the fallback for unbound legacy keys only.
|
|
115
|
+
can("manage", "all");
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Phase 1c reads `OrgAgentType.intrinsicPolicy` here and constructs
|
|
119
|
+
// rules from it. For Phase 1b there is no such column yet, so the
|
|
120
|
+
// non-legacy branch is a placeholder with no rules — callers get a
|
|
121
|
+
// "deny by default" ability if they construct a non-legacy OrgAgent
|
|
122
|
+
// principal. This is the correct failure mode until 1c lands.
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case "delegated_agent": {
|
|
127
|
+
// Rehydrate from the captured snapshot. We bypass the AbilityBuilder
|
|
128
|
+
// because the rules are already fully formed; pass them straight to
|
|
129
|
+
// `createMongoAbility` to get an ability with those exact rules.
|
|
130
|
+
const rules = normalizeCapturedAbility(principal.capturedAbility);
|
|
131
|
+
return createMongoAbility(rules);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return build();
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Coerce a `DelegatedAgentPrincipal.capturedAbility` payload (typed
|
|
138
|
+
* `unknown` at the principal boundary) into a `RawRuleOf<AppAbility>[]` that
|
|
139
|
+
* CASL can consume. If the payload is malformed, returns an empty rule set —
|
|
140
|
+
* which yields a deny-by-default ability. This is intentionally strict:
|
|
141
|
+
* garbage in means no access, not crash.
|
|
142
|
+
*
|
|
143
|
+
* Phase 1d adds the real end-to-end test of the capture-and-rehydrate loop;
|
|
144
|
+
* for Phase 1b this function just has to round-trip a sensible array and
|
|
145
|
+
* reject obvious garbage.
|
|
146
|
+
*/
|
|
147
|
+
function normalizeCapturedAbility(captured) {
|
|
148
|
+
if (!Array.isArray(captured)) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
// We do not re-validate each rule's shape here — CASL itself will throw
|
|
152
|
+
// at `createMongoAbility` time if a rule is fundamentally malformed, and
|
|
153
|
+
// in Phase 1b the delegation path is synthetic (tests) or not exercised
|
|
154
|
+
// (no code mints delegation tokens yet). When Phase 1d wires this for
|
|
155
|
+
// real, consider adding a Zod schema here.
|
|
156
|
+
return captured;
|
|
157
|
+
}
|
|
158
|
+
// ── Rule-set intersection (Phase 1d) ────────────────────────────────────────
|
|
159
|
+
/**
|
|
160
|
+
* Thrown when `intersect()` is asked to combine rule sets whose shape is
|
|
161
|
+
* outside the starter CASL vocabulary — currently any rule that carries
|
|
162
|
+
* CASL `conditions` or `fields`. See `intersect` docstring.
|
|
163
|
+
*/
|
|
164
|
+
export class UnsupportedIntersectionError extends Error {
|
|
165
|
+
constructor(reason) {
|
|
166
|
+
super(`intersect(): ${reason}`);
|
|
167
|
+
this.name = "UnsupportedIntersectionError";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The non-wildcard subjects we enumerate when computing an intersection.
|
|
172
|
+
* Keep this in sync with the `AppSubject` type definition above — if a new
|
|
173
|
+
* concrete subject is added to `AppSubject`, it must be added here too or
|
|
174
|
+
* the intersection will silently drop any rules that target it.
|
|
175
|
+
*
|
|
176
|
+
* The `satisfies` clause gives us a compile error if a listed element is
|
|
177
|
+
* not a valid `AppSubject`, but it does NOT enforce exhaustiveness. The
|
|
178
|
+
* comment above is the current guardrail; an exhaustiveness check can be
|
|
179
|
+
* added later if the vocabulary grows past a handful of subjects.
|
|
180
|
+
*/
|
|
181
|
+
const CONCRETE_SUBJECTS = [
|
|
182
|
+
"Organization",
|
|
183
|
+
"Membership",
|
|
184
|
+
"Invite",
|
|
185
|
+
"User",
|
|
186
|
+
"OrgConfig",
|
|
187
|
+
"AgentSchedule",
|
|
188
|
+
"Pii",
|
|
189
|
+
"Node",
|
|
190
|
+
"Search",
|
|
191
|
+
"Import",
|
|
192
|
+
"Domain",
|
|
193
|
+
"Workspace",
|
|
194
|
+
"AgentRun",
|
|
195
|
+
"OrgAgentConfig",
|
|
196
|
+
"OrgEmbedding",
|
|
197
|
+
"UserSecret",
|
|
198
|
+
];
|
|
199
|
+
/** The non-wildcard actions. Same sync-with-type-definition caveat as above. */
|
|
200
|
+
const CONCRETE_ACTIONS = [
|
|
201
|
+
"create",
|
|
202
|
+
"read",
|
|
203
|
+
"update",
|
|
204
|
+
"delete",
|
|
205
|
+
"bypass",
|
|
206
|
+
];
|
|
207
|
+
/**
|
|
208
|
+
* Compute the intersection of two raw CASL rule sets, producing a rule set
|
|
209
|
+
* that grants access only where BOTH inputs grant access.
|
|
210
|
+
*
|
|
211
|
+
* Used at agent spawn time to capture the effective authority of a
|
|
212
|
+
* `DelegatedAgentPrincipal`: the user's ability at spawn time, intersected
|
|
213
|
+
* with the agent's intrinsic policy, is the ceiling on what the agent can
|
|
214
|
+
* do for the life of the run. The result is persisted on
|
|
215
|
+
* `AgentRun.captured_ability` (the Phase 1c column) and rehydrated by
|
|
216
|
+
* `buildAbility(DelegatedAgentPrincipal)` on every callback from the
|
|
217
|
+
* sandbox. See the Phase 1d user story `ed8fcc68` for the full rationale.
|
|
218
|
+
*
|
|
219
|
+
* **Semantics.** The intersection is computed by building a CASL ability
|
|
220
|
+
* from each input and enumerating every concrete `(action, subject)` pair
|
|
221
|
+
* in the `AppAction × AppSubject` cross product. A rule is emitted for
|
|
222
|
+
* each pair where both abilities grant access. CASL's `manage` and `all`
|
|
223
|
+
* wildcards are handled implicitly because `.can("read", "Organization")`
|
|
224
|
+
* on a `manage all` ability returns true.
|
|
225
|
+
*
|
|
226
|
+
* The returned rules are NOT compressed — a full-overlap intersection
|
|
227
|
+
* yields `CONCRETE_ACTIONS.length × CONCRETE_SUBJECTS.length` concrete
|
|
228
|
+
* rules (currently 5 × 7 = 35) rather than
|
|
229
|
+
* `[{action:"manage", subject:"all"}]`. This is semantically equivalent
|
|
230
|
+
* but more verbose. If audit-log readability ever becomes a concern, a
|
|
231
|
+
* `compress()` helper can be added as a separate function; for now,
|
|
232
|
+
* verbosity is the price of simplicity.
|
|
233
|
+
*
|
|
234
|
+
* **Phase 1d limitation.** CASL conditions and field scopes are not
|
|
235
|
+
* supported on either side of the intersection. The current `AppAbility`
|
|
236
|
+
* vocabulary explicitly does not use them (see the docstring at the top
|
|
237
|
+
* of this file — "No field conditions in Phase 1b rules"), so this is
|
|
238
|
+
* the correct shape for the current starter vocabulary. If an input
|
|
239
|
+
* rule carries `conditions` or `fields`, this function throws
|
|
240
|
+
* `UnsupportedIntersectionError` rather than silently producing an
|
|
241
|
+
* over-permissive result. When a future phase grows the CASL vocabulary
|
|
242
|
+
* to use conditions, this function grows with it.
|
|
243
|
+
*
|
|
244
|
+
* **Edge cases.**
|
|
245
|
+
* - Empty ∩ X → empty (deny by default).
|
|
246
|
+
* - X ∩ empty → empty.
|
|
247
|
+
* - Disjoint inputs → empty.
|
|
248
|
+
* - Full overlap (both sides grant `manage all`) → all concrete rules
|
|
249
|
+
* in the cross product (5 × 7 = 35 today), which rehydrates to a
|
|
250
|
+
* functionally-manage-all ability.
|
|
251
|
+
* - One side is `manage all`, the other is a specific rule → the specific
|
|
252
|
+
* rule (expanded to concrete (action, subject) form).
|
|
253
|
+
*/
|
|
254
|
+
export function intersect(a, b) {
|
|
255
|
+
assertUnconditional(a, "left");
|
|
256
|
+
assertUnconditional(b, "right");
|
|
257
|
+
const abilityA = createMongoAbility(a);
|
|
258
|
+
const abilityB = createMongoAbility(b);
|
|
259
|
+
const result = [];
|
|
260
|
+
for (const subject of CONCRETE_SUBJECTS) {
|
|
261
|
+
for (const action of CONCRETE_ACTIONS) {
|
|
262
|
+
if (abilityA.can(action, subject) && abilityB.can(action, subject)) {
|
|
263
|
+
result.push({ action, subject });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Walk a rule set and throw if any rule carries `conditions` or `fields`.
|
|
271
|
+
* The `side` label is threaded into the error message so callers can tell
|
|
272
|
+
* which argument was the offender.
|
|
273
|
+
*/
|
|
274
|
+
function assertUnconditional(rules, side) {
|
|
275
|
+
for (let i = 0; i < rules.length; i++) {
|
|
276
|
+
const rule = rules[i];
|
|
277
|
+
if (rule.conditions !== undefined) {
|
|
278
|
+
throw new UnsupportedIntersectionError(`${side}[${i}] has CASL conditions, which the Phase 1d starter vocabulary does not support`);
|
|
279
|
+
}
|
|
280
|
+
if (rule.fields !== undefined) {
|
|
281
|
+
throw new UnsupportedIntersectionError(`${side}[${i}] has CASL field scoping, which the Phase 1d starter vocabulary does not support`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
//# sourceMappingURL=ability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ability.js","sourceRoot":"","sources":["../../src/auth/ability.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ability.test.d.ts","sourceRoot":"","sources":["../../src/auth/ability.test.ts"],"names":[],"mappings":""}
|