permdock 0.0.0 → 0.2.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 +327 -1
- package/dist/a2a/index.d.ts +169 -0
- package/dist/a2a/index.js +232 -0
- package/dist/adapter-context-mlWsVGGg.js +23 -0
- package/dist/ai-sdk/index.d.ts +95 -0
- package/dist/ai-sdk/index.js +154 -0
- package/dist/approval-required-TzNB9oQA.js +6 -0
- package/dist/approvals/index.d.ts +3 -0
- package/dist/approvals/index.js +5 -0
- package/dist/approvals-DbYAuNkG.js +224 -0
- package/dist/approvers-DT4AVyIg.js +52 -0
- package/dist/arazzo-BAgw9ap6.js +70 -0
- package/dist/arazzo-cmvX2jDe.js +54 -0
- package/dist/authzen/index.d.ts +39 -0
- package/dist/authzen/index.js +302 -0
- package/dist/better-auth/index.d.ts +109 -0
- package/dist/better-auth/index.js +344 -0
- package/dist/better-supabase/index.d.ts +206 -0
- package/dist/better-supabase/index.js +620 -0
- package/dist/bind-CVy3Espv.d.ts +25 -0
- package/dist/bind-VnYn2HKq.js +97 -0
- package/dist/boundary-BM8a_P6E.js +13 -0
- package/dist/boundary-BmHxVH1J.d.ts +15 -0
- package/dist/budget-JqcRqyhK.d.ts +11 -0
- package/dist/budget-NbEtbJC_.js +11 -0
- package/dist/canonical-json-DemiK4Vp.js +26 -0
- package/dist/capability-4qPdO9gk.js +173 -0
- package/dist/capability-Dq43tT1u.d.ts +99 -0
- package/dist/catalog/index.d.ts +12 -0
- package/dist/catalog/index.js +2 -0
- package/dist/catalog-BjZSovXQ.js +2 -0
- package/dist/catalog-doc-Dsc9y7sd.js +309 -0
- package/dist/catalog-h2tlKFkq.js +48 -0
- package/dist/catalog-path-CnI0XTFK.js +12 -0
- package/dist/catalog-z8q6bi_r.js +55 -0
- package/dist/claude-agent/index.d.ts +72 -0
- package/dist/claude-agent/index.js +77 -0
- package/dist/clerk/index.d.ts +65 -0
- package/dist/clerk/index.js +301 -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/clients-C7zLFuWT.d.ts +12 -0
- package/dist/clients-DjU5ABMy.js +23 -0
- package/dist/cloud/index.d.ts +90 -0
- package/dist/cloud/index.js +404 -0
- package/dist/cloud-BGhHS-lY.js +50 -0
- package/dist/cloud-Bo3e9Ak3.js +118 -0
- package/dist/collect-DDDv_ESs.js +502 -0
- package/dist/collect-DLlujdHC.js +83 -0
- package/dist/collect-rG2IQ3t0.js +2 -0
- package/dist/commands-DWOrSQic.js +97 -0
- package/dist/compact-y6vfu2_f.js +19 -0
- package/dist/compare-pfyVnBjE.js +12 -0
- package/dist/compile/index.d.ts +3 -0
- package/dist/compile/index.js +2 -0
- package/dist/compile-Ba4blROR.js +613 -0
- package/dist/compile-CNnl6uIt.d.ts +94 -0
- package/dist/config-BJiM-xAm.js +157 -0
- package/dist/config-Bn00Z3ks.js +36 -0
- package/dist/config-report-Dw_JcUqT.js +44 -0
- package/dist/connection-CU8MQxRi.js +194 -0
- package/dist/context-Ttp0ekMT.js +82 -0
- package/dist/convex/index.d.ts +45 -0
- package/dist/convex/index.js +65 -0
- package/dist/create-BWyx3jua.d.ts +245 -0
- package/dist/create-CPhDL6qx.js +253 -0
- package/dist/credential-8P81ff4u.js +306 -0
- package/dist/delegation-D8pUEuce.js +103 -0
- package/dist/describe-B_gVZEVi.js +94 -0
- package/dist/describe-PdoqESvg.d.ts +36 -0
- package/dist/diff-B8Tunc3o.js +451 -0
- package/dist/diff-CzqLxZ77.js +48 -0
- package/dist/digest-B3kc_PgQ.d.ts +19 -0
- package/dist/digest-BI-6aa6d.js +27 -0
- package/dist/doctor-WpvG6p4k.js +1194 -0
- package/dist/doctor-collect-ByfYlNv1.js +664 -0
- package/dist/doctor-rOr-4ZfS.js +53 -0
- package/dist/drizzle/index.d.ts +73 -0
- package/dist/drizzle/index.js +160 -0
- package/dist/duration-xK2jGFVc.js +25 -0
- package/dist/elysia/index.d.ts +49 -0
- package/dist/elysia/index.js +59 -0
- package/dist/errors-CWfoNg9N.js +47 -0
- package/dist/errors-Cz5zn0mW.js +123 -0
- package/dist/errors-DAqevpNk.d.ts +125 -0
- package/dist/errors-DRnGZHzF.js +30 -0
- package/dist/evaluation-items-CfCCejQd.js +40 -0
- package/dist/evaluations-tz6D9A2G.js +461 -0
- package/dist/eve/index.d.ts +86 -0
- package/dist/eve/index.js +176 -0
- package/dist/expiry-BoBJ8Tvc.js +7 -0
- package/dist/express/index.d.ts +23 -0
- package/dist/express/index.js +78 -0
- package/dist/fastify/index.d.ts +33 -0
- package/dist/fastify/index.js +92 -0
- package/dist/fields-BMl1RxoX.js +26 -0
- package/dist/files-DWUN9PzI.js +125 -0
- package/dist/fixtures-ht28Cplr.js +67 -0
- package/dist/freeze-CrU8nQeX.js +60 -0
- package/dist/from-snapshot-C3sHpZNS.js +569 -0
- package/dist/generate-CH7ThJ14.js +173 -0
- package/dist/grantee-Cv5Sogfv.js +602 -0
- package/dist/graph-sql-B-dsAT6j.d.ts +29 -0
- package/dist/headers-BZSDieNT.js +6 -0
- package/dist/headers-Cf-zWvsO.d.ts +6 -0
- package/dist/helpers-H_KARDVC.js +421 -0
- package/dist/hono/index.d.ts +48 -0
- package/dist/hono/index.js +99 -0
- package/dist/hosted-DJQw46hn.js +266 -0
- package/dist/http-BIhmLfCc.js +95 -0
- package/dist/http-DPuDaChb.d.ts +13 -0
- package/dist/http-SO7qGttf.js +44 -0
- package/dist/ids-DC4V4CFu.js +18 -0
- package/dist/index-A0YNL8jZ.d.ts +177 -0
- package/dist/index-CtE07CBF.d.ts +6 -0
- package/dist/index-D3vbkoSI.d.ts +483 -0
- package/dist/index-D6_cSa1z.d.ts +201 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +26 -0
- package/dist/instance-options-BK_rNOSC.d.ts +11 -0
- package/dist/instance-options-Dhhcnm3y.js +22 -0
- package/dist/json-schema-BsAflUGW.js +106 -0
- package/dist/jwt/index.d.ts +91 -0
- package/dist/jwt/index.js +590 -0
- package/dist/kernel-GSQnMz3O.js +208 -0
- package/dist/kysely/index.d.ts +78 -0
- package/dist/kysely/index.js +129 -0
- package/dist/live-options-H2eG8dfJ.js +60 -0
- package/dist/load-DhWEB9P1.js +75 -0
- package/dist/load-DwJUafYN.js +2 -0
- package/dist/load-jose-BL6LH4bL.js +11 -0
- package/dist/lru-8q4WZjBV.js +28 -0
- package/dist/manifest-iaoBXHKc.js +783 -0
- package/dist/manifest-uDS9y5de.d.ts +206 -0
- package/dist/map-AcxK9o_D.js +147 -0
- package/dist/map-claims-Z4QZOiFF.js +369 -0
- package/dist/map-error-pgUWNNtG.js +23 -0
- package/dist/markers-B0hYLvMD.js +60 -0
- package/dist/may-use-DndQ5tQ-.js +77 -0
- package/dist/mcp/index.d.ts +159 -0
- package/dist/mcp/index.js +603 -0
- package/dist/nest/index.d.ts +75 -0
- package/dist/nest/index.js +261 -0
- package/dist/next/client.d.ts +23 -0
- package/dist/next/client.js +33 -0
- package/dist/next/index.d.ts +93 -0
- package/dist/next/index.js +180 -0
- package/dist/next/plugin.d.ts +14 -0
- package/dist/next/plugin.js +33 -0
- package/dist/node/index.d.ts +24 -0
- package/dist/node/index.js +34 -0
- package/dist/openai/index.d.ts +62 -0
- package/dist/openai/index.js +92 -0
- package/dist/openapi/index.d.ts +176 -0
- package/dist/openapi/index.js +3 -0
- package/dist/openapi-Bbg4wGVL.js +164 -0
- package/dist/openapi-BqRBvAng.js +538 -0
- package/dist/openapi-CqqEbHsL.js +250 -0
- package/dist/openapi-import-cmwetbzw.js +5959 -0
- package/dist/openapi-schema--ARjyfQr.js +6902 -0
- package/dist/orpc/index.d.ts +51 -0
- package/dist/orpc/index.js +145 -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-BuyjxhZV.js +53 -0
- package/dist/parse-local-manifest-BkGQNzAh.d.ts +10 -0
- package/dist/parse-local-manifest-BrMozJCo.js +36 -0
- package/dist/pdp/index.d.ts +22 -0
- package/dist/pdp/index.js +682 -0
- package/dist/peer-BOr_kJE1.js +31 -0
- package/dist/permdock-CzqXjCpU.js +3471 -0
- package/dist/permissions-CDgvKG5L.d.ts +52 -0
- package/dist/permissions-CpZrivYv.js +142 -0
- package/dist/permissions-DVoF6dMt.js +662 -0
- package/dist/permissions-DttnqqxC.d.ts +214 -0
- package/dist/pins-8ILTftC9.js +12 -0
- package/dist/policy-CFZAr_Al.d.ts +2252 -0
- package/dist/policy-D8UFyERw.d.ts +20 -0
- package/dist/policy-xyWg7QdR.js +1122 -0
- package/dist/powersync-A8x_Yyvh.js +2 -0
- package/dist/powersync-C7kLglqX.js +540 -0
- package/dist/powersync-yW1UUFzp.js +62 -0
- package/dist/prisma/index.d.ts +136 -0
- package/dist/prisma/index.js +455 -0
- package/dist/problem-B43ztKIm.js +191 -0
- package/dist/problem-details-CkL50EEf.d.ts +7 -0
- package/dist/problem-details-_AMOYz-w.js +144 -0
- package/dist/prompt-CXYhLfsS.js +32 -0
- package/dist/protected-CNsMzrVd.d.ts +27 -0
- package/dist/protected-Cs6kTaHo.js +100 -0
- package/dist/protected-bZs6rbT1.d.ts +40 -0
- package/dist/provider-Cjr62YMV.d.ts +6 -0
- package/dist/provider-DLzpb2Xz.js +52 -0
- package/dist/provider-store-DNyC1y48.js +43 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.js +7 -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 +198 -0
- package/dist/react-native/index.js +696 -0
- package/dist/refs-KYb4ntaz.js +91 -0
- package/dist/relations-cjOL01hZ.js +505 -0
- package/dist/replay-BsOyTrdV.js +28 -0
- package/dist/replay-sqBGMnNs.d.ts +5 -0
- package/dist/resource-ref-CaS9yVPi.js +20 -0
- package/dist/revocations-Bhr0aMIC.js +27 -0
- package/dist/revocations-Cy06JNx2.d.ts +21 -0
- package/dist/rls-BMNydpHk.js +2521 -0
- package/dist/rls-CEvs64Kj.js +238 -0
- package/dist/rls-fields-BMasREhR.js +3502 -0
- package/dist/rls-graph-pc9fuXYm.js +435 -0
- package/dist/rls-import-D8F7j35Q.js +943 -0
- package/dist/rls-migrate-7wikfrVI.js +367 -0
- package/dist/rls-ownership-B5Lkq62W.js +843 -0
- package/dist/rls-shared-aOQ4hGTO.js +493 -0
- package/dist/rls-verify-D8B7dOTh.js +745 -0
- package/dist/row-check-Dt35czLy.d.ts +40 -0
- package/dist/rpc-DOK54_CS.js +89 -0
- package/dist/run-YXCJDNYk.js +174 -0
- package/dist/runtime-CB8gQFvS.js +11 -0
- package/dist/saas-Bj_FFkjN.js +1203 -0
- package/dist/schema-D3MnDMxa.js +300 -0
- package/dist/scim/index.d.ts +2 -0
- package/dist/scim/index.js +2 -0
- package/dist/scim-BFGBBAqR.js +1445 -0
- package/dist/scopes-CVdPrWN-.js +239 -0
- package/dist/server/index.d.ts +134 -0
- package/dist/server/index.js +11 -0
- package/dist/server-DlZ28Oz5.js +268 -0
- package/dist/sha256-BO0swRXL.js +150 -0
- package/dist/shared-DRu8VFVM.js +123 -0
- package/dist/sink-Bd9PSyfq.js +141 -0
- package/dist/sink-Gs6TTVa-.d.ts +104 -0
- package/dist/skills-Biq5uup3.js +192 -0
- package/dist/skills-JaymNCdK.js +56 -0
- package/dist/snapshot-Enxd0n-s.js +975 -0
- package/dist/snapshot-cache-D6jLelln.js +44 -0
- package/dist/snapshot-cache-DwQH8ufx.d.ts +45 -0
- package/dist/solid/index.d.ts +93 -0
- package/dist/solid/index.js +188 -0
- package/dist/sources-B0UvrJ21.js +534 -0
- package/dist/sources-BcJgw0lt.d.ts +192 -0
- package/dist/sql-xdg5f4Fi.js +27 -0
- package/dist/src-CSbUiozF.js +568 -0
- package/dist/ssf/index.d.ts +4 -0
- package/dist/ssf/index.js +596 -0
- package/dist/store-DMAWwAJV.js +371 -0
- package/dist/store-D_pjn91-.d.ts +36 -0
- package/dist/store-options-yO0V6gRn.js +716 -0
- package/dist/stores-4_sqiOA1.js +87 -0
- package/dist/stores-DVR5NHLE.d.ts +52 -0
- package/dist/stream-DDpvynSB.js +66 -0
- package/dist/stream-DlvG2hs-.d.ts +12 -0
- package/dist/style-CTm1AXDT.js +22 -0
- package/dist/subject-C6dLxqHU.js +24 -0
- package/dist/subject-settings-BwBmCdTR.js +128 -0
- package/dist/supabase/index.d.ts +183 -0
- package/dist/supabase/index.js +5 -0
- package/dist/supabase/middleware.d.ts +82 -0
- package/dist/supabase/middleware.js +102 -0
- package/dist/supabase-CwafwxxF.js +83 -0
- package/dist/supabase-DTBaC20O.js +774 -0
- package/dist/supabase-config-Bs44dare.js +472 -0
- package/dist/supabase-hook-BuxLQLpU.js +1814 -0
- package/dist/supabase-hook-D5qVd_D8.js +2 -0
- package/dist/supautils-DYZnzRWV.js +91 -0
- package/dist/svelte/PermissionBoundary.svelte +37 -0
- package/dist/svelte/Protected.svelte +40 -0
- package/dist/svelte/index.d.ts +13 -0
- package/dist/svelte/index.js +107 -0
- package/dist/svelte/runtime.d.ts +7 -0
- package/dist/svelte/runtime.js +4 -0
- package/dist/svelte/source.d.ts +13 -0
- package/dist/svelte/source.js +11 -0
- package/dist/terminal/index.d.ts +163 -0
- package/dist/terminal/index.js +833 -0
- package/dist/testing/index.d.ts +745 -0
- package/dist/testing/index.js +3672 -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/token-CIWxJOCh.js +50 -0
- package/dist/trpc/index.d.ts +61 -0
- package/dist/trpc/index.js +122 -0
- package/dist/types-9AWO9EGt.d.ts +197 -0
- package/dist/types-BPxgszlT.d.ts +550 -0
- package/dist/types-BVyImy5I.d.ts +120 -0
- package/dist/types-Bvc4oWT6.d.ts +9 -0
- package/dist/types-C3s9xVJG.d.ts +48 -0
- package/dist/types-Dnemwd77.d.ts +183 -0
- package/dist/types-kxznc7cn.d.ts +52 -0
- package/dist/types-vKyAMtcX.d.ts +157 -0
- package/dist/types-vNSDF7Dq.d.ts +66 -0
- package/dist/types-yF5LmQVD.d.ts +306 -0
- package/dist/unplugin/index.d.ts +7 -0
- package/dist/unplugin/index.js +31 -0
- package/dist/usage-D07N9QJ3.js +42 -0
- package/dist/usage-D82l1CwK.js +2 -0
- package/dist/usage-DG1oeCca.js +693 -0
- package/dist/validation-error-CnjZ4t92.js +33 -0
- package/dist/validation-error-CtF23SrC.d.ts +26 -0
- package/dist/validity-uOp8UpYn.js +245 -0
- package/dist/verifier-BxBo6I44.js +466 -0
- package/dist/version-BvacVTZR.js +17 -0
- package/dist/views-Soe7bGSC.js +95 -0
- package/dist/vocabulary-DA8a8zif.d.ts +56 -0
- package/dist/vue/index.d.ts +79 -0
- package/dist/vue/index.js +196 -0
- package/dist/watch-Etoa_Mvr.js +2 -0
- package/dist/watch-UihESpsJ.js +128 -0
- package/dist/webmcp/index.d.ts +75 -0
- package/dist/webmcp/index.js +307 -0
- package/package.json +450 -5
- package/rulesets/permdock.yaml +29 -0
- package/schemas/approval-request-v1.json +175 -0
- package/schemas/catalog-v1.json +535 -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 +144 -0
- package/schemas/supabase-manifest-v1.json +361 -0
- package/skills/permdock/SKILL.md +66 -0
- package/skills/permdock-agents/SKILL.md +77 -0
- package/skills/permdock-agents/references/runtimes.md +213 -0
- package/skills/permdock-approvals/SKILL.md +80 -0
- package/skills/permdock-approvals/references/stores-and-resume.md +94 -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 +132 -0
- package/skills/permdock-data/SKILL.md +57 -0
- package/skills/permdock-data/references/queries-and-rls.md +62 -0
- package/skills/permdock-data/references/relationships.md +67 -0
- package/skills/permdock-tenancy/SKILL.md +80 -0
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +89 -0
- package/skills/permdock-wire/SKILL.md +116 -0
- package/skills/permdock-wire/references/adapters.md +492 -0
- package/skills/permdock-wire/references/policy-options.md +45 -0
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
import { l as scopeChain, o as resolveScope } from "./scopes-CVdPrWN-.js";
|
|
2
|
+
import { S as findRole } from "./grantee-Cv5Sogfv.js";
|
|
3
|
+
import { s as roleColumn } from "./sources-B0UvrJ21.js";
|
|
4
|
+
import { F as memberForSources, G as quoteLiteral, I as memberForTable, J as roleKindSql, K as quoteTable, R as memberRoleOf, U as qualifiedTable, W as quoteIdent, X as scopeTable, Y as scopeSources, a as qualified, it as tenantTypeOf, j as kindFilterSql, nt as subjectIdSql, s as signedIn, t as CUSTOM_ROLES, x as globalKindFilterSql, z as memberVia } from "./rls-shared-aOQ4hGTO.js";
|
|
5
|
+
import { D as claimKeptLines, I as sourceFilters, L as sourcesKeep, M as membershipRows, N as membershipTable, O as disabledColumn, P as roleRows, R as hasCustomRoleChecksFor, j as memberColumn, k as globalRoleRows } from "./rls-fields-BMasREhR.js";
|
|
6
|
+
//#region src/cli/rls-ownership.ts
|
|
7
|
+
/** Objects the ownership rules add next to the helpers. Names are part of the SQL contract. */
|
|
8
|
+
const OWNERSHIP = {
|
|
9
|
+
canAssign: "permdock_can_assign",
|
|
10
|
+
canAssignFor: "permdock_can_assign_for",
|
|
11
|
+
canAssignAny: "permdock_can_assign_any",
|
|
12
|
+
canAssignAnyFor: "permdock_can_assign_any_for",
|
|
13
|
+
holders: "permdock_holders",
|
|
14
|
+
transferOnly: "permdock_transfer_only"
|
|
15
|
+
};
|
|
16
|
+
function roleScope(policy, scopes, name) {
|
|
17
|
+
const on = policy.rolesByName.get(name)?.on ?? findRole(policy.vocabulary.roles, name)?.on;
|
|
18
|
+
return typeof on === "string" ? resolveScope(scopes, on) : void 0;
|
|
19
|
+
}
|
|
20
|
+
function isGlobalRole(policy, name) {
|
|
21
|
+
const binding = policy.rolesByName.get(name);
|
|
22
|
+
if (binding !== void 0) return binding.on === void 0;
|
|
23
|
+
const leaf = findRole(policy.vocabulary.roles, name);
|
|
24
|
+
return leaf !== void 0 && leaf.on === void 0;
|
|
25
|
+
}
|
|
26
|
+
/** The rules `rls generate` enforces; `undefined` when no role declares one. */
|
|
27
|
+
function ownershipRules(policy, scopes) {
|
|
28
|
+
const kinds = {};
|
|
29
|
+
const assigns = [];
|
|
30
|
+
const counted = [];
|
|
31
|
+
for (const binding of policy.roles) {
|
|
32
|
+
const scope = roleScope(policy, scopes, binding.name);
|
|
33
|
+
if (binding.for !== void 0) kinds[binding.name] = binding.for;
|
|
34
|
+
const assigner = scope ?? (binding.on === void 0 ? "global" : void 0);
|
|
35
|
+
for (const target of binding.assigns ?? []) {
|
|
36
|
+
const at = roleScope(policy, scopes, target);
|
|
37
|
+
if (assigner !== void 0 && at !== void 0 && (assigner === "global" || scopeChain(scopes, at).includes(assigner))) assigns.push({
|
|
38
|
+
assigner: binding.name,
|
|
39
|
+
scope: assigner,
|
|
40
|
+
role: target,
|
|
41
|
+
at
|
|
42
|
+
});
|
|
43
|
+
else if (assigner === "global" && isGlobalRole(policy, target)) assigns.push({
|
|
44
|
+
assigner: binding.name,
|
|
45
|
+
scope: "global",
|
|
46
|
+
role: target,
|
|
47
|
+
at: "global"
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const min = binding.min ?? 0;
|
|
51
|
+
const transferOnly = binding.transferOnly === true;
|
|
52
|
+
if (scope !== void 0 && (min > 0 || binding.max !== void 0 || transferOnly)) counted.push(binding.max === void 0 ? {
|
|
53
|
+
role: binding.name,
|
|
54
|
+
scope,
|
|
55
|
+
min,
|
|
56
|
+
transferOnly
|
|
57
|
+
} : {
|
|
58
|
+
role: binding.name,
|
|
59
|
+
scope,
|
|
60
|
+
min,
|
|
61
|
+
max: binding.max,
|
|
62
|
+
transferOnly
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (Object.keys(kinds).length === 0 && assigns.length === 0 && counted.length === 0) return;
|
|
66
|
+
return {
|
|
67
|
+
kinds,
|
|
68
|
+
assigns,
|
|
69
|
+
counted
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function objectName(prefix, scope) {
|
|
73
|
+
return quoteIdent(`${prefix}_${scope}`);
|
|
74
|
+
}
|
|
75
|
+
/** Live holders of `roleExpr` in instance `idExpr`, with the same kind and expiry rules as the helpers. */
|
|
76
|
+
function holdersSql(ctx, table, column, idExpr, role, into) {
|
|
77
|
+
const roleExpr = typeof role === "string" ? quoteLiteral(role) : role.expr;
|
|
78
|
+
const held = memberRoleOf(table, "m", " ");
|
|
79
|
+
const filters = [`m.${quoteIdent(column)} = ${idExpr}`, `${held.sql} = ${roleExpr}`];
|
|
80
|
+
if (table.expiresAt !== void 0) {
|
|
81
|
+
const expires = memberColumn(table.expiresAt);
|
|
82
|
+
filters.push(`(${expires} is null or ${expires} > now())`);
|
|
83
|
+
}
|
|
84
|
+
const disabled = disabledColumn(table);
|
|
85
|
+
if (disabled !== void 0) filters.push(`${disabled} is null`);
|
|
86
|
+
const via = memberVia(table);
|
|
87
|
+
const kind = typeof role === "string" ? roleKindSql(ctx, role, via) : kindFilterSql(ctx, roleExpr, via);
|
|
88
|
+
if (kind !== void 0) filters.push(kind);
|
|
89
|
+
return `select count(distinct m.${quoteIdent(table.user)}) into ${into}
|
|
90
|
+
from ${membershipTable(table.table)} m${held.join}
|
|
91
|
+
where ${filters.join("\n and ")}`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* `min` and `max` as a deferred constraint trigger: checked at commit, so a
|
|
95
|
+
* transfer (demote one holder, promote another) passes in one transaction. An
|
|
96
|
+
* instance with no memberships left is being torn down and skips `min`.
|
|
97
|
+
*/
|
|
98
|
+
function holdersTriggerSql(ctx, scope, table, column, counted) {
|
|
99
|
+
const fn = qualified(ctx, `${OWNERSHIP.holders}_${scope}`);
|
|
100
|
+
const col = quoteIdent(column);
|
|
101
|
+
const checks = counted.map((rule) => {
|
|
102
|
+
const lines = [` select count(*) into v_total from ${membershipTable(table.table)} m where m.${col} = v_key;`, ` ${holdersSql(ctx, table, column, "v_key", rule.role, "v_count")};`];
|
|
103
|
+
if (rule.min > 0) lines.push(` if v_total > 0 and v_count < ${String(rule.min)} then
|
|
104
|
+
raise exception using
|
|
105
|
+
errcode = '23514',
|
|
106
|
+
message = ${quoteLiteral(`permdock: ${scope} `)} || v_id || ${quoteLiteral(` keeps at least ${String(rule.min)} ${rule.role}`)},
|
|
107
|
+
hint = 'last-holder';
|
|
108
|
+
end if;`);
|
|
109
|
+
if (rule.max !== void 0) lines.push(` if v_count > ${String(rule.max)} then
|
|
110
|
+
raise exception using
|
|
111
|
+
errcode = '23514',
|
|
112
|
+
message = ${quoteLiteral(`permdock: ${scope} `)} || v_id || ${quoteLiteral(` has at most ${String(rule.max)} ${rule.role}`)},
|
|
113
|
+
hint = 'max-holders';
|
|
114
|
+
end if;`);
|
|
115
|
+
return lines.join("\n");
|
|
116
|
+
});
|
|
117
|
+
return `-- ${scope}: holder counts (min / max), checked at commit
|
|
118
|
+
create or replace function ${fn}()
|
|
119
|
+
returns trigger
|
|
120
|
+
language plpgsql
|
|
121
|
+
security definer
|
|
122
|
+
set search_path = ''
|
|
123
|
+
as $$
|
|
124
|
+
declare
|
|
125
|
+
v_ids text[] := '{}';
|
|
126
|
+
v_id text;
|
|
127
|
+
v_key ${membershipTable(table.table)}.${col}%type;
|
|
128
|
+
v_total bigint;
|
|
129
|
+
v_count bigint;
|
|
130
|
+
begin
|
|
131
|
+
if tg_op in ('UPDATE', 'DELETE') then
|
|
132
|
+
v_ids := v_ids || old.${col}::text;
|
|
133
|
+
end if;
|
|
134
|
+
if tg_op in ('INSERT', 'UPDATE') then
|
|
135
|
+
v_ids := v_ids || new.${col}::text;
|
|
136
|
+
end if;
|
|
137
|
+
foreach v_id in array v_ids loop
|
|
138
|
+
continue when v_id is null;
|
|
139
|
+
v_key := v_id;
|
|
140
|
+
${checks.join("\n")}
|
|
141
|
+
end loop;
|
|
142
|
+
return null;
|
|
143
|
+
end;
|
|
144
|
+
$$;
|
|
145
|
+
drop trigger if exists ${objectName(OWNERSHIP.holders, scope)} on ${membershipTable(table.table)};
|
|
146
|
+
create constraint trigger ${objectName(OWNERSHIP.holders, scope)}
|
|
147
|
+
after insert or update or delete on ${membershipTable(table.table)}
|
|
148
|
+
deferrable initially deferred
|
|
149
|
+
for each row execute function ${fn}();`;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* `transferOnly` as statement triggers over transition tables: a statement
|
|
153
|
+
* that changes a role's holder count in an instance that had holders before
|
|
154
|
+
* and still has some after is refused. Creating the first holder and removing
|
|
155
|
+
* the last (which `min` guards) are not transfers.
|
|
156
|
+
*/
|
|
157
|
+
function transferTriggerSql(ctx, scope, table, column, roles) {
|
|
158
|
+
const fn = qualified(ctx, `${OWNERSHIP.transferOnly}_${scope}`);
|
|
159
|
+
const col = quoteIdent(column);
|
|
160
|
+
const list = `array[${roles.map(quoteLiteral).join(", ")}]::text[]`;
|
|
161
|
+
const rows = (alias, source, delta) => {
|
|
162
|
+
const role = memberRoleOf(table, alias);
|
|
163
|
+
return `select ${alias}.${col} as id, ${role.sql} as role, ${delta} as delta from ${source} ${alias}${role.join} where ${role.sql} = any(${list})`;
|
|
164
|
+
};
|
|
165
|
+
const loop = (source) => ` for v_change in
|
|
166
|
+
select c.id, c.role, sum(c.delta) as delta
|
|
167
|
+
from (${source}) c
|
|
168
|
+
group by c.id, c.role
|
|
169
|
+
loop
|
|
170
|
+
continue when v_change.delta = 0 or v_change.id is null;
|
|
171
|
+
${holdersSql(ctx, table, column, "v_change.id", { expr: "v_change.role" }, "v_after")};
|
|
172
|
+
if v_after > 0 and v_after - v_change.delta > 0 then
|
|
173
|
+
raise exception using
|
|
174
|
+
errcode = '23514',
|
|
175
|
+
message = 'permdock: ' || v_change.role || ${quoteLiteral(` in ${scope} `)} || v_change.id || ' moves only by transfer',
|
|
176
|
+
hint = 'transfer-only';
|
|
177
|
+
end if;
|
|
178
|
+
end loop;`;
|
|
179
|
+
const inserted = rows("n", "permdock_new", "1");
|
|
180
|
+
const deleted = rows("o", "permdock_old", "-1");
|
|
181
|
+
const target = membershipTable(table.table);
|
|
182
|
+
const trigger = (op) => {
|
|
183
|
+
const name = quoteIdent(`${OWNERSHIP.transferOnly}_${scope}_${op}`);
|
|
184
|
+
return `drop trigger if exists ${name} on ${target};
|
|
185
|
+
create trigger ${name}
|
|
186
|
+
after ${op} on ${target}
|
|
187
|
+
referencing ${op === "insert" ? "new table as permdock_new" : op === "delete" ? "old table as permdock_old" : "old table as permdock_old new table as permdock_new"}
|
|
188
|
+
for each statement execute function ${fn}();`;
|
|
189
|
+
};
|
|
190
|
+
return `-- ${scope}: transfer-only roles (${roles.join(", ")}) keep their holder count per statement
|
|
191
|
+
create or replace function ${fn}()
|
|
192
|
+
returns trigger
|
|
193
|
+
language plpgsql
|
|
194
|
+
security definer
|
|
195
|
+
set search_path = ''
|
|
196
|
+
as $$
|
|
197
|
+
declare
|
|
198
|
+
v_change record;
|
|
199
|
+
v_after bigint;
|
|
200
|
+
begin
|
|
201
|
+
if tg_op = 'INSERT' then
|
|
202
|
+
${loop(inserted)}
|
|
203
|
+
elsif tg_op = 'DELETE' then
|
|
204
|
+
${loop(deleted)}
|
|
205
|
+
else
|
|
206
|
+
${loop(`${inserted} union all ${deleted}`)}
|
|
207
|
+
end if;
|
|
208
|
+
return null;
|
|
209
|
+
end;
|
|
210
|
+
$$;
|
|
211
|
+
${trigger("insert")}
|
|
212
|
+
${trigger("update")}
|
|
213
|
+
${trigger("delete")}`;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The membership sources holding scope `name`: `undefined` when there are
|
|
217
|
+
* none, `'not-tables'` when one of them carries no table to put a trigger on.
|
|
218
|
+
*/
|
|
219
|
+
function holderSources(ctx, name) {
|
|
220
|
+
const sources = memberForSources(ctx, name);
|
|
221
|
+
if (sources.length === 0) return;
|
|
222
|
+
const held = [];
|
|
223
|
+
for (const source of sources) {
|
|
224
|
+
if (source.sql.holders === void 0) return "not-tables";
|
|
225
|
+
held.push({
|
|
226
|
+
table: source.sql.table,
|
|
227
|
+
holders: source.sql.holders
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return held;
|
|
231
|
+
}
|
|
232
|
+
function indent(sql, by) {
|
|
233
|
+
return sql.replaceAll(/^/gmu, by);
|
|
234
|
+
}
|
|
235
|
+
/** Every source's rows of one instance, each compared with its own typed `v_key_<n>`. */
|
|
236
|
+
function sourceRowsOf(scope, sources) {
|
|
237
|
+
return sources.map((source, index) => indent(source.holders.rows(scope, { id: `v_key_${String(index)}` }), " ")).join("\n union all\n");
|
|
238
|
+
}
|
|
239
|
+
function sourceKeys(sources) {
|
|
240
|
+
return {
|
|
241
|
+
declare: sources.map((source, index) => ` v_key_${String(index)} ${source.holders.idType};`).join("\n"),
|
|
242
|
+
assign: (value, by) => sources.map((_, index) => `${by}v_key_${String(index)} := ${value};`).join("\n")
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function regclassOf(table) {
|
|
246
|
+
return `${quoteLiteral(membershipTable(table))}::regclass`;
|
|
247
|
+
}
|
|
248
|
+
function sourceTables(sources) {
|
|
249
|
+
return [...new Set(sources.map((source) => membershipTable(source.table)))];
|
|
250
|
+
}
|
|
251
|
+
/** `min` and `max` over membership sources: the same commit-time check as on a mapped table, counted over every source of the scope. */
|
|
252
|
+
function sourceHoldersTriggerSql(ctx, scope, sources, counted) {
|
|
253
|
+
const fn = qualified(ctx, `${OWNERSHIP.holders}_${scope}`);
|
|
254
|
+
const keys = sourceKeys(sources);
|
|
255
|
+
const rows = sourceRowsOf(scope, sources);
|
|
256
|
+
const collect = sources.map((source) => ` if tg_relid = ${regclassOf(source.table)} then
|
|
257
|
+
if tg_op in ('UPDATE', 'DELETE') then
|
|
258
|
+
v_ids := v_ids || ${source.holders.id(scope, "old")};
|
|
259
|
+
end if;
|
|
260
|
+
if tg_op in ('INSERT', 'UPDATE') then
|
|
261
|
+
v_ids := v_ids || ${source.holders.id(scope, "new")};
|
|
262
|
+
end if;
|
|
263
|
+
end if;`);
|
|
264
|
+
const checks = counted.map((rule) => {
|
|
265
|
+
const filters = ["h.live", `h.role = ${quoteLiteral(rule.role)}`];
|
|
266
|
+
const kind = roleKindSql(ctx, rule.role, "h.via");
|
|
267
|
+
if (kind !== void 0) filters.push(kind);
|
|
268
|
+
const lines = [` select count(distinct h.user_id) into v_count
|
|
269
|
+
from (
|
|
270
|
+
${rows}
|
|
271
|
+
) h
|
|
272
|
+
where ${filters.join("\n and ")};`];
|
|
273
|
+
if (rule.min > 0) lines.push(` if v_total > 0 and v_count < ${String(rule.min)} then
|
|
274
|
+
raise exception using
|
|
275
|
+
errcode = '23514',
|
|
276
|
+
message = ${quoteLiteral(`permdock: ${scope} `)} || v_id || ${quoteLiteral(` keeps at least ${String(rule.min)} ${rule.role}`)},
|
|
277
|
+
hint = 'last-holder';
|
|
278
|
+
end if;`);
|
|
279
|
+
if (rule.max !== void 0) lines.push(` if v_count > ${String(rule.max)} then
|
|
280
|
+
raise exception using
|
|
281
|
+
errcode = '23514',
|
|
282
|
+
message = ${quoteLiteral(`permdock: ${scope} `)} || v_id || ${quoteLiteral(` has at most ${String(rule.max)} ${rule.role}`)},
|
|
283
|
+
hint = 'max-holders';
|
|
284
|
+
end if;`);
|
|
285
|
+
return lines.join("\n");
|
|
286
|
+
});
|
|
287
|
+
const triggers = sourceTables(sources).map((table) => `drop trigger if exists ${objectName(OWNERSHIP.holders, scope)} on ${table};
|
|
288
|
+
create constraint trigger ${objectName(OWNERSHIP.holders, scope)}
|
|
289
|
+
after insert or update or delete on ${table}
|
|
290
|
+
deferrable initially deferred
|
|
291
|
+
for each row execute function ${fn}();`);
|
|
292
|
+
return `-- ${scope}: holder counts (min / max) over the membership sources, checked at commit
|
|
293
|
+
create or replace function ${fn}()
|
|
294
|
+
returns trigger
|
|
295
|
+
language plpgsql
|
|
296
|
+
security definer
|
|
297
|
+
set search_path = ''
|
|
298
|
+
as $$
|
|
299
|
+
declare
|
|
300
|
+
v_ids text[] := '{}';
|
|
301
|
+
v_id text;
|
|
302
|
+
${keys.declare}
|
|
303
|
+
v_total bigint;
|
|
304
|
+
v_count bigint;
|
|
305
|
+
begin
|
|
306
|
+
${collect.join("\n")}
|
|
307
|
+
foreach v_id in array v_ids loop
|
|
308
|
+
continue when v_id is null;
|
|
309
|
+
${keys.assign("v_id", " ")}
|
|
310
|
+
select count(*) into v_total
|
|
311
|
+
from (
|
|
312
|
+
${rows}
|
|
313
|
+
) h;
|
|
314
|
+
${checks.join("\n")}
|
|
315
|
+
end loop;
|
|
316
|
+
return null;
|
|
317
|
+
end;
|
|
318
|
+
$$;
|
|
319
|
+
${triggers.join("\n")}`;
|
|
320
|
+
}
|
|
321
|
+
function collectChangesSql(changed) {
|
|
322
|
+
return ` v_changes := v_changes || coalesce((select jsonb_agg(to_jsonb(c)) from (${changed}) c), '[]'::jsonb);`;
|
|
323
|
+
}
|
|
324
|
+
/** `transferOnly` over membership sources: each source table's statement triggers diff its transition tables, and the count after reads every source. */
|
|
325
|
+
function sourceTransferTriggerSql(ctx, scope, sources, roles) {
|
|
326
|
+
const fn = qualified(ctx, `${OWNERSHIP.transferOnly}_${scope}`);
|
|
327
|
+
const keys = sourceKeys(sources);
|
|
328
|
+
const list = `array[${roles.map(quoteLiteral).join(", ")}]::text[]`;
|
|
329
|
+
const kind = kindFilterSql(ctx, "h.role", "h.via");
|
|
330
|
+
const changes = (source, from, delta) => `select h.id, h.role, ${delta} as delta from (${source.holders.rows(scope, { from }).replaceAll("\n", " ")}) h where h.role = any(${list})`;
|
|
331
|
+
const branches = sources.map((source) => ` if tg_relid = ${regclassOf(source.table)} then
|
|
332
|
+
if tg_op in ('INSERT', 'UPDATE') then
|
|
333
|
+
${collectChangesSql(changes(source, "permdock_new", "1"))}
|
|
334
|
+
end if;
|
|
335
|
+
if tg_op in ('UPDATE', 'DELETE') then
|
|
336
|
+
${collectChangesSql(changes(source, "permdock_old", "-1"))}
|
|
337
|
+
end if;
|
|
338
|
+
end if;`);
|
|
339
|
+
const triggers = sourceTables(sources).flatMap((table) => [
|
|
340
|
+
"insert",
|
|
341
|
+
"update",
|
|
342
|
+
"delete"
|
|
343
|
+
].map((op) => {
|
|
344
|
+
const name = quoteIdent(`${OWNERSHIP.transferOnly}_${scope}_${op}`);
|
|
345
|
+
return `drop trigger if exists ${name} on ${table};
|
|
346
|
+
create trigger ${name}
|
|
347
|
+
after ${op} on ${table}
|
|
348
|
+
referencing ${op === "insert" ? "new table as permdock_new" : op === "delete" ? "old table as permdock_old" : "old table as permdock_old new table as permdock_new"}
|
|
349
|
+
for each statement execute function ${fn}();`;
|
|
350
|
+
}));
|
|
351
|
+
return `-- ${scope}: transfer-only roles (${roles.join(", ")}) keep their holder count per statement, over the membership sources
|
|
352
|
+
create or replace function ${fn}()
|
|
353
|
+
returns trigger
|
|
354
|
+
language plpgsql
|
|
355
|
+
security definer
|
|
356
|
+
set search_path = ''
|
|
357
|
+
as $$
|
|
358
|
+
declare
|
|
359
|
+
v_changes jsonb := '[]'::jsonb;
|
|
360
|
+
v_change record;
|
|
361
|
+
v_after bigint;
|
|
362
|
+
${keys.declare}
|
|
363
|
+
begin
|
|
364
|
+
${branches.join("\n")}
|
|
365
|
+
for v_change in
|
|
366
|
+
select x.id, x.role, sum(x.delta) as delta
|
|
367
|
+
from jsonb_to_recordset(v_changes) x(id text, role text, delta bigint)
|
|
368
|
+
group by x.id, x.role
|
|
369
|
+
loop
|
|
370
|
+
continue when v_change.delta = 0 or v_change.id is null;
|
|
371
|
+
${keys.assign("v_change.id", " ")}
|
|
372
|
+
select count(distinct h.user_id) into v_after
|
|
373
|
+
from (
|
|
374
|
+
${sourceRowsOf(scope, sources)}
|
|
375
|
+
) h
|
|
376
|
+
where h.live
|
|
377
|
+
and h.role = v_change.role${kind === void 0 ? "" : `\n and ${kind}`};
|
|
378
|
+
if v_after > 0 and v_after - v_change.delta > 0 then
|
|
379
|
+
raise exception using
|
|
380
|
+
errcode = '23514',
|
|
381
|
+
message = 'permdock: ' || v_change.role || ${quoteLiteral(` in ${scope} `)} || v_change.id || ' moves only by transfer',
|
|
382
|
+
hint = 'transfer-only';
|
|
383
|
+
end if;
|
|
384
|
+
end loop;
|
|
385
|
+
return null;
|
|
386
|
+
end;
|
|
387
|
+
$$;
|
|
388
|
+
${triggers.join("\n")}`;
|
|
389
|
+
}
|
|
390
|
+
function pairsSql(pairs) {
|
|
391
|
+
return `values ${pairs.map((pair) => `(${quoteLiteral(pair.assigner)}, ${quoteLiteral(pair.role)})`).join(", ")}`;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Membership rows of the membership sources for scope `name`, each source's
|
|
395
|
+
* `select` comparing its user column with a variable `user` names.
|
|
396
|
+
*/
|
|
397
|
+
function sourceRowsSql(sources, user) {
|
|
398
|
+
const keep = sources.some((source) => source.sql.keeps);
|
|
399
|
+
return sources.map((source) => source.sql.select(user(source), keep).replaceAll(/^/gmu, " ")).join("\n union all\n");
|
|
400
|
+
}
|
|
401
|
+
/** Whether the signed-in user (or `p_user` in the `_for` form) holds, in instance `p_scope_id`, a role whose `assigns` lists `p_role`. */
|
|
402
|
+
function canAssignSql(base, own, forUser = false) {
|
|
403
|
+
const ctx = forUser ? {
|
|
404
|
+
...base,
|
|
405
|
+
subjectId: "p_user",
|
|
406
|
+
tenants: "all"
|
|
407
|
+
} : base;
|
|
408
|
+
const fn = qualified(ctx, forUser ? OWNERSHIP.canAssignFor : OWNERSHIP.canAssign);
|
|
409
|
+
const parts = [];
|
|
410
|
+
const used = [];
|
|
411
|
+
const userOf = (source) => {
|
|
412
|
+
const index = used.includes(source) ? used.indexOf(source) : used.push(source) - 1;
|
|
413
|
+
return `v_user_${String(index)}`;
|
|
414
|
+
};
|
|
415
|
+
const global = own.assigns.filter((pair) => pair.scope === "global");
|
|
416
|
+
for (const [instance, pairs] of [["p_scope_id is not null", global.filter((pair) => pair.at !== "global")], ["p_scope_id is null", global.filter((pair) => pair.at === "global")]]) {
|
|
417
|
+
if (pairs.length === 0) continue;
|
|
418
|
+
if (ctx.authorize === "database") {
|
|
419
|
+
const ur = globalRoleRows(ctx);
|
|
420
|
+
const kind = globalKindFilterSql(ctx, ur.roleSql);
|
|
421
|
+
parts.push(`${instance} and exists (
|
|
422
|
+
select 1 from ${ur.from}
|
|
423
|
+
where ${ur.userSql} = ${subjectIdSql(ctx)}
|
|
424
|
+
and (${ur.roleSql}, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}
|
|
425
|
+
)`);
|
|
426
|
+
} else {
|
|
427
|
+
const kind = globalKindFilterSql(ctx, "r.role");
|
|
428
|
+
parts.push(`${instance} and exists (
|
|
429
|
+
select 1 from ${roleRows(ctx)}
|
|
430
|
+
where (r.role, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}
|
|
431
|
+
)`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
for (const { name } of ctx.scopes) {
|
|
435
|
+
const pairs = own.assigns.filter((pair) => pair.scope === name);
|
|
436
|
+
if (pairs.length === 0) continue;
|
|
437
|
+
const sources = scopeSources(ctx, name);
|
|
438
|
+
if (sources.length > 0) {
|
|
439
|
+
const kind = kindFilterSql(ctx, "r.role", "ms.via");
|
|
440
|
+
parts.push(`exists (
|
|
441
|
+
select 1
|
|
442
|
+
from (
|
|
443
|
+
${sourceRowsSql(sources, userOf)}
|
|
444
|
+
) ms
|
|
445
|
+
cross join lateral jsonb_array_elements_text(
|
|
446
|
+
case jsonb_typeof(ms.roles) when 'array' then ms.roles else '[]'::jsonb end
|
|
447
|
+
) r(role)
|
|
448
|
+
where ms.scope = ${quoteLiteral(name)}
|
|
449
|
+
and ms.id = p_scope_id
|
|
450
|
+
and (r.role, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}${sourceFilters(ctx, name, void 0, void 0, sourcesKeep(sources)).replaceAll("\n and ", "\n and ")}
|
|
451
|
+
)`);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (ctx.authorize === "database") {
|
|
455
|
+
const mapped = scopeTable(ctx, name);
|
|
456
|
+
if (mapped === void 0) continue;
|
|
457
|
+
const { table, column } = mapped;
|
|
458
|
+
const role = memberRoleOf(table, "m", " ");
|
|
459
|
+
const filters = [
|
|
460
|
+
`${memberColumn(table.user)} = ${subjectIdSql(ctx)}`,
|
|
461
|
+
`${memberColumn(column)}::text = p_scope_id`,
|
|
462
|
+
`(${role.sql}, p_role) in (${pairsSql(pairs)})`
|
|
463
|
+
];
|
|
464
|
+
if (table.expiresAt !== void 0) {
|
|
465
|
+
const expires = memberColumn(table.expiresAt);
|
|
466
|
+
filters.push(`(${expires} is null or ${expires} > now())`);
|
|
467
|
+
}
|
|
468
|
+
const disabled = disabledColumn(table);
|
|
469
|
+
if (disabled !== void 0) filters.push(`${disabled} is null`);
|
|
470
|
+
const kind = kindFilterSql(ctx, role.sql, memberVia(table));
|
|
471
|
+
if (kind !== void 0) filters.push(kind);
|
|
472
|
+
parts.push(`exists (
|
|
473
|
+
select 1 from ${membershipTable(table.table)} m${role.join}
|
|
474
|
+
where ${filters.join("\n and ")}
|
|
475
|
+
)`);
|
|
476
|
+
} else {
|
|
477
|
+
const kind = kindFilterSql(ctx, "r.role", "m ->> 'via'");
|
|
478
|
+
parts.push(`exists (
|
|
479
|
+
select 1 from ${membershipRows(ctx)}
|
|
480
|
+
where m ->> 'scope' = ${quoteLiteral(name)}
|
|
481
|
+
and m ->> 'id' = p_scope_id
|
|
482
|
+
and (r.role, p_role) in (${pairsSql(pairs)})
|
|
483
|
+
and case jsonb_typeof(m -> 'expiresAt')
|
|
484
|
+
when 'number' then (m ->> 'expiresAt')::numeric > extract(epoch from now())
|
|
485
|
+
else true
|
|
486
|
+
end${kind === void 0 ? "" : `\n and ${kind}`}${claimKeptLines(ctx, " ").map((line) => `\n${line}`).join("")}
|
|
487
|
+
)`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const body = parts.length === 0 ? "false" : parts.join("\n or ");
|
|
491
|
+
const language = used.length === 0 ? `language sql
|
|
492
|
+
stable
|
|
493
|
+
security definer
|
|
494
|
+
set search_path = ''
|
|
495
|
+
as $$
|
|
496
|
+
select ${signedIn(ctx)} and (
|
|
497
|
+
${body}
|
|
498
|
+
)
|
|
499
|
+
$$;` : `language plpgsql
|
|
500
|
+
stable
|
|
501
|
+
security definer
|
|
502
|
+
set search_path = ''
|
|
503
|
+
as $$
|
|
504
|
+
declare
|
|
505
|
+
${used.map((source, index) => ` v_user_${String(index)} ${source.sql.userType} := ${subjectIdSql(ctx)};`).join("\n")}
|
|
506
|
+
begin
|
|
507
|
+
return ${signedIn(ctx)} and (
|
|
508
|
+
${body}
|
|
509
|
+
);
|
|
510
|
+
end;
|
|
511
|
+
$$;`;
|
|
512
|
+
if (forUser) {
|
|
513
|
+
const user = ctx.dialect === "supabase" ? "uuid" : "text";
|
|
514
|
+
return `-- who may assign, for a user the caller names: trusted SQL acting later for a stored user; no client role may execute it
|
|
515
|
+
create or replace function ${fn}(p_user ${user}, p_role text, p_scope_id text)
|
|
516
|
+
returns boolean
|
|
517
|
+
${language}
|
|
518
|
+
revoke execute on function ${fn}(${user}, text, text) from public, anon, authenticated;`;
|
|
519
|
+
}
|
|
520
|
+
return `-- who may assign: a held role whose assigns lists p_role, in instance p_scope_id (its scope or an ancestor's); a null p_scope_id assigns a global role, which only global assigners may
|
|
521
|
+
create or replace function ${fn}(p_role text, p_scope_id text)
|
|
522
|
+
returns boolean
|
|
523
|
+
${language}
|
|
524
|
+
revoke execute on function ${fn}(text, text) from public, anon;
|
|
525
|
+
grant execute on function ${fn}(text, text) to authenticated;`;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* The ownership objects: holder-count and transfer-only triggers on each
|
|
529
|
+
* scope's membership table, and `permdock_can_assign` for the application's
|
|
530
|
+
* own policies on those tables. Empty when no role declares a rule.
|
|
531
|
+
*/
|
|
532
|
+
function ownershipSql(ctx) {
|
|
533
|
+
const own = ctx.ownership;
|
|
534
|
+
if (own === void 0) return "";
|
|
535
|
+
const chunks = [];
|
|
536
|
+
const skipped = ctx.skipOwnershipTriggers;
|
|
537
|
+
for (const { name } of ctx.scopes) {
|
|
538
|
+
const counted = own.counted.filter((rule) => rule.scope === name);
|
|
539
|
+
if (counted.length === 0) continue;
|
|
540
|
+
if (skipped === "all" || skipped?.includes(name) === true) {
|
|
541
|
+
chunks.push(`-- ${name}: rls.ownershipTriggers leaves out the triggers, so min, max and transferOnly are checked only by decideRoleChange`);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const transfer = counted.filter((rule) => rule.transferOnly).map((rule) => rule.role);
|
|
545
|
+
const mapped = scopeTable(ctx, name);
|
|
546
|
+
if (mapped === void 0) {
|
|
547
|
+
const sources = holderSources(ctx, name);
|
|
548
|
+
if (sources === void 0) chunks.push(`-- ${name}: no memberships table configured, so min, max and transferOnly are checked only by decideRoleChange`);
|
|
549
|
+
else if (sources === "not-tables") chunks.push(`-- ${name}: a membership source is not a table, so min, max and transferOnly are checked only by decideRoleChange`);
|
|
550
|
+
else {
|
|
551
|
+
chunks.push(sourceHoldersTriggerSql(ctx, name, sources, counted));
|
|
552
|
+
if (transfer.length > 0) chunks.push(sourceTransferTriggerSql(ctx, name, sources, transfer));
|
|
553
|
+
}
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
chunks.push(holdersTriggerSql(ctx, name, mapped.table, mapped.column, counted));
|
|
557
|
+
if (transfer.length > 0) chunks.push(transferTriggerSql(ctx, name, mapped.table, mapped.column, transfer));
|
|
558
|
+
}
|
|
559
|
+
if (own.assigns.length > 0) {
|
|
560
|
+
chunks.push(canAssignSql(ctx, own));
|
|
561
|
+
if (ctx.authorize === "database") chunks.push(canAssignSql(ctx, own, true));
|
|
562
|
+
const custom = assignsCustomRoles(ctx);
|
|
563
|
+
if (custom) {
|
|
564
|
+
chunks.push(canAssignCustomSql(ctx));
|
|
565
|
+
if (hasCustomRoleChecksFor(ctx)) chunks.push(canAssignCustomSql(ctx, true));
|
|
566
|
+
}
|
|
567
|
+
chunks.push(canAssignAnySql(ctx, custom));
|
|
568
|
+
if (hasCanAssignAnyFor(ctx)) chunks.push(canAssignAnySql(ctx, custom, true));
|
|
569
|
+
}
|
|
570
|
+
return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
|
|
571
|
+
}
|
|
572
|
+
/** Whether the helpers keep custom roles in tables, so assigning one is checked against its stored definition. */
|
|
573
|
+
function assignsCustomRoles(ctx) {
|
|
574
|
+
return ctx.customRoles !== void 0 && ctx.authorize === "database";
|
|
575
|
+
}
|
|
576
|
+
/** Whether `permdock_can_assign_any_for` is written: every function it calls has a `_for` form. */
|
|
577
|
+
function hasCanAssignAnyFor(ctx) {
|
|
578
|
+
return ctx.authorize === "database" && (!assignsCustomRoles(ctx) || hasCustomRoleChecksFor(ctx));
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* `permdock_can_assign_any`: one check for a declared or a custom role, so
|
|
582
|
+
* SQL that assigns roles calls one function: `permdock_can_assign` for a
|
|
583
|
+
* declared role, `permdock_can_assign_custom_role` for a custom one. `p_scope`
|
|
584
|
+
* `'global'` (no tenant, no instance) assigns a platform role.
|
|
585
|
+
*/
|
|
586
|
+
function canAssignAnySql(ctx, custom, forUser = false) {
|
|
587
|
+
const tenantType = tenantTypeOf(ctx);
|
|
588
|
+
const fn = qualified(ctx, forUser ? OWNERSHIP.canAssignAnyFor : OWNERSHIP.canAssignAny);
|
|
589
|
+
const declaredFn = qualified(ctx, forUser ? OWNERSHIP.canAssignFor : OWNERSHIP.canAssign);
|
|
590
|
+
const customFn = qualified(ctx, forUser ? ASSIGNMENTS.customFor : ASSIGNMENTS.custom);
|
|
591
|
+
const userType = ctx.dialect === "supabase" ? "uuid" : "text";
|
|
592
|
+
const userParam = forUser ? `p_user ${userType}, ` : "";
|
|
593
|
+
const userArg = forUser ? `${userType}, ` : "";
|
|
594
|
+
const user = forUser ? "p_user, " : "";
|
|
595
|
+
const declared = `${declaredFn}(${user}p_role, case when p_scope = 'global' then null else p_scope_id end)`;
|
|
596
|
+
const body = custom && ctx.customRoles !== void 0 ? `case
|
|
597
|
+
when p_role = any(${declaredArray(ctx.customRoles.declared)}) then ${declared}
|
|
598
|
+
when p_scope = 'global' then ${customFn}(${user}null, 'global', null, p_role)
|
|
599
|
+
else ${customFn}(${user}p_tenant, p_scope, p_scope_id, p_role)
|
|
600
|
+
end` : declared;
|
|
601
|
+
const grants = forUser ? `revoke execute on function ${fn}(${userArg}text, ${tenantType}, text, text) from public, anon, authenticated;` : `revoke execute on function ${fn}(text, ${tenantType}, text, text) from public, anon;
|
|
602
|
+
grant execute on function ${fn}(text, ${tenantType}, text, text) to authenticated;`;
|
|
603
|
+
return `-- whether ${forUser ? "user p_user" : "the caller"} may assign p_role at instance p_scope_id of p_scope in tenant p_tenant, declared or custom (p_scope 'global' for a platform role)${forUser ? "; no client role may execute it" : ""}
|
|
604
|
+
create or replace function ${fn}(${userParam}p_role text, p_tenant ${tenantType}, p_scope text, p_scope_id text)
|
|
605
|
+
returns boolean
|
|
606
|
+
language sql
|
|
607
|
+
stable
|
|
608
|
+
security definer
|
|
609
|
+
set search_path = ''
|
|
610
|
+
as $$
|
|
611
|
+
select coalesce(${body}, false)
|
|
612
|
+
$$;
|
|
613
|
+
${grants}`;
|
|
614
|
+
}
|
|
615
|
+
/** Objects `rls.assignments` adds. Names are part of the SQL contract. */
|
|
616
|
+
const ASSIGNMENTS = {
|
|
617
|
+
custom: "permdock_can_assign_custom_role",
|
|
618
|
+
customFor: "permdock_can_assign_custom_role_for",
|
|
619
|
+
trigger: "permdock_assignment"
|
|
620
|
+
};
|
|
621
|
+
/** The client roles whose writes the assignment triggers check; any other role is a trusted path. */
|
|
622
|
+
const CLIENT_ROLES = [
|
|
623
|
+
"anon",
|
|
624
|
+
"anonymous",
|
|
625
|
+
"authenticated"
|
|
626
|
+
];
|
|
627
|
+
/** The mapped membership tables, `rls.assignments.tables` and the global-roles table `rls.roles`. */
|
|
628
|
+
function assignedTables(ctx) {
|
|
629
|
+
const out = [];
|
|
630
|
+
for (const { name } of ctx.scopes) {
|
|
631
|
+
const mapped = scopeTable(ctx, name);
|
|
632
|
+
if (mapped === void 0) continue;
|
|
633
|
+
out.push({
|
|
634
|
+
table: mapped.table.table,
|
|
635
|
+
scope: name,
|
|
636
|
+
id: mapped.column,
|
|
637
|
+
tenant: mapped.tenantColumn ?? mapped.column,
|
|
638
|
+
role: mapped.table.role,
|
|
639
|
+
user: mapped.table.user
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
if (ctx.roles !== void 0) out.push({
|
|
643
|
+
table: ctx.roles.table,
|
|
644
|
+
scope: "global",
|
|
645
|
+
role: ctx.roles.role ?? "role",
|
|
646
|
+
user: ctx.roles.user ?? "user_id"
|
|
647
|
+
});
|
|
648
|
+
for (const extra of ctx.assignments?.tables ?? []) {
|
|
649
|
+
const scope = resolveScope(ctx.scopes, extra.scope);
|
|
650
|
+
if (scope === void 0) throw new Error(`PermDock CLI: rls.assignments.tables names scope '${extra.scope}', which the policy does not declare`);
|
|
651
|
+
if (scope !== ctx.scopes[0]?.name && extra.tenant === void 0) throw new Error(`PermDock CLI: rls.assignments.tables entry ${extra.table} assigns at ${scope}, below the first scope: name its tenant column`);
|
|
652
|
+
out.push({
|
|
653
|
+
table: extra.table,
|
|
654
|
+
scope,
|
|
655
|
+
id: extra.id,
|
|
656
|
+
tenant: extra.tenant ?? extra.id,
|
|
657
|
+
role: extra.role,
|
|
658
|
+
...extra.user === void 0 ? {} : { user: extra.user }
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
return out;
|
|
662
|
+
}
|
|
663
|
+
/** The tables `rls.assignments.ownRole` refuses a caller's own rows on, by table key. */
|
|
664
|
+
function ownRoleTables(ctx, tables) {
|
|
665
|
+
const own = ctx.assignments?.ownRole;
|
|
666
|
+
if (own === void 0) return /* @__PURE__ */ new Set();
|
|
667
|
+
if (own === "refuse") return new Set(tables.filter((entry) => entry.user !== void 0).map((entry) => qualifiedTable(entry.table)));
|
|
668
|
+
const out = /* @__PURE__ */ new Set();
|
|
669
|
+
for (const [name, value] of Object.entries(own)) {
|
|
670
|
+
const entry = tables.find((table) => qualifiedTable(table.table) === qualifiedTable(name));
|
|
671
|
+
if (entry === void 0) throw new Error(`PermDock CLI: rls.assignments.ownRole names ${name}, which no assignment trigger guards`);
|
|
672
|
+
if (value !== "refuse") throw new Error(`PermDock CLI: rls.assignments.ownRole.${name} must be 'refuse'`);
|
|
673
|
+
if (entry.user === void 0) throw new Error(`PermDock CLI: rls.assignments.ownRole names ${name}, which has no user column: set its user in rls.assignments.tables`);
|
|
674
|
+
out.add(qualifiedTable(entry.table));
|
|
675
|
+
}
|
|
676
|
+
return out;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* The tables `rls.assignments` guards, schema-qualified: each scope's mapped
|
|
680
|
+
* membership table, the global-roles table (`rls.roles`, else
|
|
681
|
+
* `supabase.hook.roles`) and `rls.assignments.tables`. Empty without
|
|
682
|
+
* `rls.assignments`.
|
|
683
|
+
*/
|
|
684
|
+
function guardedTables(config, scopes) {
|
|
685
|
+
const rls = config.rls;
|
|
686
|
+
if (rls?.assignments === void 0) return [];
|
|
687
|
+
const hook = config.supabase?.hook?.roles;
|
|
688
|
+
const roles = rls.roles ?? (hook === false ? void 0 : hook);
|
|
689
|
+
const tables = [
|
|
690
|
+
...scopes.flatMap((scope) => {
|
|
691
|
+
const mapped = memberForTable({
|
|
692
|
+
scopes,
|
|
693
|
+
...rls.memberships === void 0 ? {} : { memberships: rls.memberships }
|
|
694
|
+
}, scope.name);
|
|
695
|
+
return mapped === void 0 ? [] : [mapped.table];
|
|
696
|
+
}),
|
|
697
|
+
...roles === void 0 ? [] : [roles.table],
|
|
698
|
+
...(rls.assignments === true ? [] : rls.assignments.tables ?? []).map((entry) => entry.table)
|
|
699
|
+
];
|
|
700
|
+
return [...new Set(tables.map(qualifiedTable))];
|
|
701
|
+
}
|
|
702
|
+
function isRoleList(role) {
|
|
703
|
+
return Array.isArray(role);
|
|
704
|
+
}
|
|
705
|
+
/** The role keys a row variable holds, one scalar per role source. */
|
|
706
|
+
function rowRoles(entry, row) {
|
|
707
|
+
return (isRoleList(entry.role) ? entry.role : [entry.role]).map((part) => roleColumn(part, qualifiedTable(entry.table), row, {
|
|
708
|
+
label: `rls.assignments ${entry.table} role`,
|
|
709
|
+
prefix: "PermDock CLI"
|
|
710
|
+
}).lookup);
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* `permdock_can_assign_custom_role`: whether the signed-in caller may assign a
|
|
714
|
+
* custom role of the tenant at the instance, by the checks the custom-role
|
|
715
|
+
* write functions run on its stored definition.
|
|
716
|
+
*/
|
|
717
|
+
function canAssignCustomSql(ctx, forUser = false) {
|
|
718
|
+
const tenantType = tenantTypeOf(ctx);
|
|
719
|
+
const fn = qualified(ctx, forUser ? ASSIGNMENTS.customFor : ASSIGNMENTS.custom);
|
|
720
|
+
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
721
|
+
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
722
|
+
const guard = qualified(ctx, forUser ? CUSTOM_ROLES.guardFor : CUSTOM_ROLES.guard);
|
|
723
|
+
const userParam = forUser ? "p_user uuid, " : "";
|
|
724
|
+
const userArg = forUser ? "uuid, " : "";
|
|
725
|
+
const stored = `c.tenant_id is not distinct from p_tenant and c.scope = p_scope and c.role = p_role and (c.scope_id is null or c.scope_id = p_scope_id)`;
|
|
726
|
+
const grants = forUser ? `revoke execute on function ${fn}(${userArg}${tenantType}, text, text, text) from public, anon, authenticated;` : `revoke execute on function ${fn}(${tenantType}, text, text, text) from public, anon;
|
|
727
|
+
grant execute on function ${fn}(${tenantType}, text, text, text) to authenticated;`;
|
|
728
|
+
return `-- whether ${forUser ? "user p_user" : "the caller"} may assign custom role p_role at instance p_scope_id (a platform role: null tenant, scope global): the custom-role write checks on its stored definition${forUser ? "; no client role may execute it" : ""}
|
|
729
|
+
create or replace function ${fn}(${userParam}p_tenant ${tenantType}, p_scope text, p_scope_id text, p_role text)
|
|
730
|
+
returns boolean
|
|
731
|
+
language plpgsql
|
|
732
|
+
stable
|
|
733
|
+
security definer
|
|
734
|
+
set search_path = ''
|
|
735
|
+
as $$
|
|
736
|
+
declare
|
|
737
|
+
v_scope_id text;
|
|
738
|
+
v_found boolean := false;
|
|
739
|
+
begin
|
|
740
|
+
select true, c.scope_id into v_found, v_scope_id
|
|
741
|
+
from (
|
|
742
|
+
select c.scope_id from ${perms} c where ${stored}
|
|
743
|
+
union all
|
|
744
|
+
select c.scope_id from ${includes} c where ${stored}
|
|
745
|
+
) c
|
|
746
|
+
order by c.scope_id nulls first
|
|
747
|
+
limit 1;
|
|
748
|
+
if not coalesce(v_found, false) then
|
|
749
|
+
return false;
|
|
750
|
+
end if;
|
|
751
|
+
perform ${guard}(${forUser ? "p_user, " : ""}p_tenant, p_scope, v_scope_id, p_role);
|
|
752
|
+
return true;
|
|
753
|
+
exception
|
|
754
|
+
when insufficient_privilege or invalid_parameter_value then
|
|
755
|
+
return false;
|
|
756
|
+
end;
|
|
757
|
+
$$;
|
|
758
|
+
${grants}`;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* The assignment triggers (\`rls.assignments\`): before a client role inserts,
|
|
762
|
+
* updates or deletes a row that assigns a role, every role the old and new
|
|
763
|
+
* row hold must be one the caller may assign at that instance, by
|
|
764
|
+
* \`permdock_can_assign\` for a declared role and
|
|
765
|
+
* \`permdock_can_assign_custom_role\` for a custom one. The functions run as
|
|
766
|
+
* the invoker, so \`current_user\` tells a client write from a trusted one.
|
|
767
|
+
*/
|
|
768
|
+
function assignmentSql(ctx) {
|
|
769
|
+
const own = ctx.ownership;
|
|
770
|
+
if (ctx.assignments === void 0 || own === void 0 || own.assigns.length === 0) return "";
|
|
771
|
+
const canAssign = qualified(ctx, OWNERSHIP.canAssign);
|
|
772
|
+
const custom = ctx.customRoles !== void 0 && ctx.authorize === "database";
|
|
773
|
+
const declared = ctx.customRoles?.declared;
|
|
774
|
+
const tenantType = tenantTypeOf(ctx);
|
|
775
|
+
const chunks = [];
|
|
776
|
+
const clients = `current_user::text = any(array[${CLIENT_ROLES.map(quoteLiteral).join(", ")}])`;
|
|
777
|
+
const tables = assignedTables(ctx);
|
|
778
|
+
const ownRole = ownRoleTables(ctx, tables);
|
|
779
|
+
for (const entry of tables) {
|
|
780
|
+
const table = quoteTable(qualifiedTable(entry.table));
|
|
781
|
+
const suffix = entry.table.replaceAll(/[^a-z0-9_]/gu, "_");
|
|
782
|
+
const fn = qualified(ctx, quoteIdent(`${ASSIGNMENTS.trigger}_${suffix}`));
|
|
783
|
+
const ownUser = entry.user !== void 0 && ownRole.has(qualifiedTable(entry.table)) ? entry.user : void 0;
|
|
784
|
+
const ownCheck = (row, reason) => ownUser === void 0 ? "" : ` if ${row}.${quoteIdent(ownUser)}::text = (${subjectIdSql(ctx)})::text then
|
|
785
|
+
raise exception using
|
|
786
|
+
errcode = '42501',
|
|
787
|
+
message = 'permdock: the caller may not change their own roles',
|
|
788
|
+
hint = '${reason}';
|
|
789
|
+
end if;
|
|
790
|
+
`;
|
|
791
|
+
const check = (row) => {
|
|
792
|
+
const id = entry.id === void 0 ? "null::text" : `${row}.${quoteIdent(entry.id)}::text`;
|
|
793
|
+
const customFn = qualified(ctx, ASSIGNMENTS.custom);
|
|
794
|
+
let allowed = `${canAssign}(v_role, ${id})`;
|
|
795
|
+
if (custom && declared !== void 0) {
|
|
796
|
+
const platform = `${customFn}(null, 'global', null, v_role)`;
|
|
797
|
+
allowed = entry.id === void 0 || entry.tenant === void 0 ? `case when v_role = any(${declaredArray(declared)}) then ${canAssign}(v_role, ${id}) else ${platform} end` : `case when v_role = any(${declaredArray(declared)}) then ${canAssign}(v_role, ${id}) when ${id} is null then ${platform} else ${customFn}(${row}.${quoteIdent(entry.tenant)}::${tenantType}, ${quoteLiteral(entry.scope)}, ${id}, v_role) end`;
|
|
798
|
+
}
|
|
799
|
+
return ` foreach v_role in array array[${rowRoles(entry, row).join(", ")}]::text[] loop
|
|
800
|
+
if v_role is not null and not coalesce(${allowed}, false) then
|
|
801
|
+
raise exception using
|
|
802
|
+
errcode = '42501',
|
|
803
|
+
message = 'permdock: the caller may not assign ' || v_role || ' in ' || coalesce(${id}, 'null'),
|
|
804
|
+
hint = 'not-assignable-by';
|
|
805
|
+
end if;
|
|
806
|
+
end loop;`;
|
|
807
|
+
};
|
|
808
|
+
const what = entry.id === void 0 ? "global roles" : `${entry.scope} roles`;
|
|
809
|
+
chunks.push(`-- ${entry.table}: a client role may write only the ${what} it may assign${ownUser === void 0 ? "" : ", and none on its own rows"}
|
|
810
|
+
create or replace function ${fn}()
|
|
811
|
+
returns trigger
|
|
812
|
+
language plpgsql
|
|
813
|
+
volatile
|
|
814
|
+
set search_path = ''
|
|
815
|
+
as $$
|
|
816
|
+
declare
|
|
817
|
+
v_role text;
|
|
818
|
+
begin
|
|
819
|
+
if not (${clients}) then
|
|
820
|
+
return coalesce(new, old);
|
|
821
|
+
end if;
|
|
822
|
+
if tg_op in ('UPDATE', 'DELETE') then
|
|
823
|
+
${ownCheck("old", "self-demotion")}${check("old")}
|
|
824
|
+
end if;
|
|
825
|
+
if tg_op in ('INSERT', 'UPDATE') then
|
|
826
|
+
${ownCheck("new", "not-assignable-by")}${check("new")}
|
|
827
|
+
end if;
|
|
828
|
+
return coalesce(new, old);
|
|
829
|
+
end;
|
|
830
|
+
$$;
|
|
831
|
+
revoke execute on function ${fn}() from public, anon, authenticated;
|
|
832
|
+
drop trigger if exists "permdock_assignment" on ${table};
|
|
833
|
+
create trigger "permdock_assignment"
|
|
834
|
+
before insert or update or delete on ${table}
|
|
835
|
+
for each row execute function ${fn}();`);
|
|
836
|
+
}
|
|
837
|
+
return `${chunks.join("\n\n")}\n`;
|
|
838
|
+
}
|
|
839
|
+
function declaredArray(values) {
|
|
840
|
+
return values.length === 0 ? `'{}'::text[]` : `array[${values.map(quoteLiteral).join(", ")}]::text[]`;
|
|
841
|
+
}
|
|
842
|
+
//#endregion
|
|
843
|
+
export { ownershipRules as a, guardedTables as i, OWNERSHIP as n, ownershipSql as o, assignmentSql as r, ASSIGNMENTS as t };
|