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,43 +1,46 @@
|
|
|
1
1
|
import { a as sole, r as isRecord } from "./compact-CAf557ZB.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { c as listPermissions, h as formerKeys, n as findPermission } from "./permission-registry-
|
|
5
|
-
import {
|
|
6
|
-
import { f as nowMillis, u as isoAt } from "./time-
|
|
7
|
-
import { t as canonicalJson } from "./canonical-json-
|
|
8
|
-
import {
|
|
9
|
-
import { o as requiresStandIn } from "./delegation-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { p as scopeField } from "./tenancy-DN2QAx9z.js";
|
|
13
|
-
import { s as policyLevels } from "./policy-DbM5YYtV.js";
|
|
2
|
+
import { l as isConditionRef, o as hasConditionOp } from "./refs-C8BKxc_e.js";
|
|
3
|
+
import { A as memoryCustomRoleSource, b as memoryRelationSource } from "./src-DZ0l9UAg.js";
|
|
4
|
+
import { c as listPermissions, h as formerKeys, n as findPermission } from "./permission-registry-B8tYxPIt.js";
|
|
5
|
+
import { c as isEdgeRelation, l as isFieldRelation, u as isPrincipalRelation } from "./resource-relations-8-pCScBO.js";
|
|
6
|
+
import { f as nowMillis, u as isoAt } from "./time-nR1JesoH.js";
|
|
7
|
+
import { t as canonicalJson } from "./canonical-json-DwilZ1iB.js";
|
|
8
|
+
import { f as scopeList, l as rootScope, o as normalizeMemberships } from "./scopes-0Up_UA2t.js";
|
|
9
|
+
import { o as requiresStandIn } from "./delegation-CDB6_keF.js";
|
|
10
|
+
import { p as scopeField } from "./tenancy-5d1zVBvK.js";
|
|
11
|
+
import { l as policyLevels, t as coveredLevels } from "./policy-CLQ8__GF.js";
|
|
14
12
|
import { t as requiresApproval } from "./approval-required-TzNB9oQA.js";
|
|
15
|
-
import { c as andWhere, m as customRoleScope, v as leveled, y as membershipsClaim } from "./evaluate-
|
|
16
|
-
import { t as getPermDock } from "./permdock-
|
|
17
|
-
import { a as describeError, i as configError, u as usageResult } from "./errors-
|
|
18
|
-
import { t as policyRowConditionKeys } from "./row-conditions-
|
|
19
|
-
import { s as
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import "./
|
|
23
|
-
import {
|
|
24
|
-
import "./
|
|
13
|
+
import { c as andWhere, m as customRoleScope, v as leveled, y as membershipsClaim } from "./evaluate-Dd8Ta0Ie.js";
|
|
14
|
+
import { t as getPermDock } from "./permdock-L-_I_3zP.js";
|
|
15
|
+
import { a as describeError, i as configError, u as usageResult } from "./errors-Vpu6rKHl.js";
|
|
16
|
+
import { t as policyRowConditionKeys } from "./row-conditions-BAGHOmVq.js";
|
|
17
|
+
import { s as writeFileEnsuringDir } from "./files-BSjrtIHw.js";
|
|
18
|
+
import { c as jsonSchemaOf } from "./catalog-doc-DjMKzJp9.js";
|
|
19
|
+
import { o as loadPolicyAt } from "./load-DjSU5KXO.js";
|
|
20
|
+
import { i as qualifySqlTable, n as defaultSqlCommand, t as SQL_IDENT } from "./sql-0WjxIp9g.js";
|
|
21
|
+
import { a as scopeMembershipTable, i as scopeColumn } from "./compile-EzEMB-7v.js";
|
|
22
|
+
import "./budget-BELja5A9.js";
|
|
23
|
+
import "./sources-CMAM-2A3.js";
|
|
25
24
|
import { t as beforeFirst } from "./text-BMFRfBqc.js";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
25
|
+
import { t as DOCS_BASE } from "./manifest-Cgiq5oqc.js";
|
|
26
|
+
import { n as fixtureSubject, r as loadFixtures, t as fixtureRow } from "./fixtures-A1_sp_tR.js";
|
|
27
|
+
import { A as quoteQualified, N as arrayColumnsOf, O as quoteIdent, P as columnTypesOf, _ as subjectClaimJsonSql, a as customRolesOn, j as quoteTable, k as quoteLiteral, r as HELPERS, v as subjectClaimSql } from "./shared-Afctg7B0.js";
|
|
28
|
+
import { C as accessSql, D as tokenScopeAllowsCall, S as requiresSql, ht as scopeTable, k as capabilityAccessSql, n as tableNameOf, o as graphPlan, q as apiKeyAllowsCall, r as tableNames } from "./tables-DeMNEy6V.js";
|
|
29
|
+
import { c as breakGlassKey, f as compileConditionSql, l as collectGrants, m as sqlFunctionNames, p as contextRefs, s as breakGlassHolder } from "./ownership-B4L59T9u.js";
|
|
30
|
+
import { t as membershipSourcesOf } from "./sources-qoIJQrJv.js";
|
|
31
|
+
import { n as pgConnector, t as connectPgPool } from "./pg-DQTqRrou.js";
|
|
32
|
+
import { _ as wrapSql, g as validitySql, m as andSql, s as viewName, t as FIELD_VIEWS } from "./fields-D0vUI6dq.js";
|
|
33
|
+
import { resolve } from "node:path";
|
|
31
34
|
import { createHash, randomUUID } from "node:crypto";
|
|
32
35
|
//#region src/cli/rls/indexes.ts
|
|
33
36
|
/** Postgres truncates identifiers longer than this, in bytes. */
|
|
34
37
|
const MAX_IDENT = 63;
|
|
35
38
|
/**
|
|
36
|
-
* The row fields `condition` compares with the subject, a claim or a
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* The row fields `condition` compares with the subject, a claim or a membership
|
|
40
|
+
* (`ownerId: principal.id`, `memberOf`), through `and` and `or`: the selective
|
|
41
|
+
* comparisons an index serves. A comparison with a constant (`status: 'sent'`)
|
|
42
|
+
* narrows rows the scope key already found, a negation scans anyway, and a
|
|
43
|
+
* dotted field is a JSON path a plain index does not serve.
|
|
41
44
|
*/
|
|
42
45
|
function indexedFields(condition) {
|
|
43
46
|
if (condition === void 0) return [];
|
|
@@ -67,9 +70,9 @@ function plainField(field) {
|
|
|
67
70
|
return field.includes(".") ? [] : [field];
|
|
68
71
|
}
|
|
69
72
|
/**
|
|
70
|
-
* Every index the generated SQL reads through: the row columns policies
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
+
* Every index the generated SQL reads through: the row columns policies filter
|
|
74
|
+
* on (scope keys and selective condition fields), and the membership tables'
|
|
75
|
+
* user column, followed by the scope column the helpers select.
|
|
73
76
|
*/
|
|
74
77
|
function indexTargets(ctx, rowColumns) {
|
|
75
78
|
const targets = /* @__PURE__ */ new Map();
|
|
@@ -102,7 +105,10 @@ function indexName(target) {
|
|
|
102
105
|
function indexesSql(targets) {
|
|
103
106
|
return targets.map((target) => `create index if not exists ${quoteIdent(indexName(target))} on ${quoteTable(target.table)} (${target.columns.map(quoteIdent).join(", ")});`).join("\n");
|
|
104
107
|
}
|
|
105
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* Every live column of the tables in `$1` (`schema.table`), as `target` and
|
|
110
|
+
* `name`.
|
|
111
|
+
*/
|
|
106
112
|
const COLUMNS_SQL = `select n.nspname || '.' || c.relname as target, a.attname as name
|
|
107
113
|
from pg_attribute a
|
|
108
114
|
join pg_class c on c.oid = a.attrelid
|
|
@@ -187,25 +193,30 @@ function levelReachOf(policy, keys) {
|
|
|
187
193
|
const { item } = entry;
|
|
188
194
|
const { grant } = item;
|
|
189
195
|
if (item.access.kind !== "role" || grant.effect !== "allow" || grant.permission.kind !== "instance" || grant.level !== void 0) continue;
|
|
190
|
-
const
|
|
196
|
+
const resource = grant.permission.resource;
|
|
197
|
+
const levels = policyLevels(policy, resource);
|
|
191
198
|
if (levels.length > 0) {
|
|
192
199
|
const set = reach.get(grantKey) ?? /* @__PURE__ */ new Set();
|
|
193
|
-
|
|
200
|
+
const reached = coveredLevels(policy, resource, reachedLevels(grant.where, levels));
|
|
201
|
+
for (const name of reached) set.add(name);
|
|
194
202
|
reach.set(grantKey, set);
|
|
195
203
|
}
|
|
196
204
|
}
|
|
205
|
+
const resourceOf = new Map([...keys].map(([entry, grantKey]) => [grantKey, entry.item.grant.permission.resource]));
|
|
197
206
|
const out = [];
|
|
198
207
|
for (const grantKey of new Set(keys.values())) {
|
|
199
208
|
const at = grantKey.lastIndexOf("@");
|
|
200
209
|
const base = at === -1 ? grantKey : grantKey.slice(0, at);
|
|
201
210
|
const level = at === -1 ? void 0 : grantKey.slice(at + 1);
|
|
202
|
-
|
|
211
|
+
const covered = level === void 0 ? void 0 : coveredLevels(policy, resourceOf.get(grantKey) ?? "", [level]);
|
|
212
|
+
for (const name of reach.get(base) ?? []) if (covered === void 0 || covered.includes(name)) out.push([grantKey, name]);
|
|
203
213
|
}
|
|
204
214
|
return out.toSorted(([a, x], [b, y]) => a === b ? x < y ? -1 : 1 : a < b ? -1 : 1);
|
|
205
215
|
}
|
|
206
216
|
/**
|
|
207
|
-
* The SQL command `action` compiles to: `rls.actions` first, then the six
|
|
208
|
-
* Any other verb is `undefined`, as `'none'` is: no policy, and
|
|
217
|
+
* The SQL command `action` compiles to: `rls.actions` first, then the six
|
|
218
|
+
* default verbs. Any other verb is `undefined`, as `'none'` is: no policy, and
|
|
219
|
+
* its grants are still seeded.
|
|
209
220
|
*/
|
|
210
221
|
function commandFor(action, actions) {
|
|
211
222
|
if (actions !== void 0 && Object.hasOwn(actions, action)) {
|
|
@@ -214,7 +225,7 @@ function commandFor(action, actions) {
|
|
|
214
225
|
}
|
|
215
226
|
return defaultSqlCommand(action);
|
|
216
227
|
}
|
|
217
|
-
function tableFor
|
|
228
|
+
function tableFor(resource, tables) {
|
|
218
229
|
return tables?.[resource] ?? resource;
|
|
219
230
|
}
|
|
220
231
|
function ancestorsOf(policy, name) {
|
|
@@ -262,8 +273,8 @@ function resourceCondition(item, policy, ctx) {
|
|
|
262
273
|
}
|
|
263
274
|
/**
|
|
264
275
|
* The access a link capability has to a resource-scoped grant, over the same
|
|
265
|
-
* holders as `resourceCondition`. It reads the claim, so ancestor hops need
|
|
266
|
-
*
|
|
276
|
+
* holders as `resourceCondition`. It reads the claim, so ancestor hops need no
|
|
277
|
+
* memberships table. `undefined` when no holder is on the row's chain.
|
|
267
278
|
*/
|
|
268
279
|
function capabilityAccess(item, policy, ctx) {
|
|
269
280
|
if (item.access.kind !== "resource") return;
|
|
@@ -279,7 +290,7 @@ function capabilityAccess(item, policy, ctx) {
|
|
|
279
290
|
if (hops.length === 0) return;
|
|
280
291
|
return hops.length === 1 ? hops[0] : hops.map(wrapSql).join(" or ");
|
|
281
292
|
}
|
|
282
|
-
function prepare(item, tables, warnings, skipClosures,
|
|
293
|
+
function prepare(item, tables, warnings, skipClosures, sql, rowActions = false) {
|
|
283
294
|
const { grant, label } = item;
|
|
284
295
|
if (grant.breakGlass !== void 0) {
|
|
285
296
|
warnings.push(`break-glass grant ${label}/${grant.permission.key} reads through permdock_break_glass_${grant.permission.resource}, not a policy`);
|
|
@@ -304,9 +315,14 @@ function prepare(item, tables, warnings, skipClosures, actions, rowActions = fal
|
|
|
304
315
|
warnings.push(`skipped approval grant ${label}/${grant.permission.key}`);
|
|
305
316
|
return;
|
|
306
317
|
}
|
|
307
|
-
const
|
|
308
|
-
const
|
|
318
|
+
const tableless = sql.tableless?.has(grant.permission.resource) === true;
|
|
319
|
+
const command = tableless ? void 0 : commandFor(grant.permission.action, sql.actions);
|
|
320
|
+
const table = tableFor(grant.permission.resource, tables);
|
|
309
321
|
if (command === void 0) {
|
|
322
|
+
if (tableless && item.access.kind !== "role") {
|
|
323
|
+
warnings.push(`skipped ${label}/${grant.permission.key}: rls.tables.${grant.permission.resource} is false, so the resource has no table, and only role grants are seeded`);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
310
326
|
if (item.access.kind !== "role" && rowActions) return {
|
|
311
327
|
item,
|
|
312
328
|
command: "none",
|
|
@@ -350,9 +366,9 @@ function signature(entry, byFields) {
|
|
|
350
366
|
/**
|
|
351
367
|
* Grant keys per permission: the key is the permission, split into
|
|
352
368
|
* `permission#n` when role grants carry different portable conditions (or
|
|
353
|
-
* effects), one key per condition group; a group whose grants set `group`
|
|
354
|
-
*
|
|
355
|
-
*
|
|
369
|
+
* effects), one key per condition group; a group whose grants set `group` is
|
|
370
|
+
* `permission#<group>` instead. With field views, grants that differ only in
|
|
371
|
+
* `fields` get their own keys too, so a view can tell them apart.
|
|
356
372
|
*/
|
|
357
373
|
function assignKeys(entries, byFields) {
|
|
358
374
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -372,7 +388,7 @@ function assignKeys(entries, byFields) {
|
|
|
372
388
|
const group = groupOf(permission, list);
|
|
373
389
|
if (group !== void 0 && named.has(group)) throw configError(`PermDock CLI: grants of ${permission} with different conditions share group '${group}'; a group names one condition`);
|
|
374
390
|
if (group !== void 0) named.add(group);
|
|
375
|
-
const grantKey = group
|
|
391
|
+
const grantKey = group === void 0 ? lists.length === 1 ? permission : `${permission}#${index + 1}` : `${permission}#${group}`;
|
|
376
392
|
for (const entry of list) keys.set(entry, grantKey);
|
|
377
393
|
}
|
|
378
394
|
}
|
|
@@ -384,19 +400,19 @@ function groupOf(permission, list) {
|
|
|
384
400
|
return names[0];
|
|
385
401
|
}
|
|
386
402
|
/**
|
|
387
|
-
* One entry per level for every allow of
|
|
388
|
-
*
|
|
389
|
-
*
|
|
403
|
+
* One entry per level for every allow of a role on a resource with levels,
|
|
404
|
+
* keyed `<grant key>@<level>`, whether or not the role is assignable: every
|
|
405
|
+
* role holds the same leveled keys, and `permdock_ceiling` keeps only the
|
|
406
|
+
* assignable roles'. Only `permdock_custom_keys` hands these keys out, to a
|
|
407
|
+
* custom role whose stored allow names the level.
|
|
390
408
|
*/
|
|
391
409
|
function levelEntries(policy, ctx, keys, tables, warnings, skipClosures) {
|
|
392
|
-
|
|
393
|
-
if (custom?.levels !== true) return [];
|
|
394
|
-
const assignable = new Set(custom.assignable);
|
|
410
|
+
if (ctx.customRoles?.levels !== true) return [];
|
|
395
411
|
const out = [];
|
|
396
412
|
for (const [entry, grantKey] of Array.from(keys)) {
|
|
397
413
|
const { item } = entry;
|
|
398
414
|
const { grant } = item;
|
|
399
|
-
if (item.access.kind !== "role" || grant.effect !== "allow" || grant.permission.kind !== "instance"
|
|
415
|
+
if (item.access.kind !== "role" || grant.effect !== "allow" || grant.permission.kind !== "instance") continue;
|
|
400
416
|
if (grant.permission.key.includes("@")) throw configError(`PermDock CLI: permission key '${grant.permission.key}' holds '@', which separates a level in custom-role grant keys`);
|
|
401
417
|
for (const [level, condition] of policyLevels(policy, grant.permission.resource)) {
|
|
402
418
|
const where = andWhere(item.where, condition);
|
|
@@ -404,7 +420,7 @@ function levelEntries(policy, ctx, keys, tables, warnings, skipClosures) {
|
|
|
404
420
|
...item,
|
|
405
421
|
grant: leveled(grant, level, condition),
|
|
406
422
|
...where === void 0 ? {} : { where }
|
|
407
|
-
}, tables, warnings, skipClosures, ctx
|
|
423
|
+
}, tables, warnings, skipClosures, ctx);
|
|
408
424
|
if (prepared !== void 0) {
|
|
409
425
|
keys.set(prepared, `${grantKey}@${level}`);
|
|
410
426
|
out.push(prepared);
|
|
@@ -423,19 +439,22 @@ function noteConditions(entry, warnings) {
|
|
|
423
439
|
if (hasConditionOp(entry.using, "opaque") || hasConditionOp(entry.check, "opaque")) warnings.push(`opaque SQL on ${label}/${grant.permission.key} is untestable app-side`);
|
|
424
440
|
}
|
|
425
441
|
/**
|
|
426
|
-
* Compiles every grant (role and top-level) to branches whose role check is
|
|
427
|
-
*
|
|
442
|
+
* Compiles every grant (role and top-level) to branches whose role check is a
|
|
443
|
+
* helper call keyed by grant key. Nothing in a branch is per-row except the
|
|
428
444
|
* portable row condition and resource-membership joins.
|
|
429
445
|
*/
|
|
430
|
-
/**
|
|
446
|
+
/**
|
|
447
|
+
* A Supabase token from a permanent user: anonymous sign-ins carry
|
|
448
|
+
* `is_anonymous: true`.
|
|
449
|
+
*/
|
|
431
450
|
const PERMANENT_USER = `((select auth.jwt()) ->> 'is_anonymous') is distinct from 'true'`;
|
|
432
451
|
function compileGrants(policy, ctx, tables, warnings, skipClosures, rowActions = false) {
|
|
433
452
|
const items = collectGrants(policy);
|
|
434
453
|
const entries = items.flatMap((item) => {
|
|
435
|
-
const entry = prepare(item, tables, warnings, skipClosures, ctx
|
|
454
|
+
const entry = prepare(item, tables, warnings, skipClosures, ctx, rowActions);
|
|
436
455
|
return entry === void 0 ? [] : [entry];
|
|
437
456
|
});
|
|
438
|
-
const keys = assignKeys(entries, ctx.fields
|
|
457
|
+
const keys = assignKeys(entries, ctx.fields !== void 0);
|
|
439
458
|
entries.push(...levelEntries(policy, ctx, keys, tables, warnings, skipClosures));
|
|
440
459
|
const conditionedKeys = /* @__PURE__ */ new Set();
|
|
441
460
|
for (const [entry, grantKey] of keys) if (entry.using !== void 0 || entry.check !== void 0 || entry.item.grant.validity !== void 0 || entry.item.grant.requires !== void 0) conditionedKeys.add(grantKey);
|
|
@@ -492,7 +511,7 @@ function compileGrants(policy, ctx, tables, warnings, skipClosures, rowActions =
|
|
|
492
511
|
};
|
|
493
512
|
rows.set(`${row.role}\u0000${row.grantKey}\u0000${row.scope}`, row);
|
|
494
513
|
}
|
|
495
|
-
if (!rowActions) continue;
|
|
514
|
+
if (!rowActions || ctx.tableless?.has(grant.permission.resource) === true) continue;
|
|
496
515
|
}
|
|
497
516
|
const actionOnly = entry.command === "none";
|
|
498
517
|
const command = actionOnly ? "select" : entry.command;
|
|
@@ -506,8 +525,10 @@ function compileGrants(policy, ctx, tables, warnings, skipClosures, rowActions =
|
|
|
506
525
|
const grantKey = keys.get(entry);
|
|
507
526
|
let accessExpr;
|
|
508
527
|
if (access.kind === "role" && grantKey !== void 0) {
|
|
509
|
-
const
|
|
510
|
-
|
|
528
|
+
const via = ctx.vias?.[grant.permission.resource];
|
|
529
|
+
if (via !== void 0 && access.scope !== "global" && access.scope !== via.scope) throw configError(`PermDock CLI: ${label}/${grant.permission.key} is granted at scope ${access.scope}, but rls.tables.${grant.permission.resource}.via reaches ${via.scope}; map the scope with via.scope or grant at ${via.scope}`);
|
|
530
|
+
const column = access.scope === "global" ? void 0 : via?.key ?? scopeField(policy.resources.get(grant.permission.resource), access.scope, policy.scopes);
|
|
531
|
+
accessExpr = accessSql(ctx, access.scope, grantKey, column, access.scope === "global" ? void 0 : via?.helper);
|
|
511
532
|
if (column !== void 0) filters(grant.permission.resource, table, column);
|
|
512
533
|
const row = {
|
|
513
534
|
role: access.role,
|
|
@@ -529,8 +550,10 @@ function compileGrants(policy, ctx, tables, warnings, skipClosures, rowActions =
|
|
|
529
550
|
accessExpr = andSql(accessExpr, validity);
|
|
530
551
|
for (const key of grant.requires ?? []) accessExpr = andSql(accessExpr, requiresSql(ctx, key, policy.resources.get(grant.permission.resource)));
|
|
531
552
|
if (ctx.anonymousSignIns === "deny" && access.kind !== "anyone") accessExpr = andSql(accessExpr, PERMANENT_USER);
|
|
532
|
-
if (ctx.apiKeys !== void 0 && grant.effect === "allow" && access.kind !== "anyone" && access.kind !== "role") {
|
|
533
|
-
const
|
|
553
|
+
if ((ctx.apiKeys !== void 0 || ctx.tokenScopes !== void 0) && grant.effect === "allow" && access.kind !== "anyone" && access.kind !== "role") {
|
|
554
|
+
const covered = requiresStandIn(grant) ? grant.requires ?? [] : [grant.permission.key, ...grant.requires ?? []];
|
|
555
|
+
const ceilings = [...ctx.apiKeys === void 0 ? [] : [apiKeyAllowsCall], ...ctx.tokenScopes === void 0 ? [] : [tokenScopeAllowsCall]];
|
|
556
|
+
const calls = covered.flatMap((key) => ceilings.map((call) => `(select ${call(ctx, quoteLiteral(key))})`));
|
|
534
557
|
accessExpr = andSql(accessExpr, calls.length === 1 ? calls[0] ?? "" : `(${calls.join(" and ")})`);
|
|
535
558
|
}
|
|
536
559
|
const using = compileOptional(entry.using, rowCtx);
|
|
@@ -576,8 +599,8 @@ function compileGrants(policy, ctx, tables, warnings, skipClosures, rowActions =
|
|
|
576
599
|
}
|
|
577
600
|
/**
|
|
578
601
|
* Each row again under every key its permission was renamed from, so SQL that
|
|
579
|
-
* still calls `permdock_has('<old key>')` keeps the access the current key
|
|
580
|
-
*
|
|
602
|
+
* still calls `permdock_has('<old key>')` keeps the access the current key has.
|
|
603
|
+
* A split grant key (`key#n`) keeps its suffix; break-glass rows are not
|
|
581
604
|
* copied.
|
|
582
605
|
*/
|
|
583
606
|
function withAliasRows(policy, rows) {
|
|
@@ -596,8 +619,8 @@ function withAliasRows(policy, rows) {
|
|
|
596
619
|
}
|
|
597
620
|
/**
|
|
598
621
|
* Postgres needs SELECT access to find rows for UPDATE / DELETE and for
|
|
599
|
-
* RETURNING, so a table with only update or delete grants gets matching
|
|
600
|
-
*
|
|
622
|
+
* RETURNING, so a table with only update or delete grants gets matching SELECT
|
|
623
|
+
* branches.
|
|
601
624
|
*/
|
|
602
625
|
function ensureSelectCoverage(branches, warnings) {
|
|
603
626
|
const extra = [];
|
|
@@ -627,18 +650,21 @@ function ensureSelectCoverage(branches, warnings) {
|
|
|
627
650
|
const HELPER_TABLES = ["storage.objects", "realtime.messages"];
|
|
628
651
|
const HELPER_CALL = new RegExp(String.raw`\b(${HELPERS.has}(?:_permission)?|permitted_[a-z][a-z0-9_]*_ids(?:_by_permission)?)\s*\(\s*'((?:[^']|'')+)'(\s*,\s*(false\b)?)?`, "giu");
|
|
629
652
|
const ANY_HELPER = new RegExp(String.raw`\b(?:${HELPERS.has}(?:_permission)?(?:_for)?|permitted_[a-z][a-z0-9_]*_ids(?:_by_permission)?(?:_for)?|member_[a-z][a-z0-9_]*_ids(?:_for)?)\s*\(`, "iu");
|
|
630
|
-
/**
|
|
653
|
+
/**
|
|
654
|
+
* Whether a policy expression calls any generated helper, the key-less
|
|
655
|
+
* `member_<scope>_ids` included.
|
|
656
|
+
*/
|
|
631
657
|
function callsHelper(sql) {
|
|
632
658
|
return ANY_HELPER.test(sql);
|
|
633
659
|
}
|
|
634
660
|
/**
|
|
635
661
|
* The permission keys a policy expression passes to `permdock_has`,
|
|
636
662
|
* `permitted_<scope>_ids` or their permission-key forms, `#n` stripped. With
|
|
637
|
-
* `blind`, only the calls that ignore row conditions: the grant-key forms,
|
|
638
|
-
*
|
|
663
|
+
* `blind`, only the calls that ignore row conditions: the grant-key forms, and
|
|
664
|
+
* a permission-key form asked for conditioned allows. A plain
|
|
639
665
|
* `permdock_has_permission(key)` or `permitted_<scope>_ids_by_permission(key)`
|
|
640
|
-
* counts only allows without a row condition, so it never grants more than
|
|
641
|
-
*
|
|
666
|
+
* counts only allows without a row condition, so it never grants more than the
|
|
667
|
+
* application does.
|
|
642
668
|
*/
|
|
643
669
|
function helperCallKeys(sql, blind = false) {
|
|
644
670
|
const keys = /* @__PURE__ */ new Set();
|
|
@@ -658,7 +684,10 @@ function helperTable(name) {
|
|
|
658
684
|
const table = tableName(name);
|
|
659
685
|
return HELPER_TABLES.find((candidate) => candidate === table);
|
|
660
686
|
}
|
|
661
|
-
/**
|
|
687
|
+
/**
|
|
688
|
+
* `create policy` statements on {@link HELPER_TABLES} in migration SQL, with
|
|
689
|
+
* the keys their condition-blind helper calls pass.
|
|
690
|
+
*/
|
|
662
691
|
function helperTablePolicies(sql) {
|
|
663
692
|
const text = sql.replaceAll(/--[^\n]*/gu, "").replaceAll(/\/\*[\s\S]*?\*\//gu, "");
|
|
664
693
|
const found = [];
|
|
@@ -673,19 +702,25 @@ function helperTablePolicies(sql) {
|
|
|
673
702
|
}
|
|
674
703
|
return found;
|
|
675
704
|
}
|
|
676
|
-
/**
|
|
705
|
+
/**
|
|
706
|
+
* `pg_policies` rows on {@link HELPER_TABLES}, read by `rls verify
|
|
707
|
+
* --db-url-stdin`.
|
|
708
|
+
*/
|
|
677
709
|
const HELPER_TABLE_POLICIES_SQL = `select schemaname || '.' || tablename as target, policyname, coalesce(qual, '') || ' ' || coalesce(with_check, '') as body
|
|
678
710
|
from pg_policies
|
|
679
711
|
where (schemaname, tablename) in (('storage', 'objects'), ('realtime', 'messages'))`;
|
|
712
|
+
function textOf(value) {
|
|
713
|
+
return typeof value === "string" ? value : "";
|
|
714
|
+
}
|
|
680
715
|
/** The same rows as {@link helperTablePolicies}, from `pg_policies`. */
|
|
681
716
|
function helperTablePoliciesFromRows(rows) {
|
|
682
717
|
const found = [];
|
|
683
718
|
for (const row of rows) {
|
|
684
|
-
const table = helperTable(
|
|
685
|
-
const keys = helperCallKeys(
|
|
719
|
+
const table = helperTable(textOf(row["target"]));
|
|
720
|
+
const keys = helperCallKeys(textOf(row["body"]), true);
|
|
686
721
|
if (table !== void 0 && keys.length > 0) found.push({
|
|
687
722
|
table,
|
|
688
|
-
name:
|
|
723
|
+
name: textOf(row["policyname"]),
|
|
689
724
|
keys
|
|
690
725
|
});
|
|
691
726
|
}
|
|
@@ -697,8 +732,196 @@ function rowConditionMessage(policy, keys) {
|
|
|
697
732
|
}
|
|
698
733
|
const ROW_CONDITION_FIX = "call permitted_<scope>_ids_by_permission('<key>') or permdock_has_permission('<key>'), which count only the allows without a row condition (a relationship grant never reaches a topic or a folder), or use a permission whose catalog entry has rowConditions: false";
|
|
699
734
|
//#endregion
|
|
735
|
+
//#region src/cli/rls/verify-findings.ts
|
|
736
|
+
const VERIFY_DOCS = `${DOCS_BASE}/rls#verify`;
|
|
737
|
+
function rlsFinding(code, table, key, message, severity = "error") {
|
|
738
|
+
return {
|
|
739
|
+
code,
|
|
740
|
+
table,
|
|
741
|
+
key,
|
|
742
|
+
message,
|
|
743
|
+
severity
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
/** `<code>:<table>:<key>`: what `--ignore` and `rls.verify.ignore` name. */
|
|
747
|
+
function findingId(finding) {
|
|
748
|
+
return `${finding.code}:${finding.table}:${finding.key}`;
|
|
749
|
+
}
|
|
750
|
+
/** `rls.verify.ignore`, checked, then one entry per `--ignore` id. */
|
|
751
|
+
function verifyIgnores(config, flags = []) {
|
|
752
|
+
const entries = config.rls?.verify?.ignore ?? [];
|
|
753
|
+
if (!Array.isArray(entries)) throw configError("PermDock CLI: rls.verify.ignore must be a list of { id, reason }");
|
|
754
|
+
const out = [];
|
|
755
|
+
for (const [index, entry] of entries.entries()) {
|
|
756
|
+
if (!isRecord(entry) || typeof entry["id"] !== "string" || entry["id"] === "" || typeof entry["reason"] !== "string" || entry["reason"].trim() === "") throw configError(`PermDock CLI: rls.verify.ignore[${String(index)}] needs an id and a non-empty reason`);
|
|
757
|
+
out.push({
|
|
758
|
+
id: entry["id"],
|
|
759
|
+
reason: entry["reason"]
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
for (const id of flags) out.push({
|
|
763
|
+
id,
|
|
764
|
+
reason: "--ignore"
|
|
765
|
+
});
|
|
766
|
+
return out;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Splits `findings` by `ignores`; an id listed twice counts once, with its
|
|
770
|
+
* first reason.
|
|
771
|
+
*/
|
|
772
|
+
function applyIgnores(findings, ignores) {
|
|
773
|
+
const reasons = /* @__PURE__ */ new Map();
|
|
774
|
+
for (const entry of ignores) if (!reasons.has(entry.id)) reasons.set(entry.id, entry.reason);
|
|
775
|
+
const matched = /* @__PURE__ */ new Set();
|
|
776
|
+
const kept = [];
|
|
777
|
+
const ignored = [];
|
|
778
|
+
for (const finding of findings) {
|
|
779
|
+
const id = findingId(finding);
|
|
780
|
+
const reason = reasons.get(id);
|
|
781
|
+
if (reason === void 0) kept.push({
|
|
782
|
+
...finding,
|
|
783
|
+
id
|
|
784
|
+
});
|
|
785
|
+
else {
|
|
786
|
+
matched.add(id);
|
|
787
|
+
ignored.push({
|
|
788
|
+
id,
|
|
789
|
+
reason,
|
|
790
|
+
message: finding.message
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
findings: kept,
|
|
796
|
+
ignored,
|
|
797
|
+
stale: [...reasons].filter(([id]) => !matched.has(id)).map(([id, reason]) => ({
|
|
798
|
+
id,
|
|
799
|
+
reason
|
|
800
|
+
}))
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function prefixOf(severity) {
|
|
804
|
+
switch (severity) {
|
|
805
|
+
case "error": return "";
|
|
806
|
+
case "warning": return "warning: ";
|
|
807
|
+
case "info": return "info: ";
|
|
808
|
+
default: return severity;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* The `rls verify` result: exit 1 when an error finding is left after the
|
|
813
|
+
* ignores. Text lines carry each finding's id; `--json` data is `{ findings,
|
|
814
|
+
* ignored, stale }`; `--format github|sarif` renders `findings`.
|
|
815
|
+
*/
|
|
816
|
+
function verifyResult(input) {
|
|
817
|
+
const report = applyIgnores(input.findings, input.ignore);
|
|
818
|
+
const failed = report.findings.some((item) => item.severity === "error");
|
|
819
|
+
const lines = [
|
|
820
|
+
...report.findings.map((item) => `${prefixOf(item.severity)}${item.message} [${item.id}]`),
|
|
821
|
+
...report.ignored.map((item) => `ignored ${item.id}: ${item.reason}`),
|
|
822
|
+
...report.stale.map((item) => `warning: ignore entry ${item.id} matches no finding; remove it from rls.verify.ignore`),
|
|
823
|
+
...failed ? [] : [input.summary],
|
|
824
|
+
...input.notes ?? []
|
|
825
|
+
];
|
|
826
|
+
return {
|
|
827
|
+
code: failed ? 1 : 0,
|
|
828
|
+
output: lines.join("\n"),
|
|
829
|
+
data: report,
|
|
830
|
+
findings: [...report.findings.map((item) => ({
|
|
831
|
+
code: item.code,
|
|
832
|
+
id: item.id,
|
|
833
|
+
severity: item.severity,
|
|
834
|
+
message: item.message,
|
|
835
|
+
docs: VERIFY_DOCS
|
|
836
|
+
})), ...report.stale.map((item) => ({
|
|
837
|
+
code: "ignore-stale",
|
|
838
|
+
id: `ignore-stale:${item.id}`,
|
|
839
|
+
severity: "warning",
|
|
840
|
+
message: `ignore entry ${item.id} matches no finding`,
|
|
841
|
+
fix: "remove it from rls.verify.ignore",
|
|
842
|
+
docs: VERIFY_DOCS
|
|
843
|
+
}))]
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/cli/rls/verify-pgtap-memberships.ts
|
|
848
|
+
function insert(table, columns) {
|
|
849
|
+
return `insert into ${quoteTable(table)} (${columns.map(([name]) => quoteIdent(name)).join(", ")}) values (${columns.map(([, value]) => quoteLiteral(value)).join(", ")}) on conflict do nothing;`;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* The rows of a table mapped by `rls.memberships`: one per role, for a plain
|
|
853
|
+
* role column.
|
|
854
|
+
*/
|
|
855
|
+
function mappedRows(config, scopes, user, membership) {
|
|
856
|
+
const { scope, id } = membership;
|
|
857
|
+
if (scope === void 0 || id === void 0) return [];
|
|
858
|
+
const table = scopeMembershipTable(config.rls?.memberships, scopes, scope);
|
|
859
|
+
const idColumn = table === void 0 ? void 0 : scopeColumn(table, scopes, scope);
|
|
860
|
+
const roleColumn = table?.role;
|
|
861
|
+
if (table === void 0 || idColumn === void 0 || typeof roleColumn !== "string") return [];
|
|
862
|
+
const rootName = rootScope(scopes);
|
|
863
|
+
const tenantColumn = rootName === void 0 || rootName === scope ? void 0 : scopeColumn(table, scopes, rootName);
|
|
864
|
+
const tenant = rootName === void 0 ? void 0 : membership.within?.[rootName];
|
|
865
|
+
const extra = [];
|
|
866
|
+
for (const [name, value] of Object.entries(membership.within ?? {})) {
|
|
867
|
+
const column = scopeColumn(table, scopes, name);
|
|
868
|
+
if (column !== void 0 && name !== scope) extra.push([column, value]);
|
|
869
|
+
}
|
|
870
|
+
if (tenantColumn !== void 0 && tenant !== void 0 && !extra.some(([column]) => column === tenantColumn)) extra.push([tenantColumn, tenant]);
|
|
871
|
+
return membership.roles.map((role) => insert(table.table, [
|
|
872
|
+
[table.user, user],
|
|
873
|
+
[idColumn, id],
|
|
874
|
+
...extra,
|
|
875
|
+
[roleColumn, role]
|
|
876
|
+
]));
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* The rows of a `membershipSources` source: only a source whose columns are
|
|
880
|
+
* plain, never a `through` or `map`.
|
|
881
|
+
*/
|
|
882
|
+
function sourceRows(source, scopes, user, membership) {
|
|
883
|
+
const { scope, id } = membership;
|
|
884
|
+
if (scope === void 0 || id === void 0) return [];
|
|
885
|
+
const names = new Set(scopes.map((entry) => entry.name));
|
|
886
|
+
if ("through" in source.user || !names.has(scope) || "value" in source.scope && source.scope.value !== scope) return [];
|
|
887
|
+
const role = source.role;
|
|
888
|
+
const roleColumns = Array.isArray(role) || !("column" in role) || "through" in role ? void 0 : "map" in role ? void 0 : role.column;
|
|
889
|
+
const fixed = "value" in source.scope ? [] : [[source.scope.column, scope]];
|
|
890
|
+
const within = source.within;
|
|
891
|
+
const ancestors = within === void 0 || !("columns" in within) ? [] : Object.entries(within.columns).flatMap(([name, column]) => {
|
|
892
|
+
const value = membership.within?.[name];
|
|
893
|
+
return value === void 0 ? [] : [[column, value]];
|
|
894
|
+
});
|
|
895
|
+
const base = [
|
|
896
|
+
[source.user.column, user],
|
|
897
|
+
[source.id.column, id],
|
|
898
|
+
...fixed,
|
|
899
|
+
...ancestors
|
|
900
|
+
];
|
|
901
|
+
if (roleColumns === void 0) return [];
|
|
902
|
+
return membership.roles.map((value) => insert(source.table, [...base, [roleColumns, value]]));
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* The statements that put a fixture's memberships into the configured
|
|
906
|
+
* membership tables and sources, so the helpers of `authorize: 'database'` find
|
|
907
|
+
* them. A membership no plain table or source can hold (a `through` or mapped
|
|
908
|
+
* role column, an `on` membership) is listed in a comment.
|
|
909
|
+
*/
|
|
910
|
+
function fixtureMembershipSql(config, scopes, fixture, sources) {
|
|
911
|
+
const lines = [];
|
|
912
|
+
for (const membership of normalizeMemberships(fixture.subject.memberships ?? [], scopes)) {
|
|
913
|
+
const rows = [...mappedRows(config, scopes, fixture.subject.id, membership), ...sources.flatMap((source) => sourceRows(source, scopes, fixture.subject.id, membership))];
|
|
914
|
+
if (rows.length === 0) lines.push(`-- no membership table holds ${membership.scope ?? "on"} ${membership.id ?? ""}: not seeded`);
|
|
915
|
+
lines.push(...rows);
|
|
916
|
+
}
|
|
917
|
+
return lines;
|
|
918
|
+
}
|
|
919
|
+
//#endregion
|
|
700
920
|
//#region src/cli/rls/verify-pgtap.ts
|
|
701
|
-
/**
|
|
921
|
+
/**
|
|
922
|
+
* A custom role's scope and pinned instance, as the `custom_role_*` tables
|
|
923
|
+
* store them.
|
|
924
|
+
*/
|
|
702
925
|
function customRoleAt(role, scopes) {
|
|
703
926
|
return {
|
|
704
927
|
scope: customRoleScope(role, scopes),
|
|
@@ -751,7 +974,10 @@ function statementFor(fixture, action, table) {
|
|
|
751
974
|
function rowId(row) {
|
|
752
975
|
if (isRecord(row) && "id" in row) return row["id"];
|
|
753
976
|
}
|
|
754
|
-
/**
|
|
977
|
+
/**
|
|
978
|
+
* A fixture value as a SQL literal; untyped, so it coerces to the column like a
|
|
979
|
+
* bound parameter.
|
|
980
|
+
*/
|
|
755
981
|
function sqlValue(value) {
|
|
756
982
|
if (value === null || value === void 0) return "null";
|
|
757
983
|
return quoteLiteral(typeof value === "string" ? value : JSON.stringify(value));
|
|
@@ -784,12 +1010,24 @@ function subjectOf(config, customRoles, scopes) {
|
|
|
784
1010
|
scopes
|
|
785
1011
|
};
|
|
786
1012
|
}
|
|
787
|
-
/**
|
|
1013
|
+
/**
|
|
1014
|
+
* The settings that bind a fixture's subject: the GUCs, or the Supabase JWT
|
|
1015
|
+
* claims.
|
|
1016
|
+
*/
|
|
788
1017
|
function subjectSettings(fixture, subject) {
|
|
789
1018
|
const roles = fixture.subject.roles ?? [];
|
|
790
1019
|
const memberships = membershipsClaim(fixture.subject.memberships ?? [], subject.customRoles, subject.scopes);
|
|
791
1020
|
if (subject.dialect === "guc") {
|
|
1021
|
+
const fixed = /* @__PURE__ */ new Set([
|
|
1022
|
+
"user_id",
|
|
1023
|
+
"sub",
|
|
1024
|
+
"role",
|
|
1025
|
+
"memberships",
|
|
1026
|
+
subject.roleClaim,
|
|
1027
|
+
subject.tenantClaim
|
|
1028
|
+
]);
|
|
792
1029
|
const settings = [
|
|
1030
|
+
...Object.entries(fixture.subject.claims ?? {}).flatMap(([name, value]) => fixed.has(name) || !SQL_IDENT.test(name) ? [] : [[`${subject.gucPrefix}.${name}`, typeof value === "string" ? value : JSON.stringify(value)]]),
|
|
793
1031
|
[`${subject.gucPrefix}.user_id`, fixture.subject.id],
|
|
794
1032
|
[`${subject.gucPrefix}.${subject.roleClaim}`, roles.join(",")],
|
|
795
1033
|
[`${subject.gucPrefix}.memberships`, JSON.stringify(memberships)]
|
|
@@ -807,7 +1045,10 @@ function subjectSettings(fixture, subject) {
|
|
|
807
1045
|
};
|
|
808
1046
|
return [["request.jwt.claims", JSON.stringify(claims)]];
|
|
809
1047
|
}
|
|
810
|
-
/**
|
|
1048
|
+
/**
|
|
1049
|
+
* The `custom_role_*` rows for the fixtures' custom roles; existing rows are
|
|
1050
|
+
* kept.
|
|
1051
|
+
*/
|
|
811
1052
|
function customRoleRows(schema, customRoles, scopes) {
|
|
812
1053
|
const table = (name) => `${quoteIdent(schema)}.${quoteIdent(name)}`;
|
|
813
1054
|
const rows = [];
|
|
@@ -839,16 +1080,19 @@ function customRoleRows(schema, customRoles, scopes) {
|
|
|
839
1080
|
}
|
|
840
1081
|
return rows;
|
|
841
1082
|
}
|
|
842
|
-
function
|
|
1083
|
+
function databaseMode(config) {
|
|
843
1084
|
const rls = config.rls;
|
|
844
|
-
return
|
|
1085
|
+
return (rls?.authorize ?? rls?.rbac?.authorize ?? "jwt") === "database";
|
|
1086
|
+
}
|
|
1087
|
+
function seedsCustomRoleTables(config) {
|
|
1088
|
+
return customRolesOn(config.rls) && databaseMode(config);
|
|
845
1089
|
}
|
|
846
1090
|
function customRoleSchema(config) {
|
|
847
1091
|
return config.rls?.schema ?? config.rls?.rbac?.schema ?? "permdock";
|
|
848
1092
|
}
|
|
849
1093
|
/**
|
|
850
|
-
* A pgTAP script that runs each fixture's statement under the fixture's
|
|
851
|
-
*
|
|
1094
|
+
* A pgTAP script that runs each fixture's statement under the fixture's subject
|
|
1095
|
+
* and asserts the outcome `can()` gave in-process.
|
|
852
1096
|
*/
|
|
853
1097
|
function emitPgtap(input) {
|
|
854
1098
|
const scopes = scopeList(input.policy.scopes);
|
|
@@ -859,6 +1103,7 @@ function emitPgtap(input) {
|
|
|
859
1103
|
PGTAP_DECISION_SQL
|
|
860
1104
|
];
|
|
861
1105
|
if (seedsCustomRoleTables(input.config)) for (const row of customRoleRows(customRoleSchema(input.config), input.customRoles, scopes)) lines.push(`${inlineStatement(row)};`);
|
|
1106
|
+
const sources = membershipSourcesOf(input.config).map((source) => source.sql.manifest);
|
|
862
1107
|
for (const [index, fixture] of input.fixtures.entries()) {
|
|
863
1108
|
const permission = findPermission(input.policy.permissions, fixture.action);
|
|
864
1109
|
const status = input.inProcess[index];
|
|
@@ -868,359 +1113,19 @@ function emitPgtap(input) {
|
|
|
868
1113
|
lines.push(`select skip(${quoteLiteral(`${label}: opaque grant untestable app-side`)}, 1);`);
|
|
869
1114
|
continue;
|
|
870
1115
|
}
|
|
1116
|
+
if (input.config.rls?.tables?.[permission.resource] === false) {
|
|
1117
|
+
lines.push(`select skip(${quoteLiteral(`${label}: rls.tables.${permission.resource} is false, so there is no table`)}, 1);`);
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
871
1120
|
const outcome = status.granted ? "granted" : "denied";
|
|
872
|
-
const table = input.config.rls?.tables?.[permission.resource]
|
|
1121
|
+
const table = tableNameOf(input.config.rls?.tables?.[permission.resource], permission.resource);
|
|
873
1122
|
const statement = inlineStatement(statementFor(fixture, permission.action, table));
|
|
874
|
-
lines.push(`-- ${label}`, "savepoint permdock_fixture;", "set local role \"authenticated\";", ...subjectSettings(fixture, subject).map(([name, value]) => `select set_config(${quoteLiteral(name)}, ${quoteLiteral(value)}, true);`), `select is(${PGTAP_DECISION}(${quoteLiteral(statement)}), ${quoteLiteral(outcome)}, ${quoteLiteral(`${label} ${outcome}`)});`, "rollback to savepoint permdock_fixture;");
|
|
1123
|
+
lines.push(`-- ${label}`, "savepoint permdock_fixture;", ...databaseMode(input.config) ? fixtureMembershipSql(input.config, scopes, fixture, sources) : [], "set local role \"authenticated\";", ...subjectSettings(fixture, subject).map(([name, value]) => `select set_config(${quoteLiteral(name)}, ${quoteLiteral(value)}, true);`), `select is(${PGTAP_DECISION}(${quoteLiteral(statement)}), ${quoteLiteral(outcome)}, ${quoteLiteral(`${label} ${outcome}`)});`, "rollback to savepoint permdock_fixture;");
|
|
875
1124
|
}
|
|
876
1125
|
lines.push("select * from finish();", "rollback;");
|
|
877
1126
|
return `${lines.join("\n")}\n`;
|
|
878
1127
|
}
|
|
879
1128
|
//#endregion
|
|
880
|
-
//#region src/cli/rls/graph.ts
|
|
881
|
-
function tableFor(resource, tables) {
|
|
882
|
-
return tables?.[resource] ?? resource;
|
|
883
|
-
}
|
|
884
|
-
/** Every `related` condition the policy's grants compile to, with the resource graph they need, groups included. */
|
|
885
|
-
function graphPlan(policy) {
|
|
886
|
-
const scopes = scopeList(policy.scopes);
|
|
887
|
-
const plan = /* @__PURE__ */ new Map();
|
|
888
|
-
const entryFor = (node) => {
|
|
889
|
-
const entry = plan.get(node.name) ?? {
|
|
890
|
-
node,
|
|
891
|
-
relations: /* @__PURE__ */ new Set(),
|
|
892
|
-
links: /* @__PURE__ */ new Set()
|
|
893
|
-
};
|
|
894
|
-
plan.set(node.name, entry);
|
|
895
|
-
return entry;
|
|
896
|
-
};
|
|
897
|
-
const addRelation = (node, relation) => {
|
|
898
|
-
const entry = entryFor(node);
|
|
899
|
-
if (entry.relations.has(relation)) return;
|
|
900
|
-
entry.relations.add(relation);
|
|
901
|
-
for (const name of expandRelation(node, relation)) {
|
|
902
|
-
const spec = node.relations[name];
|
|
903
|
-
if (!isEdgeRelation(spec) || spec.groups === void 0) continue;
|
|
904
|
-
for (const { resource, relation: groupRelation } of groupEntries(spec)) {
|
|
905
|
-
const target = policy.resources.get(resource);
|
|
906
|
-
if (target !== void 0 && target.name !== node.name) addRelation(target, groupRelation);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
};
|
|
910
|
-
const addLinks = (hops) => {
|
|
911
|
-
for (let index = 1; index < hops.length; index += 1) {
|
|
912
|
-
const from = policy.resources.get(hops[index - 1]?.resource ?? "");
|
|
913
|
-
const hop = hops[index];
|
|
914
|
-
if (from !== void 0 && hop !== void 0) entryFor(from).links.add(hop.link);
|
|
915
|
-
}
|
|
916
|
-
};
|
|
917
|
-
const addAbove = (above) => {
|
|
918
|
-
const node = above === void 0 ? void 0 : policy.resources.get(above.resource);
|
|
919
|
-
if (above === void 0 || node === void 0) return;
|
|
920
|
-
const entry = entryFor(node);
|
|
921
|
-
entry.closure = Math.max(entry.closure ?? 0, above.depth);
|
|
922
|
-
entry.restrictedAncestors = above;
|
|
923
|
-
};
|
|
924
|
-
for (const grant of policy.grants) {
|
|
925
|
-
const row = policy.resources.get(grant.permission.resource);
|
|
926
|
-
for (const item of flattenGrantee(grant.to)) {
|
|
927
|
-
if (item.kind === "inherit") {
|
|
928
|
-
const inherited = inheritCondition(item, row, policy.resources);
|
|
929
|
-
addLinks(inherited?.hops ?? []);
|
|
930
|
-
addAbove(inherited?.restrictedAncestors);
|
|
931
|
-
continue;
|
|
932
|
-
}
|
|
933
|
-
if (item.kind !== "relation") continue;
|
|
934
|
-
const condition = relationCondition(item, row, scopes, { resources: policy.resources });
|
|
935
|
-
if (condition?.op !== "related") continue;
|
|
936
|
-
const node = policy.resources.get(condition.resource);
|
|
937
|
-
if (node === void 0) continue;
|
|
938
|
-
addRelation(node, condition.relation);
|
|
939
|
-
const entry = entryFor(node);
|
|
940
|
-
if (condition.depth > 0 && isSelfParented(node)) entry.closure = Math.max(entry.closure ?? 0, condition.depth);
|
|
941
|
-
addLinks(condition.hops ?? []);
|
|
942
|
-
addAbove(condition.restrictedAncestors);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
return plan;
|
|
946
|
-
}
|
|
947
|
-
/** The closure depth per walked resource, as `RlsSqlContext.graph` carries it. */
|
|
948
|
-
function closureDepths(plan) {
|
|
949
|
-
const out = {};
|
|
950
|
-
for (const [name, entry] of plan) if (entry.closure !== void 0) out[name] = entry.closure;
|
|
951
|
-
return out;
|
|
952
|
-
}
|
|
953
|
-
/**
|
|
954
|
-
* The `permitted_<resource>_ids(relation)` arm for one concrete relation:
|
|
955
|
-
* ids of `resource` the subject holds it on, directly or through a group.
|
|
956
|
-
* `asked` are the relation names that include it, so a call for any of them
|
|
957
|
-
* (or for all, with null) reads this arm.
|
|
958
|
-
*/
|
|
959
|
-
function relationArm(ctx, plan, node, relation, asked, tables) {
|
|
960
|
-
const spec = node.relations[relation];
|
|
961
|
-
if (!isEdgeRelation(spec) && !isPrincipalRelation(spec) && (!isFieldRelation(spec) || spec.memberOf !== void 0)) throw configError(`PermDock CLI: relation '${relation}' on ${node.name} is not a graph relation RLS can compile`);
|
|
962
|
-
const guard = asked.length === 1 ? `(p_relation is null or p_relation = ${quoteLiteral(asked[0] ?? relation)})` : `(p_relation is null or p_relation in (${asked.map(quoteLiteral).join(", ")}))`;
|
|
963
|
-
return ` select a.id from (${graphSqlText(relationArmSql(node.name, relation, {
|
|
964
|
-
resources: new Map([...plan].map(([name, item]) => [name, item.node])),
|
|
965
|
-
...tables === void 0 ? {} : { tables },
|
|
966
|
-
qualify: qualifySqlTable,
|
|
967
|
-
holders: (resource, groupRelation) => [{ text: `select ${qualified(ctx, graphHelper(resource))}(${quoteLiteral(groupRelation)})` }]
|
|
968
|
-
}), ctx)}) a\n where ${guard}${activeUserSql(ctx).map((part) => ` and ${part}`).join("")}`;
|
|
969
|
-
}
|
|
970
|
-
function permittedSql(ctx, plan, entry, tables) {
|
|
971
|
-
const fn = qualified(ctx, graphHelper(entry.node.name));
|
|
972
|
-
const asked = [...entry.relations].toSorted();
|
|
973
|
-
const arms = [...new Set(asked.flatMap((name) => expandRelation(entry.node, name)))].toSorted().map((relation) => relationArm(ctx, plan, entry.node, relation, asked.filter((name) => expandRelation(entry.node, name).includes(relation)), tables));
|
|
974
|
-
if (arms.length === 0) arms.push(" select null::text where false");
|
|
975
|
-
return `-- ${entry.node.name}: ids the subject holds a relation on (all relations when p_relation is null)
|
|
976
|
-
create or replace function ${fn}(p_relation text)
|
|
977
|
-
returns setof text
|
|
978
|
-
language sql
|
|
979
|
-
stable
|
|
980
|
-
security definer
|
|
981
|
-
set search_path = ''
|
|
982
|
-
as $$
|
|
983
|
-
${arms.join("\n union\n")}
|
|
984
|
-
$$;
|
|
985
|
-
revoke execute on function ${fn}(text) from public, anon;
|
|
986
|
-
grant execute on function ${fn}(text) to authenticated;`;
|
|
987
|
-
}
|
|
988
|
-
/** `permdock_link_<resource>_<link>(ids)`: ids of `resource` whose link points into `ids`, read past the table's own policies. */
|
|
989
|
-
function linkSql(ctx, node, link, tables) {
|
|
990
|
-
const spec = Object.hasOwn(node.links, link) ? node.links[link] : void 0;
|
|
991
|
-
if (spec === void 0) throw configError(`PermDock CLI: ${node.name} has no link '${link}'`);
|
|
992
|
-
const fn = qualified(ctx, linkHelper(node.name, link));
|
|
993
|
-
return `-- ${node.name}.${link}: rows whose ${spec.field} points at one of the given ${spec.resource} ids
|
|
994
|
-
create or replace function ${fn}(p_ids text[])
|
|
995
|
-
returns setof text
|
|
996
|
-
language sql
|
|
997
|
-
stable
|
|
998
|
-
security definer
|
|
999
|
-
set search_path = ''
|
|
1000
|
-
as $$
|
|
1001
|
-
select t.${quoteIdent(node.id)}::text from ${quoteQualified(tableFor(node.name, tables))} t
|
|
1002
|
-
where t.${quoteIdent(spec.field)}::text = any(p_ids)
|
|
1003
|
-
$$;
|
|
1004
|
-
revoke execute on function ${fn}(text[]) from public, anon;
|
|
1005
|
-
grant execute on function ${fn}(text[]) to authenticated;`;
|
|
1006
|
-
}
|
|
1007
|
-
function closureTableSql(ctx) {
|
|
1008
|
-
const table = qualified(ctx, CLOSURE.table);
|
|
1009
|
-
return `-- the object graph: one row per (ancestor, descendant) within each walked resource's depth,
|
|
1010
|
-
-- never crossing a restricted row; kept current by statement triggers on each self-parented table
|
|
1011
|
-
create table if not exists ${table} (
|
|
1012
|
-
resource text not null,
|
|
1013
|
-
ancestor text not null,
|
|
1014
|
-
descendant text not null,
|
|
1015
|
-
depth integer not null check (depth >= 0),
|
|
1016
|
-
primary key (resource, descendant, ancestor)
|
|
1017
|
-
);
|
|
1018
|
-
create index if not exists ${quoteIdent(`${CLOSURE.table}_ancestor`)} on ${table} (resource, ancestor);
|
|
1019
|
-
alter table ${table} enable row level security;
|
|
1020
|
-
revoke all on table ${table} from anon, authenticated, public;
|
|
1021
|
-
grant select on table ${table} to authenticated;`;
|
|
1022
|
-
}
|
|
1023
|
-
function closureObjectsSql(ctx, entry, depth, tables) {
|
|
1024
|
-
const { node } = entry;
|
|
1025
|
-
const parent = node.parent;
|
|
1026
|
-
if (parent === void 0) return "";
|
|
1027
|
-
const name = node.name;
|
|
1028
|
-
const sqlName = graphSqlName(name) ?? name;
|
|
1029
|
-
const closure = qualified(ctx, CLOSURE.table);
|
|
1030
|
-
const table = quoteQualified(tableFor(name, tables));
|
|
1031
|
-
const id = quoteIdent(node.id);
|
|
1032
|
-
const up = quoteIdent(parent.field);
|
|
1033
|
-
const restricted = restrictedFor(node, "parent");
|
|
1034
|
-
const stop = restricted === void 0 ? "false" : `coalesce(t.${quoteIdent(restricted)}, false)`;
|
|
1035
|
-
const stopParent = restricted === void 0 ? "false" : `coalesce(p.${quoteIdent(restricted)}, false)`;
|
|
1036
|
-
const refresh = qualified(ctx, `${CLOSURE.table}_${sqlName}`);
|
|
1037
|
-
const changed = qualified(ctx, `${CLOSURE.table}_${sqlName}_changed`);
|
|
1038
|
-
const literal = quoteLiteral(name);
|
|
1039
|
-
const restrictedChanged = restricted === void 0 ? "" : ` or n.${quoteIdent(restricted)} is distinct from o.${quoteIdent(restricted)}`;
|
|
1040
|
-
const trigger = (suffix) => quoteIdent(`${CLOSURE.table}_${sqlName}_${suffix}`);
|
|
1041
|
-
return `-- ${name}: ${restricted === void 0 ? `rows reach their ancestors up to depth ${String(depth)}` : `rows reach their ancestors up to depth ${String(depth)}, stopping at a restricted row`}
|
|
1042
|
-
drop policy if exists ${quoteIdent(`${CLOSURE.table}_${sqlName}`)} on ${closure};
|
|
1043
|
-
create policy ${quoteIdent(`${CLOSURE.table}_${sqlName}`)} on ${closure}
|
|
1044
|
-
for select to authenticated
|
|
1045
|
-
using (resource = ${literal} and ancestor in (select ${qualified(ctx, graphHelper(name))}(null)));
|
|
1046
|
-
create or replace function ${refresh}(p_ids text[])
|
|
1047
|
-
returns void
|
|
1048
|
-
language plpgsql
|
|
1049
|
-
security definer
|
|
1050
|
-
set search_path = ''
|
|
1051
|
-
as $$
|
|
1052
|
-
declare
|
|
1053
|
-
v_cycle text;
|
|
1054
|
-
v_affected text[];
|
|
1055
|
-
begin
|
|
1056
|
-
with recursive walk(start, next, path) as (
|
|
1057
|
-
select t.${id}, t.${up}, array[t.${id}::text]
|
|
1058
|
-
from ${table} t
|
|
1059
|
-
where t.${id}::text = any(p_ids)
|
|
1060
|
-
union all
|
|
1061
|
-
select walk.start, t.${up}, walk.path || t.${id}::text
|
|
1062
|
-
from walk join ${table} t on t.${id} = walk.next
|
|
1063
|
-
where not (walk.next::text = any(walk.path))
|
|
1064
|
-
)
|
|
1065
|
-
select walk.start::text into v_cycle
|
|
1066
|
-
from walk
|
|
1067
|
-
where walk.next::text = any(walk.path)
|
|
1068
|
-
limit 1;
|
|
1069
|
-
if v_cycle is not null then
|
|
1070
|
-
raise exception 'permdock: % % is its own ancestor', ${literal}, v_cycle
|
|
1071
|
-
using errcode = 'check_violation';
|
|
1072
|
-
end if;
|
|
1073
|
-
with recursive down(id, level) as (
|
|
1074
|
-
select t.${id}, 0
|
|
1075
|
-
from ${table} t
|
|
1076
|
-
where t.${id}::text = any(p_ids) or t.${up}::text = any(p_ids)
|
|
1077
|
-
union
|
|
1078
|
-
select t.${id}, down.level + 1
|
|
1079
|
-
from down join ${table} t on t.${up} = down.id
|
|
1080
|
-
where down.level < ${String(depth)}
|
|
1081
|
-
)
|
|
1082
|
-
select coalesce(array_agg(distinct down.id::text), '{}') into v_affected from down;
|
|
1083
|
-
delete from ${closure} c
|
|
1084
|
-
where c.resource = ${literal}
|
|
1085
|
-
and (c.descendant = any(p_ids) or c.descendant = any(v_affected));
|
|
1086
|
-
insert into ${closure} (resource, ancestor, descendant, depth)
|
|
1087
|
-
with recursive walk(descendant, ancestor, parent, depth, stop) as (
|
|
1088
|
-
select t.${id}::text, t.${id}, t.${up}, 0, ${stop}
|
|
1089
|
-
from ${table} t
|
|
1090
|
-
where t.${id}::text = any(v_affected)
|
|
1091
|
-
union all
|
|
1092
|
-
select walk.descendant, p.${id}, p.${up}, walk.depth + 1, ${stopParent}
|
|
1093
|
-
from walk join ${table} p on p.${id} = walk.parent
|
|
1094
|
-
where not walk.stop and walk.depth < ${String(depth)}
|
|
1095
|
-
)
|
|
1096
|
-
select ${literal}, walk.ancestor::text, walk.descendant, walk.depth from walk
|
|
1097
|
-
on conflict do nothing;
|
|
1098
|
-
end
|
|
1099
|
-
$$;
|
|
1100
|
-
revoke execute on function ${refresh}(text[]) from public, anon, authenticated;
|
|
1101
|
-
create or replace function ${changed}()
|
|
1102
|
-
returns trigger
|
|
1103
|
-
language plpgsql
|
|
1104
|
-
security definer
|
|
1105
|
-
set search_path = ''
|
|
1106
|
-
as $$
|
|
1107
|
-
begin
|
|
1108
|
-
if tg_op = 'TRUNCATE' then
|
|
1109
|
-
delete from ${closure} where resource = ${literal};
|
|
1110
|
-
elsif tg_op = 'INSERT' then
|
|
1111
|
-
perform ${refresh}(array(select n.${id}::text from new_rows n));
|
|
1112
|
-
elsif tg_op = 'DELETE' then
|
|
1113
|
-
perform ${refresh}(array(select o.${id}::text from old_rows o));
|
|
1114
|
-
else
|
|
1115
|
-
perform ${refresh}(array(
|
|
1116
|
-
select n.${id}::text
|
|
1117
|
-
from new_rows n left join old_rows o on o.${id} = n.${id}
|
|
1118
|
-
where o.${id} is null or n.${up} is distinct from o.${up}${restrictedChanged}
|
|
1119
|
-
union
|
|
1120
|
-
select o.${id}::text
|
|
1121
|
-
from old_rows o left join new_rows n on n.${id} = o.${id}
|
|
1122
|
-
where n.${id} is null
|
|
1123
|
-
));
|
|
1124
|
-
end if;
|
|
1125
|
-
return null;
|
|
1126
|
-
end
|
|
1127
|
-
$$;
|
|
1128
|
-
revoke execute on function ${changed}() from public, anon, authenticated;
|
|
1129
|
-
drop trigger if exists ${trigger("insert")} on ${table};
|
|
1130
|
-
create trigger ${trigger("insert")} after insert on ${table}
|
|
1131
|
-
referencing new table as new_rows
|
|
1132
|
-
for each statement execute function ${changed}();
|
|
1133
|
-
drop trigger if exists ${trigger("update")} on ${table};
|
|
1134
|
-
create trigger ${trigger("update")} after update on ${table}
|
|
1135
|
-
referencing old table as old_rows new table as new_rows
|
|
1136
|
-
for each statement execute function ${changed}();
|
|
1137
|
-
drop trigger if exists ${trigger("delete")} on ${table};
|
|
1138
|
-
create trigger ${trigger("delete")} after delete on ${table}
|
|
1139
|
-
referencing old table as old_rows
|
|
1140
|
-
for each statement execute function ${changed}();
|
|
1141
|
-
drop trigger if exists ${trigger("truncate")} on ${table};
|
|
1142
|
-
create trigger ${trigger("truncate")} after truncate on ${table}
|
|
1143
|
-
for each statement execute function ${changed}();
|
|
1144
|
-
select ${refresh}(array(select t.${id}::text from ${table} t));`;
|
|
1145
|
-
}
|
|
1146
|
-
function restrictedRowsHelperSql(ctx, plan, entry, tables) {
|
|
1147
|
-
const above = entry.restrictedAncestors;
|
|
1148
|
-
if (above === void 0) return "";
|
|
1149
|
-
const fn = qualified(ctx, restrictedHelper(entry.node.name));
|
|
1150
|
-
const body = graphSqlText(restrictedRowsSql(above, {
|
|
1151
|
-
resources: new Map([...plan].map(([name, item]) => [name, item.node])),
|
|
1152
|
-
...tables === void 0 ? {} : { tables },
|
|
1153
|
-
qualify: qualifySqlTable,
|
|
1154
|
-
closure: `${helperSchema(ctx)}.${CLOSURE.table}`,
|
|
1155
|
-
closureDepths: closureDepths(plan)
|
|
1156
|
-
}), ctx);
|
|
1157
|
-
return `-- ${entry.node.name}: rows a stopped link does not reach below: restricted, under a restricted row within ${String(above.depth - 1)} levels, or deeper
|
|
1158
|
-
create or replace function ${fn}()
|
|
1159
|
-
returns setof text
|
|
1160
|
-
language sql
|
|
1161
|
-
stable
|
|
1162
|
-
security definer
|
|
1163
|
-
set search_path = ''
|
|
1164
|
-
as $$
|
|
1165
|
-
${body}
|
|
1166
|
-
$$;
|
|
1167
|
-
revoke execute on function ${fn}() from public, anon;
|
|
1168
|
-
grant execute on function ${fn}() to authenticated;`;
|
|
1169
|
-
}
|
|
1170
|
-
/** Group resources a resource's asked relations name, other than itself. */
|
|
1171
|
-
function groupTargets(entry) {
|
|
1172
|
-
const out = /* @__PURE__ */ new Set();
|
|
1173
|
-
for (const asked of entry.relations) for (const name of expandRelation(entry.node, asked)) {
|
|
1174
|
-
const spec = entry.node.relations[name];
|
|
1175
|
-
const resources = isEdgeRelation(spec) ? groupEntries(spec).map((group) => group.resource) : [];
|
|
1176
|
-
for (const resource of resources) if (resource !== entry.node.name) out.add(resource);
|
|
1177
|
-
}
|
|
1178
|
-
return [...out].toSorted();
|
|
1179
|
-
}
|
|
1180
|
-
/** Plan entries by name, each after the group resources its helper calls (SQL function bodies are checked on create). */
|
|
1181
|
-
function dependencyOrder(plan) {
|
|
1182
|
-
const out = [];
|
|
1183
|
-
const done = /* @__PURE__ */ new Set();
|
|
1184
|
-
const visit = (name) => {
|
|
1185
|
-
const entry = plan.get(name);
|
|
1186
|
-
if (entry === void 0 || done.has(name)) return;
|
|
1187
|
-
done.add(name);
|
|
1188
|
-
for (const target of groupTargets(entry)) visit(target);
|
|
1189
|
-
out.push(entry);
|
|
1190
|
-
};
|
|
1191
|
-
for (const name of [...plan.keys()].toSorted()) visit(name);
|
|
1192
|
-
return out;
|
|
1193
|
-
}
|
|
1194
|
-
/**
|
|
1195
|
-
* The graph preamble: `permitted_<resource>_ids` for every resource a graph
|
|
1196
|
-
* grant reads, and the closure table with its triggers for every
|
|
1197
|
-
* self-parented resource a grant walks. Empty when no grant is a graph grant.
|
|
1198
|
-
*/
|
|
1199
|
-
function graphSql(ctx, plan, tables) {
|
|
1200
|
-
if (plan.size === 0) return "";
|
|
1201
|
-
const sqlNames = /* @__PURE__ */ new Map();
|
|
1202
|
-
for (const name of plan.keys()) {
|
|
1203
|
-
const helper = graphHelper(name);
|
|
1204
|
-
const sql = graphSqlName(name) ?? name;
|
|
1205
|
-
if (ctx.scopes.some((scope) => scope.name === sql)) throw configError(`PermDock CLI: resource '${name}' has graph relations and shares its SQL name with the ${sql} scope, so ${helper} would replace the scope helper; rename the resource (permdock doctor PD032)`);
|
|
1206
|
-
const other = sqlNames.get(sql);
|
|
1207
|
-
if (other !== void 0) throw configError(`PermDock CLI: resources '${other}' and '${name}' both name the SQL helper ${helper}; rename one (permdock doctor PD032)`);
|
|
1208
|
-
sqlNames.set(sql, name);
|
|
1209
|
-
}
|
|
1210
|
-
const entries = dependencyOrder(plan);
|
|
1211
|
-
const chunks = entries.flatMap((entry) => [...entry.relations.size === 0 && entry.closure === void 0 ? [] : [permittedSql(ctx, plan, entry, tables)], ...[...entry.links].toSorted().map((link) => linkSql(ctx, entry.node, link, tables))]);
|
|
1212
|
-
const walked = entries.filter((entry) => entry.closure !== void 0);
|
|
1213
|
-
if (walked.length > 0) {
|
|
1214
|
-
chunks.push(closureTableSql(ctx));
|
|
1215
|
-
for (const entry of walked) chunks.push(closureObjectsSql(ctx, entry, entry.closure ?? 0, tables));
|
|
1216
|
-
for (const entry of walked) {
|
|
1217
|
-
const helper = restrictedRowsHelperSql(ctx, plan, entry, tables);
|
|
1218
|
-
if (helper !== "") chunks.push(helper);
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
return `${chunks.join("\n\n")}\n`;
|
|
1222
|
-
}
|
|
1223
|
-
//#endregion
|
|
1224
1129
|
//#region src/cli/rls/verify-tree.ts
|
|
1225
1130
|
const SYSTEM_ENTROPY = Object.freeze({
|
|
1226
1131
|
now: () => nowMillis(),
|
|
@@ -1236,9 +1141,9 @@ function idFactory(node, entropy) {
|
|
|
1236
1141
|
} : () => entropy.randomUUID();
|
|
1237
1142
|
}
|
|
1238
1143
|
/**
|
|
1239
|
-
* Two spines one level deeper than the walk depth, so the depth cap is
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1144
|
+
* Two spines one level deeper than the walk depth, so the depth cap is crossed;
|
|
1145
|
+
* side branches under the first levels alternate restricted, and the second
|
|
1146
|
+
* spine is restricted two levels down.
|
|
1242
1147
|
*/
|
|
1243
1148
|
function generateTree(node, depth, entropy) {
|
|
1244
1149
|
const id = idFactory(node, entropy);
|
|
@@ -1359,9 +1264,9 @@ function asRequiredColumn(row) {
|
|
|
1359
1264
|
};
|
|
1360
1265
|
}
|
|
1361
1266
|
/**
|
|
1362
|
-
* Values for the required columns a generated row leaves out, such as a
|
|
1363
|
-
*
|
|
1364
|
-
*
|
|
1267
|
+
* Values for the required columns a generated row leaves out, such as a tenant
|
|
1268
|
+
* column or a name: an existing value of the referenced table for a foreign
|
|
1269
|
+
* key, a placeholder of the column's type otherwise.
|
|
1365
1270
|
*/
|
|
1366
1271
|
async function requiredValues(query, table, set, entropy) {
|
|
1367
1272
|
const columns = (await query(REQUIRED_COLUMNS_SQL, [table])).rows.map(asRequiredColumn).filter((column) => column !== void 0 && !set.has(column.name));
|
|
@@ -1391,15 +1296,15 @@ async function seedRows(query, table, rows, given, entropy) {
|
|
|
1391
1296
|
}
|
|
1392
1297
|
/**
|
|
1393
1298
|
* Parity between `decide` and RLS on a generated object graph: for every
|
|
1394
|
-
* self-parented resource a graph grant walks, a tree with restricted
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1397
|
-
*
|
|
1299
|
+
* self-parented resource a graph grant walks, a tree with restricted branches
|
|
1300
|
+
* and a chain past the walk depth, child rows under it, and edges for a few
|
|
1301
|
+
* generated principals. Everything runs in one transaction that is rolled
|
|
1302
|
+
* back.
|
|
1398
1303
|
*/
|
|
1399
1304
|
async function verifyTree(input) {
|
|
1400
1305
|
const { policy, query } = input;
|
|
1401
1306
|
const entropy = input.entropy ?? SYSTEM_ENTROPY;
|
|
1402
|
-
const tables = input.config.rls?.tables;
|
|
1307
|
+
const tables = tableNames(input.config.rls?.tables);
|
|
1403
1308
|
const treeValues = input.config.rls?.treeValues ?? {};
|
|
1404
1309
|
const seed = (table, list) => seedRows(query, table, list, treeValues[table] ?? {}, entropy);
|
|
1405
1310
|
const walked = [...graphPlan(policy).values()].filter((entry) => entry.closure !== void 0);
|
|
@@ -1506,7 +1411,7 @@ async function verifyTree(input) {
|
|
|
1506
1411
|
checked += 1;
|
|
1507
1412
|
const id = String(row[node.id]);
|
|
1508
1413
|
const allowed = permdock.can(permission, row);
|
|
1509
|
-
if (allowed !== seen.has(id)) mismatches.push(`${permission.key} ${id}: in-process ${allowed ? "granted" : "denied"}, database ${seen.has(id) ? "allowed" : "filtered"}`);
|
|
1414
|
+
if (allowed !== seen.has(id)) mismatches.push(rlsFinding("tree-mismatch", node.name, permission.key, `${permission.key} ${id}: in-process ${allowed ? "granted" : "denied"}, database ${seen.has(id) ? "allowed" : "filtered"}`));
|
|
1510
1415
|
else if (allowed) granted += 1;
|
|
1511
1416
|
}
|
|
1512
1417
|
}
|
|
@@ -1520,6 +1425,10 @@ async function verifyTree(input) {
|
|
|
1520
1425
|
}
|
|
1521
1426
|
//#endregion
|
|
1522
1427
|
//#region src/cli/rls/verify-database.ts
|
|
1428
|
+
/** A fixture's part of a finding id: its subject and row. */
|
|
1429
|
+
function fixtureKey(fixture) {
|
|
1430
|
+
return `${fixture.subject.id}/${String(rowId(fixture.row))}`;
|
|
1431
|
+
}
|
|
1523
1432
|
/** Columns of `row` that hold a value; a masked column reads as null. */
|
|
1524
1433
|
function valuedColumns(row) {
|
|
1525
1434
|
return isRecord(row) ? Object.keys(row).filter((name) => row[name] !== null && row[name] !== void 0).toSorted() : [];
|
|
@@ -1536,10 +1445,14 @@ function expectedFields(granted, picked, key, row) {
|
|
|
1536
1445
|
}
|
|
1537
1446
|
function fieldsMismatch(fixture, want, seen) {
|
|
1538
1447
|
const kept = want.join(", ");
|
|
1539
|
-
|
|
1540
|
-
|
|
1448
|
+
const mismatch = (message) => rlsFinding("field-mismatch", fixture.action, fixtureKey(fixture), message);
|
|
1449
|
+
if (typeof seen === "string") return [mismatch(`${fixture.action}: field view read failed (${seen}), pick keeps [${kept}]`)];
|
|
1450
|
+
return seen.join(", ") === kept ? [] : [mismatch(`${fixture.action} ${String(rowId(fixture.row))}: field view returns [${seen.join(", ")}], pick keeps [${kept}]`)];
|
|
1541
1451
|
}
|
|
1542
|
-
/**
|
|
1452
|
+
/**
|
|
1453
|
+
* The field view's columns for the fixture's row: the view, or the table itself
|
|
1454
|
+
* when no grant limits its fields.
|
|
1455
|
+
*/
|
|
1543
1456
|
async function viewFields(query, table, key) {
|
|
1544
1457
|
const view = quoteTable(viewName(table, FIELD_VIEWS.view));
|
|
1545
1458
|
const id = quoteIdent("id");
|
|
@@ -1551,7 +1464,10 @@ async function viewFields(query, table, key) {
|
|
|
1551
1464
|
const base = await query(`select * from ${quoteTable(table)} where ${id} = $1`, [key]);
|
|
1552
1465
|
return base.code ?? valuedColumns(base.rows[0]);
|
|
1553
1466
|
}
|
|
1554
|
-
/**
|
|
1467
|
+
/**
|
|
1468
|
+
* Writes the custom roles into the `database`-mode tables; the fixture
|
|
1469
|
+
* transaction rolls them back.
|
|
1470
|
+
*/
|
|
1555
1471
|
async function seedCustomRoles(query, schema, customRoles, scopes) {
|
|
1556
1472
|
for (const row of customRoleRows(schema, customRoles, scopes)) {
|
|
1557
1473
|
const result = await query(row.sql, row.values);
|
|
@@ -1590,7 +1506,7 @@ async function verifyAgainstDatabase(input) {
|
|
|
1590
1506
|
const conditioned = policyRowConditionKeys(input.policy);
|
|
1591
1507
|
for (const found of helperTablePoliciesFromRows((await query(HELPER_TABLE_POLICIES_SQL)).rows)) {
|
|
1592
1508
|
const keys = found.keys.filter((key) => conditioned.has(key));
|
|
1593
|
-
if (keys.length > 0) mismatches.push(`PD037 ${rowConditionMessage(found, keys)}`);
|
|
1509
|
+
if (keys.length > 0) mismatches.push(rlsFinding("condition-blind", found.table, found.name, `PD037 ${rowConditionMessage(found, keys)}`));
|
|
1594
1510
|
}
|
|
1595
1511
|
for (const [index, fixture] of input.fixtures.entries()) {
|
|
1596
1512
|
const permission = findPermission(input.policy.permissions, fixture.action);
|
|
@@ -1600,7 +1516,11 @@ async function verifyAgainstDatabase(input) {
|
|
|
1600
1516
|
notes.push(`${fixture.action}: opaque grant untestable app-side`);
|
|
1601
1517
|
continue;
|
|
1602
1518
|
}
|
|
1603
|
-
|
|
1519
|
+
if (input.config.rls?.tables?.[permission.resource] === false) {
|
|
1520
|
+
notes.push(`${fixture.action}: rls.tables.${permission.resource} is false, so there is no table`);
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
const table = tableNameOf(input.config.rls?.tables?.[permission.resource], permission.resource);
|
|
1604
1524
|
await query("begin");
|
|
1605
1525
|
try {
|
|
1606
1526
|
if (seedsTables) await seedCustomRoles(query, schema, input.customRoles, scopes);
|
|
@@ -1609,7 +1529,7 @@ async function verifyAgainstDatabase(input) {
|
|
|
1609
1529
|
const result = await query(statement.sql, statement.values);
|
|
1610
1530
|
const count = result.rowCount ?? result.rows.length;
|
|
1611
1531
|
const database = result.code === "42501" ? "rejected" : count > 0 ? "allowed" : "filtered";
|
|
1612
|
-
if (!(status.granted ? database === "allowed" : database === "filtered" || database === "rejected")) mismatches.push(`${fixture.action}: in-process ${status.granted ? "granted" : "denied"}, database ${database}`);
|
|
1532
|
+
if (!(status.granted ? database === "allowed" : database === "filtered" || database === "rejected")) mismatches.push(rlsFinding("parity-mismatch", fixture.action, fixtureKey(fixture), `${fixture.action}: in-process ${status.granted ? "granted" : "denied"}, database ${database}`));
|
|
1613
1533
|
else if (status.kind === "sqlFunction") notes.push(`${fixture.action}: verified through twin`);
|
|
1614
1534
|
mismatches.push(...status.fields === void 0 ? [] : fieldsMismatch(fixture, status.fields, await viewFields(query, table, rowId(fixture.row))));
|
|
1615
1535
|
} finally {
|
|
@@ -1624,8 +1544,11 @@ async function verifyAgainstDatabase(input) {
|
|
|
1624
1544
|
notes
|
|
1625
1545
|
};
|
|
1626
1546
|
}
|
|
1627
|
-
/**
|
|
1628
|
-
|
|
1547
|
+
/**
|
|
1548
|
+
* `--tree`: seeds a generated object graph in a rolled-back transaction and
|
|
1549
|
+
* compares `can()` with RLS.
|
|
1550
|
+
*/
|
|
1551
|
+
async function verifyTreeAgainstDatabase(db, policy, config, connect, ignore) {
|
|
1629
1552
|
const client = await connect(db);
|
|
1630
1553
|
const scopes = scopeList(policy.scopes);
|
|
1631
1554
|
const query = async (sql, values) => {
|
|
@@ -1647,15 +1570,12 @@ async function verifyTreeAgainstDatabase(db, policy, config, connect) {
|
|
|
1647
1570
|
action: "read"
|
|
1648
1571
|
}, subjectOf(config, [], scopes))
|
|
1649
1572
|
});
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
code: 0,
|
|
1657
|
-
output: result.notes.length === 0 ? verified : `${verified}\n${result.notes.join("\n")}`
|
|
1658
|
-
};
|
|
1573
|
+
return verifyResult({
|
|
1574
|
+
findings: result.mismatches,
|
|
1575
|
+
ignore,
|
|
1576
|
+
notes: result.notes,
|
|
1577
|
+
summary: `verified ${String(result.checked)} tree check(s) against the database (${String(result.granted)} granted)`
|
|
1578
|
+
});
|
|
1659
1579
|
} catch (cause) {
|
|
1660
1580
|
return {
|
|
1661
1581
|
code: 2,
|
|
@@ -1668,6 +1588,8 @@ async function verifyTreeAgainstDatabase(db, policy, config, connect) {
|
|
|
1668
1588
|
}
|
|
1669
1589
|
//#endregion
|
|
1670
1590
|
//#region src/cli/rls/verify.ts
|
|
1591
|
+
/** Where `rls verify --kind pgtap` writes, the folder `supabase test db` reads. */
|
|
1592
|
+
const PGTAP_OUT = "supabase/tests/permdock_rls.test.sql";
|
|
1671
1593
|
function canFixture(permdock, permission, row) {
|
|
1672
1594
|
if (permission.kind === "collection") return permdock.can(permission, row);
|
|
1673
1595
|
return permdock.can(permission, row);
|
|
@@ -1679,6 +1601,11 @@ function grantKind(policy, key) {
|
|
|
1679
1601
|
return "portable";
|
|
1680
1602
|
}
|
|
1681
1603
|
async function runRlsVerify(input) {
|
|
1604
|
+
const ignore = input.ignore ?? [];
|
|
1605
|
+
if (input.out !== void 0 && input.format !== "pgtap") return {
|
|
1606
|
+
code: 2,
|
|
1607
|
+
output: "PermDock CLI: rls verify --out needs --kind pgtap"
|
|
1608
|
+
};
|
|
1682
1609
|
const connect = input.connect ?? pgConnector("permdock rls verify --db-url-stdin", input.cwd);
|
|
1683
1610
|
const policyPath = input.from ?? input.config.policy;
|
|
1684
1611
|
if (policyPath === void 0) return {
|
|
@@ -1692,7 +1619,7 @@ async function runRlsVerify(input) {
|
|
|
1692
1619
|
output: "PermDock CLI: rls verify --tree needs a database: --db-url-stdin, DATABASE_URL or SUPABASE_DB_URL"
|
|
1693
1620
|
};
|
|
1694
1621
|
try {
|
|
1695
|
-
return await verifyTreeAgainstDatabase(input.db, policy, input.config, connect);
|
|
1622
|
+
return await verifyTreeAgainstDatabase(input.db, policy, input.config, connect, ignore);
|
|
1696
1623
|
} catch (cause) {
|
|
1697
1624
|
return usageResult(cause);
|
|
1698
1625
|
}
|
|
@@ -1702,11 +1629,11 @@ async function runRlsVerify(input) {
|
|
|
1702
1629
|
const mismatches = [];
|
|
1703
1630
|
const notes = [];
|
|
1704
1631
|
const inProcess = [];
|
|
1705
|
-
const fieldsMode = input.config.rls?.fields
|
|
1632
|
+
const fieldsMode = input.config.rls?.fields !== void 0;
|
|
1706
1633
|
for (const fixture of fixtures) {
|
|
1707
1634
|
const permission = findPermission(policy.permissions, fixture.action);
|
|
1708
1635
|
if (permission === void 0) {
|
|
1709
|
-
mismatches.push(`${fixture.action}: unknown permission`);
|
|
1636
|
+
mismatches.push(rlsFinding("unknown-permission", fixture.action, fixtureKey(fixture), `${fixture.action}: unknown permission`));
|
|
1710
1637
|
inProcess.push({
|
|
1711
1638
|
action: fixture.action,
|
|
1712
1639
|
granted: false,
|
|
@@ -1718,7 +1645,7 @@ async function runRlsVerify(input) {
|
|
|
1718
1645
|
const permdock = await getPermDock(policy, fixtureSubject(fixture.subject), { customRoles: memoryCustomRoleSource(customRoles) });
|
|
1719
1646
|
const granted = canFixture(permdock, permission, fixtureRow(fixture, permission.kind));
|
|
1720
1647
|
const outcome = granted ? "granted" : "denied";
|
|
1721
|
-
if (fixture.expected !== void 0 && fixture.expected !== outcome) mismatches.push(`${fixture.action}: in-process ${outcome}, expected ${fixture.expected}`);
|
|
1648
|
+
if (fixture.expected !== void 0 && fixture.expected !== outcome) mismatches.push(rlsFinding("expected-mismatch", fixture.action, fixtureKey(fixture), `${fixture.action}: in-process ${outcome}, expected ${fixture.expected}`));
|
|
1722
1649
|
if (kind === "opaque") notes.push(`${fixture.action}: opaque grant untestable app-side`);
|
|
1723
1650
|
const fields = fieldsMode && permission.kind === "instance" && commandFor(permission.action, input.config.rls?.actions) === "select" ? expectedFields(granted, permdock.pick(permission, fixture.row), policy.resources.get(permission.resource)?.id ?? "id", fixture.row) : void 0;
|
|
1724
1651
|
inProcess.push({
|
|
@@ -1728,19 +1655,23 @@ async function runRlsVerify(input) {
|
|
|
1728
1655
|
...fields === void 0 ? {} : { fields }
|
|
1729
1656
|
});
|
|
1730
1657
|
}
|
|
1731
|
-
if (input.format === "pgtap")
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1658
|
+
if (input.format === "pgtap") {
|
|
1659
|
+
const outRel = input.out ?? PGTAP_OUT;
|
|
1660
|
+
const result = verifyResult({
|
|
1661
|
+
findings: mismatches,
|
|
1662
|
+
ignore,
|
|
1663
|
+
summary: `wrote ${outRel}`
|
|
1664
|
+
});
|
|
1665
|
+
if (result.code !== 0) return result;
|
|
1666
|
+
writeFileEnsuringDir(resolve(input.cwd, outRel), emitPgtap({
|
|
1737
1667
|
fixtures,
|
|
1738
1668
|
inProcess,
|
|
1739
1669
|
policy,
|
|
1740
1670
|
config: input.config,
|
|
1741
1671
|
customRoles
|
|
1742
|
-
})
|
|
1743
|
-
|
|
1672
|
+
}));
|
|
1673
|
+
return result;
|
|
1674
|
+
}
|
|
1744
1675
|
if (input.db !== void 0) try {
|
|
1745
1676
|
const database = await verifyAgainstDatabase({
|
|
1746
1677
|
db: input.db,
|
|
@@ -1756,15 +1687,12 @@ async function runRlsVerify(input) {
|
|
|
1756
1687
|
} catch (cause) {
|
|
1757
1688
|
return usageResult(cause);
|
|
1758
1689
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
code: 0,
|
|
1766
|
-
output: notes.length === 0 ? verified : `${verified}\n${notes.join("\n")}`
|
|
1767
|
-
};
|
|
1690
|
+
return verifyResult({
|
|
1691
|
+
findings: mismatches,
|
|
1692
|
+
ignore,
|
|
1693
|
+
notes,
|
|
1694
|
+
summary: `verified ${String(fixtures.length)} fixture(s) ${input.db === void 0 ? "in-process" : "in-process and against the database"}`
|
|
1695
|
+
});
|
|
1768
1696
|
}
|
|
1769
1697
|
//#endregion
|
|
1770
|
-
export {
|
|
1698
|
+
export { indexTargets as _, verifyResult as a, readTableIndexFacts as b, callsHelper as c, rowConditionMessage as d, commandFor as f, INDEX_COLUMNS_SQL as g, COLUMNS_SQL as h, verifyIgnores as i, helperCallKeys as l, tableFor as m, runRlsVerify as n, HELPER_TABLES as o, compileGrants as p, rlsFinding as r, ROW_CONDITION_FIX as s, canFixture as t, helperTablePolicies as u, indexesSql as v, pruneIndexTargets as y };
|