@protobase/schema 0.12.7 → 0.12.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access/index.d.ts +3 -2
- package/dist/access/resolve.d.ts +4 -2
- package/dist/access/resolve.js +36 -14
- package/dist/access/role-entry.d.ts +22 -0
- package/dist/access/role-entry.js +55 -0
- package/dist/access/roles.d.ts +19 -3
- package/dist/access/roles.js +40 -9
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/package.json +1 -1
package/dist/access/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { AccessAction, AccessContext, AccessFn, AccessResult, AccessUser, FieldAccess, FieldRule } from "./types.js";
|
|
2
2
|
import { Capability, CapabilityAction, CapabilityScope } from "./capability.js";
|
|
3
|
+
import { CapabilityLimit, RoleEntry, anonymousRole } from "./role-entry.js";
|
|
3
4
|
import { Rule, rule } from "./rule.js";
|
|
4
|
-
import { Roles, RolesOptions, defineRoles } from "./roles.js";
|
|
5
|
+
import { RoleLimit, Roles, RolesOptions, defineRoles } from "./roles.js";
|
|
5
6
|
import { pickView } from "./pick-view.js";
|
|
6
7
|
import { AccessSource, ResolveOptions, ResolvedAccess, RowFilterOperation, accessWarnings, checkRecord, fieldsUsedBy, resolveAccess } from "./resolve.js";
|
|
7
8
|
import { restrictModel } from "./restrict.js";
|
|
8
|
-
export { type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, type Capability, type CapabilityAction, type CapabilityScope, type FieldAccess, type FieldRule, type ResolveOptions, type ResolvedAccess, type Roles, type RolesOptions, type RowFilterOperation, type Rule, accessWarnings, checkRecord, defineRoles, fieldsUsedBy, pickView, resolveAccess, restrictModel, rule };
|
|
9
|
+
export { type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, type Capability, type CapabilityAction, type CapabilityLimit, type CapabilityScope, type FieldAccess, type FieldRule, type ResolveOptions, type ResolvedAccess, type RoleEntry, type RoleLimit, type Roles, type RolesOptions, type RowFilterOperation, type Rule, accessWarnings, anonymousRole, checkRecord, defineRoles, fieldsUsedBy, pickView, resolveAccess, restrictModel, rule };
|
package/dist/access/resolve.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CheckedFilter, ResourceModel } from "../model.js";
|
|
2
|
-
import { AccessAction, AccessContext, AccessFn, FieldAccess } from "./types.js";
|
|
2
|
+
import { AccessAction, AccessContext, AccessFn, AccessUser, FieldAccess } from "./types.js";
|
|
3
3
|
import { EvaluateOptions } from "../filter/evaluate.js";
|
|
4
4
|
import "../filter/index.js";
|
|
5
5
|
import { Rule } from "./rule.js";
|
|
@@ -14,10 +14,12 @@ export type ResolveOptions = {
|
|
|
14
14
|
/**
|
|
15
15
|
* The project's roles. When given, an operation without a rule is denied unless the user's
|
|
16
16
|
* roles grant `<resource>.<action>` (so admin's `*` still passes). Fields without a rule
|
|
17
|
-
* follow their resource.
|
|
17
|
+
* follow their resource, within the fields the roles list for the capability. With an `anonymous` role, a caller
|
|
18
|
+
* holding only that role needs its capability even where a rule or `defaultAccess` would allow.
|
|
18
19
|
*/
|
|
19
20
|
roles?: {
|
|
20
21
|
can: (capability: any) => Rule;
|
|
22
|
+
fields?: (user: AccessUser, capability: string) => readonly string[] | undefined;
|
|
21
23
|
definitions?: Record<string, unknown>;
|
|
22
24
|
};
|
|
23
25
|
/**
|
package/dist/access/resolve.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { checkFilter } from "../filter/check.js";
|
|
2
2
|
import { evaluateFilter } from "../filter/evaluate.js";
|
|
3
3
|
import { andResults, normalize } from "./combine.js";
|
|
4
|
+
import "./role-entry.js";
|
|
4
5
|
//#region src/access/resolve.ts
|
|
5
6
|
/** Startup warnings for risky option combinations; the caller decides how to report them. */
|
|
6
7
|
const accessWarnings = (options) => options.roles && options.defaultAccess === "allow" ? ["Roles are configured but defaultAccess is \"allow\": operations without an access rule are open to every user."] : [];
|
|
@@ -11,12 +12,19 @@ const operations = [
|
|
|
11
12
|
"update",
|
|
12
13
|
"delete"
|
|
13
14
|
];
|
|
14
|
-
const
|
|
15
|
-
|
|
15
|
+
const capabilityOf = (model, operation) => `${model.name}.${operation === "list" ? "read" : operation}`;
|
|
16
|
+
const explicitRule = (source, operation) => source.accessRules[operation] ?? (operation === "list" ? source.accessRules.read : void 0);
|
|
17
|
+
const anonymousCaller = (options, user) => {
|
|
18
|
+
const roles = user?.roles ?? [];
|
|
19
|
+
return Object.hasOwn(options.roles?.definitions ?? {}, "anonymous") && roles.length > 0 && roles.every((role) => role === "anonymous");
|
|
20
|
+
};
|
|
21
|
+
const ruleFor = (source, model, operation, options, anonymous = false) => {
|
|
22
|
+
const explicit = explicitRule(source, operation);
|
|
16
23
|
if (explicit) return explicit;
|
|
24
|
+
if (anonymous) return options.roles.can(capabilityOf(model, operation));
|
|
17
25
|
if (options.defaultAccess) return options.defaultAccess === "allow";
|
|
18
26
|
if (!options.roles) return true;
|
|
19
|
-
return options.roles.can(
|
|
27
|
+
return options.roles.can(capabilityOf(model, operation));
|
|
20
28
|
};
|
|
21
29
|
const checked = (model, expr, operation) => {
|
|
22
30
|
const trusted = {
|
|
@@ -54,25 +62,30 @@ const resolveAccess = async (source, ctx, options = {}) => {
|
|
|
54
62
|
const allowed = {};
|
|
55
63
|
const recordChecks = [];
|
|
56
64
|
const filters = {};
|
|
65
|
+
const anonymous = anonymousCaller(options, ctx.user);
|
|
57
66
|
for (const operation of operations) {
|
|
58
67
|
if (operation === "list" && !source.accessRules.list) {
|
|
59
68
|
allowed.list = allowed.read;
|
|
60
69
|
if (recordChecks.includes("read")) recordChecks.push("list");
|
|
61
70
|
continue;
|
|
62
71
|
}
|
|
63
|
-
const
|
|
64
|
-
const decision = typeof rule === "boolean" ? rule : await decide(rule, {
|
|
72
|
+
const context = {
|
|
65
73
|
...ctx,
|
|
66
74
|
model,
|
|
67
75
|
operation
|
|
68
|
-
}
|
|
69
|
-
|
|
76
|
+
};
|
|
77
|
+
const rule = ruleFor(source, model, operation, options, anonymous);
|
|
78
|
+
const decision = typeof rule === "boolean" ? rule : await decide(rule, context);
|
|
79
|
+
const required = anonymous && explicitRule(source, operation) ? normalize(await options.roles.can(capabilityOf(model, operation))(context)) : true;
|
|
80
|
+
if (required === false) allowed[operation] = false;
|
|
81
|
+
else if (decision === "record") {
|
|
70
82
|
allowed[operation] = true;
|
|
71
83
|
recordChecks.push(operation);
|
|
72
|
-
|
|
73
|
-
else {
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
if (required !== true) filters[operation] = required;
|
|
85
|
+
} else {
|
|
86
|
+
const combined = andResults(decision, required);
|
|
87
|
+
allowed[operation] = combined !== false;
|
|
88
|
+
if (typeof combined === "object") filters[operation] = combined;
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
91
|
const fieldRules = source.fieldAccess();
|
|
@@ -81,6 +94,13 @@ const resolveAccess = async (source, ctx, options = {}) => {
|
|
|
81
94
|
model,
|
|
82
95
|
operation
|
|
83
96
|
});
|
|
97
|
+
const listed = (operation) => options.roles?.fields?.(ctx.user, capabilityOf(model, operation));
|
|
98
|
+
const lists = {
|
|
99
|
+
read: listed("read"),
|
|
100
|
+
create: listed("create"),
|
|
101
|
+
update: listed("update")
|
|
102
|
+
};
|
|
103
|
+
const inList = (list, name) => list === void 0 || list.includes(name);
|
|
84
104
|
const names = Object.keys(model.fields);
|
|
85
105
|
const readableFields = [];
|
|
86
106
|
const writable = {
|
|
@@ -90,11 +110,11 @@ const resolveAccess = async (source, ctx, options = {}) => {
|
|
|
90
110
|
for (const name of names) {
|
|
91
111
|
const rules = fieldRules[name];
|
|
92
112
|
const field = model.fields[name];
|
|
93
|
-
if (allowed.list || allowed.read) {
|
|
113
|
+
if ((allowed.list || allowed.read) && inList(lists.read, name)) {
|
|
94
114
|
if (await verdict(rules?.read, fieldContext("read"), true, `The read rule of field "${name}"`)) readableFields.push(name);
|
|
95
115
|
}
|
|
96
116
|
for (const operation of ["create", "update"]) {
|
|
97
|
-
if (!allowed[operation] || field.readOnly) continue;
|
|
117
|
+
if (!allowed[operation] || field.readOnly || !inList(lists[operation], name)) continue;
|
|
98
118
|
if (await verdict(rules?.[operation], fieldContext(operation), true, `The ${operation} rule of field "${name}"`)) writable[operation].push(name);
|
|
99
119
|
}
|
|
100
120
|
}
|
|
@@ -123,7 +143,9 @@ const resolveAccess = async (source, ctx, options = {}) => {
|
|
|
123
143
|
*/
|
|
124
144
|
const checkRecord = async (source, ctx, operation, record, input, options = {}) => {
|
|
125
145
|
const model = source.toModel();
|
|
126
|
-
const
|
|
146
|
+
const anonymous = anonymousCaller(options, ctx.user);
|
|
147
|
+
const found = ruleFor(source, model, operation, options, anonymous);
|
|
148
|
+
const rule = anonymous && explicitRule(source, operation) ? options.roles.can(capabilityOf(model, operation)).and(found) : found;
|
|
127
149
|
if (typeof rule === "boolean") return rule;
|
|
128
150
|
const result = normalize(await rule({
|
|
129
151
|
...ctx,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FilterExpr } from "../model.js";
|
|
2
|
+
import { Capability } from "./capability.js";
|
|
3
|
+
//#region src/access/role-entry.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* What a capability is limited to: the fields it allows, an allowlist (`['sku', 'price']`), a row filter in AIP-160
|
|
6
|
+
* text that is ANDed into its rows (`{ filter: 'published = true' }`), or both (`{ fields: [...], filter: '...' }`).
|
|
7
|
+
*/
|
|
8
|
+
export type CapabilityLimit = readonly string[] | {
|
|
9
|
+
readonly fields?: readonly string[];
|
|
10
|
+
readonly filter?: string;
|
|
11
|
+
};
|
|
12
|
+
/** One entry of a role: a capability, or capabilities each with its limit, `{ 'products.read': ['sku', 'price'] }`. */
|
|
13
|
+
export type RoleEntry<Name extends string = string> = Capability<Name> | { readonly [C in Capability<Name>]?: CapabilityLimit; };
|
|
14
|
+
/** A capability a role holds, with the fields and rows it is limited to when it has a limit. */
|
|
15
|
+
export type HeldCapability = {
|
|
16
|
+
capability: string;
|
|
17
|
+
fields?: readonly string[];
|
|
18
|
+
filter?: FilterExpr;
|
|
19
|
+
};
|
|
20
|
+
/** Requests without a token act as this role, when the project defines it. */
|
|
21
|
+
export declare const anonymousRole = "anonymous";
|
|
22
|
+
//#endregion
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { parseFilter } from "../filter/parse.js";
|
|
2
|
+
import { parseCapability } from "./capability.js";
|
|
3
|
+
//#region src/access/role-entry.ts
|
|
4
|
+
/** Requests without a token act as this role, when the project defines it. */
|
|
5
|
+
const anonymousRole = "anonymous";
|
|
6
|
+
const fieldActions = [
|
|
7
|
+
"read",
|
|
8
|
+
"create",
|
|
9
|
+
"update"
|
|
10
|
+
];
|
|
11
|
+
const parsedFilter = (role, capability, text) => {
|
|
12
|
+
const parsed = parseFilter(text);
|
|
13
|
+
if (!parsed.ok || !parsed.ast) throw new Error(`Role "${role}" limits "${capability}" to an invalid filter "${text}": ${parsed.errors[0]?.message ?? "empty"}`);
|
|
14
|
+
return parsed.ast;
|
|
15
|
+
};
|
|
16
|
+
const limited = (role, capability, limit) => {
|
|
17
|
+
const { name, action } = parseCapability(capability);
|
|
18
|
+
const { fields, filter } = Array.isArray(limit) ? {
|
|
19
|
+
fields: limit,
|
|
20
|
+
filter: void 0
|
|
21
|
+
} : limit;
|
|
22
|
+
const unknown = Array.isArray(limit) ? [] : Object.keys(limit).filter((key) => key !== "fields" && key !== "filter");
|
|
23
|
+
if (unknown.length > 0) throw new Error(`Role "${role}" limits "${capability}" with "${unknown[0]}"; a limit has \`fields\` and \`filter\``);
|
|
24
|
+
if (fields === void 0 && filter === void 0) throw new Error(`Role "${role}" limits "${capability}" to nothing; give \`fields\`, \`filter\` or both, or the plain capability`);
|
|
25
|
+
if (name === "*") throw new Error(`Role "${role}" limits "${capability}" but fields and filters belong to one resource; name it instead of "*"`);
|
|
26
|
+
if (fields !== void 0 && !fieldActions.includes(action)) throw new Error(`Role "${role}" lists fields for "${capability}"; fields apply to ${fieldActions.join(", ")}`);
|
|
27
|
+
return {
|
|
28
|
+
capability,
|
|
29
|
+
...fields !== void 0 && { fields: [...fields] },
|
|
30
|
+
...filter !== void 0 && { filter: parsedFilter(role, capability, filter) }
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
const checkAnonymous = (held) => {
|
|
34
|
+
for (const { capability, fields } of held) {
|
|
35
|
+
const { name, action, scope } = parseCapability(capability);
|
|
36
|
+
if (action !== "read") throw new Error(`The anonymous role may only read, but it grants "${capability}"`);
|
|
37
|
+
if (name === "*") throw new Error(`The anonymous role must name each resource it reads, not "${capability}"`);
|
|
38
|
+
if (scope !== void 0) throw new Error(`The anonymous role grants "${capability}", but an anonymous caller owns no rows`);
|
|
39
|
+
if (!fields) throw new Error(`The anonymous role must list the fields it reads: { '${capability}': ['name', ...] }`);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/** A role's entries as the capabilities it holds; throws on anything that is not a capability or a valid limit. */
|
|
43
|
+
const heldCapabilities = (role, entries) => {
|
|
44
|
+
const held = entries.flatMap((entry) => {
|
|
45
|
+
if (typeof entry === "string") {
|
|
46
|
+
parseCapability(entry);
|
|
47
|
+
return [{ capability: entry }];
|
|
48
|
+
}
|
|
49
|
+
return Object.entries(entry).map(([capability, limit]) => limited(role, capability, limit));
|
|
50
|
+
});
|
|
51
|
+
if (role === "anonymous") checkAnonymous(held);
|
|
52
|
+
return held;
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
export { anonymousRole, heldCapabilities };
|
package/dist/access/roles.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AccessUser } from "./types.js";
|
|
2
2
|
import { Capability, Grant } from "./capability.js";
|
|
3
|
+
import { HeldCapability, RoleEntry } from "./role-entry.js";
|
|
3
4
|
import { Rule } from "./rule.js";
|
|
4
5
|
//#region src/access/roles.d.ts
|
|
5
6
|
export type RolesOptions = {
|
|
@@ -8,19 +9,34 @@ export type RolesOptions = {
|
|
|
8
9
|
/** Resource and area names; when given, unknown names in capabilities throw at definition. */
|
|
9
10
|
names?: readonly string[];
|
|
10
11
|
};
|
|
12
|
+
/** A capability of a role that is limited to fields or rows; the server checks them against the resources at startup. */
|
|
13
|
+
export type RoleLimit = HeldCapability & {
|
|
14
|
+
role: string;
|
|
15
|
+
};
|
|
11
16
|
/**
|
|
12
17
|
* Defines roles as bundles of capability strings. Pass the resource and area names as the type
|
|
13
18
|
* parameter to reject unknown names at compile time:
|
|
14
19
|
* `defineRoles<'orders' | 'costs'>({ sales: ['orders.read.own'] })`.
|
|
20
|
+
*
|
|
21
|
+
* An entry may limit capabilities to fields (an allowlist) and rows (an AIP-160 filter):
|
|
22
|
+
* `{ 'products.read': ['sku', 'price'] }`, `{ 'products.read': { fields: ['name'], filter: 'published = true' } }`.
|
|
23
|
+
* The role `anonymous`, when defined, is what requests without a token act as; it may only read listed fields.
|
|
15
24
|
*/
|
|
16
|
-
export declare const defineRoles: <Name extends string = string>(definitions: Record<string, readonly
|
|
17
|
-
definitions: Record<string, readonly
|
|
25
|
+
export declare const defineRoles: <Name extends string = string>(definitions: Record<string, readonly RoleEntry<Name>[]>, options?: RolesOptions) => {
|
|
26
|
+
definitions: Record<string, readonly RoleEntry<Name>[]>;
|
|
18
27
|
grants: (user: AccessUser, capability: string) => Set<Grant>;
|
|
28
|
+
/** The capabilities limited to fields or rows, by role. */
|
|
29
|
+
limits: RoleLimit[];
|
|
19
30
|
/** Whether the user holds the capability at all, in any scope. For areas and field rules. */
|
|
20
31
|
has: (user: AccessUser, capability: Capability<Name>) => boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The fields the user's roles allow for a capability: the union of their lists, or `undefined` when a role grants
|
|
34
|
+
* it without a list (every field) or none grants it.
|
|
35
|
+
*/
|
|
36
|
+
fields: (user: AccessUser, capability: string) => readonly string[] | undefined;
|
|
21
37
|
/**
|
|
22
38
|
* A rule that allows when the user's roles grant the capability: `true` for an unscoped
|
|
23
|
-
* grant, a row filter for `.own` and
|
|
39
|
+
* grant, a row filter for `.own`, `.team` and a limit's filter, `false` otherwise.
|
|
24
40
|
*/
|
|
25
41
|
can: (capability: Capability<Name>) => Rule;
|
|
26
42
|
/** A rule that allows when the user has any of the roles. */
|
package/dist/access/roles.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { orResults, where } from "./combine.js";
|
|
1
|
+
import { andResults, orResults, where } from "./combine.js";
|
|
2
2
|
import { grantsOf, parseCapability } from "./capability.js";
|
|
3
|
+
import { heldCapabilities } from "./role-entry.js";
|
|
3
4
|
import { rule } from "./rule.js";
|
|
4
5
|
//#region src/access/roles.ts
|
|
5
6
|
const rolesOf = (user) => user.roles ?? [];
|
|
@@ -7,17 +8,22 @@ const rolesOf = (user) => user.roles ?? [];
|
|
|
7
8
|
* Defines roles as bundles of capability strings. Pass the resource and area names as the type
|
|
8
9
|
* parameter to reject unknown names at compile time:
|
|
9
10
|
* `defineRoles<'orders' | 'costs'>({ sales: ['orders.read.own'] })`.
|
|
11
|
+
*
|
|
12
|
+
* An entry may limit capabilities to fields (an allowlist) and rows (an AIP-160 filter):
|
|
13
|
+
* `{ 'products.read': ['sku', 'price'] }`, `{ 'products.read': { fields: ['name'], filter: 'published = true' } }`.
|
|
14
|
+
* The role `anonymous`, when defined, is what requests without a token act as; it may only read listed fields.
|
|
10
15
|
*/
|
|
11
16
|
const defineRoles = (definitions, options = {}) => {
|
|
12
|
-
|
|
17
|
+
const held = new Map(Object.entries(definitions).map(([role, entries]) => [role, heldCapabilities(role, entries)]));
|
|
18
|
+
for (const [role, capabilities] of held) for (const { capability } of capabilities) {
|
|
13
19
|
const { name } = parseCapability(capability);
|
|
14
20
|
if (options.names && name !== "*" && !options.names.includes(name)) throw new Error(`Role "${role}" grants "${capability}" but "${name}" is not a known resource or area`);
|
|
15
21
|
}
|
|
22
|
+
const heldBy = (user) => rolesOf(user).flatMap((role) => held.get(role) ?? []);
|
|
16
23
|
/** Scopes granted by the user's roles for a capability, without applying them. */
|
|
17
24
|
const grants = (user, capability) => {
|
|
18
25
|
const wanted = parseCapability(capability);
|
|
19
|
-
|
|
20
|
-
return new Set(held.flatMap((capability) => grantsOf(capability, wanted)));
|
|
26
|
+
return new Set(heldBy(user).flatMap((held) => grantsOf(held.capability, wanted)));
|
|
21
27
|
};
|
|
22
28
|
const scoped = async (ctx, scope, capability) => {
|
|
23
29
|
const owner = ctx.model?.owner;
|
|
@@ -30,18 +36,43 @@ const defineRoles = (definitions, options = {}) => {
|
|
|
30
36
|
return {
|
|
31
37
|
definitions,
|
|
32
38
|
grants,
|
|
39
|
+
/** The capabilities limited to fields or rows, by role. */
|
|
40
|
+
limits: [...held].flatMap(([role, capabilities]) => capabilities.filter((held) => held.fields || held.filter).map((held) => ({
|
|
41
|
+
role,
|
|
42
|
+
...held
|
|
43
|
+
}))),
|
|
33
44
|
/** Whether the user holds the capability at all, in any scope. For areas and field rules. */
|
|
34
45
|
has: (user, capability) => grants(user, capability).size > 0,
|
|
35
46
|
/**
|
|
47
|
+
* The fields the user's roles allow for a capability: the union of their lists, or `undefined` when a role grants
|
|
48
|
+
* it without a list (every field) or none grants it.
|
|
49
|
+
*/
|
|
50
|
+
fields: (user, capability) => {
|
|
51
|
+
const wanted = parseCapability(capability);
|
|
52
|
+
const matching = heldBy(user).filter((held) => grantsOf(held.capability, wanted).length > 0);
|
|
53
|
+
if (matching.length === 0 || matching.some((held) => !held.fields)) return void 0;
|
|
54
|
+
return [...new Set(matching.flatMap((held) => held.fields))];
|
|
55
|
+
},
|
|
56
|
+
/**
|
|
36
57
|
* A rule that allows when the user's roles grant the capability: `true` for an unscoped
|
|
37
|
-
* grant, a row filter for `.own` and
|
|
58
|
+
* grant, a row filter for `.own`, `.team` and a limit's filter, `false` otherwise.
|
|
38
59
|
*/
|
|
39
60
|
can: (capability) => {
|
|
40
|
-
parseCapability(capability);
|
|
61
|
+
const wanted = parseCapability(capability);
|
|
41
62
|
return rule(async (ctx) => {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
const matching = heldBy(ctx.user ?? {}).flatMap((held) => grantsOf(held.capability, wanted).map((grant) => ({
|
|
64
|
+
grant,
|
|
65
|
+
filter: held.filter
|
|
66
|
+
})));
|
|
67
|
+
if (matching.some(({ grant, filter }) => grant === "all" && !filter)) return true;
|
|
68
|
+
const unfiltered = [...new Set(matching.filter(({ filter }) => !filter).map(({ grant }) => grant))].map((grant) => ({
|
|
69
|
+
grant,
|
|
70
|
+
filter: void 0
|
|
71
|
+
}));
|
|
72
|
+
return (await Promise.all([...unfiltered, ...matching.filter(({ filter }) => filter)].map(async ({ grant, filter }) => {
|
|
73
|
+
const rows = grant === "all" ? true : await scoped(ctx, grant, capability);
|
|
74
|
+
return filter ? andResults(rows, filter) : rows;
|
|
75
|
+
}))).reduce((result, filter) => orResults(result, filter), false);
|
|
45
76
|
});
|
|
46
77
|
},
|
|
47
78
|
/** A rule that allows when the user has any of the roles. */
|
package/dist/index.d.ts
CHANGED
|
@@ -34,8 +34,9 @@ import { OrderByCheck, OrderByParse, checkOrderBy, parseOrderBy, printOrderBy }
|
|
|
34
34
|
import { createWhere, where } from "./filter/where.js";
|
|
35
35
|
import "./filter/index.js";
|
|
36
36
|
import { Capability, CapabilityAction, CapabilityScope } from "./access/capability.js";
|
|
37
|
+
import { CapabilityLimit, RoleEntry, anonymousRole } from "./access/role-entry.js";
|
|
37
38
|
import { Rule, rule } from "./access/rule.js";
|
|
38
|
-
import { Roles, RolesOptions, defineRoles } from "./access/roles.js";
|
|
39
|
+
import { RoleLimit, Roles, RolesOptions, defineRoles } from "./access/roles.js";
|
|
39
40
|
import { pickView } from "./access/pick-view.js";
|
|
40
41
|
import { AccessSource, ResolveOptions, ResolvedAccess, RowFilterOperation, accessWarnings, checkRecord, fieldsUsedBy, resolveAccess } from "./access/resolve.js";
|
|
41
42
|
import { restrictModel } from "./access/restrict.js";
|
|
@@ -43,4 +44,4 @@ import "./access/index.js";
|
|
|
43
44
|
import { AssistantAction, AssistantActionRequest, AssistantCardPart, AssistantDiff, AssistantEvent, AssistantMessage, AssistantMessageRequest, AssistantPart, AssistantState, AssistantStep, AssistantStepState, AssistantTablePart, AssistantTextPart, AssistantTone, AssistantWidgetPart } from "./assistant/protocol.js";
|
|
44
45
|
import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
|
|
45
46
|
import { readEventStream } from "./assistant/event-stream.js";
|
|
46
|
-
export { type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, type ActionModel, type ActionRun, type AddressCheck, type AppAddress, type AssistantAction, type AssistantActionRequest, type AssistantCardPart, type AssistantDiff, type AssistantEvent, type AssistantMessage, type AssistantMessageRequest, type AssistantPart, type AssistantState, type AssistantStep, type AssistantStepState, type AssistantTablePart, type AssistantTextPart, type AssistantTone, type AssistantWidgetPart, type BottomBarItemModel, type BottomBarModel, type BottomBarNames, type BottomBarSource, type Capability, type CapabilityAction, type CapabilityScope, type ChartGranularity, type ChartModel, type ChartRange, type CheckResult, type CheckedFilter, type CompareOp, type ComponentRef, type CustomSectionModel, type DatePreset, type DurationLiteral, type DurationUnit, type EvaluateOptions, type EventActor, type EventContext, type EventHelpers, type EventMatch, type EventModel, type EventOperation, type EventSpec, type Field, type FieldAccess, type FieldFormat, type FieldModel, type FieldPath, type FieldRefValue, type FieldRule, type FieldType, type FieldViewModel, type FieldWidgetModel, type FileField, type FileFieldModel, type FileProcessor, type FileProcessorInput, type FilterError, type FilterErrorCode, type FilterExpr, type FilterLayout, type FilterLimits, type FilterOf, type FilterParse, type FilterValue, type FilterWidgetModel, type InferRecord, type JsonValue, type KeyType, type KeyValue, type LayoutItemModel, type ListDisplayModel, type ListModel, type ListQuery, type Literal, type NavModel, type NavRecentModel, type NowValue, type OrderByCheck, type OrderByItem, type OrderByParse, type PluginHost, type PluginRequirement, type PropShapes, type RecordKey, type RefShape, type RelatedModel, type ResolveOptions, type ResolvedAccess, type ResourceModel, type Roles, type RolesOptions, type RowFilterOperation, type Rule, type SaveFeedback, type SearchMatch, type SearchResultModel, type SearchTerm, type SortSpec, type Span, type StatusTone, type UserMenuItemModel, type UserMenuModel, type UserMenuSource, type ViewModel, type ViewProps, accessWarnings, addressErrors, addressPlaceholders, addressWords, appHrefError, appPath, applyAssistantEvent, bottomBar, bottomBarErrors, bottomBarFor, checkEventFilter, checkFilter, checkOrderBy, checkRecord, checkedAccept, checkedProvider, createWhere, decodeKey, defaultRecentLimit, defineRoles, displayFields, emptyAssistantState, encodeKey, enumLabel, evaluateFilter, f, fieldRef, fieldRefsIn, fieldsUsedBy, fillAddress, hrefPlaceholders, inRange, isFieldRef, keyTypes, layout as l, leavesApp, markFieldRefs, matchedSearchField, matchesSearch, maxBottomBarItems, navRecentErrors, parseFilter, parseOrderBy, parseSize, pickView, pluginProblem, printFilter, printOrderBy, pruneByShape, readAppPath, readEventStream, redactSecrets, resolveAccess, resource, restrictModel, rule, searchTerms, shapes, statusTones, tableDisplay, termMatches, unmarkFieldRefs, userMenu, view, webhookResources, where };
|
|
47
|
+
export { type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, type ActionModel, type ActionRun, type AddressCheck, type AppAddress, type AssistantAction, type AssistantActionRequest, type AssistantCardPart, type AssistantDiff, type AssistantEvent, type AssistantMessage, type AssistantMessageRequest, type AssistantPart, type AssistantState, type AssistantStep, type AssistantStepState, type AssistantTablePart, type AssistantTextPart, type AssistantTone, type AssistantWidgetPart, type BottomBarItemModel, type BottomBarModel, type BottomBarNames, type BottomBarSource, type Capability, type CapabilityAction, type CapabilityLimit, type CapabilityScope, type ChartGranularity, type ChartModel, type ChartRange, type CheckResult, type CheckedFilter, type CompareOp, type ComponentRef, type CustomSectionModel, type DatePreset, type DurationLiteral, type DurationUnit, type EvaluateOptions, type EventActor, type EventContext, type EventHelpers, type EventMatch, type EventModel, type EventOperation, type EventSpec, type Field, type FieldAccess, type FieldFormat, type FieldModel, type FieldPath, type FieldRefValue, type FieldRule, type FieldType, type FieldViewModel, type FieldWidgetModel, type FileField, type FileFieldModel, type FileProcessor, type FileProcessorInput, type FilterError, type FilterErrorCode, type FilterExpr, type FilterLayout, type FilterLimits, type FilterOf, type FilterParse, type FilterValue, type FilterWidgetModel, type InferRecord, type JsonValue, type KeyType, type KeyValue, type LayoutItemModel, type ListDisplayModel, type ListModel, type ListQuery, type Literal, type NavModel, type NavRecentModel, type NowValue, type OrderByCheck, type OrderByItem, type OrderByParse, type PluginHost, type PluginRequirement, type PropShapes, type RecordKey, type RefShape, type RelatedModel, type ResolveOptions, type ResolvedAccess, type ResourceModel, type RoleEntry, type RoleLimit, type Roles, type RolesOptions, type RowFilterOperation, type Rule, type SaveFeedback, type SearchMatch, type SearchResultModel, type SearchTerm, type SortSpec, type Span, type StatusTone, type UserMenuItemModel, type UserMenuModel, type UserMenuSource, type ViewModel, type ViewProps, accessWarnings, addressErrors, addressPlaceholders, addressWords, anonymousRole, appHrefError, appPath, applyAssistantEvent, bottomBar, bottomBarErrors, bottomBarFor, checkEventFilter, checkFilter, checkOrderBy, checkRecord, checkedAccept, checkedProvider, createWhere, decodeKey, defaultRecentLimit, defineRoles, displayFields, emptyAssistantState, encodeKey, enumLabel, evaluateFilter, f, fieldRef, fieldRefsIn, fieldsUsedBy, fillAddress, hrefPlaceholders, inRange, isFieldRef, keyTypes, layout as l, leavesApp, markFieldRefs, matchedSearchField, matchesSearch, maxBottomBarItems, navRecentErrors, parseFilter, parseOrderBy, parseSize, pickView, pluginProblem, printFilter, printOrderBy, pruneByShape, readAppPath, readEventStream, redactSecrets, resolveAccess, resource, restrictModel, rule, searchTerms, shapes, statusTones, tableDisplay, termMatches, unmarkFieldRefs, userMenu, view, webhookResources, where };
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { evaluateFilter } from "./filter/evaluate.js";
|
|
|
6
6
|
import { printFilter } from "./filter/print.js";
|
|
7
7
|
import { checkOrderBy, parseOrderBy, printOrderBy } from "./filter/order-by.js";
|
|
8
8
|
import { createWhere, where } from "./filter/where.js";
|
|
9
|
+
import { anonymousRole } from "./access/role-entry.js";
|
|
9
10
|
import { accessWarnings, checkRecord, fieldsUsedBy, resolveAccess } from "./access/resolve.js";
|
|
10
11
|
import { checkEventFilter } from "./events/event-models.js";
|
|
11
12
|
import { resource } from "./resource.js";
|
|
@@ -33,4 +34,4 @@ import { pickView } from "./access/pick-view.js";
|
|
|
33
34
|
import { restrictModel } from "./access/restrict.js";
|
|
34
35
|
import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
|
|
35
36
|
import { readEventStream } from "./assistant/event-stream.js";
|
|
36
|
-
export { accessWarnings, addressErrors, addressPlaceholders, addressWords, appHrefError, appPath, applyAssistantEvent, bottomBar, bottomBarErrors, bottomBarFor, checkEventFilter, checkFilter, checkOrderBy, checkRecord, checkedAccept, checkedProvider, createWhere, decodeKey, defaultRecentLimit, defineRoles, displayFields, emptyAssistantState, encodeKey, enumLabel, evaluateFilter, f, fieldRef, fieldRefsIn, fieldsUsedBy, fillAddress, hrefPlaceholders, inRange, isFieldRef, keyTypes, layout as l, leavesApp, markFieldRefs, matchedSearchField, matchesSearch, maxBottomBarItems, navRecentErrors, parseFilter, parseOrderBy, parseSize, pickView, pluginProblem, printFilter, printOrderBy, pruneByShape, readAppPath, readEventStream, redactSecrets, resolveAccess, resource, restrictModel, rule, searchTerms, shapes, statusTones, tableDisplay, termMatches, unmarkFieldRefs, userMenu, view, webhookResources, where };
|
|
37
|
+
export { accessWarnings, addressErrors, addressPlaceholders, addressWords, anonymousRole, appHrefError, appPath, applyAssistantEvent, bottomBar, bottomBarErrors, bottomBarFor, checkEventFilter, checkFilter, checkOrderBy, checkRecord, checkedAccept, checkedProvider, createWhere, decodeKey, defaultRecentLimit, defineRoles, displayFields, emptyAssistantState, encodeKey, enumLabel, evaluateFilter, f, fieldRef, fieldRefsIn, fieldsUsedBy, fillAddress, hrefPlaceholders, inRange, isFieldRef, keyTypes, layout as l, leavesApp, markFieldRefs, matchedSearchField, matchesSearch, maxBottomBarItems, navRecentErrors, parseFilter, parseOrderBy, parseSize, pickView, pluginProblem, printFilter, printOrderBy, pruneByShape, readAppPath, readEventStream, redactSecrets, resolveAccess, resource, restrictModel, rule, searchTerms, shapes, statusTones, tableDisplay, termMatches, unmarkFieldRefs, userMenu, view, webhookResources, where };
|