@aventara/access 0.0.0-stage → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +877 -0
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +2536 -2
- package/dist/access/access-options.d.ts +74 -0
- package/dist/access/access-options.js +5 -0
- package/dist/access/access.error.d.ts +35 -0
- package/dist/access/access.error.js +11 -0
- package/dist/access/access.factory.d.ts +275 -0
- package/dist/access/access.factory.js +440 -0
- package/dist/access/better-auth.hardening.d.ts +120 -0
- package/dist/access/better-auth.hardening.js +420 -0
- package/dist/access/better-auth.loader.d.ts +67 -0
- package/dist/access/better-auth.loader.js +65 -0
- package/dist/access/better-auth.range.d.ts +9 -0
- package/dist/access/better-auth.range.js +28 -0
- package/dist/access/better-auth.require.d.ts +90 -0
- package/dist/access/better-auth.require.js +6 -0
- package/dist/access/better-auth.require.node.d.ts +7 -0
- package/dist/access/better-auth.require.node.js +52 -0
- package/dist/access/claims.mapper.d.ts +79 -0
- package/dist/access/claims.mapper.js +152 -0
- package/dist/access/key-set.cache.d.ts +10 -0
- package/dist/access/key-set.cache.js +63 -0
- package/dist/access/policies-block.checker.d.ts +64 -0
- package/dist/access/policies-block.checker.js +138 -0
- package/dist/access/principal.subject.d.ts +22 -0
- package/dist/access/principal.subject.js +89 -0
- package/dist/access/profile.policy.d.ts +74 -0
- package/dist/access/profile.policy.js +110 -0
- package/dist/access/request.state.d.ts +89 -0
- package/dist/access/request.state.js +266 -0
- package/dist/access/session.hooks.d.ts +114 -0
- package/dist/access/session.hooks.js +258 -0
- package/dist/access/token.verifier.d.ts +35 -0
- package/dist/access/token.verifier.js +69 -0
- package/dist/access-schema.d.ts +48 -0
- package/dist/access-schema.js +5 -0
- package/dist/access-warning.emitter.d.ts +4 -0
- package/dist/access-warning.emitter.js +24 -0
- package/dist/access.builder.d.ts +340 -0
- package/dist/access.builder.js +75 -0
- package/dist/access.engine.d.ts +155 -0
- package/dist/access.engine.js +477 -0
- package/dist/audit/audit-event.d.ts +135 -0
- package/dist/audit/audit-event.js +8 -0
- package/dist/audit/audit.log.d.ts +122 -0
- package/dist/audit/audit.log.js +345 -0
- package/dist/audit/audit.schema.d.ts +143 -0
- package/dist/audit/audit.schema.js +50 -0
- package/dist/auth/index.d.ts +12 -0
- package/dist/auth/index.js +2 -0
- package/dist/aventara-where.translator.d.ts +19 -0
- package/dist/aventara-where.translator.js +87 -0
- package/dist/beacon/beacon.check.d.ts +59 -0
- package/dist/beacon/beacon.check.js +128 -0
- package/dist/beacon/beacon.schema.d.ts +41 -0
- package/dist/beacon/beacon.schema.js +48 -0
- package/dist/beacon/beacon.store.d.ts +29 -0
- package/dist/beacon/beacon.store.js +78 -0
- package/dist/better-auth/access.client-plugin.d.ts +75 -0
- package/dist/better-auth/access.client-plugin.js +133 -0
- package/dist/better-auth/access.plugin.d.ts +164 -0
- package/dist/better-auth/access.plugin.js +348 -0
- package/dist/better-auth/access.provider.d.ts +80 -0
- package/dist/better-auth/access.provider.js +100 -0
- package/dist/better-auth/index.d.ts +7 -0
- package/dist/better-auth/index.js +4 -0
- package/dist/better-auth/organization.bridge.d.ts +66 -0
- package/dist/better-auth/organization.bridge.js +144 -0
- package/dist/canonical-json.writer.d.ts +12 -0
- package/dist/canonical-json.writer.js +30 -0
- package/dist/cli/avaccess.bin.d.ts +2 -0
- package/dist/cli/avaccess.bin.js +26 -0
- package/dist/cli/avaccess.program.d.ts +15 -0
- package/dist/cli/avaccess.program.js +166 -0
- package/dist/cli/beacon-pins.file.d.ts +14 -0
- package/dist/cli/beacon-pins.file.js +62 -0
- package/dist/cli/beacon.loader.d.ts +20 -0
- package/dist/cli/beacon.loader.js +71 -0
- package/dist/cli/cli.context.d.ts +49 -0
- package/dist/cli/cli.context.js +66 -0
- package/dist/cli/diff.command.d.ts +2 -0
- package/dist/cli/diff.command.js +66 -0
- package/dist/cli/generate.command.d.ts +2 -0
- package/dist/cli/generate.command.js +82 -0
- package/dist/cli/keys.command.d.ts +2 -0
- package/dist/cli/keys.command.js +96 -0
- package/dist/cli/lock.file.d.ts +31 -0
- package/dist/cli/lock.file.js +181 -0
- package/dist/cli/module.loader.d.ts +13 -0
- package/dist/cli/module.loader.js +63 -0
- package/dist/cli/policies.diff.d.ts +16 -0
- package/dist/cli/policies.diff.js +101 -0
- package/dist/cli/pull.command.d.ts +2 -0
- package/dist/cli/pull.command.js +234 -0
- package/dist/cli/push.command.d.ts +2 -0
- package/dist/cli/push.command.js +59 -0
- package/dist/cli/registry.client.d.ts +63 -0
- package/dist/cli/registry.client.js +287 -0
- package/dist/cli/schema.command.d.ts +2 -0
- package/dist/cli/schema.command.js +60 -0
- package/dist/cli/serve.command.d.ts +6 -0
- package/dist/cli/serve.command.js +200 -0
- package/dist/cli/status.command.d.ts +2 -0
- package/dist/cli/status.command.js +31 -0
- package/dist/cli/translator.flag.d.ts +6 -0
- package/dist/cli/translator.flag.js +24 -0
- package/dist/cli/verify.command.d.ts +2 -0
- package/dist/cli/verify.command.js +40 -0
- package/dist/client/access-client.factory.d.ts +92 -0
- package/dist/client/access-client.factory.js +269 -0
- package/dist/client/access-verbs.schema.d.ts +66 -0
- package/dist/client/access-verbs.schema.js +159 -0
- package/dist/client/verb-rate.limiter.d.ts +116 -0
- package/dist/client/verb-rate.limiter.js +358 -0
- package/dist/foreign/foreign-facts.d.ts +30 -0
- package/dist/foreign/foreign-facts.js +75 -0
- package/dist/foreign/foreign.merger.d.ts +10 -0
- package/dist/foreign/foreign.merger.js +52 -0
- package/dist/foreign-references.check.d.ts +6 -0
- package/dist/foreign-references.check.js +131 -0
- package/dist/generator/client.emitter.d.ts +28 -0
- package/dist/generator/client.emitter.js +411 -0
- package/dist/generator/copied-modules.reader.d.ts +14 -0
- package/dist/generator/copied-modules.reader.js +80 -0
- package/dist/generator/namespace.facts.d.ts +28 -0
- package/dist/generator/namespace.facts.js +32 -0
- package/dist/generator/orm.translator.d.ts +48 -0
- package/dist/generator/orm.translator.js +29 -0
- package/dist/generator/prisma7/prisma-schema.reader.d.ts +9 -0
- package/dist/generator/prisma7/prisma-schema.reader.js +394 -0
- package/dist/generator/prisma7/prisma-where.translator.d.ts +73 -0
- package/dist/generator/prisma7/prisma-where.translator.js +138 -0
- package/dist/generator/prisma7/prisma7.translator.d.ts +2 -0
- package/dist/generator/prisma7/prisma7.translator.js +87 -0
- package/dist/generator/schema.emitter.d.ts +14 -0
- package/dist/generator/schema.emitter.js +23 -0
- package/dist/generator/tsconfig.reader.d.ts +44 -0
- package/dist/generator/tsconfig.reader.js +110 -0
- package/dist/generator/typescript-literal.writer.d.ts +5 -0
- package/dist/generator/typescript-literal.writer.js +42 -0
- package/dist/guardrails/guardrails-contract.d.ts +45 -0
- package/dist/guardrails/guardrails-contract.js +12 -0
- package/dist/guardrails/guardrails.builder.d.ts +48 -0
- package/dist/guardrails/guardrails.builder.js +32 -0
- package/dist/guardrails/guardrails.compiler.d.ts +10 -0
- package/dist/guardrails/guardrails.compiler.js +254 -0
- package/dist/guardrails/guardrails.composer.d.ts +7 -0
- package/dist/guardrails/guardrails.composer.js +95 -0
- package/dist/guardrails/guardrails.hash.d.ts +10 -0
- package/dist/guardrails/guardrails.hash.js +38 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +6 -0
- package/dist/kiosk/kiosk-verbs.schema.d.ts +19 -0
- package/dist/kiosk/kiosk-verbs.schema.js +61 -0
- package/dist/kiosk/kiosk.plugin.d.ts +217 -0
- package/dist/kiosk/kiosk.plugin.js +877 -0
- package/dist/kiosk/kiosk.schema.d.ts +176 -0
- package/dist/kiosk/kiosk.schema.js +101 -0
- package/dist/kiosk/kiosk.store.d.ts +171 -0
- package/dist/kiosk/kiosk.store.js +478 -0
- package/dist/kiosk/lan-range.policy.d.ts +31 -0
- package/dist/kiosk/lan-range.policy.js +113 -0
- package/dist/kiosk/pin.policy.d.ts +9 -0
- package/dist/kiosk/pin.policy.js +143 -0
- package/dist/kiosk/session-use.marker.d.ts +4 -0
- package/dist/kiosk/session-use.marker.js +16 -0
- package/dist/lock/lock-contract.d.ts +38 -0
- package/dist/lock/lock-contract.js +67 -0
- package/dist/lock/lock.verifier.d.ts +27 -0
- package/dist/lock/lock.verifier.js +62 -0
- package/dist/mcp/agent-scope.gate.d.ts +13 -0
- package/dist/mcp/agent-scope.gate.js +75 -0
- package/dist/mcp/agent.grant.d.ts +40 -0
- package/dist/mcp/agent.grant.js +92 -0
- package/dist/mcp/mcp-options.d.ts +60 -0
- package/dist/mcp/mcp-options.js +72 -0
- package/dist/oauth/access-token.verifier.d.ts +66 -0
- package/dist/oauth/access-token.verifier.js +201 -0
- package/dist/oauth/oauth-provider.hardening.d.ts +62 -0
- package/dist/oauth/oauth-provider.hardening.js +261 -0
- package/dist/passwords/argon2id.hasher.d.ts +97 -0
- package/dist/passwords/argon2id.hasher.js +256 -0
- package/dist/passwords/node-rs-argon2.adapter.d.ts +25 -0
- package/dist/passwords/node-rs-argon2.adapter.js +42 -0
- package/dist/passwords/passwords.options.d.ts +79 -0
- package/dist/passwords/passwords.options.js +212 -0
- package/dist/policies-contract.d.ts +219 -0
- package/dist/policies-contract.js +8 -0
- package/dist/policies.compiler.d.ts +38 -0
- package/dist/policies.compiler.js +1433 -0
- package/dist/policies.decider.d.ts +103 -0
- package/dist/policies.decider.js +390 -0
- package/dist/policies.hash.d.ts +10 -0
- package/dist/policies.hash.js +54 -0
- package/dist/predicate.evaluator.d.ts +62 -0
- package/dist/predicate.evaluator.js +378 -0
- package/dist/random-secret.generator.d.ts +2 -0
- package/dist/random-secret.generator.js +15 -0
- package/dist/read-governed-unique.check.d.ts +3 -0
- package/dist/read-governed-unique.check.js +21 -0
- package/dist/reference-reads.check.d.ts +16 -0
- package/dist/reference-reads.check.js +158 -0
- package/dist/registry/access.registry.d.ts +173 -0
- package/dist/registry/access.registry.js +865 -0
- package/dist/registry/beacon.ledger.d.ts +30 -0
- package/dist/registry/beacon.ledger.js +93 -0
- package/dist/registry/ownership.check.d.ts +54 -0
- package/dist/registry/ownership.check.js +221 -0
- package/dist/registry/registry.endpoints.d.ts +50 -0
- package/dist/registry/registry.endpoints.js +193 -0
- package/dist/registry/registry.key.d.ts +7 -0
- package/dist/registry/registry.key.js +13 -0
- package/dist/registry/registry.options.d.ts +13 -0
- package/dist/registry/registry.options.js +28 -0
- package/dist/registry/registry.refusal.d.ts +6 -0
- package/dist/registry/registry.refusal.js +11 -0
- package/dist/registry/registry.schema.d.ts +238 -0
- package/dist/registry/registry.schema.js +127 -0
- package/dist/resources/auth-tables.catalog.d.ts +96 -0
- package/dist/resources/auth-tables.catalog.js +110 -0
- package/dist/resources/auth-tables.guard.d.ts +106 -0
- package/dist/resources/auth-tables.guard.js +462 -0
- package/dist/scope.resolver.d.ts +144 -0
- package/dist/scope.resolver.js +529 -0
- package/dist/sha256.digest.d.ts +2 -0
- package/dist/sha256.digest.js +81 -0
- package/dist/standalone/access.server.d.ts +41 -0
- package/dist/standalone/access.server.js +301 -0
- package/dist/standalone/index.d.ts +1 -0
- package/dist/standalone/index.js +1 -0
- package/package.json +102 -3
- package/skill/SKILL.md +170 -0
- package/skill/reference/callers.md +99 -0
- package/skill/reference/errors.md +89 -0
- package/skill/reference/recipes.md +281 -0
- package/skill/reference/rules.md +153 -0
- package/skill/reference/setup.md +334 -0
- package/src/aventara-where.translator.ts +116 -0
- package/src/beacon/beacon.check.ts +204 -0
- package/src/canonical-json.writer.ts +51 -0
- package/src/generator/prisma7/prisma-where.translator.ts +243 -0
- package/src/policies-contract.ts +215 -0
- package/src/policies.decider.ts +662 -0
- package/src/predicate.evaluator.ts +574 -0
- package/src/scope.resolver.ts +819 -0
- package/src/sha256.digest.ts +85 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
// The decisions a policies file makes: `can`, `grantsOf`, `scopeOf` and `fieldsOf`,
|
|
2
|
+
// answered from the file alone. Default deny, any grant allows, any deny wins, order
|
|
3
|
+
// never matters. The engine (`access.engine.ts`) runs this with its lock, guardrails,
|
|
4
|
+
// decision observer and `explain` around it; `avaccess generate` copies this module,
|
|
5
|
+
// with the modules it imports, into a service's generated client.
|
|
6
|
+
//
|
|
7
|
+
// Like those modules, it imports nothing but them: no Node.js, no package.
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type AccessWhere,
|
|
11
|
+
translateAnswer,
|
|
12
|
+
translateWhere,
|
|
13
|
+
} from "./aventara-where.translator.js";
|
|
14
|
+
import { type CanonicalJson, canonicalJson } from "./canonical-json.writer.js";
|
|
15
|
+
import type {
|
|
16
|
+
AccessAction,
|
|
17
|
+
AccessGrantRule,
|
|
18
|
+
AccessOrigin,
|
|
19
|
+
AccessPolicies,
|
|
20
|
+
} from "./policies-contract.js";
|
|
21
|
+
import {
|
|
22
|
+
type AccessExists,
|
|
23
|
+
type AccessLookup,
|
|
24
|
+
type AccessRow,
|
|
25
|
+
type AccessTruth,
|
|
26
|
+
decideOnRow,
|
|
27
|
+
type Evaluated,
|
|
28
|
+
evaluateOnRow,
|
|
29
|
+
} from "./predicate.evaluator.js";
|
|
30
|
+
import {
|
|
31
|
+
FALSE,
|
|
32
|
+
type FoldedDeny,
|
|
33
|
+
type FoldedGrant,
|
|
34
|
+
foldDenies,
|
|
35
|
+
foldGrants,
|
|
36
|
+
governedFor,
|
|
37
|
+
nullSafe,
|
|
38
|
+
orOf,
|
|
39
|
+
ownOf,
|
|
40
|
+
principalFactsOf,
|
|
41
|
+
type Resolved,
|
|
42
|
+
rulesFor,
|
|
43
|
+
scopeCondition,
|
|
44
|
+
TRUE,
|
|
45
|
+
} from "./scope.resolver.js";
|
|
46
|
+
import { sha256Hex } from "./sha256.digest.js";
|
|
47
|
+
|
|
48
|
+
/** What every question states. */
|
|
49
|
+
export type AccessQuestion = {
|
|
50
|
+
/** What the host derived for the request: an object, or `null` (anonymous). */
|
|
51
|
+
readonly principal: unknown;
|
|
52
|
+
readonly origin: AccessOrigin;
|
|
53
|
+
readonly resource: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** `can`: without a row, admission; with one, the row a create inserts or an update leaves. */
|
|
57
|
+
export type AccessCanQuestion = AccessQuestion & {
|
|
58
|
+
readonly action: AccessAction;
|
|
59
|
+
readonly row?: AccessRow;
|
|
60
|
+
/**
|
|
61
|
+
* On an update with a `row` only: the row as it was before the update. One update
|
|
62
|
+
* grant must then cover both rows, its `where` this one and its `where` and
|
|
63
|
+
* `after` the row the update leaves. Without it, any update grant that covers the
|
|
64
|
+
* row the update leaves lets it in. Read nowhere else.
|
|
65
|
+
*/
|
|
66
|
+
readonly before?: AccessRow;
|
|
67
|
+
/**
|
|
68
|
+
* On an update with a `row` only: what `grantsOf` answered about the row before the
|
|
69
|
+
* update, asked before the write. The grant that covers `row` (its `where` and
|
|
70
|
+
* `after`) must then be one of these. With `before` too, each such grant's `where`
|
|
71
|
+
* is checked on `before` in memory, so a list from another row or principal fails
|
|
72
|
+
* on a scalar condition; a relation condition on it is not asked again: it was
|
|
73
|
+
* decided before anything the update wrote. Read nowhere else.
|
|
74
|
+
*/
|
|
75
|
+
readonly beforeGrants?: readonly string[];
|
|
76
|
+
readonly exists?: AccessExists;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* `grantsOf`: the update grants that cover `row`, the row an update will change, as it
|
|
81
|
+
* is now. Ask it before the write, and hand its answer back to `can` as `beforeGrants`.
|
|
82
|
+
*/
|
|
83
|
+
export type AccessGrantsQuestion = AccessQuestion & {
|
|
84
|
+
readonly action: "update";
|
|
85
|
+
readonly row: AccessRow;
|
|
86
|
+
readonly exists?: AccessExists;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** `scopeOf`: the rows the principal may read, update or delete. */
|
|
90
|
+
export type AccessScopeQuestion = AccessQuestion & {
|
|
91
|
+
readonly action: "read" | "update" | "delete";
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** `fieldsOf`: reading, or writing by one action. */
|
|
95
|
+
export type AccessFieldsQuestion = AccessQuestion &
|
|
96
|
+
(
|
|
97
|
+
| { readonly access: "read" }
|
|
98
|
+
| { readonly access: "write"; readonly action?: "create" | "update" }
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
/** `fieldsOf`'s answer: fields usable on every reachable row, and fields usable on some rows. */
|
|
102
|
+
export type AccessFields = {
|
|
103
|
+
readonly always: "all" | readonly string[];
|
|
104
|
+
readonly perRow: { readonly [field: string]: AccessWhere };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** How one rule took part in a decision. */
|
|
108
|
+
export type AccessRuleOutcome =
|
|
109
|
+
| "matched"
|
|
110
|
+
| "not matched"
|
|
111
|
+
| "unknown"
|
|
112
|
+
| "not asked";
|
|
113
|
+
|
|
114
|
+
/** How each rule took part in one `can` decision, and the `exists` lookups it made. */
|
|
115
|
+
export type DecisionTrace = {
|
|
116
|
+
readonly grants: Map<FoldedGrant, AccessRuleOutcome>;
|
|
117
|
+
readonly denies: Map<FoldedDeny, AccessRuleOutcome>;
|
|
118
|
+
readonly lookups: AccessLookup[];
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** A trace with nothing in it yet. */
|
|
122
|
+
export const newTrace = (): DecisionTrace => ({
|
|
123
|
+
grants: new Map(),
|
|
124
|
+
denies: new Map(),
|
|
125
|
+
lookups: [],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The Resources where the row an update leaves can fail what allowed the update:
|
|
130
|
+
* an update grant whose condition reads the row (a `where`, an `after`, a role held
|
|
131
|
+
* on a row), or an update deny with a `where`. Core asks `can` about each row such
|
|
132
|
+
* an update changed, and refuses these updates where it has no interactive
|
|
133
|
+
* transaction to roll one back in.
|
|
134
|
+
*/
|
|
135
|
+
export const afterChecksOf = (
|
|
136
|
+
policies: AccessPolicies,
|
|
137
|
+
): Record<string, "update"[]> => {
|
|
138
|
+
const afterChecks: Record<string, "update"[]> = {};
|
|
139
|
+
const reads = (grant: AccessGrantRule): boolean =>
|
|
140
|
+
grant.where !== undefined ||
|
|
141
|
+
grant.after !== undefined ||
|
|
142
|
+
grant.in !== undefined ||
|
|
143
|
+
grant.subjects.some((subject) => {
|
|
144
|
+
const role = ownOf(policies.roles, subject);
|
|
145
|
+
return role !== undefined && "on" in role;
|
|
146
|
+
});
|
|
147
|
+
for (const grant of policies.grants) {
|
|
148
|
+
if (
|
|
149
|
+
grant.resource !== "*" &&
|
|
150
|
+
grant.actions.includes("update") &&
|
|
151
|
+
reads(grant)
|
|
152
|
+
) {
|
|
153
|
+
afterChecks[grant.resource] = ["update"];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const deny of policies.denies) {
|
|
157
|
+
if (
|
|
158
|
+
deny.resource !== "*" &&
|
|
159
|
+
deny.where !== undefined &&
|
|
160
|
+
(deny.actions === "*" || deny.actions.includes("update"))
|
|
161
|
+
) {
|
|
162
|
+
afterChecks[deny.resource] = ["update"];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return afterChecks;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The fields each grant's `write.except` locks. A relation and its reference fields
|
|
170
|
+
* are locked together: writing the relation (`$connect`, `$set`) writes those
|
|
171
|
+
* fields, and writing a reference field moves the relation, so naming either one
|
|
172
|
+
* locks both.
|
|
173
|
+
*/
|
|
174
|
+
const lockedFieldsOf = (
|
|
175
|
+
policies: AccessPolicies,
|
|
176
|
+
): Map<AccessGrantRule, ReadonlySet<string>> => {
|
|
177
|
+
const locks = new Map<AccessGrantRule, ReadonlySet<string>>();
|
|
178
|
+
for (const grant of policies.grants) {
|
|
179
|
+
if (grant.write === undefined) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const named = new Set(grant.write.except);
|
|
183
|
+
const locked = new Set(named);
|
|
184
|
+
const relations = ownOf(policies.schema, grant.resource)?.relations ?? {};
|
|
185
|
+
for (const [relation, fact] of Object.entries(relations)) {
|
|
186
|
+
const references = fact.referenceFields ?? [];
|
|
187
|
+
if (named.has(relation) || references.some((field) => named.has(field))) {
|
|
188
|
+
locked.add(relation);
|
|
189
|
+
for (const field of references) {
|
|
190
|
+
locked.add(field);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
locks.set(grant, locked);
|
|
195
|
+
}
|
|
196
|
+
return locks;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* What is wrong with a `fieldsOf` question the types rule out but a caller can still
|
|
201
|
+
* send, or `undefined`: answered as asked, it would read as a read, or as no field.
|
|
202
|
+
*/
|
|
203
|
+
export const fieldsQuestionProblem = (
|
|
204
|
+
question: AccessFieldsQuestion,
|
|
205
|
+
): string | undefined => {
|
|
206
|
+
const { access, action } = question as {
|
|
207
|
+
readonly access?: unknown;
|
|
208
|
+
readonly action?: unknown;
|
|
209
|
+
};
|
|
210
|
+
const fits =
|
|
211
|
+
access === "read"
|
|
212
|
+
? action === undefined
|
|
213
|
+
: access === "write" &&
|
|
214
|
+
(action === undefined || action === "create" || action === "update");
|
|
215
|
+
return fits
|
|
216
|
+
? undefined
|
|
217
|
+
: `fieldsOf takes \`access: "read"\`, or \`access: "write"\` with an optional \`action\` ("create" or "update"); it was asked ${JSON.stringify({ access, action })}`;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const outcomeOf = (truth: AccessTruth): AccessRuleOutcome =>
|
|
221
|
+
truth === "unknown" ? "unknown" : truth ? "matched" : "not matched";
|
|
222
|
+
|
|
223
|
+
const isDecided = (
|
|
224
|
+
node: Evaluated,
|
|
225
|
+
): node is { readonly k: "true" } | { readonly k: "false" } =>
|
|
226
|
+
node.k === "true" || node.k === "false";
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* What is wrong with a `grantsOf` question the types rule out but a caller can still
|
|
230
|
+
* send, or `undefined`: it asks about an update, and nothing else.
|
|
231
|
+
*/
|
|
232
|
+
export const grantsQuestionProblem = (
|
|
233
|
+
question: AccessGrantsQuestion,
|
|
234
|
+
): string | undefined => {
|
|
235
|
+
const { action } = question as { readonly action?: unknown };
|
|
236
|
+
return action === "update"
|
|
237
|
+
? undefined
|
|
238
|
+
: `grantsOf takes \`action: "update"\`; it was asked ${JSON.stringify({ action })}`;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/** The questions a policies file answers. */
|
|
242
|
+
export type PoliciesDecider = {
|
|
243
|
+
/** `can`, recording each rule's part in the answer in `trace`. */
|
|
244
|
+
can(
|
|
245
|
+
question: AccessCanQuestion,
|
|
246
|
+
trace: DecisionTrace,
|
|
247
|
+
): boolean | Promise<boolean>;
|
|
248
|
+
/**
|
|
249
|
+
* The keys of the update grants whose `where`, and their subject's row condition,
|
|
250
|
+
* hold on `row` as it is now: opaque and stable for this file, never rule data.
|
|
251
|
+
* None when an update deny is decided on `row` in memory.
|
|
252
|
+
*/
|
|
253
|
+
grantsOf(
|
|
254
|
+
question: AccessGrantsQuestion,
|
|
255
|
+
): readonly string[] | Promise<readonly string[]>;
|
|
256
|
+
scopeOf(question: AccessScopeQuestion): boolean | AccessWhere;
|
|
257
|
+
fieldsOf(question: AccessFieldsQuestion): AccessFields;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/** Whether two field lists name the same fields. */
|
|
261
|
+
const sameFields = (a: readonly string[], b: readonly string[]): boolean =>
|
|
262
|
+
a.length === b.length && a.every((field) => b.includes(field));
|
|
263
|
+
|
|
264
|
+
/** The decisions of one policies file, already checked. */
|
|
265
|
+
export const createDecider = (file: AccessPolicies): PoliciesDecider => {
|
|
266
|
+
const locks = lockedFieldsOf(file);
|
|
267
|
+
// A grant's key: a short hash of the file's grants, in their order, then its place
|
|
268
|
+
// in the file and its subject's place in the grant. It names a grant for one
|
|
269
|
+
// subject without carrying any of its data, a list from another file names no grant
|
|
270
|
+
// of this one, and it is the same for the folded grant `grantsOf` reads (its
|
|
271
|
+
// `where`) and the one `can` reads (its `where` and `after`).
|
|
272
|
+
const mark = sha256Hex(
|
|
273
|
+
canonicalJson(file.grants as unknown as CanonicalJson),
|
|
274
|
+
).slice(0, 8);
|
|
275
|
+
const places = new Map(file.grants.map((grant, index) => [grant, index]));
|
|
276
|
+
const keyOf = (grant: FoldedGrant): string =>
|
|
277
|
+
`${mark}:${places.get(grant.grant) ?? -1}.${grant.grant.subjects.indexOf(grant.subject)}`;
|
|
278
|
+
|
|
279
|
+
const can = (
|
|
280
|
+
question: AccessCanQuestion,
|
|
281
|
+
trace: DecisionTrace,
|
|
282
|
+
): boolean | Promise<boolean> => {
|
|
283
|
+
const facts = principalFactsOf(question.principal, question.origin);
|
|
284
|
+
const { grants, denies } = rulesFor(
|
|
285
|
+
file,
|
|
286
|
+
question.resource,
|
|
287
|
+
question.action,
|
|
288
|
+
);
|
|
289
|
+
const { row } = question;
|
|
290
|
+
|
|
291
|
+
if (row === undefined) {
|
|
292
|
+
// Admission: could this principal ever do this? Row conditions are the scope's.
|
|
293
|
+
const folded = foldGrants(file, grants, facts);
|
|
294
|
+
const foldedDenies = foldDenies(file, denies, facts);
|
|
295
|
+
for (const grant of folded) {
|
|
296
|
+
trace.grants.set(
|
|
297
|
+
grant,
|
|
298
|
+
grant.condition.k === "false" ? "not matched" : "matched",
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
for (const deny of foldedDenies) {
|
|
302
|
+
trace.denies.set(
|
|
303
|
+
deny,
|
|
304
|
+
deny.condition.k === "true" ? "matched" : "not matched",
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return (
|
|
308
|
+
folded.some((grant) => grant.condition.k !== "false") &&
|
|
309
|
+
!foldedDenies.some((deny) => deny.condition.k === "true")
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (typeof row !== "object" || row === null) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
// The row a create inserts, or the row an update leaves (core's after check, or
|
|
317
|
+
// any caller's): an update's row must still meet the `where` and the `after` of
|
|
318
|
+
// one update grant, and no update deny may match it.
|
|
319
|
+
const isUpdate = question.action === "update";
|
|
320
|
+
const kind = question.action === "create" ? "new" : "existing";
|
|
321
|
+
// With the row before the update: the grant that covers the row
|
|
322
|
+
// the update leaves must be one whose `where` covered the row before it, so no
|
|
323
|
+
// update moves a row from the rows one grant reaches to another's. With
|
|
324
|
+
// `beforeGrants`, what covered it was decided before the write (`grantsOf`):
|
|
325
|
+
// `before` is still held to each listed grant's `where` in memory, since the list
|
|
326
|
+
// is the caller's to hand back, but a relation condition on it is not asked again.
|
|
327
|
+
const beforeGrants = isUpdate ? question.beforeGrants : undefined;
|
|
328
|
+
if (
|
|
329
|
+
beforeGrants !== undefined &&
|
|
330
|
+
(!Array.isArray(beforeGrants) ||
|
|
331
|
+
beforeGrants.some((key) => typeof key !== "string"))
|
|
332
|
+
) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
const coveredBefore =
|
|
336
|
+
beforeGrants === undefined ? undefined : new Set(beforeGrants);
|
|
337
|
+
const before = isUpdate ? question.before : undefined;
|
|
338
|
+
if (
|
|
339
|
+
before !== undefined &&
|
|
340
|
+
(typeof before !== "object" || before === null)
|
|
341
|
+
) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
const folded = foldGrants(file, grants, facts, isUpdate);
|
|
345
|
+
const reached =
|
|
346
|
+
before === undefined ? undefined : foldGrants(file, grants, facts);
|
|
347
|
+
const foldedDenies = foldDenies(file, denies, facts);
|
|
348
|
+
|
|
349
|
+
const grantResiduals: {
|
|
350
|
+
readonly grant: FoldedGrant;
|
|
351
|
+
readonly residual: Resolved;
|
|
352
|
+
/** What the row before the update leaves of the grant's `where`; `undefined` when decided true. */
|
|
353
|
+
readonly prior: Resolved | undefined;
|
|
354
|
+
}[] = [];
|
|
355
|
+
let granted = false;
|
|
356
|
+
for (const [index, grant] of folded.entries()) {
|
|
357
|
+
if (coveredBefore !== undefined && !coveredBefore.has(keyOf(grant))) {
|
|
358
|
+
// It did not cover the row before the update.
|
|
359
|
+
trace.grants.set(grant, "not matched");
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const residual = evaluateOnRow(
|
|
363
|
+
file,
|
|
364
|
+
question.resource,
|
|
365
|
+
grant.condition,
|
|
366
|
+
row,
|
|
367
|
+
);
|
|
368
|
+
// Folded alike, the two lists pair grant for grant; a missing pair covers nothing.
|
|
369
|
+
const evaluated =
|
|
370
|
+
reached === undefined || before === undefined
|
|
371
|
+
? undefined
|
|
372
|
+
: evaluateOnRow(
|
|
373
|
+
file,
|
|
374
|
+
question.resource,
|
|
375
|
+
reached?.[index]?.condition ?? FALSE,
|
|
376
|
+
before,
|
|
377
|
+
);
|
|
378
|
+
// Listed in `beforeGrants`: what is left of its `where` on `before` is a relation
|
|
379
|
+
// condition `grantsOf` decided before the write; only a decided false, or a
|
|
380
|
+
// value of the wrong type, refuses.
|
|
381
|
+
const prior =
|
|
382
|
+
coveredBefore !== undefined &&
|
|
383
|
+
evaluated?.k !== "false" &&
|
|
384
|
+
evaluated?.k !== "unknown"
|
|
385
|
+
? undefined
|
|
386
|
+
: evaluated;
|
|
387
|
+
if (residual.k === "unknown" || prior?.k === "unknown") {
|
|
388
|
+
// A value of the wrong type: no grant.
|
|
389
|
+
trace.grants.set(grant, "unknown");
|
|
390
|
+
} else if (residual.k === "false" || prior?.k === "false") {
|
|
391
|
+
trace.grants.set(grant, "not matched");
|
|
392
|
+
} else if (
|
|
393
|
+
residual.k === "true" &&
|
|
394
|
+
(prior === undefined || prior.k === "true")
|
|
395
|
+
) {
|
|
396
|
+
trace.grants.set(grant, "matched");
|
|
397
|
+
granted = true;
|
|
398
|
+
} else {
|
|
399
|
+
trace.grants.set(grant, "not asked");
|
|
400
|
+
grantResiduals.push({
|
|
401
|
+
grant,
|
|
402
|
+
residual,
|
|
403
|
+
prior: prior === undefined || prior.k === "true" ? undefined : prior,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const denyResiduals: {
|
|
408
|
+
readonly deny: FoldedDeny;
|
|
409
|
+
readonly residual: Resolved;
|
|
410
|
+
}[] = [];
|
|
411
|
+
for (const deny of foldedDenies) {
|
|
412
|
+
const residual = evaluateOnRow(
|
|
413
|
+
file,
|
|
414
|
+
question.resource,
|
|
415
|
+
deny.condition,
|
|
416
|
+
row,
|
|
417
|
+
);
|
|
418
|
+
if (residual.k === "unknown") {
|
|
419
|
+
// A value of the wrong type: the deny counts (fail closed).
|
|
420
|
+
trace.denies.set(deny, "unknown");
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
if (isDecided(residual)) {
|
|
424
|
+
trace.denies.set(
|
|
425
|
+
deny,
|
|
426
|
+
residual.k === "true" ? "matched" : "not matched",
|
|
427
|
+
);
|
|
428
|
+
if (residual.k === "true") {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
trace.denies.set(deny, "not asked");
|
|
433
|
+
denyResiduals.push({ deny, residual });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (!granted && grantResiduals.length === 0) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
if (denyResiduals.length === 0 && granted) {
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const ask = (
|
|
444
|
+
condition: Resolved,
|
|
445
|
+
on: AccessRow = row,
|
|
446
|
+
): Promise<AccessTruth> =>
|
|
447
|
+
decideOnRow({
|
|
448
|
+
policies: file,
|
|
449
|
+
resource: question.resource,
|
|
450
|
+
condition,
|
|
451
|
+
row: on,
|
|
452
|
+
kind,
|
|
453
|
+
exists: question.exists,
|
|
454
|
+
lookups: trace.lookups,
|
|
455
|
+
});
|
|
456
|
+
return (async () => {
|
|
457
|
+
for (const { deny, residual } of denyResiduals) {
|
|
458
|
+
const truth = await ask(residual);
|
|
459
|
+
trace.denies.set(deny, outcomeOf(truth));
|
|
460
|
+
// A deny that cannot be decided counts: fail closed.
|
|
461
|
+
if (truth !== false) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (granted) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
for (const { grant, residual, prior } of grantResiduals) {
|
|
469
|
+
let truth = await ask(residual);
|
|
470
|
+
if (truth === true && prior !== undefined && before !== undefined) {
|
|
471
|
+
truth = await ask(prior, before);
|
|
472
|
+
}
|
|
473
|
+
trace.grants.set(grant, outcomeOf(truth));
|
|
474
|
+
if (truth === true) {
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return false;
|
|
479
|
+
})();
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const fieldsOf = (question: AccessFieldsQuestion): AccessFields => {
|
|
483
|
+
const problem = fieldsQuestionProblem(question);
|
|
484
|
+
if (problem !== undefined) {
|
|
485
|
+
throw new Error(`Aventara Access: ${problem}`);
|
|
486
|
+
}
|
|
487
|
+
const facts = principalFactsOf(question.principal, question.origin);
|
|
488
|
+
// The fields governed for this access: only a grant that names one opens it.
|
|
489
|
+
const governed = governedFor(file, question.resource, question.access);
|
|
490
|
+
// `"all"` stands for every field the read side does not govern (the framework's
|
|
491
|
+
// default projection, whatever the access): a write answers it only when the
|
|
492
|
+
// write side governs the same fields.
|
|
493
|
+
const allStandsFor =
|
|
494
|
+
question.access === "read" ||
|
|
495
|
+
sameFields(governed, governedFor(file, question.resource, "read"));
|
|
496
|
+
const actions: AccessAction[] =
|
|
497
|
+
question.access === "read"
|
|
498
|
+
? ["read"]
|
|
499
|
+
: question.action !== undefined
|
|
500
|
+
? [question.action]
|
|
501
|
+
: ["create", "update"];
|
|
502
|
+
const grants = [
|
|
503
|
+
...new Set(
|
|
504
|
+
actions.flatMap(
|
|
505
|
+
(action) => rulesFor(file, question.resource, action).grants,
|
|
506
|
+
),
|
|
507
|
+
),
|
|
508
|
+
];
|
|
509
|
+
const live = foldGrants(file, grants, facts).filter(
|
|
510
|
+
(grant) => grant.condition.k !== "false",
|
|
511
|
+
);
|
|
512
|
+
if (live.length === 0) {
|
|
513
|
+
return { always: [], perRow: {} };
|
|
514
|
+
}
|
|
515
|
+
const excludes = (grant: FoldedGrant, field: string): boolean =>
|
|
516
|
+
question.access === "write" &&
|
|
517
|
+
(locks.get(grant.grant)?.has(field) ?? false);
|
|
518
|
+
const names = (grant: FoldedGrant, field: string): boolean =>
|
|
519
|
+
grant.grant.fields?.includes(`${question.resource}.${field}`) ?? false;
|
|
520
|
+
const answer = (condition: Resolved): AccessWhere =>
|
|
521
|
+
condition.k === "true"
|
|
522
|
+
? {}
|
|
523
|
+
: translateWhere(nullSafe(file, question.resource, condition));
|
|
524
|
+
|
|
525
|
+
const perRow: Record<string, AccessWhere> = {};
|
|
526
|
+
for (const field of governed) {
|
|
527
|
+
const condition = orOf(
|
|
528
|
+
live
|
|
529
|
+
.filter((grant) => names(grant, field) && !excludes(grant, field))
|
|
530
|
+
.map((grant) => grant.condition),
|
|
531
|
+
);
|
|
532
|
+
if (condition.k !== "false") {
|
|
533
|
+
perRow[field] = answer(condition);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const excluded = new Set(
|
|
537
|
+
live.flatMap((grant) =>
|
|
538
|
+
question.access === "write" ? [...(locks.get(grant.grant) ?? [])] : [],
|
|
539
|
+
),
|
|
540
|
+
);
|
|
541
|
+
if (excluded.size === 0 && allStandsFor) {
|
|
542
|
+
return { always: "all", perRow };
|
|
543
|
+
}
|
|
544
|
+
const always: string[] = [];
|
|
545
|
+
for (const field of ownOf(file.schema, question.resource)?.fields ?? []) {
|
|
546
|
+
if (governed.includes(field)) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (!excluded.has(field)) {
|
|
550
|
+
always.push(field);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const condition = orOf(
|
|
554
|
+
live
|
|
555
|
+
.filter((grant) => !excludes(grant, field))
|
|
556
|
+
.map((grant) => grant.condition),
|
|
557
|
+
);
|
|
558
|
+
if (condition.k === "true") {
|
|
559
|
+
always.push(field);
|
|
560
|
+
} else if (condition.k !== "false") {
|
|
561
|
+
perRow[field] = answer(condition);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return { always, perRow };
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const scopeOf = (question: AccessScopeQuestion): boolean | AccessWhere => {
|
|
568
|
+
const facts = principalFactsOf(question.principal, question.origin);
|
|
569
|
+
const { grants, denies } = rulesFor(
|
|
570
|
+
file,
|
|
571
|
+
question.resource,
|
|
572
|
+
question.action,
|
|
573
|
+
);
|
|
574
|
+
return translateAnswer(
|
|
575
|
+
nullSafe(
|
|
576
|
+
file,
|
|
577
|
+
question.resource,
|
|
578
|
+
scopeCondition(
|
|
579
|
+
foldGrants(file, grants, facts),
|
|
580
|
+
foldDenies(file, denies, facts),
|
|
581
|
+
),
|
|
582
|
+
),
|
|
583
|
+
);
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const grantsOf = (
|
|
587
|
+
question: AccessGrantsQuestion,
|
|
588
|
+
): readonly string[] | Promise<readonly string[]> => {
|
|
589
|
+
const problem = grantsQuestionProblem(question);
|
|
590
|
+
if (problem !== undefined) {
|
|
591
|
+
throw new Error(`Aventara Access: ${problem}`);
|
|
592
|
+
}
|
|
593
|
+
const { row } = question;
|
|
594
|
+
if (typeof row !== "object" || row === null) {
|
|
595
|
+
// No row, no grant: a `can` handed this answer refuses.
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
598
|
+
const facts = principalFactsOf(question.principal, question.origin);
|
|
599
|
+
const { grants, denies } = rulesFor(file, question.resource, "update");
|
|
600
|
+
// An update deny decided on the row as it is (or on a value of the wrong type,
|
|
601
|
+
// failing closed): no grant lets the update in. A deny on a relation is the row
|
|
602
|
+
// after's to answer.
|
|
603
|
+
if (
|
|
604
|
+
foldDenies(file, denies, facts).some((deny) => {
|
|
605
|
+
const truth = evaluateOnRow(
|
|
606
|
+
file,
|
|
607
|
+
question.resource,
|
|
608
|
+
deny.condition,
|
|
609
|
+
row,
|
|
610
|
+
).k;
|
|
611
|
+
return truth === "true" || truth === "unknown";
|
|
612
|
+
})
|
|
613
|
+
) {
|
|
614
|
+
return [];
|
|
615
|
+
}
|
|
616
|
+
// Each grant's `where` and its subject's row condition, without its `after`: the
|
|
617
|
+
// condition `scopeOf` reads, asked about the row as it is. The answer lists the
|
|
618
|
+
// keys in the file's order.
|
|
619
|
+
const decided: { readonly key: string; residual: Evaluated }[] = foldGrants(
|
|
620
|
+
file,
|
|
621
|
+
grants,
|
|
622
|
+
facts,
|
|
623
|
+
).map((grant) => ({
|
|
624
|
+
key: keyOf(grant),
|
|
625
|
+
residual: evaluateOnRow(file, question.resource, grant.condition, row),
|
|
626
|
+
}));
|
|
627
|
+
const covering = (): string[] =>
|
|
628
|
+
decided.flatMap(({ key, residual }) =>
|
|
629
|
+
residual.k === "true" ? [key] : [],
|
|
630
|
+
);
|
|
631
|
+
if (
|
|
632
|
+
decided.every(
|
|
633
|
+
({ residual }) => isDecided(residual) || residual.k === "unknown",
|
|
634
|
+
)
|
|
635
|
+
) {
|
|
636
|
+
return covering();
|
|
637
|
+
}
|
|
638
|
+
return (async () => {
|
|
639
|
+
const lookups: AccessLookup[] = [];
|
|
640
|
+
for (const entry of decided) {
|
|
641
|
+
const { residual } = entry;
|
|
642
|
+
if (isDecided(residual) || residual.k === "unknown") {
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const truth = await decideOnRow({
|
|
646
|
+
policies: file,
|
|
647
|
+
resource: question.resource,
|
|
648
|
+
condition: residual,
|
|
649
|
+
row,
|
|
650
|
+
kind: "existing",
|
|
651
|
+
exists: question.exists,
|
|
652
|
+
lookups,
|
|
653
|
+
});
|
|
654
|
+
// A grant that cannot be decided does not cover the row.
|
|
655
|
+
entry.residual = truth === true ? TRUE : FALSE;
|
|
656
|
+
}
|
|
657
|
+
return covering();
|
|
658
|
+
})();
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
return { can, grantsOf, scopeOf, fieldsOf };
|
|
662
|
+
};
|