permdock 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/dist/{2020-C9VN-x8c.js → 2020-CJbO8Bvz.js} +1 -3
- package/dist/a2a/index.d.ts +7 -7
- package/dist/a2a/index.js +9 -9
- package/dist/action-result-CL-QCITA.d.ts +19 -0
- package/dist/{adapter-context-CRhqyzCk.js → adapter-context-CIQpQxbE.js} +8 -2
- package/dist/{adoption-scan-DCYe60Pi.js → adoption-scan-BLs67P1l.js} +5 -5
- package/dist/agent/index.d.ts +3 -3
- package/dist/agent/index.js +1 -1
- package/dist/ai-sdk/index.d.ts +31 -10
- package/dist/ai-sdk/index.js +22 -12
- package/dist/{app-data-MuyzVqX8.js → app-data-UNNm07jV.js} +10 -7
- package/dist/{approval-header-CJACtHkv.js → approval-header-8QV6MU4H.js} +6 -3
- package/dist/{approval-prompt-Dg5LeAcT.js → approval-prompt-B8hQY69x.js} +5 -6
- package/dist/{approval-prompt-DvSSPQJX.d.ts → approval-prompt-PYagyQUj.d.ts} +13 -11
- package/dist/approvals/index.d.ts +5 -5
- package/dist/approvals/index.js +7 -7
- package/dist/{approvals-p1PGoVjb.js → approvals-DnIBTuXE.js} +128 -78
- package/dist/{approvers-CqPOLD-o.js → approvers-B4r0N_Uz.js} +8 -2
- package/dist/{arazzo-41WuzHou.js → arazzo-3RhGfkAK.js} +2 -2
- package/dist/{arazzo-CaUVBn7D.js → arazzo-B5-ulWif.js} +6 -6
- package/dist/{args-IIolxxKA.js → args-0FrVs5EH.js} +125 -8
- package/dist/astro/index.d.ts +30 -12
- package/dist/astro/index.js +2 -2
- package/dist/{audience-DabClFLQ.js → audience-BAkBDMqn.js} +8 -8
- package/dist/{auth-DRLEizbL.js → auth--yPyZNgr.js} +21 -10
- package/dist/authzen/index.d.ts +171 -25
- package/dist/authzen/index.js +321 -149
- package/dist/{automation-BNM3TtDo.d.ts → automation-DqEoWHBc.d.ts} +42 -32
- package/dist/{automation-DVjOckjV.js → automation-x3KEUusw.js} +22 -21
- package/dist/base-url-BQbddZO5.js +9 -0
- package/dist/better-auth/index.d.ts +2 -2
- package/dist/better-auth/index.js +13 -7
- package/dist/better-supabase/index.d.ts +392 -220
- package/dist/better-supabase/index.js +293 -152
- package/dist/{bind-DOyrv6zi.d.ts → bind-Cp2hBHzP.d.ts} +27 -12
- package/dist/{bind-CAvgUuKs.js → bind-D_sZSVA3.js} +22 -10
- package/dist/{body-CH5of8Ne.js → body-CJQwYVa8.js} +3 -3
- package/dist/{body-text-QcXok_SI.js → body-text-DQhcmqdY.js} +8 -4
- package/dist/{boundary-BM8a_P6E.js → boundary-BV3O1eud.js} +8 -2
- package/dist/{boundary-BmHxVH1J.d.ts → boundary-C9ck-3uK.d.ts} +13 -4
- package/dist/{budget-NbEtbJC_.js → budget-BELja5A9.js} +6 -3
- package/dist/{budget-JqcRqyhK.d.ts → budget-hYsPz_yE.d.ts} +6 -3
- package/dist/{build-instance-Bu_eDqYw.js → build-instance-CNWaYJyr.js} +3 -3
- package/dist/{canonical-json-BC8_nMTw.js → canonical-json-DwilZ1iB.js} +6 -3
- package/dist/{capability-N1K9IpqS.js → capability-BqX94r07.js} +31 -25
- package/dist/{capability-Drg-1YhL.d.ts → capability-ChuiONQU.d.ts} +41 -29
- package/dist/{casbin-Dyu2gnUD.js → casbin-H0rKxBx2.js} +16 -8
- package/dist/{casbin-Cr7RLtA3.js → casbin-cbSbhT_k.js} +2 -2
- package/dist/{casl-64gpT-8x.js → casl-C4OINZHI.js} +2 -2
- package/dist/{casl-DjEDhTCP.js → casl-CHHoCldP.js} +19 -10
- package/dist/catalog/index.d.ts +104 -23
- package/dist/catalog/index.js +117 -8
- package/dist/{catalog-HD2gf5mY.js → catalog-BFpz2EmT.js} +57 -26
- package/dist/{catalog-DZVbrF-z.js → catalog-DCspHDc8.js} +3 -3
- package/dist/{catalog-doc-Cdn7-0pQ.js → catalog-doc-DjMKzJp9.js} +53 -16
- package/dist/{catalog-fingerprint-CCP0-yQS.js → catalog-fingerprint-e6aCWosn.js} +19 -15
- package/dist/catalog-mRQQQhNK.js +1 -0
- package/dist/catalog-operations-Dadrl7Z-.js +60 -0
- package/dist/{create-CQWIpUG-.js → challenge-DImNcR7B.js} +157 -39
- package/dist/chat/index.d.ts +1 -1
- package/dist/chat/index.js +13 -10
- package/dist/chat-sdk/index.d.ts +44 -26
- package/dist/chat-sdk/index.js +17 -17
- package/dist/checks-CGkP3Z7A.js +40 -0
- package/dist/{claims-map-7oQ3ue4X.js → claims-map-CKkoAAYF.js} +5 -2
- package/dist/{claims-map-Ca-QX8C6.d.ts → claims-map-D1Pb5oRq.d.ts} +8 -5
- package/dist/claude-agent/index.d.ts +6 -6
- package/dist/claude-agent/index.js +6 -3
- package/dist/clerk/index.d.ts +11 -7
- package/dist/clerk/index.js +6 -6
- package/dist/cli/bin.js +4 -4
- package/dist/cli/index.d.ts +82 -7
- package/dist/cli/index.js +6 -6
- package/dist/{clients-C7zLFuWT.d.ts → clients-C_NOET3z.d.ts} +5 -5
- package/dist/{clients-DjU5ABMy.js → clients-DYr3U86j.js} +3 -3
- package/dist/cloud/index.d.ts +186 -38
- package/dist/cloud/index.js +293 -31
- package/dist/{cloud-ytQ3uviA.js → cloud-BFlGMaak.js} +5 -6
- package/dist/{codemod-5mpSpnUY.js → codemod-DLQ9Zpo4.js} +3 -4
- package/dist/{codemod-ua2hHy7w.js → codemod-DWSN2erG.js} +523 -201
- package/dist/collect-B5BL3klN.js +1 -0
- package/dist/{collect-mLTY4nwb.js → collect-B7ZFAJU3.js} +36 -64
- package/dist/{collect-Ci6akVwH.js → collect-Bzv3le0A.js} +6 -6
- package/dist/{compare-pfyVnBjE.js → compare-DTfyUunF.js} +2 -2
- package/dist/compile/index.d.ts +2 -2
- package/dist/compile/index.js +1 -1
- package/dist/{compile-1bsAezL0.d.ts → compile-BFzysW3o.d.ts} +36 -12
- package/dist/{compile-CMqMx_IN.js → compile-EzEMB-7v.js} +77 -33
- package/dist/{completion-BTFrHZhX.js → completion-saoYiRFO.js} +17 -8
- package/dist/config-BwPBfN0t.js +108 -0
- package/dist/{config-Bj2Ixq3d.js → config-DBeXXmBF.js} +2 -2
- package/dist/{config-report-DSpsLpCE.js → config-report-BipSWA6V.js} +9 -6
- package/dist/config-shape-ykV0Vm2n.js +134 -0
- package/dist/{connection-DjhIJSs2.js → connection-B72dg5Q2.js} +23 -22
- package/dist/connector-schema-CYLQNMYq.js +434 -0
- package/dist/{context-B_u3_bDJ.js → context-BnIzxTNE.js} +4 -3
- package/dist/convex/index.d.ts +13 -7
- package/dist/convex/index.js +9 -6
- package/dist/core-H-mbhpZc.js +341 -0
- package/dist/core-hWIARPDz.d.ts +206 -0
- package/dist/{create-CBMmFie-.d.ts → create-BSNrVuxh.d.ts} +87 -34
- package/dist/{create-B30OMUOJ.d.ts → create-BVq1lNQE.d.ts} +49 -28
- package/dist/{create-D_NmPOdH.d.ts → create-CM1FciYG.d.ts} +37 -29
- package/dist/{create-CGm28gyI.js → create-Cq4Uox0f.js} +13 -9
- package/dist/{create-DCF2SvoT.js → create-_noSrY1T.js} +15 -21
- package/dist/{credential-BcoiejpY.js → credential-CyyyvCsL.js} +42 -39
- package/dist/{credentials-DPyHmYz5.js → credentials-CdDxp7jN.js} +6 -6
- package/dist/{deciding-columns-Sq12mEA6.js → deciding-columns-NB4wcyBf.js} +13 -2
- package/dist/{delegation-j6-rc37o.js → delegation-CDB6_keF.js} +31 -22
- package/dist/delivery/index.d.ts +5 -5
- package/dist/delivery/index.js +6 -5
- package/dist/{delivery-DCAgNyCn.js → delivery-BupfvJIV.js} +81 -20
- package/dist/{delivery-event-xX4obB8F.js → delivery-event-JioK5YbS.js} +6 -6
- package/dist/denied-text-CHNlVInN.d.ts +22 -0
- package/dist/{describe-BybMRY7A.js → describe-Bb4hGK5d.js} +6 -5
- package/dist/{describe-B5qgDvEW.d.ts → describe-ByS9LgHK.d.ts} +5 -5
- package/dist/{device-BARQ8eYG.js → device-BwbJNmb2.js} +11 -8
- package/dist/{diff-CmuSD3vc.js → diff-CN8IYhC_.js} +2 -2
- package/dist/{diff-3jbx6Rm4.js → diff-iZWV65nC.js} +23 -14
- package/dist/{digest-B3kc_PgQ.d.ts → digest-8Yx2CchM.d.ts} +3 -3
- package/dist/{doctor-BdZw53-k.js → doctor-CHD1M9R5.js} +485 -141
- package/dist/{doctor-3ZqNHX_G.js → doctor-D3wo-yuz.js} +4 -4
- package/dist/drizzle/index.d.ts +33 -19
- package/dist/drizzle/index.js +17 -9
- package/dist/elysia/index.d.ts +17 -11
- package/dist/elysia/index.js +5 -5
- package/dist/{errors-BKy0EfhN.js → errors-B6R_lg2a.js} +13 -4
- package/dist/{errors-BWXBMaKK.d.ts → errors-CHwoRRpB.d.ts} +4 -1
- package/dist/{errors-CWTY6uih.js → errors-Vpu6rKHl.js} +14 -10
- package/dist/{errors-DH5QEGXh.d.ts → errors-zi_4T-rx.d.ts} +22 -6
- package/dist/{evaluate-CNClBvcj.js → evaluate-Dd8Ta0Ie.js} +185 -80
- package/dist/{evaluations-beAooV0n.js → evaluations-IBceAeKT.js} +41 -20
- package/dist/{evaluations-C_1vdSZc.d.ts → evaluations-z8dQFDaN.d.ts} +12 -6
- package/dist/eve/index.d.ts +9 -6
- package/dist/eve/index.js +4 -4
- package/dist/{event-time-CsegxXxB.js → event-time-BGy48400.js} +8 -4
- package/dist/{expiry-D4_YKRKD.js → expiry-6XByK0dm.js} +8 -5
- package/dist/express/index.d.ts +9 -6
- package/dist/express/index.js +9 -6
- package/dist/fastify/index.d.ts +16 -10
- package/dist/fastify/index.js +8 -5
- package/dist/{fetch-adapter-Dw0MTS3D.d.ts → fetch-adapter-C1DCvpqV.d.ts} +9 -6
- package/dist/{fetch-adapter-DD_gfmrC.js → fetch-adapter-JL43jD3E.js} +14 -11
- package/dist/fields-D0vUI6dq.js +498 -0
- package/dist/{files-C5ZI27eV.js → files-B3AA05f1.js} +21 -5
- package/dist/{files-D_JIzUrP.js → files-BSjrtIHw.js} +11 -4
- package/dist/{fixtures-B7XrLeY7.js → fixtures-A1_sp_tR.js} +15 -6
- package/dist/{flags-C3DktBDY.js → flags-DXjoTVbS.js} +2 -2
- package/dist/{freeze-CrU8nQeX.js → freeze-jjan0lCv.js} +4 -1
- package/dist/{from-snapshot-CTCMsFZk.js → from-snapshot-DCakWRsr.js} +26 -16
- package/dist/gen-B5kvbAoZ.js +58 -0
- package/dist/gen-DWCYcMQM.js +827 -0
- package/dist/generate-CRVV5V7l.js +165 -0
- package/dist/generate-CezbCaTI.js +275 -0
- package/dist/{grantee-hxrUsbpM.js → grantee-Dke-9rd1.js} +4 -4
- package/dist/{grantee-conditions-CcXf8hMM.js → grantee-conditions-BrYPe-w0.js} +12 -12
- package/dist/{grantee-match-DbENBHLB.js → grantee-match-7RZZvoT_.js} +12 -6
- package/dist/{graph-sql-B-dsAT6j.d.ts → graph-sql-BwFC4Qjs.d.ts} +3 -3
- package/dist/h3/index.d.ts +10 -7
- package/dist/h3/index.js +3 -3
- package/dist/headers-B9jQxjtW.d.ts +10 -0
- package/dist/{helpers-CBYVMjwh.js → helpers-DJicaG6f.js} +40 -28
- package/dist/hono/index.d.ts +16 -10
- package/dist/hono/index.js +5 -5
- package/dist/{hosted-BPk51-kX.d.ts → hosted-Cmtxogd0.d.ts} +44 -20
- package/dist/{hosted-B46-5LYE.js → hosted-D0IzESu_.js} +68 -25
- package/dist/{http-ujKm0JXE.js → http-DH94eh6f.js} +4 -4
- package/dist/{http-BBoKefeS.js → http-DKLXrFGk.js} +13 -7
- package/dist/{http-DXJ37RDA.js → http-DOL33cO0.js} +11 -8
- package/dist/{import-wyqXUyjm.js → import-CST57XLp.js} +64 -51
- package/dist/{import-CNV7I2Dw.js → import-_YqhhwvX.js} +6 -6
- package/dist/{index-B8YjkEZ4.d.ts → index-B8s204GL.d.ts} +64 -52
- package/dist/{index-Ch8LpNDp.d.ts → index-BT5-1VsX.d.ts} +64 -25
- package/dist/{index-DuWxc5dt.d.ts → index-BW_0MZOS.d.ts} +388 -149
- package/dist/index-BhxEUY9J.d.ts +233 -0
- package/dist/index-Bv5iXPqU.d.ts +210 -0
- package/dist/{index-C-6Letxg.d.ts → index-_9UiQIkt.d.ts} +164 -77
- package/dist/index.d.ts +22 -21
- package/dist/index.js +35 -33
- package/dist/{init-GG6Jh8HD.js → init-Dz_PsHdv.js} +5 -5
- package/dist/{init-DeMYsnYJ.js → init-cE3A8aXq.js} +10 -10
- package/dist/{instance-options-BsVpyGIk.d.ts → instance-options-B0KXqTrJ.d.ts} +4 -4
- package/dist/{instance-snapshot-BOEhcqIF.js → instance-snapshot--JWiTrvO.js} +81 -56
- package/dist/{instant-BluqDvyS.js → instant-DK3Iz83e.js} +2 -2
- package/dist/{internals-Dk8A8fX3.js → internals-B3IgHrvo.js} +6 -3
- package/dist/{json-schema-B-vAwlde.js → json-schema-BPGa-RjQ.js} +9 -3
- package/dist/{json-schema-CN_iLFH2.d.ts → json-schema-BfXgnRNT.d.ts} +4 -4
- package/dist/jwt/index.d.ts +125 -17
- package/dist/jwt/index.js +157 -16
- package/dist/{kernel-D0NVb1cK.js → kernel-C-fYydV2.js} +151 -16
- package/dist/kysely/index.d.ts +49 -33
- package/dist/kysely/index.js +9 -7
- package/dist/{live-options-BaAFCIiu.js → live-options-BBIT68le.js} +2 -2
- package/dist/{load-DFS_ry0W.js → load-DjSU5KXO.js} +6 -6
- package/dist/load-Dm0pSYH3.js +1 -0
- package/dist/{login-Dfe5RlYk.js → login-D-8tMqzo.js} +4 -4
- package/dist/{login-imhuwvn3.js → login-DplQ0s84.js} +18 -15
- package/dist/{manifest-dvL2XEe1.js → manifest-Cgiq5oqc.js} +67 -37
- package/dist/{manifest-mx5ghSFM.d.ts → manifest-CzzWtnAb.d.ts} +144 -28
- package/dist/{manifest-schema-C0kK_8V0.js → manifest-schema-2r37XRhD.js} +80 -9
- package/dist/{manifest-C8IzW3Do.js → manifest-wg8WkkpD.js} +11 -7
- package/dist/{map-claims-D3oRmTk0.js → map-claims-BNWCJtmP.js} +27 -18
- package/dist/{map-error-Dr70oA_3.js → map-error-pasIGqMT.js} +7 -7
- package/dist/{markers-C6joDI3o.js → markers-BeHZ4x9Q.js} +20 -5
- package/dist/mcp/index.d.ts +154 -47
- package/dist/mcp/index.js +200 -41
- package/dist/{migrate-CIq4-E9F.js → migrate-B7oP_sQU.js} +102 -31
- package/dist/module-values-BJTBdo6y.js +107 -0
- package/dist/nest/index.d.ts +23 -13
- package/dist/nest/index.js +18 -11
- package/dist/next/client.d.ts +5 -2
- package/dist/next/client.js +1 -1
- package/dist/next/index.d.ts +72 -26
- package/dist/next/index.js +110 -6
- package/dist/next/plugin.d.ts +1 -1
- package/dist/next/plugin.js +4 -3
- package/dist/node/index.d.ts +5 -5
- package/dist/node/index.js +3 -3
- package/dist/{ocsf-BFp-PZZA.js → ocsf-TcjRBgIs.js} +15 -5
- package/dist/openai/index.d.ts +13 -7
- package/dist/openai/index.js +3 -3
- package/dist/openapi/index.d.ts +61 -30
- package/dist/openapi/index.js +2 -2
- package/dist/{openapi-B7HZ0f-r.js → openapi-BOsx0xn9.js} +34 -24
- package/dist/{openapi-C4dDTVEd.js → openapi-CBZUhA3l.js} +4 -5
- package/dist/{openapi-DD1SV-5-.js → openapi-VeHHU2l9.js} +23 -14
- package/dist/{operation-findings-DgATDpvh.js → operation-findings-CXtFzlKJ.js} +3 -3
- package/dist/{operations-X0VM7F3m.d.ts → operations-Ci_rT6Uo.d.ts} +10 -7
- package/dist/orpc/index.d.ts +68 -14
- package/dist/orpc/index.js +62 -13
- package/dist/otel/index.d.ts +7 -4
- package/dist/otel/index.js +8 -5
- package/dist/output-files-BHOIlrfD.js +1 -0
- package/dist/{output-files-DhRBBE4n.js → output-files-BIQ_9pIs.js} +29 -17
- package/dist/{output-1im__-08.js → output-tkju6U7R.js} +15 -7
- package/dist/ownership-B4L59T9u.js +1054 -0
- package/dist/parse-1_ZheTW5.js +151 -0
- package/dist/parse-local-manifest-BQhTlLQS.d.ts +10 -0
- package/dist/parse-local-manifest-DEiCF4ah.js +69 -0
- package/dist/{parse-request-D_tUXG6M.js → parse-request-CwCZpyjV.js} +7 -8
- package/dist/{paths-DXSHAMIa.js → paths-CUvURzHp.js} +4 -1
- package/dist/pdp/index.d.ts +9 -6
- package/dist/pdp/index.js +23 -12
- package/dist/{peer-CRvC7Hg1.js → peer-C9LvBCcb.js} +1 -1
- package/dist/peer-Q0fuZ6hx.js +982 -0
- package/dist/{permdock-SV8ZINYT.d.ts → permdock-B6Sf4T_S.d.ts} +270 -117
- package/dist/{permdock-BrAscV28.js → permdock-L-_I_3zP.js} +226 -74
- package/dist/{permission-registry-Br4jSNVi.js → permission-registry-B8tYxPIt.js} +24 -11
- package/dist/{vocabulary-CN8H_9UF.d.ts → permission-types-CAY-GU7e.d.ts} +130 -104
- package/dist/{permissions-u_KHg-sN.d.ts → permissions-Bx6UI8n1.d.ts} +6 -2
- package/dist/{permissions-B-wCP5ZC.js → permissions-CVXmRILq.js} +3 -3
- package/dist/{permissions-BPxlwVhN.js → permissions-mDcTWoBs.js} +8 -5
- package/dist/pg-DQTqRrou.js +60 -0
- package/dist/{pins-8ILTftC9.js → pins-nqarl3oE.js} +4 -1
- package/dist/pkce-CnmmYqUZ.js +21 -0
- package/dist/{policy-77FSAq_V.d.ts → policy-Ba4bDhaj.d.ts} +5 -5
- package/dist/{policy-DbM5YYtV.js → policy-CLQ8__GF.js} +49 -20
- package/dist/{policy-from-catalog-C2gMa2Dj.js → policy-from-catalog-CJgOm6aq.js} +28 -25
- package/dist/{policy-module-CTlwMvLI.js → policy-module-xym9fnSq.js} +12 -6
- package/dist/{policy-types-n0uRTsW8.d.ts → policy-types-CCiDRZBH.d.ts} +802 -334
- package/dist/postgres-store-DcyrPS-r.js +1028 -0
- package/dist/{postgrest-DAjUkF9w.js → postgrest-D4nuPXIQ.js} +88 -36
- package/dist/{postgrest-CEgRbefE.d.ts → postgrest-DicQyqm-.d.ts} +60 -30
- package/dist/powersync-DHNQX06W.js +1117 -0
- package/dist/{powersync-DISt6SUe.js → powersync-DXqGuq9A.js} +3 -3
- package/dist/powersync-Djf-zCpB.js +1 -0
- package/dist/preset/index.d.ts +38 -28
- package/dist/preset/index.js +1 -1
- package/dist/{preset-DZdU0tne.js → preset-BjLzeFpp.js} +58 -34
- package/dist/{principal-source-pHneukrx.js → principal-source-C6N60GtM.js} +10 -10
- package/dist/prisma/index.d.ts +69 -41
- package/dist/prisma/index.js +21 -17
- package/dist/{problem-CTviyH5L.js → problem-BVLHFK33.js} +41 -21
- package/dist/{problem-details-cn5X30gZ.js → problem-details-CLDgX1KV.js} +13 -4
- package/dist/{problem-types-CJ2325SZ.js → problem-types-DSsRfONQ.js} +4 -1
- package/dist/prompt-BYc6drYW.js +938 -0
- package/dist/protected-action-BE7N_Luo.d.ts +58 -0
- package/dist/protected-action-DUYqb-Ma.js +80 -0
- package/dist/{protected-BlF0F6wL.js → protected-action-QEA8arRB.js} +48 -14
- package/dist/{provider-B1al-AZz.js → provider-C55OGXJ7.js} +5 -4
- package/dist/{provider-DILL9oPK.d.ts → provider-DXfq5c2d.d.ts} +1 -1
- package/dist/{provider-store-DM6DNcta.js → provider-store-CaTv00tr.js} +13 -9
- package/dist/pull-B65ndFNZ.js +1 -0
- package/dist/{pull-jEwbf80J.js → pull-CwpGaQSi.js} +12 -11
- package/dist/{push-BD4f15co.js → push-DgssgzkK.js} +62 -16
- package/dist/{pushed-policy-DCW91KQU.d.ts → pushed-policy-D1Eoi6NM.d.ts} +12 -6
- package/dist/{pushed-policy-CqtxkV4G.js → pushed-policy-ITwYKvvH.js} +10 -7
- package/dist/react/index.d.ts +9 -8
- package/dist/react/index.js +7 -6
- package/dist/react/provider-client.d.ts +1 -1
- package/dist/react/provider-client.js +1 -1
- package/dist/react-native/index.d.ts +104 -51
- package/dist/react-native/index.js +394 -67
- package/dist/{refs-K6eWUJ0H.js → refs-C8BKxc_e.js} +17 -3
- package/dist/{relation-walk-3OyDBF0S.js → relation-walk-DK_gkgL7.js} +12 -9
- package/dist/{relations-BnCWnJGI.js → relations-BxvRvDHK.js} +3 -3
- package/dist/{replay-Du1tJjG7.js → replay-BU09ad4L.js} +1 -1
- package/dist/{replay-Bkm-Ne4l.d.ts → replay-DNftIb_R.d.ts} +1 -1
- package/dist/{request-context-C81Pip9n.js → request-context-C25ilqiJ.js} +8 -8
- package/dist/{request-schema-CMreVuFM.js → request-schema-DhX7UbfQ.js} +4 -1
- package/dist/resend/index.d.ts +34 -28
- package/dist/resend/index.js +29 -29
- package/dist/resolve-names-CF7xXLSG.js +201 -0
- package/dist/{resource-relations-D_jw0BFx.js → resource-relations-8-pCScBO.js} +18 -9
- package/dist/{retire-CALjIQ7e.js → retire-B2xffQgY.js} +15 -6
- package/dist/{revocations-R07lFBnX.d.ts → revocations-BaUXHM1N.d.ts} +6 -3
- package/dist/{rls-DTvYd-lI.js → rls-CNEH2_BG.js} +743 -624
- package/dist/{rls-BAqP4q66.js → rls-Ca8Jz9b5.js} +41 -10
- package/dist/{role-builder-uWQZ4v84.js → role-builder-BK2-T6oD.js} +41 -20
- package/dist/rolldown-runtime-C3I60J0A.js +4 -0
- package/dist/{row-conditions-h2HythXQ.js → row-conditions-BAGHOmVq.js} +4 -3
- package/dist/{rpc-VSl5ylaQ.js → rpc-fNqAIqQk.js} +8 -5
- package/dist/{run-CUKuz0A8.js → run-Bz26OTfO.js} +26 -23
- package/dist/run-D-JE8QWb.js +1 -0
- package/dist/{run-ticket-DocwaTKn.d.ts → run-ticket-DbWI9f99.d.ts} +33 -13
- package/dist/{run-ticket-sxFI6LAk.js → run-ticket-Dv7aF7Dk.js} +29 -8
- package/dist/{runtime-D3VTAiCA.js → runtime-BGnrOkVH.js} +5 -2
- package/dist/{saas-BCXRuXca.js → saas-iWnUdwCt.js} +42 -29
- package/dist/{scan-qvOe0hS3.js → scan-DCj1KQfn.js} +1 -1
- package/dist/{schema-B4v0Qrac.js → schema-BOJjqlWL.js} +3 -2
- package/dist/{schema-yLuBWWsh.js → schema-Dst9I9Gs.js} +9 -5
- package/dist/scim/index.d.ts +3 -2
- package/dist/scim/index.js +3 -1
- package/dist/scim-D8YupzX1.js +44 -0
- package/dist/{scim-ClHtpyV3.js → scim-jJBJ7g1Z.js} +35 -551
- package/dist/{scopes-CUPSjo9q.js → scopes-0Up_UA2t.js} +42 -24
- package/dist/server/index.d.ts +12 -12
- package/dist/server/index.js +13 -13
- package/dist/{server-C3WXAHZj.js → server-3cVlTLde.js} +31 -31
- package/dist/{server-types-CKQiCnON.d.ts → server-types-CIlpSA2G.d.ts} +81 -46
- package/dist/{sha256-D9ihqmvd.js → sha256-C-4WXKIu.js} +4 -3
- package/dist/{shared-BAK8t4Uo.js → shared-Afctg7B0.js} +100 -61
- package/dist/{shared-DvvFzYXe.js → shared-l0oGGetg.js} +9 -6
- package/dist/{signals-D3NcTKUS.js → signals-B1vHR3fN.js} +2 -2
- package/dist/{sink-BEZSlMS_.js → sink-D9_vh372.js} +32 -21
- package/dist/{sink-4-eQIRWf.d.ts → sink-Dvi7YZG_.d.ts} +44 -20
- package/dist/{skills-B6lVdD_W.js → skills-Bjj7BNEQ.js} +16 -12
- package/dist/{skills-CTYwkl44.js → skills-OuoMR49L.js} +3 -3
- package/dist/{snapshot-B7DkYg6g.js → snapshot-CXlzi69u.js} +21 -18
- package/dist/{snapshot-cache-PXI-vGpY.d.ts → snapshot-cache-BAXZ0KOp.d.ts} +21 -12
- package/dist/{snapshot-cache-DZHQ0KF6.js → snapshot-cache-Bx_4xC_6.js} +14 -11
- package/dist/solid/index.d.ts +55 -15
- package/dist/solid/index.js +46 -12
- package/dist/solid-start/index.d.ts +30 -12
- package/dist/solid-start/index.js +3 -3
- package/dist/sources-B1Os_YUu.d.ts +312 -0
- package/dist/{sources-vs5dGOsS.js → sources-CMAM-2A3.js} +156 -51
- package/dist/sources-qoIJQrJv.js +11 -0
- package/dist/{sql-B5T1IL83.js → sql-0WjxIp9g.js} +32 -7
- package/dist/sql-B80DFdpG.d.ts +10 -0
- package/dist/{src-Cu2m75uo.js → src-DZ0l9UAg.js} +305 -123
- package/dist/ssf/index.d.ts +4 -4
- package/dist/ssf/index.js +665 -224
- package/dist/{standard-webhooks-rbD7LS6h.d.ts → standard-webhooks-C-qX4mJW.d.ts} +7 -7
- package/dist/{standard-webhooks-DgFFD-Ga.js → standard-webhooks-D67a3nzk.js} +5 -5
- package/dist/{store-BDutJkDL.js → store-RNFvaJJv.js} +43 -29
- package/dist/{store-options-u0ycEe8s.js → store-options-Dil3lraY.js} +105 -15
- package/dist/stores-C0Kta5Ft.d.ts +59 -0
- package/dist/{stores-CGhM8BN_.js → stores-DxZWDXCs.js} +42 -5
- package/dist/{stream-CvdUS9V1.js → stream-CDvF0sJm.js} +5 -5
- package/dist/{stream-CeCMJmBB.d.ts → stream-ltuLdtTG.d.ts} +4 -3
- package/dist/{style-DiKWnAvn.js → style-DblOtSga.js} +1 -1
- package/dist/{style-Uao76wO7.js → style-Ds11Hmk4.js} +3 -1
- package/dist/{subject-settings-CkEwrw3W.js → subject-settings-8sH5HlII.js} +8 -4
- package/dist/{row-check-Dt35czLy.d.ts → subject-settings-vNW8qmUH.d.ts} +30 -18
- package/dist/supabase/index.d.ts +48 -37
- package/dist/supabase/index.js +6 -6
- package/dist/supabase/middleware.d.ts +19 -13
- package/dist/supabase/middleware.js +7 -7
- package/dist/{supabase-x5JtR2tc.js → supabase-BL1qJzzY.js} +82 -36
- package/dist/{supabase-By_frk3U.js → supabase-D39cuGPt.js} +3 -3
- package/dist/{supabase-hook-BMoEhZir.js → supabase-hook-B-yXnzUl.js} +1206 -286
- package/dist/supabase-hook-H-MtE9Wd.js +1 -0
- package/dist/supautils-BU-pAEpf.js +808 -0
- package/dist/{support-session-C6xJQYut.js → support-session-BsmPCbz8.js} +22 -22
- package/dist/svelte/PermissionBoundary.svelte +2 -6
- package/dist/svelte/Protected.svelte +2 -8
- package/dist/svelte/index.d.ts +9 -8
- package/dist/svelte/index.js +10 -9
- package/dist/svelte/runtime.d.ts +4 -4
- package/dist/svelte/runtime.js +4 -4
- package/dist/svelte/source.d.ts +9 -8
- package/dist/svelte/source.js +6 -5
- package/dist/sveltekit/index.d.ts +30 -12
- package/dist/sveltekit/index.js +2 -2
- package/dist/{ownership-DqaJNJPn.js → tables-DeMNEy6V.js} +1025 -1085
- package/dist/tanstack-start/index.d.ts +22 -10
- package/dist/tanstack-start/index.js +2 -2
- package/dist/{target-Bw37TFNA.js → target-qdEAnnhU.js} +6 -6
- package/dist/{tenancy-DN2QAx9z.js → tenancy-5d1zVBvK.js} +36 -21
- package/dist/terminal/index.d.ts +23 -14
- package/dist/terminal/index.js +17 -20
- package/dist/testing/core.d.ts +2 -0
- package/dist/testing/core.js +1 -0
- package/dist/testing/fixtures/index.d.ts +15 -0
- package/dist/testing/fixtures/index.js +831 -0
- package/dist/testing/index.d.ts +235 -285
- package/dist/testing/index.js +502 -927
- package/dist/testing/saas/index.d.ts +43 -27
- package/dist/testing/saas/index.js +2 -2
- package/dist/testing/saas/permissions.d.ts +1 -1
- package/dist/testing/saas/permissions.js +1 -1
- package/dist/text-diff-BtfSmdUo.js +444 -0
- package/dist/{thenable-yD7SabqG.js → thenable-BNSM1U2R.js} +12 -3
- package/dist/{time-DkWjdL98.js → time-nR1JesoH.js} +32 -8
- package/dist/{token-BLZcjQgt.js → token-DC9EQiet.js} +15 -13
- package/dist/{tool-refusal-CMLN5xhn.js → tool-refusal-Qpjs_W6M.js} +5 -2
- package/dist/trpc/index.d.ts +11 -8
- package/dist/trpc/index.js +15 -9
- package/dist/type-helpers-BIPIDa6a.d.ts +50 -0
- package/dist/{types-8RnwUAvE.d.ts → types-AMJ5qoU_.d.ts} +49 -30
- package/dist/{types-C_IiEs64.d.ts → types-B2mlGuhW.d.ts} +14 -11
- package/dist/{types-_WhUP7ud.d.ts → types-B4uqKDnp.d.ts} +25 -12
- package/dist/{types-CX7AvRqP.d.ts → types-BJk0BKRX.d.ts} +24 -14
- package/dist/types-B_d_g7T3.d.ts +262 -0
- package/dist/{types-BdtSDbBO.d.ts → types-B_tiYvk8.d.ts} +59 -32
- package/dist/{types-DqzsnlSA.js → types-CcUXciSj.js} +17 -5
- package/dist/{types-Dfv0aTER.d.ts → types-DC1Loeh3.d.ts} +449 -168
- package/dist/{types-lUlG3TVd.d.ts → types-DHKPcqzr.d.ts} +2 -2
- package/dist/{types-C791HFu9.d.ts → types-DjuMBNF9.d.ts} +68 -32
- package/dist/{types-C863OQiR.d.ts → types-HzqOtvsJ.d.ts} +23 -12
- package/dist/{types-Druu9wLM.d.ts → types-ZzRGoViD.d.ts} +77 -24
- package/dist/{types-BzzaHxt3.d.ts → types-ah9NlU6j.d.ts} +137 -48
- package/dist/{types-CVZSNocA.d.ts → types-djVzLlIV.d.ts} +31 -14
- package/dist/{types-BtHtcKMr.d.ts → types-yUuPR5Hy.d.ts} +47 -24
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +2 -2
- package/dist/usage-CYeXek0p.js +1 -0
- package/dist/{usage-SveTRTyr.js → usage-CxEzS4ey.js} +50 -26
- package/dist/{usage-CRVl9V3t.js → usage-ab0PdusX.js} +2 -2
- package/dist/{validation-error-ZfUHbKvf.js → validation-error-CYg2kJEw.js} +4 -1
- package/dist/{validation-error-DHUrAkku.d.ts → validation-error-YGg2mXyH.d.ts} +5 -2
- package/dist/{validity-CZ7-y0Cj.js → validity-Buc7w8E9.js} +13 -10
- package/dist/{verifier-DHuh-okk.js → verifier-DUV6pLqZ.js} +10 -4
- package/dist/{verify-BaAKx7t6.js → verify-DZeqoWu5.js} +417 -489
- package/dist/version-DMQ0dnN5.js +48 -0
- package/dist/version-DizhmD72.js +1 -0
- package/dist/{views-DTIIO-oQ.js → views-CD8wuL4Z.js} +19 -6
- package/dist/vocabulary-DEJ5j4mR.d.ts +70 -0
- package/dist/vue/index.d.ts +71 -21
- package/dist/vue/index.js +50 -13
- package/dist/{waiter-B986w1oK.d.ts → waiter-Bu9zAlE0.d.ts} +13 -10
- package/dist/{waiter-BGUtGYoU.js → waiter-aBLM63_f.js} +1 -1
- package/dist/{watch-vea54PRw.js → watch-Cx_WciLE.js} +8 -8
- package/dist/watch-DB42U1ib.js +1 -0
- package/dist/webmcp/index.d.ts +11 -11
- package/dist/webmcp/index.js +8 -5
- package/dist/{where-scope-C9vfEphl.js → where-scope-DlCdCM_8.js} +56 -30
- package/dist/{who-can-DWBAcaCQ.js → who-can-rI7dCR9Q.js} +23 -20
- package/dist/workflow/index.d.ts +3 -3
- package/dist/workflow/index.js +13 -13
- package/dist/workflow-sdk/index.d.ts +19 -13
- package/dist/workflow-sdk/index.js +6 -6
- package/dist/{wrap-gX61EtpB.js → wrap-B2wziJI_.js} +11 -5
- package/dist/{wrap-C5jC6sKI.d.ts → wrap-DlJE3gS_.d.ts} +8 -5
- package/package.json +25 -14
- package/schemas/catalog-v1.json +3 -0
- package/schemas/cli-result-v1.json +1 -0
- package/schemas/cloud-api-v1.json +152 -7
- package/schemas/connector-v1.json +8 -0
- package/schemas/supabase-claims-v1.json +5 -0
- package/schemas/supabase-manifest-v1.json +308 -75
- package/skills/permdock/SKILL.md +1 -0
- package/skills/permdock-agents/references/runtimes.md +3 -1
- package/skills/permdock-approvals/references/stores-and-resume.md +2 -1
- package/skills/permdock-credentials/references/tokens-keys-links.md +8 -6
- package/skills/permdock-data/references/queries-and-rls.md +6 -2
- package/skills/permdock-tenancy/SKILL.md +1 -1
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +2 -1
- package/skills/permdock-wire/references/adapters.md +28 -22
- package/skills/permdock-wire/references/policy-options.md +1 -0
- package/dist/catalog-BoWglDPI.js +0 -1
- package/dist/collect-D7eQDffE.js +0 -1
- package/dist/config-bs_kZeYq.js +0 -162
- package/dist/denied-text-CiFQNuMV.d.ts +0 -19
- package/dist/fields-OYG_JR78.js +0 -203
- package/dist/generate-CNU1pIOo.js +0 -171
- package/dist/headers-BIjfMaoC.js +0 -8
- package/dist/headers-CIbDTUTK.d.ts +0 -7
- package/dist/index-Cp_ilKi1.d.ts +0 -91
- package/dist/index-CueFnxQc.d.ts +0 -110
- package/dist/load-BxEtwlBS.js +0 -1
- package/dist/output-files-B3IZJksC.js +0 -1
- package/dist/package-root-Dk_ETdkp.js +0 -31
- package/dist/parse-Cxq0WxrA.js +0 -53
- package/dist/parse-local-manifest-5ODwqu8b.d.ts +0 -10
- package/dist/parse-local-manifest-CVSs6J9I.js +0 -34
- package/dist/peer-C8swJ2GH.js +0 -50
- package/dist/policies-DP2nqRoF.js +0 -295
- package/dist/powersync-BkWWCS1K.js +0 -538
- package/dist/powersync-MMJ2mI2q.js +0 -1
- package/dist/problem-details-B45V5d2b.d.ts +0 -7
- package/dist/prompt-DfZLJLIn.js +0 -32
- package/dist/protected-RFGpakN9.d.ts +0 -38
- package/dist/pull-CvpINcMK.js +0 -1
- package/dist/run-DDwUaJDU.js +0 -1
- package/dist/sources-Xc4zkszI.d.ts +0 -190
- package/dist/stores-BUGNbJTt.d.ts +0 -30
- package/dist/supabase-hook-CCw5m35C.js +0 -1
- package/dist/supautils-B6HYL9eL.js +0 -91
- package/dist/text-diff-BQcUapOb.js +0 -15
- package/dist/types-B3_ooZgZ.d.ts +0 -172
- package/dist/usage-CZpSQP6L.js +0 -1
- package/dist/version-CEbQ5UeM.js +0 -18
- package/dist/version-DXkYUISx.js +0 -1
- package/dist/watch--ovlPOc0.js +0 -1
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
import { t as compact } from "./compact-CAf557ZB.js";
|
|
2
|
-
import { c as listPermissions, u as renamedKeys } from "./permission-registry-
|
|
3
|
-
import { i as dateAt } from "./time-
|
|
4
|
-
import {
|
|
5
|
-
import { o as
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import "./
|
|
11
|
-
import {
|
|
12
|
-
import { l as roleColumn } from "./sources-vs5dGOsS.js";
|
|
13
|
-
import { t as policyFromCatalog } from "./policy-from-catalog-C2gMa2Dj.js";
|
|
2
|
+
import { c as listPermissions, u as renamedKeys } from "./permission-registry-B8tYxPIt.js";
|
|
3
|
+
import { i as dateAt } from "./time-nR1JesoH.js";
|
|
4
|
+
import { p as scopeField } from "./tenancy-5d1zVBvK.js";
|
|
5
|
+
import { a as describeError, i as configError, o as exitCodeOfError, u as usageResult } from "./errors-Vpu6rKHl.js";
|
|
6
|
+
import { t as policyRowConditionKeys } from "./row-conditions-BAGHOmVq.js";
|
|
7
|
+
import { o as sqlFiles } from "./files-BSjrtIHw.js";
|
|
8
|
+
import { c as requirePolicy } from "./load-DjSU5KXO.js";
|
|
9
|
+
import { i as qualifySqlTable, r as escapeSqlIdent } from "./sql-0WjxIp9g.js";
|
|
10
|
+
import "./sources-CMAM-2A3.js";
|
|
11
|
+
import { t as policyFromCatalog } from "./policy-from-catalog-CJgOm6aq.js";
|
|
14
12
|
import { t as beforeFirst } from "./text-BMFRfBqc.js";
|
|
15
|
-
import { i as INDEXES_MARKER, n as GRANTS_MARKER, o as SEEDS_MARKER, t as BACKFILL_MARKER } from "./markers-
|
|
16
|
-
import {
|
|
17
|
-
import { t as
|
|
18
|
-
import { t as
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import { a as
|
|
27
|
-
import { t as
|
|
13
|
+
import { i as INDEXES_MARKER, n as GRANTS_MARKER, o as SEEDS_MARKER, t as BACKFILL_MARKER } from "./markers-BeHZ4x9Q.js";
|
|
14
|
+
import { a as nonEmptyOr } from "./output-tkju6U7R.js";
|
|
15
|
+
import { i as requirePeer, n as packageManagerOf, t as addLine } from "./peer-Q0fuZ6hx.js";
|
|
16
|
+
import { t as POLICY_NOT_PUBLISHED } from "./problem-types-DSsRfONQ.js";
|
|
17
|
+
import { t as fetchProjectPolicy } from "./pull-CwpGaQSi.js";
|
|
18
|
+
import { n as MIGRATION_DIRS } from "./constants-DcJABbLW.js";
|
|
19
|
+
import { a as isSeedsDirectory, c as parseSplit, d as seedsMigration, f as writeOutputFiles, l as partPath, n as dataMigrationFile, r as driftOf, u as pgDeltaPath } from "./output-files-BIQ_9pIs.js";
|
|
20
|
+
import { d as schemasRefusal, f as declarativeSql, l as isSupabaseSchemas, o as supabaseHookSql, s as approvalsSql, t as grantsLabel, u as resolveSchemas, v as parseRbacAuthorize, y as rbacScaffold } from "./supabase-hook-B-yXnzUl.js";
|
|
21
|
+
import { A as quoteQualified, D as qualified$1, I as scopeTypeOf, O as quoteIdent$1, _ as subjectClaimJsonSql, c as SESSION_LIVE_HELPER, g as sessionLiveHelperSql, j as quoteTable, k as quoteLiteral, r as HELPERS, t as CUSTOM_ROLES, y as subjectIdSql } from "./shared-Afctg7B0.js";
|
|
22
|
+
import { C as accessSql, G as byKey, T as seedSql, Z as graphHelper, ct as memberIdsHelper, ft as permittedIdsHelper, ht as scopeTable, mt as scopeSources, r as tableNames, s as graphSql, w as helpersSql, x as permittedByPermissionHelper, y as permissionHelpersSql } from "./tables-DeMNEy6V.js";
|
|
23
|
+
import { c as breakGlassKey, i as assignmentSql, l as collectGrants, n as ownershipSql, s as breakGlassHolder } from "./ownership-B4L59T9u.js";
|
|
24
|
+
import { a as splitUndiffed } from "./deciding-columns-NB4wcyBf.js";
|
|
25
|
+
import { t as connectPgPool } from "./pg-DQTqRrou.js";
|
|
26
|
+
import { n as permissionHoldersSql } from "./resolve-names-CF7xXLSG.js";
|
|
27
|
+
import { a as fieldViewsSql, c as ACTOR_MAY_WRITE, f as readOnlyActorKinds, i as fieldViews, l as actorMayWriteSql, n as columnGrantSql, o as rowBranches, p as readOnlyActorPolicies, r as columnRevokeSql, u as assemblePolicies } from "./fields-D0vUI6dq.js";
|
|
28
|
+
import { _ as indexTargets, a as verifyResult, b as readTableIndexFacts, c as callsHelper, g as INDEX_COLUMNS_SQL, h as COLUMNS_SQL, i as verifyIgnores, l as helperCallKeys, m as tableFor, n as runRlsVerify, o as HELPER_TABLES, p as compileGrants, r as rlsFinding, v as indexesSql, y as pruneIndexTargets } from "./verify-DZeqoWu5.js";
|
|
29
|
+
import { a as generateOptions, c as buildContext, i as rowHelpersSql, l as DROPPED_LIST, n as inheritTargets, o as isRefusal, r as levelKeys, s as splitsPart, t as supautilsRejections, u as rebuildPlan } from "./supautils-BU-pAEpf.js";
|
|
30
|
+
import { readFileSync } from "node:fs";
|
|
28
31
|
import { basename, delimiter, join } from "node:path";
|
|
29
32
|
import { spawn } from "node:child_process";
|
|
30
33
|
//#region src/cli/rls/advisors.ts
|
|
@@ -136,7 +139,7 @@ async function runRlsAdvisors(input) {
|
|
|
136
139
|
const message = [text(error, "message"), text(error, "suggestion")].filter((part) => part !== "").join(" ");
|
|
137
140
|
return {
|
|
138
141
|
code: 2,
|
|
139
|
-
output: `supabase db advisors failed: ${message === "" ? result.stderr.trim()
|
|
142
|
+
output: `supabase db advisors failed: ${message === "" ? nonEmptyOr(result.stderr.trim(), `exit ${String(result.status)}`) : message}`
|
|
140
143
|
};
|
|
141
144
|
}
|
|
142
145
|
const failing = findings.filter((finding) => finding.level === "WARN" || finding.level === "ERROR");
|
|
@@ -159,142 +162,164 @@ async function runRlsAdvisors(input) {
|
|
|
159
162
|
};
|
|
160
163
|
}
|
|
161
164
|
//#endregion
|
|
162
|
-
//#region src/cli/rls/
|
|
163
|
-
/**
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
/** Declared role names, and those a tenant admin may compose into custom roles. */
|
|
180
|
-
function customRoleNames(policy, requires) {
|
|
181
|
-
const declared = [...roleNames(policy)].toSorted();
|
|
182
|
-
const assignable = declared.filter((name) => policy.rolesByName.get(name)?.assignable ?? listRoles(policy.vocabulary.roles).some((leaf) => leaf.key === name && leaf.assignable));
|
|
183
|
-
const renamed = renamedKeys(policy.vocabulary.permissions);
|
|
184
|
-
const leaves = listPermissions(policy.permissions);
|
|
185
|
-
const manage = leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key);
|
|
186
|
-
return {
|
|
187
|
-
declared,
|
|
188
|
-
assignable,
|
|
189
|
-
...renamed.size === 0 ? {} : { renamed: Object.fromEntries(renamed) },
|
|
190
|
-
permissions: leaves.map((leaf) => leaf.key).toSorted(),
|
|
191
|
-
...manage.length === 0 ? {} : { manage: manage.toSorted() },
|
|
192
|
-
...requires === void 0 ? {} : { requires },
|
|
193
|
-
...policy.levels === void 0 ? {} : { levels: true }
|
|
194
|
-
};
|
|
165
|
+
//#region src/cli/rls/bindings.ts
|
|
166
|
+
/**
|
|
167
|
+
* The table each resource with row policies binds to. With `helpersOnly`, a
|
|
168
|
+
* resource `rls.tables` does not name has no table and no binding.
|
|
169
|
+
*/
|
|
170
|
+
function tableBindings(resources, tables, helpersOnly) {
|
|
171
|
+
const out = [];
|
|
172
|
+
for (const resource of [...new Set(resources)].toSorted()) {
|
|
173
|
+
const named = tables !== void 0 && Object.hasOwn(tables, resource);
|
|
174
|
+
if (helpersOnly && !named) continue;
|
|
175
|
+
out.push({
|
|
176
|
+
resource,
|
|
177
|
+
table: qualifySqlTable(tableFor(resource, tables)),
|
|
178
|
+
inferred: !named
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
195
182
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
183
|
+
/**
|
|
184
|
+
* The generate notes for `bindings`: the inferred ones, and where to see them
|
|
185
|
+
* all.
|
|
186
|
+
*/
|
|
187
|
+
function bindingNotes(bindings, withDatabase) {
|
|
188
|
+
const inferred = bindings.filter((binding) => binding.inferred);
|
|
189
|
+
if (inferred.length === 0) return [];
|
|
190
|
+
return [`table bindings (no rls.tables entry, so the table is named after the resource): ${inferred.map((binding) => `${binding.resource} -> ${binding.table}`).join(", ")}${withDatabase ? "" : "; run permdock rls explain, or generate with --db-url-stdin, to check each table exists"}`];
|
|
203
191
|
}
|
|
204
|
-
/**
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const sources = authorize === "database" ? memberSources : void 0;
|
|
217
|
-
const scopes = scopeList(policy.scopes);
|
|
218
|
-
const suspension = checkSuspension(rls?.suspension, scopes);
|
|
219
|
-
const hookRoles = input.config.supabase?.hook?.roles;
|
|
220
|
-
const roles = rls?.roles ?? (hookRoles === false ? void 0 : hookRoles);
|
|
221
|
-
if (roles !== void 0 && input.rbac) return { refusal: "rls generate --rbac supabase creates its own user_roles (user_id, role app_role); drop rls.roles (or supabase.hook.roles) or the --rbac flag" };
|
|
222
|
-
const requires = writeRequirement(policy, rls?.customRoleWrites?.requires);
|
|
223
|
-
if (requires.error !== void 0) return { refusal: requires.error };
|
|
224
|
-
const readers = rls?.trustedReaders;
|
|
225
|
-
if (readers !== void 0 && (!Array.isArray(readers) || !readers.every((name) => typeof name === "string" && name !== ""))) return { refusal: "rls.trustedReaders must list Postgres role names, such as ['service_role']" };
|
|
226
|
-
const ownership = ownershipRules(policy, scopes);
|
|
227
|
-
const graph = graphPlan(policy);
|
|
228
|
-
const apiKeys = apiKeysPlan(rls?.apiKeys, new Set(roleNames(policy)), Object.fromEntries(renamedKeys(policy.vocabulary.permissions)));
|
|
229
|
-
return {
|
|
230
|
-
ctx: {
|
|
231
|
-
dialect: input.dialect,
|
|
232
|
-
scopes,
|
|
233
|
-
tenantClaim: rls?.tenantClaim ?? "tenant_id",
|
|
234
|
-
...roleMemberScopesOf(rls?.migrate),
|
|
235
|
-
...rls?.roleValues === void 0 ? {} : { roleValues: rls.roleValues },
|
|
236
|
-
...rls?.tenants === "all" ? { tenants: "all" } : {},
|
|
237
|
-
gucPrefix: input.gucPrefix ?? rls?.gucPrefix ?? "app",
|
|
238
|
-
...rls?.actions === void 0 ? {} : { actions: rls.actions },
|
|
239
|
-
inlineFunctions: input.inlineFunctions || rls?.inlineFunctions === true,
|
|
240
|
-
schema,
|
|
241
|
-
authorize,
|
|
242
|
-
roleClaim: rls?.roleClaim ?? "user_role",
|
|
243
|
-
tenantType: input.tenantType ?? rls?.tenantType ?? "uuid",
|
|
244
|
-
...rls?.teamType === void 0 ? {} : { teamType: rls.teamType },
|
|
245
|
-
...rls?.scopeTypes === void 0 ? {} : { scopeTypes: rls.scopeTypes },
|
|
246
|
-
...memberships === void 0 ? {} : { memberships },
|
|
247
|
-
...sources === void 0 || sources.length === 0 ? {} : { sources },
|
|
248
|
-
...memberSources === void 0 || memberSources.length === 0 ? {} : { memberSources },
|
|
249
|
-
...suspension === void 0 ? {} : { suspension },
|
|
250
|
-
...roles === void 0 ? {} : { roles },
|
|
251
|
-
...input.customRoles === true || customRolesOn(rls) ? { customRoles: {
|
|
252
|
-
...customRoleNames(policy, requires.keys),
|
|
253
|
-
...rls?.customRoleWrites?.roles === void 0 ? {} : { table: rls.customRoleWrites.roles }
|
|
254
|
-
} } : {},
|
|
255
|
-
...input.capabilities === true || rls?.capabilities === true ? { capabilities: true } : {},
|
|
256
|
-
...apiKeys === void 0 ? {} : { apiKeys },
|
|
257
|
-
...rls?.anonymousSignIns === "deny" ? { anonymousSignIns: "deny" } : {},
|
|
258
|
-
...ownership === void 0 ? {} : { ownership },
|
|
259
|
-
...rls?.ownershipTriggers === void 0 ? {} : { skipOwnershipTriggers: skippedOwnershipTriggers(rls.ownershipTriggers, scopes) },
|
|
260
|
-
...rls?.assignments === void 0 ? {} : { assignments: rls.assignments === true ? { tables: [] } : {
|
|
261
|
-
tables: rls.assignments.tables ?? [],
|
|
262
|
-
...rls.assignments.ownRole === void 0 ? {} : { ownRole: rls.assignments.ownRole }
|
|
263
|
-
} },
|
|
264
|
-
...options.fieldsMode === void 0 ? {} : { fields: options.fieldsMode },
|
|
265
|
-
...graph.size === 0 ? {} : { graph: {
|
|
266
|
-
closures: closureDepths(graph),
|
|
267
|
-
resources: policy.resources,
|
|
268
|
-
...rls?.tables === void 0 ? {} : { tables: rls.tables }
|
|
269
|
-
} }
|
|
270
|
-
},
|
|
271
|
-
graph,
|
|
272
|
-
ownership,
|
|
273
|
-
schema,
|
|
274
|
-
authorize
|
|
275
|
-
};
|
|
192
|
+
/**
|
|
193
|
+
* The generate notes for the bindings whose table no project SQL file creates.
|
|
194
|
+
* A project with no `create table` in those files gets none: its schema lives
|
|
195
|
+
* elsewhere (an ORM, another repository).
|
|
196
|
+
*/
|
|
197
|
+
function unknownTableNotes(bindings, created) {
|
|
198
|
+
if (created.size === 0) return [];
|
|
199
|
+
return bindings.filter((binding) => !created.has(binding.table) && !created.has(binding.table.toLowerCase())).map((binding) => `resource ${binding.resource} is bound to ${binding.table}, which no SQL file in the migration or schema folders creates; name its table in rls.tables, or set rls.tables.${binding.resource}: false when it has none`);
|
|
200
|
+
}
|
|
201
|
+
/** The inferred bindings whose table `facts` shows missing. */
|
|
202
|
+
function missingBindings(bindings, columns) {
|
|
203
|
+
return bindings.filter((binding) => binding.inferred && columns[binding.table] === void 0);
|
|
276
204
|
}
|
|
277
205
|
//#endregion
|
|
278
|
-
//#region src/cli/rls/
|
|
279
|
-
|
|
280
|
-
|
|
206
|
+
//#region src/cli/rls/trigger-notes.ts
|
|
207
|
+
const CREATE = /^create trigger\s+("[^"]+"|[\w$]+)[^;]*?\bon\s+("[^"]+"(?:\."[^"]+")?|[\w$.]+)/gmu;
|
|
208
|
+
function unquote(name) {
|
|
209
|
+
return name.replaceAll("\"", "");
|
|
210
|
+
}
|
|
211
|
+
function reasonOf(name) {
|
|
212
|
+
if (name === "permdock_custom_role_name_guard") return "customRoleWrites.roles: a tenant role cannot take a declared or shared role's name";
|
|
213
|
+
if (name === "permdock_custom_role_cascade") return "customRoleWrites.roles: a rename or delete of a role carries its grants";
|
|
214
|
+
if (name.startsWith("permdock_sync_custom_role_guard")) return "customRoles.from.sync: a signed-in caller writes only entries they may hand out";
|
|
215
|
+
if (name.startsWith("permdock_sync_custom_role")) return "customRoles.from.sync: each write to the legacy table rebuilds the role's grants";
|
|
216
|
+
if (name.startsWith("permdock_holders") || name.startsWith("permdock_transfer_only")) return "ownershipTriggers; set rls.ownershipTriggers.<scope>: false to skip";
|
|
217
|
+
if (name === "permdock_assignment") return "assignments: a role assignment is checked against the caller's own roles";
|
|
218
|
+
if (name.startsWith("permdock_closure")) return "the relationship closure table follows its source table";
|
|
219
|
+
return "generated by rls generate";
|
|
220
|
+
}
|
|
221
|
+
/** The triggers `sql` creates, in order, each with the option behind it. */
|
|
222
|
+
function generatedTriggers(sql) {
|
|
223
|
+
const found = [];
|
|
224
|
+
for (const match of sql.matchAll(CREATE)) {
|
|
225
|
+
const name = unquote(match.slice(1, 2).join(""));
|
|
226
|
+
const table = unquote(match.slice(2, 3).join(""));
|
|
227
|
+
found.push({
|
|
228
|
+
name,
|
|
229
|
+
table,
|
|
230
|
+
reason: reasonOf(name)
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return found;
|
|
281
234
|
}
|
|
282
|
-
/**
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
235
|
+
/** One generate note per trigger. */
|
|
236
|
+
function triggerNotes(sql) {
|
|
237
|
+
return generatedTriggers(sql).map((trigger) => `generated trigger ${trigger.name} on ${trigger.table} (${trigger.reason})`);
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/cli/rls/explain.ts
|
|
241
|
+
function holds(table) {
|
|
242
|
+
if (table.tenant === void 0) return ["global"];
|
|
243
|
+
return [
|
|
244
|
+
"tenant",
|
|
245
|
+
...table.shared === void 0 ? [] : ["shared"],
|
|
246
|
+
"global"
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
function explainRls(input) {
|
|
250
|
+
const { config, ctx, generated } = input;
|
|
251
|
+
const rls = config.rls;
|
|
252
|
+
const bindings = tableBindings(generated.compiled.rowColumns.map((item) => item.resource), tableNames(rls?.tables), generated.helpersOnly);
|
|
253
|
+
const sources = ctx.memberSources ?? ctx.sources ?? [];
|
|
254
|
+
const roles = rls?.customRoleWrites?.roles;
|
|
255
|
+
const wanted = rls?.rowHelpers;
|
|
256
|
+
const resources = wanted === void 0 ? [] : wanted === true ? [...new Set(generated.compiled.branches.flatMap((branch) => branch.resource === void 0 ? [] : [branch.resource]))].toSorted() : [...new Set(wanted)];
|
|
289
257
|
return {
|
|
290
|
-
|
|
291
|
-
|
|
258
|
+
schema: input.schema,
|
|
259
|
+
authorize: input.authorize,
|
|
260
|
+
scopes: ctx.scopes.map((scope) => scope.within === void 0 ? {
|
|
261
|
+
name: scope.name,
|
|
262
|
+
key: scope.key ?? scope.name
|
|
263
|
+
} : {
|
|
264
|
+
name: scope.name,
|
|
265
|
+
key: scope.key ?? scope.name,
|
|
266
|
+
within: scope.within
|
|
267
|
+
}),
|
|
268
|
+
bindings: bindings.map((binding) => {
|
|
269
|
+
const via = ctx.vias?.[binding.resource];
|
|
270
|
+
return via === void 0 ? binding : Object.assign({}, binding, { via: `${via.helper} over ${via.table}.${via.column} (${binding.table}.${via.key})` });
|
|
271
|
+
}),
|
|
272
|
+
membershipSources: sources.map((source) => ({
|
|
273
|
+
table: source.sql.table,
|
|
274
|
+
select: source.sql.select("$1")
|
|
275
|
+
})),
|
|
276
|
+
...roles === void 0 ? {} : { customRoles: {
|
|
277
|
+
table: roles.table,
|
|
278
|
+
key: roles.key,
|
|
279
|
+
...roles.tenant === void 0 ? {} : { tenant: roles.tenant },
|
|
280
|
+
...roles.shared === void 0 ? {} : { shared: roles.shared },
|
|
281
|
+
...roles.skip === void 0 ? {} : { skip: roles.skip },
|
|
282
|
+
holds: holds(roles)
|
|
283
|
+
} },
|
|
284
|
+
rowHelpers: resources.map((resource) => ({
|
|
285
|
+
resource,
|
|
286
|
+
permissions: [...new Set(generated.compiled.branches.flatMap((branch) => branch.resource === resource && branch.coverage !== true ? [branch.permissionKey] : []))].toSorted()
|
|
287
|
+
})),
|
|
288
|
+
triggers: generatedTriggers(input.text)
|
|
292
289
|
};
|
|
293
290
|
}
|
|
294
|
-
/**
|
|
295
|
-
function
|
|
296
|
-
const
|
|
297
|
-
|
|
291
|
+
/** The text report of `explain`. */
|
|
292
|
+
function describeExplain(explain) {
|
|
293
|
+
const lines = [
|
|
294
|
+
`schema ${explain.schema}, authorize ${explain.authorize}`,
|
|
295
|
+
`scopes: ${explain.scopes.map((scope) => `${scope.name} (${scope.key}${scope.within === void 0 ? "" : `, within ${scope.within}`})`).join(", ")}`,
|
|
296
|
+
"",
|
|
297
|
+
"table bindings:",
|
|
298
|
+
...explain.bindings.map((binding) => ` ${binding.resource} -> ${binding.table}${binding.inferred ? " (named after the resource; set rls.tables to change)" : ""}${binding.via === void 0 ? "" : ` via ${binding.via}`}${binding.exists === false ? " MISSING" : ""}`)
|
|
299
|
+
];
|
|
300
|
+
if (explain.membershipSources.length > 0) {
|
|
301
|
+
lines.push("", "membership sources:");
|
|
302
|
+
for (const source of explain.membershipSources) lines.push(` ${source.table}`, ` ${source.select.replaceAll("\n", "\n ")}`);
|
|
303
|
+
}
|
|
304
|
+
if (explain.customRoles !== void 0) {
|
|
305
|
+
const roles = explain.customRoles;
|
|
306
|
+
const columns = [
|
|
307
|
+
`key ${roles.key}`,
|
|
308
|
+
...roles.tenant === void 0 ? [] : [`tenant ${roles.tenant}`],
|
|
309
|
+
...roles.shared === void 0 ? [] : [`shared ${roles.shared}`],
|
|
310
|
+
...roles.skip === void 0 ? [] : [`skip ${roles.skip}`]
|
|
311
|
+
];
|
|
312
|
+
lines.push("", `custom roles: ${roles.table} (${columns.join(", ")}); holds ${roles.holds.join(", ")} roles`);
|
|
313
|
+
}
|
|
314
|
+
if (explain.rowHelpers.length > 0) {
|
|
315
|
+
lines.push("", "row helpers:");
|
|
316
|
+
for (const helper of explain.rowHelpers) lines.push(` ${helper.resource}: ${helper.permissions.join(", ")}`);
|
|
317
|
+
}
|
|
318
|
+
if (explain.triggers.length > 0) {
|
|
319
|
+
lines.push("", "generated triggers:");
|
|
320
|
+
for (const trigger of explain.triggers) lines.push(` ${trigger.name} on ${trigger.table} (${trigger.reason})`);
|
|
321
|
+
}
|
|
322
|
+
return lines.join("\n");
|
|
298
323
|
}
|
|
299
324
|
//#endregion
|
|
300
325
|
//#region src/cli/rls/generate-output.ts
|
|
@@ -312,8 +337,8 @@ function moveUndiffed(sql, label, part) {
|
|
|
312
337
|
}
|
|
313
338
|
/** Why `--split` cannot be written for `mode`, or `undefined`. */
|
|
314
339
|
function splitRefusal(mode, plan) {
|
|
315
|
-
const seedsWithoutOut = plan.split.includes("seeds") && plan.seedsOut === void 0;
|
|
316
|
-
const helpersWithoutSeeds = plan.split.includes("helpers") && !plan.split.includes("seeds");
|
|
340
|
+
const seedsWithoutOut = plan.split.includes("seeds") && plan.seedsOut === void 0 && !plan.noSeeds && !plan.seedView;
|
|
341
|
+
const helpersWithoutSeeds = plan.split.includes("helpers") && !plan.noSeeds && !plan.split.includes("seeds") && !plan.seedFunction && !plan.seedView;
|
|
317
342
|
switch (mode) {
|
|
318
343
|
case "pg-delta":
|
|
319
344
|
if (plan.explicitOut && !plan.placeholder) return `rls generate --split needs {part} in --out, or no --out for pg-delta's per-schema layout (got ${plan.outRel})`;
|
|
@@ -333,7 +358,10 @@ function splitRefusal(mode, plan) {
|
|
|
333
358
|
default: return mode;
|
|
334
359
|
}
|
|
335
360
|
}
|
|
336
|
-
/**
|
|
361
|
+
/**
|
|
362
|
+
* The files `generate` writes: one, or one per `--split` part, plus the hook's
|
|
363
|
+
* grants under migra.
|
|
364
|
+
*/
|
|
337
365
|
function outputFiles(plan) {
|
|
338
366
|
const { input } = plan;
|
|
339
367
|
const split = parseSplit(input.split);
|
|
@@ -342,6 +370,7 @@ function outputFiles(plan) {
|
|
|
342
370
|
if (split === void 0) {
|
|
343
371
|
if (input.grantsOut !== void 0) return "rls generate --grants-out needs --split with the helpers or hook part";
|
|
344
372
|
if (input.seedsOut !== void 0) return "rls generate --seeds-out needs --split with the seeds part";
|
|
373
|
+
if (input.noSeeds === true) return "rls generate --no-seeds needs --split: without it the seeds are part of the one file";
|
|
345
374
|
return [{
|
|
346
375
|
part: "rls",
|
|
347
376
|
rel: plan.outRel,
|
|
@@ -358,11 +387,15 @@ function outputFiles(plan) {
|
|
|
358
387
|
explicitOut: input.out !== void 0,
|
|
359
388
|
outRel: plan.outRel,
|
|
360
389
|
grantsOut: input.grantsOut,
|
|
361
|
-
seedsOut: input.seedsOut
|
|
390
|
+
seedsOut: input.seedsOut,
|
|
391
|
+
noSeeds: input.noSeeds === true,
|
|
392
|
+
seedFunction: input.config.rls?.seeds === "function",
|
|
393
|
+
seedView: input.config.rls?.seeds === "view"
|
|
362
394
|
});
|
|
363
395
|
if (refused !== void 0) return refused;
|
|
364
396
|
const at = (part, schema = plan.schema) => pgDelta === void 0 || placeholder ? partPath(plan.outRel, part) : pgDeltaPath(pgDelta.schemaDir, part, schema);
|
|
365
397
|
if (input.grantsOut !== void 0 && !split.includes("hook") && !split.includes("helpers")) return "rls generate --grants-out needs the helpers or hook part in --split";
|
|
398
|
+
if (input.noSeeds === true && input.seedsOut !== void 0) return "rls generate --no-seeds and --seeds-out exclude each other: drop one";
|
|
366
399
|
if (input.seedsOut !== void 0 && !split.includes("seeds")) return "rls generate --seeds-out needs the seeds part in --split";
|
|
367
400
|
if (split.includes("hook") && input.config.supabase?.hook === void 0) return "rls generate --split hook needs supabase.hook in permdock.config.ts";
|
|
368
401
|
const sql = plan.sql();
|
|
@@ -393,6 +426,10 @@ function outputFiles(plan) {
|
|
|
393
426
|
break;
|
|
394
427
|
}
|
|
395
428
|
case "seeds": {
|
|
429
|
+
if (input.noSeeds === true) {
|
|
430
|
+
if (relocated !== void 0 && split.includes("policies")) return "rls generate --no-seeds cannot drop the seeds part under pg-delta with rls.realtime or rls.storage: their policies live in it";
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
396
433
|
const out = input.seedsOut;
|
|
397
434
|
if (out !== void 0 && isSeedsDirectory(input.cwd, out)) {
|
|
398
435
|
const planned = seedsMigration(input.cwd, out, SEEDS_MARKER, seeds, input.io.now?.() ?? dateAt());
|
|
@@ -438,6 +475,12 @@ function outputFiles(plan) {
|
|
|
438
475
|
}
|
|
439
476
|
default: return part;
|
|
440
477
|
}
|
|
478
|
+
if (pgDelta !== void 0 && !placeholder) {
|
|
479
|
+
for (const [index, file] of files.entries()) if (file.part !== "seeds" || input.seedsOut === void 0) files[index] = {
|
|
480
|
+
...file,
|
|
481
|
+
text: declarativeSql(file.text)
|
|
482
|
+
};
|
|
483
|
+
}
|
|
441
484
|
if (input.grantsOut !== void 0) files.push({
|
|
442
485
|
part: "grants",
|
|
443
486
|
rel: input.grantsOut,
|
|
@@ -447,124 +490,18 @@ function outputFiles(plan) {
|
|
|
447
490
|
}
|
|
448
491
|
//#endregion
|
|
449
492
|
//#region src/cli/rls/backfill.ts
|
|
450
|
-
const PREFIX$1 = "PermDock CLI";
|
|
451
|
-
/** A column of the roles-table row `r`. */
|
|
452
|
-
function col(name) {
|
|
453
|
-
return `r.${quoteIdent$1(name)}`;
|
|
454
|
-
}
|
|
455
|
-
function schemaOf(table) {
|
|
456
|
-
return table.includes(".") ? beforeFirst(table, ".") : "public";
|
|
457
|
-
}
|
|
458
|
-
/** The roles-table column a reference joins on, after checking it points at `roles`. */
|
|
459
|
-
function roleRef(role, table, roles, label) {
|
|
460
|
-
const pairs = Object.entries(role.on);
|
|
461
|
-
const [pair] = pairs;
|
|
462
|
-
if (pair === void 0 || pairs.length !== 1) throw configError(`${PREFIX$1}: ${label}.on must map exactly one column of ${table} to ${role.through}, for example { role_id: 'id' }`);
|
|
463
|
-
const through = role.through.includes(".") ? role.through : `${schemaOf(table)}.${role.through}`;
|
|
464
|
-
if (qualifySqlTable(through) !== qualifySqlTable(roles.table)) throw configError(`${PREFIX$1}: ${label}.through must be rls.customRoleWrites.roles.table (${roles.table}), got ${role.through}`);
|
|
465
|
-
if (role.column !== roles.key) throw configError(`${PREFIX$1}: ${label}.column must be rls.customRoleWrites.roles.key (${roles.key}), got ${role.column}`);
|
|
466
|
-
const [ref, id] = pair;
|
|
467
|
-
return {
|
|
468
|
-
ref,
|
|
469
|
-
id
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
493
|
/**
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const exact = new Map(Object.entries(renamed));
|
|
479
|
-
for (const [from, to] of Object.entries(migrate?.keys ?? {})) exact.set(from, to);
|
|
480
|
-
const prefixes = Object.entries(migrate?.prefixes ?? {}).toSorted(([a], [b]) => b.length - a.length || byCodeUnit(a, b));
|
|
481
|
-
const prefixed = prefixes.length === 0 ? expr : `(case ${prefixes.map(([from, to]) => `when left(${expr}, ${String(from.length)}) = ${quoteLiteral(from)} then ${quoteLiteral(to)} || substr(${expr}, ${String(from.length + 1)})`).join(" ")} else ${expr} end)`;
|
|
482
|
-
const mapped = exact.size === 0 ? prefixed : `(case ${expr} ${[...exact.entries()].toSorted(byKey).map(([from, to]) => `when ${quoteLiteral(from)} then ${quoteLiteral(to)}`).join(" ")} else ${prefixed} end)`;
|
|
483
|
-
const renames = Object.entries(renamed).toSorted(byKey);
|
|
484
|
-
return currentKeyExpr(renames, mapped);
|
|
485
|
-
}
|
|
486
|
-
function sourceSql(source, table, label) {
|
|
487
|
-
return roleColumn(source, table, "x", {
|
|
488
|
-
label,
|
|
489
|
-
prefix: PREFIX$1
|
|
490
|
-
}).lookup;
|
|
491
|
-
}
|
|
492
|
-
/**
|
|
493
|
-
* The `--backfill-out` migration: every custom role in the application's
|
|
494
|
-
* roles table that has no rows in `custom_role_permissions` or
|
|
495
|
-
* `custom_role_includes` yet is saved through
|
|
496
|
-
* `permdock_trusted_replace_custom_role_grants`, with the entries PermDock
|
|
497
|
-
* cannot hold left out and listed in one warning. Running it again copies
|
|
498
|
-
* nothing, so a role edited after the copy keeps its edits.
|
|
494
|
+
* The `--backfill-out` migration: every custom role in the application's roles
|
|
495
|
+
* table that has no rows in `custom_role_permissions` or `custom_role_includes`
|
|
496
|
+
* yet is saved through `permdock_trusted_replace_custom_role_grants`, with the
|
|
497
|
+
* entries PermDock cannot hold left out and listed in one warning. Running it
|
|
498
|
+
* again copies nothing, so a role edited after the copy keeps its edits.
|
|
499
499
|
*/
|
|
500
500
|
function backfillSql(ctx, from, migrate) {
|
|
501
|
-
const
|
|
502
|
-
const
|
|
503
|
-
if (custom === void 0 || root === void 0) throw configError(`${PREFIX$1}: rls.customRoles.from needs custom roles on`);
|
|
504
|
-
if (ctx.authorize !== "database") throw configError(`${PREFIX$1}: rls.customRoles.from needs database mode; in jwt mode custom roles ride the token, so build the grants claim with customRoleClaim`);
|
|
505
|
-
const roles = custom.table;
|
|
506
|
-
if (roles === void 0) throw configError(`${PREFIX$1}: rls.customRoles.from needs rls.customRoleWrites.roles, the roles table each row belongs to`);
|
|
507
|
-
const leveled = custom.levels === true;
|
|
508
|
-
if (from.permissions.level !== void 0 && !leveled) throw configError(`${PREFIX$1}: rls.customRoles.from.permissions.level is set, but no resource declares levels`);
|
|
509
|
-
const tenantType = tenantTypeOf(ctx);
|
|
510
|
-
const rolesTable = quoteQualified(roles.table);
|
|
511
|
-
const tenant = roles.tenant === void 0 ? `null::${tenantType}` : `${col(roles.tenant)}::${tenantType}`;
|
|
512
|
-
const scope = rolesTableScope(roles, tenant, root, col);
|
|
513
|
-
const scopeId = roles.id === void 0 ? "null::text" : `${col(roles.id)}::text`;
|
|
514
|
-
const skipped = roles.skip === void 0 ? "false" : `coalesce(${col(roles.skip)}, false)`;
|
|
515
|
-
const renamed = custom.renamed ?? {};
|
|
516
|
-
const renames = Object.entries(renamed).toSorted(byKey);
|
|
517
|
-
const current = (expr) => currentKeyExpr(renames, expr);
|
|
518
|
-
const known = sqlTextArray([...custom.permissions ?? [], ...renames.map(([former]) => former)]);
|
|
519
|
-
const declared = sqlTextArray(custom.declared);
|
|
520
|
-
const scopes = sqlTextArray(ctx.scopes.map((item) => item.name));
|
|
521
|
-
const ceiling = qualified(ctx, CUSTOM_ROLES.ceiling);
|
|
522
|
-
const rp = qualified(ctx, "role_permissions");
|
|
523
|
-
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
524
|
-
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
525
|
-
const replace = qualified(ctx, CUSTOM_ROLES.trustedReplace);
|
|
526
|
-
const p = from.permissions;
|
|
527
|
-
const pTable = qualifySqlTable(p.table);
|
|
528
|
-
const pRef = roleRef(p.role, p.table, roles, "rls.customRoles.from.permissions.role");
|
|
529
|
-
const legacy = sourceSql(p.permission, pTable, "rls.customRoles.from.permissions.permission");
|
|
530
|
-
const effect = p.effect === void 0 ? "'allow'" : `coalesce(lower(x.${quoteIdent$1(p.effect)}::text), 'allow')`;
|
|
531
|
-
const level = p.level === void 0 ? "null::text" : `nullif(x.${quoteIdent$1(p.level)}::text, '')`;
|
|
532
|
-
const inCeiling = (key) => `exists (select 1 from ${ceiling} c where c.scope = v.scope and c.effect = 'allow' and ${current("c.permission")} = ${key})`;
|
|
533
|
-
const levelCheck = leveled ? `
|
|
534
|
-
when m.effect = 'allow' and m.level is not null and not exists (
|
|
535
|
-
select 1 from ${ceiling} c
|
|
536
|
-
where c.scope = v.scope and c.effect = 'allow' and ${current("c.permission")} = m.key
|
|
537
|
-
and split_part(c.grant_key, '@', 2) = m.level
|
|
538
|
-
) then 'level ' || m.level || ' is not declared'` : "";
|
|
539
|
-
const allowEntry = leveled ? "j.key || coalesce('@' || j.level, '')" : "j.key";
|
|
540
|
-
const i = from.includes;
|
|
541
|
-
const iTable = i === void 0 ? void 0 : qualifySqlTable(i.table);
|
|
542
|
-
const iRef = i === void 0 || iTable === void 0 ? void 0 : roleRef(i.role, i.table, roles, "rls.customRoles.from.includes.role");
|
|
543
|
-
const included = i === void 0 || iTable === void 0 ? void 0 : sourceSql(i.include, iTable, "rls.customRoles.from.includes.include");
|
|
544
|
-
const refs = [`${col(pRef.id)} as permissions_ref`, ...iRef === void 0 ? [] : [`${col(iRef.id)} as includes_ref`]].join(", ");
|
|
545
|
-
const includeBlock = i === void 0 || iTable === void 0 || iRef === void 0 || included === void 0 ? " v_include := null;" : ` with raw as (
|
|
546
|
-
select ${included} as role
|
|
547
|
-
from ${quoteTable(iTable)} x
|
|
548
|
-
where x.${quoteIdent$1(iRef.ref)} = v.includes_ref
|
|
549
|
-
),
|
|
550
|
-
judged as (
|
|
551
|
-
select n.role, case
|
|
552
|
-
when n.role is null or not (n.role = any(${declared})) then 'not a declared role'
|
|
553
|
-
when exists (
|
|
554
|
-
select 1 from ${rp} rp
|
|
555
|
-
where rp.role = n.role and rp.effect = 'allow'
|
|
556
|
-
and not ${inCeiling(current("rp.permission"))}
|
|
557
|
-
) then 'allows permissions outside the ceiling of ' || v.scope
|
|
558
|
-
end as dropped
|
|
559
|
-
from raw n
|
|
560
|
-
)
|
|
561
|
-
select array_agg(distinct j.role) filter (where j.dropped is null),
|
|
562
|
-
array_agg(v.role || ': includes ' || coalesce(j.role, 'null') || ', ' || j.dropped) filter (where j.dropped is not null)
|
|
563
|
-
into v_include, v_lost
|
|
564
|
-
from judged j;
|
|
565
|
-
v_dropped := v_dropped || coalesce(v_lost, '{}'::text[]);`;
|
|
501
|
+
const plan = rebuildPlan(ctx, from, migrate);
|
|
502
|
+
const { rolesTable, perms, includes, replace, scopes } = plan;
|
|
566
503
|
return `${BACKFILL_MARKER} schema=${ctx.schema ?? "permdock"}
|
|
567
|
-
-- custom roles copied from ${qualifySqlTable(roles.table)} (rls.customRoles.from) through ${replace}
|
|
504
|
+
-- custom roles copied from ${qualifySqlTable(plan.roles.table)} (rls.customRoles.from) through ${replace}
|
|
568
505
|
-- a role that already has rows is left as it is; entries PermDock cannot hold are dropped and listed in one warning
|
|
569
506
|
do $permdock$
|
|
570
507
|
declare
|
|
@@ -578,9 +515,9 @@ declare
|
|
|
578
515
|
v_present integer := 0;
|
|
579
516
|
begin
|
|
580
517
|
for v in
|
|
581
|
-
|
|
518
|
+
${plan.identity}
|
|
582
519
|
from ${rolesTable} r
|
|
583
|
-
where
|
|
520
|
+
where ${plan.eligible}
|
|
584
521
|
order by 4, 2, 1, 3
|
|
585
522
|
loop
|
|
586
523
|
if v.role is null or not (v.scope = 'global' or v.scope = any(${scopes})) then
|
|
@@ -597,30 +534,7 @@ begin
|
|
|
597
534
|
v_present := v_present + 1;
|
|
598
535
|
continue;
|
|
599
536
|
end if;
|
|
600
|
-
|
|
601
|
-
select ${legacy} as legacy, ${effect} as effect, ${level} as level
|
|
602
|
-
from ${quoteTable(pTable)} x
|
|
603
|
-
where x.${quoteIdent$1(pRef.ref)} = v.permissions_ref
|
|
604
|
-
),
|
|
605
|
-
mapped as (
|
|
606
|
-
select n.legacy, ${legacyKeySql("n.legacy", migrate, renamed)} as key, n.effect, n.level
|
|
607
|
-
from raw n
|
|
608
|
-
),
|
|
609
|
-
judged as (
|
|
610
|
-
select m.*, case
|
|
611
|
-
when m.effect not in ('allow', 'deny') then 'effect ' || m.effect || ' is not allow or deny'
|
|
612
|
-
when m.key is null or not (m.key = any(${known})) then 'not a declared permission'
|
|
613
|
-
when m.effect = 'allow' and not ${inCeiling("m.key")} then 'outside the ceiling of ' || v.scope${levelCheck}
|
|
614
|
-
end as dropped
|
|
615
|
-
from mapped m
|
|
616
|
-
)
|
|
617
|
-
select array_agg(distinct ${allowEntry}) filter (where j.dropped is null and j.effect = 'allow'),
|
|
618
|
-
array_agg(distinct j.key) filter (where j.dropped is null and j.effect = 'deny'),
|
|
619
|
-
array_agg(v.role || ': ' || coalesce(j.legacy, 'null') || ', ' || j.dropped) filter (where j.dropped is not null)
|
|
620
|
-
into v_allow, v_deny, v_lost
|
|
621
|
-
from judged j;
|
|
622
|
-
v_dropped := v_dropped || coalesce(v_lost, '{}'::text[]);
|
|
623
|
-
${includeBlock}
|
|
537
|
+
${plan.collect}
|
|
624
538
|
if v_allow is null and v_deny is null and v_include is null then
|
|
625
539
|
continue;
|
|
626
540
|
end if;
|
|
@@ -629,7 +543,7 @@ ${includeBlock}
|
|
|
629
543
|
end loop;
|
|
630
544
|
raise notice 'permdock backfill: copied % custom role(s), % already present', v_copied, v_present;
|
|
631
545
|
if cardinality(v_dropped) > 0 then
|
|
632
|
-
raise warning 'permdock backfill: dropped % entr(ies) PermDock cannot hold: %', cardinality(v_dropped),
|
|
546
|
+
raise warning 'permdock backfill: dropped % entr(ies) PermDock cannot hold: %', cardinality(v_dropped), ${DROPPED_LIST};
|
|
633
547
|
end if;
|
|
634
548
|
end
|
|
635
549
|
$permdock$;
|
|
@@ -637,9 +551,12 @@ $permdock$;
|
|
|
637
551
|
}
|
|
638
552
|
//#endregion
|
|
639
553
|
//#region src/cli/rls/break-glass.ts
|
|
640
|
-
/**
|
|
554
|
+
/**
|
|
555
|
+
* The audit table every generated break-glass read writes to. Part of the SQL
|
|
556
|
+
* contract.
|
|
557
|
+
*/
|
|
641
558
|
const BREAK_GLASS_AUDIT = "permdock_break_glass_audit";
|
|
642
|
-
const IDENT$
|
|
559
|
+
const IDENT$2 = /^[a-z][a-z0-9_]*$/u;
|
|
643
560
|
/**
|
|
644
561
|
* The resources a `breakGlass` grant targets, with the table each reads. A
|
|
645
562
|
* break-glass grant is non-portable, so `compileGrants` never turns it into a
|
|
@@ -678,17 +595,17 @@ function breakGlassEntries(policy, tables) {
|
|
|
678
595
|
}));
|
|
679
596
|
}
|
|
680
597
|
/**
|
|
681
|
-
* Rows the subject's roles reach for `p_permission`: a break-glass grant
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
598
|
+
* Rows the subject's roles reach for `p_permission`: a break-glass grant lifts
|
|
599
|
+
* denies, never the tenant boundary. A grant to no role reads the rows of the
|
|
600
|
+
* root scopes the subject is a member of, or every row when the policy declares
|
|
601
|
+
* no scopes.
|
|
685
602
|
*/
|
|
686
603
|
function rowsFilter(ctx, entry) {
|
|
687
604
|
const arms = entry.grants.flatMap((grant) => {
|
|
688
605
|
const asked = `p_permission = ${quoteLiteral(grant.permission)}`;
|
|
689
606
|
if (grant.scope === "anyone") {
|
|
690
607
|
if (grant.root === void 0) return [`(${asked})`];
|
|
691
|
-
return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified(ctx, memberIdsHelper(grant.root))}()))`];
|
|
608
|
+
return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified$1(ctx, memberIdsHelper(grant.root))}()))`];
|
|
692
609
|
}
|
|
693
610
|
if (grant.scope !== "global" && grant.column === void 0) return [];
|
|
694
611
|
return [`(${asked} and ${accessSql(ctx, grant.scope, breakGlassKey(grant.permission), grant.column)})`];
|
|
@@ -696,11 +613,11 @@ function rowsFilter(ctx, entry) {
|
|
|
696
613
|
return arms.length === 0 ? "false" : [...new Set(arms)].join("\n or ");
|
|
697
614
|
}
|
|
698
615
|
function functionName$1(resource) {
|
|
699
|
-
if (!IDENT$
|
|
616
|
+
if (!IDENT$2.test(resource)) throw configError(`PermDock CLI: unsafe break-glass resource '${resource}'`);
|
|
700
617
|
return `permdock_break_glass_${resource}`;
|
|
701
618
|
}
|
|
702
619
|
function auditSql$1(ctx) {
|
|
703
|
-
const audit = qualified(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
|
|
620
|
+
const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
|
|
704
621
|
return `-- break-glass audit: one row per session that read restricted data
|
|
705
622
|
-- RLS never compiles a break-glass override, so restricted rows stay denied to a direct read
|
|
706
623
|
create table if not exists ${audit} (
|
|
@@ -715,8 +632,8 @@ alter table ${audit} enable row level security;
|
|
|
715
632
|
revoke all on table ${audit} from anon, authenticated, public;`;
|
|
716
633
|
}
|
|
717
634
|
function readFunctionSql(ctx, entry) {
|
|
718
|
-
const fn = qualified(ctx, functionName$1(entry.resource));
|
|
719
|
-
const audit = qualified(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
|
|
635
|
+
const fn = qualified$1(ctx, functionName$1(entry.resource));
|
|
636
|
+
const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
|
|
720
637
|
const table = quoteQualified(entry.table);
|
|
721
638
|
const session = subjectClaimJsonSql(ctx, "break_glass");
|
|
722
639
|
const subject = subjectIdSql(ctx);
|
|
@@ -756,12 +673,12 @@ revoke execute on function ${fn}(text) from public, anon;
|
|
|
756
673
|
grant execute on function ${fn}(text) to authenticated;`;
|
|
757
674
|
}
|
|
758
675
|
/**
|
|
759
|
-
* The `security definer` read path for every `breakGlass` grant: an audit
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
676
|
+
* The `security definer` read path for every `breakGlass` grant: an audit table
|
|
677
|
+
* and one `permdock_break_glass_<resource>(permission)` per resource that
|
|
678
|
+
* checks a signed break-glass session, writes an audit row and returns the rows
|
|
679
|
+
* in the scope instances where the subject holds a role with the break-glass
|
|
680
|
+
* grant (bypassing the table's policies as the definer). Empty when the policy
|
|
681
|
+
* has no break-glass grant.
|
|
765
682
|
*/
|
|
766
683
|
function breakGlassSql(ctx, entries) {
|
|
767
684
|
if (entries.length === 0) return "";
|
|
@@ -770,9 +687,9 @@ function breakGlassSql(ctx, entries) {
|
|
|
770
687
|
//#endregion
|
|
771
688
|
//#region src/cli/rls/emit.ts
|
|
772
689
|
const HEADER = "-- generated by permdock rls generate\n-- fail-closed: never target a bypass role\n";
|
|
773
|
-
const TRUSTED_READER_GRANT = /^grant execute on function [^\n]+\.permdock_trusted_role_permissions
|
|
690
|
+
const TRUSTED_READER_GRANT = /^grant usage on schema "[^"\n]+" to [^\n;]+;\ngrant execute on function [^\n]+\.permdock_trusted_role_permissions(?:_by_id)?\([^)\n]*\) to [^\n;]+;$/gmu;
|
|
774
691
|
function assertNoServiceRole(text) {
|
|
775
|
-
if (/\bservice_role\b/iu.test(text.
|
|
692
|
+
if (/\bservice_role\b/iu.test(text.replaceAll(TRUSTED_READER_GRANT, ""))) throw new Error("PermDock CLI: generated RLS must never emit service_role");
|
|
776
693
|
}
|
|
777
694
|
function assertSupautilsSafe(text) {
|
|
778
695
|
const [first] = supautilsRejections(text);
|
|
@@ -780,8 +697,8 @@ function assertSupautilsSafe(text) {
|
|
|
780
697
|
}
|
|
781
698
|
/**
|
|
782
699
|
* Neon's Data API and `pg_session_jwt` name the anonymous role `anonymous`;
|
|
783
|
-
* Supabase, PostgREST and the GUC pattern use `anon`. Role lists end a line
|
|
784
|
-
*
|
|
700
|
+
* Supabase, PostgREST and the GUC pattern use `anon`. Role lists end a line or
|
|
701
|
+
* a statement after `to` or `from`, so only those are renamed.
|
|
785
702
|
*/
|
|
786
703
|
function dialectRoles(text, dialect) {
|
|
787
704
|
if (dialect !== "neon") return text;
|
|
@@ -790,7 +707,10 @@ function dialectRoles(text, dialect) {
|
|
|
790
707
|
function tablesOf(policies) {
|
|
791
708
|
return [...new Set(policies.map((item) => item.table))];
|
|
792
709
|
}
|
|
793
|
-
/**
|
|
710
|
+
/**
|
|
711
|
+
* Enable (and force) row level security, then the table grants for the commands
|
|
712
|
+
* a policy allows.
|
|
713
|
+
*/
|
|
794
714
|
function tableSql(table, policies, force, views) {
|
|
795
715
|
const closed = views.find((view) => view.table === table && view.companion !== void 0);
|
|
796
716
|
const target = quoteQualified(table);
|
|
@@ -811,7 +731,10 @@ function tableSql(table, policies, force, views) {
|
|
|
811
731
|
if (closed !== void 0) lines.push(columnRevokeSql(closed));
|
|
812
732
|
return lines.join("\n");
|
|
813
733
|
}
|
|
814
|
-
/**
|
|
734
|
+
/**
|
|
735
|
+
* `drop policy if exists` and `create policy` for one policy, without the
|
|
736
|
+
* table's grants.
|
|
737
|
+
*/
|
|
815
738
|
function policySql(item) {
|
|
816
739
|
const target = quoteQualified(item.table);
|
|
817
740
|
const lines = [
|
|
@@ -838,8 +761,9 @@ function emitSql(policies, preamble, force = false, views = []) {
|
|
|
838
761
|
return text;
|
|
839
762
|
}
|
|
840
763
|
/**
|
|
841
|
-
* The SQL a Drizzle or Prisma schema cannot declare: helpers, grants, `force`
|
|
842
|
-
* `rls generate` writes it next to `out` for a custom
|
|
764
|
+
* The SQL a Drizzle or Prisma schema cannot declare: helpers, grants, `force`
|
|
765
|
+
* and field views. `rls generate` writes it next to `out` for a custom
|
|
766
|
+
* migration.
|
|
843
767
|
*/
|
|
844
768
|
function migrationSql(policies, preamble, force, views) {
|
|
845
769
|
const parts = [
|
|
@@ -857,7 +781,7 @@ function helpersNote(helpers, how) {
|
|
|
857
781
|
return helpers === void 0 ? [] : [`// run ${helpers} ${how}`];
|
|
858
782
|
}
|
|
859
783
|
function camelCase(name) {
|
|
860
|
-
return name.
|
|
784
|
+
return name.replaceAll(/[_-]+([a-z0-9])/giu, (_, char) => char.toUpperCase());
|
|
861
785
|
}
|
|
862
786
|
function tableName(table) {
|
|
863
787
|
return table.slice(table.lastIndexOf(".") + 1);
|
|
@@ -866,16 +790,16 @@ function pascalCase(name) {
|
|
|
866
790
|
const camel = camelCase(name);
|
|
867
791
|
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
868
792
|
}
|
|
869
|
-
const IDENTIFIER = /^[A-Za-z_$][\w$]
|
|
793
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/u;
|
|
870
794
|
function identifier(name, what) {
|
|
871
795
|
if (!IDENTIFIER.test(name)) throw configError(`PermDock CLI: ${what} ${JSON.stringify(name)} is not an identifier`);
|
|
872
796
|
return name;
|
|
873
797
|
}
|
|
874
798
|
function quoted(text) {
|
|
875
|
-
return `'${text.
|
|
799
|
+
return `'${text.replaceAll(/[\\']/gu, (match) => `\\${match}`)}'`;
|
|
876
800
|
}
|
|
877
801
|
function templateText(text) {
|
|
878
|
-
return text.
|
|
802
|
+
return text.replaceAll(/[\\`]|\$\{/gu, (match) => `\\${match}`);
|
|
879
803
|
}
|
|
880
804
|
const AUTH_UID = "(select auth.uid())";
|
|
881
805
|
function drizzleRoles(list) {
|
|
@@ -920,7 +844,10 @@ function emitDrizzle(policies, options) {
|
|
|
920
844
|
assertNoServiceRole(text);
|
|
921
845
|
return text;
|
|
922
846
|
}
|
|
923
|
-
/**
|
|
847
|
+
/**
|
|
848
|
+
* Prisma 8 `policy_<command>` blocks; each target model declares `@@rls`
|
|
849
|
+
* itself.
|
|
850
|
+
*/
|
|
924
851
|
function emitPrisma(policies, options = {}) {
|
|
925
852
|
const modelOf = (table) => identifier(options.models?.[table] ?? pascalCase(tableName(table)), "model");
|
|
926
853
|
const models = [...new Set(policies.map((item) => modelOf(item.table)))];
|
|
@@ -977,12 +904,12 @@ function declaredScope(facts, scope, where) {
|
|
|
977
904
|
return scope;
|
|
978
905
|
}
|
|
979
906
|
/**
|
|
980
|
-
* The ids of `scope` where the subject holds `key` through an allow without
|
|
981
|
-
*
|
|
982
|
-
*
|
|
907
|
+
* The ids of `scope` where the subject holds `key` through an allow without a
|
|
908
|
+
* row condition, minus a deny of it, as text: `<value> in (…)`. A relationship
|
|
909
|
+
* or a `where` on the same permission never widens it.
|
|
983
910
|
*/
|
|
984
911
|
function permitted(ctx, value, scope, key) {
|
|
985
|
-
return `${value} in (select x::text from ${qualified(ctx, permittedByPermissionHelper(scope))}(${quoteLiteral(key)}) x)`;
|
|
912
|
+
return `${value} in (select x::text from ${qualified$1(ctx, permittedByPermissionHelper(scope))}(${quoteLiteral(key)}) x)`;
|
|
986
913
|
}
|
|
987
914
|
function escapeRegex(text) {
|
|
988
915
|
return text.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
|
|
@@ -1083,9 +1010,9 @@ function storagePolicies(ctx, facts, storage, warnings) {
|
|
|
1083
1010
|
* The `realtime.messages` and `storage.objects` policies `rls.realtime` and
|
|
1084
1011
|
* `rls.storage` describe. Each one admits a row only where the subject holds
|
|
1085
1012
|
* the permission in the scope instance its topic segment or folder names,
|
|
1086
|
-
* through an allow without a row condition, so the helpers' API-key ceiling
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1013
|
+
* through an allow without a row condition, so the helpers' API-key ceiling and
|
|
1014
|
+
* suspension rules apply and a relationship grant on the same permission never
|
|
1015
|
+
* reaches a topic or a folder.
|
|
1089
1016
|
*/
|
|
1090
1017
|
function realtimeStoragePolicies(ctx, facts, config, warnings = []) {
|
|
1091
1018
|
if (config.realtime === void 0 && config.storage === void 0) return [];
|
|
@@ -1099,172 +1026,12 @@ function realtimeStoragePolicies(ctx, facts, config, warnings = []) {
|
|
|
1099
1026
|
return policies;
|
|
1100
1027
|
}
|
|
1101
1028
|
//#endregion
|
|
1102
|
-
//#region src/cli/rls/rows.ts
|
|
1103
|
-
/** `permitted_<resource>_rows(p_permission)`: the resource's row ids the subject may act on with one permission. */
|
|
1104
|
-
function rowsHelper(resource) {
|
|
1105
|
-
const name = graphSqlName(resource);
|
|
1106
|
-
const helper = `permitted_${name ?? resource}_rows`;
|
|
1107
|
-
if (name === void 0 || new TextEncoder().encode(`${helper}_for`).length > 63) throw configError(`PermDock CLI: rls.rowHelpers cannot name a helper for resource '${resource}'; use a SQL name of at most 45 bytes`);
|
|
1108
|
-
return helper;
|
|
1109
|
-
}
|
|
1110
|
-
/** The branches a row check of a permission reads: its allows and denies on rows that exist, for signed-in callers. */
|
|
1111
|
-
function rowBranchesOf(branches, resource) {
|
|
1112
|
-
const out = /* @__PURE__ */ new Map();
|
|
1113
|
-
for (const branch of branches) {
|
|
1114
|
-
if (branch.resource !== resource || branch.command === "insert" || branch.coverage === true || !branch.roles.includes("authenticated")) continue;
|
|
1115
|
-
out.set(branch.permissionKey, [...out.get(branch.permissionKey) ?? [], branch]);
|
|
1116
|
-
}
|
|
1117
|
-
return out;
|
|
1118
|
-
}
|
|
1119
|
-
function usingOf(branches) {
|
|
1120
|
-
const parts = branches.map((branch) => branchClauses(branch).using ?? "true");
|
|
1121
|
-
return parts.length === 0 ? void 0 : orSql(parts);
|
|
1122
|
-
}
|
|
1123
|
-
function permissionCase(key, branches) {
|
|
1124
|
-
const allow = usingOf(branches.filter((branch) => branch.effect === "allow"));
|
|
1125
|
-
if (allow === void 0) return;
|
|
1126
|
-
const deny = usingOf(branches.filter((branch) => branch.effect === "deny"));
|
|
1127
|
-
const granted = `coalesce((${allow}), false)`;
|
|
1128
|
-
return ` when ${quoteLiteral(key)} then ${deny === void 0 ? granted : `${granted} and (${deny}) is not true`}`;
|
|
1129
|
-
}
|
|
1130
|
-
function forUserSql(ctx, fn, helper) {
|
|
1131
|
-
const user = ctx.dialect === "supabase" ? "uuid" : "text";
|
|
1132
|
-
const signature = `${user}, text, jsonb`;
|
|
1133
|
-
const head = `create or replace function ${fn}(p_user ${user}, p_permission text, p_claims jsonb default '{}')
|
|
1134
|
-
returns setof text
|
|
1135
|
-
language plpgsql
|
|
1136
|
-
volatile
|
|
1137
|
-
security definer
|
|
1138
|
-
set search_path = ''
|
|
1139
|
-
as $$`;
|
|
1140
|
-
const tail = `revoke execute on function ${fn}(${signature}) from public, anon, authenticated;`;
|
|
1141
|
-
if (ctx.dialect === "supabase") return `${head}
|
|
1142
|
-
declare
|
|
1143
|
-
v_claims text := pg_catalog.current_setting('request.jwt.claims', true);
|
|
1144
|
-
begin
|
|
1145
|
-
perform pg_catalog.set_config(
|
|
1146
|
-
'request.jwt.claims',
|
|
1147
|
-
(coalesce(p_claims, '{}'::jsonb) || pg_catalog.jsonb_build_object('sub', p_user::text, 'role', 'authenticated'))::text,
|
|
1148
|
-
true
|
|
1149
|
-
);
|
|
1150
|
-
return query select r.id from ${helper}(p_permission) r(id);
|
|
1151
|
-
perform pg_catalog.set_config('request.jwt.claims', coalesce(v_claims, ''), true);
|
|
1152
|
-
end
|
|
1153
|
-
$$;
|
|
1154
|
-
${tail}`;
|
|
1155
|
-
const prefix = ctx.gucPrefix;
|
|
1156
|
-
return `${head}
|
|
1157
|
-
declare
|
|
1158
|
-
v_saved jsonb := '{}'::jsonb;
|
|
1159
|
-
v_key text;
|
|
1160
|
-
v_value text;
|
|
1161
|
-
begin
|
|
1162
|
-
for v_key, v_value in
|
|
1163
|
-
select 'user_id', p_user
|
|
1164
|
-
union all
|
|
1165
|
-
select c.key, c.value from pg_catalog.jsonb_each_text(coalesce(p_claims, '{}'::jsonb)) c where c.key <> 'user_id'
|
|
1166
|
-
loop
|
|
1167
|
-
v_saved := v_saved || pg_catalog.jsonb_build_object(v_key, coalesce(pg_catalog.current_setting(${quoteLiteral(`${prefix}.`)} || v_key, true), ''));
|
|
1168
|
-
perform pg_catalog.set_config(${quoteLiteral(`${prefix}.`)} || v_key, v_value, true);
|
|
1169
|
-
end loop;
|
|
1170
|
-
return query select r.id from ${helper}(p_permission) r(id);
|
|
1171
|
-
for v_key, v_value in select s.key, s.value from pg_catalog.jsonb_each_text(v_saved) s loop
|
|
1172
|
-
perform pg_catalog.set_config(${quoteLiteral(`${prefix}.`)} || v_key, v_value, true);
|
|
1173
|
-
end loop;
|
|
1174
|
-
end
|
|
1175
|
-
$$;
|
|
1176
|
-
${tail}`;
|
|
1177
|
-
}
|
|
1178
|
-
function inheritTargets(policy) {
|
|
1179
|
-
const out = /* @__PURE__ */ new Set();
|
|
1180
|
-
for (const grant of policy.grants) for (const item of flattenGrantee(grant.to)) if (item.kind === "inherit") out.add(item.resource);
|
|
1181
|
-
return [...out].toSorted();
|
|
1182
|
-
}
|
|
1183
|
-
function stubSql(ctx, resource) {
|
|
1184
|
-
return `create or replace function ${qualified(ctx, rowsHelper(resource))}(p_permission text)
|
|
1185
|
-
returns setof text
|
|
1186
|
-
language sql
|
|
1187
|
-
stable
|
|
1188
|
-
security definer
|
|
1189
|
-
set search_path = ''
|
|
1190
|
-
as $$
|
|
1191
|
-
select null::text where false
|
|
1192
|
-
$$;`;
|
|
1193
|
-
}
|
|
1194
|
-
function rowSql(ctx, rows, table, where) {
|
|
1195
|
-
const fn = qualified(ctx, rows.replace(/_rows$/u, "_row"));
|
|
1196
|
-
return `create or replace function ${fn}(p_row ${table}, p_permission text)
|
|
1197
|
-
returns boolean
|
|
1198
|
-
language sql
|
|
1199
|
-
stable
|
|
1200
|
-
security definer
|
|
1201
|
-
set search_path = ''
|
|
1202
|
-
as $$
|
|
1203
|
-
select exists (select 1 from (select (p_row).*) r where ${where})
|
|
1204
|
-
$$;
|
|
1205
|
-
revoke execute on function ${fn}(${table}, text) from public, anon;
|
|
1206
|
-
grant execute on function ${fn}(${table}, text) to authenticated;`;
|
|
1207
|
-
}
|
|
1208
|
-
function resourceSql(ctx, node, branches, tables) {
|
|
1209
|
-
const name = rowsHelper(node.name);
|
|
1210
|
-
const fn = qualified(ctx, name);
|
|
1211
|
-
const table = quoteQualified(tableFor(node.name, tables));
|
|
1212
|
-
const cases = [...branches.keys()].toSorted().flatMap((key) => {
|
|
1213
|
-
const sql = permissionCase(key, branches.get(key) ?? []);
|
|
1214
|
-
return sql === void 0 ? [] : [sql];
|
|
1215
|
-
});
|
|
1216
|
-
const where = cases.length === 0 ? "false" : `case p_permission\n${cases.join("\n")}\n else false\n end`;
|
|
1217
|
-
const rows = `-- ${node.name}: ids of the rows the caller may act on with p_permission, as the table's policies decide it
|
|
1218
|
-
create or replace function ${fn}(p_permission text)
|
|
1219
|
-
returns setof text
|
|
1220
|
-
language sql
|
|
1221
|
-
stable
|
|
1222
|
-
security definer
|
|
1223
|
-
set search_path = ''
|
|
1224
|
-
as $$
|
|
1225
|
-
select ${quoteIdent$1(node.id)}::text from ${table}
|
|
1226
|
-
where ${where}
|
|
1227
|
-
$$;
|
|
1228
|
-
revoke execute on function ${fn}(text) from public, anon;
|
|
1229
|
-
grant execute on function ${fn}(text) to authenticated;
|
|
1230
|
-
${rowSql(ctx, name, table, where)}`;
|
|
1231
|
-
if (ctx.dialect === "neon") return rows;
|
|
1232
|
-
return `${rows}
|
|
1233
|
-
-- the same rows for a user the caller names, with claims it supplies; no client role may execute it
|
|
1234
|
-
${forUserSql(ctx, qualified(ctx, `${name}_for`), fn)}`;
|
|
1235
|
-
}
|
|
1236
|
-
/**
|
|
1237
|
-
* `permitted_<resource>_rows(p_permission)` for each resource `select`
|
|
1238
|
-
* names (or every resource a grant reaches with `true`): the ids of its rows
|
|
1239
|
-
* the caller may read, update or delete with that permission, from the same
|
|
1240
|
-
* compiled allows and denies the table policies use, so closure walks, link
|
|
1241
|
-
* hops, the restricted stop, `includes`, groups and `requires` all apply. A
|
|
1242
|
-
* `security definer` helper, so hand-written policies and functions call it
|
|
1243
|
-
* without restating the graph. Outside the `neon` dialect a
|
|
1244
|
-
* `permitted_<resource>_rows_for(p_user, p_permission, p_claims)` form runs it
|
|
1245
|
-
* as the named user, with the claims the caller supplies, for trusted SQL.
|
|
1246
|
-
*/
|
|
1247
|
-
function rowHelpersSql(ctx, policy, branches, select, tables, warnings) {
|
|
1248
|
-
const names = select === true ? [...new Set(branches.flatMap((branch) => branch.resource === void 0 ? [] : [branch.resource]))].toSorted() : [...new Set(select)];
|
|
1249
|
-
const called = inheritTargets(policy);
|
|
1250
|
-
for (const name of called) if (!names.includes(name)) names.push(name);
|
|
1251
|
-
const chunks = called.map((name) => stubSql(ctx, name));
|
|
1252
|
-
for (const name of names) {
|
|
1253
|
-
const node = policy.resources.get(name);
|
|
1254
|
-
if (node === void 0) throw configError(`PermDock CLI: rls.rowHelpers names '${name}', which the policy does not declare`);
|
|
1255
|
-
chunks.push(resourceSql(ctx, node, rowBranchesOf(branches, name), tables));
|
|
1256
|
-
}
|
|
1257
|
-
if (chunks.length > 0 && ctx.dialect === "neon") warnings.push("rls.rowHelpers: the neon dialect reads the subject from auth.session(), which SQL cannot set, so no permitted_<resource>_rows_for is written");
|
|
1258
|
-
return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
|
|
1259
|
-
}
|
|
1260
|
-
//#endregion
|
|
1261
1029
|
//#region src/cli/rls/generate-policies.ts
|
|
1262
1030
|
const SERVICE_TABLES_HEADER = "-- Supabase Realtime and Storage: rls.realtime and rls.storage. RLS on both tables is Supabase's.";
|
|
1263
1031
|
/**
|
|
1264
|
-
* A `realtime.messages` or `storage.objects` policy that applies only where
|
|
1265
|
-
*
|
|
1266
|
-
*
|
|
1267
|
-
* none.
|
|
1032
|
+
* A `realtime.messages` or `storage.objects` policy that applies only where the
|
|
1033
|
+
* table exists. The Realtime and Storage services create those tables, so a
|
|
1034
|
+
* local stack with the service off, and pg-delta's shadow database, have none.
|
|
1268
1035
|
*/
|
|
1269
1036
|
function guardedPolicySql(item) {
|
|
1270
1037
|
const body = policySql(item).trimEnd().split("\n").map((line) => ` ${line}`).join("\n");
|
|
@@ -1277,7 +1044,10 @@ end
|
|
|
1277
1044
|
$permdock$;
|
|
1278
1045
|
`;
|
|
1279
1046
|
}
|
|
1280
|
-
/**
|
|
1047
|
+
/**
|
|
1048
|
+
* Appends the `realtime.messages` and `storage.objects` policies, which never
|
|
1049
|
+
* get `tableSql`.
|
|
1050
|
+
*/
|
|
1281
1051
|
function withHelperTables(sql, helperTables) {
|
|
1282
1052
|
if (helperTables === void 0) return sql;
|
|
1283
1053
|
const tables = `${helperTables.trimEnd()}\n`;
|
|
@@ -1287,18 +1057,22 @@ function databaseModeWarnings(policy, { ctx }, warnings) {
|
|
|
1287
1057
|
if (ctx.authorize !== "database") return;
|
|
1288
1058
|
for (const { name } of ctx.scopes) if (policy.grants.some((grant) => grant.scope === name) && scopeTable(ctx, name) === void 0 && scopeSources(ctx, name).length === 0) warnings.push(`${name}-scoped grants read the ${name} memberships table in database mode: set rls.memberships.scopes.${name}, rls.membershipSources or supabase.hook.memberships (or --memberships for the first scope), otherwise they deny`);
|
|
1289
1059
|
}
|
|
1290
|
-
/**
|
|
1060
|
+
/**
|
|
1061
|
+
* The grants compiled into table policies, and the Realtime and Storage
|
|
1062
|
+
* policies beside them.
|
|
1063
|
+
*/
|
|
1291
1064
|
function generatePolicies(input, policy, options, built, warnings) {
|
|
1292
1065
|
const rls = input.config.rls;
|
|
1293
1066
|
const { ctx } = built;
|
|
1294
1067
|
const { fieldsMode, revokeColumns } = options;
|
|
1295
1068
|
databaseModeWarnings(policy, built, warnings);
|
|
1296
|
-
const compiled = compileGrants(policy, ctx, rls?.tables, warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
|
|
1069
|
+
const compiled = compileGrants(policy, ctx, tableNames(rls?.tables), warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
|
|
1297
1070
|
const helpersOnly = input.helpersOnly === true || rls?.helpersOnly === true;
|
|
1298
1071
|
if (helpersOnly && (input.target !== "sql" || revokeColumns)) return { refusal: "rls generate --helpers-only needs --target sql and no --revoke-columns: the table grants are hand-written, so revoke the restricted columns there" };
|
|
1299
1072
|
const views = fieldsMode === void 0 ? [] : fieldViews(policy, ctx, compiled.branches, {
|
|
1300
1073
|
revokeColumns,
|
|
1301
|
-
helpersOnly
|
|
1074
|
+
helpersOnly,
|
|
1075
|
+
mask: fieldsMode === "mask"
|
|
1302
1076
|
}, warnings);
|
|
1303
1077
|
const force = input.force === true || rls?.force === true;
|
|
1304
1078
|
if (force && views.some((view) => view.companion !== void 0)) warnings.push("--force with --revoke-columns: each <table>_visible_fields companion reads as its owner, so its owner needs BYPASSRLS or the restricted columns read as null");
|
|
@@ -1308,7 +1082,7 @@ function generatePolicies(input, policy, options, built, warnings) {
|
|
|
1308
1082
|
...policyName === void 0 ? {} : { name: policyName }
|
|
1309
1083
|
});
|
|
1310
1084
|
const actorKinds = readOnlyActorKinds(rls?.readOnlyActors);
|
|
1311
|
-
if (actorKinds !== void 0 && helpersOnly) warnings.push(`rls.readOnlyActors with --helpers-only writes no policies: add \`and (select ${qualified(ctx, ACTOR_MAY_WRITE)}())\` to each hand-written insert, update and delete policy, or a restrictive policy that calls it`);
|
|
1085
|
+
if (actorKinds !== void 0 && helpersOnly) warnings.push(`rls.readOnlyActors with --helpers-only writes no policies: add \`and (select ${qualified$1(ctx, ACTOR_MAY_WRITE)}())\` to each hand-written insert, update and delete policy, or a restrictive policy that calls it`);
|
|
1312
1086
|
const withActors = (items) => actorKinds === void 0 ? items : [...items, ...readOnlyActorPolicies(items, subjectClaimJsonSql(ctx, "act"), actorKinds)];
|
|
1313
1087
|
let tablePolicies;
|
|
1314
1088
|
try {
|
|
@@ -1341,10 +1115,10 @@ function generatePolicies(input, policy, options, built, warnings) {
|
|
|
1341
1115
|
}
|
|
1342
1116
|
//#endregion
|
|
1343
1117
|
//#region src/cli/rls/shims.ts
|
|
1344
|
-
const NAME = /^[a-z_][a-z0-9_]*$/u;
|
|
1118
|
+
const NAME$1 = /^[a-z_][a-z0-9_]*$/u;
|
|
1345
1119
|
/**
|
|
1346
|
-
* The key a legacy caller passes, mapped the way `rls migrate` maps it:
|
|
1347
|
-
*
|
|
1120
|
+
* The key a legacy caller passes, mapped the way `rls migrate` maps it: `keys`
|
|
1121
|
+
* and `renamed` exactly, then the longest `prefixes` entry.
|
|
1348
1122
|
*/
|
|
1349
1123
|
function mappedKey(config, renamed) {
|
|
1350
1124
|
const exact = {
|
|
@@ -1365,10 +1139,10 @@ function addKey(list, key) {
|
|
|
1365
1139
|
}
|
|
1366
1140
|
/**
|
|
1367
1141
|
* The grant keys behind each permission, per scope: the keys of its
|
|
1368
|
-
* unconditional allows, and every deny key. A shim cannot apply a row
|
|
1369
|
-
*
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1142
|
+
* unconditional allows, and every deny key. A shim cannot apply a row condition
|
|
1143
|
+
* or a validity window, so a conditional allow answers nothing and a
|
|
1144
|
+
* conditional deny always subtracts; the conditioned keys are listed apart. A
|
|
1145
|
+
* break-glass key answers only the break-glass read, so it is left out too.
|
|
1372
1146
|
* Rows seeded under a former key are left out; the shim maps a former key to
|
|
1373
1147
|
* the current one first.
|
|
1374
1148
|
*/
|
|
@@ -1422,9 +1196,12 @@ grant execute on function ${name}(${argTypes}) to authenticated;`;
|
|
|
1422
1196
|
function grantKeys(keys, scope, effect) {
|
|
1423
1197
|
return `pg_catalog.jsonb_array_elements_text(coalesce(${keys.map(scope)} -> (${keys.key}) -> ${quoteLiteral(effect)}, '[]'::jsonb)) g(grant_key)`;
|
|
1424
1198
|
}
|
|
1425
|
-
/**
|
|
1199
|
+
/**
|
|
1200
|
+
* The instances of `scope` the caller holds an unconditional allow of the key
|
|
1201
|
+
* in, minus those it holds a deny in.
|
|
1202
|
+
*/
|
|
1426
1203
|
function idsSql(ctx, keys, scope) {
|
|
1427
|
-
const helper = qualified(ctx, permittedIdsHelper(scope));
|
|
1204
|
+
const helper = qualified$1(ctx, permittedIdsHelper(scope));
|
|
1428
1205
|
return ` select a.id
|
|
1429
1206
|
from ${grantKeys(keys, scope, "allow")}
|
|
1430
1207
|
cross join lateral ${helper}(g.grant_key) a(id)
|
|
@@ -1434,7 +1211,7 @@ function idsSql(ctx, keys, scope) {
|
|
|
1434
1211
|
cross join lateral ${helper}(g.grant_key) d(id)`;
|
|
1435
1212
|
}
|
|
1436
1213
|
function hasSql(ctx, keys) {
|
|
1437
|
-
const has = qualified(ctx, HELPERS.has);
|
|
1214
|
+
const has = qualified$1(ctx, HELPERS.has);
|
|
1438
1215
|
return `exists (select 1 from ${grantKeys(keys, "global", "allow")} where ${has}(g.grant_key))
|
|
1439
1216
|
and not exists (select 1 from ${grantKeys(keys, "global", "deny")} where ${has}(g.grant_key))`;
|
|
1440
1217
|
}
|
|
@@ -1452,14 +1229,14 @@ function shim(ctx, config, keys, fn, helper) {
|
|
|
1452
1229
|
case "membership": {
|
|
1453
1230
|
const scope = scopeOf(ctx, helper.scope);
|
|
1454
1231
|
const type = scopeTypeOf(ctx, scope);
|
|
1455
|
-
return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified(ctx, memberIdsHelper(scope))}()), false)`);
|
|
1232
|
+
return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified$1(ctx, memberIdsHelper(scope))}()), false)`);
|
|
1456
1233
|
}
|
|
1457
1234
|
case "role": {
|
|
1458
1235
|
const scope = scopeOf(ctx, helper.scope);
|
|
1459
1236
|
const type = scopeTypeOf(ctx, scope);
|
|
1460
1237
|
const values = Object.entries(ctx.roleValues ?? {});
|
|
1461
1238
|
const role = values.length === 0 ? "p_role" : `case p_role ${values.map(([from, to]) => `when ${quoteLiteral(from)} then ${quoteLiteral(to)}`).join(" ")} else p_role end`;
|
|
1462
|
-
return shimFunction(fn, `p_id ${type}, p_role text`, `${type}, text`, "boolean", ` select coalesce(p_id in (select ${qualified(ctx, memberIdsHelper(scope))}(array[${role}])), false)`);
|
|
1239
|
+
return shimFunction(fn, `p_id ${type}, p_role text`, `${type}, text`, "boolean", ` select coalesce(p_id in (select ${qualified$1(ctx, memberIdsHelper(scope))}(array[${role}])), false)`);
|
|
1463
1240
|
}
|
|
1464
1241
|
case "global": return shimFunction(fn, "p_key text", "text", "boolean", ` select ${hasSql(ctx, keys)}`);
|
|
1465
1242
|
case "scoped": {
|
|
@@ -1476,10 +1253,10 @@ function shim(ctx, config, keys, fn, helper) {
|
|
|
1476
1253
|
* One wrapper per `rls.migrate.helpers` entry under its legacy name, so SQL
|
|
1477
1254
|
* that `rls migrate` has not rewritten yet (function bodies, views, triggers)
|
|
1478
1255
|
* answers from the PermDock helpers. A wrapper maps the legacy key to the
|
|
1479
|
-
* permission key, then to the grant keys `grants` lists for the helper's
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1482
|
-
*
|
|
1256
|
+
* permission key, then to the grant keys `grants` lists for the helper's scope,
|
|
1257
|
+
* so a permission whose grants are split by condition still answers. Each is
|
|
1258
|
+
* `security definer` with an empty `search_path`, so a caller needs `execute`
|
|
1259
|
+
* on the wrapper only, not usage on the helper schema.
|
|
1483
1260
|
*/
|
|
1484
1261
|
function shimsSql(ctx, config, shims, renamed, grants) {
|
|
1485
1262
|
const schema = shims.schema ?? "public";
|
|
@@ -1489,7 +1266,7 @@ function shimsSql(ctx, config, shims, renamed, grants) {
|
|
|
1489
1266
|
};
|
|
1490
1267
|
const chunks = [`-- permdock shims: legacy helper names over the permdock helpers; drop each once doctor reports no caller (PD056)`];
|
|
1491
1268
|
for (const [name, helper] of Object.entries(config.helpers).toSorted(byKey)) {
|
|
1492
|
-
if (!NAME.test(name)) throw configError(`PermDock CLI: unsafe rls.migrate helper name '${name}'`);
|
|
1269
|
+
if (!NAME$1.test(name)) throw configError(`PermDock CLI: unsafe rls.migrate helper name '${name}'`);
|
|
1493
1270
|
chunks.push(shim(ctx, config, keys, `${quoteIdent$1(schema)}.${name}`, helper));
|
|
1494
1271
|
}
|
|
1495
1272
|
return `${chunks.join("\n\n")}\n`;
|
|
@@ -1508,7 +1285,7 @@ const AUDITED = [
|
|
|
1508
1285
|
CUSTOM_ROLES.includes,
|
|
1509
1286
|
"user_roles"
|
|
1510
1287
|
];
|
|
1511
|
-
const IDENT = /^[a-z_][a-z0-9_$]*$/u;
|
|
1288
|
+
const IDENT$1 = /^[a-z_][a-z0-9_$]*$/u;
|
|
1512
1289
|
/** The tables `sql` creates in `schema`, by name without the schema. */
|
|
1513
1290
|
function generatedTables(sql, schema) {
|
|
1514
1291
|
const prefix = `create table if not exists ${quoteIdent$1(schema)}.`;
|
|
@@ -1560,8 +1337,8 @@ function triggersSql(triggers, schema, tables) {
|
|
|
1560
1337
|
}
|
|
1561
1338
|
/**
|
|
1562
1339
|
* `rls.audit`: one call of the audit function per generated table, which
|
|
1563
|
-
* registers the table with the application's audit module. Statements that
|
|
1564
|
-
*
|
|
1340
|
+
* registers the table with the application's audit module. Statements that run
|
|
1341
|
+
* code, so they travel with the seeds.
|
|
1565
1342
|
*/
|
|
1566
1343
|
function auditSql(audit, schema, tables) {
|
|
1567
1344
|
if (audit === void 0) return "";
|
|
@@ -1570,7 +1347,7 @@ function auditSql(audit, schema, tables) {
|
|
|
1570
1347
|
if (listed !== void 0) for (const table of listed) knownTable(table, tables, "rls.audit.tables");
|
|
1571
1348
|
const targets = listed ?? AUDITED.filter((table) => tables.includes(table));
|
|
1572
1349
|
const args = Object.entries(audit.args ?? {}).map(([name, value]) => {
|
|
1573
|
-
if (!IDENT.test(name)) throw configError(`${PREFIX}: rls.audit.args names ${JSON.stringify(name)}, which is not a lower-case parameter name`);
|
|
1350
|
+
if (!IDENT$1.test(name)) throw configError(`${PREFIX}: rls.audit.args names ${JSON.stringify(name)}, which is not a lower-case parameter name`);
|
|
1574
1351
|
return `, ${quoteIdent$1(name)} => ${quoteLiteral(value)}`;
|
|
1575
1352
|
});
|
|
1576
1353
|
if (targets.length === 0) return "";
|
|
@@ -1580,7 +1357,10 @@ ${targets.map((table) => `select ${fn}(${quoteLiteral(`${quoteIdent$1(schema)}.$
|
|
|
1580
1357
|
}
|
|
1581
1358
|
//#endregion
|
|
1582
1359
|
//#region src/cli/rls/generate-sql.ts
|
|
1583
|
-
/**
|
|
1360
|
+
/**
|
|
1361
|
+
* The checks that need the SQL context: `--backfill-out`,
|
|
1362
|
+
* `rls.anonymousSignIns`, `rls.jsonSchema`.
|
|
1363
|
+
*/
|
|
1584
1364
|
function preflight(input, { ctx }) {
|
|
1585
1365
|
const rls = input.config.rls;
|
|
1586
1366
|
const from = typeof rls?.customRoles === "object" ? rls.customRoles.from : void 0;
|
|
@@ -1633,14 +1413,15 @@ function sectionsSql(input, policy, options, built, generated, jsonSchema, warni
|
|
|
1633
1413
|
} catch (cause) {
|
|
1634
1414
|
return { refusal: describeError(cause) };
|
|
1635
1415
|
}
|
|
1636
|
-
|
|
1416
|
+
if (typeof rls?.approvals === "object" && rls.approvals.delegations === true && ctx.authorize !== "database") warnings.push("rls.approvals.delegations with --authorize jwt: permdock_is_approver cannot read a delegator's permissions from the caller's token, so in SQL a cover never satisfies a holder() approver; approvalsHandler still does");
|
|
1417
|
+
const graphed = graphSql(ctx, graph, tableNames(rls?.tables));
|
|
1637
1418
|
let rowHelpers = "";
|
|
1638
1419
|
if (rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0) try {
|
|
1639
|
-
rowHelpers = rowHelpersSql(ctx, policy, [...options.fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], rls?.tables, warnings);
|
|
1420
|
+
rowHelpers = rowHelpersSql(ctx, policy, [...options.fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], tableNames(rls?.tables), warnings);
|
|
1640
1421
|
} catch (cause) {
|
|
1641
1422
|
return { refusal: describeError(cause) };
|
|
1642
1423
|
}
|
|
1643
|
-
const breakGlass = breakGlassSql(ctx, breakGlassEntries(policy, rls?.tables));
|
|
1424
|
+
const breakGlass = breakGlassSql(ctx, breakGlassEntries(policy, tableNames(rls?.tables)));
|
|
1644
1425
|
return [
|
|
1645
1426
|
approvals,
|
|
1646
1427
|
owned === "" ? void 0 : owned,
|
|
@@ -1649,7 +1430,10 @@ function sectionsSql(input, policy, options, built, generated, jsonSchema, warni
|
|
|
1649
1430
|
rowHelpers === "" ? void 0 : rowHelpers
|
|
1650
1431
|
];
|
|
1651
1432
|
}
|
|
1652
|
-
/**
|
|
1433
|
+
/**
|
|
1434
|
+
* Whether `anon` calls a helper: `rls.anonExecute`, an anon field view or an
|
|
1435
|
+
* anon branch reading the user id.
|
|
1436
|
+
*/
|
|
1653
1437
|
function anonExecutes(input, { compiled, views }) {
|
|
1654
1438
|
return input.config.rls?.anonExecute === true || views.some((view) => view.roles.includes("anon")) || compiled.branches.some((branch) => branch.roles.includes("anon") && [
|
|
1655
1439
|
branch.access,
|
|
@@ -1657,7 +1441,10 @@ function anonExecutes(input, { compiled, views }) {
|
|
|
1657
1441
|
branch.check
|
|
1658
1442
|
].some((sql) => sql?.includes("permdock_user_id") === true));
|
|
1659
1443
|
}
|
|
1660
|
-
/**
|
|
1444
|
+
/**
|
|
1445
|
+
* Everything before the table policies: helpers, sections, shims, scaffold and
|
|
1446
|
+
* triggers.
|
|
1447
|
+
*/
|
|
1661
1448
|
function preambleSql(input, policy, options, built, generated, jsonSchema, warnings) {
|
|
1662
1449
|
const rls = input.config.rls;
|
|
1663
1450
|
const { ctx } = built;
|
|
@@ -1689,7 +1476,7 @@ function preambleSql(input, policy, options, built, generated, jsonSchema, warni
|
|
|
1689
1476
|
...rls?.trustedReaders === void 0 ? {} : { trustedReaders: rls.trustedReaders }
|
|
1690
1477
|
}),
|
|
1691
1478
|
permissionHelpersSql(ctx, grants, renamed, anonExecute),
|
|
1692
|
-
permissionHoldersSql(ctx)
|
|
1479
|
+
nonEmptyOr(permissionHoldersSql(ctx), void 0),
|
|
1693
1480
|
actorKinds === void 0 ? void 0 : actorMayWriteSql(ctx, actorKinds),
|
|
1694
1481
|
usesLiveSession ? sessionLiveHelperSql(ctx) : void 0,
|
|
1695
1482
|
...sections,
|
|
@@ -1715,6 +1502,14 @@ function preambleSql(input, policy, options, built, generated, jsonSchema, warni
|
|
|
1715
1502
|
audit
|
|
1716
1503
|
};
|
|
1717
1504
|
}
|
|
1505
|
+
function seedsBody(ctx, schema, rows) {
|
|
1506
|
+
switch (ctx.seeds) {
|
|
1507
|
+
case "function": return `select ${quoteQualified(`${schema}.seed`)}();`;
|
|
1508
|
+
case "view": return "-- rls.seeds: 'view': the role_permissions rows are in the helpers part";
|
|
1509
|
+
case void 0: return seedSql(ctx, rows);
|
|
1510
|
+
default: return ctx.seeds;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1718
1513
|
function forceWarnings({ graph }, warnings) {
|
|
1719
1514
|
for (const entry of graph.values()) {
|
|
1720
1515
|
const own = [...entry.relations].filter((name) => !("edge" in (entry.node.relations[name] ?? {})));
|
|
@@ -1722,9 +1517,9 @@ function forceWarnings({ graph }, warnings) {
|
|
|
1722
1517
|
}
|
|
1723
1518
|
}
|
|
1724
1519
|
/**
|
|
1725
|
-
* The row columns that belong to a table. With `rls.tables` set, a resource
|
|
1726
|
-
*
|
|
1727
|
-
*
|
|
1520
|
+
* The row columns that belong to a table. With `rls.tables` set, a resource it
|
|
1521
|
+
* does not name is a table only when generated policies are written for it;
|
|
1522
|
+
* with `--helpers-only` it has none, so it gets no index.
|
|
1728
1523
|
*/
|
|
1729
1524
|
function tableColumns(rowColumns, tables, helpersOnly, warnings) {
|
|
1730
1525
|
if (tables === void 0) return rowColumns;
|
|
@@ -1774,7 +1569,10 @@ function emitTarget(input, built, generated, preamble) {
|
|
|
1774
1569
|
outRel
|
|
1775
1570
|
};
|
|
1776
1571
|
}
|
|
1777
|
-
/**
|
|
1572
|
+
/**
|
|
1573
|
+
* The generated SQL for one run, before `--db-url-stdin` prunes the index
|
|
1574
|
+
* targets.
|
|
1575
|
+
*/
|
|
1778
1576
|
function generateSql(input, policy, options, built, warnings) {
|
|
1779
1577
|
const rls = input.config.rls;
|
|
1780
1578
|
const { ctx } = built;
|
|
@@ -1795,18 +1593,163 @@ function generateSql(input, policy, options, built, warnings) {
|
|
|
1795
1593
|
...generated,
|
|
1796
1594
|
...emitted,
|
|
1797
1595
|
backfill: checked.backfill,
|
|
1798
|
-
indexes: indexTargets(ctx, tableColumns(generated.compiled.rowColumns, rls?.tables, helpersOnly, warnings)),
|
|
1596
|
+
indexes: indexTargets(ctx, tableColumns(generated.compiled.rowColumns, tableNames(rls?.tables), helpersOnly, warnings)),
|
|
1799
1597
|
parts: (indexes) => ({
|
|
1800
1598
|
policies: dialectRoles(withHelperTables(emitSql(policies, "", force, views), helperTables), ctx.dialect),
|
|
1801
1599
|
declarativePolicies: dialectRoles(emitSql(policies, "", force, views), ctx.dialect),
|
|
1802
1600
|
...guardedTables === void 0 ? {} : { servicePolicies: dialectRoles(guardedTables, ctx.dialect) },
|
|
1803
1601
|
helpers: dialectRoles(emitSql([], preamble, false, helpersOnly ? views : []), ctx.dialect),
|
|
1804
|
-
seeds: `${SEEDS_MARKER} schema=${built.schema}\n${
|
|
1602
|
+
seeds: `${SEEDS_MARKER} schema=${built.schema}\n${seedsBody(ctx, built.schema, generated.compiled.rolePermissions)}\n${seedsSplit ? audit : ""}`,
|
|
1805
1603
|
indexes: `${INDEXES_MARKER}\n${indexesSql(indexes)}\n`
|
|
1806
1604
|
})
|
|
1807
1605
|
};
|
|
1808
1606
|
}
|
|
1809
1607
|
//#endregion
|
|
1608
|
+
//#region src/cli/rls/index-scan.ts
|
|
1609
|
+
/**
|
|
1610
|
+
* An index PermDock generated (`permdock_<table>_<columns>_idx`), in the
|
|
1611
|
+
* indexes part or in a migration derived from it: it never counts as serving
|
|
1612
|
+
* its own target.
|
|
1613
|
+
*/
|
|
1614
|
+
const GENERATED_INDEX = /\bcreate\s+index\s+(?:if\s+not\s+exists\s+)?"?permdock_\w*_idx"?[^;]*;/giu;
|
|
1615
|
+
const IDENT = String.raw`(?:"(?:[^"]|"")+"|[A-Za-z_][\w$]*)`;
|
|
1616
|
+
const NAME = String.raw`${IDENT}(?:\s*\.\s*${IDENT})?`;
|
|
1617
|
+
const CREATE_INDEX = new RegExp(String.raw`\bcreate\s+(?:unique\s+)?index\s+(?:concurrently\s+)?(?:if\s+not\s+exists\s+)?(?:${IDENT}\s+)?on\s+(?:only\s+)?(${NAME})\s*(?:using\s+\w+\s*)?\(`, "giu");
|
|
1618
|
+
const ADD_KEY = new RegExp(String.raw`\balter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?(${NAME})\s+add\s+(?:constraint\s+${IDENT}\s+)?(?:unique|primary\s+key)\s*(?:nulls\s+not\s+distinct\s*)?\(`, "giu");
|
|
1619
|
+
const CREATE_TABLE = new RegExp(String.raw`\bcreate\s+(?:unlogged\s+|temp(?:orary)?\s+)?table\s+(?:if\s+not\s+exists\s+)?(${NAME})\s*\(`, "giu");
|
|
1620
|
+
const TABLE_NAME = new RegExp(String.raw`\bcreate\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(${NAME})`, "giu");
|
|
1621
|
+
const TABLE_KEY = new RegExp(String.raw`^(?:constraint\s+${IDENT}\s+)?(?:unique|primary\s+key)\s*(?:nulls\s+not\s+distinct\s*)?\(`, "iu");
|
|
1622
|
+
const COLUMN_KEY = new RegExp(String.raw`^(${IDENT})\s+[^,]*?\b(?:primary\s+key|unique)\b`, "iu");
|
|
1623
|
+
const NOT_A_COLUMN = /^(?:constraint|check|foreign|exclude|like)\b/iu;
|
|
1624
|
+
function bare(part) {
|
|
1625
|
+
const trimmed = part.trim();
|
|
1626
|
+
return trimmed.startsWith("\"") ? trimmed.slice(1, -1).replaceAll("\"\"", "\"") : trimmed.toLowerCase();
|
|
1627
|
+
}
|
|
1628
|
+
/** `schema.table`, with `public` for an unqualified name. */
|
|
1629
|
+
function qualified(raw) {
|
|
1630
|
+
const parts = raw.split(".").map(bare);
|
|
1631
|
+
return parts.length === 1 ? `public.${parts.join("")}` : parts.join(".");
|
|
1632
|
+
}
|
|
1633
|
+
function stripComments(sql) {
|
|
1634
|
+
return sql.replaceAll(/\/\*[\s\S]*?\*\//gu, " ").replaceAll(/--[^\n]*/gu, " ");
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* The text between the `(` that ends `match` and its closing `)`, and the index
|
|
1638
|
+
* after it.
|
|
1639
|
+
*/
|
|
1640
|
+
function group(text, open) {
|
|
1641
|
+
let depth = 1;
|
|
1642
|
+
for (let index = open; index < text.length; index += 1) {
|
|
1643
|
+
const char = text[index];
|
|
1644
|
+
if (char === "'" || char === "\"") {
|
|
1645
|
+
const close = text.indexOf(char, index + 1);
|
|
1646
|
+
if (close === -1) return;
|
|
1647
|
+
index = close;
|
|
1648
|
+
} else if (char === "(") depth += 1;
|
|
1649
|
+
else if (char === ")") {
|
|
1650
|
+
depth -= 1;
|
|
1651
|
+
if (depth === 0) return {
|
|
1652
|
+
body: text.slice(open, index),
|
|
1653
|
+
end: index + 1
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function splitTop(body) {
|
|
1659
|
+
const items = [];
|
|
1660
|
+
let depth = 0;
|
|
1661
|
+
let from = 0;
|
|
1662
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
1663
|
+
const char = body[index];
|
|
1664
|
+
if (char === "'" || char === "\"") index = Math.max(body.indexOf(char, index + 1), index);
|
|
1665
|
+
else if (char === "(") depth += 1;
|
|
1666
|
+
else if (char === ")") depth -= 1;
|
|
1667
|
+
else if (char === "," && depth === 0) {
|
|
1668
|
+
items.push(body.slice(from, index));
|
|
1669
|
+
from = index + 1;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
items.push(body.slice(from));
|
|
1673
|
+
return items.map((item) => item.trim());
|
|
1674
|
+
}
|
|
1675
|
+
/** The first capture group of a match; every pattern here captures it. */
|
|
1676
|
+
function captured(match) {
|
|
1677
|
+
return match.slice(1, 2).join("");
|
|
1678
|
+
}
|
|
1679
|
+
/** The index just past a match. */
|
|
1680
|
+
function endOf(match) {
|
|
1681
|
+
return match.index + match[0].length;
|
|
1682
|
+
}
|
|
1683
|
+
/** The plain key columns of an index column list, cut at the first expression. */
|
|
1684
|
+
function keyColumns(body) {
|
|
1685
|
+
const columns = [];
|
|
1686
|
+
for (const item of splitTop(body)) {
|
|
1687
|
+
const match = new RegExp(`^(${IDENT})(?:\\s|$)`, "u").exec(item);
|
|
1688
|
+
if (match === null || item.startsWith("(")) break;
|
|
1689
|
+
columns.push(bare(captured(match)));
|
|
1690
|
+
}
|
|
1691
|
+
return columns;
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Key columns of each index, unique constraint and primary key the SQL creates,
|
|
1695
|
+
* per `schema.table`.
|
|
1696
|
+
*/
|
|
1697
|
+
function indexKeysFromSql(sql) {
|
|
1698
|
+
const text = stripComments(sql);
|
|
1699
|
+
const keys = {};
|
|
1700
|
+
const add = (table, columns) => {
|
|
1701
|
+
if (columns.length > 0) (keys[qualified(table)] ??= []).push(columns);
|
|
1702
|
+
};
|
|
1703
|
+
for (const pattern of [CREATE_INDEX, ADD_KEY]) for (const match of text.matchAll(pattern)) {
|
|
1704
|
+
const found = group(text, endOf(match));
|
|
1705
|
+
if (found !== void 0) add(captured(match), keyColumns(found.body));
|
|
1706
|
+
}
|
|
1707
|
+
for (const match of text.matchAll(CREATE_TABLE)) {
|
|
1708
|
+
const found = group(text, endOf(match));
|
|
1709
|
+
if (found === void 0) continue;
|
|
1710
|
+
for (const item of splitTop(found.body)) if (TABLE_KEY.test(item)) {
|
|
1711
|
+
const inner = group(item, item.indexOf("(") + 1);
|
|
1712
|
+
if (inner !== void 0) add(captured(match), keyColumns(inner.body));
|
|
1713
|
+
} else if (!NOT_A_COLUMN.test(item)) {
|
|
1714
|
+
const column = COLUMN_KEY.exec(item)?.[1];
|
|
1715
|
+
if (column !== void 0) add(captured(match), [bare(column)]);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
return keys;
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* The index keys of the SQL files under the migration and schema folders of
|
|
1722
|
+
* `cwd`.
|
|
1723
|
+
*/
|
|
1724
|
+
function indexKeysFromProject(cwd) {
|
|
1725
|
+
const all = {};
|
|
1726
|
+
for (const file of sqlFiles(cwd, [...MIGRATION_DIRS])) {
|
|
1727
|
+
const keys = indexKeysFromSql(readFileSync(file, "utf8").replaceAll(GENERATED_INDEX, ""));
|
|
1728
|
+
for (const [table, list] of Object.entries(keys)) (all[table] ??= []).push(...list);
|
|
1729
|
+
}
|
|
1730
|
+
return all;
|
|
1731
|
+
}
|
|
1732
|
+
/** Every `schema.table` the SQL creates, temporary tables left out. */
|
|
1733
|
+
function tablesFromSql(sql) {
|
|
1734
|
+
return new Set([...stripComments(sql).matchAll(TABLE_NAME)].map((match) => qualified(captured(match))));
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Every `schema.table` the SQL files under the migration and schema folders of
|
|
1738
|
+
* `cwd` create.
|
|
1739
|
+
*/
|
|
1740
|
+
function tablesFromProject(cwd) {
|
|
1741
|
+
const all = /* @__PURE__ */ new Set();
|
|
1742
|
+
for (const file of sqlFiles(cwd, [...MIGRATION_DIRS])) for (const table of tablesFromSql(readFileSync(file, "utf8"))) all.add(table);
|
|
1743
|
+
return all;
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* The targets no index in `keys` leads with: its key columns start with the
|
|
1747
|
+
* target's, as `pruneIndexTargets` reads a database.
|
|
1748
|
+
*/
|
|
1749
|
+
function withoutServedTargets(targets, keys) {
|
|
1750
|
+
return targets.filter((target) => !(keys[target.table] ?? []).some((key) => target.columns.every((column, at) => key[at] === column)));
|
|
1751
|
+
}
|
|
1752
|
+
//#endregion
|
|
1810
1753
|
//#region src/cli/rls/generate.ts
|
|
1811
1754
|
function loadPolicy(cwd, config, from) {
|
|
1812
1755
|
const policyPath = from === "drizzle" ? config.policy : from ?? config.policy;
|
|
@@ -1814,8 +1757,8 @@ function loadPolicy(cwd, config, from) {
|
|
|
1814
1757
|
}
|
|
1815
1758
|
const PROJECT_COMMAND = "permdock rls generate";
|
|
1816
1759
|
/**
|
|
1817
|
-
* The policy `policyFromCatalog` rebuilds from the project's pushed catalog
|
|
1818
|
-
*
|
|
1760
|
+
* The policy `policyFromCatalog` rebuilds from the project's pushed catalog and
|
|
1761
|
+
* policy; a project with no pushed policy fails with `policy-not-published`.
|
|
1819
1762
|
*/
|
|
1820
1763
|
async function projectPolicyOf(input, project) {
|
|
1821
1764
|
const fetched = await fetchProjectPolicy({
|
|
@@ -1884,8 +1827,19 @@ async function runRlsGenerate(input) {
|
|
|
1884
1827
|
const { ctx } = built;
|
|
1885
1828
|
const { text, migration, migrationRel, helpersOnly } = generated;
|
|
1886
1829
|
let indexes = generated.indexes;
|
|
1830
|
+
const bindings = tableBindings(generated.compiled.rowColumns.map((item) => item.resource), tableNames(input.config.rls?.tables), helpersOnly);
|
|
1831
|
+
if (input.config.rls?.tables === void 0) warnings.push(...bindingNotes(bindings, input.db !== void 0));
|
|
1832
|
+
warnings.push(...triggerNotes(text));
|
|
1833
|
+
if (ctx.seeds === "function") warnings.push(`run select ${built.schema}.seed(); after each deploy`);
|
|
1834
|
+
if (input.db === void 0) {
|
|
1835
|
+
indexes = withoutServedTargets(indexes, indexKeysFromProject(input.cwd));
|
|
1836
|
+
warnings.push(...unknownTableNotes(bindings, tablesFromProject(input.cwd)));
|
|
1837
|
+
}
|
|
1887
1838
|
if (input.db !== void 0) try {
|
|
1888
|
-
const
|
|
1839
|
+
const facts = await readTableIndexFacts(input.db, [.../* @__PURE__ */ new Set([...indexes.map((target) => target.table), ...bindings.map((binding) => binding.table)])], input.connect);
|
|
1840
|
+
const missing = missingBindings(bindings, facts.columns);
|
|
1841
|
+
if (missing.length > 0) return refused(`rls generate: ${missing.map((binding) => `resource ${binding.resource} is bound to ${binding.table}, which the database does not have`).join("; ")}; name the table in rls.tables, or set rls.tables.<resource>: false for a resource with no table`);
|
|
1842
|
+
const pruned = pruneIndexTargets(indexes, facts);
|
|
1889
1843
|
indexes = pruned.targets;
|
|
1890
1844
|
warnings.push(...pruned.warnings);
|
|
1891
1845
|
} catch (cause) {
|
|
@@ -1907,10 +1861,19 @@ async function runRlsGenerate(input) {
|
|
|
1907
1861
|
keys: {
|
|
1908
1862
|
permissions: listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key),
|
|
1909
1863
|
rowConditions: [...policyRowConditionKeys(policy)],
|
|
1864
|
+
levels: levelKeys(policy),
|
|
1910
1865
|
renamed: Object.fromEntries(renamedKeys(policy.vocabulary.permissions))
|
|
1911
1866
|
},
|
|
1912
1867
|
schema: built.schema,
|
|
1913
1868
|
helpersOnly,
|
|
1869
|
+
explain: explainRls({
|
|
1870
|
+
config: input.config,
|
|
1871
|
+
ctx,
|
|
1872
|
+
schema: built.schema,
|
|
1873
|
+
authorize: built.authorize,
|
|
1874
|
+
generated,
|
|
1875
|
+
text
|
|
1876
|
+
}),
|
|
1914
1877
|
...extras
|
|
1915
1878
|
};
|
|
1916
1879
|
const planned = outputFiles({
|
|
@@ -1974,7 +1937,10 @@ async function runRlsGenerate(input) {
|
|
|
1974
1937
|
//#endregion
|
|
1975
1938
|
//#region src/cli/rls/introspect.ts
|
|
1976
1939
|
const ROLES = ["anon", "authenticated"];
|
|
1977
|
-
/**
|
|
1940
|
+
/**
|
|
1941
|
+
* Neon's `anonymous` is the role `rls generate` writes as `anon` for the other
|
|
1942
|
+
* dialects.
|
|
1943
|
+
*/
|
|
1978
1944
|
function liveRole(role) {
|
|
1979
1945
|
return role === "anonymous" ? "anon" : role;
|
|
1980
1946
|
}
|
|
@@ -1990,14 +1956,107 @@ function normalizeTableName(table) {
|
|
|
1990
1956
|
return parts.length === 1 ? `public.${parts[0] ?? ""}` : parts.join(".");
|
|
1991
1957
|
}
|
|
1992
1958
|
const FUNCTION_HEADER = /create or replace function ("?)([A-Za-z_][\w$]*)\1\.("?)([A-Za-z_][\w$]*)\3\s*\(([\s\S]*?)\nas \$/gu;
|
|
1959
|
+
const TYPE_ALIASES = {
|
|
1960
|
+
int: "integer",
|
|
1961
|
+
int4: "integer",
|
|
1962
|
+
int2: "smallint",
|
|
1963
|
+
int8: "bigint",
|
|
1964
|
+
bool: "boolean",
|
|
1965
|
+
varchar: "character varying",
|
|
1966
|
+
char: "character",
|
|
1967
|
+
float4: "real",
|
|
1968
|
+
float8: "double precision",
|
|
1969
|
+
timestamp: "timestamp without time zone",
|
|
1970
|
+
timestamptz: "timestamp with time zone",
|
|
1971
|
+
time: "time without time zone",
|
|
1972
|
+
timetz: "time with time zone"
|
|
1973
|
+
};
|
|
1974
|
+
/**
|
|
1975
|
+
* A parameter type as `oidvectortypes` prints it: lower case, no quotes, no
|
|
1976
|
+
* schema, no type modifier, aliases spelled out. A `%type` reference is `*`,
|
|
1977
|
+
* which matches any type.
|
|
1978
|
+
*/
|
|
1979
|
+
function normalizeParameterType(type) {
|
|
1980
|
+
const text = type.trim().toLowerCase().replaceAll("\"", "");
|
|
1981
|
+
if (text.endsWith("%type")) return "*";
|
|
1982
|
+
const array = /(?:\[\d*\])+$/u.exec(text)?.[0] ?? "";
|
|
1983
|
+
const base = text.slice(0, text.length - array.length).replaceAll(/\([^)]*\)/gu, "").replaceAll(/\s+/gu, " ").trim();
|
|
1984
|
+
const unqualified = /^[\w$]+(?:\.[\w$]+)+$/u.test(base) ? base.split(".").at(-1) ?? base : base;
|
|
1985
|
+
return `${TYPE_ALIASES[unqualified] ?? unqualified}${"[]".repeat(array.split("[").length - 1)}`;
|
|
1986
|
+
}
|
|
1987
|
+
function splitTopLevel(text) {
|
|
1988
|
+
const parts = [];
|
|
1989
|
+
let depth = 0;
|
|
1990
|
+
let quoted = false;
|
|
1991
|
+
let start = 0;
|
|
1992
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1993
|
+
const char = text[index];
|
|
1994
|
+
if (char === "'") quoted = !quoted;
|
|
1995
|
+
else if (!quoted && char === "(") depth += 1;
|
|
1996
|
+
else if (!quoted && char === ")") depth -= 1;
|
|
1997
|
+
else if (!quoted && depth === 0 && char === ",") {
|
|
1998
|
+
parts.push(text.slice(start, index));
|
|
1999
|
+
start = index + 1;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
parts.push(text.slice(start));
|
|
2003
|
+
return parts.map((part) => part.trim()).filter((part) => part !== "");
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* The input parameter types of a function header that starts after its opening
|
|
2007
|
+
* parenthesis. Every generated parameter is named, so the first word of a
|
|
2008
|
+
* parameter with more than one is its name.
|
|
2009
|
+
*/
|
|
2010
|
+
function parameterTypes(header) {
|
|
2011
|
+
let depth = 0;
|
|
2012
|
+
let end = header.length;
|
|
2013
|
+
for (let index = 0; index < header.length; index += 1) {
|
|
2014
|
+
const char = header[index];
|
|
2015
|
+
if (char === "(") depth += 1;
|
|
2016
|
+
else if (char === ")") {
|
|
2017
|
+
if (depth === 0) {
|
|
2018
|
+
end = index;
|
|
2019
|
+
break;
|
|
2020
|
+
}
|
|
2021
|
+
depth -= 1;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
return splitTopLevel(header.slice(0, end)).flatMap((parameter) => {
|
|
2025
|
+
const words = parameter.replace(/\s+(?:default\b|=)[\s\S]*$/iu, "").split(/\s+/u);
|
|
2026
|
+
const mode = words[0]?.toLowerCase();
|
|
2027
|
+
if (mode === "out") return [];
|
|
2028
|
+
const rest = mode === "in" || mode === "inout" || mode === "variadic" ? words.slice(1) : words;
|
|
2029
|
+
return [normalizeParameterType((rest.length > 1 ? rest.slice(1) : rest).join(" "))];
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
/** `schema.name(type, type)`, the key a helper is checked by. */
|
|
2033
|
+
function signatureOf(name, types) {
|
|
2034
|
+
return `${name}(${types.join(", ")})`;
|
|
2035
|
+
}
|
|
1993
2036
|
function helpersOf(sql) {
|
|
1994
2037
|
const found = /* @__PURE__ */ new Set();
|
|
1995
2038
|
for (const match of sql.matchAll(FUNCTION_HEADER)) {
|
|
1996
|
-
const [header = "", , schema = "", , name = ""] = match;
|
|
1997
|
-
if (/\bsecurity definer\b/u.test(header)) found.add(`${schema}.${name}
|
|
2039
|
+
const [header = "", , schema = "", , name = "", rest = ""] = match;
|
|
2040
|
+
if (/\bsecurity definer\b/u.test(header)) found.add(signatureOf(`${schema}.${name}`, parameterTypes(rest)));
|
|
1998
2041
|
}
|
|
1999
2042
|
return [...found];
|
|
2000
2043
|
}
|
|
2044
|
+
function nameOfSignature(signature) {
|
|
2045
|
+
return signature.slice(0, signature.indexOf("("));
|
|
2046
|
+
}
|
|
2047
|
+
function typesOfSignature(signature) {
|
|
2048
|
+
return splitTopLevel(signature.slice(signature.indexOf("(") + 1, signature.lastIndexOf(")"))).map(normalizeParameterType);
|
|
2049
|
+
}
|
|
2050
|
+
/** The actual overload `expected` names; `*` matches any parameter type. */
|
|
2051
|
+
function matchHelper(expected, actual) {
|
|
2052
|
+
if (Object.hasOwn(actual, expected)) return actual[expected];
|
|
2053
|
+
const name = nameOfSignature(expected);
|
|
2054
|
+
const want = typesOfSignature(expected);
|
|
2055
|
+
for (const [signature, helper] of Object.entries(actual)) {
|
|
2056
|
+
const have = typesOfSignature(signature);
|
|
2057
|
+
if (nameOfSignature(signature) === name && have.length === want.length && want.every((type, index) => type === "*" || type === have[index])) return helper;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2001
2060
|
function expectedRls(policies, sql, indexes = []) {
|
|
2002
2061
|
const tables = [...new Set(policies.map((item) => normalizeTableName(item.table)))];
|
|
2003
2062
|
const grants = {};
|
|
@@ -2027,14 +2086,14 @@ function expectedRls(policies, sql, indexes = []) {
|
|
|
2027
2086
|
};
|
|
2028
2087
|
}
|
|
2029
2088
|
/**
|
|
2030
|
-
* One warning per index target no index starts with. A missing index slows
|
|
2031
|
-
*
|
|
2089
|
+
* One warning per index target no index starts with. A missing index slows the
|
|
2090
|
+
* policy down but grants nothing, so these do not fail `verify`.
|
|
2032
2091
|
*/
|
|
2033
2092
|
function missingIndexes(expected, actual) {
|
|
2034
2093
|
return expected.indexes.flatMap((target) => {
|
|
2035
2094
|
const column = target.columns[0];
|
|
2036
2095
|
const columns = actual.columns?.[target.table];
|
|
2037
|
-
return column === void 0 || columns !== void 0 && !columns.includes(column) || actual.leadingColumns[target.table]?.includes(column) === true ? [] : [
|
|
2096
|
+
return column === void 0 || columns !== void 0 && !columns.includes(column) || actual.leadingColumns[target.table]?.includes(column) === true ? [] : [rlsFinding("index-missing", target.table, column, `${target.table}: no index starts with ${column}, which the policies or helpers filter on; add it, or write it with rls generate --split ...,indexes`, "warning")];
|
|
2038
2097
|
});
|
|
2039
2098
|
}
|
|
2040
2099
|
function sameList(left, right) {
|
|
@@ -2042,42 +2101,45 @@ function sameList(left, right) {
|
|
|
2042
2101
|
const b = right.toSorted();
|
|
2043
2102
|
return a.length === b.length && a.every((item, index) => item === b[index]);
|
|
2044
2103
|
}
|
|
2045
|
-
/**
|
|
2104
|
+
/**
|
|
2105
|
+
* One finding per difference; policy expressions are not compared, since
|
|
2106
|
+
* Postgres rewrites their text.
|
|
2107
|
+
*/
|
|
2046
2108
|
function diffRls(expected, actual, options = {}) {
|
|
2047
2109
|
const out = [];
|
|
2048
2110
|
const found = new Map(actual.policies.map((item) => [`${item.table}\u0000${item.name}`, item]));
|
|
2049
2111
|
for (const want of expected.policies) {
|
|
2050
2112
|
const have = found.get(`${want.table}\u0000${want.name}`);
|
|
2051
2113
|
if (have === void 0) {
|
|
2052
|
-
out.push(`${want.table}: policy ${want.name} is missing`);
|
|
2114
|
+
out.push(rlsFinding("policy-missing", want.table, want.name, `${want.table}: policy ${want.name} is missing`));
|
|
2053
2115
|
continue;
|
|
2054
2116
|
}
|
|
2055
|
-
if (have.command !== want.command) out.push(`${want.table}: policy ${want.name} is for ${have.command}, expected ${want.command}`);
|
|
2056
|
-
if (have.permissive !== want.permissive) out.push(`${want.table}: policy ${want.name} is ${have.permissive ? "permissive" : "restrictive"}, expected ${want.permissive ? "permissive" : "restrictive"}`);
|
|
2057
|
-
if (!sameList(have.roles, want.roles)) out.push(`${want.table}: policy ${want.name} applies to ${have.roles.join(", ")}, expected ${want.roles.join(", ")}`);
|
|
2117
|
+
if (have.command !== want.command) out.push(rlsFinding("policy-command", want.table, want.name, `${want.table}: policy ${want.name} is for ${have.command}, expected ${want.command}`));
|
|
2118
|
+
if (have.permissive !== want.permissive) out.push(rlsFinding("policy-mode", want.table, want.name, `${want.table}: policy ${want.name} is ${have.permissive ? "permissive" : "restrictive"}, expected ${want.permissive ? "permissive" : "restrictive"}`));
|
|
2119
|
+
if (!sameList(have.roles, want.roles)) out.push(rlsFinding("policy-roles", want.table, want.name, `${want.table}: policy ${want.name} applies to ${have.roles.join(", ")}, expected ${want.roles.join(", ")}`));
|
|
2058
2120
|
}
|
|
2059
2121
|
const wanted = new Set(expected.policies.map((item) => `${item.table}\u0000${item.name}`));
|
|
2060
|
-
for (const have of actual.policies) if (expected.tables.includes(have.table) && !wanted.has(`${have.table}\u0000${have.name}`)) out.push(`${have.table}: policy ${have.name} is not generated (${have.permissive ? "permissive" : "restrictive"} ${have.command}); a permissive one widens access`);
|
|
2122
|
+
for (const have of actual.policies) if (expected.tables.includes(have.table) && !wanted.has(`${have.table}\u0000${have.name}`)) out.push(rlsFinding("policy-extra", have.table, have.name, `${have.table}: policy ${have.name} is not generated (${have.permissive ? "permissive" : "restrictive"} ${have.command}); a permissive one widens access`));
|
|
2061
2123
|
for (const table of expected.tables) {
|
|
2062
2124
|
if (actual.rlsEnabled[table] !== true) {
|
|
2063
|
-
out.push(actual.rlsEnabled[table] === void 0 ? `${table}: table is missing` : `${table}: row level security is disabled`);
|
|
2125
|
+
out.push(actual.rlsEnabled[table] === void 0 ? rlsFinding("table-missing", table, "table", `${table}: table is missing`) : rlsFinding("rls-disabled", table, "table", `${table}: row level security is disabled`));
|
|
2064
2126
|
continue;
|
|
2065
2127
|
}
|
|
2066
2128
|
for (const role of ROLES) {
|
|
2067
2129
|
const want = expected.grants[table]?.[role] ?? [];
|
|
2068
2130
|
const have = actual.grants[table]?.[role] ?? [];
|
|
2069
|
-
for (const privilege of want) if (!have.includes(privilege) && !(privilege === "select" && options.columnGrants === true)) out.push(`${table}: ${role} lacks ${privilege}, so the policy answers 42501 instead of filtering`);
|
|
2070
|
-
for (const privilege of have) if (!want.includes(privilege)) out.push(`${table}: ${role} holds ${privilege}, which no generated policy allows`);
|
|
2131
|
+
for (const privilege of want) if (!have.includes(privilege) && !(privilege === "select" && options.columnGrants === true)) out.push(rlsFinding("grant-missing", table, `${role}.${privilege}`, `${table}: ${role} lacks ${privilege}, so the policy answers 42501 instead of filtering`));
|
|
2132
|
+
for (const privilege of have) if (!want.includes(privilege)) out.push(rlsFinding("grant-extra", table, `${role}.${privilege}`, `${table}: ${role} holds ${privilege}, which no generated policy allows`));
|
|
2071
2133
|
}
|
|
2072
2134
|
}
|
|
2073
2135
|
for (const helper of expected.helpers) {
|
|
2074
|
-
const have = actual.helpers
|
|
2136
|
+
const have = matchHelper(helper, actual.helpers);
|
|
2075
2137
|
if (have === void 0) {
|
|
2076
|
-
out.push(`${helper}: helper is missing`);
|
|
2138
|
+
out.push(rlsFinding("helper-missing", helper, "helper", `${helper}: helper is missing`));
|
|
2077
2139
|
continue;
|
|
2078
2140
|
}
|
|
2079
|
-
if (!have.securityDefiner) out.push(`${helper}: helper is not security definer`);
|
|
2080
|
-
if (!have.emptySearchPath) out.push(`${helper}: helper does not set search_path = ''`);
|
|
2141
|
+
if (!have.securityDefiner) out.push(rlsFinding("helper-definer", helper, "helper", `${helper}: helper is not security definer`));
|
|
2142
|
+
if (!have.emptySearchPath) out.push(rlsFinding("helper-search-path", helper, "helper", `${helper}: helper does not set search_path = ''`));
|
|
2081
2143
|
}
|
|
2082
2144
|
return out;
|
|
2083
2145
|
}
|
|
@@ -2093,25 +2155,22 @@ from information_schema.role_table_grants
|
|
|
2093
2155
|
where grantee in ('anon', 'anonymous', 'authenticated')
|
|
2094
2156
|
and lower(privilege_type) in ('select', 'insert', 'update', 'delete')
|
|
2095
2157
|
and table_schema || '.' || table_name = any($1::text[])`;
|
|
2096
|
-
const HELPERS_SQL = `select n.nspname || '.' || p.proname as target, p.prosecdef as definer,
|
|
2158
|
+
const HELPERS_SQL = `select n.nspname || '.' || p.proname || '(' || oidvectortypes(p.proargtypes) || ')' as target, p.prosecdef as definer,
|
|
2097
2159
|
coalesce(array_to_string(p.proconfig, ','), '') as config
|
|
2098
2160
|
from pg_proc p
|
|
2099
2161
|
join pg_namespace n on n.oid = p.pronamespace
|
|
2100
2162
|
where n.nspname || '.' || p.proname = any($1::text[])`;
|
|
2101
|
-
const INDEXES_SQL = `select n.nspname || '.' || c.relname as target, a.attname as leading
|
|
2102
|
-
from pg_index i
|
|
2103
|
-
join pg_class c on c.oid = i.indrelid
|
|
2104
|
-
join pg_namespace n on n.oid = c.relnamespace
|
|
2105
|
-
join pg_attribute a on a.attrelid = i.indrelid and a.attnum = i.indkey[0]
|
|
2106
|
-
where i.indpred is null and n.nspname || '.' || c.relname = any($1::text[])`;
|
|
2107
2163
|
function commandOf(cmd) {
|
|
2108
|
-
const text =
|
|
2164
|
+
const text = typeof cmd === "string" ? cmd.toLowerCase() : "";
|
|
2109
2165
|
return text === "*" ? "all" : text;
|
|
2110
2166
|
}
|
|
2111
2167
|
function connectIntrospect(db) {
|
|
2112
2168
|
return connectPgPool(db, "permdock rls verify --introspect");
|
|
2113
2169
|
}
|
|
2114
|
-
/**
|
|
2170
|
+
/**
|
|
2171
|
+
* Reads the catalogs for the tables and helpers `expected` names
|
|
2172
|
+
* (postgres-meta's queries, trimmed).
|
|
2173
|
+
*/
|
|
2115
2174
|
async function introspectRls(db, expected, connect = connectIntrospect) {
|
|
2116
2175
|
const client = await connect(db);
|
|
2117
2176
|
try {
|
|
@@ -2121,14 +2180,17 @@ async function introspectRls(db, expected, connect = connectIntrospect) {
|
|
|
2121
2180
|
client.query(POLICIES_SQL, [tables]),
|
|
2122
2181
|
client.query(TABLES_SQL, [tables]),
|
|
2123
2182
|
client.query(GRANTS_SQL, [tables]),
|
|
2124
|
-
client.query(HELPERS_SQL, [[...expected.helpers]]),
|
|
2125
|
-
client.query(
|
|
2183
|
+
client.query(HELPERS_SQL, [[...new Set(expected.helpers.map(nameOfSignature))]]),
|
|
2184
|
+
client.query(INDEX_COLUMNS_SQL, [indexed]),
|
|
2126
2185
|
client.query(COLUMNS_SQL, [indexed])
|
|
2127
2186
|
]);
|
|
2128
2187
|
const columnsOf = {};
|
|
2129
2188
|
for (const row of columns.rows) (columnsOf[String(row["target"])] ??= []).push(String(row["name"]));
|
|
2130
2189
|
const leadingColumns = {};
|
|
2131
|
-
for (const row of indexes.rows)
|
|
2190
|
+
for (const row of indexes.rows) {
|
|
2191
|
+
const lead = (Array.isArray(row["columns"]) ? row["columns"] : []).at(0);
|
|
2192
|
+
if (typeof lead === "string") (leadingColumns[String(row["target"])] ??= []).push(lead);
|
|
2193
|
+
}
|
|
2132
2194
|
const byTable = {};
|
|
2133
2195
|
for (const row of grants.rows) {
|
|
2134
2196
|
const table = String(row["target"]);
|
|
@@ -2146,7 +2208,7 @@ async function introspectRls(db, expected, connect = connectIntrospect) {
|
|
|
2146
2208
|
})),
|
|
2147
2209
|
rlsEnabled: Object.fromEntries(enabled.rows.map((row) => [String(row["target"]), row["enabled"] === true])),
|
|
2148
2210
|
grants: byTable,
|
|
2149
|
-
helpers: Object.fromEntries(helpers.rows.map((row) => [String(row["target"]), {
|
|
2211
|
+
helpers: Object.fromEntries(helpers.rows.map((row) => [signatureOf(nameOfSignature(String(row["target"])), typesOfSignature(String(row["target"]))), {
|
|
2150
2212
|
securityDefiner: row["definer"] === true,
|
|
2151
2213
|
emptySearchPath: String(row["config"]).split(",").some((item) => item === "search_path=\"\"" || item === "search_path=''")
|
|
2152
2214
|
}])),
|
|
@@ -2163,10 +2225,10 @@ const WRITES = [
|
|
|
2163
2225
|
"delete"
|
|
2164
2226
|
];
|
|
2165
2227
|
/**
|
|
2166
|
-
* Each table and write command whose permissive policies reach
|
|
2167
|
-
*
|
|
2168
|
-
*
|
|
2169
|
-
*
|
|
2228
|
+
* Each table and write command whose permissive policies reach `authenticated`
|
|
2229
|
+
* (or `public`) without the read-only actor check: no restrictive policy for
|
|
2230
|
+
* the command calls `permdock_actor_may_write()`, and some permissive one does
|
|
2231
|
+
* not.
|
|
2170
2232
|
*/
|
|
2171
2233
|
function unguardedWrites(policies, fn) {
|
|
2172
2234
|
const out = [];
|
|
@@ -2176,7 +2238,7 @@ function unguardedWrites(policies, fn) {
|
|
|
2176
2238
|
const permissive = applying.filter((policy) => policy.restrictive !== true && (policy.roles ?? []).some((role) => role === "authenticated" || role === "public"));
|
|
2177
2239
|
if (permissive.length === 0) continue;
|
|
2178
2240
|
const calls = (expression) => expression.includes(fn);
|
|
2179
|
-
if (!(applying.some((policy) => policy.restrictive === true && calls(policy.expression)) || permissive.every((policy) => calls(policy.expression)))) out.push(`${table}: ${command} policies for authenticated do not call ${fn}(), so a read-only support or impersonation session can write (rls.readOnlyActors)`);
|
|
2241
|
+
if (!(applying.some((policy) => policy.restrictive === true && calls(policy.expression)) || permissive.every((policy) => calls(policy.expression)))) out.push(rlsFinding("write-unguarded", table, command, `${table}: ${command} policies for authenticated do not call ${fn}(), so a read-only support or impersonation session can write (rls.readOnlyActors)`));
|
|
2180
2242
|
}
|
|
2181
2243
|
return out;
|
|
2182
2244
|
}
|
|
@@ -2205,26 +2267,40 @@ function inScope(table) {
|
|
|
2205
2267
|
function seedLine(row) {
|
|
2206
2268
|
return `${row.role} ${row.effect} ${row.grantKey} on ${row.scope}`;
|
|
2207
2269
|
}
|
|
2208
|
-
|
|
2270
|
+
function seedKey(line) {
|
|
2271
|
+
return line.replace(" on ", " ").replaceAll(" ", "/");
|
|
2272
|
+
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Drift is an error; `info` names RLS tables whose policies call no PermDock
|
|
2275
|
+
* helper.
|
|
2276
|
+
*/
|
|
2209
2277
|
function diffMixed(expected, actual) {
|
|
2210
2278
|
const drift = [];
|
|
2279
|
+
const seeds = `${expected.schema}.role_permissions`;
|
|
2211
2280
|
const want = new Set(expected.seeds.map(seedLine));
|
|
2212
2281
|
const have = new Set(actual.seeds.map(seedLine));
|
|
2213
|
-
for (const line of want) if (!have.has(line)) drift.push(`${
|
|
2214
|
-
for (const line of have) if (!want.has(line)) drift.push(`${
|
|
2282
|
+
for (const line of want) if (!have.has(line)) drift.push(rlsFinding("seed-missing", seeds, seedKey(line), `${seeds}: missing ${line}`));
|
|
2283
|
+
for (const line of have) if (!want.has(line)) drift.push(rlsFinding("seed-extra", seeds, seedKey(line), `${seeds}: unexpected ${line}`));
|
|
2215
2284
|
const permissions = new Set(expected.permissions);
|
|
2216
2285
|
const conditioned = new Set(expected.rowConditions);
|
|
2286
|
+
const levels = new Map((expected.levels ?? []).map((level) => [level.key, level]));
|
|
2287
|
+
const notes = [];
|
|
2217
2288
|
const covered = /* @__PURE__ */ new Set();
|
|
2218
2289
|
for (const policy of actual.policies) {
|
|
2219
2290
|
if (callsHelper(policy.expression)) covered.add(policy.table);
|
|
2220
2291
|
const blind = new Set(helperCallKeys(policy.expression, true));
|
|
2221
|
-
for (const key of helperCallKeys(policy.expression))
|
|
2222
|
-
|
|
2292
|
+
for (const key of helperCallKeys(policy.expression)) {
|
|
2293
|
+
const level = levels.get(key);
|
|
2294
|
+
if (level !== void 0) {
|
|
2295
|
+
if (level.conditioned) notes.push(rlsFinding("level-unconditioned", policy.table, `${policy.name}/${key}`, `${policy.table}: policy ${policy.name} passes ${key}, which skips the level's row condition, so holders of it are denied there; use ${level.rowHelper}(${policy.table.slice(policy.table.lastIndexOf(".") + 1)}, '${level.permission}')`, "info"));
|
|
2296
|
+
} else if (!permissions.has(key)) drift.push(rlsFinding("undeclared-key", policy.table, `${policy.name}/${key}`, `${policy.table}: policy ${policy.name} passes ${key}, which the policy does not declare, so it always denies`));
|
|
2297
|
+
else if (conditioned.has(key) && blind.has(key)) drift.push(rlsFinding("condition-blind", policy.table, `${policy.name}/${key}`, `${policy.table}: policy ${policy.name} passes ${key}, whose grants carry row conditions the helpers do not check: the policy grants more than the application does`));
|
|
2298
|
+
}
|
|
2223
2299
|
}
|
|
2224
2300
|
if (expected.readOnlyActors === true) drift.push(...unguardedWrites(actual.policies, ACTOR_MAY_WRITE));
|
|
2225
2301
|
return {
|
|
2226
2302
|
drift,
|
|
2227
|
-
info: actual.rlsTables.filter((table) => !covered.has(table)).map((table) => `${table}: no policy calls a PermDock helper
|
|
2303
|
+
info: [...notes, ...actual.rlsTables.filter((table) => !covered.has(table)).map((table) => rlsFinding("helper-uncalled", table, "table", `${table}: no policy calls a PermDock helper`, "info"))]
|
|
2228
2304
|
};
|
|
2229
2305
|
}
|
|
2230
2306
|
const ALL_POLICIES_SQL = `select schemaname || '.' || tablename as target, policyname,
|
|
@@ -2237,7 +2313,10 @@ from pg_class c
|
|
|
2237
2313
|
join pg_namespace n on n.oid = c.relnamespace
|
|
2238
2314
|
where c.relkind in ('r', 'p') and c.relrowsecurity`;
|
|
2239
2315
|
const quoteIdent = escapeSqlIdent;
|
|
2240
|
-
/**
|
|
2316
|
+
/**
|
|
2317
|
+
* Reads `role_permissions`, `pg_policies` and the RLS-enabled tables for
|
|
2318
|
+
* {@link diffMixed}.
|
|
2319
|
+
*/
|
|
2241
2320
|
async function introspectMixed(db, schema, connect = connectIntrospect) {
|
|
2242
2321
|
const client = await connect(db);
|
|
2243
2322
|
try {
|
|
@@ -2260,7 +2339,7 @@ async function introspectMixed(db, schema, connect = connectIntrospect) {
|
|
|
2260
2339
|
expression: String(row["expression"]),
|
|
2261
2340
|
command: String(row["command"]),
|
|
2262
2341
|
restrictive: row["restrictive"] === true,
|
|
2263
|
-
roles:
|
|
2342
|
+
roles: (typeof row["roles"] === "string" ? row["roles"] : "").split(",").filter((role) => role !== "")
|
|
2264
2343
|
})).filter((row) => inScope(row.table)),
|
|
2265
2344
|
rlsTables: tables.rows.map((row) => String(row["target"])).filter(inScope).toSorted()
|
|
2266
2345
|
};
|
|
@@ -2270,12 +2349,15 @@ async function introspectMixed(db, schema, connect = connectIntrospect) {
|
|
|
2270
2349
|
}
|
|
2271
2350
|
//#endregion
|
|
2272
2351
|
//#region src/cli/rls/rls.ts
|
|
2273
|
-
|
|
2352
|
+
/** `rls migrate` and `rls import` read the pgsql-parser 18 AST. */
|
|
2353
|
+
const PGSQL_PARSER_MAJOR = 18;
|
|
2354
|
+
const RLS_HELP = `permdock rls generate | import | verify | migrate | explain
|
|
2274
2355
|
|
|
2275
2356
|
generate Compile the policy into RLS policies and helpers
|
|
2276
2357
|
import Turn existing policies (--sql or --db-url-stdin) into a permissions module
|
|
2277
2358
|
verify Run the fixtures, or compare a live database with --introspect
|
|
2278
2359
|
migrate Rewrite the rls.migrate helpers' calls onto the generated helpers
|
|
2360
|
+
explain Show the tables, membership sources, custom roles and triggers the SQL binds to
|
|
2279
2361
|
|
|
2280
2362
|
Run permdock rls <command> --help for its flags.
|
|
2281
2363
|
Never emits service_role, except in the execute grant rls.trustedReaders asks for. memberOf compiles through the dialect memberships mapping.
|
|
@@ -2318,11 +2400,48 @@ function generateInput(input, target, dialect) {
|
|
|
2318
2400
|
...isSupabaseSchemas(input.schemas) ? { schemas: input.schemas } : {},
|
|
2319
2401
|
...input.grantsOut === void 0 ? {} : { grantsOut: input.grantsOut },
|
|
2320
2402
|
...input.seedsOut === void 0 ? {} : { seedsOut: input.seedsOut },
|
|
2403
|
+
...input.noSeeds === true ? { noSeeds: true } : {},
|
|
2321
2404
|
...input.backfillOut === void 0 ? {} : { backfillOut: input.backfillOut },
|
|
2322
2405
|
helpersOnly: input.helpersOnly,
|
|
2323
2406
|
shims: input.shims === true
|
|
2324
2407
|
};
|
|
2325
2408
|
}
|
|
2409
|
+
async function explain(input) {
|
|
2410
|
+
const dialect = asDialect(input.dialect ?? input.config.rls?.dialect);
|
|
2411
|
+
if (dialect === void 0) return {
|
|
2412
|
+
code: 2,
|
|
2413
|
+
output: "rls explain --dialect (or rls.dialect) must be supabase, neon or guc"
|
|
2414
|
+
};
|
|
2415
|
+
const generated = await runRlsGenerate({
|
|
2416
|
+
...generateInput(input, "sql", dialect),
|
|
2417
|
+
check: false,
|
|
2418
|
+
write: false
|
|
2419
|
+
});
|
|
2420
|
+
if (generated.explain === void 0) return {
|
|
2421
|
+
code: generated.code,
|
|
2422
|
+
output: generated.output
|
|
2423
|
+
};
|
|
2424
|
+
let report = generated.explain;
|
|
2425
|
+
if (input.db !== void 0) try {
|
|
2426
|
+
const facts = await readTableIndexFacts(input.db, report.bindings.map((binding) => binding.table), input.connect);
|
|
2427
|
+
report = {
|
|
2428
|
+
...report,
|
|
2429
|
+
bindings: report.bindings.map((binding) => ({
|
|
2430
|
+
...binding,
|
|
2431
|
+
exists: facts.columns[binding.table] !== void 0
|
|
2432
|
+
}))
|
|
2433
|
+
};
|
|
2434
|
+
} catch (cause) {
|
|
2435
|
+
return {
|
|
2436
|
+
code: exitCodeOfError(cause),
|
|
2437
|
+
output: describeError(cause)
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
return {
|
|
2441
|
+
code: 0,
|
|
2442
|
+
output: input.json ? JSON.stringify(report, null, 2) : describeExplain(report)
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2326
2445
|
async function migrate(input) {
|
|
2327
2446
|
const config = input.config.rls?.migrate;
|
|
2328
2447
|
if (config === void 0) return {
|
|
@@ -2347,8 +2466,8 @@ async function migrate(input) {
|
|
|
2347
2466
|
code: 2,
|
|
2348
2467
|
output: generated.output
|
|
2349
2468
|
};
|
|
2350
|
-
await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls migrate", input.cwd);
|
|
2351
|
-
const { migrateTarget, runRlsMigrate } = await import("./migrate-
|
|
2469
|
+
await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls migrate", input.cwd, PGSQL_PARSER_MAJOR);
|
|
2470
|
+
const { migrateTarget, runRlsMigrate } = await import("./migrate-B7oP_sQU.js");
|
|
2352
2471
|
return runRlsMigrate({
|
|
2353
2472
|
cwd: input.cwd,
|
|
2354
2473
|
sql: input.sql,
|
|
@@ -2364,8 +2483,11 @@ async function migrate(input) {
|
|
|
2364
2483
|
...input.io.now === void 0 ? {} : { now: input.io.now() }
|
|
2365
2484
|
});
|
|
2366
2485
|
}
|
|
2367
|
-
/**
|
|
2368
|
-
|
|
2486
|
+
/**
|
|
2487
|
+
* `verify --introspect` with `--helpers-only`: helpers and seeds exactly,
|
|
2488
|
+
* hand-written policies by their keys.
|
|
2489
|
+
*/
|
|
2490
|
+
async function introspectHelpersOnly(db, expected, generated, connect, readOnlyActors, ignore) {
|
|
2369
2491
|
const schema = generated.schema ?? "permdock";
|
|
2370
2492
|
try {
|
|
2371
2493
|
const actual = await introspectRls(db, expected, connect);
|
|
@@ -2376,31 +2498,28 @@ async function introspectHelpersOnly(db, expected, generated, connect, readOnlyA
|
|
|
2376
2498
|
seeds: generated.seeds ?? [],
|
|
2377
2499
|
permissions: generated.keys?.permissions ?? [],
|
|
2378
2500
|
rowConditions: generated.keys?.rowConditions ?? [],
|
|
2501
|
+
levels: generated.keys?.levels ?? [],
|
|
2379
2502
|
readOnlyActors
|
|
2380
2503
|
}, await introspectMixed(db, schema, connect));
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
output: [
|
|
2386
|
-
...drift,
|
|
2387
|
-
...warnings,
|
|
2388
|
-
...info
|
|
2389
|
-
].join("\n")
|
|
2390
|
-
} : {
|
|
2391
|
-
code: 0,
|
|
2392
|
-
output: [
|
|
2504
|
+
return verifyResult({
|
|
2505
|
+
findings: [
|
|
2506
|
+
...helpers,
|
|
2507
|
+
...mixed.drift,
|
|
2393
2508
|
...warnings,
|
|
2394
|
-
...info
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2509
|
+
...mixed.info
|
|
2510
|
+
],
|
|
2511
|
+
ignore,
|
|
2512
|
+
summary: `introspected ${String(expected.helpers.length)} helper(s) and ${String(generated.seeds?.length ?? 0)} seeded row(s), helpers only: no drift`
|
|
2513
|
+
});
|
|
2398
2514
|
} catch (cause) {
|
|
2399
2515
|
return usageResult(cause);
|
|
2400
2516
|
}
|
|
2401
2517
|
}
|
|
2402
|
-
/**
|
|
2403
|
-
|
|
2518
|
+
/**
|
|
2519
|
+
* `verify --introspect`: the catalogs against what `generate` would write with
|
|
2520
|
+
* the same flags.
|
|
2521
|
+
*/
|
|
2522
|
+
async function introspect(input, ignore) {
|
|
2404
2523
|
if (input.db === void 0) return {
|
|
2405
2524
|
code: 2,
|
|
2406
2525
|
output: "PermDock CLI: rls verify --introspect needs a database: --db-url-stdin, DATABASE_URL or SUPABASE_DB_URL"
|
|
@@ -2420,19 +2539,15 @@ async function introspect(input) {
|
|
|
2420
2539
|
output: generated.output
|
|
2421
2540
|
};
|
|
2422
2541
|
const expected = expectedRls(generated.policies, generated.text, generated.indexes);
|
|
2423
|
-
if (generated.helpersOnly === true) return introspectHelpersOnly(input.db, expected, generated, input.connect, readOnlyActorKinds(input.config.rls?.readOnlyActors) !== void 0);
|
|
2542
|
+
if (generated.helpersOnly === true) return introspectHelpersOnly(input.db, expected, generated, input.connect, readOnlyActorKinds(input.config.rls?.readOnlyActors) !== void 0, ignore);
|
|
2424
2543
|
try {
|
|
2425
2544
|
const actual = await introspectRls(input.db, expected, input.connect);
|
|
2426
|
-
const drift = diffRls(expected, actual, { columnGrants: (input.fields ?? input.config.rls?.fields)
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
};
|
|
2432
|
-
return {
|
|
2433
|
-
code: 0,
|
|
2434
|
-
output: `${warnings.map((line) => `${line}\n`).join("")}introspected ${String(expected.policies.length)} policies on ${String(expected.tables.length)} table(s) and ${String(expected.helpers.length)} helper(s): no drift`
|
|
2435
|
-
};
|
|
2545
|
+
const drift = diffRls(expected, actual, { columnGrants: (input.fields ?? input.config.rls?.fields) !== void 0 });
|
|
2546
|
+
return verifyResult({
|
|
2547
|
+
findings: [...drift, ...missingIndexes(expected, actual)],
|
|
2548
|
+
ignore,
|
|
2549
|
+
summary: `introspected ${String(expected.policies.length)} policies on ${String(expected.tables.length)} table(s) and ${String(expected.helpers.length)} helper(s): no drift`
|
|
2550
|
+
});
|
|
2436
2551
|
} catch (cause) {
|
|
2437
2552
|
return usageResult(cause);
|
|
2438
2553
|
}
|
|
@@ -2467,8 +2582,8 @@ async function runRls(input) {
|
|
|
2467
2582
|
});
|
|
2468
2583
|
}
|
|
2469
2584
|
case "import": {
|
|
2470
|
-
await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls import", input.cwd);
|
|
2471
|
-
const { runRlsImport } = await import("./import-
|
|
2585
|
+
await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls import", input.cwd, PGSQL_PARSER_MAJOR);
|
|
2586
|
+
const { runRlsImport } = await import("./import-CST57XLp.js");
|
|
2472
2587
|
return runRlsImport({
|
|
2473
2588
|
cwd: input.cwd,
|
|
2474
2589
|
config: input.config,
|
|
@@ -2490,7 +2605,8 @@ async function runRls(input) {
|
|
|
2490
2605
|
...input.db === void 0 ? {} : { db: input.db },
|
|
2491
2606
|
...input.exec === void 0 ? {} : { exec: input.exec }
|
|
2492
2607
|
});
|
|
2493
|
-
|
|
2608
|
+
const ignore = verifyIgnores(input.config, input.ignore);
|
|
2609
|
+
if (input.introspect) return introspect(input, ignore);
|
|
2494
2610
|
const format = input.format ?? "node";
|
|
2495
2611
|
if (format !== "node" && format !== "pgtap") return {
|
|
2496
2612
|
code: 2,
|
|
@@ -2502,13 +2618,16 @@ async function runRls(input) {
|
|
|
2502
2618
|
format,
|
|
2503
2619
|
tree: input.tree,
|
|
2504
2620
|
io: input.io,
|
|
2621
|
+
...input.out === void 0 ? {} : { out: input.out },
|
|
2505
2622
|
...input.fixtures === void 0 ? {} : { fixtures: input.fixtures },
|
|
2506
2623
|
...input.db === void 0 ? {} : { db: input.db },
|
|
2507
2624
|
...input.from === void 0 ? {} : { from: input.from },
|
|
2508
|
-
...input.connect === void 0 ? {} : { connect: input.connect }
|
|
2625
|
+
...input.connect === void 0 ? {} : { connect: input.connect },
|
|
2626
|
+
ignore
|
|
2509
2627
|
});
|
|
2510
2628
|
}
|
|
2511
2629
|
case "migrate": return migrate(input);
|
|
2630
|
+
case "explain": return explain(input);
|
|
2512
2631
|
default: return {
|
|
2513
2632
|
code: 2,
|
|
2514
2633
|
output: RLS_HELP
|