@intentius/chant-lexicon-cedar 0.44.8
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 +190 -0
- package/dist/avp/ambient.d.ts +54 -0
- package/dist/avp/ambient.d.ts.map +1 -0
- package/dist/avp/client.d.ts +127 -0
- package/dist/avp/client.d.ts.map +1 -0
- package/dist/avp/describe-resources.d.ts +42 -0
- package/dist/avp/describe-resources.d.ts.map +1 -0
- package/dist/avp/embed.d.ts +120 -0
- package/dist/avp/embed.d.ts.map +1 -0
- package/dist/avp/live-export.d.ts +94 -0
- package/dist/avp/live-export.d.ts.map +1 -0
- package/dist/avp/ownership.d.ts +97 -0
- package/dist/avp/ownership.d.ts.map +1 -0
- package/dist/avp/statement.d.ts +22 -0
- package/dist/avp/statement.d.ts.map +1 -0
- package/dist/avp/store.d.ts +82 -0
- package/dist/avp/store.d.ts.map +1 -0
- package/dist/avp/testdata/mock-transport.d.ts +55 -0
- package/dist/avp/testdata/mock-transport.d.ts.map +1 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts +26 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/emit.d.ts +85 -0
- package/dist/codegen/emit.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +3 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts +37 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/naming.d.ts +48 -0
- package/dist/codegen/naming.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +10 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/composites/deny-by-default-set.d.ts +58 -0
- package/dist/composites/deny-by-default-set.d.ts.map +1 -0
- package/dist/composites/index.d.ts +12 -0
- package/dist/composites/index.d.ts.map +1 -0
- package/dist/composites/owner-can-manage.d.ts +48 -0
- package/dist/composites/owner-can-manage.d.ts.map +1 -0
- package/dist/config.d.ts +102 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/coverage.d.ts +59 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/detect.d.ts +18 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/generated/index.d.ts +251 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/runtime.d.ts +2 -0
- package/dist/generated/runtime.d.ts.map +1 -0
- package/dist/import/adapter.d.ts +21 -0
- package/dist/import/adapter.d.ts.map +1 -0
- package/dist/import/clause-text.d.ts +38 -0
- package/dist/import/clause-text.d.ts.map +1 -0
- package/dist/import/generator.d.ts +53 -0
- package/dist/import/generator.d.ts.map +1 -0
- package/dist/import/parser.d.ts +57 -0
- package/dist/import/parser.d.ts.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/init-templates.d.ts +32 -0
- package/dist/init-templates.d.ts.map +1 -0
- package/dist/integrity.json +22 -0
- package/dist/lint/audit-catalog.d.ts +29 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/post-synth/cedar-helpers.d.ts +97 -0
- package/dist/lint/post-synth/cedar-helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc010.d.ts +17 -0
- package/dist/lint/post-synth/cedc010.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc011.d.ts +17 -0
- package/dist/lint/post-synth/cedc011.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc012.d.ts +21 -0
- package/dist/lint/post-synth/cedc012.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc013.d.ts +20 -0
- package/dist/lint/post-synth/cedc013.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc014.d.ts +19 -0
- package/dist/lint/post-synth/cedc014.d.ts.map +1 -0
- package/dist/lint/post-synth/cede010.d.ts +34 -0
- package/dist/lint/post-synth/cede010.d.ts.map +1 -0
- package/dist/lint/post-synth/cede011.d.ts +22 -0
- package/dist/lint/post-synth/cede011.d.ts.map +1 -0
- package/dist/lint/post-synth/ceds010.d.ts +24 -0
- package/dist/lint/post-synth/ceds010.d.ts.map +1 -0
- package/dist/lint/post-synth/ceds011.d.ts +19 -0
- package/dist/lint/post-synth/ceds011.d.ts.map +1 -0
- package/dist/lint/post-synth/ceds012.d.ts +17 -0
- package/dist/lint/post-synth/ceds012.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/post-synth/wasm-helpers.d.ts +108 -0
- package/dist/lint/post-synth/wasm-helpers.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +10 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/policy-shape.d.ts +31 -0
- package/dist/lint/rules/policy-shape.d.ts.map +1 -0
- package/dist/lsp/completions.d.ts +24 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/hover.d.ts +11 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/lsp/registry.d.ts +45 -0
- package/dist/lsp/registry.d.ts.map +1 -0
- package/dist/manifest.json +8 -0
- package/dist/mcp/index.d.ts +34 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/policy-coverage.d.ts +81 -0
- package/dist/mcp/policy-coverage.d.ts.map +1 -0
- package/dist/meta.json +699 -0
- package/dist/okf/index.md +37 -0
- package/dist/okf/rules/CEDC001.md +11 -0
- package/dist/okf/rules/CEDC010.md +11 -0
- package/dist/okf/rules/CEDC011.md +15 -0
- package/dist/okf/rules/CEDC012.md +11 -0
- package/dist/okf/rules/CEDC013.md +15 -0
- package/dist/okf/rules/CEDC014.md +15 -0
- package/dist/okf/rules/CEDE010.md +15 -0
- package/dist/okf/rules/CEDE011.md +15 -0
- package/dist/okf/rules/CEDS010.md +15 -0
- package/dist/okf/rules/CEDS011.md +11 -0
- package/dist/okf/rules/CEDS012.md +15 -0
- package/dist/okf/types/AdminAction.md +14 -0
- package/dist/okf/types/Application.md +14 -0
- package/dist/okf/types/ApproveAction.md +13 -0
- package/dist/okf/types/CommentAction.md +14 -0
- package/dist/okf/types/CreateAction.md +14 -0
- package/dist/okf/types/DeleteAction.md +14 -0
- package/dist/okf/types/Document.md +18 -0
- package/dist/okf/types/Folder.md +15 -0
- package/dist/okf/types/Group.md +14 -0
- package/dist/okf/types/ListAction.md +14 -0
- package/dist/okf/types/Policy.md +29 -0
- package/dist/okf/types/ReadAction.md +14 -0
- package/dist/okf/types/ServiceAccount.md +15 -0
- package/dist/okf/types/ShareAction.md +14 -0
- package/dist/okf/types/Team.md +14 -0
- package/dist/okf/types/User.md +18 -0
- package/dist/okf/types/WriteAction.md +14 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/rules/cedar-helpers.ts +209 -0
- package/dist/rules/cedc010.ts +80 -0
- package/dist/rules/cedc011.ts +47 -0
- package/dist/rules/cedc012.ts +64 -0
- package/dist/rules/cedc013.ts +68 -0
- package/dist/rules/cedc014.ts +73 -0
- package/dist/rules/cede010.ts +89 -0
- package/dist/rules/cede011.ts +58 -0
- package/dist/rules/ceds010.ts +57 -0
- package/dist/rules/ceds011.ts +42 -0
- package/dist/rules/ceds012.ts +50 -0
- package/dist/rules/policy-shape.ts +148 -0
- package/dist/rules/wasm-helpers.ts +315 -0
- package/dist/serializer.d.ts +138 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/spec/fetch.d.ts +71 -0
- package/dist/spec/fetch.d.ts.map +1 -0
- package/dist/spec/parse.d.ts +113 -0
- package/dist/spec/parse.d.ts.map +1 -0
- package/dist/spec/pin.d.ts +116 -0
- package/dist/spec/pin.d.ts.map +1 -0
- package/dist/spec/pinned-names.json +18 -0
- package/dist/spec/wasm.d.ts +143 -0
- package/dist/spec/wasm.d.ts.map +1 -0
- package/dist/types/index.d.ts +219 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate.d.ts +25 -0
- package/dist/validate.d.ts.map +1 -0
- package/package.json +75 -0
- package/src/avp/OWNERSHIP.md +125 -0
- package/src/avp/ambient.test.ts +113 -0
- package/src/avp/ambient.ts +124 -0
- package/src/avp/client.ts +310 -0
- package/src/avp/describe-resources.test.ts +316 -0
- package/src/avp/describe-resources.ts +215 -0
- package/src/avp/embed.test.ts +114 -0
- package/src/avp/embed.ts +190 -0
- package/src/avp/live-export.test.ts +232 -0
- package/src/avp/live-export.ts +185 -0
- package/src/avp/ownership.test.ts +101 -0
- package/src/avp/ownership.ts +170 -0
- package/src/avp/statement.ts +50 -0
- package/src/avp/store.ts +152 -0
- package/src/avp/testdata/mock-transport.ts +147 -0
- package/src/codegen/docs-cli.ts +7 -0
- package/src/codegen/docs.ts +873 -0
- package/src/codegen/emit.ts +496 -0
- package/src/codegen/generate-cli.ts +18 -0
- package/src/codegen/generate.test.ts +128 -0
- package/src/codegen/generate.ts +123 -0
- package/src/codegen/naming.ts +101 -0
- package/src/codegen/package.ts +52 -0
- package/src/composites/composites.test.ts +206 -0
- package/src/composites/deny-by-default-set.ts +98 -0
- package/src/composites/index.ts +13 -0
- package/src/composites/owner-can-manage.ts +80 -0
- package/src/config-namespace.test.ts +44 -0
- package/src/config.test.ts +82 -0
- package/src/config.ts +119 -0
- package/src/coverage.test.ts +61 -0
- package/src/coverage.ts +166 -0
- package/src/detect.test.ts +64 -0
- package/src/detect.ts +59 -0
- package/src/generated/index.d.ts +219 -0
- package/src/generated/index.ts +279 -0
- package/src/generated/lexicon-cedar.json +699 -0
- package/src/generated/runtime.ts +2 -0
- package/src/import/adapter.ts +63 -0
- package/src/import/clause-text.ts +178 -0
- package/src/import/generator.test.ts +133 -0
- package/src/import/generator.ts +219 -0
- package/src/import/parser.test.ts +265 -0
- package/src/import/parser.ts +352 -0
- package/src/import/roundtrip.test.ts +127 -0
- package/src/import/testdata/full.cedar +36 -0
- package/src/import/testdata/full.cedar.json +238 -0
- package/src/import/testdata/realistic.cedar +35 -0
- package/src/import/testdata/simple.cedar +6 -0
- package/src/index.ts +94 -0
- package/src/init-templates.test.ts +158 -0
- package/src/init-templates.ts +358 -0
- package/src/lint/audit-catalog.ts +130 -0
- package/src/lint/post-synth/cedar-helpers.ts +209 -0
- package/src/lint/post-synth/cedc010.ts +80 -0
- package/src/lint/post-synth/cedc011.ts +47 -0
- package/src/lint/post-synth/cedc012.ts +64 -0
- package/src/lint/post-synth/cedc013.ts +68 -0
- package/src/lint/post-synth/cedc014.ts +73 -0
- package/src/lint/post-synth/cede010.ts +89 -0
- package/src/lint/post-synth/cede011.ts +58 -0
- package/src/lint/post-synth/ceds010.ts +57 -0
- package/src/lint/post-synth/ceds011.ts +42 -0
- package/src/lint/post-synth/ceds012.ts +50 -0
- package/src/lint/post-synth/index.ts +25 -0
- package/src/lint/post-synth/post-synth.test.ts +474 -0
- package/src/lint/post-synth/wasm-helpers.ts +315 -0
- package/src/lint/rules/index.ts +12 -0
- package/src/lint/rules/policy-shape.test.ts +90 -0
- package/src/lint/rules/policy-shape.ts +148 -0
- package/src/lsp/completions.test.ts +111 -0
- package/src/lsp/completions.ts +55 -0
- package/src/lsp/hover.test.ts +82 -0
- package/src/lsp/hover.ts +75 -0
- package/src/lsp/registry.ts +74 -0
- package/src/mcp/index.ts +103 -0
- package/src/mcp/policy-coverage.test.ts +176 -0
- package/src/mcp/policy-coverage.ts +205 -0
- package/src/package-cli.ts +22 -0
- package/src/plugin.test.ts +115 -0
- package/src/plugin.ts +312 -0
- package/src/serializer.test.ts +502 -0
- package/src/serializer.ts +335 -0
- package/src/skills/chant-cedar-authoring.md +180 -0
- package/src/skills/chant-cedar-avp-embedding.md +125 -0
- package/src/skills/chant-cedar-meta-policy.md +119 -0
- package/src/spec/default-schema.cedarschema +108 -0
- package/src/spec/fetch.ts +107 -0
- package/src/spec/parse.test.ts +123 -0
- package/src/spec/parse.ts +253 -0
- package/src/spec/pin.test.ts +88 -0
- package/src/spec/pin.ts +243 -0
- package/src/spec/pinned-names.json +18 -0
- package/src/spec/wasm.ts +283 -0
- package/src/validate-cli.ts +8 -0
- package/src/validate.ts +85 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policies that are simply *there* (#1652, chant #1278).
|
|
3
|
+
*
|
|
4
|
+
* For most lexicons ambient discovery is housekeeping — an unattached security
|
|
5
|
+
* group, an orphaned volume. For an authorization store it is a security
|
|
6
|
+
* finding. A policy in the store that no chant entity declares is a grant
|
|
7
|
+
* nobody in the source tree can see: it was added in the console, or by a
|
|
8
|
+
* previous tool, or by a teammate's script, and it is being evaluated on every
|
|
9
|
+
* request. `describeResources` structurally cannot report it, because it
|
|
10
|
+
* resolves outward from what was declared and this is precisely what was not.
|
|
11
|
+
*
|
|
12
|
+
* The reader reports the policy and its statement. It does not decide that an
|
|
13
|
+
* ambient `permit` is dangerous — that is a conclusion, and putting conclusions
|
|
14
|
+
* in observations is the mistake chant #1271 undid. What it does carry is the
|
|
15
|
+
* `effect`, because "which ambient policies are permits" is the first question
|
|
16
|
+
* anyone asks and re-parsing the statement to answer it is work every consumer
|
|
17
|
+
* would repeat.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { ResourceMetadata } from "@intentius/chant/lexicon";
|
|
21
|
+
import { CEDAR_POLICY_TYPE } from "../serializer";
|
|
22
|
+
import { credentialsAvailable, type AvpClientOptions } from "./client";
|
|
23
|
+
import { ownershipFromDescription } from "./ownership";
|
|
24
|
+
import { loadLivePolicies, resolvePolicyStoreId } from "./store";
|
|
25
|
+
import { effectFromStatement } from "./statement";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The kinds this lexicon can enumerate beyond the declared estate.
|
|
29
|
+
*
|
|
30
|
+
* One, and it is the only kind cedar deploys. Declared separately from the
|
|
31
|
+
* reader so `chant search` can say `--ambient` is relevant to a policy query
|
|
32
|
+
* without paying for a scan to find out.
|
|
33
|
+
*/
|
|
34
|
+
export const AVP_AMBIENT_KINDS: readonly string[] = [CEDAR_POLICY_TYPE];
|
|
35
|
+
|
|
36
|
+
export interface ObserveAvpAmbientOptions {
|
|
37
|
+
environment: string;
|
|
38
|
+
/** Entity types the project declares — the bound on what to enumerate. */
|
|
39
|
+
kinds: string[];
|
|
40
|
+
/** Already-observed managed resources, to exclude. */
|
|
41
|
+
observed: Record<string, ResourceMetadata>;
|
|
42
|
+
/** Cedar ids the project declares, so a declared-but-unobserved policy is not called ambient. */
|
|
43
|
+
declaredPolicyIds?: Iterable<string>;
|
|
44
|
+
policyStoreId?: string;
|
|
45
|
+
entities?: Map<string, { entityType: string; props: Record<string, unknown> }>;
|
|
46
|
+
client?: AvpClientOptions;
|
|
47
|
+
env?: Record<string, string | undefined>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Policies in the store that nothing declares and nothing already observed.
|
|
52
|
+
*
|
|
53
|
+
* Best-effort by contract: ambient discovery is additive, and a failure here
|
|
54
|
+
* must not sink a managed observation that already succeeded, so the whole scan
|
|
55
|
+
* degrades to `{}` rather than throwing. The managed answer is complete without
|
|
56
|
+
* any of this.
|
|
57
|
+
*/
|
|
58
|
+
export async function observeAvpAmbient(
|
|
59
|
+
options: ObserveAvpAmbientOptions,
|
|
60
|
+
): Promise<Record<string, ResourceMetadata>> {
|
|
61
|
+
if (!options.kinds.includes(CEDAR_POLICY_TYPE)) return {};
|
|
62
|
+
|
|
63
|
+
const env = options.env ?? process.env;
|
|
64
|
+
const policyStoreId = resolvePolicyStoreId({
|
|
65
|
+
environment: options.environment,
|
|
66
|
+
...(options.policyStoreId ? { policyStoreId: options.policyStoreId } : {}),
|
|
67
|
+
...(options.entities ? { entities: options.entities } : {}),
|
|
68
|
+
env,
|
|
69
|
+
});
|
|
70
|
+
if (!policyStoreId || !credentialsAvailable(env)) return {};
|
|
71
|
+
|
|
72
|
+
let policies;
|
|
73
|
+
try {
|
|
74
|
+
policies = await loadLivePolicies({
|
|
75
|
+
policyStoreId,
|
|
76
|
+
...(options.client ? { client: options.client } : {}),
|
|
77
|
+
withStatements: true,
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Two exclusions, because "managed" has two spellings here: the AVP policy id
|
|
84
|
+
// the observation recorded as a physicalId, and the Cedar id the source
|
|
85
|
+
// declares. A declared policy whose read failed has no physicalId, and
|
|
86
|
+
// calling it ambient would turn a hole into a security finding.
|
|
87
|
+
const observedIds = new Set(
|
|
88
|
+
Object.values(options.observed)
|
|
89
|
+
.map((meta) => meta.physicalId)
|
|
90
|
+
.filter((id): id is string => typeof id === "string" && id.length > 0),
|
|
91
|
+
);
|
|
92
|
+
const declaredIds = new Set(options.declaredPolicyIds ?? []);
|
|
93
|
+
|
|
94
|
+
const ambient: Record<string, ResourceMetadata> = {};
|
|
95
|
+
for (const policy of policies) {
|
|
96
|
+
if (observedIds.has(policy.policyId)) continue;
|
|
97
|
+
if (policy.cedarPolicyId !== undefined && declaredIds.has(policy.cedarPolicyId)) continue;
|
|
98
|
+
|
|
99
|
+
const effect = policy.statement ? effectFromStatement(policy.statement) : undefined;
|
|
100
|
+
|
|
101
|
+
ambient[`policy/${policy.policyId}`] = {
|
|
102
|
+
type: CEDAR_POLICY_TYPE,
|
|
103
|
+
status: policy.policyType || "STATIC",
|
|
104
|
+
physicalId: policy.policyId,
|
|
105
|
+
...(policy.lastUpdatedDate ? { lastUpdated: policy.lastUpdatedDate } : {}),
|
|
106
|
+
ownership: ownershipFromDescription(policy.description),
|
|
107
|
+
ambient: true,
|
|
108
|
+
attributes: {
|
|
109
|
+
policyStoreId: policy.policyStoreId,
|
|
110
|
+
policyId: policy.policyId,
|
|
111
|
+
policyType: policy.policyType,
|
|
112
|
+
...(effect ? { effect } : {}),
|
|
113
|
+
...(policy.cedarPolicyId ? { cedarPolicyId: policy.cedarPolicyId } : {}),
|
|
114
|
+
...(policy.authoredDescription ? { description: policy.authoredDescription } : {}),
|
|
115
|
+
// The statement itself, so the consumer can judge the grant rather than
|
|
116
|
+
// trusting a summary this reader invented.
|
|
117
|
+
...(policy.statement ? { statement: policy.statement } : {}),
|
|
118
|
+
...(policy.createdDate ? { createdDate: policy.createdDate } : {}),
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return ambient;
|
|
124
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AVP read transport (#1652).
|
|
3
|
+
*
|
|
4
|
+
* Amazon Verified Permissions speaks AWS JSON 1.0 — a POST of a JSON body with
|
|
5
|
+
* `x-amz-target: VerifiedPermissions.<Operation>` — so this is the same shape
|
|
6
|
+
* as `lexicons/aws/src/api/read-client.ts`'s Cloud Control half, pointed at a
|
|
7
|
+
* different service. That file is the precedent for the decision this module
|
|
8
|
+
* embodies: **no AWS SDK dependency**. The repo has none anywhere (the aws
|
|
9
|
+
* lexicon's runtime deps are `fflate` and `js-yaml`), and adding
|
|
10
|
+
* `@aws-sdk/client-verifiedpermissions` — 30-odd transitive packages — to the
|
|
11
|
+
* cedar lexicon so that one reader can issue three calls would put an AWS SDK
|
|
12
|
+
* in the dependency tree of a lexicon whose whole premise is that Cedar is
|
|
13
|
+
* vendor-neutral.
|
|
14
|
+
*
|
|
15
|
+
* ## Signing
|
|
16
|
+
*
|
|
17
|
+
* Requests are unsigned, exactly like the aws lexicon's read client. Real AWS
|
|
18
|
+
* rejects them; an emulator with an endpoint override does not. This is
|
|
19
|
+
* therefore an emulator-and-test transport today, and the honest consequence is
|
|
20
|
+
* encoded in {@link credentialsAvailable}: with no credentials and no endpoint
|
|
21
|
+
* override, the observation reports every entity NOT-OBSERVED with
|
|
22
|
+
* `no-credentials` rather than issuing a request that will fail. The signed
|
|
23
|
+
* path lands when SigV4 lands in `lexicons/aws/src/api/read-client.ts`, which
|
|
24
|
+
* is where it belongs — one implementation, not two.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const DEFAULT_REGION = "us-east-1";
|
|
28
|
+
const TARGET_PREFIX = "VerifiedPermissions";
|
|
29
|
+
const SERVICE = "verifiedpermissions";
|
|
30
|
+
|
|
31
|
+
/** Injectable HTTP, mirroring `AwsReadHttp` in the aws lexicon so tests avoid the network. */
|
|
32
|
+
export type AvpHttp = (
|
|
33
|
+
url: string,
|
|
34
|
+
init: { headers: Record<string, string>; body: string },
|
|
35
|
+
signal?: AbortSignal,
|
|
36
|
+
) => Promise<{ status: number; text: string }>;
|
|
37
|
+
|
|
38
|
+
const defaultHttp: AvpHttp = async (url, init, signal) => {
|
|
39
|
+
const res = await fetch(url, { method: "POST", headers: init.headers, body: init.body, signal });
|
|
40
|
+
return { status: res.status, text: await res.text() };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** A failed AVP read, carrying enough to classify it without matching on prose. */
|
|
44
|
+
export class AvpReadError extends Error {
|
|
45
|
+
constructor(
|
|
46
|
+
message: string,
|
|
47
|
+
readonly status: number,
|
|
48
|
+
/** The service's own error code (`ResourceNotFoundException`, `AccessDeniedException`, …). */
|
|
49
|
+
readonly code?: string,
|
|
50
|
+
) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "AvpReadError";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AvpClientOptions {
|
|
57
|
+
/** Endpoint override (an emulator, or a VPC endpoint). Omit for real AWS hosts. */
|
|
58
|
+
endpoint?: string;
|
|
59
|
+
/** Region for the real-AWS host and the credential scope. */
|
|
60
|
+
region?: string;
|
|
61
|
+
http?: AvpHttp;
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
/** Environment to read credentials from. Defaults to `process.env`; injectable for tests. */
|
|
64
|
+
env?: Record<string, string | undefined>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Service host for AVP, honouring an endpoint override. */
|
|
68
|
+
export function avpUrl(endpoint?: string, region = DEFAULT_REGION): string {
|
|
69
|
+
return `${(endpoint ?? `https://${SERVICE}.${region}.amazonaws.com`).replace(/\/$/, "")}/`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether this process has anything to authenticate with.
|
|
74
|
+
*
|
|
75
|
+
* An endpoint override counts: an emulator is reached without credentials, and
|
|
76
|
+
* refusing to look at one because `AWS_ACCESS_KEY_ID` is unset would make the
|
|
77
|
+
* local lanes unobservable. Everything else is the ordinary AWS credential
|
|
78
|
+
* surface, minus the instance-metadata path this transport cannot use anyway.
|
|
79
|
+
*/
|
|
80
|
+
export function credentialsAvailable(env: Record<string, string | undefined> = process.env): boolean {
|
|
81
|
+
return Boolean(
|
|
82
|
+
env.AWS_ENDPOINT_URL ||
|
|
83
|
+
env.AWS_ACCESS_KEY_ID ||
|
|
84
|
+
env.AWS_PROFILE ||
|
|
85
|
+
env.AWS_SESSION_TOKEN ||
|
|
86
|
+
env.AWS_ROLE_ARN ||
|
|
87
|
+
env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI ||
|
|
88
|
+
env.AWS_WEB_IDENTITY_TOKEN_FILE,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The credential scope header.
|
|
94
|
+
*
|
|
95
|
+
* This is NOT SigV4 — the signature is a placeholder, exactly as in the aws
|
|
96
|
+
* lexicon's read client, and it must not be mistaken for a signed read path. It
|
|
97
|
+
* carries the region so that an endpoint override (one host for every region)
|
|
98
|
+
* still reaches the right one.
|
|
99
|
+
*/
|
|
100
|
+
function regionScope(region: string | undefined, env: Record<string, string | undefined>): Record<string, string> {
|
|
101
|
+
if (!region) return {};
|
|
102
|
+
const day = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
103
|
+
const key = env.AWS_ACCESS_KEY_ID || "chant";
|
|
104
|
+
return {
|
|
105
|
+
authorization:
|
|
106
|
+
`AWS4-HMAC-SHA256 Credential=${key}/${day}/${region}/${SERVICE}/aws4_request, ` +
|
|
107
|
+
"SignedHeaders=host, Signature=unsigned",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
112
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** One AVP call. Throws {@link AvpReadError} carrying the service's `__type`. */
|
|
116
|
+
export async function avpCall(
|
|
117
|
+
operation: string,
|
|
118
|
+
payload: Record<string, unknown>,
|
|
119
|
+
options: AvpClientOptions = {},
|
|
120
|
+
): Promise<Record<string, unknown>> {
|
|
121
|
+
const env = options.env ?? process.env;
|
|
122
|
+
const http = options.http ?? defaultHttp;
|
|
123
|
+
const url = avpUrl(options.endpoint ?? env.AWS_ENDPOINT_URL, options.region);
|
|
124
|
+
const res = await http(
|
|
125
|
+
url,
|
|
126
|
+
{
|
|
127
|
+
headers: {
|
|
128
|
+
"content-type": "application/x-amz-json-1.0",
|
|
129
|
+
"x-amz-target": `${TARGET_PREFIX}.${operation}`,
|
|
130
|
+
...regionScope(options.region, env),
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify(payload),
|
|
133
|
+
},
|
|
134
|
+
options.signal,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
let parsed: unknown;
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(res.text);
|
|
140
|
+
} catch {
|
|
141
|
+
throw new AvpReadError(`unparseable ${operation} response`, res.status);
|
|
142
|
+
}
|
|
143
|
+
const body = isRecord(parsed) ? parsed : {};
|
|
144
|
+
const type = typeof body.__type === "string" ? body.__type.split("#").pop() : undefined;
|
|
145
|
+
if (type || res.status >= 400) {
|
|
146
|
+
const message = typeof body.message === "string" ? body.message : `${operation} failed with HTTP ${res.status}`;
|
|
147
|
+
throw new AvpReadError(message, res.status, type);
|
|
148
|
+
}
|
|
149
|
+
return body;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ── Shapes ───────────────────────────────────────────────────────────────── */
|
|
153
|
+
|
|
154
|
+
/** One policy, as `ListPolicies` reports it — no statement, only the description. */
|
|
155
|
+
export interface AvpPolicySummary {
|
|
156
|
+
policyStoreId: string;
|
|
157
|
+
policyId: string;
|
|
158
|
+
policyType: string;
|
|
159
|
+
/** `definition.static.description` — the channel chant's marker rides (see ./ownership.ts). */
|
|
160
|
+
description?: string;
|
|
161
|
+
createdDate?: string;
|
|
162
|
+
lastUpdatedDate?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** One policy, as `GetPolicy` reports it — the statement included. */
|
|
166
|
+
export interface AvpPolicyDetail extends AvpPolicySummary {
|
|
167
|
+
/** The Cedar policy text. */
|
|
168
|
+
statement: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function summaryOf(raw: Record<string, unknown>, fallbackStore: string): AvpPolicySummary {
|
|
172
|
+
const definition = isRecord(raw.definition) ? raw.definition : {};
|
|
173
|
+
const staticDef = isRecord(definition.static) ? definition.static : {};
|
|
174
|
+
const description = typeof staticDef.description === "string" ? staticDef.description : undefined;
|
|
175
|
+
return {
|
|
176
|
+
policyStoreId: typeof raw.policyStoreId === "string" ? raw.policyStoreId : fallbackStore,
|
|
177
|
+
policyId: typeof raw.policyId === "string" ? raw.policyId : "",
|
|
178
|
+
policyType: typeof raw.policyType === "string" ? raw.policyType : "STATIC",
|
|
179
|
+
...(description !== undefined ? { description } : {}),
|
|
180
|
+
...(typeof raw.createdDate === "string" ? { createdDate: raw.createdDate } : {}),
|
|
181
|
+
...(typeof raw.lastUpdatedDate === "string" ? { lastUpdatedDate: raw.lastUpdatedDate } : {}),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** `ListPolicies` for one store, paginated to exhaustion. */
|
|
186
|
+
export async function listPolicies(
|
|
187
|
+
policyStoreId: string,
|
|
188
|
+
options: AvpClientOptions = {},
|
|
189
|
+
): Promise<AvpPolicySummary[]> {
|
|
190
|
+
const out: AvpPolicySummary[] = [];
|
|
191
|
+
let nextToken: string | undefined;
|
|
192
|
+
do {
|
|
193
|
+
const body = await avpCall(
|
|
194
|
+
"ListPolicies",
|
|
195
|
+
{ policyStoreId, ...(nextToken ? { nextToken } : {}) },
|
|
196
|
+
options,
|
|
197
|
+
);
|
|
198
|
+
const rows = Array.isArray(body.policies) ? body.policies : [];
|
|
199
|
+
for (const row of rows) {
|
|
200
|
+
if (isRecord(row)) out.push(summaryOf(row, policyStoreId));
|
|
201
|
+
}
|
|
202
|
+
nextToken = typeof body.nextToken === "string" && body.nextToken.length > 0 ? body.nextToken : undefined;
|
|
203
|
+
} while (nextToken);
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* `GetPolicy` — the full record for one policy, statement included.
|
|
209
|
+
*
|
|
210
|
+
* A template-linked policy has no `definition.static`, so its statement comes
|
|
211
|
+
* back empty; callers treat an empty statement as "not a static policy this
|
|
212
|
+
* lexicon authored" rather than as a parse failure.
|
|
213
|
+
*/
|
|
214
|
+
export async function getPolicy(
|
|
215
|
+
policyStoreId: string,
|
|
216
|
+
policyId: string,
|
|
217
|
+
options: AvpClientOptions = {},
|
|
218
|
+
): Promise<AvpPolicyDetail> {
|
|
219
|
+
const body = await avpCall("GetPolicy", { policyStoreId, policyId }, options);
|
|
220
|
+
const definition = isRecord(body.definition) ? body.definition : {};
|
|
221
|
+
const staticDef = isRecord(definition.static) ? definition.static : {};
|
|
222
|
+
return {
|
|
223
|
+
...summaryOf(body, policyStoreId),
|
|
224
|
+
policyId: typeof body.policyId === "string" ? body.policyId : policyId,
|
|
225
|
+
statement: typeof staticDef.statement === "string" ? staticDef.statement : "",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** One policy store, as `GetPolicyStore` reports it. */
|
|
230
|
+
export interface AvpPolicyStore {
|
|
231
|
+
policyStoreId: string;
|
|
232
|
+
arn?: string;
|
|
233
|
+
description?: string;
|
|
234
|
+
createdDate?: string;
|
|
235
|
+
lastUpdatedDate?: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** `GetPolicyStore` — used to resolve the store ARN the tag read needs. */
|
|
239
|
+
export async function getPolicyStore(
|
|
240
|
+
policyStoreId: string,
|
|
241
|
+
options: AvpClientOptions = {},
|
|
242
|
+
): Promise<AvpPolicyStore> {
|
|
243
|
+
const body = await avpCall("GetPolicyStore", { policyStoreId }, options);
|
|
244
|
+
return {
|
|
245
|
+
policyStoreId: typeof body.policyStoreId === "string" ? body.policyStoreId : policyStoreId,
|
|
246
|
+
...(typeof body.arn === "string" ? { arn: body.arn } : {}),
|
|
247
|
+
...(typeof body.description === "string" ? { description: body.description } : {}),
|
|
248
|
+
...(typeof body.createdDate === "string" ? { createdDate: body.createdDate } : {}),
|
|
249
|
+
...(typeof body.lastUpdatedDate === "string" ? { lastUpdatedDate: body.lastUpdatedDate } : {}),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** `ListTagsForResource` — the store-level half of the ownership channel. */
|
|
254
|
+
export async function listStoreTags(
|
|
255
|
+
resourceArn: string,
|
|
256
|
+
options: AvpClientOptions = {},
|
|
257
|
+
): Promise<Record<string, string>> {
|
|
258
|
+
const body = await avpCall("ListTagsForResource", { resourceArn }, options);
|
|
259
|
+
const tags = isRecord(body.tags) ? body.tags : {};
|
|
260
|
+
const out: Record<string, string> = {};
|
|
261
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
262
|
+
if (typeof value === "string") out[key] = value;
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* ── Classification ───────────────────────────────────────────────────────── */
|
|
268
|
+
|
|
269
|
+
/** True when the failure means the store itself is not there. */
|
|
270
|
+
export function storeDoesNotExist(err: unknown): boolean {
|
|
271
|
+
if (!(err instanceof AvpReadError)) return false;
|
|
272
|
+
return err.code === "ResourceNotFoundException" || /not\s*found|does not exist/i.test(err.message);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const CREDENTIAL_PATTERN =
|
|
276
|
+
/credential|token|expired|AccessDenied|not authorized|Unauthorized|UnrecognizedClient|InvalidSignature/i;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Map a transport failure onto the closed {@link UnobservedReason} set.
|
|
280
|
+
*
|
|
281
|
+
* Anything that is not demonstrably a credential problem is `read-failed`,
|
|
282
|
+
* because guessing wider would let a throttle or a DNS failure masquerade as a
|
|
283
|
+
* verdict about the estate.
|
|
284
|
+
*/
|
|
285
|
+
export function classifyAvpFailure(err: unknown): { reason: "no-credentials" | "read-failed"; detail: string } {
|
|
286
|
+
const code = err instanceof AvpReadError && err.code ? `${err.code}: ` : "";
|
|
287
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
288
|
+
const detail = `${code}${message}`;
|
|
289
|
+
return {
|
|
290
|
+
reason: CREDENTIAL_PATTERN.test(detail) ? "no-credentials" : "read-failed",
|
|
291
|
+
detail,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The address a read was issued against, for the observation's `queried` map
|
|
297
|
+
* (chant #1620).
|
|
298
|
+
*
|
|
299
|
+
* An absence is only as trustworthy as the address behind it, and an AVP read
|
|
300
|
+
* has three ways to be pointed at the wrong place: the region, the store id,
|
|
301
|
+
* and the `@id` annotation the policy is matched on. All three are in here.
|
|
302
|
+
*/
|
|
303
|
+
export function policyAddress(policyStoreId: string, cedarPolicyId: string, region?: string): string {
|
|
304
|
+
return `avp://${region ?? DEFAULT_REGION}/policy-store/${policyStoreId}/policy?@id=${encodeURIComponent(cedarPolicyId)}`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The address of the store-wide enumeration itself. */
|
|
308
|
+
export function storeAddress(policyStoreId: string, region?: string): string {
|
|
309
|
+
return `avp://${region ?? DEFAULT_REGION}/policy-store/${policyStoreId}/policies`;
|
|
310
|
+
}
|