@sanity-labs/client-fake-for-test 0.13.0 → 0.15.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/README.md +7 -2
- package/dist/index.cjs +208 -83
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -6
- package/dist/index.d.ts +35 -6
- package/dist/index.js +208 -83
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -841,6 +841,12 @@ export declare type ReleaseType = "asap" | "scheduled" | "undecided";
|
|
|
841
841
|
export declare interface ResourceAccess {
|
|
842
842
|
roles?: NonNullable<CurrentUser["roles"]>;
|
|
843
843
|
permissions?: string[];
|
|
844
|
+
/**
|
|
845
|
+
* The user's attributes in the organization, as an administrator or SAML
|
|
846
|
+
* sync set them; read on `"organization:<id>"` entries only. Served by
|
|
847
|
+
* `GET /organizations/<id>/users/me/attributes`.
|
|
848
|
+
*/
|
|
849
|
+
attributes?: UserAttribute[];
|
|
844
850
|
}
|
|
845
851
|
|
|
846
852
|
/** Resource types `@sanity/client`'s `resource` config can name (the ones staging probed). */
|
|
@@ -1183,8 +1189,8 @@ export declare interface TestClientAccessControl {
|
|
|
1183
1189
|
*/
|
|
1184
1190
|
aclPath?: string;
|
|
1185
1191
|
/**
|
|
1186
|
-
* Access API entries, keyed `"<access type>:<id>"` — `"
|
|
1187
|
-
* `"media-library:<id>"`, `"canvas:<id>"`, `"dashboard:<id>"`. See {@link ResourceAccess}.
|
|
1192
|
+
* Access API entries, keyed `"<access type>:<id>"` — `"organization:<id>"`,
|
|
1193
|
+
* `"project:<projectId>"`, `"media-library:<id>"`, `"canvas:<id>"`, `"dashboard:<id>"`. See {@link ResourceAccess}.
|
|
1188
1194
|
*/
|
|
1189
1195
|
access?: Record<string, ResourceAccess>;
|
|
1190
1196
|
}
|
|
@@ -1225,10 +1231,10 @@ export declare interface TestClientIdentity {
|
|
|
1225
1231
|
/** Grants served at the ACL path and enforced on reads/writes. */
|
|
1226
1232
|
grants: Grant[];
|
|
1227
1233
|
/**
|
|
1228
|
-
* Access API entries, keyed `"<access type>:<id>"` (`"
|
|
1229
|
-
* `"
|
|
1230
|
-
* non-
|
|
1231
|
-
* there. Project membership needs no entry: every identity is a member of
|
|
1234
|
+
* Access API entries, keyed `"<access type>:<id>"` (`"organization:<id>"`,
|
|
1235
|
+
* `"media-library:<id>"`, `"canvas:<id>"`, `"dashboard:<id>"`,
|
|
1236
|
+
* `"project:<projectId>"`). On a non-project resource, an entry with
|
|
1237
|
+
* `roles` makes the identity a member there. Project membership needs no entry: every identity is a member of
|
|
1232
1238
|
* every project with its project-scoped roles.
|
|
1233
1239
|
*/
|
|
1234
1240
|
access?: Record<string, ResourceAccess>;
|
|
@@ -1365,6 +1371,29 @@ declare interface UnsupportedEntry {
|
|
|
1365
1371
|
workaround: string;
|
|
1366
1372
|
}
|
|
1367
1373
|
|
|
1374
|
+
/**
|
|
1375
|
+
* One user attribute: its key, its type, and its value from each source.
|
|
1376
|
+
* The served row adds `activeSource` and `activeValue`: the Sanity value
|
|
1377
|
+
* when there is one, else the SAML value.
|
|
1378
|
+
*/
|
|
1379
|
+
export declare interface UserAttribute {
|
|
1380
|
+
key: string;
|
|
1381
|
+
type: string;
|
|
1382
|
+
values: {
|
|
1383
|
+
saml?: UserAttributeValue;
|
|
1384
|
+
sanity?: UserAttributeValue;
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/** A value a user attribute can hold. */
|
|
1389
|
+
export declare type UserAttributeValue =
|
|
1390
|
+
| string
|
|
1391
|
+
| number
|
|
1392
|
+
| boolean
|
|
1393
|
+
| string[]
|
|
1394
|
+
| number[]
|
|
1395
|
+
| boolean[];
|
|
1396
|
+
|
|
1368
1397
|
export declare type VersionAction =
|
|
1369
1398
|
| {
|
|
1370
1399
|
actionType: "sanity.action.document.version.create";
|
package/dist/index.d.ts
CHANGED
|
@@ -841,6 +841,12 @@ export declare type ReleaseType = "asap" | "scheduled" | "undecided";
|
|
|
841
841
|
export declare interface ResourceAccess {
|
|
842
842
|
roles?: NonNullable<CurrentUser["roles"]>;
|
|
843
843
|
permissions?: string[];
|
|
844
|
+
/**
|
|
845
|
+
* The user's attributes in the organization, as an administrator or SAML
|
|
846
|
+
* sync set them; read on `"organization:<id>"` entries only. Served by
|
|
847
|
+
* `GET /organizations/<id>/users/me/attributes`.
|
|
848
|
+
*/
|
|
849
|
+
attributes?: UserAttribute[];
|
|
844
850
|
}
|
|
845
851
|
|
|
846
852
|
/** Resource types `@sanity/client`'s `resource` config can name (the ones staging probed). */
|
|
@@ -1183,8 +1189,8 @@ export declare interface TestClientAccessControl {
|
|
|
1183
1189
|
*/
|
|
1184
1190
|
aclPath?: string;
|
|
1185
1191
|
/**
|
|
1186
|
-
* Access API entries, keyed `"<access type>:<id>"` — `"
|
|
1187
|
-
* `"media-library:<id>"`, `"canvas:<id>"`, `"dashboard:<id>"`. See {@link ResourceAccess}.
|
|
1192
|
+
* Access API entries, keyed `"<access type>:<id>"` — `"organization:<id>"`,
|
|
1193
|
+
* `"project:<projectId>"`, `"media-library:<id>"`, `"canvas:<id>"`, `"dashboard:<id>"`. See {@link ResourceAccess}.
|
|
1188
1194
|
*/
|
|
1189
1195
|
access?: Record<string, ResourceAccess>;
|
|
1190
1196
|
}
|
|
@@ -1225,10 +1231,10 @@ export declare interface TestClientIdentity {
|
|
|
1225
1231
|
/** Grants served at the ACL path and enforced on reads/writes. */
|
|
1226
1232
|
grants: Grant[];
|
|
1227
1233
|
/**
|
|
1228
|
-
* Access API entries, keyed `"<access type>:<id>"` (`"
|
|
1229
|
-
* `"
|
|
1230
|
-
* non-
|
|
1231
|
-
* there. Project membership needs no entry: every identity is a member of
|
|
1234
|
+
* Access API entries, keyed `"<access type>:<id>"` (`"organization:<id>"`,
|
|
1235
|
+
* `"media-library:<id>"`, `"canvas:<id>"`, `"dashboard:<id>"`,
|
|
1236
|
+
* `"project:<projectId>"`). On a non-project resource, an entry with
|
|
1237
|
+
* `roles` makes the identity a member there. Project membership needs no entry: every identity is a member of
|
|
1232
1238
|
* every project with its project-scoped roles.
|
|
1233
1239
|
*/
|
|
1234
1240
|
access?: Record<string, ResourceAccess>;
|
|
@@ -1365,6 +1371,29 @@ declare interface UnsupportedEntry {
|
|
|
1365
1371
|
workaround: string;
|
|
1366
1372
|
}
|
|
1367
1373
|
|
|
1374
|
+
/**
|
|
1375
|
+
* One user attribute: its key, its type, and its value from each source.
|
|
1376
|
+
* The served row adds `activeSource` and `activeValue`: the Sanity value
|
|
1377
|
+
* when there is one, else the SAML value.
|
|
1378
|
+
*/
|
|
1379
|
+
export declare interface UserAttribute {
|
|
1380
|
+
key: string;
|
|
1381
|
+
type: string;
|
|
1382
|
+
values: {
|
|
1383
|
+
saml?: UserAttributeValue;
|
|
1384
|
+
sanity?: UserAttributeValue;
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/** A value a user attribute can hold. */
|
|
1389
|
+
export declare type UserAttributeValue =
|
|
1390
|
+
| string
|
|
1391
|
+
| number
|
|
1392
|
+
| boolean
|
|
1393
|
+
| string[]
|
|
1394
|
+
| number[]
|
|
1395
|
+
| boolean[];
|
|
1396
|
+
|
|
1368
1397
|
export declare type VersionAction =
|
|
1369
1398
|
| {
|
|
1370
1399
|
actionType: "sanity.action.document.version.create";
|
package/dist/index.js
CHANGED
|
@@ -102,7 +102,7 @@ function accessApiError(args) {
|
|
|
102
102
|
...request,
|
|
103
103
|
statusCode,
|
|
104
104
|
statusMessage,
|
|
105
|
-
body: { statusCode, message }
|
|
105
|
+
body: { statusCode, error: statusMessage, message }
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
108
|
const noPermission = (request) => accessApiError({
|
|
@@ -185,14 +185,7 @@ const RESOURCE_CAPABILITIES = {
|
|
|
185
185
|
history: "served",
|
|
186
186
|
actions: "served",
|
|
187
187
|
refusedActions: {},
|
|
188
|
-
access: {
|
|
189
|
-
type: "project",
|
|
190
|
-
id: (id) => datasetParts(id).projectId,
|
|
191
|
-
roles: "served",
|
|
192
|
-
users: "served",
|
|
193
|
-
// Every registered identity is a member of every project, with its project roles.
|
|
194
|
-
memberRoles: ({ identity, id }) => (identity.usersByProject?.[id] ?? identity.user).roles ?? []
|
|
195
|
-
}
|
|
188
|
+
access: { type: "project", id: (id) => datasetParts(id).projectId }
|
|
196
189
|
},
|
|
197
190
|
"media-library": {
|
|
198
191
|
label: (id) => `media library "${id}"`,
|
|
@@ -204,15 +197,7 @@ const RESOURCE_CAPABILITIES = {
|
|
|
204
197
|
history: "served",
|
|
205
198
|
actions: "served",
|
|
206
199
|
refusedActions: { "sanity.action.release.create": releaseLimitExceeded },
|
|
207
|
-
access: {
|
|
208
|
-
type: "media-library",
|
|
209
|
-
id: (id) => id,
|
|
210
|
-
// Unproven on staging (no token held roles read): refused like a missing permission.
|
|
211
|
-
roles: noPermission,
|
|
212
|
-
users: "served",
|
|
213
|
-
unknownId: notFound("Media library"),
|
|
214
|
-
memberRoles: accessEntryRoles("media-library")
|
|
215
|
-
}
|
|
200
|
+
access: { type: "media-library", id: (id) => id }
|
|
216
201
|
},
|
|
217
202
|
canvas: {
|
|
218
203
|
label: (id) => `canvas "${id}"`,
|
|
@@ -224,15 +209,7 @@ const RESOURCE_CAPABILITIES = {
|
|
|
224
209
|
history: "served",
|
|
225
210
|
actions: notMounted,
|
|
226
211
|
refusedActions: {},
|
|
227
|
-
access: {
|
|
228
|
-
type: "canvas",
|
|
229
|
-
id: (id) => id,
|
|
230
|
-
// Unproven on staging (no canvas roles/members permission exists to grant).
|
|
231
|
-
roles: noPermission,
|
|
232
|
-
users: noPermission,
|
|
233
|
-
unknownId: notFound("Canvas"),
|
|
234
|
-
memberRoles: accessEntryRoles("canvas")
|
|
235
|
-
}
|
|
212
|
+
access: { type: "canvas", id: (id) => id }
|
|
236
213
|
},
|
|
237
214
|
dashboard: {
|
|
238
215
|
label: (id) => `dashboard "${id}"`,
|
|
@@ -244,19 +221,11 @@ const RESOURCE_CAPABILITIES = {
|
|
|
244
221
|
history: notMounted,
|
|
245
222
|
actions: notMounted,
|
|
246
223
|
refusedActions: {},
|
|
247
|
-
access: {
|
|
248
|
-
type: "dashboard",
|
|
249
|
-
id: (id) => id,
|
|
250
|
-
// Unproven on staging, as for canvas.
|
|
251
|
-
roles: noPermission,
|
|
252
|
-
users: noPermission,
|
|
253
|
-
unknownId: notFound("Dashboard"),
|
|
254
|
-
memberRoles: accessEntryRoles("dashboard")
|
|
255
|
-
}
|
|
224
|
+
access: { type: "dashboard", id: (id) => id }
|
|
256
225
|
}
|
|
257
226
|
};
|
|
258
227
|
function capabilitiesOf(type) {
|
|
259
|
-
const row = RESOURCE_CAPABILITIES[type];
|
|
228
|
+
const row = Object.hasOwn(RESOURCE_CAPABILITIES, type) ? RESOURCE_CAPABILITIES[type] : void 0;
|
|
260
229
|
if (row === void 0)
|
|
261
230
|
throw fakeError(
|
|
262
231
|
`Unsupported resource type ${JSON.stringify(type)}. The fake serves ${Object.keys(RESOURCE_CAPABILITIES).map((t) => `"${t}"`).join(", ")}.`
|
|
@@ -278,9 +247,77 @@ function matchResourcePath(path) {
|
|
|
278
247
|
return { resource: { type, id: match.id }, rest: match.rest };
|
|
279
248
|
}
|
|
280
249
|
}
|
|
281
|
-
function
|
|
282
|
-
|
|
283
|
-
|
|
250
|
+
function accessResourceOf(resource) {
|
|
251
|
+
const { access } = capabilitiesOf(resource.type);
|
|
252
|
+
return { type: access.type, id: access.id(resource.id) };
|
|
253
|
+
}
|
|
254
|
+
function holdsResource(type) {
|
|
255
|
+
return (registry, id) => registry.resources().some((resource) => {
|
|
256
|
+
const access = accessResourceOf(resource);
|
|
257
|
+
return access.type === type && access.id === id;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const ACCESS_CAPABILITIES = {
|
|
261
|
+
organization: {
|
|
262
|
+
// Proven on staging, but outside what the fake serves: refused like a missing permission.
|
|
263
|
+
roles: noPermission,
|
|
264
|
+
users: "served",
|
|
265
|
+
user: "served",
|
|
266
|
+
// Granted by the built-in organization `member` role.
|
|
267
|
+
usersPermission: "sanity.organization.members.read",
|
|
268
|
+
unknown: {
|
|
269
|
+
holds: (registry, id) => id === registry.organizationId,
|
|
270
|
+
refusal: notFound("Organization")
|
|
271
|
+
},
|
|
272
|
+
memberRoles: accessEntryRoles("organization"),
|
|
273
|
+
// Every resource of the family belongs to its organization (see `organizationId`).
|
|
274
|
+
owns: (registry) => {
|
|
275
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
276
|
+
for (const resource of registry.resources()) {
|
|
277
|
+
const access = accessResourceOf(resource);
|
|
278
|
+
byKey.set(accessKey(access.type, access.id), access);
|
|
279
|
+
}
|
|
280
|
+
return [...byKey.values()];
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
project: {
|
|
284
|
+
roles: "served",
|
|
285
|
+
users: "served",
|
|
286
|
+
// Single-user reads are served only where the fake checks members read (the
|
|
287
|
+
// organization); elsewhere they refuse rather than answer unchecked.
|
|
288
|
+
user: noPermission,
|
|
289
|
+
// Every registered identity is a member of every project, with its project roles.
|
|
290
|
+
memberRoles: ({ identity, id }) => (identity.usersByProject?.[id] ?? identity.user).roles ?? []
|
|
291
|
+
},
|
|
292
|
+
"media-library": {
|
|
293
|
+
// Staging refuses it only because no probe token held roles read.
|
|
294
|
+
roles: "served",
|
|
295
|
+
rolesPermission: "sanity.media-library.roles.read",
|
|
296
|
+
users: "served",
|
|
297
|
+
// Unproven on staging (only the listing was probed); refused as on `project`.
|
|
298
|
+
user: noPermission,
|
|
299
|
+
unknown: { holds: holdsResource("media-library"), refusal: notFound("Media library") },
|
|
300
|
+
memberRoles: accessEntryRoles("media-library")
|
|
301
|
+
},
|
|
302
|
+
canvas: {
|
|
303
|
+
// Unproven on staging (no canvas roles/members permission exists to grant).
|
|
304
|
+
roles: noPermission,
|
|
305
|
+
users: noPermission,
|
|
306
|
+
user: noPermission,
|
|
307
|
+
unknown: { holds: holdsResource("canvas"), refusal: notFound("Canvas") },
|
|
308
|
+
memberRoles: accessEntryRoles("canvas")
|
|
309
|
+
},
|
|
310
|
+
dashboard: {
|
|
311
|
+
// Unproven on staging, as for canvas.
|
|
312
|
+
roles: noPermission,
|
|
313
|
+
users: noPermission,
|
|
314
|
+
user: noPermission,
|
|
315
|
+
unknown: { holds: holdsResource("dashboard"), refusal: notFound("Dashboard") },
|
|
316
|
+
memberRoles: accessEntryRoles("dashboard")
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
function accessCapabilitiesOf(type) {
|
|
320
|
+
return Object.hasOwn(ACCESS_CAPABILITIES, type) ? ACCESS_CAPABILITIES[type] : void 0;
|
|
284
321
|
}
|
|
285
322
|
function dataUrl(args) {
|
|
286
323
|
const { resource, dataset, operation, path } = args;
|
|
@@ -294,12 +331,21 @@ function dataUrl(args) {
|
|
|
294
331
|
const base = `/${operation}/${dataset}`;
|
|
295
332
|
return `/data${path !== void 0 ? `${base}/${path}` : base}`.replace(/\/($|\?)/, "$1");
|
|
296
333
|
}
|
|
297
|
-
const ACCESS_PATH = /^\/access\/([^/]+)\/([^/]+)(\/.*)?$/, DATA_HISTORY_PATH = /^\/data\/history\/([^/]+)(\/.*)$/;
|
|
334
|
+
const ACCESS_PATH = /^\/access\/([^/]+)\/([^/]+)(\/.*)?$/, ACCESS_USER_PATH = /^\/users\/([^/]+)$/, ACCESS_ROLE_PATH = /^\/roles\/([^/]+)$/, MY_ATTRIBUTES_PATH = /^\/organizations\/([^/]+)\/users\/me\/attributes$/, DATA_HISTORY_PATH = /^\/data\/history\/([^/]+)(\/.*)$/;
|
|
298
335
|
function page(data) {
|
|
299
336
|
return { data, nextCursor: null };
|
|
300
337
|
}
|
|
338
|
+
function usersPage(data) {
|
|
339
|
+
return { ...page(data), totalCount: data.length };
|
|
340
|
+
}
|
|
341
|
+
const UNMODELLED_PROFILE = { provider: "sanity", createdAt: (/* @__PURE__ */ new Date(0)).toISOString() };
|
|
301
342
|
async function servePlatformRequest(ctx, url) {
|
|
302
|
-
const queryStart = url.indexOf("?"), path = queryStart === -1 ? url : url.slice(0, queryStart), query = new URLSearchParams(queryStart === -1 ? "" : url.slice(queryStart + 1)), request = { method: "GET", path },
|
|
343
|
+
const queryStart = url.indexOf("?"), path = queryStart === -1 ? url : url.slice(0, queryStart), query = new URLSearchParams(queryStart === -1 ? "" : url.slice(queryStart + 1)), request = { method: "GET", path }, myAttributes = path.match(MY_ATTRIBUTES_PATH);
|
|
344
|
+
if (myAttributes !== null) {
|
|
345
|
+
const organizationId = decodeURIComponent(myAttributes[1]);
|
|
346
|
+
return serveMyAttributes({ ctx, request, organizationId, query });
|
|
347
|
+
}
|
|
348
|
+
const access = path.match(ACCESS_PATH);
|
|
303
349
|
if (access !== null) {
|
|
304
350
|
const [, type, id, rest2] = access;
|
|
305
351
|
return serveAccess({ ctx, request, type, id: decodeURIComponent(id), rest: rest2 ?? "", query });
|
|
@@ -367,20 +413,46 @@ async function readHistory(args) {
|
|
|
367
413
|
};
|
|
368
414
|
}
|
|
369
415
|
function serveAccess(args) {
|
|
370
|
-
const { ctx, request, type, id, rest, query } = args,
|
|
371
|
-
if (
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
416
|
+
const { ctx, request, type, id, rest, query } = args, row = accessCapabilitiesOf(type);
|
|
417
|
+
if (row === void 0) throw invalidAccessType(request, type);
|
|
418
|
+
const resource = { type, id }, registry = {
|
|
419
|
+
organizationId: ctx.organizationId,
|
|
420
|
+
resources: ctx.resources
|
|
421
|
+
}, permissions = ctx.actorAccess()?.[accessKey(resource.type, id)]?.permissions ?? [];
|
|
422
|
+
if (rest === "/user-permissions/me/check") {
|
|
423
|
+
const requested = query.getAll("permissions").flatMap((value) => value.split(","));
|
|
424
|
+
return {
|
|
425
|
+
value: { data: Object.fromEntries(requested.map((p) => [p, permissions.includes(p)])) }
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (row.unknown !== void 0 && !row.unknown.holds(registry, id))
|
|
429
|
+
throw row.unknown.refusal(request);
|
|
430
|
+
const gated = (capability, permission, serve) => serveCapability(capability, request, () => {
|
|
431
|
+
if (permission !== void 0 && !permissions.includes(permission))
|
|
432
|
+
throw noPermission(request);
|
|
433
|
+
return serve();
|
|
434
|
+
}), users = (capability, serve) => gated(capability, row.usersPermission, serve), roles = () => rolesOn({ resource, identities: ctx.identities() }), user = rest.match(ACCESS_USER_PATH);
|
|
435
|
+
if (user !== null)
|
|
436
|
+
return users(row.user, () => {
|
|
437
|
+
const sanityUserId = decodeURIComponent(user[1]), record = usersOn({ resource, registry, identities: ctx.identities() }).find(
|
|
438
|
+
(candidate) => candidate.sanityUserId === sanityUserId
|
|
439
|
+
);
|
|
440
|
+
if (record === void 0) throw notFound("User")(request);
|
|
441
|
+
return { value: record };
|
|
442
|
+
});
|
|
443
|
+
const role = rest.match(ACCESS_ROLE_PATH);
|
|
444
|
+
if (role !== null)
|
|
445
|
+
return gated(row.roles, row.rolesPermission, () => {
|
|
446
|
+
const name = decodeURIComponent(role[1]), record = roles().find((candidate) => candidate.name === name);
|
|
447
|
+
if (record === void 0) throw notFound("Role")(request);
|
|
448
|
+
return { value: record };
|
|
449
|
+
});
|
|
376
450
|
switch (rest) {
|
|
377
451
|
case "/roles":
|
|
378
|
-
return
|
|
379
|
-
value: page(rolesOn({ row: found.row, id, identities: ctx.identities() }))
|
|
380
|
-
}));
|
|
452
|
+
return gated(row.roles, row.rolesPermission, () => ({ value: page(roles()) }));
|
|
381
453
|
case "/users":
|
|
382
|
-
return
|
|
383
|
-
value:
|
|
454
|
+
return users(row.users, () => ({
|
|
455
|
+
value: usersPage(usersOn({ resource, registry, identities: ctx.identities() }))
|
|
384
456
|
}));
|
|
385
457
|
case "/user-permissions/me":
|
|
386
458
|
return {
|
|
@@ -389,63 +461,102 @@ function serveAccess(args) {
|
|
|
389
461
|
const dot = permission.lastIndexOf(".");
|
|
390
462
|
return {
|
|
391
463
|
type: permission.slice(0, dot),
|
|
392
|
-
resourceType:
|
|
464
|
+
resourceType: resource.type,
|
|
393
465
|
resourceId: id,
|
|
394
466
|
action: permission.slice(dot + 1)
|
|
395
467
|
};
|
|
396
468
|
})
|
|
397
469
|
)
|
|
398
470
|
};
|
|
399
|
-
case "/user-permissions/me/check": {
|
|
400
|
-
const requested = query.getAll("permissions").flatMap((value) => value.split(","));
|
|
401
|
-
return {
|
|
402
|
-
value: {
|
|
403
|
-
data: Object.fromEntries(requested.map((p) => [p, permissions.includes(p)]))
|
|
404
|
-
}
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
471
|
default:
|
|
408
472
|
return;
|
|
409
473
|
}
|
|
410
474
|
}
|
|
475
|
+
const UNMODELLED_ROLE = { appliesToUsers: !0, appliesToRobots: !0 };
|
|
411
476
|
function rolesOn(args) {
|
|
412
|
-
const byName = /* @__PURE__ */ new Map();
|
|
413
|
-
for (const identity of args.identities)
|
|
414
|
-
|
|
477
|
+
const { resource } = args, byName = /* @__PURE__ */ new Map();
|
|
478
|
+
for (const identity of args.identities) {
|
|
479
|
+
const roles = ACCESS_CAPABILITIES[resource.type].memberRoles({ identity, id: resource.id });
|
|
480
|
+
for (const role of roles ?? [])
|
|
415
481
|
byName.set(role.name, {
|
|
416
482
|
name: role.name,
|
|
417
|
-
|
|
418
|
-
...role.description !== void 0 ? { description: role.description } : {}
|
|
483
|
+
title: role.title ?? role.name,
|
|
484
|
+
...role.description !== void 0 ? { description: role.description } : {},
|
|
485
|
+
resourceType: resource.type,
|
|
486
|
+
resourceId: resource.id,
|
|
487
|
+
...UNMODELLED_ROLE,
|
|
488
|
+
permissions: []
|
|
419
489
|
});
|
|
490
|
+
}
|
|
420
491
|
return [...byName.values()].toSorted((a, b) => a.name.localeCompare(b.name));
|
|
421
492
|
}
|
|
493
|
+
function membershipOn(identity, resource) {
|
|
494
|
+
const roles = ACCESS_CAPABILITIES[resource.type].memberRoles({ identity, id: resource.id });
|
|
495
|
+
if (roles !== void 0)
|
|
496
|
+
return {
|
|
497
|
+
resourceType: resource.type,
|
|
498
|
+
resourceId: resource.id,
|
|
499
|
+
roleNames: roles.map((role) => role.name).toSorted()
|
|
500
|
+
};
|
|
501
|
+
}
|
|
422
502
|
function usersOn(args) {
|
|
423
|
-
const {
|
|
503
|
+
const { resource, registry, identities } = args, owned = ACCESS_CAPABILITIES[resource.type].owns?.(registry) ?? [], bySanityUserId = /* @__PURE__ */ new Map();
|
|
424
504
|
for (const identity of identities) {
|
|
425
|
-
const
|
|
426
|
-
if (
|
|
505
|
+
const own = membershipOn(identity, resource);
|
|
506
|
+
if (own === void 0) continue;
|
|
427
507
|
const user = identity.globalUser ?? identity.user, record = {
|
|
428
508
|
sanityUserId: user.id,
|
|
429
509
|
profile: {
|
|
510
|
+
id: user.id,
|
|
430
511
|
displayName: user.name ?? user.id,
|
|
431
|
-
|
|
512
|
+
email: user.email ?? "",
|
|
513
|
+
...UNMODELLED_PROFILE
|
|
432
514
|
},
|
|
433
515
|
memberships: [
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
516
|
+
own,
|
|
517
|
+
// A user is part of a resource only through a role on it.
|
|
518
|
+
...owned.flatMap((child) => {
|
|
519
|
+
const membership = membershipOn(identity, child);
|
|
520
|
+
return membership === void 0 || membership.roleNames.length === 0 ? [] : [membership];
|
|
521
|
+
})
|
|
439
522
|
]
|
|
440
523
|
}, existing = bySanityUserId.get(record.sanityUserId);
|
|
441
524
|
if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(record))
|
|
442
525
|
throw fakeError(
|
|
443
|
-
`request(): two registered
|
|
526
|
+
`request(): two identities (registered, or the handle's inline accessControl) resolve to user "${record.sanityUserId}" on ${resource.type} "${resource.id}" with different records. Fix the identities so duplicate ids carry identical records.`
|
|
444
527
|
);
|
|
445
528
|
bySanityUserId.set(record.sanityUserId, record);
|
|
446
529
|
}
|
|
447
530
|
return [...bySanityUserId.values()];
|
|
448
531
|
}
|
|
532
|
+
const DEFAULT_ATTRIBUTES_LIMIT = 50;
|
|
533
|
+
function serveMyAttributes(args) {
|
|
534
|
+
const { ctx, request, organizationId, query } = args, organization = ACCESS_CAPABILITIES.organization, registry = { organizationId: ctx.organizationId, resources: ctx.resources };
|
|
535
|
+
if (organization.unknown !== void 0 && !organization.unknown.holds(registry, organizationId))
|
|
536
|
+
throw organization.unknown.refusal(request);
|
|
537
|
+
const entry = ctx.actorAccess()?.[accessKey("organization", organizationId)];
|
|
538
|
+
if (entry?.roles === void 0) throw notFound("User")(request);
|
|
539
|
+
const limit = attributesLimit(query), cursor = query.get("cursor"), attributes = (entry.attributes ?? []).map(attributeRecord).toSorted((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0).filter((attribute) => cursor === null || attribute.key > cursor).slice(0, limit);
|
|
540
|
+
return { value: { sanityUserId: ctx.actorUser().id, organizationId, attributes } };
|
|
541
|
+
}
|
|
542
|
+
function attributesLimit(query) {
|
|
543
|
+
const raw = query.get("limit");
|
|
544
|
+
if (raw === null) return DEFAULT_ATTRIBUTES_LIMIT;
|
|
545
|
+
const limit = Number(raw);
|
|
546
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
547
|
+
throw fakeError(
|
|
548
|
+
`request(): users/me/attributes got limit=${JSON.stringify(raw)}; the fake serves positive integer limits only (the platform's answer to others is not simulated).`
|
|
549
|
+
);
|
|
550
|
+
return limit;
|
|
551
|
+
}
|
|
552
|
+
function attributeRecord(attribute) {
|
|
553
|
+
const { key, type, values } = attribute, activeSource = values.sanity !== void 0 ? "sanity" : "saml", activeValue = values[activeSource];
|
|
554
|
+
if (activeValue === void 0)
|
|
555
|
+
throw fakeError(
|
|
556
|
+
`request(): user attribute "${key}" is seeded with no value; give it a \`saml\` or \`sanity\` value.`
|
|
557
|
+
);
|
|
558
|
+
return { type, key, values: { ...values }, activeSource, activeValue };
|
|
559
|
+
}
|
|
449
560
|
class WriteAccessDeniedError extends Error {
|
|
450
561
|
documentId;
|
|
451
562
|
permission;
|
|
@@ -517,7 +628,7 @@ function consistentProjectRecords(args) {
|
|
|
517
628
|
const existing = byId.get(record.id);
|
|
518
629
|
if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(record))
|
|
519
630
|
throw fakeError(
|
|
520
|
-
`request(): two registered
|
|
631
|
+
`request(): two identities (registered, or the handle's inline accessControl) resolve to project user "${record.id}" in project "${args.projectId}" with different records. The project records cannot be served unambiguously \u2014 fix the identities so duplicate ids carry identical records.`
|
|
521
632
|
);
|
|
522
633
|
byId.set(record.id, record);
|
|
523
634
|
}
|
|
@@ -602,7 +713,7 @@ function makeRequestImpl(ctx) {
|
|
|
602
713
|
const served = path === void 0 ? void 0 : await servePlatformRequest(platform, path);
|
|
603
714
|
if (served !== void 0) return served.value;
|
|
604
715
|
throw fakeError(
|
|
605
|
-
`request(): no handler for path "${path}". With access control configured, the fake serves "/users/me", the handle's ACL ("<data base>/acl"), "/projects/<projectId>" (members), "/projects/<projectId>/users/<ids>", the History API (client.getDataUrl("history", \u2026)), resource lookup ("/media-libraries/<id>", "/canvases/<id>", "/dashboards/<id>") and the Access API ("/access/<type>/<id>/roles | users | user-permissions/me[/check]"). For any other endpoint, register a stub (client.stub("request", fn)) or intercept the HTTP call with a tool like MSW.`
|
|
716
|
+
`request(): no handler for path "${path}". With access control configured, the fake serves "/users/me", the handle's ACL ("<data base>/acl"), "/projects/<projectId>" (members), "/projects/<projectId>/users/<ids>", the History API (client.getDataUrl("history", \u2026)), resource lookup ("/media-libraries/<id>", "/canvases/<id>", "/dashboards/<id>") and the Access API ("/access/<type>/<id>/roles[/<name>] | users[/<id>] | user-permissions/me[/check]") and the caller's attributes ("/organizations/<id>/users/me/attributes"). For any other endpoint, register a stub (client.stub("request", fn)) or intercept the HTTP call with a tool like MSW.`
|
|
606
717
|
);
|
|
607
718
|
};
|
|
608
719
|
}
|
|
@@ -1665,7 +1776,17 @@ function makeHandle(handleArgs) {
|
|
|
1665
1776
|
if (access === void 0)
|
|
1666
1777
|
throw fakeError("this handle has no acting identity (no token or accessControl bound).");
|
|
1667
1778
|
return access;
|
|
1668
|
-
},
|
|
1779
|
+
}, members = () => [
|
|
1780
|
+
...backend.identities.values(),
|
|
1781
|
+
...token === void 0 && accessControl !== void 0 ? [
|
|
1782
|
+
{
|
|
1783
|
+
user: accessControl.currentUser,
|
|
1784
|
+
...accessControl.globalUser !== void 0 ? { globalUser: accessControl.globalUser } : {},
|
|
1785
|
+
grants: accessControl.grants,
|
|
1786
|
+
...accessControl.access !== void 0 ? { access: accessControl.access } : {}
|
|
1787
|
+
}
|
|
1788
|
+
] : []
|
|
1789
|
+
], capabilities = capabilitiesOf(target.type), targetLabel = capabilities.label(target.id), { store, history } = ensureEntry(registry, target), concurrentEditKey = (id) => registryKey(target) + " " + id, actorId = () => {
|
|
1669
1790
|
const access = activeAccess();
|
|
1670
1791
|
if (access !== void 0)
|
|
1671
1792
|
return capabilities.identity === "global" ? (access.globalUser ?? access.currentUser).id : access.currentUser.id;
|
|
@@ -2021,7 +2142,7 @@ function makeHandle(handleArgs) {
|
|
|
2021
2142
|
// client to the global API host.
|
|
2022
2143
|
useProjectHostname: useProjectHostname && resource === void 0,
|
|
2023
2144
|
organizationId: backend.organizationId,
|
|
2024
|
-
listIdentities:
|
|
2145
|
+
listIdentities: members,
|
|
2025
2146
|
platform: {
|
|
2026
2147
|
projectId,
|
|
2027
2148
|
organizationId: backend.organizationId,
|
|
@@ -2031,9 +2152,13 @@ function makeHandle(handleArgs) {
|
|
|
2031
2152
|
history,
|
|
2032
2153
|
canRead
|
|
2033
2154
|
} : void 0,
|
|
2155
|
+
actorUser: () => {
|
|
2156
|
+
const access = requireActiveAccess();
|
|
2157
|
+
return access.globalUser ?? access.currentUser;
|
|
2158
|
+
},
|
|
2034
2159
|
actorAccess: () => requireActiveAccess().access,
|
|
2035
|
-
identities:
|
|
2036
|
-
|
|
2160
|
+
identities: members,
|
|
2161
|
+
resources: () => [...registry.values()].map((entry) => entry.resource)
|
|
2037
2162
|
}
|
|
2038
2163
|
})
|
|
2039
2164
|
} : {},
|