permdock 0.1.0 → 0.3.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 +106 -47
- package/dist/a2a/index.d.ts +7 -16
- package/dist/a2a/index.js +59 -98
- package/dist/adapter-context-mlWsVGGg.js +23 -0
- package/dist/ai-sdk/index.d.ts +37 -16
- package/dist/ai-sdk/index.js +30 -13
- package/dist/approvals/index.d.ts +3 -3
- package/dist/approvals/index.js +5 -4
- package/dist/{approvals-DaxY-Bdg.js → approvals-DbYAuNkG.js} +17 -11
- package/dist/approvers-DT4AVyIg.js +52 -0
- package/dist/{arazzo-CTeVuXr5.js → arazzo-BAgw9ap6.js} +2 -2
- package/dist/{arazzo-aoTkqYsJ.js → arazzo-CKwwqFWz.js} +2 -2
- package/dist/authzen/index.d.ts +4 -13
- package/dist/authzen/index.js +16 -50
- package/dist/better-auth/index.d.ts +20 -3
- package/dist/better-auth/index.js +27 -7
- package/dist/better-supabase/index.d.ts +223 -0
- package/dist/better-supabase/index.js +648 -0
- package/dist/bind-C_ob3LKa.d.ts +25 -0
- package/dist/bind-CuzGytRf.js +97 -0
- package/dist/boundary-BM8a_P6E.js +13 -0
- package/dist/boundary-BmHxVH1J.d.ts +15 -0
- package/dist/canonical-json-DemiK4Vp.js +26 -0
- package/dist/{capability-BDG_nkxt.d.ts → capability-2kYTMWDv.d.ts} +2 -2
- package/dist/{capability-B68OFnZx.js → capability-4qPdO9gk.js} +7 -5
- package/dist/catalog/index.d.ts +1 -1
- package/dist/catalog/index.js +1 -1
- package/dist/catalog-CRW5Mx_B.js +2 -0
- package/dist/{catalog-H-vFGwWK.js → catalog-DWF5cYqB.js} +4 -4
- package/dist/{catalog-B0E6bggc.js → catalog-dFZdWCGN.js} +2 -2
- package/dist/{catalog-doc-BSo_Q_-6.js → catalog-doc-DdGm82XT.js} +20 -32
- package/dist/claude-agent/index.d.ts +5 -14
- package/dist/claude-agent/index.js +4 -10
- package/dist/clerk/index.d.ts +12 -3
- package/dist/clerk/index.js +26 -5
- package/dist/cli/bin.js +1 -1
- package/dist/cli/index.d.ts +3 -3
- package/dist/cli/index.js +2 -2
- package/dist/clients-C7zLFuWT.d.ts +12 -0
- package/dist/clients-DjU5ABMy.js +23 -0
- package/dist/cloud/index.d.ts +4 -5
- package/dist/cloud/index.js +20 -6
- package/dist/{cloud-BNQTrmrr.js → cloud-CB_ELYuX.js} +2 -2
- package/dist/{cloud-Cuxr6Czw.js → cloud-DWLXO2af.js} +7 -6
- package/dist/collect-DClJsheJ.js +83 -0
- package/dist/{collect-O-tOtbnk.js → collect-DSTE3ml9.js} +194 -42
- package/dist/collect-DSWOk19R.js +2 -0
- package/dist/{commands-BMh_Etm2.js → commands-DSxCqG9A.js} +16 -12
- package/dist/compile/index.d.ts +3 -0
- package/dist/compile/index.js +2 -0
- package/dist/{compile-D22iwvuV.js → compile-Ba4blROR.js} +85 -30
- package/dist/compile-BkRyBDBw.d.ts +94 -0
- package/dist/config-BJiM-xAm.js +157 -0
- package/dist/config-Dnszbv-6.js +36 -0
- package/dist/config-report-DMvWKUxL.js +44 -0
- package/dist/{connection-JvqwHODC.js → connection-CU8MQxRi.js} +1 -1
- package/dist/{context-CKEAabd7.js → context-Bcy85rGi.js} +36 -27
- package/dist/convex/index.d.ts +9 -4
- package/dist/convex/index.js +14 -4
- package/dist/create-B1d519XK.d.ts +245 -0
- package/dist/create-CMwIl-w2.js +253 -0
- package/dist/{credential-B_rkUNlY.js → credential-8P81ff4u.js} +26 -46
- package/dist/delegation-D8pUEuce.js +103 -0
- package/dist/{describe-Bt1YD5uS.js → describe-B_gVZEVi.js} +10 -15
- package/dist/{describe-DernyItf.d.ts → describe-DMYiCjJV.d.ts} +9 -10
- package/dist/{diff-x5Il8LVa.js → diff-2VaeChzX.js} +44 -11
- package/dist/{diff-BUzf5Poi.js → diff-DBIhz_f2.js} +2 -2
- package/dist/{doctor-Bqo6FwUa.js → doctor-BwCAMLx_.js} +2 -2
- package/dist/doctor-collect-CHk6XWOA.js +665 -0
- package/dist/doctor-igIiM_Hl.js +1194 -0
- package/dist/drizzle/index.d.ts +4 -3
- package/dist/drizzle/index.js +3 -3
- package/dist/elysia/index.d.ts +6 -28
- package/dist/elysia/index.js +16 -60
- package/dist/{errors-DSMnAVNa.js → errors-CWfoNg9N.js} +8 -4
- package/dist/{errors-Iy71s7Oj.js → errors-Cz5zn0mW.js} +17 -71
- package/dist/{errors-DbZ1YOnb.d.ts → errors-DPdqI50v.d.ts} +8 -21
- package/dist/{errors-m3u9TmDu.js → errors-DRnGZHzF.js} +4 -1
- package/dist/evaluation-items-CfCCejQd.js +40 -0
- package/dist/{evaluations-DsAU9ppl.js → evaluations-tz6D9A2G.js} +28 -46
- package/dist/eve/index.d.ts +6 -15
- package/dist/eve/index.js +7 -12
- package/dist/expiry-BoBJ8Tvc.js +7 -0
- package/dist/express/index.d.ts +6 -29
- package/dist/express/index.js +28 -58
- package/dist/fastify/index.d.ts +6 -29
- package/dist/fastify/index.js +16 -65
- package/dist/fields-BMl1RxoX.js +26 -0
- package/dist/{files-B_wAGYmt.js → files-DWUN9PzI.js} +5 -1
- package/dist/{fixtures-Cvaw0gDG.js → fixtures-ht28Cplr.js} +5 -2
- package/dist/{freeze-y1_jQL4v.js → freeze-CrU8nQeX.js} +31 -2
- package/dist/{from-snapshot-BMGnhmgv.js → from-snapshot-CTLijvdm.js} +66 -48
- package/dist/{grantee-Bv04_Vzf.js → grantee-Cv5Sogfv.js} +88 -7
- package/dist/graph-sql-B-dsAT6j.d.ts +29 -0
- package/dist/{headers-B5RRv3Xm.js → headers-BZSDieNT.js} +1 -1
- package/dist/{headers-C8SHQccD.d.ts → headers-Cf-zWvsO.d.ts} +1 -1
- package/dist/helpers-H_KARDVC.js +421 -0
- package/dist/hono/index.d.ts +6 -28
- package/dist/hono/index.js +11 -43
- package/dist/{hosted-BwKQ1BTA.js → hosted-DJQw46hn.js} +6 -5
- package/dist/{http-GgVApX1k.js → http-BIhmLfCc.js} +4 -20
- package/dist/http-SO7qGttf.js +44 -0
- package/dist/ids-DC4V4CFu.js +18 -0
- package/dist/index-0nNqHspG.d.ts +6 -0
- package/dist/{index-xIxJLwOQ.d.ts → index-BNc7Wer_.d.ts} +75 -7
- package/dist/{index-CltcQ4Bf.d.ts → index-C7Tw7MaW.d.ts} +68 -5
- package/dist/{index-LF_hS-yn.d.ts → index-C8z4RjQV.d.ts} +1 -1
- package/dist/index.d.ts +14 -9
- package/dist/index.js +22 -17
- package/dist/instance-options-Dhhcnm3y.js +22 -0
- package/dist/instance-options-jARdwfPN.d.ts +11 -0
- package/dist/{parse-DPfIvFMU.js → json-schema-BsAflUGW.js} +28 -57
- package/dist/jwt/index.d.ts +10 -6
- package/dist/jwt/index.js +32 -13
- package/dist/kernel-_RRHmnMY.js +208 -0
- package/dist/kysely/index.d.ts +4 -3
- package/dist/kysely/index.js +3 -3
- package/dist/live-options-H2eG8dfJ.js +60 -0
- package/dist/{load-BZ24CpZP.js → load-DhWEB9P1.js} +16 -10
- package/dist/load-DwJUafYN.js +2 -0
- package/dist/manifest-iaoBXHKc.js +783 -0
- package/dist/manifest-uDS9y5de.d.ts +206 -0
- package/dist/{map-DowSO96j.js → map-AcxK9o_D.js} +4 -15
- package/dist/{map-claims-B_6I1Rn-.js → map-claims-Z4QZOiFF.js} +13 -4
- package/dist/map-error-pgUWNNtG.js +23 -0
- package/dist/may-use-DndQ5tQ-.js +77 -0
- package/dist/mcp/index.d.ts +54 -22
- package/dist/mcp/index.js +218 -178
- package/dist/nest/index.d.ts +26 -31
- package/dist/nest/index.js +75 -84
- package/dist/next/client.d.ts +7 -8
- package/dist/next/client.js +11 -9
- package/dist/next/index.d.ts +30 -47
- package/dist/next/index.js +45 -60
- package/dist/next/plugin.d.ts +1 -1
- package/dist/next/plugin.js +32 -1
- package/dist/node/index.d.ts +9 -28
- package/dist/node/index.js +11 -43
- package/dist/openai/index.d.ts +6 -15
- package/dist/openai/index.js +7 -20
- package/dist/openapi/index.d.ts +87 -4
- package/dist/openapi/index.js +3 -3
- package/dist/{openapi-DcqM8nU7.js → openapi-BqRBvAng.js} +237 -5
- package/dist/{openapi-DWD0Uc_4.js → openapi-CqqEbHsL.js} +5 -5
- package/dist/{openapi-import-BWRUNNeW.js → openapi-import-cmwetbzw.js} +1 -1
- package/dist/{openapi-CwePaF7C.js → openapi-l0xYHkcv.js} +3 -3
- package/dist/{openapi-schema-D8j_T9tO.js → openapi-schema--ARjyfQr.js} +2 -2
- package/dist/orpc/index.d.ts +15 -28
- package/dist/orpc/index.js +74 -117
- package/dist/otel/index.d.ts +2 -2
- package/dist/parse-BuyjxhZV.js +53 -0
- package/dist/parse-local-manifest-BrMozJCo.js +36 -0
- package/dist/parse-local-manifest-JLnaBH0S.d.ts +10 -0
- package/dist/pdp/index.d.ts +2 -2
- package/dist/pdp/index.js +14 -127
- package/dist/{permdock-yoZ7EjxX.js → permdock-CbgvLnuo.js} +672 -149
- package/dist/{permissions-B8vCg_bI.d.ts → permissions-CDgvKG5L.d.ts} +2 -2
- package/dist/{permissions-DC7GROPA.js → permissions-DVoF6dMt.js} +77 -15
- package/dist/{permissions-B4apnahQ.d.ts → permissions-DttnqqxC.d.ts} +34 -5
- package/dist/{permissions-CofRqesP.js → permissions-RJrovw98.js} +3 -3
- package/dist/{pins-CqE1jZuF.js → pins-8ILTftC9.js} +2 -2
- package/dist/{policy-Cq2fB8Q5.d.ts → policy-D2W7uv19.d.ts} +2 -2
- package/dist/{policy-CInmNGkM.d.ts → policy-IB6yxpC3.d.ts} +351 -42
- package/dist/{policy-ANp36PnI.js → policy-xyWg7QdR.js} +173 -22
- package/dist/powersync-BylawD_U.js +540 -0
- package/dist/powersync-DliZK_-y.js +62 -0
- package/dist/powersync-e_AZrKbg.js +2 -0
- package/dist/prisma/index.d.ts +3 -2
- package/dist/prisma/index.js +45 -20
- package/dist/{problem-Cgp9-KZA.js → problem-B43ztKIm.js} +74 -109
- package/dist/problem-details-DX075l9V.d.ts +7 -0
- package/dist/problem-details-_AMOYz-w.js +144 -0
- package/dist/{protected-BuPAycJT.d.ts → protected-BdnIEFkY.d.ts} +5 -4
- package/dist/protected-utU-SLuy.js +100 -0
- package/dist/protected-xAr24FEC.d.ts +40 -0
- package/dist/provider-CSQd7m92.js +52 -0
- package/dist/{provider-wTAUda2v.d.ts → provider-DZY281jO.d.ts} +1 -1
- package/dist/provider-store-Cfbvo4DE.js +43 -0
- package/dist/react/index.d.ts +7 -5
- package/dist/react/index.js +5 -4
- package/dist/react/provider-client.d.ts +1 -1
- package/dist/react/provider-client.js +1 -1
- package/dist/react-native/index.d.ts +166 -10
- package/dist/react-native/index.js +607 -85
- package/dist/{refs-ChLSxbST.js → refs-KYb4ntaz.js} +4 -3
- package/dist/{relations-r4soIFZB.js → relations-cjOL01hZ.js} +110 -31
- package/dist/replay-BsOyTrdV.js +28 -0
- package/dist/replay-CYyiLiwO.d.ts +5 -0
- package/dist/resource-ref-CaS9yVPi.js +20 -0
- package/dist/revocations-Bhr0aMIC.js +27 -0
- package/dist/{rls-BOFU3chG.js → rls-B4ur8nKQ.js} +8 -3
- package/dist/rls-DQtdgTHB.js +2521 -0
- package/dist/rls-fields-74oZPXWL.js +3582 -0
- package/dist/{rls-graph-DxQ39PJj.js → rls-graph-CHwx0V8q.js} +100 -36
- package/dist/{rls-import-CXV6LBlS.js → rls-import-BTFVk3jH.js} +7 -6
- package/dist/{rls-migrate-DrFPbf8p.js → rls-migrate-CN4TlJTO.js} +6 -5
- package/dist/rls-ownership-RuFqaqGz.js +843 -0
- package/dist/rls-shared-Cewd_-zI.js +495 -0
- package/dist/rls-verify-CVQTHTh5.js +746 -0
- package/dist/{row-check-BajTgQZ-.d.ts → row-check-Dt35czLy.d.ts} +12 -15
- package/dist/row-conditions-h2HythXQ.js +27 -0
- package/dist/rpc-DL9Sqvb1.js +89 -0
- package/dist/{run-CJ6UvS3a.js → run-DKDbM4Du.js} +31 -12
- package/dist/runtime-CB8gQFvS.js +11 -0
- package/dist/{saas-BRrZzJkM.js → saas-DhpJjnBA.js} +17 -7
- package/dist/{schema-P8yoma__.js → schema-D3MnDMxa.js} +8 -4
- package/dist/scim/index.d.ts +1 -1
- package/dist/scim/index.js +1 -1
- package/dist/{scim-9NFa0XVZ.js → scim-BFGBBAqR.js} +1 -1
- package/dist/{scopes-cKVp_WTw.js → scopes-CVdPrWN-.js} +35 -8
- package/dist/server/index.d.ts +22 -11
- package/dist/server/index.js +11 -8
- package/dist/{credentials-BgRX3WEg.js → server-DlZ28Oz5.js} +1 -1
- package/dist/shared-DRu8VFVM.js +123 -0
- package/dist/{sink-Cfr9-7YC.d.ts → sink-B1fZzIMf.d.ts} +1 -1
- package/dist/skills-Biq5uup3.js +192 -0
- package/dist/{skills-C_lpV4Jx.js → skills-Bx-U0irG.js} +10 -4
- package/dist/{snapshot-DE8dE_hz.js → snapshot-Enxd0n-s.js} +138 -214
- package/dist/snapshot-cache-B-LxUahj.d.ts +45 -0
- package/dist/snapshot-cache-D6jLelln.js +44 -0
- package/dist/solid/index.d.ts +39 -9
- package/dist/solid/index.js +76 -85
- package/dist/sources-4p0-4xQm.js +545 -0
- package/dist/sources-BdOYLw9j.d.ts +192 -0
- package/dist/{sql-Bll1d7y0.js → sql-xdg5f4Fi.js} +6 -1
- package/dist/{src-CTNT-PBW.js → src-BDwEZWrZ.js} +149 -11
- package/dist/ssf/index.d.ts +3 -2
- package/dist/ssf/index.js +3 -29
- package/dist/store-DMAWwAJV.js +371 -0
- package/dist/{store-Bq90395i.d.ts → store-jOBYpiXC.d.ts} +13 -4
- package/dist/store-options-ByhZq1jV.js +716 -0
- package/dist/{stores-CkG2HrSg.js → stores-BT2pSCub.js} +10 -63
- package/dist/{stores-CvOnu_2T.d.ts → stores-C0IMQrGI.d.ts} +20 -9
- package/dist/{stream-BhMfZOwr.d.ts → stream-D1RXubw2.d.ts} +2 -2
- package/dist/{stream-Q_xlB6Ko.js → stream-DDpvynSB.js} +16 -2
- package/dist/subject-settings-BwBmCdTR.js +128 -0
- package/dist/supabase/index.d.ts +133 -6
- package/dist/supabase/index.js +4 -3
- package/dist/supabase/middleware.d.ts +32 -27
- package/dist/supabase/middleware.js +56 -22
- package/dist/{supabase-D8UlBA7Z.js → supabase-Boqb9a_Y.js} +3 -3
- package/dist/{supabase-DTxGtYdi.js → supabase-C0qESOa-.js} +347 -367
- package/dist/supabase-config-BpPXt3x1.js +472 -0
- package/dist/supabase-hook-6sYLXg_u.js +2 -0
- package/dist/{supabase-hook-D_OqGb_V.js → supabase-hook-CSLQ4zJ3.js} +1037 -705
- package/dist/supautils-Cn4R4cJ4.js +91 -0
- package/dist/svelte/PermissionBoundary.svelte +37 -0
- package/dist/svelte/Protected.svelte +8 -2
- package/dist/svelte/index.d.ts +9 -6
- package/dist/svelte/index.js +61 -7
- package/dist/svelte/runtime.d.ts +4 -3
- package/dist/svelte/runtime.js +4 -3
- package/dist/svelte/source.d.ts +9 -6
- package/dist/svelte/source.js +7 -4
- package/dist/terminal/index.d.ts +6 -14
- package/dist/terminal/index.js +44 -32
- package/dist/testing/index.d.ts +105 -16
- package/dist/testing/index.js +504 -40
- package/dist/testing/saas/index.d.ts +4 -4
- package/dist/testing/saas/index.js +2 -2
- package/dist/testing/saas/permissions.d.ts +1 -1
- package/dist/testing/saas/permissions.js +1 -1
- package/dist/token-CIWxJOCh.js +50 -0
- package/dist/trpc/index.d.ts +7 -26
- package/dist/trpc/index.js +17 -100
- package/dist/types-BnIEz1sU.d.ts +48 -0
- package/dist/{types-CxquUByI.d.ts → types-Bvc4oWT6.d.ts} +1 -1
- package/dist/{sources-DOpW5Ila.d.ts → types-C97IskBw.d.ts} +86 -127
- package/dist/types-CKNSyIf_.d.ts +52 -0
- package/dist/types-Cq_H8N_X.d.ts +197 -0
- package/dist/{types-D6Z6-rKJ.d.ts → types-DbjRN55U.d.ts} +273 -22
- package/dist/{types-BBK-exvJ.d.ts → types-Dnemwd77.d.ts} +7 -1
- package/dist/{types-BvBk1wgn.d.ts → types-DxiCGmmd.d.ts} +5 -3
- package/dist/{types-B9dmiiab.d.ts → types-QCuYJh4q.d.ts} +1 -1
- package/dist/{types-B8w5PFtS.d.ts → types-aqOHTHcp.d.ts} +17 -1
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +10 -9
- package/dist/usage-DorAJiU_.js +2 -0
- package/dist/{usage-1ozMubLY.js → usage-GTixvDDU.js} +2 -2
- package/dist/{usage-CUnK6Ou9.js → usage-ItS7nLcl.js} +225 -62
- package/dist/validation-error-Ba264OaJ.d.ts +26 -0
- package/dist/validation-error-CnjZ4t92.js +33 -0
- package/dist/{validity-D0uRmJKJ.js → validity-uOp8UpYn.js} +57 -36
- package/dist/{verifier-B90agnhl.js → verifier-BxBo6I44.js} +10 -5
- package/dist/version-BvacVTZR.js +17 -0
- package/dist/views-Soe7bGSC.js +95 -0
- package/dist/{vocabulary-Dk0fs-Jk.d.ts → vocabulary-DA8a8zif.d.ts} +1 -1
- package/dist/vue/index.d.ts +35 -13
- package/dist/vue/index.js +86 -93
- package/dist/watch-BwxLTYMZ.js +2 -0
- package/dist/watch-DVBJ9cBq.js +128 -0
- package/dist/webmcp/index.d.ts +2 -2
- package/dist/webmcp/index.js +7 -13
- package/package.json +62 -36
- package/schemas/approval-request-v1.json +175 -0
- package/schemas/catalog-v1.json +29 -1
- package/schemas/supabase-claims-v1.json +6 -1
- package/schemas/supabase-manifest-v1.json +150 -6
- package/skills/permdock/SKILL.md +7 -7
- package/skills/permdock-agents/SKILL.md +5 -5
- package/skills/permdock-agents/references/runtimes.md +12 -10
- package/skills/permdock-approvals/SKILL.md +4 -4
- package/skills/permdock-approvals/references/stores-and-resume.md +15 -4
- package/skills/permdock-audit/SKILL.md +3 -3
- package/skills/permdock-credentials/SKILL.md +4 -4
- package/skills/permdock-credentials/references/tokens-keys-links.md +7 -2
- package/skills/permdock-data/SKILL.md +4 -4
- package/skills/permdock-data/references/queries-and-rls.md +14 -6
- package/skills/permdock-data/references/relationships.md +5 -3
- package/skills/permdock-tenancy/SKILL.md +9 -6
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +14 -8
- package/skills/permdock-wire/SKILL.md +4 -4
- package/skills/permdock-wire/references/adapters.md +42 -30
- package/skills/permdock-wire/references/policy-options.md +5 -3
- package/dist/approvers-aH0PJkmf.js +0 -20
- package/dist/catalog-idsHc0vo.js +0 -2
- package/dist/collect-B7_TIfmg.js +0 -49
- package/dist/collect-fRPAQIWb.js +0 -2
- package/dist/compile-CQbvyIBr.d.ts +0 -30
- package/dist/config-BPYmjTOp.js +0 -26
- package/dist/context-gtAwAILx.js +0 -6
- package/dist/create-BKF3dgFk.d.ts +0 -162
- package/dist/create-CSkhDU9T.js +0 -222
- package/dist/doctor-BGle8Gqi.js +0 -1314
- package/dist/helpers-DnSOrhB9.js +0 -682
- package/dist/index-fG6NSXlc.d.ts +0 -9
- package/dist/kernel-YOGCWNLf.js +0 -211
- package/dist/load-QOOm1z70.js +0 -2
- package/dist/manifest-DDQbC6K0.d.ts +0 -118
- package/dist/map-error-RJ3ZsUyA.js +0 -16
- package/dist/may-use-CnFEjVEp.js +0 -37
- package/dist/pg-4GJWDUxG.js +0 -953
- package/dist/plugin-4gr7IxFJ.js +0 -65
- package/dist/protected-B4K3BKoX.js +0 -138
- package/dist/protected-wY7nXhY9.d.ts +0 -23
- package/dist/provider-_wx46HpB.js +0 -48
- package/dist/rls-CJVja9Ww.js +0 -2119
- package/dist/rls-helpers-BxRuif0l.js +0 -1346
- package/dist/runtime-DP9ttFh1.js +0 -27
- package/dist/skills-Dx3QLnMY.js +0 -110
- package/dist/source-DoBBYJi2.js +0 -6
- package/dist/sources-DVHM3Mwq.js +0 -284
- package/dist/store-C6hFW6ta.js +0 -508
- package/dist/subject-settings-xt7V845g.js +0 -82
- package/dist/supabase-hook-DW-xP4hU.js +0 -2
- package/dist/types-DbWdEfcm.d.ts +0 -87
- package/dist/types-bva3i4Gf.d.ts +0 -98
- package/dist/types-i4BAOvdb.d.ts +0 -97
- package/dist/usage-PyEor6GA.js +0 -2
- package/dist/version-CnSh33Fj.js +0 -16
|
@@ -1,1346 +0,0 @@
|
|
|
1
|
-
import { n as isReadonlyArray, r as sole } from "./compact-y6vfu2_f.js";
|
|
2
|
-
import { S as isForbiddenKey } from "./permissions-DC7GROPA.js";
|
|
3
|
-
import { l as isSqlFunctionField } from "./refs-ChLSxbST.js";
|
|
4
|
-
import { a as resolveScope, c as scopeChain, s as rootScope } from "./scopes-cKVp_WTw.js";
|
|
5
|
-
import { t as byCodePoint } from "./compare-pfyVnBjE.js";
|
|
6
|
-
import { i as scopeMembershipTable, o as relatedSql, r as scopeColumn } from "./compile-D22iwvuV.js";
|
|
7
|
-
import { a as quoteSqlTable, i as quoteSqlLiteral, r as quoteSqlIdent, t as SQL_IDENT } from "./sql-Bll1d7y0.js";
|
|
8
|
-
import "./sources-DVHM3Mwq.js";
|
|
9
|
-
//#region src/cli/rls-sql.ts
|
|
10
|
-
const CLI = "PermDock CLI";
|
|
11
|
-
const CLAIM = SQL_IDENT;
|
|
12
|
-
function checkActiveRow(label, row) {
|
|
13
|
-
quoteTable(row.table);
|
|
14
|
-
quoteIdent(row.id);
|
|
15
|
-
if (row.disabledAt !== void 0) quoteIdent(row.disabledAt);
|
|
16
|
-
if (row.status !== void 0) {
|
|
17
|
-
quoteIdent(row.status);
|
|
18
|
-
if (row.active === void 0 || row.active.length === 0) throw new Error(`PermDock CLI: ${label}.status needs the active values in ${label}.active`);
|
|
19
|
-
}
|
|
20
|
-
if (row.disabledAt === void 0 && row.status === void 0) throw new Error(`PermDock CLI: ${label} needs disabledAt or status to tell an active row`);
|
|
21
|
-
}
|
|
22
|
-
/** Validates `rls.suspension` and keys its scopes by declared name. */
|
|
23
|
-
function checkSuspension(suspension, scopes) {
|
|
24
|
-
if (suspension === void 0) return;
|
|
25
|
-
if (suspension.users !== void 0) checkActiveRow("rls.suspension.users", suspension.users);
|
|
26
|
-
const byName = {};
|
|
27
|
-
for (const [key, row] of Object.entries(suspension.scopes ?? {})) {
|
|
28
|
-
const name = resolveScope(scopes, key);
|
|
29
|
-
if (name === void 0) throw new Error(`PermDock CLI: rls.suspension.scopes.${key} names a scope the policy does not declare`);
|
|
30
|
-
checkActiveRow(`rls.suspension.scopes.${key}`, row);
|
|
31
|
-
byName[name] = row;
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
...suspension.users === void 0 ? {} : { users: suspension.users },
|
|
35
|
-
...Object.keys(byName).length === 0 ? {} : { scopes: byName }
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* `exists` over an active row whose id column equals `id`: a missing row is
|
|
40
|
-
* suspended, so the check fails closed.
|
|
41
|
-
*/
|
|
42
|
-
function activeRowSql(row, id) {
|
|
43
|
-
const table = quoteTable(row.table.includes(".") ? row.table : `public.${row.table}`);
|
|
44
|
-
const parts = [`s.${quoteIdent(row.id)} = ${id}`];
|
|
45
|
-
if (row.disabledAt !== void 0) parts.push(`s.${quoteIdent(row.disabledAt)} is null`);
|
|
46
|
-
if (row.status !== void 0) {
|
|
47
|
-
const values = (row.active ?? []).map(quoteLiteral).join(", ");
|
|
48
|
-
parts.push(`s.${quoteIdent(row.status)}::text = any(array[${values}]::text[])`);
|
|
49
|
-
}
|
|
50
|
-
return `exists (select 1 from ${table} s where ${parts.join(" and ")})`;
|
|
51
|
-
}
|
|
52
|
-
/** The active-user check for `user`; empty without `rls.suspension.users`. */
|
|
53
|
-
function activeUserSql(ctx, user = subjectIdSql(ctx)) {
|
|
54
|
-
const row = ctx.suspension?.users;
|
|
55
|
-
return row === void 0 ? [] : [activeRowSql(row, user)];
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* The active-instance check for every suspendable scope on `scope`'s chain:
|
|
59
|
-
* its own and each ancestor's. `idOf` gives the SQL for the id the membership
|
|
60
|
-
* holds for a scope on that chain.
|
|
61
|
-
*/
|
|
62
|
-
function activeInstancesSql(ctx, scope, idOf) {
|
|
63
|
-
const parts = [];
|
|
64
|
-
for (const name of scopeChain(ctx.scopes, scope)) {
|
|
65
|
-
const row = ctx.suspension?.scopes?.[name];
|
|
66
|
-
if (row === void 0) continue;
|
|
67
|
-
const id = idOf(name);
|
|
68
|
-
if (id === void 0) throw new Error(`PermDock CLI: rls.suspension.scopes.${name} needs the ${name} id on ${scope} memberships: add columns.${name} to the ${scope} memberships table`);
|
|
69
|
-
parts.push(activeRowSql(row, `(${id})::${scopeTypeOf(ctx, name)}`));
|
|
70
|
-
}
|
|
71
|
-
return parts;
|
|
72
|
-
}
|
|
73
|
-
const SQL_TYPE = /^[A-Za-z_][A-Za-z0-9_]*( [A-Za-z_][A-Za-z0-9_]*)*(\[\])?$/u;
|
|
74
|
-
function sqlType(name) {
|
|
75
|
-
if (!SQL_TYPE.test(name)) throw new Error(`PermDock CLI: unsafe SQL type '${name}'`);
|
|
76
|
-
return name;
|
|
77
|
-
}
|
|
78
|
-
function tenantTypeOf(ctx) {
|
|
79
|
-
return sqlType(ctx.tenantType ?? "uuid");
|
|
80
|
-
}
|
|
81
|
-
function teamTypeOf(ctx) {
|
|
82
|
-
return sqlType(ctx.teamType ?? ctx.tenantType ?? "uuid");
|
|
83
|
-
}
|
|
84
|
-
/** Postgres type of scope `name`'s id: `scopeTypes`, then the tenant / team type by position. */
|
|
85
|
-
function scopeTypeOf(ctx, name) {
|
|
86
|
-
const declared = ctx.scopeTypes?.[name];
|
|
87
|
-
if (declared !== void 0) return sqlType(declared);
|
|
88
|
-
return name === ctx.scopes[1]?.name ? teamTypeOf(ctx) : tenantTypeOf(ctx);
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* A role with `for` counts only on a membership of one of those kinds:
|
|
92
|
-
* `case role when 'admin' then via = any(...) ... else true end`. A missing
|
|
93
|
-
* kind (`viaExpr` null) holds none of them. `undefined` when no role has `for`.
|
|
94
|
-
*/
|
|
95
|
-
function kindFilterSql(ctx, roleExpr, viaExpr) {
|
|
96
|
-
const kinds = Object.entries(ctx.ownership?.kinds ?? {}).toSorted(([a], [b]) => byCodePoint(a, b));
|
|
97
|
-
if (kinds.length === 0) return;
|
|
98
|
-
return `case ${roleExpr} ${kinds.map(([role, allowed]) => `when ${quoteLiteral(role)} then coalesce(${viaExpr}, '') = any(array[${allowed.map(quoteLiteral).join(", ")}]::text[])`).join(" ")} else true end`;
|
|
99
|
-
}
|
|
100
|
-
/** The kind check for one known role; `undefined` when it has no `for`. */
|
|
101
|
-
function roleKindSql(ctx, role, viaExpr) {
|
|
102
|
-
const allowed = ctx.ownership?.kinds[role];
|
|
103
|
-
return allowed === void 0 ? void 0 : `coalesce(${viaExpr}, '') = any(array[${allowed.map(quoteLiteral).join(", ")}]::text[])`;
|
|
104
|
-
}
|
|
105
|
-
/** Roles with `for` held globally (no membership, so no kind) grant nothing. */
|
|
106
|
-
function globalKindFilterSql(ctx, roleExpr) {
|
|
107
|
-
const roles = Object.keys(ctx.ownership?.kinds ?? {}).toSorted();
|
|
108
|
-
return roles.length === 0 ? void 0 : `not (${roleExpr} = any(array[${roles.map(quoteLiteral).join(", ")}]::text[]))`;
|
|
109
|
-
}
|
|
110
|
-
/** The closure table graph grants read. Part of the SQL contract. */
|
|
111
|
-
const CLOSURE = { table: "permdock_closure" };
|
|
112
|
-
/** The helper returning the ids of `resource` the subject holds a relation on. */
|
|
113
|
-
function graphHelper(resource) {
|
|
114
|
-
return permittedIdsHelper(resource);
|
|
115
|
-
}
|
|
116
|
-
/** The helper returning the ids of `resource` whose `link` points into the ids it is given. */
|
|
117
|
-
function linkHelper(resource, link) {
|
|
118
|
-
if (!/^[a-z][a-z0-9_]*$/u.test(link)) throw new Error(`PermDock CLI: link '${link}' on ${resource} is not a lowercase SQL name, so rls generate cannot name its helper`);
|
|
119
|
-
permittedIdsHelper(resource);
|
|
120
|
-
return `permdock_link_${resource}_${link}`;
|
|
121
|
-
}
|
|
122
|
-
/** Graph SQL parts as RLS text: values inline as literals, the subject from the dialect's claim. */
|
|
123
|
-
function graphSqlText(parts, ctx) {
|
|
124
|
-
return parts.map((part) => {
|
|
125
|
-
if ("text" in part) return part.text;
|
|
126
|
-
if ("column" in part) return quoteIdent(part.column);
|
|
127
|
-
if ("subject" in part) return subjectIdSql(ctx);
|
|
128
|
-
return typeof part.value === "string" ? quoteLiteral(part.value) : String(part.value);
|
|
129
|
-
}).join("");
|
|
130
|
-
}
|
|
131
|
-
/** The schema-qualified name of a table under `search_path = ''`. */
|
|
132
|
-
function qualifiedTable(name) {
|
|
133
|
-
return name.includes(".") ? name : `public.${name}`;
|
|
134
|
-
}
|
|
135
|
-
/** The helper returning the ids of scope `name` a grant key reaches. */
|
|
136
|
-
function permittedIdsHelper(name) {
|
|
137
|
-
if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw new Error(`PermDock CLI: unsafe scope name '${name}'`);
|
|
138
|
-
return `permitted_${name}_ids`;
|
|
139
|
-
}
|
|
140
|
-
/** The helper returning the ids of scope `name` the subject holds any live membership of. */
|
|
141
|
-
function memberIdsHelper(name) {
|
|
142
|
-
if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw new Error(`PermDock CLI: unsafe scope name '${name}'`);
|
|
143
|
-
return `member_${name}_ids`;
|
|
144
|
-
}
|
|
145
|
-
/** `member_<scope>_ids_for(p_user uuid)`: the same rows for a user the caller names; the token hook's helper. */
|
|
146
|
-
function memberForHelper(name) {
|
|
147
|
-
return `${memberIdsHelper(name)}_for`;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Whether `member_<scope>_ids_for` is generated for `name`: Supabase only
|
|
151
|
-
* (its one caller is `supabase_auth_admin`), and only where `memberships` maps
|
|
152
|
-
* a table for the scope or a membership source can hold it.
|
|
153
|
-
*/
|
|
154
|
-
function hasMemberFor(input, name) {
|
|
155
|
-
if (input.dialect !== "supabase") return false;
|
|
156
|
-
return memberForTable(input, name) !== void 0 || memberForSources(input, name).length > 0;
|
|
157
|
-
}
|
|
158
|
-
/** The table `member_<scope>_ids_for` reads for scope `name`, when `memberships` maps one with its id column. */
|
|
159
|
-
function memberForTable(input, name) {
|
|
160
|
-
const table = scopeMembershipTable(input.memberships, input.scopes, name);
|
|
161
|
-
return table !== void 0 && scopeColumn(table, input.scopes, name) !== void 0 ? table : void 0;
|
|
162
|
-
}
|
|
163
|
-
/** The sources `member_<scope>_ids_for` reads: none when a table is mapped, as in `database` mode. */
|
|
164
|
-
function memberForSources(input, name) {
|
|
165
|
-
if (memberForTable(input, name) !== void 0) return [];
|
|
166
|
-
return (input.memberSources ?? []).filter((source) => source.sql.scope === void 0 || source.sql.scope === name);
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* The membership sources that can hold scope `name` when `memberships` maps
|
|
170
|
-
* no table for it: every `fromTable` source and the `fromJunction` sources of
|
|
171
|
-
* that scope. Empty outside `database` mode.
|
|
172
|
-
*/
|
|
173
|
-
function scopeSources(ctx, name) {
|
|
174
|
-
if (ctx.authorize !== "database" || scopeTable(ctx, name) !== void 0) return [];
|
|
175
|
-
return (ctx.sources ?? []).filter((source) => source.sql.scope === void 0 || source.sql.scope === name);
|
|
176
|
-
}
|
|
177
|
-
/** The membership table mapped for scope `name`, with the column of its id and of the first scope's id. */
|
|
178
|
-
function scopeTable(ctx, name) {
|
|
179
|
-
const table = scopeMembershipTable(ctx.memberships, ctx.scopes, name);
|
|
180
|
-
if (table === void 0) return;
|
|
181
|
-
const column = scopeColumn(table, ctx.scopes, name);
|
|
182
|
-
if (column === void 0) return;
|
|
183
|
-
const root = rootScope(ctx.scopes);
|
|
184
|
-
const tenantColumn = root !== void 0 && scopeChain(ctx.scopes, name).includes(root) ? scopeColumn(table, ctx.scopes, root) : void 0;
|
|
185
|
-
return tenantColumn === void 0 ? {
|
|
186
|
-
table,
|
|
187
|
-
column
|
|
188
|
-
} : {
|
|
189
|
-
table,
|
|
190
|
-
column,
|
|
191
|
-
tenantColumn
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
/** The active-tenant claim cast to the tenant column's type, so the comparison uses the column's index. */
|
|
195
|
-
function tenantClaimSql(ctx) {
|
|
196
|
-
return `${subjectClaimSql(ctx, ctx.tenantClaim)}::${tenantTypeOf(ctx)}`;
|
|
197
|
-
}
|
|
198
|
-
function quoteIdent(name) {
|
|
199
|
-
return quoteSqlIdent(name, CLI);
|
|
200
|
-
}
|
|
201
|
-
function quoteTable(name) {
|
|
202
|
-
return quoteSqlTable(name, CLI);
|
|
203
|
-
}
|
|
204
|
-
function quoteLiteral(value) {
|
|
205
|
-
return quoteSqlLiteral(value);
|
|
206
|
-
}
|
|
207
|
-
function subjectIdSql(ctx) {
|
|
208
|
-
switch (ctx.dialect) {
|
|
209
|
-
case "supabase": return "(select auth.uid())";
|
|
210
|
-
case "neon": return "(select auth.user_id())";
|
|
211
|
-
case "guc": return `(select current_setting(${quoteLiteral(`${ctx.gucPrefix}.user_id`)}, true))`;
|
|
212
|
-
default: return ctx.dialect;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
function subjectClaimSql(ctx, claim) {
|
|
216
|
-
if (!CLAIM.test(claim)) throw new Error(`PermDock CLI: unsafe claim name '${claim}'`);
|
|
217
|
-
switch (ctx.dialect) {
|
|
218
|
-
case "supabase": return `((select auth.jwt()) ->> ${quoteLiteral(claim)})`;
|
|
219
|
-
case "neon": return `((select auth.session()) ->> ${quoteLiteral(claim)})`;
|
|
220
|
-
case "guc": return `(select current_setting(${quoteLiteral(`${ctx.gucPrefix}.${claim}`)}, true))`;
|
|
221
|
-
default: return ctx.dialect;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
/** A claim as `jsonb`; the `guc` dialect stores JSON text in `<prefix>.<claim>`. */
|
|
225
|
-
function subjectClaimJsonSql(ctx, claim) {
|
|
226
|
-
if (!CLAIM.test(claim)) throw new Error(`PermDock CLI: unsafe claim name '${claim}'`);
|
|
227
|
-
switch (ctx.dialect) {
|
|
228
|
-
case "supabase": return `((select auth.jwt()) -> ${quoteLiteral(claim)})`;
|
|
229
|
-
case "neon": return `((select auth.session()) -> ${quoteLiteral(claim)})`;
|
|
230
|
-
case "guc": return `nullif((select current_setting(${quoteLiteral(`${ctx.gucPrefix}.${claim}`)}, true)), '')::jsonb`;
|
|
231
|
-
default: return ctx.dialect;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
function isRecord(value) {
|
|
235
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
236
|
-
}
|
|
237
|
-
/** The Postgres type a JSON Schema property compares as; `undefined` for text or an unknown shape. */
|
|
238
|
-
function columnTypeOf(property) {
|
|
239
|
-
if (!isRecord(property)) return;
|
|
240
|
-
const variants = [property["anyOf"], property["oneOf"]].find(Array.isArray);
|
|
241
|
-
if (variants !== void 0) {
|
|
242
|
-
const present = variants.filter((item) => !(isRecord(item) && item["type"] === "null"));
|
|
243
|
-
return present.length === 1 ? columnTypeOf(present[0]) : void 0;
|
|
244
|
-
}
|
|
245
|
-
const types = (Array.isArray(property["type"]) ? property["type"] : [property["type"]]).filter((item) => item !== "null");
|
|
246
|
-
if (types.length !== 1) return;
|
|
247
|
-
switch (types[0]) {
|
|
248
|
-
case "integer":
|
|
249
|
-
case "number": return "numeric";
|
|
250
|
-
case "boolean": return "boolean";
|
|
251
|
-
case "string": switch (property["format"]) {
|
|
252
|
-
case "date-time": return "timestamptz";
|
|
253
|
-
case "date": return "date";
|
|
254
|
-
case "uuid": return "uuid";
|
|
255
|
-
default: return;
|
|
256
|
-
}
|
|
257
|
-
default: return;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Column types from a resource's JSON Schema (`~standard.jsonSchema`): numbers
|
|
262
|
-
* compare as `numeric`, booleans as `boolean`, and `date-time`, `date` and
|
|
263
|
-
* `uuid` strings as `timestamptz`, `date` and `uuid`. Other columns are left
|
|
264
|
-
* out and compare as text.
|
|
265
|
-
*/
|
|
266
|
-
function columnTypesOf(schema) {
|
|
267
|
-
const properties = isRecord(schema) ? schema["properties"] : void 0;
|
|
268
|
-
if (!isRecord(properties)) return {};
|
|
269
|
-
const types = {};
|
|
270
|
-
for (const [name, property] of Object.entries(properties)) {
|
|
271
|
-
const type = isForbiddenKey(name) ? void 0 : columnTypeOf(property);
|
|
272
|
-
if (type !== void 0) types[name] = type;
|
|
273
|
-
}
|
|
274
|
-
return types;
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Array columns from a resource's JSON Schema, each with the type its items
|
|
278
|
-
* compare as (`text` when `columnTypesOf` would leave the item out).
|
|
279
|
-
*/
|
|
280
|
-
function arrayColumnsOf(schema) {
|
|
281
|
-
const properties = isRecord(schema) ? schema["properties"] : void 0;
|
|
282
|
-
if (!isRecord(properties)) return {};
|
|
283
|
-
const arrays = {};
|
|
284
|
-
for (const [name, property] of Object.entries(properties)) {
|
|
285
|
-
if (isForbiddenKey(name) || !isRecord(property)) continue;
|
|
286
|
-
const types = (Array.isArray(property["type"]) ? property["type"] : [property["type"]]).filter((item) => item !== "null");
|
|
287
|
-
if (types.length === 1 && types[0] === "array") arrays[name] = columnTypeOf(property["items"]) ?? "text";
|
|
288
|
-
}
|
|
289
|
-
return arrays;
|
|
290
|
-
}
|
|
291
|
-
const MAX_CLAIM_DEPTH = 8;
|
|
292
|
-
/**
|
|
293
|
-
* The claim path of a `principal.claim.*` / `principal.claims.*` ref, one
|
|
294
|
-
* segment per JSON key; `undefined` for any other ref.
|
|
295
|
-
*/
|
|
296
|
-
function claimPath(ref) {
|
|
297
|
-
const prefix = ["principal.claim.", "principal.claims."].find((item) => ref.startsWith(item));
|
|
298
|
-
if (prefix === void 0) return;
|
|
299
|
-
const segments = ref.slice(prefix.length).split(".");
|
|
300
|
-
if (segments.length > MAX_CLAIM_DEPTH) throw new Error(`PermDock CLI: claim path '${ref}' is deeper than ${MAX_CLAIM_DEPTH}`);
|
|
301
|
-
for (const segment of segments) if (!CLAIM.test(segment) || isForbiddenKey(segment)) throw new Error(`PermDock CLI: unsafe claim name '${segment}'`);
|
|
302
|
-
return segments;
|
|
303
|
-
}
|
|
304
|
-
function jsonPath(base, keys, last) {
|
|
305
|
-
let sql = base;
|
|
306
|
-
for (const [index, key] of keys.entries()) sql += ` ${index === keys.length - 1 ? last : "->"} ${quoteLiteral(key)}`;
|
|
307
|
-
return sql;
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* The JSON document a claim path starts from and the keys to walk in it. The
|
|
311
|
-
* `guc` dialect keeps each top-level claim in its own setting as JSON text.
|
|
312
|
-
*/
|
|
313
|
-
function claimRoot(ctx, path) {
|
|
314
|
-
const [head, ...rest] = path;
|
|
315
|
-
if (head === void 0) throw new Error("PermDock CLI: empty claim path");
|
|
316
|
-
switch (ctx.dialect) {
|
|
317
|
-
case "supabase": return {
|
|
318
|
-
root: "(select auth.jwt())",
|
|
319
|
-
keys: path
|
|
320
|
-
};
|
|
321
|
-
case "neon": return {
|
|
322
|
-
root: "(select auth.session())",
|
|
323
|
-
keys: path
|
|
324
|
-
};
|
|
325
|
-
case "guc": return {
|
|
326
|
-
root: subjectClaimJsonSql(ctx, head),
|
|
327
|
-
keys: rest
|
|
328
|
-
};
|
|
329
|
-
default: return ctx.dialect;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
/** The claim at `path` as `jsonb`. */
|
|
333
|
-
function claimJsonSql(ctx, path) {
|
|
334
|
-
const { root, keys } = claimRoot(ctx, path);
|
|
335
|
-
return keys.length === 0 ? root : `(${jsonPath(root, keys, "->")})`;
|
|
336
|
-
}
|
|
337
|
-
/** The claim at `path` as text: `->>` on the last key, or the plain setting for a one-segment `guc` claim. */
|
|
338
|
-
function claimTextSql(ctx, path) {
|
|
339
|
-
const [head] = path;
|
|
340
|
-
if (path.length === 1 && head !== void 0) return subjectClaimSql(ctx, head);
|
|
341
|
-
const { root, keys } = claimRoot(ctx, path);
|
|
342
|
-
return `(${jsonPath(root, keys, "->>")})`;
|
|
343
|
-
}
|
|
344
|
-
/** The JSON kind a claim must have to compare with a column of `type`. */
|
|
345
|
-
function jsonKindOf(type) {
|
|
346
|
-
if (type === "numeric") return "number";
|
|
347
|
-
return type === "boolean" ? "boolean" : "string";
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* A claim compared with a column of `type`. Numbers and booleans must be JSON
|
|
351
|
-
* of that kind (anything else is `null`, so the comparison is false, as in
|
|
352
|
-
* memory); other types cast the text form. A one-segment `guc` claim is text
|
|
353
|
-
* and is cast as is.
|
|
354
|
-
*/
|
|
355
|
-
function typedClaimSql(ctx, path, type) {
|
|
356
|
-
const text = claimTextSql(ctx, path);
|
|
357
|
-
if (type === void 0 || type === "text") return text;
|
|
358
|
-
const cast = `${text}::${sqlType(type)}`;
|
|
359
|
-
const kind = jsonKindOf(type);
|
|
360
|
-
if (kind === "string" || ctx.dialect === "guc" && path.length === 1) return `(${cast})`;
|
|
361
|
-
return `(case when jsonb_typeof(${claimJsonSql(ctx, path)}) = '${kind}' then ${cast} end)`;
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* The elements of an array claim as a Postgres array of the column's type,
|
|
365
|
-
* built once per statement (an uncorrelated `array(select ...)` is an InitPlan).
|
|
366
|
-
* A missing or non-array claim is the empty array; elements of another JSON
|
|
367
|
-
* kind are left out.
|
|
368
|
-
*/
|
|
369
|
-
function claimArraySql(ctx, path, type) {
|
|
370
|
-
const json = claimJsonSql(ctx, path);
|
|
371
|
-
return `array(select ${type === void 0 || type === "text" ? `(e #>> '{}')` : `(e #>> '{}')::${sqlType(type)}`} from jsonb_array_elements(case when jsonb_typeof(${json}) = 'array' then ${json} else '[]'::jsonb end) e where jsonb_typeof(e) = '${jsonKindOf(type)}')`;
|
|
372
|
-
}
|
|
373
|
-
function sqlValue(value, ctx, field) {
|
|
374
|
-
if (value !== null && typeof value === "object" && "ref" in value) return compileRef(value.ref, ctx, field);
|
|
375
|
-
if (value !== null && typeof value === "object" && "date" in value) return quoteLiteral(value.date);
|
|
376
|
-
if (isReadonlyArray(value)) return `array[${value.map((item) => sqlValue(item, ctx)).join(", ")}]`;
|
|
377
|
-
if (value === null) return "null";
|
|
378
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
379
|
-
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "null";
|
|
380
|
-
if (typeof value === "string") return quoteLiteral(value);
|
|
381
|
-
throw new Error("PermDock CLI: non-portable condition value");
|
|
382
|
-
}
|
|
383
|
-
function compileRef(ref, ctx, field) {
|
|
384
|
-
if (ref === "principal.id") return subjectIdSql(ctx);
|
|
385
|
-
if (ref === "now") return "now()";
|
|
386
|
-
const path = claimPath(ref);
|
|
387
|
-
if (ref === "principal.tenant" || path?.length === 1 && path[0] === ctx.tenantClaim) return tenantClaimSql(ctx);
|
|
388
|
-
if (path !== void 0) return typedClaimSql(ctx, path, field === void 0 ? void 0 : ctx.columnTypes?.[field]);
|
|
389
|
-
if (ref === "context" || ref.startsWith("context.")) throw new Error(`PermDock CLI: '${ref}' is request context, which is not in the token; RLS cannot read it (permdock doctor PD027)`);
|
|
390
|
-
throw new Error(`PermDock CLI: non-portable subject ref '${ref}'`);
|
|
391
|
-
}
|
|
392
|
-
/** `value` with `\`, `%` and `_` escaped for `like ... escape '\'`, as `escapeLike` does in process. */
|
|
393
|
-
function likeLiteralSql(value) {
|
|
394
|
-
return `replace(replace(replace(${value}, '\\', '\\\\'), '%', '\\%'), '_', '\\_')`;
|
|
395
|
-
}
|
|
396
|
-
function compareSql(op, field, value) {
|
|
397
|
-
const left = quoteIdent(field);
|
|
398
|
-
switch (op) {
|
|
399
|
-
case "eq": return `${left} = ${value}`;
|
|
400
|
-
case "ne": return `${left} <> ${value}`;
|
|
401
|
-
case "gt": return `${left} > ${value}`;
|
|
402
|
-
case "gte": return `${left} >= ${value}`;
|
|
403
|
-
case "lt": return `${left} < ${value}`;
|
|
404
|
-
case "lte": return `${left} <= ${value}`;
|
|
405
|
-
case "contains": return `${left}::text like '%' || ${likeLiteralSql(`${value}::text`)} || '%' escape '\\'`;
|
|
406
|
-
default: return op;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
function existsSql(table, rowColumn, rowField, roles, ctx, tenantColumn, scope) {
|
|
410
|
-
const parts = [`m.${quoteIdent(rowColumn)} = ${quoteIdent(rowField)}`, `m.${quoteIdent(table.user)} = ${subjectIdSql(ctx)}`];
|
|
411
|
-
if (roles.length > 0) {
|
|
412
|
-
const roleList = roles.map((role) => role.replaceAll("'", "''")).join(",");
|
|
413
|
-
parts.push(`m.${quoteIdent(table.role)} = any('{${roleList}}')`);
|
|
414
|
-
const via = table.via === void 0 ? "null::text" : `m.${quoteIdent(table.via)}::text`;
|
|
415
|
-
const single = sole(roles);
|
|
416
|
-
const kind = single === void 0 ? kindFilterSql(ctx, `m.${quoteIdent(table.role)}::text`, via) : roleKindSql(ctx, single, via);
|
|
417
|
-
if (kind !== void 0) parts.push(kind);
|
|
418
|
-
}
|
|
419
|
-
if (table.expiresAt !== void 0) parts.push(`(m.${quoteIdent(table.expiresAt)} is null or m.${quoteIdent(table.expiresAt)} > now())`);
|
|
420
|
-
if (tenantColumn !== void 0) parts.push(`m.${quoteIdent(tenantColumn)} = ${tenantClaimSql(ctx)}`);
|
|
421
|
-
parts.push(...activeUserSql(ctx));
|
|
422
|
-
if (scope !== void 0) parts.push(...activeInstancesSql(ctx, scope, (name) => {
|
|
423
|
-
const held = scopeColumn(table, ctx.scopes, name);
|
|
424
|
-
return held === void 0 ? void 0 : `m.${quoteIdent(held)}`;
|
|
425
|
-
}));
|
|
426
|
-
return `exists (select 1 from ${quoteTable(table.table)} m where ${parts.join(" and ")})`;
|
|
427
|
-
}
|
|
428
|
-
function compileMemberOf(condition, ctx) {
|
|
429
|
-
const scope = condition.scope === "resource" ? void 0 : resolveScope(ctx.scopes, condition.scope);
|
|
430
|
-
if (condition.scope !== "resource" && scope === void 0) throw new Error(`PermDock CLI: memberOf ${condition.scope} names a scope the policy does not declare`);
|
|
431
|
-
const mapping = scope === void 0 ? condition.resource === void 0 ? void 0 : ctx.memberships?.resource?.[condition.resource] : scopeMembershipTable(ctx.memberships, ctx.scopes, scope);
|
|
432
|
-
if (mapping !== void 0) {
|
|
433
|
-
const named = scope === void 0 ? void 0 : scopeTable(ctx, scope);
|
|
434
|
-
const rowColumn = scope === void 0 ? mapping.id : named?.column;
|
|
435
|
-
if (rowColumn === void 0) throw new Error(`PermDock CLI: memberships mapping for ${condition.scope} is missing the row column`);
|
|
436
|
-
const tenantColumn = scope === void 0 || scope === rootScope(ctx.scopes) ? void 0 : named?.tenantColumn;
|
|
437
|
-
const primary = existsSql(mapping, rowColumn, condition.field, condition.roles, ctx, tenantColumn, scope);
|
|
438
|
-
if (condition.scope !== "resource" || condition.parents === void 0) return primary;
|
|
439
|
-
return `(${[primary, ...condition.parents.flatMap((parent) => {
|
|
440
|
-
if (typeof parent === "string") return [existsSql(mapping, rowColumn, parent, condition.roles, ctx, tenantColumn)];
|
|
441
|
-
const hopTable = ctx.memberships?.resource?.[parent.resource];
|
|
442
|
-
return hopTable?.id === void 0 ? [] : [existsSql(hopTable, hopTable.id, parent.field, condition.roles, ctx)];
|
|
443
|
-
})].join(" or ")})`;
|
|
444
|
-
}
|
|
445
|
-
if (scope !== void 0 && condition.roles.length === 0 && scopeSources(ctx, scope).length > 0) return `${quoteIdent(condition.field)} in (select ${quoteIdent(ctx.schema ?? "permdock")}.${memberIdsHelper(scope)}())`;
|
|
446
|
-
if (scope !== void 0 && scope === rootScope(ctx.scopes)) {
|
|
447
|
-
const parts = [
|
|
448
|
-
`${quoteIdent(condition.field)} = ${tenantClaimSql(ctx)}`,
|
|
449
|
-
...activeUserSql(ctx),
|
|
450
|
-
...activeInstancesSql(ctx, scope, () => quoteIdent(condition.field))
|
|
451
|
-
];
|
|
452
|
-
return parts.length === 1 ? parts.join("") : `(${parts.join(" and ")})`;
|
|
453
|
-
}
|
|
454
|
-
throw new Error(`PermDock CLI: memberOf ${condition.scope} needs a memberships table mapping`);
|
|
455
|
-
}
|
|
456
|
-
function compileConditionSql(condition, ctx) {
|
|
457
|
-
switch (condition.op) {
|
|
458
|
-
case "eq":
|
|
459
|
-
case "ne":
|
|
460
|
-
case "gt":
|
|
461
|
-
case "gte":
|
|
462
|
-
case "lt":
|
|
463
|
-
case "lte": return compareSql(condition.op, condition.field, sqlValue(condition.value, ctx, condition.field));
|
|
464
|
-
case "contains": {
|
|
465
|
-
const element = ctx.arrayColumns?.[condition.field];
|
|
466
|
-
if (element !== void 0) return `${sqlValue(condition.value, {
|
|
467
|
-
...ctx,
|
|
468
|
-
columnTypes: {
|
|
469
|
-
...ctx.columnTypes,
|
|
470
|
-
[condition.field]: element
|
|
471
|
-
}
|
|
472
|
-
}, condition.field)} = any(${quoteIdent(condition.field)})`;
|
|
473
|
-
return compareSql(condition.op, condition.field, sqlValue(condition.value, ctx, condition.field));
|
|
474
|
-
}
|
|
475
|
-
case "in":
|
|
476
|
-
case "notIn": {
|
|
477
|
-
const keyword = condition.op === "in" ? "in" : "not in";
|
|
478
|
-
if (!Array.isArray(condition.value) && typeof condition.value === "object" && "ref" in condition.value) {
|
|
479
|
-
const path = claimPath(condition.value.ref);
|
|
480
|
-
if (path === void 0) {
|
|
481
|
-
compileRef(condition.value.ref, ctx);
|
|
482
|
-
throw new Error(`PermDock CLI: non-portable ${condition.op} against '${condition.value.ref}'`);
|
|
483
|
-
}
|
|
484
|
-
const column = quoteIdent(condition.field);
|
|
485
|
-
const list = claimArraySql(ctx, path, ctx.columnTypes?.[condition.field]);
|
|
486
|
-
return condition.op === "in" ? `${column} = any (${list})` : `(${column} is not null and not (${column} = any (${list})))`;
|
|
487
|
-
}
|
|
488
|
-
const values = condition.value.map((item) => sqlValue(item, ctx));
|
|
489
|
-
return `${quoteIdent(condition.field)} ${keyword} (${values.join(", ")})`;
|
|
490
|
-
}
|
|
491
|
-
case "isNull": return `${quoteIdent(condition.field)} is ${condition.value ? "" : "not "}null`;
|
|
492
|
-
case "and": return `(${condition.conditions.map((item) => compileConditionSql(item, ctx)).join(" and ")})`;
|
|
493
|
-
case "or":
|
|
494
|
-
if (condition.conditions.length === 0) return "false";
|
|
495
|
-
return `(${condition.conditions.map((item) => compileConditionSql(item, ctx)).join(" or ")})`;
|
|
496
|
-
case "not": return `not (${compileConditionSql(condition.condition, ctx)})`;
|
|
497
|
-
case "memberOf": return compileMemberOf(condition, ctx);
|
|
498
|
-
case "related": return compileRelatedSql(condition, ctx);
|
|
499
|
-
case "sqlFunction":
|
|
500
|
-
if (ctx.inlineFunctions === true) return compileConditionSql(condition.twin, ctx);
|
|
501
|
-
return `${quoteTable(condition.name)}(${condition.args.map((arg) => isSqlFunctionField(arg) ? quoteIdent(arg.field) : sqlValue(arg, ctx)).join(", ")})`;
|
|
502
|
-
case "opaque": return condition.sql;
|
|
503
|
-
default: return condition;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
function parseMembershipsFlag(raw) {
|
|
507
|
-
if (raw === void 0 || raw === "") return;
|
|
508
|
-
const colon = raw.indexOf(":");
|
|
509
|
-
const table = colon === -1 ? raw : raw.slice(0, colon);
|
|
510
|
-
const cols = (colon === -1 ? "" : raw.slice(colon + 1)).split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
511
|
-
return { tenant: {
|
|
512
|
-
table,
|
|
513
|
-
tenant: cols[0] ?? "tenant_id",
|
|
514
|
-
user: cols[1] ?? "user_id",
|
|
515
|
-
role: cols[2] ?? "role",
|
|
516
|
-
...cols[3] === void 0 ? {} : { expiresAt: cols[3] }
|
|
517
|
-
} };
|
|
518
|
-
}
|
|
519
|
-
function andConditions(left, right) {
|
|
520
|
-
if (left === void 0) return right;
|
|
521
|
-
if (right === void 0) return left;
|
|
522
|
-
return {
|
|
523
|
-
op: "and",
|
|
524
|
-
conditions: [left, right]
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
function valueContextRefs(value) {
|
|
528
|
-
if (Array.isArray(value)) return value.flatMap(valueContextRefs);
|
|
529
|
-
if (isRecord(value) && typeof value["ref"] === "string" && (value["ref"] === "context" || value["ref"].startsWith("context."))) return [value["ref"]];
|
|
530
|
-
return [];
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* The `context.*` refs a condition reads. The request context is not in the
|
|
534
|
-
* token, so RLS cannot evaluate them.
|
|
535
|
-
*/
|
|
536
|
-
function contextRefs(condition) {
|
|
537
|
-
if (condition === void 0) return [];
|
|
538
|
-
switch (condition.op) {
|
|
539
|
-
case "eq":
|
|
540
|
-
case "ne":
|
|
541
|
-
case "gt":
|
|
542
|
-
case "gte":
|
|
543
|
-
case "lt":
|
|
544
|
-
case "lte":
|
|
545
|
-
case "contains":
|
|
546
|
-
case "in":
|
|
547
|
-
case "notIn": return valueContextRefs(condition.value);
|
|
548
|
-
case "and":
|
|
549
|
-
case "or": return condition.conditions.flatMap((child) => contextRefs(child));
|
|
550
|
-
case "not": return contextRefs(condition.condition);
|
|
551
|
-
case "sqlFunction": return [...condition.args.flatMap(valueContextRefs), ...contextRefs(condition.twin)];
|
|
552
|
-
case "isNull":
|
|
553
|
-
case "memberOf":
|
|
554
|
-
case "related":
|
|
555
|
-
case "opaque": return [];
|
|
556
|
-
default: return condition;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
function sqlFunctionNames(condition) {
|
|
560
|
-
if (condition === void 0) return [];
|
|
561
|
-
switch (condition.op) {
|
|
562
|
-
case "sqlFunction": return [condition.name, ...sqlFunctionNames(condition.twin)];
|
|
563
|
-
case "and":
|
|
564
|
-
case "or": return condition.conditions.flatMap((child) => sqlFunctionNames(child));
|
|
565
|
-
case "not": return sqlFunctionNames(condition.condition);
|
|
566
|
-
case "eq":
|
|
567
|
-
case "ne":
|
|
568
|
-
case "gt":
|
|
569
|
-
case "gte":
|
|
570
|
-
case "lt":
|
|
571
|
-
case "lte":
|
|
572
|
-
case "contains":
|
|
573
|
-
case "in":
|
|
574
|
-
case "notIn":
|
|
575
|
-
case "isNull":
|
|
576
|
-
case "memberOf":
|
|
577
|
-
case "related":
|
|
578
|
-
case "opaque": return [];
|
|
579
|
-
default: return condition;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
/**
|
|
583
|
-
* A `related` condition as uncorrelated subqueries, so Postgres runs the
|
|
584
|
-
* helper once per statement: the row's field in the closure's descendants of
|
|
585
|
-
* the ids the subject holds the relation on (or those ids alone at depth 0).
|
|
586
|
-
* The helper sits in `array(...)` so it stays an InitPlan; a plain `in` gets
|
|
587
|
-
* pulled into a join that can rescan it per closure row.
|
|
588
|
-
*/
|
|
589
|
-
function compileRelatedSql(condition, ctx) {
|
|
590
|
-
if (condition.hops !== void 0 && condition.hops.length > 0) return compileHoppedSql(condition, ctx);
|
|
591
|
-
const type = ctx.columnTypes?.[condition.field];
|
|
592
|
-
const cast = type === void 0 || type === "text" ? "" : `::${type}`;
|
|
593
|
-
const column = type === void 0 ? `${quoteIdent(condition.field)}::text` : quoteIdent(condition.field);
|
|
594
|
-
const helper = `${`${quoteIdent(ctx.schema ?? "permdock")}.${graphHelper(condition.resource)}`}(${quoteLiteral(condition.relation)})`;
|
|
595
|
-
const cap = ctx.graph?.closures[condition.resource];
|
|
596
|
-
let inner;
|
|
597
|
-
if (condition.depth > 0 && cap !== void 0) {
|
|
598
|
-
const depth = condition.depth < cap ? ` and depth <= ${String(condition.depth)}` : "";
|
|
599
|
-
inner = `select descendant${cast} from ${`${quoteIdent(ctx.schema ?? "permdock")}.${CLOSURE.table}`} where resource = ${quoteLiteral(condition.resource)}${depth} and ancestor = any (array(select ${helper}))`;
|
|
600
|
-
} else inner = cast === "" ? `select ${helper}` : `select p.id${cast} from ${helper} as p(id)`;
|
|
601
|
-
const reach = `${column} in (${inner})`;
|
|
602
|
-
if (condition.parent === true && condition.restricted !== void 0) return `(${reach} and ${quoteIdent(condition.restricted)} is not true)`;
|
|
603
|
-
return reach;
|
|
604
|
-
}
|
|
605
|
-
/**
|
|
606
|
-
* A `related` condition that crosses link hops: the reached instances of the
|
|
607
|
-
* last resource, carried back over each link by its security definer helper,
|
|
608
|
-
* so the hop reads a table the subject may not see.
|
|
609
|
-
*/
|
|
610
|
-
function compileHoppedSql(condition, ctx) {
|
|
611
|
-
const resources = ctx.graph?.resources;
|
|
612
|
-
if (resources === void 0) throw new Error("PermDock CLI: a related condition with link hops needs the policy resources in the RLS context");
|
|
613
|
-
const schema = quoteIdent(ctx.schema ?? "permdock");
|
|
614
|
-
return graphSqlText(relatedSql(condition, {
|
|
615
|
-
resources,
|
|
616
|
-
...ctx.graph?.tables === void 0 ? {} : { tables: ctx.graph.tables },
|
|
617
|
-
closure: `${ctx.schema ?? "permdock"}.${CLOSURE.table}`,
|
|
618
|
-
closureDepths: ctx.graph?.closures ?? {},
|
|
619
|
-
qualify: qualifiedTable,
|
|
620
|
-
holders: (resource, relation) => [{ text: `select ${schema}.${graphHelper(resource)}(${quoteLiteral(relation)})` }],
|
|
621
|
-
linked: (resource, link, targets) => [
|
|
622
|
-
{ text: `select ${schema}.${linkHelper(resource, link)}(array(` },
|
|
623
|
-
...targets,
|
|
624
|
-
{ text: "))" }
|
|
625
|
-
]
|
|
626
|
-
}), ctx);
|
|
627
|
-
}
|
|
628
|
-
//#endregion
|
|
629
|
-
//#region src/cli/global-roles.ts
|
|
630
|
-
function qualify(name, schema) {
|
|
631
|
-
return name.includes(".") ? name : `${schema}.${name}`;
|
|
632
|
-
}
|
|
633
|
-
/** Resolves `source` against `schema`, aliasing the table `alias` and the roles table `<alias>k`. */
|
|
634
|
-
function globalRoleSource(source, schema, alias) {
|
|
635
|
-
const table = qualify(source.table, schema);
|
|
636
|
-
const user = source.user ?? "user_id";
|
|
637
|
-
const userSql = `${alias}.${quoteIdent(user)}`;
|
|
638
|
-
const role = source.role ?? "role";
|
|
639
|
-
if (typeof role === "string") return {
|
|
640
|
-
table,
|
|
641
|
-
user,
|
|
642
|
-
from: `${quoteTable(table)} ${alias}`,
|
|
643
|
-
userSql,
|
|
644
|
-
roleSql: `${alias}.${quoteIdent(role)}::text`
|
|
645
|
-
};
|
|
646
|
-
const pairs = Object.entries(role.on);
|
|
647
|
-
const [pair] = pairs;
|
|
648
|
-
if (pair === void 0 || pairs.length !== 1) throw new Error(`PermDock CLI: roles.role.on must map exactly one column of ${table} to ${role.through}, for example { role_id: 'id' }`);
|
|
649
|
-
const [ref, id] = pair;
|
|
650
|
-
const through = qualify(role.through, table.split(".")[0] ?? schema);
|
|
651
|
-
const keys = `${alias}k`;
|
|
652
|
-
return {
|
|
653
|
-
table,
|
|
654
|
-
user,
|
|
655
|
-
from: `${quoteTable(table)} ${alias}
|
|
656
|
-
join ${quoteTable(through)} ${keys} on ${keys}.${quoteIdent(id)} = ${alias}.${quoteIdent(ref)}`,
|
|
657
|
-
userSql,
|
|
658
|
-
roleSql: `${keys}.${quoteIdent(role.column)}::text`,
|
|
659
|
-
through: {
|
|
660
|
-
table: through,
|
|
661
|
-
id,
|
|
662
|
-
key: role.column,
|
|
663
|
-
ref
|
|
664
|
-
}
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
//#endregion
|
|
668
|
-
//#region src/cli/rls-helpers.ts
|
|
669
|
-
/**
|
|
670
|
-
* The per-statement helpers every generated policy calls: `permdock_has` for
|
|
671
|
-
* global roles and one `permitted_<scope>_ids` per scope
|
|
672
|
-
* (`permittedIdsHelper`). Names are part of the SQL contract.
|
|
673
|
-
*/
|
|
674
|
-
const HELPERS = { has: "permdock_has" };
|
|
675
|
-
/** The helper `--capabilities` adds: ids of one resource a link capability claim reaches. Part of the SQL contract. */
|
|
676
|
-
const CAPABILITIES = { ids: "permdock_capability_ids" };
|
|
677
|
-
/** Objects `--custom-roles` adds next to the helpers. Names are part of the SQL contract. */
|
|
678
|
-
const CUSTOM_ROLES = {
|
|
679
|
-
permissions: "custom_role_permissions",
|
|
680
|
-
includes: "custom_role_includes",
|
|
681
|
-
ceiling: "permdock_ceiling",
|
|
682
|
-
keys: "permdock_custom_keys"
|
|
683
|
-
};
|
|
684
|
-
function helperSchema(ctx) {
|
|
685
|
-
return ctx.schema ?? "permdock";
|
|
686
|
-
}
|
|
687
|
-
function qualified(ctx, name) {
|
|
688
|
-
return `${quoteIdent(helperSchema(ctx))}.${name}`;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* The policy-side call for a grant key. Every form is uncorrelated, so
|
|
692
|
-
* Postgres runs the helper once per statement (an InitPlan, or a hashed
|
|
693
|
-
* SubPlan for `in`), never once per row.
|
|
694
|
-
*/
|
|
695
|
-
function accessSql(ctx, scope, grantKey, column) {
|
|
696
|
-
const key = quoteLiteral(grantKey);
|
|
697
|
-
if (scope === "global") return `(select ${qualified(ctx, HELPERS.has)}(${key}))`;
|
|
698
|
-
if (column === void 0) throw new Error(`PermDock CLI: ${scope}-scoped grant needs definePolicy({ scopes.${scope} })`);
|
|
699
|
-
return `${quoteIdent(column)} in (select ${qualified(ctx, permittedIdsHelper(scope))}(${key}))`;
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* The `anon` branch access for a resource-scoped grant reached through a link
|
|
703
|
-
* capability: the row's `field` holds an id the capability claim names for
|
|
704
|
-
* `resource`, `role` and `permission`. Uncorrelated, so it runs once per
|
|
705
|
-
* statement; the column is compared as text because the claim holds text.
|
|
706
|
-
*/
|
|
707
|
-
function capabilityAccessSql(ctx, field, resource, role, permission) {
|
|
708
|
-
const args = [
|
|
709
|
-
resource,
|
|
710
|
-
role,
|
|
711
|
-
permission
|
|
712
|
-
].map(quoteLiteral).join(", ");
|
|
713
|
-
return `${quoteIdent(field)}::text in (select ${qualified(ctx, CAPABILITIES.ids)}(${args}))`;
|
|
714
|
-
}
|
|
715
|
-
function capabilitiesSql(ctx) {
|
|
716
|
-
if (ctx.capabilities !== true) return "";
|
|
717
|
-
const fn = qualified(ctx, CAPABILITIES.ids);
|
|
718
|
-
return `-- link capabilities: the capability claim exchangeCapability mints (role anon, short-lived)
|
|
719
|
-
create or replace function ${fn}(p_resource text, p_role text, p_permission text)
|
|
720
|
-
returns setof text
|
|
721
|
-
language sql
|
|
722
|
-
stable
|
|
723
|
-
set search_path = ''
|
|
724
|
-
as $$
|
|
725
|
-
select c -> 'on' ->> 'id'
|
|
726
|
-
from (select ${subjectClaimJsonSql(ctx, "capability")} as c) capability
|
|
727
|
-
where jsonb_typeof(c) = 'object'
|
|
728
|
-
and c ->> 'v' = '1'
|
|
729
|
-
and c ->> 'holder' = 'link'
|
|
730
|
-
and c -> 'on' ->> 'resource' = p_resource
|
|
731
|
-
and jsonb_typeof(c -> 'roles') = 'array'
|
|
732
|
-
and c -> 'roles' @> jsonb_build_array(p_role)
|
|
733
|
-
and (c -> 'permissions' is null or c -> 'permissions' @> jsonb_build_array(p_permission))
|
|
734
|
-
and jsonb_typeof(c -> 'expiresAt') = 'number'
|
|
735
|
-
and (c ->> 'expiresAt')::numeric > extract(epoch from now())
|
|
736
|
-
$$;
|
|
737
|
-
revoke execute on function ${fn}(text, text, text) from public;
|
|
738
|
-
grant execute on function ${fn}(text, text, text) to anon, authenticated;`;
|
|
739
|
-
}
|
|
740
|
-
function membershipTable(name) {
|
|
741
|
-
return quoteTable(name.includes(".") ? name : `public.${name}`);
|
|
742
|
-
}
|
|
743
|
-
/** `user` is the SQL for the user id: `auth.uid()` (the default) or the `p_user` parameter of a `_for` helper. */
|
|
744
|
-
function signedIn(ctx, user = subjectIdSql(ctx)) {
|
|
745
|
-
return `coalesce(${user}::text, '') <> ''`;
|
|
746
|
-
}
|
|
747
|
-
function activeTenant(ctx) {
|
|
748
|
-
return `nullif(${subjectClaimSql(ctx, ctx.tenantClaim)}, '')`;
|
|
749
|
-
}
|
|
750
|
-
function rootName(ctx) {
|
|
751
|
-
return ctx.scopes[0]?.name ?? "tenant";
|
|
752
|
-
}
|
|
753
|
-
/** Whether the active tenant narrows memberships of `scope`: the first scope is on its chain. */
|
|
754
|
-
function underRoot(ctx, scope) {
|
|
755
|
-
const root = ctx.scopes[0]?.name;
|
|
756
|
-
let current = ctx.scopes.find((item) => item.name === scope);
|
|
757
|
-
const seen = /* @__PURE__ */ new Set();
|
|
758
|
-
while (current !== void 0 && !seen.has(current.name)) {
|
|
759
|
-
if (current.name === root) return true;
|
|
760
|
-
seen.add(current.name);
|
|
761
|
-
const parent = current.within;
|
|
762
|
-
current = parent === void 0 ? void 0 : ctx.scopes.find((item) => item.name === parent);
|
|
763
|
-
}
|
|
764
|
-
return false;
|
|
765
|
-
}
|
|
766
|
-
/** `and exists (...)` lines for an active user; empty without `rls.suspension.users`. */
|
|
767
|
-
function userActive(ctx, indent, user) {
|
|
768
|
-
return activeUserSql(ctx, user).map((part) => `${indent}and ${part}`);
|
|
769
|
-
}
|
|
770
|
-
/** `and exists (...)` lines for every suspendable instance on the chain of `scope`'s membership. */
|
|
771
|
-
function instancesActive(ctx, scope, idOf, indent) {
|
|
772
|
-
return activeInstancesSql(ctx, scope, idOf).map((part) => `${indent}and ${part}`);
|
|
773
|
-
}
|
|
774
|
-
function roleRows(ctx) {
|
|
775
|
-
const claim = ctx.roleClaim ?? "user_role";
|
|
776
|
-
switch (ctx.dialect) {
|
|
777
|
-
case "guc": return `unnest(string_to_array(nullif(current_setting(${quoteLiteral(`${ctx.gucPrefix}.${claim}`)}, true), ''), ',')) r(role)`;
|
|
778
|
-
case "supabase":
|
|
779
|
-
case "neon": {
|
|
780
|
-
const raw = subjectClaimJsonSql(ctx, claim);
|
|
781
|
-
const value = ctx.dialect === "supabase" ? `coalesce(nullif(${raw}, 'null'::jsonb), (select auth.jwt()) -> 'app_metadata' -> ${quoteLiteral(claim)})` : raw;
|
|
782
|
-
return `jsonb_array_elements_text(
|
|
783
|
-
case jsonb_typeof(${value})
|
|
784
|
-
when 'array' then ${value}
|
|
785
|
-
when 'string' then jsonb_build_array(${value})
|
|
786
|
-
else '[]'::jsonb
|
|
787
|
-
end
|
|
788
|
-
) r(role)`;
|
|
789
|
-
}
|
|
790
|
-
default: return ctx.dialect;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
function membershipRows(ctx) {
|
|
794
|
-
const raw = subjectClaimJsonSql(ctx, "memberships");
|
|
795
|
-
const value = ctx.dialect === "supabase" ? `coalesce(${raw}, (select auth.jwt()) -> 'app_metadata' -> 'memberships')` : raw;
|
|
796
|
-
return `jsonb_array_elements(
|
|
797
|
-
case jsonb_typeof(${value}) when 'array' then ${value} else '[]'::jsonb end
|
|
798
|
-
) m
|
|
799
|
-
cross join lateral jsonb_array_elements_text(
|
|
800
|
-
case jsonb_typeof(m -> 'roles') when 'array' then m -> 'roles' else '[]'::jsonb end
|
|
801
|
-
) r(role)`;
|
|
802
|
-
}
|
|
803
|
-
function andLine(indent, condition) {
|
|
804
|
-
return condition === void 0 ? "" : `\n${indent}and ${condition}`;
|
|
805
|
-
}
|
|
806
|
-
/** `rls.roles`, or the generated `user_roles (user_id, role)`, aliased `ur`. */
|
|
807
|
-
function globalRoleRows(ctx) {
|
|
808
|
-
return ctx.roles === void 0 ? {
|
|
809
|
-
from: `${qualified(ctx, "user_roles")} ur`,
|
|
810
|
-
userSql: "ur.user_id",
|
|
811
|
-
roleSql: "ur.role::text"
|
|
812
|
-
} : globalRoleSource(ctx.roles, "public", "ur");
|
|
813
|
-
}
|
|
814
|
-
function hasBody(ctx) {
|
|
815
|
-
const rp = qualified(ctx, "role_permissions");
|
|
816
|
-
const active = userActive(ctx, " ").map((line) => `\n${line}`).join("");
|
|
817
|
-
const custom = ctx.customRoles;
|
|
818
|
-
if (ctx.authorize === "database") {
|
|
819
|
-
const ur = globalRoleRows(ctx);
|
|
820
|
-
const declared = ` select exists (
|
|
821
|
-
select 1
|
|
822
|
-
from ${ur.from}
|
|
823
|
-
join ${rp} rp on rp.role = ${ur.roleSql}
|
|
824
|
-
where ${ur.userSql} = ${subjectIdSql(ctx)}
|
|
825
|
-
and rp.grant_key = p_grant
|
|
826
|
-
and rp.scope = 'global'${andLine(" ", globalKindFilterSql(ctx, ur.roleSql))}${active}
|
|
827
|
-
)`;
|
|
828
|
-
if (custom === void 0) return declared;
|
|
829
|
-
const match = `c.tenant_id is null and c.scope = 'global' and c.scope_id is null and c.role = ${ur.roleSql}`;
|
|
830
|
-
const rows = (source, value, extra) => `array(select c.${value} from ${qualified(ctx, source)} c where ${match}${extra})`;
|
|
831
|
-
return `${declared}
|
|
832
|
-
or exists (
|
|
833
|
-
select 1
|
|
834
|
-
from ${ur.from}
|
|
835
|
-
where ${ur.userSql} = ${subjectIdSql(ctx)}
|
|
836
|
-
and not (${ur.roleSql} = any(${textArray(custom.declared)}))${active}
|
|
837
|
-
${customKeysSql(ctx, "global", rows(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'allow'"), rows(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), rows(CUSTOM_ROLES.includes, "include_role", "")).trimStart()}
|
|
838
|
-
)`;
|
|
839
|
-
}
|
|
840
|
-
const declared = ` select ${signedIn(ctx)} and exists (
|
|
841
|
-
select 1
|
|
842
|
-
from ${roleRows(ctx)}
|
|
843
|
-
join ${rp} rp on rp.role = r.role
|
|
844
|
-
where rp.grant_key = p_grant
|
|
845
|
-
and rp.scope = 'global'${andLine(" ", globalKindFilterSql(ctx, "r.role"))}${active}
|
|
846
|
-
)`;
|
|
847
|
-
if (custom === void 0) return declared;
|
|
848
|
-
const claim = subjectClaimJsonSql(ctx, GLOBAL_GRANTS_CLAIM);
|
|
849
|
-
return `${declared}
|
|
850
|
-
or (${signedIn(ctx)} and exists (
|
|
851
|
-
select 1
|
|
852
|
-
from ${roleRows(ctx)}
|
|
853
|
-
cross join lateral (select ${claim} -> r.role as g) cg
|
|
854
|
-
where jsonb_typeof(cg.g) = 'array'
|
|
855
|
-
and not (r.role = any(${textArray(custom.declared)}))${active}
|
|
856
|
-
${customKeysSql(ctx, "global", claimEntries("left(e, 1) not in ('-', '@')", "e"), claimEntries("left(e, 1) = '-'", "substr(e, 2)"), claimEntries("left(e, 1) = '@'", "substr(e, 2)")).trimStart()}
|
|
857
|
-
))`;
|
|
858
|
-
}
|
|
859
|
-
/** The claim `jwt` mode reads platform custom roles from: `customRoleClaim(globalRoles)`. */
|
|
860
|
-
const GLOBAL_GRANTS_CLAIM = "role_grants";
|
|
861
|
-
function memberColumn(name) {
|
|
862
|
-
return `m.${quoteIdent(name)}`;
|
|
863
|
-
}
|
|
864
|
-
/** Entries of a compact custom-role claim (`cg.g`) that match `where`. */
|
|
865
|
-
function claimEntries(where, value) {
|
|
866
|
-
return `array(select ${value} from jsonb_array_elements_text(cg.g) e where ${where})`;
|
|
867
|
-
}
|
|
868
|
-
function textArray(values) {
|
|
869
|
-
return values.length === 0 ? `'{}'::text[]` : `array[${values.map(quoteLiteral).join(", ")}]::text[]`;
|
|
870
|
-
}
|
|
871
|
-
/** `p_grant in (select permdock_custom_keys(allows, denies, includes, scope))`. */
|
|
872
|
-
function customKeysSql(ctx, scope, allows, denies, includes) {
|
|
873
|
-
return ` and p_grant in (select ${qualified(ctx, CUSTOM_ROLES.keys)}(
|
|
874
|
-
${allows},
|
|
875
|
-
${denies},
|
|
876
|
-
${includes},
|
|
877
|
-
${quoteLiteral(scope)}
|
|
878
|
-
))`;
|
|
879
|
-
}
|
|
880
|
-
function tableBody(ctx, scope, type) {
|
|
881
|
-
const mapped = scopeTable(ctx, scope);
|
|
882
|
-
if (mapped === void 0) return ` select null::${type} where false -- no ${scope} memberships table configured`;
|
|
883
|
-
const { table, column, tenantColumn } = mapped;
|
|
884
|
-
const filters = [` where ${memberColumn(table.user)} = ${subjectIdSql(ctx)}`];
|
|
885
|
-
if (table.expiresAt !== void 0) {
|
|
886
|
-
const expires = memberColumn(table.expiresAt);
|
|
887
|
-
filters.push(` and (${expires} is null or ${expires} > now())`);
|
|
888
|
-
}
|
|
889
|
-
if (tenantColumn !== void 0) filters.push(` and (${activeTenant(ctx)} is null or ${memberColumn(tenantColumn)}::text = ${activeTenant(ctx)})`);
|
|
890
|
-
filters.push(...userActive(ctx, " "), ...instancesActive(ctx, scope, (name) => {
|
|
891
|
-
const held = scopeColumn(table, ctx.scopes, name);
|
|
892
|
-
return held === void 0 ? void 0 : memberColumn(held);
|
|
893
|
-
}, " "));
|
|
894
|
-
const [owner, ...rest] = filters;
|
|
895
|
-
const kind = kindFilterSql(ctx, `${memberColumn(table.role)}::text`, table.via === void 0 ? "null::text" : `${memberColumn(table.via)}::text`);
|
|
896
|
-
const lines = [
|
|
897
|
-
` select ${memberColumn(column)}::${type}`,
|
|
898
|
-
` from ${membershipTable(table.table)} m`,
|
|
899
|
-
` join ${qualified(ctx, "role_permissions")} rp on rp.role = ${memberColumn(table.role)}::text`,
|
|
900
|
-
owner ?? "",
|
|
901
|
-
" and rp.grant_key = p_grant",
|
|
902
|
-
` and rp.scope = ${quoteLiteral(scope)}`,
|
|
903
|
-
...kind === void 0 ? [] : [` and ${kind}`],
|
|
904
|
-
...rest
|
|
905
|
-
];
|
|
906
|
-
const custom = ctx.customRoles;
|
|
907
|
-
if (custom === void 0) return lines.join("\n");
|
|
908
|
-
const match = [
|
|
909
|
-
`c.tenant_id::text = ${memberColumn(tenantColumn ?? column)}::text`,
|
|
910
|
-
`c.scope = ${quoteLiteral(scope)}`,
|
|
911
|
-
`(c.scope_id is null or c.scope_id = ${memberColumn(column)}::text)`,
|
|
912
|
-
`c.role = ${memberColumn(table.role)}::text`
|
|
913
|
-
].join(" and ");
|
|
914
|
-
const rows = (source, value, extra) => `array(select c.${value} from ${qualified(ctx, source)} c where ${match}${extra})`;
|
|
915
|
-
return [
|
|
916
|
-
...lines,
|
|
917
|
-
" union",
|
|
918
|
-
` select ${memberColumn(column)}::${type}`,
|
|
919
|
-
` from ${membershipTable(table.table)} m`,
|
|
920
|
-
...filters,
|
|
921
|
-
` and not (${memberColumn(table.role)}::text = any(${textArray(custom.declared)}))`,
|
|
922
|
-
customKeysSql(ctx, scope, rows(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'allow'"), rows(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), rows(CUSTOM_ROLES.includes, "include_role", ""))
|
|
923
|
-
].join("\n");
|
|
924
|
-
}
|
|
925
|
-
/** The membership's instance of the first scope, from the canonical claim. */
|
|
926
|
-
function claimTenant(ctx, scope) {
|
|
927
|
-
const root = rootName(ctx);
|
|
928
|
-
return scope === root ? `m ->> 'id'` : `m -> 'within' ->> ${quoteLiteral(root)}`;
|
|
929
|
-
}
|
|
930
|
-
function claimBody(ctx, scope, type) {
|
|
931
|
-
const narrow = underRoot(ctx, scope) ? `
|
|
932
|
-
and (${activeTenant(ctx)} is null or ${claimTenant(ctx, scope)} = ${activeTenant(ctx)})` : "";
|
|
933
|
-
const filters = ` and m ->> 'scope' = ${quoteLiteral(scope)}
|
|
934
|
-
and m ->> 'id' is not null${narrow}
|
|
935
|
-
and case jsonb_typeof(m -> 'expiresAt')
|
|
936
|
-
when 'number' then (m ->> 'expiresAt')::numeric > extract(epoch from now())
|
|
937
|
-
else true
|
|
938
|
-
end${[...userActive(ctx, " "), ...instancesActive(ctx, scope, (name) => name === scope ? `m ->> 'id'` : `m -> 'within' ->> ${quoteLiteral(name)}`, " ")].map((line) => `\n${line}`).join("")}`;
|
|
939
|
-
const declared = ` select (m ->> 'id')::${type}
|
|
940
|
-
from ${membershipRows(ctx)}
|
|
941
|
-
join ${qualified(ctx, "role_permissions")} rp on rp.role = r.role
|
|
942
|
-
where ${signedIn(ctx)}
|
|
943
|
-
and rp.grant_key = p_grant
|
|
944
|
-
and rp.scope = ${quoteLiteral(scope)}${andLine(" ", kindFilterSql(ctx, "r.role", "m ->> 'via'"))}
|
|
945
|
-
${filters}`;
|
|
946
|
-
const custom = ctx.customRoles;
|
|
947
|
-
if (custom === void 0) return declared;
|
|
948
|
-
return `${declared}
|
|
949
|
-
union
|
|
950
|
-
select (m ->> 'id')::${type}
|
|
951
|
-
from ${membershipRows(ctx)}
|
|
952
|
-
cross join lateral (select m -> 'grants' -> r.role as g) cg
|
|
953
|
-
where ${signedIn(ctx)}
|
|
954
|
-
and jsonb_typeof(cg.g) = 'array'
|
|
955
|
-
and not (r.role = any(${textArray(custom.declared)}))
|
|
956
|
-
${filters}
|
|
957
|
-
${customKeysSql(ctx, scope, claimEntries("left(e, 1) not in ('-', '@')", "e"), claimEntries("left(e, 1) = '-'", "substr(e, 2)"), claimEntries("left(e, 1) = '@'", "substr(e, 2)"))}`;
|
|
958
|
-
}
|
|
959
|
-
/**
|
|
960
|
-
* The custom-role objects: the tables (`database` mode), the ceiling view
|
|
961
|
-
* over the `assignable` declared roles, and `permdock_custom_keys`, which
|
|
962
|
-
* resolves one custom role to the grant keys it holds in a scope. A row in
|
|
963
|
-
* the tables can never reach a key outside the view.
|
|
964
|
-
*/
|
|
965
|
-
function customRolesSql(ctx) {
|
|
966
|
-
const custom = ctx.customRoles;
|
|
967
|
-
if (custom === void 0) return "";
|
|
968
|
-
const rp = qualified(ctx, "role_permissions");
|
|
969
|
-
const ceiling = qualified(ctx, CUSTOM_ROLES.ceiling);
|
|
970
|
-
const keys = qualified(ctx, CUSTOM_ROLES.keys);
|
|
971
|
-
const chunks = [];
|
|
972
|
-
if (ctx.authorize === "database") {
|
|
973
|
-
const tenantType = tenantTypeOf(ctx);
|
|
974
|
-
for (const [name, column, check] of [[
|
|
975
|
-
CUSTOM_ROLES.permissions,
|
|
976
|
-
"permission",
|
|
977
|
-
`,
|
|
978
|
-
effect text not null default 'allow' check (effect in ('allow', 'deny'))`
|
|
979
|
-
], [
|
|
980
|
-
CUSTOM_ROLES.includes,
|
|
981
|
-
"include_role",
|
|
982
|
-
""
|
|
983
|
-
]]) {
|
|
984
|
-
const table = qualified(ctx, name);
|
|
985
|
-
const unique = `${column}${name === CUSTOM_ROLES.permissions ? ", effect" : ""}`;
|
|
986
|
-
chunks.push(`create table if not exists ${table} (
|
|
987
|
-
tenant_id ${tenantType},
|
|
988
|
-
scope text not null default ${quoteLiteral(rootName(ctx))},
|
|
989
|
-
scope_id text,
|
|
990
|
-
role text not null,
|
|
991
|
-
${column} text not null${check},
|
|
992
|
-
check ((scope = 'global') = (tenant_id is null)),
|
|
993
|
-
check (scope <> 'global' or scope_id is null)
|
|
994
|
-
);
|
|
995
|
-
create unique index if not exists ${quoteIdent(`${name}_key`)}
|
|
996
|
-
on ${table} (coalesce(tenant_id::text, ''), scope, coalesce(scope_id, ''), role, ${unique});
|
|
997
|
-
alter table ${table} enable row level security;
|
|
998
|
-
revoke all on table ${table} from anon, authenticated, public;`);
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
chunks.push(`-- the ceiling: keys of assignable declared roles, and the denies of those roles on the same permissions
|
|
1002
|
-
create or replace view ${ceiling} with (security_invoker = true) as
|
|
1003
|
-
select rp.scope, rp.role, rp.permission, rp.grant_key, rp.effect
|
|
1004
|
-
from ${rp} rp
|
|
1005
|
-
where rp.role = any(${textArray(custom.assignable)})
|
|
1006
|
-
and exists (
|
|
1007
|
-
select 1 from ${rp} a
|
|
1008
|
-
where a.role = rp.role
|
|
1009
|
-
and a.permission = rp.permission
|
|
1010
|
-
and a.scope = rp.scope
|
|
1011
|
-
and a.effect = 'allow'
|
|
1012
|
-
);
|
|
1013
|
-
revoke all on table ${ceiling} from anon, authenticated, public;`);
|
|
1014
|
-
const rename = renamesSql(Object.entries(custom.renamed ?? {}).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
1015
|
-
chunks.push(`-- one custom role: included ceiling keys and same-scope denies, plus every ceiling key of an allowed permission, minus denied permissions
|
|
1016
|
-
create or replace function ${keys}(p_allow text[], p_deny text[], p_include text[], p_scope text)
|
|
1017
|
-
returns setof text
|
|
1018
|
-
language sql
|
|
1019
|
-
stable
|
|
1020
|
-
set search_path = ''
|
|
1021
|
-
as $$
|
|
1022
|
-
with ${rename.prelude}ceiling as (
|
|
1023
|
-
select c.role, ${rename.permission("c")}, c.grant_key, c.effect
|
|
1024
|
-
from ${ceiling} c${rename.join("c")}
|
|
1025
|
-
where c.scope = p_scope
|
|
1026
|
-
),
|
|
1027
|
-
included as (
|
|
1028
|
-
select rp.role, ${rename.permission("rp")}, rp.grant_key, rp.scope, rp.effect
|
|
1029
|
-
from ${rp} rp${rename.join("rp")}
|
|
1030
|
-
where rp.role = any(coalesce(p_include, '{}'::text[]))
|
|
1031
|
-
),
|
|
1032
|
-
kept as (
|
|
1033
|
-
select i.permission, i.grant_key
|
|
1034
|
-
from included i
|
|
1035
|
-
where i.effect = 'allow'
|
|
1036
|
-
and i.scope = p_scope
|
|
1037
|
-
and exists (
|
|
1038
|
-
select 1 from ceiling c
|
|
1039
|
-
where c.role = i.role and c.grant_key = i.grant_key and c.effect = 'allow'
|
|
1040
|
-
)
|
|
1041
|
-
),
|
|
1042
|
-
wanted as (
|
|
1043
|
-
${rename.allow}
|
|
1044
|
-
union
|
|
1045
|
-
select i.permission
|
|
1046
|
-
from included i
|
|
1047
|
-
where i.effect = 'allow'
|
|
1048
|
-
and not (i.scope = p_scope and exists (
|
|
1049
|
-
select 1 from ceiling c
|
|
1050
|
-
where c.role = i.role and c.grant_key = i.grant_key and c.effect = 'allow'
|
|
1051
|
-
))
|
|
1052
|
-
),
|
|
1053
|
-
allowed as (
|
|
1054
|
-
select w.permission
|
|
1055
|
-
from wanted w
|
|
1056
|
-
where not (${rename.denied("w")})
|
|
1057
|
-
and not exists (select 1 from kept k where k.permission = w.permission)
|
|
1058
|
-
)
|
|
1059
|
-
select k.grant_key from kept k
|
|
1060
|
-
where not (${rename.denied("k")})
|
|
1061
|
-
union
|
|
1062
|
-
select i.grant_key from included i
|
|
1063
|
-
where i.effect = 'deny' and i.scope = p_scope
|
|
1064
|
-
union
|
|
1065
|
-
select c.grant_key from ceiling c
|
|
1066
|
-
where c.permission in (select a.permission from allowed a)
|
|
1067
|
-
$$;
|
|
1068
|
-
revoke execute on function ${keys}(text[], text[], text[], text) from public, anon, authenticated;`);
|
|
1069
|
-
return chunks.join("\n\n");
|
|
1070
|
-
}
|
|
1071
|
-
function currentKeysSql(array) {
|
|
1072
|
-
return `select coalesce(r.key, e.permission) as permission
|
|
1073
|
-
from unnest(coalesce(${array}, '{}'::text[])) as e(permission)
|
|
1074
|
-
left join renamed r on r.former = e.permission`;
|
|
1075
|
-
}
|
|
1076
|
-
/**
|
|
1077
|
-
* SQL fragments that read a stored former key as its current key inside
|
|
1078
|
-
* `permdock_custom_keys`. Without renames they are the plain column and
|
|
1079
|
-
* array reads.
|
|
1080
|
-
*/
|
|
1081
|
-
function renamesSql(renames) {
|
|
1082
|
-
if (renames.length === 0) return {
|
|
1083
|
-
prelude: "",
|
|
1084
|
-
permission: (alias) => `${alias}.permission`,
|
|
1085
|
-
join: () => "",
|
|
1086
|
-
allow: "select unnest(coalesce(p_allow, '{}'::text[])) as permission",
|
|
1087
|
-
denied: (alias) => `${alias}.permission = any(coalesce(p_deny, '{}'::text[]))`
|
|
1088
|
-
};
|
|
1089
|
-
return {
|
|
1090
|
-
prelude: `renamed (former, key) as (values ${renames.map(([former, current]) => `(${quoteLiteral(former)}, ${quoteLiteral(current)})`).join(", ")}),
|
|
1091
|
-
denied as (
|
|
1092
|
-
${currentKeysSql("p_deny")}
|
|
1093
|
-
),
|
|
1094
|
-
`,
|
|
1095
|
-
permission: (alias) => `coalesce(r_${alias}.key, ${alias}.permission) as permission`,
|
|
1096
|
-
join: (alias) => ` left join renamed r_${alias} on r_${alias}.former = ${alias}.permission`,
|
|
1097
|
-
allow: currentKeysSql("p_allow"),
|
|
1098
|
-
denied: (alias) => `${alias}.permission in (select d.permission from denied d)`
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
/**
|
|
1102
|
-
* The rows the membership sources select for the subject, one per instance
|
|
1103
|
-
* (`scope`, `id`, `within`, `roles`, `via`), with each source's own expiry
|
|
1104
|
-
* and suspension filters: the statements the token hook runs.
|
|
1105
|
-
*/
|
|
1106
|
-
function sourceRows(sources) {
|
|
1107
|
-
return sources.map((source, index) => source.sql.select(sourceUser(index)).replaceAll(/^/gmu, " ")).join("\n union all\n");
|
|
1108
|
-
}
|
|
1109
|
-
function sourceUser(index) {
|
|
1110
|
-
return `v_user_${String(index)}`;
|
|
1111
|
-
}
|
|
1112
|
-
function sqlBody(sql) {
|
|
1113
|
-
return {
|
|
1114
|
-
sql,
|
|
1115
|
-
vars: []
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
function sourcesBodyOf(sql, sources, user) {
|
|
1119
|
-
return {
|
|
1120
|
-
sql,
|
|
1121
|
-
vars: sources.map((source, index) => `${sourceUser(index)} ${source.sql.userType} := ${user};`)
|
|
1122
|
-
};
|
|
1123
|
-
}
|
|
1124
|
-
/** `language sql`, or `plpgsql` returning the query when the body declares variables. */
|
|
1125
|
-
function functionBody(body) {
|
|
1126
|
-
if (body.vars.length === 0) return `language sql
|
|
1127
|
-
stable
|
|
1128
|
-
security definer
|
|
1129
|
-
set search_path = ''
|
|
1130
|
-
as $$
|
|
1131
|
-
${body.sql}
|
|
1132
|
-
$$;`;
|
|
1133
|
-
return `language plpgsql
|
|
1134
|
-
stable
|
|
1135
|
-
security definer
|
|
1136
|
-
set search_path = ''
|
|
1137
|
-
as $$
|
|
1138
|
-
declare
|
|
1139
|
-
${body.vars.map((line) => ` ${line}`).join("\n")}
|
|
1140
|
-
begin
|
|
1141
|
-
return query
|
|
1142
|
-
${body.sql};
|
|
1143
|
-
end;
|
|
1144
|
-
$$;`;
|
|
1145
|
-
}
|
|
1146
|
-
/**
|
|
1147
|
-
* `ms.id` for the scope itself and `ms.within ->> name` for an ancestor. The
|
|
1148
|
-
* alias is not `s`, which `activeRowSql` binds inside its `exists`.
|
|
1149
|
-
*/
|
|
1150
|
-
function sourceIdOf(scope) {
|
|
1151
|
-
return (name) => name === scope ? "ms.id" : `ms.within ->> ${quoteLiteral(name)}`;
|
|
1152
|
-
}
|
|
1153
|
-
function sourceFilters(ctx, scope, user = subjectIdSql(ctx)) {
|
|
1154
|
-
return [...userActive(ctx, " ", user), ...instancesActive(ctx, scope, sourceIdOf(scope), " ")].map((line) => `\n${line}`).join("");
|
|
1155
|
-
}
|
|
1156
|
-
function sourcesBody(ctx, scope, type) {
|
|
1157
|
-
if (ctx.customRoles !== void 0) throw new Error(`PermDock CLI: --custom-roles in database mode needs rls.memberships.scopes.${scope}: the membership sources carry no custom roles`);
|
|
1158
|
-
const root = rootName(ctx);
|
|
1159
|
-
const narrow = underRoot(ctx, scope) ? `\n and (${activeTenant(ctx)} is null or ${sourceIdOf(scope)(root)} = ${activeTenant(ctx)})` : "";
|
|
1160
|
-
const sources = scopeSources(ctx, scope);
|
|
1161
|
-
return sourcesBodyOf(` select (ms.id)::${type}
|
|
1162
|
-
from (
|
|
1163
|
-
${sourceRows(sources)}
|
|
1164
|
-
) ms
|
|
1165
|
-
cross join lateral jsonb_array_elements_text(
|
|
1166
|
-
case jsonb_typeof(ms.roles) when 'array' then ms.roles else '[]'::jsonb end
|
|
1167
|
-
) r(role)
|
|
1168
|
-
join ${qualified(ctx, "role_permissions")} rp on rp.role = r.role
|
|
1169
|
-
where ${signedIn(ctx)}
|
|
1170
|
-
and ms.scope = ${quoteLiteral(scope)}
|
|
1171
|
-
and rp.grant_key = p_grant
|
|
1172
|
-
and rp.scope = ${quoteLiteral(scope)}${andLine(" ", kindFilterSql(ctx, "r.role", "ms.via"))}${narrow}${sourceFilters(ctx, scope)}`, sources, subjectIdSql(ctx));
|
|
1173
|
-
}
|
|
1174
|
-
function scopedBody(ctx, scope, type) {
|
|
1175
|
-
if (ctx.authorize !== "database") return sqlBody(claimBody(ctx, scope, type));
|
|
1176
|
-
return scopeSources(ctx, scope).length > 0 ? sourcesBody(ctx, scope, type) : sqlBody(tableBody(ctx, scope, type));
|
|
1177
|
-
}
|
|
1178
|
-
/**
|
|
1179
|
-
* The body of `member_<scope>_ids()`: instances of `scope` the subject holds
|
|
1180
|
-
* a live membership of, with any role. Not narrowed to the active tenant, and
|
|
1181
|
-
* no cascade: a membership of a child scope is not one of its parent.
|
|
1182
|
-
*/
|
|
1183
|
-
function memberBody(ctx, scope, type) {
|
|
1184
|
-
if (ctx.authorize !== "database") return sqlBody(` select distinct (m ->> 'id')::${type}
|
|
1185
|
-
from jsonb_array_elements(
|
|
1186
|
-
case jsonb_typeof(${membershipClaim(ctx)}) when 'array' then ${membershipClaim(ctx)} else '[]'::jsonb end
|
|
1187
|
-
) m
|
|
1188
|
-
where ${signedIn(ctx)}
|
|
1189
|
-
and m ->> 'scope' = ${quoteLiteral(scope)}
|
|
1190
|
-
and m ->> 'id' is not null
|
|
1191
|
-
and jsonb_typeof(m -> 'roles') = 'array'
|
|
1192
|
-
and jsonb_array_length(m -> 'roles') > 0
|
|
1193
|
-
and case jsonb_typeof(m -> 'expiresAt')
|
|
1194
|
-
when 'number' then (m ->> 'expiresAt')::numeric > extract(epoch from now())
|
|
1195
|
-
else true
|
|
1196
|
-
end${[...userActive(ctx, " "), ...instancesActive(ctx, scope, (name) => name === scope ? `m ->> 'id'` : `m -> 'within' ->> ${quoteLiteral(name)}`, " ")].map((line) => `\n${line}`).join("")}`);
|
|
1197
|
-
return memberRowsBody(ctx, scope, type, subjectIdSql(ctx), scopeSources(ctx, scope));
|
|
1198
|
-
}
|
|
1199
|
-
/**
|
|
1200
|
-
* The membership rows of `user` for `scope`, from the sources or else the
|
|
1201
|
-
* mapped table, with the same expiry and suspension filters either way.
|
|
1202
|
-
*/
|
|
1203
|
-
function memberRowsBody(ctx, scope, type, user, sources) {
|
|
1204
|
-
if (sources.length > 0) return sourcesBodyOf(` select distinct (ms.id)::${type}
|
|
1205
|
-
from (
|
|
1206
|
-
${sourceRows(sources)}
|
|
1207
|
-
) ms
|
|
1208
|
-
where ${signedIn(ctx, user)}
|
|
1209
|
-
and ms.scope = ${quoteLiteral(scope)}
|
|
1210
|
-
and jsonb_typeof(ms.roles) = 'array'
|
|
1211
|
-
and jsonb_array_length(ms.roles) > 0${sourceFilters(ctx, scope, user)}`, sources, user);
|
|
1212
|
-
const mapped = scopeTable(ctx, scope);
|
|
1213
|
-
if (mapped === void 0) return sqlBody(` select null::${type} where false -- no ${scope} memberships table configured`);
|
|
1214
|
-
const { table, column } = mapped;
|
|
1215
|
-
const lines = [
|
|
1216
|
-
` select distinct ${memberColumn(column)}::${type}`,
|
|
1217
|
-
` from ${membershipTable(table.table)} m`,
|
|
1218
|
-
` where ${memberColumn(table.user)} = ${user}`,
|
|
1219
|
-
` and ${memberColumn(table.role)} is not null`
|
|
1220
|
-
];
|
|
1221
|
-
if (table.expiresAt !== void 0) {
|
|
1222
|
-
const expires = memberColumn(table.expiresAt);
|
|
1223
|
-
lines.push(` and (${expires} is null or ${expires} > now())`);
|
|
1224
|
-
}
|
|
1225
|
-
lines.push(...userActive(ctx, " ", user), ...instancesActive(ctx, scope, (name) => {
|
|
1226
|
-
const held = scopeColumn(table, ctx.scopes, name);
|
|
1227
|
-
return held === void 0 ? void 0 : memberColumn(held);
|
|
1228
|
-
}, " "));
|
|
1229
|
-
return sqlBody(lines.join("\n"));
|
|
1230
|
-
}
|
|
1231
|
-
function membershipClaim(ctx) {
|
|
1232
|
-
const raw = subjectClaimJsonSql(ctx, "memberships");
|
|
1233
|
-
return ctx.dialect === "supabase" ? `coalesce(${raw}, (select auth.jwt()) -> 'app_metadata' -> 'memberships')` : raw;
|
|
1234
|
-
}
|
|
1235
|
-
function memberFunction(ctx, scope, type, anonExecute) {
|
|
1236
|
-
const fn = qualified(ctx, memberIdsHelper(scope));
|
|
1237
|
-
const grants = anonExecute ? `revoke execute on function ${fn}() from public;
|
|
1238
|
-
grant execute on function ${fn}() to anon, authenticated;` : `revoke execute on function ${fn}() from public, anon;
|
|
1239
|
-
grant execute on function ${fn}() to authenticated;`;
|
|
1240
|
-
return `create or replace function ${fn}()
|
|
1241
|
-
returns setof ${type}
|
|
1242
|
-
${functionBody(memberBody(ctx, scope, type))}
|
|
1243
|
-
${grants}`;
|
|
1244
|
-
}
|
|
1245
|
-
/**
|
|
1246
|
-
* `member_<scope>_ids_for(p_user uuid)`: `member_<scope>_ids()` for a user the
|
|
1247
|
-
* caller names, read from the membership sources (or the mapped table) in
|
|
1248
|
-
* either mode, so a token hook can call it before any claim exists. Naming
|
|
1249
|
-
* the user is why no client role may execute it; the hook's grants give
|
|
1250
|
-
* `supabase_auth_admin` execute.
|
|
1251
|
-
*/
|
|
1252
|
-
function memberForFunction(ctx, scope, type) {
|
|
1253
|
-
const fn = qualified(ctx, memberForHelper(scope));
|
|
1254
|
-
return `create or replace function ${fn}(p_user uuid)
|
|
1255
|
-
returns setof ${type}
|
|
1256
|
-
${functionBody(memberRowsBody(ctx, scope, type, "p_user", memberForSources(ctx, scope)))}
|
|
1257
|
-
revoke execute on function ${fn}(uuid) from public, anon, authenticated;`;
|
|
1258
|
-
}
|
|
1259
|
-
function helperFunction(ctx, name, returns, body, anonExecute) {
|
|
1260
|
-
const fn = qualified(ctx, name);
|
|
1261
|
-
const grants = anonExecute ? `revoke execute on function ${fn}(text) from public;
|
|
1262
|
-
grant execute on function ${fn}(text) to anon, authenticated;` : `revoke execute on function ${fn}(text) from public, anon;
|
|
1263
|
-
grant execute on function ${fn}(text) to authenticated;`;
|
|
1264
|
-
return `create or replace function ${fn}(p_grant text)
|
|
1265
|
-
returns ${returns}
|
|
1266
|
-
${functionBody(body)}
|
|
1267
|
-
${grants}`;
|
|
1268
|
-
}
|
|
1269
|
-
function userIdType(ctx) {
|
|
1270
|
-
return ctx.dialect === "supabase" ? "uuid not null references auth.users on delete cascade" : "text not null";
|
|
1271
|
-
}
|
|
1272
|
-
/**
|
|
1273
|
-
* The `role_permissions` rows the policy compiles to: an upsert and a delete
|
|
1274
|
-
* of every other row. Data, not schema, so `supabase db diff` drops it from a
|
|
1275
|
-
* declarative schema; `--split ...,seeds` writes it as its own migration.
|
|
1276
|
-
*/
|
|
1277
|
-
function seedSql(ctx, rows) {
|
|
1278
|
-
const table = qualified(ctx, "role_permissions");
|
|
1279
|
-
if (rows.length === 0) return `delete from ${table};`;
|
|
1280
|
-
return `insert into ${table} (role, permission, grant_key, scope, effect) values
|
|
1281
|
-
${rows.map((row) => ` (${[
|
|
1282
|
-
row.role,
|
|
1283
|
-
row.permission,
|
|
1284
|
-
row.grantKey,
|
|
1285
|
-
row.scope,
|
|
1286
|
-
row.effect
|
|
1287
|
-
].map(quoteLiteral).join(", ")})`).join(",\n")}
|
|
1288
|
-
on conflict (role, grant_key, scope) do update
|
|
1289
|
-
set permission = excluded.permission, effect = excluded.effect;
|
|
1290
|
-
delete from ${table}
|
|
1291
|
-
where (role, grant_key, scope) not in (values
|
|
1292
|
-
${rows.map((row) => ` (${[
|
|
1293
|
-
row.role,
|
|
1294
|
-
row.grantKey,
|
|
1295
|
-
row.scope
|
|
1296
|
-
].map(quoteLiteral).join(", ")})`).join(",\n")}
|
|
1297
|
-
);`;
|
|
1298
|
-
}
|
|
1299
|
-
/**
|
|
1300
|
-
* `role_permissions`, its seeds, and the three `security definer` helpers
|
|
1301
|
-
* with `search_path = ''`. `execute` goes to `authenticated` only.
|
|
1302
|
-
*/
|
|
1303
|
-
function helpersSql(ctx, rows, options) {
|
|
1304
|
-
const schema = helperSchema(ctx);
|
|
1305
|
-
const s = quoteIdent(schema);
|
|
1306
|
-
const rp = qualified(ctx, "role_permissions");
|
|
1307
|
-
const chunks = [`-- permdock helpers (${ctx.authorize === "database" ? "database: reads the membership and user_roles tables" : "jwt: reads the role and memberships claims"})
|
|
1308
|
-
-- policies call them uncorrelated, so Postgres evaluates each once per statement`];
|
|
1309
|
-
if (schema !== "public") chunks.push(`create schema if not exists ${s};\nrevoke all on schema ${s} from public;\ngrant usage on schema ${s} to ${options.anonExecute === true ? "anon, authenticated" : "authenticated"};`);
|
|
1310
|
-
chunks.push(`create table if not exists ${rp} (
|
|
1311
|
-
role text not null,
|
|
1312
|
-
permission text not null,
|
|
1313
|
-
grant_key text not null,
|
|
1314
|
-
scope text not null check (scope ~ '^[a-z][a-z0-9_]*$'),
|
|
1315
|
-
effect text not null default 'allow' check (effect in ('allow', 'deny')),
|
|
1316
|
-
primary key (role, grant_key, scope)
|
|
1317
|
-
);
|
|
1318
|
-
alter table ${rp} enable row level security;
|
|
1319
|
-
revoke all on table ${rp} from anon, authenticated, public;`);
|
|
1320
|
-
if (options.withoutSeeds !== true) chunks.push(seedSql(ctx, rows));
|
|
1321
|
-
if (ctx.authorize === "database" && options.userRoles && ctx.roles === void 0) {
|
|
1322
|
-
const ur = qualified(ctx, "user_roles");
|
|
1323
|
-
chunks.push(`create table if not exists ${ur} (
|
|
1324
|
-
user_id ${userIdType(ctx)},
|
|
1325
|
-
role text not null,
|
|
1326
|
-
primary key (user_id, role)
|
|
1327
|
-
);
|
|
1328
|
-
alter table ${ur} enable row level security;
|
|
1329
|
-
revoke all on table ${ur} from anon, authenticated, public;`);
|
|
1330
|
-
}
|
|
1331
|
-
const custom = customRolesSql(ctx);
|
|
1332
|
-
if (custom !== "") chunks.push(custom);
|
|
1333
|
-
const anon = options.anonExecute === true;
|
|
1334
|
-
chunks.push(helperFunction(ctx, HELPERS.has, "boolean", sqlBody(hasBody(ctx)), anon));
|
|
1335
|
-
const capabilities = capabilitiesSql(ctx);
|
|
1336
|
-
if (capabilities !== "") chunks.push(capabilities);
|
|
1337
|
-
for (const scope of ctx.scopes) {
|
|
1338
|
-
const type = scopeTypeOf(ctx, scope.name);
|
|
1339
|
-
chunks.push(helperFunction(ctx, permittedIdsHelper(scope.name), `setof ${type}`, scopedBody(ctx, scope.name, type), anon));
|
|
1340
|
-
chunks.push(memberFunction(ctx, scope.name, type, anon));
|
|
1341
|
-
if (hasMemberFor(ctx, scope.name)) chunks.push(memberForFunction(ctx, scope.name, type));
|
|
1342
|
-
}
|
|
1343
|
-
return `${chunks.join("\n\n")}\n`;
|
|
1344
|
-
}
|
|
1345
|
-
//#endregion
|
|
1346
|
-
export { memberForSources as A, scopeSources as B, globalKindFilterSql as C, kindFilterSql as D, hasMemberFor as E, qualifiedTable as F, subjectClaimSql as G, scopeTypeOf as H, quoteIdent as I, subjectIdSql as K, quoteLiteral as L, memberIdsHelper as M, parseMembershipsFlag as N, linkHelper as O, permittedIdsHelper as P, quoteTable as R, contextRefs as S, graphSqlText as T, sqlFunctionNames as U, scopeTable as V, subjectClaimJsonSql as W, andConditions as _, helpersSql as a, columnTypesOf as b, membershipTable as c, seedSql as d, signedIn as f, activeUserSql as g, activeRowSql as h, globalRoleRows as i, memberForTable as j, memberForHelper as k, qualified as l, CLOSURE as m, accessSql as n, memberColumn as o, globalRoleSource as p, capabilityAccessSql as r, membershipRows as s, HELPERS as t, roleRows as u, arrayColumnsOf as v, graphHelper as w, compileConditionSql as x, checkSuspension as y, roleKindSql as z };
|