permdock 0.6.0 → 0.8.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 +2 -3
- package/dist/{2020-C9VN-x8c.js → 2020-CJbO8Bvz.js} +1 -3
- package/dist/a2a/index.d.ts +7 -7
- package/dist/a2a/index.js +9 -9
- package/dist/action-result-CL-QCITA.d.ts +19 -0
- package/dist/{adapter-context-CRhqyzCk.js → adapter-context-CIQpQxbE.js} +8 -2
- package/dist/{adoption-scan-DCYe60Pi.js → adoption-scan-BLs67P1l.js} +5 -5
- package/dist/agent/index.d.ts +3 -3
- package/dist/agent/index.js +1 -1
- package/dist/ai-sdk/index.d.ts +31 -10
- package/dist/ai-sdk/index.js +22 -12
- package/dist/{app-data-MuyzVqX8.js → app-data-UNNm07jV.js} +10 -7
- package/dist/{approval-header-CJACtHkv.js → approval-header-8QV6MU4H.js} +6 -3
- package/dist/{approval-prompt-Dg5LeAcT.js → approval-prompt-B8hQY69x.js} +5 -6
- package/dist/{approval-prompt-DvSSPQJX.d.ts → approval-prompt-PYagyQUj.d.ts} +13 -11
- package/dist/approvals/index.d.ts +5 -5
- package/dist/approvals/index.js +7 -7
- package/dist/{approvals-p1PGoVjb.js → approvals-DnIBTuXE.js} +128 -78
- package/dist/{approvers-CqPOLD-o.js → approvers-B4r0N_Uz.js} +8 -2
- package/dist/{arazzo-41WuzHou.js → arazzo-3RhGfkAK.js} +2 -2
- package/dist/{arazzo-CaUVBn7D.js → arazzo-B5-ulWif.js} +6 -6
- package/dist/{args-IIolxxKA.js → args-0FrVs5EH.js} +125 -8
- package/dist/astro/index.d.ts +30 -12
- package/dist/astro/index.js +2 -2
- package/dist/{audience-DabClFLQ.js → audience-BAkBDMqn.js} +8 -8
- package/dist/{auth-DRLEizbL.js → auth--yPyZNgr.js} +21 -10
- package/dist/authzen/index.d.ts +171 -25
- package/dist/authzen/index.js +321 -149
- package/dist/{automation-BNM3TtDo.d.ts → automation-DqEoWHBc.d.ts} +42 -32
- package/dist/{automation-DVjOckjV.js → automation-x3KEUusw.js} +22 -21
- package/dist/base-url-BQbddZO5.js +9 -0
- package/dist/better-auth/index.d.ts +2 -2
- package/dist/better-auth/index.js +13 -7
- package/dist/better-supabase/index.d.ts +392 -220
- package/dist/better-supabase/index.js +293 -152
- package/dist/{bind-DOyrv6zi.d.ts → bind-Cp2hBHzP.d.ts} +27 -12
- package/dist/{bind-CAvgUuKs.js → bind-D_sZSVA3.js} +22 -10
- package/dist/{body-CH5of8Ne.js → body-CJQwYVa8.js} +3 -3
- package/dist/{body-text-QcXok_SI.js → body-text-DQhcmqdY.js} +8 -4
- package/dist/{boundary-BM8a_P6E.js → boundary-BV3O1eud.js} +8 -2
- package/dist/{boundary-BmHxVH1J.d.ts → boundary-C9ck-3uK.d.ts} +13 -4
- package/dist/{budget-NbEtbJC_.js → budget-BELja5A9.js} +6 -3
- package/dist/{budget-JqcRqyhK.d.ts → budget-hYsPz_yE.d.ts} +6 -3
- package/dist/{build-instance-Bu_eDqYw.js → build-instance-CNWaYJyr.js} +3 -3
- package/dist/{canonical-json-BC8_nMTw.js → canonical-json-DwilZ1iB.js} +6 -3
- package/dist/{capability-N1K9IpqS.js → capability-BqX94r07.js} +31 -25
- package/dist/{capability-Drg-1YhL.d.ts → capability-ChuiONQU.d.ts} +41 -29
- package/dist/{casbin-Dyu2gnUD.js → casbin-H0rKxBx2.js} +16 -8
- package/dist/{casbin-Cr7RLtA3.js → casbin-cbSbhT_k.js} +2 -2
- package/dist/{casl-64gpT-8x.js → casl-C4OINZHI.js} +2 -2
- package/dist/{casl-DjEDhTCP.js → casl-CHHoCldP.js} +19 -10
- package/dist/catalog/index.d.ts +104 -23
- package/dist/catalog/index.js +117 -8
- package/dist/{catalog-HD2gf5mY.js → catalog-BFpz2EmT.js} +57 -26
- package/dist/{catalog-DZVbrF-z.js → catalog-DCspHDc8.js} +3 -3
- package/dist/{catalog-doc-Cdn7-0pQ.js → catalog-doc-DjMKzJp9.js} +53 -16
- package/dist/{catalog-fingerprint-CCP0-yQS.js → catalog-fingerprint-e6aCWosn.js} +19 -15
- package/dist/catalog-mRQQQhNK.js +1 -0
- package/dist/catalog-operations-Dadrl7Z-.js +60 -0
- package/dist/{create-CQWIpUG-.js → challenge-DImNcR7B.js} +157 -39
- package/dist/chat/index.d.ts +1 -1
- package/dist/chat/index.js +13 -10
- package/dist/chat-sdk/index.d.ts +44 -26
- package/dist/chat-sdk/index.js +17 -17
- package/dist/checks-CGkP3Z7A.js +40 -0
- package/dist/{claims-map-7oQ3ue4X.js → claims-map-CKkoAAYF.js} +5 -2
- package/dist/{claims-map-Ca-QX8C6.d.ts → claims-map-D1Pb5oRq.d.ts} +8 -5
- package/dist/claude-agent/index.d.ts +6 -6
- package/dist/claude-agent/index.js +6 -3
- package/dist/clerk/index.d.ts +11 -7
- package/dist/clerk/index.js +6 -6
- package/dist/cli/bin.js +4 -4
- package/dist/cli/index.d.ts +82 -7
- package/dist/cli/index.js +6 -6
- package/dist/{clients-C7zLFuWT.d.ts → clients-C_NOET3z.d.ts} +5 -5
- package/dist/{clients-DjU5ABMy.js → clients-DYr3U86j.js} +3 -3
- package/dist/cloud/index.d.ts +186 -38
- package/dist/cloud/index.js +293 -31
- package/dist/{cloud-ytQ3uviA.js → cloud-BFlGMaak.js} +5 -6
- package/dist/{codemod-5mpSpnUY.js → codemod-DLQ9Zpo4.js} +3 -4
- package/dist/{codemod-ua2hHy7w.js → codemod-DWSN2erG.js} +523 -201
- package/dist/collect-B5BL3klN.js +1 -0
- package/dist/{collect-mLTY4nwb.js → collect-B7ZFAJU3.js} +36 -64
- package/dist/{collect-Ci6akVwH.js → collect-Bzv3le0A.js} +6 -6
- package/dist/{compare-pfyVnBjE.js → compare-DTfyUunF.js} +2 -2
- package/dist/compile/index.d.ts +2 -2
- package/dist/compile/index.js +1 -1
- package/dist/{compile-1bsAezL0.d.ts → compile-BFzysW3o.d.ts} +36 -12
- package/dist/{compile-CMqMx_IN.js → compile-EzEMB-7v.js} +77 -33
- package/dist/{completion-BTFrHZhX.js → completion-saoYiRFO.js} +17 -8
- package/dist/config-BwPBfN0t.js +108 -0
- package/dist/{config-Bj2Ixq3d.js → config-DBeXXmBF.js} +2 -2
- package/dist/{config-report-DSpsLpCE.js → config-report-BipSWA6V.js} +9 -6
- package/dist/config-shape-ykV0Vm2n.js +134 -0
- package/dist/{connection-DjhIJSs2.js → connection-B72dg5Q2.js} +23 -22
- package/dist/connector-schema-CYLQNMYq.js +434 -0
- package/dist/{context-B_u3_bDJ.js → context-BnIzxTNE.js} +4 -3
- package/dist/convex/index.d.ts +13 -7
- package/dist/convex/index.js +9 -6
- package/dist/core-H-mbhpZc.js +341 -0
- package/dist/core-hWIARPDz.d.ts +206 -0
- package/dist/{create-CBMmFie-.d.ts → create-BSNrVuxh.d.ts} +87 -34
- package/dist/{create-B30OMUOJ.d.ts → create-BVq1lNQE.d.ts} +49 -28
- package/dist/{create-D_NmPOdH.d.ts → create-CM1FciYG.d.ts} +37 -29
- package/dist/{create-CGm28gyI.js → create-Cq4Uox0f.js} +13 -9
- package/dist/{create-DCF2SvoT.js → create-_noSrY1T.js} +15 -21
- package/dist/{credential-BcoiejpY.js → credential-CyyyvCsL.js} +42 -39
- package/dist/{credentials-DPyHmYz5.js → credentials-CdDxp7jN.js} +6 -6
- package/dist/{deciding-columns-Sq12mEA6.js → deciding-columns-NB4wcyBf.js} +13 -2
- package/dist/{delegation-j6-rc37o.js → delegation-CDB6_keF.js} +31 -22
- package/dist/delivery/index.d.ts +5 -5
- package/dist/delivery/index.js +6 -5
- package/dist/{delivery-DCAgNyCn.js → delivery-BupfvJIV.js} +81 -20
- package/dist/{delivery-event-xX4obB8F.js → delivery-event-JioK5YbS.js} +6 -6
- package/dist/denied-text-CHNlVInN.d.ts +22 -0
- package/dist/{describe-BybMRY7A.js → describe-Bb4hGK5d.js} +6 -5
- package/dist/{describe-B5qgDvEW.d.ts → describe-ByS9LgHK.d.ts} +5 -5
- package/dist/{device-BARQ8eYG.js → device-BwbJNmb2.js} +11 -8
- package/dist/{diff-CmuSD3vc.js → diff-CN8IYhC_.js} +2 -2
- package/dist/{diff-3jbx6Rm4.js → diff-iZWV65nC.js} +23 -14
- package/dist/{digest-B3kc_PgQ.d.ts → digest-8Yx2CchM.d.ts} +3 -3
- package/dist/{doctor-BdZw53-k.js → doctor-CHD1M9R5.js} +485 -141
- package/dist/{doctor-3ZqNHX_G.js → doctor-D3wo-yuz.js} +4 -4
- package/dist/drizzle/index.d.ts +33 -19
- package/dist/drizzle/index.js +17 -9
- package/dist/elysia/index.d.ts +17 -11
- package/dist/elysia/index.js +5 -5
- package/dist/{errors-BKy0EfhN.js → errors-B6R_lg2a.js} +13 -4
- package/dist/{errors-BWXBMaKK.d.ts → errors-CHwoRRpB.d.ts} +4 -1
- package/dist/{errors-CWTY6uih.js → errors-Vpu6rKHl.js} +14 -10
- package/dist/{errors-DH5QEGXh.d.ts → errors-zi_4T-rx.d.ts} +22 -6
- package/dist/{evaluate-CNClBvcj.js → evaluate-Dd8Ta0Ie.js} +185 -80
- package/dist/{evaluations-beAooV0n.js → evaluations-IBceAeKT.js} +41 -20
- package/dist/{evaluations-C_1vdSZc.d.ts → evaluations-z8dQFDaN.d.ts} +12 -6
- package/dist/eve/index.d.ts +9 -6
- package/dist/eve/index.js +4 -4
- package/dist/{event-time-CsegxXxB.js → event-time-BGy48400.js} +8 -4
- package/dist/{expiry-D4_YKRKD.js → expiry-6XByK0dm.js} +8 -5
- package/dist/express/index.d.ts +9 -6
- package/dist/express/index.js +9 -6
- package/dist/fastify/index.d.ts +16 -10
- package/dist/fastify/index.js +8 -5
- package/dist/{fetch-adapter-Dw0MTS3D.d.ts → fetch-adapter-C1DCvpqV.d.ts} +9 -6
- package/dist/{fetch-adapter-DD_gfmrC.js → fetch-adapter-JL43jD3E.js} +14 -11
- package/dist/fields-D0vUI6dq.js +498 -0
- package/dist/{files-C5ZI27eV.js → files-B3AA05f1.js} +21 -5
- package/dist/{files-D_JIzUrP.js → files-BSjrtIHw.js} +11 -4
- package/dist/{fixtures-B7XrLeY7.js → fixtures-A1_sp_tR.js} +15 -6
- package/dist/{flags-C3DktBDY.js → flags-DXjoTVbS.js} +2 -2
- package/dist/{freeze-CrU8nQeX.js → freeze-jjan0lCv.js} +4 -1
- package/dist/{from-snapshot-CTCMsFZk.js → from-snapshot-DCakWRsr.js} +26 -16
- package/dist/gen-B5kvbAoZ.js +58 -0
- package/dist/gen-DWCYcMQM.js +827 -0
- package/dist/generate-CRVV5V7l.js +165 -0
- package/dist/generate-CezbCaTI.js +275 -0
- package/dist/{grantee-hxrUsbpM.js → grantee-Dke-9rd1.js} +4 -4
- package/dist/{grantee-conditions-CcXf8hMM.js → grantee-conditions-BrYPe-w0.js} +12 -12
- package/dist/{grantee-match-DbENBHLB.js → grantee-match-7RZZvoT_.js} +12 -6
- package/dist/{graph-sql-B-dsAT6j.d.ts → graph-sql-BwFC4Qjs.d.ts} +3 -3
- package/dist/h3/index.d.ts +10 -7
- package/dist/h3/index.js +3 -3
- package/dist/headers-B9jQxjtW.d.ts +10 -0
- package/dist/{helpers-CBYVMjwh.js → helpers-DJicaG6f.js} +40 -28
- package/dist/hono/index.d.ts +16 -10
- package/dist/hono/index.js +5 -5
- package/dist/{hosted-BPk51-kX.d.ts → hosted-Cmtxogd0.d.ts} +44 -20
- package/dist/{hosted-B46-5LYE.js → hosted-D0IzESu_.js} +68 -25
- package/dist/{http-ujKm0JXE.js → http-DH94eh6f.js} +4 -4
- package/dist/{http-BBoKefeS.js → http-DKLXrFGk.js} +13 -7
- package/dist/{http-DXJ37RDA.js → http-DOL33cO0.js} +11 -8
- package/dist/{import-wyqXUyjm.js → import-CST57XLp.js} +64 -51
- package/dist/{import-CNV7I2Dw.js → import-_YqhhwvX.js} +6 -6
- package/dist/{index-B8YjkEZ4.d.ts → index-B8s204GL.d.ts} +64 -52
- package/dist/{index-Ch8LpNDp.d.ts → index-BT5-1VsX.d.ts} +64 -25
- package/dist/{index-DuWxc5dt.d.ts → index-BW_0MZOS.d.ts} +388 -149
- package/dist/index-BhxEUY9J.d.ts +233 -0
- package/dist/index-Bv5iXPqU.d.ts +210 -0
- package/dist/{index-C-6Letxg.d.ts → index-_9UiQIkt.d.ts} +164 -77
- package/dist/index.d.ts +22 -21
- package/dist/index.js +35 -33
- package/dist/{init-GG6Jh8HD.js → init-Dz_PsHdv.js} +5 -5
- package/dist/{init-DeMYsnYJ.js → init-cE3A8aXq.js} +10 -10
- package/dist/{instance-options-BsVpyGIk.d.ts → instance-options-B0KXqTrJ.d.ts} +4 -4
- package/dist/{instance-snapshot-BOEhcqIF.js → instance-snapshot--JWiTrvO.js} +81 -56
- package/dist/{instant-BluqDvyS.js → instant-DK3Iz83e.js} +2 -2
- package/dist/{internals-Dk8A8fX3.js → internals-B3IgHrvo.js} +6 -3
- package/dist/{json-schema-B-vAwlde.js → json-schema-BPGa-RjQ.js} +9 -3
- package/dist/{json-schema-CN_iLFH2.d.ts → json-schema-BfXgnRNT.d.ts} +4 -4
- package/dist/jwt/index.d.ts +125 -17
- package/dist/jwt/index.js +157 -16
- package/dist/{kernel-D0NVb1cK.js → kernel-C-fYydV2.js} +151 -16
- package/dist/kysely/index.d.ts +49 -33
- package/dist/kysely/index.js +9 -7
- package/dist/{live-options-BaAFCIiu.js → live-options-BBIT68le.js} +2 -2
- package/dist/{load-DFS_ry0W.js → load-DjSU5KXO.js} +6 -6
- package/dist/load-Dm0pSYH3.js +1 -0
- package/dist/{login-Dfe5RlYk.js → login-D-8tMqzo.js} +4 -4
- package/dist/{login-imhuwvn3.js → login-DplQ0s84.js} +18 -15
- package/dist/{manifest-dvL2XEe1.js → manifest-Cgiq5oqc.js} +67 -37
- package/dist/{manifest-mx5ghSFM.d.ts → manifest-CzzWtnAb.d.ts} +144 -28
- package/dist/{manifest-schema-C0kK_8V0.js → manifest-schema-2r37XRhD.js} +80 -9
- package/dist/{manifest-C8IzW3Do.js → manifest-wg8WkkpD.js} +11 -7
- package/dist/{map-claims-D3oRmTk0.js → map-claims-BNWCJtmP.js} +27 -18
- package/dist/{map-error-Dr70oA_3.js → map-error-pasIGqMT.js} +7 -7
- package/dist/{markers-C6joDI3o.js → markers-BeHZ4x9Q.js} +20 -5
- package/dist/mcp/index.d.ts +154 -47
- package/dist/mcp/index.js +200 -41
- package/dist/{migrate-CIq4-E9F.js → migrate-B7oP_sQU.js} +102 -31
- package/dist/module-values-BJTBdo6y.js +107 -0
- package/dist/nest/index.d.ts +23 -13
- package/dist/nest/index.js +18 -11
- package/dist/next/client.d.ts +5 -2
- package/dist/next/client.js +1 -1
- package/dist/next/index.d.ts +72 -26
- package/dist/next/index.js +110 -6
- package/dist/next/plugin.d.ts +1 -1
- package/dist/next/plugin.js +4 -3
- package/dist/node/index.d.ts +5 -5
- package/dist/node/index.js +3 -3
- package/dist/{ocsf-BFp-PZZA.js → ocsf-TcjRBgIs.js} +15 -5
- package/dist/openai/index.d.ts +13 -7
- package/dist/openai/index.js +3 -3
- package/dist/openapi/index.d.ts +61 -30
- package/dist/openapi/index.js +2 -2
- package/dist/{openapi-B7HZ0f-r.js → openapi-BOsx0xn9.js} +34 -24
- package/dist/{openapi-C4dDTVEd.js → openapi-CBZUhA3l.js} +4 -5
- package/dist/{openapi-DD1SV-5-.js → openapi-VeHHU2l9.js} +23 -14
- package/dist/{operation-findings-DgATDpvh.js → operation-findings-CXtFzlKJ.js} +3 -3
- package/dist/{operations-X0VM7F3m.d.ts → operations-Ci_rT6Uo.d.ts} +10 -7
- package/dist/orpc/index.d.ts +68 -14
- package/dist/orpc/index.js +62 -13
- package/dist/otel/index.d.ts +7 -4
- package/dist/otel/index.js +8 -5
- package/dist/output-files-BHOIlrfD.js +1 -0
- package/dist/{output-files-DhRBBE4n.js → output-files-BIQ_9pIs.js} +29 -17
- package/dist/{output-1im__-08.js → output-tkju6U7R.js} +15 -7
- package/dist/ownership-B4L59T9u.js +1054 -0
- package/dist/parse-1_ZheTW5.js +151 -0
- package/dist/parse-local-manifest-BQhTlLQS.d.ts +10 -0
- package/dist/parse-local-manifest-DEiCF4ah.js +69 -0
- package/dist/{parse-request-D_tUXG6M.js → parse-request-CwCZpyjV.js} +7 -8
- package/dist/{paths-DXSHAMIa.js → paths-CUvURzHp.js} +4 -1
- package/dist/pdp/index.d.ts +9 -6
- package/dist/pdp/index.js +23 -12
- package/dist/{peer-CRvC7Hg1.js → peer-C9LvBCcb.js} +1 -1
- package/dist/peer-Q0fuZ6hx.js +982 -0
- package/dist/{permdock-SV8ZINYT.d.ts → permdock-B6Sf4T_S.d.ts} +270 -117
- package/dist/{permdock-BrAscV28.js → permdock-L-_I_3zP.js} +226 -74
- package/dist/{permission-registry-Br4jSNVi.js → permission-registry-B8tYxPIt.js} +24 -11
- package/dist/{vocabulary-CN8H_9UF.d.ts → permission-types-CAY-GU7e.d.ts} +130 -104
- package/dist/{permissions-u_KHg-sN.d.ts → permissions-Bx6UI8n1.d.ts} +6 -2
- package/dist/{permissions-B-wCP5ZC.js → permissions-CVXmRILq.js} +3 -3
- package/dist/{permissions-BPxlwVhN.js → permissions-mDcTWoBs.js} +8 -5
- package/dist/pg-DQTqRrou.js +60 -0
- package/dist/{pins-8ILTftC9.js → pins-nqarl3oE.js} +4 -1
- package/dist/pkce-CnmmYqUZ.js +21 -0
- package/dist/{policy-77FSAq_V.d.ts → policy-Ba4bDhaj.d.ts} +5 -5
- package/dist/{policy-DbM5YYtV.js → policy-CLQ8__GF.js} +49 -20
- package/dist/{policy-from-catalog-C2gMa2Dj.js → policy-from-catalog-CJgOm6aq.js} +28 -25
- package/dist/{policy-module-CTlwMvLI.js → policy-module-xym9fnSq.js} +12 -6
- package/dist/{policy-types-n0uRTsW8.d.ts → policy-types-CCiDRZBH.d.ts} +802 -334
- package/dist/postgres-store-DcyrPS-r.js +1028 -0
- package/dist/{postgrest-DAjUkF9w.js → postgrest-D4nuPXIQ.js} +88 -36
- package/dist/{postgrest-CEgRbefE.d.ts → postgrest-DicQyqm-.d.ts} +60 -30
- package/dist/powersync-DHNQX06W.js +1117 -0
- package/dist/{powersync-DISt6SUe.js → powersync-DXqGuq9A.js} +3 -3
- package/dist/powersync-Djf-zCpB.js +1 -0
- package/dist/preset/index.d.ts +38 -28
- package/dist/preset/index.js +1 -1
- package/dist/{preset-DZdU0tne.js → preset-BjLzeFpp.js} +58 -34
- package/dist/{principal-source-pHneukrx.js → principal-source-C6N60GtM.js} +10 -10
- package/dist/prisma/index.d.ts +69 -41
- package/dist/prisma/index.js +21 -17
- package/dist/{problem-CTviyH5L.js → problem-BVLHFK33.js} +41 -21
- package/dist/{problem-details-cn5X30gZ.js → problem-details-CLDgX1KV.js} +13 -4
- package/dist/{problem-types-CJ2325SZ.js → problem-types-DSsRfONQ.js} +4 -1
- package/dist/prompt-BYc6drYW.js +938 -0
- package/dist/protected-action-BE7N_Luo.d.ts +58 -0
- package/dist/protected-action-DUYqb-Ma.js +80 -0
- package/dist/{protected-BlF0F6wL.js → protected-action-QEA8arRB.js} +48 -14
- package/dist/{provider-B1al-AZz.js → provider-C55OGXJ7.js} +5 -4
- package/dist/{provider-DILL9oPK.d.ts → provider-DXfq5c2d.d.ts} +1 -1
- package/dist/{provider-store-DM6DNcta.js → provider-store-CaTv00tr.js} +13 -9
- package/dist/pull-B65ndFNZ.js +1 -0
- package/dist/{pull-jEwbf80J.js → pull-CwpGaQSi.js} +12 -11
- package/dist/{push-BD4f15co.js → push-DgssgzkK.js} +62 -16
- package/dist/{pushed-policy-DCW91KQU.d.ts → pushed-policy-D1Eoi6NM.d.ts} +12 -6
- package/dist/{pushed-policy-CqtxkV4G.js → pushed-policy-ITwYKvvH.js} +10 -7
- package/dist/react/index.d.ts +9 -8
- package/dist/react/index.js +7 -6
- package/dist/react/provider-client.d.ts +1 -1
- package/dist/react/provider-client.js +1 -1
- package/dist/react-native/index.d.ts +104 -51
- package/dist/react-native/index.js +394 -67
- package/dist/{refs-K6eWUJ0H.js → refs-C8BKxc_e.js} +17 -3
- package/dist/{relation-walk-3OyDBF0S.js → relation-walk-DK_gkgL7.js} +12 -9
- package/dist/{relations-BnCWnJGI.js → relations-BxvRvDHK.js} +3 -3
- package/dist/{replay-Du1tJjG7.js → replay-BU09ad4L.js} +1 -1
- package/dist/{replay-Bkm-Ne4l.d.ts → replay-DNftIb_R.d.ts} +1 -1
- package/dist/{request-context-C81Pip9n.js → request-context-C25ilqiJ.js} +8 -8
- package/dist/{request-schema-CMreVuFM.js → request-schema-DhX7UbfQ.js} +4 -1
- package/dist/resend/index.d.ts +34 -28
- package/dist/resend/index.js +29 -29
- package/dist/resolve-names-CF7xXLSG.js +201 -0
- package/dist/{resource-relations-D_jw0BFx.js → resource-relations-8-pCScBO.js} +18 -9
- package/dist/{retire-CALjIQ7e.js → retire-B2xffQgY.js} +15 -6
- package/dist/{revocations-R07lFBnX.d.ts → revocations-BaUXHM1N.d.ts} +6 -3
- package/dist/{rls-DTvYd-lI.js → rls-CNEH2_BG.js} +743 -624
- package/dist/{rls-BAqP4q66.js → rls-Ca8Jz9b5.js} +41 -10
- package/dist/{role-builder-uWQZ4v84.js → role-builder-BK2-T6oD.js} +41 -20
- package/dist/rolldown-runtime-C3I60J0A.js +4 -0
- package/dist/{row-conditions-h2HythXQ.js → row-conditions-BAGHOmVq.js} +4 -3
- package/dist/{rpc-VSl5ylaQ.js → rpc-fNqAIqQk.js} +8 -5
- package/dist/{run-CUKuz0A8.js → run-Bz26OTfO.js} +26 -23
- package/dist/run-D-JE8QWb.js +1 -0
- package/dist/{run-ticket-DocwaTKn.d.ts → run-ticket-DbWI9f99.d.ts} +33 -13
- package/dist/{run-ticket-sxFI6LAk.js → run-ticket-Dv7aF7Dk.js} +29 -8
- package/dist/{runtime-D3VTAiCA.js → runtime-BGnrOkVH.js} +5 -2
- package/dist/{saas-BCXRuXca.js → saas-iWnUdwCt.js} +42 -29
- package/dist/{scan-qvOe0hS3.js → scan-DCj1KQfn.js} +1 -1
- package/dist/{schema-B4v0Qrac.js → schema-BOJjqlWL.js} +3 -2
- package/dist/{schema-yLuBWWsh.js → schema-Dst9I9Gs.js} +9 -5
- package/dist/scim/index.d.ts +3 -2
- package/dist/scim/index.js +3 -1
- package/dist/scim-D8YupzX1.js +44 -0
- package/dist/{scim-ClHtpyV3.js → scim-jJBJ7g1Z.js} +35 -551
- package/dist/{scopes-CUPSjo9q.js → scopes-0Up_UA2t.js} +42 -24
- package/dist/server/index.d.ts +12 -12
- package/dist/server/index.js +13 -13
- package/dist/{server-C3WXAHZj.js → server-3cVlTLde.js} +31 -31
- package/dist/{server-types-CKQiCnON.d.ts → server-types-CIlpSA2G.d.ts} +81 -46
- package/dist/{sha256-D9ihqmvd.js → sha256-C-4WXKIu.js} +4 -3
- package/dist/{shared-BAK8t4Uo.js → shared-Afctg7B0.js} +100 -61
- package/dist/{shared-DvvFzYXe.js → shared-l0oGGetg.js} +9 -6
- package/dist/{signals-D3NcTKUS.js → signals-B1vHR3fN.js} +2 -2
- package/dist/{sink-BEZSlMS_.js → sink-D9_vh372.js} +32 -21
- package/dist/{sink-4-eQIRWf.d.ts → sink-Dvi7YZG_.d.ts} +44 -20
- package/dist/{skills-B6lVdD_W.js → skills-Bjj7BNEQ.js} +16 -12
- package/dist/{skills-CTYwkl44.js → skills-OuoMR49L.js} +3 -3
- package/dist/{snapshot-B7DkYg6g.js → snapshot-CXlzi69u.js} +21 -18
- package/dist/{snapshot-cache-PXI-vGpY.d.ts → snapshot-cache-BAXZ0KOp.d.ts} +21 -12
- package/dist/{snapshot-cache-DZHQ0KF6.js → snapshot-cache-Bx_4xC_6.js} +14 -11
- package/dist/solid/index.d.ts +55 -15
- package/dist/solid/index.js +46 -12
- package/dist/solid-start/index.d.ts +30 -12
- package/dist/solid-start/index.js +3 -3
- package/dist/sources-B1Os_YUu.d.ts +312 -0
- package/dist/{sources-vs5dGOsS.js → sources-CMAM-2A3.js} +156 -51
- package/dist/sources-qoIJQrJv.js +11 -0
- package/dist/{sql-B5T1IL83.js → sql-0WjxIp9g.js} +32 -7
- package/dist/sql-B80DFdpG.d.ts +10 -0
- package/dist/{src-Cu2m75uo.js → src-DZ0l9UAg.js} +305 -123
- package/dist/ssf/index.d.ts +4 -4
- package/dist/ssf/index.js +665 -224
- package/dist/{standard-webhooks-rbD7LS6h.d.ts → standard-webhooks-C-qX4mJW.d.ts} +7 -7
- package/dist/{standard-webhooks-DgFFD-Ga.js → standard-webhooks-D67a3nzk.js} +5 -5
- package/dist/{store-BDutJkDL.js → store-RNFvaJJv.js} +43 -29
- package/dist/{store-options-u0ycEe8s.js → store-options-Dil3lraY.js} +105 -15
- package/dist/stores-C0Kta5Ft.d.ts +59 -0
- package/dist/{stores-CGhM8BN_.js → stores-DxZWDXCs.js} +42 -5
- package/dist/{stream-CvdUS9V1.js → stream-CDvF0sJm.js} +5 -5
- package/dist/{stream-CeCMJmBB.d.ts → stream-ltuLdtTG.d.ts} +4 -3
- package/dist/{style-DiKWnAvn.js → style-DblOtSga.js} +1 -1
- package/dist/{style-Uao76wO7.js → style-Ds11Hmk4.js} +3 -1
- package/dist/{subject-settings-CkEwrw3W.js → subject-settings-8sH5HlII.js} +8 -4
- package/dist/{row-check-Dt35czLy.d.ts → subject-settings-vNW8qmUH.d.ts} +30 -18
- package/dist/supabase/index.d.ts +48 -37
- package/dist/supabase/index.js +6 -6
- package/dist/supabase/middleware.d.ts +19 -13
- package/dist/supabase/middleware.js +7 -7
- package/dist/{supabase-x5JtR2tc.js → supabase-BL1qJzzY.js} +82 -36
- package/dist/{supabase-By_frk3U.js → supabase-D39cuGPt.js} +3 -3
- package/dist/{supabase-hook-BMoEhZir.js → supabase-hook-B-yXnzUl.js} +1206 -286
- package/dist/supabase-hook-H-MtE9Wd.js +1 -0
- package/dist/supautils-BU-pAEpf.js +808 -0
- package/dist/{support-session-C6xJQYut.js → support-session-BsmPCbz8.js} +22 -22
- package/dist/svelte/PermissionBoundary.svelte +2 -6
- package/dist/svelte/Protected.svelte +2 -8
- package/dist/svelte/index.d.ts +9 -8
- package/dist/svelte/index.js +10 -9
- package/dist/svelte/runtime.d.ts +4 -4
- package/dist/svelte/runtime.js +4 -4
- package/dist/svelte/source.d.ts +9 -8
- package/dist/svelte/source.js +6 -5
- package/dist/sveltekit/index.d.ts +30 -12
- package/dist/sveltekit/index.js +2 -2
- package/dist/{ownership-DqaJNJPn.js → tables-DeMNEy6V.js} +1025 -1085
- package/dist/tanstack-start/index.d.ts +22 -10
- package/dist/tanstack-start/index.js +2 -2
- package/dist/{target-Bw37TFNA.js → target-qdEAnnhU.js} +6 -6
- package/dist/{tenancy-DN2QAx9z.js → tenancy-5d1zVBvK.js} +36 -21
- package/dist/terminal/index.d.ts +23 -14
- package/dist/terminal/index.js +17 -20
- package/dist/testing/core.d.ts +2 -0
- package/dist/testing/core.js +1 -0
- package/dist/testing/fixtures/index.d.ts +15 -0
- package/dist/testing/fixtures/index.js +831 -0
- package/dist/testing/index.d.ts +235 -285
- package/dist/testing/index.js +502 -927
- package/dist/testing/saas/index.d.ts +43 -27
- 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/text-diff-BtfSmdUo.js +444 -0
- package/dist/{thenable-yD7SabqG.js → thenable-BNSM1U2R.js} +12 -3
- package/dist/{time-DkWjdL98.js → time-nR1JesoH.js} +32 -8
- package/dist/{token-BLZcjQgt.js → token-DC9EQiet.js} +15 -13
- package/dist/{tool-refusal-CMLN5xhn.js → tool-refusal-Qpjs_W6M.js} +5 -2
- package/dist/trpc/index.d.ts +11 -8
- package/dist/trpc/index.js +15 -9
- package/dist/type-helpers-BIPIDa6a.d.ts +50 -0
- package/dist/{types-8RnwUAvE.d.ts → types-AMJ5qoU_.d.ts} +49 -30
- package/dist/{types-C_IiEs64.d.ts → types-B2mlGuhW.d.ts} +14 -11
- package/dist/{types-_WhUP7ud.d.ts → types-B4uqKDnp.d.ts} +25 -12
- package/dist/{types-CX7AvRqP.d.ts → types-BJk0BKRX.d.ts} +24 -14
- package/dist/types-B_d_g7T3.d.ts +262 -0
- package/dist/{types-BdtSDbBO.d.ts → types-B_tiYvk8.d.ts} +59 -32
- package/dist/{types-DqzsnlSA.js → types-CcUXciSj.js} +17 -5
- package/dist/{types-Dfv0aTER.d.ts → types-DC1Loeh3.d.ts} +449 -168
- package/dist/{types-lUlG3TVd.d.ts → types-DHKPcqzr.d.ts} +2 -2
- package/dist/{types-C791HFu9.d.ts → types-DjuMBNF9.d.ts} +68 -32
- package/dist/{types-C863OQiR.d.ts → types-HzqOtvsJ.d.ts} +23 -12
- package/dist/{types-Druu9wLM.d.ts → types-ZzRGoViD.d.ts} +77 -24
- package/dist/{types-BzzaHxt3.d.ts → types-ah9NlU6j.d.ts} +137 -48
- package/dist/{types-CVZSNocA.d.ts → types-djVzLlIV.d.ts} +31 -14
- package/dist/{types-BtHtcKMr.d.ts → types-yUuPR5Hy.d.ts} +47 -24
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +2 -2
- package/dist/usage-CYeXek0p.js +1 -0
- package/dist/{usage-SveTRTyr.js → usage-CxEzS4ey.js} +50 -26
- package/dist/{usage-CRVl9V3t.js → usage-ab0PdusX.js} +2 -2
- package/dist/{validation-error-ZfUHbKvf.js → validation-error-CYg2kJEw.js} +4 -1
- package/dist/{validation-error-DHUrAkku.d.ts → validation-error-YGg2mXyH.d.ts} +5 -2
- package/dist/{validity-CZ7-y0Cj.js → validity-Buc7w8E9.js} +13 -10
- package/dist/{verifier-DHuh-okk.js → verifier-DUV6pLqZ.js} +10 -4
- package/dist/{verify-BaAKx7t6.js → verify-DZeqoWu5.js} +417 -489
- package/dist/version-DMQ0dnN5.js +48 -0
- package/dist/version-DizhmD72.js +1 -0
- package/dist/{views-DTIIO-oQ.js → views-CD8wuL4Z.js} +19 -6
- package/dist/vocabulary-DEJ5j4mR.d.ts +70 -0
- package/dist/vue/index.d.ts +71 -21
- package/dist/vue/index.js +50 -13
- package/dist/{waiter-B986w1oK.d.ts → waiter-Bu9zAlE0.d.ts} +13 -10
- package/dist/{waiter-BGUtGYoU.js → waiter-aBLM63_f.js} +1 -1
- package/dist/{watch-vea54PRw.js → watch-Cx_WciLE.js} +8 -8
- package/dist/watch-DB42U1ib.js +1 -0
- package/dist/webmcp/index.d.ts +11 -11
- package/dist/webmcp/index.js +8 -5
- package/dist/{where-scope-C9vfEphl.js → where-scope-DlCdCM_8.js} +56 -30
- package/dist/{who-can-DWBAcaCQ.js → who-can-rI7dCR9Q.js} +23 -20
- package/dist/workflow/index.d.ts +3 -3
- package/dist/workflow/index.js +13 -13
- package/dist/workflow-sdk/index.d.ts +19 -13
- package/dist/workflow-sdk/index.js +6 -6
- package/dist/{wrap-gX61EtpB.js → wrap-B2wziJI_.js} +11 -5
- package/dist/{wrap-C5jC6sKI.d.ts → wrap-DlJE3gS_.d.ts} +8 -5
- package/package.json +25 -14
- package/schemas/catalog-v1.json +3 -0
- package/schemas/cli-result-v1.json +1 -0
- package/schemas/cloud-api-v1.json +152 -7
- package/schemas/connector-v1.json +8 -0
- package/schemas/supabase-claims-v1.json +5 -0
- package/schemas/supabase-manifest-v1.json +308 -75
- package/skills/permdock/SKILL.md +1 -0
- package/skills/permdock-agents/references/runtimes.md +3 -1
- package/skills/permdock-approvals/references/stores-and-resume.md +2 -1
- package/skills/permdock-credentials/references/tokens-keys-links.md +8 -6
- package/skills/permdock-data/references/queries-and-rls.md +6 -2
- package/skills/permdock-tenancy/SKILL.md +1 -1
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +2 -1
- package/skills/permdock-wire/references/adapters.md +28 -22
- package/skills/permdock-wire/references/policy-options.md +1 -0
- package/dist/catalog-BoWglDPI.js +0 -1
- package/dist/collect-D7eQDffE.js +0 -1
- package/dist/config-bs_kZeYq.js +0 -162
- package/dist/denied-text-CiFQNuMV.d.ts +0 -19
- package/dist/fields-OYG_JR78.js +0 -203
- package/dist/generate-CNU1pIOo.js +0 -171
- package/dist/headers-BIjfMaoC.js +0 -8
- package/dist/headers-CIbDTUTK.d.ts +0 -7
- package/dist/index-Cp_ilKi1.d.ts +0 -91
- package/dist/index-CueFnxQc.d.ts +0 -110
- package/dist/load-BxEtwlBS.js +0 -1
- package/dist/output-files-B3IZJksC.js +0 -1
- package/dist/package-root-Dk_ETdkp.js +0 -31
- package/dist/parse-Cxq0WxrA.js +0 -53
- package/dist/parse-local-manifest-5ODwqu8b.d.ts +0 -10
- package/dist/parse-local-manifest-CVSs6J9I.js +0 -34
- package/dist/peer-C8swJ2GH.js +0 -50
- package/dist/policies-DP2nqRoF.js +0 -295
- package/dist/powersync-BkWWCS1K.js +0 -538
- package/dist/powersync-MMJ2mI2q.js +0 -1
- package/dist/problem-details-B45V5d2b.d.ts +0 -7
- package/dist/prompt-DfZLJLIn.js +0 -32
- package/dist/protected-RFGpakN9.d.ts +0 -38
- package/dist/pull-CvpINcMK.js +0 -1
- package/dist/run-DDwUaJDU.js +0 -1
- package/dist/sources-Xc4zkszI.d.ts +0 -190
- package/dist/stores-BUGNbJTt.d.ts +0 -30
- package/dist/supabase-hook-CCw5m35C.js +0 -1
- package/dist/supautils-B6HYL9eL.js +0 -91
- package/dist/text-diff-BQcUapOb.js +0 -15
- package/dist/types-B3_ooZgZ.d.ts +0 -172
- package/dist/usage-CZpSQP6L.js +0 -1
- package/dist/version-CEbQ5UeM.js +0 -18
- package/dist/version-DXkYUISx.js +0 -1
- package/dist/watch--ovlPOc0.js +0 -1
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { l as
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { i as
|
|
11
|
-
import {
|
|
12
|
-
import { i as
|
|
13
|
-
import { d as keptKeys, l as roleColumn, u as roleManifest } from "./sources-vs5dGOsS.js";
|
|
14
|
-
import { A as declaredArray, D as sqlType, E as scopeTypeOf, F as quoteQualified, I as quoteTable, L as textArrayLiteral, M as qualified, N as quoteIdent, O as tenantTypeOf, P as quoteLiteral, S as CLAIM, _ as subjectClaimJsonSql, b as tenantClaimSql, d as keyTenantSql, f as kindFilterSql, h as roleKindSql, i as allowEntry, j as helperSchema, k as CLI, m as memberVia, n as GLOBAL_ROLES, o as rootName, p as liveSessionSql, r as HELPERS, s as signedIn, t as CUSTOM_ROLES, u as globalKindFilterSql, v as subjectClaimSql, x as userIdHelperSql, y as subjectIdSql } from "./shared-BAK8t4Uo.js";
|
|
1
|
+
import { c as listPermissions, h as formerKeys } from "./permission-registry-B8tYxPIt.js";
|
|
2
|
+
import { a as expandRelation, c as isEdgeRelation, d as isSelfParented, l as isFieldRelation, o as groupEntries, p as restrictedFor, u as isPrincipalRelation } from "./resource-relations-8-pCScBO.js";
|
|
3
|
+
import { f as scopeList, l as rootScope, s as resolveScope, u as scopeChain } from "./scopes-0Up_UA2t.js";
|
|
4
|
+
import { o as flattenGrantee } from "./grantee-Dke-9rd1.js";
|
|
5
|
+
import { r as relationCondition, t as inheritCondition } from "./grantee-conditions-BrYPe-w0.js";
|
|
6
|
+
import { l as partitionsOf, p as scopeField } from "./tenancy-5d1zVBvK.js";
|
|
7
|
+
import { a as delegatedScopeMap } from "./permdock-L-_I_3zP.js";
|
|
8
|
+
import { i as configError } from "./errors-Vpu6rKHl.js";
|
|
9
|
+
import { d as sqlTextArray, i as qualifySqlTable, t as SQL_IDENT } from "./sql-0WjxIp9g.js";
|
|
10
|
+
import { a as scopeMembershipTable, d as restrictedRowsSql, i as scopeColumn, l as relationArmSql } from "./compile-EzEMB-7v.js";
|
|
11
|
+
import { d as keptKeys, l as roleColumn, u as roleManifest } from "./sources-CMAM-2A3.js";
|
|
12
|
+
import { A as quoteQualified, D as qualified, E as helperSchema, I as scopeTypeOf, O as quoteIdent, R as tenantTypeOf, _ as subjectClaimJsonSql, d as keyTenantSql, f as kindFilterSql, h as roleKindSql, i as allowEntry, j as quoteTable, k as quoteLiteral, m as memberVia, n as GLOBAL_ROLES, o as rootName, r as HELPERS, s as signedIn, t as CUSTOM_ROLES, u as globalKindFilterSql, v as subjectClaimSql, w as CLI, x as userIdHelperSql, y as subjectIdSql } from "./shared-Afctg7B0.js";
|
|
15
13
|
//#region src/cli/sql/active.ts
|
|
16
14
|
function checkActiveRow(label, row) {
|
|
17
15
|
quoteTable(row.table);
|
|
@@ -84,16 +82,19 @@ function activeUserSql(ctx, user = subjectIdSql(ctx)) {
|
|
|
84
82
|
const row = ctx.suspension?.users;
|
|
85
83
|
return row === void 0 ? [] : [activeRowSql(row, user)];
|
|
86
84
|
}
|
|
87
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* The permission of a grant key held in `grant`: `key#group` and `key@level`
|
|
87
|
+
* name `key`.
|
|
88
|
+
*/
|
|
88
89
|
function grantPermissionSql(grant) {
|
|
89
90
|
return { sql: `split_part(split_part(${grant}, '#', 1), '@', 1)` };
|
|
90
91
|
}
|
|
91
92
|
/**
|
|
92
|
-
* The active-instance check for every suspendable scope on `scope`'s chain:
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
93
|
+
* The active-instance check for every suspendable scope on `scope`'s chain: its
|
|
94
|
+
* own and each ancestor's. `idOf` gives the SQL for the id the membership holds
|
|
95
|
+
* for a scope on that chain. A scope's `keep` lets `permission` through a
|
|
96
|
+
* suspended instance: a kept key known now drops its check, and one read at run
|
|
97
|
+
* time is compared with the kept keys.
|
|
97
98
|
*/
|
|
98
99
|
function activeInstancesSql(ctx, scope, idOf, permission = ctx.permission === void 0 ? void 0 : { key: ctx.permission }) {
|
|
99
100
|
const parts = [];
|
|
@@ -115,8 +116,8 @@ function activeInstancesSql(ctx, scope, idOf, permission = ctx.permission === vo
|
|
|
115
116
|
const CLOSURE = { table: "permdock_closure" };
|
|
116
117
|
/**
|
|
117
118
|
* The snake_case SQL name of a resource or link (`chatThread` becomes
|
|
118
|
-
* `chat_thread`), or `undefined` when it is not made of ASCII letters,
|
|
119
|
-
*
|
|
119
|
+
* `chat_thread`), or `undefined` when it is not made of ASCII letters, digits
|
|
120
|
+
* and underscores starting with a letter.
|
|
120
121
|
*/
|
|
121
122
|
function graphSqlName(name) {
|
|
122
123
|
if (!/^[A-Za-z][A-Za-z0-9_]*$/u.test(name)) return;
|
|
@@ -127,21 +128,30 @@ function sqlNameOf(name, label) {
|
|
|
127
128
|
if (sql === void 0) throw configError(`PermDock CLI: ${label} is not a SQL name (letters, digits and underscores, starting with a letter), so rls generate cannot name its helper`);
|
|
128
129
|
return sql;
|
|
129
130
|
}
|
|
130
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* The helper returning the ids of `resource` the subject holds a relation on:
|
|
133
|
+
* `permitted_<snake_case resource>_ids`.
|
|
134
|
+
*/
|
|
131
135
|
function graphHelper(resource) {
|
|
132
136
|
return permittedIdsHelper(sqlNameOf(resource, `resource '${resource}'`));
|
|
133
137
|
}
|
|
134
138
|
function inheritedRowsHelper(resource) {
|
|
135
139
|
return `permitted_${sqlNameOf(resource, `resource '${resource}'`)}_rows`;
|
|
136
140
|
}
|
|
137
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* The helper returning the ids of `resource` whose `link` points into the ids
|
|
143
|
+
* it is given.
|
|
144
|
+
*/
|
|
138
145
|
function linkHelper(resource, link) {
|
|
139
146
|
return `permdock_link_${sqlNameOf(resource, `resource '${resource}'`)}_${sqlNameOf(link, `link '${link}' on ${resource}`)}`;
|
|
140
147
|
}
|
|
141
148
|
function restrictedHelper(resource) {
|
|
142
149
|
return `permdock_restricted_${sqlNameOf(resource, `resource '${resource}'`)}`;
|
|
143
150
|
}
|
|
144
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Graph SQL parts as RLS text: values inline as literals, the subject from the
|
|
153
|
+
* dialect's claim.
|
|
154
|
+
*/
|
|
145
155
|
function graphSqlText(parts, ctx) {
|
|
146
156
|
return parts.map((part) => {
|
|
147
157
|
if ("text" in part) return part.text;
|
|
@@ -168,23 +178,51 @@ function permittedIdsHelper(name) {
|
|
|
168
178
|
if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw configError(`PermDock CLI: unsafe scope name '${name}'`);
|
|
169
179
|
return `permitted_${name}_ids`;
|
|
170
180
|
}
|
|
171
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* `permitted_<scope>_ids_for(p_user, p_grant)`: the same ids for a user the
|
|
183
|
+
* caller names.
|
|
184
|
+
*/
|
|
172
185
|
function permittedForHelper(name) {
|
|
173
186
|
return `${permittedIdsHelper(name)}_for`;
|
|
174
187
|
}
|
|
175
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* The helper returning the ids of scope `name` the subject holds any live
|
|
190
|
+
* membership of.
|
|
191
|
+
*/
|
|
176
192
|
function memberIdsHelper(name) {
|
|
177
193
|
if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw configError(`PermDock CLI: unsafe scope name '${name}'`);
|
|
178
194
|
return `member_${name}_ids`;
|
|
179
195
|
}
|
|
180
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* `member_<scope>_ids_for(p_user uuid)`: the same rows for a user the caller
|
|
198
|
+
* names; the token hook's helper.
|
|
199
|
+
*/
|
|
181
200
|
function memberForHelper(name) {
|
|
182
201
|
return `${memberIdsHelper(name)}_for`;
|
|
183
202
|
}
|
|
184
203
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
204
|
+
* `member_<scope>_ids_of(p_user uuid)`: `member_<scope>_ids_for(p_user)` when
|
|
205
|
+
* `p_user` is the caller and no rows otherwise, so `authenticated` may execute
|
|
206
|
+
* it; better-supabase's `teamIdsOf` template calls it.
|
|
207
|
+
*/
|
|
208
|
+
function memberOfHelper(name) {
|
|
209
|
+
return `${memberIdsHelper(name)}_of`;
|
|
210
|
+
}
|
|
211
|
+
/** A scope better-supabase reads as its teams: `team` or `teams` (doctor BS417). */
|
|
212
|
+
function isTeamScope(name) {
|
|
213
|
+
return /^teams?$/u.test(name);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Whether `member_<scope>_ids_of` is generated: a team scope with
|
|
217
|
+
* `member_<scope>_ids_for`.
|
|
218
|
+
*/
|
|
219
|
+
function hasMemberOf(input, name) {
|
|
220
|
+
return isTeamScope(name) && hasMemberFor(input, name);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Whether `member_<scope>_ids_for` is generated for `name`: Supabase only (its
|
|
224
|
+
* one caller is `supabase_auth_admin`), and only where `memberships` maps a
|
|
225
|
+
* table for the scope or a membership source can hold it.
|
|
188
226
|
*/
|
|
189
227
|
function hasMemberFor(input, name) {
|
|
190
228
|
if (input.dialect !== "supabase") return false;
|
|
@@ -199,26 +237,36 @@ function hasPermittedUsers(input, name) {
|
|
|
199
237
|
const sources = memberForSources(input, name);
|
|
200
238
|
return sources.length > 0 && sources.every((source) => source.sql.holders !== void 0);
|
|
201
239
|
}
|
|
202
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* The table `member_<scope>_ids_for` reads for scope `name`, when `memberships`
|
|
242
|
+
* maps one with its id column.
|
|
243
|
+
*/
|
|
203
244
|
function memberForTable(input, name) {
|
|
204
245
|
const table = scopeMembershipTable(input.memberships, input.scopes, name);
|
|
205
246
|
return table !== void 0 && scopeColumn(table, input.scopes, name) !== void 0 ? table : void 0;
|
|
206
247
|
}
|
|
207
|
-
/**
|
|
248
|
+
/**
|
|
249
|
+
* The sources `member_<scope>_ids_for` reads: none when a table is mapped, as
|
|
250
|
+
* in `database` mode.
|
|
251
|
+
*/
|
|
208
252
|
function memberForSources(input, name) {
|
|
209
253
|
if (memberForTable(input, name) !== void 0) return [];
|
|
210
254
|
return (input.memberSources ?? []).filter((source) => source.sql.scope === void 0 || source.sql.scope === name);
|
|
211
255
|
}
|
|
212
256
|
/**
|
|
213
|
-
* The membership sources that can hold scope `name` when `memberships` maps
|
|
214
|
-
*
|
|
215
|
-
* that scope. Empty outside `database`
|
|
257
|
+
* The membership sources that can hold scope `name` when `memberships` maps no
|
|
258
|
+
* table for it: every `tableMembershipSource` source and the
|
|
259
|
+
* `junctionMembershipSource` sources of that scope. Empty outside `database`
|
|
260
|
+
* mode.
|
|
216
261
|
*/
|
|
217
262
|
function scopeSources(ctx, name) {
|
|
218
263
|
if (ctx.authorize !== "database" || scopeTable(ctx, name) !== void 0) return [];
|
|
219
264
|
return (ctx.sources ?? []).filter((source) => source.sql.scope === void 0 || source.sql.scope === name);
|
|
220
265
|
}
|
|
221
|
-
/**
|
|
266
|
+
/**
|
|
267
|
+
* The membership table mapped for scope `name`, with the column of its id and
|
|
268
|
+
* of the first scope's id.
|
|
269
|
+
*/
|
|
222
270
|
function scopeTable(ctx, name) {
|
|
223
271
|
const table = scopeMembershipTable(ctx.memberships, ctx.scopes, name);
|
|
224
272
|
if (table === void 0) return;
|
|
@@ -236,477 +284,6 @@ function scopeTable(ctx, name) {
|
|
|
236
284
|
};
|
|
237
285
|
}
|
|
238
286
|
//#endregion
|
|
239
|
-
//#region src/cli/sql/conditions.ts
|
|
240
|
-
const MAX_CLAIM_DEPTH = 8;
|
|
241
|
-
/**
|
|
242
|
-
* The claim path of a `principal.claim.*` / `principal.claims.*` ref, one
|
|
243
|
-
* segment per JSON key; `undefined` for any other ref.
|
|
244
|
-
*/
|
|
245
|
-
function claimPath(ref) {
|
|
246
|
-
const prefix = ["principal.claim.", "principal.claims."].find((item) => ref.startsWith(item));
|
|
247
|
-
if (prefix === void 0) return;
|
|
248
|
-
const segments = ref.slice(prefix.length).split(".");
|
|
249
|
-
if (segments.length > MAX_CLAIM_DEPTH) throw configError(`PermDock CLI: claim path '${ref}' is deeper than ${MAX_CLAIM_DEPTH}`);
|
|
250
|
-
for (const segment of segments) if (!CLAIM.test(segment) || isForbiddenKey(segment)) throw configError(`PermDock CLI: unsafe claim name '${segment}'`);
|
|
251
|
-
return segments;
|
|
252
|
-
}
|
|
253
|
-
function jsonPath(base, keys, last) {
|
|
254
|
-
let sql = base;
|
|
255
|
-
for (const [index, key] of keys.entries()) sql += ` ${index === keys.length - 1 ? last : "->"} ${quoteLiteral(key)}`;
|
|
256
|
-
return sql;
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* The JSON document a claim path starts from and the keys to walk in it. The
|
|
260
|
-
* `guc` dialect keeps each top-level claim in its own setting as JSON text.
|
|
261
|
-
*/
|
|
262
|
-
function claimRoot(ctx, path) {
|
|
263
|
-
const [head, ...rest] = path;
|
|
264
|
-
if (head === void 0) throw configError("PermDock CLI: empty claim path");
|
|
265
|
-
switch (ctx.dialect) {
|
|
266
|
-
case "supabase": return {
|
|
267
|
-
root: "(select auth.jwt())",
|
|
268
|
-
keys: path
|
|
269
|
-
};
|
|
270
|
-
case "neon": return {
|
|
271
|
-
root: "(select auth.session())",
|
|
272
|
-
keys: path
|
|
273
|
-
};
|
|
274
|
-
case "guc": return {
|
|
275
|
-
root: subjectClaimJsonSql(ctx, head),
|
|
276
|
-
keys: rest
|
|
277
|
-
};
|
|
278
|
-
default: return ctx.dialect;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
/** The claim at `path` as `jsonb`. */
|
|
282
|
-
function claimJsonSql(ctx, path) {
|
|
283
|
-
const { root, keys } = claimRoot(ctx, path);
|
|
284
|
-
return keys.length === 0 ? root : `(${jsonPath(root, keys, "->")})`;
|
|
285
|
-
}
|
|
286
|
-
/** The claim at `path` as text: `->>` on the last key, or the plain setting for a one-segment `guc` claim. */
|
|
287
|
-
function claimTextSql(ctx, path) {
|
|
288
|
-
const [head] = path;
|
|
289
|
-
if (path.length === 1 && head !== void 0) return subjectClaimSql(ctx, head);
|
|
290
|
-
const { root, keys } = claimRoot(ctx, path);
|
|
291
|
-
return `(${jsonPath(root, keys, "->>")})`;
|
|
292
|
-
}
|
|
293
|
-
/** The JSON kind a claim must have to compare with a column of `type`. */
|
|
294
|
-
function jsonKindOf(type) {
|
|
295
|
-
if (type === "numeric") return "number";
|
|
296
|
-
return type === "boolean" ? "boolean" : "string";
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* A claim compared with a column of `type`. Numbers and booleans must be JSON
|
|
300
|
-
* of that kind (anything else is `null`, so the comparison is false, as in
|
|
301
|
-
* memory); other types cast the text form. A one-segment `guc` claim is text
|
|
302
|
-
* and is cast as is.
|
|
303
|
-
*/
|
|
304
|
-
function typedClaimSql(ctx, path, type) {
|
|
305
|
-
const text = claimTextSql(ctx, path);
|
|
306
|
-
if (type === void 0 || type === "text") return text;
|
|
307
|
-
const cast = `${text}::${sqlType(type)}`;
|
|
308
|
-
const kind = jsonKindOf(type);
|
|
309
|
-
if (kind === "string" || ctx.dialect === "guc" && path.length === 1) return `(${cast})`;
|
|
310
|
-
return `(case when jsonb_typeof(${claimJsonSql(ctx, path)}) = '${kind}' then ${cast} end)`;
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* The elements of an array claim as a Postgres array of the column's type,
|
|
314
|
-
* built once per statement (an uncorrelated `array(select ...)` is an InitPlan).
|
|
315
|
-
* A missing or non-array claim is the empty array; elements of another JSON
|
|
316
|
-
* kind are left out.
|
|
317
|
-
*/
|
|
318
|
-
function claimArraySql(ctx, path, type) {
|
|
319
|
-
const json = claimJsonSql(ctx, path);
|
|
320
|
-
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)}')`;
|
|
321
|
-
}
|
|
322
|
-
function sqlValue(value, ctx, field) {
|
|
323
|
-
if (value !== null && typeof value === "object" && "ref" in value) return compileRef(value.ref, ctx, field);
|
|
324
|
-
if (value !== null && typeof value === "object" && "date" in value) {
|
|
325
|
-
const instant = isoInstant(value.date);
|
|
326
|
-
return instant === void 0 ? "null" : quoteLiteral(isoAt(instant));
|
|
327
|
-
}
|
|
328
|
-
if (isReadonlyArray(value)) return `array[${value.map((item) => sqlValue(item, ctx)).join(", ")}]`;
|
|
329
|
-
if (value === null) return "null";
|
|
330
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
331
|
-
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "null";
|
|
332
|
-
if (typeof value === "string") return quoteLiteral(value);
|
|
333
|
-
throw configError("PermDock CLI: non-portable condition value");
|
|
334
|
-
}
|
|
335
|
-
function compileRef(ref, ctx, field) {
|
|
336
|
-
if (ref === "principal.id") return subjectIdSql(ctx);
|
|
337
|
-
if (ref === "now") return "now()";
|
|
338
|
-
const path = claimPath(ref);
|
|
339
|
-
if (ref === "principal.tenant" || path?.length === 1 && path[0] === ctx.tenantClaim) return tenantClaimSql(ctx);
|
|
340
|
-
if (path !== void 0) return typedClaimSql(ctx, path, field === void 0 ? void 0 : ctx.columnTypes?.[field]);
|
|
341
|
-
if (ref === "context" || ref.startsWith("context.")) throw configError(`PermDock CLI: '${ref}' is request context, which is not in the token; RLS cannot read it (permdock doctor PD027)`);
|
|
342
|
-
throw configError(`PermDock CLI: non-portable subject ref '${ref}'`);
|
|
343
|
-
}
|
|
344
|
-
/** `value` with `\`, `%` and `_` escaped for `like ... escape '\'`, as `escapeLike` does in process. */
|
|
345
|
-
function likeLiteralSql(value) {
|
|
346
|
-
return `replace(replace(replace(${value}, '\\', '\\\\'), '%', '\\%'), '_', '\\_')`;
|
|
347
|
-
}
|
|
348
|
-
function compareSql(op, field, value) {
|
|
349
|
-
const left = quoteIdent(field);
|
|
350
|
-
switch (op) {
|
|
351
|
-
case "eq": return `${left} = ${value}`;
|
|
352
|
-
case "ne": return `${left} <> ${value}`;
|
|
353
|
-
case "gt": return `${left} > ${value}`;
|
|
354
|
-
case "gte": return `${left} >= ${value}`;
|
|
355
|
-
case "lt": return `${left} < ${value}`;
|
|
356
|
-
case "lte": return `${left} <= ${value}`;
|
|
357
|
-
case "contains": return `${left}::text like '%' || ${likeLiteralSql(`${value}::text`)} || '%' escape '\\'`;
|
|
358
|
-
default: return op;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Whether the row's `rowField` is among the ids in `table.rowColumn` the
|
|
363
|
-
* subject holds a matching membership on. The row field stays outside the
|
|
364
|
-
* subquery: inside it, an unqualified name resolves to the membership table
|
|
365
|
-
* first, so `m.org_id = "org_id"` would compare the membership with itself.
|
|
366
|
-
*/
|
|
367
|
-
function membershipSql(table, rowColumn, rowField, roles, ctx, tenantColumn, scope) {
|
|
368
|
-
const parts = [`m.${quoteIdent(table.user)} = ${subjectIdSql(ctx)}`];
|
|
369
|
-
const role = memberRoleOf(table);
|
|
370
|
-
if (roles.length > 0) {
|
|
371
|
-
const held = role.through === void 0 && !role.lateral ? `m.${quoteIdent(role.column)}` : role.lookup;
|
|
372
|
-
parts.push(`${held} = any(${textArrayLiteral(roles)})`);
|
|
373
|
-
const via = memberVia(table);
|
|
374
|
-
const single = sole(roles);
|
|
375
|
-
const kind = single === void 0 ? kindFilterSql(ctx, role.lookup, via) : roleKindSql(ctx, single, via);
|
|
376
|
-
if (kind !== void 0) parts.push(kind);
|
|
377
|
-
}
|
|
378
|
-
if (table.expiresAt !== void 0) parts.push(`(m.${quoteIdent(table.expiresAt)} is null or m.${quoteIdent(table.expiresAt)} > now())`);
|
|
379
|
-
parts.push(...activeMembershipSql(ctx, table.disabledAt === void 0 ? void 0 : `m.${quoteIdent(table.disabledAt)}`));
|
|
380
|
-
if (tenantColumn !== void 0 && ctx.tenants !== "all") parts.push(`m.${quoteIdent(tenantColumn)} = ${tenantClaimSql(ctx)}`);
|
|
381
|
-
parts.push(...activeUserSql(ctx));
|
|
382
|
-
if (scope !== void 0) {
|
|
383
|
-
const tenantOf = scope === rootScope(ctx.scopes) ? rowColumn : tenantColumn;
|
|
384
|
-
const keyed = keyTenantSql(ctx, tenantOf === void 0 ? void 0 : `m.${quoteIdent(tenantOf)}`);
|
|
385
|
-
if (keyed !== void 0) parts.push(keyed);
|
|
386
|
-
parts.push(...activeInstancesSql(ctx, scope, (name) => {
|
|
387
|
-
const held = scopeColumn(table, ctx.scopes, name);
|
|
388
|
-
return held === void 0 ? void 0 : `m.${quoteIdent(held)}`;
|
|
389
|
-
}));
|
|
390
|
-
}
|
|
391
|
-
const expand = roles.length > 0 && role.lateral ? role.join : "";
|
|
392
|
-
return `${quoteIdent(rowField)} in (select m.${quoteIdent(rowColumn)} from ${quoteTable(table.table)} m${expand} where ${parts.join(" and ")})`;
|
|
393
|
-
}
|
|
394
|
-
function compileMemberOf(condition, ctx) {
|
|
395
|
-
const scope = condition.scope === "resource" ? void 0 : resolveScope(ctx.scopes, condition.scope);
|
|
396
|
-
if (condition.scope !== "resource" && scope === void 0) throw configError(`PermDock CLI: memberOf ${condition.scope} names a scope the policy does not declare`);
|
|
397
|
-
const mapping = scope === void 0 ? condition.resource === void 0 ? void 0 : ctx.memberships?.resource?.[condition.resource] : scopeMembershipTable(ctx.memberships, ctx.scopes, scope);
|
|
398
|
-
if (mapping !== void 0) {
|
|
399
|
-
const named = scope === void 0 ? void 0 : scopeTable(ctx, scope);
|
|
400
|
-
const rowColumn = scope === void 0 ? mapping.id : named?.column;
|
|
401
|
-
if (rowColumn === void 0) throw configError(`PermDock CLI: memberships mapping for ${condition.scope} is missing the row column`);
|
|
402
|
-
const tenantColumn = scope === void 0 || scope === rootScope(ctx.scopes) ? void 0 : named?.tenantColumn;
|
|
403
|
-
const primary = membershipSql(mapping, rowColumn, condition.field, condition.roles, ctx, tenantColumn, scope);
|
|
404
|
-
if (condition.scope !== "resource" || condition.parents === void 0) return primary;
|
|
405
|
-
return `(${[primary, ...condition.parents.flatMap((parent) => {
|
|
406
|
-
if (typeof parent === "string") return [membershipSql(mapping, rowColumn, parent, condition.roles, ctx, tenantColumn)];
|
|
407
|
-
const hopTable = ctx.memberships?.resource?.[parent.resource];
|
|
408
|
-
return hopTable?.id === void 0 ? [] : [membershipSql(hopTable, hopTable.id, parent.field, condition.roles, ctx)];
|
|
409
|
-
})].join(" or ")})`;
|
|
410
|
-
}
|
|
411
|
-
if (scope !== void 0 && condition.roles.length === 0 && scopeSources(ctx, scope).length > 0) return `${quoteIdent(condition.field)} in (select ${qualified(ctx, memberIdsHelper(scope))}())`;
|
|
412
|
-
if (scope !== void 0 && scope === rootScope(ctx.scopes)) {
|
|
413
|
-
const parts = [
|
|
414
|
-
ctx.tenants === "all" ? `${quoteIdent(condition.field)} in (select ${qualified(ctx, memberIdsHelper(scope))}())` : `${quoteIdent(condition.field)} = ${tenantClaimSql(ctx)}`,
|
|
415
|
-
...ctx.tenants === "all" ? [] : [keyTenantSql(ctx, quoteIdent(condition.field))].filter((part) => part !== void 0),
|
|
416
|
-
...activeUserSql(ctx),
|
|
417
|
-
...activeInstancesSql(ctx, scope, () => quoteIdent(condition.field))
|
|
418
|
-
];
|
|
419
|
-
return parts.length === 1 ? parts.join("") : `(${parts.join(" and ")})`;
|
|
420
|
-
}
|
|
421
|
-
throw configError(`PermDock CLI: memberOf ${condition.scope} needs a memberships table mapping`);
|
|
422
|
-
}
|
|
423
|
-
function compileConditionSql(condition, ctx) {
|
|
424
|
-
switch (condition.op) {
|
|
425
|
-
case "eq":
|
|
426
|
-
case "ne":
|
|
427
|
-
case "gt":
|
|
428
|
-
case "gte":
|
|
429
|
-
case "lt":
|
|
430
|
-
case "lte": return compareSql(condition.op, condition.field, sqlValue(condition.value, ctx, condition.field));
|
|
431
|
-
case "contains": {
|
|
432
|
-
const element = ctx.arrayColumns?.[condition.field];
|
|
433
|
-
if (element !== void 0) return `${sqlValue(condition.value, {
|
|
434
|
-
...ctx,
|
|
435
|
-
columnTypes: {
|
|
436
|
-
...ctx.columnTypes,
|
|
437
|
-
[condition.field]: element
|
|
438
|
-
}
|
|
439
|
-
}, condition.field)} = any(${quoteIdent(condition.field)})`;
|
|
440
|
-
return compareSql(condition.op, condition.field, sqlValue(condition.value, ctx, condition.field));
|
|
441
|
-
}
|
|
442
|
-
case "in":
|
|
443
|
-
case "notIn": {
|
|
444
|
-
const keyword = condition.op === "in" ? "in" : "not in";
|
|
445
|
-
if (!Array.isArray(condition.value) && typeof condition.value === "object" && "ref" in condition.value) {
|
|
446
|
-
const path = claimPath(condition.value.ref);
|
|
447
|
-
if (path === void 0) {
|
|
448
|
-
compileRef(condition.value.ref, ctx);
|
|
449
|
-
throw configError(`PermDock CLI: non-portable ${condition.op} against '${condition.value.ref}'`);
|
|
450
|
-
}
|
|
451
|
-
const column = quoteIdent(condition.field);
|
|
452
|
-
const list = claimArraySql(ctx, path, ctx.columnTypes?.[condition.field]);
|
|
453
|
-
return condition.op === "in" ? `${column} = any (${list})` : `(${column} is not null and not (${column} = any (${list})))`;
|
|
454
|
-
}
|
|
455
|
-
const values = condition.value.map((item) => sqlValue(item, ctx));
|
|
456
|
-
if (values.length === 0) return condition.op === "in" ? "false" : `${quoteIdent(condition.field)} is not null`;
|
|
457
|
-
return `${quoteIdent(condition.field)} ${keyword} (${values.join(", ")})`;
|
|
458
|
-
}
|
|
459
|
-
case "isNull": return `${quoteIdent(condition.field)} is ${condition.value ? "" : "not "}null`;
|
|
460
|
-
case "and": return `(${condition.conditions.map((item) => compileConditionSql(item, ctx)).join(" and ")})`;
|
|
461
|
-
case "or":
|
|
462
|
-
if (condition.conditions.length === 0) return "false";
|
|
463
|
-
return `(${condition.conditions.map((item) => compileConditionSql(item, ctx)).join(" or ")})`;
|
|
464
|
-
case "not": return `(${compileConditionSql(condition.condition, ctx)}) is not true`;
|
|
465
|
-
case "memberOf": return compileMemberOf(condition, ctx);
|
|
466
|
-
case "related": return compileRelatedSql(condition, ctx);
|
|
467
|
-
case "sqlFunction":
|
|
468
|
-
if (ctx.inlineFunctions === true) return compileConditionSql(condition.twin, ctx);
|
|
469
|
-
return `${quoteTable(condition.name)}(${condition.args.map((arg) => isSqlFunctionField(arg) ? quoteIdent(arg.field) : sqlValue(arg, ctx)).join(", ")})`;
|
|
470
|
-
case "opaque": return condition.sql;
|
|
471
|
-
case "liveSession": return liveSessionSql(ctx);
|
|
472
|
-
default: return condition;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
function andConditions(left, right) {
|
|
476
|
-
if (left === void 0) return right;
|
|
477
|
-
if (right === void 0) return left;
|
|
478
|
-
return {
|
|
479
|
-
op: "and",
|
|
480
|
-
conditions: [left, right]
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
function valueContextRefs(value) {
|
|
484
|
-
if (Array.isArray(value)) return value.flatMap(valueContextRefs);
|
|
485
|
-
if (isRecord(value) && typeof value["ref"] === "string" && (value["ref"] === "context" || value["ref"].startsWith("context."))) return [value["ref"]];
|
|
486
|
-
return [];
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* The `context.*` refs a condition reads. The request context is not in the
|
|
490
|
-
* token, so RLS cannot evaluate them.
|
|
491
|
-
*/
|
|
492
|
-
function contextRefs(condition) {
|
|
493
|
-
if (condition === void 0) return [];
|
|
494
|
-
switch (condition.op) {
|
|
495
|
-
case "eq":
|
|
496
|
-
case "ne":
|
|
497
|
-
case "gt":
|
|
498
|
-
case "gte":
|
|
499
|
-
case "lt":
|
|
500
|
-
case "lte":
|
|
501
|
-
case "contains":
|
|
502
|
-
case "in":
|
|
503
|
-
case "notIn": return valueContextRefs(condition.value);
|
|
504
|
-
case "and":
|
|
505
|
-
case "or": return condition.conditions.flatMap((child) => contextRefs(child));
|
|
506
|
-
case "not": return contextRefs(condition.condition);
|
|
507
|
-
case "sqlFunction": return [...condition.args.flatMap(valueContextRefs), ...contextRefs(condition.twin)];
|
|
508
|
-
case "isNull":
|
|
509
|
-
case "memberOf":
|
|
510
|
-
case "related":
|
|
511
|
-
case "opaque":
|
|
512
|
-
case "liveSession": return [];
|
|
513
|
-
default: return condition;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
function sqlFunctionNames(condition) {
|
|
517
|
-
if (condition === void 0) return [];
|
|
518
|
-
switch (condition.op) {
|
|
519
|
-
case "sqlFunction": return [condition.name, ...sqlFunctionNames(condition.twin)];
|
|
520
|
-
case "and":
|
|
521
|
-
case "or": return condition.conditions.flatMap((child) => sqlFunctionNames(child));
|
|
522
|
-
case "not": return sqlFunctionNames(condition.condition);
|
|
523
|
-
case "eq":
|
|
524
|
-
case "ne":
|
|
525
|
-
case "gt":
|
|
526
|
-
case "gte":
|
|
527
|
-
case "lt":
|
|
528
|
-
case "lte":
|
|
529
|
-
case "contains":
|
|
530
|
-
case "in":
|
|
531
|
-
case "notIn":
|
|
532
|
-
case "isNull":
|
|
533
|
-
case "memberOf":
|
|
534
|
-
case "related":
|
|
535
|
-
case "opaque":
|
|
536
|
-
case "liveSession": return [];
|
|
537
|
-
default: return condition;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
/**
|
|
541
|
-
* A `related` condition as uncorrelated subqueries, so Postgres runs the
|
|
542
|
-
* helper once per statement: the row's field in the closure's descendants of
|
|
543
|
-
* the ids the subject holds the relation on (or those ids alone at depth 0).
|
|
544
|
-
* The helper sits in `array(...)` so it stays an InitPlan; a plain `in` gets
|
|
545
|
-
* pulled into a join that can rescan it per closure row.
|
|
546
|
-
*/
|
|
547
|
-
function compileRelatedSql(condition, ctx) {
|
|
548
|
-
if (condition.hops !== void 0 && condition.hops.length > 0) return compileHoppedSql(condition, ctx);
|
|
549
|
-
const type = ctx.columnTypes?.[condition.field];
|
|
550
|
-
const cast = type === void 0 || type === "text" ? "" : `::${type}`;
|
|
551
|
-
const column = type === void 0 ? `${quoteIdent(condition.field)}::text` : quoteIdent(condition.field);
|
|
552
|
-
const helper = condition.permission === void 0 ? `${qualified(ctx, graphHelper(condition.resource))}(${quoteLiteral(condition.relation)})` : `${qualified(ctx, inheritedRowsHelper(condition.resource))}(${quoteLiteral(condition.permission)})`;
|
|
553
|
-
const cap = ctx.graph?.closures[condition.resource];
|
|
554
|
-
let inner;
|
|
555
|
-
if (condition.depth > 0 && cap !== void 0) {
|
|
556
|
-
const depth = condition.depth < cap ? ` and depth <= ${String(condition.depth)}` : "";
|
|
557
|
-
inner = `select descendant${cast} from ${qualified(ctx, CLOSURE.table)} where resource = ${quoteLiteral(condition.resource)}${depth} and ancestor = any (array(select ${helper}))`;
|
|
558
|
-
} else inner = cast === "" ? `select ${helper}` : `select p.id${cast} from ${helper} as p(id)`;
|
|
559
|
-
const reach = `${column} in (${inner})`;
|
|
560
|
-
if (condition.parent === true && condition.restricted !== void 0) return `(${reach} and ${quoteIdent(condition.restricted)} is not true)`;
|
|
561
|
-
return reach;
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* A `related` condition that crosses link hops: the reached instances of the
|
|
565
|
-
* last resource, carried back over each link by its security definer helper,
|
|
566
|
-
* so the hop reads a table the subject may not see.
|
|
567
|
-
*/
|
|
568
|
-
function compileHoppedSql(condition, ctx) {
|
|
569
|
-
const resources = ctx.graph?.resources;
|
|
570
|
-
if (resources === void 0) throw configError("PermDock CLI: a related condition with link hops needs the policy resources in the RLS context");
|
|
571
|
-
const schema = quoteIdent(helperSchema(ctx));
|
|
572
|
-
return graphSqlText(relatedSql(condition, {
|
|
573
|
-
resources,
|
|
574
|
-
...ctx.graph?.tables === void 0 ? {} : { tables: ctx.graph.tables },
|
|
575
|
-
closure: `${helperSchema(ctx)}.${CLOSURE.table}`,
|
|
576
|
-
closureDepths: ctx.graph?.closures ?? {},
|
|
577
|
-
qualify: qualifySqlTable,
|
|
578
|
-
holders: (resource, relation) => [{ text: `select ${schema}.${graphHelper(resource)}(${quoteLiteral(relation)})` }],
|
|
579
|
-
permitted: (resource, permission) => [{ text: `select ${schema}.${inheritedRowsHelper(resource)}(${quoteLiteral(permission)})` }],
|
|
580
|
-
restrictedRows: (resource) => [{ text: `select ${schema}.${restrictedHelper(resource)}()` }],
|
|
581
|
-
linked: (resource, link, targets) => [
|
|
582
|
-
{ text: `select ${schema}.${linkHelper(resource, link)}(array(` },
|
|
583
|
-
...targets,
|
|
584
|
-
{ text: "))" }
|
|
585
|
-
]
|
|
586
|
-
}), ctx);
|
|
587
|
-
}
|
|
588
|
-
//#endregion
|
|
589
|
-
//#region src/cli/sql/grants.ts
|
|
590
|
-
/**
|
|
591
|
-
* The `role_permissions` grant key of a break-glass grant. It differs from
|
|
592
|
-
* the permission key, so `authorize()` and the table policies never count it;
|
|
593
|
-
* only the break-glass read asks for it.
|
|
594
|
-
*/
|
|
595
|
-
function breakGlassKey(permission) {
|
|
596
|
-
return `${permission}#break-glass`;
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* The role and scope holding a break-glass grant. `breakGlass()` sets
|
|
600
|
-
* `to: authenticated`, so inside `role()` the role is on the grant itself.
|
|
601
|
-
*/
|
|
602
|
-
function breakGlassHolder(item) {
|
|
603
|
-
if (item.access.kind === "role") return {
|
|
604
|
-
role: item.access.role,
|
|
605
|
-
scope: item.access.scope
|
|
606
|
-
};
|
|
607
|
-
const { role, scope } = item.grant;
|
|
608
|
-
return role === null || typeof scope !== "string" ? void 0 : {
|
|
609
|
-
role,
|
|
610
|
-
scope
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
function granteeItems(to) {
|
|
614
|
-
return Array.isArray(to) ? to : [to];
|
|
615
|
-
}
|
|
616
|
-
function relationWhere(policy, grant, grantee) {
|
|
617
|
-
const node = policy.resources.get(grant.permission.resource);
|
|
618
|
-
const where = relationCondition(grantee, node, scopeList(policy.scopes), {
|
|
619
|
-
resources: policy.resources,
|
|
620
|
-
now: { ref: "now" }
|
|
621
|
-
});
|
|
622
|
-
if (where === void 0) throw configError(`PermDock CLI: grant ${grant.permission.key} names relation '${grantee.relation}', which ${grant.permission.resource} does not declare`);
|
|
623
|
-
return where;
|
|
624
|
-
}
|
|
625
|
-
function inheritWhere(policy, grant, grantee) {
|
|
626
|
-
const where = inheritCondition(grantee, policy.resources.get(grant.permission.resource), policy.resources);
|
|
627
|
-
if (where === void 0) throw configError(`PermDock CLI: grant ${grant.permission.key} inherits ${grantee.permission}, which ${grant.permission.resource} cannot reach`);
|
|
628
|
-
return where;
|
|
629
|
-
}
|
|
630
|
-
/** Only a deny for `oauth-client` actors compiles: Postgres sees `client_id` and `act`, not other actor kinds. */
|
|
631
|
-
function actorAccess(grant, grantee, others) {
|
|
632
|
-
if (grant.effect !== "deny" || grantee.actor !== "oauth-client" || others > 0) throw configError(`PermDock CLI: grant ${grant.permission.key} is limited to an actor grantee; RLS compiles only deny(…, { to: actor('oauth-client') }) on its own, so enforce it in the application`);
|
|
633
|
-
return {
|
|
634
|
-
kind: "actor",
|
|
635
|
-
actor: "oauth-client"
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
function accessOf(grant, roles, anyone) {
|
|
639
|
-
const [held, ...more] = roles;
|
|
640
|
-
if (more.length > 0) throw configError(`PermDock CLI: grant ${grant.permission.key} requires several roles at once (${roles.map((item) => item.role).join(", ")}); RLS compiles one role per grant`);
|
|
641
|
-
if (held === void 0) return anyone ? { kind: "anyone" } : { kind: "authenticated" };
|
|
642
|
-
if (typeof held.scope === "object") return {
|
|
643
|
-
kind: "resource",
|
|
644
|
-
role: held.role,
|
|
645
|
-
resource: held.scope.resource
|
|
646
|
-
};
|
|
647
|
-
return {
|
|
648
|
-
kind: "role",
|
|
649
|
-
role: held.role,
|
|
650
|
-
scope: held.scope
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
/**
|
|
654
|
-
* Every grant RLS must enforce: role grants and top-level
|
|
655
|
-
* `definePolicy({ grants })` alike (`policy.grants` holds both). A grantee
|
|
656
|
-
* Postgres cannot see (plan, actor, assurance) fails with the permission named.
|
|
657
|
-
*/
|
|
658
|
-
function collectGrants(policy) {
|
|
659
|
-
return policy.grants.map((grant) => {
|
|
660
|
-
const items = granteeItems(grant.to);
|
|
661
|
-
const roles = [];
|
|
662
|
-
let anyone = true;
|
|
663
|
-
let where = grant.where;
|
|
664
|
-
let delegated;
|
|
665
|
-
for (const item of items) switch (item.kind) {
|
|
666
|
-
case "role":
|
|
667
|
-
roles.push(item);
|
|
668
|
-
break;
|
|
669
|
-
case "anyone": break;
|
|
670
|
-
case "authenticated":
|
|
671
|
-
anyone = false;
|
|
672
|
-
break;
|
|
673
|
-
case "relation":
|
|
674
|
-
anyone = false;
|
|
675
|
-
where = andConditions(where, relationWhere(policy, grant, item));
|
|
676
|
-
break;
|
|
677
|
-
case "inherit":
|
|
678
|
-
anyone = false;
|
|
679
|
-
where = andConditions(where, inheritWhere(policy, grant, item));
|
|
680
|
-
break;
|
|
681
|
-
case "actor":
|
|
682
|
-
delegated = actorAccess(grant, item, items.length - 1);
|
|
683
|
-
break;
|
|
684
|
-
case "plan":
|
|
685
|
-
case "assurance": throw configError(`PermDock CLI: grant ${grant.permission.key} is limited to a ${item.kind} grantee, which RLS cannot compile; enforce it in the application`);
|
|
686
|
-
default: return item;
|
|
687
|
-
}
|
|
688
|
-
const access = delegated ?? accessOf(grant, roles, anyone);
|
|
689
|
-
const label = access.kind === "role" || access.kind === "resource" ? access.role : access.kind;
|
|
690
|
-
return where === void 0 ? {
|
|
691
|
-
grant,
|
|
692
|
-
label,
|
|
693
|
-
access
|
|
694
|
-
} : {
|
|
695
|
-
grant,
|
|
696
|
-
label,
|
|
697
|
-
access,
|
|
698
|
-
where
|
|
699
|
-
};
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
/** Declared role names (bindings and `defineRoles` leaves) plus any role a top-level grant names. */
|
|
703
|
-
function roleNames(policy) {
|
|
704
|
-
const names = new Set(policy.roles.map((item) => item.name));
|
|
705
|
-
for (const leaf of listRoles(policy.vocabulary.roles)) names.add(leaf.key);
|
|
706
|
-
for (const item of collectGrants(policy)) if (item.access.kind === "role" || item.access.kind === "resource") names.add(item.access.role);
|
|
707
|
-
return [...names];
|
|
708
|
-
}
|
|
709
|
-
//#endregion
|
|
710
287
|
//#region src/cli/rls/api-keys.ts
|
|
711
288
|
const API_KEY_ALLOWS = "permdock_api_key_allows";
|
|
712
289
|
const DEFAULTS = {
|
|
@@ -832,7 +409,10 @@ ${body}
|
|
|
832
409
|
}
|
|
833
410
|
//#endregion
|
|
834
411
|
//#region src/cli/shared/compare.ts
|
|
835
|
-
/**
|
|
412
|
+
/**
|
|
413
|
+
* UTF-16 code-unit order, never `localeCompare`: generated SQL must match byte
|
|
414
|
+
* for byte on every machine.
|
|
415
|
+
*/
|
|
836
416
|
function byCodeUnit(a, b) {
|
|
837
417
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
838
418
|
}
|
|
@@ -848,10 +428,10 @@ const READERS = {
|
|
|
848
428
|
trusted: "permdock_trusted_role_permissions"
|
|
849
429
|
};
|
|
850
430
|
/**
|
|
851
|
-
* The custom-role objects: the tables (`database` mode), the ceiling view
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
431
|
+
* The custom-role objects: the tables (`database` mode), the ceiling view over
|
|
432
|
+
* the `assignable` declared roles, and `permdock_custom_keys`, which resolves
|
|
433
|
+
* one custom role to the grant keys it holds in a scope. A row in the tables
|
|
434
|
+
* can never reach a key outside the view.
|
|
855
435
|
*/
|
|
856
436
|
function customRolesSql(ctx) {
|
|
857
437
|
const custom = ctx.customRoles;
|
|
@@ -978,7 +558,10 @@ revoke execute on function ${keys}(text[], text[], text[], text) from public, an
|
|
|
978
558
|
if (shared !== "") chunks.push(shared);
|
|
979
559
|
return chunks.join("\n\n");
|
|
980
560
|
}
|
|
981
|
-
/**
|
|
561
|
+
/**
|
|
562
|
+
* `permdock_is_shared_role(p_scope, p_role)` (`database` mode): whether a
|
|
563
|
+
* shared custom role of that name exists at the scope.
|
|
564
|
+
*/
|
|
982
565
|
function sharedRoleSql(ctx) {
|
|
983
566
|
if (ctx.authorize !== "database") return "";
|
|
984
567
|
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
@@ -1000,8 +583,8 @@ $$;
|
|
|
1000
583
|
revoke execute on function ${shared}(text, text) from public, anon, authenticated;`;
|
|
1001
584
|
}
|
|
1002
585
|
/**
|
|
1003
|
-
* `permdock_custom_keys` when resources declare levels. An allow entry is
|
|
1004
|
-
*
|
|
586
|
+
* `permdock_custom_keys` when resources declare levels. An allow entry is `key`
|
|
587
|
+
* or `key@level`. A level reaches only the ceiling's `<grant key>@<level>`
|
|
1005
588
|
* keys, plus the ceiling denies of that permission; a level no ceiling key
|
|
1006
589
|
* carries removes the permission, as `resolveCustomRole` drops it with
|
|
1007
590
|
* `unknown-level`.
|
|
@@ -1106,8 +689,8 @@ function currentKeysSql(array) {
|
|
|
1106
689
|
}
|
|
1107
690
|
/**
|
|
1108
691
|
* SQL fragments that read a stored former key as its current key inside
|
|
1109
|
-
* `permdock_custom_keys`. Without renames they are the plain column and
|
|
1110
|
-
*
|
|
692
|
+
* `permdock_custom_keys`. Without renames they are the plain column and array
|
|
693
|
+
* reads.
|
|
1111
694
|
*/
|
|
1112
695
|
function renamesSql(renames) {
|
|
1113
696
|
if (renames.length === 0) return {
|
|
@@ -1131,14 +714,14 @@ function renamesSql(renames) {
|
|
|
1131
714
|
}
|
|
1132
715
|
/**
|
|
1133
716
|
* The catalog reader `permdock_permission_keys()` and the role reader
|
|
1134
|
-
* `permdock_role_permissions(p_role, p_scope, p_tenant, p_scope_id)`, both
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
717
|
+
* `permdock_role_permissions(p_role, p_scope, p_tenant, p_scope_id)`, both for
|
|
718
|
+
* `authenticated`. A declared role answers from `role_permissions`, which is
|
|
719
|
+
* the catalog. A custom role (`database` mode) answers from the tenant's own
|
|
720
|
+
* rows through `permdock_custom_keys`, only to a member of the tenant or a
|
|
721
|
+
* holder of a `meta.manageRoles` permission for a platform role; a shared role
|
|
722
|
+
* (no tenant below global) to any signed-in caller. The effect is `deny` for a
|
|
723
|
+
* deny the role carries after the ceiling, as `resolveCustomRole` returns it,
|
|
724
|
+
* never for a deny that merely removes an allow.
|
|
1142
725
|
*/
|
|
1143
726
|
function roleReadersSql(ctx, permissions, trustedReaders) {
|
|
1144
727
|
const rp = qualified(ctx, "role_permissions");
|
|
@@ -1205,7 +788,7 @@ end;`;
|
|
|
1205
788
|
and ${qualified(ctx, HELPERS.has)}(m.grant_key)
|
|
1206
789
|
)`;
|
|
1207
790
|
const match = `c.tenant_id is not distinct from p_tenant and c.scope = p_scope and c.scope_id is not distinct from p_scope_id and c.role = p_role`;
|
|
1208
|
-
const allowEntryOf =
|
|
791
|
+
const allowEntryOf = `c.${allowEntry(ctx)}`;
|
|
1209
792
|
const checked = (gate) => `begin
|
|
1210
793
|
if p_role = any(${sqlTextArray(custom.declared)}) then
|
|
1211
794
|
return query
|
|
@@ -1248,7 +831,7 @@ revoke execute on function ${reader}(text, text, ${tenantType}, text) from publi
|
|
|
1248
831
|
grant execute on function ${reader}(text, text, ${tenantType}, text) to authenticated;`);
|
|
1249
832
|
const trusted = qualified(ctx, READERS.trusted);
|
|
1250
833
|
const signature = `text, text, ${tenantType}, text`;
|
|
1251
|
-
const grants = trustedReaders === void 0 || trustedReaders.length === 0 ? "" : `\
|
|
834
|
+
const grants = trustedReaders === void 0 || trustedReaders.length === 0 ? "" : `\n${trustedReaderGrants(ctx, `${trusted}(${signature})`, trustedReaders)}`;
|
|
1252
835
|
chunks.push(`create or replace function ${trusted}(p_role text, p_scope text, p_tenant ${tenantType} default null, p_scope_id text default null)
|
|
1253
836
|
returns table (permission text, effect text)
|
|
1254
837
|
language plpgsql
|
|
@@ -1261,33 +844,42 @@ $$;
|
|
|
1261
844
|
revoke execute on function ${trusted}(${signature}) from public, anon, authenticated;${grants}`);
|
|
1262
845
|
return chunks.join("\n\n");
|
|
1263
846
|
}
|
|
1264
|
-
/**
|
|
847
|
+
/**
|
|
848
|
+
* The stored definition of `p_role` at `p_scope` for `p_tenant`: the shared
|
|
849
|
+
* rows when a shared role has the name.
|
|
850
|
+
*/
|
|
1265
851
|
function storedTenant(ctx) {
|
|
1266
852
|
return `case when ${qualified(ctx, CUSTOM_ROLES.shared)}(p_scope, p_role) then c.tenant_id is null else c.tenant_id is not distinct from p_tenant end`;
|
|
1267
853
|
}
|
|
1268
854
|
/**
|
|
1269
|
-
* The tenant half of a custom-role row match for a membership: the
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
855
|
+
* The tenant half of a custom-role row match for a membership: the membership's
|
|
856
|
+
* tenant, or no tenant when a shared role of that name exists at the scope, so
|
|
857
|
+
* a shared role shadows a tenant role of the same name.
|
|
1272
858
|
*/
|
|
1273
859
|
function customTenantMatch(ctx, tenant, scope, role) {
|
|
1274
860
|
return `case when ${qualified(ctx, CUSTOM_ROLES.shared)}(${quoteLiteral(scope)}, ${role}) then c.tenant_id is null else c.tenant_id::text = ${tenant} end`;
|
|
1275
861
|
}
|
|
1276
862
|
/**
|
|
1277
|
-
* The scope of a row of the application's roles table: its scope column (or
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
863
|
+
* The scope of a row of the application's roles table: its scope column (or the
|
|
864
|
+
* first scope) with a tenant; with no tenant, the same scope when the `shared`
|
|
865
|
+
* column is true, else `'global'`.
|
|
1280
866
|
*/
|
|
1281
867
|
function rolesTableScope(table, tenant, root, column) {
|
|
1282
868
|
const own = table.scope === void 0 ? quoteLiteral(root) : `${column(table.scope)}::text`;
|
|
1283
869
|
return `case when ${tenant} is null then ${table.shared === void 0 ? "'global'" : `case when coalesce(${column(table.shared)}, false) then ${own} else 'global' end`} else ${own} end`;
|
|
1284
870
|
}
|
|
1285
|
-
/**
|
|
871
|
+
/**
|
|
872
|
+
* `expr` read as its current key: a former key maps to the key it was renamed
|
|
873
|
+
* to.
|
|
874
|
+
*/
|
|
1286
875
|
function currentKeyExpr(renames, expr) {
|
|
1287
876
|
if (renames.length === 0) return expr;
|
|
1288
877
|
return `(case ${expr} ${renames.map(([former, current]) => `when ${quoteLiteral(former)} then ${quoteLiteral(current)}`).join(" ")} else ${expr} end)`;
|
|
1289
878
|
}
|
|
1290
|
-
/**
|
|
879
|
+
/**
|
|
880
|
+
* `case p_scope when '<scope>' then <sql for scope> ... else false end` over
|
|
881
|
+
* the declared scopes.
|
|
882
|
+
*/
|
|
1291
883
|
function perScope(ctx, sql) {
|
|
1292
884
|
const arms = ctx.scopes.map((scope) => `when ${quoteLiteral(scope.name)} then ${sql(scope.name)}`).join(" ");
|
|
1293
885
|
return arms === "" ? "false" : `(case p_scope ${arms} else false end)`;
|
|
@@ -1301,6 +893,16 @@ ${indent} hint = ${quoteLiteral(hint)};`;
|
|
|
1301
893
|
function hasCustomRoleChecksFor(ctx) {
|
|
1302
894
|
return ctx.authorize === "database" && ctx.customRoles !== void 0 && ctx.scopes.length > 0 && ctx.scopes.every((scope) => hasMemberFor(ctx, scope.name));
|
|
1303
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* The grants a `rls.trustedReaders` role needs for one reader: usage on the
|
|
898
|
+
* helper schema, which no client role reaches through these lines, and execute
|
|
899
|
+
* on the function.
|
|
900
|
+
*/
|
|
901
|
+
function trustedReaderGrants(ctx, fn, readers) {
|
|
902
|
+
const to = readers.map(quoteIdent).join(", ");
|
|
903
|
+
return `grant usage on schema ${quoteIdent(helperSchema(ctx))} to ${to};
|
|
904
|
+
grant execute on function ${fn} to ${to};`;
|
|
905
|
+
}
|
|
1304
906
|
//#endregion
|
|
1305
907
|
//#region src/cli/sql/global-roles.ts
|
|
1306
908
|
function qualify(name, schema) {
|
|
@@ -1310,7 +912,10 @@ function throughTenant(through, roles) {
|
|
|
1310
912
|
if (through === void 0 || roles?.tenant === void 0) return;
|
|
1311
913
|
return qualify(roles.table, "public") === through.table ? roles.tenant : void 0;
|
|
1312
914
|
}
|
|
1313
|
-
/**
|
|
915
|
+
/**
|
|
916
|
+
* Resolves `source` against `schema`, aliasing the table `alias` and the roles
|
|
917
|
+
* table `<alias>k`.
|
|
918
|
+
*/
|
|
1314
919
|
function globalRoleSource(source, schema, alias, roles) {
|
|
1315
920
|
const table = qualify(source.table, schema);
|
|
1316
921
|
const user = source.user ?? "user_id";
|
|
@@ -1328,6 +933,7 @@ function globalRoleSource(source, schema, alias, roles) {
|
|
|
1328
933
|
from: `${quoteTable(table)} ${alias}${role.join}${global}`,
|
|
1329
934
|
userSql: `${alias}.${quoteIdent(user)}`,
|
|
1330
935
|
roleSql: role.sql,
|
|
936
|
+
nullable: role.nullable,
|
|
1331
937
|
role: roleManifest(role),
|
|
1332
938
|
...role.through === void 0 ? {} : { through: role.through },
|
|
1333
939
|
...tenant === void 0 ? {} : { tenant },
|
|
@@ -1343,11 +949,17 @@ function keyTenantLine(ctx, scope, tenant) {
|
|
|
1343
949
|
const keyed = keyTenantSql(ctx, underRoot(ctx, scope) ? tenant : void 0);
|
|
1344
950
|
return keyed === void 0 ? "" : `\n and ${keyed}`;
|
|
1345
951
|
}
|
|
1346
|
-
/**
|
|
952
|
+
/**
|
|
953
|
+
* Whether the helper for `scope` narrows to the tenant claim: unless
|
|
954
|
+
* `rls.tenants` is `'all'`, when the first scope is on its chain.
|
|
955
|
+
*/
|
|
1347
956
|
function narrowsTo(ctx, scope) {
|
|
1348
957
|
return ctx.tenants !== "all" && underRoot(ctx, scope);
|
|
1349
958
|
}
|
|
1350
|
-
/**
|
|
959
|
+
/**
|
|
960
|
+
* Whether the active tenant narrows memberships of `scope`: the first scope is
|
|
961
|
+
* on its chain.
|
|
962
|
+
*/
|
|
1351
963
|
function underRoot(ctx, scope) {
|
|
1352
964
|
const root = ctx.scopes[0]?.name;
|
|
1353
965
|
let current = ctx.scopes.find((item) => item.name === scope);
|
|
@@ -1360,11 +972,17 @@ function underRoot(ctx, scope) {
|
|
|
1360
972
|
}
|
|
1361
973
|
return false;
|
|
1362
974
|
}
|
|
1363
|
-
/**
|
|
975
|
+
/**
|
|
976
|
+
* `and exists (...)` lines for an active user; empty without
|
|
977
|
+
* `rls.suspension.users`.
|
|
978
|
+
*/
|
|
1364
979
|
function userActive(ctx, indent, user) {
|
|
1365
980
|
return activeUserSql(ctx, user).map((part) => `${indent}and ${part}`);
|
|
1366
981
|
}
|
|
1367
|
-
/**
|
|
982
|
+
/**
|
|
983
|
+
* `and exists (...)` lines for every suspendable instance on the chain of
|
|
984
|
+
* `scope`'s membership.
|
|
985
|
+
*/
|
|
1368
986
|
function instancesActive(ctx, scope, idOf, indent, permission) {
|
|
1369
987
|
return activeInstancesSql(ctx, scope, idOf, permission).map((part) => `${indent}and ${part}`);
|
|
1370
988
|
}
|
|
@@ -1503,7 +1121,10 @@ function hasBody(ctx) {
|
|
|
1503
1121
|
${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()}
|
|
1504
1122
|
))`;
|
|
1505
1123
|
}
|
|
1506
|
-
/**
|
|
1124
|
+
/**
|
|
1125
|
+
* The claim `jwt` mode reads platform custom roles from:
|
|
1126
|
+
* `customRoleClaim(globalRoles)`.
|
|
1127
|
+
*/
|
|
1507
1128
|
const GLOBAL_GRANTS_CLAIM = "role_grants";
|
|
1508
1129
|
function memberColumn(name) {
|
|
1509
1130
|
return `m.${quoteIdent(name)}`;
|
|
@@ -1646,8 +1267,8 @@ ${customKeysSql(ctx, scope, claimEntries("left(e, 1) not in ('-', '@')", "e"), c
|
|
|
1646
1267
|
}
|
|
1647
1268
|
/**
|
|
1648
1269
|
* The rows the membership sources select for the subject, one per instance
|
|
1649
|
-
* (`scope`, `id`, `within`, `roles`, `via`), with each source's own expiry
|
|
1650
|
-
*
|
|
1270
|
+
* (`scope`, `id`, `within`, `roles`, `via`), with each source's own expiry and
|
|
1271
|
+
* suspension filters: the statements the token hook runs.
|
|
1651
1272
|
*/
|
|
1652
1273
|
function sourcesKeep(sources) {
|
|
1653
1274
|
return sources.some((source) => source.sql.keeps);
|
|
@@ -1671,7 +1292,10 @@ function sourcesBodyOf(sql, sources, user) {
|
|
|
1671
1292
|
vars: sources.map((source, index) => `${sourceUser(index)} ${source.sql.userType} := ${user};`)
|
|
1672
1293
|
};
|
|
1673
1294
|
}
|
|
1674
|
-
/**
|
|
1295
|
+
/**
|
|
1296
|
+
* `language sql`, or `plpgsql` returning the query when the body declares
|
|
1297
|
+
* variables.
|
|
1298
|
+
*/
|
|
1675
1299
|
function functionBody(body) {
|
|
1676
1300
|
if (body.vars.length === 0) return `language sql
|
|
1677
1301
|
stable
|
|
@@ -1738,7 +1362,7 @@ ${rows}
|
|
|
1738
1362
|
union
|
|
1739
1363
|
select (ms.id)::${type}, ck.grant_key
|
|
1740
1364
|
${rows}
|
|
1741
|
-
${customKeysJoin(ctx, scope, entries(CUSTOM_ROLES.permissions,
|
|
1365
|
+
${customKeysJoin(ctx, scope, entries(CUSTOM_ROLES.permissions, allowEntry(ctx), " and c.effect = 'allow'"), entries(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), entries(CUSTOM_ROLES.includes, "include_role", ""))}
|
|
1742
1366
|
where ${signedIn(ctx)}
|
|
1743
1367
|
and ms.scope = ${quoteLiteral(scope)}
|
|
1744
1368
|
and not (r.role = any(${sqlTextArray(custom.declared)}))${narrow}${sourceFilters(ctx, scope, subjectIdSql(ctx), grantPermissionSql("ck.grant_key"), sourcesKeep(sources))}`, sources, subjectIdSql(ctx));
|
|
@@ -1749,7 +1373,7 @@ ${rows}
|
|
|
1749
1373
|
where ${signedIn(ctx)}
|
|
1750
1374
|
and ms.scope = ${quoteLiteral(scope)}
|
|
1751
1375
|
and not (r.role = any(${sqlTextArray(custom.declared)}))${narrow}${sourceFilters(ctx, scope, subjectIdSql(ctx), grantPermissionSql("p_grant"), sourcesKeep(sources))}
|
|
1752
|
-
${customKeysSql(ctx, scope, entries(CUSTOM_ROLES.permissions,
|
|
1376
|
+
${customKeysSql(ctx, scope, entries(CUSTOM_ROLES.permissions, allowEntry(ctx), " and c.effect = 'allow'"), entries(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), entries(CUSTOM_ROLES.includes, "include_role", ""))}`, sources, subjectIdSql(ctx));
|
|
1753
1377
|
}
|
|
1754
1378
|
function scopedBody(ctx, scope, type) {
|
|
1755
1379
|
if (ctx.authorize !== "database") return sqlBody(claimBody(ctx, scope, type));
|
|
@@ -1757,7 +1381,10 @@ function scopedBody(ctx, scope, type) {
|
|
|
1757
1381
|
}
|
|
1758
1382
|
//#endregion
|
|
1759
1383
|
//#region src/cli/rls/capabilities.ts
|
|
1760
|
-
/**
|
|
1384
|
+
/**
|
|
1385
|
+
* The helper `--capabilities` adds: ids of one resource a link capability claim
|
|
1386
|
+
* reaches. Part of the SQL contract.
|
|
1387
|
+
*/
|
|
1761
1388
|
const CAPABILITIES = { ids: "permdock_capability_ids" };
|
|
1762
1389
|
/**
|
|
1763
1390
|
* The `anon` branch access for a resource-scoped grant reached through a link
|
|
@@ -1799,6 +1426,137 @@ revoke execute on function ${fn}(text, text, text) from public;
|
|
|
1799
1426
|
grant execute on function ${fn}(text, text, text) to anon, authenticated;`;
|
|
1800
1427
|
}
|
|
1801
1428
|
//#endregion
|
|
1429
|
+
//#region src/cli/rls/custom-role-by-id.ts
|
|
1430
|
+
const PREFIX = "PermDock CLI";
|
|
1431
|
+
const col = (name) => `r.${quoteIdent(name)}`;
|
|
1432
|
+
/**
|
|
1433
|
+
* Custom-role functions keyed by the application's role row
|
|
1434
|
+
* (`rls.customRoleWrites.roles.primaryKey`): the tenant, scope, instance and
|
|
1435
|
+
* name come from the row, never from the caller. The writers delegate to
|
|
1436
|
+
* `permdock_replace_custom_role_grants` (or its trusted variant), so every
|
|
1437
|
+
* caller and definition check applies, and keep the stored denies and includes.
|
|
1438
|
+
* The reader returns declared allow keys only and is executable by
|
|
1439
|
+
* `rls.trustedReaders` alone.
|
|
1440
|
+
*/
|
|
1441
|
+
function customRoleByIdSql(ctx, trustedReaders) {
|
|
1442
|
+
const custom = ctx.customRoles;
|
|
1443
|
+
const roles = custom?.table;
|
|
1444
|
+
const root = ctx.scopes[0]?.name;
|
|
1445
|
+
if (custom === void 0 || roles?.primaryKey === void 0 || ctx.authorize !== "database" || root === void 0) return "";
|
|
1446
|
+
const pk = roles.primaryKey;
|
|
1447
|
+
if (pk === "") throw configError(`${PREFIX}: rls.customRoleWrites.roles.primaryKey names no column`);
|
|
1448
|
+
const leveled = custom.levels === true;
|
|
1449
|
+
const tenantType = tenantTypeOf(ctx);
|
|
1450
|
+
const table = quoteQualified(roles.table);
|
|
1451
|
+
const idType = `${table}.${quoteIdent(pk)}%type`;
|
|
1452
|
+
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
1453
|
+
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
1454
|
+
const replace = qualified(ctx, CUSTOM_ROLES.replace);
|
|
1455
|
+
const trustedReplace = qualified(ctx, CUSTOM_ROLES.trustedReplace);
|
|
1456
|
+
const byId = qualified(ctx, CUSTOM_ROLES.replaceById);
|
|
1457
|
+
const trustedById = qualified(ctx, CUSTOM_ROLES.trustedReplaceById);
|
|
1458
|
+
const reader = qualified(ctx, CUSTOM_ROLES.trustedReadById);
|
|
1459
|
+
const renames = Object.entries(custom.renamed ?? {}).toSorted(byKey);
|
|
1460
|
+
const current = (expr) => currentKeyExpr(renames, expr);
|
|
1461
|
+
const declaredKeys = sqlTextArray(custom.permissions ?? []);
|
|
1462
|
+
const declaredRoles = sqlTextArray(custom.declared);
|
|
1463
|
+
const tenant = roles.tenant === void 0 ? `null::${tenantType}` : `${col(roles.tenant)}::${tenantType}`;
|
|
1464
|
+
const scope = rolesTableScope(roles, tenant, root, col);
|
|
1465
|
+
const scopeId = roles.id === void 0 || roles.tenant === void 0 ? "null::text" : `case when ${tenant} is null then null else ${col(roles.id)}::text end`;
|
|
1466
|
+
const skipped = roles.skip === void 0 || roles.tenant === void 0 ? "false" : `(${tenant} is not null and coalesce(${col(roles.skip)}, false))`;
|
|
1467
|
+
const match = "c.tenant_id is not distinct from v_tenant and c.scope = v_scope and c.scope_id is not distinct from v_scope_id and c.role = v_key";
|
|
1468
|
+
const audit = custom.audit;
|
|
1469
|
+
const auditCall = audit === void 0 ? "" : `
|
|
1470
|
+
perform ${quoteQualified(audit.function)}(${[
|
|
1471
|
+
"event_type => 'permdock.custom_role.replaced'",
|
|
1472
|
+
"target_type => 'role'",
|
|
1473
|
+
"record_id => p_role_id::text",
|
|
1474
|
+
"tenant => v_tenant",
|
|
1475
|
+
"metadata => jsonb_build_object('operation', 'replace', 'role', v_key, 'permissions', p_permissions)",
|
|
1476
|
+
...Object.entries(audit.args ?? {}).toSorted(byKey).map(([name, value]) => `${quoteIdent(name)} => ${quoteLiteral(value)}`)
|
|
1477
|
+
].join(", ")});`;
|
|
1478
|
+
const body = (write) => `declare
|
|
1479
|
+
v_tenant ${tenantType};
|
|
1480
|
+
v_scope text;
|
|
1481
|
+
v_scope_id text;
|
|
1482
|
+
v_key text;
|
|
1483
|
+
v_skipped boolean;
|
|
1484
|
+
v_deny text[];
|
|
1485
|
+
v_include text[];
|
|
1486
|
+
begin
|
|
1487
|
+
select ${tenant}, ${scope}, ${scopeId}, ${col(roles.key)}::text, ${skipped}
|
|
1488
|
+
into v_tenant, v_scope, v_scope_id, v_key, v_skipped
|
|
1489
|
+
from ${table} r
|
|
1490
|
+
where ${col(pk)} = p_role_id;
|
|
1491
|
+
if not found then
|
|
1492
|
+
${raiseSql(" ", "P0002", "'permdock: no role row has id ' || coalesce(p_role_id::text, 'null')", "unknown-role")}
|
|
1493
|
+
end if;
|
|
1494
|
+
if v_skipped or v_key = any(${declaredRoles}) then
|
|
1495
|
+
${raiseSql(" ", "22023", "'permdock: ' || coalesce(v_key, 'null') || ' is a declared role, not a custom role'", "declared-role")}
|
|
1496
|
+
end if;
|
|
1497
|
+
select array_agg(c.permission) into v_deny
|
|
1498
|
+
from ${perms} c
|
|
1499
|
+
where ${match} and c.effect = 'deny';
|
|
1500
|
+
select array_agg(c.include_role) into v_include
|
|
1501
|
+
from ${includes} c
|
|
1502
|
+
where ${match};
|
|
1503
|
+
perform ${write}(v_tenant, v_scope, v_scope_id, v_key, p_permissions, v_deny, v_include);${auditCall}
|
|
1504
|
+
end;`;
|
|
1505
|
+
const writer = (comment, name, write, member) => `-- ${comment}
|
|
1506
|
+
create or replace function ${name}(p_role_id ${idType}, p_permissions text[])
|
|
1507
|
+
returns void
|
|
1508
|
+
language plpgsql
|
|
1509
|
+
volatile
|
|
1510
|
+
security definer
|
|
1511
|
+
set search_path = ''
|
|
1512
|
+
as $$
|
|
1513
|
+
${body(write)}
|
|
1514
|
+
$$;
|
|
1515
|
+
${member ? `revoke execute on function ${name}(${idType}, text[]) from public, anon;
|
|
1516
|
+
grant execute on function ${name}(${idType}, text[]) to authenticated;` : `revoke execute on function ${name}(${idType}, text[]) from public, anon, authenticated;`}`;
|
|
1517
|
+
const readerEntry = leveled ? `${current("c.permission")} || coalesce('@' || c.level, '')` : current("c.permission");
|
|
1518
|
+
const readerGrant = trustedReaders === void 0 || trustedReaders.length === 0 ? "" : `\n${trustedReaderGrants(ctx, `${reader}(text[], ${tenantType})`, trustedReaders)}`;
|
|
1519
|
+
return `${writer("save a custom role by its row id: the row supplies tenant, scope, instance and name; the caller checks of permdock_replace_custom_role_grants apply, and stored denies and includes stay", byId, replace, true)}
|
|
1520
|
+
|
|
1521
|
+
${writer("save a custom role by its row id for a trusted caller: the definition checks without the caller checks", trustedById, trustedReplace, false)}
|
|
1522
|
+
|
|
1523
|
+
-- the declared allow keys of custom roles by their row ids as text, for a trusted reader; with p_tenant, an id of another tenant is refused
|
|
1524
|
+
create or replace function ${reader}(p_role_ids text[], p_tenant ${tenantType} default null)
|
|
1525
|
+
returns table (role_id text, permission text)
|
|
1526
|
+
language plpgsql
|
|
1527
|
+
stable
|
|
1528
|
+
security definer
|
|
1529
|
+
set search_path = ''
|
|
1530
|
+
as $$
|
|
1531
|
+
begin
|
|
1532
|
+
if p_tenant is not null and exists (
|
|
1533
|
+
select 1 from ${table} r
|
|
1534
|
+
where ${col(pk)}::text = any(p_role_ids) and ${tenant} is distinct from p_tenant
|
|
1535
|
+
) then
|
|
1536
|
+
${raiseSql(" ", "42501", "'permdock: a role id belongs to another tenant than ' || p_tenant::text", "not-member")}
|
|
1537
|
+
end if;
|
|
1538
|
+
return query
|
|
1539
|
+
select distinct v.role_id, ${readerEntry}
|
|
1540
|
+
from (
|
|
1541
|
+
select ${col(pk)}::text as role_id, ${tenant} as tenant_id, ${scope} as scope, ${scopeId} as scope_id, ${col(roles.key)}::text as role
|
|
1542
|
+
from ${table} r
|
|
1543
|
+
where ${col(pk)}::text = any(p_role_ids)
|
|
1544
|
+
and not ${skipped}
|
|
1545
|
+
and not (${col(roles.key)}::text = any(${declaredRoles}))
|
|
1546
|
+
) v
|
|
1547
|
+
join ${perms} c
|
|
1548
|
+
on c.tenant_id is not distinct from v.tenant_id
|
|
1549
|
+
and c.scope = v.scope
|
|
1550
|
+
and c.scope_id is not distinct from v.scope_id
|
|
1551
|
+
and c.role = v.role
|
|
1552
|
+
where c.effect = 'allow'
|
|
1553
|
+
and ${current("c.permission")} = any(${declaredKeys})
|
|
1554
|
+
order by 1, 2;
|
|
1555
|
+
end;
|
|
1556
|
+
$$;
|
|
1557
|
+
revoke execute on function ${reader}(text[], ${tenantType}) from public, anon, authenticated;${readerGrant}`;
|
|
1558
|
+
}
|
|
1559
|
+
//#endregion
|
|
1802
1560
|
//#region src/cli/rls/custom-role-cascade.ts
|
|
1803
1561
|
/** `row.column`, for a trigger's `old` or `new`. */
|
|
1804
1562
|
function rowColumn(row, name) {
|
|
@@ -1811,12 +1569,11 @@ function cascadeMatch(prefix) {
|
|
|
1811
1569
|
/**
|
|
1812
1570
|
* The trigger on the application's roles table (`rls.customRoleWrites.roles`):
|
|
1813
1571
|
* a rename, a move to another tenant, scope or instance, or a delete of a
|
|
1814
|
-
* custom role's row carries its grants and includes along. A signed-in
|
|
1815
|
-
*
|
|
1816
|
-
*
|
|
1817
|
-
*
|
|
1818
|
-
*
|
|
1819
|
-
* function owner.
|
|
1572
|
+
* custom role's row carries its grants and includes along. A signed-in caller
|
|
1573
|
+
* at trigger depth 1 passes the write functions' checks: authority over the
|
|
1574
|
+
* stored definition where it was, and, after a move, membership and authority
|
|
1575
|
+
* where it lands and entries inside that scope's ceiling. Other paths (a
|
|
1576
|
+
* migration, a job, a nested trigger) move the rows as the function owner.
|
|
1820
1577
|
*/
|
|
1821
1578
|
function customRoleCascadeSql(ctx) {
|
|
1822
1579
|
const custom = ctx.customRoles;
|
|
@@ -1830,11 +1587,11 @@ function customRoleCascadeSql(ctx) {
|
|
|
1830
1587
|
const target = quoteTable(qualifySqlTable(table.table));
|
|
1831
1588
|
const tenant = (row) => table.tenant === void 0 ? `null::${tenantType}` : `${rowColumn(row, table.tenant)}::${tenantType}`;
|
|
1832
1589
|
const scope = (row) => rolesTableScope(table, tenant(row), root, (name) => rowColumn(row, name));
|
|
1833
|
-
const id = (row) => table.id === void 0 ? "null::text" :
|
|
1834
|
-
const skipped = (row) => table.skip === void 0 ? "false" : `coalesce(${rowColumn(row, table.skip)}, false)`;
|
|
1590
|
+
const id = (row) => table.id === void 0 || table.tenant === void 0 ? "null::text" : `case when ${tenant(row)} is null then null else ${rowColumn(row, table.id)}::text end`;
|
|
1591
|
+
const skipped = (row) => table.skip === void 0 || table.tenant === void 0 ? "false" : `(${tenant(row)} is not null and coalesce(${rowColumn(row, table.skip)}, false))`;
|
|
1835
1592
|
const guard = qualified(ctx, CUSTOM_ROLES.guard);
|
|
1836
1593
|
const entries = qualified(ctx, CUSTOM_ROLES.entries);
|
|
1837
|
-
const allowEntryOf =
|
|
1594
|
+
const allowEntryOf = `c.${allowEntry(ctx)}`;
|
|
1838
1595
|
return `-- the application's roles table: renames, moves and deletes of a custom role carry its grants and includes
|
|
1839
1596
|
create or replace function ${fn}()
|
|
1840
1597
|
returns trigger
|
|
@@ -1913,6 +1670,58 @@ create trigger "permdock_custom_role_cascade"
|
|
|
1913
1670
|
`;
|
|
1914
1671
|
}
|
|
1915
1672
|
//#endregion
|
|
1673
|
+
//#region src/cli/rls/custom-role-guard.ts
|
|
1674
|
+
/** A column of the roles-table row a trigger sees as `new`. */
|
|
1675
|
+
function column(name) {
|
|
1676
|
+
return `new.${quoteIdent(name)}`;
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* The trigger on the application's roles table (`rls.customRoleWrites.roles`)
|
|
1680
|
+
* that refuses a tenant row named like a declared role (`declared-role`) or
|
|
1681
|
+
* like a shared custom role (`name-taken`). A row the `skip` column marks is a
|
|
1682
|
+
* system role of the tenant and passes. Empty without a tenant column: then no
|
|
1683
|
+
* row is a tenant row.
|
|
1684
|
+
*/
|
|
1685
|
+
function customRoleNameGuardSql(ctx) {
|
|
1686
|
+
const custom = ctx.customRoles;
|
|
1687
|
+
const table = custom?.table;
|
|
1688
|
+
const root = ctx.scopes[0]?.name;
|
|
1689
|
+
if (custom === void 0 || table?.tenant === void 0 || root === void 0) return "";
|
|
1690
|
+
const tenant = `${column(table.tenant)}::${tenantTypeOf(ctx)}`;
|
|
1691
|
+
const skipped = table.skip === void 0 ? "false" : `coalesce(${column(table.skip)}, false)`;
|
|
1692
|
+
const fn = qualified(ctx, CUSTOM_ROLES.nameGuard);
|
|
1693
|
+
const target = quoteTable(qualifySqlTable(table.table));
|
|
1694
|
+
const key = `${column(table.key)}::text`;
|
|
1695
|
+
const scope = rolesTableScope(table, tenant, root, column);
|
|
1696
|
+
return `-- the application's roles table: a tenant row cannot take the name of a declared or a shared role
|
|
1697
|
+
create or replace function ${fn}()
|
|
1698
|
+
returns trigger
|
|
1699
|
+
language plpgsql
|
|
1700
|
+
volatile
|
|
1701
|
+
security definer
|
|
1702
|
+
set search_path = ''
|
|
1703
|
+
as $$
|
|
1704
|
+
begin
|
|
1705
|
+
if ${tenant} is null or ${skipped} then
|
|
1706
|
+
return new;
|
|
1707
|
+
end if;
|
|
1708
|
+
if ${key} = any(${sqlTextArray(custom.declared)}) then
|
|
1709
|
+
${raiseSql(" ", "22023", `'permdock: ' || ${key} || ' is a declared role'`, "declared-role")}
|
|
1710
|
+
end if;
|
|
1711
|
+
if ${qualified(ctx, CUSTOM_ROLES.shared)}(${scope}, ${key}) then
|
|
1712
|
+
${raiseSql(" ", "22023", `'permdock: ' || ${key} || ' is a shared role'`, "name-taken")}
|
|
1713
|
+
end if;
|
|
1714
|
+
return new;
|
|
1715
|
+
end;
|
|
1716
|
+
$$;
|
|
1717
|
+
revoke execute on function ${fn}() from public, anon, authenticated;
|
|
1718
|
+
drop trigger if exists "permdock_custom_role_name_guard" on ${target};
|
|
1719
|
+
create trigger "permdock_custom_role_name_guard"
|
|
1720
|
+
before insert or update on ${target}
|
|
1721
|
+
for each row execute function ${fn}();
|
|
1722
|
+
`;
|
|
1723
|
+
}
|
|
1724
|
+
//#endregion
|
|
1916
1725
|
//#region src/cli/rls/custom-role-writes.ts
|
|
1917
1726
|
function callerFunctionsSql(ctx, custom, root, levelReach, forUser) {
|
|
1918
1727
|
const leveled = custom.levels === true;
|
|
@@ -2038,8 +1847,8 @@ revoke execute on function ${guard}(${userArg}${tenantType}, text, text, text) f
|
|
|
2038
1847
|
};
|
|
2039
1848
|
}
|
|
2040
1849
|
/**
|
|
2041
|
-
* The functions an application calls to save, rename and delete a custom
|
|
2042
|
-
*
|
|
1850
|
+
* The functions an application calls to save, rename and delete a custom role
|
|
1851
|
+
* in \`database\` mode, with the rules of \`validateCustomRole\` and
|
|
2043
1852
|
* \`assignablePermissions\`: entries inside the ceiling of the role's scope,
|
|
2044
1853
|
* includes naming declared roles, and only permissions and levels the caller
|
|
2045
1854
|
* may hand out, for the new definition and the stored one alike. A platform
|
|
@@ -2063,6 +1872,7 @@ function customRoleWritesSql(ctx, levelReach) {
|
|
|
2063
1872
|
const guard = qualified(ctx, CUSTOM_ROLES.guard);
|
|
2064
1873
|
const shape = qualified(ctx, CUSTOM_ROLES.shape);
|
|
2065
1874
|
const entries = qualified(ctx, CUSTOM_ROLES.entries);
|
|
1875
|
+
const problem = qualified(ctx, CUSTOM_ROLES.entryProblem);
|
|
2066
1876
|
const renames = Object.entries(custom.renamed ?? {}).toSorted(byKey);
|
|
2067
1877
|
const current = (expr) => currentKeyExpr(renames, expr);
|
|
2068
1878
|
const known = sqlTextArray([...custom.permissions ?? [], ...renames.map(([former]) => former)]);
|
|
@@ -2150,6 +1960,38 @@ ${callerFunctionsSql(ctx, custom, root, levelReach, true).beyond}
|
|
|
2150
1960
|
|
|
2151
1961
|
${callerFunctionsSql(ctx, custom, root, levelReach, true).guard}
|
|
2152
1962
|
` : ""}
|
|
1963
|
+
-- null when an allow entry is a declared permission inside the ceiling of the scope${leveled ? ", at a level the resource declares" : ""}; else unknown-permission, outside-ceiling${leveled ? " or unknown-level" : ""}
|
|
1964
|
+
create or replace function ${problem}(p_scope text, p_entry text)
|
|
1965
|
+
returns text
|
|
1966
|
+
language plpgsql
|
|
1967
|
+
stable
|
|
1968
|
+
security definer
|
|
1969
|
+
set search_path = ''
|
|
1970
|
+
as $$
|
|
1971
|
+
declare
|
|
1972
|
+
v_key text := ${current(leveled ? "split_part(p_entry, '@', 1)" : "p_entry")};
|
|
1973
|
+
begin
|
|
1974
|
+
if p_entry is null or not (v_key = any(${known})) then
|
|
1975
|
+
return 'unknown-permission';
|
|
1976
|
+
end if;
|
|
1977
|
+
if not exists (
|
|
1978
|
+
select 1 from ${ceiling} c
|
|
1979
|
+
where c.scope = p_scope and c.effect = 'allow' and ${current("c.permission")} = v_key
|
|
1980
|
+
) then
|
|
1981
|
+
return 'outside-ceiling';
|
|
1982
|
+
end if;${leveled ? `
|
|
1983
|
+
if strpos(p_entry, '@') > 0 and not exists (
|
|
1984
|
+
select 1 from ${ceiling} c
|
|
1985
|
+
where c.scope = p_scope and c.effect = 'allow' and ${current("c.permission")} = v_key
|
|
1986
|
+
and split_part(c.grant_key, '@', 2) = split_part(p_entry, '@', 2)
|
|
1987
|
+
) then
|
|
1988
|
+
return 'unknown-level';
|
|
1989
|
+
end if;` : ""}
|
|
1990
|
+
return null;
|
|
1991
|
+
end;
|
|
1992
|
+
$$;
|
|
1993
|
+
revoke execute on function ${problem}(text, text) from public, anon, authenticated;
|
|
1994
|
+
|
|
2153
1995
|
-- the entries of a definition: declared keys and levels inside the ceiling of the scope, and declared includes
|
|
2154
1996
|
create or replace function ${entries}(p_scope text, p_allow text[], p_deny text[], p_include text[])
|
|
2155
1997
|
returns void
|
|
@@ -2161,23 +2003,18 @@ as $$
|
|
|
2161
2003
|
declare
|
|
2162
2004
|
v_entry text;
|
|
2163
2005
|
v_key text;
|
|
2006
|
+
v_problem text;
|
|
2164
2007
|
begin
|
|
2165
2008
|
foreach v_entry in array coalesce(p_allow, '{}'::text[]) loop
|
|
2166
2009
|
v_key := ${current(leveled ? "split_part(v_entry, '@', 1)" : "v_entry")};
|
|
2167
|
-
|
|
2010
|
+
v_problem := ${problem}(p_scope, v_entry);
|
|
2011
|
+
if v_problem = 'unknown-permission' then
|
|
2168
2012
|
${raiseSql(" ", "22023", `'permdock: ' || v_entry || ' is not a declared permission'`, "unknown-permission")}
|
|
2169
2013
|
end if;
|
|
2170
|
-
if
|
|
2171
|
-
select 1 from ${ceiling} c
|
|
2172
|
-
where c.scope = p_scope and c.effect = 'allow' and ${current("c.permission")} = v_key
|
|
2173
|
-
) then
|
|
2014
|
+
if v_problem = 'outside-ceiling' then
|
|
2174
2015
|
${raiseSql(" ", "22023", `'permdock: ' || v_key || ' is outside the ceiling of ' || p_scope`, "outside-ceiling")}
|
|
2175
2016
|
end if;${leveled ? `
|
|
2176
|
-
if
|
|
2177
|
-
select 1 from ${ceiling} c
|
|
2178
|
-
where c.scope = p_scope and c.effect = 'allow' and ${current("c.permission")} = v_key
|
|
2179
|
-
and split_part(c.grant_key, '@', 2) = split_part(v_entry, '@', 2)
|
|
2180
|
-
) then
|
|
2017
|
+
if v_problem = 'unknown-level' then
|
|
2181
2018
|
${raiseSql(" ", "22023", `'permdock: ' || v_entry || ' names a level the resource does not declare'`, "unknown-level")}
|
|
2182
2019
|
end if;` : ""}
|
|
2183
2020
|
end loop;
|
|
@@ -2249,9 +2086,9 @@ end;`, false)}`;
|
|
|
2249
2086
|
//#region src/cli/rls/global-roles.ts
|
|
2250
2087
|
/**
|
|
2251
2088
|
* `permdock_replace_global_roles(p_user, p_roles)`: sets the global roles of
|
|
2252
|
-
* one user in `rls.roles` in one statement pair. It runs as the invoker, so
|
|
2253
|
-
*
|
|
2254
|
-
*
|
|
2089
|
+
* one user in `rls.roles` in one statement pair. It runs as the invoker, so the
|
|
2090
|
+
* table's policies and the `rls.assignments` trigger judge every row it deletes
|
|
2091
|
+
* or inserts.
|
|
2255
2092
|
*/
|
|
2256
2093
|
function globalRolesSql(ctx) {
|
|
2257
2094
|
if (ctx.roles === void 0) return "";
|
|
@@ -2324,9 +2161,9 @@ grant execute on function ${fn}(${args}) to authenticated;`;
|
|
|
2324
2161
|
//#endregion
|
|
2325
2162
|
//#region src/cli/rls/helper-members.ts
|
|
2326
2163
|
/**
|
|
2327
|
-
* The body of `member_<scope>_ids()`: instances of `scope` the subject holds
|
|
2328
|
-
*
|
|
2329
|
-
*
|
|
2164
|
+
* The body of `member_<scope>_ids()`: instances of `scope` the subject holds a
|
|
2165
|
+
* live membership of, with any role. Not narrowed to the active tenant, and no
|
|
2166
|
+
* cascade: a membership of a child scope is not one of its parent.
|
|
2330
2167
|
*/
|
|
2331
2168
|
function memberBody(ctx, scope, type, roles) {
|
|
2332
2169
|
if (ctx.authorize !== "database") return sqlBody(` select distinct (m ->> 'id')::${type}
|
|
@@ -2426,8 +2263,8 @@ ${grants}`;
|
|
|
2426
2263
|
/**
|
|
2427
2264
|
* `member_<scope>_ids_for(p_user uuid)`: `member_<scope>_ids()` for a user the
|
|
2428
2265
|
* caller names, read from the membership sources (or the mapped table) in
|
|
2429
|
-
* either mode, so a token hook can call it before any claim exists. Naming
|
|
2430
|
-
*
|
|
2266
|
+
* either mode, so a token hook can call it before any claim exists. Naming the
|
|
2267
|
+
* user is why no client role may execute it; the hook's grants give
|
|
2431
2268
|
* `supabase_auth_admin` execute.
|
|
2432
2269
|
*/
|
|
2433
2270
|
function memberForFunction(ctx, scope, type) {
|
|
@@ -2437,18 +2274,125 @@ returns setof ${type}
|
|
|
2437
2274
|
${functionBody(memberRowsBody(ctx, scope, type, "p_user", memberForSources(ctx, scope)))}
|
|
2438
2275
|
revoke execute on function ${fn}(uuid) from public, anon, authenticated;`;
|
|
2439
2276
|
}
|
|
2277
|
+
/**
|
|
2278
|
+
* `member_<scope>_ids_of(p_user uuid)`: `member_<scope>_ids_for(p_user)` for
|
|
2279
|
+
* the caller only. Any other `p_user` returns no rows, so a client role may
|
|
2280
|
+
* execute it without learning another user's memberships.
|
|
2281
|
+
*/
|
|
2282
|
+
function memberOfFunction(ctx, scope, type) {
|
|
2283
|
+
const fn = qualified(ctx, memberOfHelper(scope));
|
|
2284
|
+
return `create or replace function ${fn}(p_user uuid)
|
|
2285
|
+
returns setof ${type}
|
|
2286
|
+
${functionBody(sqlBody(` select m.id
|
|
2287
|
+
from ${qualified(ctx, memberForHelper(scope))}(p_user) as m(id)
|
|
2288
|
+
where p_user = ${subjectIdSql(ctx)}`))}
|
|
2289
|
+
revoke execute on function ${fn}(uuid) from public, anon;
|
|
2290
|
+
grant execute on function ${fn}(uuid) to authenticated;`;
|
|
2291
|
+
}
|
|
2292
|
+
//#endregion
|
|
2293
|
+
//#region src/cli/rls/token-scopes.ts
|
|
2294
|
+
const TOKEN_SCOPE_ALLOWS = "permdock_token_scope_allows";
|
|
2295
|
+
/**
|
|
2296
|
+
* `rls.tokenScopes` as the SQL generator needs it, or `undefined` when it is
|
|
2297
|
+
* off.
|
|
2298
|
+
*/
|
|
2299
|
+
function tokenScopesPlan(setting, policy) {
|
|
2300
|
+
if (setting !== true) return;
|
|
2301
|
+
const permissions = listPermissions(policy.permissions).map((leaf) => ({
|
|
2302
|
+
key: leaf.key,
|
|
2303
|
+
scope: leaf.scope,
|
|
2304
|
+
renamedFrom: formerKeys(leaf)
|
|
2305
|
+
}));
|
|
2306
|
+
return { scopes: delegatedScopeMap(permissions, policy.oauthScopes) };
|
|
2307
|
+
}
|
|
2308
|
+
function tokenScopeAllowsCall(ctx, grant) {
|
|
2309
|
+
return `${qualified(ctx, TOKEN_SCOPE_ALLOWS)}(${grant})`;
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* The token's scopes as rows of `s(scope)`: a space-separated string or,
|
|
2313
|
+
* outside `guc`, a JSON array.
|
|
2314
|
+
*/
|
|
2315
|
+
function tokenScopeRows(ctx) {
|
|
2316
|
+
if (ctx.dialect === "guc") return `regexp_split_to_table(btrim(coalesce(${subjectClaimSql(ctx, "scope")}, '')), '\\s+') s(scope)`;
|
|
2317
|
+
const scope = subjectClaimJsonSql(ctx, "scope");
|
|
2318
|
+
return `unnest(case jsonb_typeof(${scope})
|
|
2319
|
+
when 'string' then regexp_split_to_array(btrim(${scope} #>> '{}'), '\\s+')
|
|
2320
|
+
when 'array' then array(select jsonb_array_elements_text(${scope}))
|
|
2321
|
+
else '{}'::text[]
|
|
2322
|
+
end) s(scope)`;
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* `permdock_token_scope_allows(p_grant)`: a delegated token (a `client_id`, or
|
|
2326
|
+
* an `act` that is not a support or impersonation session) reaches a grant key
|
|
2327
|
+
* only when one of its scopes maps to the key's permission. better-supabase's
|
|
2328
|
+
* `cap_allows` reads the same tokens the same way.
|
|
2329
|
+
*/
|
|
2330
|
+
function tokenScopeAllowsSql(ctx, plan, anonExecute) {
|
|
2331
|
+
const fn = qualified(ctx, TOKEN_SCOPE_ALLOWS);
|
|
2332
|
+
const act = subjectClaimJsonSql(ctx, "act");
|
|
2333
|
+
const grants = anonExecute ? `revoke execute on function ${fn}(text) from public;
|
|
2334
|
+
grant execute on function ${fn}(text) to anon, authenticated;` : `revoke execute on function ${fn}(text) from public, anon;
|
|
2335
|
+
grant execute on function ${fn}(text) to authenticated;`;
|
|
2336
|
+
return `-- Token scopes: a delegated token holds only the permissions its scope claim maps to; denies always apply, and a token without client_id or an agent act is not limited
|
|
2337
|
+
create or replace function ${fn}(p_grant text)
|
|
2338
|
+
returns boolean
|
|
2339
|
+
language sql
|
|
2340
|
+
stable
|
|
2341
|
+
security definer
|
|
2342
|
+
set search_path = ''
|
|
2343
|
+
as $$
|
|
2344
|
+
select case
|
|
2345
|
+
when coalesce(${subjectClaimSql(ctx, "client_id")}, '') = ''
|
|
2346
|
+
and (${act} is null
|
|
2347
|
+
or ${act} = 'null'::jsonb
|
|
2348
|
+
or (jsonb_typeof(${act}) = 'object'
|
|
2349
|
+
and ${act} ->> 'kind' in ('support', 'impersonation'))) then true
|
|
2350
|
+
when exists (
|
|
2351
|
+
select 1 from ${qualified(ctx, "role_permissions")} rp
|
|
2352
|
+
where rp.grant_key = p_grant and rp.effect = 'deny'
|
|
2353
|
+
) then true
|
|
2354
|
+
else exists (
|
|
2355
|
+
select 1
|
|
2356
|
+
from ${tokenScopeRows(ctx)}
|
|
2357
|
+
where s.scope <> ''
|
|
2358
|
+
and coalesce(${quoteLiteral(JSON.stringify(plan.scopes))}::jsonb -> s.scope, '[]'::jsonb)
|
|
2359
|
+
? split_part(split_part(p_grant, '#', 1), '@', 1)
|
|
2360
|
+
)
|
|
2361
|
+
end
|
|
2362
|
+
$$;
|
|
2363
|
+
${grants}`;
|
|
2364
|
+
}
|
|
2440
2365
|
//#endregion
|
|
2441
2366
|
//#region src/cli/rls/helpers.ts
|
|
2442
2367
|
/**
|
|
2443
|
-
* The policy-side call for a grant key. Every form is uncorrelated, so
|
|
2444
|
-
*
|
|
2445
|
-
*
|
|
2368
|
+
* The policy-side call for a grant key. Every form is uncorrelated, so Postgres
|
|
2369
|
+
* runs the helper once per statement (an InitPlan, or a hashed SubPlan for
|
|
2370
|
+
* `in`), never once per row.
|
|
2446
2371
|
*/
|
|
2447
|
-
function accessSql(ctx, scope, grantKey, column) {
|
|
2372
|
+
function accessSql(ctx, scope, grantKey, column, helper) {
|
|
2448
2373
|
const key = quoteLiteral(grantKey);
|
|
2449
2374
|
if (scope === "global") return `(select ${qualified(ctx, HELPERS.has)}(${key}))`;
|
|
2450
2375
|
if (column === void 0) throw configError(`PermDock CLI: ${scope}-scoped grant needs definePolicy({ scopes.${scope} })`);
|
|
2451
|
-
return `${quoteIdent(column)} in (select ${qualified(ctx, permittedIdsHelper(scope))}(${key}))`;
|
|
2376
|
+
return `${quoteIdent(column)} in (select ${qualified(ctx, helper ?? permittedIdsHelper(scope))}(${key}))`;
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* `permitted_<parent>_ids(p_grant)` for each \`rls.tables\` \`via\`: the parent
|
|
2380
|
+
* rows whose scope instance the grant reaches. \`security definer\`, so the
|
|
2381
|
+
* caller needs no read on the parent, and called uncorrelated like
|
|
2382
|
+
* \`permitted_<scope>_ids\`.
|
|
2383
|
+
*/
|
|
2384
|
+
function viaHelpersSql(ctx, anonExecute) {
|
|
2385
|
+
const done = /* @__PURE__ */ new Set();
|
|
2386
|
+
const chunks = [];
|
|
2387
|
+
for (const via of Object.values(ctx.vias ?? {})) {
|
|
2388
|
+
if (done.has(via.helper)) continue;
|
|
2389
|
+
done.add(via.helper);
|
|
2390
|
+
chunks.push(`-- rows of ${via.table} whose ${via.column} is a ${via.scope} the grant reaches
|
|
2391
|
+
${helperFunction(ctx, via.helper, `setof ${via.type}`, sqlBody(` select p.${quoteIdent(via.id)}
|
|
2392
|
+
from ${quoteQualified(via.table)} p
|
|
2393
|
+
where p.${quoteIdent(via.column)} in (select ${qualified(ctx, permittedIdsHelper(via.scope))}(p_grant))`), anonExecute)}`);
|
|
2394
|
+
}
|
|
2395
|
+
return chunks;
|
|
2452
2396
|
}
|
|
2453
2397
|
function userIdSql(ctx, anonExecute) {
|
|
2454
2398
|
const fn = userIdHelperSql(ctx);
|
|
@@ -2482,11 +2426,11 @@ ${grants}`;
|
|
|
2482
2426
|
* `permdock_has_for(p_user, p_grant)` and one
|
|
2483
2427
|
* `permitted_<scope>_ids_for(p_user, p_grant)` per scope in `database` mode:
|
|
2484
2428
|
* the bodies of `permdock_has` and `permitted_<scope>_ids` with `p_user` for
|
|
2485
|
-
* the signed-in user and no tenant-claim narrowing, since a named user has
|
|
2486
|
-
*
|
|
2487
|
-
*
|
|
2488
|
-
*
|
|
2489
|
-
*
|
|
2429
|
+
* the signed-in user and no tenant-claim narrowing, since a named user has no
|
|
2430
|
+
* token. For trusted SQL that acts for a stored user (jobs, triggers, approvals
|
|
2431
|
+
* resolved later). Naming the user is why no client role may execute them:
|
|
2432
|
+
* `security definer` callers run them as their owner, and a backend role needs
|
|
2433
|
+
* its own grant.
|
|
2490
2434
|
*/
|
|
2491
2435
|
function forUserSql(ctx) {
|
|
2492
2436
|
if (ctx.authorize !== "database") return "";
|
|
@@ -2509,8 +2453,8 @@ function userIdType(ctx) {
|
|
|
2509
2453
|
return ctx.dialect === "supabase" ? "uuid not null references auth.users on delete cascade" : "text not null";
|
|
2510
2454
|
}
|
|
2511
2455
|
/**
|
|
2512
|
-
* The `role_permissions` rows the policy compiles to: an upsert and a delete
|
|
2513
|
-
*
|
|
2456
|
+
* The `role_permissions` rows the policy compiles to: an upsert and a delete of
|
|
2457
|
+
* every other row. Data, not schema, so `supabase db diff` drops it from a
|
|
2514
2458
|
* declarative schema; `--split ...,seeds` writes it as its own migration.
|
|
2515
2459
|
*/
|
|
2516
2460
|
function seedSql(ctx, rows) {
|
|
@@ -2536,8 +2480,45 @@ ${rows.map((row) => ` (${[
|
|
|
2536
2480
|
);`;
|
|
2537
2481
|
}
|
|
2538
2482
|
/**
|
|
2539
|
-
* `role_permissions
|
|
2540
|
-
*
|
|
2483
|
+
* `role_permissions` as a view over the compiled rows (`rls.seeds: 'view'`).
|
|
2484
|
+
* `security_invoker` makes the view read as its caller, so it adds no
|
|
2485
|
+
* privilege; the table's revoke applies to the view the same way.
|
|
2486
|
+
*/
|
|
2487
|
+
function seedViewSql(ctx, rows) {
|
|
2488
|
+
const view = qualified(ctx, "role_permissions");
|
|
2489
|
+
const columns = "role, permission, grant_key, scope, effect";
|
|
2490
|
+
return `-- rls.seeds: 'view': role_permissions is a view, so the rows travel with the schema
|
|
2491
|
+
create or replace view ${view} (${columns}) with (security_invoker = true) as
|
|
2492
|
+
${rows.length === 0 ? `select null::text, null::text, null::text, null::text, null::text where false` : `select * from (values\n${rows.map((row) => ` (${[
|
|
2493
|
+
row.role,
|
|
2494
|
+
row.permission,
|
|
2495
|
+
row.grantKey,
|
|
2496
|
+
row.scope,
|
|
2497
|
+
row.effect
|
|
2498
|
+
].map(quoteLiteral).join(", ")})`).join(",\n")}\n) as v(${columns})`};
|
|
2499
|
+
revoke all on table ${view} from anon, authenticated, public;`;
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* `<schema>.seed()`: the same upsert and delete as `seedSql`, for a declarative
|
|
2503
|
+
* schema that carries no rows.
|
|
2504
|
+
*/
|
|
2505
|
+
function seedFunctionSql(ctx, rows) {
|
|
2506
|
+
const fn = qualified(ctx, "seed");
|
|
2507
|
+
return `-- rls.seeds: 'function': the role_permissions rows are data, so a declarative diff leaves them out; run select ${fn}(); after each deploy
|
|
2508
|
+
create or replace function ${fn}()
|
|
2509
|
+
returns void
|
|
2510
|
+
language sql
|
|
2511
|
+
volatile
|
|
2512
|
+
security definer
|
|
2513
|
+
set search_path = ''
|
|
2514
|
+
as $permdock$
|
|
2515
|
+
${seedSql(ctx, rows)}
|
|
2516
|
+
$permdock$;
|
|
2517
|
+
revoke execute on function ${fn}() from public, anon, authenticated;`;
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* `role_permissions`, its seeds, and the three `security definer` helpers with
|
|
2521
|
+
* `search_path = ''`. `execute` goes to `authenticated` only.
|
|
2541
2522
|
*/
|
|
2542
2523
|
function helpersSql(ctx, rows, options) {
|
|
2543
2524
|
const schema = helperSchema(ctx);
|
|
@@ -2545,9 +2526,13 @@ function helpersSql(ctx, rows, options) {
|
|
|
2545
2526
|
const rp = qualified(ctx, "role_permissions");
|
|
2546
2527
|
const chunks = [`-- permdock helpers (${ctx.authorize === "database" ? "database: reads the membership and user_roles tables" : "jwt: reads the role and memberships claims"})
|
|
2547
2528
|
-- policies call them uncorrelated, so Postgres evaluates each once per statement`];
|
|
2548
|
-
if (schema !== "public")
|
|
2529
|
+
if (schema !== "public") {
|
|
2530
|
+
const anonUsage = options.anonExecute === true || ctx.capabilities === true;
|
|
2531
|
+
chunks.push(`create schema if not exists ${s};\nrevoke all on schema ${s} from public;\ngrant usage on schema ${s} to ${anonUsage ? "anon, authenticated" : "authenticated"};`);
|
|
2532
|
+
}
|
|
2549
2533
|
if (ctx.dialect === "supabase") chunks.push(userIdSql(ctx, options.anonExecute === true));
|
|
2550
|
-
|
|
2534
|
+
if (ctx.seeds === "view") chunks.push(seedViewSql(ctx, rows));
|
|
2535
|
+
else chunks.push(`create table if not exists ${rp} (
|
|
2551
2536
|
role text not null,
|
|
2552
2537
|
permission text not null,
|
|
2553
2538
|
grant_key text not null,
|
|
@@ -2557,7 +2542,8 @@ function helpersSql(ctx, rows, options) {
|
|
|
2557
2542
|
);
|
|
2558
2543
|
alter table ${rp} enable row level security;
|
|
2559
2544
|
revoke all on table ${rp} from anon, authenticated, public;`);
|
|
2560
|
-
if (
|
|
2545
|
+
if (ctx.seeds === "function") chunks.push(seedFunctionSql(ctx, rows));
|
|
2546
|
+
else if (options.withoutSeeds !== true && ctx.seeds !== "view") chunks.push(seedSql(ctx, rows));
|
|
2561
2547
|
if (ctx.authorize === "database" && options.userRoles && ctx.roles === void 0) {
|
|
2562
2548
|
const ur = qualified(ctx, "user_roles");
|
|
2563
2549
|
chunks.push(`create table if not exists ${ur} (
|
|
@@ -2573,6 +2559,7 @@ revoke all on table ${ur} from anon, authenticated, public;`);
|
|
|
2573
2559
|
const anon = options.anonExecute === true;
|
|
2574
2560
|
const keys = ctx.apiKeys;
|
|
2575
2561
|
if (keys !== void 0) chunks.push(apiKeyAllowsSql(ctx, keys, anon));
|
|
2562
|
+
if (ctx.tokenScopes !== void 0) chunks.push(tokenScopeAllowsSql(ctx, ctx.tokenScopes, anon));
|
|
2576
2563
|
const bodies = grantBodies(ctx);
|
|
2577
2564
|
chunks.push(helperFunction(ctx, HELPERS.has, "boolean", bodies.has, anon));
|
|
2578
2565
|
const capabilities = capabilitiesSql(ctx);
|
|
@@ -2583,7 +2570,9 @@ revoke all on table ${ur} from anon, authenticated, public;`);
|
|
|
2583
2570
|
chunks.push(memberFunction(ctx, scope.name, type, anon));
|
|
2584
2571
|
if (ctx.roleMemberScopes?.includes(scope.name) === true) chunks.push(memberRolesFunction(ctx, scope.name, type, anon));
|
|
2585
2572
|
if (hasMemberFor(ctx, scope.name)) chunks.push(memberForFunction(ctx, scope.name, type));
|
|
2573
|
+
if (hasMemberOf(ctx, scope.name)) chunks.push(memberOfFunction(ctx, scope.name, type));
|
|
2586
2574
|
}
|
|
2575
|
+
chunks.push(...viaHelpersSql(ctx, anon));
|
|
2587
2576
|
const forUser = forUserSql(ctx);
|
|
2588
2577
|
if (forUser !== "") chunks.push(forUser);
|
|
2589
2578
|
const writes = customRoleWritesSql(ctx, options.levelReach ?? []);
|
|
@@ -2591,6 +2580,12 @@ revoke all on table ${ur} from anon, authenticated, public;`);
|
|
|
2591
2580
|
chunks.push(writes);
|
|
2592
2581
|
const cascade = customRoleCascadeSql(ctx);
|
|
2593
2582
|
if (cascade !== "") chunks.push(cascade);
|
|
2583
|
+
const sync = ctx.customRoles?.sync;
|
|
2584
|
+
if (sync !== void 0) chunks.push(sync);
|
|
2585
|
+
const byId = customRoleByIdSql(ctx, options.trustedReaders);
|
|
2586
|
+
if (byId !== "") chunks.push(byId);
|
|
2587
|
+
const nameGuard = customRoleNameGuardSql(ctx);
|
|
2588
|
+
if (nameGuard !== "") chunks.push(nameGuard);
|
|
2594
2589
|
}
|
|
2595
2590
|
const globalRoles = globalRolesSql(ctx);
|
|
2596
2591
|
if (globalRoles !== "") chunks.push(globalRoles);
|
|
@@ -2600,20 +2595,26 @@ revoke all on table ${ur} from anon, authenticated, public;`);
|
|
|
2600
2595
|
}
|
|
2601
2596
|
function grantBodies(ctx) {
|
|
2602
2597
|
const keys = ctx.apiKeys;
|
|
2598
|
+
const scoped = ctx.tokenScopes !== void 0;
|
|
2603
2599
|
const set = ctx.grantSet === true;
|
|
2604
|
-
const
|
|
2605
|
-
|
|
2600
|
+
const ceiling = (grant) => {
|
|
2601
|
+
const calls = [...keys === void 0 ? [] : [apiKeyAllowsCall(ctx, grant)], ...scoped ? [tokenScopeAllowsCall(ctx, grant)] : []];
|
|
2602
|
+
return calls.length === 0 ? void 0 : calls.join(" and ");
|
|
2603
|
+
};
|
|
2604
|
+
const allows = ceiling(set ? "ids.grant_key" : "p_grant");
|
|
2605
|
+
const keyTenant = keys === void 0 ? "" : ` and ${keyTenantSql(ctx, void 0) ?? "true"}`;
|
|
2606
|
+
const hasAllows = ceiling(set ? "g.grant_key" : "p_grant");
|
|
2606
2607
|
return {
|
|
2607
|
-
has: sqlBody(
|
|
2608
|
+
has: sqlBody(hasAllows === void 0 ? hasBody(ctx) : set ? ` select g.grant_key
|
|
2608
2609
|
from (
|
|
2609
2610
|
${hasBody(ctx)}
|
|
2610
2611
|
) g(grant_key)
|
|
2611
|
-
where ${
|
|
2612
|
+
where ${hasAllows}${keyTenant}` : ceilingHasSql(hasBody(ctx), `${hasAllows}${keyTenant}`)),
|
|
2612
2613
|
ids: (scope) => {
|
|
2613
2614
|
const type = scopeTypeOf(ctx, scope);
|
|
2614
2615
|
const body = scopedBody(ctx, scope, type);
|
|
2615
|
-
if (
|
|
2616
|
-
const sql = scope === rootName(ctx) ? `${body.sql}\n union\n${serviceKeyIdsSql(ctx, keys, scope, type)}` : body.sql;
|
|
2616
|
+
if (allows === void 0) return body;
|
|
2617
|
+
const sql = keys !== void 0 && scope === rootName(ctx) ? `${body.sql}\n union\n${serviceKeyIdsSql(ctx, keys, scope, type)}` : body.sql;
|
|
2617
2618
|
return {
|
|
2618
2619
|
sql: set ? ` select ids.id, ids.grant_key
|
|
2619
2620
|
from (
|
|
@@ -2626,7 +2627,7 @@ ${sql}
|
|
|
2626
2627
|
};
|
|
2627
2628
|
}
|
|
2628
2629
|
function forUserContext(ctx) {
|
|
2629
|
-
const { apiKeys: _apiKeys, ...unkeyed } = ctx;
|
|
2630
|
+
const { apiKeys: _apiKeys, tokenScopes: _tokenScopes, ...unkeyed } = ctx;
|
|
2630
2631
|
return {
|
|
2631
2632
|
...unkeyed,
|
|
2632
2633
|
subjectId: "p_user",
|
|
@@ -2635,18 +2636,27 @@ function forUserContext(ctx) {
|
|
|
2635
2636
|
}
|
|
2636
2637
|
//#endregion
|
|
2637
2638
|
//#region src/cli/rls/permission-keys.ts
|
|
2638
|
-
/**
|
|
2639
|
+
/**
|
|
2640
|
+
* The helpers that take a permission key instead of a grant key. Names are part
|
|
2641
|
+
* of the SQL contract.
|
|
2642
|
+
*/
|
|
2639
2643
|
const PERMISSION_HELPERS = {
|
|
2640
2644
|
grantKeys: "grant_keys",
|
|
2641
2645
|
has: "permdock_has_permission",
|
|
2642
2646
|
hasFor: "permdock_has_permission_for"
|
|
2643
2647
|
};
|
|
2644
|
-
/**
|
|
2648
|
+
/**
|
|
2649
|
+
* `permitted_<scope>_permission_keys(p_id)`: every permission key the subject
|
|
2650
|
+
* holds on one instance.
|
|
2651
|
+
*/
|
|
2645
2652
|
function permissionKeysHelper(scope) {
|
|
2646
2653
|
permittedIdsHelper(scope);
|
|
2647
2654
|
return `permitted_${scope}_permission_keys`;
|
|
2648
2655
|
}
|
|
2649
|
-
/**
|
|
2656
|
+
/**
|
|
2657
|
+
* `permitted_<scope>_permission_keys_for(p_user, p_id)`, written in `database`
|
|
2658
|
+
* mode.
|
|
2659
|
+
*/
|
|
2650
2660
|
function permissionKeysForHelper(scope) {
|
|
2651
2661
|
return `${permissionKeysHelper(scope)}_for`;
|
|
2652
2662
|
}
|
|
@@ -2666,7 +2676,10 @@ function requiresSql(ctx, key, resource) {
|
|
|
2666
2676
|
return `${quoteIdent(field)} in (select ${qualified(ctx, checkName(permittedByPermissionHelper(scope)))}(${literal}))`;
|
|
2667
2677
|
})].join(" or ")})`;
|
|
2668
2678
|
}
|
|
2669
|
-
/**
|
|
2679
|
+
/**
|
|
2680
|
+
* Postgres truncates an identifier past 63 bytes, which would merge two
|
|
2681
|
+
* helpers.
|
|
2682
|
+
*/
|
|
2670
2683
|
function checkName(name) {
|
|
2671
2684
|
if (new TextEncoder().encode(name).length > 63) throw configError(`PermDock CLI: the helper name ${name} is longer than 63 bytes; use a shorter scope name`);
|
|
2672
2685
|
return name;
|
|
@@ -2717,17 +2730,17 @@ ${access}`;
|
|
|
2717
2730
|
}
|
|
2718
2731
|
/**
|
|
2719
2732
|
* The permission-key forms of the helpers: `grant_keys`, which maps a
|
|
2720
|
-
* permission key to the grant keys of its unconditional allows on a scope
|
|
2721
|
-
*
|
|
2733
|
+
* permission key to the grant keys of its unconditional allows on a scope (or
|
|
2734
|
+
* every deny key, or the conditioned allow or deny keys),
|
|
2722
2735
|
* `permdock_has_permission(p_permission)` and one
|
|
2723
2736
|
* `permitted_<scope>_ids_by_permission(p_permission)` per scope, plus their
|
|
2724
2737
|
* `_for(p_user, ...)` forms in `database` mode. They answer with the
|
|
2725
|
-
* unconditional allows minus any deny, so a permission whose allows all carry
|
|
2726
|
-
*
|
|
2738
|
+
* unconditional allows minus any deny, so a permission whose allows all carry a
|
|
2739
|
+
* row condition answers nothing. The
|
|
2727
2740
|
* `permitted_<scope>_ids_by_permission(p_permission, p_conditioned)` overload
|
|
2728
2741
|
* with `true` also lists the instances a conditioned allow reaches and
|
|
2729
|
-
* subtracts only unconditional denies, leaving the row condition to the
|
|
2730
|
-
*
|
|
2742
|
+
* subtracts only unconditional denies, leaving the row condition to the caller,
|
|
2743
|
+
* as `permittedIds(..., { conditioned: true })` does in process.
|
|
2731
2744
|
*/
|
|
2732
2745
|
function permissionHelpersSql(ctx, grants, renamed, anonExecute) {
|
|
2733
2746
|
const keys = qualified(ctx, PERMISSION_HELPERS.grantKeys);
|
|
@@ -2851,7 +2864,10 @@ ${keysFunction(keysFor, `p_user ${user}, p_id ${type}, p_keys text[]`, keysBody(
|
|
|
2851
2864
|
}
|
|
2852
2865
|
//#endregion
|
|
2853
2866
|
//#region src/cli/rls/ownership-names.ts
|
|
2854
|
-
/**
|
|
2867
|
+
/**
|
|
2868
|
+
* Objects the ownership rules add next to the helpers. Names are part of the
|
|
2869
|
+
* SQL contract.
|
|
2870
|
+
*/
|
|
2855
2871
|
const OWNERSHIP = {
|
|
2856
2872
|
canAssign: "permdock_can_assign",
|
|
2857
2873
|
canAssignFor: "permdock_can_assign_for",
|
|
@@ -2867,231 +2883,14 @@ const ASSIGNMENTS = {
|
|
|
2867
2883
|
trigger: "permdock_assignment"
|
|
2868
2884
|
};
|
|
2869
2885
|
//#endregion
|
|
2870
|
-
//#region src/cli/rls/assignments.ts
|
|
2871
|
-
/** The client roles whose writes the assignment triggers check; any other role is a trusted path. */
|
|
2872
|
-
const CLIENT_ROLES = [
|
|
2873
|
-
"anon",
|
|
2874
|
-
"anonymous",
|
|
2875
|
-
"authenticated"
|
|
2876
|
-
];
|
|
2877
|
-
/** The mapped membership tables, `rls.assignments.tables` and the global-roles table `rls.roles`. */
|
|
2878
|
-
function assignedTables(ctx) {
|
|
2879
|
-
const out = [];
|
|
2880
|
-
for (const { name } of ctx.scopes) {
|
|
2881
|
-
const mapped = scopeTable(ctx, name);
|
|
2882
|
-
if (mapped === void 0) continue;
|
|
2883
|
-
out.push({
|
|
2884
|
-
table: mapped.table.table,
|
|
2885
|
-
scope: name,
|
|
2886
|
-
id: mapped.column,
|
|
2887
|
-
tenant: mapped.tenantColumn ?? mapped.column,
|
|
2888
|
-
role: mapped.table.role,
|
|
2889
|
-
user: mapped.table.user
|
|
2890
|
-
});
|
|
2891
|
-
}
|
|
2892
|
-
if (ctx.roles !== void 0) out.push({
|
|
2893
|
-
table: ctx.roles.table,
|
|
2894
|
-
scope: "global",
|
|
2895
|
-
role: ctx.roles.role ?? "role",
|
|
2896
|
-
user: ctx.roles.user ?? "user_id"
|
|
2897
|
-
});
|
|
2898
|
-
for (const extra of ctx.assignments?.tables ?? []) {
|
|
2899
|
-
const scope = resolveScope(ctx.scopes, extra.scope);
|
|
2900
|
-
if (scope === void 0) throw configError(`PermDock CLI: rls.assignments.tables names scope '${extra.scope}', which the policy does not declare`);
|
|
2901
|
-
if (scope !== ctx.scopes[0]?.name && extra.tenant === void 0) throw configError(`PermDock CLI: rls.assignments.tables entry ${extra.table} assigns at ${scope}, below the first scope: name its tenant column`);
|
|
2902
|
-
out.push({
|
|
2903
|
-
table: extra.table,
|
|
2904
|
-
scope,
|
|
2905
|
-
id: extra.id,
|
|
2906
|
-
tenant: extra.tenant ?? extra.id,
|
|
2907
|
-
role: extra.role,
|
|
2908
|
-
...extra.user === void 0 ? {} : { user: extra.user }
|
|
2909
|
-
});
|
|
2910
|
-
}
|
|
2911
|
-
return out;
|
|
2912
|
-
}
|
|
2913
|
-
/** The tables `rls.assignments.ownRole` refuses a caller's own rows on, by table key. */
|
|
2914
|
-
function ownRoleTables(ctx, tables) {
|
|
2915
|
-
const own = ctx.assignments?.ownRole;
|
|
2916
|
-
if (own === void 0) return /* @__PURE__ */ new Set();
|
|
2917
|
-
if (own === "refuse") return new Set(tables.filter((entry) => entry.user !== void 0).map((entry) => qualifySqlTable(entry.table)));
|
|
2918
|
-
const out = /* @__PURE__ */ new Set();
|
|
2919
|
-
for (const [name, value] of Object.entries(own)) {
|
|
2920
|
-
const entry = tables.find((table) => qualifySqlTable(table.table) === qualifySqlTable(name));
|
|
2921
|
-
if (entry === void 0) throw configError(`PermDock CLI: rls.assignments.ownRole names ${name}, which no assignment trigger guards`);
|
|
2922
|
-
if (value !== "refuse") throw configError(`PermDock CLI: rls.assignments.ownRole.${name} must be 'refuse'`);
|
|
2923
|
-
if (entry.user === void 0) throw configError(`PermDock CLI: rls.assignments.ownRole names ${name}, which has no user column: set its user in rls.assignments.tables`);
|
|
2924
|
-
out.add(qualifySqlTable(entry.table));
|
|
2925
|
-
}
|
|
2926
|
-
return out;
|
|
2927
|
-
}
|
|
2928
|
-
/**
|
|
2929
|
-
* The tables `rls.assignments` guards, schema-qualified: each scope's mapped
|
|
2930
|
-
* membership table, the global-roles table (`rls.roles`, else
|
|
2931
|
-
* `supabase.hook.roles`) and `rls.assignments.tables`. Empty without
|
|
2932
|
-
* `rls.assignments`.
|
|
2933
|
-
*/
|
|
2934
|
-
function guardedTables(config, scopes) {
|
|
2935
|
-
const rls = config.rls;
|
|
2936
|
-
if (rls?.assignments === void 0) return [];
|
|
2937
|
-
const hook = config.supabase?.hook?.roles;
|
|
2938
|
-
const roles = rls.roles ?? (hook === false ? void 0 : hook);
|
|
2939
|
-
const tables = [
|
|
2940
|
-
...scopes.flatMap((scope) => {
|
|
2941
|
-
const mapped = memberForTable({
|
|
2942
|
-
scopes,
|
|
2943
|
-
...rls.memberships === void 0 ? {} : { memberships: rls.memberships }
|
|
2944
|
-
}, scope.name);
|
|
2945
|
-
return mapped === void 0 ? [] : [mapped.table];
|
|
2946
|
-
}),
|
|
2947
|
-
...roles === void 0 ? [] : [roles.table],
|
|
2948
|
-
...(rls.assignments === true ? [] : rls.assignments.tables ?? []).map((entry) => entry.table)
|
|
2949
|
-
];
|
|
2950
|
-
return [...new Set(tables.map(qualifySqlTable))];
|
|
2951
|
-
}
|
|
2952
|
-
function isRoleList(role) {
|
|
2953
|
-
return Array.isArray(role);
|
|
2954
|
-
}
|
|
2955
|
-
/** The role keys a row variable holds, one scalar per role source. */
|
|
2956
|
-
function rowRoles(entry, row) {
|
|
2957
|
-
return (isRoleList(entry.role) ? entry.role : [entry.role]).map((part) => roleColumn(part, qualifySqlTable(entry.table), row, {
|
|
2958
|
-
label: `rls.assignments ${entry.table} role`,
|
|
2959
|
-
prefix: "PermDock CLI"
|
|
2960
|
-
}).lookup);
|
|
2961
|
-
}
|
|
2962
|
-
/**
|
|
2963
|
-
* `permdock_can_assign_custom_role`: whether the signed-in caller may assign a
|
|
2964
|
-
* custom role of the tenant at the instance, by the checks the custom-role
|
|
2965
|
-
* write functions run on its stored definition.
|
|
2966
|
-
*/
|
|
2967
|
-
function canAssignCustomSql(ctx, forUser = false) {
|
|
2968
|
-
const tenantType = tenantTypeOf(ctx);
|
|
2969
|
-
const fn = qualified(ctx, forUser ? ASSIGNMENTS.customFor : ASSIGNMENTS.custom);
|
|
2970
|
-
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
2971
|
-
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
2972
|
-
const guard = qualified(ctx, forUser ? CUSTOM_ROLES.guardFor : CUSTOM_ROLES.guard);
|
|
2973
|
-
const userParam = forUser ? "p_user uuid, " : "";
|
|
2974
|
-
const userArg = forUser ? "uuid, " : "";
|
|
2975
|
-
const stored = `${storedTenant(ctx)} and c.scope = p_scope and c.role = p_role and (c.scope_id is null or c.scope_id = p_scope_id)`;
|
|
2976
|
-
const grants = forUser ? `revoke execute on function ${fn}(${userArg}${tenantType}, text, text, text) from public, anon, authenticated;` : `revoke execute on function ${fn}(${tenantType}, text, text, text) from public, anon;
|
|
2977
|
-
grant execute on function ${fn}(${tenantType}, text, text, text) to authenticated;`;
|
|
2978
|
-
return `-- whether ${forUser ? "user p_user" : "the caller"} may assign custom role p_role at instance p_scope_id (a platform role: null tenant, scope global): the custom-role write checks on its stored definition${forUser ? "; no client role may execute it" : ""}
|
|
2979
|
-
create or replace function ${fn}(${userParam}p_tenant ${tenantType}, p_scope text, p_scope_id text, p_role text)
|
|
2980
|
-
returns boolean
|
|
2981
|
-
language plpgsql
|
|
2982
|
-
stable
|
|
2983
|
-
security definer
|
|
2984
|
-
set search_path = ''
|
|
2985
|
-
as $$
|
|
2986
|
-
declare
|
|
2987
|
-
v_scope_id text;
|
|
2988
|
-
v_found boolean := false;
|
|
2989
|
-
begin
|
|
2990
|
-
select true, c.scope_id into v_found, v_scope_id
|
|
2991
|
-
from (
|
|
2992
|
-
select c.scope_id from ${perms} c where ${stored}
|
|
2993
|
-
union all
|
|
2994
|
-
select c.scope_id from ${includes} c where ${stored}
|
|
2995
|
-
) c
|
|
2996
|
-
order by c.scope_id nulls first
|
|
2997
|
-
limit 1;
|
|
2998
|
-
if not coalesce(v_found, false) then
|
|
2999
|
-
return false;
|
|
3000
|
-
end if;
|
|
3001
|
-
perform ${guard}(${forUser ? "p_user, " : ""}p_tenant, p_scope, v_scope_id, p_role);
|
|
3002
|
-
return true;
|
|
3003
|
-
exception
|
|
3004
|
-
when insufficient_privilege or invalid_parameter_value then
|
|
3005
|
-
return false;
|
|
3006
|
-
end;
|
|
3007
|
-
$$;
|
|
3008
|
-
${grants}`;
|
|
3009
|
-
}
|
|
3010
|
-
/**
|
|
3011
|
-
* The assignment triggers (\`rls.assignments\`): before a client role inserts,
|
|
3012
|
-
* updates or deletes a row that assigns a role, every role the old and new
|
|
3013
|
-
* row hold must be one the caller may assign at that instance, by
|
|
3014
|
-
* \`permdock_can_assign\` for a declared role and
|
|
3015
|
-
* \`permdock_can_assign_custom_role\` for a custom one. The functions run as
|
|
3016
|
-
* the invoker, so \`current_user\` tells a client write from a trusted one.
|
|
3017
|
-
*/
|
|
3018
|
-
function assignmentSql(ctx) {
|
|
3019
|
-
const own = ctx.ownership;
|
|
3020
|
-
if (ctx.assignments === void 0 || own === void 0 || own.assigns.length === 0) return "";
|
|
3021
|
-
const canAssign = qualified(ctx, OWNERSHIP.canAssign);
|
|
3022
|
-
const custom = ctx.customRoles !== void 0 && ctx.authorize === "database";
|
|
3023
|
-
const declared = ctx.customRoles?.declared;
|
|
3024
|
-
const tenantType = tenantTypeOf(ctx);
|
|
3025
|
-
const chunks = [];
|
|
3026
|
-
const clients = `current_user::text = any(array[${CLIENT_ROLES.map(quoteLiteral).join(", ")}])`;
|
|
3027
|
-
const tables = assignedTables(ctx);
|
|
3028
|
-
const ownRole = ownRoleTables(ctx, tables);
|
|
3029
|
-
for (const entry of tables) {
|
|
3030
|
-
const table = quoteQualified(entry.table);
|
|
3031
|
-
const suffix = entry.table.replaceAll(/[^a-z0-9_]/gu, "_");
|
|
3032
|
-
const fn = qualified(ctx, quoteIdent(`${ASSIGNMENTS.trigger}_${suffix}`));
|
|
3033
|
-
const ownUser = entry.user !== void 0 && ownRole.has(qualifySqlTable(entry.table)) ? entry.user : void 0;
|
|
3034
|
-
const ownCheck = (row, reason) => ownUser === void 0 ? "" : ` if ${row}.${quoteIdent(ownUser)}::text = (${subjectIdSql(ctx)})::text then
|
|
3035
|
-
raise exception using
|
|
3036
|
-
errcode = '42501',
|
|
3037
|
-
message = 'permdock: the caller may not change their own roles',
|
|
3038
|
-
hint = '${reason}';
|
|
3039
|
-
end if;
|
|
3040
|
-
`;
|
|
3041
|
-
const check = (row) => {
|
|
3042
|
-
const id = entry.id === void 0 ? "null::text" : `${row}.${quoteIdent(entry.id)}::text`;
|
|
3043
|
-
const customFn = qualified(ctx, ASSIGNMENTS.custom);
|
|
3044
|
-
let allowed = `${canAssign}(v_role, ${id})`;
|
|
3045
|
-
if (custom && declared !== void 0) {
|
|
3046
|
-
const platform = `${customFn}(null, 'global', null, v_role)`;
|
|
3047
|
-
allowed = entry.id === void 0 || entry.tenant === void 0 ? `case when v_role = any(${declaredArray(declared)}) then ${canAssign}(v_role, ${id}) else ${platform} end` : `case when v_role = any(${declaredArray(declared)}) then ${canAssign}(v_role, ${id}) when ${id} is null then ${platform} else ${customFn}(${row}.${quoteIdent(entry.tenant)}::${tenantType}, ${quoteLiteral(entry.scope)}, ${id}, v_role) end`;
|
|
3048
|
-
}
|
|
3049
|
-
return ` foreach v_role in array array[${rowRoles(entry, row).join(", ")}]::text[] loop
|
|
3050
|
-
if v_role is not null and not coalesce(${allowed}, false) then
|
|
3051
|
-
raise exception using
|
|
3052
|
-
errcode = '42501',
|
|
3053
|
-
message = 'permdock: the caller may not assign ' || v_role || ' in ' || coalesce(${id}, 'null'),
|
|
3054
|
-
hint = 'not-assignable-by';
|
|
3055
|
-
end if;
|
|
3056
|
-
end loop;`;
|
|
3057
|
-
};
|
|
3058
|
-
const what = entry.id === void 0 ? "global roles" : `${entry.scope} roles`;
|
|
3059
|
-
chunks.push(`-- ${entry.table}: a client role may write only the ${what} it may assign${ownUser === void 0 ? "" : ", and none on its own rows"}
|
|
3060
|
-
create or replace function ${fn}()
|
|
3061
|
-
returns trigger
|
|
3062
|
-
language plpgsql
|
|
3063
|
-
volatile
|
|
3064
|
-
set search_path = ''
|
|
3065
|
-
as $$
|
|
3066
|
-
declare
|
|
3067
|
-
v_role text;
|
|
3068
|
-
begin
|
|
3069
|
-
if not (${clients}) then
|
|
3070
|
-
return coalesce(new, old);
|
|
3071
|
-
end if;
|
|
3072
|
-
if tg_op in ('UPDATE', 'DELETE') then
|
|
3073
|
-
${ownCheck("old", "self-demotion")}${check("old")}
|
|
3074
|
-
end if;
|
|
3075
|
-
if tg_op in ('INSERT', 'UPDATE') then
|
|
3076
|
-
${ownCheck("new", "not-assignable-by")}${check("new")}
|
|
3077
|
-
end if;
|
|
3078
|
-
return coalesce(new, old);
|
|
3079
|
-
end;
|
|
3080
|
-
$$;
|
|
3081
|
-
revoke execute on function ${fn}() from public, anon, authenticated;
|
|
3082
|
-
drop trigger if exists "permdock_assignment" on ${table};
|
|
3083
|
-
create trigger "permdock_assignment"
|
|
3084
|
-
before insert or update or delete on ${table}
|
|
3085
|
-
for each row execute function ${fn}();`);
|
|
3086
|
-
}
|
|
3087
|
-
return `${chunks.join("\n\n")}\n`;
|
|
3088
|
-
}
|
|
3089
|
-
//#endregion
|
|
3090
2886
|
//#region src/cli/rls/ownership-triggers.ts
|
|
3091
2887
|
function objectName(prefix, scope) {
|
|
3092
2888
|
return quoteIdent(`${prefix}_${scope}`);
|
|
3093
2889
|
}
|
|
3094
|
-
/**
|
|
2890
|
+
/**
|
|
2891
|
+
* Live holders of `roleExpr` in instance `idExpr`, with the same kind and
|
|
2892
|
+
* expiry rules as the helpers.
|
|
2893
|
+
*/
|
|
3095
2894
|
function holdersSql(ctx, table, column, idExpr, role, into) {
|
|
3096
2895
|
const roleExpr = typeof role === "string" ? quoteLiteral(role) : role.expr;
|
|
3097
2896
|
const held = memberRoleOf(table, "m", " ");
|
|
@@ -3168,10 +2967,10 @@ create constraint trigger ${objectName(OWNERSHIP.holders, scope)}
|
|
|
3168
2967
|
for each row execute function ${fn}();`;
|
|
3169
2968
|
}
|
|
3170
2969
|
/**
|
|
3171
|
-
* `transferOnly` as statement triggers over transition tables: a statement
|
|
3172
|
-
*
|
|
3173
|
-
*
|
|
3174
|
-
*
|
|
2970
|
+
* `transferOnly` as statement triggers over transition tables: a statement that
|
|
2971
|
+
* changes a role's holder count in an instance that had holders before and
|
|
2972
|
+
* still has some after is refused. Creating the first holder and removing the
|
|
2973
|
+
* last (which `min` guards) are not transfers.
|
|
3175
2974
|
*/
|
|
3176
2975
|
function transferTriggerSql(ctx, scope, table, column, roles) {
|
|
3177
2976
|
const fn = qualified(ctx, `${OWNERSHIP.transferOnly}_${scope}`);
|
|
@@ -3234,8 +3033,8 @@ ${trigger("delete")}`;
|
|
|
3234
3033
|
//#endregion
|
|
3235
3034
|
//#region src/cli/rls/ownership-sources.ts
|
|
3236
3035
|
/**
|
|
3237
|
-
* The membership sources holding scope `name`: `undefined` when there are
|
|
3238
|
-
*
|
|
3036
|
+
* The membership sources holding scope `name`: `undefined` when there are none,
|
|
3037
|
+
* `'not-tables'` when one of them carries no table to put a trigger on.
|
|
3239
3038
|
*/
|
|
3240
3039
|
function holderSources(ctx, name) {
|
|
3241
3040
|
const sources = memberForSources(ctx, name);
|
|
@@ -3253,7 +3052,10 @@ function holderSources(ctx, name) {
|
|
|
3253
3052
|
function indent(sql, by) {
|
|
3254
3053
|
return sql.replaceAll(/^/gmu, by);
|
|
3255
3054
|
}
|
|
3256
|
-
/**
|
|
3055
|
+
/**
|
|
3056
|
+
* Every source's rows of one instance, each compared with its own typed
|
|
3057
|
+
* `v_key_<n>`.
|
|
3058
|
+
*/
|
|
3257
3059
|
function sourceRowsOf(scope, sources) {
|
|
3258
3060
|
return sources.map((source, index) => indent(source.holders.rows(scope, { id: `v_key_${String(index)}` }), " ")).join("\n union all\n");
|
|
3259
3061
|
}
|
|
@@ -3269,7 +3071,10 @@ function regclassOf(table) {
|
|
|
3269
3071
|
function sourceTables(sources) {
|
|
3270
3072
|
return [...new Set(sources.map((source) => quoteQualified(source.table)))];
|
|
3271
3073
|
}
|
|
3272
|
-
/**
|
|
3074
|
+
/**
|
|
3075
|
+
* `min` and `max` over membership sources: the same commit-time check as on a
|
|
3076
|
+
* mapped table, counted over every source of the scope.
|
|
3077
|
+
*/
|
|
3273
3078
|
function sourceHoldersTriggerSql(ctx, scope, sources, counted) {
|
|
3274
3079
|
const fn = qualified(ctx, `${OWNERSHIP.holders}_${scope}`);
|
|
3275
3080
|
const keys = sourceKeys(sources);
|
|
@@ -3342,7 +3147,10 @@ ${triggers.join("\n")}`;
|
|
|
3342
3147
|
function collectChangesSql(changed) {
|
|
3343
3148
|
return ` v_changes := v_changes || coalesce((select jsonb_agg(to_jsonb(c)) from (${changed}) c), '[]'::jsonb);`;
|
|
3344
3149
|
}
|
|
3345
|
-
/**
|
|
3150
|
+
/**
|
|
3151
|
+
* `transferOnly` over membership sources: each source table's statement
|
|
3152
|
+
* triggers diff its transition tables, and the count after reads every source.
|
|
3153
|
+
*/
|
|
3346
3154
|
function sourceTransferTriggerSql(ctx, scope, sources, roles) {
|
|
3347
3155
|
const fn = qualified(ctx, `${OWNERSHIP.transferOnly}_${scope}`);
|
|
3348
3156
|
const keys = sourceKeys(sources);
|
|
@@ -3420,278 +3228,410 @@ function sourceRowsSql(sources, user) {
|
|
|
3420
3228
|
return sources.map((source) => source.sql.select(user(source), keep).replaceAll(/^/gmu, " ")).join("\n union all\n");
|
|
3421
3229
|
}
|
|
3422
3230
|
//#endregion
|
|
3423
|
-
//#region src/cli/rls/
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
const
|
|
3433
|
-
const
|
|
3434
|
-
const
|
|
3435
|
-
const
|
|
3436
|
-
|
|
3231
|
+
//#region src/cli/rls/graph.ts
|
|
3232
|
+
function tableFor(resource, tables) {
|
|
3233
|
+
return tables?.[resource] ?? resource;
|
|
3234
|
+
}
|
|
3235
|
+
/**
|
|
3236
|
+
* Every `related` condition the policy's grants compile to, with the resource
|
|
3237
|
+
* graph they need, groups included.
|
|
3238
|
+
*/
|
|
3239
|
+
function graphPlan(policy) {
|
|
3240
|
+
const scopes = scopeList(policy.scopes);
|
|
3241
|
+
const plan = /* @__PURE__ */ new Map();
|
|
3242
|
+
const entryFor = (node) => {
|
|
3243
|
+
const entry = plan.get(node.name) ?? {
|
|
3244
|
+
node,
|
|
3245
|
+
relations: /* @__PURE__ */ new Set(),
|
|
3246
|
+
links: /* @__PURE__ */ new Set()
|
|
3247
|
+
};
|
|
3248
|
+
plan.set(node.name, entry);
|
|
3249
|
+
return entry;
|
|
3437
3250
|
};
|
|
3438
|
-
const
|
|
3439
|
-
|
|
3440
|
-
if (
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
const
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
} else {
|
|
3450
|
-
const kind = globalKindFilterSql(ctx, "r.role");
|
|
3451
|
-
parts.push(`${instance} and exists (
|
|
3452
|
-
select 1 from ${roleRows(ctx)}
|
|
3453
|
-
where (r.role, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}
|
|
3454
|
-
)`);
|
|
3251
|
+
const addRelation = (node, relation) => {
|
|
3252
|
+
const entry = entryFor(node);
|
|
3253
|
+
if (entry.relations.has(relation)) return;
|
|
3254
|
+
entry.relations.add(relation);
|
|
3255
|
+
for (const name of expandRelation(node, relation)) {
|
|
3256
|
+
const spec = node.relations[name];
|
|
3257
|
+
if (!isEdgeRelation(spec) || spec.groups === void 0) continue;
|
|
3258
|
+
for (const { resource, relation: groupRelation } of groupEntries(spec)) {
|
|
3259
|
+
const target = policy.resources.get(resource);
|
|
3260
|
+
if (target !== void 0 && target.name !== node.name) addRelation(target, groupRelation);
|
|
3261
|
+
}
|
|
3455
3262
|
}
|
|
3456
|
-
}
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
const kind = kindFilterSql(ctx, "r.role", "ms.via");
|
|
3463
|
-
parts.push(`exists (
|
|
3464
|
-
select 1
|
|
3465
|
-
from (
|
|
3466
|
-
${sourceRowsSql(sources, userOf)}
|
|
3467
|
-
) ms
|
|
3468
|
-
cross join lateral jsonb_array_elements_text(
|
|
3469
|
-
case jsonb_typeof(ms.roles) when 'array' then ms.roles else '[]'::jsonb end
|
|
3470
|
-
) r(role)
|
|
3471
|
-
where ms.scope = ${quoteLiteral(name)}
|
|
3472
|
-
and ms.id = p_scope_id
|
|
3473
|
-
and (r.role, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}${sourceFilters(ctx, name, void 0, void 0, sourcesKeep(sources)).replaceAll("\n and ", "\n and ")}
|
|
3474
|
-
)`);
|
|
3475
|
-
continue;
|
|
3263
|
+
};
|
|
3264
|
+
const addLinks = (hops) => {
|
|
3265
|
+
for (let index = 1; index < hops.length; index += 1) {
|
|
3266
|
+
const from = policy.resources.get(hops[index - 1]?.resource ?? "");
|
|
3267
|
+
const hop = hops[index];
|
|
3268
|
+
if (from !== void 0 && hop !== void 0) entryFor(from).links.add(hop.link);
|
|
3476
3269
|
}
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3270
|
+
};
|
|
3271
|
+
const addAbove = (above) => {
|
|
3272
|
+
const node = above === void 0 ? void 0 : policy.resources.get(above.resource);
|
|
3273
|
+
if (above === void 0 || node === void 0) return;
|
|
3274
|
+
const entry = entryFor(node);
|
|
3275
|
+
entry.closure = Math.max(entry.closure ?? 0, above.depth);
|
|
3276
|
+
entry.restrictedAncestors = above;
|
|
3277
|
+
};
|
|
3278
|
+
for (const grant of policy.grants) {
|
|
3279
|
+
const row = policy.resources.get(grant.permission.resource);
|
|
3280
|
+
for (const item of flattenGrantee(grant.to)) {
|
|
3281
|
+
if (item.kind === "inherit") {
|
|
3282
|
+
const inherited = inheritCondition(item, row, policy.resources);
|
|
3283
|
+
addLinks(inherited?.hops ?? []);
|
|
3284
|
+
addAbove(inherited?.restrictedAncestors);
|
|
3285
|
+
continue;
|
|
3490
3286
|
}
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
parts.push(`exists (
|
|
3502
|
-
select 1 from ${membershipRows(ctx)}
|
|
3503
|
-
where m ->> 'scope' = ${quoteLiteral(name)}
|
|
3504
|
-
and m ->> 'id' = p_scope_id
|
|
3505
|
-
and (r.role, p_role) in (${pairsSql(pairs)})
|
|
3506
|
-
and case jsonb_typeof(m -> 'expires_at')
|
|
3507
|
-
when 'number' then (m ->> 'expires_at')::numeric > extract(epoch from now())
|
|
3508
|
-
else true
|
|
3509
|
-
end${kind === void 0 ? "" : `\n and ${kind}`}${claimKeptLines(ctx, " ").map((line) => `\n${line}`).join("")}
|
|
3510
|
-
)`);
|
|
3287
|
+
if (item.kind !== "relation") continue;
|
|
3288
|
+
const condition = relationCondition(item, row, scopes, { resources: policy.resources });
|
|
3289
|
+
if (condition?.op !== "related") continue;
|
|
3290
|
+
const node = policy.resources.get(condition.resource);
|
|
3291
|
+
if (node === void 0) continue;
|
|
3292
|
+
addRelation(node, condition.relation);
|
|
3293
|
+
const entry = entryFor(node);
|
|
3294
|
+
if (condition.depth > 0 && isSelfParented(node)) entry.closure = Math.max(entry.closure ?? 0, condition.depth);
|
|
3295
|
+
addLinks(condition.hops ?? []);
|
|
3296
|
+
addAbove(condition.restrictedAncestors);
|
|
3511
3297
|
}
|
|
3512
3298
|
}
|
|
3513
|
-
|
|
3514
|
-
|
|
3299
|
+
return plan;
|
|
3300
|
+
}
|
|
3301
|
+
/** The closure depth per walked resource, as `RlsSqlContext.graph` carries it. */
|
|
3302
|
+
function closureDepths(plan) {
|
|
3303
|
+
const out = {};
|
|
3304
|
+
for (const [name, entry] of plan) if (entry.closure !== void 0) out[name] = entry.closure;
|
|
3305
|
+
return out;
|
|
3306
|
+
}
|
|
3307
|
+
/**
|
|
3308
|
+
* The `permitted_<resource>_ids(relation)` arm for one concrete relation: ids
|
|
3309
|
+
* of `resource` the subject holds it on, directly or through a group. `asked`
|
|
3310
|
+
* are the relation names that include it, so a call for any of them (or for
|
|
3311
|
+
* all, with null) reads this arm.
|
|
3312
|
+
*/
|
|
3313
|
+
function relationArm(ctx, plan, node, relation, asked, tables) {
|
|
3314
|
+
const spec = node.relations[relation];
|
|
3315
|
+
if (!isEdgeRelation(spec) && !isPrincipalRelation(spec) && (!isFieldRelation(spec) || spec.memberOf !== void 0)) throw configError(`PermDock CLI: relation '${relation}' on ${node.name} is not a graph relation RLS can compile`);
|
|
3316
|
+
const guard = asked.length === 1 ? `(p_relation is null or p_relation = ${quoteLiteral(asked[0] ?? relation)})` : `(p_relation is null or p_relation in (${asked.map(quoteLiteral).join(", ")}))`;
|
|
3317
|
+
return ` select a.id from (${graphSqlText(relationArmSql(node.name, relation, {
|
|
3318
|
+
resources: new Map([...plan].map(([name, item]) => [name, item.node])),
|
|
3319
|
+
...tables === void 0 ? {} : { tables },
|
|
3320
|
+
qualify: qualifySqlTable,
|
|
3321
|
+
holders: (resource, groupRelation) => [{ text: `select ${qualified(ctx, graphHelper(resource))}(${quoteLiteral(groupRelation)})` }]
|
|
3322
|
+
}), ctx)}) a\n where ${guard}${activeUserSql(ctx).map((part) => ` and ${part}`).join("")}`;
|
|
3323
|
+
}
|
|
3324
|
+
function permittedSql(ctx, plan, entry, tables) {
|
|
3325
|
+
const fn = qualified(ctx, graphHelper(entry.node.name));
|
|
3326
|
+
const asked = [...entry.relations].toSorted();
|
|
3327
|
+
const arms = [...new Set(asked.flatMap((name) => expandRelation(entry.node, name)))].toSorted().map((relation) => relationArm(ctx, plan, entry.node, relation, asked.filter((name) => expandRelation(entry.node, name).includes(relation)), tables));
|
|
3328
|
+
if (arms.length === 0) arms.push(" select null::text where false");
|
|
3329
|
+
return `-- ${entry.node.name}: ids the subject holds a relation on (all relations when p_relation is null)
|
|
3330
|
+
create or replace function ${fn}(p_relation text)
|
|
3331
|
+
returns setof text
|
|
3332
|
+
language sql
|
|
3515
3333
|
stable
|
|
3516
3334
|
security definer
|
|
3517
3335
|
set search_path = ''
|
|
3518
3336
|
as $$
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3337
|
+
${arms.join("\n union\n")}
|
|
3338
|
+
$$;
|
|
3339
|
+
revoke execute on function ${fn}(text) from public, anon;
|
|
3340
|
+
grant execute on function ${fn}(text) to authenticated;`;
|
|
3341
|
+
}
|
|
3342
|
+
/**
|
|
3343
|
+
* `permdock_link_<resource>_<link>(ids)`: ids of `resource` whose link points
|
|
3344
|
+
* into `ids`, read past the table's own policies.
|
|
3345
|
+
*/
|
|
3346
|
+
function linkSql(ctx, node, link, tables) {
|
|
3347
|
+
const spec = Object.hasOwn(node.links, link) ? node.links[link] : void 0;
|
|
3348
|
+
if (spec === void 0) throw configError(`PermDock CLI: ${node.name} has no link '${link}'`);
|
|
3349
|
+
const fn = qualified(ctx, linkHelper(node.name, link));
|
|
3350
|
+
return `-- ${node.name}.${link}: rows whose ${spec.field} points at one of the given ${spec.resource} ids
|
|
3351
|
+
create or replace function ${fn}(p_ids text[])
|
|
3352
|
+
returns setof text
|
|
3353
|
+
language sql
|
|
3523
3354
|
stable
|
|
3524
3355
|
security definer
|
|
3525
3356
|
set search_path = ''
|
|
3357
|
+
as $$
|
|
3358
|
+
select t.${quoteIdent(node.id)}::text from ${quoteQualified(tableFor(node.name, tables))} t
|
|
3359
|
+
where t.${quoteIdent(spec.field)}::text = any(p_ids)
|
|
3360
|
+
$$;
|
|
3361
|
+
revoke execute on function ${fn}(text[]) from public, anon;
|
|
3362
|
+
grant execute on function ${fn}(text[]) to authenticated;`;
|
|
3363
|
+
}
|
|
3364
|
+
function closureTableSql(ctx) {
|
|
3365
|
+
const table = qualified(ctx, CLOSURE.table);
|
|
3366
|
+
return `-- the object graph: one row per (ancestor, descendant) within each walked resource's depth,
|
|
3367
|
+
-- never crossing a restricted row; kept current by statement triggers on each self-parented table
|
|
3368
|
+
create table if not exists ${table} (
|
|
3369
|
+
resource text not null,
|
|
3370
|
+
ancestor text not null,
|
|
3371
|
+
descendant text not null,
|
|
3372
|
+
depth integer not null check (depth >= 0),
|
|
3373
|
+
primary key (resource, descendant, ancestor)
|
|
3374
|
+
);
|
|
3375
|
+
create index if not exists ${quoteIdent(`${CLOSURE.table}_ancestor`)} on ${table} (resource, ancestor);
|
|
3376
|
+
alter table ${table} enable row level security;
|
|
3377
|
+
revoke all on table ${table} from anon, authenticated, public;
|
|
3378
|
+
grant select on table ${table} to authenticated;`;
|
|
3379
|
+
}
|
|
3380
|
+
function closureObjectsSql(ctx, entry, depth, tables) {
|
|
3381
|
+
const { node } = entry;
|
|
3382
|
+
const parent = node.parent;
|
|
3383
|
+
if (parent === void 0) return "";
|
|
3384
|
+
const name = node.name;
|
|
3385
|
+
const sqlName = graphSqlName(name) ?? name;
|
|
3386
|
+
const closure = qualified(ctx, CLOSURE.table);
|
|
3387
|
+
const table = quoteQualified(tableFor(name, tables));
|
|
3388
|
+
const id = quoteIdent(node.id);
|
|
3389
|
+
const up = quoteIdent(parent.field);
|
|
3390
|
+
const restricted = restrictedFor(node, "parent");
|
|
3391
|
+
const stop = restricted === void 0 ? "false" : `coalesce(t.${quoteIdent(restricted)}, false)`;
|
|
3392
|
+
const stopParent = restricted === void 0 ? "false" : `coalesce(p.${quoteIdent(restricted)}, false)`;
|
|
3393
|
+
const refresh = qualified(ctx, `${CLOSURE.table}_${sqlName}`);
|
|
3394
|
+
const changed = qualified(ctx, `${CLOSURE.table}_${sqlName}_changed`);
|
|
3395
|
+
const literal = quoteLiteral(name);
|
|
3396
|
+
const restrictedChanged = restricted === void 0 ? "" : ` or n.${quoteIdent(restricted)} is distinct from o.${quoteIdent(restricted)}`;
|
|
3397
|
+
const trigger = (suffix) => quoteIdent(`${CLOSURE.table}_${sqlName}_${suffix}`);
|
|
3398
|
+
return `-- ${name}: ${restricted === void 0 ? `rows reach their ancestors up to depth ${String(depth)}` : `rows reach their ancestors up to depth ${String(depth)}, stopping at a restricted row`}
|
|
3399
|
+
drop policy if exists ${quoteIdent(`${CLOSURE.table}_${sqlName}`)} on ${closure};
|
|
3400
|
+
create policy ${quoteIdent(`${CLOSURE.table}_${sqlName}`)} on ${closure}
|
|
3401
|
+
for select to authenticated
|
|
3402
|
+
using (resource = ${literal} and ancestor in (select ${qualified(ctx, graphHelper(name))}(null)));
|
|
3403
|
+
create or replace function ${refresh}(p_ids text[])
|
|
3404
|
+
returns void
|
|
3405
|
+
language plpgsql
|
|
3406
|
+
security definer
|
|
3407
|
+
set search_path = ''
|
|
3526
3408
|
as $$
|
|
3527
3409
|
declare
|
|
3528
|
-
|
|
3410
|
+
v_cycle text;
|
|
3411
|
+
v_affected text[];
|
|
3529
3412
|
begin
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
}
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
}
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
}
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3413
|
+
with recursive walk(start, next, path) as (
|
|
3414
|
+
select t.${id}, t.${up}, array[t.${id}::text]
|
|
3415
|
+
from ${table} t
|
|
3416
|
+
where t.${id}::text = any(p_ids)
|
|
3417
|
+
union all
|
|
3418
|
+
select walk.start, t.${up}, walk.path || t.${id}::text
|
|
3419
|
+
from walk join ${table} t on t.${id} = walk.next
|
|
3420
|
+
where not (walk.next::text = any(walk.path))
|
|
3421
|
+
)
|
|
3422
|
+
select walk.start::text into v_cycle
|
|
3423
|
+
from walk
|
|
3424
|
+
where walk.next::text = any(walk.path)
|
|
3425
|
+
limit 1;
|
|
3426
|
+
if v_cycle is not null then
|
|
3427
|
+
raise exception 'permdock: % % is its own ancestor', ${literal}, v_cycle
|
|
3428
|
+
using errcode = 'check_violation';
|
|
3429
|
+
end if;
|
|
3430
|
+
with recursive down(id, level) as (
|
|
3431
|
+
select t.${id}, 0
|
|
3432
|
+
from ${table} t
|
|
3433
|
+
where t.${id}::text = any(p_ids) or t.${up}::text = any(p_ids)
|
|
3434
|
+
union
|
|
3435
|
+
select t.${id}, down.level + 1
|
|
3436
|
+
from down join ${table} t on t.${up} = down.id
|
|
3437
|
+
where down.level < ${String(depth)}
|
|
3438
|
+
)
|
|
3439
|
+
select coalesce(array_agg(distinct down.id::text), '{}') into v_affected from down;
|
|
3440
|
+
delete from ${closure} c
|
|
3441
|
+
where c.resource = ${literal}
|
|
3442
|
+
and (c.descendant = any(p_ids) or c.descendant = any(v_affected));
|
|
3443
|
+
insert into ${closure} (resource, ancestor, descendant, depth)
|
|
3444
|
+
with recursive walk(descendant, ancestor, parent, depth, stop) as (
|
|
3445
|
+
select t.${id}::text, t.${id}, t.${up}, 0, ${stop}
|
|
3446
|
+
from ${table} t
|
|
3447
|
+
where t.${id}::text = any(v_affected)
|
|
3448
|
+
union all
|
|
3449
|
+
select walk.descendant, p.${id}, p.${up}, walk.depth + 1, ${stopParent}
|
|
3450
|
+
from walk join ${table} p on p.${id} = walk.parent
|
|
3451
|
+
where not walk.stop and walk.depth < ${String(depth)}
|
|
3452
|
+
)
|
|
3453
|
+
select ${literal}, walk.ancestor::text, walk.descendant, walk.depth from walk
|
|
3454
|
+
on conflict do nothing;
|
|
3455
|
+
end
|
|
3456
|
+
$$;
|
|
3457
|
+
revoke execute on function ${refresh}(text[]) from public, anon, authenticated;
|
|
3458
|
+
create or replace function ${changed}()
|
|
3459
|
+
returns trigger
|
|
3460
|
+
language plpgsql
|
|
3461
|
+
security definer
|
|
3462
|
+
set search_path = ''
|
|
3463
|
+
as $$
|
|
3464
|
+
begin
|
|
3465
|
+
if tg_op = 'TRUNCATE' then
|
|
3466
|
+
delete from ${closure} where resource = ${literal};
|
|
3467
|
+
elsif tg_op = 'INSERT' then
|
|
3468
|
+
perform ${refresh}(array(select n.${id}::text from new_rows n));
|
|
3469
|
+
elsif tg_op = 'DELETE' then
|
|
3470
|
+
perform ${refresh}(array(select o.${id}::text from old_rows o));
|
|
3471
|
+
else
|
|
3472
|
+
perform ${refresh}(array(
|
|
3473
|
+
select n.${id}::text
|
|
3474
|
+
from new_rows n left join old_rows o on o.${id} = n.${id}
|
|
3475
|
+
where o.${id} is null or n.${up} is distinct from o.${up}${restrictedChanged}
|
|
3476
|
+
union
|
|
3477
|
+
select o.${id}::text
|
|
3478
|
+
from old_rows o left join new_rows n on n.${id} = o.${id}
|
|
3479
|
+
where n.${id} is null
|
|
3480
|
+
));
|
|
3481
|
+
end if;
|
|
3482
|
+
return null;
|
|
3483
|
+
end
|
|
3484
|
+
$$;
|
|
3485
|
+
revoke execute on function ${changed}() from public, anon, authenticated;
|
|
3486
|
+
drop trigger if exists ${trigger("insert")} on ${table};
|
|
3487
|
+
create trigger ${trigger("insert")} after insert on ${table}
|
|
3488
|
+
referencing new table as new_rows
|
|
3489
|
+
for each statement execute function ${changed}();
|
|
3490
|
+
drop trigger if exists ${trigger("update")} on ${table};
|
|
3491
|
+
create trigger ${trigger("update")} after update on ${table}
|
|
3492
|
+
referencing old table as old_rows new table as new_rows
|
|
3493
|
+
for each statement execute function ${changed}();
|
|
3494
|
+
drop trigger if exists ${trigger("delete")} on ${table};
|
|
3495
|
+
create trigger ${trigger("delete")} after delete on ${table}
|
|
3496
|
+
referencing old table as old_rows
|
|
3497
|
+
for each statement execute function ${changed}();
|
|
3498
|
+
drop trigger if exists ${trigger("truncate")} on ${table};
|
|
3499
|
+
create trigger ${trigger("truncate")} after truncate on ${table}
|
|
3500
|
+
for each statement execute function ${changed}();
|
|
3501
|
+
select ${refresh}(array(select t.${id}::text from ${table} t));`;
|
|
3502
|
+
}
|
|
3503
|
+
function restrictedRowsHelperSql(ctx, plan, entry, tables) {
|
|
3504
|
+
const above = entry.restrictedAncestors;
|
|
3505
|
+
if (above === void 0) return "";
|
|
3506
|
+
const fn = qualified(ctx, restrictedHelper(entry.node.name));
|
|
3507
|
+
const body = graphSqlText(restrictedRowsSql(above, {
|
|
3508
|
+
resources: new Map([...plan].map(([name, item]) => [name, item.node])),
|
|
3509
|
+
...tables === void 0 ? {} : { tables },
|
|
3510
|
+
qualify: qualifySqlTable,
|
|
3511
|
+
closure: `${helperSchema(ctx)}.${CLOSURE.table}`,
|
|
3512
|
+
closureDepths: closureDepths(plan)
|
|
3513
|
+
}), ctx);
|
|
3514
|
+
return `-- ${entry.node.name}: rows a stopped link does not reach below: restricted, under a restricted row within ${String(above.depth - 1)} levels, or deeper
|
|
3515
|
+
create or replace function ${fn}()
|
|
3516
|
+
returns setof text
|
|
3584
3517
|
language sql
|
|
3585
3518
|
stable
|
|
3586
3519
|
security definer
|
|
3587
3520
|
set search_path = ''
|
|
3588
3521
|
as $$
|
|
3589
|
-
|
|
3522
|
+
${body}
|
|
3590
3523
|
$$;
|
|
3591
|
-
${
|
|
3524
|
+
revoke execute on function ${fn}() from public, anon;
|
|
3525
|
+
grant execute on function ${fn}() to authenticated;`;
|
|
3592
3526
|
}
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
const
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
const binding = policy.rolesByName.get(name);
|
|
3601
|
-
if (binding !== void 0) return binding.on === void 0;
|
|
3602
|
-
const leaf = findRole(policy.vocabulary.roles, name);
|
|
3603
|
-
return leaf !== void 0 && leaf.on === void 0;
|
|
3604
|
-
}
|
|
3605
|
-
/** The rules `rls generate` enforces; `undefined` when no role declares one. */
|
|
3606
|
-
function ownershipRules(policy, scopes) {
|
|
3607
|
-
const kinds = {};
|
|
3608
|
-
const assigns = [];
|
|
3609
|
-
const counted = [];
|
|
3610
|
-
for (const binding of policy.roles) {
|
|
3611
|
-
const scope = roleScope(policy, scopes, binding.name);
|
|
3612
|
-
if (binding.for !== void 0) kinds[binding.name] = binding.for;
|
|
3613
|
-
const assigner = scope ?? (binding.on === void 0 ? "global" : void 0);
|
|
3614
|
-
for (const target of binding.assigns ?? []) {
|
|
3615
|
-
const at = roleScope(policy, scopes, target);
|
|
3616
|
-
if (assigner !== void 0 && at !== void 0 && (assigner === "global" || scopeChain(scopes, at).includes(assigner))) assigns.push({
|
|
3617
|
-
assigner: binding.name,
|
|
3618
|
-
scope: assigner,
|
|
3619
|
-
role: target,
|
|
3620
|
-
at
|
|
3621
|
-
});
|
|
3622
|
-
else if (assigner === "global" && isGlobalRole(policy, target)) assigns.push({
|
|
3623
|
-
assigner: binding.name,
|
|
3624
|
-
scope: "global",
|
|
3625
|
-
role: target,
|
|
3626
|
-
at: "global"
|
|
3627
|
-
});
|
|
3628
|
-
}
|
|
3629
|
-
const min = binding.min ?? 0;
|
|
3630
|
-
const transferOnly = binding.transferOnly === true;
|
|
3631
|
-
if (scope !== void 0 && (min > 0 || binding.max !== void 0 || transferOnly)) counted.push(binding.max === void 0 ? {
|
|
3632
|
-
role: binding.name,
|
|
3633
|
-
scope,
|
|
3634
|
-
min,
|
|
3635
|
-
transferOnly
|
|
3636
|
-
} : {
|
|
3637
|
-
role: binding.name,
|
|
3638
|
-
scope,
|
|
3639
|
-
min,
|
|
3640
|
-
max: binding.max,
|
|
3641
|
-
transferOnly
|
|
3642
|
-
});
|
|
3527
|
+
/** Group resources a resource's asked relations name, other than itself. */
|
|
3528
|
+
function groupTargets(entry) {
|
|
3529
|
+
const out = /* @__PURE__ */ new Set();
|
|
3530
|
+
for (const asked of entry.relations) for (const name of expandRelation(entry.node, asked)) {
|
|
3531
|
+
const spec = entry.node.relations[name];
|
|
3532
|
+
const resources = isEdgeRelation(spec) ? groupEntries(spec).map((group) => group.resource) : [];
|
|
3533
|
+
for (const resource of resources) if (resource !== entry.node.name) out.add(resource);
|
|
3643
3534
|
}
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3535
|
+
return [...out].toSorted();
|
|
3536
|
+
}
|
|
3537
|
+
/**
|
|
3538
|
+
* Plan entries by name, each after the group resources its helper calls (SQL
|
|
3539
|
+
* function bodies are checked on create).
|
|
3540
|
+
*/
|
|
3541
|
+
function dependencyOrder(plan) {
|
|
3542
|
+
const out = [];
|
|
3543
|
+
const done = /* @__PURE__ */ new Set();
|
|
3544
|
+
const visit = (name) => {
|
|
3545
|
+
const entry = plan.get(name);
|
|
3546
|
+
if (entry === void 0 || done.has(name)) return;
|
|
3547
|
+
done.add(name);
|
|
3548
|
+
for (const target of groupTargets(entry)) visit(target);
|
|
3549
|
+
out.push(entry);
|
|
3649
3550
|
};
|
|
3551
|
+
for (const name of [...plan.keys()].toSorted()) visit(name);
|
|
3552
|
+
return out;
|
|
3650
3553
|
}
|
|
3651
3554
|
/**
|
|
3652
|
-
* The
|
|
3653
|
-
*
|
|
3654
|
-
*
|
|
3555
|
+
* The graph preamble: `permitted_<resource>_ids` for every resource a graph
|
|
3556
|
+
* grant reads, and the closure table with its triggers for every self-parented
|
|
3557
|
+
* resource a grant walks. Empty when no grant is a graph grant.
|
|
3655
3558
|
*/
|
|
3656
|
-
function
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
const
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
if (
|
|
3665
|
-
|
|
3666
|
-
continue;
|
|
3667
|
-
}
|
|
3668
|
-
const transfer = counted.filter((rule) => rule.transferOnly).map((rule) => rule.role);
|
|
3669
|
-
const mapped = scopeTable(ctx, name);
|
|
3670
|
-
if (mapped === void 0) {
|
|
3671
|
-
const sources = holderSources(ctx, name);
|
|
3672
|
-
if (sources === void 0) chunks.push(`-- ${name}: no memberships table configured, so min, max and transferOnly are checked only by decideRoleChange`);
|
|
3673
|
-
else if (sources === "not-tables") chunks.push(`-- ${name}: a membership source is not a table, so min, max and transferOnly are checked only by decideRoleChange`);
|
|
3674
|
-
else {
|
|
3675
|
-
chunks.push(sourceHoldersTriggerSql(ctx, name, sources, counted));
|
|
3676
|
-
if (transfer.length > 0) chunks.push(sourceTransferTriggerSql(ctx, name, sources, transfer));
|
|
3677
|
-
}
|
|
3678
|
-
continue;
|
|
3679
|
-
}
|
|
3680
|
-
chunks.push(holdersTriggerSql(ctx, name, mapped.table, mapped.column, counted));
|
|
3681
|
-
if (transfer.length > 0) chunks.push(transferTriggerSql(ctx, name, mapped.table, mapped.column, transfer));
|
|
3559
|
+
function graphSql(ctx, plan, tables) {
|
|
3560
|
+
if (plan.size === 0) return "";
|
|
3561
|
+
const sqlNames = /* @__PURE__ */ new Map();
|
|
3562
|
+
for (const name of plan.keys()) {
|
|
3563
|
+
const helper = graphHelper(name);
|
|
3564
|
+
const sql = graphSqlName(name) ?? name;
|
|
3565
|
+
if (ctx.scopes.some((scope) => scope.name === sql)) throw configError(`PermDock CLI: resource '${name}' has graph relations and shares its SQL name with the ${sql} scope, so ${helper} would replace the scope helper; rename the resource (permdock doctor PD032)`);
|
|
3566
|
+
const other = sqlNames.get(sql);
|
|
3567
|
+
if (other !== void 0) throw configError(`PermDock CLI: resources '${other}' and '${name}' both name the SQL helper ${helper}; rename one (permdock doctor PD032)`);
|
|
3568
|
+
sqlNames.set(sql, name);
|
|
3682
3569
|
}
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3570
|
+
const entries = dependencyOrder(plan);
|
|
3571
|
+
const chunks = entries.flatMap((entry) => [...entry.relations.size === 0 && entry.closure === void 0 ? [] : [permittedSql(ctx, plan, entry, tables)], ...[...entry.links].toSorted().map((link) => linkSql(ctx, entry.node, link, tables))]);
|
|
3572
|
+
const walked = entries.filter((entry) => entry.closure !== void 0);
|
|
3573
|
+
if (walked.length > 0) {
|
|
3574
|
+
chunks.push(closureTableSql(ctx));
|
|
3575
|
+
for (const entry of walked) chunks.push(closureObjectsSql(ctx, entry, entry.closure ?? 0, tables));
|
|
3576
|
+
for (const entry of walked) {
|
|
3577
|
+
const helper = restrictedRowsHelperSql(ctx, plan, entry, tables);
|
|
3578
|
+
if (helper !== "") chunks.push(helper);
|
|
3690
3579
|
}
|
|
3691
|
-
chunks.push(canAssignAnySql(ctx, custom));
|
|
3692
|
-
if (hasCanAssignAnyFor(ctx)) chunks.push(canAssignAnySql(ctx, custom, true));
|
|
3693
3580
|
}
|
|
3694
|
-
return
|
|
3581
|
+
return `${chunks.join("\n\n")}\n`;
|
|
3582
|
+
}
|
|
3583
|
+
//#endregion
|
|
3584
|
+
//#region src/cli/sql/tables.ts
|
|
3585
|
+
/**
|
|
3586
|
+
* The `schema.table` an `rls.tables` entry names, or `resource` when it names
|
|
3587
|
+
* none or is `false`.
|
|
3588
|
+
*/
|
|
3589
|
+
function tableNameOf(entry, resource) {
|
|
3590
|
+
if (entry === void 0 || entry === false) return resource;
|
|
3591
|
+
return typeof entry === "string" ? entry : entry.table ?? resource;
|
|
3592
|
+
}
|
|
3593
|
+
/**
|
|
3594
|
+
* `rls.tables` as resource to table name, the `via` objects reduced to their
|
|
3595
|
+
* `table` and the `false` entries left out.
|
|
3596
|
+
*/
|
|
3597
|
+
function tableNames(tables) {
|
|
3598
|
+
return tables === void 0 ? void 0 : Object.fromEntries(Object.entries(tables).filter(([, entry]) => entry !== false).map(([resource, entry]) => [resource, tableNameOf(entry, resource)]));
|
|
3599
|
+
}
|
|
3600
|
+
/** The resources `rls.tables` marks `false`: no table, so no policies. */
|
|
3601
|
+
function tablelessResources(tables) {
|
|
3602
|
+
return new Set(Object.entries(tables ?? {}).filter(([, entry]) => entry === false).map(([resource]) => resource));
|
|
3603
|
+
}
|
|
3604
|
+
function singular(name) {
|
|
3605
|
+
if (name.endsWith("ies")) return `${name.slice(0, -3)}y`;
|
|
3606
|
+
if (name.endsWith("sses")) return name.slice(0, -2);
|
|
3607
|
+
return name.endsWith("s") && !name.endsWith("ss") ? name.slice(0, -1) : name;
|
|
3608
|
+
}
|
|
3609
|
+
/** The `via` entries of `rls.tables`, by resource. */
|
|
3610
|
+
function resolveVias(tables, scopes, dialect) {
|
|
3611
|
+
const out = {};
|
|
3612
|
+
for (const [resource, entry] of Object.entries(tables ?? {})) {
|
|
3613
|
+
if (typeof entry === "string" || entry === false) continue;
|
|
3614
|
+
const { via } = entry;
|
|
3615
|
+
const scope = via.scope ?? scopes[0]?.name;
|
|
3616
|
+
if (scope === void 0 || !scopes.some((item) => item.name === scope)) throw configError(`PermDock CLI: rls.tables.${resource}.via.scope '${scope ?? ""}' is not a scope of the policy (${scopes.map((item) => item.name).join(", ")})`);
|
|
3617
|
+
const name = singular(via.table.slice(via.table.lastIndexOf(".") + 1));
|
|
3618
|
+
out[resource] = {
|
|
3619
|
+
helper: permittedIdsHelper(name),
|
|
3620
|
+
key: via.key ?? `${name}_id`,
|
|
3621
|
+
scope,
|
|
3622
|
+
table: via.table.includes(".") ? via.table : `public.${via.table}`,
|
|
3623
|
+
column: via.column,
|
|
3624
|
+
id: via.id ?? "id",
|
|
3625
|
+
type: via.type ?? (dialect === "supabase" ? "uuid" : "text")
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
const helpers = /* @__PURE__ */ new Map();
|
|
3629
|
+
for (const [resource, via] of Object.entries(out)) {
|
|
3630
|
+
const seen = helpers.get(via.helper);
|
|
3631
|
+
if (seen !== void 0 && (seen.table !== via.table || seen.column !== via.column || seen.id !== via.id || seen.scope !== via.scope || seen.type !== via.type)) throw configError(`PermDock CLI: rls.tables.${resource}.via names ${via.helper}, which another resource defines over a different parent; give the parents different table names`);
|
|
3632
|
+
helpers.set(via.helper, via);
|
|
3633
|
+
}
|
|
3634
|
+
return Object.keys(out).length === 0 ? void 0 : out;
|
|
3695
3635
|
}
|
|
3696
3636
|
//#endregion
|
|
3697
|
-
export {
|
|
3637
|
+
export { graphSqlText as $, claimKeptLines as A, hasCustomRoleChecksFor as B, accessSql as C, tokenScopeAllowsCall as D, TOKEN_SCOPE_ALLOWS as E, globalRoleRows as F, byKey as G, rolesTableScope as H, membershipRows as I, apiKeyFields as J, API_KEY_ALLOWS as K, roleRows as L, memberColumn as M, sourceFilters as N, tokenScopesPlan as O, sourcesKeep as P, graphSqlName as Q, globalRoleSource as R, requiresSql as S, seedSql as T, storedTenant as U, raiseSql as V, byCodeUnit as W, CLOSURE as X, apiKeysPlan as Y, graphHelper as Z, PERMISSION_HELPERS as _, activeMembershipSql as _t, closureDepths as a, memberForHelper as at, permissionKeysForHelper as b, checkSuspension as bt, holderSources as c, memberIdsHelper as ct, sourceRowsSql as d, permittedForHelper as dt, hasMemberFor as et, sourceTransferTriggerSql as f, permittedIdsHelper as ft, OWNERSHIP as g, activeInstancesSql as gt, ASSIGNMENTS as h, scopeTable as ht, tablelessResources as i, linkHelper as it, disabledColumn as j, capabilityAccessSql as k, pairsSql as l, memberOfHelper as lt, transferTriggerSql as m, scopeSources as mt, tableNameOf as n, inheritedRowsHelper as nt, graphPlan as o, memberForSources as ot, holdersTriggerSql as p, restrictedHelper as pt, apiKeyAllowsCall as q, tableNames as r, isTeamScope as rt, graphSql as s, memberForTable as st, resolveVias as t, hasPermittedUsers as tt, sourceHoldersTriggerSql as u, memberRoleOf as ut, checkName as v, activeRowSql as vt, helpersSql as w, permittedByPermissionHelper as x, permissionHelpersSql as y, activeUserSql as yt, currentKeyExpr as z };
|