permdock 0.7.0 → 0.9.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 +1 -3
- package/dist/a2a/index.d.ts +7 -7
- package/dist/a2a/index.js +8 -8
- package/dist/action-result-BcudBQOF.d.ts +19 -0
- package/dist/{adapter-context-CRhqyzCk.js → adapter-context-CIQpQxbE.js} +8 -2
- package/dist/{adoption-scan-D-feE7BE.js → adoption-scan-Ch07MdxN.js} +5 -5
- package/dist/agent/index.d.ts +2 -2
- package/dist/agent/index.js +1 -1
- package/dist/ai-sdk/index.d.ts +19 -13
- package/dist/ai-sdk/index.js +6 -6
- 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-K0faOLFp.d.ts → approval-prompt-3BqWczTt.d.ts} +13 -11
- package/dist/{approval-prompt-Dg5LeAcT.js → approval-prompt-B8hQY69x.js} +5 -6
- package/dist/approvals/index.d.ts +4 -4
- package/dist/approvals/index.js +6 -6
- package/dist/{approvals-CMRfsbBh.js → approvals-DnIBTuXE.js} +66 -42
- package/dist/{approvers-CqPOLD-o.js → approvers-B4r0N_Uz.js} +8 -2
- package/dist/{arazzo-ClrF3PVb.js → arazzo-BpS7LnV0.js} +2 -2
- package/dist/{arazzo-rdeJ8n1J.js → arazzo-CmQaGPkM.js} +6 -6
- package/dist/{args-CC3OwyHW.js → args-0FrVs5EH.js} +14 -7
- package/dist/astro/index.d.ts +30 -12
- package/dist/astro/index.js +2 -2
- package/dist/{audience-BrMs8c4o.js → audience-DTjR8xet.js} +8 -8
- package/dist/{auth-CscUX8Sy.js → auth--yPyZNgr.js} +19 -7
- package/dist/authzen/index.d.ts +34 -28
- package/dist/authzen/index.js +23 -20
- package/dist/{automation-DOtAFWdy.d.ts → automation-C6Jg75xq.d.ts} +42 -32
- package/dist/{automation-ySsxp_Yp.js → automation-x3KEUusw.js} +22 -21
- package/dist/better-auth/index.d.ts +2 -2
- package/dist/better-auth/index.js +13 -7
- package/dist/better-supabase/index.d.ts +357 -227
- package/dist/better-supabase/index.js +219 -140
- package/dist/{bind-D1nO6fiC.js → bind-BNQA9JK9.js} +26 -10
- package/dist/{bind-JDuvxjuW.d.ts → bind-YYjvfpB9.d.ts} +32 -12
- 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-DxE7czo9.js → build-instance-BWXqwQYU.js} +3 -3
- package/dist/{capability-L7NnfhiE.js → capability-BqX94r07.js} +31 -25
- package/dist/{capability-D4UC9TTW.d.ts → capability-Di9tiUvi.d.ts} +41 -29
- package/dist/{casbin-BHijLuHY.js → casbin-B7_yS-oo.js} +2 -2
- package/dist/{casbin-BW9pS47J.js → casbin-bHyIxTNQ.js} +16 -8
- package/dist/{casl-CmJlgziS.js → casl-C4OINZHI.js} +2 -2
- package/dist/{casl-UbI8v-Wq.js → casl-CHHoCldP.js} +19 -10
- package/dist/catalog/index.d.ts +51 -30
- package/dist/catalog/index.js +18 -15
- package/dist/{catalog-D69WhFii.js → catalog-CGlSwU6j.js} +20 -19
- package/dist/catalog-D3oS2GH3.js +1 -0
- package/dist/{catalog-BaMQfQ6Y.js → catalog-amPDr0JQ.js} +3 -3
- package/dist/{catalog-doc-CV9s6B1S.js → catalog-doc-DjMKzJp9.js} +28 -14
- package/dist/{catalog-fingerprint-C144oW4M.js → catalog-fingerprint-e6aCWosn.js} +18 -14
- package/dist/catalog-operations-DeHTrJSV.js +60 -0
- package/dist/{challenge-nGWma08S.js → challenge-DImNcR7B.js} +48 -27
- 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-BC_0mPIq.js → checks-CGkP3Z7A.js} +4 -1
- 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 +84 -52
- package/dist/cloud/index.js +53 -39
- package/dist/{cloud-CO8pYtwr.js → cloud-3kWfjnYy.js} +4 -5
- package/dist/{codemod-BP6n8N9L.js → codemod-Bv5i-Mzo.js} +523 -201
- package/dist/{codemod-i4N2BhDc.js → codemod-D9Aiii1r.js} +3 -4
- package/dist/{collect-DNm3oKOk.js → collect-B80GYEot.js} +4 -5
- package/dist/collect-D-eroIfV.js +1 -0
- package/dist/{collect-BaWeMjwV.js → collect-WCl8cJQV.js} +26 -63
- 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-COM2VKuN.d.ts → compile-BEklRUYo.d.ts} +36 -12
- package/dist/{compile-BAEO5bOk.js → compile-EzEMB-7v.js} +77 -33
- package/dist/{completion-BNdVf7X2.js → completion-_bc0OUQ-.js} +17 -8
- package/dist/config-BwPBfN0t.js +108 -0
- package/dist/{config-DGZzMV1B.js → config-D5XrLWpk.js} +2 -2
- package/dist/{config-report-Cm7UITNA.js → config-report-BmWIskD7.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-_VCSsdtD.js → connector-schema-CYLQNMYq.js} +3 -2
- package/dist/{context--uM9V3Ui.js → context-BnIzxTNE.js} +4 -3
- package/dist/convex/index.d.ts +13 -7
- package/dist/convex/index.js +9 -6
- package/dist/{core-CQkE8Esr.d.ts → core-CFepFiDP.d.ts} +49 -22
- package/dist/{core-B5-w6Kw9.js → core-DzVINFbU.js} +26 -20
- package/dist/{create-Cb86Z8Im.js → create-6EwRQlig.js} +10 -7
- package/dist/{create-DJrlhEp9.js → create-BLPKnJKa.js} +35 -25
- package/dist/{create-UTnUVETj.d.ts → create-BPUopdfL.d.ts} +70 -37
- package/dist/{create-QHuuPw3q.d.ts → create-BamXmeob.d.ts} +37 -29
- package/dist/{create-B2-CQqCI.d.ts → create-DBvUsgzK.d.ts} +49 -28
- package/dist/{credential-CIM7wHwA.js → credential-CyyyvCsL.js} +41 -38
- package/dist/{credentials-BpuTwd7a.js → credentials-CdDxp7jN.js} +6 -6
- package/dist/{deciding-columns-DRo2QM6E.js → deciding-columns-NB4wcyBf.js} +2 -2
- package/dist/{delegation-C5rOdTp5.js → delegation-CDB6_keF.js} +31 -22
- package/dist/delivery/index.d.ts +4 -4
- package/dist/delivery/index.js +5 -5
- package/dist/{delivery-rwOKLWRm.js → delivery-BupfvJIV.js} +34 -22
- package/dist/{delivery-event-xX4obB8F.js → delivery-event-JioK5YbS.js} +6 -6
- package/dist/denied-text-DHldhVUv.d.ts +22 -0
- package/dist/{describe-DZlnEtLy.js → describe-Bb4hGK5d.js} +5 -5
- package/dist/{describe-O0SwYIK8.d.ts → describe-BbFPBSms.d.ts} +5 -5
- package/dist/{device-BARQ8eYG.js → device-BwbJNmb2.js} +11 -8
- package/dist/{diff-Btr1c1Ow.js → diff-BwR1JdP2.js} +2 -2
- package/dist/{diff-DzT-xD13.js → diff-CQegtBAo.js} +22 -13
- package/dist/{digest-B3kc_PgQ.d.ts → digest-8Yx2CchM.d.ts} +3 -3
- package/dist/{doctor-BU1szn63.js → doctor-ClRVAtXQ.js} +287 -135
- package/dist/{doctor-BoK_y0KO.js → doctor-LPMYjSqO.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-B_Qg9D3e.d.ts → errors-BIM4MFsJ.d.ts} +22 -6
- package/dist/{errors-BWXBMaKK.d.ts → errors-CHwoRRpB.d.ts} +4 -1
- package/dist/{errors-DpP_U6Bf.js → errors-Vpu6rKHl.js} +13 -10
- package/dist/{evaluate-B_oqFbWC.js → evaluate-BAmf_AQd.js} +153 -82
- package/dist/{evaluations-BCddEOfO.d.ts → evaluations-DOWuBpWv.d.ts} +11 -5
- package/dist/{evaluations-DReoiVBw.js → evaluations-IBceAeKT.js} +40 -19
- 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-CuhyYwC1.d.ts → fetch-adapter-BIbBAtPZ.d.ts} +9 -6
- package/dist/{fetch-adapter-De-u6vhJ.js → fetch-adapter-dPTgJVHd.js} +14 -11
- package/dist/fields-D0vUI6dq.js +498 -0
- package/dist/{files-acUbNwWt.js → files-B3AA05f1.js} +8 -5
- package/dist/{files-D_JIzUrP.js → files-BSjrtIHw.js} +11 -4
- package/dist/{fixtures-BTMW6zgU.js → fixtures-A1_sp_tR.js} +15 -6
- package/dist/{flags-aaeRAdiD.js → flags-DXjoTVbS.js} +2 -2
- package/dist/{freeze-CrU8nQeX.js → freeze-jjan0lCv.js} +4 -1
- package/dist/{from-snapshot-Bz469dIh.js → from-snapshot-DCakWRsr.js} +18 -15
- package/dist/gen-BFfzZSLH.js +58 -0
- package/dist/gen-BtfFWGbz.js +827 -0
- package/dist/{generate-3FNO5P5Q.js → generate-CRVV5V7l.js} +14 -8
- package/dist/generate-CezbCaTI.js +275 -0
- package/dist/{grantee-BrRANrpT.js → grantee-Dke-9rd1.js} +4 -4
- package/dist/{grantee-conditions-C6P1Tvol.js → grantee-conditions-BrYPe-w0.js} +12 -12
- package/dist/{grantee-match-0RtHWQDx.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-DY3vXuGs.js → helpers-DJicaG6f.js} +40 -28
- package/dist/hono/index.d.ts +16 -10
- package/dist/hono/index.js +5 -5
- package/dist/{hosted-XBgU6RGT.js → hosted-D0IzESu_.js} +42 -21
- package/dist/{hosted-BmjzxQhp.d.ts → hosted-DVxlLZMe.d.ts} +33 -21
- package/dist/{http-ujKm0JXE.js → http-DH94eh6f.js} +4 -4
- package/dist/{http-BBoKefeS.js → http-DKLXrFGk.js} +13 -7
- package/dist/{http-Ca3Sr5x1.js → http-DOL33cO0.js} +11 -8
- package/dist/{import-DB2EG_iy.js → import-ACkOrv6g.js} +6 -6
- package/dist/{import-Bz0GefWg.js → import-CST57XLp.js} +64 -51
- package/dist/{index-CMiPH2Tv.d.ts → index-BinlXeU-.d.ts} +64 -37
- package/dist/{index-BSfRl0hZ.d.ts → index-CV-7DWeD.d.ts} +64 -52
- package/dist/{index-BE8y3moU.d.ts → index-DJwEgYzA.d.ts} +127 -82
- package/dist/{index-DH9DUDEM.d.ts → index-Dj2qF9VC.d.ts} +54 -30
- package/dist/{index-BTkwrK7v.d.ts → index-vYjMrXyK.d.ts} +336 -173
- package/dist/{index-DLwcuCN_.d.ts → index-ymept53A.d.ts} +51 -30
- package/dist/index.d.ts +22 -22
- package/dist/index.js +34 -34
- package/dist/{init-B-4t1NzC.js → init-BjuFDYT1.js} +10 -10
- package/dist/{init-5TXNji88.js → init-txYTuW9u.js} +5 -5
- package/dist/{instance-options-CU0YQCLH.d.ts → instance-options-CoHn-0e8.d.ts} +4 -4
- package/dist/{instance-snapshot-CjcwnEiV.js → instance-snapshot-DuWf1cT3.js} +86 -61
- 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 +57 -27
- package/dist/jwt/index.js +24 -21
- package/dist/{kernel-DVExUwkp.js → kernel-qv4cr9w8.js} +32 -23
- 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-DxroEzH1.js → load-DjSU5KXO.js} +6 -6
- package/dist/load-Dm0pSYH3.js +1 -0
- package/dist/{login-ClSJh5t5.js → login-CfImTJGb.js} +4 -4
- package/dist/{login-WC9HPJpU.js → login-CgfCTO3F.js} +16 -9
- package/dist/{manifest-Dv6q4i5s.js → manifest-B2QPh19C.js} +49 -38
- package/dist/{manifest-BVhExYhw.d.ts → manifest-CzzWtnAb.d.ts} +126 -31
- package/dist/{manifest-schema-Cv492Qev.js → manifest-schema-2r37XRhD.js} +44 -3
- package/dist/{manifest-B79YuF2T.js → manifest-wg8WkkpD.js} +6 -6
- package/dist/{map-claims-etGIS_Dv.js → map-claims-C-nv08_n.js} +16 -9
- package/dist/{map-error-BQGyDm_M.js → map-error-pasIGqMT.js} +7 -7
- package/dist/{markers-C6joDI3o.js → markers-BeHZ4x9Q.js} +20 -5
- package/dist/mcp/index.d.ts +114 -61
- package/dist/mcp/index.js +70 -40
- package/dist/{migrate-Dodel0IJ.js → migrate-B7oP_sQU.js} +35 -17
- 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 +79 -26
- package/dist/next/index.js +121 -10
- 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 +51 -30
- package/dist/openapi/index.js +2 -2
- package/dist/{openapi-ztVaux6j.js → openapi-BGeuunWC.js} +23 -14
- package/dist/{openapi-DMocl1bh.js → openapi-BIpw3Mb7.js} +4 -5
- package/dist/{openapi-Ddb4628T.js → openapi-BOsx0xn9.js} +33 -18
- package/dist/{operation-findings-kRcEp8lE.js → operation-findings-CXtFzlKJ.js} +3 -3
- package/dist/{operations-BNLes61x.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-DgSDLrta.js → output-DV5dwOTs.js} +9 -5
- package/dist/output-files-BHOIlrfD.js +1 -0
- package/dist/{output-files-CCMdcFWb.js → output-files-BIQ_9pIs.js} +29 -17
- package/dist/ownership-DA5YdYTm.js +1059 -0
- package/dist/{parse-Ddhz2xf4.js → parse-1_ZheTW5.js} +16 -13
- package/dist/parse-local-manifest-CCmbhnxP.d.ts +10 -0
- package/dist/{parse-local-manifest-T1Q8X3A2.js → parse-local-manifest-DEiCF4ah.js} +19 -7
- 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 +20 -13
- package/dist/{peer-DOzzMfZL.js → peer-C9LvBCcb.js} +1 -1
- package/dist/{peer-12Q5I_RH.js → peer-Q0fuZ6hx.js} +9 -3
- package/dist/{permdock-BfWEeMqk.js → permdock-BvjivVb9.js} +200 -140
- package/dist/{permdock-Dco6dyCK.d.ts → permdock-CprAPN3w.d.ts} +260 -129
- package/dist/{permission-registry-DpPtSKNZ.js → permission-registry-B8tYxPIt.js} +23 -10
- package/dist/{permission-types-DhNEmoRG.d.ts → permission-types-CAY-GU7e.d.ts} +127 -41
- package/dist/{permissions-CoqCOfJ3.d.ts → permissions-Bx6UI8n1.d.ts} +6 -3
- package/dist/{permissions-CyH2MV_B.js → permissions-CVXmRILq.js} +3 -3
- package/dist/{permissions-DArULThq.js → permissions-CYcHj_w7.js} +8 -5
- package/dist/pg-DQTqRrou.js +60 -0
- package/dist/{pins-8ILTftC9.js → pins-nqarl3oE.js} +4 -1
- package/dist/{pkce-5h8NgMQJ.js → pkce-CnmmYqUZ.js} +1 -1
- package/dist/{policy-B7dg_yzu.d.ts → policy-BycVKnXv.d.ts} +5 -5
- package/dist/{policy-DaJy4nK4.js → policy-CLQ8__GF.js} +43 -23
- package/dist/{policy-from-catalog-DFWYkNdu.js → policy-from-catalog-CU3An8uP.js} +28 -25
- package/dist/{policy-module-CTlwMvLI.js → policy-module-xym9fnSq.js} +12 -6
- package/dist/{policy-types-B-F-qAZk.d.ts → policy-types-DGIO_gxq.d.ts} +782 -357
- package/dist/{postgres-store-BL9M_CA1.js → postgres-store-DcyrPS-r.js} +43 -16
- package/dist/{postgrest-r5faDIzD.js → postgrest-5Dr3W64L.js} +84 -86
- package/dist/{postgrest-CYacM_u7.d.ts → postgrest-D5nUGm-c.d.ts} +55 -35
- package/dist/powersync-B701tvhE.js +1 -0
- package/dist/{powersync-13odBwiM.js → powersync-Bg8u-JcR.js} +3 -3
- package/dist/{powersync-53ryOZip.js → powersync-CJrT1DSc.js} +127 -69
- package/dist/preset/index.d.ts +38 -28
- package/dist/preset/index.js +1 -1
- package/dist/{preset-CHTbd-DQ.js → preset-BjLzeFpp.js} +56 -32
- 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-DwtVemER.js → prompt-BYc6drYW.js} +4 -1
- package/dist/protected-action-D8x73IGm.d.ts +58 -0
- package/dist/protected-action-DUYqb-Ma.js +80 -0
- package/dist/{protected-DXE3cQYg.js → protected-action-QEA8arRB.js} +48 -14
- package/dist/{provider-BQsTYYy0.js → provider-C55OGXJ7.js} +5 -4
- package/dist/{provider-Csk-THAn.d.ts → provider-DfLeek9O.d.ts} +1 -1
- package/dist/{provider-store-IgqQipCO.js → provider-store-CaTv00tr.js} +13 -9
- package/dist/{pull-CN5OLxrF.js → pull-2nJr0qg_.js} +12 -11
- package/dist/pull-C3EEGzSw.js +1 -0
- package/dist/{push-tVdG8dUL.js → push-Bn-lMEmI.js} +19 -13
- package/dist/{pushed-policy-CmYeSQqq.js → pushed-policy-CDuOL2de.js} +10 -7
- package/dist/{pushed-policy-BbcY9r_4.d.ts → pushed-policy-CryQheKq.d.ts} +12 -6
- 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 +94 -55
- package/dist/react-native/index.js +200 -61
- package/dist/{refs-CNJBiTFo.js → refs-C8BKxc_e.js} +2 -2
- 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-DCDD_ZfL.d.ts → replay-Bs9Df2fQ.d.ts} +1 -1
- package/dist/{request-context-BykKLmyE.js → request-context-Duzdf0cd.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-ConCSj1y.js +201 -0
- package/dist/{resource-relations-D_jw0BFx.js → resource-relations-8-pCScBO.js} +18 -9
- package/dist/{retire-Wj6dkyDE.js → retire-B2xffQgY.js} +15 -6
- package/dist/{revocations-R07lFBnX.d.ts → revocations-BaUXHM1N.d.ts} +6 -3
- package/dist/{rls-Ck_PocSp.js → rls-Ba9wN0SP.js} +13 -6
- package/dist/{rls-CxBZb4rZ.js → rls-Cx0dtsB7.js} +310 -128
- package/dist/{role-builder-C1VJzvcr.js → role-builder-BK2-T6oD.js} +41 -20
- package/dist/{row-conditions-h2HythXQ.js → row-conditions-BAGHOmVq.js} +4 -3
- package/dist/{rpc-Bk1bE-ch.js → rpc-vHIwyXCp.js} +8 -5
- package/dist/run-Ck22anC2.js +1 -0
- package/dist/{run-uLD2aWuO.js → run-PbRlXTfh.js} +26 -23
- package/dist/{run-ticket-BpNi7BCi.d.ts → run-ticket-B2o2Kxz8.d.ts} +29 -14
- package/dist/{run-ticket-B5d_63Ma.js → run-ticket-Dv7aF7Dk.js} +9 -9
- package/dist/{runtime-Vif3_-zf.js → runtime-BGnrOkVH.js} +5 -2
- package/dist/{saas-Az_Kz-4D.js → saas-DK4V-3Is.js} +42 -29
- package/dist/{scan-DSljDE1Y.js → scan-CRTj6fke.js} +1 -1
- package/dist/{schema-B4v0Qrac.js → schema-BOJjqlWL.js} +3 -2
- package/dist/{schema-gIcCx7zM.js → schema-BTsE1WRT.js} +8 -4
- package/dist/scim/index.d.ts +2 -2
- package/dist/scim/index.js +2 -2
- package/dist/{scim-C7JYhySg.js → scim-D8YupzX1.js} +2 -2
- package/dist/{scim-YNieBO1U.js → scim-jJBJ7g1Z.js} +33 -15
- package/dist/{scopes-BHsc-wp-.js → scopes-0Up_UA2t.js} +41 -23
- package/dist/server/index.d.ts +11 -11
- package/dist/server/index.js +12 -12
- package/dist/{server-Bnga6I50.js → server-3cVlTLde.js} +31 -31
- package/dist/{server-types-C4gZUEZy.d.ts → server-types-CyRN0-Cd.d.ts} +88 -46
- package/dist/{sha256-D9ihqmvd.js → sha256-C-4WXKIu.js} +4 -3
- package/dist/{shared-fMKOv8hP.js → shared-Afctg7B0.js} +65 -32
- package/dist/{shared-C8LjUdVU.js → shared-l0oGGetg.js} +9 -6
- package/dist/{signals-D3NcTKUS.js → signals-B1vHR3fN.js} +2 -2
- package/dist/{sink-Cp_Eot9W.d.ts → sink-B5zoUW_O.d.ts} +44 -20
- package/dist/{sink-BEZSlMS_.js → sink-D9_vh372.js} +32 -21
- package/dist/{skills-g_3BS9NC.js → skills-C9uKfSF5.js} +3 -3
- package/dist/{skills-Dcjw9_1C.js → skills-CQQcuGBi.js} +12 -8
- package/dist/{snapshot-CbGmYiJx.js → snapshot-CXlzi69u.js} +21 -18
- package/dist/{snapshot-cache-Pahd3IYJ.d.ts → snapshot-cache-BjAWkchD.d.ts} +21 -12
- package/dist/{snapshot-cache-DT2Kx1F8.js → snapshot-cache-Bx_4xC_6.js} +14 -11
- package/dist/solid/index.d.ts +55 -16
- 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-B-zYB4GS.js → sources-CMAM-2A3.js} +46 -27
- package/dist/{sources-DEEuGb8S.d.ts → sources-CWG32C0G.d.ts} +126 -53
- package/dist/{sql-B96movIM.js → sql-0WjxIp9g.js} +28 -7
- package/dist/{sql-CDEPCA0g.d.ts → sql-B80DFdpG.d.ts} +2 -2
- package/dist/{src-DLFrtrrC.js → src-C_pi2Rnh.js} +292 -101
- package/dist/ssf/index.d.ts +3 -3
- package/dist/ssf/index.js +47 -35
- package/dist/{standard-webhooks-DgFFD-Ga.js → standard-webhooks-D67a3nzk.js} +5 -5
- package/dist/{standard-webhooks-B_qRg0ws.d.ts → standard-webhooks-DOYEuOLx.d.ts} +7 -7
- package/dist/{store-DSP9oGvh.js → store-RNFvaJJv.js} +32 -26
- package/dist/{store-options-B_poyYvg.js → store-options-Dil3lraY.js} +105 -15
- package/dist/stores-DZJvxFIZ.d.ts +59 -0
- package/dist/{stores-BqEuHC05.js → stores-DxZWDXCs.js} +42 -5
- package/dist/{stream-2Vwhl7Er.d.ts → stream-9y9qw51m.d.ts} +4 -3
- package/dist/{stream-CvdUS9V1.js → stream-CDvF0sJm.js} +5 -5
- package/dist/{style-Ds1gNm77.js → style-DblOtSga.js} +1 -1
- package/dist/{style-Cqo7-73x.js → style-Ds11Hmk4.js} +2 -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 -38
- package/dist/supabase/index.js +5 -5
- package/dist/supabase/middleware.d.ts +19 -13
- package/dist/supabase/middleware.js +7 -7
- package/dist/{supabase--0aJdLEd.js → supabase-DnMMg5Fn.js} +79 -35
- package/dist/{supabase-Bqp7iIY2.js → supabase-SBs3MYUE.js} +3 -3
- package/dist/supabase-hook-C0FyEACZ.js +1 -0
- package/dist/{supabase-hook-Dg0SrQWR.js → supabase-hook-CrgU06WH.js} +175 -196
- package/dist/{supautils-MESd81bF.js → supautils-ByezN3b8.js} +385 -219
- package/dist/{support-session-Cna49-3f.js → support-session-BsmPCbz8.js} +21 -21
- 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-Z2LpxsGa.js → tables-BqrxBCir.js} +641 -1104
- 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-CmeBQ6Nv.js → tenancy-5d1zVBvK.js} +25 -19
- package/dist/terminal/index.d.ts +23 -14
- package/dist/terminal/index.js +17 -20
- package/dist/testing/core.d.ts +1 -1
- package/dist/testing/core.js +1 -1
- package/dist/testing/fixtures/index.d.ts +8 -5
- package/dist/testing/fixtures/index.js +78 -17
- package/dist/testing/index.d.ts +225 -141
- package/dist/testing/index.js +352 -234
- 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-DhITNCnD.js → text-diff-BtfSmdUo.js} +2 -2
- package/dist/{thenable-yD7SabqG.js → thenable-BNSM1U2R.js} +12 -3
- package/dist/{time-DkWjdL98.js → time-nR1JesoH.js} +32 -8
- package/dist/{token-2qYDvXz2.js → token-DC9EQiet.js} +14 -12
- package/dist/{tool-refusal-reAhIzdl.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-c7Xo_2Cu.d.ts +50 -0
- package/dist/{types-CfiKC5aN.d.ts → types-B2efzoSX.d.ts} +14 -11
- package/dist/{types-CIeZcG8v.d.ts → types-B6ExllNo.d.ts} +2 -2
- package/dist/{types-DBHftjgS.d.ts → types-BB-zWDcR.d.ts} +45 -33
- package/dist/{types-BfVmNJO5.d.ts → types-BMVDHvI0.d.ts} +128 -50
- package/dist/{types-DXmwu78Z.d.ts → types-CEinc15V.d.ts} +31 -14
- package/dist/{types-g7Oy6Zg4.d.ts → types-CN0LsqbK.d.ts} +47 -24
- package/dist/{types-CeIscIKp.d.ts → types-CWBsAwqn.d.ts} +23 -12
- package/dist/{types-DqzsnlSA.js → types-CcUXciSj.js} +17 -5
- package/dist/{types-BWZybqs9.d.ts → types-CzOeVvcC.d.ts} +384 -203
- package/dist/{types-DC44-hqb.d.ts → types-DLvhVm1r.d.ts} +59 -32
- package/dist/{types-D3A19w4A.d.ts → types-DlelvQk4.d.ts} +22 -13
- package/dist/{types-cTos-H7p.d.ts → types-DsHGPtkR.d.ts} +68 -32
- package/dist/{types-BXMYB7BY.d.ts → types-ZzRGoViD.d.ts} +77 -24
- package/dist/{types-CQVEQawv.d.ts → types-rCRV4Ge-.d.ts} +24 -14
- package/dist/types-tQB2CEnY.d.ts +262 -0
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +2 -2
- package/dist/usage-0hjZPIRX.js +1 -0
- package/dist/{usage-Bybmck7f.js → usage-B9EtHWLa.js} +50 -26
- package/dist/{usage-CDaLXCfL.js → usage-CBzgwfIs.js} +2 -2
- package/dist/{validation-error-ZfUHbKvf.js → validation-error-CYg2kJEw.js} +4 -1
- package/dist/{validation-error-CQpoquwj.d.ts → validation-error-uvp-kyfO.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-pe8rL_Qw.js → verify-DlUcTDBz.js} +148 -487
- package/dist/version-DMQ0dnN5.js +48 -0
- package/dist/version-DizhmD72.js +1 -0
- package/dist/{views-BB8E9dTP.js → views-CD8wuL4Z.js} +19 -6
- package/dist/{vocabulary-Bg3eeT2H.d.ts → vocabulary-DEJ5j4mR.d.ts} +7 -4
- package/dist/vue/index.d.ts +71 -22
- package/dist/vue/index.js +50 -13
- package/dist/{waiter-DwEyouan.d.ts → waiter-CqUKX6CY.d.ts} +13 -10
- package/dist/{waiter-BGUtGYoU.js → waiter-aBLM63_f.js} +1 -1
- package/dist/{watch-C-4VmFlL.js → watch-BFDKqmUa.js} +8 -8
- package/dist/watch-ClLuO2TL.js +1 -0
- package/dist/webmcp/index.d.ts +11 -11
- package/dist/webmcp/index.js +8 -5
- package/dist/{where-scope-wq2coopv.js → where-scope-DlCdCM_8.js} +24 -18
- package/dist/{who-can-BwNsI_Js.js → who-can-BoDJm0Nc.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-CiSc_KCq.js → wrap-B2wziJI_.js} +4 -4
- package/dist/{wrap-B6R0cMRL.d.ts → wrap-DxXB54Wu.d.ts} +8 -5
- package/package.json +8 -8
- package/schemas/catalog-v1.json +3 -0
- package/schemas/supabase-manifest-v1.json +33 -1
- package/skills/permdock-credentials/references/tokens-keys-links.md +8 -6
- package/skills/permdock-data/references/queries-and-rls.md +2 -1
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +2 -2
- package/skills/permdock-wire/references/adapters.md +16 -23
- package/dist/catalog-BCCjSAdH.js +0 -1
- package/dist/collect-k_Dulxr5.js +0 -1
- package/dist/config-CoQHDrDi.js +0 -166
- package/dist/denied-text-D4qkEtFz.d.ts +0 -19
- package/dist/fields-ClcQLrVc.js +0 -238
- package/dist/generate-CNU1pIOo.js +0 -171
- package/dist/headers-BIjfMaoC.js +0 -8
- package/dist/headers-CIbDTUTK.d.ts +0 -7
- package/dist/load-DrMHXzgN.js +0 -1
- package/dist/output-files-DPAY7-GX.js +0 -1
- package/dist/package-root-Dk_ETdkp.js +0 -31
- package/dist/parse-local-manifest-e4mdwAKd.d.ts +0 -10
- package/dist/policies-B2lZIurj.js +0 -295
- package/dist/powersync-CExtTLyA.js +0 -1
- package/dist/problem-details-Tcfmj3bV.d.ts +0 -7
- package/dist/protected-BwZYL1X_.d.ts +0 -39
- package/dist/pull-Cty6Ovyu.js +0 -1
- package/dist/run-Bz7DXGEv.js +0 -1
- package/dist/stores-BAZwSYZA.d.ts +0 -31
- package/dist/supabase-hook-DJxC4PoP.js +0 -1
- package/dist/types-CKV71kKh.d.ts +0 -172
- package/dist/usage-y-kBNdqs.js +0 -1
- package/dist/version-CEbQ5UeM.js +0 -18
- package/dist/version-DXkYUISx.js +0 -1
- package/dist/watch-Bd9mx3rd.js +0 -1
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { u as
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { i as
|
|
13
|
-
import { d as sqlTextArray, i as qualifySqlTable, t as SQL_IDENT } from "./sql-B96movIM.js";
|
|
14
|
-
import { i as scopeMembershipTable, o as relatedSql, r as scopeColumn } from "./compile-BAEO5bOk.js";
|
|
15
|
-
import { d as keptKeys, l as roleColumn, u as roleManifest } from "./sources-B-zYB4GS.js";
|
|
16
|
-
import { A as quoteQualified, D as qualified, E as helperSchema, F as scopeTypeOf, I as sqlType, L as tenantTypeOf, M as textArrayLiteral, O as quoteIdent, S as CLAIM, T as declaredArray, _ as subjectClaimJsonSql, b as tenantClaimSql, 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, p as liveSessionSql, 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-fMKOv8hP.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-BvjivVb9.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";
|
|
17
13
|
//#region src/cli/sql/active.ts
|
|
18
14
|
function checkActiveRow(label, row) {
|
|
19
15
|
quoteTable(row.table);
|
|
@@ -86,16 +82,19 @@ function activeUserSql(ctx, user = subjectIdSql(ctx)) {
|
|
|
86
82
|
const row = ctx.suspension?.users;
|
|
87
83
|
return row === void 0 ? [] : [activeRowSql(row, user)];
|
|
88
84
|
}
|
|
89
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* The permission of a grant key held in `grant`: `key#group` and `key@level`
|
|
87
|
+
* name `key`.
|
|
88
|
+
*/
|
|
90
89
|
function grantPermissionSql(grant) {
|
|
91
90
|
return { sql: `split_part(split_part(${grant}, '#', 1), '@', 1)` };
|
|
92
91
|
}
|
|
93
92
|
/**
|
|
94
|
-
* The active-instance check for every suspendable scope on `scope`'s chain:
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
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.
|
|
99
98
|
*/
|
|
100
99
|
function activeInstancesSql(ctx, scope, idOf, permission = ctx.permission === void 0 ? void 0 : { key: ctx.permission }) {
|
|
101
100
|
const parts = [];
|
|
@@ -117,8 +116,8 @@ function activeInstancesSql(ctx, scope, idOf, permission = ctx.permission === vo
|
|
|
117
116
|
const CLOSURE = { table: "permdock_closure" };
|
|
118
117
|
/**
|
|
119
118
|
* The snake_case SQL name of a resource or link (`chatThread` becomes
|
|
120
|
-
* `chat_thread`), or `undefined` when it is not made of ASCII letters,
|
|
121
|
-
*
|
|
119
|
+
* `chat_thread`), or `undefined` when it is not made of ASCII letters, digits
|
|
120
|
+
* and underscores starting with a letter.
|
|
122
121
|
*/
|
|
123
122
|
function graphSqlName(name) {
|
|
124
123
|
if (!/^[A-Za-z][A-Za-z0-9_]*$/u.test(name)) return;
|
|
@@ -129,21 +128,30 @@ function sqlNameOf(name, label) {
|
|
|
129
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`);
|
|
130
129
|
return sql;
|
|
131
130
|
}
|
|
132
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* The helper returning the ids of `resource` the subject holds a relation on:
|
|
133
|
+
* `permitted_<snake_case resource>_ids`.
|
|
134
|
+
*/
|
|
133
135
|
function graphHelper(resource) {
|
|
134
136
|
return permittedIdsHelper(sqlNameOf(resource, `resource '${resource}'`));
|
|
135
137
|
}
|
|
136
138
|
function inheritedRowsHelper(resource) {
|
|
137
139
|
return `permitted_${sqlNameOf(resource, `resource '${resource}'`)}_rows`;
|
|
138
140
|
}
|
|
139
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* The helper returning the ids of `resource` whose `link` points into the ids
|
|
143
|
+
* it is given.
|
|
144
|
+
*/
|
|
140
145
|
function linkHelper(resource, link) {
|
|
141
146
|
return `permdock_link_${sqlNameOf(resource, `resource '${resource}'`)}_${sqlNameOf(link, `link '${link}' on ${resource}`)}`;
|
|
142
147
|
}
|
|
143
148
|
function restrictedHelper(resource) {
|
|
144
149
|
return `permdock_restricted_${sqlNameOf(resource, `resource '${resource}'`)}`;
|
|
145
150
|
}
|
|
146
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Graph SQL parts as RLS text: values inline as literals, the subject from the
|
|
153
|
+
* dialect's claim.
|
|
154
|
+
*/
|
|
147
155
|
function graphSqlText(parts, ctx) {
|
|
148
156
|
return parts.map((part) => {
|
|
149
157
|
if ("text" in part) return part.text;
|
|
@@ -170,23 +178,51 @@ function permittedIdsHelper(name) {
|
|
|
170
178
|
if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw configError(`PermDock CLI: unsafe scope name '${name}'`);
|
|
171
179
|
return `permitted_${name}_ids`;
|
|
172
180
|
}
|
|
173
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* `permitted_<scope>_ids_for(p_user, p_grant)`: the same ids for a user the
|
|
183
|
+
* caller names.
|
|
184
|
+
*/
|
|
174
185
|
function permittedForHelper(name) {
|
|
175
186
|
return `${permittedIdsHelper(name)}_for`;
|
|
176
187
|
}
|
|
177
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* The helper returning the ids of scope `name` the subject holds any live
|
|
190
|
+
* membership of.
|
|
191
|
+
*/
|
|
178
192
|
function memberIdsHelper(name) {
|
|
179
193
|
if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw configError(`PermDock CLI: unsafe scope name '${name}'`);
|
|
180
194
|
return `member_${name}_ids`;
|
|
181
195
|
}
|
|
182
|
-
/**
|
|
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
|
+
*/
|
|
183
200
|
function memberForHelper(name) {
|
|
184
201
|
return `${memberIdsHelper(name)}_for`;
|
|
185
202
|
}
|
|
186
203
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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.
|
|
190
226
|
*/
|
|
191
227
|
function hasMemberFor(input, name) {
|
|
192
228
|
if (input.dialect !== "supabase") return false;
|
|
@@ -201,26 +237,36 @@ function hasPermittedUsers(input, name) {
|
|
|
201
237
|
const sources = memberForSources(input, name);
|
|
202
238
|
return sources.length > 0 && sources.every((source) => source.sql.holders !== void 0);
|
|
203
239
|
}
|
|
204
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* The table `member_<scope>_ids_for` reads for scope `name`, when `memberships`
|
|
242
|
+
* maps one with its id column.
|
|
243
|
+
*/
|
|
205
244
|
function memberForTable(input, name) {
|
|
206
245
|
const table = scopeMembershipTable(input.memberships, input.scopes, name);
|
|
207
246
|
return table !== void 0 && scopeColumn(table, input.scopes, name) !== void 0 ? table : void 0;
|
|
208
247
|
}
|
|
209
|
-
/**
|
|
248
|
+
/**
|
|
249
|
+
* The sources `member_<scope>_ids_for` reads: none when a table is mapped, as
|
|
250
|
+
* in `database` mode.
|
|
251
|
+
*/
|
|
210
252
|
function memberForSources(input, name) {
|
|
211
253
|
if (memberForTable(input, name) !== void 0) return [];
|
|
212
254
|
return (input.memberSources ?? []).filter((source) => source.sql.scope === void 0 || source.sql.scope === name);
|
|
213
255
|
}
|
|
214
256
|
/**
|
|
215
|
-
* The membership sources that can hold scope `name` when `memberships` maps
|
|
216
|
-
*
|
|
217
|
-
* 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.
|
|
218
261
|
*/
|
|
219
262
|
function scopeSources(ctx, name) {
|
|
220
263
|
if (ctx.authorize !== "database" || scopeTable(ctx, name) !== void 0) return [];
|
|
221
264
|
return (ctx.sources ?? []).filter((source) => source.sql.scope === void 0 || source.sql.scope === name);
|
|
222
265
|
}
|
|
223
|
-
/**
|
|
266
|
+
/**
|
|
267
|
+
* The membership table mapped for scope `name`, with the column of its id and
|
|
268
|
+
* of the first scope's id.
|
|
269
|
+
*/
|
|
224
270
|
function scopeTable(ctx, name) {
|
|
225
271
|
const table = scopeMembershipTable(ctx.memberships, ctx.scopes, name);
|
|
226
272
|
if (table === void 0) return;
|
|
@@ -238,481 +284,6 @@ function scopeTable(ctx, name) {
|
|
|
238
284
|
};
|
|
239
285
|
}
|
|
240
286
|
//#endregion
|
|
241
|
-
//#region src/cli/sql/conditions.ts
|
|
242
|
-
const MAX_CLAIM_DEPTH = 8;
|
|
243
|
-
/**
|
|
244
|
-
* The claim path of a `principal.claim.*` / `principal.claims.*` ref, one
|
|
245
|
-
* segment per JSON key; `undefined` for any other ref.
|
|
246
|
-
*/
|
|
247
|
-
function claimPath(ref) {
|
|
248
|
-
const prefix = ["principal.claim.", "principal.claims."].find((item) => ref.startsWith(item));
|
|
249
|
-
if (prefix === void 0) return;
|
|
250
|
-
const segments = ref.slice(prefix.length).split(".");
|
|
251
|
-
if (segments.length > MAX_CLAIM_DEPTH) throw configError(`PermDock CLI: claim path '${ref}' is deeper than ${MAX_CLAIM_DEPTH}`);
|
|
252
|
-
for (const segment of segments) if (!CLAIM.test(segment) || isForbiddenKey(segment)) throw configError(`PermDock CLI: unsafe claim name '${segment}'`);
|
|
253
|
-
return segments;
|
|
254
|
-
}
|
|
255
|
-
function jsonPath(base, keys, last) {
|
|
256
|
-
let sql = base;
|
|
257
|
-
for (const [index, key] of keys.entries()) sql += ` ${index === keys.length - 1 ? last : "->"} ${quoteLiteral(key)}`;
|
|
258
|
-
return sql;
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* The JSON document a claim path starts from and the keys to walk in it. The
|
|
262
|
-
* `guc` dialect keeps each top-level claim in its own setting as JSON text.
|
|
263
|
-
*/
|
|
264
|
-
function claimRoot(ctx, path) {
|
|
265
|
-
const [head, ...rest] = path;
|
|
266
|
-
if (head === void 0) throw configError("PermDock CLI: empty claim path");
|
|
267
|
-
switch (ctx.dialect) {
|
|
268
|
-
case "supabase": return {
|
|
269
|
-
root: "(select auth.jwt())",
|
|
270
|
-
keys: path
|
|
271
|
-
};
|
|
272
|
-
case "neon": return {
|
|
273
|
-
root: "(select auth.session())",
|
|
274
|
-
keys: path
|
|
275
|
-
};
|
|
276
|
-
case "guc": return {
|
|
277
|
-
root: subjectClaimJsonSql(ctx, head),
|
|
278
|
-
keys: rest
|
|
279
|
-
};
|
|
280
|
-
default: return ctx.dialect;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
/** The claim at `path` as `jsonb`. */
|
|
284
|
-
function claimJsonSql(ctx, path) {
|
|
285
|
-
const { root, keys } = claimRoot(ctx, path);
|
|
286
|
-
return keys.length === 0 ? root : `(${jsonPath(root, keys, "->")})`;
|
|
287
|
-
}
|
|
288
|
-
/** The claim at `path` as text: `->>` on the last key, or the plain setting for a one-segment `guc` claim. */
|
|
289
|
-
function claimTextSql(ctx, path) {
|
|
290
|
-
const [head] = path;
|
|
291
|
-
if (path.length === 1 && head !== void 0) return subjectClaimSql(ctx, head);
|
|
292
|
-
const { root, keys } = claimRoot(ctx, path);
|
|
293
|
-
return `(${jsonPath(root, keys, "->>")})`;
|
|
294
|
-
}
|
|
295
|
-
/** The JSON kind a claim must have to compare with a column of `type`. */
|
|
296
|
-
function jsonKindOf(type) {
|
|
297
|
-
if (type === "numeric") return "number";
|
|
298
|
-
return type === "boolean" ? "boolean" : "string";
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* A claim compared with a column of `type`. Numbers and booleans must be JSON
|
|
302
|
-
* of that kind (anything else is `null`, so the comparison is false, as in
|
|
303
|
-
* memory); other types cast the text form. A one-segment `guc` claim is text
|
|
304
|
-
* and is cast as is.
|
|
305
|
-
*/
|
|
306
|
-
function typedClaimSql(ctx, path, type) {
|
|
307
|
-
const text = claimTextSql(ctx, path);
|
|
308
|
-
if (type === void 0 || type === "text") return text;
|
|
309
|
-
const cast = `${text}::${sqlType(type)}`;
|
|
310
|
-
const kind = jsonKindOf(type);
|
|
311
|
-
if (kind === "string" || ctx.dialect === "guc" && path.length === 1) return `(${cast})`;
|
|
312
|
-
return `(case when jsonb_typeof(${claimJsonSql(ctx, path)}) = '${kind}' then ${cast} end)`;
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* The elements of an array claim as a Postgres array of the column's type,
|
|
316
|
-
* built once per statement (an uncorrelated `array(select ...)` is an InitPlan).
|
|
317
|
-
* A missing or non-array claim is the empty array; elements of another JSON
|
|
318
|
-
* kind are left out.
|
|
319
|
-
*/
|
|
320
|
-
function claimArraySql(ctx, path, type) {
|
|
321
|
-
const json = claimJsonSql(ctx, path);
|
|
322
|
-
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)}')`;
|
|
323
|
-
}
|
|
324
|
-
function sqlValue(value, ctx, field) {
|
|
325
|
-
if (value !== null && typeof value === "object" && "ref" in value) return compileRef(value.ref, ctx, field);
|
|
326
|
-
if (value !== null && typeof value === "object" && "date" in value) {
|
|
327
|
-
const instant = isoInstant(value.date);
|
|
328
|
-
return instant === void 0 ? "null" : quoteLiteral(isoAt(instant));
|
|
329
|
-
}
|
|
330
|
-
if (isReadonlyArray(value)) return `array[${value.map((item) => sqlValue(item, ctx)).join(", ")}]`;
|
|
331
|
-
if (value === null) return "null";
|
|
332
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
333
|
-
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "null";
|
|
334
|
-
if (typeof value === "string") return quoteLiteral(value);
|
|
335
|
-
throw configError("PermDock CLI: non-portable condition value");
|
|
336
|
-
}
|
|
337
|
-
function compileRef(ref, ctx, field) {
|
|
338
|
-
if (ref === "principal.id") return subjectIdSql(ctx);
|
|
339
|
-
if (ref === "now") return "now()";
|
|
340
|
-
const path = claimPath(ref);
|
|
341
|
-
if (ref === "principal.tenant" || path?.length === 1 && path[0] === ctx.tenantClaim) return tenantClaimSql(ctx);
|
|
342
|
-
if (path !== void 0) return typedClaimSql(ctx, path, field === void 0 ? void 0 : ctx.columnTypes?.[field]);
|
|
343
|
-
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)`);
|
|
344
|
-
throw configError(`PermDock CLI: non-portable subject ref '${ref}'`);
|
|
345
|
-
}
|
|
346
|
-
/** `value` with `\`, `%` and `_` escaped for `like ... escape '\'`, as `escapeLike` does in process. */
|
|
347
|
-
function likeLiteralSql(value) {
|
|
348
|
-
return `replace(replace(replace(${value}, '\\', '\\\\'), '%', '\\%'), '_', '\\_')`;
|
|
349
|
-
}
|
|
350
|
-
function compareSql(op, field, value) {
|
|
351
|
-
const left = quoteIdent(field);
|
|
352
|
-
switch (op) {
|
|
353
|
-
case "eq": return `${left} = ${value}`;
|
|
354
|
-
case "ne": return `${left} <> ${value}`;
|
|
355
|
-
case "gt": return `${left} > ${value}`;
|
|
356
|
-
case "gte": return `${left} >= ${value}`;
|
|
357
|
-
case "lt": return `${left} < ${value}`;
|
|
358
|
-
case "lte": return `${left} <= ${value}`;
|
|
359
|
-
case "contains": return `${left}::text like '%' || ${likeLiteralSql(`${value}::text`)} || '%' escape '\\'`;
|
|
360
|
-
default: return op;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Whether the row's `rowField` is among the ids in `table.rowColumn` the
|
|
365
|
-
* subject holds a matching membership on. The row field stays outside the
|
|
366
|
-
* subquery: inside it, an unqualified name resolves to the membership table
|
|
367
|
-
* first, so `m.org_id = "org_id"` would compare the membership with itself.
|
|
368
|
-
*/
|
|
369
|
-
function membershipSql(table, rowColumn, rowField, roles, ctx, tenantColumn, scope) {
|
|
370
|
-
const parts = [`m.${quoteIdent(table.user)} = ${subjectIdSql(ctx)}`];
|
|
371
|
-
const role = memberRoleOf(table);
|
|
372
|
-
if (roles.length > 0) {
|
|
373
|
-
const held = role.through === void 0 && !role.lateral ? `m.${quoteIdent(role.column)}` : role.lookup;
|
|
374
|
-
parts.push(`${held} = any(${textArrayLiteral(roles)})`);
|
|
375
|
-
const via = memberVia(table);
|
|
376
|
-
const single = sole(roles);
|
|
377
|
-
const kind = single === void 0 ? kindFilterSql(ctx, role.lookup, via) : roleKindSql(ctx, single, via);
|
|
378
|
-
if (kind !== void 0) parts.push(kind);
|
|
379
|
-
}
|
|
380
|
-
if (table.expiresAt !== void 0) parts.push(`(m.${quoteIdent(table.expiresAt)} is null or m.${quoteIdent(table.expiresAt)} > now())`);
|
|
381
|
-
parts.push(...activeMembershipSql(ctx, table.disabledAt === void 0 ? void 0 : `m.${quoteIdent(table.disabledAt)}`));
|
|
382
|
-
if (tenantColumn !== void 0 && ctx.tenants !== "all") parts.push(`m.${quoteIdent(tenantColumn)} = ${tenantClaimSql(ctx)}`);
|
|
383
|
-
parts.push(...activeUserSql(ctx));
|
|
384
|
-
if (scope !== void 0) {
|
|
385
|
-
const tenantOf = scope === rootScope(ctx.scopes) ? rowColumn : tenantColumn;
|
|
386
|
-
const keyed = keyTenantSql(ctx, tenantOf === void 0 ? void 0 : `m.${quoteIdent(tenantOf)}`);
|
|
387
|
-
if (keyed !== void 0) parts.push(keyed);
|
|
388
|
-
parts.push(...activeInstancesSql(ctx, scope, (name) => {
|
|
389
|
-
const held = scopeColumn(table, ctx.scopes, name);
|
|
390
|
-
return held === void 0 ? void 0 : `m.${quoteIdent(held)}`;
|
|
391
|
-
}));
|
|
392
|
-
}
|
|
393
|
-
const expand = roles.length > 0 && role.lateral ? role.join : "";
|
|
394
|
-
return `${quoteIdent(rowField)} in (select m.${quoteIdent(rowColumn)} from ${quoteTable(table.table)} m${expand} where ${parts.join(" and ")})`;
|
|
395
|
-
}
|
|
396
|
-
function compileMemberOf(condition, ctx) {
|
|
397
|
-
const scope = condition.scope === "resource" ? void 0 : resolveScope(ctx.scopes, condition.scope);
|
|
398
|
-
if (condition.scope !== "resource" && scope === void 0) throw configError(`PermDock CLI: memberOf ${condition.scope} names a scope the policy does not declare`);
|
|
399
|
-
const mapping = scope === void 0 ? condition.resource === void 0 ? void 0 : ctx.memberships?.resource?.[condition.resource] : scopeMembershipTable(ctx.memberships, ctx.scopes, scope);
|
|
400
|
-
if (mapping !== void 0) {
|
|
401
|
-
const named = scope === void 0 ? void 0 : scopeTable(ctx, scope);
|
|
402
|
-
const rowColumn = scope === void 0 ? mapping.id : named?.column;
|
|
403
|
-
if (rowColumn === void 0) throw configError(`PermDock CLI: memberships mapping for ${condition.scope} is missing the row column`);
|
|
404
|
-
const tenantColumn = scope === void 0 || scope === rootScope(ctx.scopes) ? void 0 : named?.tenantColumn;
|
|
405
|
-
const primary = membershipSql(mapping, rowColumn, condition.field, condition.roles, ctx, tenantColumn, scope);
|
|
406
|
-
if (condition.scope !== "resource" || condition.parents === void 0) return primary;
|
|
407
|
-
return `(${[primary, ...condition.parents.flatMap((parent) => {
|
|
408
|
-
if (typeof parent === "string") return [membershipSql(mapping, rowColumn, parent, condition.roles, ctx, tenantColumn)];
|
|
409
|
-
const hopTable = ctx.memberships?.resource?.[parent.resource];
|
|
410
|
-
return hopTable?.id === void 0 ? [] : [membershipSql(hopTable, hopTable.id, parent.field, condition.roles, ctx)];
|
|
411
|
-
})].join(" or ")})`;
|
|
412
|
-
}
|
|
413
|
-
if (scope !== void 0 && condition.roles.length === 0 && scopeSources(ctx, scope).length > 0) return `${quoteIdent(condition.field)} in (select ${qualified(ctx, memberIdsHelper(scope))}())`;
|
|
414
|
-
if (scope !== void 0 && scope === rootScope(ctx.scopes)) {
|
|
415
|
-
const parts = [
|
|
416
|
-
ctx.tenants === "all" ? `${quoteIdent(condition.field)} in (select ${qualified(ctx, memberIdsHelper(scope))}())` : `${quoteIdent(condition.field)} = ${tenantClaimSql(ctx)}`,
|
|
417
|
-
...ctx.tenants === "all" ? [] : [keyTenantSql(ctx, quoteIdent(condition.field))].filter((part) => part !== void 0),
|
|
418
|
-
...activeUserSql(ctx),
|
|
419
|
-
...activeInstancesSql(ctx, scope, () => quoteIdent(condition.field))
|
|
420
|
-
];
|
|
421
|
-
return parts.length === 1 ? parts.join("") : `(${parts.join(" and ")})`;
|
|
422
|
-
}
|
|
423
|
-
throw configError(`PermDock CLI: memberOf ${condition.scope} needs a memberships table mapping`);
|
|
424
|
-
}
|
|
425
|
-
function compileConditionSql(condition, ctx) {
|
|
426
|
-
switch (condition.op) {
|
|
427
|
-
case "eq":
|
|
428
|
-
case "ne":
|
|
429
|
-
case "gt":
|
|
430
|
-
case "gte":
|
|
431
|
-
case "lt":
|
|
432
|
-
case "lte": return compareSql(condition.op, condition.field, sqlValue(condition.value, ctx, condition.field));
|
|
433
|
-
case "contains": {
|
|
434
|
-
const element = ctx.arrayColumns?.[condition.field];
|
|
435
|
-
if (element !== void 0) return `${sqlValue(condition.value, {
|
|
436
|
-
...ctx,
|
|
437
|
-
columnTypes: {
|
|
438
|
-
...ctx.columnTypes,
|
|
439
|
-
[condition.field]: element
|
|
440
|
-
}
|
|
441
|
-
}, condition.field)} = any(${quoteIdent(condition.field)})`;
|
|
442
|
-
return compareSql(condition.op, condition.field, sqlValue(condition.value, ctx, condition.field));
|
|
443
|
-
}
|
|
444
|
-
case "in":
|
|
445
|
-
case "notIn": {
|
|
446
|
-
const keyword = condition.op === "in" ? "in" : "not in";
|
|
447
|
-
if (!Array.isArray(condition.value) && typeof condition.value === "object" && "ref" in condition.value) {
|
|
448
|
-
const path = claimPath(condition.value.ref);
|
|
449
|
-
if (path === void 0) {
|
|
450
|
-
compileRef(condition.value.ref, ctx);
|
|
451
|
-
throw configError(`PermDock CLI: non-portable ${condition.op} against '${condition.value.ref}'`);
|
|
452
|
-
}
|
|
453
|
-
const column = quoteIdent(condition.field);
|
|
454
|
-
const list = claimArraySql(ctx, path, ctx.columnTypes?.[condition.field]);
|
|
455
|
-
return condition.op === "in" ? `${column} = any (${list})` : `(${column} is not null and not (${column} = any (${list})))`;
|
|
456
|
-
}
|
|
457
|
-
const values = condition.value.map((item) => sqlValue(item, ctx));
|
|
458
|
-
if (values.length === 0) return condition.op === "in" ? "false" : `${quoteIdent(condition.field)} is not null`;
|
|
459
|
-
return `${quoteIdent(condition.field)} ${keyword} (${values.join(", ")})`;
|
|
460
|
-
}
|
|
461
|
-
case "isNull": return `${quoteIdent(condition.field)} is ${condition.value ? "" : "not "}null`;
|
|
462
|
-
case "and": return `(${condition.conditions.map((item) => compileConditionSql(item, ctx)).join(" and ")})`;
|
|
463
|
-
case "or":
|
|
464
|
-
if (condition.conditions.length === 0) return "false";
|
|
465
|
-
return `(${condition.conditions.map((item) => compileConditionSql(item, ctx)).join(" or ")})`;
|
|
466
|
-
case "not": return `(${compileConditionSql(condition.condition, ctx)}) is not true`;
|
|
467
|
-
case "memberOf": return compileMemberOf(condition, ctx);
|
|
468
|
-
case "related": return compileRelatedSql(condition, ctx);
|
|
469
|
-
case "sqlFunction":
|
|
470
|
-
if (ctx.inlineFunctions === true) return compileConditionSql(condition.twin, ctx);
|
|
471
|
-
return `${quoteTable(condition.name)}(${condition.args.map((arg) => isSqlFunctionField(arg) ? quoteIdent(arg.field) : sqlValue(arg, ctx)).join(", ")})`;
|
|
472
|
-
case "opaque": return condition.sql;
|
|
473
|
-
case "liveSession": return liveSessionSql(ctx);
|
|
474
|
-
default: return condition;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
function andConditions(left, right) {
|
|
478
|
-
if (left === void 0) return right;
|
|
479
|
-
if (right === void 0) return left;
|
|
480
|
-
return {
|
|
481
|
-
op: "and",
|
|
482
|
-
conditions: [left, right]
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
function valueContextRefs(value) {
|
|
486
|
-
if (Array.isArray(value)) return value.flatMap(valueContextRefs);
|
|
487
|
-
if (isRecord(value) && typeof value["ref"] === "string" && (value["ref"] === "context" || value["ref"].startsWith("context."))) return [value["ref"]];
|
|
488
|
-
return [];
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* The `context.*` refs a condition reads. The request context is not in the
|
|
492
|
-
* token, so RLS cannot evaluate them.
|
|
493
|
-
*/
|
|
494
|
-
function contextRefs(condition) {
|
|
495
|
-
if (condition === void 0) return [];
|
|
496
|
-
switch (condition.op) {
|
|
497
|
-
case "eq":
|
|
498
|
-
case "ne":
|
|
499
|
-
case "gt":
|
|
500
|
-
case "gte":
|
|
501
|
-
case "lt":
|
|
502
|
-
case "lte":
|
|
503
|
-
case "contains":
|
|
504
|
-
case "in":
|
|
505
|
-
case "notIn": return valueContextRefs(condition.value);
|
|
506
|
-
case "and":
|
|
507
|
-
case "or": return condition.conditions.flatMap((child) => contextRefs(child));
|
|
508
|
-
case "not": return contextRefs(condition.condition);
|
|
509
|
-
case "sqlFunction": return [...condition.args.flatMap(valueContextRefs), ...contextRefs(condition.twin)];
|
|
510
|
-
case "isNull":
|
|
511
|
-
case "memberOf":
|
|
512
|
-
case "related":
|
|
513
|
-
case "opaque":
|
|
514
|
-
case "liveSession": return [];
|
|
515
|
-
default: return condition;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
function sqlFunctionNames(condition) {
|
|
519
|
-
if (condition === void 0) return [];
|
|
520
|
-
switch (condition.op) {
|
|
521
|
-
case "sqlFunction": return [condition.name, ...sqlFunctionNames(condition.twin)];
|
|
522
|
-
case "and":
|
|
523
|
-
case "or": return condition.conditions.flatMap((child) => sqlFunctionNames(child));
|
|
524
|
-
case "not": return sqlFunctionNames(condition.condition);
|
|
525
|
-
case "eq":
|
|
526
|
-
case "ne":
|
|
527
|
-
case "gt":
|
|
528
|
-
case "gte":
|
|
529
|
-
case "lt":
|
|
530
|
-
case "lte":
|
|
531
|
-
case "contains":
|
|
532
|
-
case "in":
|
|
533
|
-
case "notIn":
|
|
534
|
-
case "isNull":
|
|
535
|
-
case "memberOf":
|
|
536
|
-
case "related":
|
|
537
|
-
case "opaque":
|
|
538
|
-
case "liveSession": return [];
|
|
539
|
-
default: return condition;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
/**
|
|
543
|
-
* A `related` condition as uncorrelated subqueries, so Postgres runs the
|
|
544
|
-
* helper once per statement: the row's field in the closure's descendants of
|
|
545
|
-
* the ids the subject holds the relation on (or those ids alone at depth 0).
|
|
546
|
-
* The helper sits in `array(...)` so it stays an InitPlan; a plain `in` gets
|
|
547
|
-
* pulled into a join that can rescan it per closure row.
|
|
548
|
-
*/
|
|
549
|
-
function compileRelatedSql(condition, ctx) {
|
|
550
|
-
if (condition.hops !== void 0 && condition.hops.length > 0) return compileHoppedSql(condition, ctx);
|
|
551
|
-
const type = ctx.columnTypes?.[condition.field];
|
|
552
|
-
const cast = type === void 0 || type === "text" ? "" : `::${type}`;
|
|
553
|
-
const column = type === void 0 ? `${quoteIdent(condition.field)}::text` : quoteIdent(condition.field);
|
|
554
|
-
const helper = condition.permission === void 0 ? `${qualified(ctx, graphHelper(condition.resource))}(${quoteLiteral(condition.relation)})` : `${qualified(ctx, inheritedRowsHelper(condition.resource))}(${quoteLiteral(condition.permission)})`;
|
|
555
|
-
const cap = ctx.graph?.closures[condition.resource];
|
|
556
|
-
let inner;
|
|
557
|
-
if (condition.depth > 0 && cap !== void 0) {
|
|
558
|
-
const depth = condition.depth < cap ? ` and depth <= ${String(condition.depth)}` : "";
|
|
559
|
-
inner = `select descendant${cast} from ${qualified(ctx, CLOSURE.table)} where resource = ${quoteLiteral(condition.resource)}${depth} and ancestor = any (array(select ${helper}))`;
|
|
560
|
-
} else inner = cast === "" ? `select ${helper}` : `select p.id${cast} from ${helper} as p(id)`;
|
|
561
|
-
const reach = `${column} in (${inner})`;
|
|
562
|
-
if (condition.parent === true && condition.restricted !== void 0) return `(${reach} and ${quoteIdent(condition.restricted)} is not true)`;
|
|
563
|
-
return reach;
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* A `related` condition that crosses link hops: the reached instances of the
|
|
567
|
-
* last resource, carried back over each link by its security definer helper,
|
|
568
|
-
* so the hop reads a table the subject may not see.
|
|
569
|
-
*/
|
|
570
|
-
function compileHoppedSql(condition, ctx) {
|
|
571
|
-
const resources = ctx.graph?.resources;
|
|
572
|
-
if (resources === void 0) throw configError("PermDock CLI: a related condition with link hops needs the policy resources in the RLS context");
|
|
573
|
-
const schema = quoteIdent(helperSchema(ctx));
|
|
574
|
-
return graphSqlText(relatedSql(condition, {
|
|
575
|
-
resources,
|
|
576
|
-
...ctx.graph?.tables === void 0 ? {} : { tables: ctx.graph.tables },
|
|
577
|
-
closure: `${helperSchema(ctx)}.${CLOSURE.table}`,
|
|
578
|
-
closureDepths: ctx.graph?.closures ?? {},
|
|
579
|
-
qualify: qualifySqlTable,
|
|
580
|
-
holders: (resource, relation) => [{ text: `select ${schema}.${graphHelper(resource)}(${quoteLiteral(relation)})` }],
|
|
581
|
-
permitted: (resource, permission) => [{ text: `select ${schema}.${inheritedRowsHelper(resource)}(${quoteLiteral(permission)})` }],
|
|
582
|
-
restrictedRows: (resource) => [{ text: `select ${schema}.${restrictedHelper(resource)}()` }],
|
|
583
|
-
linked: (resource, link, targets) => [
|
|
584
|
-
{ text: `select ${schema}.${linkHelper(resource, link)}(array(` },
|
|
585
|
-
...targets,
|
|
586
|
-
{ text: "))" }
|
|
587
|
-
]
|
|
588
|
-
}), ctx);
|
|
589
|
-
}
|
|
590
|
-
//#endregion
|
|
591
|
-
//#region src/cli/sql/grants.ts
|
|
592
|
-
/**
|
|
593
|
-
* The `role_permissions` grant key of a break-glass grant. It differs from
|
|
594
|
-
* the permission key, so `authorize()` and the table policies never count it;
|
|
595
|
-
* only the break-glass read asks for it.
|
|
596
|
-
*/
|
|
597
|
-
function breakGlassKey(permission) {
|
|
598
|
-
return `${permission}#break-glass`;
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* The role and scope holding a break-glass grant. `breakGlass()` sets
|
|
602
|
-
* `to: authenticated`, so inside `role()` the role is on the grant itself.
|
|
603
|
-
*/
|
|
604
|
-
function breakGlassHolder(item) {
|
|
605
|
-
if (item.access.kind === "role") return {
|
|
606
|
-
role: item.access.role,
|
|
607
|
-
scope: item.access.scope
|
|
608
|
-
};
|
|
609
|
-
const { role, scope } = item.grant;
|
|
610
|
-
return role === null || typeof scope !== "string" ? void 0 : {
|
|
611
|
-
role,
|
|
612
|
-
scope
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
function granteeItems(to) {
|
|
616
|
-
return Array.isArray(to) ? to : [to];
|
|
617
|
-
}
|
|
618
|
-
function relationWhere(policy, grant, grantee) {
|
|
619
|
-
const node = policy.resources.get(grant.permission.resource);
|
|
620
|
-
const where = relationCondition(grantee, node, scopeList(policy.scopes), {
|
|
621
|
-
resources: policy.resources,
|
|
622
|
-
now: { ref: "now" }
|
|
623
|
-
});
|
|
624
|
-
if (where === void 0) throw configError(`PermDock CLI: grant ${grant.permission.key} names relation '${grantee.relation}', which ${grant.permission.resource} does not declare`);
|
|
625
|
-
return where;
|
|
626
|
-
}
|
|
627
|
-
function inheritWhere(policy, grant, grantee) {
|
|
628
|
-
const where = inheritCondition(grantee, policy.resources.get(grant.permission.resource), policy.resources);
|
|
629
|
-
if (where === void 0) throw configError(`PermDock CLI: grant ${grant.permission.key} inherits ${grantee.permission}, which ${grant.permission.resource} cannot reach`);
|
|
630
|
-
return where;
|
|
631
|
-
}
|
|
632
|
-
/** Only a deny for `oauth-client` actors compiles: Postgres sees `client_id` and `act`, not other actor kinds. */
|
|
633
|
-
function actorAccess(grant, grantee, others) {
|
|
634
|
-
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`);
|
|
635
|
-
return {
|
|
636
|
-
kind: "actor",
|
|
637
|
-
actor: "oauth-client"
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
function accessOf(grant, roles, anyone) {
|
|
641
|
-
const [held, ...more] = roles;
|
|
642
|
-
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`);
|
|
643
|
-
if (held === void 0) return anyone ? { kind: "anyone" } : { kind: "authenticated" };
|
|
644
|
-
if (typeof held.scope === "object") return {
|
|
645
|
-
kind: "resource",
|
|
646
|
-
role: held.role,
|
|
647
|
-
resource: held.scope.resource
|
|
648
|
-
};
|
|
649
|
-
return {
|
|
650
|
-
kind: "role",
|
|
651
|
-
role: held.role,
|
|
652
|
-
scope: held.scope
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
/**
|
|
656
|
-
* Every grant RLS must enforce: role grants and top-level
|
|
657
|
-
* `definePolicy({ grants })` alike (`policy.grants` holds both). A grantee
|
|
658
|
-
* Postgres cannot see (plan, actor, assurance) fails with the permission named.
|
|
659
|
-
*/
|
|
660
|
-
function collectGrants(policy) {
|
|
661
|
-
return policy.grants.map((grant) => {
|
|
662
|
-
const items = granteeItems(grant.to);
|
|
663
|
-
const roles = [];
|
|
664
|
-
let anyone = true;
|
|
665
|
-
let where = grant.where;
|
|
666
|
-
let delegated;
|
|
667
|
-
for (const item of items) switch (item.kind) {
|
|
668
|
-
case "role":
|
|
669
|
-
roles.push(item);
|
|
670
|
-
break;
|
|
671
|
-
case "anyone": break;
|
|
672
|
-
case "authenticated":
|
|
673
|
-
anyone = false;
|
|
674
|
-
break;
|
|
675
|
-
case "relation":
|
|
676
|
-
anyone = false;
|
|
677
|
-
where = andConditions(where, relationWhere(policy, grant, item));
|
|
678
|
-
break;
|
|
679
|
-
case "inherit":
|
|
680
|
-
anyone = false;
|
|
681
|
-
where = andConditions(where, inheritWhere(policy, grant, item));
|
|
682
|
-
break;
|
|
683
|
-
case "actor":
|
|
684
|
-
delegated = actorAccess(grant, item, items.length - 1);
|
|
685
|
-
break;
|
|
686
|
-
case "plan":
|
|
687
|
-
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`);
|
|
688
|
-
default: return item;
|
|
689
|
-
}
|
|
690
|
-
const access = delegated ?? accessOf(grant, roles, anyone);
|
|
691
|
-
const label = access.kind === "role" || access.kind === "resource" ? access.role : access.kind;
|
|
692
|
-
return where === void 0 ? {
|
|
693
|
-
grant,
|
|
694
|
-
label,
|
|
695
|
-
access
|
|
696
|
-
} : {
|
|
697
|
-
grant,
|
|
698
|
-
label,
|
|
699
|
-
access,
|
|
700
|
-
where
|
|
701
|
-
};
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
/** The declared roles a custom role may draw grants from: the custom-role ceiling. */
|
|
705
|
-
function assignableRoles(policy) {
|
|
706
|
-
return [...roleNames(policy)].toSorted().filter((name) => policy.rolesByName.get(name)?.assignable ?? listRoles(policy.vocabulary.roles).some((leaf) => leaf.key === name && leaf.assignable));
|
|
707
|
-
}
|
|
708
|
-
/** Declared role names (bindings and `defineRoles` leaves) plus any role a top-level grant names. */
|
|
709
|
-
function roleNames(policy) {
|
|
710
|
-
const names = new Set(policy.roles.map((item) => item.name));
|
|
711
|
-
for (const leaf of listRoles(policy.vocabulary.roles)) names.add(leaf.key);
|
|
712
|
-
for (const item of collectGrants(policy)) if (item.access.kind === "role" || item.access.kind === "resource") names.add(item.access.role);
|
|
713
|
-
return [...names];
|
|
714
|
-
}
|
|
715
|
-
//#endregion
|
|
716
287
|
//#region src/cli/rls/api-keys.ts
|
|
717
288
|
const API_KEY_ALLOWS = "permdock_api_key_allows";
|
|
718
289
|
const DEFAULTS = {
|
|
@@ -838,7 +409,10 @@ ${body}
|
|
|
838
409
|
}
|
|
839
410
|
//#endregion
|
|
840
411
|
//#region src/cli/shared/compare.ts
|
|
841
|
-
/**
|
|
412
|
+
/**
|
|
413
|
+
* UTF-16 code-unit order, never `localeCompare`: generated SQL must match byte
|
|
414
|
+
* for byte on every machine.
|
|
415
|
+
*/
|
|
842
416
|
function byCodeUnit(a, b) {
|
|
843
417
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
844
418
|
}
|
|
@@ -854,10 +428,10 @@ const READERS = {
|
|
|
854
428
|
trusted: "permdock_trusted_role_permissions"
|
|
855
429
|
};
|
|
856
430
|
/**
|
|
857
|
-
* The custom-role objects: the tables (`database` mode), the ceiling view
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
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.
|
|
861
435
|
*/
|
|
862
436
|
function customRolesSql(ctx) {
|
|
863
437
|
const custom = ctx.customRoles;
|
|
@@ -984,7 +558,10 @@ revoke execute on function ${keys}(text[], text[], text[], text) from public, an
|
|
|
984
558
|
if (shared !== "") chunks.push(shared);
|
|
985
559
|
return chunks.join("\n\n");
|
|
986
560
|
}
|
|
987
|
-
/**
|
|
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
|
+
*/
|
|
988
565
|
function sharedRoleSql(ctx) {
|
|
989
566
|
if (ctx.authorize !== "database") return "";
|
|
990
567
|
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
@@ -1006,8 +583,8 @@ $$;
|
|
|
1006
583
|
revoke execute on function ${shared}(text, text) from public, anon, authenticated;`;
|
|
1007
584
|
}
|
|
1008
585
|
/**
|
|
1009
|
-
* `permdock_custom_keys` when resources declare levels. An allow entry is
|
|
1010
|
-
*
|
|
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>`
|
|
1011
588
|
* keys, plus the ceiling denies of that permission; a level no ceiling key
|
|
1012
589
|
* carries removes the permission, as `resolveCustomRole` drops it with
|
|
1013
590
|
* `unknown-level`.
|
|
@@ -1112,8 +689,8 @@ function currentKeysSql(array) {
|
|
|
1112
689
|
}
|
|
1113
690
|
/**
|
|
1114
691
|
* SQL fragments that read a stored former key as its current key inside
|
|
1115
|
-
* `permdock_custom_keys`. Without renames they are the plain column and
|
|
1116
|
-
*
|
|
692
|
+
* `permdock_custom_keys`. Without renames they are the plain column and array
|
|
693
|
+
* reads.
|
|
1117
694
|
*/
|
|
1118
695
|
function renamesSql(renames) {
|
|
1119
696
|
if (renames.length === 0) return {
|
|
@@ -1137,14 +714,14 @@ function renamesSql(renames) {
|
|
|
1137
714
|
}
|
|
1138
715
|
/**
|
|
1139
716
|
* The catalog reader `permdock_permission_keys()` and the role reader
|
|
1140
|
-
* `permdock_role_permissions(p_role, p_scope, p_tenant, p_scope_id)`, both
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1147
|
-
*
|
|
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.
|
|
1148
725
|
*/
|
|
1149
726
|
function roleReadersSql(ctx, permissions, trustedReaders) {
|
|
1150
727
|
const rp = qualified(ctx, "role_permissions");
|
|
@@ -1267,33 +844,42 @@ $$;
|
|
|
1267
844
|
revoke execute on function ${trusted}(${signature}) from public, anon, authenticated;${grants}`);
|
|
1268
845
|
return chunks.join("\n\n");
|
|
1269
846
|
}
|
|
1270
|
-
/**
|
|
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
|
+
*/
|
|
1271
851
|
function storedTenant(ctx) {
|
|
1272
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`;
|
|
1273
853
|
}
|
|
1274
854
|
/**
|
|
1275
|
-
* The tenant half of a custom-role row match for a membership: the
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
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.
|
|
1278
858
|
*/
|
|
1279
859
|
function customTenantMatch(ctx, tenant, scope, role) {
|
|
1280
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`;
|
|
1281
861
|
}
|
|
1282
862
|
/**
|
|
1283
|
-
* The scope of a row of the application's roles table: its scope column (or
|
|
1284
|
-
*
|
|
1285
|
-
*
|
|
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'`.
|
|
1286
866
|
*/
|
|
1287
867
|
function rolesTableScope(table, tenant, root, column) {
|
|
1288
868
|
const own = table.scope === void 0 ? quoteLiteral(root) : `${column(table.scope)}::text`;
|
|
1289
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`;
|
|
1290
870
|
}
|
|
1291
|
-
/**
|
|
871
|
+
/**
|
|
872
|
+
* `expr` read as its current key: a former key maps to the key it was renamed
|
|
873
|
+
* to.
|
|
874
|
+
*/
|
|
1292
875
|
function currentKeyExpr(renames, expr) {
|
|
1293
876
|
if (renames.length === 0) return expr;
|
|
1294
877
|
return `(case ${expr} ${renames.map(([former, current]) => `when ${quoteLiteral(former)} then ${quoteLiteral(current)}`).join(" ")} else ${expr} end)`;
|
|
1295
878
|
}
|
|
1296
|
-
/**
|
|
879
|
+
/**
|
|
880
|
+
* `case p_scope when '<scope>' then <sql for scope> ... else false end` over
|
|
881
|
+
* the declared scopes.
|
|
882
|
+
*/
|
|
1297
883
|
function perScope(ctx, sql) {
|
|
1298
884
|
const arms = ctx.scopes.map((scope) => `when ${quoteLiteral(scope.name)} then ${sql(scope.name)}`).join(" ");
|
|
1299
885
|
return arms === "" ? "false" : `(case p_scope ${arms} else false end)`;
|
|
@@ -1309,8 +895,8 @@ function hasCustomRoleChecksFor(ctx) {
|
|
|
1309
895
|
}
|
|
1310
896
|
/**
|
|
1311
897
|
* The grants a `rls.trustedReaders` role needs for one reader: usage on the
|
|
1312
|
-
* helper schema, which no client role reaches through these lines, and
|
|
1313
|
-
*
|
|
898
|
+
* helper schema, which no client role reaches through these lines, and execute
|
|
899
|
+
* on the function.
|
|
1314
900
|
*/
|
|
1315
901
|
function trustedReaderGrants(ctx, fn, readers) {
|
|
1316
902
|
const to = readers.map(quoteIdent).join(", ");
|
|
@@ -1326,7 +912,10 @@ function throughTenant(through, roles) {
|
|
|
1326
912
|
if (through === void 0 || roles?.tenant === void 0) return;
|
|
1327
913
|
return qualify(roles.table, "public") === through.table ? roles.tenant : void 0;
|
|
1328
914
|
}
|
|
1329
|
-
/**
|
|
915
|
+
/**
|
|
916
|
+
* Resolves `source` against `schema`, aliasing the table `alias` and the roles
|
|
917
|
+
* table `<alias>k`.
|
|
918
|
+
*/
|
|
1330
919
|
function globalRoleSource(source, schema, alias, roles) {
|
|
1331
920
|
const table = qualify(source.table, schema);
|
|
1332
921
|
const user = source.user ?? "user_id";
|
|
@@ -1360,11 +949,17 @@ function keyTenantLine(ctx, scope, tenant) {
|
|
|
1360
949
|
const keyed = keyTenantSql(ctx, underRoot(ctx, scope) ? tenant : void 0);
|
|
1361
950
|
return keyed === void 0 ? "" : `\n and ${keyed}`;
|
|
1362
951
|
}
|
|
1363
|
-
/**
|
|
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
|
+
*/
|
|
1364
956
|
function narrowsTo(ctx, scope) {
|
|
1365
957
|
return ctx.tenants !== "all" && underRoot(ctx, scope);
|
|
1366
958
|
}
|
|
1367
|
-
/**
|
|
959
|
+
/**
|
|
960
|
+
* Whether the active tenant narrows memberships of `scope`: the first scope is
|
|
961
|
+
* on its chain.
|
|
962
|
+
*/
|
|
1368
963
|
function underRoot(ctx, scope) {
|
|
1369
964
|
const root = ctx.scopes[0]?.name;
|
|
1370
965
|
let current = ctx.scopes.find((item) => item.name === scope);
|
|
@@ -1377,11 +972,17 @@ function underRoot(ctx, scope) {
|
|
|
1377
972
|
}
|
|
1378
973
|
return false;
|
|
1379
974
|
}
|
|
1380
|
-
/**
|
|
975
|
+
/**
|
|
976
|
+
* `and exists (...)` lines for an active user; empty without
|
|
977
|
+
* `rls.suspension.users`.
|
|
978
|
+
*/
|
|
1381
979
|
function userActive(ctx, indent, user) {
|
|
1382
980
|
return activeUserSql(ctx, user).map((part) => `${indent}and ${part}`);
|
|
1383
981
|
}
|
|
1384
|
-
/**
|
|
982
|
+
/**
|
|
983
|
+
* `and exists (...)` lines for every suspendable instance on the chain of
|
|
984
|
+
* `scope`'s membership.
|
|
985
|
+
*/
|
|
1385
986
|
function instancesActive(ctx, scope, idOf, indent, permission) {
|
|
1386
987
|
return activeInstancesSql(ctx, scope, idOf, permission).map((part) => `${indent}and ${part}`);
|
|
1387
988
|
}
|
|
@@ -1520,7 +1121,10 @@ function hasBody(ctx) {
|
|
|
1520
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()}
|
|
1521
1122
|
))`;
|
|
1522
1123
|
}
|
|
1523
|
-
/**
|
|
1124
|
+
/**
|
|
1125
|
+
* The claim `jwt` mode reads platform custom roles from:
|
|
1126
|
+
* `customRoleClaim(globalRoles)`.
|
|
1127
|
+
*/
|
|
1524
1128
|
const GLOBAL_GRANTS_CLAIM = "role_grants";
|
|
1525
1129
|
function memberColumn(name) {
|
|
1526
1130
|
return `m.${quoteIdent(name)}`;
|
|
@@ -1663,8 +1267,8 @@ ${customKeysSql(ctx, scope, claimEntries("left(e, 1) not in ('-', '@')", "e"), c
|
|
|
1663
1267
|
}
|
|
1664
1268
|
/**
|
|
1665
1269
|
* The rows the membership sources select for the subject, one per instance
|
|
1666
|
-
* (`scope`, `id`, `within`, `roles`, `via`), with each source's own expiry
|
|
1667
|
-
*
|
|
1270
|
+
* (`scope`, `id`, `within`, `roles`, `via`), with each source's own expiry and
|
|
1271
|
+
* suspension filters: the statements the token hook runs.
|
|
1668
1272
|
*/
|
|
1669
1273
|
function sourcesKeep(sources) {
|
|
1670
1274
|
return sources.some((source) => source.sql.keeps);
|
|
@@ -1688,7 +1292,10 @@ function sourcesBodyOf(sql, sources, user) {
|
|
|
1688
1292
|
vars: sources.map((source, index) => `${sourceUser(index)} ${source.sql.userType} := ${user};`)
|
|
1689
1293
|
};
|
|
1690
1294
|
}
|
|
1691
|
-
/**
|
|
1295
|
+
/**
|
|
1296
|
+
* `language sql`, or `plpgsql` returning the query when the body declares
|
|
1297
|
+
* variables.
|
|
1298
|
+
*/
|
|
1692
1299
|
function functionBody(body) {
|
|
1693
1300
|
if (body.vars.length === 0) return `language sql
|
|
1694
1301
|
stable
|
|
@@ -1774,7 +1381,10 @@ function scopedBody(ctx, scope, type) {
|
|
|
1774
1381
|
}
|
|
1775
1382
|
//#endregion
|
|
1776
1383
|
//#region src/cli/rls/capabilities.ts
|
|
1777
|
-
/**
|
|
1384
|
+
/**
|
|
1385
|
+
* The helper `--capabilities` adds: ids of one resource a link capability claim
|
|
1386
|
+
* reaches. Part of the SQL contract.
|
|
1387
|
+
*/
|
|
1778
1388
|
const CAPABILITIES = { ids: "permdock_capability_ids" };
|
|
1779
1389
|
/**
|
|
1780
1390
|
* The `anon` branch access for a resource-scoped grant reached through a link
|
|
@@ -1824,8 +1434,8 @@ const col = (name) => `r.${quoteIdent(name)}`;
|
|
|
1824
1434
|
* (`rls.customRoleWrites.roles.primaryKey`): the tenant, scope, instance and
|
|
1825
1435
|
* name come from the row, never from the caller. The writers delegate to
|
|
1826
1436
|
* `permdock_replace_custom_role_grants` (or its trusted variant), so every
|
|
1827
|
-
* caller and definition check applies, and keep the stored denies and
|
|
1828
|
-
*
|
|
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
|
|
1829
1439
|
* `rls.trustedReaders` alone.
|
|
1830
1440
|
*/
|
|
1831
1441
|
function customRoleByIdSql(ctx, trustedReaders) {
|
|
@@ -1959,12 +1569,11 @@ function cascadeMatch(prefix) {
|
|
|
1959
1569
|
/**
|
|
1960
1570
|
* The trigger on the application's roles table (`rls.customRoleWrites.roles`):
|
|
1961
1571
|
* a rename, a move to another tenant, scope or instance, or a delete of a
|
|
1962
|
-
* custom role's row carries its grants and includes along. A signed-in
|
|
1963
|
-
*
|
|
1964
|
-
*
|
|
1965
|
-
*
|
|
1966
|
-
*
|
|
1967
|
-
* 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.
|
|
1968
1577
|
*/
|
|
1969
1578
|
function customRoleCascadeSql(ctx) {
|
|
1970
1579
|
const custom = ctx.customRoles;
|
|
@@ -2069,9 +1678,9 @@ function column(name) {
|
|
|
2069
1678
|
/**
|
|
2070
1679
|
* The trigger on the application's roles table (`rls.customRoleWrites.roles`)
|
|
2071
1680
|
* that refuses a tenant row named like a declared role (`declared-role`) or
|
|
2072
|
-
* like a shared custom role (`name-taken`). A row the `skip` column marks is
|
|
2073
|
-
*
|
|
2074
|
-
*
|
|
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.
|
|
2075
1684
|
*/
|
|
2076
1685
|
function customRoleNameGuardSql(ctx) {
|
|
2077
1686
|
const custom = ctx.customRoles;
|
|
@@ -2238,8 +1847,8 @@ revoke execute on function ${guard}(${userArg}${tenantType}, text, text, text) f
|
|
|
2238
1847
|
};
|
|
2239
1848
|
}
|
|
2240
1849
|
/**
|
|
2241
|
-
* The functions an application calls to save, rename and delete a custom
|
|
2242
|
-
*
|
|
1850
|
+
* The functions an application calls to save, rename and delete a custom role
|
|
1851
|
+
* in \`database\` mode, with the rules of \`validateCustomRole\` and
|
|
2243
1852
|
* \`assignablePermissions\`: entries inside the ceiling of the role's scope,
|
|
2244
1853
|
* includes naming declared roles, and only permissions and levels the caller
|
|
2245
1854
|
* may hand out, for the new definition and the stored one alike. A platform
|
|
@@ -2477,9 +2086,9 @@ end;`, false)}`;
|
|
|
2477
2086
|
//#region src/cli/rls/global-roles.ts
|
|
2478
2087
|
/**
|
|
2479
2088
|
* `permdock_replace_global_roles(p_user, p_roles)`: sets the global roles of
|
|
2480
|
-
* one user in `rls.roles` in one statement pair. It runs as the invoker, so
|
|
2481
|
-
*
|
|
2482
|
-
*
|
|
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.
|
|
2483
2092
|
*/
|
|
2484
2093
|
function globalRolesSql(ctx) {
|
|
2485
2094
|
if (ctx.roles === void 0) return "";
|
|
@@ -2552,9 +2161,9 @@ grant execute on function ${fn}(${args}) to authenticated;`;
|
|
|
2552
2161
|
//#endregion
|
|
2553
2162
|
//#region src/cli/rls/helper-members.ts
|
|
2554
2163
|
/**
|
|
2555
|
-
* The body of `member_<scope>_ids()`: instances of `scope` the subject holds
|
|
2556
|
-
*
|
|
2557
|
-
*
|
|
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.
|
|
2558
2167
|
*/
|
|
2559
2168
|
function memberBody(ctx, scope, type, roles) {
|
|
2560
2169
|
if (ctx.authorize !== "database") return sqlBody(` select distinct (m ->> 'id')::${type}
|
|
@@ -2654,8 +2263,8 @@ ${grants}`;
|
|
|
2654
2263
|
/**
|
|
2655
2264
|
* `member_<scope>_ids_for(p_user uuid)`: `member_<scope>_ids()` for a user the
|
|
2656
2265
|
* caller names, read from the membership sources (or the mapped table) in
|
|
2657
|
-
* either mode, so a token hook can call it before any claim exists. Naming
|
|
2658
|
-
*
|
|
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
|
|
2659
2268
|
* `supabase_auth_admin` execute.
|
|
2660
2269
|
*/
|
|
2661
2270
|
function memberForFunction(ctx, scope, type) {
|
|
@@ -2665,10 +2274,28 @@ returns setof ${type}
|
|
|
2665
2274
|
${functionBody(memberRowsBody(ctx, scope, type, "p_user", memberForSources(ctx, scope)))}
|
|
2666
2275
|
revoke execute on function ${fn}(uuid) from public, anon, authenticated;`;
|
|
2667
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
|
+
}
|
|
2668
2292
|
//#endregion
|
|
2669
2293
|
//#region src/cli/rls/token-scopes.ts
|
|
2670
2294
|
const TOKEN_SCOPE_ALLOWS = "permdock_token_scope_allows";
|
|
2671
|
-
/**
|
|
2295
|
+
/**
|
|
2296
|
+
* `rls.tokenScopes` as the SQL generator needs it, or `undefined` when it is
|
|
2297
|
+
* off.
|
|
2298
|
+
*/
|
|
2672
2299
|
function tokenScopesPlan(setting, policy) {
|
|
2673
2300
|
if (setting !== true) return;
|
|
2674
2301
|
const permissions = listPermissions(policy.permissions).map((leaf) => ({
|
|
@@ -2681,7 +2308,10 @@ function tokenScopesPlan(setting, policy) {
|
|
|
2681
2308
|
function tokenScopeAllowsCall(ctx, grant) {
|
|
2682
2309
|
return `${qualified(ctx, TOKEN_SCOPE_ALLOWS)}(${grant})`;
|
|
2683
2310
|
}
|
|
2684
|
-
/**
|
|
2311
|
+
/**
|
|
2312
|
+
* The token's scopes as rows of `s(scope)`: a space-separated string or,
|
|
2313
|
+
* outside `guc`, a JSON array.
|
|
2314
|
+
*/
|
|
2685
2315
|
function tokenScopeRows(ctx) {
|
|
2686
2316
|
if (ctx.dialect === "guc") return `regexp_split_to_table(btrim(coalesce(${subjectClaimSql(ctx, "scope")}, '')), '\\s+') s(scope)`;
|
|
2687
2317
|
const scope = subjectClaimJsonSql(ctx, "scope");
|
|
@@ -2735,9 +2365,9 @@ ${grants}`;
|
|
|
2735
2365
|
//#endregion
|
|
2736
2366
|
//#region src/cli/rls/helpers.ts
|
|
2737
2367
|
/**
|
|
2738
|
-
* The policy-side call for a grant key. Every form is uncorrelated, so
|
|
2739
|
-
*
|
|
2740
|
-
*
|
|
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.
|
|
2741
2371
|
*/
|
|
2742
2372
|
function accessSql(ctx, scope, grantKey, column, helper) {
|
|
2743
2373
|
const key = quoteLiteral(grantKey);
|
|
@@ -2796,11 +2426,11 @@ ${grants}`;
|
|
|
2796
2426
|
* `permdock_has_for(p_user, p_grant)` and one
|
|
2797
2427
|
* `permitted_<scope>_ids_for(p_user, p_grant)` per scope in `database` mode:
|
|
2798
2428
|
* the bodies of `permdock_has` and `permitted_<scope>_ids` with `p_user` for
|
|
2799
|
-
* the signed-in user and no tenant-claim narrowing, since a named user has
|
|
2800
|
-
*
|
|
2801
|
-
*
|
|
2802
|
-
*
|
|
2803
|
-
*
|
|
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.
|
|
2804
2434
|
*/
|
|
2805
2435
|
function forUserSql(ctx) {
|
|
2806
2436
|
if (ctx.authorize !== "database") return "";
|
|
@@ -2823,8 +2453,8 @@ function userIdType(ctx) {
|
|
|
2823
2453
|
return ctx.dialect === "supabase" ? "uuid not null references auth.users on delete cascade" : "text not null";
|
|
2824
2454
|
}
|
|
2825
2455
|
/**
|
|
2826
|
-
* The `role_permissions` rows the policy compiles to: an upsert and a delete
|
|
2827
|
-
*
|
|
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
|
|
2828
2458
|
* declarative schema; `--split ...,seeds` writes it as its own migration.
|
|
2829
2459
|
*/
|
|
2830
2460
|
function seedSql(ctx, rows) {
|
|
@@ -2868,7 +2498,10 @@ ${rows.length === 0 ? `select null::text, null::text, null::text, null::text, nu
|
|
|
2868
2498
|
].map(quoteLiteral).join(", ")})`).join(",\n")}\n) as v(${columns})`};
|
|
2869
2499
|
revoke all on table ${view} from anon, authenticated, public;`;
|
|
2870
2500
|
}
|
|
2871
|
-
/**
|
|
2501
|
+
/**
|
|
2502
|
+
* `<schema>.seed()`: the same upsert and delete as `seedSql`, for a declarative
|
|
2503
|
+
* schema that carries no rows.
|
|
2504
|
+
*/
|
|
2872
2505
|
function seedFunctionSql(ctx, rows) {
|
|
2873
2506
|
const fn = qualified(ctx, "seed");
|
|
2874
2507
|
return `-- rls.seeds: 'function': the role_permissions rows are data, so a declarative diff leaves them out; run select ${fn}(); after each deploy
|
|
@@ -2884,8 +2517,8 @@ $permdock$;
|
|
|
2884
2517
|
revoke execute on function ${fn}() from public, anon, authenticated;`;
|
|
2885
2518
|
}
|
|
2886
2519
|
/**
|
|
2887
|
-
* `role_permissions`, its seeds, and the three `security definer` helpers
|
|
2888
|
-
*
|
|
2520
|
+
* `role_permissions`, its seeds, and the three `security definer` helpers with
|
|
2521
|
+
* `search_path = ''`. `execute` goes to `authenticated` only.
|
|
2889
2522
|
*/
|
|
2890
2523
|
function helpersSql(ctx, rows, options) {
|
|
2891
2524
|
const schema = helperSchema(ctx);
|
|
@@ -2937,6 +2570,7 @@ revoke all on table ${ur} from anon, authenticated, public;`);
|
|
|
2937
2570
|
chunks.push(memberFunction(ctx, scope.name, type, anon));
|
|
2938
2571
|
if (ctx.roleMemberScopes?.includes(scope.name) === true) chunks.push(memberRolesFunction(ctx, scope.name, type, anon));
|
|
2939
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));
|
|
2940
2574
|
}
|
|
2941
2575
|
chunks.push(...viaHelpersSql(ctx, anon));
|
|
2942
2576
|
const forUser = forUserSql(ctx);
|
|
@@ -3002,18 +2636,27 @@ function forUserContext(ctx) {
|
|
|
3002
2636
|
}
|
|
3003
2637
|
//#endregion
|
|
3004
2638
|
//#region src/cli/rls/permission-keys.ts
|
|
3005
|
-
/**
|
|
2639
|
+
/**
|
|
2640
|
+
* The helpers that take a permission key instead of a grant key. Names are part
|
|
2641
|
+
* of the SQL contract.
|
|
2642
|
+
*/
|
|
3006
2643
|
const PERMISSION_HELPERS = {
|
|
3007
2644
|
grantKeys: "grant_keys",
|
|
3008
2645
|
has: "permdock_has_permission",
|
|
3009
2646
|
hasFor: "permdock_has_permission_for"
|
|
3010
2647
|
};
|
|
3011
|
-
/**
|
|
2648
|
+
/**
|
|
2649
|
+
* `permitted_<scope>_permission_keys(p_id)`: every permission key the subject
|
|
2650
|
+
* holds on one instance.
|
|
2651
|
+
*/
|
|
3012
2652
|
function permissionKeysHelper(scope) {
|
|
3013
2653
|
permittedIdsHelper(scope);
|
|
3014
2654
|
return `permitted_${scope}_permission_keys`;
|
|
3015
2655
|
}
|
|
3016
|
-
/**
|
|
2656
|
+
/**
|
|
2657
|
+
* `permitted_<scope>_permission_keys_for(p_user, p_id)`, written in `database`
|
|
2658
|
+
* mode.
|
|
2659
|
+
*/
|
|
3017
2660
|
function permissionKeysForHelper(scope) {
|
|
3018
2661
|
return `${permissionKeysHelper(scope)}_for`;
|
|
3019
2662
|
}
|
|
@@ -3033,7 +2676,10 @@ function requiresSql(ctx, key, resource) {
|
|
|
3033
2676
|
return `${quoteIdent(field)} in (select ${qualified(ctx, checkName(permittedByPermissionHelper(scope)))}(${literal}))`;
|
|
3034
2677
|
})].join(" or ")})`;
|
|
3035
2678
|
}
|
|
3036
|
-
/**
|
|
2679
|
+
/**
|
|
2680
|
+
* Postgres truncates an identifier past 63 bytes, which would merge two
|
|
2681
|
+
* helpers.
|
|
2682
|
+
*/
|
|
3037
2683
|
function checkName(name) {
|
|
3038
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`);
|
|
3039
2685
|
return name;
|
|
@@ -3084,17 +2730,17 @@ ${access}`;
|
|
|
3084
2730
|
}
|
|
3085
2731
|
/**
|
|
3086
2732
|
* The permission-key forms of the helpers: `grant_keys`, which maps a
|
|
3087
|
-
* permission key to the grant keys of its unconditional allows on a scope
|
|
3088
|
-
*
|
|
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),
|
|
3089
2735
|
* `permdock_has_permission(p_permission)` and one
|
|
3090
2736
|
* `permitted_<scope>_ids_by_permission(p_permission)` per scope, plus their
|
|
3091
2737
|
* `_for(p_user, ...)` forms in `database` mode. They answer with the
|
|
3092
|
-
* unconditional allows minus any deny, so a permission whose allows all carry
|
|
3093
|
-
*
|
|
2738
|
+
* unconditional allows minus any deny, so a permission whose allows all carry a
|
|
2739
|
+
* row condition answers nothing. The
|
|
3094
2740
|
* `permitted_<scope>_ids_by_permission(p_permission, p_conditioned)` overload
|
|
3095
2741
|
* with `true` also lists the instances a conditioned allow reaches and
|
|
3096
|
-
* subtracts only unconditional denies, leaving the row condition to the
|
|
3097
|
-
*
|
|
2742
|
+
* subtracts only unconditional denies, leaving the row condition to the caller,
|
|
2743
|
+
* as `permittedIds(..., { conditioned: true })` does in process.
|
|
3098
2744
|
*/
|
|
3099
2745
|
function permissionHelpersSql(ctx, grants, renamed, anonExecute) {
|
|
3100
2746
|
const keys = qualified(ctx, PERMISSION_HELPERS.grantKeys);
|
|
@@ -3218,7 +2864,10 @@ ${keysFunction(keysFor, `p_user ${user}, p_id ${type}, p_keys text[]`, keysBody(
|
|
|
3218
2864
|
}
|
|
3219
2865
|
//#endregion
|
|
3220
2866
|
//#region src/cli/rls/ownership-names.ts
|
|
3221
|
-
/**
|
|
2867
|
+
/**
|
|
2868
|
+
* Objects the ownership rules add next to the helpers. Names are part of the
|
|
2869
|
+
* SQL contract.
|
|
2870
|
+
*/
|
|
3222
2871
|
const OWNERSHIP = {
|
|
3223
2872
|
canAssign: "permdock_can_assign",
|
|
3224
2873
|
canAssignFor: "permdock_can_assign_for",
|
|
@@ -3234,231 +2883,14 @@ const ASSIGNMENTS = {
|
|
|
3234
2883
|
trigger: "permdock_assignment"
|
|
3235
2884
|
};
|
|
3236
2885
|
//#endregion
|
|
3237
|
-
//#region src/cli/rls/assignments.ts
|
|
3238
|
-
/** The client roles whose writes the assignment triggers check; any other role is a trusted path. */
|
|
3239
|
-
const CLIENT_ROLES = [
|
|
3240
|
-
"anon",
|
|
3241
|
-
"anonymous",
|
|
3242
|
-
"authenticated"
|
|
3243
|
-
];
|
|
3244
|
-
/** The mapped membership tables, `rls.assignments.tables` and the global-roles table `rls.roles`. */
|
|
3245
|
-
function assignedTables(ctx) {
|
|
3246
|
-
const out = [];
|
|
3247
|
-
for (const { name } of ctx.scopes) {
|
|
3248
|
-
const mapped = scopeTable(ctx, name);
|
|
3249
|
-
if (mapped === void 0) continue;
|
|
3250
|
-
out.push({
|
|
3251
|
-
table: mapped.table.table,
|
|
3252
|
-
scope: name,
|
|
3253
|
-
id: mapped.column,
|
|
3254
|
-
tenant: mapped.tenantColumn ?? mapped.column,
|
|
3255
|
-
role: mapped.table.role,
|
|
3256
|
-
user: mapped.table.user
|
|
3257
|
-
});
|
|
3258
|
-
}
|
|
3259
|
-
if (ctx.roles !== void 0) out.push({
|
|
3260
|
-
table: ctx.roles.table,
|
|
3261
|
-
scope: "global",
|
|
3262
|
-
role: ctx.roles.role ?? "role",
|
|
3263
|
-
user: ctx.roles.user ?? "user_id"
|
|
3264
|
-
});
|
|
3265
|
-
for (const extra of ctx.assignments?.tables ?? []) {
|
|
3266
|
-
const scope = resolveScope(ctx.scopes, extra.scope);
|
|
3267
|
-
if (scope === void 0) throw configError(`PermDock CLI: rls.assignments.tables names scope '${extra.scope}', which the policy does not declare`);
|
|
3268
|
-
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`);
|
|
3269
|
-
out.push({
|
|
3270
|
-
table: extra.table,
|
|
3271
|
-
scope,
|
|
3272
|
-
id: extra.id,
|
|
3273
|
-
tenant: extra.tenant ?? extra.id,
|
|
3274
|
-
role: extra.role,
|
|
3275
|
-
...extra.user === void 0 ? {} : { user: extra.user }
|
|
3276
|
-
});
|
|
3277
|
-
}
|
|
3278
|
-
return out;
|
|
3279
|
-
}
|
|
3280
|
-
/** The tables `rls.assignments.ownRole` refuses a caller's own rows on, by table key. */
|
|
3281
|
-
function ownRoleTables(ctx, tables) {
|
|
3282
|
-
const own = ctx.assignments?.ownRole;
|
|
3283
|
-
if (own === void 0) return /* @__PURE__ */ new Set();
|
|
3284
|
-
if (own === "refuse") return new Set(tables.filter((entry) => entry.user !== void 0).map((entry) => qualifySqlTable(entry.table)));
|
|
3285
|
-
const out = /* @__PURE__ */ new Set();
|
|
3286
|
-
for (const [name, value] of Object.entries(own)) {
|
|
3287
|
-
const entry = tables.find((table) => qualifySqlTable(table.table) === qualifySqlTable(name));
|
|
3288
|
-
if (entry === void 0) throw configError(`PermDock CLI: rls.assignments.ownRole names ${name}, which no assignment trigger guards`);
|
|
3289
|
-
if (value !== "refuse") throw configError(`PermDock CLI: rls.assignments.ownRole.${name} must be 'refuse'`);
|
|
3290
|
-
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`);
|
|
3291
|
-
out.add(qualifySqlTable(entry.table));
|
|
3292
|
-
}
|
|
3293
|
-
return out;
|
|
3294
|
-
}
|
|
3295
|
-
/**
|
|
3296
|
-
* The tables `rls.assignments` guards, schema-qualified: each scope's mapped
|
|
3297
|
-
* membership table, the global-roles table (`rls.roles`, else
|
|
3298
|
-
* `supabase.hook.roles`) and `rls.assignments.tables`. Empty without
|
|
3299
|
-
* `rls.assignments`.
|
|
3300
|
-
*/
|
|
3301
|
-
function guardedTables(config, scopes) {
|
|
3302
|
-
const rls = config.rls;
|
|
3303
|
-
if (rls?.assignments === void 0) return [];
|
|
3304
|
-
const hook = config.supabase?.hook?.roles;
|
|
3305
|
-
const roles = rls.roles ?? (hook === false ? void 0 : hook);
|
|
3306
|
-
const tables = [
|
|
3307
|
-
...scopes.flatMap((scope) => {
|
|
3308
|
-
const mapped = memberForTable({
|
|
3309
|
-
scopes,
|
|
3310
|
-
...rls.memberships === void 0 ? {} : { memberships: rls.memberships }
|
|
3311
|
-
}, scope.name);
|
|
3312
|
-
return mapped === void 0 ? [] : [mapped.table];
|
|
3313
|
-
}),
|
|
3314
|
-
...roles === void 0 ? [] : [roles.table],
|
|
3315
|
-
...(rls.assignments === true ? [] : rls.assignments.tables ?? []).map((entry) => entry.table)
|
|
3316
|
-
];
|
|
3317
|
-
return [...new Set(tables.map(qualifySqlTable))];
|
|
3318
|
-
}
|
|
3319
|
-
function isRoleList(role) {
|
|
3320
|
-
return Array.isArray(role);
|
|
3321
|
-
}
|
|
3322
|
-
/** The role keys a row variable holds, one scalar per role source. */
|
|
3323
|
-
function rowRoles(entry, row) {
|
|
3324
|
-
return (isRoleList(entry.role) ? entry.role : [entry.role]).map((part) => roleColumn(part, qualifySqlTable(entry.table), row, {
|
|
3325
|
-
label: `rls.assignments ${entry.table} role`,
|
|
3326
|
-
prefix: "PermDock CLI"
|
|
3327
|
-
}).lookup);
|
|
3328
|
-
}
|
|
3329
|
-
/**
|
|
3330
|
-
* `permdock_can_assign_custom_role`: whether the signed-in caller may assign a
|
|
3331
|
-
* custom role of the tenant at the instance, by the checks the custom-role
|
|
3332
|
-
* write functions run on its stored definition.
|
|
3333
|
-
*/
|
|
3334
|
-
function canAssignCustomSql(ctx, forUser = false) {
|
|
3335
|
-
const tenantType = tenantTypeOf(ctx);
|
|
3336
|
-
const fn = qualified(ctx, forUser ? ASSIGNMENTS.customFor : ASSIGNMENTS.custom);
|
|
3337
|
-
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
3338
|
-
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
3339
|
-
const guard = qualified(ctx, forUser ? CUSTOM_ROLES.guardFor : CUSTOM_ROLES.guard);
|
|
3340
|
-
const userParam = forUser ? "p_user uuid, " : "";
|
|
3341
|
-
const userArg = forUser ? "uuid, " : "";
|
|
3342
|
-
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)`;
|
|
3343
|
-
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;
|
|
3344
|
-
grant execute on function ${fn}(${tenantType}, text, text, text) to authenticated;`;
|
|
3345
|
-
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" : ""}
|
|
3346
|
-
create or replace function ${fn}(${userParam}p_tenant ${tenantType}, p_scope text, p_scope_id text, p_role text)
|
|
3347
|
-
returns boolean
|
|
3348
|
-
language plpgsql
|
|
3349
|
-
stable
|
|
3350
|
-
security definer
|
|
3351
|
-
set search_path = ''
|
|
3352
|
-
as $$
|
|
3353
|
-
declare
|
|
3354
|
-
v_scope_id text;
|
|
3355
|
-
v_found boolean := false;
|
|
3356
|
-
begin
|
|
3357
|
-
select true, c.scope_id into v_found, v_scope_id
|
|
3358
|
-
from (
|
|
3359
|
-
select c.scope_id from ${perms} c where ${stored}
|
|
3360
|
-
union all
|
|
3361
|
-
select c.scope_id from ${includes} c where ${stored}
|
|
3362
|
-
) c
|
|
3363
|
-
order by c.scope_id nulls first
|
|
3364
|
-
limit 1;
|
|
3365
|
-
if not coalesce(v_found, false) then
|
|
3366
|
-
return false;
|
|
3367
|
-
end if;
|
|
3368
|
-
perform ${guard}(${forUser ? "p_user, " : ""}p_tenant, p_scope, v_scope_id, p_role);
|
|
3369
|
-
return true;
|
|
3370
|
-
exception
|
|
3371
|
-
when insufficient_privilege or invalid_parameter_value then
|
|
3372
|
-
return false;
|
|
3373
|
-
end;
|
|
3374
|
-
$$;
|
|
3375
|
-
${grants}`;
|
|
3376
|
-
}
|
|
3377
|
-
/**
|
|
3378
|
-
* The assignment triggers (\`rls.assignments\`): before a client role inserts,
|
|
3379
|
-
* updates or deletes a row that assigns a role, every role the old and new
|
|
3380
|
-
* row hold must be one the caller may assign at that instance, by
|
|
3381
|
-
* \`permdock_can_assign\` for a declared role and
|
|
3382
|
-
* \`permdock_can_assign_custom_role\` for a custom one. The functions run as
|
|
3383
|
-
* the invoker, so \`current_user\` tells a client write from a trusted one.
|
|
3384
|
-
*/
|
|
3385
|
-
function assignmentSql(ctx) {
|
|
3386
|
-
const own = ctx.ownership;
|
|
3387
|
-
if (ctx.assignments === void 0 || own === void 0 || own.assigns.length === 0) return "";
|
|
3388
|
-
const canAssign = qualified(ctx, OWNERSHIP.canAssign);
|
|
3389
|
-
const custom = ctx.customRoles !== void 0 && ctx.authorize === "database";
|
|
3390
|
-
const declared = ctx.customRoles?.declared;
|
|
3391
|
-
const tenantType = tenantTypeOf(ctx);
|
|
3392
|
-
const chunks = [];
|
|
3393
|
-
const clients = `current_user::text = any(array[${CLIENT_ROLES.map(quoteLiteral).join(", ")}])`;
|
|
3394
|
-
const tables = assignedTables(ctx);
|
|
3395
|
-
const ownRole = ownRoleTables(ctx, tables);
|
|
3396
|
-
for (const entry of tables) {
|
|
3397
|
-
const table = quoteQualified(entry.table);
|
|
3398
|
-
const suffix = entry.table.replaceAll(/[^a-z0-9_]/gu, "_");
|
|
3399
|
-
const fn = qualified(ctx, quoteIdent(`${ASSIGNMENTS.trigger}_${suffix}`));
|
|
3400
|
-
const ownUser = entry.user !== void 0 && ownRole.has(qualifySqlTable(entry.table)) ? entry.user : void 0;
|
|
3401
|
-
const ownCheck = (row, reason) => ownUser === void 0 ? "" : ` if ${row}.${quoteIdent(ownUser)}::text = (${subjectIdSql(ctx)})::text then
|
|
3402
|
-
raise exception using
|
|
3403
|
-
errcode = '42501',
|
|
3404
|
-
message = 'permdock: the caller may not change their own roles',
|
|
3405
|
-
hint = '${reason}';
|
|
3406
|
-
end if;
|
|
3407
|
-
`;
|
|
3408
|
-
const check = (row) => {
|
|
3409
|
-
const id = entry.id === void 0 ? "null::text" : `${row}.${quoteIdent(entry.id)}::text`;
|
|
3410
|
-
const customFn = qualified(ctx, ASSIGNMENTS.custom);
|
|
3411
|
-
let allowed = `${canAssign}(v_role, ${id})`;
|
|
3412
|
-
if (custom && declared !== void 0) {
|
|
3413
|
-
const platform = `${customFn}(null, 'global', null, v_role)`;
|
|
3414
|
-
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`;
|
|
3415
|
-
}
|
|
3416
|
-
return ` foreach v_role in array array[${rowRoles(entry, row).join(", ")}]::text[] loop
|
|
3417
|
-
if v_role is not null and not coalesce(${allowed}, false) then
|
|
3418
|
-
raise exception using
|
|
3419
|
-
errcode = '42501',
|
|
3420
|
-
message = 'permdock: the caller may not assign ' || v_role || ' in ' || coalesce(${id}, 'null'),
|
|
3421
|
-
hint = 'not-assignable-by';
|
|
3422
|
-
end if;
|
|
3423
|
-
end loop;`;
|
|
3424
|
-
};
|
|
3425
|
-
const what = entry.id === void 0 ? "global roles" : `${entry.scope} roles`;
|
|
3426
|
-
chunks.push(`-- ${entry.table}: a client role may write only the ${what} it may assign${ownUser === void 0 ? "" : ", and none on its own rows"}
|
|
3427
|
-
create or replace function ${fn}()
|
|
3428
|
-
returns trigger
|
|
3429
|
-
language plpgsql
|
|
3430
|
-
volatile
|
|
3431
|
-
set search_path = ''
|
|
3432
|
-
as $$
|
|
3433
|
-
declare
|
|
3434
|
-
v_role text;
|
|
3435
|
-
begin
|
|
3436
|
-
if not (${clients}) then
|
|
3437
|
-
return coalesce(new, old);
|
|
3438
|
-
end if;
|
|
3439
|
-
if tg_op in ('UPDATE', 'DELETE') then
|
|
3440
|
-
${ownCheck("old", "self-demotion")}${check("old")}
|
|
3441
|
-
end if;
|
|
3442
|
-
if tg_op in ('INSERT', 'UPDATE') then
|
|
3443
|
-
${ownCheck("new", "not-assignable-by")}${check("new")}
|
|
3444
|
-
end if;
|
|
3445
|
-
return coalesce(new, old);
|
|
3446
|
-
end;
|
|
3447
|
-
$$;
|
|
3448
|
-
revoke execute on function ${fn}() from public, anon, authenticated;
|
|
3449
|
-
drop trigger if exists "permdock_assignment" on ${table};
|
|
3450
|
-
create trigger "permdock_assignment"
|
|
3451
|
-
before insert or update or delete on ${table}
|
|
3452
|
-
for each row execute function ${fn}();`);
|
|
3453
|
-
}
|
|
3454
|
-
return `${chunks.join("\n\n")}\n`;
|
|
3455
|
-
}
|
|
3456
|
-
//#endregion
|
|
3457
2886
|
//#region src/cli/rls/ownership-triggers.ts
|
|
3458
2887
|
function objectName(prefix, scope) {
|
|
3459
2888
|
return quoteIdent(`${prefix}_${scope}`);
|
|
3460
2889
|
}
|
|
3461
|
-
/**
|
|
2890
|
+
/**
|
|
2891
|
+
* Live holders of `roleExpr` in instance `idExpr`, with the same kind and
|
|
2892
|
+
* expiry rules as the helpers.
|
|
2893
|
+
*/
|
|
3462
2894
|
function holdersSql(ctx, table, column, idExpr, role, into) {
|
|
3463
2895
|
const roleExpr = typeof role === "string" ? quoteLiteral(role) : role.expr;
|
|
3464
2896
|
const held = memberRoleOf(table, "m", " ");
|
|
@@ -3535,10 +2967,10 @@ create constraint trigger ${objectName(OWNERSHIP.holders, scope)}
|
|
|
3535
2967
|
for each row execute function ${fn}();`;
|
|
3536
2968
|
}
|
|
3537
2969
|
/**
|
|
3538
|
-
* `transferOnly` as statement triggers over transition tables: a statement
|
|
3539
|
-
*
|
|
3540
|
-
*
|
|
3541
|
-
*
|
|
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.
|
|
3542
2974
|
*/
|
|
3543
2975
|
function transferTriggerSql(ctx, scope, table, column, roles) {
|
|
3544
2976
|
const fn = qualified(ctx, `${OWNERSHIP.transferOnly}_${scope}`);
|
|
@@ -3601,8 +3033,8 @@ ${trigger("delete")}`;
|
|
|
3601
3033
|
//#endregion
|
|
3602
3034
|
//#region src/cli/rls/ownership-sources.ts
|
|
3603
3035
|
/**
|
|
3604
|
-
* The membership sources holding scope `name`: `undefined` when there are
|
|
3605
|
-
*
|
|
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.
|
|
3606
3038
|
*/
|
|
3607
3039
|
function holderSources(ctx, name) {
|
|
3608
3040
|
const sources = memberForSources(ctx, name);
|
|
@@ -3620,7 +3052,10 @@ function holderSources(ctx, name) {
|
|
|
3620
3052
|
function indent(sql, by) {
|
|
3621
3053
|
return sql.replaceAll(/^/gmu, by);
|
|
3622
3054
|
}
|
|
3623
|
-
/**
|
|
3055
|
+
/**
|
|
3056
|
+
* Every source's rows of one instance, each compared with its own typed
|
|
3057
|
+
* `v_key_<n>`.
|
|
3058
|
+
*/
|
|
3624
3059
|
function sourceRowsOf(scope, sources) {
|
|
3625
3060
|
return sources.map((source, index) => indent(source.holders.rows(scope, { id: `v_key_${String(index)}` }), " ")).join("\n union all\n");
|
|
3626
3061
|
}
|
|
@@ -3636,7 +3071,10 @@ function regclassOf(table) {
|
|
|
3636
3071
|
function sourceTables(sources) {
|
|
3637
3072
|
return [...new Set(sources.map((source) => quoteQualified(source.table)))];
|
|
3638
3073
|
}
|
|
3639
|
-
/**
|
|
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
|
+
*/
|
|
3640
3078
|
function sourceHoldersTriggerSql(ctx, scope, sources, counted) {
|
|
3641
3079
|
const fn = qualified(ctx, `${OWNERSHIP.holders}_${scope}`);
|
|
3642
3080
|
const keys = sourceKeys(sources);
|
|
@@ -3709,7 +3147,10 @@ ${triggers.join("\n")}`;
|
|
|
3709
3147
|
function collectChangesSql(changed) {
|
|
3710
3148
|
return ` v_changes := v_changes || coalesce((select jsonb_agg(to_jsonb(c)) from (${changed}) c), '[]'::jsonb);`;
|
|
3711
3149
|
}
|
|
3712
|
-
/**
|
|
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
|
+
*/
|
|
3713
3154
|
function sourceTransferTriggerSql(ctx, scope, sources, roles) {
|
|
3714
3155
|
const fn = qualified(ctx, `${OWNERSHIP.transferOnly}_${scope}`);
|
|
3715
3156
|
const keys = sourceKeys(sources);
|
|
@@ -3787,314 +3228,410 @@ function sourceRowsSql(sources, user) {
|
|
|
3787
3228
|
return sources.map((source) => source.sql.select(user(source), keep).replaceAll(/^/gmu, " ")).join("\n union all\n");
|
|
3788
3229
|
}
|
|
3789
3230
|
//#endregion
|
|
3790
|
-
//#region src/cli/rls/
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
const
|
|
3800
|
-
const
|
|
3801
|
-
const
|
|
3802
|
-
const
|
|
3803
|
-
|
|
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;
|
|
3804
3250
|
};
|
|
3805
|
-
const
|
|
3806
|
-
|
|
3807
|
-
if (
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
const
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
} else {
|
|
3817
|
-
const kind = globalKindFilterSql(ctx, "r.role");
|
|
3818
|
-
parts.push(`${instance} and exists (
|
|
3819
|
-
select 1 from ${roleRows(ctx)}
|
|
3820
|
-
where (r.role, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}
|
|
3821
|
-
)`);
|
|
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
|
+
}
|
|
3822
3262
|
}
|
|
3823
|
-
}
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
const kind = kindFilterSql(ctx, "r.role", "ms.via");
|
|
3830
|
-
parts.push(`exists (
|
|
3831
|
-
select 1
|
|
3832
|
-
from (
|
|
3833
|
-
${sourceRowsSql(sources, userOf)}
|
|
3834
|
-
) ms
|
|
3835
|
-
cross join lateral jsonb_array_elements_text(
|
|
3836
|
-
case jsonb_typeof(ms.roles) when 'array' then ms.roles else '[]'::jsonb end
|
|
3837
|
-
) r(role)
|
|
3838
|
-
where ms.scope = ${quoteLiteral(name)}
|
|
3839
|
-
and ms.id = p_scope_id
|
|
3840
|
-
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 ")}
|
|
3841
|
-
)`);
|
|
3842
|
-
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);
|
|
3843
3269
|
}
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
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;
|
|
3857
3286
|
}
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
parts.push(`exists (
|
|
3869
|
-
select 1 from ${membershipRows(ctx)}
|
|
3870
|
-
where m ->> 'scope' = ${quoteLiteral(name)}
|
|
3871
|
-
and m ->> 'id' = p_scope_id
|
|
3872
|
-
and (r.role, p_role) in (${pairsSql(pairs)})
|
|
3873
|
-
and case jsonb_typeof(m -> 'expires_at')
|
|
3874
|
-
when 'number' then (m ->> 'expires_at')::numeric > extract(epoch from now())
|
|
3875
|
-
else true
|
|
3876
|
-
end${kind === void 0 ? "" : `\n and ${kind}`}${claimKeptLines(ctx, " ").map((line) => `\n${line}`).join("")}
|
|
3877
|
-
)`);
|
|
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);
|
|
3878
3297
|
}
|
|
3879
3298
|
}
|
|
3880
|
-
|
|
3881
|
-
|
|
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
|
|
3882
3333
|
stable
|
|
3883
3334
|
security definer
|
|
3884
3335
|
set search_path = ''
|
|
3885
3336
|
as $$
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
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
|
|
3890
3354
|
stable
|
|
3891
3355
|
security definer
|
|
3892
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 = ''
|
|
3893
3408
|
as $$
|
|
3894
3409
|
declare
|
|
3895
|
-
|
|
3410
|
+
v_cycle text;
|
|
3411
|
+
v_affected text[];
|
|
3896
3412
|
begin
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
}
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
}
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
}
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
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
|
|
3951
3517
|
language sql
|
|
3952
3518
|
stable
|
|
3953
3519
|
security definer
|
|
3954
3520
|
set search_path = ''
|
|
3955
3521
|
as $$
|
|
3956
|
-
|
|
3522
|
+
${body}
|
|
3957
3523
|
$$;
|
|
3958
|
-
${
|
|
3524
|
+
revoke execute on function ${fn}() from public, anon;
|
|
3525
|
+
grant execute on function ${fn}() to authenticated;`;
|
|
3959
3526
|
}
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
const
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
const binding = policy.rolesByName.get(name);
|
|
3968
|
-
if (binding !== void 0) return binding.on === void 0;
|
|
3969
|
-
const leaf = findRole(policy.vocabulary.roles, name);
|
|
3970
|
-
return leaf !== void 0 && leaf.on === void 0;
|
|
3971
|
-
}
|
|
3972
|
-
/** The rules `rls generate` enforces; `undefined` when no role declares one. */
|
|
3973
|
-
function ownershipRules(policy, scopes) {
|
|
3974
|
-
const kinds = {};
|
|
3975
|
-
const assigns = [];
|
|
3976
|
-
const counted = [];
|
|
3977
|
-
for (const binding of policy.roles) {
|
|
3978
|
-
const scope = roleScope(policy, scopes, binding.name);
|
|
3979
|
-
if (binding.for !== void 0) kinds[binding.name] = binding.for;
|
|
3980
|
-
const assigner = scope ?? (binding.on === void 0 ? "global" : void 0);
|
|
3981
|
-
for (const target of binding.assigns ?? []) {
|
|
3982
|
-
const at = roleScope(policy, scopes, target);
|
|
3983
|
-
if (assigner !== void 0 && at !== void 0 && (assigner === "global" || scopeChain(scopes, at).includes(assigner))) assigns.push({
|
|
3984
|
-
assigner: binding.name,
|
|
3985
|
-
scope: assigner,
|
|
3986
|
-
role: target,
|
|
3987
|
-
at
|
|
3988
|
-
});
|
|
3989
|
-
else if (assigner === "global" && isGlobalRole(policy, target)) assigns.push({
|
|
3990
|
-
assigner: binding.name,
|
|
3991
|
-
scope: "global",
|
|
3992
|
-
role: target,
|
|
3993
|
-
at: "global"
|
|
3994
|
-
});
|
|
3995
|
-
}
|
|
3996
|
-
const min = binding.min ?? 0;
|
|
3997
|
-
const transferOnly = binding.transferOnly === true;
|
|
3998
|
-
if (scope !== void 0 && (min > 0 || binding.max !== void 0 || transferOnly)) counted.push(binding.max === void 0 ? {
|
|
3999
|
-
role: binding.name,
|
|
4000
|
-
scope,
|
|
4001
|
-
min,
|
|
4002
|
-
transferOnly
|
|
4003
|
-
} : {
|
|
4004
|
-
role: binding.name,
|
|
4005
|
-
scope,
|
|
4006
|
-
min,
|
|
4007
|
-
max: binding.max,
|
|
4008
|
-
transferOnly
|
|
4009
|
-
});
|
|
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);
|
|
4010
3534
|
}
|
|
4011
|
-
|
|
4012
|
-
return {
|
|
4013
|
-
kinds,
|
|
4014
|
-
assigns,
|
|
4015
|
-
counted
|
|
4016
|
-
};
|
|
3535
|
+
return [...out].toSorted();
|
|
4017
3536
|
}
|
|
4018
3537
|
/**
|
|
4019
|
-
*
|
|
4020
|
-
*
|
|
4021
|
-
* can hold any kind, and so does a source with no `via` (it is not
|
|
4022
|
-
* compared) and a scope with no table or source.
|
|
3538
|
+
* Plan entries by name, each after the group resources its helper calls (SQL
|
|
3539
|
+
* function bodies are checked on create).
|
|
4023
3540
|
*/
|
|
4024
|
-
function
|
|
3541
|
+
function dependencyOrder(plan) {
|
|
4025
3542
|
const out = [];
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
via: typeof via === "string" ? void 0 : via.value ?? void 0
|
|
4036
|
-
});
|
|
4037
|
-
} else for (const source of ctx.memberSources ?? ctx.sources ?? []) {
|
|
4038
|
-
if (source.sql.scope !== void 0 && source.sql.scope !== scope) continue;
|
|
4039
|
-
const via = source.sql.manifest.via;
|
|
4040
|
-
held.push({
|
|
4041
|
-
label: source.sql.table,
|
|
4042
|
-
via: via === void 0 ? void 0 : "value" in via && typeof via.value === "string" ? via.value : void 0
|
|
4043
|
-
});
|
|
4044
|
-
}
|
|
4045
|
-
if (held.length === 0 || held.some((entry) => entry.via === void 0 || kinds.includes(entry.via ?? ""))) continue;
|
|
4046
|
-
out.push({
|
|
4047
|
-
role,
|
|
4048
|
-
for: kinds,
|
|
4049
|
-
sources: held.map((entry) => `${entry.label} (via ${entry.via ?? ""})`)
|
|
4050
|
-
});
|
|
4051
|
-
}
|
|
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);
|
|
3550
|
+
};
|
|
3551
|
+
for (const name of [...plan.keys()].toSorted()) visit(name);
|
|
4052
3552
|
return out;
|
|
4053
3553
|
}
|
|
4054
3554
|
/**
|
|
4055
|
-
* The
|
|
4056
|
-
*
|
|
4057
|
-
*
|
|
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.
|
|
4058
3558
|
*/
|
|
4059
|
-
function
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
const
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
if (
|
|
4068
|
-
|
|
4069
|
-
continue;
|
|
4070
|
-
}
|
|
4071
|
-
const transfer = counted.filter((rule) => rule.transferOnly).map((rule) => rule.role);
|
|
4072
|
-
const mapped = scopeTable(ctx, name);
|
|
4073
|
-
if (mapped === void 0) {
|
|
4074
|
-
const sources = holderSources(ctx, name);
|
|
4075
|
-
if (sources === void 0) chunks.push(`-- ${name}: no memberships table configured, so min, max and transferOnly are checked only by decideRoleChange`);
|
|
4076
|
-
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`);
|
|
4077
|
-
else {
|
|
4078
|
-
chunks.push(sourceHoldersTriggerSql(ctx, name, sources, counted));
|
|
4079
|
-
if (transfer.length > 0) chunks.push(sourceTransferTriggerSql(ctx, name, sources, transfer));
|
|
4080
|
-
}
|
|
4081
|
-
continue;
|
|
4082
|
-
}
|
|
4083
|
-
chunks.push(holdersTriggerSql(ctx, name, mapped.table, mapped.column, counted));
|
|
4084
|
-
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);
|
|
4085
3569
|
}
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
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);
|
|
4093
3579
|
}
|
|
4094
|
-
chunks.push(canAssignAnySql(ctx, custom));
|
|
4095
|
-
if (hasCanAssignAnyFor(ctx)) chunks.push(canAssignAnySql(ctx, custom, true));
|
|
4096
3580
|
}
|
|
4097
|
-
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;
|
|
4098
3635
|
}
|
|
4099
3636
|
//#endregion
|
|
4100
|
-
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 };
|