@aventara/access 0.0.0-stage → 0.1.0-rc.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/CHANGELOG.md +831 -0
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +2439 -2
- package/dist/access/access-options.d.ts +66 -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 +273 -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 +12 -0
- package/dist/access/principal.subject.js +60 -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 +196 -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 +2 -0
- package/dist/access-warning.emitter.js +20 -0
- package/dist/access.builder.d.ts +299 -0
- package/dist/access.builder.js +66 -0
- package/dist/access.engine.d.ts +142 -0
- package/dist/access.engine.js +470 -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 +78 -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 +83 -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 +12 -0
- package/dist/foreign-references.check.js +134 -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 +72 -0
- package/dist/generator/prisma7/prisma-where.translator.js +136 -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 +11 -0
- package/dist/mcp/agent-scope.gate.js +75 -0
- package/dist/mcp/agent.grant.d.ts +36 -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 +192 -0
- package/dist/policies-contract.js +8 -0
- package/dist/policies.compiler.d.ts +36 -0
- package/dist/policies.compiler.js +1358 -0
- package/dist/policies.decider.d.ts +103 -0
- package/dist/policies.decider.js +382 -0
- package/dist/policies.hash.d.ts +10 -0
- package/dist/policies.hash.js +53 -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/reference-reads.check.d.ts +12 -0
- package/dist/reference-reads.check.js +148 -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 +215 -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 +91 -0
- package/dist/resources/auth-tables.catalog.js +102 -0
- package/dist/resources/auth-tables.guard.d.ts +95 -0
- package/dist/resources/auth-tables.guard.js +405 -0
- package/dist/scope.resolver.d.ts +138 -0
- package/dist/scope.resolver.js +512 -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 +101 -3
- 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 +241 -0
- package/src/policies-contract.ts +184 -0
- package/src/policies.decider.ts +650 -0
- package/src/predicate.evaluator.ts +574 -0
- package/src/scope.resolver.ts +791 -0
- package/src/sha256.digest.ts +85 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { AccessFields, AccessFieldsQuestion } from "../policies.decider.js";
|
|
2
|
+
import type { AccessDenyRule, AccessPolicies } from "../policies-contract.js";
|
|
3
|
+
import { type AuthResourceKey, type AuthTable, type AuthTableNaming } from "./auth-tables.catalog.js";
|
|
4
|
+
/**
|
|
5
|
+
* The `resources` block: which of Better Auth's tables are Resources of the app's
|
|
6
|
+
* rules. Every other table of Better Auth's stays closed.
|
|
7
|
+
*/
|
|
8
|
+
export type AccessResourcesOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* Better Auth's `user`, `session` and `account` tables: read as the rules grant,
|
|
11
|
+
* secret columns excepted; through Aventara, a user's `role` is the one field a
|
|
12
|
+
* write may set.
|
|
13
|
+
*/
|
|
14
|
+
readonly users?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The organization plugin's `member` table (and its `organization`, read only):
|
|
17
|
+
* read as the rules grant; a membership may be created, removed, and its `role`
|
|
18
|
+
* changed. Needs `organization()` in `betterAuth.plugins`.
|
|
19
|
+
*/
|
|
20
|
+
readonly members?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Which Resource of the rules' schema is each of Better Auth's tables, by Better
|
|
23
|
+
* Auth model name (`{ user: "User", session: "AuthSession", jwks: null, … }`),
|
|
24
|
+
* `null` for a table the schema does not have (not "closed": a table the schema
|
|
25
|
+
* has is named, and closed or opened by its key). Each named Resource has every
|
|
26
|
+
* column of its table, by Better Auth's `fieldName`. Required, naming every table
|
|
27
|
+
* Better Auth is built with (its plugins' and Access's own included), when Better
|
|
28
|
+
* Auth does not run on its Prisma adapter and the rules have a schema: only the
|
|
29
|
+
* Prisma adapter ties a table to a model by name. Refused with the Prisma adapter.
|
|
30
|
+
*/
|
|
31
|
+
readonly authTables?: {
|
|
32
|
+
readonly [model: string]: string | null;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/** The `resources` block, checked. */
|
|
36
|
+
export type ResourcesChoice = {
|
|
37
|
+
readonly [K in AuthResourceKey]: boolean;
|
|
38
|
+
} & {
|
|
39
|
+
readonly authTables: {
|
|
40
|
+
readonly [model: string]: string | null;
|
|
41
|
+
} | undefined;
|
|
42
|
+
};
|
|
43
|
+
/** The fields a write may set on one opened table, by action. */
|
|
44
|
+
type WritableSets = {
|
|
45
|
+
readonly create: ReadonlySet<string>;
|
|
46
|
+
readonly update: ReadonlySet<string>;
|
|
47
|
+
};
|
|
48
|
+
/** The built-in rules over Better Auth's tables, as the engine runs them. */
|
|
49
|
+
export type AuthTablesGuard = {
|
|
50
|
+
/** Denies added to the rules': closed tables, and the writes an opened one never takes. */
|
|
51
|
+
readonly denies: readonly AccessDenyRule[];
|
|
52
|
+
/** The secret columns, by Resource: never in an answer of `fieldsOf`. */
|
|
53
|
+
readonly sealed: ReadonlyMap<string, ReadonlySet<string>>;
|
|
54
|
+
/** The fields a write may set, by opened Resource. */
|
|
55
|
+
readonly writable: ReadonlyMap<string, WritableSets>;
|
|
56
|
+
};
|
|
57
|
+
/** The `resources` block, checked: `users` and `members` `true` or `false`, `authTables` a map of names or `null`. */
|
|
58
|
+
export declare const checkResourcesOptions: (value: unknown) => ResourcesChoice;
|
|
59
|
+
/**
|
|
60
|
+
* How the rules' schema names Better Auth's tables, checked when the instance is
|
|
61
|
+
* built: with Better Auth on its Prisma adapter, by the adapter's own delegates
|
|
62
|
+
* (and no `authTables`); else by `authTables`, which must name every table
|
|
63
|
+
* (`models`) and nothing else. A rules' schema with no Resource needs neither.
|
|
64
|
+
*/
|
|
65
|
+
export declare const authTableNaming: (input: {
|
|
66
|
+
readonly prisma: {
|
|
67
|
+
readonly usePlural: boolean;
|
|
68
|
+
} | undefined;
|
|
69
|
+
readonly authTables: ResourcesChoice["authTables"];
|
|
70
|
+
readonly models: readonly string[];
|
|
71
|
+
readonly schemaEmpty: boolean;
|
|
72
|
+
}) => AuthTableNaming;
|
|
73
|
+
/**
|
|
74
|
+
* The built-in rules for one set of rules: `resources` as the app gave it, Better
|
|
75
|
+
* Auth's tables as it is built (`authTablesOf`), and the rules' file. Throws
|
|
76
|
+
* `ACCESS_RESOURCE_MISSING` when a key's tables are missing, and
|
|
77
|
+
* `ACCESS_OPTIONS_INVALID` for a grant the built-in rules would leave without effect.
|
|
78
|
+
*/
|
|
79
|
+
export declare const guardAuthTables: (input: {
|
|
80
|
+
readonly resources: { readonly [K in AuthResourceKey]: boolean; };
|
|
81
|
+
readonly tables: ReadonlyMap<string, AuthTable>;
|
|
82
|
+
readonly naming: AuthTableNaming;
|
|
83
|
+
readonly policies: AccessPolicies;
|
|
84
|
+
}) => AuthTablesGuard;
|
|
85
|
+
/**
|
|
86
|
+
* `fieldsOf`'s answer with the built-in rules applied: a secret column is never
|
|
87
|
+
* listed, and a write lists only the fields its action may set on an opened table.
|
|
88
|
+
* `"all"` becomes the list it stands for (every field that is not governed) when
|
|
89
|
+
* something is taken out of it.
|
|
90
|
+
*/
|
|
91
|
+
export declare const guardedFields: (guard: AuthTablesGuard, question: AccessFieldsQuestion, answer: AccessFields, resource: {
|
|
92
|
+
readonly fields: readonly string[];
|
|
93
|
+
readonly governed: readonly string[];
|
|
94
|
+
}) => AccessFields;
|
|
95
|
+
export {};
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// The built-in rules Access adds over Better Auth's tables, from the `resources` block
|
|
2
|
+
// and the tables Better Auth is built with. The engine runs them beside the app's
|
|
3
|
+
// rules, and they only narrow:
|
|
4
|
+
//
|
|
5
|
+
// - a table no `resources` key opens is closed: a deny on every action;
|
|
6
|
+
// - an opened table takes the app's grants for reading, and only the writes its
|
|
7
|
+
// key allows (a deny on the other actions; the fields a write may set);
|
|
8
|
+
// - a secret column (a password hash, a session token, a signing key) is never in
|
|
9
|
+
// an answer of `fieldsOf`, whatever the grants say: naming it is refused.
|
|
10
|
+
//
|
|
11
|
+
// At startup (the first read of `access.policy`, which is the framework's
|
|
12
|
+
// construction), a key whose tables are missing (from Better Auth, or from the
|
|
13
|
+
// rules' schema) is refused, and so is a grant that could never apply: one on a
|
|
14
|
+
// closed table, or one naming a write the table never takes.
|
|
15
|
+
import { AccessError } from "../access/access.error.js";
|
|
16
|
+
import { ACTIONS } from "../policies.compiler.js";
|
|
17
|
+
import { ownOf } from "../scope.resolver.js";
|
|
18
|
+
import { KEY_TABLES, OPENED_TABLES, SECRET_COLUMNS, } from "./auth-tables.catalog.js";
|
|
19
|
+
/** The prefix of the built-in deny ids, so `explain` and the audit log tell them from the rules'. */
|
|
20
|
+
const BUILT_IN_ID = "better-auth:";
|
|
21
|
+
const KEYS = ["users", "members"];
|
|
22
|
+
const refuse = (message) => {
|
|
23
|
+
throw new AccessError("ACCESS_OPTIONS_INVALID", message);
|
|
24
|
+
};
|
|
25
|
+
const missing = (message) => {
|
|
26
|
+
throw new AccessError("ACCESS_RESOURCE_MISSING", message);
|
|
27
|
+
};
|
|
28
|
+
/** The `resources` block, checked: `users` and `members` `true` or `false`, `authTables` a map of names or `null`. */
|
|
29
|
+
export const checkResourcesOptions = (value) => {
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
return { users: false, members: false, authTables: undefined };
|
|
32
|
+
}
|
|
33
|
+
const shape = "`resources` is `{ users?: boolean, members?: boolean, authTables?: { [model]: Resource | null } }`: which of Better Auth's tables are Resources of the rules";
|
|
34
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
35
|
+
return refuse(`${shape}.`);
|
|
36
|
+
}
|
|
37
|
+
const block = value;
|
|
38
|
+
for (const [key, on] of Object.entries(block)) {
|
|
39
|
+
if (key === "authTables") {
|
|
40
|
+
if (typeof on !== "object" || on === null || Array.isArray(on)) {
|
|
41
|
+
refuse(`${shape}; \`resources.authTables\` names each table's Resource, or \`null\`.`);
|
|
42
|
+
}
|
|
43
|
+
for (const [model, resource] of Object.entries(on)) {
|
|
44
|
+
if (resource !== null && typeof resource !== "string") {
|
|
45
|
+
refuse(`${shape}; \`resources.authTables.${model}\` is a Resource's name or \`null\`.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!KEYS.includes(key)) {
|
|
51
|
+
refuse(`${shape}; it does not take \`${key}\`.`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof on !== "boolean") {
|
|
54
|
+
refuse(`${shape}; \`resources.${key}\` was ${JSON.stringify(on)}.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
users: block.users === true,
|
|
59
|
+
members: block.members === true,
|
|
60
|
+
authTables: block.authTables,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* How the rules' schema names Better Auth's tables, checked when the instance is
|
|
65
|
+
* built: with Better Auth on its Prisma adapter, by the adapter's own delegates
|
|
66
|
+
* (and no `authTables`); else by `authTables`, which must name every table
|
|
67
|
+
* (`models`) and nothing else. A rules' schema with no Resource needs neither.
|
|
68
|
+
*/
|
|
69
|
+
export const authTableNaming = (input) => {
|
|
70
|
+
const { prisma, authTables, models, schemaEmpty } = input;
|
|
71
|
+
if (prisma !== undefined) {
|
|
72
|
+
if (authTables !== undefined) {
|
|
73
|
+
refuse("`resources.authTables` is for Better Auth on another adapter than Prisma's: on Prisma's, Access finds each table by the Prisma model its adapter reads. Remove it.");
|
|
74
|
+
}
|
|
75
|
+
return { kind: "prisma", usePlural: prisma.usePlural };
|
|
76
|
+
}
|
|
77
|
+
if (schemaEmpty && authTables === undefined) {
|
|
78
|
+
return { kind: "map", map: {} };
|
|
79
|
+
}
|
|
80
|
+
const map = authTables ?? {};
|
|
81
|
+
const unnamed = [];
|
|
82
|
+
for (const model of models) {
|
|
83
|
+
if (!Object.hasOwn(map, model)) {
|
|
84
|
+
unnamed.push(model);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (unnamed.length > 0) {
|
|
88
|
+
refuse(`Better Auth does not run on its Prisma adapter here, so nothing ties its tables to the rules' Resources by name (a Prisma schema can call any table anything, with \`@@map\`), and a table Access does not know would be an ordinary Resource, open to every grant, its secret columns included. Name the Resource each table is in \`resources.authTables\`, or \`null\` for a table the rules' schema does not have; it lacks ${unnamed.map((model) => `\`${model}\``).join(", ")}.`);
|
|
89
|
+
}
|
|
90
|
+
for (const model of Object.keys(map)) {
|
|
91
|
+
if (!models.includes(model)) {
|
|
92
|
+
refuse(`\`resources.authTables\` names \`${model}\`, which is no table of Better Auth's as it is built (its tables: ${models.join(", ")}).`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { kind: "map", map };
|
|
96
|
+
};
|
|
97
|
+
/** One Better Auth field's name in the app's schema, when the Resource has it. */
|
|
98
|
+
const fieldOf = (table, facts, field) => {
|
|
99
|
+
const name = table.fields.get(field) ?? field;
|
|
100
|
+
return facts.fields.includes(name) ? name : undefined;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* The fields a write may set: Better Auth's fields the catalog lists, the row's
|
|
104
|
+
* identifier for a create, and each relation whose reference fields are all among
|
|
105
|
+
* them (writing the relation writes those fields).
|
|
106
|
+
*/
|
|
107
|
+
const writableSet = (table, facts, fields, withIdentifier) => {
|
|
108
|
+
const set = new Set();
|
|
109
|
+
for (const field of fields ?? []) {
|
|
110
|
+
const name = fieldOf(table, facts, field);
|
|
111
|
+
if (name !== undefined) {
|
|
112
|
+
set.add(name);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (fields !== undefined && withIdentifier) {
|
|
116
|
+
for (const field of facts.identifiers[0] ?? []) {
|
|
117
|
+
set.add(field);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const [relation, fact] of Object.entries(facts.relations)) {
|
|
121
|
+
if (fact.referenceFields?.every((field) => set.has(field))) {
|
|
122
|
+
set.add(relation);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return set;
|
|
126
|
+
};
|
|
127
|
+
const actionList = (actions) => actions.length === 1
|
|
128
|
+
? (actions[0] ?? "")
|
|
129
|
+
: `${actions.slice(0, -1).join(", ")} and ${actions.at(-1)}`;
|
|
130
|
+
/** Better Auth's columns of a table, as the app's schema names them (`fieldName`). */
|
|
131
|
+
const columnsOf = (table) => {
|
|
132
|
+
const columns = [];
|
|
133
|
+
for (const name of table?.fields.values() ?? []) {
|
|
134
|
+
columns.push(name);
|
|
135
|
+
}
|
|
136
|
+
return columns;
|
|
137
|
+
};
|
|
138
|
+
/** Whether a Resource has every one of `columns` (and there is at least one). */
|
|
139
|
+
const hasAll = (facts, columns) => {
|
|
140
|
+
for (const column of columns) {
|
|
141
|
+
if (!facts.fields.includes(column)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return columns.length > 0;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* `resources.authTables`, held to the rules' schema: a table's Resource exists, is
|
|
149
|
+
* no other table's, and has every column of the table (by its `fieldName`); a table
|
|
150
|
+
* the map says the schema does not have (`null`) is not, column for column, a
|
|
151
|
+
* Resource the map leaves unnamed. A map that says otherwise than the schema would
|
|
152
|
+
* leave a table of Better Auth's an ordinary Resource.
|
|
153
|
+
*/
|
|
154
|
+
const checkAuthTablesMap = (map, tables, policies) => {
|
|
155
|
+
const named = new Map();
|
|
156
|
+
for (const [model, resource] of Object.entries(map)) {
|
|
157
|
+
if (resource === null) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const facts = ownOf(policies.schema, resource);
|
|
161
|
+
if (facts === undefined) {
|
|
162
|
+
refuse(`\`resources.authTables\` puts Better Auth's \`${model}\` table in ${resource}, and the rules' schema has no ${resource}.`);
|
|
163
|
+
}
|
|
164
|
+
const other = named.get(resource);
|
|
165
|
+
if (other !== undefined) {
|
|
166
|
+
refuse(`\`resources.authTables\` puts Better Auth's \`${other}\` and \`${model}\` tables both in ${resource}.`);
|
|
167
|
+
}
|
|
168
|
+
named.set(resource, model);
|
|
169
|
+
const lacking = [];
|
|
170
|
+
for (const column of columnsOf(tables.get(model))) {
|
|
171
|
+
if (!facts?.fields.includes(column)) {
|
|
172
|
+
lacking.push(column);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (lacking.length > 0) {
|
|
176
|
+
refuse(`\`resources.authTables\` puts Better Auth's \`${model}\` table in ${resource}, which lacks its columns ${lacking.join(", ")}: ${resource} is not that table, or names its columns otherwise than Better Auth's \`fieldName\`s say. Name the table's Resource, and its columns as Better Auth names them.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const [model, resource] of Object.entries(map)) {
|
|
180
|
+
if (resource !== null) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const table = tables.get(model);
|
|
184
|
+
const columns = columnsOf(table);
|
|
185
|
+
const secrets = [];
|
|
186
|
+
for (const field of SECRET_COLUMNS[model] ?? []) {
|
|
187
|
+
secrets.push(table?.fields.get(field) ?? field);
|
|
188
|
+
}
|
|
189
|
+
for (const [other, facts] of Object.entries(policies.schema)) {
|
|
190
|
+
if (named.has(other)) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const held = [];
|
|
194
|
+
let secret = false;
|
|
195
|
+
for (const column of columns) {
|
|
196
|
+
if (facts.fields.includes(column)) {
|
|
197
|
+
held.push(column);
|
|
198
|
+
secret ||= secrets.includes(column);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Every column of the table, or one of its secrets with another of its
|
|
202
|
+
// columns beside it (a model that keeps only some): it may be the table.
|
|
203
|
+
if (hasAll(facts, columns) || (secret && held.length > 1)) {
|
|
204
|
+
refuse(`\`resources.authTables\` says the rules' schema does not have Better Auth's \`${model}\` table, and ${other} has columns of it (${held.join(", ")}), ${secret ? "a secret among them" : "every one"}. If ${other} is that table, name it: \`${model}: "${other}"\`; if it is your own, it cannot keep Better Auth's secret beside its columns.`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* On Prisma's adapter, refuses a table the adapter reads through a delegate no
|
|
211
|
+
* Resource answers to, while the schema has one that differs from it only in case
|
|
212
|
+
* or a plural `s`: the schema and the adapter disagree (an adapter without
|
|
213
|
+
* `usePlural` on a plural schema, say), and the table the adapter really reads is
|
|
214
|
+
* not certain to be closed.
|
|
215
|
+
*/
|
|
216
|
+
const refuseDisagreement = (tables, usePlural, policies) => {
|
|
217
|
+
for (const table of tables.values()) {
|
|
218
|
+
if (table.resource !== undefined) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const delegate = (usePlural ? `${table.name}s` : table.name).toLowerCase();
|
|
222
|
+
for (const resource of Object.keys(policies.schema)) {
|
|
223
|
+
const lowered = resource.toLowerCase();
|
|
224
|
+
if (lowered === delegate ||
|
|
225
|
+
lowered === `${delegate}s` ||
|
|
226
|
+
`${lowered}s` === delegate) {
|
|
227
|
+
refuse(`Better Auth's Prisma adapter reads its \`${table.model}\` table as \`prisma.${usePlural ? `${table.name}s` : table.name}\`, and the rules' schema has no such model but has ${resource}: the schema and the adapter disagree (\`usePlural\`?). Make them agree, so Access knows which Resource is the table.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
/** Whether a Resource's name is the ownership beacon's table's: `AventaraBeacon`, `_aventara_beacon`, their plurals, in any case. */
|
|
233
|
+
const isBeaconName = (resource) => /^aventarabeacons?$/.test(resource.replace(/_/g, "").toLowerCase());
|
|
234
|
+
/**
|
|
235
|
+
* Closes every Resource that is the ownership beacon's table, whoever's it is: the
|
|
236
|
+
* Access server's (Better Auth's `aventaraBeacon`, found as its tables are) and one
|
|
237
|
+
* that only looks like it by name (a service's own `_aventara_beacon` in the rules'
|
|
238
|
+
* schema). Each is denied every action and its `value` sealed, whatever grants
|
|
239
|
+
* name it (`"*"` included): any deny wins. A grant on the Access server's table is
|
|
240
|
+
* refused as on any closed Better Auth table. A beacon is read by the startup check
|
|
241
|
+
* through the service's own connection, never through a grant.
|
|
242
|
+
*/
|
|
243
|
+
const closeBeaconTables = (policies, tables, denies, sealed) => {
|
|
244
|
+
const found = tables.get(BEACON_TABLE_MODEL)?.resource;
|
|
245
|
+
for (const [resource, facts] of Object.entries(policies.schema)) {
|
|
246
|
+
if (facts.foreign !== undefined ||
|
|
247
|
+
(resource !== found && !isBeaconName(resource))) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (!denies.some((deny) => deny.id === `${BUILT_IN_ID}${resource}`)) {
|
|
251
|
+
denies.push({
|
|
252
|
+
id: `${BUILT_IN_ID}${resource}`,
|
|
253
|
+
actions: "*",
|
|
254
|
+
resource,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const secrets = new Set(sealed.get(resource) ?? []);
|
|
258
|
+
const value = resource === found
|
|
259
|
+
? (tables.get(BEACON_TABLE_MODEL)?.fields.get("value") ?? "value")
|
|
260
|
+
: "value";
|
|
261
|
+
if (facts.fields.includes(value)) {
|
|
262
|
+
secrets.add(value);
|
|
263
|
+
}
|
|
264
|
+
if (secrets.size > 0) {
|
|
265
|
+
sealed.set(resource, secrets);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
/** The Access server's beacon table, by Better Auth model name. */
|
|
270
|
+
const BEACON_TABLE_MODEL = "aventaraBeacon";
|
|
271
|
+
/**
|
|
272
|
+
* The built-in rules for one set of rules: `resources` as the app gave it, Better
|
|
273
|
+
* Auth's tables as it is built (`authTablesOf`), and the rules' file. Throws
|
|
274
|
+
* `ACCESS_RESOURCE_MISSING` when a key's tables are missing, and
|
|
275
|
+
* `ACCESS_OPTIONS_INVALID` for a grant the built-in rules would leave without effect.
|
|
276
|
+
*/
|
|
277
|
+
export const guardAuthTables = (input) => {
|
|
278
|
+
const { resources, tables, naming, policies } = input;
|
|
279
|
+
if (naming.kind === "map") {
|
|
280
|
+
checkAuthTablesMap(naming.map, tables, policies);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
refuseDisagreement(tables, naming.usePlural, policies);
|
|
284
|
+
}
|
|
285
|
+
for (const key of KEYS) {
|
|
286
|
+
if (!resources[key]) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
for (const model of KEY_TABLES[key]) {
|
|
290
|
+
const table = tables.get(model);
|
|
291
|
+
const plugin = OPENED_TABLES[model]?.plugin;
|
|
292
|
+
if (table === undefined) {
|
|
293
|
+
missing(`\`resources.${key}\` opens Better Auth's \`${model}\` table, and Better Auth has none${plugin === undefined ? "" : `: add ${plugin} to betterAuth.plugins`}.`);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const facts = table.resource === undefined
|
|
297
|
+
? undefined
|
|
298
|
+
: ownOf(policies.schema, table.resource);
|
|
299
|
+
if (facts === undefined) {
|
|
300
|
+
missing(naming.kind === "prisma"
|
|
301
|
+
? `\`resources.${key}\` opens Better Auth's \`${model}\` table, and the rules' schema has no model Better Auth's Prisma adapter reads it through (\`prisma.${table.name}${naming.usePlural ? "s" : ""}\`). Run Better Auth's \`npx auth generate\`, then regenerate the model the rules are built from.`
|
|
302
|
+
: `\`resources.${key}\` opens Better Auth's \`${model}\` table, and \`resources.authTables\` puts it in no Resource of the rules' schema. Name its Resource there.`);
|
|
303
|
+
}
|
|
304
|
+
if (facts?.foreign !== undefined) {
|
|
305
|
+
refuse(`\`resources.${key}\` opens Better Auth's \`${model}\` table as the Resource ${table.resource}, which the rules read as namespace "${facts.foreign}"'s (foreign). Open it in the service that owns it.`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const denies = [];
|
|
310
|
+
const sealed = new Map();
|
|
311
|
+
const writable = new Map();
|
|
312
|
+
for (const table of tables.values()) {
|
|
313
|
+
const { resource } = table;
|
|
314
|
+
const facts = resource === undefined ? undefined : ownOf(policies.schema, resource);
|
|
315
|
+
if (resource === undefined ||
|
|
316
|
+
facts === undefined ||
|
|
317
|
+
facts.foreign !== undefined) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const opened = OPENED_TABLES[table.model];
|
|
321
|
+
const open = opened !== undefined && resources[opened.key];
|
|
322
|
+
const secrets = [];
|
|
323
|
+
for (const field of SECRET_COLUMNS[table.model] ?? []) {
|
|
324
|
+
const name = fieldOf(table, facts, field);
|
|
325
|
+
if (name !== undefined) {
|
|
326
|
+
secrets.push(name);
|
|
327
|
+
}
|
|
328
|
+
else if (open) {
|
|
329
|
+
// An opened table's secret must be found to be sealed: one the schema
|
|
330
|
+
// names otherwise (a Prisma `@map`) would be readable.
|
|
331
|
+
refuse(`Better Auth's \`${table.model}\` table keeps a secret in \`${field}\`, and ${resource} has no field \`${table.fields.get(field) ?? field}\`: Access could not keep it sealed. Name the field as Better Auth's \`fieldName\` for it says, or leave the table closed.`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (secrets.length > 0) {
|
|
335
|
+
sealed.set(resource, new Set(secrets));
|
|
336
|
+
}
|
|
337
|
+
const grants = policies.grants.filter((grant) => grant.resource === resource);
|
|
338
|
+
if (opened === undefined || !open) {
|
|
339
|
+
denies.push({
|
|
340
|
+
id: `${BUILT_IN_ID}${resource}`,
|
|
341
|
+
actions: "*",
|
|
342
|
+
resource,
|
|
343
|
+
});
|
|
344
|
+
const [grant] = grants;
|
|
345
|
+
if (grant !== undefined) {
|
|
346
|
+
refuse(opened === undefined
|
|
347
|
+
? `the rules grant ${actionList(grant.actions)} on ${resource} (grant "${grant.id}"), Better Auth's \`${table.model}\` table, which Access keeps closed: no grant opens it. Remove the grant.`
|
|
348
|
+
: `the rules grant ${actionList(grant.actions)} on ${resource} (grant "${grant.id}"), Better Auth's \`${table.model}\` table, which is closed until \`resources: { ${opened.key}: true }\` opens it as a Resource of the rules.`);
|
|
349
|
+
}
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const { writes } = opened;
|
|
353
|
+
const refused = ACTIONS.filter((action) => (action === "create" && writes.create === undefined) ||
|
|
354
|
+
(action === "update" && writes.update === undefined) ||
|
|
355
|
+
(action === "delete" && !writes.delete));
|
|
356
|
+
if (refused.length > 0) {
|
|
357
|
+
denies.push({
|
|
358
|
+
id: `${BUILT_IN_ID}${resource}/${refused.join(",")}`,
|
|
359
|
+
actions: refused,
|
|
360
|
+
resource,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
for (const grant of grants) {
|
|
364
|
+
const never = grant.actions.filter((action) => refused.includes(action));
|
|
365
|
+
if (never.length > 0) {
|
|
366
|
+
refuse(`the rules grant ${actionList(never)} on ${resource} (grant "${grant.id}"), Better Auth's \`${table.model}\` table, which Access never writes that way: a write through Aventara skips Better Auth's hooks and caches. Use Better Auth's own API for it, and remove ${actionList(never)} from the grant.`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
writable.set(resource, {
|
|
370
|
+
create: writableSet(table, facts, writes.create, true),
|
|
371
|
+
update: writableSet(table, facts, writes.update, false),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
closeBeaconTables(policies, tables, denies, sealed);
|
|
375
|
+
return { denies, sealed, writable };
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* `fieldsOf`'s answer with the built-in rules applied: a secret column is never
|
|
379
|
+
* listed, and a write lists only the fields its action may set on an opened table.
|
|
380
|
+
* `"all"` becomes the list it stands for (every field that is not governed) when
|
|
381
|
+
* something is taken out of it.
|
|
382
|
+
*/
|
|
383
|
+
export const guardedFields = (guard, question, answer, resource) => {
|
|
384
|
+
const secrets = guard.sealed.get(question.resource);
|
|
385
|
+
const sets = question.access === "write"
|
|
386
|
+
? guard.writable.get(question.resource)
|
|
387
|
+
: undefined;
|
|
388
|
+
const writes = sets === undefined || question.access !== "write"
|
|
389
|
+
? undefined
|
|
390
|
+
: question.action === "create"
|
|
391
|
+
? sets.create
|
|
392
|
+
: question.action === "update"
|
|
393
|
+
? sets.update
|
|
394
|
+
: new Set([...sets.create].filter((field) => sets.update.has(field)));
|
|
395
|
+
if (secrets === undefined && writes === undefined) {
|
|
396
|
+
return answer;
|
|
397
|
+
}
|
|
398
|
+
const keeps = (field) => !(secrets?.has(field) ?? false) && (writes?.has(field) ?? true);
|
|
399
|
+
return {
|
|
400
|
+
always: answer.always === "all"
|
|
401
|
+
? resource.fields.filter((field) => !resource.governed.includes(field) && keeps(field))
|
|
402
|
+
: answer.always.filter(keeps),
|
|
403
|
+
perRow: Object.fromEntries(Object.entries(answer.perRow).filter(([field]) => keeps(field))),
|
|
404
|
+
};
|
|
405
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { AccessAction, AccessComparison, AccessDenyRule, AccessGrantRule, AccessLiteral, AccessOrigin, AccessPolicies, AccessPredicate, AccessPrincipalCondition } from "./policies-contract.js";
|
|
2
|
+
/**
|
|
3
|
+
* A row condition after the principal fold. `and` is a condition the author wrote
|
|
4
|
+
* as one object (the translator may write it back as one); `all` is a conjunction
|
|
5
|
+
* the engine builds (grant parts, a scope's grants and denies), always written as
|
|
6
|
+
* `AND`.
|
|
7
|
+
*/
|
|
8
|
+
export type Resolved = {
|
|
9
|
+
readonly k: "true";
|
|
10
|
+
} | {
|
|
11
|
+
readonly k: "false";
|
|
12
|
+
} | {
|
|
13
|
+
readonly k: "and" | "all" | "or";
|
|
14
|
+
readonly args: readonly Resolved[];
|
|
15
|
+
} | {
|
|
16
|
+
readonly k: "not";
|
|
17
|
+
readonly arg: Resolved;
|
|
18
|
+
} | {
|
|
19
|
+
readonly k: "cmp";
|
|
20
|
+
readonly field: string;
|
|
21
|
+
readonly cmp: AccessComparison;
|
|
22
|
+
readonly value: AccessLiteral | readonly AccessLiteral[];
|
|
23
|
+
} | {
|
|
24
|
+
readonly k: "rel";
|
|
25
|
+
readonly relation: string;
|
|
26
|
+
readonly target: string;
|
|
27
|
+
readonly where: Resolved;
|
|
28
|
+
} | {
|
|
29
|
+
readonly k: "some" | "every" | "none";
|
|
30
|
+
readonly relation: string;
|
|
31
|
+
readonly target: string;
|
|
32
|
+
readonly where: Resolved;
|
|
33
|
+
};
|
|
34
|
+
export declare const TRUE: Resolved;
|
|
35
|
+
export declare const FALSE: Resolved;
|
|
36
|
+
/** A conjunction the author wrote as one object. */
|
|
37
|
+
export declare const andOf: (args: readonly Resolved[]) => Resolved;
|
|
38
|
+
/** A conjunction the engine builds. */
|
|
39
|
+
export declare const allOf: (args: readonly Resolved[]) => Resolved;
|
|
40
|
+
export declare const orOf: (args: readonly Resolved[]) => Resolved;
|
|
41
|
+
export declare const notOf: (arg: Resolved) => Resolved;
|
|
42
|
+
type Schema = AccessPolicies["schema"];
|
|
43
|
+
type Facts = Schema[string];
|
|
44
|
+
/**
|
|
45
|
+
* A record's own member: `undefined` for a key the record only inherits
|
|
46
|
+
* (`constructor`, `toString`, `__proto__`), so no name in a file, a row or a principal
|
|
47
|
+
* ever reads `Object.prototype`. One signature per record of the file, not a generic
|
|
48
|
+
* one: each call would instantiate it, and the lookups are many.
|
|
49
|
+
*/
|
|
50
|
+
export declare function ownOf(record: Schema | undefined, key: string): Facts | undefined;
|
|
51
|
+
export declare function ownOf(record: AccessPolicies["roles"] | undefined, key: string): AccessPolicies["roles"][string] | undefined;
|
|
52
|
+
export declare function ownOf(record: Facts["relations"] | undefined, key: string): Facts["relations"][string] | undefined;
|
|
53
|
+
export declare function ownOf(record: Facts["types"] | undefined, key: string): NonNullable<Facts["types"]>[string] | undefined;
|
|
54
|
+
export declare function ownOf(record: AccessPolicies["claims"] | undefined, key: string): AccessPolicies["claims"][string] | undefined;
|
|
55
|
+
export declare function ownOf(record: {
|
|
56
|
+
readonly [key: string]: readonly string[];
|
|
57
|
+
} | undefined, key: string): readonly string[] | undefined;
|
|
58
|
+
/** A plain object: made by `{}` or JSON, not an array, a `Date`, a `Map` or a class instance. */
|
|
59
|
+
export declare const isPlainObject: (value: unknown) => value is {
|
|
60
|
+
readonly [key: string]: unknown;
|
|
61
|
+
};
|
|
62
|
+
/** What every question states about who asks. */
|
|
63
|
+
export type PrincipalFacts = {
|
|
64
|
+
/** The principal: an object, or `null` (anonymous). */
|
|
65
|
+
readonly principal: {
|
|
66
|
+
readonly [key: string]: unknown;
|
|
67
|
+
} | null;
|
|
68
|
+
readonly origin: AccessOrigin;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Reads the principal off a question: any object, an instance of a class included
|
|
72
|
+
* (an entity), is a principal; an array, a function or a primitive is anonymous.
|
|
73
|
+
*/
|
|
74
|
+
export declare const principalFactsOf: (principal: unknown, origin: AccessOrigin) => PrincipalFacts;
|
|
75
|
+
/**
|
|
76
|
+
* Folds a condition on the principal alone (claims, origin) to a boolean. A claim
|
|
77
|
+
* the principal lacks, or holds as another type, fails closed: a grant's global role
|
|
78
|
+
* is not held, and a deny's `if` holds, whatever value it compares with. Access mints
|
|
79
|
+
* every declared boolean claim, `false` when unset, so a token it issued lacks none;
|
|
80
|
+
* a foreign or stale token without `banned` is denied by `banned: true`. The
|
|
81
|
+
* anonymous caller is the one exception: a comparison with `true` never holds for it.
|
|
82
|
+
*/
|
|
83
|
+
export declare const foldPrincipalCondition: (condition: AccessPrincipalCondition, facts: PrincipalFacts, rule: "grant" | "deny") => boolean;
|
|
84
|
+
/**
|
|
85
|
+
* How a rule reads a `me` value the principal lacks. A grant drops: it never
|
|
86
|
+
* matches, whatever `NOT` surrounds the value. A deny matches as a whole, whatever
|
|
87
|
+
* `not` or `NOT` surrounds the value. Both fail closed.
|
|
88
|
+
*/
|
|
89
|
+
export type MissingMode = "drop-rule" | "match-rule";
|
|
90
|
+
/**
|
|
91
|
+
* Substitutes the principal into a row condition. A `me` value the principal lacks
|
|
92
|
+
* makes the whole rule `false` for a grant (`drop-rule`) and `true` for a deny
|
|
93
|
+
* (`match-rule`).
|
|
94
|
+
*/
|
|
95
|
+
export declare const resolvePredicate: (policies: AccessPolicies, predicate: AccessPredicate, facts: PrincipalFacts, mode: MissingMode) => Resolved;
|
|
96
|
+
/**
|
|
97
|
+
* The grants and denies a question about `action` on `resource` reads. A grant on
|
|
98
|
+
* `"*"` reaches the file's own Resources only: never a foreign one, and never one
|
|
99
|
+
* its schema does not list. A deny on `"*"` reaches every Resource.
|
|
100
|
+
*/
|
|
101
|
+
export declare const rulesFor: (policies: AccessPolicies, resource: string, action: AccessAction) => {
|
|
102
|
+
grants: AccessGrantRule[];
|
|
103
|
+
denies: AccessDenyRule[];
|
|
104
|
+
};
|
|
105
|
+
/** One grant, for one of its subjects, folded for the principal. */
|
|
106
|
+
export type FoldedGrant = {
|
|
107
|
+
readonly grant: AccessGrantRule;
|
|
108
|
+
readonly subject: string;
|
|
109
|
+
readonly condition: Resolved;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Each grant's condition per subject: its `where`, then the subject's (its row
|
|
113
|
+
* role, through `in`), then, with `after`, the `after` condition. A grant that reads
|
|
114
|
+
* a `me` value the principal lacks drops.
|
|
115
|
+
*/
|
|
116
|
+
export declare const foldGrants: (policies: AccessPolicies, grants: readonly AccessGrantRule[], facts: PrincipalFacts, withAfter?: boolean) => FoldedGrant[];
|
|
117
|
+
/** One deny, folded for the principal. */
|
|
118
|
+
export type FoldedDeny = {
|
|
119
|
+
readonly deny: AccessDenyRule;
|
|
120
|
+
readonly condition: Resolved;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Each deny's row condition: `false` when its `if` does not hold for this principal,
|
|
124
|
+
* `true` when its `where` reads a `me` value the principal lacks.
|
|
125
|
+
*/
|
|
126
|
+
export declare const foldDenies: (policies: AccessPolicies, denies: readonly AccessDenyRule[], facts: PrincipalFacts) => FoldedDeny[];
|
|
127
|
+
/** The rows a principal may `action`: OR of the grants, AND NOT each deny. */
|
|
128
|
+
export declare const scopeCondition: (grants: readonly FoldedGrant[], denies: readonly FoldedDeny[]) => Resolved;
|
|
129
|
+
/**
|
|
130
|
+
* A folded condition on `resource` in the form a database reads as `can` does.
|
|
131
|
+
* `can` reads a comparison on a NULL field as false, so its `NOT` keeps the row; SQL
|
|
132
|
+
* reads it as unknown, and its `NOT` drops the row. Each negated comparison on a
|
|
133
|
+
* nullable field (the schema entry's `nullable`) gets its own guard, `NOT (f = v) OR
|
|
134
|
+
* f IS NULL`, and a negated to-one step whose reference is nullable gets `OR fk IS
|
|
135
|
+
* NULL`. A condition with neither is returned as it is.
|
|
136
|
+
*/
|
|
137
|
+
export declare const nullSafe: (policies: AccessPolicies, resource: string, node: Resolved) => Resolved;
|
|
138
|
+
export {};
|