@intentius/chant-lexicon-cedar 0.44.13 → 0.45.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/README.md +13 -4
- package/dist/avp/client.d.ts +18 -0
- package/dist/avp/client.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +5 -9
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/generate.d.ts +35 -8
- package/dist/codegen/generate.d.ts.map +1 -1
- package/dist/commands.d.ts +14 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/config.d.ts +38 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/init-templates.d.ts +1 -1
- package/dist/integrity.json +4 -4
- package/dist/lint/post-synth/cede010.d.ts +5 -4
- package/dist/lint/post-synth/cede010.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/cede010.ts +5 -4
- package/dist/schema-artifact.d.ts +50 -0
- package/dist/schema-artifact.d.ts.map +1 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skills/chant-cedar-authoring.md +9 -3
- package/dist/spec/fetch.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/avp/OWNERSHIP.md +4 -1
- package/src/avp/client.test.ts +35 -0
- package/src/avp/client.ts +29 -2
- package/src/codegen/docs.ts +5 -799
- package/src/codegen/generate-cli.ts +8 -6
- package/src/codegen/generate.ts +63 -14
- package/src/codegen/output-dir.test.ts +137 -0
- package/src/commands.test.ts +93 -0
- package/src/commands.ts +95 -0
- package/src/config.ts +50 -4
- package/src/index.ts +10 -2
- package/src/init-templates.test.ts +22 -4
- package/src/init-templates.ts +16 -16
- package/src/lint/post-synth/cede010.ts +5 -4
- package/src/plugin.ts +33 -8
- package/src/schema-artifact.test.ts +160 -0
- package/src/schema-artifact.ts +87 -0
- package/src/serializer.ts +17 -1
- package/src/skills/chant-cedar-authoring.md +9 -3
- package/src/spec/fetch.ts +23 -2
- package/dist/codegen/docs-dogwood.d.ts +0 -21
- package/dist/codegen/docs-dogwood.d.ts.map +0 -1
- package/src/codegen/docs-dogwood.ts +0 -1119
package/src/init-templates.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* The policy files use `entityType`-string scopes (`{ is: "App::Document" }`)
|
|
19
19
|
* and generated action constants, so a scaffolded project compiles the moment
|
|
20
|
-
* `chant generate` has run against its own schema.
|
|
20
|
+
* `chant cedar generate` has run against its own schema (#1696).
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import type { InitTemplateSet } from "@intentius/chant/lexicon";
|
|
@@ -30,12 +30,16 @@ function readme(title: string, body: string): string {
|
|
|
30
30
|
|
|
31
31
|
\`\`\`bash
|
|
32
32
|
npm install
|
|
33
|
-
npx chant generate
|
|
34
|
-
npx chant build
|
|
33
|
+
npx chant cedar generate # schema.cedarschema -> src/generated/cedar/
|
|
34
|
+
npx chant build # emits .cedar text, policies.cedar.json, and the schema
|
|
35
35
|
\`\`\`
|
|
36
36
|
|
|
37
|
-
Generate first. The classes and action constants \`src/policies.ts\` imports
|
|
38
|
-
not exist until \`chant generate\` has read
|
|
37
|
+
Generate first. The classes and action constants \`src/policies.ts\` imports
|
|
38
|
+
from \`./generated/cedar\` do not exist until \`chant cedar generate\` has read
|
|
39
|
+
\`${SCHEMA_FILE}\`. The output is yours: it lives in this project, not in
|
|
40
|
+
\`node_modules\`, so commit it (or regenerate it in CI before \`chant build\`)
|
|
41
|
+
and re-run generate whenever the schema changes. \`cedar.outDir\` in
|
|
42
|
+
\`chant.config.ts\` moves it.
|
|
39
43
|
|
|
40
44
|
${body}
|
|
41
45
|
|
|
@@ -63,6 +67,7 @@ this project never declared.
|
|
|
63
67
|
|------|--------------|
|
|
64
68
|
| \`dist/*.cedar\` | Every Cedar evaluator — AVP, cedar-agent, an embedded cedar-wasm |
|
|
65
69
|
| \`dist/policies.cedar.json\` | The Cedar JSON policy format; also the parse source for import |
|
|
70
|
+
| \`dist/${SCHEMA_FILE}\` | Your schema, beside the policies, so the build validates against it and a bundle can carry it |
|
|
66
71
|
`;
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -70,7 +75,7 @@ this project never declared.
|
|
|
70
75
|
|
|
71
76
|
const DEFAULT_SCHEMA = `// The entity model your policies are typed against.
|
|
72
77
|
//
|
|
73
|
-
// \`chant generate
|
|
78
|
+
// \`chant cedar generate\` turns every declaration below into a
|
|
74
79
|
// TypeScript class or constant, so a renamed entity type becomes a
|
|
75
80
|
// compiler-guided refactor rather than a runtime validation failure.
|
|
76
81
|
|
|
@@ -96,7 +101,7 @@ namespace App {
|
|
|
96
101
|
}
|
|
97
102
|
`;
|
|
98
103
|
|
|
99
|
-
const DEFAULT_POLICIES = `import { Policy, ReadAction, WriteAction } from "
|
|
104
|
+
const DEFAULT_POLICIES = `import { Policy, ReadAction, WriteAction } from "./generated/cedar";
|
|
100
105
|
|
|
101
106
|
/**
|
|
102
107
|
* A permit and a forbid — the smallest policy set worth deploying.
|
|
@@ -160,7 +165,7 @@ namespace Store {
|
|
|
160
165
|
}
|
|
161
166
|
`;
|
|
162
167
|
|
|
163
|
-
const AVP_POLICIES = `import { Policy, EditAction, ViewAction } from "
|
|
168
|
+
const AVP_POLICIES = `import { Policy, EditAction, ViewAction } from "./generated/cedar";
|
|
164
169
|
|
|
165
170
|
/**
|
|
166
171
|
* Policies destined for an AVP policy store.
|
|
@@ -239,13 +244,8 @@ namespace Gateway {
|
|
|
239
244
|
}
|
|
240
245
|
`;
|
|
241
246
|
|
|
242
|
-
const GATEWAY_POLICIES = `import {
|
|
243
|
-
|
|
244
|
-
DenyByDefaultSet,
|
|
245
|
-
GetAction,
|
|
246
|
-
Policy,
|
|
247
|
-
PostAction,
|
|
248
|
-
} from "@intentius/chant-lexicon-cedar";
|
|
247
|
+
const GATEWAY_POLICIES = `import { DenyByDefaultSet } from "@intentius/chant-lexicon-cedar";
|
|
248
|
+
import { DeleteAction, GetAction, Policy, PostAction } from "./generated/cedar";
|
|
249
249
|
|
|
250
250
|
/**
|
|
251
251
|
* A gateway policy set with an explicit deny floor.
|
|
@@ -298,7 +298,7 @@ export const authenticatedWriteGrant = guarded.members[1];
|
|
|
298
298
|
*/
|
|
299
299
|
export function cedarInitTemplates(template?: string): InitTemplateSet {
|
|
300
300
|
const scripts = {
|
|
301
|
-
generate: "chant generate
|
|
301
|
+
generate: "chant cedar generate",
|
|
302
302
|
build: "chant build",
|
|
303
303
|
};
|
|
304
304
|
|
|
@@ -24,10 +24,11 @@
|
|
|
24
24
|
* When the build emits no schema there is nothing to validate against, and
|
|
25
25
|
* `validate()` cannot be called at all — it has no optional-schema mode, and an
|
|
26
26
|
* empty schema is legal Cedar that reports every policy as inapplicable, which
|
|
27
|
-
* would be worse than useless.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* would be worse than useless. A project schema is emitted beside the policies
|
|
28
|
+
* by the plugin's `buildRoots` hook (#1697, `src/schema-artifact.ts`), so the
|
|
29
|
+
* no-schema state is now the project with no schema of its own; there this
|
|
30
|
+
* check says so once, as an advisory, rather than passing silently and
|
|
31
|
+
* claiming a guarantee it did not check. CEDC010's parse gate still applies.
|
|
31
32
|
*/
|
|
32
33
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
33
34
|
import { parsedPolicySets } from "./cedar-helpers";
|
package/src/plugin.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { CedarTemplateGenerator, CedarTemplateParser } from "./import/adapter";
|
|
|
20
20
|
import type { ResourceMetadata } from "@intentius/chant/lexicon";
|
|
21
21
|
import { AVP_OWNERSHIP_KEYS } from "./avp/ownership";
|
|
22
22
|
import { AVP_AMBIENT_KINDS } from "./avp/ambient";
|
|
23
|
+
import { cedarCommandGroup } from "./commands";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* cedar lexicon plugin.
|
|
@@ -35,11 +36,17 @@ export const cedarPlugin: LexiconPlugin = {
|
|
|
35
36
|
async generate(options?: { verbose?: boolean }): Promise<void> {
|
|
36
37
|
// Cedar's codegen input is the project's own schema, so the `cedar` config
|
|
37
38
|
// namespace is read here rather than being decoration (#1650).
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
writeGeneratedFiles(
|
|
39
|
+
//
|
|
40
|
+
// The output goes into the *project* (#1696): `cedar.outDir`, else
|
|
41
|
+
// `src/generated/cedar` under the config's directory. Only when the project
|
|
42
|
+
// is this package itself does it land in `src/generated/`.
|
|
43
|
+
const { generate, resolveGeneratedDir, writeGeneratedFiles } = await import("./codegen/generate");
|
|
44
|
+
const { loadCedarProject } = await import("./config");
|
|
45
|
+
const { projectRoot, config } = await loadCedarProject(process.cwd());
|
|
46
|
+
const result = await generate({ ...options, projectRoot, config });
|
|
47
|
+
const outDir = resolveGeneratedDir({ projectRoot, config });
|
|
48
|
+
writeGeneratedFiles(result, outDir);
|
|
49
|
+
if (options?.verbose) console.error(`cedar: wrote generated classes to ${outDir}`);
|
|
43
50
|
},
|
|
44
51
|
|
|
45
52
|
async validate(options?: { verbose?: boolean }): Promise<void> {
|
|
@@ -51,11 +58,15 @@ export const cedarPlugin: LexiconPlugin = {
|
|
|
51
58
|
|
|
52
59
|
async coverage(options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
|
|
53
60
|
const { analyzeCedarCoverage } = await import("./coverage");
|
|
54
|
-
const {
|
|
55
|
-
const
|
|
61
|
+
const { resolveGeneratedDir } = await import("./codegen/generate");
|
|
62
|
+
const { loadCedarProject } = await import("./config");
|
|
63
|
+
const { projectRoot, config } = await loadCedarProject(process.cwd());
|
|
56
64
|
analyzeCedarCoverage({
|
|
57
65
|
projectRoot,
|
|
58
|
-
config
|
|
66
|
+
config,
|
|
67
|
+
// Coverage compares the project's schema with the project's generated
|
|
68
|
+
// artifacts, which live where generate put them (#1696).
|
|
69
|
+
generatedDir: resolveGeneratedDir({ projectRoot, config }),
|
|
59
70
|
verbose: options?.verbose,
|
|
60
71
|
minOverall: options?.minOverall,
|
|
61
72
|
});
|
|
@@ -76,6 +87,20 @@ export const cedarPlugin: LexiconPlugin = {
|
|
|
76
87
|
|
|
77
88
|
// ── Optional extensions ────────────────────────────────────
|
|
78
89
|
|
|
90
|
+
// #1697 — the project's `.cedarschema` joins the build as a `Cedar::Schema`
|
|
91
|
+
// entity, so it is emitted beside the policies and CEDE010 validates
|
|
92
|
+
// against it rather than reporting that it could not. Nothing is
|
|
93
|
+
// contributed when only the bundled default schema applies.
|
|
94
|
+
async buildRoots(ctx) {
|
|
95
|
+
const { schemaBuildRoot } = await import("./schema-artifact");
|
|
96
|
+
return schemaBuildRoot(ctx);
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// `chant cedar generate` and `chant cedar coverage` (#1696, via #1078).
|
|
100
|
+
commands() {
|
|
101
|
+
return cedarCommandGroup();
|
|
102
|
+
},
|
|
103
|
+
|
|
79
104
|
lintRules() {
|
|
80
105
|
return rules;
|
|
81
106
|
},
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project schema as a build artifact (#1697).
|
|
3
|
+
*
|
|
4
|
+
* End to end: a project schema goes in through the plugin's `buildRoots`
|
|
5
|
+
* hook, comes out of the serializer as `schema.cedarschema` beside the
|
|
6
|
+
* policies, and CEDE010 validates against it instead of saying it could not.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
10
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
14
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
15
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
16
|
+
import { cedarPlugin } from "./plugin";
|
|
17
|
+
import { cedarSerializer, type CedarPolicyProps } from "./serializer";
|
|
18
|
+
import { Policy, type PolicyProps } from "./generated/index";
|
|
19
|
+
import { CEDAR_SCHEMA_ENTITY_NAME, CEDAR_SCHEMA_FILENAME, Schema, schemaBuildRoot } from "./schema-artifact";
|
|
20
|
+
import { cede010 } from "./lint/post-synth/cede010";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The package's `PolicyProps` narrows scopes to the bundled sample schema's
|
|
24
|
+
* names. These tests use the `Shop` schema above, so they build props as the
|
|
25
|
+
* string-typed `CedarPolicyProps` and widen at the constructor.
|
|
26
|
+
*/
|
|
27
|
+
function shopPolicy(props: CedarPolicyProps): InstanceType<typeof Policy> {
|
|
28
|
+
return new Policy(props as unknown as PolicyProps);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SCHEMA = `namespace Shop {
|
|
32
|
+
entity Customer = { "email": String };
|
|
33
|
+
entity Order = { "owner": Customer };
|
|
34
|
+
action view appliesTo {
|
|
35
|
+
principal: [Customer],
|
|
36
|
+
resource: [Order],
|
|
37
|
+
context: { "mfa": Bool }
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
function serialize(entities: Map<string, Declarable>): SerializerResult {
|
|
43
|
+
const out = cedarSerializer.serialize(entities);
|
|
44
|
+
if (typeof out === "string") return { primary: out };
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function postSynthCtx(result: SerializerResult): PostSynthContext {
|
|
49
|
+
return {
|
|
50
|
+
outputs: new Map([["cedar", result]]),
|
|
51
|
+
entities: new Map(),
|
|
52
|
+
} as unknown as PostSynthContext;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("Schema declarable", () => {
|
|
56
|
+
it("is emitted as a .cedarschema beside the policies", () => {
|
|
57
|
+
const entities = new Map<string, Declarable>([
|
|
58
|
+
["authz", new Schema({ text: SCHEMA })],
|
|
59
|
+
[
|
|
60
|
+
"viewOwn",
|
|
61
|
+
shopPolicy({
|
|
62
|
+
effect: "permit",
|
|
63
|
+
principal: { is: "Shop::Customer" },
|
|
64
|
+
action: { in: ['Shop::Action::"view"'] },
|
|
65
|
+
resource: { is: "Shop::Order" },
|
|
66
|
+
when: ["resource.owner == principal"],
|
|
67
|
+
}),
|
|
68
|
+
],
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const result = serialize(entities);
|
|
72
|
+
expect(result.files?.[CEDAR_SCHEMA_FILENAME]).toBe(SCHEMA);
|
|
73
|
+
expect(result.primary).toContain("permit");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("honours an explicit filename and is emitted even with no policies", () => {
|
|
77
|
+
const entities = new Map<string, Declarable>([["authz", new Schema({ text: SCHEMA, filename: "shop.cedarschema" })]]);
|
|
78
|
+
const result = serialize(entities);
|
|
79
|
+
expect(Object.keys(result.files ?? {})).toEqual(["shop.cedarschema"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("warns rather than silently overwriting when two schemas share a filename", () => {
|
|
83
|
+
const entities = new Map<string, Declarable>([
|
|
84
|
+
["a", new Schema({ text: "namespace A { entity X; }\n" })],
|
|
85
|
+
["b", new Schema({ text: "namespace B { entity Y; }\n" })],
|
|
86
|
+
]);
|
|
87
|
+
const result = serialize(entities);
|
|
88
|
+
expect(result.files?.[CEDAR_SCHEMA_FILENAME]).toBe("namespace A { entity X; }\n");
|
|
89
|
+
expect(result.warnings?.join("\n")).toMatch(/"b" targets schema\.cedarschema/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("turns CEDE010 from an advisory into validation", () => {
|
|
93
|
+
const badPolicy = shopPolicy({
|
|
94
|
+
effect: "permit",
|
|
95
|
+
principal: { is: "Shop::Customer" },
|
|
96
|
+
action: { in: ['Shop::Action::"view"'] },
|
|
97
|
+
resource: { is: "Shop::Nope" },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const without = cede010.check(postSynthCtx(serialize(new Map([["p", badPolicy]]))));
|
|
101
|
+
expect(without.map((d) => d.severity)).toEqual(["info"]);
|
|
102
|
+
|
|
103
|
+
const withSchema = cede010.check(
|
|
104
|
+
postSynthCtx(
|
|
105
|
+
serialize(
|
|
106
|
+
new Map<string, Declarable>([
|
|
107
|
+
["p", badPolicy],
|
|
108
|
+
["authz", new Schema({ text: SCHEMA })],
|
|
109
|
+
]),
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
expect(withSchema.some((d) => d.severity === "error" && /Shop::Nope/.test(d.message))).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("schemaBuildRoot", () => {
|
|
118
|
+
let root: string;
|
|
119
|
+
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
root = mkdtempSync(join(tmpdir(), "cedar-schema-root-"));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
afterEach(() => {
|
|
125
|
+
rmSync(root, { recursive: true, force: true });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("contributes the project schema when the project has one", async () => {
|
|
129
|
+
writeFileSync(join(root, "authz.cedarschema"), SCHEMA);
|
|
130
|
+
const contribution = await schemaBuildRoot({
|
|
131
|
+
projectRoot: root,
|
|
132
|
+
config: { cedar: { schema: "authz.cedarschema" } },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const entity = contribution.entities.get(CEDAR_SCHEMA_ENTITY_NAME);
|
|
136
|
+
expect(entity).toBeDefined();
|
|
137
|
+
|
|
138
|
+
const result = serialize(contribution.entities);
|
|
139
|
+
expect(result.files?.["authz.cedarschema"]).toBe(SCHEMA);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("finds schema.cedarschema in the project root with no config at all", async () => {
|
|
143
|
+
writeFileSync(join(root, CEDAR_SCHEMA_FILENAME), SCHEMA);
|
|
144
|
+
const contribution = await schemaBuildRoot({ projectRoot: root, config: {} });
|
|
145
|
+
expect(contribution.entities.size).toBe(1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("contributes nothing when only the bundled default schema applies", async () => {
|
|
149
|
+
// Emitting the default would validate a project's policies against an
|
|
150
|
+
// entity model it never wrote; the CEDE010 advisory is the honest outcome.
|
|
151
|
+
const contribution = await schemaBuildRoot({ projectRoot: root, config: {} });
|
|
152
|
+
expect(contribution.entities.size).toBe(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("is wired into the plugin", async () => {
|
|
156
|
+
writeFileSync(join(root, CEDAR_SCHEMA_FILENAME), SCHEMA);
|
|
157
|
+
const contribution = await cedarPlugin.buildRoots!({ projectRoot: root, config: {} });
|
|
158
|
+
expect(contribution.entities.has(CEDAR_SCHEMA_ENTITY_NAME)).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project schema as a build artifact (#1697).
|
|
3
|
+
*
|
|
4
|
+
* CEDE010 can only validate a policy set against a schema the build emitted
|
|
5
|
+
* beside it, and until now nothing emitted one: the `.cedarschema` drove
|
|
6
|
+
* codegen and then stayed in the source tree, so every consumer build got the
|
|
7
|
+
* parse-only advisory. This module closes that gap from two directions.
|
|
8
|
+
*
|
|
9
|
+
* `Schema` is a declarable a project can author by hand when it wants a
|
|
10
|
+
* particular filename or a schema that is not the one in `chant.config.ts`.
|
|
11
|
+
* `schemaBuildRoot` is the automatic path: the plugin's `buildRoots` hook
|
|
12
|
+
* contributes one `Schema` entity per build, holding the resolved project
|
|
13
|
+
* schema, so a project with a `cedar.schema` gets `dist/schema.cedarschema`
|
|
14
|
+
* and full validation without declaring anything.
|
|
15
|
+
*
|
|
16
|
+
* The bundled default schema is never emitted. A project without its own
|
|
17
|
+
* schema has policies the default may well reject, and CEDE010 failing a
|
|
18
|
+
* build against a schema the project never wrote would be a worse surprise
|
|
19
|
+
* than the advisory it replaces.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
23
|
+
import { isPropertyDeclarable } from "@intentius/chant/declarable";
|
|
24
|
+
import type { BuildRootContext, BuildRootContribution } from "@intentius/chant/lexicon";
|
|
25
|
+
import { createResource } from "@intentius/chant/runtime";
|
|
26
|
+
import { getProps } from "./policy-text";
|
|
27
|
+
import { resolveSchemaPath } from "./spec/fetch";
|
|
28
|
+
import type { CedarGenerateConfig } from "./codegen/generate";
|
|
29
|
+
import { readFileSync } from "fs";
|
|
30
|
+
import { basename } from "path";
|
|
31
|
+
|
|
32
|
+
/** The entity type of a {@link Schema}. */
|
|
33
|
+
export const CEDAR_SCHEMA_TYPE = "Cedar::Schema";
|
|
34
|
+
|
|
35
|
+
/** The logical name the build-root contribution uses. */
|
|
36
|
+
export const CEDAR_SCHEMA_ENTITY_NAME = "cedarSchema";
|
|
37
|
+
|
|
38
|
+
/** Filename the schema is emitted under when `filename` is unset. */
|
|
39
|
+
export const CEDAR_SCHEMA_FILENAME = "schema.cedarschema";
|
|
40
|
+
|
|
41
|
+
export interface SchemaProps {
|
|
42
|
+
/** Human-readable Cedar schema text, emitted verbatim. */
|
|
43
|
+
text: string;
|
|
44
|
+
/** Defaults to {@link CEDAR_SCHEMA_FILENAME}. Must end in `.cedarschema`. */
|
|
45
|
+
filename?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A `.cedarschema` emitted with the policy set, so post-synth validation has something to validate against. */
|
|
49
|
+
export const Schema = createResource(CEDAR_SCHEMA_TYPE, "cedar", {}) as unknown as new (props: SchemaProps) => Declarable;
|
|
50
|
+
|
|
51
|
+
/** One schema file the serializer should write. */
|
|
52
|
+
export interface SchemaFile {
|
|
53
|
+
name: string;
|
|
54
|
+
filename: string;
|
|
55
|
+
text: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Every `Cedar::Schema` in the entity map, with its props read. */
|
|
59
|
+
export function schemaEntities(entities: Map<string, Declarable>): SchemaFile[] {
|
|
60
|
+
const files: SchemaFile[] = [];
|
|
61
|
+
for (const [name, entity] of entities) {
|
|
62
|
+
if (isPropertyDeclarable(entity)) continue;
|
|
63
|
+
if (entity.entityType !== CEDAR_SCHEMA_TYPE) continue;
|
|
64
|
+
const props = getProps(entity) as Partial<SchemaProps>;
|
|
65
|
+
if (typeof props.text !== "string") continue;
|
|
66
|
+
files.push({
|
|
67
|
+
name,
|
|
68
|
+
filename: typeof props.filename === "string" ? props.filename : CEDAR_SCHEMA_FILENAME,
|
|
69
|
+
text: props.text.endsWith("\n") ? props.text : props.text + "\n",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The `buildRoots` contribution: the project's resolved schema as one
|
|
77
|
+
* {@link Schema} entity, or nothing when only the bundled default applies.
|
|
78
|
+
*/
|
|
79
|
+
export async function schemaBuildRoot(ctx: BuildRootContext): Promise<BuildRootContribution> {
|
|
80
|
+
const config = (ctx.config as { cedar?: CedarGenerateConfig }).cedar ?? {};
|
|
81
|
+
const source = resolveSchemaPath({ projectRoot: ctx.projectRoot, config });
|
|
82
|
+
if (source.isDefault) return { entities: new Map() };
|
|
83
|
+
|
|
84
|
+
const text = readFileSync(source.path, "utf-8");
|
|
85
|
+
const entity = new Schema({ text, filename: basename(source.path) });
|
|
86
|
+
return { entities: new Map([[CEDAR_SCHEMA_ENTITY_NAME, entity]]) };
|
|
87
|
+
}
|
package/src/serializer.ts
CHANGED
|
@@ -34,6 +34,7 @@ import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
|
34
34
|
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
35
35
|
import { policyToJson, splitPolicySet, templateToJson, type PolicyJson } from "./spec/wasm";
|
|
36
36
|
import { serializeDogwood } from "./dogwood/serialize";
|
|
37
|
+
import { schemaEntities } from "./schema-artifact";
|
|
37
38
|
import {
|
|
38
39
|
conditionStrings,
|
|
39
40
|
getProps,
|
|
@@ -263,10 +264,25 @@ export const cedarSerializer: Serializer = {
|
|
|
263
264
|
// and a project holding only temporal policies still gets its files.
|
|
264
265
|
const dogwood = serializeDogwood(entities);
|
|
265
266
|
|
|
266
|
-
|
|
267
|
+
// The project schema, as a file beside the policies (#1697). This is what
|
|
268
|
+
// turns CEDE010's parse-only advisory into validation: `findSchema` in
|
|
269
|
+
// lint/post-synth/wasm-helpers.ts reads it straight back out of `files`.
|
|
270
|
+
const schemas = schemaEntities(entities);
|
|
271
|
+
|
|
272
|
+
if (policyText.length === 0 && !dogwood && schemas.length === 0) return "";
|
|
267
273
|
|
|
268
274
|
const files: Record<string, string> = {};
|
|
269
275
|
const warnings: string[] = [...(dogwood?.warnings ?? [])];
|
|
276
|
+
|
|
277
|
+
for (const schema of schemas) {
|
|
278
|
+
if (files[schema.filename] !== undefined) {
|
|
279
|
+
warnings.push(
|
|
280
|
+
`cedar: schema "${schema.name}" targets ${schema.filename}, which another schema already wrote; give one of them an explicit \`filename\``,
|
|
281
|
+
);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
files[schema.filename] = schema.text;
|
|
285
|
+
}
|
|
270
286
|
let primary = "";
|
|
271
287
|
|
|
272
288
|
if (policyText.length > 0) {
|
|
@@ -51,9 +51,15 @@ between "your entity types" and "somebody else's" is invisible.
|
|
|
51
51
|
### 2. Run generate
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
npx chant generate
|
|
54
|
+
npx chant cedar generate
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
The output is written into the project, at `src/generated/cedar/` (or
|
|
58
|
+
`cedar.outDir`), never into `node_modules`. Commit it or regenerate it in CI;
|
|
59
|
+
re-run it whenever the schema changes. Policies import from that directory,
|
|
60
|
+
not from `@intentius/chant-lexicon-cedar`, whose own `Policy` and classes
|
|
61
|
+
describe the package's bundled sample schema.
|
|
62
|
+
|
|
57
63
|
That produces, per declaration in the schema:
|
|
58
64
|
|
|
59
65
|
| Schema declaration | Generated |
|
|
@@ -68,7 +74,7 @@ compile error, not a validation error hours later.
|
|
|
68
74
|
### 3. Write policies
|
|
69
75
|
|
|
70
76
|
```typescript
|
|
71
|
-
import { Policy, ReadAction, WriteAction, type UserUid } from "
|
|
77
|
+
import { Policy, ReadAction, WriteAction, type UserUid } from "./generated/cedar";
|
|
72
78
|
|
|
73
79
|
const archivist: UserUid = 'App::User::"archivist"';
|
|
74
80
|
|
|
@@ -159,7 +165,7 @@ wider grant.
|
|
|
159
165
|
|
|
160
166
|
```bash
|
|
161
167
|
npx chant build # emits both artifacts, runs the CED rules
|
|
162
|
-
npx chant coverage
|
|
168
|
+
npx chant cedar coverage # is every schema declaration generated?
|
|
163
169
|
```
|
|
164
170
|
|
|
165
171
|
The MCP tool `cedar:coverage` answers the other question: which schema entity
|
package/src/spec/fetch.ts
CHANGED
|
@@ -32,11 +32,32 @@ export const DEFAULT_SCHEMA_FILENAME = "default-schema.cedarschema";
|
|
|
32
32
|
/** Map key used for the single schema entry, so `parseSchema` can tell them apart. */
|
|
33
33
|
export const DEFAULT_SCHEMA_KEY = "<bundled-default>";
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
let specDir: string | undefined;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Directory this module lives in, resolved on first use.
|
|
39
|
+
*
|
|
40
|
+
* Kept lazy so importing the module never touches `import.meta.url`; edge
|
|
41
|
+
* bundles that pull this file in through the lint barrel have no such URL and
|
|
42
|
+
* would crash at module init otherwise.
|
|
43
|
+
*/
|
|
44
|
+
function resolveSpecDir(): string | undefined {
|
|
45
|
+
if (specDir !== undefined) return specDir;
|
|
46
|
+
try {
|
|
47
|
+
specDir = dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
return specDir;
|
|
52
|
+
}
|
|
36
53
|
|
|
37
54
|
/** Absolute path to the schema bundled with this package. */
|
|
38
55
|
export function defaultSchemaPath(): string {
|
|
39
|
-
|
|
56
|
+
const dir = resolveSpecDir();
|
|
57
|
+
if (dir === undefined) {
|
|
58
|
+
throw new Error("cedar: the bundled default schema is not reachable from this runtime (no module URL).");
|
|
59
|
+
}
|
|
60
|
+
return join(dir, DEFAULT_SCHEMA_FILENAME);
|
|
40
61
|
}
|
|
41
62
|
|
|
42
63
|
export interface SchemaSource {
|