permdock 0.4.0 → 0.6.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/{schema-Bol6gqXA.js → 2020-C9VN-x8c.js} +5 -130
- package/dist/a2a/index.d.ts +6 -6
- package/dist/a2a/index.js +14 -17
- package/dist/{adapter-context-mlWsVGGg.js → adapter-context-CRhqyzCk.js} +1 -1
- package/dist/adoption-scan-DCYe60Pi.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 -35
- package/dist/ai-sdk/index.js +37 -23
- package/dist/{app-data-CV8pqqMi.js → app-data-MuyzVqX8.js} +2 -1
- package/dist/approval-header-CJACtHkv.js +23 -0
- package/dist/approval-prompt-Dg5LeAcT.js +62 -0
- package/dist/approval-prompt-DvSSPQJX.d.ts +57 -0
- package/dist/approvals/index.d.ts +5 -3
- package/dist/approvals/index.js +8 -5
- package/dist/approvals-p1PGoVjb.js +995 -0
- package/dist/arazzo-41WuzHou.js +36 -0
- package/dist/{arazzo-C4BcskbX.js → arazzo-CaUVBn7D.js} +20 -14
- 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-DabClFLQ.js +55 -0
- package/dist/auth-DRLEizbL.js +178 -0
- package/dist/authzen/index.d.ts +26 -6
- package/dist/authzen/index.js +94 -79
- package/dist/automation-BNM3TtDo.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 +9 -8
- package/dist/better-supabase/index.d.ts +357 -30
- package/dist/better-supabase/index.js +936 -194
- package/dist/{bind-kk-uyCwH.js → bind-CAvgUuKs.js} +14 -10
- package/dist/{bind-BzqG8SMb.d.ts → bind-DOyrv6zi.d.ts} +12 -10
- package/dist/body-CH5of8Ne.js +10 -0
- package/dist/body-text-QcXok_SI.js +52 -0
- package/dist/{build-instance-DIIV9DbC.js → build-instance-Bu_eDqYw.js} +4 -3
- package/dist/{canonical-json-CFKyFOke.js → canonical-json-BC8_nMTw.js} +5 -3
- package/dist/{capability-CksyCWDH.d.ts → capability-Drg-1YhL.d.ts} +44 -25
- package/dist/{capability-txATtKV4.js → capability-N1K9IpqS.js} +68 -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-BoWglDPI.js +1 -0
- package/dist/catalog-DZVbrF-z.js +52 -0
- package/dist/{catalog-tZUyOfOv.js → catalog-HD2gf5mY.js} +51 -6
- package/dist/{catalog-doc-DWtg0gGR.js → catalog-doc-Cdn7-0pQ.js} +45 -23
- 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 +24 -27
- package/dist/clerk/index.d.ts +9 -8
- package/dist/clerk/index.js +9 -14
- package/dist/cli/bin.js +36 -17
- package/dist/cli/index.d.ts +19 -5
- package/dist/cli/index.js +164 -4
- package/dist/cloud/index.d.ts +102 -63
- package/dist/cloud/index.js +239 -340
- package/dist/cloud-ytQ3uviA.js +87 -0
- package/dist/codemod-5mpSpnUY.js +37 -0
- package/dist/codemod-ua2hHy7w.js +2084 -0
- package/dist/collect-Ci6akVwH.js +55 -0
- package/dist/collect-D7eQDffE.js +1 -0
- package/dist/{collect-2SrB7lfk.js → collect-mLTY4nwb.js} +75 -11
- package/dist/command-L8EstFNm.js +6 -0
- package/dist/compile/index.d.ts +1 -1
- package/dist/compile/index.js +1 -2
- package/dist/{compile-CtTP7eur.d.ts → compile-1bsAezL0.d.ts} +5 -3
- package/dist/{compile-CcEu2ZQ1.js → compile-CMqMx_IN.js} +11 -8
- package/dist/completion-BTFrHZhX.js +143 -0
- package/dist/config-Bj2Ixq3d.js +26 -0
- package/dist/{config-BIjREpyE.js → config-bs_kZeYq.js} +12 -7
- package/dist/{config-report--Xb9APJS.js → config-report-DSpsLpCE.js} +15 -5
- package/dist/{connection-DUsczpB5.js → connection-DjhIJSs2.js} +6 -5
- package/dist/{context-DieZVJXu.js → context-B_u3_bDJ.js} +2 -2
- package/dist/convex/index.d.ts +11 -10
- package/dist/convex/index.js +6 -4
- package/dist/create-B30OMUOJ.d.ts +107 -0
- package/dist/create-CBMmFie-.d.ts +138 -0
- package/dist/create-CGm28gyI.js +130 -0
- package/dist/create-CQWIpUG-.js +403 -0
- package/dist/{create-BBO24vj6.js → create-DCF2SvoT.js} +31 -32
- package/dist/create-D_NmPOdH.d.ts +110 -0
- package/dist/{credential-kue0qpbm.js → credential-BcoiejpY.js} +152 -31
- package/dist/credentials-DPyHmYz5.js +74 -0
- package/dist/delegation-j6-rc37o.js +286 -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-DfRk15uw.d.ts → denied-text-CiFQNuMV.d.ts} +5 -5
- package/dist/{describe-C3ldA9os.d.ts → describe-B5qgDvEW.d.ts} +2 -2
- package/dist/{describe-Cgkp7eP0.js → describe-BybMRY7A.js} +6 -2
- package/dist/device-BARQ8eYG.js +193 -0
- package/dist/{diff-rpqoe7Ag.js → diff-3jbx6Rm4.js} +60 -54
- package/dist/diff-CmuSD3vc.js +37 -0
- package/dist/doctor-3ZqNHX_G.js +44 -0
- package/dist/{doctor-owlv6ddh.js → doctor-BdZw53-k.js} +245 -50
- package/dist/drizzle/index.d.ts +5 -3
- package/dist/drizzle/index.js +2 -2
- package/dist/elysia/index.d.ts +16 -13
- package/dist/elysia/index.js +13 -12
- package/dist/env-DJZrPjRi.js +8 -0
- package/dist/errors-BDUwg2qC.js +27 -0
- package/dist/{errors-drnfxYtm.js → errors-BKy0EfhN.js} +5 -9
- package/dist/errors-BWXBMaKK.d.ts +11 -0
- package/dist/errors-CWTY6uih.js +143 -0
- package/dist/{errors-OD_4c__H.d.ts → errors-DH5QEGXh.d.ts} +2 -2
- package/dist/evaluate-CNClBvcj.js +1785 -0
- package/dist/evaluations-C_1vdSZc.d.ts +32 -0
- package/dist/{evaluations-C9YJsv3H.js → evaluations-beAooV0n.js} +145 -34
- package/dist/eve/index.d.ts +12 -16
- package/dist/eve/index.js +22 -28
- 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 +12 -11
- package/dist/fastify/index.d.ts +25 -22
- package/dist/fastify/index.js +13 -12
- package/dist/fetch-adapter-DD_gfmrC.js +120 -0
- package/dist/fetch-adapter-Dw0MTS3D.d.ts +31 -0
- package/dist/fields-OYG_JR78.js +203 -0
- package/dist/files-C5ZI27eV.js +124 -0
- package/dist/{fixtures-h2eG2OZV.js → fixtures-B7XrLeY7.js} +8 -7
- package/dist/flags-C3DktBDY.js +86 -0
- package/dist/{from-snapshot-7REQZe0H.js → from-snapshot-CTCMsFZk.js} +55 -52
- package/dist/{grantee-conditions-MQs5NIuX.js → grantee-conditions-CcXf8hMM.js} +4 -3
- package/dist/{grantee-CXY-0C87.js → grantee-hxrUsbpM.js} +16 -6
- package/dist/{grantee-match-fxYp9Nmt.js → grantee-match-DbENBHLB.js} +5 -4
- 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-C9YmyBIa.js → helpers-CBYVMjwh.js} +45 -38
- package/dist/hono/index.d.ts +12 -9
- package/dist/hono/index.js +13 -12
- package/dist/hosted-B46-5LYE.js +315 -0
- package/dist/hosted-BPk51-kX.d.ts +78 -0
- package/dist/{http-DPuDaChb.d.ts → http-B_LphE5Y.d.ts} +1 -2
- package/dist/http-DXJ37RDA.js +46 -0
- package/dist/{http-BEbdMZ05.js → http-ujKm0JXE.js} +1 -2
- package/dist/{import-UHk1N7vH.js → import-CNV7I2Dw.js} +25 -20
- package/dist/{import-BFIlhetD.js → import-wyqXUyjm.js} +190 -53
- package/dist/index-B8YjkEZ4.d.ts +210 -0
- package/dist/index-C-6Letxg.d.ts +431 -0
- package/dist/index-CaxrMD1A.d.ts +1 -0
- package/dist/{index-BwMe94Xq.d.ts → index-Ch8LpNDp.d.ts} +34 -16
- package/dist/index-Cp_ilKi1.d.ts +91 -0
- package/dist/index-CueFnxQc.d.ts +110 -0
- package/dist/index-DuWxc5dt.d.ts +909 -0
- package/dist/index.d.ts +19 -15
- package/dist/index.js +31 -21
- package/dist/init-DeMYsnYJ.js +96 -0
- package/dist/init-GG6Jh8HD.js +95 -0
- package/dist/{instance-options-plkrsAog.d.ts → instance-options-BsVpyGIk.d.ts} +2 -2
- package/dist/instance-snapshot-BOEhcqIF.js +601 -0
- package/dist/instant-BluqDvyS.js +19 -0
- package/dist/internals-Dk8A8fX3.js +17 -0
- package/dist/{json-schema-DBkdUuCO.js → json-schema-B-vAwlde.js} +45 -6
- package/dist/json-schema-CN_iLFH2.d.ts +40 -0
- package/dist/jwt/index.d.ts +18 -11
- package/dist/jwt/index.js +156 -135
- package/dist/{kernel-CB3LrIJM.js → kernel-D0NVb1cK.js} +52 -24
- package/dist/kysely/index.d.ts +5 -3
- package/dist/kysely/index.js +2 -2
- package/dist/load-BxEtwlBS.js +1 -0
- package/dist/{load-C3_oTHyO.js → load-DFS_ry0W.js} +22 -6
- 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-dvL2XEe1.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-AVlBaJoa.js → map-claims-D3oRmTk0.js} +13 -10
- package/dist/{map-error-BE27PYkE.js → map-error-Dr70oA_3.js} +3 -3
- package/dist/{markers-CiwAOZtO.js → markers-C6joDI3o.js} +2 -1
- package/dist/mcp/index.d.ts +22 -12
- package/dist/mcp/index.js +57 -39
- package/dist/{migrate-BwlB5-Wk.js → migrate-CIq4-E9F.js} +53 -14
- package/dist/nest/index.d.ts +12 -9
- package/dist/nest/index.js +15 -14
- package/dist/next/index.d.ts +18 -21
- package/dist/next/index.js +13 -11
- 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-uYFXGVw2.js → ocsf-BFp-PZZA.js} +22 -11
- package/dist/openai/index.d.ts +9 -16
- package/dist/openai/index.js +17 -24
- package/dist/openapi/index.d.ts +3 -3
- package/dist/openapi/index.js +1 -1
- package/dist/{openapi-DBzHQbov.js → openapi-B7HZ0f-r.js} +8 -8
- package/dist/openapi-C4dDTVEd.js +138 -0
- package/dist/{openapi-BJdBmYDG.js → openapi-DD1SV-5-.js} +10 -9
- package/dist/operation-findings-DgATDpvh.js +36 -0
- package/dist/operations-X0VM7F3m.d.ts +59 -0
- package/dist/orpc/index.d.ts +13 -10
- package/dist/orpc/index.js +20 -15
- package/dist/otel/index.d.ts +3 -2
- package/dist/otel/index.js +30 -7
- package/dist/output-1im__-08.js +88 -0
- package/dist/output-files-B3IZJksC.js +1 -0
- package/dist/{output-files-CgIsvdSa.js → output-files-DhRBBE4n.js} +24 -26
- package/dist/{permission-keys-B26jeyUv.js → ownership-DqaJNJPn.js} +1045 -153
- package/dist/{package-root-DtNnpmUd.js → package-root-Dk_ETdkp.js} +1 -1
- package/dist/{parse-CVFQHk6c.js → parse-Cxq0WxrA.js} +2 -2
- package/dist/{parse-local-manifest-DvYUR0QL.d.ts → parse-local-manifest-5ODwqu8b.d.ts} +1 -1
- package/dist/{parse-local-manifest-C-q02Pal.js → parse-local-manifest-CVSs6J9I.js} +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 +45 -26
- package/dist/{peer-BOr_kJE1.js → peer-C8swJ2GH.js} +23 -4
- package/dist/peer-CRvC7Hg1.js +1 -0
- package/dist/permdock-BrAscV28.js +868 -0
- package/dist/permdock-SV8ZINYT.d.ts +502 -0
- package/dist/{permission-registry-BsblpB-z.js → permission-registry-Br4jSNVi.js} +3 -3
- package/dist/permissions-B-wCP5ZC.js +30 -0
- package/dist/{permissions-CCJ6lmxB.js → permissions-BPxlwVhN.js} +4 -3
- package/dist/{permissions-COH-KRgT.d.ts → permissions-u_KHg-sN.d.ts} +1 -2
- package/dist/policies-DP2nqRoF.js +295 -0
- package/dist/{policy-ur0BBK_e.d.ts → policy-77FSAq_V.d.ts} +2 -2
- package/dist/{hosted-CBHgkzDl.js → policy-DbM5YYtV.js} +17 -273
- package/dist/policy-from-catalog-C2gMa2Dj.js +450 -0
- package/dist/policy-module-CTlwMvLI.js +89 -0
- package/dist/{policy-types-sp9P1iIV.d.ts → policy-types-n0uRTsW8.d.ts} +460 -632
- package/dist/postgrest-CEgRbefE.d.ts +100 -0
- package/dist/postgrest-DAjUkF9w.js +575 -0
- package/dist/{powersync-cZe4kzhL.js → powersync-BkWWCS1K.js} +23 -24
- package/dist/powersync-DISt6SUe.js +62 -0
- package/dist/powersync-MMJ2mI2q.js +1 -0
- 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 +2 -2
- package/dist/{problem-CnWraVhC.js → problem-CTviyH5L.js} +13 -23
- package/dist/{problem-details-rIIo8B2C.d.ts → problem-details-B45V5d2b.d.ts} +1 -1
- package/dist/{problem-details-DECUnXAn.js → problem-details-cn5X30gZ.js} +43 -40
- package/dist/problem-types-CJ2325SZ.js +5 -0
- package/dist/{prompt-CXYhLfsS.js → prompt-DfZLJLIn.js} +2 -2
- package/dist/{protected-BN7BUdY1.js → protected-BlF0F6wL.js} +21 -4
- package/dist/{protected-B3_P7xuw.d.ts → protected-RFGpakN9.d.ts} +16 -8
- package/dist/{provider-Bi-0HNL3.js → provider-B1al-AZz.js} +2 -2
- package/dist/{provider-kwHC9CZD.d.ts → provider-DILL9oPK.d.ts} +1 -1
- package/dist/{provider-store-Ctt-81Ni.js → provider-store-DM6DNcta.js} +2 -2
- package/dist/pull-CvpINcMK.js +1 -0
- package/dist/pull-jEwbf80J.js +256 -0
- package/dist/push-BD4f15co.js +116 -0
- package/dist/pushed-policy-CqtxkV4G.js +238 -0
- package/dist/pushed-policy-DCW91KQU.d.ts +57 -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 -72
- package/dist/react-native/index.js +29 -25
- package/dist/{refs-f3imMjNW.js → refs-K6eWUJ0H.js} +1 -1
- package/dist/{relation-walk-BXnlLtg2.js → relation-walk-3OyDBF0S.js} +2 -2
- package/dist/{relations-CvmSpG67.js → relations-BnCWnJGI.js} +3 -3
- package/dist/renames-CGj7YwcQ.js +175 -0
- package/dist/{replay-COpmZUfx.d.ts → replay-Bkm-Ne4l.d.ts} +1 -1
- package/dist/{replay-BsOyTrdV.js → replay-Du1tJjG7.js} +2 -1
- package/dist/{request-context-BVM7aWNs.js → request-context-C81Pip9n.js} +3 -3
- 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-D1Un3pcz.js → resource-relations-D_jw0BFx.js} +1 -1
- package/dist/{retire-BFcxL6un.js → retire-CALjIQ7e.js} +2 -2
- package/dist/{revocations-Cy06JNx2.d.ts → revocations-R07lFBnX.d.ts} +2 -0
- package/dist/rls-BAqP4q66.js +336 -0
- package/dist/{rls-BEktUaqg.js → rls-DTvYd-lI.js} +288 -690
- package/dist/{role-builder-B4GbKM-i.js → role-builder-uWQZ4v84.js} +14 -207
- package/dist/{rpc-nYC3cR_u.js → rpc-VSl5ylaQ.js} +7 -7
- package/dist/run-CUKuz0A8.js +723 -0
- package/dist/run-DDwUaJDU.js +1 -0
- package/dist/run-ticket-DocwaTKn.d.ts +124 -0
- package/dist/run-ticket-sxFI6LAk.js +337 -0
- package/dist/{runtime-D_tMXnsq.js → runtime-D3VTAiCA.js} +1 -1
- package/dist/{saas-DtaA4JuW.js → saas-BCXRuXca.js} +16 -14
- package/dist/scan-qvOe0hS3.js +29 -0
- package/dist/{schema-DjQBcAP9.js → schema-B4v0Qrac.js} +102 -8
- 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-vFR1O5oG.js → scim-ClHtpyV3.js} +208 -76
- package/dist/{scopes-tvpa34ep.js → scopes-CUPSjo9q.js} +12 -7
- package/dist/server/index.d.ts +12 -143
- package/dist/server/index.js +12 -9
- package/dist/server-C3WXAHZj.js +453 -0
- package/dist/{server-types-DdqBwZN_.d.ts → server-types-CKQiCnON.d.ts} +51 -68
- package/dist/{sha256-BO0swRXL.js → sha256-D9ihqmvd.js} +8 -1
- package/dist/{shared-1uhI-e-b.js → shared-BAK8t4Uo.js} +20 -9
- package/dist/{shared-JhMeOsDF.js → shared-DvvFzYXe.js} +8 -9
- package/dist/signals-D3NcTKUS.js +32 -0
- package/dist/sink-4-eQIRWf.d.ts +225 -0
- package/dist/sink-BEZSlMS_.js +957 -0
- package/dist/{skills-BZdRQkxs.js → skills-B6lVdD_W.js} +13 -6
- package/dist/skills-CTYwkl44.js +51 -0
- package/dist/snapshot-B7DkYg6g.js +269 -0
- package/dist/{snapshot-cache-Cod40D-X.js → snapshot-cache-DZHQ0KF6.js} +11 -5
- package/dist/{snapshot-cache-J5T7t_vD.d.ts → snapshot-cache-PXI-vGpY.d.ts} +7 -5
- package/dist/solid/index.d.ts +11 -12
- package/dist/solid/index.js +7 -7
- package/dist/solid-start/index.d.ts +52 -0
- package/dist/solid-start/index.js +49 -0
- package/dist/{sources-Dljy06zL.d.ts → sources-Xc4zkszI.d.ts} +7 -13
- package/dist/{sources-B1-b-R0t.js → sources-vs5dGOsS.js} +125 -119
- package/dist/{sql-t8teWOfo.js → sql-B5T1IL83.js} +13 -1
- package/dist/src-Cu2m75uo.js +1860 -0
- package/dist/ssf/index.d.ts +4 -4
- package/dist/ssf/index.js +384 -47
- package/dist/standard-webhooks-DgFFD-Ga.js +228 -0
- package/dist/standard-webhooks-rbD7LS6h.d.ts +88 -0
- package/dist/storage-C2bWdvJH.js +132 -0
- package/dist/{store-ILkLUj_o.js → store-BDutJkDL.js} +40 -53
- package/dist/{store-options-CbUHDoSe.js → store-options-u0ycEe8s.js} +26 -21
- package/dist/{stores-Cxz_80g-.d.ts → stores-BUGNbJTt.d.ts} +7 -8
- package/dist/{stores-BHXdSbw9.js → stores-CGhM8BN_.js} +2 -2
- package/dist/{stream-Dwsg6WwE.d.ts → stream-CeCMJmBB.d.ts} +2 -2
- package/dist/{stream-BMui98KK.js → stream-CvdUS9V1.js} +9 -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/supabase/index.d.ts +23 -106
- package/dist/supabase/index.js +5 -4
- package/dist/supabase/middleware.d.ts +7 -6
- package/dist/supabase/middleware.js +13 -11
- package/dist/supabase-By_frk3U.js +92 -0
- package/dist/{supabase-hook-Cva3ikXz.js → supabase-hook-BMoEhZir.js} +960 -64
- package/dist/supabase-hook-CCw5m35C.js +1 -0
- package/dist/{supabase-C1TWlnYt.js → supabase-x5JtR2tc.js} +99 -206
- package/dist/support-session-C6xJQYut.js +158 -0
- package/dist/svelte/Protected.svelte +4 -4
- package/dist/svelte/index.d.ts +7 -7
- package/dist/svelte/index.js +10 -10
- package/dist/svelte/runtime.d.ts +3 -3
- package/dist/svelte/runtime.js +3 -3
- package/dist/svelte/source.d.ts +7 -7
- package/dist/svelte/source.js +4 -4
- 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/{tenancy-DshjqY0n.js → tenancy-DN2QAx9z.js} +5 -5
- package/dist/terminal/index.d.ts +11 -9
- package/dist/terminal/index.js +23 -287
- package/dist/testing/index.d.ts +311 -38
- package/dist/testing/index.js +3475 -201
- 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-Bx9qy30p.js → token-BLZcjQgt.js} +15 -6
- package/dist/{tool-refusal-CQ7OEia9.js → tool-refusal-CMLN5xhn.js} +2 -2
- package/dist/trpc/index.d.ts +12 -9
- package/dist/trpc/index.js +16 -14
- package/dist/types-8RnwUAvE.d.ts +107 -0
- package/dist/{store-9KO4BlkB.d.ts → types-B3_ooZgZ.d.ts} +100 -75
- package/dist/{types-Br9IXUBG.d.ts → types-BdtSDbBO.d.ts} +77 -17
- package/dist/types-BtHtcKMr.d.ts +157 -0
- package/dist/{types-CqrjB9o6.d.ts → types-BzzaHxt3.d.ts} +23 -11
- package/dist/types-C791HFu9.d.ts +332 -0
- package/dist/types-C863OQiR.d.ts +57 -0
- package/dist/{types-ByUo56un.d.ts → types-CVZSNocA.d.ts} +23 -22
- package/dist/{types-DHrmBJLH.d.ts → types-CX7AvRqP.d.ts} +7 -7
- package/dist/types-C_IiEs64.d.ts +65 -0
- package/dist/{types-7rJVLWe5.d.ts → types-Dfv0aTER.d.ts} +63 -11
- package/dist/types-DqzsnlSA.js +67 -0
- package/dist/{types-DpWfQdSh.d.ts → types-Druu9wLM.d.ts} +46 -11
- package/dist/{types-CIlTojhq.d.ts → types-_WhUP7ud.d.ts} +9 -6
- package/dist/{types-CUcFZfpU.d.ts → types-lUlG3TVd.d.ts} +2 -1
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +2 -2
- package/dist/usage-CRVl9V3t.js +32 -0
- package/dist/usage-CZpSQP6L.js +1 -0
- package/dist/{usage-CG9nOYZw.js → usage-SveTRTyr.js} +54 -11
- package/dist/{validation-error-BSSw0l_8.d.ts → validation-error-DHUrAkku.d.ts} +1 -1
- package/dist/{validity-dVrUEPJt.js → validity-CZ7-y0Cj.js} +32 -15
- package/dist/{verifier-DMr_Zmb_.js → verifier-DHuh-okk.js} +18 -7
- package/dist/{verify-BztUB3GX.js → verify-BaAKx7t6.js} +645 -50
- package/dist/{version-BEogC-rB.js → version-CEbQ5UeM.js} +4 -3
- package/dist/version-DXkYUISx.js +1 -0
- package/dist/{views-CJsrBS1f.js → views-DTIIO-oQ.js} +22 -5
- package/dist/{permission-types-Dg39z9Kv.d.ts → vocabulary-CN8H_9UF.d.ts} +67 -2
- package/dist/vue/index.d.ts +9 -10
- package/dist/vue/index.js +8 -8
- package/dist/waiter-B986w1oK.d.ts +36 -0
- package/dist/waiter-BGUtGYoU.js +67 -0
- package/dist/watch--ovlPOc0.js +1 -0
- package/dist/{watch-zeucB7eF.js → watch-vea54PRw.js} +44 -5
- package/dist/webmcp/index.d.ts +2 -2
- package/dist/webmcp/index.js +3 -3
- package/dist/where-scope-C9vfEphl.js +306 -0
- package/dist/who-can-DWBAcaCQ.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-Cr93qqdP.d.ts → wrap-C5jC6sKI.d.ts} +2 -1
- package/dist/{wrap-DWsU_5Bd.js → wrap-gX61EtpB.js} +7 -6
- package/package.json +136 -37
- 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 +123 -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 +14 -10
- package/skills/permdock-data/SKILL.md +6 -6
- package/skills/permdock-data/references/queries-and-rls.md +10 -9
- 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 +17 -11
- package/skills/permdock-wire/references/adapters.md +139 -51
- package/skills/permdock-wire/references/policy-options.md +5 -5
- package/skills/permdock-wire/references/presets-and-recipes.md +58 -0
- package/dist/approvals-DZL76lKW.js +0 -240
- package/dist/arazzo-CD8d0WLb.js +0 -54
- package/dist/catalog-6yWAX6Ps.js +0 -2
- package/dist/catalog-WHG4kBq7.js +0 -48
- package/dist/cloud-C7bSDgX4.js +0 -50
- package/dist/cloud-PeBN3IEn.js +0 -118
- package/dist/collect-Bw2Jdq1q.js +0 -2
- package/dist/collect-CQx4hX4N.js +0 -83
- package/dist/commands-Cpip4ULx.js +0 -97
- package/dist/config-DLt6BvV7.js +0 -36
- package/dist/diff-Cnghh_Ip.js +0 -48
- package/dist/doctor-CuVJvhV_.js +0 -53
- package/dist/duration-xK2jGFVc.js +0 -25
- package/dist/errors-CWfoNg9N.js +0 -47
- package/dist/errors-DRnGZHzF.js +0 -30
- package/dist/expiry-BoBJ8Tvc.js +0 -7
- package/dist/fields-fwfXN01n.js +0 -1016
- 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-DgM7w96K.d.ts +0 -6
- package/dist/index-Dz7didpC.d.ts +0 -201
- package/dist/index-UdVQgq48.d.ts +0 -550
- package/dist/load-B_shHrmd.js +0 -2
- package/dist/manifest-BngWKryL.js +0 -770
- package/dist/may-use-BOn_dR_w.js +0 -78
- package/dist/openapi-DcQ1Kg40.js +0 -164
- package/dist/ownership-BgnbOJFT.js +0 -851
- package/dist/permdock-L26udHSk.js +0 -3646
- package/dist/powersync-BFSbSElQ.js +0 -62
- package/dist/powersync-DCDgZgfZ.js +0 -2
- package/dist/rls-55DQTuf5.js +0 -250
- package/dist/run-DMcNW_8T.js +0 -174
- package/dist/server-ZYEHtNHS.js +0 -268
- package/dist/sink-BTtmfPnE.js +0 -141
- package/dist/sink-CPN4zLfs.d.ts +0 -104
- package/dist/skills-Dle4ZoLF.js +0 -56
- package/dist/snapshot-Dc9CHPME.js +0 -1078
- package/dist/src-Bb7no-hS.js +0 -639
- package/dist/subject-C6dLxqHU.js +0 -24
- package/dist/supabase-Dbqr141s.js +0 -83
- package/dist/supabase-hook-BULg7Ut_.js +0 -2
- package/dist/thenable-QSmvt-vZ.js +0 -65
- package/dist/types-C0dee0Qf.d.ts +0 -197
- package/dist/types-DvupZE0w.d.ts +0 -9
- package/dist/usage-4G6t643F.js +0 -42
- package/dist/usage-iFLJ0-rr.js +0 -2
- package/dist/vocabulary-hhEFLFoL.d.ts +0 -65
- package/dist/watch-C8TpsAl4.js +0 -2
|
@@ -1,79 +1,17 @@
|
|
|
1
1
|
import { a as sole, n as isReadonlyArray, r as isRecord } from "./compact-CAf557ZB.js";
|
|
2
|
-
import {
|
|
3
|
-
import { l as isSqlFunctionField } from "./refs-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { t as
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
const CONNECT_TIMEOUT_MS = 1e4;
|
|
16
|
-
const STATEMENT_TIMEOUT_MS = 6e4;
|
|
17
|
-
function loadPg(command) {
|
|
18
|
-
return requirePeer(() => import("pg"), "pg", command);
|
|
19
|
-
}
|
|
20
|
-
function options(db) {
|
|
21
|
-
return {
|
|
22
|
-
connectionString: db,
|
|
23
|
-
connectionTimeoutMillis: CONNECT_TIMEOUT_MS,
|
|
24
|
-
statement_timeout: STATEMENT_TIMEOUT_MS
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
/** Ctrl-C ends the connection, so a query that hangs cannot keep the process alive. */
|
|
28
|
-
function endOnInterrupt(end) {
|
|
29
|
-
const interrupt = () => {
|
|
30
|
-
end().catch(() => void 0);
|
|
31
|
-
process.exitCode = 130;
|
|
32
|
-
};
|
|
33
|
-
process.once("SIGINT", interrupt);
|
|
34
|
-
return async () => {
|
|
35
|
-
process.off("SIGINT", interrupt);
|
|
36
|
-
await end();
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
function unreachable(command, cause) {
|
|
40
|
-
return new CliError("unavailable", `PermDock CLI: ${command.replace(/^permdock /u, "")} could not connect`, { cause });
|
|
41
|
-
}
|
|
42
|
-
/** The default `connect` of a command: one connection, failing with a message that names `command`. */
|
|
43
|
-
function pgConnector(command) {
|
|
44
|
-
return (db) => connectPg(db, command);
|
|
45
|
-
}
|
|
46
|
-
/** One connection, for commands that run a transaction. */
|
|
47
|
-
async function connectPg(db, command) {
|
|
48
|
-
const client = new (await (loadPg(command))).Client(options(db));
|
|
49
|
-
try {
|
|
50
|
-
await client.connect();
|
|
51
|
-
} catch (cause) {
|
|
52
|
-
throw unreachable(command, cause);
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
query: (sql, values) => client.query(sql, values),
|
|
56
|
-
end: endOnInterrupt(() => client.end())
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
/** A small pool, for independent catalog reads that can run in parallel. */
|
|
60
|
-
async function connectPgPool(db, command) {
|
|
61
|
-
const pool = new (await (loadPg(command))).Pool({
|
|
62
|
-
...options(db),
|
|
63
|
-
max: 4
|
|
64
|
-
});
|
|
65
|
-
try {
|
|
66
|
-
(await pool.connect()).release();
|
|
67
|
-
} catch (cause) {
|
|
68
|
-
await pool.end();
|
|
69
|
-
throw unreachable(command, cause);
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
query: (sql, values) => pool.query(sql, values),
|
|
73
|
-
end: endOnInterrupt(() => pool.end())
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
//#endregion
|
|
2
|
+
import { n as isForbiddenKey } from "./paths-DXSHAMIa.js";
|
|
3
|
+
import { l as isSqlFunctionField } from "./refs-K6eWUJ0H.js";
|
|
4
|
+
import { u as isoAt } from "./time-DkWjdL98.js";
|
|
5
|
+
import { c as rootScope, d as scopeList, l as scopeChain, o as resolveScope } from "./scopes-CUPSjo9q.js";
|
|
6
|
+
import { t as isoInstant } from "./instant-BluqDvyS.js";
|
|
7
|
+
import { m as findRole, v as listRoles } from "./grantee-hxrUsbpM.js";
|
|
8
|
+
import { r as relationCondition, t as inheritCondition } from "./grantee-conditions-CcXf8hMM.js";
|
|
9
|
+
import { l as partitionsOf, p as scopeField } from "./tenancy-DN2QAx9z.js";
|
|
10
|
+
import { i as configError } from "./errors-CWTY6uih.js";
|
|
11
|
+
import { i as scopeMembershipTable, o as relatedSql, r as scopeColumn } from "./compile-CMqMx_IN.js";
|
|
12
|
+
import { i as qualifySqlTable, t as SQL_IDENT, u as sqlTextArray } from "./sql-B5T1IL83.js";
|
|
13
|
+
import { d as keptKeys, l as roleColumn, u as roleManifest } from "./sources-vs5dGOsS.js";
|
|
14
|
+
import { A as declaredArray, D as sqlType, E as scopeTypeOf, F as quoteQualified, I as quoteTable, L as textArrayLiteral, M as qualified, N as quoteIdent, O as tenantTypeOf, P as quoteLiteral, S as CLAIM, _ as subjectClaimJsonSql, b as tenantClaimSql, d as keyTenantSql, f as kindFilterSql, h as roleKindSql, i as allowEntry, j as helperSchema, k as CLI, m as memberVia, n as GLOBAL_ROLES, o as rootName, p as liveSessionSql, r as HELPERS, s as signedIn, t as CUSTOM_ROLES, u as globalKindFilterSql, v as subjectClaimSql, x as userIdHelperSql, y as subjectIdSql } from "./shared-BAK8t4Uo.js";
|
|
77
15
|
//#region src/cli/sql/active.ts
|
|
78
16
|
function checkActiveRow(label, row) {
|
|
79
17
|
quoteTable(row.table);
|
|
@@ -81,9 +19,9 @@ function checkActiveRow(label, row) {
|
|
|
81
19
|
if (row.disabledAt !== void 0) quoteIdent(row.disabledAt);
|
|
82
20
|
if (row.status !== void 0) {
|
|
83
21
|
quoteIdent(row.status);
|
|
84
|
-
if (row.active === void 0 || row.active.length === 0) throw
|
|
22
|
+
if (row.active === void 0 || row.active.length === 0) throw configError(`PermDock CLI: ${label}.status needs the active values in ${label}.active`);
|
|
85
23
|
}
|
|
86
|
-
if (row.disabledAt === void 0 && row.status === void 0) throw
|
|
24
|
+
if (row.disabledAt === void 0 && row.status === void 0) throw configError(`PermDock CLI: ${label} needs disabledAt or status to tell an active row`);
|
|
87
25
|
}
|
|
88
26
|
/** Validates `rls.suspension` and keys its scopes by declared name. */
|
|
89
27
|
function checkSuspension(suspension, scopes) {
|
|
@@ -92,9 +30,9 @@ function checkSuspension(suspension, scopes) {
|
|
|
92
30
|
const byName = {};
|
|
93
31
|
for (const [key, row] of Object.entries(suspension.scopes ?? {})) {
|
|
94
32
|
const name = resolveScope(scopes, key);
|
|
95
|
-
if (name === void 0) throw
|
|
33
|
+
if (name === void 0) throw configError(`PermDock CLI: rls.suspension.scopes.${key} names a scope the policy does not declare`);
|
|
96
34
|
checkActiveRow(`rls.suspension.scopes.${key}`, row);
|
|
97
|
-
if (row.keep !== void 0 && !Array.isArray(row.keep)) throw
|
|
35
|
+
if (row.keep !== void 0 && !Array.isArray(row.keep)) throw configError(`PermDock CLI: rls.suspension.scopes.${key}.keep is a list of permissions or permission keys`);
|
|
98
36
|
const keep = keptKeys(row);
|
|
99
37
|
const { keep: _given, ...active } = row;
|
|
100
38
|
byName[name] = keep.length === 0 ? active : {
|
|
@@ -103,7 +41,7 @@ function checkSuspension(suspension, scopes) {
|
|
|
103
41
|
};
|
|
104
42
|
}
|
|
105
43
|
const memberships = suspension.memberships;
|
|
106
|
-
if (memberships?.keep !== void 0 && !Array.isArray(memberships.keep)) throw
|
|
44
|
+
if (memberships?.keep !== void 0 && !Array.isArray(memberships.keep)) throw configError("PermDock CLI: rls.suspension.memberships.keep is a list of permissions or permission keys");
|
|
107
45
|
const keep = keptKeys(memberships);
|
|
108
46
|
return {
|
|
109
47
|
...suspension.users === void 0 ? {} : { users: suspension.users },
|
|
@@ -126,18 +64,20 @@ function keptRowSql(keep, permission) {
|
|
|
126
64
|
return `case ${kind} when 'null' then true when 'array' then ${keep} @> jsonb_build_array(${"key" in permission ? quoteLiteral(permission.key) : permission.sql}) else false end`;
|
|
127
65
|
}
|
|
128
66
|
/**
|
|
129
|
-
* `
|
|
130
|
-
*
|
|
67
|
+
* Whether `id` is among the active rows' ids: a missing row is suspended, so
|
|
68
|
+
* the check fails closed, and so does a null `id`. `id` stays outside the
|
|
69
|
+
* subquery so a column of the same name on `row.table` cannot capture it.
|
|
131
70
|
*/
|
|
132
71
|
function activeRowSql(row, id) {
|
|
133
72
|
const table = quoteTable(qualifySqlTable(row.table));
|
|
134
|
-
const parts = [
|
|
73
|
+
const parts = [];
|
|
135
74
|
if (row.disabledAt !== void 0) parts.push(`s.${quoteIdent(row.disabledAt)} is null`);
|
|
136
75
|
if (row.status !== void 0) {
|
|
137
76
|
const values = (row.active ?? []).map(quoteLiteral).join(", ");
|
|
138
77
|
parts.push(`s.${quoteIdent(row.status)}::text = any(array[${values}]::text[])`);
|
|
139
78
|
}
|
|
140
|
-
|
|
79
|
+
const where = parts.length === 0 ? "" : ` where ${parts.join(" and ")}`;
|
|
80
|
+
return `coalesce(${id} in (select s.${quoteIdent(row.id)} from ${table} s${where}), false)`;
|
|
141
81
|
}
|
|
142
82
|
/** The active-user check for `user`; empty without `rls.suspension.users`. */
|
|
143
83
|
function activeUserSql(ctx, user = subjectIdSql(ctx)) {
|
|
@@ -161,7 +101,7 @@ function activeInstancesSql(ctx, scope, idOf, permission = ctx.permission === vo
|
|
|
161
101
|
const row = ctx.suspension?.scopes?.[name];
|
|
162
102
|
if (row === void 0) continue;
|
|
163
103
|
const id = idOf(name);
|
|
164
|
-
if (id === void 0) throw
|
|
104
|
+
if (id === void 0) throw configError(`PermDock CLI: rls.suspension.scopes.${name} needs the ${name} id on ${scope} memberships: add columns.${name} to the ${scope} memberships table`);
|
|
165
105
|
const keep = keptKeys(row);
|
|
166
106
|
if (permission !== void 0 && "key" in permission && keep.includes(permission.key)) continue;
|
|
167
107
|
const active = activeRowSql(row, `(${id})::${scopeTypeOf(ctx, name)}`);
|
|
@@ -184,7 +124,7 @@ function graphSqlName(name) {
|
|
|
184
124
|
}
|
|
185
125
|
function sqlNameOf(name, label) {
|
|
186
126
|
const sql = graphSqlName(name);
|
|
187
|
-
if (sql === void 0) throw
|
|
127
|
+
if (sql === void 0) throw configError(`PermDock CLI: ${label} is not a SQL name (letters, digits and underscores, starting with a letter), so rls generate cannot name its helper`);
|
|
188
128
|
return sql;
|
|
189
129
|
}
|
|
190
130
|
/** The helper returning the ids of `resource` the subject holds a relation on: `permitted_<snake_case resource>_ids`. */
|
|
@@ -225,7 +165,7 @@ function memberRoleOf(table, alias = "m", indent) {
|
|
|
225
165
|
/** The schema-qualified name of a table under `search_path = ''`. */
|
|
226
166
|
/** The helper returning the ids of scope `name` a grant key reaches. */
|
|
227
167
|
function permittedIdsHelper(name) {
|
|
228
|
-
if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw
|
|
168
|
+
if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw configError(`PermDock CLI: unsafe scope name '${name}'`);
|
|
229
169
|
return `permitted_${name}_ids`;
|
|
230
170
|
}
|
|
231
171
|
/** `permitted_<scope>_ids_for(p_user, p_grant)`: the same ids for a user the caller names. */
|
|
@@ -234,7 +174,7 @@ function permittedForHelper(name) {
|
|
|
234
174
|
}
|
|
235
175
|
/** The helper returning the ids of scope `name` the subject holds any live membership of. */
|
|
236
176
|
function memberIdsHelper(name) {
|
|
237
|
-
if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw
|
|
177
|
+
if (!/^[a-z][a-z0-9_]*$/u.test(name)) throw configError(`PermDock CLI: unsafe scope name '${name}'`);
|
|
238
178
|
return `member_${name}_ids`;
|
|
239
179
|
}
|
|
240
180
|
/** `member_<scope>_ids_for(p_user uuid)`: the same rows for a user the caller names; the token hook's helper. */
|
|
@@ -250,6 +190,15 @@ function hasMemberFor(input, name) {
|
|
|
250
190
|
if (input.dialect !== "supabase") return false;
|
|
251
191
|
return memberForTable(input, name) !== void 0 || memberForSources(input, name).length > 0;
|
|
252
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Whether `permitted_<scope>_users` can list the members of one instance of
|
|
195
|
+
* `name`: a mapped membership table, or sources that all read a table.
|
|
196
|
+
*/
|
|
197
|
+
function hasPermittedUsers(input, name) {
|
|
198
|
+
if (memberForTable(input, name) !== void 0) return true;
|
|
199
|
+
const sources = memberForSources(input, name);
|
|
200
|
+
return sources.length > 0 && sources.every((source) => source.sql.holders !== void 0);
|
|
201
|
+
}
|
|
253
202
|
/** The table `member_<scope>_ids_for` reads for scope `name`, when `memberships` maps one with its id column. */
|
|
254
203
|
function memberForTable(input, name) {
|
|
255
204
|
const table = scopeMembershipTable(input.memberships, input.scopes, name);
|
|
@@ -262,7 +211,7 @@ function memberForSources(input, name) {
|
|
|
262
211
|
}
|
|
263
212
|
/**
|
|
264
213
|
* The membership sources that can hold scope `name` when `memberships` maps
|
|
265
|
-
* no table for it: every `
|
|
214
|
+
* no table for it: every `tableMembershipSource` source and the `junctionMembershipSource` sources of
|
|
266
215
|
* that scope. Empty outside `database` mode.
|
|
267
216
|
*/
|
|
268
217
|
function scopeSources(ctx, name) {
|
|
@@ -297,8 +246,8 @@ function claimPath(ref) {
|
|
|
297
246
|
const prefix = ["principal.claim.", "principal.claims."].find((item) => ref.startsWith(item));
|
|
298
247
|
if (prefix === void 0) return;
|
|
299
248
|
const segments = ref.slice(prefix.length).split(".");
|
|
300
|
-
if (segments.length > MAX_CLAIM_DEPTH) throw
|
|
301
|
-
for (const segment of segments) if (!CLAIM.test(segment) || isForbiddenKey(segment)) throw
|
|
249
|
+
if (segments.length > MAX_CLAIM_DEPTH) throw configError(`PermDock CLI: claim path '${ref}' is deeper than ${MAX_CLAIM_DEPTH}`);
|
|
250
|
+
for (const segment of segments) if (!CLAIM.test(segment) || isForbiddenKey(segment)) throw configError(`PermDock CLI: unsafe claim name '${segment}'`);
|
|
302
251
|
return segments;
|
|
303
252
|
}
|
|
304
253
|
function jsonPath(base, keys, last) {
|
|
@@ -312,7 +261,7 @@ function jsonPath(base, keys, last) {
|
|
|
312
261
|
*/
|
|
313
262
|
function claimRoot(ctx, path) {
|
|
314
263
|
const [head, ...rest] = path;
|
|
315
|
-
if (head === void 0) throw
|
|
264
|
+
if (head === void 0) throw configError("PermDock CLI: empty claim path");
|
|
316
265
|
switch (ctx.dialect) {
|
|
317
266
|
case "supabase": return {
|
|
318
267
|
root: "(select auth.jwt())",
|
|
@@ -372,13 +321,16 @@ function claimArraySql(ctx, path, type) {
|
|
|
372
321
|
}
|
|
373
322
|
function sqlValue(value, ctx, field) {
|
|
374
323
|
if (value !== null && typeof value === "object" && "ref" in value) return compileRef(value.ref, ctx, field);
|
|
375
|
-
if (value !== null && typeof value === "object" && "date" in value)
|
|
324
|
+
if (value !== null && typeof value === "object" && "date" in value) {
|
|
325
|
+
const instant = isoInstant(value.date);
|
|
326
|
+
return instant === void 0 ? "null" : quoteLiteral(isoAt(instant));
|
|
327
|
+
}
|
|
376
328
|
if (isReadonlyArray(value)) return `array[${value.map((item) => sqlValue(item, ctx)).join(", ")}]`;
|
|
377
329
|
if (value === null) return "null";
|
|
378
330
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
379
331
|
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "null";
|
|
380
332
|
if (typeof value === "string") return quoteLiteral(value);
|
|
381
|
-
throw
|
|
333
|
+
throw configError("PermDock CLI: non-portable condition value");
|
|
382
334
|
}
|
|
383
335
|
function compileRef(ref, ctx, field) {
|
|
384
336
|
if (ref === "principal.id") return subjectIdSql(ctx);
|
|
@@ -386,8 +338,8 @@ function compileRef(ref, ctx, field) {
|
|
|
386
338
|
const path = claimPath(ref);
|
|
387
339
|
if (ref === "principal.tenant" || path?.length === 1 && path[0] === ctx.tenantClaim) return tenantClaimSql(ctx);
|
|
388
340
|
if (path !== void 0) return typedClaimSql(ctx, path, field === void 0 ? void 0 : ctx.columnTypes?.[field]);
|
|
389
|
-
if (ref === "context" || ref.startsWith("context.")) throw
|
|
390
|
-
throw
|
|
341
|
+
if (ref === "context" || ref.startsWith("context.")) throw configError(`PermDock CLI: '${ref}' is request context, which is not in the token; RLS cannot read it (permdock doctor PD027)`);
|
|
342
|
+
throw configError(`PermDock CLI: non-portable subject ref '${ref}'`);
|
|
391
343
|
}
|
|
392
344
|
/** `value` with `\`, `%` and `_` escaped for `like ... escape '\'`, as `escapeLike` does in process. */
|
|
393
345
|
function likeLiteralSql(value) {
|
|
@@ -406,13 +358,18 @@ function compareSql(op, field, value) {
|
|
|
406
358
|
default: return op;
|
|
407
359
|
}
|
|
408
360
|
}
|
|
409
|
-
|
|
410
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Whether the row's `rowField` is among the ids in `table.rowColumn` the
|
|
363
|
+
* subject holds a matching membership on. The row field stays outside the
|
|
364
|
+
* subquery: inside it, an unqualified name resolves to the membership table
|
|
365
|
+
* first, so `m.org_id = "org_id"` would compare the membership with itself.
|
|
366
|
+
*/
|
|
367
|
+
function membershipSql(table, rowColumn, rowField, roles, ctx, tenantColumn, scope) {
|
|
368
|
+
const parts = [`m.${quoteIdent(table.user)} = ${subjectIdSql(ctx)}`];
|
|
411
369
|
const role = memberRoleOf(table);
|
|
412
370
|
if (roles.length > 0) {
|
|
413
|
-
const roleList = roles.map((name) => name.replaceAll("'", "''")).join(",");
|
|
414
371
|
const held = role.through === void 0 && !role.lateral ? `m.${quoteIdent(role.column)}` : role.lookup;
|
|
415
|
-
parts.push(`${held} = any(
|
|
372
|
+
parts.push(`${held} = any(${textArrayLiteral(roles)})`);
|
|
416
373
|
const via = memberVia(table);
|
|
417
374
|
const single = sole(roles);
|
|
418
375
|
const kind = single === void 0 ? kindFilterSql(ctx, role.lookup, via) : roleKindSql(ctx, single, via);
|
|
@@ -432,23 +389,23 @@ function existsSql(table, rowColumn, rowField, roles, ctx, tenantColumn, scope)
|
|
|
432
389
|
}));
|
|
433
390
|
}
|
|
434
391
|
const expand = roles.length > 0 && role.lateral ? role.join : "";
|
|
435
|
-
return
|
|
392
|
+
return `${quoteIdent(rowField)} in (select m.${quoteIdent(rowColumn)} from ${quoteTable(table.table)} m${expand} where ${parts.join(" and ")})`;
|
|
436
393
|
}
|
|
437
394
|
function compileMemberOf(condition, ctx) {
|
|
438
395
|
const scope = condition.scope === "resource" ? void 0 : resolveScope(ctx.scopes, condition.scope);
|
|
439
|
-
if (condition.scope !== "resource" && scope === void 0) throw
|
|
396
|
+
if (condition.scope !== "resource" && scope === void 0) throw configError(`PermDock CLI: memberOf ${condition.scope} names a scope the policy does not declare`);
|
|
440
397
|
const mapping = scope === void 0 ? condition.resource === void 0 ? void 0 : ctx.memberships?.resource?.[condition.resource] : scopeMembershipTable(ctx.memberships, ctx.scopes, scope);
|
|
441
398
|
if (mapping !== void 0) {
|
|
442
399
|
const named = scope === void 0 ? void 0 : scopeTable(ctx, scope);
|
|
443
400
|
const rowColumn = scope === void 0 ? mapping.id : named?.column;
|
|
444
|
-
if (rowColumn === void 0) throw
|
|
401
|
+
if (rowColumn === void 0) throw configError(`PermDock CLI: memberships mapping for ${condition.scope} is missing the row column`);
|
|
445
402
|
const tenantColumn = scope === void 0 || scope === rootScope(ctx.scopes) ? void 0 : named?.tenantColumn;
|
|
446
|
-
const primary =
|
|
403
|
+
const primary = membershipSql(mapping, rowColumn, condition.field, condition.roles, ctx, tenantColumn, scope);
|
|
447
404
|
if (condition.scope !== "resource" || condition.parents === void 0) return primary;
|
|
448
405
|
return `(${[primary, ...condition.parents.flatMap((parent) => {
|
|
449
|
-
if (typeof parent === "string") return [
|
|
406
|
+
if (typeof parent === "string") return [membershipSql(mapping, rowColumn, parent, condition.roles, ctx, tenantColumn)];
|
|
450
407
|
const hopTable = ctx.memberships?.resource?.[parent.resource];
|
|
451
|
-
return hopTable?.id === void 0 ? [] : [
|
|
408
|
+
return hopTable?.id === void 0 ? [] : [membershipSql(hopTable, hopTable.id, parent.field, condition.roles, ctx)];
|
|
452
409
|
})].join(" or ")})`;
|
|
453
410
|
}
|
|
454
411
|
if (scope !== void 0 && condition.roles.length === 0 && scopeSources(ctx, scope).length > 0) return `${quoteIdent(condition.field)} in (select ${qualified(ctx, memberIdsHelper(scope))}())`;
|
|
@@ -461,7 +418,7 @@ function compileMemberOf(condition, ctx) {
|
|
|
461
418
|
];
|
|
462
419
|
return parts.length === 1 ? parts.join("") : `(${parts.join(" and ")})`;
|
|
463
420
|
}
|
|
464
|
-
throw
|
|
421
|
+
throw configError(`PermDock CLI: memberOf ${condition.scope} needs a memberships table mapping`);
|
|
465
422
|
}
|
|
466
423
|
function compileConditionSql(condition, ctx) {
|
|
467
424
|
switch (condition.op) {
|
|
@@ -489,13 +446,14 @@ function compileConditionSql(condition, ctx) {
|
|
|
489
446
|
const path = claimPath(condition.value.ref);
|
|
490
447
|
if (path === void 0) {
|
|
491
448
|
compileRef(condition.value.ref, ctx);
|
|
492
|
-
throw
|
|
449
|
+
throw configError(`PermDock CLI: non-portable ${condition.op} against '${condition.value.ref}'`);
|
|
493
450
|
}
|
|
494
451
|
const column = quoteIdent(condition.field);
|
|
495
452
|
const list = claimArraySql(ctx, path, ctx.columnTypes?.[condition.field]);
|
|
496
453
|
return condition.op === "in" ? `${column} = any (${list})` : `(${column} is not null and not (${column} = any (${list})))`;
|
|
497
454
|
}
|
|
498
455
|
const values = condition.value.map((item) => sqlValue(item, ctx));
|
|
456
|
+
if (values.length === 0) return condition.op === "in" ? "false" : `${quoteIdent(condition.field)} is not null`;
|
|
499
457
|
return `${quoteIdent(condition.field)} ${keyword} (${values.join(", ")})`;
|
|
500
458
|
}
|
|
501
459
|
case "isNull": return `${quoteIdent(condition.field)} is ${condition.value ? "" : "not "}null`;
|
|
@@ -609,7 +567,7 @@ function compileRelatedSql(condition, ctx) {
|
|
|
609
567
|
*/
|
|
610
568
|
function compileHoppedSql(condition, ctx) {
|
|
611
569
|
const resources = ctx.graph?.resources;
|
|
612
|
-
if (resources === void 0) throw
|
|
570
|
+
if (resources === void 0) throw configError("PermDock CLI: a related condition with link hops needs the policy resources in the RLS context");
|
|
613
571
|
const schema = quoteIdent(helperSchema(ctx));
|
|
614
572
|
return graphSqlText(relatedSql(condition, {
|
|
615
573
|
resources,
|
|
@@ -661,17 +619,17 @@ function relationWhere(policy, grant, grantee) {
|
|
|
661
619
|
resources: policy.resources,
|
|
662
620
|
now: { ref: "now" }
|
|
663
621
|
});
|
|
664
|
-
if (where === void 0) throw
|
|
622
|
+
if (where === void 0) throw configError(`PermDock CLI: grant ${grant.permission.key} names relation '${grantee.relation}', which ${grant.permission.resource} does not declare`);
|
|
665
623
|
return where;
|
|
666
624
|
}
|
|
667
625
|
function inheritWhere(policy, grant, grantee) {
|
|
668
626
|
const where = inheritCondition(grantee, policy.resources.get(grant.permission.resource), policy.resources);
|
|
669
|
-
if (where === void 0) throw
|
|
627
|
+
if (where === void 0) throw configError(`PermDock CLI: grant ${grant.permission.key} inherits ${grantee.permission}, which ${grant.permission.resource} cannot reach`);
|
|
670
628
|
return where;
|
|
671
629
|
}
|
|
672
630
|
/** Only a deny for `oauth-client` actors compiles: Postgres sees `client_id` and `act`, not other actor kinds. */
|
|
673
631
|
function actorAccess(grant, grantee, others) {
|
|
674
|
-
if (grant.effect !== "deny" || grantee.actor !== "oauth-client" || others > 0) throw
|
|
632
|
+
if (grant.effect !== "deny" || grantee.actor !== "oauth-client" || others > 0) throw configError(`PermDock CLI: grant ${grant.permission.key} is limited to an actor grantee; RLS compiles only deny(…, { to: actor('oauth-client') }) on its own, so enforce it in the application`);
|
|
675
633
|
return {
|
|
676
634
|
kind: "actor",
|
|
677
635
|
actor: "oauth-client"
|
|
@@ -679,7 +637,7 @@ function actorAccess(grant, grantee, others) {
|
|
|
679
637
|
}
|
|
680
638
|
function accessOf(grant, roles, anyone) {
|
|
681
639
|
const [held, ...more] = roles;
|
|
682
|
-
if (more.length > 0) throw
|
|
640
|
+
if (more.length > 0) throw configError(`PermDock CLI: grant ${grant.permission.key} requires several roles at once (${roles.map((item) => item.role).join(", ")}); RLS compiles one role per grant`);
|
|
683
641
|
if (held === void 0) return anyone ? { kind: "anyone" } : { kind: "authenticated" };
|
|
684
642
|
if (typeof held.scope === "object") return {
|
|
685
643
|
kind: "resource",
|
|
@@ -724,7 +682,7 @@ function collectGrants(policy) {
|
|
|
724
682
|
delegated = actorAccess(grant, item, items.length - 1);
|
|
725
683
|
break;
|
|
726
684
|
case "plan":
|
|
727
|
-
case "assurance": throw
|
|
685
|
+
case "assurance": throw configError(`PermDock CLI: grant ${grant.permission.key} is limited to a ${item.kind} grantee, which RLS cannot compile; enforce it in the application`);
|
|
728
686
|
default: return item;
|
|
729
687
|
}
|
|
730
688
|
const access = delegated ?? accessOf(grant, roles, anyone);
|
|
@@ -759,7 +717,7 @@ const DEFAULTS = {
|
|
|
759
717
|
};
|
|
760
718
|
function field(settings, name) {
|
|
761
719
|
const value = settings[name] ?? DEFAULTS[name];
|
|
762
|
-
if (typeof value !== "string" || !SQL_IDENT.test(value)) throw
|
|
720
|
+
if (typeof value !== "string" || !SQL_IDENT.test(value)) throw configError(`PermDock CLI: rls.apiKeys.${name} must be a claim field name matching ${SQL_IDENT.source}`);
|
|
763
721
|
return value;
|
|
764
722
|
}
|
|
765
723
|
function apiKeyFields(setting) {
|
|
@@ -776,7 +734,7 @@ function apiKeysPlan(setting, declared, renamed) {
|
|
|
776
734
|
if (setting === void 0) return;
|
|
777
735
|
const fields = apiKeyFields(setting);
|
|
778
736
|
const unknown = fields.serviceRoles.filter((role) => !declared.has(role));
|
|
779
|
-
if (unknown.length > 0) throw
|
|
737
|
+
if (unknown.length > 0) throw configError(`PermDock CLI: rls.apiKeys.serviceRoles names roles the policy does not declare: ${unknown.join(", ")}`);
|
|
780
738
|
return {
|
|
781
739
|
...fields,
|
|
782
740
|
...Object.keys(renamed).length === 0 ? {} : { renamed }
|
|
@@ -873,6 +831,15 @@ ${body}
|
|
|
873
831
|
), false)`;
|
|
874
832
|
}
|
|
875
833
|
//#endregion
|
|
834
|
+
//#region src/cli/shared/compare.ts
|
|
835
|
+
/** UTF-16 code-unit order, never `localeCompare`: generated SQL must match byte for byte on every machine. */
|
|
836
|
+
function byCodeUnit(a, b) {
|
|
837
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
838
|
+
}
|
|
839
|
+
function byKey([a], [b]) {
|
|
840
|
+
return byCodeUnit(a, b);
|
|
841
|
+
}
|
|
842
|
+
//#endregion
|
|
876
843
|
//#region src/cli/rls/custom-roles.ts
|
|
877
844
|
/** The readers every helpers file ends with. Names are part of the SQL contract. */
|
|
878
845
|
const READERS = {
|
|
@@ -916,9 +883,14 @@ create unique index if not exists ${quoteIdent(`${name}_level_key`)}` : `create
|
|
|
916
883
|
scope text not null default ${quoteLiteral(rootName(ctx))},
|
|
917
884
|
scope_id text,
|
|
918
885
|
role text not null,
|
|
919
|
-
${column} text not null${check}
|
|
920
|
-
|
|
921
|
-
|
|
886
|
+
${column} text not null${check}
|
|
887
|
+
);
|
|
888
|
+
-- global: no tenant or instance; a tenant role: a tenant; a shared role: no tenant or instance
|
|
889
|
+
alter table ${table} drop constraint if exists ${quoteIdent(`${name}_check`)};
|
|
890
|
+
alter table ${table} drop constraint if exists ${quoteIdent(`${name}_shape`)};
|
|
891
|
+
alter table ${table} add constraint ${quoteIdent(`${name}_shape`)} check (
|
|
892
|
+
case when scope = 'global' then tenant_id is null and scope_id is null
|
|
893
|
+
else tenant_id is not null or scope_id is null end
|
|
922
894
|
);
|
|
923
895
|
${index}
|
|
924
896
|
on ${table} (coalesce(tenant_id::text, ''), scope, coalesce(scope_id, ''), role, ${unique});
|
|
@@ -941,9 +913,11 @@ where rp.role = any(${sqlTextArray(custom.assignable)})
|
|
|
941
913
|
and a.effect = 'allow'
|
|
942
914
|
);
|
|
943
915
|
revoke all on table ${ceiling} from anon, authenticated, public;`);
|
|
944
|
-
const renames = Object.entries(custom.renamed ?? {}).toSorted(
|
|
916
|
+
const renames = Object.entries(custom.renamed ?? {}).toSorted(byKey);
|
|
917
|
+
const shared = sharedRoleSql(ctx);
|
|
945
918
|
if (custom.levels === true) {
|
|
946
919
|
chunks.push(leveledKeysSql(ctx, renames));
|
|
920
|
+
if (shared !== "") chunks.push(shared);
|
|
947
921
|
return chunks.join("\n\n");
|
|
948
922
|
}
|
|
949
923
|
const rename = renamesSql(renames);
|
|
@@ -1001,8 +975,30 @@ as $$
|
|
|
1001
975
|
where c.permission in (select a.permission from allowed a)
|
|
1002
976
|
$$;
|
|
1003
977
|
revoke execute on function ${keys}(text[], text[], text[], text) from public, anon, authenticated;`);
|
|
978
|
+
if (shared !== "") chunks.push(shared);
|
|
1004
979
|
return chunks.join("\n\n");
|
|
1005
980
|
}
|
|
981
|
+
/** `permdock_is_shared_role(p_scope, p_role)` (`database` mode): whether a shared custom role of that name exists at the scope. */
|
|
982
|
+
function sharedRoleSql(ctx) {
|
|
983
|
+
if (ctx.authorize !== "database") return "";
|
|
984
|
+
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
985
|
+
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
986
|
+
const shared = qualified(ctx, CUSTOM_ROLES.shared);
|
|
987
|
+
return `-- a shared custom role: rows with no tenant at a scope below global; its name wins over a tenant role's in every tenant
|
|
988
|
+
create or replace function ${shared}(p_scope text, p_role text)
|
|
989
|
+
returns boolean
|
|
990
|
+
language sql
|
|
991
|
+
stable
|
|
992
|
+
security definer
|
|
993
|
+
set search_path = ''
|
|
994
|
+
as $$
|
|
995
|
+
select p_scope <> 'global' and (
|
|
996
|
+
exists (select 1 from ${perms} c where c.tenant_id is null and c.scope = p_scope and c.role = p_role)
|
|
997
|
+
or exists (select 1 from ${includes} c where c.tenant_id is null and c.scope = p_scope and c.role = p_role)
|
|
998
|
+
)
|
|
999
|
+
$$;
|
|
1000
|
+
revoke execute on function ${shared}(text, text) from public, anon, authenticated;`;
|
|
1001
|
+
}
|
|
1006
1002
|
/**
|
|
1007
1003
|
* `permdock_custom_keys` when resources declare levels. An allow entry is
|
|
1008
1004
|
* `key` or `key@level`. A level reaches only the ceiling's `<grant key>@<level>`
|
|
@@ -1139,7 +1135,8 @@ function renamesSql(renames) {
|
|
|
1139
1135
|
* for `authenticated`. A declared role answers from `role_permissions`, which
|
|
1140
1136
|
* is the catalog. A custom role (`database` mode) answers from the tenant's
|
|
1141
1137
|
* own rows through `permdock_custom_keys`, only to a member of the tenant or
|
|
1142
|
-
* a holder of a `meta.manageRoles` permission for a platform role
|
|
1138
|
+
* a holder of a `meta.manageRoles` permission for a platform role; a shared
|
|
1139
|
+
* role (no tenant below global) to any signed-in caller. The
|
|
1143
1140
|
* effect is `deny` for a deny the role carries after the ceiling, as
|
|
1144
1141
|
* `resolveCustomRole` returns it, never for a deny that merely removes an allow.
|
|
1145
1142
|
*/
|
|
@@ -1230,6 +1227,7 @@ end;`;
|
|
|
1230
1227
|
body = checked(`
|
|
1231
1228
|
if not (${signedIn(ctx)} and (
|
|
1232
1229
|
${platform}
|
|
1230
|
+
or (p_scope <> 'global' and p_tenant is null and p_scope_id is null)
|
|
1233
1231
|
or (p_scope <> 'global' and (${memberOf}))
|
|
1234
1232
|
)) then
|
|
1235
1233
|
${raiseSql(" ", "42501", `'permdock: the caller is not a member of ' || coalesce(p_tenant::text, 'the platform')`, "not-member")}
|
|
@@ -1263,6 +1261,27 @@ $$;
|
|
|
1263
1261
|
revoke execute on function ${trusted}(${signature}) from public, anon, authenticated;${grants}`);
|
|
1264
1262
|
return chunks.join("\n\n");
|
|
1265
1263
|
}
|
|
1264
|
+
/** The stored definition of `p_role` at `p_scope` for `p_tenant`: the shared rows when a shared role has the name. */
|
|
1265
|
+
function storedTenant(ctx) {
|
|
1266
|
+
return `case when ${qualified(ctx, CUSTOM_ROLES.shared)}(p_scope, p_role) then c.tenant_id is null else c.tenant_id is not distinct from p_tenant end`;
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* The tenant half of a custom-role row match for a membership: the
|
|
1270
|
+
* membership's tenant, or no tenant when a shared role of that name exists at
|
|
1271
|
+
* the scope, so a shared role shadows a tenant role of the same name.
|
|
1272
|
+
*/
|
|
1273
|
+
function customTenantMatch(ctx, tenant, scope, role) {
|
|
1274
|
+
return `case when ${qualified(ctx, CUSTOM_ROLES.shared)}(${quoteLiteral(scope)}, ${role}) then c.tenant_id is null else c.tenant_id::text = ${tenant} end`;
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* The scope of a row of the application's roles table: its scope column (or
|
|
1278
|
+
* the first scope) with a tenant; with no tenant, the same scope when the
|
|
1279
|
+
* `shared` column is true, else `'global'`.
|
|
1280
|
+
*/
|
|
1281
|
+
function rolesTableScope(table, tenant, root, column) {
|
|
1282
|
+
const own = table.scope === void 0 ? quoteLiteral(root) : `${column(table.scope)}::text`;
|
|
1283
|
+
return `case when ${tenant} is null then ${table.shared === void 0 ? "'global'" : `case when coalesce(${column(table.shared)}, false) then ${own} else 'global' end`} else ${own} end`;
|
|
1284
|
+
}
|
|
1266
1285
|
/** `expr` read as its current key: a former key maps to the key it was renamed to. */
|
|
1267
1286
|
function currentKeyExpr(renames, expr) {
|
|
1268
1287
|
if (renames.length === 0) return expr;
|
|
@@ -1301,7 +1320,8 @@ function globalRoleSource(source, schema, alias, roles) {
|
|
|
1301
1320
|
indent: " "
|
|
1302
1321
|
});
|
|
1303
1322
|
const tenant = role.lateral ? void 0 : throughTenant(role.through, roles);
|
|
1304
|
-
const
|
|
1323
|
+
const shared = tenant === void 0 ? void 0 : roles?.shared;
|
|
1324
|
+
const global = tenant === void 0 ? "" : ` and ${alias}k.${quoteIdent(tenant)} is null${shared === void 0 ? "" : ` and ${alias}k.${quoteIdent(shared)} is not true`}`;
|
|
1305
1325
|
return {
|
|
1306
1326
|
table,
|
|
1307
1327
|
user,
|
|
@@ -1310,7 +1330,8 @@ function globalRoleSource(source, schema, alias, roles) {
|
|
|
1310
1330
|
roleSql: role.sql,
|
|
1311
1331
|
role: roleManifest(role),
|
|
1312
1332
|
...role.through === void 0 ? {} : { through: role.through },
|
|
1313
|
-
...tenant === void 0 ? {} : { tenant }
|
|
1333
|
+
...tenant === void 0 ? {} : { tenant },
|
|
1334
|
+
...shared === void 0 ? {} : { shared }
|
|
1314
1335
|
};
|
|
1315
1336
|
}
|
|
1316
1337
|
//#endregion
|
|
@@ -1544,7 +1565,7 @@ function tableBody(ctx, scope, type) {
|
|
|
1544
1565
|
const custom = ctx.customRoles;
|
|
1545
1566
|
if (custom === void 0) return lines.join("\n");
|
|
1546
1567
|
const match = [
|
|
1547
|
-
|
|
1568
|
+
customTenantMatch(ctx, `${memberColumn(tenantColumn ?? column)}::text`, scope, role.sql),
|
|
1548
1569
|
`c.scope = ${quoteLiteral(scope)}`,
|
|
1549
1570
|
`(c.scope_id is null or c.scope_id = ${memberColumn(column)}::text)`,
|
|
1550
1571
|
`c.role = ${role.sql}`
|
|
@@ -1585,8 +1606,8 @@ function claimBody(ctx, scope, type) {
|
|
|
1585
1606
|
and (${activeTenant(ctx)} is null or ${claimTenant(ctx, scope)} = ${activeTenant(ctx)})` : "";
|
|
1586
1607
|
const filtersFor = (grant) => ` and m ->> 'scope' = ${quoteLiteral(scope)}
|
|
1587
1608
|
and m ->> 'id' is not null${narrow}${keyTenantLine(ctx, scope, claimTenant(ctx, scope))}
|
|
1588
|
-
and case jsonb_typeof(m -> '
|
|
1589
|
-
when 'number' then (m ->> '
|
|
1609
|
+
and case jsonb_typeof(m -> 'expires_at')
|
|
1610
|
+
when 'number' then (m ->> 'expires_at')::numeric > extract(epoch from now())
|
|
1590
1611
|
else true
|
|
1591
1612
|
end${[
|
|
1592
1613
|
...claimKeptLines(ctx, " ", grantPermissionSql(grant)),
|
|
@@ -1707,7 +1728,7 @@ ${rows}
|
|
|
1707
1728
|
const custom = ctx.customRoles;
|
|
1708
1729
|
if (custom === void 0) return sourcesBodyOf(declared, sources, subjectIdSql(ctx));
|
|
1709
1730
|
const match = [
|
|
1710
|
-
`
|
|
1731
|
+
customTenantMatch(ctx, `(${tenant})::text`, scope, "r.role"),
|
|
1711
1732
|
`c.scope = ${quoteLiteral(scope)}`,
|
|
1712
1733
|
"(c.scope_id is null or c.scope_id = (ms.id)::text)",
|
|
1713
1734
|
"c.role = r.role"
|
|
@@ -1771,8 +1792,8 @@ as $$
|
|
|
1771
1792
|
and jsonb_typeof(c -> 'roles') = 'array'
|
|
1772
1793
|
and c -> 'roles' @> jsonb_build_array(p_role)
|
|
1773
1794
|
and (c -> 'permissions' is null or c -> 'permissions' @> jsonb_build_array(p_permission))
|
|
1774
|
-
and jsonb_typeof(c -> '
|
|
1775
|
-
and (c ->> '
|
|
1795
|
+
and jsonb_typeof(c -> 'expires_at') = 'number'
|
|
1796
|
+
and (c ->> 'expires_at')::numeric > extract(epoch from now())
|
|
1776
1797
|
$$;
|
|
1777
1798
|
revoke execute on function ${fn}(text, text, text) from public;
|
|
1778
1799
|
grant execute on function ${fn}(text, text, text) to anon, authenticated;`;
|
|
@@ -1808,7 +1829,7 @@ function customRoleCascadeSql(ctx) {
|
|
|
1808
1829
|
const fn = qualified(ctx, CUSTOM_ROLES.cascade);
|
|
1809
1830
|
const target = quoteTable(qualifySqlTable(table.table));
|
|
1810
1831
|
const tenant = (row) => table.tenant === void 0 ? `null::${tenantType}` : `${rowColumn(row, table.tenant)}::${tenantType}`;
|
|
1811
|
-
const scope = (row) => table
|
|
1832
|
+
const scope = (row) => rolesTableScope(table, tenant(row), root, (name) => rowColumn(row, name));
|
|
1812
1833
|
const id = (row) => table.id === void 0 ? "null::text" : `${rowColumn(row, table.id)}::text`;
|
|
1813
1834
|
const skipped = (row) => table.skip === void 0 ? "false" : `coalesce(${rowColumn(row, table.skip)}, false)`;
|
|
1814
1835
|
const guard = qualified(ctx, CUSTOM_ROLES.guard);
|
|
@@ -1942,7 +1963,7 @@ ${raiseSql(" ", "42501", `'permdock: the caller may not manage roles in ' ||
|
|
|
1942
1963
|
)` : "";
|
|
1943
1964
|
const reachCte = leveled && reachRows !== "" ? `,
|
|
1944
1965
|
reach (grant_key, level) as (values ${reachRows})` : "";
|
|
1945
|
-
const match =
|
|
1966
|
+
const match = `${storedTenant(ctx)} and c.scope = p_scope and c.scope_id is not distinct from p_scope_id`;
|
|
1946
1967
|
const memberOf = `p_tenant::text in ${members(root)}
|
|
1947
1968
|
or ${perScope(ctx, (scope) => scope === root ? "false" : `p_scope_id in ${members(scope)}`)}`;
|
|
1948
1969
|
return {
|
|
@@ -1994,7 +2015,7 @@ begin
|
|
|
1994
2015
|
perform ${shape}(p_tenant, p_scope, p_scope_id, p_role);
|
|
1995
2016
|
if not (${signedIn(ctx, user)} and (
|
|
1996
2017
|
${platform}
|
|
1997
|
-
or (p_scope <> 'global' and (${memberOf}))
|
|
2018
|
+
or (p_scope <> 'global' and coalesce(${memberOf}, false))
|
|
1998
2019
|
)) then
|
|
1999
2020
|
${raiseSql(" ", "42501", `'permdock: the caller is not a member of ' || coalesce(p_tenant::text, 'the platform')`, "not-member")}
|
|
2000
2021
|
end if;${requires}
|
|
@@ -2022,8 +2043,9 @@ revoke execute on function ${guard}(${userArg}${tenantType}, text, text, text) f
|
|
|
2022
2043
|
* \`assignablePermissions\`: entries inside the ceiling of the role's scope,
|
|
2023
2044
|
* includes naming declared roles, and only permissions and levels the caller
|
|
2024
2045
|
* may hand out, for the new definition and the stored one alike. A platform
|
|
2025
|
-
* custom role (\`p_scope = 'global'\`, no tenant)
|
|
2026
|
-
*
|
|
2046
|
+
* custom role (\`p_scope = 'global'\`, no tenant) and a shared custom role (no
|
|
2047
|
+
* tenant at a lower scope) are written by a caller who holds a
|
|
2048
|
+
* \`meta.manageRoles\` permission through a global role. The
|
|
2027
2049
|
* \`permdock_trusted_*\` variants skip the caller checks and keep the
|
|
2028
2050
|
* definition checks; no client role may execute them.
|
|
2029
2051
|
*/
|
|
@@ -2041,7 +2063,7 @@ function customRoleWritesSql(ctx, levelReach) {
|
|
|
2041
2063
|
const guard = qualified(ctx, CUSTOM_ROLES.guard);
|
|
2042
2064
|
const shape = qualified(ctx, CUSTOM_ROLES.shape);
|
|
2043
2065
|
const entries = qualified(ctx, CUSTOM_ROLES.entries);
|
|
2044
|
-
const renames = Object.entries(custom.renamed ?? {}).toSorted(
|
|
2066
|
+
const renames = Object.entries(custom.renamed ?? {}).toSorted(byKey);
|
|
2045
2067
|
const current = (expr) => currentKeyExpr(renames, expr);
|
|
2046
2068
|
const known = sqlTextArray([...custom.permissions ?? [], ...renames.map(([former]) => former)]);
|
|
2047
2069
|
const caller = callerFunctionsSql(ctx, custom, root, levelReach, false);
|
|
@@ -2049,9 +2071,15 @@ function customRoleWritesSql(ctx, levelReach) {
|
|
|
2049
2071
|
const scopes = sqlTextArray(ctx.scopes.map((scope) => scope.name));
|
|
2050
2072
|
const roleName = (param) => `coalesce(${param}, '') = '' or ${param} = any(${sqlTextArray(custom.declared)})`;
|
|
2051
2073
|
const levelOf = leveled ? ", nullif(split_part(e, '@', 2), '')" : "";
|
|
2052
|
-
const
|
|
2074
|
+
const levelColumn = leveled ? ", level" : "";
|
|
2075
|
+
const sharedRole = qualified(ctx, CUSTOM_ROLES.shared);
|
|
2076
|
+
const taken = (param) => ` if p_tenant is not null and ${sharedRole}(p_scope, ${param}) then
|
|
2077
|
+
${raiseSql(" ", "22023", `'permdock: ' || ${param} || ' is the name of a shared custom role'`, "name-taken")}
|
|
2078
|
+
end if;
|
|
2079
|
+
`;
|
|
2080
|
+
const write = `${taken("p_role")} delete from ${perms} c where ${match} and c.role = p_role;
|
|
2053
2081
|
delete from ${includes} c where ${match} and c.role = p_role;
|
|
2054
|
-
insert into ${perms} (tenant_id, scope, scope_id, role, permission, effect${
|
|
2082
|
+
insert into ${perms} (tenant_id, scope, scope_id, role, permission, effect${levelColumn})
|
|
2055
2083
|
select distinct p_tenant, p_scope, p_scope_id, p_role, ${current(leveled ? "split_part(e, '@', 1)" : "e")}, 'allow'${levelOf}
|
|
2056
2084
|
from unnest(coalesce(p_allow, '{}'::text[])) e
|
|
2057
2085
|
union
|
|
@@ -2060,7 +2088,7 @@ function customRoleWritesSql(ctx, levelReach) {
|
|
|
2060
2088
|
insert into ${includes} (tenant_id, scope, scope_id, role, include_role)
|
|
2061
2089
|
select distinct p_tenant, p_scope, p_scope_id, p_role, e
|
|
2062
2090
|
from unnest(coalesce(p_include, '{}'::text[])) e;`;
|
|
2063
|
-
const move =
|
|
2091
|
+
const move = `${taken("p_to")} if ${roleName("p_to")} then
|
|
2064
2092
|
${raiseSql(" ", "22023", `'permdock: ' || coalesce(p_to, 'null') || ' is not a custom role name'`, "declared-role")}
|
|
2065
2093
|
end if;
|
|
2066
2094
|
if exists (select 1 from ${perms} c where ${match} and c.role = p_to)
|
|
@@ -2091,7 +2119,7 @@ grant execute on function ${qualified(ctx, name)}(${args}) to authenticated;` :
|
|
|
2091
2119
|
const removeParams = `p_tenant ${tenantType}, p_scope text, p_scope_id text, p_role text`;
|
|
2092
2120
|
return `${caller.beyond}
|
|
2093
2121
|
|
|
2094
|
-
-- where a custom role may live: a scope of a tenant, or the platform (scope global, no tenant or instance), under an undeclared name
|
|
2122
|
+
-- where a custom role may live: a scope of a tenant, a scope of every tenant (shared: no tenant or instance), or the platform (scope global, no tenant or instance), under an undeclared name
|
|
2095
2123
|
create or replace function ${shape}(p_tenant ${tenantType}, p_scope text, p_scope_id text, p_role text)
|
|
2096
2124
|
returns void
|
|
2097
2125
|
language plpgsql
|
|
@@ -2103,8 +2131,10 @@ begin
|
|
|
2103
2131
|
if p_tenant is not null or p_scope_id is not null then
|
|
2104
2132
|
${raiseSql(" ", "22023", `'permdock: a platform custom role has no tenant or instance'`, "unknown-scope")}
|
|
2105
2133
|
end if;
|
|
2106
|
-
elsif
|
|
2134
|
+
elsif not (p_scope = any(${scopes})) then
|
|
2107
2135
|
${raiseSql(" ", "22023", `'permdock: custom roles are held at a scope of a tenant or at global, not ' || coalesce(p_scope, 'null')`, "unknown-scope")}
|
|
2136
|
+
elsif p_tenant is null and p_scope_id is not null then
|
|
2137
|
+
${raiseSql(" ", "22023", `'permdock: a shared custom role has no instance'`, "unknown-scope")}
|
|
2108
2138
|
end if;
|
|
2109
2139
|
if ${roleName("p_role")} then
|
|
2110
2140
|
${raiseSql(" ", "22023", `'permdock: ' || coalesce(p_role, 'null') || ' is not a custom role name'`, "declared-role")}
|
|
@@ -2250,7 +2280,7 @@ function globalRolesSql(ctx) {
|
|
|
2250
2280
|
const id = quoteIdent(through.id);
|
|
2251
2281
|
const key = quoteIdent(through.key);
|
|
2252
2282
|
const ref = quoteIdent(through.ref);
|
|
2253
|
-
const global = source.tenant === void 0 ? "" : ` and k.${quoteIdent(source.tenant)} is null`;
|
|
2283
|
+
const global = source.tenant === void 0 ? "" : ` and k.${quoteIdent(source.tenant)} is null${source.shared === void 0 ? "" : ` and k.${quoteIdent(source.shared)} is not true`}`;
|
|
2254
2284
|
body = ` select string_agg(w.r, ', ' order by w.r) into v_unknown
|
|
2255
2285
|
from (${wanted}) w(r)
|
|
2256
2286
|
where not exists (select 1 from ${keys} k where k.${key}::text = w.r${global});
|
|
@@ -2298,7 +2328,7 @@ grant execute on function ${fn}(${args}) to authenticated;`;
|
|
|
2298
2328
|
* a live membership of, with any role. Not narrowed to the active tenant, and
|
|
2299
2329
|
* no cascade: a membership of a child scope is not one of its parent.
|
|
2300
2330
|
*/
|
|
2301
|
-
function memberBody(ctx, scope, type) {
|
|
2331
|
+
function memberBody(ctx, scope, type, roles) {
|
|
2302
2332
|
if (ctx.authorize !== "database") return sqlBody(` select distinct (m ->> 'id')::${type}
|
|
2303
2333
|
from jsonb_array_elements(
|
|
2304
2334
|
case jsonb_typeof(${membershipClaim(ctx)}) when 'array' then ${membershipClaim(ctx)} else '[]'::jsonb end
|
|
@@ -2307,22 +2337,22 @@ function memberBody(ctx, scope, type) {
|
|
|
2307
2337
|
and m ->> 'scope' = ${quoteLiteral(scope)}
|
|
2308
2338
|
and m ->> 'id' is not null${keyTenantLine(ctx, scope, claimTenant(ctx, scope))}
|
|
2309
2339
|
and jsonb_typeof(m -> 'roles') = 'array'
|
|
2310
|
-
and jsonb_array_length(m -> 'roles') > 0
|
|
2311
|
-
and case jsonb_typeof(m -> '
|
|
2312
|
-
when 'number' then (m ->> '
|
|
2340
|
+
and jsonb_array_length(m -> 'roles') > 0${roles === void 0 ? "" : `\n and (m -> 'roles') ?| ${roles}`}
|
|
2341
|
+
and case jsonb_typeof(m -> 'expires_at')
|
|
2342
|
+
when 'number' then (m ->> 'expires_at')::numeric > extract(epoch from now())
|
|
2313
2343
|
else true
|
|
2314
2344
|
end${[
|
|
2315
2345
|
...claimKeptLines(ctx, " "),
|
|
2316
2346
|
...userActive(ctx, " "),
|
|
2317
2347
|
...instancesActive(ctx, scope, (name) => name === scope ? `m ->> 'id'` : `m -> 'within' ->> ${quoteLiteral(name)}`, " ")
|
|
2318
2348
|
].map((line) => `\n${line}`).join("")}`);
|
|
2319
|
-
return memberRowsBody(ctx, scope, type, subjectIdSql(ctx), scopeSources(ctx, scope), true);
|
|
2349
|
+
return memberRowsBody(ctx, scope, type, subjectIdSql(ctx), scopeSources(ctx, scope), true, roles);
|
|
2320
2350
|
}
|
|
2321
2351
|
/**
|
|
2322
2352
|
* The membership rows of `user` for `scope`, from the sources or else the
|
|
2323
2353
|
* mapped table, with the same expiry and suspension filters either way.
|
|
2324
2354
|
*/
|
|
2325
|
-
function memberRowsBody(ctx, scope, type, user, sources, keyed = false) {
|
|
2355
|
+
function memberRowsBody(ctx, scope, type, user, sources, keyed = false, roles) {
|
|
2326
2356
|
if (sources.length > 0) {
|
|
2327
2357
|
const narrow = keyed ? keyTenantLine(ctx, scope, sourceIdOf(scope)(rootName(ctx))) : "";
|
|
2328
2358
|
return sourcesBodyOf(` select distinct (ms.id)::${type}
|
|
@@ -2332,7 +2362,7 @@ ${sourceRows(sources)}
|
|
|
2332
2362
|
where ${signedIn(ctx, user)}
|
|
2333
2363
|
and ms.scope = ${quoteLiteral(scope)}
|
|
2334
2364
|
and jsonb_typeof(ms.roles) = 'array'
|
|
2335
|
-
and jsonb_array_length(ms.roles) > 0${narrow}${sourceFilters(ctx, scope, user, void 0, sourcesKeep(sources))}`, sources, user);
|
|
2365
|
+
and jsonb_array_length(ms.roles) > 0${roles === void 0 ? "" : `\n and ms.roles ?| ${roles}`}${narrow}${sourceFilters(ctx, scope, user, void 0, sourcesKeep(sources))}`, sources, user);
|
|
2336
2366
|
}
|
|
2337
2367
|
const mapped = scopeTable(ctx, scope);
|
|
2338
2368
|
if (mapped === void 0) return sqlBody(` select null::${type} where false -- no ${scope} memberships table configured`);
|
|
@@ -2344,6 +2374,7 @@ ${sourceRows(sources)}
|
|
|
2344
2374
|
` where ${memberColumn(table.user)} = ${user}`,
|
|
2345
2375
|
` and ${role.through === void 0 && !role.lateral ? memberColumn(role.column) : role.sql} is not null`
|
|
2346
2376
|
];
|
|
2377
|
+
if (roles !== void 0) lines.push(` and (${role.through === void 0 && !role.lateral ? memberColumn(role.column) : role.sql})::text = any(${roles})`);
|
|
2347
2378
|
const narrow = keyed ? keyTenantSql(ctx, tenantColumn === void 0 ? void 0 : memberColumn(tenantColumn)) : void 0;
|
|
2348
2379
|
if (narrow !== void 0) lines.push(` and ${narrow}`);
|
|
2349
2380
|
if (table.expiresAt !== void 0) {
|
|
@@ -2377,6 +2408,22 @@ ${functionBody(keyed)}
|
|
|
2377
2408
|
${grants}`;
|
|
2378
2409
|
}
|
|
2379
2410
|
/**
|
|
2411
|
+
* `member_<scope>_ids(p_roles text[])`: `member_<scope>_ids()` narrowed to
|
|
2412
|
+
* memberships holding one of `p_roles` by name, for `rls migrate`'s `role`
|
|
2413
|
+
* helpers. Generated only for a scope such a helper names.
|
|
2414
|
+
*/
|
|
2415
|
+
function memberRolesFunction(ctx, scope, type, anonExecute) {
|
|
2416
|
+
const fn = qualified(ctx, memberIdsHelper(scope));
|
|
2417
|
+
const body = memberBody(ctx, scope, type, "p_roles");
|
|
2418
|
+
const grants = anonExecute ? `revoke execute on function ${fn}(text[]) from public;
|
|
2419
|
+
grant execute on function ${fn}(text[]) to anon, authenticated;` : `revoke execute on function ${fn}(text[]) from public, anon;
|
|
2420
|
+
grant execute on function ${fn}(text[]) to authenticated;`;
|
|
2421
|
+
return `create or replace function ${fn}(p_roles text[])
|
|
2422
|
+
returns setof ${type}
|
|
2423
|
+
${functionBody(body)}
|
|
2424
|
+
${grants}`;
|
|
2425
|
+
}
|
|
2426
|
+
/**
|
|
2380
2427
|
* `member_<scope>_ids_for(p_user uuid)`: `member_<scope>_ids()` for a user the
|
|
2381
2428
|
* caller names, read from the membership sources (or the mapped table) in
|
|
2382
2429
|
* either mode, so a token hook can call it before any claim exists. Naming
|
|
@@ -2400,7 +2447,7 @@ revoke execute on function ${fn}(uuid) from public, anon, authenticated;`;
|
|
|
2400
2447
|
function accessSql(ctx, scope, grantKey, column) {
|
|
2401
2448
|
const key = quoteLiteral(grantKey);
|
|
2402
2449
|
if (scope === "global") return `(select ${qualified(ctx, HELPERS.has)}(${key}))`;
|
|
2403
|
-
if (column === void 0) throw
|
|
2450
|
+
if (column === void 0) throw configError(`PermDock CLI: ${scope}-scoped grant needs definePolicy({ scopes.${scope} })`);
|
|
2404
2451
|
return `${quoteIdent(column)} in (select ${qualified(ctx, permittedIdsHelper(scope))}(${key}))`;
|
|
2405
2452
|
}
|
|
2406
2453
|
function userIdSql(ctx, anonExecute) {
|
|
@@ -2534,6 +2581,7 @@ revoke all on table ${ur} from anon, authenticated, public;`);
|
|
|
2534
2581
|
const type = scopeTypeOf(ctx, scope.name);
|
|
2535
2582
|
chunks.push(helperFunction(ctx, permittedIdsHelper(scope.name), `setof ${type}`, bodies.ids(scope.name), anon));
|
|
2536
2583
|
chunks.push(memberFunction(ctx, scope.name, type, anon));
|
|
2584
|
+
if (ctx.roleMemberScopes?.includes(scope.name) === true) chunks.push(memberRolesFunction(ctx, scope.name, type, anon));
|
|
2537
2585
|
if (hasMemberFor(ctx, scope.name)) chunks.push(memberForFunction(ctx, scope.name, type));
|
|
2538
2586
|
}
|
|
2539
2587
|
const forUser = forUserSql(ctx);
|
|
@@ -2620,12 +2668,12 @@ function requiresSql(ctx, key, resource) {
|
|
|
2620
2668
|
}
|
|
2621
2669
|
/** Postgres truncates an identifier past 63 bytes, which would merge two helpers. */
|
|
2622
2670
|
function checkName(name) {
|
|
2623
|
-
if (new TextEncoder().encode(name).length > 63) throw
|
|
2671
|
+
if (new TextEncoder().encode(name).length > 63) throw configError(`PermDock CLI: the helper name ${name} is longer than 63 bytes; use a shorter scope name`);
|
|
2624
2672
|
return name;
|
|
2625
2673
|
}
|
|
2626
2674
|
function grantsFunction(ctx, grants, renamed, anonExecute) {
|
|
2627
2675
|
const fn = qualified(ctx, PERMISSION_HELPERS.grantKeys);
|
|
2628
|
-
const map = Object.fromEntries([...grants].toSorted(
|
|
2676
|
+
const map = Object.fromEntries([...grants].toSorted(byKey));
|
|
2629
2677
|
const current = Object.keys(renamed).length === 0 ? "p_permission" : `coalesce(${quoteLiteral(JSON.stringify(renamed))}::jsonb ->> p_permission, p_permission)`;
|
|
2630
2678
|
return `-- the grant keys a permission key reaches on one scope: its unconditional allows, with p_effect 'deny' every deny, and with 'conditioned-allow' or 'conditioned-deny' the allows or denies that carry a row condition
|
|
2631
2679
|
create or replace function ${fn}(p_permission text, p_scope text, p_effect text default 'allow')
|
|
@@ -2712,7 +2760,7 @@ function permissionHelpersSql(ctx, grants, renamed, anonExecute) {
|
|
|
2712
2760
|
) g
|
|
2713
2761
|
cross join lateral ${helper}(${user}g.grant_key) d(id)`;
|
|
2714
2762
|
const keysBody = (scope, bodies, filtered) => {
|
|
2715
|
-
const rows = ["global", scope].flatMap((at) => Object.entries(grants.get(at) ?? {}).toSorted(
|
|
2763
|
+
const rows = ["global", scope].flatMap((at) => Object.entries(grants.get(at) ?? {}).toSorted(byKey).flatMap(([key, effects]) => ["allow", "deny"].flatMap((effect) => effects[effect].map((grant) => `(${[
|
|
2716
2764
|
key,
|
|
2717
2765
|
at,
|
|
2718
2766
|
effect,
|
|
@@ -2802,4 +2850,848 @@ ${keysFunction(keysFor, `p_user ${user}, p_id ${type}, p_keys text[]`, keysBody(
|
|
|
2802
2850
|
return `${chunks.join("\n\n")}\n`;
|
|
2803
2851
|
}
|
|
2804
2852
|
//#endregion
|
|
2805
|
-
|
|
2853
|
+
//#region src/cli/rls/ownership-names.ts
|
|
2854
|
+
/** Objects the ownership rules add next to the helpers. Names are part of the SQL contract. */
|
|
2855
|
+
const OWNERSHIP = {
|
|
2856
|
+
canAssign: "permdock_can_assign",
|
|
2857
|
+
canAssignFor: "permdock_can_assign_for",
|
|
2858
|
+
canAssignAny: "permdock_can_assign_any",
|
|
2859
|
+
canAssignAnyFor: "permdock_can_assign_any_for",
|
|
2860
|
+
holders: "permdock_holders",
|
|
2861
|
+
transferOnly: "permdock_transfer_only"
|
|
2862
|
+
};
|
|
2863
|
+
/** Objects `rls.assignments` adds. Names are part of the SQL contract. */
|
|
2864
|
+
const ASSIGNMENTS = {
|
|
2865
|
+
custom: "permdock_can_assign_custom_role",
|
|
2866
|
+
customFor: "permdock_can_assign_custom_role_for",
|
|
2867
|
+
trigger: "permdock_assignment"
|
|
2868
|
+
};
|
|
2869
|
+
//#endregion
|
|
2870
|
+
//#region src/cli/rls/assignments.ts
|
|
2871
|
+
/** The client roles whose writes the assignment triggers check; any other role is a trusted path. */
|
|
2872
|
+
const CLIENT_ROLES = [
|
|
2873
|
+
"anon",
|
|
2874
|
+
"anonymous",
|
|
2875
|
+
"authenticated"
|
|
2876
|
+
];
|
|
2877
|
+
/** The mapped membership tables, `rls.assignments.tables` and the global-roles table `rls.roles`. */
|
|
2878
|
+
function assignedTables(ctx) {
|
|
2879
|
+
const out = [];
|
|
2880
|
+
for (const { name } of ctx.scopes) {
|
|
2881
|
+
const mapped = scopeTable(ctx, name);
|
|
2882
|
+
if (mapped === void 0) continue;
|
|
2883
|
+
out.push({
|
|
2884
|
+
table: mapped.table.table,
|
|
2885
|
+
scope: name,
|
|
2886
|
+
id: mapped.column,
|
|
2887
|
+
tenant: mapped.tenantColumn ?? mapped.column,
|
|
2888
|
+
role: mapped.table.role,
|
|
2889
|
+
user: mapped.table.user
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
if (ctx.roles !== void 0) out.push({
|
|
2893
|
+
table: ctx.roles.table,
|
|
2894
|
+
scope: "global",
|
|
2895
|
+
role: ctx.roles.role ?? "role",
|
|
2896
|
+
user: ctx.roles.user ?? "user_id"
|
|
2897
|
+
});
|
|
2898
|
+
for (const extra of ctx.assignments?.tables ?? []) {
|
|
2899
|
+
const scope = resolveScope(ctx.scopes, extra.scope);
|
|
2900
|
+
if (scope === void 0) throw configError(`PermDock CLI: rls.assignments.tables names scope '${extra.scope}', which the policy does not declare`);
|
|
2901
|
+
if (scope !== ctx.scopes[0]?.name && extra.tenant === void 0) throw configError(`PermDock CLI: rls.assignments.tables entry ${extra.table} assigns at ${scope}, below the first scope: name its tenant column`);
|
|
2902
|
+
out.push({
|
|
2903
|
+
table: extra.table,
|
|
2904
|
+
scope,
|
|
2905
|
+
id: extra.id,
|
|
2906
|
+
tenant: extra.tenant ?? extra.id,
|
|
2907
|
+
role: extra.role,
|
|
2908
|
+
...extra.user === void 0 ? {} : { user: extra.user }
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
return out;
|
|
2912
|
+
}
|
|
2913
|
+
/** The tables `rls.assignments.ownRole` refuses a caller's own rows on, by table key. */
|
|
2914
|
+
function ownRoleTables(ctx, tables) {
|
|
2915
|
+
const own = ctx.assignments?.ownRole;
|
|
2916
|
+
if (own === void 0) return /* @__PURE__ */ new Set();
|
|
2917
|
+
if (own === "refuse") return new Set(tables.filter((entry) => entry.user !== void 0).map((entry) => qualifySqlTable(entry.table)));
|
|
2918
|
+
const out = /* @__PURE__ */ new Set();
|
|
2919
|
+
for (const [name, value] of Object.entries(own)) {
|
|
2920
|
+
const entry = tables.find((table) => qualifySqlTable(table.table) === qualifySqlTable(name));
|
|
2921
|
+
if (entry === void 0) throw configError(`PermDock CLI: rls.assignments.ownRole names ${name}, which no assignment trigger guards`);
|
|
2922
|
+
if (value !== "refuse") throw configError(`PermDock CLI: rls.assignments.ownRole.${name} must be 'refuse'`);
|
|
2923
|
+
if (entry.user === void 0) throw configError(`PermDock CLI: rls.assignments.ownRole names ${name}, which has no user column: set its user in rls.assignments.tables`);
|
|
2924
|
+
out.add(qualifySqlTable(entry.table));
|
|
2925
|
+
}
|
|
2926
|
+
return out;
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2929
|
+
* The tables `rls.assignments` guards, schema-qualified: each scope's mapped
|
|
2930
|
+
* membership table, the global-roles table (`rls.roles`, else
|
|
2931
|
+
* `supabase.hook.roles`) and `rls.assignments.tables`. Empty without
|
|
2932
|
+
* `rls.assignments`.
|
|
2933
|
+
*/
|
|
2934
|
+
function guardedTables(config, scopes) {
|
|
2935
|
+
const rls = config.rls;
|
|
2936
|
+
if (rls?.assignments === void 0) return [];
|
|
2937
|
+
const hook = config.supabase?.hook?.roles;
|
|
2938
|
+
const roles = rls.roles ?? (hook === false ? void 0 : hook);
|
|
2939
|
+
const tables = [
|
|
2940
|
+
...scopes.flatMap((scope) => {
|
|
2941
|
+
const mapped = memberForTable({
|
|
2942
|
+
scopes,
|
|
2943
|
+
...rls.memberships === void 0 ? {} : { memberships: rls.memberships }
|
|
2944
|
+
}, scope.name);
|
|
2945
|
+
return mapped === void 0 ? [] : [mapped.table];
|
|
2946
|
+
}),
|
|
2947
|
+
...roles === void 0 ? [] : [roles.table],
|
|
2948
|
+
...(rls.assignments === true ? [] : rls.assignments.tables ?? []).map((entry) => entry.table)
|
|
2949
|
+
];
|
|
2950
|
+
return [...new Set(tables.map(qualifySqlTable))];
|
|
2951
|
+
}
|
|
2952
|
+
function isRoleList(role) {
|
|
2953
|
+
return Array.isArray(role);
|
|
2954
|
+
}
|
|
2955
|
+
/** The role keys a row variable holds, one scalar per role source. */
|
|
2956
|
+
function rowRoles(entry, row) {
|
|
2957
|
+
return (isRoleList(entry.role) ? entry.role : [entry.role]).map((part) => roleColumn(part, qualifySqlTable(entry.table), row, {
|
|
2958
|
+
label: `rls.assignments ${entry.table} role`,
|
|
2959
|
+
prefix: "PermDock CLI"
|
|
2960
|
+
}).lookup);
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* `permdock_can_assign_custom_role`: whether the signed-in caller may assign a
|
|
2964
|
+
* custom role of the tenant at the instance, by the checks the custom-role
|
|
2965
|
+
* write functions run on its stored definition.
|
|
2966
|
+
*/
|
|
2967
|
+
function canAssignCustomSql(ctx, forUser = false) {
|
|
2968
|
+
const tenantType = tenantTypeOf(ctx);
|
|
2969
|
+
const fn = qualified(ctx, forUser ? ASSIGNMENTS.customFor : ASSIGNMENTS.custom);
|
|
2970
|
+
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
2971
|
+
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
2972
|
+
const guard = qualified(ctx, forUser ? CUSTOM_ROLES.guardFor : CUSTOM_ROLES.guard);
|
|
2973
|
+
const userParam = forUser ? "p_user uuid, " : "";
|
|
2974
|
+
const userArg = forUser ? "uuid, " : "";
|
|
2975
|
+
const stored = `${storedTenant(ctx)} and c.scope = p_scope and c.role = p_role and (c.scope_id is null or c.scope_id = p_scope_id)`;
|
|
2976
|
+
const grants = forUser ? `revoke execute on function ${fn}(${userArg}${tenantType}, text, text, text) from public, anon, authenticated;` : `revoke execute on function ${fn}(${tenantType}, text, text, text) from public, anon;
|
|
2977
|
+
grant execute on function ${fn}(${tenantType}, text, text, text) to authenticated;`;
|
|
2978
|
+
return `-- whether ${forUser ? "user p_user" : "the caller"} may assign custom role p_role at instance p_scope_id (a platform role: null tenant, scope global): the custom-role write checks on its stored definition${forUser ? "; no client role may execute it" : ""}
|
|
2979
|
+
create or replace function ${fn}(${userParam}p_tenant ${tenantType}, p_scope text, p_scope_id text, p_role text)
|
|
2980
|
+
returns boolean
|
|
2981
|
+
language plpgsql
|
|
2982
|
+
stable
|
|
2983
|
+
security definer
|
|
2984
|
+
set search_path = ''
|
|
2985
|
+
as $$
|
|
2986
|
+
declare
|
|
2987
|
+
v_scope_id text;
|
|
2988
|
+
v_found boolean := false;
|
|
2989
|
+
begin
|
|
2990
|
+
select true, c.scope_id into v_found, v_scope_id
|
|
2991
|
+
from (
|
|
2992
|
+
select c.scope_id from ${perms} c where ${stored}
|
|
2993
|
+
union all
|
|
2994
|
+
select c.scope_id from ${includes} c where ${stored}
|
|
2995
|
+
) c
|
|
2996
|
+
order by c.scope_id nulls first
|
|
2997
|
+
limit 1;
|
|
2998
|
+
if not coalesce(v_found, false) then
|
|
2999
|
+
return false;
|
|
3000
|
+
end if;
|
|
3001
|
+
perform ${guard}(${forUser ? "p_user, " : ""}p_tenant, p_scope, v_scope_id, p_role);
|
|
3002
|
+
return true;
|
|
3003
|
+
exception
|
|
3004
|
+
when insufficient_privilege or invalid_parameter_value then
|
|
3005
|
+
return false;
|
|
3006
|
+
end;
|
|
3007
|
+
$$;
|
|
3008
|
+
${grants}`;
|
|
3009
|
+
}
|
|
3010
|
+
/**
|
|
3011
|
+
* The assignment triggers (\`rls.assignments\`): before a client role inserts,
|
|
3012
|
+
* updates or deletes a row that assigns a role, every role the old and new
|
|
3013
|
+
* row hold must be one the caller may assign at that instance, by
|
|
3014
|
+
* \`permdock_can_assign\` for a declared role and
|
|
3015
|
+
* \`permdock_can_assign_custom_role\` for a custom one. The functions run as
|
|
3016
|
+
* the invoker, so \`current_user\` tells a client write from a trusted one.
|
|
3017
|
+
*/
|
|
3018
|
+
function assignmentSql(ctx) {
|
|
3019
|
+
const own = ctx.ownership;
|
|
3020
|
+
if (ctx.assignments === void 0 || own === void 0 || own.assigns.length === 0) return "";
|
|
3021
|
+
const canAssign = qualified(ctx, OWNERSHIP.canAssign);
|
|
3022
|
+
const custom = ctx.customRoles !== void 0 && ctx.authorize === "database";
|
|
3023
|
+
const declared = ctx.customRoles?.declared;
|
|
3024
|
+
const tenantType = tenantTypeOf(ctx);
|
|
3025
|
+
const chunks = [];
|
|
3026
|
+
const clients = `current_user::text = any(array[${CLIENT_ROLES.map(quoteLiteral).join(", ")}])`;
|
|
3027
|
+
const tables = assignedTables(ctx);
|
|
3028
|
+
const ownRole = ownRoleTables(ctx, tables);
|
|
3029
|
+
for (const entry of tables) {
|
|
3030
|
+
const table = quoteQualified(entry.table);
|
|
3031
|
+
const suffix = entry.table.replaceAll(/[^a-z0-9_]/gu, "_");
|
|
3032
|
+
const fn = qualified(ctx, quoteIdent(`${ASSIGNMENTS.trigger}_${suffix}`));
|
|
3033
|
+
const ownUser = entry.user !== void 0 && ownRole.has(qualifySqlTable(entry.table)) ? entry.user : void 0;
|
|
3034
|
+
const ownCheck = (row, reason) => ownUser === void 0 ? "" : ` if ${row}.${quoteIdent(ownUser)}::text = (${subjectIdSql(ctx)})::text then
|
|
3035
|
+
raise exception using
|
|
3036
|
+
errcode = '42501',
|
|
3037
|
+
message = 'permdock: the caller may not change their own roles',
|
|
3038
|
+
hint = '${reason}';
|
|
3039
|
+
end if;
|
|
3040
|
+
`;
|
|
3041
|
+
const check = (row) => {
|
|
3042
|
+
const id = entry.id === void 0 ? "null::text" : `${row}.${quoteIdent(entry.id)}::text`;
|
|
3043
|
+
const customFn = qualified(ctx, ASSIGNMENTS.custom);
|
|
3044
|
+
let allowed = `${canAssign}(v_role, ${id})`;
|
|
3045
|
+
if (custom && declared !== void 0) {
|
|
3046
|
+
const platform = `${customFn}(null, 'global', null, v_role)`;
|
|
3047
|
+
allowed = entry.id === void 0 || entry.tenant === void 0 ? `case when v_role = any(${declaredArray(declared)}) then ${canAssign}(v_role, ${id}) else ${platform} end` : `case when v_role = any(${declaredArray(declared)}) then ${canAssign}(v_role, ${id}) when ${id} is null then ${platform} else ${customFn}(${row}.${quoteIdent(entry.tenant)}::${tenantType}, ${quoteLiteral(entry.scope)}, ${id}, v_role) end`;
|
|
3048
|
+
}
|
|
3049
|
+
return ` foreach v_role in array array[${rowRoles(entry, row).join(", ")}]::text[] loop
|
|
3050
|
+
if v_role is not null and not coalesce(${allowed}, false) then
|
|
3051
|
+
raise exception using
|
|
3052
|
+
errcode = '42501',
|
|
3053
|
+
message = 'permdock: the caller may not assign ' || v_role || ' in ' || coalesce(${id}, 'null'),
|
|
3054
|
+
hint = 'not-assignable-by';
|
|
3055
|
+
end if;
|
|
3056
|
+
end loop;`;
|
|
3057
|
+
};
|
|
3058
|
+
const what = entry.id === void 0 ? "global roles" : `${entry.scope} roles`;
|
|
3059
|
+
chunks.push(`-- ${entry.table}: a client role may write only the ${what} it may assign${ownUser === void 0 ? "" : ", and none on its own rows"}
|
|
3060
|
+
create or replace function ${fn}()
|
|
3061
|
+
returns trigger
|
|
3062
|
+
language plpgsql
|
|
3063
|
+
volatile
|
|
3064
|
+
set search_path = ''
|
|
3065
|
+
as $$
|
|
3066
|
+
declare
|
|
3067
|
+
v_role text;
|
|
3068
|
+
begin
|
|
3069
|
+
if not (${clients}) then
|
|
3070
|
+
return coalesce(new, old);
|
|
3071
|
+
end if;
|
|
3072
|
+
if tg_op in ('UPDATE', 'DELETE') then
|
|
3073
|
+
${ownCheck("old", "self-demotion")}${check("old")}
|
|
3074
|
+
end if;
|
|
3075
|
+
if tg_op in ('INSERT', 'UPDATE') then
|
|
3076
|
+
${ownCheck("new", "not-assignable-by")}${check("new")}
|
|
3077
|
+
end if;
|
|
3078
|
+
return coalesce(new, old);
|
|
3079
|
+
end;
|
|
3080
|
+
$$;
|
|
3081
|
+
revoke execute on function ${fn}() from public, anon, authenticated;
|
|
3082
|
+
drop trigger if exists "permdock_assignment" on ${table};
|
|
3083
|
+
create trigger "permdock_assignment"
|
|
3084
|
+
before insert or update or delete on ${table}
|
|
3085
|
+
for each row execute function ${fn}();`);
|
|
3086
|
+
}
|
|
3087
|
+
return `${chunks.join("\n\n")}\n`;
|
|
3088
|
+
}
|
|
3089
|
+
//#endregion
|
|
3090
|
+
//#region src/cli/rls/ownership-triggers.ts
|
|
3091
|
+
function objectName(prefix, scope) {
|
|
3092
|
+
return quoteIdent(`${prefix}_${scope}`);
|
|
3093
|
+
}
|
|
3094
|
+
/** Live holders of `roleExpr` in instance `idExpr`, with the same kind and expiry rules as the helpers. */
|
|
3095
|
+
function holdersSql(ctx, table, column, idExpr, role, into) {
|
|
3096
|
+
const roleExpr = typeof role === "string" ? quoteLiteral(role) : role.expr;
|
|
3097
|
+
const held = memberRoleOf(table, "m", " ");
|
|
3098
|
+
const filters = [`m.${quoteIdent(column)} = ${idExpr}`, `${held.sql} = ${roleExpr}`];
|
|
3099
|
+
if (table.expiresAt !== void 0) {
|
|
3100
|
+
const expires = memberColumn(table.expiresAt);
|
|
3101
|
+
filters.push(`(${expires} is null or ${expires} > now())`);
|
|
3102
|
+
}
|
|
3103
|
+
const disabled = disabledColumn(table);
|
|
3104
|
+
if (disabled !== void 0) filters.push(`${disabled} is null`);
|
|
3105
|
+
const via = memberVia(table);
|
|
3106
|
+
const kind = typeof role === "string" ? roleKindSql(ctx, role, via) : kindFilterSql(ctx, roleExpr, via);
|
|
3107
|
+
if (kind !== void 0) filters.push(kind);
|
|
3108
|
+
return `select count(distinct m.${quoteIdent(table.user)}) into ${into}
|
|
3109
|
+
from ${quoteQualified(table.table)} m${held.join}
|
|
3110
|
+
where ${filters.join("\n and ")}`;
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* `min` and `max` as a deferred constraint trigger: checked at commit, so a
|
|
3114
|
+
* transfer (demote one holder, promote another) passes in one transaction. An
|
|
3115
|
+
* instance with no memberships left is being torn down and skips `min`.
|
|
3116
|
+
*/
|
|
3117
|
+
function holdersTriggerSql(ctx, scope, table, column, counted) {
|
|
3118
|
+
const fn = qualified(ctx, `${OWNERSHIP.holders}_${scope}`);
|
|
3119
|
+
const col = quoteIdent(column);
|
|
3120
|
+
const checks = counted.map((rule) => {
|
|
3121
|
+
const lines = [` select count(*) into v_total from ${quoteQualified(table.table)} m where m.${col} = v_key;`, ` ${holdersSql(ctx, table, column, "v_key", rule.role, "v_count")};`];
|
|
3122
|
+
if (rule.min > 0) lines.push(` if v_total > 0 and v_count < ${String(rule.min)} then
|
|
3123
|
+
raise exception using
|
|
3124
|
+
errcode = '23514',
|
|
3125
|
+
message = ${quoteLiteral(`permdock: ${scope} `)} || v_id || ${quoteLiteral(` keeps at least ${String(rule.min)} ${rule.role}`)},
|
|
3126
|
+
hint = 'last-holder';
|
|
3127
|
+
end if;`);
|
|
3128
|
+
if (rule.max !== void 0) lines.push(` if v_count > ${String(rule.max)} then
|
|
3129
|
+
raise exception using
|
|
3130
|
+
errcode = '23514',
|
|
3131
|
+
message = ${quoteLiteral(`permdock: ${scope} `)} || v_id || ${quoteLiteral(` has at most ${String(rule.max)} ${rule.role}`)},
|
|
3132
|
+
hint = 'max-holders';
|
|
3133
|
+
end if;`);
|
|
3134
|
+
return lines.join("\n");
|
|
3135
|
+
});
|
|
3136
|
+
return `-- ${scope}: holder counts (min / max), checked at commit
|
|
3137
|
+
create or replace function ${fn}()
|
|
3138
|
+
returns trigger
|
|
3139
|
+
language plpgsql
|
|
3140
|
+
security definer
|
|
3141
|
+
set search_path = ''
|
|
3142
|
+
as $$
|
|
3143
|
+
declare
|
|
3144
|
+
v_ids text[] := '{}';
|
|
3145
|
+
v_id text;
|
|
3146
|
+
v_key ${quoteQualified(table.table)}.${col}%type;
|
|
3147
|
+
v_total bigint;
|
|
3148
|
+
v_count bigint;
|
|
3149
|
+
begin
|
|
3150
|
+
if tg_op in ('UPDATE', 'DELETE') then
|
|
3151
|
+
v_ids := v_ids || old.${col}::text;
|
|
3152
|
+
end if;
|
|
3153
|
+
if tg_op in ('INSERT', 'UPDATE') then
|
|
3154
|
+
v_ids := v_ids || new.${col}::text;
|
|
3155
|
+
end if;
|
|
3156
|
+
foreach v_id in array v_ids loop
|
|
3157
|
+
continue when v_id is null;
|
|
3158
|
+
v_key := v_id;
|
|
3159
|
+
${checks.join("\n")}
|
|
3160
|
+
end loop;
|
|
3161
|
+
return null;
|
|
3162
|
+
end;
|
|
3163
|
+
$$;
|
|
3164
|
+
drop trigger if exists ${objectName(OWNERSHIP.holders, scope)} on ${quoteQualified(table.table)};
|
|
3165
|
+
create constraint trigger ${objectName(OWNERSHIP.holders, scope)}
|
|
3166
|
+
after insert or update or delete on ${quoteQualified(table.table)}
|
|
3167
|
+
deferrable initially deferred
|
|
3168
|
+
for each row execute function ${fn}();`;
|
|
3169
|
+
}
|
|
3170
|
+
/**
|
|
3171
|
+
* `transferOnly` as statement triggers over transition tables: a statement
|
|
3172
|
+
* that changes a role's holder count in an instance that had holders before
|
|
3173
|
+
* and still has some after is refused. Creating the first holder and removing
|
|
3174
|
+
* the last (which `min` guards) are not transfers.
|
|
3175
|
+
*/
|
|
3176
|
+
function transferTriggerSql(ctx, scope, table, column, roles) {
|
|
3177
|
+
const fn = qualified(ctx, `${OWNERSHIP.transferOnly}_${scope}`);
|
|
3178
|
+
const col = quoteIdent(column);
|
|
3179
|
+
const list = `array[${roles.map(quoteLiteral).join(", ")}]::text[]`;
|
|
3180
|
+
const rows = (alias, source, delta) => {
|
|
3181
|
+
const role = memberRoleOf(table, alias);
|
|
3182
|
+
return `select ${alias}.${col} as id, ${role.sql} as role, ${delta} as delta from ${source} ${alias}${role.join} where ${role.sql} = any(${list})`;
|
|
3183
|
+
};
|
|
3184
|
+
const loop = (source) => ` for v_change in
|
|
3185
|
+
select c.id, c.role, sum(c.delta) as delta
|
|
3186
|
+
from (${source}) c
|
|
3187
|
+
group by c.id, c.role
|
|
3188
|
+
loop
|
|
3189
|
+
continue when v_change.delta = 0 or v_change.id is null;
|
|
3190
|
+
${holdersSql(ctx, table, column, "v_change.id", { expr: "v_change.role" }, "v_after")};
|
|
3191
|
+
if v_after > 0 and v_after - v_change.delta > 0 then
|
|
3192
|
+
raise exception using
|
|
3193
|
+
errcode = '23514',
|
|
3194
|
+
message = 'permdock: ' || v_change.role || ${quoteLiteral(` in ${scope} `)} || v_change.id || ' moves only by transfer',
|
|
3195
|
+
hint = 'transfer-only';
|
|
3196
|
+
end if;
|
|
3197
|
+
end loop;`;
|
|
3198
|
+
const inserted = rows("n", "permdock_new", "1");
|
|
3199
|
+
const deleted = rows("o", "permdock_old", "-1");
|
|
3200
|
+
const target = quoteQualified(table.table);
|
|
3201
|
+
const trigger = (op) => {
|
|
3202
|
+
const name = quoteIdent(`${OWNERSHIP.transferOnly}_${scope}_${op}`);
|
|
3203
|
+
return `drop trigger if exists ${name} on ${target};
|
|
3204
|
+
create trigger ${name}
|
|
3205
|
+
after ${op} on ${target}
|
|
3206
|
+
referencing ${op === "insert" ? "new table as permdock_new" : op === "delete" ? "old table as permdock_old" : "old table as permdock_old new table as permdock_new"}
|
|
3207
|
+
for each statement execute function ${fn}();`;
|
|
3208
|
+
};
|
|
3209
|
+
return `-- ${scope}: transfer-only roles (${roles.join(", ")}) keep their holder count per statement
|
|
3210
|
+
create or replace function ${fn}()
|
|
3211
|
+
returns trigger
|
|
3212
|
+
language plpgsql
|
|
3213
|
+
security definer
|
|
3214
|
+
set search_path = ''
|
|
3215
|
+
as $$
|
|
3216
|
+
declare
|
|
3217
|
+
v_change record;
|
|
3218
|
+
v_after bigint;
|
|
3219
|
+
begin
|
|
3220
|
+
if tg_op = 'INSERT' then
|
|
3221
|
+
${loop(inserted)}
|
|
3222
|
+
elsif tg_op = 'DELETE' then
|
|
3223
|
+
${loop(deleted)}
|
|
3224
|
+
else
|
|
3225
|
+
${loop(`${inserted} union all ${deleted}`)}
|
|
3226
|
+
end if;
|
|
3227
|
+
return null;
|
|
3228
|
+
end;
|
|
3229
|
+
$$;
|
|
3230
|
+
${trigger("insert")}
|
|
3231
|
+
${trigger("update")}
|
|
3232
|
+
${trigger("delete")}`;
|
|
3233
|
+
}
|
|
3234
|
+
//#endregion
|
|
3235
|
+
//#region src/cli/rls/ownership-sources.ts
|
|
3236
|
+
/**
|
|
3237
|
+
* The membership sources holding scope `name`: `undefined` when there are
|
|
3238
|
+
* none, `'not-tables'` when one of them carries no table to put a trigger on.
|
|
3239
|
+
*/
|
|
3240
|
+
function holderSources(ctx, name) {
|
|
3241
|
+
const sources = memberForSources(ctx, name);
|
|
3242
|
+
if (sources.length === 0) return;
|
|
3243
|
+
const held = [];
|
|
3244
|
+
for (const source of sources) {
|
|
3245
|
+
if (source.sql.holders === void 0) return "not-tables";
|
|
3246
|
+
held.push({
|
|
3247
|
+
table: source.sql.table,
|
|
3248
|
+
holders: source.sql.holders
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
return held;
|
|
3252
|
+
}
|
|
3253
|
+
function indent(sql, by) {
|
|
3254
|
+
return sql.replaceAll(/^/gmu, by);
|
|
3255
|
+
}
|
|
3256
|
+
/** Every source's rows of one instance, each compared with its own typed `v_key_<n>`. */
|
|
3257
|
+
function sourceRowsOf(scope, sources) {
|
|
3258
|
+
return sources.map((source, index) => indent(source.holders.rows(scope, { id: `v_key_${String(index)}` }), " ")).join("\n union all\n");
|
|
3259
|
+
}
|
|
3260
|
+
function sourceKeys(sources) {
|
|
3261
|
+
return {
|
|
3262
|
+
declare: sources.map((source, index) => ` v_key_${String(index)} ${source.holders.idType};`).join("\n"),
|
|
3263
|
+
assign: (value, by) => sources.map((_, index) => `${by}v_key_${String(index)} := ${value};`).join("\n")
|
|
3264
|
+
};
|
|
3265
|
+
}
|
|
3266
|
+
function regclassOf(table) {
|
|
3267
|
+
return `${quoteLiteral(quoteQualified(table))}::regclass`;
|
|
3268
|
+
}
|
|
3269
|
+
function sourceTables(sources) {
|
|
3270
|
+
return [...new Set(sources.map((source) => quoteQualified(source.table)))];
|
|
3271
|
+
}
|
|
3272
|
+
/** `min` and `max` over membership sources: the same commit-time check as on a mapped table, counted over every source of the scope. */
|
|
3273
|
+
function sourceHoldersTriggerSql(ctx, scope, sources, counted) {
|
|
3274
|
+
const fn = qualified(ctx, `${OWNERSHIP.holders}_${scope}`);
|
|
3275
|
+
const keys = sourceKeys(sources);
|
|
3276
|
+
const rows = sourceRowsOf(scope, sources);
|
|
3277
|
+
const collect = sources.map((source) => ` if tg_relid = ${regclassOf(source.table)} then
|
|
3278
|
+
if tg_op in ('UPDATE', 'DELETE') then
|
|
3279
|
+
v_ids := v_ids || ${source.holders.id(scope, "old")};
|
|
3280
|
+
end if;
|
|
3281
|
+
if tg_op in ('INSERT', 'UPDATE') then
|
|
3282
|
+
v_ids := v_ids || ${source.holders.id(scope, "new")};
|
|
3283
|
+
end if;
|
|
3284
|
+
end if;`);
|
|
3285
|
+
const checks = counted.map((rule) => {
|
|
3286
|
+
const filters = ["h.live", `h.role = ${quoteLiteral(rule.role)}`];
|
|
3287
|
+
const kind = roleKindSql(ctx, rule.role, "h.via");
|
|
3288
|
+
if (kind !== void 0) filters.push(kind);
|
|
3289
|
+
const lines = [` select count(distinct h.user_id) into v_count
|
|
3290
|
+
from (
|
|
3291
|
+
${rows}
|
|
3292
|
+
) h
|
|
3293
|
+
where ${filters.join("\n and ")};`];
|
|
3294
|
+
if (rule.min > 0) lines.push(` if v_total > 0 and v_count < ${String(rule.min)} then
|
|
3295
|
+
raise exception using
|
|
3296
|
+
errcode = '23514',
|
|
3297
|
+
message = ${quoteLiteral(`permdock: ${scope} `)} || v_id || ${quoteLiteral(` keeps at least ${String(rule.min)} ${rule.role}`)},
|
|
3298
|
+
hint = 'last-holder';
|
|
3299
|
+
end if;`);
|
|
3300
|
+
if (rule.max !== void 0) lines.push(` if v_count > ${String(rule.max)} then
|
|
3301
|
+
raise exception using
|
|
3302
|
+
errcode = '23514',
|
|
3303
|
+
message = ${quoteLiteral(`permdock: ${scope} `)} || v_id || ${quoteLiteral(` has at most ${String(rule.max)} ${rule.role}`)},
|
|
3304
|
+
hint = 'max-holders';
|
|
3305
|
+
end if;`);
|
|
3306
|
+
return lines.join("\n");
|
|
3307
|
+
});
|
|
3308
|
+
const triggers = sourceTables(sources).map((table) => `drop trigger if exists ${objectName(OWNERSHIP.holders, scope)} on ${table};
|
|
3309
|
+
create constraint trigger ${objectName(OWNERSHIP.holders, scope)}
|
|
3310
|
+
after insert or update or delete on ${table}
|
|
3311
|
+
deferrable initially deferred
|
|
3312
|
+
for each row execute function ${fn}();`);
|
|
3313
|
+
return `-- ${scope}: holder counts (min / max) over the membership sources, checked at commit
|
|
3314
|
+
create or replace function ${fn}()
|
|
3315
|
+
returns trigger
|
|
3316
|
+
language plpgsql
|
|
3317
|
+
security definer
|
|
3318
|
+
set search_path = ''
|
|
3319
|
+
as $$
|
|
3320
|
+
declare
|
|
3321
|
+
v_ids text[] := '{}';
|
|
3322
|
+
v_id text;
|
|
3323
|
+
${keys.declare}
|
|
3324
|
+
v_total bigint;
|
|
3325
|
+
v_count bigint;
|
|
3326
|
+
begin
|
|
3327
|
+
${collect.join("\n")}
|
|
3328
|
+
foreach v_id in array v_ids loop
|
|
3329
|
+
continue when v_id is null;
|
|
3330
|
+
${keys.assign("v_id", " ")}
|
|
3331
|
+
select count(*) into v_total
|
|
3332
|
+
from (
|
|
3333
|
+
${rows}
|
|
3334
|
+
) h;
|
|
3335
|
+
${checks.join("\n")}
|
|
3336
|
+
end loop;
|
|
3337
|
+
return null;
|
|
3338
|
+
end;
|
|
3339
|
+
$$;
|
|
3340
|
+
${triggers.join("\n")}`;
|
|
3341
|
+
}
|
|
3342
|
+
function collectChangesSql(changed) {
|
|
3343
|
+
return ` v_changes := v_changes || coalesce((select jsonb_agg(to_jsonb(c)) from (${changed}) c), '[]'::jsonb);`;
|
|
3344
|
+
}
|
|
3345
|
+
/** `transferOnly` over membership sources: each source table's statement triggers diff its transition tables, and the count after reads every source. */
|
|
3346
|
+
function sourceTransferTriggerSql(ctx, scope, sources, roles) {
|
|
3347
|
+
const fn = qualified(ctx, `${OWNERSHIP.transferOnly}_${scope}`);
|
|
3348
|
+
const keys = sourceKeys(sources);
|
|
3349
|
+
const list = `array[${roles.map(quoteLiteral).join(", ")}]::text[]`;
|
|
3350
|
+
const kind = kindFilterSql(ctx, "h.role", "h.via");
|
|
3351
|
+
const changes = (source, from, delta) => `select h.id, h.role, ${delta} as delta from (${source.holders.rows(scope, { from }).replaceAll("\n", " ")}) h where h.role = any(${list})`;
|
|
3352
|
+
const branches = sources.map((source) => ` if tg_relid = ${regclassOf(source.table)} then
|
|
3353
|
+
if tg_op in ('INSERT', 'UPDATE') then
|
|
3354
|
+
${collectChangesSql(changes(source, "permdock_new", "1"))}
|
|
3355
|
+
end if;
|
|
3356
|
+
if tg_op in ('UPDATE', 'DELETE') then
|
|
3357
|
+
${collectChangesSql(changes(source, "permdock_old", "-1"))}
|
|
3358
|
+
end if;
|
|
3359
|
+
end if;`);
|
|
3360
|
+
const triggers = sourceTables(sources).flatMap((table) => [
|
|
3361
|
+
"insert",
|
|
3362
|
+
"update",
|
|
3363
|
+
"delete"
|
|
3364
|
+
].map((op) => {
|
|
3365
|
+
const name = quoteIdent(`${OWNERSHIP.transferOnly}_${scope}_${op}`);
|
|
3366
|
+
return `drop trigger if exists ${name} on ${table};
|
|
3367
|
+
create trigger ${name}
|
|
3368
|
+
after ${op} on ${table}
|
|
3369
|
+
referencing ${op === "insert" ? "new table as permdock_new" : op === "delete" ? "old table as permdock_old" : "old table as permdock_old new table as permdock_new"}
|
|
3370
|
+
for each statement execute function ${fn}();`;
|
|
3371
|
+
}));
|
|
3372
|
+
return `-- ${scope}: transfer-only roles (${roles.join(", ")}) keep their holder count per statement, over the membership sources
|
|
3373
|
+
create or replace function ${fn}()
|
|
3374
|
+
returns trigger
|
|
3375
|
+
language plpgsql
|
|
3376
|
+
security definer
|
|
3377
|
+
set search_path = ''
|
|
3378
|
+
as $$
|
|
3379
|
+
declare
|
|
3380
|
+
v_changes jsonb := '[]'::jsonb;
|
|
3381
|
+
v_change record;
|
|
3382
|
+
v_after bigint;
|
|
3383
|
+
${keys.declare}
|
|
3384
|
+
begin
|
|
3385
|
+
${branches.join("\n")}
|
|
3386
|
+
for v_change in
|
|
3387
|
+
select x.id, x.role, sum(x.delta) as delta
|
|
3388
|
+
from jsonb_to_recordset(v_changes) x(id text, role text, delta bigint)
|
|
3389
|
+
group by x.id, x.role
|
|
3390
|
+
loop
|
|
3391
|
+
continue when v_change.delta = 0 or v_change.id is null;
|
|
3392
|
+
${keys.assign("v_change.id", " ")}
|
|
3393
|
+
select count(distinct h.user_id) into v_after
|
|
3394
|
+
from (
|
|
3395
|
+
${sourceRowsOf(scope, sources)}
|
|
3396
|
+
) h
|
|
3397
|
+
where h.live
|
|
3398
|
+
and h.role = v_change.role${kind === void 0 ? "" : `\n and ${kind}`};
|
|
3399
|
+
if v_after > 0 and v_after - v_change.delta > 0 then
|
|
3400
|
+
raise exception using
|
|
3401
|
+
errcode = '23514',
|
|
3402
|
+
message = 'permdock: ' || v_change.role || ${quoteLiteral(` in ${scope} `)} || v_change.id || ' moves only by transfer',
|
|
3403
|
+
hint = 'transfer-only';
|
|
3404
|
+
end if;
|
|
3405
|
+
end loop;
|
|
3406
|
+
return null;
|
|
3407
|
+
end;
|
|
3408
|
+
$$;
|
|
3409
|
+
${triggers.join("\n")}`;
|
|
3410
|
+
}
|
|
3411
|
+
function pairsSql(pairs) {
|
|
3412
|
+
return `values ${pairs.map((pair) => `(${quoteLiteral(pair.assigner)}, ${quoteLiteral(pair.role)})`).join(", ")}`;
|
|
3413
|
+
}
|
|
3414
|
+
/**
|
|
3415
|
+
* Membership rows of the membership sources for scope `name`, each source's
|
|
3416
|
+
* `select` comparing its user column with a variable `user` names.
|
|
3417
|
+
*/
|
|
3418
|
+
function sourceRowsSql(sources, user) {
|
|
3419
|
+
const keep = sources.some((source) => source.sql.keeps);
|
|
3420
|
+
return sources.map((source) => source.sql.select(user(source), keep).replaceAll(/^/gmu, " ")).join("\n union all\n");
|
|
3421
|
+
}
|
|
3422
|
+
//#endregion
|
|
3423
|
+
//#region src/cli/rls/can-assign.ts
|
|
3424
|
+
/** Whether the signed-in user (or `p_user` in the `_for` form) holds, in instance `p_scope_id`, a role whose `assigns` lists `p_role`. */
|
|
3425
|
+
function canAssignSql(base, own, forUser = false) {
|
|
3426
|
+
const ctx = forUser ? {
|
|
3427
|
+
...base,
|
|
3428
|
+
subjectId: "p_user",
|
|
3429
|
+
tenants: "all"
|
|
3430
|
+
} : base;
|
|
3431
|
+
const fn = qualified(ctx, forUser ? OWNERSHIP.canAssignFor : OWNERSHIP.canAssign);
|
|
3432
|
+
const parts = [];
|
|
3433
|
+
const used = [];
|
|
3434
|
+
const userOf = (source) => {
|
|
3435
|
+
const index = used.includes(source) ? used.indexOf(source) : used.push(source) - 1;
|
|
3436
|
+
return `v_user_${String(index)}`;
|
|
3437
|
+
};
|
|
3438
|
+
const global = own.assigns.filter((pair) => pair.scope === "global");
|
|
3439
|
+
for (const [instance, pairs] of [["p_scope_id is not null", global.filter((pair) => pair.at !== "global")], ["p_scope_id is null", global.filter((pair) => pair.at === "global")]]) {
|
|
3440
|
+
if (pairs.length === 0) continue;
|
|
3441
|
+
if (ctx.authorize === "database") {
|
|
3442
|
+
const ur = globalRoleRows(ctx);
|
|
3443
|
+
const kind = globalKindFilterSql(ctx, ur.roleSql);
|
|
3444
|
+
parts.push(`${instance} and exists (
|
|
3445
|
+
select 1 from ${ur.from}
|
|
3446
|
+
where ${ur.userSql} = ${subjectIdSql(ctx)}
|
|
3447
|
+
and (${ur.roleSql}, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}
|
|
3448
|
+
)`);
|
|
3449
|
+
} else {
|
|
3450
|
+
const kind = globalKindFilterSql(ctx, "r.role");
|
|
3451
|
+
parts.push(`${instance} and exists (
|
|
3452
|
+
select 1 from ${roleRows(ctx)}
|
|
3453
|
+
where (r.role, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}
|
|
3454
|
+
)`);
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
for (const { name } of ctx.scopes) {
|
|
3458
|
+
const pairs = own.assigns.filter((pair) => pair.scope === name);
|
|
3459
|
+
if (pairs.length === 0) continue;
|
|
3460
|
+
const sources = scopeSources(ctx, name);
|
|
3461
|
+
if (sources.length > 0) {
|
|
3462
|
+
const kind = kindFilterSql(ctx, "r.role", "ms.via");
|
|
3463
|
+
parts.push(`exists (
|
|
3464
|
+
select 1
|
|
3465
|
+
from (
|
|
3466
|
+
${sourceRowsSql(sources, userOf)}
|
|
3467
|
+
) ms
|
|
3468
|
+
cross join lateral jsonb_array_elements_text(
|
|
3469
|
+
case jsonb_typeof(ms.roles) when 'array' then ms.roles else '[]'::jsonb end
|
|
3470
|
+
) r(role)
|
|
3471
|
+
where ms.scope = ${quoteLiteral(name)}
|
|
3472
|
+
and ms.id = p_scope_id
|
|
3473
|
+
and (r.role, p_role) in (${pairsSql(pairs)})${kind === void 0 ? "" : `\n and ${kind}`}${sourceFilters(ctx, name, void 0, void 0, sourcesKeep(sources)).replaceAll("\n and ", "\n and ")}
|
|
3474
|
+
)`);
|
|
3475
|
+
continue;
|
|
3476
|
+
}
|
|
3477
|
+
if (ctx.authorize === "database") {
|
|
3478
|
+
const mapped = scopeTable(ctx, name);
|
|
3479
|
+
if (mapped === void 0) continue;
|
|
3480
|
+
const { table, column } = mapped;
|
|
3481
|
+
const role = memberRoleOf(table, "m", " ");
|
|
3482
|
+
const filters = [
|
|
3483
|
+
`${memberColumn(table.user)} = ${subjectIdSql(ctx)}`,
|
|
3484
|
+
`${memberColumn(column)}::text = p_scope_id`,
|
|
3485
|
+
`(${role.sql}, p_role) in (${pairsSql(pairs)})`
|
|
3486
|
+
];
|
|
3487
|
+
if (table.expiresAt !== void 0) {
|
|
3488
|
+
const expires = memberColumn(table.expiresAt);
|
|
3489
|
+
filters.push(`(${expires} is null or ${expires} > now())`);
|
|
3490
|
+
}
|
|
3491
|
+
const disabled = disabledColumn(table);
|
|
3492
|
+
if (disabled !== void 0) filters.push(`${disabled} is null`);
|
|
3493
|
+
const kind = kindFilterSql(ctx, role.sql, memberVia(table));
|
|
3494
|
+
if (kind !== void 0) filters.push(kind);
|
|
3495
|
+
parts.push(`exists (
|
|
3496
|
+
select 1 from ${quoteQualified(table.table)} m${role.join}
|
|
3497
|
+
where ${filters.join("\n and ")}
|
|
3498
|
+
)`);
|
|
3499
|
+
} else {
|
|
3500
|
+
const kind = kindFilterSql(ctx, "r.role", "m ->> 'via'");
|
|
3501
|
+
parts.push(`exists (
|
|
3502
|
+
select 1 from ${membershipRows(ctx)}
|
|
3503
|
+
where m ->> 'scope' = ${quoteLiteral(name)}
|
|
3504
|
+
and m ->> 'id' = p_scope_id
|
|
3505
|
+
and (r.role, p_role) in (${pairsSql(pairs)})
|
|
3506
|
+
and case jsonb_typeof(m -> 'expires_at')
|
|
3507
|
+
when 'number' then (m ->> 'expires_at')::numeric > extract(epoch from now())
|
|
3508
|
+
else true
|
|
3509
|
+
end${kind === void 0 ? "" : `\n and ${kind}`}${claimKeptLines(ctx, " ").map((line) => `\n${line}`).join("")}
|
|
3510
|
+
)`);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
const body = parts.length === 0 ? "false" : parts.join("\n or ");
|
|
3514
|
+
const language = used.length === 0 ? `language sql
|
|
3515
|
+
stable
|
|
3516
|
+
security definer
|
|
3517
|
+
set search_path = ''
|
|
3518
|
+
as $$
|
|
3519
|
+
select ${signedIn(ctx)} and (
|
|
3520
|
+
${body}
|
|
3521
|
+
)
|
|
3522
|
+
$$;` : `language plpgsql
|
|
3523
|
+
stable
|
|
3524
|
+
security definer
|
|
3525
|
+
set search_path = ''
|
|
3526
|
+
as $$
|
|
3527
|
+
declare
|
|
3528
|
+
${used.map((source, index) => ` v_user_${String(index)} ${source.sql.userType} := ${subjectIdSql(ctx)};`).join("\n")}
|
|
3529
|
+
begin
|
|
3530
|
+
return ${signedIn(ctx)} and (
|
|
3531
|
+
${body}
|
|
3532
|
+
);
|
|
3533
|
+
end;
|
|
3534
|
+
$$;`;
|
|
3535
|
+
if (forUser) {
|
|
3536
|
+
const user = ctx.dialect === "supabase" ? "uuid" : "text";
|
|
3537
|
+
return `-- who may assign, for a user the caller names: trusted SQL acting later for a stored user; no client role may execute it
|
|
3538
|
+
create or replace function ${fn}(p_user ${user}, p_role text, p_scope_id text)
|
|
3539
|
+
returns boolean
|
|
3540
|
+
${language}
|
|
3541
|
+
revoke execute on function ${fn}(${user}, text, text) from public, anon, authenticated;`;
|
|
3542
|
+
}
|
|
3543
|
+
return `-- who may assign: a held role whose assigns lists p_role, in instance p_scope_id (its scope or an ancestor's); a null p_scope_id assigns a global role, which only global assigners may
|
|
3544
|
+
create or replace function ${fn}(p_role text, p_scope_id text)
|
|
3545
|
+
returns boolean
|
|
3546
|
+
${language}
|
|
3547
|
+
revoke execute on function ${fn}(text, text) from public, anon;
|
|
3548
|
+
grant execute on function ${fn}(text, text) to authenticated;`;
|
|
3549
|
+
}
|
|
3550
|
+
/** Whether the helpers keep custom roles in tables, so assigning one is checked against its stored definition. */
|
|
3551
|
+
function assignsCustomRoles(ctx) {
|
|
3552
|
+
return ctx.customRoles !== void 0 && ctx.authorize === "database";
|
|
3553
|
+
}
|
|
3554
|
+
/** Whether `permdock_can_assign_any_for` is written: every function it calls has a `_for` form. */
|
|
3555
|
+
function hasCanAssignAnyFor(ctx) {
|
|
3556
|
+
return ctx.authorize === "database" && (!assignsCustomRoles(ctx) || hasCustomRoleChecksFor(ctx));
|
|
3557
|
+
}
|
|
3558
|
+
/**
|
|
3559
|
+
* `permdock_can_assign_any`: one check for a declared or a custom role, so
|
|
3560
|
+
* SQL that assigns roles calls one function: `permdock_can_assign` for a
|
|
3561
|
+
* declared role, `permdock_can_assign_custom_role` for a custom one. `p_scope`
|
|
3562
|
+
* `'global'` (no tenant, no instance) assigns a platform role.
|
|
3563
|
+
*/
|
|
3564
|
+
function canAssignAnySql(ctx, custom, forUser = false) {
|
|
3565
|
+
const tenantType = tenantTypeOf(ctx);
|
|
3566
|
+
const fn = qualified(ctx, forUser ? OWNERSHIP.canAssignAnyFor : OWNERSHIP.canAssignAny);
|
|
3567
|
+
const declaredFn = qualified(ctx, forUser ? OWNERSHIP.canAssignFor : OWNERSHIP.canAssign);
|
|
3568
|
+
const customFn = qualified(ctx, forUser ? ASSIGNMENTS.customFor : ASSIGNMENTS.custom);
|
|
3569
|
+
const userType = ctx.dialect === "supabase" ? "uuid" : "text";
|
|
3570
|
+
const userParam = forUser ? `p_user ${userType}, ` : "";
|
|
3571
|
+
const userArg = forUser ? `${userType}, ` : "";
|
|
3572
|
+
const user = forUser ? "p_user, " : "";
|
|
3573
|
+
const declared = `${declaredFn}(${user}p_role, case when p_scope = 'global' then null else p_scope_id end)`;
|
|
3574
|
+
const body = custom && ctx.customRoles !== void 0 ? `case
|
|
3575
|
+
when p_role = any(${declaredArray(ctx.customRoles.declared)}) then ${declared}
|
|
3576
|
+
when p_scope = 'global' then ${customFn}(${user}null, 'global', null, p_role)
|
|
3577
|
+
else ${customFn}(${user}p_tenant, p_scope, p_scope_id, p_role)
|
|
3578
|
+
end` : declared;
|
|
3579
|
+
const grants = forUser ? `revoke execute on function ${fn}(${userArg}text, ${tenantType}, text, text) from public, anon, authenticated;` : `revoke execute on function ${fn}(text, ${tenantType}, text, text) from public, anon;
|
|
3580
|
+
grant execute on function ${fn}(text, ${tenantType}, text, text) to authenticated;`;
|
|
3581
|
+
return `-- whether ${forUser ? "user p_user" : "the caller"} may assign p_role at instance p_scope_id of p_scope in tenant p_tenant, declared or custom (p_scope 'global' for a platform role)${forUser ? "; no client role may execute it" : ""}
|
|
3582
|
+
create or replace function ${fn}(${userParam}p_role text, p_tenant ${tenantType}, p_scope text, p_scope_id text)
|
|
3583
|
+
returns boolean
|
|
3584
|
+
language sql
|
|
3585
|
+
stable
|
|
3586
|
+
security definer
|
|
3587
|
+
set search_path = ''
|
|
3588
|
+
as $$
|
|
3589
|
+
select coalesce(${body}, false)
|
|
3590
|
+
$$;
|
|
3591
|
+
${grants}`;
|
|
3592
|
+
}
|
|
3593
|
+
//#endregion
|
|
3594
|
+
//#region src/cli/rls/ownership.ts
|
|
3595
|
+
function roleScope(policy, scopes, name) {
|
|
3596
|
+
const on = policy.rolesByName.get(name)?.on ?? findRole(policy.vocabulary.roles, name)?.on;
|
|
3597
|
+
return typeof on === "string" ? resolveScope(scopes, on) : void 0;
|
|
3598
|
+
}
|
|
3599
|
+
function isGlobalRole(policy, name) {
|
|
3600
|
+
const binding = policy.rolesByName.get(name);
|
|
3601
|
+
if (binding !== void 0) return binding.on === void 0;
|
|
3602
|
+
const leaf = findRole(policy.vocabulary.roles, name);
|
|
3603
|
+
return leaf !== void 0 && leaf.on === void 0;
|
|
3604
|
+
}
|
|
3605
|
+
/** The rules `rls generate` enforces; `undefined` when no role declares one. */
|
|
3606
|
+
function ownershipRules(policy, scopes) {
|
|
3607
|
+
const kinds = {};
|
|
3608
|
+
const assigns = [];
|
|
3609
|
+
const counted = [];
|
|
3610
|
+
for (const binding of policy.roles) {
|
|
3611
|
+
const scope = roleScope(policy, scopes, binding.name);
|
|
3612
|
+
if (binding.for !== void 0) kinds[binding.name] = binding.for;
|
|
3613
|
+
const assigner = scope ?? (binding.on === void 0 ? "global" : void 0);
|
|
3614
|
+
for (const target of binding.assigns ?? []) {
|
|
3615
|
+
const at = roleScope(policy, scopes, target);
|
|
3616
|
+
if (assigner !== void 0 && at !== void 0 && (assigner === "global" || scopeChain(scopes, at).includes(assigner))) assigns.push({
|
|
3617
|
+
assigner: binding.name,
|
|
3618
|
+
scope: assigner,
|
|
3619
|
+
role: target,
|
|
3620
|
+
at
|
|
3621
|
+
});
|
|
3622
|
+
else if (assigner === "global" && isGlobalRole(policy, target)) assigns.push({
|
|
3623
|
+
assigner: binding.name,
|
|
3624
|
+
scope: "global",
|
|
3625
|
+
role: target,
|
|
3626
|
+
at: "global"
|
|
3627
|
+
});
|
|
3628
|
+
}
|
|
3629
|
+
const min = binding.min ?? 0;
|
|
3630
|
+
const transferOnly = binding.transferOnly === true;
|
|
3631
|
+
if (scope !== void 0 && (min > 0 || binding.max !== void 0 || transferOnly)) counted.push(binding.max === void 0 ? {
|
|
3632
|
+
role: binding.name,
|
|
3633
|
+
scope,
|
|
3634
|
+
min,
|
|
3635
|
+
transferOnly
|
|
3636
|
+
} : {
|
|
3637
|
+
role: binding.name,
|
|
3638
|
+
scope,
|
|
3639
|
+
min,
|
|
3640
|
+
max: binding.max,
|
|
3641
|
+
transferOnly
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
if (Object.keys(kinds).length === 0 && assigns.length === 0 && counted.length === 0) return;
|
|
3645
|
+
return {
|
|
3646
|
+
kinds,
|
|
3647
|
+
assigns,
|
|
3648
|
+
counted
|
|
3649
|
+
};
|
|
3650
|
+
}
|
|
3651
|
+
/**
|
|
3652
|
+
* The ownership objects: holder-count and transfer-only triggers on each
|
|
3653
|
+
* scope's membership table, and `permdock_can_assign` for the application's
|
|
3654
|
+
* own policies on those tables. Empty when no role declares a rule.
|
|
3655
|
+
*/
|
|
3656
|
+
function ownershipSql(ctx) {
|
|
3657
|
+
const own = ctx.ownership;
|
|
3658
|
+
if (own === void 0) return "";
|
|
3659
|
+
const chunks = [];
|
|
3660
|
+
const skipped = ctx.skipOwnershipTriggers;
|
|
3661
|
+
for (const { name } of ctx.scopes) {
|
|
3662
|
+
const counted = own.counted.filter((rule) => rule.scope === name);
|
|
3663
|
+
if (counted.length === 0) continue;
|
|
3664
|
+
if (skipped === "all" || skipped?.includes(name) === true) {
|
|
3665
|
+
chunks.push(`-- ${name}: rls.ownershipTriggers leaves out the triggers, so min, max and transferOnly are checked only by decideRoleChange`);
|
|
3666
|
+
continue;
|
|
3667
|
+
}
|
|
3668
|
+
const transfer = counted.filter((rule) => rule.transferOnly).map((rule) => rule.role);
|
|
3669
|
+
const mapped = scopeTable(ctx, name);
|
|
3670
|
+
if (mapped === void 0) {
|
|
3671
|
+
const sources = holderSources(ctx, name);
|
|
3672
|
+
if (sources === void 0) chunks.push(`-- ${name}: no memberships table configured, so min, max and transferOnly are checked only by decideRoleChange`);
|
|
3673
|
+
else if (sources === "not-tables") chunks.push(`-- ${name}: a membership source is not a table, so min, max and transferOnly are checked only by decideRoleChange`);
|
|
3674
|
+
else {
|
|
3675
|
+
chunks.push(sourceHoldersTriggerSql(ctx, name, sources, counted));
|
|
3676
|
+
if (transfer.length > 0) chunks.push(sourceTransferTriggerSql(ctx, name, sources, transfer));
|
|
3677
|
+
}
|
|
3678
|
+
continue;
|
|
3679
|
+
}
|
|
3680
|
+
chunks.push(holdersTriggerSql(ctx, name, mapped.table, mapped.column, counted));
|
|
3681
|
+
if (transfer.length > 0) chunks.push(transferTriggerSql(ctx, name, mapped.table, mapped.column, transfer));
|
|
3682
|
+
}
|
|
3683
|
+
if (own.assigns.length > 0) {
|
|
3684
|
+
chunks.push(canAssignSql(ctx, own));
|
|
3685
|
+
if (ctx.authorize === "database") chunks.push(canAssignSql(ctx, own, true));
|
|
3686
|
+
const custom = assignsCustomRoles(ctx);
|
|
3687
|
+
if (custom) {
|
|
3688
|
+
chunks.push(canAssignCustomSql(ctx));
|
|
3689
|
+
if (hasCustomRoleChecksFor(ctx)) chunks.push(canAssignCustomSql(ctx, true));
|
|
3690
|
+
}
|
|
3691
|
+
chunks.push(canAssignAnySql(ctx, custom));
|
|
3692
|
+
if (hasCanAssignAnyFor(ctx)) chunks.push(canAssignAnySql(ctx, custom, true));
|
|
3693
|
+
}
|
|
3694
|
+
return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
|
|
3695
|
+
}
|
|
3696
|
+
//#endregion
|
|
3697
|
+
export { activeUserSql as $, collectGrants as A, hasPermittedUsers as B, byKey as C, apiKeysPlan as D, apiKeyFields as E, CLOSURE as F, memberIdsHelper as G, memberForHelper as H, graphHelper as I, permittedIdsHelper as J, memberRoleOf as K, graphSqlName as L, compileConditionSql as M, contextRefs as N, breakGlassHolder as O, sqlFunctionNames as P, activeRowSql as Q, graphSqlText as R, byCodeUnit as S, apiKeyAllowsCall as T, memberForSources as U, linkHelper as V, memberForTable as W, scopeSources as X, restrictedHelper as Y, scopeTable as Z, capabilityAccessSql as _, guardedTables as a, currentKeyExpr as b, PERMISSION_HELPERS as c, permissionKeysForHelper as d, checkSuspension as et, permittedByPermissionHelper as f, seedSql as g, helpersSql as h, assignmentSql as i, roleNames as j, breakGlassKey as k, checkName as l, accessSql as m, ownershipSql as n, ASSIGNMENTS as o, requiresSql as p, permittedForHelper as q, holderSources as r, OWNERSHIP as s, ownershipRules as t, permissionHelpersSql as u, globalRoleRows as v, API_KEY_ALLOWS as w, rolesTableScope as x, globalRoleSource as y, hasMemberFor as z };
|