@protobase/schema 0.60.1 → 0.62.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/dist/access/resolve.d.ts +2 -2
- package/dist/access/resolve.js +1 -1
- package/dist/access/roles.d.ts +6 -3
- package/dist/access/roles.js +8 -5
- package/dist/model.d.ts +2 -0
- package/dist/resource-model.d.ts +1 -0
- package/dist/resource-model.js +1 -0
- package/dist/resource.d.ts +6 -0
- package/dist/resource.js +8 -0
- package/dist/webhooks/webhook-resources.js +7 -4
- package/package.json +1 -1
package/dist/access/resolve.d.ts
CHANGED
|
@@ -24,8 +24,8 @@ export type ResolveOptions = {
|
|
|
24
24
|
roles?: {
|
|
25
25
|
/** The rule allowing a capability, as `defineRoles` makes it. */
|
|
26
26
|
can: (capability: any) => Rule;
|
|
27
|
-
/** The fields the user's roles list for a capability, or `undefined` for every field. */
|
|
28
|
-
fields?: (user: AccessUser, capability: string) => readonly string[] | undefined;
|
|
27
|
+
/** The fields the user's roles list for a capability on the resource, or `undefined` for every field. */
|
|
28
|
+
fields?: (user: AccessUser, capability: string, model: ResourceModel) => readonly string[] | undefined;
|
|
29
29
|
/** The roles by name, to tell whether the project has an `anonymous` role. */
|
|
30
30
|
definitions?: Record<string, unknown>;
|
|
31
31
|
/** Whether the user's roles grant all of a capability, as `defineRoles` makes it; the gate of API functions. */
|
package/dist/access/resolve.js
CHANGED
|
@@ -104,7 +104,7 @@ const resolveAccess = async (source, ctx, options = {}) => {
|
|
|
104
104
|
model,
|
|
105
105
|
operation
|
|
106
106
|
});
|
|
107
|
-
const listed = (operation) => options.roles?.fields?.(ctx.user, capabilityOf(model, operation));
|
|
107
|
+
const listed = (operation) => options.roles?.fields?.(ctx.user, capabilityOf(model, operation), model);
|
|
108
108
|
const lists = {
|
|
109
109
|
read: listed("read"),
|
|
110
110
|
create: listed("create"),
|
package/dist/access/roles.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ResourceModel } from "../model.js";
|
|
1
2
|
import { AccessUser } from "./types.js";
|
|
2
3
|
import { Capability, ExactCapability, Grant } from "./capability.js";
|
|
3
4
|
import { HeldCapability, RoleEntry } from "./role-entry.js";
|
|
@@ -51,12 +52,14 @@ export declare const defineRoles: <Name extends string = string>(definitions: Re
|
|
|
51
52
|
holds: (user: AccessUser, capability: Capability<Name>) => boolean;
|
|
52
53
|
/**
|
|
53
54
|
* The fields the user's roles allow for a capability: the union of their lists, or `undefined` when a role grants
|
|
54
|
-
* it without a list (every field) or none grants it.
|
|
55
|
+
* it without a list (every field) or none grants it. With the resource's model, as `can` has it, wildcards over
|
|
56
|
+
* resources count only where the resource takes them.
|
|
55
57
|
*/
|
|
56
|
-
fields: (user: AccessUser, capability: string) => readonly string[] | undefined;
|
|
58
|
+
fields: (user: AccessUser, capability: string, model?: ResourceModel) => readonly string[] | undefined;
|
|
57
59
|
/**
|
|
58
60
|
* A rule that allows when the user's roles grant the capability: `true` for an unscoped
|
|
59
|
-
* grant, a row filter for `.own`, `.team` and a limit's filter, `false` otherwise. Refuses a wildcard.
|
|
61
|
+
* grant, a row filter for `.own`, `.team` and a limit's filter, `false` otherwise. Refuses a wildcard. On a resource
|
|
62
|
+
* with `.explicitGrants()`, a role's wildcard over resources such as `*.read` grants nothing.
|
|
60
63
|
*/
|
|
61
64
|
can: (capability: ExactCapability<Name>) => Rule;
|
|
62
65
|
/** A rule that allows when the user has any of the roles. */
|
package/dist/access/roles.js
CHANGED
|
@@ -4,6 +4,7 @@ import { heldCapabilities } from "./role-entry.js";
|
|
|
4
4
|
import { rule } from "./rule.js";
|
|
5
5
|
//#region src/access/roles.ts
|
|
6
6
|
const rolesOf = (user) => user.roles ?? [];
|
|
7
|
+
const reaches = (held, model) => !model?.explicitGrants || held.capability === "*" || parseCapability(held.capability).name !== "*";
|
|
7
8
|
/**
|
|
8
9
|
* Defines roles as bundles of capability strings. Pass the resource and area names as the type
|
|
9
10
|
* parameter to reject unknown names at compile time:
|
|
@@ -66,22 +67,24 @@ const defineRoles = (definitions, options = {}) => {
|
|
|
66
67
|
holds: (user, capability) => heldBy(user).some((held) => !held.fields && !held.filter && holdsAll(held.capability, capability)),
|
|
67
68
|
/**
|
|
68
69
|
* The fields the user's roles allow for a capability: the union of their lists, or `undefined` when a role grants
|
|
69
|
-
* it without a list (every field) or none grants it.
|
|
70
|
+
* it without a list (every field) or none grants it. With the resource's model, as `can` has it, wildcards over
|
|
71
|
+
* resources count only where the resource takes them.
|
|
70
72
|
*/
|
|
71
|
-
fields: (user, capability) => {
|
|
73
|
+
fields: (user, capability, model) => {
|
|
72
74
|
const wanted = parseCapability(capability);
|
|
73
|
-
const matching = heldBy(user).filter((held) => grantsOf(held.capability, wanted).length > 0);
|
|
75
|
+
const matching = heldBy(user).filter((held) => reaches(held, model) && grantsOf(held.capability, wanted).length > 0);
|
|
74
76
|
if (matching.length === 0 || matching.some((held) => !held.fields)) return void 0;
|
|
75
77
|
return [...new Set(matching.flatMap((held) => held.fields))];
|
|
76
78
|
},
|
|
77
79
|
/**
|
|
78
80
|
* A rule that allows when the user's roles grant the capability: `true` for an unscoped
|
|
79
|
-
* grant, a row filter for `.own`, `.team` and a limit's filter, `false` otherwise. Refuses a wildcard.
|
|
81
|
+
* grant, a row filter for `.own`, `.team` and a limit's filter, `false` otherwise. Refuses a wildcard. On a resource
|
|
82
|
+
* with `.explicitGrants()`, a role's wildcard over resources such as `*.read` grants nothing.
|
|
80
83
|
*/
|
|
81
84
|
can: (capability) => {
|
|
82
85
|
const wanted = parseExactCapability(capability);
|
|
83
86
|
return rule(async (ctx) => {
|
|
84
|
-
const matching = heldBy(ctx.user ?? {}).flatMap((held) => grantsOf(held.capability, wanted).map((grant) => ({
|
|
87
|
+
const matching = heldBy(ctx.user ?? {}).filter((held) => reaches(held, ctx.model)).flatMap((held) => grantsOf(held.capability, wanted).map((grant) => ({
|
|
85
88
|
grant,
|
|
86
89
|
filter: held.filter
|
|
87
90
|
})));
|
package/dist/model.d.ts
CHANGED
|
@@ -149,6 +149,8 @@ export type ResourceModel = {
|
|
|
149
149
|
comments?: true;
|
|
150
150
|
/** Whether open pages refresh as soon as its records change (`.live()`). */
|
|
151
151
|
live?: true;
|
|
152
|
+
/** Whether role wildcards such as `*.read` leave it out, so only `*` and capabilities naming it grant it (`.explicitGrants()`). */
|
|
153
|
+
explicitGrants?: true;
|
|
152
154
|
/**
|
|
153
155
|
* The fields of a unique key whose conflict on create answers the record already holding those values, to a caller
|
|
154
156
|
* who may read it (`.openExisting()`).
|
package/dist/resource-model.d.ts
CHANGED
package/dist/resource-model.js
CHANGED
|
@@ -75,6 +75,7 @@ const assembleModel = (state) => {
|
|
|
75
75
|
...state.history && { history: true },
|
|
76
76
|
...state.comments && { comments: true },
|
|
77
77
|
...state.live && { live: true },
|
|
78
|
+
...state.explicitGrants && { explicitGrants: true },
|
|
78
79
|
...state.position && { position: { ...state.position } },
|
|
79
80
|
...state.parents && { parents: state.parents.map((parent) => ({ ...parent })) },
|
|
80
81
|
...state.openExisting && { openExisting: [...state.openExisting] }
|
package/dist/resource.d.ts
CHANGED
|
@@ -170,6 +170,12 @@ export declare class ResourceBuilder<F extends FieldsInput = {}, K = never> {
|
|
|
170
170
|
* [Live updates](docs/website/src/content/docs/reference/live.mdx).
|
|
171
171
|
*/
|
|
172
172
|
live(): ResourceBuilder<F, K>;
|
|
173
|
+
/**
|
|
174
|
+
* Leaves it out of role wildcards over resources such as `*.read`: with roles, only `*` itself and capabilities that
|
|
175
|
+
* name it, such as `webhook_subscriptions.read`, grant it. For resources whose records hold secrets, which a role
|
|
176
|
+
* written as "read everything" should not reach. See [Resources that need named grants](docs/website/src/content/docs/reference/access.mdx#resources-that-need-named-grants).
|
|
177
|
+
*/
|
|
178
|
+
explicitGrants(): ResourceBuilder<F, K>;
|
|
173
179
|
/** The code predicates (`match`) of its events, by event name. */
|
|
174
180
|
eventMatches(): Record<string, EventMatch<any>>;
|
|
175
181
|
/**
|
package/dist/resource.js
CHANGED
|
@@ -192,6 +192,14 @@ var ResourceBuilder = class ResourceBuilder {
|
|
|
192
192
|
live() {
|
|
193
193
|
return this.next({ live: true });
|
|
194
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Leaves it out of role wildcards over resources such as `*.read`: with roles, only `*` itself and capabilities that
|
|
197
|
+
* name it, such as `webhook_subscriptions.read`, grant it. For resources whose records hold secrets, which a role
|
|
198
|
+
* written as "read everything" should not reach. See [Resources that need named grants](docs/website/src/content/docs/reference/access.mdx#resources-that-need-named-grants).
|
|
199
|
+
*/
|
|
200
|
+
explicitGrants() {
|
|
201
|
+
return this.next({ explicitGrants: true });
|
|
202
|
+
}
|
|
195
203
|
/** The code predicates (`match`) of its events, by event name. */
|
|
196
204
|
eventMatches() {
|
|
197
205
|
return eventMatches(this.toModel(), this.state.events ?? []);
|
|
@@ -3,7 +3,10 @@ import { resource } from "../resource.js";
|
|
|
3
3
|
import { view } from "../view.js";
|
|
4
4
|
import { layout } from "../layout.js";
|
|
5
5
|
//#region src/webhooks/webhook-resources.ts
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Webhook subscriptions: a row per subscription someone made in the admin app, scoped to their tenant. Their URLs and
|
|
8
|
+
* secrets let anyone call the receivers, so a role's wildcard such as `*.read` reaches none of the webhook resources.
|
|
9
|
+
*/
|
|
7
10
|
const subscriptionsResource = (schema) => resource("webhook_subscriptions").table(`${schema}.subscriptions`).fields({
|
|
8
11
|
id: f.uuid().readOnly().dbDefault(),
|
|
9
12
|
name: f.text().min(1).max(100).filterable().sortable(),
|
|
@@ -26,7 +29,7 @@ const subscriptionsResource = (schema) => resource("webhook_subscriptions").tabl
|
|
|
26
29
|
rotate_secret: f.boolean().default(false),
|
|
27
30
|
send_test: f.boolean().default(false),
|
|
28
31
|
resend_failed: f.boolean().default(false)
|
|
29
|
-
}).primaryKey((r) => r.id).tenant((r) => r.tenant_id).owner((r) => r.owner_id).search((r) => [r.name, r.url]);
|
|
32
|
+
}).primaryKey((r) => r.id).tenant((r) => r.tenant_id).owner((r) => r.owner_id).search((r) => [r.name, r.url]).explicitGrants();
|
|
30
33
|
const subscriptionsView = view("webhook_subscriptions").names({
|
|
31
34
|
singular: "Webhook subscription",
|
|
32
35
|
plural: "Webhook subscriptions"
|
|
@@ -117,7 +120,7 @@ const deliveriesResource = (schema) => resource("webhook_deliveries").table(`${s
|
|
|
117
120
|
last_error: f.text().optional().readOnly(),
|
|
118
121
|
created_at: f.timestamp().readOnly().sortable(),
|
|
119
122
|
resend: f.boolean()
|
|
120
|
-
}).primaryKey((r) => r.id).tenant((r) => r.tenant_id).access({
|
|
123
|
+
}).primaryKey((r) => r.id).tenant((r) => r.tenant_id).explicitGrants().access({
|
|
121
124
|
create: () => false,
|
|
122
125
|
delete: () => false
|
|
123
126
|
});
|
|
@@ -169,7 +172,7 @@ const deliveryLogResource = (schema) => resource("webhook_delivery_log").table(`
|
|
|
169
172
|
response: f.text().optional().readOnly(),
|
|
170
173
|
duration_ms: f.integer().readOnly(),
|
|
171
174
|
at: f.timestamp().readOnly().filterable().sortable()
|
|
172
|
-
}).primaryKey((r) => r.id).tenant((r) => r.tenant_id).access({
|
|
175
|
+
}).primaryKey((r) => r.id).tenant((r) => r.tenant_id).explicitGrants().access({
|
|
173
176
|
create: () => false,
|
|
174
177
|
update: () => false,
|
|
175
178
|
delete: () => false
|