permdock 0.0.0 → 0.1.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 +268 -1
- package/dist/a2a/index.d.ts +178 -0
- package/dist/a2a/index.js +271 -0
- package/dist/ai-sdk/index.d.ts +74 -0
- package/dist/ai-sdk/index.js +137 -0
- package/dist/approval-required-TzNB9oQA.js +6 -0
- package/dist/approvals/index.d.ts +3 -0
- package/dist/approvals/index.js +4 -0
- package/dist/approvals-DaxY-Bdg.js +218 -0
- package/dist/approvers-aH0PJkmf.js +20 -0
- package/dist/arazzo-CTeVuXr5.js +70 -0
- package/dist/arazzo-aoTkqYsJ.js +54 -0
- package/dist/authzen/index.d.ts +48 -0
- package/dist/authzen/index.js +336 -0
- package/dist/better-auth/index.d.ts +92 -0
- package/dist/better-auth/index.js +324 -0
- package/dist/budget-JqcRqyhK.d.ts +11 -0
- package/dist/budget-NbEtbJC_.js +11 -0
- package/dist/capability-B68OFnZx.js +171 -0
- package/dist/capability-BDG_nkxt.d.ts +99 -0
- package/dist/catalog/index.d.ts +12 -0
- package/dist/catalog/index.js +2 -0
- package/dist/catalog-B0E6bggc.js +48 -0
- package/dist/catalog-H-vFGwWK.js +55 -0
- package/dist/catalog-doc-BSo_Q_-6.js +298 -0
- package/dist/catalog-idsHc0vo.js +2 -0
- package/dist/catalog-path-CnI0XTFK.js +12 -0
- package/dist/claude-agent/index.d.ts +81 -0
- package/dist/claude-agent/index.js +83 -0
- package/dist/clerk/index.d.ts +56 -0
- package/dist/clerk/index.js +280 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +20 -0
- package/dist/cli/index.d.ts +46 -0
- package/dist/cli/index.js +5 -0
- package/dist/cloud/index.d.ts +91 -0
- package/dist/cloud/index.js +390 -0
- package/dist/cloud-BNQTrmrr.js +50 -0
- package/dist/cloud-Cuxr6Czw.js +117 -0
- package/dist/collect-B7_TIfmg.js +49 -0
- package/dist/collect-O-tOtbnk.js +350 -0
- package/dist/collect-fRPAQIWb.js +2 -0
- package/dist/commands-BMh_Etm2.js +93 -0
- package/dist/compact-y6vfu2_f.js +19 -0
- package/dist/compare-pfyVnBjE.js +12 -0
- package/dist/compile-CQbvyIBr.d.ts +30 -0
- package/dist/compile-D22iwvuV.js +558 -0
- package/dist/config-BPYmjTOp.js +26 -0
- package/dist/connection-JvqwHODC.js +194 -0
- package/dist/context-CKEAabd7.js +73 -0
- package/dist/context-gtAwAILx.js +6 -0
- package/dist/convex/index.d.ts +40 -0
- package/dist/convex/index.js +55 -0
- package/dist/create-BKF3dgFk.d.ts +162 -0
- package/dist/create-CSkhDU9T.js +222 -0
- package/dist/credential-B_rkUNlY.js +326 -0
- package/dist/credentials-BgRX3WEg.js +268 -0
- package/dist/describe-Bt1YD5uS.js +99 -0
- package/dist/describe-DernyItf.d.ts +37 -0
- package/dist/diff-BUzf5Poi.js +48 -0
- package/dist/diff-x5Il8LVa.js +418 -0
- package/dist/digest-B3kc_PgQ.d.ts +19 -0
- package/dist/digest-BI-6aa6d.js +27 -0
- package/dist/doctor-BGle8Gqi.js +1314 -0
- package/dist/doctor-Bqo6FwUa.js +53 -0
- package/dist/drizzle/index.d.ts +72 -0
- package/dist/drizzle/index.js +160 -0
- package/dist/duration-xK2jGFVc.js +25 -0
- package/dist/elysia/index.d.ts +71 -0
- package/dist/elysia/index.js +103 -0
- package/dist/errors-DSMnAVNa.js +43 -0
- package/dist/errors-DbZ1YOnb.d.ts +138 -0
- package/dist/errors-Iy71s7Oj.js +177 -0
- package/dist/errors-m3u9TmDu.js +27 -0
- package/dist/evaluations-DsAU9ppl.js +479 -0
- package/dist/eve/index.d.ts +95 -0
- package/dist/eve/index.js +181 -0
- package/dist/express/index.d.ts +46 -0
- package/dist/express/index.js +108 -0
- package/dist/fastify/index.d.ts +56 -0
- package/dist/fastify/index.js +141 -0
- package/dist/files-B_wAGYmt.js +121 -0
- package/dist/fixtures-Cvaw0gDG.js +64 -0
- package/dist/freeze-y1_jQL4v.js +31 -0
- package/dist/from-snapshot-BMGnhmgv.js +552 -0
- package/dist/generate-CH7ThJ14.js +173 -0
- package/dist/grantee-Bv04_Vzf.js +521 -0
- package/dist/headers-B5RRv3Xm.js +6 -0
- package/dist/headers-C8SHQccD.d.ts +6 -0
- package/dist/helpers-DnSOrhB9.js +682 -0
- package/dist/hono/index.d.ts +70 -0
- package/dist/hono/index.js +131 -0
- package/dist/hosted-BwKQ1BTA.js +265 -0
- package/dist/http-DPuDaChb.d.ts +13 -0
- package/dist/http-GgVApX1k.js +111 -0
- package/dist/index-CltcQ4Bf.d.ts +138 -0
- package/dist/index-LF_hS-yn.d.ts +177 -0
- package/dist/index-fG6NSXlc.d.ts +9 -0
- package/dist/index-xIxJLwOQ.d.ts +415 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +21 -0
- package/dist/jwt/index.d.ts +87 -0
- package/dist/jwt/index.js +571 -0
- package/dist/kernel-YOGCWNLf.js +211 -0
- package/dist/kysely/index.d.ts +77 -0
- package/dist/kysely/index.js +129 -0
- package/dist/load-BZ24CpZP.js +69 -0
- package/dist/load-QOOm1z70.js +2 -0
- package/dist/load-jose-BL6LH4bL.js +11 -0
- package/dist/lru-8q4WZjBV.js +28 -0
- package/dist/manifest-DDQbC6K0.d.ts +118 -0
- package/dist/map-DowSO96j.js +158 -0
- package/dist/map-claims-B_6I1Rn-.js +360 -0
- package/dist/map-error-RJ3ZsUyA.js +16 -0
- package/dist/markers-B0hYLvMD.js +60 -0
- package/dist/may-use-CnFEjVEp.js +37 -0
- package/dist/mcp/index.d.ts +127 -0
- package/dist/mcp/index.js +563 -0
- package/dist/nest/index.d.ts +80 -0
- package/dist/nest/index.js +270 -0
- package/dist/next/client.d.ts +24 -0
- package/dist/next/client.js +31 -0
- package/dist/next/index.d.ts +110 -0
- package/dist/next/index.js +195 -0
- package/dist/next/plugin.d.ts +14 -0
- package/dist/next/plugin.js +2 -0
- package/dist/node/index.d.ts +43 -0
- package/dist/node/index.js +66 -0
- package/dist/openai/index.d.ts +71 -0
- package/dist/openai/index.js +105 -0
- package/dist/openapi/index.d.ts +93 -0
- package/dist/openapi/index.js +3 -0
- package/dist/openapi-CwePaF7C.js +164 -0
- package/dist/openapi-DWD0Uc_4.js +250 -0
- package/dist/openapi-DcqM8nU7.js +306 -0
- package/dist/openapi-import-BWRUNNeW.js +5959 -0
- package/dist/openapi-schema-D8j_T9tO.js +6902 -0
- package/dist/orpc/index.d.ts +64 -0
- package/dist/orpc/index.js +188 -0
- package/dist/otel/index.d.ts +13 -0
- package/dist/otel/index.js +202 -0
- package/dist/package-root-zGkxO3oN.js +30 -0
- package/dist/parse-DPfIvFMU.js +135 -0
- package/dist/pdp/index.d.ts +22 -0
- package/dist/pdp/index.js +795 -0
- package/dist/peer-BOr_kJE1.js +31 -0
- package/dist/permdock-yoZ7EjxX.js +2956 -0
- package/dist/permissions-B4apnahQ.d.ts +185 -0
- package/dist/permissions-B8vCg_bI.d.ts +52 -0
- package/dist/permissions-CofRqesP.js +142 -0
- package/dist/permissions-DC7GROPA.js +600 -0
- package/dist/pg-4GJWDUxG.js +953 -0
- package/dist/pins-CqE1jZuF.js +12 -0
- package/dist/plugin-4gr7IxFJ.js +65 -0
- package/dist/policy-ANp36PnI.js +971 -0
- package/dist/policy-CInmNGkM.d.ts +1959 -0
- package/dist/policy-Cq2fB8Q5.d.ts +20 -0
- package/dist/prisma/index.d.ts +135 -0
- package/dist/prisma/index.js +430 -0
- package/dist/problem-Cgp9-KZA.js +226 -0
- package/dist/prompt-CXYhLfsS.js +32 -0
- package/dist/protected-B4K3BKoX.js +138 -0
- package/dist/protected-BuPAycJT.d.ts +26 -0
- package/dist/protected-wY7nXhY9.d.ts +23 -0
- package/dist/provider-_wx46HpB.js +48 -0
- package/dist/provider-wTAUda2v.d.ts +6 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +6 -0
- package/dist/react/provider-client.d.ts +2 -0
- package/dist/react/provider-client.js +3 -0
- package/dist/react-native/index.d.ts +42 -0
- package/dist/react-native/index.js +174 -0
- package/dist/refs-ChLSxbST.js +90 -0
- package/dist/relations-r4soIFZB.js +426 -0
- package/dist/revocations-Cy06JNx2.d.ts +21 -0
- package/dist/rls-BOFU3chG.js +233 -0
- package/dist/rls-CJVja9Ww.js +2119 -0
- package/dist/rls-graph-DxQ39PJj.js +371 -0
- package/dist/rls-helpers-BxRuif0l.js +1346 -0
- package/dist/rls-import-CXV6LBlS.js +942 -0
- package/dist/rls-migrate-DrFPbf8p.js +366 -0
- package/dist/row-check-BajTgQZ-.d.ts +43 -0
- package/dist/run-CJ6UvS3a.js +155 -0
- package/dist/runtime-DP9ttFh1.js +27 -0
- package/dist/saas-BRrZzJkM.js +1193 -0
- package/dist/schema-P8yoma__.js +296 -0
- package/dist/scim/index.d.ts +2 -0
- package/dist/scim/index.js +2 -0
- package/dist/scim-9NFa0XVZ.js +1445 -0
- package/dist/scopes-cKVp_WTw.js +212 -0
- package/dist/server/index.d.ts +123 -0
- package/dist/server/index.js +8 -0
- package/dist/sha256-BO0swRXL.js +150 -0
- package/dist/sink-Bd9PSyfq.js +141 -0
- package/dist/sink-Cfr9-7YC.d.ts +104 -0
- package/dist/skills-C_lpV4Jx.js +50 -0
- package/dist/skills-Dx3QLnMY.js +110 -0
- package/dist/snapshot-DE8dE_hz.js +1051 -0
- package/dist/solid/index.d.ts +63 -0
- package/dist/solid/index.js +197 -0
- package/dist/source-DoBBYJi2.js +6 -0
- package/dist/sources-DOpW5Ila.d.ts +347 -0
- package/dist/sources-DVHM3Mwq.js +284 -0
- package/dist/sql-Bll1d7y0.js +22 -0
- package/dist/src-CTNT-PBW.js +430 -0
- package/dist/ssf/index.d.ts +3 -0
- package/dist/ssf/index.js +622 -0
- package/dist/store-Bq90395i.d.ts +27 -0
- package/dist/store-C6hFW6ta.js +508 -0
- package/dist/stores-CkG2HrSg.js +140 -0
- package/dist/stores-CvOnu_2T.d.ts +41 -0
- package/dist/stream-BhMfZOwr.d.ts +12 -0
- package/dist/stream-Q_xlB6Ko.js +52 -0
- package/dist/style-CTm1AXDT.js +22 -0
- package/dist/subject-C6dLxqHU.js +24 -0
- package/dist/subject-settings-xt7V845g.js +82 -0
- package/dist/supabase/index.d.ts +67 -0
- package/dist/supabase/index.js +4 -0
- package/dist/supabase/middleware.d.ts +77 -0
- package/dist/supabase/middleware.js +68 -0
- package/dist/supabase-D8UlBA7Z.js +83 -0
- package/dist/supabase-DTxGtYdi.js +796 -0
- package/dist/supabase-hook-DW-xP4hU.js +2 -0
- package/dist/supabase-hook-D_OqGb_V.js +1528 -0
- package/dist/svelte/Protected.svelte +34 -0
- package/dist/svelte/index.d.ts +10 -0
- package/dist/svelte/index.js +53 -0
- package/dist/svelte/runtime.d.ts +6 -0
- package/dist/svelte/runtime.js +3 -0
- package/dist/svelte/source.d.ts +10 -0
- package/dist/svelte/source.js +8 -0
- package/dist/terminal/index.d.ts +171 -0
- package/dist/terminal/index.js +821 -0
- package/dist/testing/index.d.ts +656 -0
- package/dist/testing/index.js +3208 -0
- package/dist/testing/saas/index.d.ts +194 -0
- package/dist/testing/saas/index.js +3 -0
- package/dist/testing/saas/permissions.d.ts +2 -0
- package/dist/testing/saas/permissions.js +2 -0
- package/dist/text-diff-BdkkYSbi.js +15 -0
- package/dist/thenable-okc2xDsj.js +10 -0
- package/dist/timeout-D51niuQP.js +8 -0
- package/dist/trpc/index.d.ts +80 -0
- package/dist/trpc/index.js +205 -0
- package/dist/types-B8w5PFtS.d.ts +141 -0
- package/dist/types-B9dmiiab.d.ts +66 -0
- package/dist/types-BBK-exvJ.d.ts +177 -0
- package/dist/types-BvBk1wgn.d.ts +118 -0
- package/dist/types-CxquUByI.d.ts +9 -0
- package/dist/types-D6Z6-rKJ.d.ts +312 -0
- package/dist/types-DbWdEfcm.d.ts +87 -0
- package/dist/types-bva3i4Gf.d.ts +98 -0
- package/dist/types-i4BAOvdb.d.ts +97 -0
- package/dist/unplugin/index.d.ts +7 -0
- package/dist/unplugin/index.js +30 -0
- package/dist/usage-1ozMubLY.js +42 -0
- package/dist/usage-CUnK6Ou9.js +530 -0
- package/dist/usage-PyEor6GA.js +2 -0
- package/dist/validity-D0uRmJKJ.js +224 -0
- package/dist/verifier-B90agnhl.js +461 -0
- package/dist/version-CnSh33Fj.js +16 -0
- package/dist/vocabulary-Dk0fs-Jk.d.ts +56 -0
- package/dist/vue/index.d.ts +57 -0
- package/dist/vue/index.js +203 -0
- package/dist/webmcp/index.d.ts +75 -0
- package/dist/webmcp/index.js +313 -0
- package/package.json +423 -4
- package/rulesets/permdock.yaml +29 -0
- package/schemas/catalog-v1.json +507 -0
- package/schemas/openapi/oas-3.1.json +1298 -0
- package/schemas/openapi/oas-3.2.json +1516 -0
- package/schemas/openapi/overlay-1.1.json +79 -0
- package/schemas/supabase-claims-v1.json +139 -0
- package/schemas/supabase-manifest-v1.json +217 -0
- package/skills/permdock/SKILL.md +66 -0
- package/skills/permdock-agents/SKILL.md +77 -0
- package/skills/permdock-agents/references/runtimes.md +211 -0
- package/skills/permdock-approvals/SKILL.md +80 -0
- package/skills/permdock-approvals/references/stores-and-resume.md +83 -0
- package/skills/permdock-audit/SKILL.md +67 -0
- package/skills/permdock-credentials/SKILL.md +55 -0
- package/skills/permdock-credentials/references/tokens-keys-links.md +127 -0
- package/skills/permdock-data/SKILL.md +57 -0
- package/skills/permdock-data/references/queries-and-rls.md +54 -0
- package/skills/permdock-data/references/relationships.md +65 -0
- package/skills/permdock-tenancy/SKILL.md +77 -0
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +83 -0
- package/skills/permdock-wire/SKILL.md +116 -0
- package/skills/permdock-wire/references/adapters.md +480 -0
- package/skills/permdock-wire/references/policy-options.md +43 -0
|
@@ -0,0 +1,1193 @@
|
|
|
1
|
+
import { r as principal } from "./refs-ChLSxbST.js";
|
|
2
|
+
import { a as definePolicy, d as role, m as memoryLimitStore, o as deny, t as allow } from "./policy-ANp36PnI.js";
|
|
3
|
+
import { m as memoryRoleSource } from "./src-CTNT-PBW.js";
|
|
4
|
+
import { d as relation, n as anyone } from "./grantee-Bv04_Vzf.js";
|
|
5
|
+
import { t as memoryRelations } from "./relations-r4soIFZB.js";
|
|
6
|
+
import { i as quoteSqlLiteral } from "./sql-Bll1d7y0.js";
|
|
7
|
+
import { a as saasPlans, i as saasPermissions, o as saasRoles } from "./permissions-CofRqesP.js";
|
|
8
|
+
//#region src/testing/saas/policy.ts
|
|
9
|
+
function isSubject(user) {
|
|
10
|
+
return user !== null && "context" in user && "principal" in user;
|
|
11
|
+
}
|
|
12
|
+
const p = saasPermissions;
|
|
13
|
+
const r = saasRoles;
|
|
14
|
+
const readers = [
|
|
15
|
+
r.viewer,
|
|
16
|
+
r.member,
|
|
17
|
+
r.admin,
|
|
18
|
+
r.owner
|
|
19
|
+
];
|
|
20
|
+
const writers = [
|
|
21
|
+
r.member,
|
|
22
|
+
r.admin,
|
|
23
|
+
r.owner
|
|
24
|
+
];
|
|
25
|
+
const admins = [r.admin, r.owner];
|
|
26
|
+
function each(list, build) {
|
|
27
|
+
return list.map((to) => build(to));
|
|
28
|
+
}
|
|
29
|
+
/** Daily API-key quota per subject; the bucket should be per tenant too. */
|
|
30
|
+
const SAAS_API_KEY_LIMIT = 5;
|
|
31
|
+
const saasPolicy = definePolicy({
|
|
32
|
+
permissions: saasPermissions,
|
|
33
|
+
roles: saasRoles,
|
|
34
|
+
plans: saasPlans
|
|
35
|
+
}, {
|
|
36
|
+
scopes: {
|
|
37
|
+
tenant: { key: "orgId" },
|
|
38
|
+
team: {
|
|
39
|
+
key: "teamId",
|
|
40
|
+
within: "tenant"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
subject: (user) => isSubject(user) ? user.principal : user,
|
|
44
|
+
roles: [role(r.lead, [allow(p.doc.update, { to: r.lead }), deny(p.doc.update, (doc) => doc.locked)]), role(r.collaborator, [allow(p.project.update, { to: r.collaborator })], { on: p.project })],
|
|
45
|
+
grants: [
|
|
46
|
+
...each(readers, (to) => allow(p.project.read, { to })),
|
|
47
|
+
...each(readers, (to) => allow(p.project.list, { to })),
|
|
48
|
+
...each(readers, (to) => allow(p.integration.read, { to })),
|
|
49
|
+
...each(writers, (to) => allow(p.project.create, { to })),
|
|
50
|
+
allow(p.project.update, {
|
|
51
|
+
to: r.member,
|
|
52
|
+
where: { ownerId: principal.id }
|
|
53
|
+
}),
|
|
54
|
+
allow(p.project.delete, {
|
|
55
|
+
to: r.member,
|
|
56
|
+
where: { ownerId: principal.id }
|
|
57
|
+
}),
|
|
58
|
+
...each(admins, (to) => allow(p.project.update, { to })),
|
|
59
|
+
...each(admins, (to) => allow(p.project.delete, { to })),
|
|
60
|
+
...each(admins, (to) => allow(p.member.list, { to })),
|
|
61
|
+
...each(admins, (to) => allow(p.member.invite, { to })),
|
|
62
|
+
...each(admins, (to) => allow(p.member.assignRole, { to })),
|
|
63
|
+
...each(admins, (to) => allow(p.settings.manage, { to })),
|
|
64
|
+
...each(admins, (to) => allow(p.apiKey.manage, { to })),
|
|
65
|
+
...each(admins, (to) => allow(p.apiKey.create, {
|
|
66
|
+
to,
|
|
67
|
+
limit: {
|
|
68
|
+
count: 5,
|
|
69
|
+
per: "day"
|
|
70
|
+
}
|
|
71
|
+
})),
|
|
72
|
+
allow(p.apiKey.revokeAll, {
|
|
73
|
+
to: r.admin,
|
|
74
|
+
approval: {
|
|
75
|
+
by: r.owner,
|
|
76
|
+
distinct: true
|
|
77
|
+
}
|
|
78
|
+
}),
|
|
79
|
+
allow(p.apiKey.revokeAll, { to: r.owner }),
|
|
80
|
+
...each(admins, (to) => allow(p.analytics.read, { to: [to, saasPlans.pro] })),
|
|
81
|
+
...each(admins, (to) => allow(p.audit.read, { to: [to, saasPlans.pro] })),
|
|
82
|
+
...each(admins, (to) => allow(p.sso.manage, { to: [to, saasPlans.pro] })),
|
|
83
|
+
allow(p.billing.read, { to: r.owner }),
|
|
84
|
+
allow(p.billing.manage, { to: r.owner }),
|
|
85
|
+
deny(p.project.delete, {
|
|
86
|
+
to: anyone(),
|
|
87
|
+
where: { archived: true }
|
|
88
|
+
}),
|
|
89
|
+
...each(readers, (to) => allow(p.doc.read, { to })),
|
|
90
|
+
allow(p.doc.update, { to: relation(p.doc, "team") }),
|
|
91
|
+
...each(admins, (to) => allow(p.folder.read, { to })),
|
|
92
|
+
allow(p.folder.read, { to: relation(p.folder, "viewer", {
|
|
93
|
+
through: "parent",
|
|
94
|
+
depth: 8
|
|
95
|
+
}) }),
|
|
96
|
+
allow(p.folder.update, { to: relation(p.folder, "editor", {
|
|
97
|
+
through: "parent",
|
|
98
|
+
depth: 8
|
|
99
|
+
}) })
|
|
100
|
+
]
|
|
101
|
+
});
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/testing/saas/seed.ts
|
|
104
|
+
/** Epoch seconds in the past, so the membership is expired for any clock. */
|
|
105
|
+
const SAAS_EXPIRED_AT = 1e9;
|
|
106
|
+
/**
|
|
107
|
+
* Users, one hazard each:
|
|
108
|
+
* - alice: admin in acme, viewer in globex (different role per org)
|
|
109
|
+
* - bob: member in acme (owns p1, p3, p4)
|
|
110
|
+
* - carol: owner in acme
|
|
111
|
+
* - dave: custom `contractor` role in acme (includes member)
|
|
112
|
+
* - erin: admin in acme AND globex (same role in two orgs)
|
|
113
|
+
* - frank: expired acme membership
|
|
114
|
+
* - mallory: no memberships at all
|
|
115
|
+
* - gina: `lead` of team-a in acme
|
|
116
|
+
* - hank: viewer in acme, `collaborator` on project p3 only
|
|
117
|
+
* - `user-2` and `2`: members of `org-1`; `Date.parse` maps both ids to one instant
|
|
118
|
+
* - tina: member of `tenant-1`; `Date.parse('org-1') === Date.parse('tenant-1')`
|
|
119
|
+
* - ivan: viewer in acme who holds the team role `lead` on the tenant membership (the wrong scope)
|
|
120
|
+
*
|
|
121
|
+
* Acme's folders: `root` ─ `eng` ─ `platform` ─ `infra`, and `root` ─ `hr`
|
|
122
|
+
* (restricted) ─ `payroll`. bob views `root`, hank views `hr`, gina edits
|
|
123
|
+
* `eng`, frank's view of `root` expired.
|
|
124
|
+
*/
|
|
125
|
+
const saasSeed = Object.freeze({
|
|
126
|
+
orgs: [
|
|
127
|
+
{
|
|
128
|
+
id: "acme",
|
|
129
|
+
name: "Acme",
|
|
130
|
+
plan: "free",
|
|
131
|
+
customRoles: [{
|
|
132
|
+
tenant: "acme",
|
|
133
|
+
name: "contractor",
|
|
134
|
+
includes: ["member"]
|
|
135
|
+
}]
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "globex",
|
|
139
|
+
name: "Globex",
|
|
140
|
+
plan: "pro",
|
|
141
|
+
customRoles: []
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "org-1",
|
|
145
|
+
name: "Org One",
|
|
146
|
+
plan: "pro",
|
|
147
|
+
customRoles: []
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "tenant-1",
|
|
151
|
+
name: "Tenant One",
|
|
152
|
+
plan: "pro",
|
|
153
|
+
customRoles: []
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
members: [
|
|
157
|
+
{
|
|
158
|
+
user: "alice",
|
|
159
|
+
tenant: "acme",
|
|
160
|
+
roles: ["admin"]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
user: "alice",
|
|
164
|
+
tenant: "globex",
|
|
165
|
+
roles: ["viewer"]
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
user: "bob",
|
|
169
|
+
tenant: "acme",
|
|
170
|
+
roles: ["member"]
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
user: "carol",
|
|
174
|
+
tenant: "acme",
|
|
175
|
+
roles: ["owner"]
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
user: "dave",
|
|
179
|
+
tenant: "acme",
|
|
180
|
+
roles: ["contractor"]
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
user: "erin",
|
|
184
|
+
tenant: "acme",
|
|
185
|
+
roles: ["admin"]
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
user: "erin",
|
|
189
|
+
tenant: "globex",
|
|
190
|
+
roles: ["admin"]
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
user: "frank",
|
|
194
|
+
tenant: "acme",
|
|
195
|
+
roles: ["member"],
|
|
196
|
+
expiresAt: SAAS_EXPIRED_AT
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
user: "gina",
|
|
200
|
+
tenant: "acme",
|
|
201
|
+
roles: ["viewer"]
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
user: "gina",
|
|
205
|
+
tenant: "acme",
|
|
206
|
+
team: "team-a",
|
|
207
|
+
roles: ["lead"]
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
user: "hank",
|
|
211
|
+
tenant: "acme",
|
|
212
|
+
roles: ["viewer"]
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
user: "hank",
|
|
216
|
+
on: {
|
|
217
|
+
resource: "project",
|
|
218
|
+
id: "p3"
|
|
219
|
+
},
|
|
220
|
+
roles: ["collaborator"]
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
user: "user-2",
|
|
224
|
+
tenant: "org-1",
|
|
225
|
+
roles: ["member"]
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
user: "2",
|
|
229
|
+
tenant: "org-1",
|
|
230
|
+
roles: ["member"]
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
user: "tina",
|
|
234
|
+
tenant: "tenant-1",
|
|
235
|
+
roles: ["member"]
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
user: "ivan",
|
|
239
|
+
tenant: "acme",
|
|
240
|
+
roles: ["viewer", "lead"]
|
|
241
|
+
}
|
|
242
|
+
],
|
|
243
|
+
projects: [
|
|
244
|
+
{
|
|
245
|
+
id: "p1",
|
|
246
|
+
orgId: "acme",
|
|
247
|
+
ownerId: "bob",
|
|
248
|
+
name: "Rocket",
|
|
249
|
+
archived: false
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: "p2",
|
|
253
|
+
orgId: "acme",
|
|
254
|
+
ownerId: "alice",
|
|
255
|
+
name: "Anvil",
|
|
256
|
+
archived: false
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: "p3",
|
|
260
|
+
orgId: "acme",
|
|
261
|
+
ownerId: "bob",
|
|
262
|
+
name: "Magnet",
|
|
263
|
+
archived: false
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: "p4",
|
|
267
|
+
orgId: "acme",
|
|
268
|
+
ownerId: "bob",
|
|
269
|
+
name: "Old tunnel",
|
|
270
|
+
archived: true
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: "g1",
|
|
274
|
+
orgId: "globex",
|
|
275
|
+
ownerId: "alice",
|
|
276
|
+
name: "Hammock",
|
|
277
|
+
archived: false
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: "n1",
|
|
281
|
+
orgId: "org-1",
|
|
282
|
+
ownerId: "user-2",
|
|
283
|
+
name: "Numbered",
|
|
284
|
+
archived: false
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: "o1",
|
|
288
|
+
orgId: "org-1",
|
|
289
|
+
ownerId: "tina",
|
|
290
|
+
name: "Lookalike",
|
|
291
|
+
archived: false
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
docs: [
|
|
295
|
+
{
|
|
296
|
+
id: "d1",
|
|
297
|
+
orgId: "acme",
|
|
298
|
+
teamId: "team-a",
|
|
299
|
+
title: "Roadmap",
|
|
300
|
+
locked: false
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: "d2",
|
|
304
|
+
orgId: "acme",
|
|
305
|
+
teamId: "team-a",
|
|
306
|
+
title: "Signed contract",
|
|
307
|
+
locked: true
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
id: "d3",
|
|
311
|
+
orgId: "acme",
|
|
312
|
+
teamId: "team-b",
|
|
313
|
+
title: "Other team",
|
|
314
|
+
locked: false
|
|
315
|
+
}
|
|
316
|
+
],
|
|
317
|
+
folders: [
|
|
318
|
+
{
|
|
319
|
+
id: "root",
|
|
320
|
+
orgId: "acme",
|
|
321
|
+
parentId: null,
|
|
322
|
+
name: "Acme",
|
|
323
|
+
restricted: false
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
id: "eng",
|
|
327
|
+
orgId: "acme",
|
|
328
|
+
parentId: "root",
|
|
329
|
+
name: "Engineering",
|
|
330
|
+
restricted: false
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: "platform",
|
|
334
|
+
orgId: "acme",
|
|
335
|
+
parentId: "eng",
|
|
336
|
+
name: "Platform",
|
|
337
|
+
restricted: false
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
id: "infra",
|
|
341
|
+
orgId: "acme",
|
|
342
|
+
parentId: "platform",
|
|
343
|
+
name: "Infra",
|
|
344
|
+
restricted: false
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: "hr",
|
|
348
|
+
orgId: "acme",
|
|
349
|
+
parentId: "root",
|
|
350
|
+
name: "People",
|
|
351
|
+
restricted: true
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: "payroll",
|
|
355
|
+
orgId: "acme",
|
|
356
|
+
parentId: "hr",
|
|
357
|
+
name: "Payroll",
|
|
358
|
+
restricted: false
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: "globex-root",
|
|
362
|
+
orgId: "globex",
|
|
363
|
+
parentId: null,
|
|
364
|
+
name: "Globex",
|
|
365
|
+
restricted: false
|
|
366
|
+
}
|
|
367
|
+
],
|
|
368
|
+
shares: [
|
|
369
|
+
{
|
|
370
|
+
folder: "root",
|
|
371
|
+
user: "bob",
|
|
372
|
+
relation: "viewer"
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
folder: "hr",
|
|
376
|
+
user: "hank",
|
|
377
|
+
relation: "viewer"
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
folder: "eng",
|
|
381
|
+
user: "gina",
|
|
382
|
+
relation: "editor"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
folder: "root",
|
|
386
|
+
user: "frank",
|
|
387
|
+
relation: "viewer",
|
|
388
|
+
expiresAt: SAAS_EXPIRED_AT
|
|
389
|
+
}
|
|
390
|
+
]
|
|
391
|
+
});
|
|
392
|
+
/** The seed's folder tree and shares as a `RelationSource`, for `createPermDock({ relations })`. */
|
|
393
|
+
function saasRelations(seed = saasSeed) {
|
|
394
|
+
const edges = (relation) => seed.shares.filter((share) => share.relation === relation).map((share) => share.expiresAt === void 0 ? {
|
|
395
|
+
id: share.folder,
|
|
396
|
+
principal: share.user
|
|
397
|
+
} : {
|
|
398
|
+
id: share.folder,
|
|
399
|
+
principal: share.user,
|
|
400
|
+
expiresAt: share.expiresAt
|
|
401
|
+
});
|
|
402
|
+
return memoryRelations(saasPermissions, {
|
|
403
|
+
rows: { folder: seed.folders },
|
|
404
|
+
edges: { folder: {
|
|
405
|
+
viewer: edges("viewer"),
|
|
406
|
+
editor: edges("editor")
|
|
407
|
+
} }
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
/** Every user id in the seed. */
|
|
411
|
+
const saasUsers = [...new Set(saasSeed.members.map((member) => member.user)), "mallory"];
|
|
412
|
+
function saasOrg(id) {
|
|
413
|
+
return saasSeed.orgs.find((org) => org.id === id);
|
|
414
|
+
}
|
|
415
|
+
function toMembership(member) {
|
|
416
|
+
const membership = { roles: [...member.roles] };
|
|
417
|
+
if (member.tenant !== void 0) membership.tenant = member.tenant;
|
|
418
|
+
if (member.team !== void 0) membership.team = member.team;
|
|
419
|
+
if (member.on !== void 0) membership.on = {
|
|
420
|
+
resource: member.on.resource,
|
|
421
|
+
id: member.on.id
|
|
422
|
+
};
|
|
423
|
+
if (member.expiresAt !== void 0) membership.expiresAt = member.expiresAt;
|
|
424
|
+
return membership;
|
|
425
|
+
}
|
|
426
|
+
/** Memberships of `user` as the PermDock `Membership` shape. */
|
|
427
|
+
function saasMemberships(user, members = saasSeed.members) {
|
|
428
|
+
return members.filter((member) => member.user === user).map((member) => toMembership(member));
|
|
429
|
+
}
|
|
430
|
+
/** Custom roles of every org, as `createPermDock({ customRoles })` expects. */
|
|
431
|
+
const saasCustomRoles = saasSeed.orgs.flatMap((org) => org.customRoles);
|
|
432
|
+
/**
|
|
433
|
+
* The principal for `user`, with the plans of the requested `tenant`'s org.
|
|
434
|
+
* The tenant itself is passed to `createPermDock({ tenant })`, never trusted
|
|
435
|
+
* from here.
|
|
436
|
+
*/
|
|
437
|
+
function saasPrincipal(user, tenant) {
|
|
438
|
+
const org = tenant === void 0 ? void 0 : saasOrg(tenant);
|
|
439
|
+
return {
|
|
440
|
+
id: user,
|
|
441
|
+
memberships: saasMemberships(user),
|
|
442
|
+
plans: org === void 0 ? [] : [org.plan]
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
//#endregion
|
|
446
|
+
//#region src/testing/saas/sql.ts
|
|
447
|
+
/**
|
|
448
|
+
* Postgres tables for the SaaS domain. Column names match the resource
|
|
449
|
+
* fields (`"orgId"`, `"ownerId"`) so generated RLS and ORM filters compile
|
|
450
|
+
* against them without a column map. RLS is enabled and forced on the row
|
|
451
|
+
* tables; policies come from `permdock rls generate`.
|
|
452
|
+
*/
|
|
453
|
+
const saasSchemaSql = `
|
|
454
|
+
create table org (
|
|
455
|
+
id text primary key,
|
|
456
|
+
name text not null,
|
|
457
|
+
plan text not null check (plan in ('free', 'pro'))
|
|
458
|
+
);
|
|
459
|
+
create table org_member (
|
|
460
|
+
user_id text not null,
|
|
461
|
+
org_id text references org (id),
|
|
462
|
+
team_id text,
|
|
463
|
+
resource text,
|
|
464
|
+
resource_id text,
|
|
465
|
+
role text not null,
|
|
466
|
+
expires_at bigint
|
|
467
|
+
);
|
|
468
|
+
create table project (
|
|
469
|
+
id text primary key,
|
|
470
|
+
"orgId" text not null references org (id),
|
|
471
|
+
"ownerId" text not null,
|
|
472
|
+
name text not null,
|
|
473
|
+
archived boolean not null default false
|
|
474
|
+
);
|
|
475
|
+
alter table project enable row level security;
|
|
476
|
+
alter table project force row level security;
|
|
477
|
+
create table doc (
|
|
478
|
+
id text primary key,
|
|
479
|
+
"orgId" text not null references org (id),
|
|
480
|
+
"teamId" text,
|
|
481
|
+
title text not null,
|
|
482
|
+
locked boolean not null default false
|
|
483
|
+
);
|
|
484
|
+
alter table doc enable row level security;
|
|
485
|
+
alter table doc force row level security;
|
|
486
|
+
create table folder (
|
|
487
|
+
id text primary key,
|
|
488
|
+
"orgId" text not null references org (id),
|
|
489
|
+
"parentId" text references folder (id),
|
|
490
|
+
name text not null,
|
|
491
|
+
restricted boolean not null default false
|
|
492
|
+
);
|
|
493
|
+
alter table folder enable row level security;
|
|
494
|
+
alter table folder force row level security;
|
|
495
|
+
create table folder_share (
|
|
496
|
+
folder_id text not null references folder (id) on delete cascade,
|
|
497
|
+
user_id text not null,
|
|
498
|
+
expires_at timestamptz
|
|
499
|
+
);
|
|
500
|
+
create table folder_editor (
|
|
501
|
+
folder_id text not null references folder (id) on delete cascade,
|
|
502
|
+
user_id text not null
|
|
503
|
+
);
|
|
504
|
+
`;
|
|
505
|
+
function literal(value) {
|
|
506
|
+
if (value === null || value === void 0) return "null";
|
|
507
|
+
if (typeof value === "number") {
|
|
508
|
+
if (!Number.isFinite(value)) throw new Error("PermDock: non-finite number in fixture seed");
|
|
509
|
+
return String(value);
|
|
510
|
+
}
|
|
511
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
512
|
+
return quoteSqlLiteral(value);
|
|
513
|
+
}
|
|
514
|
+
function values(rows) {
|
|
515
|
+
return rows.map((row) => `(${row.map((cell) => literal(cell)).join(", ")})`).join(",\n ");
|
|
516
|
+
}
|
|
517
|
+
/** `insert` statements for a seed; the default is the full shared seed. */
|
|
518
|
+
function saasSeedSql(seed = saasSeed) {
|
|
519
|
+
const members = seed.members.flatMap((member) => member.roles.map((role) => [
|
|
520
|
+
member.user,
|
|
521
|
+
member.tenant,
|
|
522
|
+
member.team ?? null,
|
|
523
|
+
member.on?.resource ?? null,
|
|
524
|
+
member.on?.id ?? null,
|
|
525
|
+
role,
|
|
526
|
+
member.expiresAt ?? null
|
|
527
|
+
]));
|
|
528
|
+
const statements = [`insert into org (id, name, plan) values\n ${values(seed.orgs.map((org) => [
|
|
529
|
+
org.id,
|
|
530
|
+
org.name,
|
|
531
|
+
org.plan
|
|
532
|
+
]))};`];
|
|
533
|
+
if (members.length > 0) statements.push(`insert into org_member (user_id, org_id, team_id, resource, resource_id, role, expires_at) values\n ${values(members)};`);
|
|
534
|
+
if (seed.projects.length > 0) statements.push(`insert into project (id, "orgId", "ownerId", name, archived) values\n ${values(seed.projects.map((row) => [
|
|
535
|
+
row.id,
|
|
536
|
+
row.orgId,
|
|
537
|
+
row.ownerId,
|
|
538
|
+
row.name,
|
|
539
|
+
row.archived
|
|
540
|
+
]))};`);
|
|
541
|
+
if (seed.docs.length > 0) statements.push(`insert into doc (id, "orgId", "teamId", title, locked) values\n ${values(seed.docs.map((row) => [
|
|
542
|
+
row.id,
|
|
543
|
+
row.orgId,
|
|
544
|
+
row.teamId,
|
|
545
|
+
row.title,
|
|
546
|
+
row.locked
|
|
547
|
+
]))};`);
|
|
548
|
+
if (seed.folders.length > 0) statements.push(`insert into folder (id, "orgId", "parentId", name, restricted) values\n ${values(seed.folders.map((row) => [
|
|
549
|
+
row.id,
|
|
550
|
+
row.orgId,
|
|
551
|
+
row.parentId,
|
|
552
|
+
row.name,
|
|
553
|
+
row.restricted
|
|
554
|
+
]))};`);
|
|
555
|
+
const viewers = seed.shares.filter((share) => share.relation === "viewer");
|
|
556
|
+
if (viewers.length > 0) statements.push(`insert into folder_share (folder_id, user_id, expires_at) values\n ${values(viewers.map((share) => [
|
|
557
|
+
share.folder,
|
|
558
|
+
share.user,
|
|
559
|
+
null
|
|
560
|
+
]))};`, ...viewers.filter((share) => share.expiresAt !== void 0).map((share) => `update folder_share set expires_at = to_timestamp(${String(share.expiresAt)}) where folder_id = ${literal(share.folder)} and user_id = ${literal(share.user)};`));
|
|
561
|
+
const editors = seed.shares.filter((share) => share.relation === "editor");
|
|
562
|
+
if (editors.length > 0) statements.push(`insert into folder_editor (folder_id, user_id) values\n ${values(editors.map((share) => [share.folder, share.user]))};`);
|
|
563
|
+
return `${statements.join("\n")}\n`;
|
|
564
|
+
}
|
|
565
|
+
//#endregion
|
|
566
|
+
//#region src/testing/saas/tokens.ts
|
|
567
|
+
const saasPublicJwk = {
|
|
568
|
+
kty: "EC",
|
|
569
|
+
crv: "P-256",
|
|
570
|
+
x: "p5Q0wX-3-mOBqOcCTP-RHesn80ydMMNOpr_YNY6uE1I",
|
|
571
|
+
y: "Tr8Bo2w8QPJ1l0BfNLOEUsSz2VVJGx8AWMOika2yUeA",
|
|
572
|
+
kid: "e2e",
|
|
573
|
+
alg: "ES256",
|
|
574
|
+
use: "sig"
|
|
575
|
+
};
|
|
576
|
+
const saasPrivateJwk = {
|
|
577
|
+
kty: "EC",
|
|
578
|
+
crv: "P-256",
|
|
579
|
+
x: "p5Q0wX-3-mOBqOcCTP-RHesn80ydMMNOpr_YNY6uE1I",
|
|
580
|
+
y: "Tr8Bo2w8QPJ1l0BfNLOEUsSz2VVJGx8AWMOika2yUeA",
|
|
581
|
+
kid: "e2e",
|
|
582
|
+
alg: "ES256",
|
|
583
|
+
use: "sig",
|
|
584
|
+
d: "8gTksJVtlkViFAL5tSmPaxnrR3QzONTODL6l8xDZiCk"
|
|
585
|
+
};
|
|
586
|
+
const saasJwks = { keys: [saasPublicJwk] };
|
|
587
|
+
const saasIssuer = "https://saas.permdock.test";
|
|
588
|
+
const saasAudience = "permdock-saas";
|
|
589
|
+
const SAAS_TOKEN_TTL_SECONDS = 3600;
|
|
590
|
+
function base64url(bytes) {
|
|
591
|
+
let binary = "";
|
|
592
|
+
for (const byte of bytes) binary += String.fromCodePoint(byte);
|
|
593
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
594
|
+
}
|
|
595
|
+
function encodeJson(value) {
|
|
596
|
+
return base64url(new TextEncoder().encode(JSON.stringify(value)));
|
|
597
|
+
}
|
|
598
|
+
/** Signs an ES256 access token for `sub` with WebCrypto; no dependency. */
|
|
599
|
+
async function signSaasToken(sub, options = {}) {
|
|
600
|
+
const iat = options.now ?? Math.floor(Date.now() / 1e3);
|
|
601
|
+
const memberships = options.memberships === false ? void 0 : options.memberships ?? saasMemberships(sub);
|
|
602
|
+
const header = {
|
|
603
|
+
alg: "ES256",
|
|
604
|
+
kid: saasPublicJwk.kid,
|
|
605
|
+
typ: "at+jwt"
|
|
606
|
+
};
|
|
607
|
+
const payload = {
|
|
608
|
+
...options.claims,
|
|
609
|
+
...memberships === void 0 ? {} : { memberships },
|
|
610
|
+
iss: options.issuer ?? "https://saas.permdock.test",
|
|
611
|
+
aud: options.audience ?? "permdock-saas",
|
|
612
|
+
sub,
|
|
613
|
+
iat,
|
|
614
|
+
exp: iat + (options.ttl ?? 3600)
|
|
615
|
+
};
|
|
616
|
+
const input = `${encodeJson(header)}.${encodeJson(payload)}`;
|
|
617
|
+
const key = await crypto.subtle.importKey("jwk", { ...saasPrivateJwk }, {
|
|
618
|
+
name: "ECDSA",
|
|
619
|
+
namedCurve: "P-256"
|
|
620
|
+
}, false, ["sign"]);
|
|
621
|
+
const signature = await crypto.subtle.sign({
|
|
622
|
+
name: "ECDSA",
|
|
623
|
+
hash: "SHA-256"
|
|
624
|
+
}, key, new TextEncoder().encode(input));
|
|
625
|
+
return `${input}.${base64url(new Uint8Array(signature))}`;
|
|
626
|
+
}
|
|
627
|
+
function decodeSegment(segment) {
|
|
628
|
+
const padded = segment.replaceAll("-", "+").replaceAll("_", "/");
|
|
629
|
+
const binary = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
|
|
630
|
+
return Uint8Array.from(binary, (char) => char.codePointAt(0) ?? 0);
|
|
631
|
+
}
|
|
632
|
+
function parseSegment(segment) {
|
|
633
|
+
try {
|
|
634
|
+
const parsed = JSON.parse(new TextDecoder().decode(decodeSegment(segment)));
|
|
635
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
636
|
+
} catch {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Verifies a token from `signSaasToken` (ES256, `kid`, `iss`, `aud`, `exp`)
|
|
642
|
+
* and returns its `sub`, or `null`; never throws. Identity only: memberships
|
|
643
|
+
* and plans come from the seed, never from the token.
|
|
644
|
+
*/
|
|
645
|
+
async function verifySaasToken(token, now = Math.floor(Date.now() / 1e3)) {
|
|
646
|
+
return (await verifySaasSession(token, now))?.sub ?? null;
|
|
647
|
+
}
|
|
648
|
+
/** `verifySaasToken` plus the token's `exp` as `expiresAt` (epoch seconds). */
|
|
649
|
+
async function verifySaasSession(token, now = Math.floor(Date.now() / 1e3)) {
|
|
650
|
+
const parts = typeof token === "string" ? token.split(".") : [];
|
|
651
|
+
const [head, body, signature] = parts;
|
|
652
|
+
if (parts.length !== 3 || head === void 0 || body === void 0 || signature === void 0) return null;
|
|
653
|
+
const header = parseSegment(head);
|
|
654
|
+
const payload = parseSegment(body);
|
|
655
|
+
if (header?.["alg"] !== "ES256" || header["kid"] !== saasPublicJwk.kid || payload === null) return null;
|
|
656
|
+
try {
|
|
657
|
+
const key = await crypto.subtle.importKey("jwk", { ...saasPublicJwk }, {
|
|
658
|
+
name: "ECDSA",
|
|
659
|
+
namedCurve: "P-256"
|
|
660
|
+
}, false, ["verify"]);
|
|
661
|
+
if (!await crypto.subtle.verify({
|
|
662
|
+
name: "ECDSA",
|
|
663
|
+
hash: "SHA-256"
|
|
664
|
+
}, key, decodeSegment(signature), new TextEncoder().encode(`${head}.${body}`))) return null;
|
|
665
|
+
} catch {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
const { sub, iss, aud, exp } = payload;
|
|
669
|
+
if (typeof sub !== "string" || iss !== "https://saas.permdock.test" || aud !== "permdock-saas" || typeof exp !== "number" || exp <= now) return null;
|
|
670
|
+
return {
|
|
671
|
+
sub,
|
|
672
|
+
expiresAt: exp
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
//#endregion
|
|
676
|
+
//#region src/testing/saas/scenarios.ts
|
|
677
|
+
/** What the policy's `subject` receives: the principal, or a `Subject` carrying the actor. */
|
|
678
|
+
function saasUser(scenario) {
|
|
679
|
+
const principal = saasPrincipal(scenario.user, scenario.tenant);
|
|
680
|
+
if (scenario.actor === void 0 && scenario.delegation === void 0) return principal;
|
|
681
|
+
return {
|
|
682
|
+
principal,
|
|
683
|
+
context: {},
|
|
684
|
+
...scenario.actor === void 0 ? {} : { actor: scenario.actor },
|
|
685
|
+
...scenario.delegation === void 0 ? {} : { delegation: scenario.delegation }
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
/** A memory quota store in which every subject has already used `used` units. */
|
|
689
|
+
function saasLimitStore(used = 0) {
|
|
690
|
+
const inner = memoryLimitStore();
|
|
691
|
+
const primed = /* @__PURE__ */ new Set();
|
|
692
|
+
const prime = (input) => {
|
|
693
|
+
const id = [
|
|
694
|
+
input.key,
|
|
695
|
+
input.subjectId,
|
|
696
|
+
input.tenant ?? ""
|
|
697
|
+
].join("\0");
|
|
698
|
+
if (primed.has(id)) return;
|
|
699
|
+
primed.add(id);
|
|
700
|
+
for (let index = 0; index < used; index += 1) inner.consume(input);
|
|
701
|
+
};
|
|
702
|
+
return {
|
|
703
|
+
consume(input) {
|
|
704
|
+
prime(input);
|
|
705
|
+
return inner.consume(input);
|
|
706
|
+
},
|
|
707
|
+
remaining(input) {
|
|
708
|
+
prime(input);
|
|
709
|
+
return inner.remaining(input);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
/** The `createPermDock` options a scenario runs with: tenant, custom roles, quota and folder tree. */
|
|
714
|
+
function saasScenarioOptions(scenario) {
|
|
715
|
+
return {
|
|
716
|
+
...scenario.tenant === void 0 ? {} : { tenant: scenario.tenant },
|
|
717
|
+
customRoles: memoryRoleSource(saasCustomRoles),
|
|
718
|
+
limits: saasLimitStore(scenario.quotaUsed),
|
|
719
|
+
relations: saasRelations()
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function saasProject(id) {
|
|
723
|
+
const row = saasSeed.projects.find((project) => project.id === id);
|
|
724
|
+
if (row === void 0) throw new Error(`unknown fixture project ${id}`);
|
|
725
|
+
return row;
|
|
726
|
+
}
|
|
727
|
+
function saasDoc(id) {
|
|
728
|
+
const row = saasSeed.docs.find((doc) => doc.id === id);
|
|
729
|
+
if (row === void 0) throw new Error(`unknown fixture doc ${id}`);
|
|
730
|
+
return row;
|
|
731
|
+
}
|
|
732
|
+
function saasFolder(id) {
|
|
733
|
+
const row = saasSeed.folders.find((folder) => folder.id === id);
|
|
734
|
+
if (row === void 0) throw new Error(`unknown fixture folder ${id}`);
|
|
735
|
+
return row;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* The folder tree, decided with `createPermDock({ relations: saasRelations() })`.
|
|
739
|
+
* A snapshot carries no graph, so every graph grant is server-only on a client.
|
|
740
|
+
*/
|
|
741
|
+
const saasFolderScenarios = Object.freeze([
|
|
742
|
+
{
|
|
743
|
+
name: "a viewer of the root reads a folder three levels down",
|
|
744
|
+
user: "bob",
|
|
745
|
+
tenant: "acme",
|
|
746
|
+
permission: saasPermissions.folder.read,
|
|
747
|
+
row: saasFolder("infra"),
|
|
748
|
+
expected: { outcome: "granted" },
|
|
749
|
+
clientOutcome: "denied"
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
name: "a viewer of the root does not reach a restricted branch",
|
|
753
|
+
user: "bob",
|
|
754
|
+
tenant: "acme",
|
|
755
|
+
permission: saasPermissions.folder.read,
|
|
756
|
+
row: saasFolder("payroll"),
|
|
757
|
+
expected: { outcome: "denied" }
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
name: "a viewer of the restricted folder reads inside it",
|
|
761
|
+
user: "hank",
|
|
762
|
+
tenant: "acme",
|
|
763
|
+
permission: saasPermissions.folder.read,
|
|
764
|
+
row: saasFolder("payroll"),
|
|
765
|
+
expected: { outcome: "granted" },
|
|
766
|
+
clientOutcome: "denied"
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
name: "an editor of a folder updates its descendants but not its parent",
|
|
770
|
+
user: "gina",
|
|
771
|
+
tenant: "acme",
|
|
772
|
+
permission: saasPermissions.folder.update,
|
|
773
|
+
row: saasFolder("platform"),
|
|
774
|
+
expected: { outcome: "granted" },
|
|
775
|
+
clientOutcome: "denied"
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
name: "an editor does not update above the shared folder",
|
|
779
|
+
user: "gina",
|
|
780
|
+
tenant: "acme",
|
|
781
|
+
permission: saasPermissions.folder.update,
|
|
782
|
+
row: saasFolder("root"),
|
|
783
|
+
expected: { outcome: "denied" }
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
name: "an expired share reaches nothing",
|
|
787
|
+
user: "frank",
|
|
788
|
+
tenant: "acme",
|
|
789
|
+
permission: saasPermissions.folder.read,
|
|
790
|
+
row: saasFolder("eng"),
|
|
791
|
+
expected: { outcome: "denied" }
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
name: "an admin reads every folder in the org, restricted or not",
|
|
795
|
+
user: "alice",
|
|
796
|
+
tenant: "acme",
|
|
797
|
+
permission: saasPermissions.folder.read,
|
|
798
|
+
row: saasFolder("payroll"),
|
|
799
|
+
expected: { outcome: "granted" }
|
|
800
|
+
}
|
|
801
|
+
]);
|
|
802
|
+
/**
|
|
803
|
+
* Hand-written expectations. Never derive `expected` from the engine: the
|
|
804
|
+
* list is the oracle every adapter, ORM and client suite runs against.
|
|
805
|
+
*/
|
|
806
|
+
const saasScenarios = Object.freeze([
|
|
807
|
+
{
|
|
808
|
+
name: "admin updates any project in their org",
|
|
809
|
+
user: "alice",
|
|
810
|
+
tenant: "acme",
|
|
811
|
+
permission: saasPermissions.project.update,
|
|
812
|
+
row: saasProject("p1"),
|
|
813
|
+
expected: { outcome: "granted" }
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
name: "viewer in the second org cannot update there",
|
|
817
|
+
user: "alice",
|
|
818
|
+
tenant: "globex",
|
|
819
|
+
permission: saasPermissions.project.update,
|
|
820
|
+
row: saasProject("g1"),
|
|
821
|
+
expected: { outcome: "denied" }
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
name: "viewer in the second org reads there",
|
|
825
|
+
user: "alice",
|
|
826
|
+
tenant: "globex",
|
|
827
|
+
permission: saasPermissions.project.read,
|
|
828
|
+
row: saasProject("g1"),
|
|
829
|
+
expected: { outcome: "granted" }
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
name: "admin in acme cannot read a globex row while acting in acme",
|
|
833
|
+
user: "alice",
|
|
834
|
+
tenant: "acme",
|
|
835
|
+
permission: saasPermissions.project.read,
|
|
836
|
+
row: saasProject("g1"),
|
|
837
|
+
expected: { outcome: "denied" }
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
name: "member updates their own project",
|
|
841
|
+
user: "bob",
|
|
842
|
+
tenant: "acme",
|
|
843
|
+
permission: saasPermissions.project.update,
|
|
844
|
+
row: saasProject("p1"),
|
|
845
|
+
expected: { outcome: "granted" }
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
name: "member cannot update a colleague project",
|
|
849
|
+
user: "bob",
|
|
850
|
+
tenant: "acme",
|
|
851
|
+
permission: saasPermissions.project.update,
|
|
852
|
+
row: saasProject("p2"),
|
|
853
|
+
expected: { outcome: "denied" }
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
name: "archived project cannot be deleted by its owner",
|
|
857
|
+
user: "bob",
|
|
858
|
+
tenant: "acme",
|
|
859
|
+
permission: saasPermissions.project.delete,
|
|
860
|
+
row: saasProject("p4"),
|
|
861
|
+
expected: { outcome: "denied" }
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
name: "archived project cannot be deleted by the org owner",
|
|
865
|
+
user: "carol",
|
|
866
|
+
tenant: "acme",
|
|
867
|
+
permission: saasPermissions.project.delete,
|
|
868
|
+
row: saasProject("p4"),
|
|
869
|
+
expected: { outcome: "denied" }
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
name: "owner manages billing",
|
|
873
|
+
user: "carol",
|
|
874
|
+
tenant: "acme",
|
|
875
|
+
permission: saasPermissions.billing.manage,
|
|
876
|
+
expected: { outcome: "granted" }
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
name: "admin cannot read billing",
|
|
880
|
+
user: "alice",
|
|
881
|
+
tenant: "acme",
|
|
882
|
+
permission: saasPermissions.billing.read,
|
|
883
|
+
expected: { outcome: "denied" }
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
name: "free plan hides analytics from admins",
|
|
887
|
+
user: "alice",
|
|
888
|
+
tenant: "acme",
|
|
889
|
+
permission: saasPermissions.analytics.read,
|
|
890
|
+
expected: {
|
|
891
|
+
outcome: "denied",
|
|
892
|
+
reason: "not-entitled"
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
name: "pro plan shows analytics to admins",
|
|
897
|
+
user: "erin",
|
|
898
|
+
tenant: "globex",
|
|
899
|
+
permission: saasPermissions.analytics.read,
|
|
900
|
+
expected: { outcome: "granted" }
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
name: "same admin, free org: no analytics",
|
|
904
|
+
user: "erin",
|
|
905
|
+
tenant: "acme",
|
|
906
|
+
permission: saasPermissions.analytics.read,
|
|
907
|
+
expected: {
|
|
908
|
+
outcome: "denied",
|
|
909
|
+
reason: "not-entitled"
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
name: "custom contractor role inherits member create",
|
|
914
|
+
user: "dave",
|
|
915
|
+
tenant: "acme",
|
|
916
|
+
permission: saasPermissions.project.create,
|
|
917
|
+
expected: { outcome: "granted" }
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
name: "custom contractor role cannot update a project it does not own",
|
|
921
|
+
user: "dave",
|
|
922
|
+
tenant: "acme",
|
|
923
|
+
permission: saasPermissions.project.update,
|
|
924
|
+
row: saasProject("p1"),
|
|
925
|
+
expected: { outcome: "denied" }
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
name: "expired membership reads nothing",
|
|
929
|
+
user: "frank",
|
|
930
|
+
tenant: "acme",
|
|
931
|
+
permission: saasPermissions.project.read,
|
|
932
|
+
row: saasProject("p1"),
|
|
933
|
+
expected: { outcome: "denied" }
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
name: "user without memberships reads nothing",
|
|
937
|
+
user: "mallory",
|
|
938
|
+
tenant: "acme",
|
|
939
|
+
permission: saasPermissions.project.read,
|
|
940
|
+
row: saasProject("p1"),
|
|
941
|
+
expected: { outcome: "denied" }
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
name: "user without memberships and no tenant lists nothing",
|
|
945
|
+
user: "mallory",
|
|
946
|
+
permission: saasPermissions.project.list,
|
|
947
|
+
expected: { outcome: "denied" }
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
name: "owner id user-2 updates its own row",
|
|
951
|
+
user: "user-2",
|
|
952
|
+
tenant: "org-1",
|
|
953
|
+
permission: saasPermissions.project.update,
|
|
954
|
+
row: saasProject("n1"),
|
|
955
|
+
expected: { outcome: "granted" }
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
name: "id 2 is not id user-2",
|
|
959
|
+
user: "2",
|
|
960
|
+
tenant: "org-1",
|
|
961
|
+
permission: saasPermissions.project.update,
|
|
962
|
+
row: saasProject("n1"),
|
|
963
|
+
expected: { outcome: "denied" }
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
name: "tenant-1 member cannot read an org-1 row",
|
|
967
|
+
user: "tina",
|
|
968
|
+
tenant: "tenant-1",
|
|
969
|
+
permission: saasPermissions.project.read,
|
|
970
|
+
row: saasProject("o1"),
|
|
971
|
+
expected: { outcome: "denied" }
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
name: "team lead updates a team doc",
|
|
975
|
+
user: "gina",
|
|
976
|
+
tenant: "acme",
|
|
977
|
+
permission: saasPermissions.doc.update,
|
|
978
|
+
row: saasDoc("d1"),
|
|
979
|
+
expected: { outcome: "granted" },
|
|
980
|
+
clientOutcome: "denied"
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
name: "team lead cannot update a locked doc",
|
|
984
|
+
user: "gina",
|
|
985
|
+
tenant: "acme",
|
|
986
|
+
permission: saasPermissions.doc.update,
|
|
987
|
+
row: saasDoc("d2"),
|
|
988
|
+
expected: { outcome: "denied" }
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
name: "team lead cannot update another team doc",
|
|
992
|
+
user: "gina",
|
|
993
|
+
tenant: "acme",
|
|
994
|
+
permission: saasPermissions.doc.update,
|
|
995
|
+
row: saasDoc("d3"),
|
|
996
|
+
expected: { outcome: "denied" }
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
name: "viewer reads any doc in the org",
|
|
1000
|
+
user: "gina",
|
|
1001
|
+
tenant: "acme",
|
|
1002
|
+
permission: saasPermissions.doc.read,
|
|
1003
|
+
row: saasDoc("d3"),
|
|
1004
|
+
expected: { outcome: "granted" }
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
name: "project collaborator updates that project",
|
|
1008
|
+
user: "hank",
|
|
1009
|
+
tenant: "acme",
|
|
1010
|
+
permission: saasPermissions.project.update,
|
|
1011
|
+
row: saasProject("p3"),
|
|
1012
|
+
expected: { outcome: "granted" }
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
name: "project collaborator cannot update another project",
|
|
1016
|
+
user: "hank",
|
|
1017
|
+
tenant: "acme",
|
|
1018
|
+
permission: saasPermissions.project.update,
|
|
1019
|
+
row: saasProject("p1"),
|
|
1020
|
+
expected: { outcome: "denied" }
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
name: "admin revoking every key needs an owner approval",
|
|
1024
|
+
user: "alice",
|
|
1025
|
+
tenant: "acme",
|
|
1026
|
+
permission: saasPermissions.apiKey.revokeAll,
|
|
1027
|
+
expected: { outcome: "approval-required" },
|
|
1028
|
+
client: false
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
name: "owner revokes every key directly",
|
|
1032
|
+
user: "carol",
|
|
1033
|
+
tenant: "acme",
|
|
1034
|
+
permission: saasPermissions.apiKey.revokeAll,
|
|
1035
|
+
expected: { outcome: "granted" }
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
name: "member cannot revoke keys",
|
|
1039
|
+
user: "bob",
|
|
1040
|
+
tenant: "acme",
|
|
1041
|
+
permission: saasPermissions.apiKey.revokeAll,
|
|
1042
|
+
expected: { outcome: "denied" }
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
name: "admin creates an api key within the quota",
|
|
1046
|
+
user: "alice",
|
|
1047
|
+
tenant: "acme",
|
|
1048
|
+
permission: saasPermissions.apiKey.create,
|
|
1049
|
+
expected: { outcome: "granted" },
|
|
1050
|
+
client: false
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
name: "member cannot list members",
|
|
1054
|
+
user: "bob",
|
|
1055
|
+
tenant: "acme",
|
|
1056
|
+
permission: saasPermissions.member.list,
|
|
1057
|
+
expected: { outcome: "denied" }
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
name: "admin of the second org lists its members",
|
|
1061
|
+
user: "erin",
|
|
1062
|
+
tenant: "globex",
|
|
1063
|
+
permission: saasPermissions.member.list,
|
|
1064
|
+
expected: { outcome: "granted" }
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
name: "an agent acting for an admin without a delegation reaches nothing",
|
|
1068
|
+
user: "alice",
|
|
1069
|
+
tenant: "acme",
|
|
1070
|
+
actor: {
|
|
1071
|
+
id: "agent-1",
|
|
1072
|
+
kind: "agent"
|
|
1073
|
+
},
|
|
1074
|
+
permission: saasPermissions.project.read,
|
|
1075
|
+
row: saasProject("p1"),
|
|
1076
|
+
expected: {
|
|
1077
|
+
outcome: "denied",
|
|
1078
|
+
reason: "no-delegation"
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
name: "an agent delegated read only reads",
|
|
1083
|
+
user: "alice",
|
|
1084
|
+
tenant: "acme",
|
|
1085
|
+
actor: {
|
|
1086
|
+
id: "agent-1",
|
|
1087
|
+
kind: "agent"
|
|
1088
|
+
},
|
|
1089
|
+
delegation: { scopes: [saasPermissions.project.read.scope] },
|
|
1090
|
+
permission: saasPermissions.project.read,
|
|
1091
|
+
row: saasProject("p1"),
|
|
1092
|
+
expected: { outcome: "granted" }
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
name: "an agent delegated read only cannot update what the admin can",
|
|
1096
|
+
user: "alice",
|
|
1097
|
+
tenant: "acme",
|
|
1098
|
+
actor: {
|
|
1099
|
+
id: "agent-1",
|
|
1100
|
+
kind: "agent"
|
|
1101
|
+
},
|
|
1102
|
+
delegation: { scopes: [saasPermissions.project.read.scope] },
|
|
1103
|
+
permission: saasPermissions.project.update,
|
|
1104
|
+
row: saasProject("p2"),
|
|
1105
|
+
expected: {
|
|
1106
|
+
outcome: "denied",
|
|
1107
|
+
reason: "not-delegated"
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
name: "an admin past the daily api-key quota is refused",
|
|
1112
|
+
user: "alice",
|
|
1113
|
+
tenant: "acme",
|
|
1114
|
+
permission: saasPermissions.apiKey.create,
|
|
1115
|
+
quotaUsed: 5,
|
|
1116
|
+
expected: {
|
|
1117
|
+
outcome: "denied",
|
|
1118
|
+
reason: "limit"
|
|
1119
|
+
},
|
|
1120
|
+
client: false
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
name: "the quota of one org does not spill into the other",
|
|
1124
|
+
user: "erin",
|
|
1125
|
+
tenant: "globex",
|
|
1126
|
+
permission: saasPermissions.apiKey.create,
|
|
1127
|
+
quotaUsed: 4,
|
|
1128
|
+
expected: { outcome: "granted" },
|
|
1129
|
+
client: false
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
name: "a team role held on the tenant membership grants nothing",
|
|
1133
|
+
user: "ivan",
|
|
1134
|
+
tenant: "acme",
|
|
1135
|
+
permission: saasPermissions.doc.update,
|
|
1136
|
+
row: saasDoc("d1"),
|
|
1137
|
+
expected: { outcome: "denied" }
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
name: "a tenant-1 member does not list org-1 projects",
|
|
1141
|
+
user: "tina",
|
|
1142
|
+
tenant: "org-1",
|
|
1143
|
+
permission: saasPermissions.project.list,
|
|
1144
|
+
expected: { outcome: "denied" }
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
name: "an org-1 member cannot create in tenant-1",
|
|
1148
|
+
user: "user-2",
|
|
1149
|
+
tenant: "tenant-1",
|
|
1150
|
+
permission: saasPermissions.project.create,
|
|
1151
|
+
expected: { outcome: "denied" }
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
name: "a tenant-1 member updates its own tenant-1 row only through tenant-1",
|
|
1155
|
+
user: "tina",
|
|
1156
|
+
tenant: "org-1",
|
|
1157
|
+
permission: saasPermissions.project.update,
|
|
1158
|
+
row: saasProject("o1"),
|
|
1159
|
+
expected: { outcome: "denied" }
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
name: "expired membership lists nothing",
|
|
1163
|
+
user: "frank",
|
|
1164
|
+
tenant: "acme",
|
|
1165
|
+
permission: saasPermissions.project.list,
|
|
1166
|
+
expected: { outcome: "denied" }
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
name: "expired membership creates nothing",
|
|
1170
|
+
user: "frank",
|
|
1171
|
+
tenant: "acme",
|
|
1172
|
+
permission: saasPermissions.project.create,
|
|
1173
|
+
expected: { outcome: "denied" }
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
name: "expired membership updates nothing",
|
|
1177
|
+
user: "frank",
|
|
1178
|
+
tenant: "acme",
|
|
1179
|
+
permission: saasPermissions.project.update,
|
|
1180
|
+
row: saasProject("p1"),
|
|
1181
|
+
expected: { outcome: "denied" }
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
name: "expired membership reads no doc",
|
|
1185
|
+
user: "frank",
|
|
1186
|
+
tenant: "acme",
|
|
1187
|
+
permission: saasPermissions.doc.read,
|
|
1188
|
+
row: saasDoc("d1"),
|
|
1189
|
+
expected: { outcome: "denied" }
|
|
1190
|
+
}
|
|
1191
|
+
]);
|
|
1192
|
+
//#endregion
|
|
1193
|
+
export { saasOrg as C, saasUsers as D, saasSeed as E, SAAS_API_KEY_LIMIT as O, saasMemberships as S, saasRelations as T, verifySaasToken as _, saasProject as a, SAAS_EXPIRED_AT as b, saasUser as c, saasIssuer as d, saasJwks as f, verifySaasSession as g, signSaasToken as h, saasLimitStore as i, saasPolicy as k, SAAS_TOKEN_TTL_SECONDS as l, saasPublicJwk as m, saasFolder as n, saasScenarioOptions as o, saasPrivateJwk as p, saasFolderScenarios as r, saasScenarios as s, saasDoc as t, saasAudience as u, saasSchemaSql as v, saasPrincipal as w, saasCustomRoles as x, saasSeedSql as y };
|