permdock 0.3.1 → 0.5.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 +37 -35
- package/dist/{openapi-schema--ARjyfQr.js → 2020-C9VN-x8c.js} +5 -132
- package/dist/a2a/index.d.ts +8 -6
- package/dist/a2a/index.js +21 -31
- package/dist/{adapter-context-mlWsVGGg.js → adapter-context-CRhqyzCk.js} +1 -1
- package/dist/adoption-scan-B4R5fnMU.js +107 -0
- package/dist/agent/index.d.ts +3 -0
- package/dist/agent/index.js +1 -0
- package/dist/ai-sdk/index.d.ts +18 -23
- package/dist/ai-sdk/index.js +38 -21
- package/dist/app-data-MuyzVqX8.js +60 -0
- package/dist/approval-header-CJACtHkv.js +23 -0
- package/dist/approval-prompt-Dg5LeAcT.js +62 -0
- package/dist/approval-prompt-Djo_PyZc.d.ts +57 -0
- package/dist/approvals/index.d.ts +5 -3
- package/dist/approvals/index.js +8 -5
- package/dist/approvals-DJ6Yd5z1.js +995 -0
- package/dist/{approvers-DT4AVyIg.js → approvers-CqPOLD-o.js} +1 -1
- package/dist/arazzo-B6lebmiQ.js +36 -0
- package/dist/{arazzo-BAgw9ap6.js → arazzo-BWhNF-k0.js} +24 -17
- package/dist/args-IIolxxKA.js +197 -0
- package/dist/astro/index.d.ts +43 -0
- package/dist/astro/index.js +27 -0
- package/dist/audience-Dg8XsRaO.js +55 -0
- package/dist/auth-DRLEizbL.js +178 -0
- package/dist/authzen/index.d.ts +26 -6
- package/dist/authzen/index.js +228 -75
- package/dist/automation-BMZPIVUM.d.ts +114 -0
- package/dist/automation-DVjOckjV.js +157 -0
- package/dist/better-auth/index.d.ts +8 -6
- package/dist/better-auth/index.js +11 -23
- package/dist/better-supabase/index.d.ts +438 -86
- package/dist/better-supabase/index.js +1027 -184
- package/dist/bind--9EFVYWF.d.ts +38 -0
- package/dist/{bind-CuzGytRf.js → bind-CZzhx-Vj.js} +22 -10
- package/dist/body-CH5of8Ne.js +10 -0
- package/dist/body-text-QcXok_SI.js +52 -0
- package/dist/build-instance-BMnAudHn.js +52 -0
- package/dist/{canonical-json-DemiK4Vp.js → canonical-json-BC8_nMTw.js} +6 -6
- package/dist/{capability-2kYTMWDv.d.ts → capability-BSslw1QV.d.ts} +44 -25
- package/dist/{capability-4qPdO9gk.js → capability-N1K9IpqS.js} +69 -42
- package/dist/casbin-Cr7RLtA3.js +30 -0
- package/dist/casbin-Dyu2gnUD.js +234 -0
- package/dist/casl-64gpT-8x.js +24 -0
- package/dist/casl-DjEDhTCP.js +299 -0
- package/dist/catalog/index.d.ts +74 -2
- package/dist/catalog/index.js +51 -2
- package/dist/catalog-BECMPrwc.js +100 -0
- package/dist/catalog-DjhMYYCq.js +1 -0
- package/dist/catalog-R_A__RsH.js +52 -0
- package/dist/{catalog-doc-DdGm82XT.js → catalog-doc-C9wjsfNY.js} +54 -25
- package/dist/catalog-fingerprint-CCP0-yQS.js +153 -0
- package/dist/chat/index.d.ts +2 -0
- package/dist/chat/index.js +224 -0
- package/dist/chat-sdk/index.d.ts +87 -0
- package/dist/chat-sdk/index.js +197 -0
- package/dist/claims-map-7oQ3ue4X.js +13 -0
- package/dist/claims-map-Ca-QX8C6.d.ts +32 -0
- package/dist/claude-agent/index.d.ts +7 -14
- package/dist/claude-agent/index.js +26 -30
- package/dist/clerk/index.d.ts +9 -8
- package/dist/clerk/index.js +15 -45
- package/dist/cli/bin.js +36 -17
- package/dist/cli/index.d.ts +20 -6
- package/dist/cli/index.js +164 -4
- package/dist/cloud/index.d.ts +102 -63
- package/dist/cloud/index.js +240 -347
- package/dist/cloud-CIoHNevm.js +87 -0
- package/dist/codemod-Bmj9dWMH.js +37 -0
- package/dist/codemod-Cxvvc70T.js +2084 -0
- package/dist/{collect-DSTE3ml9.js → collect-CrZJJnEF.js} +79 -17
- package/dist/collect-DloHbDVa.js +55 -0
- package/dist/collect-FlXJlhus.js +1 -0
- package/dist/command-L8EstFNm.js +6 -0
- package/dist/{compact-y6vfu2_f.js → compact-CAf557ZB.js} +11 -1
- package/dist/compile/index.d.ts +1 -1
- package/dist/compile/index.js +1 -2
- package/dist/{compile-Ba4blROR.js → compile-CMqMx_IN.js} +13 -9
- package/dist/{compile-BkRyBDBw.d.ts → compile-Ckm2HqBZ.d.ts} +5 -3
- package/dist/completion-BqhRBJD8.js +143 -0
- package/dist/config-B46rm9sc.js +26 -0
- package/dist/{config-BJiM-xAm.js → config-bs_kZeYq.js} +15 -10
- package/dist/{config-report-CdCgbRnV.js → config-report-DUgj62Ai.js} +17 -8
- package/dist/{connection-CU8MQxRi.js → connection-DjhIJSs2.js} +8 -6
- package/dist/constants-DcJABbLW.js +20 -0
- package/dist/{context-Bcy85rGi.js → context-bbFHR-0D.js} +16 -5
- package/dist/convex/index.d.ts +11 -10
- package/dist/convex/index.js +18 -18
- package/dist/create-0VZ7Pdiu.d.ts +107 -0
- package/dist/create-BDS7wc3y.d.ts +138 -0
- package/dist/create-BFfm17UA.d.ts +110 -0
- package/dist/create-BWRSXgOF.js +403 -0
- package/dist/create-C3b4B5eF.js +130 -0
- package/dist/create-DnrmLnLW.js +323 -0
- package/dist/{credential-8P81ff4u.js → credential-DM2um3ha.js} +153 -32
- package/dist/credentials-DPyHmYz5.js +74 -0
- package/dist/deciding-columns-Sq12mEA6.js +123 -0
- package/dist/delegation-DaXVdDhM.js +286 -0
- package/dist/delegation-input-FTxMK3ji.js +19 -0
- package/dist/delivery/index.d.ts +5 -0
- package/dist/delivery/index.js +5 -0
- package/dist/delivery-DCAgNyCn.js +499 -0
- package/dist/delivery-event-xX4obB8F.js +40 -0
- package/dist/denied-text-D8jDHgsl.d.ts +19 -0
- package/dist/{describe-DMYiCjJV.d.ts → describe-BZUsixiL.d.ts} +11 -2
- package/dist/{describe-B_gVZEVi.js → describe-BybMRY7A.js} +10 -5
- package/dist/device-BARQ8eYG.js +193 -0
- package/dist/diff-CxZo9pTq.js +37 -0
- package/dist/{diff-2VaeChzX.js → diff-DCePiN6M.js} +61 -55
- package/dist/doctor-CxiwSfqH.js +44 -0
- package/dist/doctor-DFjUxf2f.js +2361 -0
- package/dist/drizzle/index.d.ts +5 -3
- package/dist/drizzle/index.js +6 -14
- package/dist/elysia/index.d.ts +16 -13
- package/dist/elysia/index.js +14 -13
- package/dist/env-DJZrPjRi.js +8 -0
- package/dist/errors-BDUwg2qC.js +27 -0
- package/dist/{errors-Cz5zn0mW.js → errors-BKy0EfhN.js} +6 -10
- package/dist/errors-BWXBMaKK.d.ts +11 -0
- package/dist/{errors-DPdqI50v.d.ts → errors-C2PXIv4c.d.ts} +2 -2
- package/dist/errors-CWTY6uih.js +143 -0
- package/dist/evaluate-_R8QkWaf.js +1732 -0
- package/dist/evaluations-16UqqQ6W.d.ts +32 -0
- package/dist/{evaluations-tz6D9A2G.js → evaluations-DZ2CbV5k.js} +181 -32
- package/dist/eve/index.d.ts +12 -16
- package/dist/eve/index.js +23 -29
- package/dist/event-time-CsegxXxB.js +23 -0
- package/dist/expiry-D4_YKRKD.js +52 -0
- package/dist/express/index.d.ts +13 -10
- package/dist/express/index.js +14 -12
- package/dist/fastify/index.d.ts +26 -13
- package/dist/fastify/index.js +17 -36
- package/dist/fetch-adapter-g0ypLkID.js +120 -0
- package/dist/fetch-adapter-qKJBuRqy.d.ts +31 -0
- package/dist/fields-D9tPVUUS.js +203 -0
- package/dist/files-C5ZI27eV.js +124 -0
- package/dist/{files-DWUN9PzI.js → files-D_JIzUrP.js} +12 -3
- package/dist/{fixtures-ht28Cplr.js → fixtures-B7XrLeY7.js} +12 -12
- package/dist/flags-C3DktBDY.js +86 -0
- package/dist/{from-snapshot-CTLijvdm.js → from-snapshot-D2lZt4Dd.js} +78 -65
- package/dist/{generate-CH7ThJ14.js → generate-CNU1pIOo.js} +1 -3
- package/dist/grantee-Cn1HyUsI.js +254 -0
- package/dist/grantee-conditions-CcXf8hMM.js +271 -0
- package/dist/grantee-match-DHArqY82.js +142 -0
- package/dist/h3/index.d.ts +37 -0
- package/dist/h3/index.js +35 -0
- package/dist/headers-BIjfMaoC.js +8 -0
- package/dist/headers-CIbDTUTK.d.ts +7 -0
- package/dist/{helpers-H_KARDVC.js → helpers-DTcZOzhF.js} +47 -39
- package/dist/hono/index.d.ts +12 -9
- package/dist/hono/index.js +14 -13
- package/dist/{hosted-DJQw46hn.js → hosted-BZIAYvon.js} +64 -15
- package/dist/hosted-DBDX0M-P.d.ts +78 -0
- package/dist/{http-SO7qGttf.js → http-BBoKefeS.js} +35 -2
- package/dist/{http-DPuDaChb.d.ts → http-B_LphE5Y.d.ts} +1 -2
- package/dist/http-DXJ37RDA.js +46 -0
- package/dist/{http-BIhmLfCc.js → http-ujKm0JXE.js} +3 -26
- package/dist/{openapi-import-cmwetbzw.js → import-CNV7I2Dw.js} +30 -26
- package/dist/{rls-import-4lLW_pbb.js → import-DMmOG27O.js} +271 -128
- package/dist/index-BcHcPyIo.d.ts +110 -0
- package/dist/index-CaxrMD1A.d.ts +1 -0
- package/dist/index-Chrs5fQ-.d.ts +210 -0
- package/dist/index-CxGUswIC.d.ts +431 -0
- package/dist/index-D8EovfsP.d.ts +91 -0
- package/dist/index-DOVf7RJv.d.ts +900 -0
- package/dist/{index-C8z4RjQV.d.ts → index-SGdIQyzc.d.ts} +34 -16
- package/dist/index.d.ts +20 -14
- package/dist/index.js +33 -23
- package/dist/init-DeMYsnYJ.js +96 -0
- package/dist/init-GG6Jh8HD.js +95 -0
- package/dist/{instance-options-jARdwfPN.d.ts → instance-options-Omfd793B.d.ts} +2 -2
- package/dist/instance-snapshot-Cxq4ygFc.js +600 -0
- package/dist/instant-BluqDvyS.js +19 -0
- package/dist/internals-Dk8A8fX3.js +17 -0
- package/dist/{json-schema-BsAflUGW.js → json-schema-B-vAwlde.js} +46 -9
- package/dist/json-schema-CN_iLFH2.d.ts +40 -0
- package/dist/jwt/index.d.ts +18 -11
- package/dist/jwt/index.js +166 -145
- package/dist/{kernel-_RRHmnMY.js → kernel-5JatXyND.js} +120 -43
- package/dist/kysely/index.d.ts +5 -3
- package/dist/kysely/index.js +6 -14
- package/dist/{live-options-H2eG8dfJ.js → live-options-BaAFCIiu.js} +16 -3
- package/dist/load-BxEtwlBS.js +1 -0
- package/dist/{load-DhWEB9P1.js → load-DFS_ry0W.js} +31 -7
- package/dist/{load-jose-BL6LH4bL.js → load-jose-DZ4dnjri.js} +1 -4
- package/dist/login-Dfe5RlYk.js +68 -0
- package/dist/login-imhuwvn3.js +254 -0
- package/dist/manifest-C8IzW3Do.js +42 -0
- package/dist/manifest-CtKR3GXL.js +370 -0
- package/dist/manifest-json-CAGzls-C.js +9 -0
- package/dist/{manifest-uDS9y5de.d.ts → manifest-mx5ghSFM.d.ts} +9 -2
- package/dist/manifest-schema-C0kK_8V0.js +399 -0
- package/dist/{map-claims-Z4QZOiFF.js → map-claims-D3oRmTk0.js} +19 -25
- package/dist/{map-error-pgUWNNtG.js → map-error-TCAdw-eW.js} +5 -5
- package/dist/{markers-B0hYLvMD.js → markers-C6joDI3o.js} +8 -3
- package/dist/mcp/index.d.ts +36 -9
- package/dist/mcp/index.js +476 -448
- package/dist/{rls-migrate-CN4TlJTO.js → migrate-BShP7Vf4.js} +119 -20
- package/dist/nest/index.d.ts +12 -9
- package/dist/nest/index.js +20 -20
- package/dist/next/index.d.ts +40 -20
- package/dist/next/index.js +37 -39
- package/dist/next/plugin.d.ts +1 -1
- package/dist/next/plugin.js +2 -2
- package/dist/node/index.d.ts +10 -10
- package/dist/node/index.js +7 -9
- package/dist/ocsf-BFp-PZZA.js +126 -0
- package/dist/openai/index.d.ts +9 -16
- package/dist/openai/index.js +18 -25
- package/dist/openapi/index.d.ts +3 -3
- package/dist/openapi/index.js +1 -1
- package/dist/{openapi-BqRBvAng.js → openapi-B7HZ0f-r.js} +9 -12
- package/dist/{openapi-CqqEbHsL.js → openapi-DTaEmTSW.js} +23 -26
- package/dist/openapi-ahr8rHu3.js +138 -0
- package/dist/operation-findings-DgATDpvh.js +36 -0
- package/dist/operations-CxybZErY.d.ts +59 -0
- package/dist/orpc/index.d.ts +13 -10
- package/dist/orpc/index.js +23 -20
- package/dist/otel/index.d.ts +3 -2
- package/dist/otel/index.js +53 -27
- package/dist/output-Dyw8fyPF.js +88 -0
- package/dist/output-files-B3IZJksC.js +1 -0
- package/dist/output-files-DhRBBE4n.js +138 -0
- package/dist/{rls-fields-CZOMlwHl.js → ownership-BatFgAA4.js} +2393 -2349
- package/dist/{package-root-zGkxO3oN.js → package-root-Dk_ETdkp.js} +4 -3
- package/dist/{parse-BuyjxhZV.js → parse-Cxq0WxrA.js} +3 -3
- package/dist/{parse-local-manifest-BrMozJCo.js → parse-local-manifest-CVSs6J9I.js} +2 -4
- package/dist/{parse-local-manifest-JLnaBH0S.d.ts → parse-local-manifest-DgqtruPn.d.ts} +1 -1
- package/dist/parse-request-D_tUXG6M.js +26 -0
- package/dist/paths-DXSHAMIa.js +42 -0
- package/dist/pdp/index.d.ts +7 -3
- package/dist/pdp/index.js +47 -29
- package/dist/{peer-BOr_kJE1.js → peer-C8swJ2GH.js} +23 -4
- package/dist/peer-CRvC7Hg1.js +1 -0
- package/dist/permdock-B9JxkBwC.js +868 -0
- package/dist/permdock-D7eeLJER.d.ts +502 -0
- package/dist/permission-registry-Br4jSNVi.js +385 -0
- package/dist/permissions-B-wCP5ZC.js +30 -0
- package/dist/{permissions-RJrovw98.js → permissions-CZKiUfFM.js} +4 -3
- package/dist/{permissions-CDgvKG5L.d.ts → permissions-oEWfl-kl.d.ts} +1 -2
- package/dist/policies-CO1oOymN.js +295 -0
- package/dist/{policy-D2W7uv19.d.ts → policy-Czo4n3Hs.d.ts} +2 -2
- package/dist/policy-DLZJ5gAp.js +399 -0
- package/dist/policy-from-catalog-DzhEc3Y9.js +450 -0
- package/dist/policy-module-CTlwMvLI.js +89 -0
- package/dist/{policy-IB6yxpC3.d.ts → policy-types-DzwivBgR.d.ts} +939 -1057
- package/dist/postgrest-CW0IqC-U.js +566 -0
- package/dist/postgrest-NUqZT-RB.d.ts +100 -0
- package/dist/powersync-BHfuehf3.js +62 -0
- package/dist/powersync-CbzkqL_M.js +1 -0
- package/dist/{powersync-Dv1_aWF7.js → powersync-bYUDLOAO.js} +30 -32
- package/dist/preset/index.d.ts +146 -0
- package/dist/preset/index.js +1 -0
- package/dist/preset-DZdU0tne.js +1768 -0
- package/dist/principal-source-pHneukrx.js +103 -0
- package/dist/prisma/index.d.ts +6 -3
- package/dist/prisma/index.js +6 -12
- package/dist/{problem-B43ztKIm.js → problem-CTviyH5L.js} +18 -27
- package/dist/{problem-details-DX075l9V.d.ts → problem-details-DXlt-vks.d.ts} +1 -1
- package/dist/{problem-details-_AMOYz-w.js → problem-details-cn5X30gZ.js} +45 -42
- package/dist/problem-types-CJ2325SZ.js +5 -0
- package/dist/{prompt-CXYhLfsS.js → prompt-DfZLJLIn.js} +2 -2
- package/dist/protected-DEPHc2xV.d.ts +38 -0
- package/dist/{protected-utU-SLuy.js → protected-DfjY5mLA.js} +37 -8
- package/dist/{provider-CSQd7m92.js → provider-CC-u3bYP.js} +23 -10
- package/dist/{provider-DZY281jO.d.ts → provider-DMVt6yYn.d.ts} +1 -1
- package/dist/{provider-store-Cfbvo4DE.js → provider-store-B5Yrkhzr.js} +6 -3
- package/dist/pull-BgIfno6w.js +256 -0
- package/dist/pull-ByOks_Tg.js +1 -0
- package/dist/push-CmFVBOeI.js +116 -0
- package/dist/pushed-policy-BImMTTIM.d.ts +57 -0
- package/dist/pushed-policy-DobP7AHI.js +238 -0
- package/dist/react/index.d.ts +8 -8
- package/dist/react/index.js +6 -6
- package/dist/react/provider-client.d.ts +1 -1
- package/dist/react/provider-client.js +1 -2
- package/dist/react-native/index.d.ts +24 -63
- package/dist/react-native/index.js +45 -27
- package/dist/{refs-KYb4ntaz.js → refs-K6eWUJ0H.js} +1 -1
- package/dist/relation-walk-3OyDBF0S.js +186 -0
- package/dist/relations-BnCWnJGI.js +163 -0
- package/dist/renames-CGj7YwcQ.js +175 -0
- package/dist/{replay-CYyiLiwO.d.ts → replay-D7iKcLFe.d.ts} +1 -1
- package/dist/{replay-BsOyTrdV.js → replay-Du1tJjG7.js} +2 -1
- package/dist/request-context-BTRTG7OT.js +71 -0
- package/dist/request-context-C2_1lchh.d.ts +5 -0
- package/dist/request-schema-CMreVuFM.js +148 -0
- package/dist/resend/index.d.ts +126 -0
- package/dist/resend/index.js +379 -0
- package/dist/resource-relations-D_jw0BFx.js +243 -0
- package/dist/retire-CALjIQ7e.js +240 -0
- package/dist/{revocations-Cy06JNx2.d.ts → revocations-R07lFBnX.d.ts} +2 -0
- package/dist/rls-BA8an5Ap.js +336 -0
- package/dist/{rls-DiinlRML.js → rls-j6t43t7n.js} +1066 -1068
- package/dist/role-builder-CXbis5-X.js +623 -0
- package/dist/{rpc-DL9Sqvb1.js → rpc-aPyNi6tA.js} +18 -8
- package/dist/run-CNsApDpd.js +1 -0
- package/dist/run-DOfMtc9V.js +723 -0
- package/dist/run-ticket-BjhTOi6u.d.ts +124 -0
- package/dist/run-ticket-sxFI6LAk.js +337 -0
- package/dist/{runtime-CB8gQFvS.js → runtime-D3VTAiCA.js} +1 -1
- package/dist/{saas-DhpJjnBA.js → saas-FO-Q8oj0.js} +18 -15
- package/dist/scan-BOI5tJ-6.js +29 -0
- package/dist/{schema-D3MnDMxa.js → schema-B4v0Qrac.js} +106 -10
- package/dist/schema-yLuBWWsh.js +130 -0
- package/dist/scim/index.d.ts +2 -2
- package/dist/scim/index.js +1 -2
- package/dist/{scim-BFGBBAqR.js → scim-ClHtpyV3.js} +213 -87
- package/dist/{scopes-CVdPrWN-.js → scopes-CUPSjo9q.js} +20 -7
- package/dist/server/index.d.ts +13 -134
- package/dist/server/index.js +13 -10
- package/dist/server-DqCT4CgZ.js +453 -0
- package/dist/{create-B1d519XK.d.ts → server-types-BLd4MnBX.d.ts} +89 -70
- package/dist/{sha256-BO0swRXL.js → sha256-D9ihqmvd.js} +8 -1
- package/dist/shared-CD11nBbC.js +304 -0
- package/dist/{shared-DRu8VFVM.js → shared-DvvFzYXe.js} +8 -9
- package/dist/signals-D3NcTKUS.js +32 -0
- package/dist/sink-BEZSlMS_.js +957 -0
- package/dist/sink-Bj_lDUDq.d.ts +225 -0
- package/dist/{skills-Biq5uup3.js → skills-B6lVdD_W.js} +16 -8
- package/dist/skills-CTYwkl44.js +51 -0
- package/dist/snapshot-CXUWfsNy.js +269 -0
- package/dist/{snapshot-cache-D6jLelln.js → snapshot-cache-DZHQ0KF6.js} +11 -5
- package/dist/{snapshot-cache-B-LxUahj.d.ts → snapshot-cache-DqK9eDmM.d.ts} +7 -5
- package/dist/solid/index.d.ts +24 -25
- package/dist/solid/index.js +41 -11
- package/dist/solid-start/index.d.ts +52 -0
- package/dist/solid-start/index.js +49 -0
- package/dist/{sources-BdOYLw9j.d.ts → sources-BV9j0DeZ.d.ts} +11 -13
- package/dist/{sources-4p0-4xQm.js → sources-vs5dGOsS.js} +147 -142
- package/dist/sql-B5T1IL83.js +55 -0
- package/dist/src-DLfUWFAI.js +1851 -0
- package/dist/ssf/index.d.ts +4 -4
- package/dist/ssf/index.js +440 -106
- package/dist/standard-webhooks-6lg8v_is.d.ts +88 -0
- package/dist/standard-webhooks-DgFFD-Ga.js +228 -0
- package/dist/storage-C2bWdvJH.js +132 -0
- package/dist/{store-DMAWwAJV.js → store-4u_hxjWa.js} +42 -55
- package/dist/{store-options-ByhZq1jV.js → store-options-BqEakYRa.js} +38 -24
- package/dist/{stores-BT2pSCub.js → stores-BbsD4fAR.js} +7 -3
- package/dist/stores-ClZUgeoP.d.ts +30 -0
- package/dist/{stream-D1RXubw2.d.ts → stream-7-xmnf3U.d.ts} +2 -2
- package/dist/{stream-DDpvynSB.js → stream-CvdUS9V1.js} +10 -2
- package/dist/style-DiKWnAvn.js +1 -0
- package/dist/{style-CTm1AXDT.js → style-Uao76wO7.js} +3 -2
- package/dist/subject-BGoM5o8B.js +17 -0
- package/dist/{subject-settings-BwBmCdTR.js → subject-settings-CkEwrw3W.js} +23 -5
- package/dist/supabase/index.d.ts +23 -106
- package/dist/supabase/index.js +5 -4
- package/dist/supabase/middleware.d.ts +13 -6
- package/dist/supabase/middleware.js +16 -11
- package/dist/supabase-9pqI57pW.js +92 -0
- package/dist/{supabase-C0qESOa-.js → supabase-Dxlp8A94.js} +124 -247
- package/dist/supabase-hook-CXcGpHqq.js +1 -0
- package/dist/{supabase-hook-ntETYCQu.js → supabase-hook-DdDQhMlw.js} +1659 -819
- package/dist/{supautils-u8jXTGrt.js → supautils-B6HYL9eL.js} +2 -2
- package/dist/support-session-C6xJQYut.js +158 -0
- package/dist/svelte/Protected.svelte +11 -4
- package/dist/svelte/index.d.ts +7 -7
- package/dist/svelte/index.js +26 -12
- package/dist/svelte/runtime.d.ts +10 -3
- package/dist/svelte/runtime.js +4 -3
- package/dist/svelte/source.d.ts +7 -7
- package/dist/svelte/source.js +5 -5
- package/dist/sveltekit/index.d.ts +51 -0
- package/dist/sveltekit/index.js +27 -0
- package/dist/tanstack-start/index.d.ts +51 -0
- package/dist/tanstack-start/index.js +40 -0
- package/dist/target-Bw37TFNA.js +51 -0
- package/dist/{validity-uOp8UpYn.js → tenancy-DN2QAx9z.js} +6 -42
- package/dist/terminal/index.d.ts +11 -9
- package/dist/terminal/index.js +36 -316
- package/dist/testing/index.d.ts +391 -106
- package/dist/testing/index.js +4574 -1286
- package/dist/testing/saas/index.d.ts +9 -8
- package/dist/testing/saas/index.js +2 -2
- package/dist/testing/saas/permissions.d.ts +1 -1
- package/dist/testing/saas/permissions.js +1 -2
- package/dist/text-BMFRfBqc.js +8 -0
- package/dist/{text-diff-BdkkYSbi.js → text-diff-BQcUapOb.js} +1 -1
- package/dist/thenable-yD7SabqG.js +31 -0
- package/dist/time-DkWjdL98.js +71 -0
- package/dist/{token-CIWxJOCh.js → token-BLZcjQgt.js} +15 -6
- package/dist/tool-refusal-CMLN5xhn.js +41 -0
- package/dist/trpc/index.d.ts +12 -9
- package/dist/trpc/index.js +18 -17
- package/dist/{types-DbjRN55U.d.ts → types-5Ut_pbKy.d.ts} +141 -13
- package/dist/types-BNORmDM8.d.ts +172 -0
- package/dist/types-BjV-fqcA.d.ts +57 -0
- package/dist/{types-QCuYJh4q.d.ts → types-BqAKcRNW.d.ts} +2 -1
- package/dist/types-C8-KNsFf.d.ts +107 -0
- package/dist/{types-aqOHTHcp.d.ts → types-CCpc2Dcv.d.ts} +77 -17
- package/dist/{types-Dnemwd77.d.ts → types-CZTnPFzM.d.ts} +50 -11
- package/dist/{types-C97IskBw.d.ts → types-CdlxsYQh.d.ts} +23 -11
- package/dist/{types-DxiCGmmd.d.ts → types-ClZaWzfb.d.ts} +9 -6
- package/dist/types-DX0LaKTb.d.ts +157 -0
- package/dist/types-DiQjupG2.d.ts +332 -0
- package/dist/types-DqzsnlSA.js +67 -0
- package/dist/types-_sHy8BQf.d.ts +65 -0
- package/dist/types-fbZ7JEXr.d.ts +89 -0
- package/dist/types-tIgIeIKJ.d.ts +66 -0
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +2 -2
- package/dist/usage-CHOtwL19.js +1 -0
- package/dist/usage-DHtE8j4E.js +32 -0
- package/dist/{usage-ItS7nLcl.js → usage-T5mXq98v.js} +56 -13
- package/dist/{validation-error-Ba264OaJ.d.ts → validation-error-Bg5Tpluk.d.ts} +1 -1
- package/dist/{validation-error-CnjZ4t92.js → validation-error-ZfUHbKvf.js} +1 -1
- package/dist/validity-CZ7-y0Cj.js +79 -0
- package/dist/{verifier-BxBo6I44.js → verifier-DHuh-okk.js} +68 -21
- package/dist/verify-DbCL9bnB.js +1770 -0
- package/dist/{version-BvacVTZR.js → version-CEbQ5UeM.js} +4 -3
- package/dist/version-DXkYUISx.js +1 -0
- package/dist/{views-Soe7bGSC.js → views-DTIIO-oQ.js} +41 -2
- package/dist/{permissions-DttnqqxC.d.ts → vocabulary-D6lPdb4Z.d.ts} +120 -29
- package/dist/vue/index.d.ts +25 -21
- package/dist/vue/index.js +25 -9
- package/dist/waiter-BGUtGYoU.js +67 -0
- package/dist/waiter-CKy4MQSY.d.ts +36 -0
- package/dist/watch-Cb-NcaUw.js +1 -0
- package/dist/{watch-DVBJ9cBq.js → watch-CszoTJMq.js} +47 -8
- package/dist/webmcp/index.d.ts +2 -2
- package/dist/webmcp/index.js +13 -37
- package/dist/where-scope-Dv-WTP-w.js +306 -0
- package/dist/who-can-D0QoYbEb.js +364 -0
- package/dist/wire-delegation-Cv3W2eaM.js +36 -0
- package/dist/wire-denial-C7PaVYEM.js +27 -0
- package/dist/workflow/index.d.ts +5 -0
- package/dist/workflow/index.js +223 -0
- package/dist/workflow-sdk/index.d.ts +56 -0
- package/dist/workflow-sdk/index.js +78 -0
- package/dist/wrap-Bd1Tejur.d.ts +15 -0
- package/dist/wrap-gX61EtpB.js +27 -0
- package/package.json +140 -41
- package/schemas/access-event-v1.json +118 -0
- package/schemas/approval-request-v1.json +34 -1
- package/schemas/approval-signature-v1.json +94 -0
- package/schemas/automation-event-v1.json +113 -0
- package/schemas/capability-v1.json +84 -0
- package/schemas/catalog-event-v1.json +87 -0
- package/schemas/catalog-v1.json +129 -12
- package/schemas/cli-commands-v1.json +164 -0
- package/schemas/cli-result-v1.json +96 -0
- package/schemas/cloud-api-v1.json +5407 -0
- package/schemas/connector-v1.json +408 -0
- package/schemas/credential-event-v1.json +81 -0
- package/schemas/credential-v1.json +134 -0
- package/schemas/decision-event-v1.json +288 -0
- package/schemas/delivery-event-v1.json +117 -0
- package/schemas/directory-event-v1.json +85 -0
- package/schemas/evidence-export-v1.json +54 -0
- package/schemas/membership-event-v1.json +93 -0
- package/schemas/policy-v1.json +68 -0
- package/schemas/pushed-policy-v1.json +236 -0
- package/schemas/run-v1.json +162 -0
- package/schemas/snapshot-v1.json +138 -0
- package/schemas/supabase-claims-v1.json +4 -4
- package/schemas/supabase-manifest-v1.json +5 -1
- package/schemas/support-session-v1.json +111 -0
- package/skills/permdock/SKILL.md +16 -12
- package/skills/permdock-agents/SKILL.md +40 -23
- package/skills/permdock-agents/references/operations-automations-chat.md +221 -0
- package/skills/permdock-agents/references/runtimes.md +22 -18
- package/skills/permdock-approvals/SKILL.md +29 -13
- package/skills/permdock-approvals/references/delivery-and-waiters.md +172 -0
- package/skills/permdock-approvals/references/stores-and-resume.md +21 -7
- package/skills/permdock-audit/SKILL.md +3 -3
- package/skills/permdock-credentials/SKILL.md +5 -5
- package/skills/permdock-credentials/references/tokens-keys-links.md +19 -10
- package/skills/permdock-data/SKILL.md +6 -6
- package/skills/permdock-data/references/queries-and-rls.md +13 -8
- package/skills/permdock-data/references/relationships.md +3 -3
- package/skills/permdock-tenancy/SKILL.md +20 -7
- package/skills/permdock-tenancy/references/contacts-and-support-sessions.md +122 -0
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +6 -4
- package/skills/permdock-wire/SKILL.md +18 -12
- package/skills/permdock-wire/references/adapters.md +146 -50
- package/skills/permdock-wire/references/policy-options.md +15 -6
- package/skills/permdock-wire/references/presets-and-recipes.md +58 -0
- package/dist/approvals-DbYAuNkG.js +0 -224
- package/dist/arazzo-C9chp3ST.js +0 -54
- package/dist/bind-C_ob3LKa.d.ts +0 -25
- package/dist/catalog-CRW5Mx_B.js +0 -2
- package/dist/catalog-CYj-mX2O.js +0 -48
- package/dist/catalog-DWF5cYqB.js +0 -55
- package/dist/cloud-CfwZ39Rl.js +0 -50
- package/dist/cloud-DWLXO2af.js +0 -118
- package/dist/collect-CHB22Sx4.js +0 -83
- package/dist/collect-DSWOk19R.js +0 -2
- package/dist/commands-BeaGNj_o.js +0 -97
- package/dist/config-ir40y8m0.js +0 -36
- package/dist/create-CMwIl-w2.js +0 -253
- package/dist/delegation-D8pUEuce.js +0 -103
- package/dist/diff-C9RDjjhs.js +0 -48
- package/dist/doctor-Djh6JDEk.js +0 -53
- package/dist/doctor-DvH0IQff.js +0 -1194
- package/dist/doctor-collect-CjdO0wQX.js +0 -665
- package/dist/duration-xK2jGFVc.js +0 -25
- package/dist/errors-CWfoNg9N.js +0 -47
- package/dist/errors-DRnGZHzF.js +0 -30
- package/dist/evaluation-items-CfCCejQd.js +0 -40
- package/dist/expiry-BoBJ8Tvc.js +0 -7
- package/dist/fields-BMl1RxoX.js +0 -26
- package/dist/grantee-Cv5Sogfv.js +0 -602
- package/dist/headers-BZSDieNT.js +0 -6
- package/dist/headers-Cf-zWvsO.d.ts +0 -6
- package/dist/ids-DC4V4CFu.js +0 -18
- package/dist/index-0nNqHspG.d.ts +0 -6
- package/dist/index-BNc7Wer_.d.ts +0 -483
- package/dist/index-C7Tw7MaW.d.ts +0 -201
- package/dist/instance-options-Dhhcnm3y.js +0 -22
- package/dist/load-DwJUafYN.js +0 -2
- package/dist/manifest-iaoBXHKc.js +0 -783
- package/dist/map-AcxK9o_D.js +0 -147
- package/dist/may-use-DndQ5tQ-.js +0 -77
- package/dist/openapi-CQTsgKoB.js +0 -164
- package/dist/permdock-CbgvLnuo.js +0 -3479
- package/dist/permissions-DVoF6dMt.js +0 -662
- package/dist/policy-xyWg7QdR.js +0 -1122
- package/dist/powersync-CrBX8hY6.js +0 -2
- package/dist/powersync-DE19b9dT.js +0 -62
- package/dist/protected-BdnIEFkY.d.ts +0 -27
- package/dist/protected-xAr24FEC.d.ts +0 -40
- package/dist/relations-cjOL01hZ.js +0 -505
- package/dist/rls-P-BeBx0n.js +0 -238
- package/dist/rls-graph-CHwx0V8q.js +0 -435
- package/dist/rls-ownership-DcYlZ09B.js +0 -843
- package/dist/rls-shared-Cewd_-zI.js +0 -495
- package/dist/rls-verify-CMv9H3wV.js +0 -746
- package/dist/run-BWc9qqvE.js +0 -174
- package/dist/server-DlZ28Oz5.js +0 -268
- package/dist/sink-B1fZzIMf.d.ts +0 -104
- package/dist/sink-Bd9PSyfq.js +0 -141
- package/dist/skills-C5CbhhpG.js +0 -56
- package/dist/snapshot-Enxd0n-s.js +0 -975
- package/dist/sql-xdg5f4Fi.js +0 -27
- package/dist/src-BDwEZWrZ.js +0 -568
- package/dist/store-jOBYpiXC.d.ts +0 -36
- package/dist/stores-C0IMQrGI.d.ts +0 -52
- package/dist/subject-C6dLxqHU.js +0 -24
- package/dist/supabase-config-Bj3iDH2I.js +0 -472
- package/dist/supabase-hook-gd3s8bxB.js +0 -2
- package/dist/supabase-s4o9s1O1.js +0 -83
- package/dist/thenable-okc2xDsj.js +0 -10
- package/dist/types-BnIEz1sU.d.ts +0 -48
- package/dist/types-Bvc4oWT6.d.ts +0 -9
- package/dist/types-CKNSyIf_.d.ts +0 -52
- package/dist/types-Cq_H8N_X.d.ts +0 -197
- package/dist/usage-B0YuTQRO.js +0 -42
- package/dist/usage-DorAJiU_.js +0 -2
- package/dist/vocabulary-DA8a8zif.d.ts +0 -56
- package/dist/watch-BwxLTYMZ.js +0 -2
|
@@ -1,27 +1,33 @@
|
|
|
1
|
-
import { t as compact } from "./compact-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { t as compact } from "./compact-CAf557ZB.js";
|
|
2
|
+
import { c as listPermissions, u as renamedKeys } from "./permission-registry-Br4jSNVi.js";
|
|
3
|
+
import { i as dateAt } from "./time-DkWjdL98.js";
|
|
4
|
+
import { d as scopeList, o as resolveScope } from "./scopes-CUPSjo9q.js";
|
|
5
|
+
import { o as flattenGrantee, v as listRoles } from "./grantee-Cn1HyUsI.js";
|
|
6
|
+
import { p as scopeField } from "./tenancy-DN2QAx9z.js";
|
|
7
|
+
import { a as describeError, i as configError, o as exitCodeOfError, u as usageResult } from "./errors-CWTY6uih.js";
|
|
7
8
|
import { t as policyRowConditionKeys } from "./row-conditions-h2HythXQ.js";
|
|
8
|
-
import {
|
|
9
|
+
import { c as requirePolicy } from "./load-DFS_ry0W.js";
|
|
9
10
|
import "./budget-NbEtbJC_.js";
|
|
10
|
-
import {
|
|
11
|
-
import "./sources-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { a as
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
11
|
+
import { i as qualifySqlTable, r as escapeSqlIdent, u as sqlTextArray } from "./sql-B5T1IL83.js";
|
|
12
|
+
import { l as roleColumn } from "./sources-vs5dGOsS.js";
|
|
13
|
+
import { t as policyFromCatalog } from "./policy-from-catalog-DzhEc3Y9.js";
|
|
14
|
+
import { t as beforeFirst } from "./text-BMFRfBqc.js";
|
|
15
|
+
import { i as INDEXES_MARKER, n as GRANTS_MARKER, o as SEEDS_MARKER, t as BACKFILL_MARKER } from "./markers-C6joDI3o.js";
|
|
16
|
+
import { i as requirePeer, n as packageManagerOf, t as addLine } from "./peer-C8swJ2GH.js";
|
|
17
|
+
import { t as POLICY_NOT_PUBLISHED } from "./problem-types-CJ2325SZ.js";
|
|
18
|
+
import { t as fetchProjectPolicy } from "./pull-BgIfno6w.js";
|
|
19
|
+
import { a as isSeedsDirectory, c as parseSplit, d as seedsMigration, f as writeOutputFiles, l as partPath, n as dataMigrationFile, r as driftOf, u as pgDeltaPath } from "./output-files-DhRBBE4n.js";
|
|
20
|
+
import { c as approvalsSql, d as resolveAuthorize, h as schemasRefusal, l as parseRbacAuthorize, m as resolveSchemas, o as supabaseHookSql, p as isSupabaseSchemas, s as permissionHoldersSql, t as grantsLabel, u as rbacScaffold } from "./supabase-hook-DdDQhMlw.js";
|
|
21
|
+
import { C as parseMembershipsFlag, E as scopeTypeOf, F as quoteQualified, I as quoteTable, M as qualified, N as quoteIdent$1, O as tenantTypeOf, P as quoteLiteral, _ as subjectClaimJsonSql, a as customRolesOn, c as SESSION_LIVE_HELPER, g as sessionLiveHelperSql, r as HELPERS, t as CUSTOM_ROLES, y as subjectIdSql } from "./shared-CD11nBbC.js";
|
|
22
|
+
import { a as splitUndiffed } from "./deciding-columns-Sq12mEA6.js";
|
|
23
|
+
import { a as readOnlyActorKinds, c as branchClauses, d as connectPgPool, i as orSql, n as actorMayWriteSql, o as readOnlyActorPolicies, r as assemblePolicies, t as ACTOR_MAY_WRITE } from "./policies-CO1oOymN.js";
|
|
24
|
+
import { $ as checkSuspension, A as roleNames, D as breakGlassHolder, E as apiKeysPlan, F as graphHelper, I as graphSqlName, O as breakGlassKey, S as byKey, W as memberIdsHelper, X as scopeTable, Y as scopeSources, _ as currentKeyExpr, f as permittedByPermissionHelper, g as seedSql, h as helpersSql, i as assignmentSql, k as collectGrants, m as accessSql, n as ownershipSql, q as permittedIdsHelper, t as ownershipRules, u as permissionHelpersSql, x as byCodeUnit } from "./ownership-BatFgAA4.js";
|
|
25
|
+
import { _ as indexesSql, a as graphSql, c as callsHelper, g as indexTargets, h as COLUMNS_SQL, i as graphPlan, l as helperCallKeys, m as tableFor, n as runRlsVerify, o as HELPER_TABLES, p as compileGrants, r as closureDepths, v as pruneIndexTargets, y as readTableIndexFacts } from "./verify-DbCL9bnB.js";
|
|
26
|
+
import { a as fieldViewsSql, i as fieldViews, n as columnGrantSql, o as rowBranches, r as columnRevokeSql } from "./fields-D9tPVUUS.js";
|
|
27
|
+
import { t as supautilsRejections } from "./supautils-B6HYL9eL.js";
|
|
28
|
+
import { basename, delimiter, join } from "node:path";
|
|
29
|
+
import { spawn } from "node:child_process";
|
|
30
|
+
//#region src/cli/rls/advisors.ts
|
|
25
31
|
/** The PermDock doctor check that reports the same problem in migrations. */
|
|
26
32
|
const ADVISOR_CODES = {
|
|
27
33
|
function_search_path_mutable: "PD048",
|
|
@@ -31,19 +37,41 @@ const ADVISOR_CODES = {
|
|
|
31
37
|
anon_security_definer_function_executable: "PD049",
|
|
32
38
|
authenticated_security_definer_function_executable: "PD046"
|
|
33
39
|
};
|
|
34
|
-
const defaultExec = (command, args, options) => {
|
|
35
|
-
const
|
|
40
|
+
const defaultExec = (command, args, options) => new Promise((resolve) => {
|
|
41
|
+
const child = spawn(command, [...args], {
|
|
36
42
|
cwd: options.cwd,
|
|
37
43
|
env: options.env,
|
|
38
|
-
|
|
44
|
+
stdio: [
|
|
45
|
+
"ignore",
|
|
46
|
+
"pipe",
|
|
47
|
+
"pipe"
|
|
48
|
+
],
|
|
49
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
39
50
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
let stdout = "";
|
|
52
|
+
let stderr = "";
|
|
53
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => {
|
|
54
|
+
stdout += chunk;
|
|
55
|
+
});
|
|
56
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
57
|
+
stderr += chunk;
|
|
58
|
+
});
|
|
59
|
+
child.once("error", (error) => {
|
|
60
|
+
resolve({
|
|
61
|
+
status: null,
|
|
62
|
+
stdout,
|
|
63
|
+
stderr,
|
|
64
|
+
error
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
child.once("close", (status) => {
|
|
68
|
+
resolve({
|
|
69
|
+
status,
|
|
70
|
+
stdout,
|
|
71
|
+
stderr
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
47
75
|
function text(value, key) {
|
|
48
76
|
const field = typeof value === "object" && value !== null ? Reflect.get(value, key) : void 0;
|
|
49
77
|
return typeof field === "string" ? field : "";
|
|
@@ -64,16 +92,19 @@ function findingsOf(value) {
|
|
|
64
92
|
return code === void 0 ? finding : Object.assign(finding, { code });
|
|
65
93
|
});
|
|
66
94
|
}
|
|
67
|
-
|
|
95
|
+
function installHint(agent) {
|
|
96
|
+
return `rls verify --advisors runs the Supabase CLI: install it with ${addLine(agent, ["supabase"], { dev: true })} (or brew install supabase/tap/supabase)`;
|
|
97
|
+
}
|
|
68
98
|
/**
|
|
69
|
-
* `supabase db advisors --type security` against `--db` or the local stack
|
|
70
|
-
*
|
|
99
|
+
* `supabase db advisors --type security` against `--db-url` or the local stack,
|
|
100
|
+
* from the project's `node_modules/.bin` first, then `PATH`. Exit 1 on a WARN
|
|
101
|
+
* or ERROR row, 2 when the CLI is missing or fails.
|
|
71
102
|
*/
|
|
72
|
-
function runRlsAdvisors(input) {
|
|
103
|
+
async function runRlsAdvisors(input) {
|
|
73
104
|
const exec = input.exec ?? defaultExec;
|
|
74
105
|
const bin = join(input.cwd, "node_modules", ".bin");
|
|
75
106
|
const path = input.env["PATH"];
|
|
76
|
-
const result = exec("supabase", [
|
|
107
|
+
const result = await exec("supabase", [
|
|
77
108
|
"db",
|
|
78
109
|
"advisors",
|
|
79
110
|
...input.db === void 0 ? ["--local"] : ["--db-url", input.db],
|
|
@@ -86,11 +117,12 @@ function runRlsAdvisors(input) {
|
|
|
86
117
|
env: {
|
|
87
118
|
...input.env,
|
|
88
119
|
PATH: path === void 0 ? bin : `${bin}${delimiter}${path}`
|
|
89
|
-
}
|
|
120
|
+
},
|
|
121
|
+
...input.signal === void 0 ? {} : { signal: input.signal }
|
|
90
122
|
});
|
|
91
123
|
if (result.error !== void 0) return {
|
|
92
124
|
code: 2,
|
|
93
|
-
output:
|
|
125
|
+
output: installHint(await packageManagerOf(input.cwd))
|
|
94
126
|
};
|
|
95
127
|
let parsed;
|
|
96
128
|
try {
|
|
@@ -127,578 +159,487 @@ function runRlsAdvisors(input) {
|
|
|
127
159
|
};
|
|
128
160
|
}
|
|
129
161
|
//#endregion
|
|
130
|
-
//#region src/cli/
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
162
|
+
//#region src/cli/rls/generate-context.ts
|
|
163
|
+
/** The scopes an `rls.migrate` `role` helper names, so `rls generate` writes their role-filtered member helper. */
|
|
164
|
+
function roleMemberScopesOf(migrate) {
|
|
165
|
+
const scopes = [...new Set(Object.values(migrate?.helpers ?? {}).flatMap((helper) => helper.form === "role" ? [helper.scope] : []))].toSorted();
|
|
166
|
+
return scopes.length === 0 ? {} : { roleMemberScopes: scopes };
|
|
167
|
+
}
|
|
168
|
+
/** `rls.customRoleWrites.requires` as permission keys, or why it names none. */
|
|
169
|
+
function writeRequirement(policy, requires) {
|
|
170
|
+
if (requires === void 0) return {};
|
|
171
|
+
const leaves = listPermissions(policy.permissions);
|
|
172
|
+
const keys = requires === "manageRoles" ? leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key) : [...requires];
|
|
173
|
+
if (keys.length === 0) return { error: requires === "manageRoles" ? "rls.customRoleWrites.requires is 'manageRoles', but no permission declares meta.manageRoles" : "rls.customRoleWrites.requires names no permission" };
|
|
174
|
+
const known = new Set(leaves.map((leaf) => leaf.key));
|
|
175
|
+
const unknown = keys.filter((key) => !known.has(key));
|
|
176
|
+
if (unknown.length > 0) return { error: `rls.customRoleWrites.requires names undeclared permissions: ${unknown.join(", ")}` };
|
|
177
|
+
return { keys: [...new Set(keys)].toSorted() };
|
|
178
|
+
}
|
|
179
|
+
/** Declared role names, and those a tenant admin may compose into custom roles. */
|
|
180
|
+
function customRoleNames(policy, requires) {
|
|
181
|
+
const declared = [...roleNames(policy)].toSorted();
|
|
182
|
+
const assignable = declared.filter((name) => policy.rolesByName.get(name)?.assignable ?? listRoles(policy.vocabulary.roles).some((leaf) => leaf.key === name && leaf.assignable));
|
|
183
|
+
const renamed = renamedKeys(policy.vocabulary.permissions);
|
|
184
|
+
const leaves = listPermissions(policy.permissions);
|
|
185
|
+
const manage = leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key);
|
|
186
|
+
return {
|
|
187
|
+
declared,
|
|
188
|
+
assignable,
|
|
189
|
+
...renamed.size === 0 ? {} : { renamed: Object.fromEntries(renamed) },
|
|
190
|
+
permissions: leaves.map((leaf) => leaf.key).toSorted(),
|
|
191
|
+
...manage.length === 0 ? {} : { manage: manage.toSorted() },
|
|
192
|
+
...requires === void 0 ? {} : { requires },
|
|
193
|
+
...policy.levels === void 0 ? {} : { levels: true }
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function skippedOwnershipTriggers(triggers, scopes) {
|
|
197
|
+
if (triggers === false) return "all";
|
|
198
|
+
return Object.keys(triggers).map((key) => {
|
|
199
|
+
const name = resolveScope(scopes, key);
|
|
200
|
+
if (name === void 0) throw configError(`PermDock CLI: rls.ownershipTriggers.${key} names a scope the policy does not declare`);
|
|
201
|
+
return name;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/** The SQL context every emitter reads, from the flags, `rls` config and the policy. */
|
|
205
|
+
function buildContext(input, policy, options) {
|
|
206
|
+
const rls = input.config.rls;
|
|
207
|
+
const memberships = parseMembershipsFlag(input.memberships) ?? rls?.memberships;
|
|
208
|
+
if (input.rbac && input.dialect !== "supabase") return { refusal: `rls generate --rbac supabase needs --dialect supabase (got ${input.dialect})` };
|
|
209
|
+
const schema = input.rbacSchema ?? rls?.schema ?? rls?.rbac?.schema ?? "permdock";
|
|
210
|
+
const authorize = resolveAuthorize(input.config, {
|
|
211
|
+
authorize: input.authorize,
|
|
212
|
+
rbac: input.rbac,
|
|
213
|
+
memberships
|
|
214
|
+
});
|
|
215
|
+
const memberSources = rls?.membershipSources ?? input.config.supabase?.hook?.memberships;
|
|
216
|
+
const sources = authorize === "database" ? memberSources : void 0;
|
|
217
|
+
const scopes = scopeList(policy.scopes);
|
|
218
|
+
const suspension = checkSuspension(rls?.suspension, scopes);
|
|
219
|
+
const hookRoles = input.config.supabase?.hook?.roles;
|
|
220
|
+
const roles = rls?.roles ?? (hookRoles === false ? void 0 : hookRoles);
|
|
221
|
+
if (roles !== void 0 && input.rbac) return { refusal: "rls generate --rbac supabase creates its own user_roles (user_id, role app_role); drop rls.roles (or supabase.hook.roles) or the --rbac flag" };
|
|
222
|
+
const requires = writeRequirement(policy, rls?.customRoleWrites?.requires);
|
|
223
|
+
if (requires.error !== void 0) return { refusal: requires.error };
|
|
224
|
+
const readers = rls?.trustedReaders;
|
|
225
|
+
if (readers !== void 0 && (!Array.isArray(readers) || !readers.every((name) => typeof name === "string" && name !== ""))) return { refusal: "rls.trustedReaders must list Postgres role names, such as ['service_role']" };
|
|
226
|
+
const ownership = ownershipRules(policy, scopes);
|
|
227
|
+
const graph = graphPlan(policy);
|
|
228
|
+
const apiKeys = apiKeysPlan(rls?.apiKeys, new Set(roleNames(policy)), Object.fromEntries(renamedKeys(policy.vocabulary.permissions)));
|
|
229
|
+
return {
|
|
230
|
+
ctx: {
|
|
231
|
+
dialect: input.dialect,
|
|
232
|
+
scopes,
|
|
233
|
+
tenantClaim: rls?.tenantClaim ?? "tenant_id",
|
|
234
|
+
...roleMemberScopesOf(rls?.migrate),
|
|
235
|
+
...rls?.roleValues === void 0 ? {} : { roleValues: rls.roleValues },
|
|
236
|
+
...rls?.tenants === "all" ? { tenants: "all" } : {},
|
|
237
|
+
gucPrefix: input.gucPrefix ?? rls?.gucPrefix ?? "app",
|
|
238
|
+
...rls?.actions === void 0 ? {} : { actions: rls.actions },
|
|
239
|
+
inlineFunctions: input.inlineFunctions || rls?.inlineFunctions === true,
|
|
240
|
+
schema,
|
|
241
|
+
authorize,
|
|
242
|
+
roleClaim: rls?.roleClaim ?? "user_role",
|
|
243
|
+
tenantType: input.tenantType ?? rls?.tenantType ?? "uuid",
|
|
244
|
+
...rls?.teamType === void 0 ? {} : { teamType: rls.teamType },
|
|
245
|
+
...rls?.scopeTypes === void 0 ? {} : { scopeTypes: rls.scopeTypes },
|
|
246
|
+
...memberships === void 0 ? {} : { memberships },
|
|
247
|
+
...sources === void 0 || sources.length === 0 ? {} : { sources },
|
|
248
|
+
...memberSources === void 0 || memberSources.length === 0 ? {} : { memberSources },
|
|
249
|
+
...suspension === void 0 ? {} : { suspension },
|
|
250
|
+
...roles === void 0 ? {} : { roles },
|
|
251
|
+
...input.customRoles === true || customRolesOn(rls) ? { customRoles: {
|
|
252
|
+
...customRoleNames(policy, requires.keys),
|
|
253
|
+
...rls?.customRoleWrites?.roles === void 0 ? {} : { table: rls.customRoleWrites.roles }
|
|
254
|
+
} } : {},
|
|
255
|
+
...input.capabilities === true || rls?.capabilities === true ? { capabilities: true } : {},
|
|
256
|
+
...apiKeys === void 0 ? {} : { apiKeys },
|
|
257
|
+
...rls?.anonymousSignIns === "deny" ? { anonymousSignIns: "deny" } : {},
|
|
258
|
+
...ownership === void 0 ? {} : { ownership },
|
|
259
|
+
...rls?.ownershipTriggers === void 0 ? {} : { skipOwnershipTriggers: skippedOwnershipTriggers(rls.ownershipTriggers, scopes) },
|
|
260
|
+
...rls?.assignments === void 0 ? {} : { assignments: rls.assignments === true ? { tables: [] } : {
|
|
261
|
+
tables: rls.assignments.tables ?? [],
|
|
262
|
+
...rls.assignments.ownRole === void 0 ? {} : { ownRole: rls.assignments.ownRole }
|
|
263
|
+
} },
|
|
264
|
+
...options.fieldsMode === void 0 ? {} : { fields: options.fieldsMode },
|
|
265
|
+
...graph.size === 0 ? {} : { graph: {
|
|
266
|
+
closures: closureDepths(graph),
|
|
267
|
+
resources: policy.resources,
|
|
268
|
+
...rls?.tables === void 0 ? {} : { tables: rls.tables }
|
|
269
|
+
} }
|
|
235
270
|
},
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
at: { $ref: "#/$defs/instant" },
|
|
242
|
-
stage: {
|
|
243
|
-
type: "integer",
|
|
244
|
-
minimum: 0
|
|
245
|
-
},
|
|
246
|
-
vouched: { type: "string" }
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
//#endregion
|
|
252
|
-
//#region src/cli/rls-approvals.ts
|
|
253
|
-
/** The objects `rls.approvals` adds. Names are part of the SQL contract `supabaseApprovalStore` calls. */
|
|
254
|
-
const APPROVAL_STORE = {
|
|
255
|
-
table: "approval_requests",
|
|
256
|
-
open: "permdock_approval_open",
|
|
257
|
-
get: "permdock_approval_get",
|
|
258
|
-
resolve: "permdock_approval_resolve",
|
|
259
|
-
consume: "permdock_approval_consume",
|
|
260
|
-
list: "permdock_approval_list",
|
|
261
|
-
expire: "permdock_approval_expire",
|
|
262
|
-
cancel: "permdock_approval_cancel"
|
|
263
|
-
};
|
|
264
|
-
/** `body` as the row's filter columns hold it. */
|
|
265
|
-
const COLUMNS = `p_request ->> 'token',
|
|
266
|
-
p_request,
|
|
267
|
-
p_request ->> 'status',
|
|
268
|
-
p_request #>> '{subject,principal,tenant}',
|
|
269
|
-
p_request #>> '{subject,principal,id}',
|
|
270
|
-
p_request #>> '{subject,actor,id}',
|
|
271
|
-
p_request #>> '{subject,session}',
|
|
272
|
-
coalesce(jsonb_array_length(p_request -> 'approvals'), 0),
|
|
273
|
-
p_request ->> 'createdAt',
|
|
274
|
-
(p_request ->> 'expiresAt')::timestamptz,
|
|
275
|
-
(p_request ->> 'consumedAt')::timestamptz`;
|
|
276
|
-
/** The `ApprovalListFilter` in `p_filter`, over the row aliased `a`. */
|
|
277
|
-
const FILTER = `(p_filter ->> 'status' is null or a.status = p_filter ->> 'status')
|
|
278
|
-
and (p_filter ->> 'tenant' is null or a.tenant = p_filter ->> 'tenant')
|
|
279
|
-
and (p_filter ->> 'principalId' is null or a.principal_id = p_filter ->> 'principalId')
|
|
280
|
-
and (p_filter ->> 'actorId' is null or a.actor_id = p_filter ->> 'actorId')
|
|
281
|
-
and (p_filter ->> 'session' is null or a.session = p_filter ->> 'session')`;
|
|
282
|
-
function serverOnly(fn, args) {
|
|
283
|
-
return `revoke execute on function ${fn}(${args}) from public, anon, authenticated;`;
|
|
284
|
-
}
|
|
285
|
-
const BODY_CHECK = `${APPROVAL_STORE.table}_body_schema`;
|
|
286
|
-
/**
|
|
287
|
-
* `rls.jsonSchema`: a pg_jsonschema check that `body` matches
|
|
288
|
-
* `approval-request-v1.json`, added `not valid` and then validated. `auto`
|
|
289
|
-
* adds it only where the extension is available.
|
|
290
|
-
*/
|
|
291
|
-
function bodySchemaSql(table, mode, body = "body", check = BODY_CHECK) {
|
|
292
|
-
const statements = [
|
|
293
|
-
"create schema if not exists extensions;",
|
|
294
|
-
"create extension if not exists pg_jsonschema with schema extensions;",
|
|
295
|
-
`alter table ${table} drop constraint if exists "${check}";`,
|
|
296
|
-
`alter table ${table} add constraint "${check}" check (extensions.jsonb_matches_schema(${quoteLiteral(JSON.stringify(APPROVAL_REQUEST_SCHEMA))}::json, ${body})) not valid;`,
|
|
297
|
-
`alter table ${table} validate constraint "${check}";`
|
|
298
|
-
];
|
|
299
|
-
const comment = "-- approval store: body matches schemas/approval-request-v1.json through pg_jsonschema";
|
|
300
|
-
if (mode === true) return `\n${comment}\n${statements.join("\n")}\n`;
|
|
301
|
-
return `\n${comment}, where the extension is available
|
|
302
|
-
do $permdock$
|
|
303
|
-
begin
|
|
304
|
-
if exists (select 1 from pg_available_extensions where name = 'pg_jsonschema') then
|
|
305
|
-
${statements.map((statement) => ` ${statement}`).join("\n")}
|
|
306
|
-
end if;
|
|
307
|
-
end
|
|
308
|
-
$permdock$;\n`;
|
|
271
|
+
graph,
|
|
272
|
+
ownership,
|
|
273
|
+
schema,
|
|
274
|
+
authorize
|
|
275
|
+
};
|
|
309
276
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
* role may read the table or execute the functions: approvals are opened and
|
|
315
|
-
* resolved by the server, which grants its own role.
|
|
316
|
-
*/
|
|
317
|
-
function approvalStoreSql(ctx, jsonSchema = false) {
|
|
318
|
-
const table = qualified$1(ctx, APPROVAL_STORE.table);
|
|
319
|
-
const fn = (name) => qualified$1(ctx, name);
|
|
320
|
-
const index = (name) => `"${APPROVAL_STORE.table}_${name}"`;
|
|
321
|
-
return `-- approval store: ApprovalRequest bodies, opened, resolved and consumed through the functions below; supabaseApprovalStore calls them
|
|
322
|
-
create table if not exists ${table} (
|
|
323
|
-
token text primary key,
|
|
324
|
-
body jsonb not null,
|
|
325
|
-
status text not null check (status in ('pending', 'approved', 'rejected', 'expired')),
|
|
326
|
-
tenant text,
|
|
327
|
-
principal_id text,
|
|
328
|
-
actor_id text,
|
|
329
|
-
session text,
|
|
330
|
-
approvals integer not null default 0,
|
|
331
|
-
created_at text not null,
|
|
332
|
-
expires_at timestamptz not null,
|
|
333
|
-
consumed_at timestamptz
|
|
334
|
-
);
|
|
335
|
-
create index if not exists ${index("page")} on ${table} (created_at collate "C", token collate "C");
|
|
336
|
-
create index if not exists ${index("pending")} on ${table} (status, expires_at);
|
|
337
|
-
alter table ${table} enable row level security;
|
|
338
|
-
revoke all on table ${table} from anon, authenticated, public;
|
|
339
|
-
${jsonSchema === false ? "" : bodySchemaSql(table, jsonSchema)}
|
|
340
|
-
-- open a request; a repeated ask keeps the open record and replaces only an expired one
|
|
341
|
-
create or replace function ${fn(APPROVAL_STORE.open)}(p_request jsonb)
|
|
342
|
-
returns void
|
|
343
|
-
language sql
|
|
344
|
-
volatile
|
|
345
|
-
security definer
|
|
346
|
-
set search_path = ''
|
|
347
|
-
as $$
|
|
348
|
-
insert into ${table} as a (token, body, status, tenant, principal_id, actor_id, session, approvals, created_at, expires_at, consumed_at)
|
|
349
|
-
values (
|
|
350
|
-
${COLUMNS}
|
|
351
|
-
)
|
|
352
|
-
on conflict (token) do update
|
|
353
|
-
set body = excluded.body, status = excluded.status, tenant = excluded.tenant,
|
|
354
|
-
principal_id = excluded.principal_id, actor_id = excluded.actor_id, session = excluded.session,
|
|
355
|
-
approvals = excluded.approvals, created_at = excluded.created_at,
|
|
356
|
-
expires_at = excluded.expires_at, consumed_at = excluded.consumed_at
|
|
357
|
-
where a.status = 'expired' or a.expires_at <= now()
|
|
358
|
-
$$;
|
|
359
|
-
${serverOnly(fn(APPROVAL_STORE.open), "jsonb")}
|
|
360
|
-
|
|
361
|
-
create or replace function ${fn(APPROVAL_STORE.get)}(p_token text)
|
|
362
|
-
returns jsonb
|
|
363
|
-
language sql
|
|
364
|
-
stable
|
|
365
|
-
security definer
|
|
366
|
-
set search_path = ''
|
|
367
|
-
as $$
|
|
368
|
-
select a.body from ${table} a where a.token = p_token
|
|
369
|
-
$$;
|
|
370
|
-
${serverOnly(fn(APPROVAL_STORE.get), "text")}
|
|
371
|
-
|
|
372
|
-
-- write a verdict over the record it was computed from: still pending, unexpired, with p_seen approvals
|
|
373
|
-
create or replace function ${fn(APPROVAL_STORE.resolve)}(p_token text, p_next jsonb, p_seen integer)
|
|
374
|
-
returns jsonb
|
|
375
|
-
language sql
|
|
376
|
-
volatile
|
|
377
|
-
security definer
|
|
378
|
-
set search_path = ''
|
|
379
|
-
as $$
|
|
380
|
-
update ${table} a
|
|
381
|
-
set body = p_next,
|
|
382
|
-
status = p_next ->> 'status',
|
|
383
|
-
approvals = coalesce(jsonb_array_length(p_next -> 'approvals'), 0)
|
|
384
|
-
where a.token = p_token
|
|
385
|
-
and a.status = 'pending'
|
|
386
|
-
and a.expires_at > now()
|
|
387
|
-
and a.approvals = p_seen
|
|
388
|
-
returning a.body
|
|
389
|
-
$$;
|
|
390
|
-
${serverOnly(fn(APPROVAL_STORE.resolve), "text, jsonb, integer")}
|
|
391
|
-
|
|
392
|
-
-- consume an approved, unexpired, unconsumed request once
|
|
393
|
-
create or replace function ${fn(APPROVAL_STORE.consume)}(p_token text, p_now text)
|
|
394
|
-
returns jsonb
|
|
395
|
-
language sql
|
|
396
|
-
volatile
|
|
397
|
-
security definer
|
|
398
|
-
set search_path = ''
|
|
399
|
-
as $$
|
|
400
|
-
update ${table} a
|
|
401
|
-
set consumed_at = p_now::timestamptz,
|
|
402
|
-
body = a.body || jsonb_build_object('consumedAt', p_now)
|
|
403
|
-
where a.token = p_token
|
|
404
|
-
and a.status = 'approved'
|
|
405
|
-
and a.consumed_at is null
|
|
406
|
-
and a.expires_at > p_now::timestamptz
|
|
407
|
-
returning a.body
|
|
408
|
-
$$;
|
|
409
|
-
${serverOnly(fn(APPROVAL_STORE.consume), "text, text")}
|
|
410
|
-
|
|
411
|
-
-- one page in (createdAt, token) order after the cursor position
|
|
412
|
-
create or replace function ${fn(APPROVAL_STORE.list)}(p_filter jsonb, p_after_created text, p_after_token text, p_limit integer)
|
|
413
|
-
returns setof jsonb
|
|
414
|
-
language sql
|
|
415
|
-
stable
|
|
416
|
-
security definer
|
|
417
|
-
set search_path = ''
|
|
418
|
-
as $$
|
|
419
|
-
select a.body
|
|
420
|
-
from ${table} a
|
|
421
|
-
where ${FILTER}
|
|
422
|
-
and (p_after_created is null
|
|
423
|
-
or (a.created_at collate "C", a.token collate "C") > (p_after_created collate "C", p_after_token collate "C"))
|
|
424
|
-
order by a.created_at collate "C", a.token collate "C"
|
|
425
|
-
limit p_limit
|
|
426
|
-
$$;
|
|
427
|
-
${serverOnly(fn(APPROVAL_STORE.list), "jsonb, text, text, integer")}
|
|
428
|
-
|
|
429
|
-
create or replace function ${fn(APPROVAL_STORE.expire)}(p_now text)
|
|
430
|
-
returns integer
|
|
431
|
-
language sql
|
|
432
|
-
volatile
|
|
433
|
-
security definer
|
|
434
|
-
set search_path = ''
|
|
435
|
-
as $$
|
|
436
|
-
with expired as (
|
|
437
|
-
update ${table} a
|
|
438
|
-
set status = 'expired', body = a.body || '{"status":"expired"}'::jsonb
|
|
439
|
-
where a.status = 'pending' and a.expires_at <= p_now::timestamptz
|
|
440
|
-
returning 1
|
|
441
|
-
)
|
|
442
|
-
select count(*)::integer from expired
|
|
443
|
-
$$;
|
|
444
|
-
${serverOnly(fn(APPROVAL_STORE.expire), "text")}
|
|
445
|
-
|
|
446
|
-
-- reject every pending request the filter matches, as system:<by>
|
|
447
|
-
create or replace function ${fn(APPROVAL_STORE.cancel)}(p_filter jsonb, p_by text, p_note text, p_now text)
|
|
448
|
-
returns integer
|
|
449
|
-
language sql
|
|
450
|
-
volatile
|
|
451
|
-
security definer
|
|
452
|
-
set search_path = ''
|
|
453
|
-
as $$
|
|
454
|
-
with cancelled as (
|
|
455
|
-
update ${table} a
|
|
456
|
-
set status = 'rejected',
|
|
457
|
-
body = (a.body - 'note') || jsonb_strip_nulls(jsonb_build_object(
|
|
458
|
-
'status', 'rejected', 'resolvedAt', p_now, 'resolvedBy', 'system:' || p_by, 'note', p_note
|
|
459
|
-
))
|
|
460
|
-
where a.status = 'pending'
|
|
461
|
-
and ${FILTER.replaceAll("\n ", "\n ")}
|
|
462
|
-
returning 1
|
|
463
|
-
)
|
|
464
|
-
select count(*)::integer from cancelled
|
|
465
|
-
$$;
|
|
466
|
-
${serverOnly(fn(APPROVAL_STORE.cancel), "jsonb, text, text, text")}
|
|
467
|
-
`;
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/cli/rls/generate-options.ts
|
|
279
|
+
function isRefusal(value) {
|
|
280
|
+
return "refusal" in value;
|
|
468
281
|
}
|
|
469
|
-
/** The
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
resolvedAt: (body) => `${body} ->> 'resolvedAt'`,
|
|
481
|
-
resolvedBy: (body) => `${body} ->> 'resolvedBy'`,
|
|
482
|
-
consumedAt: (body) => `${body} ->> 'consumedAt'`
|
|
483
|
-
};
|
|
484
|
-
function mirrorsOf(adopt) {
|
|
485
|
-
const out = [];
|
|
486
|
-
for (const [field, column] of Object.entries(adopt.mirror ?? {})) {
|
|
487
|
-
if (!Object.hasOwn(MIRRORS, field)) throw new Error(`PermDock CLI: rls.approvals.mirror.${field} is not a request field; use ${Object.keys(MIRRORS).join(", ")}`);
|
|
488
|
-
if (typeof column !== "string") throw new TypeError(`PermDock CLI: rls.approvals.mirror.${field} must be a column name`);
|
|
489
|
-
out.push([column, field]);
|
|
490
|
-
}
|
|
491
|
-
return out;
|
|
282
|
+
/** The checks that need no policy: `--fields` and `--revoke-columns`. */
|
|
283
|
+
function generateOptions(input) {
|
|
284
|
+
const rls = input.config.rls;
|
|
285
|
+
const fieldsMode = input.fields ?? rls?.fields;
|
|
286
|
+
if (fieldsMode !== void 0 && fieldsMode !== "views") return { refusal: `rls generate --fields must be views (got ${fieldsMode})` };
|
|
287
|
+
const revokeColumns = input.revokeColumns === true || rls?.revokeColumns === true;
|
|
288
|
+
if (revokeColumns && fieldsMode === void 0) return { refusal: "rls generate --revoke-columns needs --fields views" };
|
|
289
|
+
return {
|
|
290
|
+
fieldsMode,
|
|
291
|
+
revokeColumns
|
|
292
|
+
};
|
|
492
293
|
}
|
|
493
|
-
/**
|
|
494
|
-
function
|
|
495
|
-
|
|
294
|
+
/** Whether `--split` names `part`, which then leaves the helpers or the warnings. */
|
|
295
|
+
function splitsPart(raw, part) {
|
|
296
|
+
const split = parseSplit(raw);
|
|
297
|
+
return typeof split !== "string" && split?.includes(part) === true;
|
|
496
298
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/cli/rls/generate-output.ts
|
|
301
|
+
function moveUndiffed(sql, label, part) {
|
|
302
|
+
const { kept, moved } = splitUndiffed(sql);
|
|
303
|
+
const [first = "", ...rest] = kept.split("\n");
|
|
304
|
+
return {
|
|
305
|
+
sql: [
|
|
306
|
+
first,
|
|
307
|
+
`-- what supabase db diff drops from this part is in ${label}`,
|
|
308
|
+
...rest
|
|
309
|
+
].join("\n"),
|
|
310
|
+
grants: `-- the privileges and view options supabase db diff drops from the ${part} part\n${moved.join("\n")}`
|
|
311
|
+
};
|
|
500
312
|
}
|
|
501
|
-
/**
|
|
502
|
-
function
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
313
|
+
/** Why `--split` cannot be written for `mode`, or `undefined`. */
|
|
314
|
+
function splitRefusal(mode, plan) {
|
|
315
|
+
const seedsWithoutOut = plan.split.includes("seeds") && plan.seedsOut === void 0;
|
|
316
|
+
const helpersWithoutSeeds = plan.split.includes("helpers") && !plan.split.includes("seeds");
|
|
317
|
+
switch (mode) {
|
|
318
|
+
case "pg-delta":
|
|
319
|
+
if (plan.explicitOut && !plan.placeholder) return `rls generate --split needs {part} in --out, or no --out for pg-delta's per-schema layout (got ${plan.outRel})`;
|
|
320
|
+
if (plan.grantsOut !== void 0) return "rls generate --grants-out is for --schemas migra: pg-delta keeps the grants in each part, so drop --grants-out";
|
|
321
|
+
if (helpersWithoutSeeds) return "rls generate --split helpers under pg-delta needs the seeds part with --seeds-out: pg-delta rejects the role_permissions rows in a declarative file";
|
|
322
|
+
if (seedsWithoutOut) return "rls generate --split seeds under pg-delta needs --seeds-out: pg-delta does not diff the role_permissions rows, so they go in a migration";
|
|
323
|
+
return;
|
|
324
|
+
case "migra":
|
|
325
|
+
if (!plan.placeholder) return `rls generate --split under migra needs {part} in --out, for example supabase/schemas/identity/056_permdock_{part}.sql (got ${plan.outRel})`;
|
|
326
|
+
if (helpersWithoutSeeds) return "rls generate --split helpers under migra needs the seeds part with --seeds-out: supabase db diff does not carry the role_permissions rows from a schema file";
|
|
327
|
+
if (seedsWithoutOut) return "rls generate --split seeds under migra needs --seeds-out: supabase db diff does not carry the role_permissions rows, so they go in a migration";
|
|
328
|
+
return;
|
|
329
|
+
case "migrations":
|
|
330
|
+
if (!plan.placeholder) return `rls generate --split needs {part} in --out, for example supabase/migrations/20260101000000_permdock_{part}.sql (got ${plan.outRel})`;
|
|
331
|
+
if (plan.grantsOut !== void 0) return "rls generate --grants-out is for --schemas migra: a hand-written migration keeps its grants, so drop --grants-out";
|
|
332
|
+
return;
|
|
333
|
+
default: return mode;
|
|
334
|
+
}
|
|
507
335
|
}
|
|
508
|
-
/**
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
${
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
336
|
+
/** The files `generate` writes: one, or one per `--split` part, plus the hook's grants under migra. */
|
|
337
|
+
function outputFiles(plan) {
|
|
338
|
+
const { input } = plan;
|
|
339
|
+
const split = parseSplit(input.split);
|
|
340
|
+
if (typeof split === "string") return split;
|
|
341
|
+
if (split?.includes("policies") === true && plan.helpersOnly) return "rls generate --helpers-only writes no policies part; drop it from --split";
|
|
342
|
+
if (split === void 0) {
|
|
343
|
+
if (input.grantsOut !== void 0) return "rls generate --grants-out needs --split with the helpers or hook part";
|
|
344
|
+
if (input.seedsOut !== void 0) return "rls generate --seeds-out needs --split with the seeds part";
|
|
345
|
+
return [{
|
|
346
|
+
part: "rls",
|
|
347
|
+
rel: plan.outRel,
|
|
348
|
+
text: plan.text
|
|
349
|
+
}];
|
|
350
|
+
}
|
|
351
|
+
if (input.target !== "sql") return "rls generate --split needs --target sql";
|
|
352
|
+
const schemas = resolveSchemas(input.cwd, input.config, input.schemas);
|
|
353
|
+
const pgDelta = schemas.mode === "pg-delta" ? schemas : void 0;
|
|
354
|
+
const placeholder = plan.outRel.includes("{part}");
|
|
355
|
+
const refused = splitRefusal(schemas.mode, {
|
|
356
|
+
split,
|
|
357
|
+
placeholder,
|
|
358
|
+
explicitOut: input.out !== void 0,
|
|
359
|
+
outRel: plan.outRel,
|
|
360
|
+
grantsOut: input.grantsOut,
|
|
361
|
+
seedsOut: input.seedsOut
|
|
362
|
+
});
|
|
363
|
+
if (refused !== void 0) return refused;
|
|
364
|
+
const at = (part, schema = plan.schema) => pgDelta === void 0 || placeholder ? partPath(plan.outRel, part) : pgDeltaPath(pgDelta.schemaDir, part, schema);
|
|
365
|
+
if (input.grantsOut !== void 0 && !split.includes("hook") && !split.includes("helpers")) return "rls generate --grants-out needs the helpers or hook part in --split";
|
|
366
|
+
if (input.seedsOut !== void 0 && !split.includes("seeds")) return "rls generate --seeds-out needs the seeds part in --split";
|
|
367
|
+
if (split.includes("hook") && input.config.supabase?.hook === void 0) return "rls generate --split hook needs supabase.hook in permdock.config.ts";
|
|
368
|
+
const sql = plan.sql();
|
|
369
|
+
const relocated = pgDelta === void 0 ? void 0 : sql.servicePolicies;
|
|
370
|
+
if (relocated !== void 0 && split.includes("policies") && !split.includes("seeds")) return "rls generate --split policies under pg-delta with rls.realtime or rls.storage needs the seeds part with --seeds-out: the declarative sync shadow database has no realtime.messages or storage.objects, so their policies go in that migration";
|
|
371
|
+
const seeds = relocated === void 0 || !split.includes("policies") ? sql.seeds : `${sql.seeds}\n${relocated}`;
|
|
372
|
+
const files = [];
|
|
373
|
+
const label = grantsLabel(input.grantsOut);
|
|
374
|
+
let grantsMarker = `${GRANTS_MARKER} schema=${plan.schema}`;
|
|
375
|
+
const grants = [];
|
|
376
|
+
for (const part of split) switch (part) {
|
|
377
|
+
case "helpers": {
|
|
378
|
+
if (label === void 0) {
|
|
379
|
+
files.push({
|
|
380
|
+
part,
|
|
381
|
+
rel: at(part),
|
|
382
|
+
text: sql.helpers
|
|
383
|
+
});
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
const moved = moveUndiffed(sql.helpers, label, part);
|
|
387
|
+
files.push({
|
|
388
|
+
part,
|
|
389
|
+
rel: at(part),
|
|
390
|
+
text: moved.sql
|
|
391
|
+
});
|
|
392
|
+
grants.push(moved.grants);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
case "seeds": {
|
|
396
|
+
const out = input.seedsOut;
|
|
397
|
+
if (out !== void 0 && isSeedsDirectory(input.cwd, out)) {
|
|
398
|
+
const planned = seedsMigration(input.cwd, out, SEEDS_MARKER, seeds, input.io.now?.() ?? dateAt());
|
|
399
|
+
if (!planned.current || input.check) files.push({
|
|
400
|
+
part,
|
|
401
|
+
rel: planned.rel,
|
|
402
|
+
text: seeds
|
|
403
|
+
});
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
files.push({
|
|
407
|
+
part,
|
|
408
|
+
rel: out ?? at(part),
|
|
409
|
+
text: seeds
|
|
410
|
+
});
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
case "indexes":
|
|
414
|
+
files.push({
|
|
415
|
+
part,
|
|
416
|
+
rel: at(part),
|
|
417
|
+
text: sql.indexes
|
|
418
|
+
});
|
|
419
|
+
break;
|
|
420
|
+
case "policies":
|
|
421
|
+
files.push({
|
|
422
|
+
part,
|
|
423
|
+
rel: at(part),
|
|
424
|
+
text: pgDelta === void 0 ? sql.policies : sql.declarativePolicies
|
|
425
|
+
});
|
|
426
|
+
break;
|
|
427
|
+
case "hook": {
|
|
428
|
+
const hook = supabaseHookSql(plan.scopes, input.config, {}, label);
|
|
429
|
+
files.push({
|
|
430
|
+
part,
|
|
431
|
+
rel: at(part, hook.manifest.hook.schema),
|
|
432
|
+
text: hook.sql
|
|
433
|
+
});
|
|
434
|
+
const [marker = grantsMarker, ...rest] = hook.grants.split("\n");
|
|
435
|
+
grantsMarker = marker;
|
|
436
|
+
grants.push(rest.join("\n").trimEnd());
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
default: return part;
|
|
440
|
+
}
|
|
441
|
+
if (input.grantsOut !== void 0) files.push({
|
|
442
|
+
part: "grants",
|
|
443
|
+
rel: input.grantsOut,
|
|
444
|
+
text: `${[grantsMarker, ...grants].join("\n")}\n`
|
|
445
|
+
});
|
|
446
|
+
return files;
|
|
447
|
+
}
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/cli/rls/backfill.ts
|
|
450
|
+
const PREFIX$1 = "PermDock CLI";
|
|
451
|
+
/** A column of the roles-table row `r`. */
|
|
452
|
+
function col(name) {
|
|
453
|
+
return `r.${quoteIdent$1(name)}`;
|
|
454
|
+
}
|
|
455
|
+
function schemaOf(table) {
|
|
456
|
+
return table.includes(".") ? beforeFirst(table, ".") : "public";
|
|
457
|
+
}
|
|
458
|
+
/** The roles-table column a reference joins on, after checking it points at `roles`. */
|
|
459
|
+
function roleRef(role, table, roles, label) {
|
|
460
|
+
const pairs = Object.entries(role.on);
|
|
461
|
+
const [pair] = pairs;
|
|
462
|
+
if (pair === void 0 || pairs.length !== 1) throw configError(`${PREFIX$1}: ${label}.on must map exactly one column of ${table} to ${role.through}, for example { role_id: 'id' }`);
|
|
463
|
+
const through = role.through.includes(".") ? role.through : `${schemaOf(table)}.${role.through}`;
|
|
464
|
+
if (qualifySqlTable(through) !== qualifySqlTable(roles.table)) throw configError(`${PREFIX$1}: ${label}.through must be rls.customRoleWrites.roles.table (${roles.table}), got ${role.through}`);
|
|
465
|
+
if (role.column !== roles.key) throw configError(`${PREFIX$1}: ${label}.column must be rls.customRoleWrites.roles.key (${roles.key}), got ${role.column}`);
|
|
466
|
+
const [ref, id] = pair;
|
|
467
|
+
return {
|
|
468
|
+
ref,
|
|
469
|
+
id
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* A legacy key as SQL the way `mapKey` maps it: `rls.migrate.keys`, then
|
|
474
|
+
* `renamed`, then the longest `rls.migrate.prefixes` match, and a former key
|
|
475
|
+
* read as its current key.
|
|
476
|
+
*/
|
|
477
|
+
function legacyKeySql(expr, migrate, renamed) {
|
|
478
|
+
const exact = new Map(Object.entries(renamed));
|
|
479
|
+
for (const [from, to] of Object.entries(migrate?.keys ?? {})) exact.set(from, to);
|
|
480
|
+
const prefixes = Object.entries(migrate?.prefixes ?? {}).toSorted(([a], [b]) => b.length - a.length || byCodeUnit(a, b));
|
|
481
|
+
const prefixed = prefixes.length === 0 ? expr : `(case ${prefixes.map(([from, to]) => `when left(${expr}, ${String(from.length)}) = ${quoteLiteral(from)} then ${quoteLiteral(to)} || substr(${expr}, ${String(from.length + 1)})`).join(" ")} else ${expr} end)`;
|
|
482
|
+
const mapped = exact.size === 0 ? prefixed : `(case ${expr} ${[...exact.entries()].toSorted(byKey).map(([from, to]) => `when ${quoteLiteral(from)} then ${quoteLiteral(to)}`).join(" ")} else ${prefixed} end)`;
|
|
483
|
+
const renames = Object.entries(renamed).toSorted(byKey);
|
|
484
|
+
return currentKeyExpr(renames, mapped);
|
|
485
|
+
}
|
|
486
|
+
function sourceSql(source, table, label) {
|
|
487
|
+
return roleColumn(source, table, "x", {
|
|
488
|
+
label,
|
|
489
|
+
prefix: PREFIX$1
|
|
490
|
+
}).lookup;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* The `--backfill-out` migration: every custom role in the application's
|
|
494
|
+
* roles table that has no rows in `custom_role_permissions` or
|
|
495
|
+
* `custom_role_includes` yet is saved through
|
|
496
|
+
* `permdock_trusted_replace_custom_role_grants`, with the entries PermDock
|
|
497
|
+
* cannot hold left out and listed in one warning. Running it again copies
|
|
498
|
+
* nothing, so a role edited after the copy keeps its edits.
|
|
499
|
+
*/
|
|
500
|
+
function backfillSql(ctx, from, migrate) {
|
|
501
|
+
const custom = ctx.customRoles;
|
|
502
|
+
const root = ctx.scopes[0]?.name;
|
|
503
|
+
if (custom === void 0 || root === void 0) throw configError(`${PREFIX$1}: rls.customRoles.from needs custom roles on`);
|
|
504
|
+
if (ctx.authorize !== "database") throw configError(`${PREFIX$1}: rls.customRoles.from needs database mode; in jwt mode custom roles ride the token, so build the grants claim with customRoleClaim`);
|
|
505
|
+
const roles = custom.table;
|
|
506
|
+
if (roles === void 0) throw configError(`${PREFIX$1}: rls.customRoles.from needs rls.customRoleWrites.roles, the roles table each row belongs to`);
|
|
507
|
+
const leveled = custom.levels === true;
|
|
508
|
+
if (from.permissions.level !== void 0 && !leveled) throw configError(`${PREFIX$1}: rls.customRoles.from.permissions.level is set, but no resource declares levels`);
|
|
509
|
+
const tenantType = tenantTypeOf(ctx);
|
|
510
|
+
const rolesTable = quoteQualified(roles.table);
|
|
511
|
+
const tenant = roles.tenant === void 0 ? `null::${tenantType}` : `${col(roles.tenant)}::${tenantType}`;
|
|
512
|
+
const scope = roles.scope === void 0 ? `case when ${tenant} is null then 'global' else ${quoteLiteral(root)} end` : `case when ${tenant} is null then 'global' else ${col(roles.scope)}::text end`;
|
|
513
|
+
const scopeId = roles.id === void 0 ? "null::text" : `${col(roles.id)}::text`;
|
|
514
|
+
const skipped = roles.skip === void 0 ? "false" : `coalesce(${col(roles.skip)}, false)`;
|
|
515
|
+
const renamed = custom.renamed ?? {};
|
|
516
|
+
const renames = Object.entries(renamed).toSorted(byKey);
|
|
517
|
+
const current = (expr) => currentKeyExpr(renames, expr);
|
|
518
|
+
const known = sqlTextArray([...custom.permissions ?? [], ...renames.map(([former]) => former)]);
|
|
519
|
+
const declared = sqlTextArray(custom.declared);
|
|
520
|
+
const scopes = sqlTextArray(ctx.scopes.map((item) => item.name));
|
|
521
|
+
const ceiling = qualified(ctx, CUSTOM_ROLES.ceiling);
|
|
522
|
+
const rp = qualified(ctx, "role_permissions");
|
|
523
|
+
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
524
|
+
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
525
|
+
const replace = qualified(ctx, CUSTOM_ROLES.trustedReplace);
|
|
526
|
+
const p = from.permissions;
|
|
527
|
+
const pTable = qualifySqlTable(p.table);
|
|
528
|
+
const pRef = roleRef(p.role, p.table, roles, "rls.customRoles.from.permissions.role");
|
|
529
|
+
const legacy = sourceSql(p.permission, pTable, "rls.customRoles.from.permissions.permission");
|
|
530
|
+
const effect = p.effect === void 0 ? "'allow'" : `coalesce(lower(x.${quoteIdent$1(p.effect)}::text), 'allow')`;
|
|
531
|
+
const level = p.level === void 0 ? "null::text" : `nullif(x.${quoteIdent$1(p.level)}::text, '')`;
|
|
532
|
+
const inCeiling = (key) => `exists (select 1 from ${ceiling} c where c.scope = v.scope and c.effect = 'allow' and ${current("c.permission")} = ${key})`;
|
|
533
|
+
const levelCheck = leveled ? `
|
|
534
|
+
when m.effect = 'allow' and m.level is not null and not exists (
|
|
535
|
+
select 1 from ${ceiling} c
|
|
536
|
+
where c.scope = v.scope and c.effect = 'allow' and ${current("c.permission")} = m.key
|
|
537
|
+
and split_part(c.grant_key, '@', 2) = m.level
|
|
538
|
+
) then 'level ' || m.level || ' is not declared'` : "";
|
|
539
|
+
const allowEntry = leveled ? "j.key || coalesce('@' || j.level, '')" : "j.key";
|
|
540
|
+
const i = from.includes;
|
|
541
|
+
const iTable = i === void 0 ? void 0 : qualifySqlTable(i.table);
|
|
542
|
+
const iRef = i === void 0 || iTable === void 0 ? void 0 : roleRef(i.role, i.table, roles, "rls.customRoles.from.includes.role");
|
|
543
|
+
const included = i === void 0 || iTable === void 0 ? void 0 : sourceSql(i.include, iTable, "rls.customRoles.from.includes.include");
|
|
544
|
+
const refs = [`${col(pRef.id)} as permissions_ref`, ...iRef === void 0 ? [] : [`${col(iRef.id)} as includes_ref`]].join(", ");
|
|
545
|
+
const includeBlock = i === void 0 || iTable === void 0 || iRef === void 0 || included === void 0 ? " v_include := null;" : ` with raw as (
|
|
546
|
+
select ${included} as role
|
|
547
|
+
from ${quoteTable(iTable)} x
|
|
548
|
+
where x.${quoteIdent$1(iRef.ref)} = v.includes_ref
|
|
549
|
+
),
|
|
550
|
+
judged as (
|
|
551
|
+
select n.role, case
|
|
552
|
+
when n.role is null or not (n.role = any(${declared})) then 'not a declared role'
|
|
553
|
+
when exists (
|
|
554
|
+
select 1 from ${rp} rp
|
|
555
|
+
where rp.role = n.role and rp.effect = 'allow'
|
|
556
|
+
and not ${inCeiling(current("rp.permission"))}
|
|
557
|
+
) then 'allows permissions outside the ceiling of ' || v.scope
|
|
558
|
+
end as dropped
|
|
559
|
+
from raw n
|
|
560
|
+
)
|
|
561
|
+
select array_agg(distinct j.role) filter (where j.dropped is null),
|
|
562
|
+
array_agg(v.role || ': includes ' || coalesce(j.role, 'null') || ', ' || j.dropped) filter (where j.dropped is not null)
|
|
563
|
+
into v_include, v_lost
|
|
564
|
+
from judged j;
|
|
565
|
+
v_dropped := v_dropped || coalesce(v_lost, '{}'::text[]);`;
|
|
566
|
+
return `${BACKFILL_MARKER} schema=${ctx.schema ?? "permdock"}
|
|
567
|
+
-- custom roles copied from ${qualifySqlTable(roles.table)} (rls.customRoles.from) through ${replace}
|
|
568
|
+
-- a role that already has rows is left as it is; entries PermDock cannot hold are dropped and listed in one warning
|
|
569
|
+
do $permdock$
|
|
570
|
+
declare
|
|
571
|
+
v record;
|
|
572
|
+
v_allow text[];
|
|
573
|
+
v_deny text[];
|
|
574
|
+
v_include text[];
|
|
575
|
+
v_lost text[];
|
|
576
|
+
v_dropped text[] := '{}'::text[];
|
|
577
|
+
v_copied integer := 0;
|
|
578
|
+
v_present integer := 0;
|
|
579
|
+
begin
|
|
580
|
+
for v in
|
|
581
|
+
select ${tenant} as tenant_id, ${scope} as scope, ${scopeId} as scope_id, ${col(roles.key)}::text as role, ${refs}
|
|
582
|
+
from ${rolesTable} r
|
|
583
|
+
where not ${skipped} and not (${col(roles.key)}::text = any(${declared}))
|
|
584
|
+
order by 4, 2, 1, 3
|
|
585
|
+
loop
|
|
586
|
+
if v.role is null or not (v.scope = 'global' or v.scope = any(${scopes})) then
|
|
587
|
+
v_dropped := v_dropped || (coalesce(v.role, 'null') || ': scope ' || coalesce(v.scope, 'null') || ' is not declared');
|
|
588
|
+
continue;
|
|
589
|
+
end if;
|
|
590
|
+
if exists (
|
|
591
|
+
select 1 from ${perms} c
|
|
592
|
+
where c.tenant_id is not distinct from v.tenant_id and c.scope = v.scope and c.scope_id is not distinct from v.scope_id and c.role = v.role
|
|
593
|
+
) or exists (
|
|
594
|
+
select 1 from ${includes} c
|
|
595
|
+
where c.tenant_id is not distinct from v.tenant_id and c.scope = v.scope and c.scope_id is not distinct from v.scope_id and c.role = v.role
|
|
596
|
+
) then
|
|
597
|
+
v_present := v_present + 1;
|
|
598
|
+
continue;
|
|
599
|
+
end if;
|
|
600
|
+
with raw as (
|
|
601
|
+
select ${legacy} as legacy, ${effect} as effect, ${level} as level
|
|
602
|
+
from ${quoteTable(pTable)} x
|
|
603
|
+
where x.${quoteIdent$1(pRef.ref)} = v.permissions_ref
|
|
604
|
+
),
|
|
605
|
+
mapped as (
|
|
606
|
+
select n.legacy, ${legacyKeySql("n.legacy", migrate, renamed)} as key, n.effect, n.level
|
|
607
|
+
from raw n
|
|
608
|
+
),
|
|
609
|
+
judged as (
|
|
610
|
+
select m.*, case
|
|
611
|
+
when m.effect not in ('allow', 'deny') then 'effect ' || m.effect || ' is not allow or deny'
|
|
612
|
+
when m.key is null or not (m.key = any(${known})) then 'not a declared permission'
|
|
613
|
+
when m.effect = 'allow' and not ${inCeiling("m.key")} then 'outside the ceiling of ' || v.scope${levelCheck}
|
|
614
|
+
end as dropped
|
|
615
|
+
from mapped m
|
|
616
|
+
)
|
|
617
|
+
select array_agg(distinct ${allowEntry}) filter (where j.dropped is null and j.effect = 'allow'),
|
|
618
|
+
array_agg(distinct j.key) filter (where j.dropped is null and j.effect = 'deny'),
|
|
619
|
+
array_agg(v.role || ': ' || coalesce(j.legacy, 'null') || ', ' || j.dropped) filter (where j.dropped is not null)
|
|
620
|
+
into v_allow, v_deny, v_lost
|
|
621
|
+
from judged j;
|
|
622
|
+
v_dropped := v_dropped || coalesce(v_lost, '{}'::text[]);
|
|
623
|
+
${includeBlock}
|
|
624
|
+
if v_allow is null and v_deny is null and v_include is null then
|
|
625
|
+
continue;
|
|
626
|
+
end if;
|
|
627
|
+
perform ${replace}(v.tenant_id, v.scope, v.scope_id, v.role, v_allow, v_deny, v_include);
|
|
628
|
+
v_copied := v_copied + 1;
|
|
629
|
+
end loop;
|
|
630
|
+
raise notice 'permdock backfill: copied % custom role(s), % already present', v_copied, v_present;
|
|
631
|
+
if cardinality(v_dropped) > 0 then
|
|
632
|
+
raise warning 'permdock backfill: dropped % entr(ies) PermDock cannot hold: %', cardinality(v_dropped), array_to_string(v_dropped[1:50], '; ');
|
|
633
|
+
end if;
|
|
634
|
+
end
|
|
635
|
+
$permdock$;
|
|
695
636
|
`;
|
|
696
637
|
}
|
|
697
638
|
//#endregion
|
|
698
|
-
//#region src/cli/rls
|
|
639
|
+
//#region src/cli/rls/break-glass.ts
|
|
699
640
|
/** The audit table every generated break-glass read writes to. Part of the SQL contract. */
|
|
700
641
|
const BREAK_GLASS_AUDIT = "permdock_break_glass_audit";
|
|
701
|
-
const IDENT = /^[a-z][a-z0-9_]*$/u;
|
|
642
|
+
const IDENT$1 = /^[a-z][a-z0-9_]*$/u;
|
|
702
643
|
/**
|
|
703
644
|
* The resources a `breakGlass` grant targets, with the table each reads. A
|
|
704
645
|
* break-glass grant is non-portable, so `compileGrants` never turns it into a
|
|
@@ -747,22 +688,19 @@ function rowsFilter(ctx, entry) {
|
|
|
747
688
|
const asked = `p_permission = ${quoteLiteral(grant.permission)}`;
|
|
748
689
|
if (grant.scope === "anyone") {
|
|
749
690
|
if (grant.root === void 0) return [`(${asked})`];
|
|
750
|
-
return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified
|
|
691
|
+
return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified(ctx, memberIdsHelper(grant.root))}()))`];
|
|
751
692
|
}
|
|
752
693
|
if (grant.scope !== "global" && grant.column === void 0) return [];
|
|
753
694
|
return [`(${asked} and ${accessSql(ctx, grant.scope, breakGlassKey(grant.permission), grant.column)})`];
|
|
754
695
|
});
|
|
755
696
|
return arms.length === 0 ? "false" : [...new Set(arms)].join("\n or ");
|
|
756
697
|
}
|
|
757
|
-
function functionName(resource) {
|
|
758
|
-
if (!IDENT.test(resource)) throw
|
|
698
|
+
function functionName$1(resource) {
|
|
699
|
+
if (!IDENT$1.test(resource)) throw configError(`PermDock CLI: unsafe break-glass resource '${resource}'`);
|
|
759
700
|
return `permdock_break_glass_${resource}`;
|
|
760
701
|
}
|
|
761
|
-
function
|
|
762
|
-
|
|
763
|
-
}
|
|
764
|
-
function auditSql(ctx) {
|
|
765
|
-
const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
|
|
702
|
+
function auditSql$1(ctx) {
|
|
703
|
+
const audit = qualified(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
|
|
766
704
|
return `-- break-glass audit: one row per session that read restricted data
|
|
767
705
|
-- RLS never compiles a break-glass override, so restricted rows stay denied to a direct read
|
|
768
706
|
create table if not exists ${audit} (
|
|
@@ -777,9 +715,9 @@ alter table ${audit} enable row level security;
|
|
|
777
715
|
revoke all on table ${audit} from anon, authenticated, public;`;
|
|
778
716
|
}
|
|
779
717
|
function readFunctionSql(ctx, entry) {
|
|
780
|
-
const fn = qualified
|
|
781
|
-
const audit = qualified
|
|
782
|
-
const table =
|
|
718
|
+
const fn = qualified(ctx, functionName$1(entry.resource));
|
|
719
|
+
const audit = qualified(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
|
|
720
|
+
const table = quoteQualified(entry.table);
|
|
783
721
|
const session = subjectClaimJsonSql(ctx, "break_glass");
|
|
784
722
|
const subject = subjectIdSql(ctx);
|
|
785
723
|
return `-- ${entry.resource}: reads restricted rows through a checked, audited break-glass session
|
|
@@ -797,8 +735,8 @@ begin
|
|
|
797
735
|
or session ->> 'v' <> '1'
|
|
798
736
|
or session ->> 'permission' <> p_permission
|
|
799
737
|
or coalesce(session ->> 'reason', '') = ''
|
|
800
|
-
or jsonb_typeof(session -> '
|
|
801
|
-
or (session ->> '
|
|
738
|
+
or jsonb_typeof(session -> 'expires_at') <> 'number'
|
|
739
|
+
or (session ->> 'expires_at')::numeric <= extract(epoch from now())
|
|
802
740
|
then
|
|
803
741
|
raise exception 'PermDock: no valid break-glass session for %', p_permission
|
|
804
742
|
using errcode = '42501';
|
|
@@ -827,10 +765,10 @@ grant execute on function ${fn}(text) to authenticated;`;
|
|
|
827
765
|
*/
|
|
828
766
|
function breakGlassSql(ctx, entries) {
|
|
829
767
|
if (entries.length === 0) return "";
|
|
830
|
-
return [auditSql(ctx), ...entries.map((entry) => readFunctionSql(ctx, entry))].join("\n\n");
|
|
768
|
+
return [auditSql$1(ctx), ...entries.map((entry) => readFunctionSql(ctx, entry))].join("\n\n");
|
|
831
769
|
}
|
|
832
770
|
//#endregion
|
|
833
|
-
//#region src/cli/rls
|
|
771
|
+
//#region src/cli/rls/emit.ts
|
|
834
772
|
const HEADER = "-- generated by permdock rls generate\n-- fail-closed: never target a bypass role\n";
|
|
835
773
|
const TRUSTED_READER_GRANT = /^grant execute on function [^\n]+\.permdock_trusted_role_permissions\([^)\n]*\) to [^\n;]+;$/gmu;
|
|
836
774
|
function assertNoServiceRole(text) {
|
|
@@ -855,7 +793,7 @@ function tablesOf(policies) {
|
|
|
855
793
|
/** Enable (and force) row level security, then the table grants for the commands a policy allows. */
|
|
856
794
|
function tableSql(table, policies, force, views) {
|
|
857
795
|
const closed = views.find((view) => view.table === table && view.companion !== void 0);
|
|
858
|
-
const target =
|
|
796
|
+
const target = quoteQualified(table);
|
|
859
797
|
const lines = [`alter table ${target} enable row level security;`];
|
|
860
798
|
if (force) lines.push(`alter table ${target} force row level security;`);
|
|
861
799
|
lines.push(`revoke all on table ${target} from anon, authenticated;`);
|
|
@@ -875,7 +813,7 @@ function tableSql(table, policies, force, views) {
|
|
|
875
813
|
}
|
|
876
814
|
/** `drop policy if exists` and `create policy` for one policy, without the table's grants. */
|
|
877
815
|
function policySql(item) {
|
|
878
|
-
const target =
|
|
816
|
+
const target = quoteQualified(item.table);
|
|
879
817
|
const lines = [
|
|
880
818
|
`drop policy if exists ${quoteIdent$1(item.name)} on ${target};`,
|
|
881
819
|
`create policy ${quoteIdent$1(item.name)}`,
|
|
@@ -930,7 +868,7 @@ function pascalCase(name) {
|
|
|
930
868
|
}
|
|
931
869
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
932
870
|
function identifier(name, what) {
|
|
933
|
-
if (!IDENTIFIER.test(name)) throw
|
|
871
|
+
if (!IDENTIFIER.test(name)) throw configError(`PermDock CLI: ${what} ${JSON.stringify(name)} is not an identifier`);
|
|
934
872
|
return name;
|
|
935
873
|
}
|
|
936
874
|
function quoted(text) {
|
|
@@ -994,7 +932,7 @@ function emitPrisma(policies, options = {}) {
|
|
|
994
932
|
""
|
|
995
933
|
];
|
|
996
934
|
for (const item of policies) {
|
|
997
|
-
if (item.effect === "deny") throw
|
|
935
|
+
if (item.effect === "deny") throw configError(`PermDock CLI: Prisma policy blocks are permissive only, so ${item.name} (a deny) needs --target sql`);
|
|
998
936
|
lines.push(`policy_${item.command} ${identifier(item.name, "policy name")} {`, ` target = ${modelOf(item.table)}`, ` roles = [${item.roles.join(", ")}]`);
|
|
999
937
|
if (item.using !== void 0) lines.push(` using = ${JSON.stringify(item.using)}`);
|
|
1000
938
|
if (item.check !== void 0) lines.push(` withCheck = ${JSON.stringify(item.check)}`);
|
|
@@ -1018,24 +956,24 @@ function defaultOut(target) {
|
|
|
1018
956
|
}
|
|
1019
957
|
}
|
|
1020
958
|
//#endregion
|
|
1021
|
-
//#region src/cli/rls
|
|
959
|
+
//#region src/cli/rls/realtime-storage.ts
|
|
1022
960
|
const SCOPE_SEGMENT = /^\{([a-z][a-z0-9_]*)\}$/u;
|
|
1023
961
|
const LITERAL_SEGMENT = /^[^{}:\s]+$/u;
|
|
1024
962
|
const TOPIC = "(select realtime.topic())";
|
|
1025
963
|
function policyName(prefix, name, command) {
|
|
1026
964
|
const full = `permdock_${prefix}_${name.replaceAll(/[^A-Za-z0-9]+/gu, "_").replaceAll(/^_|_$/gu, "")}_${command}`;
|
|
1027
|
-
if (new TextEncoder().encode(full).length > 63) throw
|
|
965
|
+
if (new TextEncoder().encode(full).length > 63) throw configError(`PermDock CLI: the ${prefix} policy name ${full} is longer than Postgres's 63 bytes; shorten '${name}'`);
|
|
1028
966
|
return full;
|
|
1029
967
|
}
|
|
1030
968
|
function grantKey(facts, ref, where, warnings) {
|
|
1031
969
|
if (ref === void 0) return;
|
|
1032
970
|
const key = typeof ref === "object" && ref !== null ? Reflect.get(ref, "key") : ref;
|
|
1033
|
-
if (typeof key !== "string" || !facts.keys.has(key)) throw
|
|
971
|
+
if (typeof key !== "string" || !facts.keys.has(key)) throw configError(`PermDock CLI: ${where} must be a permission reference from the definitions (got ${JSON.stringify(key)})`);
|
|
1034
972
|
if (facts.rowConditions.has(key)) warnings.push(`${where} is ${key}, whose grants carry row conditions (a relation, a where or another condition): the policy admits only the instances its role allows without a condition reach, through permitted_<scope>_ids_by_permission`);
|
|
1035
973
|
return key;
|
|
1036
974
|
}
|
|
1037
975
|
function declaredScope(facts, scope, where) {
|
|
1038
|
-
if (!facts.scopes.has(scope)) throw
|
|
976
|
+
if (!facts.scopes.has(scope)) throw configError(`PermDock CLI: ${where} names the scope '${scope}', which definePolicy({ scopes }) does not declare`);
|
|
1039
977
|
return scope;
|
|
1040
978
|
}
|
|
1041
979
|
/**
|
|
@@ -1044,7 +982,7 @@ function declaredScope(facts, scope, where) {
|
|
|
1044
982
|
* relationship or a `where` on the same permission never widens it.
|
|
1045
983
|
*/
|
|
1046
984
|
function permitted(ctx, value, scope, key) {
|
|
1047
|
-
return `${value} in (select x::text from ${qualified
|
|
985
|
+
return `${value} in (select x::text from ${qualified(ctx, permittedByPermissionHelper(scope))}(${quoteLiteral(key)}) x)`;
|
|
1048
986
|
}
|
|
1049
987
|
function escapeRegex(text) {
|
|
1050
988
|
return text.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
|
|
@@ -1061,7 +999,7 @@ function realtimePolicies(ctx, facts, realtime, warnings) {
|
|
|
1061
999
|
}];
|
|
1062
1000
|
});
|
|
1063
1001
|
const [placeholder] = placeholders;
|
|
1064
|
-
if (placeholder === void 0 || placeholders.length > 1 || segments.some((segment, index) => index !== placeholder.index && !LITERAL_SEGMENT.test(segment))) throw
|
|
1002
|
+
if (placeholder === void 0 || placeholders.length > 1 || segments.some((segment, index) => index !== placeholder.index && !LITERAL_SEGMENT.test(segment))) throw configError(`PermDock CLI: ${where} must be ':'-separated segments with exactly one '{<scope>}' segment, such as 'org:{organization}:chat'`);
|
|
1065
1003
|
const scope = declaredScope(facts, placeholder.scope, where);
|
|
1066
1004
|
const regex = `^${segments.map((segment, index) => index === placeholder.index ? "[^:]+" : escapeRegex(segment)).join(":")}$`;
|
|
1067
1005
|
const id = `split_part(${TOPIC}, ':', ${String(placeholder.index + 1)})`;
|
|
@@ -1070,7 +1008,7 @@ function realtimePolicies(ctx, facts, realtime, warnings) {
|
|
|
1070
1008
|
and ${permitted(ctx, id, scope, key)}`;
|
|
1071
1009
|
const read = grantKey(facts, topic.read, `${where}.read`, warnings);
|
|
1072
1010
|
const write = grantKey(facts, topic.write, `${where}.write`, warnings);
|
|
1073
|
-
if (read === void 0) throw
|
|
1011
|
+
if (read === void 0) throw configError(`PermDock CLI: ${where}.read is required`);
|
|
1074
1012
|
const policies = [{
|
|
1075
1013
|
name: policyName("realtime", pattern, "select"),
|
|
1076
1014
|
table: "realtime.messages",
|
|
@@ -1092,7 +1030,7 @@ function realtimePolicies(ctx, facts, realtime, warnings) {
|
|
|
1092
1030
|
}
|
|
1093
1031
|
function folderOf(bucket, where) {
|
|
1094
1032
|
const folder = bucket.folder ?? 1;
|
|
1095
|
-
if (!Number.isInteger(folder) || folder < 1) throw
|
|
1033
|
+
if (!Number.isInteger(folder) || folder < 1) throw configError(`PermDock CLI: ${where}.folder must be a positive integer (got ${String(folder)})`);
|
|
1096
1034
|
return folder;
|
|
1097
1035
|
}
|
|
1098
1036
|
function storagePolicies(ctx, facts, storage, warnings) {
|
|
@@ -1137,7 +1075,7 @@ function storagePolicies(ctx, facts, storage, warnings) {
|
|
|
1137
1075
|
...command === "select" || command === "delete" ? {} : { check: expression }
|
|
1138
1076
|
}];
|
|
1139
1077
|
});
|
|
1140
|
-
if (policies.length === 0) throw
|
|
1078
|
+
if (policies.length === 0) throw configError(`PermDock CLI: ${where} needs read, write or delete`);
|
|
1141
1079
|
return policies;
|
|
1142
1080
|
});
|
|
1143
1081
|
}
|
|
@@ -1151,22 +1089,22 @@ function storagePolicies(ctx, facts, storage, warnings) {
|
|
|
1151
1089
|
*/
|
|
1152
1090
|
function realtimeStoragePolicies(ctx, facts, config, warnings = []) {
|
|
1153
1091
|
if (config.realtime === void 0 && config.storage === void 0) return [];
|
|
1154
|
-
if (ctx.dialect !== "supabase") throw
|
|
1092
|
+
if (ctx.dialect !== "supabase") throw configError(`PermDock CLI: rls.realtime and rls.storage need --dialect supabase (got ${ctx.dialect})`);
|
|
1155
1093
|
const policies = [...config.realtime === void 0 ? [] : realtimePolicies(ctx, facts, config.realtime, warnings), ...config.storage === void 0 ? [] : storagePolicies(ctx, facts, config.storage, warnings)];
|
|
1156
1094
|
const seen = /* @__PURE__ */ new Set();
|
|
1157
1095
|
for (const { name } of policies) {
|
|
1158
|
-
if (seen.has(name)) throw
|
|
1096
|
+
if (seen.has(name)) throw configError(`PermDock CLI: two rls.realtime or rls.storage entries both name the policy ${name}; rename one`);
|
|
1159
1097
|
seen.add(name);
|
|
1160
1098
|
}
|
|
1161
1099
|
return policies;
|
|
1162
1100
|
}
|
|
1163
1101
|
//#endregion
|
|
1164
|
-
//#region src/cli/rls
|
|
1102
|
+
//#region src/cli/rls/rows.ts
|
|
1165
1103
|
/** `permitted_<resource>_rows(p_permission)`: the resource's row ids the subject may act on with one permission. */
|
|
1166
1104
|
function rowsHelper(resource) {
|
|
1167
1105
|
const name = graphSqlName(resource);
|
|
1168
1106
|
const helper = `permitted_${name ?? resource}_rows`;
|
|
1169
|
-
if (name === void 0 || new TextEncoder().encode(`${helper}_for`).length > 63) throw
|
|
1107
|
+
if (name === void 0 || new TextEncoder().encode(`${helper}_for`).length > 63) throw configError(`PermDock CLI: rls.rowHelpers cannot name a helper for resource '${resource}'; use a SQL name of at most 45 bytes`);
|
|
1170
1108
|
return helper;
|
|
1171
1109
|
}
|
|
1172
1110
|
/** The branches a row check of a permission reads: its allows and denies on rows that exist, for signed-in callers. */
|
|
@@ -1243,7 +1181,7 @@ function inheritTargets(policy) {
|
|
|
1243
1181
|
return [...out].toSorted();
|
|
1244
1182
|
}
|
|
1245
1183
|
function stubSql(ctx, resource) {
|
|
1246
|
-
return `create or replace function ${qualified
|
|
1184
|
+
return `create or replace function ${qualified(ctx, rowsHelper(resource))}(p_permission text)
|
|
1247
1185
|
returns setof text
|
|
1248
1186
|
language sql
|
|
1249
1187
|
stable
|
|
@@ -1254,7 +1192,7 @@ as $$
|
|
|
1254
1192
|
$$;`;
|
|
1255
1193
|
}
|
|
1256
1194
|
function rowSql(ctx, rows, table, where) {
|
|
1257
|
-
const fn = qualified
|
|
1195
|
+
const fn = qualified(ctx, rows.replace(/_rows$/u, "_row"));
|
|
1258
1196
|
return `create or replace function ${fn}(p_row ${table}, p_permission text)
|
|
1259
1197
|
returns boolean
|
|
1260
1198
|
language sql
|
|
@@ -1269,8 +1207,8 @@ grant execute on function ${fn}(${table}, text) to authenticated;`;
|
|
|
1269
1207
|
}
|
|
1270
1208
|
function resourceSql(ctx, node, branches, tables) {
|
|
1271
1209
|
const name = rowsHelper(node.name);
|
|
1272
|
-
const fn = qualified
|
|
1273
|
-
const table =
|
|
1210
|
+
const fn = qualified(ctx, name);
|
|
1211
|
+
const table = quoteQualified(tableFor(node.name, tables));
|
|
1274
1212
|
const cases = [...branches.keys()].toSorted().flatMap((key) => {
|
|
1275
1213
|
const sql = permissionCase(key, branches.get(key) ?? []);
|
|
1276
1214
|
return sql === void 0 ? [] : [sql];
|
|
@@ -1293,7 +1231,7 @@ ${rowSql(ctx, name, table, where)}`;
|
|
|
1293
1231
|
if (ctx.dialect === "neon") return rows;
|
|
1294
1232
|
return `${rows}
|
|
1295
1233
|
-- the same rows for a user the caller names, with claims it supplies; no client role may execute it
|
|
1296
|
-
${forUserSql(ctx, qualified
|
|
1234
|
+
${forUserSql(ctx, qualified(ctx, `${name}_for`), fn)}`;
|
|
1297
1235
|
}
|
|
1298
1236
|
/**
|
|
1299
1237
|
* `permitted_<resource>_rows(p_permission)` for each resource `select`
|
|
@@ -1313,14 +1251,96 @@ function rowHelpersSql(ctx, policy, branches, select, tables, warnings) {
|
|
|
1313
1251
|
const chunks = called.map((name) => stubSql(ctx, name));
|
|
1314
1252
|
for (const name of names) {
|
|
1315
1253
|
const node = policy.resources.get(name);
|
|
1316
|
-
if (node === void 0) throw
|
|
1254
|
+
if (node === void 0) throw configError(`PermDock CLI: rls.rowHelpers names '${name}', which the policy does not declare`);
|
|
1317
1255
|
chunks.push(resourceSql(ctx, node, rowBranchesOf(branches, name), tables));
|
|
1318
1256
|
}
|
|
1319
1257
|
if (chunks.length > 0 && ctx.dialect === "neon") warnings.push("rls.rowHelpers: the neon dialect reads the subject from auth.session(), which SQL cannot set, so no permitted_<resource>_rows_for is written");
|
|
1320
1258
|
return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
|
|
1321
1259
|
}
|
|
1322
1260
|
//#endregion
|
|
1323
|
-
//#region src/cli/rls-
|
|
1261
|
+
//#region src/cli/rls/generate-policies.ts
|
|
1262
|
+
const SERVICE_TABLES_HEADER = "-- Supabase Realtime and Storage: rls.realtime and rls.storage. RLS on both tables is Supabase's.";
|
|
1263
|
+
/**
|
|
1264
|
+
* A `realtime.messages` or `storage.objects` policy that applies only where
|
|
1265
|
+
* the table exists. The Realtime and Storage services create those tables, so
|
|
1266
|
+
* a local stack with the service off, and pg-delta's shadow database, have
|
|
1267
|
+
* none.
|
|
1268
|
+
*/
|
|
1269
|
+
function guardedPolicySql(item) {
|
|
1270
|
+
const body = policySql(item).trimEnd().split("\n").map((line) => ` ${line}`).join("\n");
|
|
1271
|
+
return `do $permdock$
|
|
1272
|
+
begin
|
|
1273
|
+
if to_regclass(${quoteLiteral(qualifySqlTable(item.table))}) is not null then
|
|
1274
|
+
${body}
|
|
1275
|
+
end if;
|
|
1276
|
+
end
|
|
1277
|
+
$permdock$;
|
|
1278
|
+
`;
|
|
1279
|
+
}
|
|
1280
|
+
/** Appends the `realtime.messages` and `storage.objects` policies, which never get `tableSql`. */
|
|
1281
|
+
function withHelperTables(sql, helperTables) {
|
|
1282
|
+
if (helperTables === void 0) return sql;
|
|
1283
|
+
const tables = `${helperTables.trimEnd()}\n`;
|
|
1284
|
+
return sql.trim() === "" ? tables : `${sql.trimEnd()}\n\n${tables}`;
|
|
1285
|
+
}
|
|
1286
|
+
function databaseModeWarnings(policy, { ctx }, warnings) {
|
|
1287
|
+
if (ctx.authorize !== "database") return;
|
|
1288
|
+
for (const { name } of ctx.scopes) if (policy.grants.some((grant) => grant.scope === name) && scopeTable(ctx, name) === void 0 && scopeSources(ctx, name).length === 0) warnings.push(`${name}-scoped grants read the ${name} memberships table in database mode: set rls.memberships.scopes.${name}, rls.membershipSources or supabase.hook.memberships (or --memberships for the first scope), otherwise they deny`);
|
|
1289
|
+
}
|
|
1290
|
+
/** The grants compiled into table policies, and the Realtime and Storage policies beside them. */
|
|
1291
|
+
function generatePolicies(input, policy, options, built, warnings) {
|
|
1292
|
+
const rls = input.config.rls;
|
|
1293
|
+
const { ctx } = built;
|
|
1294
|
+
const { fieldsMode, revokeColumns } = options;
|
|
1295
|
+
databaseModeWarnings(policy, built, warnings);
|
|
1296
|
+
const compiled = compileGrants(policy, ctx, rls?.tables, warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
|
|
1297
|
+
const helpersOnly = input.helpersOnly === true || rls?.helpersOnly === true;
|
|
1298
|
+
if (helpersOnly && (input.target !== "sql" || revokeColumns)) return { refusal: "rls generate --helpers-only needs --target sql and no --revoke-columns: the table grants are hand-written, so revoke the restricted columns there" };
|
|
1299
|
+
const views = fieldsMode === void 0 ? [] : fieldViews(policy, ctx, compiled.branches, {
|
|
1300
|
+
revokeColumns,
|
|
1301
|
+
helpersOnly
|
|
1302
|
+
}, warnings);
|
|
1303
|
+
const force = input.force === true || rls?.force === true;
|
|
1304
|
+
if (force && views.some((view) => view.companion !== void 0)) warnings.push("--force with --revoke-columns: each <table>_visible_fields companion reads as its owner, so its owner needs BYPASSRLS or the restricted columns read as null");
|
|
1305
|
+
const policyName = input.policyName ?? rls?.policyName;
|
|
1306
|
+
const assembled = helpersOnly ? [] : assemblePolicies(fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), {
|
|
1307
|
+
perRole: input.policyPerRole === true || rls?.policyPerRole === true,
|
|
1308
|
+
...policyName === void 0 ? {} : { name: policyName }
|
|
1309
|
+
});
|
|
1310
|
+
const actorKinds = readOnlyActorKinds(rls?.readOnlyActors);
|
|
1311
|
+
if (actorKinds !== void 0 && helpersOnly) warnings.push(`rls.readOnlyActors with --helpers-only writes no policies: add \`and (select ${qualified(ctx, ACTOR_MAY_WRITE)}())\` to each hand-written insert, update and delete policy, or a restrictive policy that calls it`);
|
|
1312
|
+
const withActors = (items) => actorKinds === void 0 ? items : [...items, ...readOnlyActorPolicies(items, subjectClaimJsonSql(ctx, "act"), actorKinds)];
|
|
1313
|
+
let tablePolicies;
|
|
1314
|
+
try {
|
|
1315
|
+
tablePolicies = realtimeStoragePolicies(ctx, {
|
|
1316
|
+
scopes: new Set(ctx.scopes.map((scope) => scope.name)),
|
|
1317
|
+
keys: new Set(listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key)),
|
|
1318
|
+
rowConditions: new Set(policyRowConditionKeys(policy))
|
|
1319
|
+
}, compact({
|
|
1320
|
+
realtime: rls?.realtime,
|
|
1321
|
+
storage: rls?.storage
|
|
1322
|
+
}), warnings);
|
|
1323
|
+
} catch (cause) {
|
|
1324
|
+
return { refusal: describeError(cause) };
|
|
1325
|
+
}
|
|
1326
|
+
if (tablePolicies.length > 0 && helpersOnly) warnings.push("rls.realtime and rls.storage add policies, and --helpers-only writes none: no realtime.messages or storage.objects policy is written");
|
|
1327
|
+
const servicePolicies = tablePolicies.length === 0 || helpersOnly ? [] : withActors(tablePolicies);
|
|
1328
|
+
const generated = {
|
|
1329
|
+
compiled,
|
|
1330
|
+
helpersOnly,
|
|
1331
|
+
force,
|
|
1332
|
+
views,
|
|
1333
|
+
policies: withActors(assembled)
|
|
1334
|
+
};
|
|
1335
|
+
if (servicePolicies.length === 0) return generated;
|
|
1336
|
+
return {
|
|
1337
|
+
...generated,
|
|
1338
|
+
helperTables: [SERVICE_TABLES_HEADER, ...servicePolicies.map(policySql)].join("\n"),
|
|
1339
|
+
guardedTables: [`${SERVICE_TABLES_HEADER} A stack without the service has no table, so each policy is skipped there.`, ...servicePolicies.map(guardedPolicySql)].join("\n")
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
//#endregion
|
|
1343
|
+
//#region src/cli/rls/shims.ts
|
|
1324
1344
|
const NAME = /^[a-z_][a-z0-9_]*$/u;
|
|
1325
1345
|
/**
|
|
1326
1346
|
* The key a legacy caller passes, mapped the way `rls migrate` maps it:
|
|
@@ -1337,7 +1357,7 @@ function mappedKey(config, renamed) {
|
|
|
1337
1357
|
return `coalesce(${quoteLiteral(JSON.stringify(exact))}::jsonb ->> p_key, ${fallback})`;
|
|
1338
1358
|
}
|
|
1339
1359
|
function scopeOf(ctx, scope) {
|
|
1340
|
-
if (!ctx.scopes.some((item) => item.name === scope)) throw
|
|
1360
|
+
if (!ctx.scopes.some((item) => item.name === scope)) throw configError(`PermDock CLI: rls.migrate helper scope '${scope}' is not a declared scope`);
|
|
1341
1361
|
return scope;
|
|
1342
1362
|
}
|
|
1343
1363
|
function addKey(list, key) {
|
|
@@ -1404,7 +1424,7 @@ function grantKeys(keys, scope, effect) {
|
|
|
1404
1424
|
}
|
|
1405
1425
|
/** The instances of `scope` the caller holds an unconditional allow of the key in, minus those it holds a deny in. */
|
|
1406
1426
|
function idsSql(ctx, keys, scope) {
|
|
1407
|
-
const helper = qualified
|
|
1427
|
+
const helper = qualified(ctx, permittedIdsHelper(scope));
|
|
1408
1428
|
return ` select a.id
|
|
1409
1429
|
from ${grantKeys(keys, scope, "allow")}
|
|
1410
1430
|
cross join lateral ${helper}(g.grant_key) a(id)
|
|
@@ -1414,7 +1434,7 @@ function idsSql(ctx, keys, scope) {
|
|
|
1414
1434
|
cross join lateral ${helper}(g.grant_key) d(id)`;
|
|
1415
1435
|
}
|
|
1416
1436
|
function hasSql(ctx, keys) {
|
|
1417
|
-
const has = qualified
|
|
1437
|
+
const has = qualified(ctx, HELPERS.has);
|
|
1418
1438
|
return `exists (select 1 from ${grantKeys(keys, "global", "allow")} where ${has}(g.grant_key))
|
|
1419
1439
|
and not exists (select 1 from ${grantKeys(keys, "global", "deny")} where ${has}(g.grant_key))`;
|
|
1420
1440
|
}
|
|
@@ -1432,7 +1452,14 @@ function shim(ctx, config, keys, fn, helper) {
|
|
|
1432
1452
|
case "membership": {
|
|
1433
1453
|
const scope = scopeOf(ctx, helper.scope);
|
|
1434
1454
|
const type = scopeTypeOf(ctx, scope);
|
|
1435
|
-
return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified
|
|
1455
|
+
return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified(ctx, memberIdsHelper(scope))}()), false)`);
|
|
1456
|
+
}
|
|
1457
|
+
case "role": {
|
|
1458
|
+
const scope = scopeOf(ctx, helper.scope);
|
|
1459
|
+
const type = scopeTypeOf(ctx, scope);
|
|
1460
|
+
const values = Object.entries(ctx.roleValues ?? {});
|
|
1461
|
+
const role = values.length === 0 ? "p_role" : `case p_role ${values.map(([from, to]) => `when ${quoteLiteral(from)} then ${quoteLiteral(to)}`).join(" ")} else p_role end`;
|
|
1462
|
+
return shimFunction(fn, `p_id ${type}, p_role text`, `${type}, text`, "boolean", ` select coalesce(p_id in (select ${qualified(ctx, memberIdsHelper(scope))}(array[${role}])), false)`);
|
|
1436
1463
|
}
|
|
1437
1464
|
case "global": return shimFunction(fn, "p_key text", "text", "boolean", ` select ${hasSql(ctx, keys)}`);
|
|
1438
1465
|
case "scoped": {
|
|
@@ -1461,229 +1488,118 @@ function shimsSql(ctx, config, shims, renamed, grants) {
|
|
|
1461
1488
|
map: (scope) => `${quoteLiteral(shimMap(grants, scope))}::jsonb`
|
|
1462
1489
|
};
|
|
1463
1490
|
const chunks = [`-- permdock shims: legacy helper names over the permdock helpers; drop each once doctor reports no caller (PD056)`];
|
|
1464
|
-
for (const [name, helper] of Object.entries(config.helpers).toSorted(
|
|
1465
|
-
if (!NAME.test(name)) throw
|
|
1491
|
+
for (const [name, helper] of Object.entries(config.helpers).toSorted(byKey)) {
|
|
1492
|
+
if (!NAME.test(name)) throw configError(`PermDock CLI: unsafe rls.migrate helper name '${name}'`);
|
|
1466
1493
|
chunks.push(shim(ctx, config, keys, `${quoteIdent$1(schema)}.${name}`, helper));
|
|
1467
1494
|
}
|
|
1468
1495
|
return `${chunks.join("\n\n")}\n`;
|
|
1469
1496
|
}
|
|
1470
1497
|
//#endregion
|
|
1471
|
-
//#region src/cli/rls
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1498
|
+
//#region src/cli/rls/triggers.ts
|
|
1499
|
+
const PREFIX = "PermDock CLI";
|
|
1500
|
+
const EVENTS = /* @__PURE__ */ new Set([
|
|
1501
|
+
"insert",
|
|
1502
|
+
"update",
|
|
1503
|
+
"delete",
|
|
1504
|
+
"truncate"
|
|
1505
|
+
]);
|
|
1506
|
+
const AUDITED = [
|
|
1507
|
+
CUSTOM_ROLES.permissions,
|
|
1508
|
+
CUSTOM_ROLES.includes,
|
|
1509
|
+
"user_roles"
|
|
1510
|
+
];
|
|
1511
|
+
const IDENT = /^[a-z_][a-z0-9_$]*$/u;
|
|
1512
|
+
/** The tables `sql` creates in `schema`, by name without the schema. */
|
|
1513
|
+
function generatedTables(sql, schema) {
|
|
1514
|
+
const prefix = `create table if not exists ${quoteIdent$1(schema)}.`;
|
|
1515
|
+
const found = /* @__PURE__ */ new Set();
|
|
1516
|
+
for (const line of sql.split("\n")) {
|
|
1517
|
+
if (!line.startsWith(prefix)) continue;
|
|
1518
|
+
const name = /^"?([A-Za-z_][\w$]*)"?/u.exec(line.slice(prefix.length))?.[1];
|
|
1519
|
+
if (name !== void 0) found.add(name);
|
|
1520
|
+
}
|
|
1521
|
+
return [...found].toSorted();
|
|
1522
|
+
}
|
|
1523
|
+
function functionName(name, label) {
|
|
1524
|
+
if (typeof name !== "string" || name.split(".").length !== 2) throw configError(`${PREFIX}: ${label} must be a schema-qualified function such as audit.log_change, got ${JSON.stringify(name)}`);
|
|
1525
|
+
return quoteTable(name);
|
|
1526
|
+
}
|
|
1527
|
+
function knownTable(table, tables, label) {
|
|
1528
|
+
if (!tables.includes(table)) throw configError(`${PREFIX}: ${label} names ${table}, which rls generate does not create with this config; it creates ${tables.join(", ")}`);
|
|
1529
|
+
}
|
|
1530
|
+
function triggerSql(table, trigger, label) {
|
|
1531
|
+
if (typeof trigger.name !== "string" || trigger.name === "" || trigger.name.toLowerCase().startsWith("permdock_")) throw configError(`${PREFIX}: ${label}.name must be a name outside permdock_, which generated triggers use, got ${JSON.stringify(trigger.name)}`);
|
|
1532
|
+
if (trigger.when !== "before" && trigger.when !== "after") throw configError(`${PREFIX}: ${label}.when must be before or after`);
|
|
1533
|
+
const events = trigger.events;
|
|
1534
|
+
if (events.length === 0 || !events.every((event) => EVENTS.has(event))) throw configError(`${PREFIX}: ${label}.events must list insert, update, delete or truncate`);
|
|
1535
|
+
const level = trigger.level ?? "row";
|
|
1536
|
+
if (level !== "row" && level !== "statement") throw configError(`${PREFIX}: ${label}.level must be row or statement`);
|
|
1537
|
+
if (level === "row" && events.includes("truncate")) throw configError(`${PREFIX}: ${label} fires on truncate, which Postgres allows only with level: 'statement'`);
|
|
1538
|
+
const fn = functionName(trigger.function, `${label}.function`);
|
|
1539
|
+
const args = (trigger.args ?? []).map((arg) => quoteLiteral(arg));
|
|
1540
|
+
const name = quoteIdent$1(trigger.name);
|
|
1541
|
+
return `drop trigger if exists ${name} on ${table};
|
|
1542
|
+
create trigger ${name}
|
|
1543
|
+
${trigger.when} ${[...new Set(events)].join(" or ")} on ${table}
|
|
1544
|
+
for each ${level} execute function ${fn}(${args.join(", ")});`;
|
|
1516
1545
|
}
|
|
1517
1546
|
/**
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1547
|
+
* `rls.triggers`: the application's triggers on generated tables, after the
|
|
1548
|
+
* tables. A table this run does not create is an error, so a trigger never
|
|
1549
|
+
* silently goes missing.
|
|
1521
1550
|
*/
|
|
1522
|
-
function
|
|
1523
|
-
if (
|
|
1524
|
-
const
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1551
|
+
function triggersSql(triggers, schema, tables) {
|
|
1552
|
+
if (triggers === void 0) return "";
|
|
1553
|
+
const chunks = [];
|
|
1554
|
+
for (const [table, list] of Object.entries(triggers).toSorted(byKey)) {
|
|
1555
|
+
knownTable(table, tables, `rls.triggers.${table}`);
|
|
1556
|
+
const target = `${quoteIdent$1(schema)}.${quoteIdent$1(table)}`;
|
|
1557
|
+
for (const [index, trigger] of list.entries()) chunks.push(triggerSql(target, trigger, `rls.triggers.${table}[${String(index)}]`));
|
|
1529
1558
|
}
|
|
1530
|
-
|
|
1531
|
-
return rowColumns;
|
|
1559
|
+
return chunks.length === 0 ? "" : `-- rls.triggers: triggers the application owns on generated tables\n${chunks.join("\n")}\n`;
|
|
1532
1560
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
const
|
|
1542
|
-
if (
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
const policy = await loadPolicy(input.cwd, input.config, input.from);
|
|
1548
|
-
const memberships = parseMembershipsFlag(input.memberships) ?? rls?.memberships;
|
|
1549
|
-
if (input.rbac && input.dialect !== "supabase") return {
|
|
1550
|
-
code: 2,
|
|
1551
|
-
output: `rls generate --rbac supabase needs --dialect supabase (got ${input.dialect})`,
|
|
1552
|
-
text: ""
|
|
1553
|
-
};
|
|
1554
|
-
const schema = input.rbacSchema ?? rls?.schema ?? rls?.rbac?.schema ?? "permdock";
|
|
1555
|
-
const authorize = resolveAuthorize(input.config, {
|
|
1556
|
-
authorize: input.authorize,
|
|
1557
|
-
rbac: input.rbac,
|
|
1558
|
-
memberships
|
|
1559
|
-
});
|
|
1560
|
-
const memberSources = rls?.membershipSources ?? input.config.supabase?.hook?.memberships;
|
|
1561
|
-
const sources = authorize === "database" ? memberSources : void 0;
|
|
1562
|
-
const scopes = scopeList(policy.scopes);
|
|
1563
|
-
const suspension = checkSuspension(rls?.suspension, scopes);
|
|
1564
|
-
const hookRoles = input.config.supabase?.hook?.roles;
|
|
1565
|
-
const roles = rls?.roles ?? (hookRoles === false ? void 0 : hookRoles);
|
|
1566
|
-
if (roles !== void 0 && input.rbac) return {
|
|
1567
|
-
code: 2,
|
|
1568
|
-
output: "rls generate --rbac supabase creates its own user_roles (user_id, role app_role); drop rls.roles (or supabase.hook.roles) or the --rbac flag",
|
|
1569
|
-
text: ""
|
|
1570
|
-
};
|
|
1571
|
-
const requires = writeRequirement(policy, rls?.customRoleWrites?.requires);
|
|
1572
|
-
if (requires.error !== void 0) return {
|
|
1573
|
-
code: 2,
|
|
1574
|
-
output: requires.error,
|
|
1575
|
-
text: ""
|
|
1576
|
-
};
|
|
1577
|
-
const readers = rls?.trustedReaders;
|
|
1578
|
-
if (readers !== void 0 && (!Array.isArray(readers) || !readers.every((name) => typeof name === "string" && name !== ""))) return {
|
|
1579
|
-
code: 2,
|
|
1580
|
-
output: "rls.trustedReaders must list Postgres role names, such as ['service_role']",
|
|
1581
|
-
text: ""
|
|
1582
|
-
};
|
|
1583
|
-
const ownership = ownershipRules(policy, scopes);
|
|
1584
|
-
const graph = graphPlan(policy);
|
|
1585
|
-
const apiKeys = apiKeysPlan(rls?.apiKeys, new Set(roleNames(policy)), Object.fromEntries(renamedKeys(policy.vocabulary.permissions)));
|
|
1586
|
-
const ctx = {
|
|
1587
|
-
dialect: input.dialect,
|
|
1588
|
-
scopes,
|
|
1589
|
-
tenantClaim: rls?.tenantClaim ?? "tenant_id",
|
|
1590
|
-
...rls?.tenants === "all" ? { tenants: "all" } : {},
|
|
1591
|
-
gucPrefix: input.gucPrefix ?? rls?.gucPrefix ?? "app",
|
|
1592
|
-
...rls?.actions === void 0 ? {} : { actions: rls.actions },
|
|
1593
|
-
inlineFunctions: input.inlineFunctions || rls?.inlineFunctions === true,
|
|
1594
|
-
schema,
|
|
1595
|
-
authorize,
|
|
1596
|
-
roleClaim: rls?.roleClaim ?? "user_role",
|
|
1597
|
-
tenantType: input.tenantType ?? rls?.tenantType ?? "uuid",
|
|
1598
|
-
...rls?.teamType === void 0 ? {} : { teamType: rls.teamType },
|
|
1599
|
-
...rls?.scopeTypes === void 0 ? {} : { scopeTypes: rls.scopeTypes },
|
|
1600
|
-
...memberships === void 0 ? {} : { memberships },
|
|
1601
|
-
...sources === void 0 || sources.length === 0 ? {} : { sources },
|
|
1602
|
-
...memberSources === void 0 || memberSources.length === 0 ? {} : { memberSources },
|
|
1603
|
-
...suspension === void 0 ? {} : { suspension },
|
|
1604
|
-
...roles === void 0 ? {} : { roles },
|
|
1605
|
-
...input.customRoles === true || rls?.customRoles === true ? { customRoles: {
|
|
1606
|
-
...customRoleNames(policy, requires.keys),
|
|
1607
|
-
...rls?.customRoleWrites?.roles === void 0 ? {} : { table: rls.customRoleWrites.roles }
|
|
1608
|
-
} } : {},
|
|
1609
|
-
...input.capabilities === true || rls?.capabilities === true ? { capabilities: true } : {},
|
|
1610
|
-
...apiKeys === void 0 ? {} : { apiKeys },
|
|
1611
|
-
...rls?.anonymousSignIns === "deny" ? { anonymousSignIns: "deny" } : {},
|
|
1612
|
-
...ownership === void 0 ? {} : { ownership },
|
|
1613
|
-
...rls?.ownershipTriggers === void 0 ? {} : { skipOwnershipTriggers: rls.ownershipTriggers === false ? "all" : Object.keys(rls.ownershipTriggers).map((key) => {
|
|
1614
|
-
const name = resolveScope(scopes, key);
|
|
1615
|
-
if (name === void 0) throw new Error(`PermDock CLI: rls.ownershipTriggers.${key} names a scope the policy does not declare`);
|
|
1616
|
-
return name;
|
|
1617
|
-
}) },
|
|
1618
|
-
...rls?.assignments === void 0 ? {} : { assignments: rls.assignments === true ? { tables: [] } : {
|
|
1619
|
-
tables: rls.assignments.tables ?? [],
|
|
1620
|
-
...rls.assignments.ownRole === void 0 ? {} : { ownRole: rls.assignments.ownRole }
|
|
1621
|
-
} },
|
|
1622
|
-
...fieldsMode === void 0 ? {} : { fields: fieldsMode },
|
|
1623
|
-
...graph.size === 0 ? {} : { graph: {
|
|
1624
|
-
closures: closureDepths(graph),
|
|
1625
|
-
resources: policy.resources,
|
|
1626
|
-
...rls?.tables === void 0 ? {} : { tables: rls.tables }
|
|
1627
|
-
} }
|
|
1628
|
-
};
|
|
1629
|
-
if (ctx.anonymousSignIns === "deny" && ctx.dialect !== "supabase") return {
|
|
1630
|
-
code: 2,
|
|
1631
|
-
output: `rls.anonymousSignIns needs --dialect supabase (got ${ctx.dialect})`,
|
|
1632
|
-
text: ""
|
|
1633
|
-
};
|
|
1634
|
-
const jsonSchema = rls?.jsonSchema;
|
|
1635
|
-
if (jsonSchema !== void 0 && jsonSchema !== "auto" && typeof jsonSchema !== "boolean") return {
|
|
1636
|
-
code: 2,
|
|
1637
|
-
output: `rls.jsonSchema must be 'auto', true or false (got ${JSON.stringify(jsonSchema)})`,
|
|
1638
|
-
text: ""
|
|
1639
|
-
};
|
|
1640
|
-
const warnings = [];
|
|
1641
|
-
if (jsonSchema !== void 0 && jsonSchema !== false && (rls?.approvals === void 0 || rls.approvals === false)) warnings.push("rls.jsonSchema constrains the approval store's body, which needs rls.approvals: true; nothing else is jsonb");
|
|
1642
|
-
if (authorize === "database") {
|
|
1643
|
-
for (const { name } of ctx.scopes) if (policy.grants.some((grant) => grant.scope === name) && scopeTable(ctx, name) === void 0 && scopeSources(ctx, name).length === 0) warnings.push(`${name}-scoped grants read the ${name} memberships table in database mode: set rls.memberships.scopes.${name}, rls.membershipSources or supabase.hook.memberships (or --memberships for the first scope), otherwise they deny`);
|
|
1644
|
-
}
|
|
1645
|
-
const compiled = compileGrants(policy, ctx, rls?.tables, warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
|
|
1646
|
-
const helpersOnly = input.helpersOnly === true || rls?.helpersOnly === true;
|
|
1647
|
-
if (helpersOnly && (input.target !== "sql" || revokeColumns)) return {
|
|
1648
|
-
code: 2,
|
|
1649
|
-
output: "rls generate --helpers-only needs --target sql and no --revoke-columns: the table grants are hand-written, so revoke the restricted columns there",
|
|
1650
|
-
text: ""
|
|
1651
|
-
};
|
|
1652
|
-
const views = fieldsMode === void 0 ? [] : fieldViews(policy, ctx, compiled.branches, {
|
|
1653
|
-
revokeColumns,
|
|
1654
|
-
helpersOnly
|
|
1655
|
-
}, warnings);
|
|
1656
|
-
const force = input.force === true || rls?.force === true;
|
|
1657
|
-
if (force && views.some((view) => view.companion !== void 0)) warnings.push("--force with --revoke-columns: each <table>_visible_fields companion reads as its owner, so its owner needs BYPASSRLS or the restricted columns read as null");
|
|
1658
|
-
const policyName = input.policyName ?? rls?.policyName;
|
|
1659
|
-
const assembled = helpersOnly ? [] : assemblePolicies(fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), {
|
|
1660
|
-
perRole: input.policyPerRole === true || rls?.policyPerRole === true,
|
|
1661
|
-
...policyName === void 0 ? {} : { name: policyName }
|
|
1561
|
+
/**
|
|
1562
|
+
* `rls.audit`: one call of the audit function per generated table, which
|
|
1563
|
+
* registers the table with the application's audit module. Statements that
|
|
1564
|
+
* run code, so they travel with the seeds.
|
|
1565
|
+
*/
|
|
1566
|
+
function auditSql(audit, schema, tables) {
|
|
1567
|
+
if (audit === void 0) return "";
|
|
1568
|
+
const fn = functionName(audit.function, "rls.audit.function");
|
|
1569
|
+
const listed = audit.tables;
|
|
1570
|
+
if (listed !== void 0) for (const table of listed) knownTable(table, tables, "rls.audit.tables");
|
|
1571
|
+
const targets = listed ?? AUDITED.filter((table) => tables.includes(table));
|
|
1572
|
+
const args = Object.entries(audit.args ?? {}).map(([name, value]) => {
|
|
1573
|
+
if (!IDENT.test(name)) throw configError(`${PREFIX}: rls.audit.args names ${JSON.stringify(name)}, which is not a lower-case parameter name`);
|
|
1574
|
+
return `, ${quoteIdent$1(name)} => ${quoteLiteral(value)}`;
|
|
1662
1575
|
});
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
text: ""
|
|
1681
|
-
};
|
|
1576
|
+
if (targets.length === 0) return "";
|
|
1577
|
+
return `-- rls.audit: generated tables registered with ${audit.function}
|
|
1578
|
+
${targets.map((table) => `select ${fn}(${quoteLiteral(`${quoteIdent$1(schema)}.${quoteIdent$1(table)}`)}::regclass${args.join("")});`).join("\n")}
|
|
1579
|
+
`;
|
|
1580
|
+
}
|
|
1581
|
+
//#endregion
|
|
1582
|
+
//#region src/cli/rls/generate-sql.ts
|
|
1583
|
+
/** The checks that need the SQL context: `--backfill-out`, `rls.anonymousSignIns`, `rls.jsonSchema`. */
|
|
1584
|
+
function preflight(input, { ctx }) {
|
|
1585
|
+
const rls = input.config.rls;
|
|
1586
|
+
const from = typeof rls?.customRoles === "object" ? rls.customRoles.from : void 0;
|
|
1587
|
+
if (input.backfillOut !== void 0 && from === void 0) return { refusal: "rls generate --backfill-out needs rls.customRoles.from in permdock.config.ts" };
|
|
1588
|
+
let backfill;
|
|
1589
|
+
if (from !== void 0) try {
|
|
1590
|
+
backfill = backfillSql(ctx, from, rls?.migrate);
|
|
1591
|
+
} catch (cause) {
|
|
1592
|
+
return { refusal: describeError(cause) };
|
|
1682
1593
|
}
|
|
1683
|
-
if (
|
|
1684
|
-
const
|
|
1685
|
-
|
|
1686
|
-
|
|
1594
|
+
if (ctx.anonymousSignIns === "deny" && ctx.dialect !== "supabase") return { refusal: `rls.anonymousSignIns needs --dialect supabase (got ${ctx.dialect})` };
|
|
1595
|
+
const jsonSchema = rls?.jsonSchema;
|
|
1596
|
+
if (jsonSchema !== void 0 && jsonSchema !== "auto" && typeof jsonSchema !== "boolean") return { refusal: `rls.jsonSchema must be 'auto', true or false (got ${JSON.stringify(jsonSchema)})` };
|
|
1597
|
+
return {
|
|
1598
|
+
backfill,
|
|
1599
|
+
jsonSchema
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
function rbacPart(input, policy, { ctx, schema, authorize }, warnings) {
|
|
1687
1603
|
const rootMapped = ctx.scopes[0] === void 0 ? void 0 : scopeTable(ctx, ctx.scopes[0].name);
|
|
1688
1604
|
const rootTable = rootMapped === void 0 ? void 0 : {
|
|
1689
1605
|
...rootMapped.table,
|
|
@@ -1702,51 +1618,67 @@ async function runRlsGenerate(input) {
|
|
|
1702
1618
|
}) : void 0;
|
|
1703
1619
|
if (rbac !== void 0) warnings.push(...rbac.warnings);
|
|
1704
1620
|
if (input.rbac) warnings.push("the token hook that writes user_role and memberships comes from permdock supabase hook generate");
|
|
1621
|
+
return rbac;
|
|
1622
|
+
}
|
|
1623
|
+
/** The ownership, approval store, graph, row-helper and break-glass sections. */
|
|
1624
|
+
function sectionsSql(input, policy, options, built, generated, jsonSchema, warnings) {
|
|
1625
|
+
const rls = input.config.rls;
|
|
1626
|
+
const { ctx, graph, ownership } = built;
|
|
1627
|
+
const { compiled } = generated;
|
|
1705
1628
|
const owned = [ownershipSql(ctx), assignmentSql(ctx)].filter((part) => part !== "").join("\n");
|
|
1706
1629
|
if (ctx.assignments !== void 0 && (ownership === void 0 || ownership.assigns.length === 0)) warnings.push("rls.assignments needs a role that declares assigns: no assignment trigger is written");
|
|
1707
1630
|
let approvals;
|
|
1708
1631
|
try {
|
|
1709
|
-
approvals =
|
|
1632
|
+
approvals = approvalsSql(ctx, rls?.approvals, jsonSchema ?? false);
|
|
1710
1633
|
} catch (cause) {
|
|
1711
|
-
return {
|
|
1712
|
-
code: 2,
|
|
1713
|
-
output: describeError(cause),
|
|
1714
|
-
text: ""
|
|
1715
|
-
};
|
|
1634
|
+
return { refusal: describeError(cause) };
|
|
1716
1635
|
}
|
|
1717
1636
|
const graphed = graphSql(ctx, graph, rls?.tables);
|
|
1718
1637
|
let rowHelpers = "";
|
|
1719
1638
|
if (rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0) try {
|
|
1720
|
-
rowHelpers = rowHelpersSql(ctx, policy, [...fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], rls?.tables, warnings);
|
|
1639
|
+
rowHelpers = rowHelpersSql(ctx, policy, [...options.fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], rls?.tables, warnings);
|
|
1721
1640
|
} catch (cause) {
|
|
1722
|
-
return {
|
|
1723
|
-
code: 2,
|
|
1724
|
-
output: describeError(cause),
|
|
1725
|
-
text: ""
|
|
1726
|
-
};
|
|
1641
|
+
return { refusal: describeError(cause) };
|
|
1727
1642
|
}
|
|
1728
1643
|
const breakGlass = breakGlassSql(ctx, breakGlassEntries(policy, rls?.tables));
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1644
|
+
return [
|
|
1645
|
+
approvals,
|
|
1646
|
+
owned === "" ? void 0 : owned,
|
|
1647
|
+
graphed === "" ? void 0 : graphed,
|
|
1648
|
+
breakGlass === "" ? void 0 : breakGlass,
|
|
1649
|
+
rowHelpers === "" ? void 0 : rowHelpers
|
|
1650
|
+
];
|
|
1651
|
+
}
|
|
1652
|
+
/** Whether `anon` calls a helper: `rls.anonExecute`, an anon field view or an anon branch reading the user id. */
|
|
1653
|
+
function anonExecutes(input, { compiled, views }) {
|
|
1654
|
+
return input.config.rls?.anonExecute === true || views.some((view) => view.roles.includes("anon")) || compiled.branches.some((branch) => branch.roles.includes("anon") && [
|
|
1739
1655
|
branch.access,
|
|
1740
1656
|
branch.using,
|
|
1741
1657
|
branch.check
|
|
1742
1658
|
].some((sql) => sql?.includes("permdock_user_id") === true));
|
|
1659
|
+
}
|
|
1660
|
+
/** Everything before the table policies: helpers, sections, shims, scaffold and triggers. */
|
|
1661
|
+
function preambleSql(input, policy, options, built, generated, jsonSchema, warnings) {
|
|
1662
|
+
const rls = input.config.rls;
|
|
1663
|
+
const { ctx } = built;
|
|
1664
|
+
const { compiled } = generated;
|
|
1665
|
+
const rbac = rbacPart(input, policy, built, warnings);
|
|
1666
|
+
const sections = sectionsSql(input, policy, options, built, generated, jsonSchema, warnings);
|
|
1667
|
+
if (isRefusal(sections)) return sections;
|
|
1668
|
+
const configured = rls?.shims;
|
|
1669
|
+
const shimsConfig = typeof configured === "object" ? configured : configured === true || input.shims === true ? {} : void 0;
|
|
1670
|
+
if (shimsConfig !== void 0 && rls?.migrate === void 0) return { refusal: "PermDock CLI: rls generate --shims needs rls.migrate.helpers in permdock.config.ts" };
|
|
1671
|
+
const renamed = Object.fromEntries(renamedKeys(policy.vocabulary.permissions));
|
|
1672
|
+
const grants = shimGrants(compiled.rolePermissions, compiled.conditionedKeys, renamed);
|
|
1673
|
+
const anonExecute = anonExecutes(input, generated);
|
|
1674
|
+
const actorKinds = readOnlyActorKinds(rls?.readOnlyActors);
|
|
1743
1675
|
const usesLiveSession = compiled.branches.some((branch) => [
|
|
1744
1676
|
branch.access,
|
|
1745
1677
|
branch.using,
|
|
1746
1678
|
branch.check
|
|
1747
1679
|
].some((sql) => sql?.includes(SESSION_LIVE_HELPER) === true));
|
|
1748
1680
|
const shims = shimsConfig === void 0 || rls?.migrate === void 0 ? void 0 : shimsSql(ctx, rls.migrate, shimsConfig, renamed, grants);
|
|
1749
|
-
const
|
|
1681
|
+
const core = [
|
|
1750
1682
|
rbac?.head,
|
|
1751
1683
|
helpersSql(ctx, compiled.rolePermissions, {
|
|
1752
1684
|
levelReach: compiled.levelReach,
|
|
@@ -1757,19 +1689,59 @@ async function runRlsGenerate(input) {
|
|
|
1757
1689
|
...rls?.trustedReaders === void 0 ? {} : { trustedReaders: rls.trustedReaders }
|
|
1758
1690
|
}),
|
|
1759
1691
|
permissionHelpersSql(ctx, grants, renamed, anonExecute),
|
|
1692
|
+
permissionHoldersSql(ctx) || void 0,
|
|
1693
|
+
actorKinds === void 0 ? void 0 : actorMayWriteSql(ctx, actorKinds),
|
|
1760
1694
|
usesLiveSession ? sessionLiveHelperSql(ctx) : void 0,
|
|
1761
|
-
|
|
1762
|
-
owned === "" ? void 0 : owned,
|
|
1763
|
-
graphed === "" ? void 0 : graphed,
|
|
1764
|
-
breakGlass === "" ? void 0 : breakGlass,
|
|
1765
|
-
rowHelpers === "" ? void 0 : rowHelpers,
|
|
1695
|
+
...sections,
|
|
1766
1696
|
shims,
|
|
1767
1697
|
rbac?.tail
|
|
1768
1698
|
].filter((part) => part !== void 0).join("\n");
|
|
1769
|
-
|
|
1699
|
+
const created = generatedTables(core, built.schema);
|
|
1700
|
+
let ownTriggers;
|
|
1701
|
+
let audit;
|
|
1702
|
+
try {
|
|
1703
|
+
ownTriggers = triggersSql(rls?.triggers, built.schema, created);
|
|
1704
|
+
audit = auditSql(rls?.audit, built.schema, created);
|
|
1705
|
+
} catch (cause) {
|
|
1706
|
+
return { refusal: describeError(cause) };
|
|
1707
|
+
}
|
|
1708
|
+
const seedsSplit = splitsPart(input.split, "seeds");
|
|
1709
|
+
return {
|
|
1710
|
+
preamble: [
|
|
1711
|
+
core,
|
|
1712
|
+
ownTriggers,
|
|
1713
|
+
seedsSplit ? "" : audit
|
|
1714
|
+
].filter((part) => part !== "").join("\n"),
|
|
1715
|
+
audit
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
function forceWarnings({ graph }, warnings) {
|
|
1719
|
+
for (const entry of graph.values()) {
|
|
1770
1720
|
const own = [...entry.relations].filter((name) => !("edge" in (entry.node.relations[name] ?? {})));
|
|
1771
1721
|
if (own.length > 0) warnings.push(`--force: ${entry.node.name} relations ${own.join(", ")} are read from the ${entry.node.name} table itself, so ${graphHelper(entry.node.name)} would recurse through its own policy; keep ${entry.node.name} unforced or move them to edge tables`);
|
|
1772
1722
|
}
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* The row columns that belong to a table. With `rls.tables` set, a resource
|
|
1726
|
+
* it does not name is a table only when generated policies are written for
|
|
1727
|
+
* it; with `--helpers-only` it has none, so it gets no index.
|
|
1728
|
+
*/
|
|
1729
|
+
function tableColumns(rowColumns, tables, helpersOnly, warnings) {
|
|
1730
|
+
if (tables === void 0) return rowColumns;
|
|
1731
|
+
const unmapped = [...new Set(rowColumns.map((item) => item.resource).filter((resource) => !Object.hasOwn(tables, resource)))].toSorted();
|
|
1732
|
+
if (unmapped.length === 0) return rowColumns;
|
|
1733
|
+
if (helpersOnly) {
|
|
1734
|
+
warnings.push(`no index is suggested for resources without an rls.tables entry: ${unmapped.join(", ")}; map each one that is a table`);
|
|
1735
|
+
return rowColumns.filter((item) => Object.hasOwn(tables, item.resource));
|
|
1736
|
+
}
|
|
1737
|
+
warnings.push(`policies for resources without an rls.tables entry target a table named after the resource: ${unmapped.map((resource) => qualifySqlTable(resource)).join(", ")}; map each one to its table`);
|
|
1738
|
+
return rowColumns;
|
|
1739
|
+
}
|
|
1740
|
+
/** The `--target` file, and for Drizzle and Prisma the SQL migration beside it. */
|
|
1741
|
+
function emitTarget(input, built, generated, preamble) {
|
|
1742
|
+
const rls = input.config.rls;
|
|
1743
|
+
const { ctx } = built;
|
|
1744
|
+
const { policies, force, views, helperTables } = generated;
|
|
1773
1745
|
const outRel = input.out ?? rls?.out ?? defaultOut(input.target);
|
|
1774
1746
|
const migration = input.target === "sql" ? "" : dialectRoles(migrationSql(policies, withHelperTables(preamble, helperTables), force, views), ctx.dialect);
|
|
1775
1747
|
const migrationRel = migration === "" ? void 0 : migrationOut(outRel);
|
|
@@ -1795,14 +1767,130 @@ async function runRlsGenerate(input) {
|
|
|
1795
1767
|
break;
|
|
1796
1768
|
default: return input.target;
|
|
1797
1769
|
}
|
|
1798
|
-
|
|
1770
|
+
return {
|
|
1771
|
+
text,
|
|
1772
|
+
migration,
|
|
1773
|
+
migrationRel,
|
|
1774
|
+
outRel
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
/** The generated SQL for one run, before `--db-url-stdin` prunes the index targets. */
|
|
1778
|
+
function generateSql(input, policy, options, built, warnings) {
|
|
1779
|
+
const rls = input.config.rls;
|
|
1780
|
+
const { ctx } = built;
|
|
1781
|
+
const checked = preflight(input, built);
|
|
1782
|
+
if (isRefusal(checked)) return checked;
|
|
1783
|
+
const { jsonSchema } = checked;
|
|
1784
|
+
if (jsonSchema !== void 0 && jsonSchema !== false && (rls?.approvals === void 0 || rls.approvals === false)) warnings.push("rls.jsonSchema constrains the approval store's body, which needs rls.approvals: true; nothing else is jsonb");
|
|
1785
|
+
const generated = generatePolicies(input, policy, options, built, warnings);
|
|
1786
|
+
if (isRefusal(generated)) return generated;
|
|
1787
|
+
const assembled = preambleSql(input, policy, options, built, generated, jsonSchema, warnings);
|
|
1788
|
+
if (isRefusal(assembled)) return assembled;
|
|
1789
|
+
const { preamble, audit } = assembled;
|
|
1790
|
+
if (generated.force) forceWarnings(built, warnings);
|
|
1791
|
+
const emitted = emitTarget(input, built, generated, preamble);
|
|
1792
|
+
const { policies, force, views, helperTables, guardedTables, helpersOnly } = generated;
|
|
1793
|
+
const seedsSplit = splitsPart(input.split, "seeds");
|
|
1794
|
+
return {
|
|
1795
|
+
...generated,
|
|
1796
|
+
...emitted,
|
|
1797
|
+
backfill: checked.backfill,
|
|
1798
|
+
indexes: indexTargets(ctx, tableColumns(generated.compiled.rowColumns, rls?.tables, helpersOnly, warnings)),
|
|
1799
|
+
parts: (indexes) => ({
|
|
1800
|
+
policies: dialectRoles(withHelperTables(emitSql(policies, "", force, views), helperTables), ctx.dialect),
|
|
1801
|
+
declarativePolicies: dialectRoles(emitSql(policies, "", force, views), ctx.dialect),
|
|
1802
|
+
...guardedTables === void 0 ? {} : { servicePolicies: dialectRoles(guardedTables, ctx.dialect) },
|
|
1803
|
+
helpers: dialectRoles(emitSql([], preamble, false, helpersOnly ? views : []), ctx.dialect),
|
|
1804
|
+
seeds: `${SEEDS_MARKER} schema=${built.schema}\n${seedSql(ctx, generated.compiled.rolePermissions)}\n${seedsSplit ? audit : ""}`,
|
|
1805
|
+
indexes: `${INDEXES_MARKER}\n${indexesSql(indexes)}\n`
|
|
1806
|
+
})
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
//#endregion
|
|
1810
|
+
//#region src/cli/rls/generate.ts
|
|
1811
|
+
function loadPolicy(cwd, config, from) {
|
|
1812
|
+
const policyPath = from === "drizzle" ? config.policy : from ?? config.policy;
|
|
1813
|
+
return requirePolicy(cwd, policyPath, "rls generate needs policy in permdock.config.ts or --policy");
|
|
1814
|
+
}
|
|
1815
|
+
const PROJECT_COMMAND = "permdock rls generate";
|
|
1816
|
+
/**
|
|
1817
|
+
* The policy `policyFromCatalog` rebuilds from the project's pushed catalog
|
|
1818
|
+
* and policy; a project with no pushed policy fails with `policy-not-published`.
|
|
1819
|
+
*/
|
|
1820
|
+
async function projectPolicyOf(input, project) {
|
|
1821
|
+
const fetched = await fetchProjectPolicy({
|
|
1822
|
+
command: PROJECT_COMMAND,
|
|
1823
|
+
url: input.url,
|
|
1824
|
+
project,
|
|
1825
|
+
json: input.json === true,
|
|
1826
|
+
env: input.io.env ?? process.env,
|
|
1827
|
+
io: input.io
|
|
1828
|
+
});
|
|
1829
|
+
if (!fetched.ok) return {
|
|
1830
|
+
...fetched.result,
|
|
1831
|
+
text: ""
|
|
1832
|
+
};
|
|
1833
|
+
const { catalog, policy } = fetched.value;
|
|
1834
|
+
if (policy === void 0) {
|
|
1835
|
+
const detail = `project ${project} has a pushed catalog but no policy; push from a project whose permdock.config.ts names one`;
|
|
1836
|
+
return {
|
|
1837
|
+
code: 1,
|
|
1838
|
+
output: input.json === true ? JSON.stringify({
|
|
1839
|
+
type: POLICY_NOT_PUBLISHED,
|
|
1840
|
+
title: "Policy not published",
|
|
1841
|
+
detail,
|
|
1842
|
+
command: PROJECT_COMMAND,
|
|
1843
|
+
exitCode: 1
|
|
1844
|
+
}, null, 2) : `${PROJECT_COMMAND}: ${detail}`,
|
|
1845
|
+
text: ""
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
try {
|
|
1849
|
+
return policyFromCatalog(catalog, policy);
|
|
1850
|
+
} catch (cause) {
|
|
1851
|
+
return {
|
|
1852
|
+
code: 1,
|
|
1853
|
+
output: `${PROJECT_COMMAND}: the pushed policy does not rebuild: ${describeError(cause)}`,
|
|
1854
|
+
text: ""
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
function isOutcome(value) {
|
|
1859
|
+
return "text" in value && "code" in value;
|
|
1860
|
+
}
|
|
1861
|
+
function refused(output) {
|
|
1862
|
+
return {
|
|
1863
|
+
code: 2,
|
|
1864
|
+
output,
|
|
1865
|
+
text: ""
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
async function runRlsGenerate(input) {
|
|
1869
|
+
const options = generateOptions(input);
|
|
1870
|
+
if (isRefusal(options)) return refused(options.refusal);
|
|
1871
|
+
if (input.project !== void 0 && input.from !== void 0 && input.from !== "drizzle") return refused("rls generate takes the policy from --project or from a --policy module, not both");
|
|
1872
|
+
let policy;
|
|
1873
|
+
if (input.project === void 0) policy = await loadPolicy(input.cwd, input.config, input.from);
|
|
1874
|
+
else {
|
|
1875
|
+
const loaded = await projectPolicyOf(input, input.project);
|
|
1876
|
+
if (isOutcome(loaded)) return loaded;
|
|
1877
|
+
policy = loaded;
|
|
1878
|
+
}
|
|
1879
|
+
const built = buildContext(input, policy, options);
|
|
1880
|
+
if (isRefusal(built)) return refused(built.refusal);
|
|
1881
|
+
const warnings = [];
|
|
1882
|
+
const generated = generateSql(input, policy, options, built, warnings);
|
|
1883
|
+
if (isRefusal(generated)) return refused(generated.refusal);
|
|
1884
|
+
const { ctx } = built;
|
|
1885
|
+
const { text, migration, migrationRel, helpersOnly } = generated;
|
|
1886
|
+
let indexes = generated.indexes;
|
|
1799
1887
|
if (input.db !== void 0) try {
|
|
1800
1888
|
const pruned = pruneIndexTargets(indexes, await readTableIndexFacts(input.db, [...new Set(indexes.map((target) => target.table))], input.connect));
|
|
1801
1889
|
indexes = pruned.targets;
|
|
1802
1890
|
warnings.push(...pruned.warnings);
|
|
1803
1891
|
} catch (cause) {
|
|
1804
1892
|
return {
|
|
1805
|
-
code:
|
|
1893
|
+
code: exitCodeOfError(cause),
|
|
1806
1894
|
output: describeError(cause),
|
|
1807
1895
|
text: ""
|
|
1808
1896
|
};
|
|
@@ -1813,33 +1901,26 @@ async function runRlsGenerate(input) {
|
|
|
1813
1901
|
code: 0,
|
|
1814
1902
|
output: warnings.join("\n"),
|
|
1815
1903
|
text,
|
|
1816
|
-
policies,
|
|
1817
|
-
seeds: compiled.rolePermissions,
|
|
1904
|
+
policies: generated.policies,
|
|
1905
|
+
seeds: generated.compiled.rolePermissions,
|
|
1818
1906
|
indexes,
|
|
1819
1907
|
keys: {
|
|
1820
1908
|
permissions: listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key),
|
|
1821
1909
|
rowConditions: [...policyRowConditionKeys(policy)],
|
|
1822
1910
|
renamed: Object.fromEntries(renamedKeys(policy.vocabulary.permissions))
|
|
1823
1911
|
},
|
|
1824
|
-
schema,
|
|
1912
|
+
schema: built.schema,
|
|
1825
1913
|
helpersOnly,
|
|
1826
1914
|
...extras
|
|
1827
1915
|
};
|
|
1828
1916
|
const planned = outputFiles({
|
|
1829
1917
|
input,
|
|
1830
|
-
outRel,
|
|
1918
|
+
outRel: generated.outRel,
|
|
1831
1919
|
text,
|
|
1832
1920
|
helpersOnly,
|
|
1833
|
-
sql: () => (
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
...guardedTables === void 0 ? {} : { servicePolicies: dialectRoles(guardedTables, ctx.dialect) },
|
|
1837
|
-
helpers: dialectRoles(emitSql([], preamble, false, helpersOnly ? views : []), ctx.dialect),
|
|
1838
|
-
seeds: `${SEEDS_MARKER} schema=${schema}\n${seedSql(ctx, compiled.rolePermissions)}\n`,
|
|
1839
|
-
indexes: `${INDEXES_MARKER}\n${indexesSql(indexes)}\n`
|
|
1840
|
-
}),
|
|
1841
|
-
scopes,
|
|
1842
|
-
schema
|
|
1921
|
+
sql: () => generated.parts(indexes),
|
|
1922
|
+
scopes: ctx.scopes,
|
|
1923
|
+
schema: built.schema
|
|
1843
1924
|
});
|
|
1844
1925
|
if (typeof planned === "string") return {
|
|
1845
1926
|
code: 2,
|
|
@@ -1853,6 +1934,19 @@ async function runRlsGenerate(input) {
|
|
|
1853
1934
|
rel: migrationRel,
|
|
1854
1935
|
text: migration
|
|
1855
1936
|
});
|
|
1937
|
+
if (input.backfillOut !== void 0 && generated.backfill !== void 0) {
|
|
1938
|
+
const file = dataMigrationFile({
|
|
1939
|
+
cwd: input.cwd,
|
|
1940
|
+
out: input.backfillOut,
|
|
1941
|
+
part: "backfill",
|
|
1942
|
+
marker: BACKFILL_MARKER,
|
|
1943
|
+
text: generated.backfill,
|
|
1944
|
+
now: input.io.now?.() ?? dateAt(),
|
|
1945
|
+
check: input.check,
|
|
1946
|
+
reserved: files.map((other) => other.rel)
|
|
1947
|
+
});
|
|
1948
|
+
if (file !== void 0) files.push(file);
|
|
1949
|
+
}
|
|
1856
1950
|
if (input.check) {
|
|
1857
1951
|
const drift = driftOf(input.cwd, files);
|
|
1858
1952
|
return drift.length === 0 ? {
|
|
@@ -1867,150 +1961,18 @@ async function runRlsGenerate(input) {
|
|
|
1867
1961
|
...extras
|
|
1868
1962
|
};
|
|
1869
1963
|
}
|
|
1870
|
-
const written =
|
|
1871
|
-
const
|
|
1964
|
+
const written = writeOutputFiles(input.cwd, files);
|
|
1965
|
+
const notes = [...written.wrote.length === 0 ? [] : [`wrote ${written.wrote.join(", ")}`], ...warnings];
|
|
1966
|
+
if (written.printed !== "") for (const note of notes) input.io.stderr(note);
|
|
1872
1967
|
return {
|
|
1873
1968
|
code: 0,
|
|
1874
|
-
output:
|
|
1969
|
+
output: written.printed === "" ? notes.join("\n") : written.printed,
|
|
1875
1970
|
text,
|
|
1876
1971
|
...extras
|
|
1877
1972
|
};
|
|
1878
1973
|
}
|
|
1879
|
-
/** Whether `--split` names `part`, which then leaves the helpers or the warnings. */
|
|
1880
|
-
const SERVICE_TABLES_HEADER = "-- Supabase Realtime and Storage: rls.realtime and rls.storage. RLS on both tables is Supabase's.";
|
|
1881
|
-
/**
|
|
1882
|
-
* A `realtime.messages` or `storage.objects` policy that applies only where
|
|
1883
|
-
* the table exists. The Realtime and Storage services create those tables, so
|
|
1884
|
-
* a local stack with the service off, and pg-delta's shadow database, have
|
|
1885
|
-
* none.
|
|
1886
|
-
*/
|
|
1887
|
-
function guardedPolicySql(item) {
|
|
1888
|
-
const body = policySql(item).trimEnd().split("\n").map((line) => ` ${line}`).join("\n");
|
|
1889
|
-
return `do $permdock$
|
|
1890
|
-
begin
|
|
1891
|
-
if to_regclass(${quoteLiteral(qualifiedTable$1(item.table))}) is not null then
|
|
1892
|
-
${body}
|
|
1893
|
-
end if;
|
|
1894
|
-
end
|
|
1895
|
-
$permdock$;
|
|
1896
|
-
`;
|
|
1897
|
-
}
|
|
1898
|
-
/** Appends the `realtime.messages` and `storage.objects` policies, which never get `tableSql`. */
|
|
1899
|
-
function withHelperTables(sql, helperTables) {
|
|
1900
|
-
if (helperTables === void 0) return sql;
|
|
1901
|
-
const tables = `${helperTables.trimEnd()}\n`;
|
|
1902
|
-
return sql.trim() === "" ? tables : `${sql.trimEnd()}\n\n${tables}`;
|
|
1903
|
-
}
|
|
1904
|
-
function splitsPart(raw, part) {
|
|
1905
|
-
const split = parseSplit(raw);
|
|
1906
|
-
return typeof split !== "string" && split?.includes(part) === true;
|
|
1907
|
-
}
|
|
1908
|
-
/** The files `generate` writes: one, or one per `--split` part, plus the hook's grants. */
|
|
1909
|
-
function outputFiles(plan) {
|
|
1910
|
-
const { input } = plan;
|
|
1911
|
-
const split = parseSplit(input.split);
|
|
1912
|
-
if (typeof split === "string") return split;
|
|
1913
|
-
if (split?.includes("policies") === true && plan.helpersOnly) return "rls generate --helpers-only writes no policies part; drop it from --split";
|
|
1914
|
-
if (split === void 0) {
|
|
1915
|
-
if (input.grantsOut !== void 0) return "rls generate --grants-out needs --split with the helpers or hook part";
|
|
1916
|
-
if (input.seedsOut !== void 0) return "rls generate --seeds-out needs --split with the seeds part";
|
|
1917
|
-
return [{
|
|
1918
|
-
part: "rls",
|
|
1919
|
-
rel: plan.outRel,
|
|
1920
|
-
text: plan.text
|
|
1921
|
-
}];
|
|
1922
|
-
}
|
|
1923
|
-
if (input.target !== "sql") return "rls generate --split needs --target sql";
|
|
1924
|
-
const pgDelta = input.out === void 0 && input.config.rls?.out === void 0 ? supabaseConfig(input.cwd).pgDelta : void 0;
|
|
1925
|
-
if (pgDelta === void 0 && !plan.outRel.includes("{part}")) return `rls generate --split needs {part} in --out, for example supabase/schemas/identity/056_permdock_{part}.sql (got ${plan.outRel})`;
|
|
1926
|
-
if (pgDelta !== void 0 && split.includes("helpers") && !split.includes("seeds")) return "rls generate --split helpers under pg-delta needs the seeds part with --seeds-out: pg-delta rejects the role_permissions rows in a declarative file";
|
|
1927
|
-
if (pgDelta !== void 0 && split.includes("seeds") && input.seedsOut === void 0) return "rls generate --split seeds under pg-delta needs --seeds-out: pg-delta does not diff the role_permissions rows, so they go in a migration";
|
|
1928
|
-
const at = (part, schema = plan.schema) => pgDelta === void 0 ? partPath(plan.outRel, part) : pgDeltaPath(pgDelta.schemaDir, part, schema);
|
|
1929
|
-
if (input.grantsOut !== void 0 && !split.includes("hook") && !split.includes("helpers")) return "rls generate --grants-out needs the helpers or hook part in --split";
|
|
1930
|
-
if (input.seedsOut !== void 0 && !split.includes("seeds")) return "rls generate --seeds-out needs the seeds part in --split";
|
|
1931
|
-
if (split.includes("hook") && input.config.supabase?.hook === void 0) return "rls generate --split hook needs supabase.hook in permdock.config.ts";
|
|
1932
|
-
const sql = plan.sql();
|
|
1933
|
-
const relocated = pgDelta === void 0 ? void 0 : sql.servicePolicies;
|
|
1934
|
-
if (relocated !== void 0 && split.includes("policies") && !split.includes("seeds")) return "rls generate --split policies under pg-delta with rls.realtime or rls.storage needs the seeds part with --seeds-out: the shadow database has no realtime.messages or storage.objects, so their policies go in that migration";
|
|
1935
|
-
const seeds = relocated === void 0 || !split.includes("policies") ? sql.seeds : `${sql.seeds}\n${relocated}`;
|
|
1936
|
-
const files = [];
|
|
1937
|
-
const label = grantsLabel(input.grantsOut);
|
|
1938
|
-
let grantsMarker = `${GRANTS_MARKER} schema=${plan.schema}`;
|
|
1939
|
-
const grants = [];
|
|
1940
|
-
for (const part of split) switch (part) {
|
|
1941
|
-
case "helpers": {
|
|
1942
|
-
if (label === void 0) {
|
|
1943
|
-
files.push({
|
|
1944
|
-
part,
|
|
1945
|
-
rel: at(part),
|
|
1946
|
-
text: sql.helpers
|
|
1947
|
-
});
|
|
1948
|
-
break;
|
|
1949
|
-
}
|
|
1950
|
-
const moved = moveUndiffed(sql.helpers, label, part);
|
|
1951
|
-
files.push({
|
|
1952
|
-
part,
|
|
1953
|
-
rel: at(part),
|
|
1954
|
-
text: moved.sql
|
|
1955
|
-
});
|
|
1956
|
-
grants.push(moved.grants);
|
|
1957
|
-
break;
|
|
1958
|
-
}
|
|
1959
|
-
case "seeds": {
|
|
1960
|
-
const out = input.seedsOut;
|
|
1961
|
-
if (out !== void 0 && isSeedsDirectory(input.cwd, out)) {
|
|
1962
|
-
const planned = seedsMigration(input.cwd, out, SEEDS_MARKER, seeds, input.io.now?.() ?? /* @__PURE__ */ new Date());
|
|
1963
|
-
if (!planned.current || input.check) files.push({
|
|
1964
|
-
part,
|
|
1965
|
-
rel: planned.rel,
|
|
1966
|
-
text: seeds
|
|
1967
|
-
});
|
|
1968
|
-
break;
|
|
1969
|
-
}
|
|
1970
|
-
files.push({
|
|
1971
|
-
part,
|
|
1972
|
-
rel: out ?? at(part),
|
|
1973
|
-
text: seeds
|
|
1974
|
-
});
|
|
1975
|
-
break;
|
|
1976
|
-
}
|
|
1977
|
-
case "indexes":
|
|
1978
|
-
files.push({
|
|
1979
|
-
part,
|
|
1980
|
-
rel: at(part),
|
|
1981
|
-
text: sql.indexes
|
|
1982
|
-
});
|
|
1983
|
-
break;
|
|
1984
|
-
case "policies":
|
|
1985
|
-
files.push({
|
|
1986
|
-
part,
|
|
1987
|
-
rel: at(part),
|
|
1988
|
-
text: pgDelta === void 0 ? sql.policies : sql.declarativePolicies
|
|
1989
|
-
});
|
|
1990
|
-
break;
|
|
1991
|
-
case "hook": {
|
|
1992
|
-
const hook = supabaseHookSql(plan.scopes, input.config, {}, label);
|
|
1993
|
-
files.push({
|
|
1994
|
-
part,
|
|
1995
|
-
rel: at(part, hook.manifest.hook.schema),
|
|
1996
|
-
text: hook.sql
|
|
1997
|
-
});
|
|
1998
|
-
const [marker = grantsMarker, ...rest] = hook.grants.split("\n");
|
|
1999
|
-
grantsMarker = marker;
|
|
2000
|
-
grants.push(rest.join("\n").trimEnd());
|
|
2001
|
-
break;
|
|
2002
|
-
}
|
|
2003
|
-
default: return part;
|
|
2004
|
-
}
|
|
2005
|
-
if (input.grantsOut !== void 0) files.push({
|
|
2006
|
-
part: "grants",
|
|
2007
|
-
rel: input.grantsOut,
|
|
2008
|
-
text: `${[grantsMarker, ...grants].join("\n")}\n`
|
|
2009
|
-
});
|
|
2010
|
-
return files;
|
|
2011
|
-
}
|
|
2012
1974
|
//#endregion
|
|
2013
|
-
//#region src/cli/rls
|
|
1975
|
+
//#region src/cli/rls/introspect.ts
|
|
2014
1976
|
const ROLES = ["anon", "authenticated"];
|
|
2015
1977
|
/** Neon's `anonymous` is the role `rls generate` writes as `anon` for the other dialects. */
|
|
2016
1978
|
function liveRole(role) {
|
|
@@ -2023,7 +1985,7 @@ const PRIVILEGES = [
|
|
|
2023
1985
|
"delete"
|
|
2024
1986
|
];
|
|
2025
1987
|
/** `posts` and `"app"."posts"` as `public.posts` and `app.posts`. */
|
|
2026
|
-
function
|
|
1988
|
+
function normalizeTableName(table) {
|
|
2027
1989
|
const parts = table.split(".").map((part) => part.replaceAll("\"", ""));
|
|
2028
1990
|
return parts.length === 1 ? `public.${parts[0] ?? ""}` : parts.join(".");
|
|
2029
1991
|
}
|
|
@@ -2037,19 +1999,19 @@ function helpersOf(sql) {
|
|
|
2037
1999
|
return [...found];
|
|
2038
2000
|
}
|
|
2039
2001
|
function expectedRls(policies, sql, indexes = []) {
|
|
2040
|
-
const tables = [...new Set(policies.map((item) =>
|
|
2002
|
+
const tables = [...new Set(policies.map((item) => normalizeTableName(item.table)))];
|
|
2041
2003
|
const grants = {};
|
|
2042
2004
|
for (const table of tables) {
|
|
2043
2005
|
const byRole = {};
|
|
2044
2006
|
for (const role of ROLES) {
|
|
2045
|
-
const commands = new Set(policies.filter((item) =>
|
|
2007
|
+
const commands = new Set(policies.filter((item) => normalizeTableName(item.table) === table && item.effect === "allow" && item.roles.includes(role)).map((item) => item.command));
|
|
2046
2008
|
byRole[role] = PRIVILEGES.filter((privilege) => commands.has(privilege));
|
|
2047
2009
|
}
|
|
2048
2010
|
grants[table] = byRole;
|
|
2049
2011
|
}
|
|
2050
2012
|
return {
|
|
2051
2013
|
policies: policies.map((item) => ({
|
|
2052
|
-
table:
|
|
2014
|
+
table: normalizeTableName(item.table),
|
|
2053
2015
|
name: item.name,
|
|
2054
2016
|
command: item.command,
|
|
2055
2017
|
permissive: item.effect === "allow",
|
|
@@ -2060,7 +2022,7 @@ function expectedRls(policies, sql, indexes = []) {
|
|
|
2060
2022
|
helpers: helpersOf(sql),
|
|
2061
2023
|
indexes: indexes.map((target) => ({
|
|
2062
2024
|
...target,
|
|
2063
|
-
table:
|
|
2025
|
+
table: normalizeTableName(target.table)
|
|
2064
2026
|
}))
|
|
2065
2027
|
};
|
|
2066
2028
|
}
|
|
@@ -2195,6 +2157,29 @@ async function introspectRls(db, expected, connect = connectIntrospect) {
|
|
|
2195
2157
|
await client.end();
|
|
2196
2158
|
}
|
|
2197
2159
|
}
|
|
2160
|
+
const WRITES = [
|
|
2161
|
+
"insert",
|
|
2162
|
+
"update",
|
|
2163
|
+
"delete"
|
|
2164
|
+
];
|
|
2165
|
+
/**
|
|
2166
|
+
* Each table and write command whose permissive policies reach
|
|
2167
|
+
* `authenticated` (or `public`) without the read-only actor check: no
|
|
2168
|
+
* restrictive policy for the command calls `permdock_actor_may_write()`, and
|
|
2169
|
+
* some permissive one does not.
|
|
2170
|
+
*/
|
|
2171
|
+
function unguardedWrites(policies, fn) {
|
|
2172
|
+
const out = [];
|
|
2173
|
+
const tables = [...new Set(policies.map((policy) => policy.table))];
|
|
2174
|
+
for (const table of tables) for (const command of WRITES) {
|
|
2175
|
+
const applying = policies.filter((policy) => policy.table === table && (policy.command === command || policy.command === "all"));
|
|
2176
|
+
const permissive = applying.filter((policy) => policy.restrictive !== true && (policy.roles ?? []).some((role) => role === "authenticated" || role === "public"));
|
|
2177
|
+
if (permissive.length === 0) continue;
|
|
2178
|
+
const calls = (expression) => expression.includes(fn);
|
|
2179
|
+
if (!(applying.some((policy) => policy.restrictive === true && calls(policy.expression)) || permissive.every((policy) => calls(policy.expression)))) out.push(`${table}: ${command} policies for authenticated do not call ${fn}(), so a read-only support or impersonation session can write (rls.readOnlyActors)`);
|
|
2180
|
+
}
|
|
2181
|
+
return out;
|
|
2182
|
+
}
|
|
2198
2183
|
const SYSTEM_SCHEMAS = [
|
|
2199
2184
|
"pg_catalog",
|
|
2200
2185
|
"information_schema",
|
|
@@ -2214,7 +2199,7 @@ const SYSTEM_SCHEMAS = [
|
|
|
2214
2199
|
"supabase_migrations"
|
|
2215
2200
|
];
|
|
2216
2201
|
function inScope(table) {
|
|
2217
|
-
const schema = table
|
|
2202
|
+
const schema = beforeFirst(table, ".");
|
|
2218
2203
|
return HELPER_TABLES.some((item) => item === table) || !(SYSTEM_SCHEMAS.some((item) => item === schema) || schema.startsWith("pg_"));
|
|
2219
2204
|
}
|
|
2220
2205
|
function seedLine(row) {
|
|
@@ -2236,13 +2221,16 @@ function diffMixed(expected, actual) {
|
|
|
2236
2221
|
for (const key of helperCallKeys(policy.expression)) if (!permissions.has(key)) drift.push(`${policy.table}: policy ${policy.name} passes ${key}, which the policy does not declare, so it always denies`);
|
|
2237
2222
|
else if (conditioned.has(key) && blind.has(key)) drift.push(`${policy.table}: policy ${policy.name} passes ${key}, whose grants carry row conditions the helpers do not check: the policy grants more than the application does`);
|
|
2238
2223
|
}
|
|
2224
|
+
if (expected.readOnlyActors === true) drift.push(...unguardedWrites(actual.policies, ACTOR_MAY_WRITE));
|
|
2239
2225
|
return {
|
|
2240
2226
|
drift,
|
|
2241
2227
|
info: actual.rlsTables.filter((table) => !covered.has(table)).map((table) => `${table}: no policy calls a PermDock helper`)
|
|
2242
2228
|
};
|
|
2243
2229
|
}
|
|
2244
2230
|
const ALL_POLICIES_SQL = `select schemaname || '.' || tablename as target, policyname,
|
|
2245
|
-
coalesce(qual, '') || ' ' || coalesce(with_check, '') as expression
|
|
2231
|
+
coalesce(qual, '') || ' ' || coalesce(with_check, '') as expression,
|
|
2232
|
+
lower(cmd) as command, permissive = 'RESTRICTIVE' as restrictive,
|
|
2233
|
+
array_to_string(roles, ',') as roles
|
|
2246
2234
|
from pg_policies`;
|
|
2247
2235
|
const RLS_TABLES_SQL = `select n.nspname || '.' || c.relname as target
|
|
2248
2236
|
from pg_class c
|
|
@@ -2269,7 +2257,10 @@ async function introspectMixed(db, schema, connect = connectIntrospect) {
|
|
|
2269
2257
|
policies: policies.rows.map((row) => ({
|
|
2270
2258
|
table: String(row["target"]),
|
|
2271
2259
|
name: String(row["policyname"]),
|
|
2272
|
-
expression: String(row["expression"])
|
|
2260
|
+
expression: String(row["expression"]),
|
|
2261
|
+
command: String(row["command"]),
|
|
2262
|
+
restrictive: row["restrictive"] === true,
|
|
2263
|
+
roles: String(row["roles"] ?? "").split(",").filter((role) => role !== "")
|
|
2273
2264
|
})).filter((row) => inScope(row.table)),
|
|
2274
2265
|
rlsTables: tables.rows.map((row) => String(row["target"])).filter(inScope).toSorted()
|
|
2275
2266
|
};
|
|
@@ -2278,26 +2269,15 @@ async function introspectMixed(db, schema, connect = connectIntrospect) {
|
|
|
2278
2269
|
}
|
|
2279
2270
|
}
|
|
2280
2271
|
//#endregion
|
|
2281
|
-
//#region src/cli/rls.ts
|
|
2272
|
+
//#region src/cli/rls/rls.ts
|
|
2282
2273
|
const RLS_HELP = `permdock rls generate | import | verify | migrate
|
|
2283
2274
|
|
|
2284
|
-
generate
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
[--capabilities] [--fields views [--revoke-columns]]
|
|
2289
|
-
[--out <path>] [--check] [--skip-closures] [--inline-functions] [--force] [--guc-prefix app]
|
|
2290
|
-
[--split helpers,seeds,policies,hook --out <dir>/056_permdock_{part}.sql]
|
|
2291
|
-
[--grants-out <file>|-] [--seeds-out <file>|-]
|
|
2292
|
-
[--helpers-only] [--shims] [--db $DATABASE_URL]
|
|
2293
|
-
import --sql schema.sql | --db $DATABASE_URL --out src/permissions.generated.ts
|
|
2294
|
-
[--schema zod|valibot|arktype] [--memberships <table>:tenant,user,role]
|
|
2295
|
-
verify [--db $DATABASE_URL] [--fixtures rls.fixtures.ts] [--format pgtap|node] [--tree]
|
|
2296
|
-
[--introspect --db $DATABASE_URL, with the generate flags]
|
|
2297
|
-
[--advisors [--db $DATABASE_URL] [--json]]: supabase db advisors, security lints
|
|
2298
|
-
migrate --sql <dir> [--write] [--json], with the generate flags
|
|
2299
|
-
rewrites the rls.migrate helpers' calls in policies onto the generated helpers
|
|
2275
|
+
generate Compile the policy into RLS policies and helpers
|
|
2276
|
+
import Turn existing policies (--sql or --db-url-stdin) into a permissions module
|
|
2277
|
+
verify Run the fixtures, or compare a live database with --introspect
|
|
2278
|
+
migrate Rewrite the rls.migrate helpers' calls onto the generated helpers
|
|
2300
2279
|
|
|
2280
|
+
Run permdock rls <command> --help for its flags.
|
|
2301
2281
|
Never emits service_role, except in the execute grant rls.trustedReaders asks for. memberOf compiles through the dialect memberships mapping.
|
|
2302
2282
|
`;
|
|
2303
2283
|
function asTarget(value) {
|
|
@@ -2322,6 +2302,9 @@ function generateInput(input, target, dialect) {
|
|
|
2322
2302
|
io: input.io,
|
|
2323
2303
|
...input.out === void 0 ? {} : { out: input.out },
|
|
2324
2304
|
...input.from === void 0 ? {} : { from: input.from },
|
|
2305
|
+
...input.project === void 0 ? {} : { project: input.project },
|
|
2306
|
+
...input.url === void 0 ? {} : { url: input.url },
|
|
2307
|
+
json: input.json,
|
|
2325
2308
|
...input.memberships === void 0 ? {} : { memberships: input.memberships },
|
|
2326
2309
|
...input.gucPrefix === void 0 ? {} : { gucPrefix: input.gucPrefix },
|
|
2327
2310
|
policyPerRole: input.policyPerRole,
|
|
@@ -2332,8 +2315,10 @@ function generateInput(input, target, dialect) {
|
|
|
2332
2315
|
...input.fields === void 0 ? {} : { fields: input.fields },
|
|
2333
2316
|
revokeColumns: input.revokeColumns,
|
|
2334
2317
|
...input.split === void 0 ? {} : { split: input.split },
|
|
2318
|
+
...isSupabaseSchemas(input.schemas) ? { schemas: input.schemas } : {},
|
|
2335
2319
|
...input.grantsOut === void 0 ? {} : { grantsOut: input.grantsOut },
|
|
2336
2320
|
...input.seedsOut === void 0 ? {} : { seedsOut: input.seedsOut },
|
|
2321
|
+
...input.backfillOut === void 0 ? {} : { backfillOut: input.backfillOut },
|
|
2337
2322
|
helpersOnly: input.helpersOnly,
|
|
2338
2323
|
shims: input.shims === true
|
|
2339
2324
|
};
|
|
@@ -2363,18 +2348,24 @@ async function migrate(input) {
|
|
|
2363
2348
|
output: generated.output
|
|
2364
2349
|
};
|
|
2365
2350
|
await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls migrate", input.cwd);
|
|
2366
|
-
const { migrateTarget, runRlsMigrate } = await import("./
|
|
2351
|
+
const { migrateTarget, runRlsMigrate } = await import("./migrate-BShP7Vf4.js");
|
|
2367
2352
|
return runRlsMigrate({
|
|
2368
2353
|
cwd: input.cwd,
|
|
2369
2354
|
sql: input.sql,
|
|
2370
2355
|
config,
|
|
2371
|
-
target:
|
|
2356
|
+
target: {
|
|
2357
|
+
...migrateTarget(generated),
|
|
2358
|
+
...input.config.rls?.roleValues === void 0 ? {} : { roleValues: input.config.rls.roleValues }
|
|
2359
|
+
},
|
|
2372
2360
|
write: input.write,
|
|
2373
|
-
|
|
2361
|
+
force: input.forceRewrite === true,
|
|
2362
|
+
json: input.json,
|
|
2363
|
+
retireOut: input.retireOut,
|
|
2364
|
+
...input.io.now === void 0 ? {} : { now: input.io.now() }
|
|
2374
2365
|
});
|
|
2375
2366
|
}
|
|
2376
2367
|
/** `verify --introspect` with `--helpers-only`: helpers and seeds exactly, hand-written policies by their keys. */
|
|
2377
|
-
async function introspectHelpersOnly(db, expected, generated, connect) {
|
|
2368
|
+
async function introspectHelpersOnly(db, expected, generated, connect, readOnlyActors) {
|
|
2378
2369
|
const schema = generated.schema ?? "permdock";
|
|
2379
2370
|
try {
|
|
2380
2371
|
const actual = await introspectRls(db, expected, connect);
|
|
@@ -2384,7 +2375,8 @@ async function introspectHelpersOnly(db, expected, generated, connect) {
|
|
|
2384
2375
|
schema,
|
|
2385
2376
|
seeds: generated.seeds ?? [],
|
|
2386
2377
|
permissions: generated.keys?.permissions ?? [],
|
|
2387
|
-
rowConditions: generated.keys?.rowConditions ?? []
|
|
2378
|
+
rowConditions: generated.keys?.rowConditions ?? [],
|
|
2379
|
+
readOnlyActors
|
|
2388
2380
|
}, await introspectMixed(db, schema, connect));
|
|
2389
2381
|
const drift = [...helpers, ...mixed.drift];
|
|
2390
2382
|
const info = mixed.info.map((line) => `info: ${line}`);
|
|
@@ -2411,7 +2403,7 @@ async function introspectHelpersOnly(db, expected, generated, connect) {
|
|
|
2411
2403
|
async function introspect(input) {
|
|
2412
2404
|
if (input.db === void 0) return {
|
|
2413
2405
|
code: 2,
|
|
2414
|
-
output: "PermDock CLI: rls verify --introspect needs --db"
|
|
2406
|
+
output: "PermDock CLI: rls verify --introspect needs a database: --db-url-stdin, DATABASE_URL or SUPABASE_DB_URL"
|
|
2415
2407
|
};
|
|
2416
2408
|
const dialect = asDialect(input.dialect ?? input.config.rls?.dialect);
|
|
2417
2409
|
if (dialect === void 0) return {
|
|
@@ -2428,7 +2420,7 @@ async function introspect(input) {
|
|
|
2428
2420
|
output: generated.output
|
|
2429
2421
|
};
|
|
2430
2422
|
const expected = expectedRls(generated.policies, generated.text, generated.indexes);
|
|
2431
|
-
if (generated.helpersOnly === true) return introspectHelpersOnly(input.db, expected, generated, input.connect);
|
|
2423
|
+
if (generated.helpersOnly === true) return introspectHelpersOnly(input.db, expected, generated, input.connect, readOnlyActorKinds(input.config.rls?.readOnlyActors) !== void 0);
|
|
2432
2424
|
try {
|
|
2433
2425
|
const actual = await introspectRls(input.db, expected, input.connect);
|
|
2434
2426
|
const drift = diffRls(expected, actual, { columnGrants: (input.fields ?? input.config.rls?.fields) === "views" });
|
|
@@ -2451,6 +2443,11 @@ async function runRls(input) {
|
|
|
2451
2443
|
code: 2,
|
|
2452
2444
|
output: RLS_HELP
|
|
2453
2445
|
};
|
|
2446
|
+
const badSchemas = schemasRefusal(`rls ${action}`, input.schemas);
|
|
2447
|
+
if (badSchemas !== void 0) return {
|
|
2448
|
+
code: 2,
|
|
2449
|
+
output: badSchemas
|
|
2450
|
+
};
|
|
2454
2451
|
switch (action) {
|
|
2455
2452
|
case "generate": {
|
|
2456
2453
|
const target = asTarget(input.target);
|
|
@@ -2471,7 +2468,7 @@ async function runRls(input) {
|
|
|
2471
2468
|
}
|
|
2472
2469
|
case "import": {
|
|
2473
2470
|
await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls import", input.cwd);
|
|
2474
|
-
const { runRlsImport } = await import("./
|
|
2471
|
+
const { runRlsImport } = await import("./import-DMmOG27O.js");
|
|
2475
2472
|
return runRlsImport({
|
|
2476
2473
|
cwd: input.cwd,
|
|
2477
2474
|
config: input.config,
|
|
@@ -2489,6 +2486,7 @@ async function runRls(input) {
|
|
|
2489
2486
|
cwd: input.cwd,
|
|
2490
2487
|
json: input.json,
|
|
2491
2488
|
env: input.io.env ?? process.env,
|
|
2489
|
+
...input.io.signal === void 0 ? {} : { signal: input.io.signal },
|
|
2492
2490
|
...input.db === void 0 ? {} : { db: input.db },
|
|
2493
2491
|
...input.exec === void 0 ? {} : { exec: input.exec }
|
|
2494
2492
|
});
|