@intentius/chant-lexicon-cedar 0.78.0 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gate-policy.d.ts +69 -0
- package/dist/gate-policy.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/gate-policy.test.ts +101 -0
- package/src/gate-policy.ts +190 -0
- package/src/index.ts +7 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cedar as a gate's approval policy (chant#2508).
|
|
3
|
+
*
|
|
4
|
+
* Two halves, one per side of the build:
|
|
5
|
+
*
|
|
6
|
+
* - {@link gatePolicy} renders a set of cedar `Policy` declarables to Cedar
|
|
7
|
+
* text at authoring time and returns core's `GatePolicyRef`, plain data that
|
|
8
|
+
* lands in the Op's build output. A gate names it as `approval.policy`.
|
|
9
|
+
* - {@link evaluateGatePolicy} is what core's `loadGatePolicyEvaluator("cedar")`
|
|
10
|
+
* imports from `@intentius/chant-lexicon-cedar/gate-policy`. `chant approve`
|
|
11
|
+
* calls it once per approval with a `PassGate` request, through the same
|
|
12
|
+
* `cedar-wasm` the post-synth checks load (`./lint/post-synth/wasm-helpers.ts`).
|
|
13
|
+
*
|
|
14
|
+
* The request uses these entity types, so a policy is written against them:
|
|
15
|
+
*
|
|
16
|
+
* | Position | Entity | Attributes / parents |
|
|
17
|
+
* |-----------|------------------------------------------|----------------------|
|
|
18
|
+
* | principal | `Chant::Human::"<name>"` or `Chant::Agent::"<name>"` | parents: one `Chant::Role::"<role>"` per claimed role |
|
|
19
|
+
* | action | `Chant::Action::"PassGate"` | |
|
|
20
|
+
* | resource | `Chant::Gate::"<op>/<gate>"` | `op`, `gate` |
|
|
21
|
+
* | context | `planDigest` when the gate binds a plan, plus the gate's `approval.context` | |
|
|
22
|
+
*
|
|
23
|
+
* So `principal is Chant::Agent` picks out an agent, `principal in
|
|
24
|
+
* Chant::Role::"maintainer"` a role, and `context.risk == "low"` a declared
|
|
25
|
+
* plan attribute.
|
|
26
|
+
*/
|
|
27
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
28
|
+
import { type GatePolicyAnswer, type GatePolicyRef, type GatePolicyRequest } from "@intentius/chant/op";
|
|
29
|
+
export declare const GATE_HUMAN_TYPE = "Chant::Human";
|
|
30
|
+
export declare const GATE_AGENT_TYPE = "Chant::Agent";
|
|
31
|
+
export declare const GATE_ROLE_TYPE = "Chant::Role";
|
|
32
|
+
export declare const GATE_RESOURCE_TYPE = "Chant::Gate";
|
|
33
|
+
/** `action == Chant::Action::"PassGate"`, ready for a `Policy`'s `action` scope. */
|
|
34
|
+
export declare const PASS_GATE_ACTION = "Chant::Action::\"PassGate\"";
|
|
35
|
+
/** The policies a gate policy set is made of: one, a list, or a record whose keys become their logical names. */
|
|
36
|
+
export type GatePolicies = Declarable | readonly Declarable[] | Readonly<Record<string, Declarable>>;
|
|
37
|
+
/**
|
|
38
|
+
* A gate policy set from cedar `Policy` declarables. `name` is recorded with
|
|
39
|
+
* every decision; ids come from each policy's `annotations.id`, else its
|
|
40
|
+
* record key, else `<name>-<index>`.
|
|
41
|
+
*
|
|
42
|
+
* Throws when a member is not a cedar policy, or when the rendered set does
|
|
43
|
+
* not parse, so a broken policy fails where it is written and not at the
|
|
44
|
+
* first `chant approve`.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* export const shipPolicy = gatePolicy("ship", {
|
|
49
|
+
* agentLowRisk: new Policy({
|
|
50
|
+
* effect: "permit",
|
|
51
|
+
* principal: { is: GATE_AGENT_TYPE },
|
|
52
|
+
* action: { eq: PASS_GATE_ACTION },
|
|
53
|
+
* when: ['context.risk == "low"'],
|
|
54
|
+
* }),
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function gatePolicy(name: string, policies: GatePolicies): GatePolicyRef;
|
|
59
|
+
/**
|
|
60
|
+
* Evaluate one `PassGate` request. Called by core through
|
|
61
|
+
* `loadGatePolicyEvaluator("cedar")`; see the module doc for the entity model.
|
|
62
|
+
*
|
|
63
|
+
* Throws when the wasm cannot be loaded or the request cannot be evaluated at
|
|
64
|
+
* all, so `chant approve` refuses rather than recording a decision nobody
|
|
65
|
+
* made. A policy that errors on this request is reported in `errors` and does
|
|
66
|
+
* not apply, which is Cedar's own rule.
|
|
67
|
+
*/
|
|
68
|
+
export declare function evaluateGatePolicy(policy: GatePolicyRef, request: GatePolicyRequest): GatePolicyAnswer;
|
|
69
|
+
//# sourceMappingURL=gate-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-policy.d.ts","sourceRoot":"","sources":["../src/gate-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACvB,MAAM,qBAAqB,CAAC;AAK7B,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAC9C,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAC9C,eAAO,MAAM,cAAc,gBAAgB,CAAC;AAC5C,eAAO,MAAM,kBAAkB,gBAAgB,CAAC;AAChD,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,gCAA8B,CAAC;AAE5D,iHAAiH;AACjH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,SAAS,UAAU,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;AAcrG;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,aAAa,CA0B9E;AA0CD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CA+BtG"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { cedarPlugin } from "./plugin.js";
|
|
2
2
|
export { cedarSerializer, policyIdFromLogicalName, resolvePolicyId, cedarPolicyRecords } from "./serializer.js";
|
|
3
3
|
export { CEDAR_POLICY_TYPE, CEDAR_JSON_FILENAME } from "./serializer.js";
|
|
4
|
+
export { gatePolicy, evaluateGatePolicy, GATE_HUMAN_TYPE, GATE_AGENT_TYPE, GATE_ROLE_TYPE, GATE_RESOURCE_TYPE, PASS_GATE_ACTION, } from "./gate-policy.js";
|
|
5
|
+
export type { GatePolicies } from "./gate-policy.js";
|
|
4
6
|
export type { CedarEffect, CedarScope, CedarPolicyProps, CedarPolicyRecord } from "./serializer.js";
|
|
5
7
|
export { avpStatement, avpStatementJSON, avpPolicyDefinition, avpPolicyResource, avpPolicySet, } from "./avp/embed.js";
|
|
6
8
|
export type { AvpEmbedOptions, AvpPolicyDefinition, AvpPolicyResource, AvpStaticDefinition, } from "./avp/embed.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAGvC,OAAO,EAAE,eAAe,EAAE,uBAAuB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC7G,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "2334a9f9352afbbd0b353415f0aa6e172cb35905cb368f2b9dae082c64382d40",
|
|
5
5
|
"meta.json": "b218779260169a193882aeee76e5ac559588f1682657a001fb0982cffb5843fc",
|
|
6
6
|
"types/index.d.ts": "9e53a1cb3c9ee38f9de2f3c0db9cc28a0b67c3649be410c341e2451b9b0076cd",
|
|
7
7
|
"rules/policy-shape.ts": "1654730d188b7163dbad3051c75a562198a96eda6d36ce35701f2b729bf45194",
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
"skills/chant-cedar-meta-policy.md": "050b310a87196a48c827f1129698e4c6bbcd96dc963782e22bf1a640bbc20a1a",
|
|
31
31
|
"skills/chant-cedar-dogwood.md": "7e52d3f045c1c34e00fdef76014a05483fb9dd58cc41037621a76bc298776d8f"
|
|
32
32
|
},
|
|
33
|
-
"composite": "
|
|
33
|
+
"composite": "0abe9aeea8c2cb18fdee0e0bbc3cba769f3796642ec3243a492131a557cc49a7"
|
|
34
34
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-cedar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.0",
|
|
4
4
|
"description": "Cedar lexicon for chant — typed authoring for Cedar authorization policies",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"zod": "^4.3.6",
|
|
77
|
-
"@intentius/chant": "^0.
|
|
77
|
+
"@intentius/chant": "^0.79.0",
|
|
78
78
|
"typescript": "^5.9.3"
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cedar policy set as a gate's approval policy (chant#2508), evaluated by
|
|
3
|
+
* the real cedar-wasm, and reached through core's loader the way `chant
|
|
4
|
+
* approve` reaches it.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, test, expect } from "vitest";
|
|
7
|
+
import { gatePolicyRequest, gatePolicyVersion, loadGatePolicyEvaluator, gateApprovalProblems } from "@intentius/chant/op";
|
|
8
|
+
import { Policy, type EntityTypeName, type PolicyRef } from "./generated/index";
|
|
9
|
+
import { DenyByDefaultSet } from "./composites/deny-by-default-set";
|
|
10
|
+
import { evaluateGatePolicy, gatePolicy, GATE_AGENT_TYPE, PASS_GATE_ACTION } from "./gate-policy";
|
|
11
|
+
|
|
12
|
+
const PLAN = `sha256:${"a".repeat(64)}`;
|
|
13
|
+
|
|
14
|
+
// The generated scope types are narrowed to this package's sample `App::`
|
|
15
|
+
// schema. A project writing gate policies declares the `Chant` namespace in
|
|
16
|
+
// its own schema, and then these names type-check without a cast.
|
|
17
|
+
const AGENT = GATE_AGENT_TYPE as EntityTypeName;
|
|
18
|
+
const PASS_GATE = PASS_GATE_ACTION as unknown as PolicyRef;
|
|
19
|
+
const MAINTAINER = 'Chant::Role::"maintainer"' as unknown as PolicyRef;
|
|
20
|
+
|
|
21
|
+
const agentLowRisk = new Policy({
|
|
22
|
+
effect: "permit",
|
|
23
|
+
principal: { is: AGENT },
|
|
24
|
+
action: { eq: PASS_GATE },
|
|
25
|
+
when: ['context.risk == "low"'],
|
|
26
|
+
});
|
|
27
|
+
const maintainers = new Policy({
|
|
28
|
+
effect: "permit",
|
|
29
|
+
principal: { in: MAINTAINER },
|
|
30
|
+
action: { eq: PASS_GATE },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function ask(kind: "human" | "agent", name: string, context: Record<string, unknown>, roles: string[] = []) {
|
|
34
|
+
return gatePolicyRequest({
|
|
35
|
+
op: "release", gate: "ship", resolvedBy: name, approver: { kind, roles }, planDigest: PLAN, context,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("gatePolicy (chant#2508)", () => {
|
|
40
|
+
test("renders the set to Cedar text with ids from the record keys, stamped with its digest", () => {
|
|
41
|
+
const ref = gatePolicy("ship", { agentLowRisk, maintainers });
|
|
42
|
+
expect(ref.kind).toBe("gate-policy");
|
|
43
|
+
expect(ref.lexicon).toBe("cedar");
|
|
44
|
+
expect(ref.text).toContain('@id("agent-low-risk")');
|
|
45
|
+
expect(ref.text).toContain('@id("maintainers")');
|
|
46
|
+
expect(ref.text).toContain("principal is Chant::Agent");
|
|
47
|
+
expect(ref.version).toBe(gatePolicyVersion(ref.text));
|
|
48
|
+
expect(gateApprovalProblems({ policy: ref, mode: "enforce" })).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("refuses a member that is not a cedar policy", () => {
|
|
52
|
+
expect(() => gatePolicy("ship", [{ entityType: "Other::Thing" } as never])).toThrow("not a Cedar::Policy");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("refuses a set that does not parse", () => {
|
|
56
|
+
const broken = new Policy({ effect: "permit", when: ["context.risk =="] });
|
|
57
|
+
expect(() => gatePolicy("ship", [broken])).toThrow("does not parse");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("evaluateGatePolicy (chant#2508)", () => {
|
|
62
|
+
const ref = gatePolicy("ship", { agentLowRisk, maintainers });
|
|
63
|
+
|
|
64
|
+
test("permits an agent on a low-risk plan, naming the rule", () => {
|
|
65
|
+
expect(evaluateGatePolicy(ref, ask("agent", "release-bot", { risk: "low" }))).toEqual({
|
|
66
|
+
decision: "allow", determining: ["agent-low-risk"], errors: [],
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("denies the same agent on a high-risk plan", () => {
|
|
71
|
+
expect(evaluateGatePolicy(ref, ask("agent", "release-bot", { risk: "high" })).decision).toBe("deny");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("reads a claimed role as a parent entity", () => {
|
|
75
|
+
expect(evaluateGatePolicy(ref, ask("human", "alex", {}, ["maintainer"])).determining).toEqual(["maintainers"]);
|
|
76
|
+
expect(evaluateGatePolicy(ref, ask("human", "pat", {}, ["viewer"])).decision).toBe("deny");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("a policy that errors on a request does not apply, and the error is reported", () => {
|
|
80
|
+
// `context.risk` is absent, so the agent rule errors rather than matching.
|
|
81
|
+
const answer = evaluateGatePolicy(ref, ask("agent", "release-bot", {}));
|
|
82
|
+
expect(answer.decision).toBe("deny");
|
|
83
|
+
expect(answer.errors.join("\n")).toContain("agent-low-risk");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a DenyByDefaultSet floor overrides the agent permit, so only human approvals count", () => {
|
|
87
|
+
const floored = gatePolicy("ship", DenyByDefaultSet({
|
|
88
|
+
policies: [agentLowRisk],
|
|
89
|
+
principal: AGENT,
|
|
90
|
+
when: ["true"],
|
|
91
|
+
}).all);
|
|
92
|
+
const answer = evaluateGatePolicy(floored, ask("agent", "release-bot", { risk: "low" }));
|
|
93
|
+
expect(answer.decision).toBe("deny");
|
|
94
|
+
expect(answer.determining).toHaveLength(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("core's loader finds this module by the lexicon-name convention", async () => {
|
|
98
|
+
const evaluator = await loadGatePolicyEvaluator("cedar");
|
|
99
|
+
expect((await evaluator.evaluateGatePolicy(ref, ask("agent", "release-bot", { risk: "low" }))).decision).toBe("allow");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cedar as a gate's approval policy (chant#2508).
|
|
3
|
+
*
|
|
4
|
+
* Two halves, one per side of the build:
|
|
5
|
+
*
|
|
6
|
+
* - {@link gatePolicy} renders a set of cedar `Policy` declarables to Cedar
|
|
7
|
+
* text at authoring time and returns core's `GatePolicyRef`, plain data that
|
|
8
|
+
* lands in the Op's build output. A gate names it as `approval.policy`.
|
|
9
|
+
* - {@link evaluateGatePolicy} is what core's `loadGatePolicyEvaluator("cedar")`
|
|
10
|
+
* imports from `@intentius/chant-lexicon-cedar/gate-policy`. `chant approve`
|
|
11
|
+
* calls it once per approval with a `PassGate` request, through the same
|
|
12
|
+
* `cedar-wasm` the post-synth checks load (`./lint/post-synth/wasm-helpers.ts`).
|
|
13
|
+
*
|
|
14
|
+
* The request uses these entity types, so a policy is written against them:
|
|
15
|
+
*
|
|
16
|
+
* | Position | Entity | Attributes / parents |
|
|
17
|
+
* |-----------|------------------------------------------|----------------------|
|
|
18
|
+
* | principal | `Chant::Human::"<name>"` or `Chant::Agent::"<name>"` | parents: one `Chant::Role::"<role>"` per claimed role |
|
|
19
|
+
* | action | `Chant::Action::"PassGate"` | |
|
|
20
|
+
* | resource | `Chant::Gate::"<op>/<gate>"` | `op`, `gate` |
|
|
21
|
+
* | context | `planDigest` when the gate binds a plan, plus the gate's `approval.context` | |
|
|
22
|
+
*
|
|
23
|
+
* So `principal is Chant::Agent` picks out an agent, `principal in
|
|
24
|
+
* Chant::Role::"maintainer"` a role, and `context.risk == "low"` a declared
|
|
25
|
+
* plan attribute.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
29
|
+
import {
|
|
30
|
+
gatePolicyVersion,
|
|
31
|
+
type GatePolicyAnswer,
|
|
32
|
+
type GatePolicyRef,
|
|
33
|
+
type GatePolicyRequest,
|
|
34
|
+
} from "@intentius/chant/op";
|
|
35
|
+
import { getProps, resolvePolicyId } from "./policy-text";
|
|
36
|
+
import { CEDAR_POLICY_TYPE, renderPolicyText } from "./serializer";
|
|
37
|
+
import { describeError, loadWasm, wasmLoadError } from "./lint/post-synth/wasm-helpers";
|
|
38
|
+
|
|
39
|
+
export const GATE_HUMAN_TYPE = "Chant::Human";
|
|
40
|
+
export const GATE_AGENT_TYPE = "Chant::Agent";
|
|
41
|
+
export const GATE_ROLE_TYPE = "Chant::Role";
|
|
42
|
+
export const GATE_RESOURCE_TYPE = "Chant::Gate";
|
|
43
|
+
/** `action == Chant::Action::"PassGate"`, ready for a `Policy`'s `action` scope. */
|
|
44
|
+
export const PASS_GATE_ACTION = 'Chant::Action::"PassGate"';
|
|
45
|
+
|
|
46
|
+
/** The policies a gate policy set is made of: one, a list, or a record whose keys become their logical names. */
|
|
47
|
+
export type GatePolicies = Declarable | readonly Declarable[] | Readonly<Record<string, Declarable>>;
|
|
48
|
+
|
|
49
|
+
function entries(name: string, policies: GatePolicies): Array<[string, Declarable]> {
|
|
50
|
+
if (Array.isArray(policies)) {
|
|
51
|
+
return (policies as readonly Declarable[]).map((p, i) => [`${name}-${i}`, p]);
|
|
52
|
+
}
|
|
53
|
+
if (isDeclarable(policies)) return [[name, policies]];
|
|
54
|
+
return Object.entries(policies as Record<string, Declarable>);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isDeclarable(value: unknown): value is Declarable {
|
|
58
|
+
return typeof value === "object" && value !== null && "entityType" in value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A gate policy set from cedar `Policy` declarables. `name` is recorded with
|
|
63
|
+
* every decision; ids come from each policy's `annotations.id`, else its
|
|
64
|
+
* record key, else `<name>-<index>`.
|
|
65
|
+
*
|
|
66
|
+
* Throws when a member is not a cedar policy, or when the rendered set does
|
|
67
|
+
* not parse, so a broken policy fails where it is written and not at the
|
|
68
|
+
* first `chant approve`.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* export const shipPolicy = gatePolicy("ship", {
|
|
73
|
+
* agentLowRisk: new Policy({
|
|
74
|
+
* effect: "permit",
|
|
75
|
+
* principal: { is: GATE_AGENT_TYPE },
|
|
76
|
+
* action: { eq: PASS_GATE_ACTION },
|
|
77
|
+
* when: ['context.risk == "low"'],
|
|
78
|
+
* }),
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function gatePolicy(name: string, policies: GatePolicies): GatePolicyRef {
|
|
83
|
+
const members = entries(name, policies);
|
|
84
|
+
if (members.length === 0) throw new Error(`gatePolicy("${name}"): the policy set is empty`);
|
|
85
|
+
|
|
86
|
+
const texts = members.map(([logical, entity]) => {
|
|
87
|
+
if (entity.entityType !== CEDAR_POLICY_TYPE) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`gatePolicy("${name}"): "${logical}" is a ${entity.entityType}, not a ${CEDAR_POLICY_TYPE}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const props = getProps(entity);
|
|
93
|
+
return renderPolicyText(resolvePolicyId(logical, props), props);
|
|
94
|
+
});
|
|
95
|
+
const text = texts.join("\n\n") + "\n";
|
|
96
|
+
|
|
97
|
+
const wasm = loadWasm();
|
|
98
|
+
if (wasm) {
|
|
99
|
+
const parsed = wasm.checkParsePolicySet({ staticPolicies: text });
|
|
100
|
+
if (parsed.type === "failure") {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`gatePolicy("${name}"): the policy set does not parse: ${parsed.errors.map(describeError).join("; ")}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { kind: "gate-policy", lexicon: "cedar", name, version: gatePolicyVersion(text), text };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
type CedarValue = string | number | boolean | CedarValue[] | { [key: string]: CedarValue };
|
|
111
|
+
|
|
112
|
+
/** A context value as Cedar JSON. Cedar has no floats or null, so those become strings or are dropped. */
|
|
113
|
+
function cedarValue(value: unknown): CedarValue | undefined {
|
|
114
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
115
|
+
if (typeof value === "number") return Number.isInteger(value) ? value : String(value);
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
return value.map(cedarValue).filter((v): v is CedarValue => v !== undefined);
|
|
118
|
+
}
|
|
119
|
+
if (typeof value === "object" && value !== null) {
|
|
120
|
+
const out: Record<string, CedarValue> = {};
|
|
121
|
+
for (const [k, v] of Object.entries(value)) {
|
|
122
|
+
const converted = cedarValue(v);
|
|
123
|
+
if (converted !== undefined) out[k] = converted;
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The set as a record keyed by each policy's `@id`. Handed the text whole,
|
|
132
|
+
* cedar-wasm names policies by position (`policy0`), and a recorded decision
|
|
133
|
+
* would then name a rule nobody wrote. A policy with no `@id` keeps its
|
|
134
|
+
* positional name.
|
|
135
|
+
*/
|
|
136
|
+
function keyedById(wasm: NonNullable<ReturnType<typeof loadWasm>>, policy: GatePolicyRef): Record<string, string> {
|
|
137
|
+
const parts = wasm.policySetTextToParts(policy.text);
|
|
138
|
+
if (parts.type === "failure") {
|
|
139
|
+
throw new Error(`policy "${policy.name}" does not parse: ${parts.errors.map(describeError).join("; ")}`);
|
|
140
|
+
}
|
|
141
|
+
const keyed: Record<string, string> = {};
|
|
142
|
+
parts.policies.forEach((text, index) => {
|
|
143
|
+
const id = /@id\(\s*"((?:[^"\\]|\\.)*)"\s*\)/.exec(text)?.[1] ?? `policy${index}`;
|
|
144
|
+
if (id in keyed) throw new Error(`policy "${policy.name}" has two policies with id "${id}"`);
|
|
145
|
+
keyed[id] = text;
|
|
146
|
+
});
|
|
147
|
+
return keyed;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Evaluate one `PassGate` request. Called by core through
|
|
152
|
+
* `loadGatePolicyEvaluator("cedar")`; see the module doc for the entity model.
|
|
153
|
+
*
|
|
154
|
+
* Throws when the wasm cannot be loaded or the request cannot be evaluated at
|
|
155
|
+
* all, so `chant approve` refuses rather than recording a decision nobody
|
|
156
|
+
* made. A policy that errors on this request is reported in `errors` and does
|
|
157
|
+
* not apply, which is Cedar's own rule.
|
|
158
|
+
*/
|
|
159
|
+
export function evaluateGatePolicy(policy: GatePolicyRef, request: GatePolicyRequest): GatePolicyAnswer {
|
|
160
|
+
const wasm = loadWasm();
|
|
161
|
+
if (!wasm) throw new Error(`@cedar-policy/cedar-wasm could not be loaded: ${wasmLoadError()}`);
|
|
162
|
+
|
|
163
|
+
const principalType = request.principal.kind === "agent" ? GATE_AGENT_TYPE : GATE_HUMAN_TYPE;
|
|
164
|
+
const roles = [...new Set(request.principal.roles)];
|
|
165
|
+
const principal = { type: principalType, id: request.principal.name };
|
|
166
|
+
const resource = { type: GATE_RESOURCE_TYPE, id: `${request.resource.op}/${request.resource.gate}` };
|
|
167
|
+
|
|
168
|
+
const answer = wasm.isAuthorized({
|
|
169
|
+
principal,
|
|
170
|
+
action: { type: "Chant::Action", id: request.action },
|
|
171
|
+
resource,
|
|
172
|
+
context: (cedarValue(request.context) ?? {}) as Record<string, never>,
|
|
173
|
+
policies: { staticPolicies: keyedById(wasm, policy) },
|
|
174
|
+
entities: [
|
|
175
|
+
{ uid: principal, attrs: {}, parents: roles.map((id) => ({ type: GATE_ROLE_TYPE, id })) },
|
|
176
|
+
...roles.map((id) => ({ uid: { type: GATE_ROLE_TYPE, id }, attrs: {}, parents: [] })),
|
|
177
|
+
{ uid: resource, attrs: { op: request.resource.op, gate: request.resource.gate }, parents: [] },
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (answer.type === "failure") {
|
|
182
|
+
throw new Error(`policy "${policy.name}" could not be evaluated: ${answer.errors.map(describeError).join("; ")}`);
|
|
183
|
+
}
|
|
184
|
+
const { decision, diagnostics } = answer.response;
|
|
185
|
+
return {
|
|
186
|
+
decision,
|
|
187
|
+
determining: [...diagnostics.reason].sort(),
|
|
188
|
+
errors: diagnostics.errors.map((e) => `${e.policyId}: ${describeError(e.error)}`),
|
|
189
|
+
};
|
|
190
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,13 @@ export { cedarPlugin } from "./plugin";
|
|
|
4
4
|
// Serializer
|
|
5
5
|
export { cedarSerializer, policyIdFromLogicalName, resolvePolicyId, cedarPolicyRecords } from "./serializer";
|
|
6
6
|
export { CEDAR_POLICY_TYPE, CEDAR_JSON_FILENAME } from "./serializer";
|
|
7
|
+
|
|
8
|
+
// Gate approval policy (chant#2508) — a cedar policy set as a gate's `approval.policy`.
|
|
9
|
+
export {
|
|
10
|
+
gatePolicy, evaluateGatePolicy,
|
|
11
|
+
GATE_HUMAN_TYPE, GATE_AGENT_TYPE, GATE_ROLE_TYPE, GATE_RESOURCE_TYPE, PASS_GATE_ACTION,
|
|
12
|
+
} from "./gate-policy";
|
|
13
|
+
export type { GatePolicies } from "./gate-policy";
|
|
7
14
|
export type { CedarEffect, CedarScope, CedarPolicyProps, CedarPolicyRecord } from "./serializer";
|
|
8
15
|
|
|
9
16
|
// AVP embedding (#1652) — the typed statement an `AWS::VerifiedPermissions::Policy`
|