permdock 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/{2020-C9VN-x8c.js → 2020-CJbO8Bvz.js} +1 -3
- package/dist/a2a/index.d.ts +5 -5
- package/dist/a2a/index.js +4 -4
- package/dist/{adoption-scan-B4R5fnMU.js → adoption-scan-D-feE7BE.js} +1 -1
- package/dist/agent/index.d.ts +3 -3
- package/dist/agent/index.js +1 -1
- package/dist/ai-sdk/index.d.ts +21 -6
- package/dist/ai-sdk/index.js +19 -9
- package/dist/{approval-prompt-Djo_PyZc.d.ts → approval-prompt-K0faOLFp.d.ts} +2 -2
- package/dist/approvals/index.d.ts +4 -4
- package/dist/approvals/index.js +4 -4
- package/dist/{approvals-DJ6Yd5z1.js → approvals-CMRfsbBh.js} +74 -48
- package/dist/{arazzo-B6lebmiQ.js → arazzo-ClrF3PVb.js} +2 -2
- package/dist/{arazzo-BWhNF-k0.js → arazzo-rdeJ8n1J.js} +4 -4
- package/dist/{args-IIolxxKA.js → args-CC3OwyHW.js} +112 -2
- package/dist/astro/index.d.ts +6 -6
- package/dist/astro/index.js +2 -2
- package/dist/{audience-Dg8XsRaO.js → audience-BrMs8c4o.js} +1 -1
- package/dist/{auth-DRLEizbL.js → auth-CscUX8Sy.js} +3 -4
- package/dist/authzen/index.d.ts +162 -22
- package/dist/authzen/index.js +315 -146
- package/dist/{automation-BMZPIVUM.d.ts → automation-DOtAFWdy.d.ts} +3 -3
- package/dist/{automation-DVjOckjV.js → automation-ySsxp_Yp.js} +1 -1
- package/dist/base-url-BQbddZO5.js +9 -0
- package/dist/better-auth/index.d.ts +2 -2
- package/dist/better-auth/index.js +2 -2
- package/dist/better-supabase/index.d.ts +50 -8
- package/dist/better-supabase/index.js +94 -32
- package/dist/{bind-CZzhx-Vj.js → bind-D1nO6fiC.js} +5 -5
- package/dist/{bind--9EFVYWF.d.ts → bind-JDuvxjuW.d.ts} +7 -7
- package/dist/{build-instance-BMnAudHn.js → build-instance-DxE7czo9.js} +1 -1
- package/dist/{canonical-json-BC8_nMTw.js → canonical-json-DwilZ1iB.js} +6 -3
- package/dist/{capability-BSslw1QV.d.ts → capability-D4UC9TTW.d.ts} +2 -2
- package/dist/{capability-N1K9IpqS.js → capability-L7NnfhiE.js} +1 -1
- package/dist/{casbin-Cr7RLtA3.js → casbin-BHijLuHY.js} +2 -2
- package/dist/{casbin-Dyu2gnUD.js → casbin-BW9pS47J.js} +1 -1
- package/dist/{casl-64gpT-8x.js → casl-CmJlgziS.js} +2 -2
- package/dist/{casl-DjEDhTCP.js → casl-UbI8v-Wq.js} +1 -1
- package/dist/catalog/index.d.ts +70 -10
- package/dist/catalog/index.js +112 -6
- package/dist/catalog-BCCjSAdH.js +1 -0
- package/dist/{catalog-R_A__RsH.js → catalog-BaMQfQ6Y.js} +3 -3
- package/dist/{catalog-BECMPrwc.js → catalog-D69WhFii.js} +43 -13
- package/dist/{catalog-doc-C9wjsfNY.js → catalog-doc-CV9s6B1S.js} +28 -5
- package/dist/{catalog-fingerprint-CCP0-yQS.js → catalog-fingerprint-C144oW4M.js} +1 -1
- package/dist/{create-BWRSXgOF.js → challenge-nGWma08S.js} +122 -25
- package/dist/chat/index.d.ts +1 -1
- package/dist/chat/index.js +2 -2
- package/dist/chat-sdk/index.d.ts +3 -3
- package/dist/checks-BC_0mPIq.js +37 -0
- package/dist/claude-agent/index.d.ts +3 -3
- package/dist/claude-agent/index.js +2 -2
- package/dist/clerk/index.d.ts +3 -3
- package/dist/clerk/index.js +1 -1
- package/dist/cli/bin.js +2 -2
- package/dist/cli/index.d.ts +3 -3
- package/dist/cli/index.js +3 -3
- package/dist/cloud/index.d.ts +127 -11
- package/dist/cloud/index.js +251 -3
- package/dist/{cloud-CIoHNevm.js → cloud-CO8pYtwr.js} +5 -5
- package/dist/{codemod-Cxvvc70T.js → codemod-BP6n8N9L.js} +3 -3
- package/dist/{codemod-Bmj9dWMH.js → codemod-i4N2BhDc.js} +3 -3
- package/dist/{collect-CrZJJnEF.js → collect-BaWeMjwV.js} +17 -8
- package/dist/{collect-DloHbDVa.js → collect-DNm3oKOk.js} +6 -5
- package/dist/collect-k_Dulxr5.js +1 -0
- package/dist/compile/index.d.ts +1 -1
- package/dist/compile/index.js +1 -1
- package/dist/{compile-CMqMx_IN.js → compile-BAEO5bOk.js} +2 -2
- package/dist/{compile-Ckm2HqBZ.d.ts → compile-COM2VKuN.d.ts} +3 -3
- package/dist/{completion-BqhRBJD8.js → completion-BNdVf7X2.js} +3 -3
- package/dist/{config-bs_kZeYq.js → config-CoQHDrDi.js} +7 -3
- package/dist/{config-B46rm9sc.js → config-DGZzMV1B.js} +2 -2
- package/dist/{config-report-DUgj62Ai.js → config-report-Cm7UITNA.js} +3 -3
- package/dist/connector-schema-_VCSsdtD.js +433 -0
- package/dist/{context-bbFHR-0D.js → context--uM9V3Ui.js} +2 -2
- package/dist/convex/index.d.ts +5 -5
- package/dist/convex/index.js +2 -2
- package/dist/core-B5-w6Kw9.js +335 -0
- package/dist/core-CQkE8Esr.d.ts +179 -0
- package/dist/{create-0VZ7Pdiu.d.ts → create-B2-CQqCI.d.ts} +5 -5
- package/dist/{create-C3b4B5eF.js → create-Cb86Z8Im.js} +9 -8
- package/dist/{create-DnrmLnLW.js → create-DJrlhEp9.js} +5 -5
- package/dist/{create-BFfm17UA.d.ts → create-QHuuPw3q.d.ts} +7 -7
- package/dist/{create-BDS7wc3y.d.ts → create-UTnUVETj.d.ts} +30 -10
- package/dist/{credential-DM2um3ha.js → credential-CIM7wHwA.js} +4 -4
- package/dist/{credentials-DPyHmYz5.js → credentials-BpuTwd7a.js} +1 -1
- package/dist/{deciding-columns-Sq12mEA6.js → deciding-columns-DRo2QM6E.js} +13 -2
- package/dist/{delegation-DaXVdDhM.js → delegation-C5rOdTp5.js} +5 -5
- package/dist/delivery/index.d.ts +5 -5
- package/dist/delivery/index.js +4 -3
- package/dist/{delivery-DCAgNyCn.js → delivery-rwOKLWRm.js} +51 -2
- package/dist/{denied-text-D8jDHgsl.d.ts → denied-text-D4qkEtFz.d.ts} +2 -2
- package/dist/{describe-BybMRY7A.js → describe-DZlnEtLy.js} +1 -0
- package/dist/{describe-BZUsixiL.d.ts → describe-O0SwYIK8.d.ts} +2 -2
- package/dist/{diff-CxZo9pTq.js → diff-Btr1c1Ow.js} +2 -2
- package/dist/{diff-DCePiN6M.js → diff-DzT-xD13.js} +10 -10
- package/dist/{doctor-DFjUxf2f.js → doctor-BU1szn63.js} +241 -49
- package/dist/{doctor-CxiwSfqH.js → doctor-BoK_y0KO.js} +4 -4
- package/dist/drizzle/index.d.ts +3 -3
- package/dist/drizzle/index.js +1 -1
- package/dist/elysia/index.d.ts +5 -5
- package/dist/elysia/index.js +3 -3
- package/dist/{errors-C2PXIv4c.d.ts → errors-B_Qg9D3e.d.ts} +2 -2
- package/dist/{errors-CWTY6uih.js → errors-DpP_U6Bf.js} +1 -0
- package/dist/{evaluate-_R8QkWaf.js → evaluate-B_oqFbWC.js} +130 -34
- package/dist/{evaluations-16UqqQ6W.d.ts → evaluations-BCddEOfO.d.ts} +4 -4
- package/dist/{evaluations-DZ2CbV5k.js → evaluations-DReoiVBw.js} +4 -4
- package/dist/eve/index.d.ts +5 -5
- package/dist/eve/index.js +3 -3
- package/dist/express/index.d.ts +5 -5
- package/dist/express/index.js +3 -3
- package/dist/fastify/index.d.ts +5 -5
- package/dist/fastify/index.js +3 -3
- package/dist/{fetch-adapter-qKJBuRqy.d.ts → fetch-adapter-CuhyYwC1.d.ts} +4 -4
- package/dist/{fetch-adapter-g0ypLkID.js → fetch-adapter-De-u6vhJ.js} +3 -3
- package/dist/{fields-D9tPVUUS.js → fields-ClcQLrVc.js} +48 -13
- package/dist/{files-C5ZI27eV.js → files-acUbNwWt.js} +15 -2
- package/dist/{fixtures-B7XrLeY7.js → fixtures-BTMW6zgU.js} +2 -2
- package/dist/{flags-C3DktBDY.js → flags-aaeRAdiD.js} +2 -2
- package/dist/{from-snapshot-D2lZt4Dd.js → from-snapshot-Bz469dIh.js} +17 -10
- package/dist/generate-3FNO5P5Q.js +159 -0
- package/dist/{grantee-Cn1HyUsI.js → grantee-BrRANrpT.js} +14 -4
- package/dist/{grantee-conditions-CcXf8hMM.js → grantee-conditions-C6P1Tvol.js} +1 -1
- package/dist/{grantee-match-DHArqY82.js → grantee-match-0RtHWQDx.js} +3 -3
- package/dist/h3/index.d.ts +4 -4
- package/dist/h3/index.js +3 -3
- package/dist/{helpers-DTcZOzhF.js → helpers-DY3vXuGs.js} +3 -3
- package/dist/hono/index.d.ts +5 -5
- package/dist/hono/index.js +3 -3
- package/dist/{hosted-DBDX0M-P.d.ts → hosted-BmjzxQhp.d.ts} +14 -2
- package/dist/{hosted-BZIAYvon.js → hosted-XBgU6RGT.js} +34 -12
- package/dist/{http-DXJ37RDA.js → http-Ca3Sr5x1.js} +1 -1
- package/dist/{import-DMmOG27O.js → import-Bz0GefWg.js} +5 -5
- package/dist/{import-CNV7I2Dw.js → import-DB2EG_iy.js} +3 -3
- package/dist/{index-CxGUswIC.d.ts → index-BE8y3moU.d.ts} +50 -8
- package/dist/{index-Chrs5fQ-.d.ts → index-BSfRl0hZ.d.ts} +6 -6
- package/dist/{index-DOVf7RJv.d.ts → index-BTkwrK7v.d.ts} +155 -17
- package/dist/index-CMiPH2Tv.d.ts +206 -0
- package/dist/{index-SGdIQyzc.d.ts → index-DH9DUDEM.d.ts} +18 -3
- package/dist/{index-BcHcPyIo.d.ts → index-DLwcuCN_.d.ts} +86 -7
- package/dist/index.d.ts +18 -17
- package/dist/index.js +27 -25
- package/dist/{init-GG6Jh8HD.js → init-5TXNji88.js} +5 -5
- package/dist/{init-DeMYsnYJ.js → init-B-4t1NzC.js} +4 -4
- package/dist/{instance-options-Omfd793B.d.ts → instance-options-CU0YQCLH.d.ts} +1 -1
- package/dist/{instance-snapshot-Cxq4ygFc.js → instance-snapshot-CjcwnEiV.js} +20 -14
- package/dist/jwt/index.d.ts +83 -5
- package/dist/jwt/index.js +143 -5
- package/dist/{kernel-5JatXyND.js → kernel-DVExUwkp.js} +138 -12
- package/dist/kysely/index.d.ts +3 -3
- package/dist/kysely/index.js +1 -1
- package/dist/load-DrMHXzgN.js +1 -0
- package/dist/{load-DFS_ry0W.js → load-DxroEzH1.js} +2 -2
- package/dist/{login-Dfe5RlYk.js → login-ClSJh5t5.js} +4 -4
- package/dist/{login-imhuwvn3.js → login-WC9HPJpU.js} +6 -10
- package/dist/{manifest-C8IzW3Do.js → manifest-B79YuF2T.js} +6 -2
- package/dist/{manifest-mx5ghSFM.d.ts → manifest-BVhExYhw.d.ts} +23 -2
- package/dist/{manifest-CtKR3GXL.js → manifest-Dv6q4i5s.js} +54 -35
- package/dist/{manifest-schema-C0kK_8V0.js → manifest-schema-Cv492Qev.js} +37 -7
- package/dist/{map-claims-D3oRmTk0.js → map-claims-etGIS_Dv.js} +14 -11
- package/dist/{map-error-TCAdw-eW.js → map-error-BQGyDm_M.js} +1 -1
- package/dist/mcp/index.d.ts +64 -10
- package/dist/mcp/index.js +151 -22
- package/dist/{migrate-BShP7Vf4.js → migrate-Dodel0IJ.js} +77 -24
- package/dist/nest/index.d.ts +5 -5
- package/dist/nest/index.js +3 -3
- package/dist/next/index.d.ts +9 -9
- package/dist/next/index.js +5 -5
- package/dist/next/plugin.d.ts +1 -1
- package/dist/next/plugin.js +2 -2
- package/dist/node/index.d.ts +5 -5
- package/dist/node/index.js +2 -2
- package/dist/openai/index.d.ts +5 -5
- package/dist/openai/index.js +3 -3
- package/dist/openapi/index.d.ts +14 -4
- package/dist/openapi/index.js +1 -1
- package/dist/{openapi-ahr8rHu3.js → openapi-DMocl1bh.js} +4 -4
- package/dist/{openapi-B7HZ0f-r.js → openapi-Ddb4628T.js} +2 -7
- package/dist/{openapi-DTaEmTSW.js → openapi-ztVaux6j.js} +8 -8
- package/dist/{operation-findings-DgATDpvh.js → operation-findings-kRcEp8lE.js} +1 -1
- package/dist/{operations-CxybZErY.d.ts → operations-BNLes61x.d.ts} +1 -1
- package/dist/orpc/index.d.ts +6 -6
- package/dist/orpc/index.js +4 -4
- package/dist/otel/index.d.ts +3 -3
- package/dist/otel/index.js +1 -1
- package/dist/{output-Dyw8fyPF.js → output-DgSDLrta.js} +7 -3
- package/dist/{output-files-DhRBBE4n.js → output-files-CCMdcFWb.js} +2 -2
- package/dist/output-files-DPAY7-GX.js +1 -0
- package/dist/{ownership-BatFgAA4.js → ownership-Z2LpxsGa.js} +1349 -883
- package/dist/parse-Ddhz2xf4.js +148 -0
- package/dist/{parse-local-manifest-CVSs6J9I.js → parse-local-manifest-T1Q8X3A2.js} +24 -1
- package/dist/{parse-local-manifest-DgqtruPn.d.ts → parse-local-manifest-e4mdwAKd.d.ts} +1 -1
- package/dist/pdp/index.d.ts +3 -3
- package/dist/pdp/index.js +7 -3
- package/dist/peer-12Q5I_RH.js +976 -0
- package/dist/{peer-CRvC7Hg1.js → peer-DOzzMfZL.js} +1 -1
- package/dist/{permdock-B9JxkBwC.js → permdock-BfWEeMqk.js} +177 -49
- package/dist/{permdock-D7eeLJER.d.ts → permdock-Dco6dyCK.d.ts} +34 -5
- package/dist/{permission-registry-Br4jSNVi.js → permission-registry-DpPtSKNZ.js} +1 -1
- package/dist/{vocabulary-D6lPdb4Z.d.ts → permission-types-DhNEmoRG.d.ts} +4 -62
- package/dist/{permissions-oEWfl-kl.d.ts → permissions-CoqCOfJ3.d.ts} +2 -1
- package/dist/{permissions-B-wCP5ZC.js → permissions-CyH2MV_B.js} +1 -1
- package/dist/{permissions-CZKiUfFM.js → permissions-DArULThq.js} +4 -4
- package/dist/pkce-5h8NgMQJ.js +21 -0
- package/dist/{policies-CO1oOymN.js → policies-B2lZIurj.js} +3 -3
- package/dist/{policy-Czo4n3Hs.d.ts → policy-B7dg_yzu.d.ts} +2 -2
- package/dist/{policy-DLZJ5gAp.js → policy-DaJy4nK4.js} +17 -8
- package/dist/{policy-from-catalog-DzhEc3Y9.js → policy-from-catalog-DFWYkNdu.js} +9 -9
- package/dist/{policy-types-DzwivBgR.d.ts → policy-types-B-F-qAZk.d.ts} +109 -5
- package/dist/postgres-store-BL9M_CA1.js +1001 -0
- package/dist/{postgrest-NUqZT-RB.d.ts → postgrest-CYacM_u7.d.ts} +12 -1
- package/dist/{postgrest-CW0IqC-U.js → postgrest-r5faDIzD.js} +47 -5
- package/dist/{powersync-BHfuehf3.js → powersync-13odBwiM.js} +3 -3
- package/dist/powersync-53ryOZip.js +1059 -0
- package/dist/powersync-CExtTLyA.js +1 -0
- package/dist/preset/index.js +1 -1
- package/dist/{preset-DZdU0tne.js → preset-CHTbd-DQ.js} +2 -2
- package/dist/prisma/index.d.ts +2 -2
- package/dist/prisma/index.js +1 -1
- package/dist/{problem-details-DXlt-vks.d.ts → problem-details-Tcfmj3bV.d.ts} +1 -1
- package/dist/prompt-DwtVemER.js +935 -0
- package/dist/{protected-DEPHc2xV.d.ts → protected-BwZYL1X_.d.ts} +6 -5
- package/dist/{protected-DfjY5mLA.js → protected-DXE3cQYg.js} +2 -2
- package/dist/{provider-CC-u3bYP.js → provider-BQsTYYy0.js} +2 -2
- package/dist/{provider-DMVt6yYn.d.ts → provider-Csk-THAn.d.ts} +1 -1
- package/dist/{provider-store-B5Yrkhzr.js → provider-store-IgqQipCO.js} +2 -2
- package/dist/{pull-BgIfno6w.js → pull-CN5OLxrF.js} +7 -7
- package/dist/pull-Cty6Ovyu.js +1 -0
- package/dist/{push-CmFVBOeI.js → push-tVdG8dUL.js} +54 -14
- package/dist/{pushed-policy-BImMTTIM.d.ts → pushed-policy-BbcY9r_4.d.ts} +1 -1
- package/dist/{pushed-policy-DobP7AHI.js → pushed-policy-CmYeSQqq.js} +1 -1
- package/dist/react/index.d.ts +6 -6
- package/dist/react/index.js +3 -3
- package/dist/react/provider-client.d.ts +1 -1
- package/dist/react/provider-client.js +1 -1
- package/dist/react-native/index.d.ts +25 -11
- package/dist/react-native/index.js +214 -24
- package/dist/{refs-K6eWUJ0H.js → refs-CNJBiTFo.js} +15 -1
- package/dist/{replay-D7iKcLFe.d.ts → replay-DCDD_ZfL.d.ts} +1 -1
- package/dist/{request-context-BTRTG7OT.js → request-context-BykKLmyE.js} +2 -2
- package/dist/resend/index.d.ts +4 -4
- package/dist/{retire-CALjIQ7e.js → retire-Wj6dkyDE.js} +2 -2
- package/dist/{rls-BA8an5Ap.js → rls-Ck_PocSp.js} +32 -8
- package/dist/{rls-j6t43t7n.js → rls-CxBZb4rZ.js} +478 -534
- package/dist/{role-builder-CXbis5-X.js → role-builder-C1VJzvcr.js} +3 -3
- package/dist/rolldown-runtime-C3I60J0A.js +4 -0
- package/dist/{rpc-aPyNi6tA.js → rpc-Bk1bE-ch.js} +1 -1
- package/dist/run-Bz7DXGEv.js +1 -0
- package/dist/{run-ticket-sxFI6LAk.js → run-ticket-B5d_63Ma.js} +22 -1
- package/dist/{run-ticket-BjhTOi6u.d.ts → run-ticket-BpNi7BCi.d.ts} +8 -3
- package/dist/{run-DOfMtc9V.js → run-uLD2aWuO.js} +7 -7
- package/dist/{runtime-D3VTAiCA.js → runtime-Vif3_-zf.js} +1 -1
- package/dist/{saas-FO-Q8oj0.js → saas-Az_Kz-4D.js} +7 -7
- package/dist/{scan-BOI5tJ-6.js → scan-DSljDE1Y.js} +1 -1
- package/dist/{schema-yLuBWWsh.js → schema-gIcCx7zM.js} +1 -1
- package/dist/scim/index.d.ts +3 -2
- package/dist/scim/index.js +3 -1
- package/dist/scim-C7JYhySg.js +44 -0
- package/dist/{scim-ClHtpyV3.js → scim-YNieBO1U.js} +3 -537
- package/dist/{scopes-CUPSjo9q.js → scopes-BHsc-wp-.js} +1 -1
- package/dist/server/index.d.ts +11 -11
- package/dist/server/index.js +7 -7
- package/dist/{server-DqCT4CgZ.js → server-Bnga6I50.js} +3 -3
- package/dist/{server-types-BLd4MnBX.d.ts → server-types-C4gZUEZy.d.ts} +9 -9
- package/dist/{shared-DvvFzYXe.js → shared-C8LjUdVU.js} +1 -1
- package/dist/{shared-CD11nBbC.js → shared-fMKOv8hP.js} +46 -39
- package/dist/{sink-Bj_lDUDq.d.ts → sink-Cp_Eot9W.d.ts} +4 -4
- package/dist/{skills-B6lVdD_W.js → skills-Dcjw9_1C.js} +5 -5
- package/dist/{skills-CTYwkl44.js → skills-g_3BS9NC.js} +3 -3
- package/dist/{snapshot-CXUWfsNy.js → snapshot-CbGmYiJx.js} +4 -4
- package/dist/{snapshot-cache-DZHQ0KF6.js → snapshot-cache-DT2Kx1F8.js} +1 -1
- package/dist/{snapshot-cache-DqK9eDmM.d.ts → snapshot-cache-Pahd3IYJ.d.ts} +1 -1
- package/dist/solid/index.d.ts +6 -5
- package/dist/solid/index.js +4 -4
- package/dist/solid-start/index.d.ts +6 -6
- package/dist/solid-start/index.js +3 -3
- package/dist/{sources-vs5dGOsS.js → sources-B-zYB4GS.js} +111 -25
- package/dist/{sources-BV9j0DeZ.d.ts → sources-DEEuGb8S.d.ts} +63 -14
- package/dist/sources-qoIJQrJv.js +11 -0
- package/dist/{sql-B5T1IL83.js → sql-B96movIM.js} +5 -1
- package/dist/sql-CDEPCA0g.d.ts +10 -0
- package/dist/{src-DLfUWFAI.js → src-DLFrtrrC.js} +154 -53
- package/dist/ssf/index.d.ts +4 -4
- package/dist/ssf/index.js +640 -211
- package/dist/{standard-webhooks-6lg8v_is.d.ts → standard-webhooks-B_qRg0ws.d.ts} +2 -2
- package/dist/{store-4u_hxjWa.js → store-DSP9oGvh.js} +15 -7
- package/dist/{store-options-BqEakYRa.js → store-options-B_poyYvg.js} +6 -6
- package/dist/{stores-ClZUgeoP.d.ts → stores-BAZwSYZA.d.ts} +6 -5
- package/dist/{stores-BbsD4fAR.js → stores-BqEuHC05.js} +2 -2
- package/dist/{stream-7-xmnf3U.d.ts → stream-2Vwhl7Er.d.ts} +2 -2
- package/dist/{style-Uao76wO7.js → style-Cqo7-73x.js} +1 -0
- package/dist/{style-DiKWnAvn.js → style-Ds1gNm77.js} +1 -1
- package/dist/supabase/index.d.ts +8 -7
- package/dist/supabase/index.js +5 -5
- package/dist/supabase/middleware.d.ts +6 -6
- package/dist/supabase/middleware.js +6 -6
- package/dist/{supabase-Dxlp8A94.js → supabase--0aJdLEd.js} +9 -7
- package/dist/{supabase-9pqI57pW.js → supabase-Bqp7iIY2.js} +3 -3
- package/dist/supabase-hook-DJxC4PoP.js +1 -0
- package/dist/{supabase-hook-DdDQhMlw.js → supabase-hook-Dg0SrQWR.js} +1069 -123
- package/dist/supautils-MESd81bF.js +689 -0
- package/dist/{support-session-C6xJQYut.js → support-session-Cna49-3f.js} +2 -2
- package/dist/svelte/index.d.ts +5 -5
- package/dist/svelte/index.js +5 -5
- package/dist/svelte/runtime.d.ts +3 -3
- package/dist/svelte/runtime.js +3 -3
- package/dist/svelte/source.d.ts +5 -5
- package/dist/svelte/source.js +2 -2
- package/dist/sveltekit/index.d.ts +6 -6
- package/dist/sveltekit/index.js +2 -2
- package/dist/tanstack-start/index.d.ts +6 -6
- package/dist/tanstack-start/index.js +2 -2
- package/dist/{tenancy-DN2QAx9z.js → tenancy-CmeBQ6Nv.js} +12 -3
- package/dist/terminal/index.d.ts +6 -6
- package/dist/terminal/index.js +3 -3
- package/dist/testing/core.d.ts +2 -0
- package/dist/testing/core.js +1 -0
- package/dist/testing/fixtures/index.d.ts +12 -0
- package/dist/testing/fixtures/index.js +770 -0
- package/dist/testing/index.d.ts +41 -174
- package/dist/testing/index.js +241 -731
- package/dist/testing/saas/index.d.ts +5 -5
- package/dist/testing/saas/index.js +2 -2
- package/dist/testing/saas/permissions.d.ts +1 -1
- package/dist/testing/saas/permissions.js +1 -1
- package/dist/text-diff-DhITNCnD.js +444 -0
- package/dist/{token-BLZcjQgt.js → token-2qYDvXz2.js} +1 -1
- package/dist/{tool-refusal-CMLN5xhn.js → tool-refusal-reAhIzdl.js} +1 -1
- package/dist/trpc/index.d.ts +6 -6
- package/dist/trpc/index.js +3 -3
- package/dist/{types-5Ut_pbKy.d.ts → types-BWZybqs9.d.ts} +112 -8
- package/dist/{types-CZTnPFzM.d.ts → types-BXMYB7BY.d.ts} +2 -2
- package/dist/{types-CdlxsYQh.d.ts → types-BfVmNJO5.d.ts} +14 -3
- package/dist/{types-BqAKcRNW.d.ts → types-CIeZcG8v.d.ts} +2 -2
- package/dist/{types-BNORmDM8.d.ts → types-CKV71kKh.d.ts} +4 -4
- package/dist/{types-tIgIeIKJ.d.ts → types-CQVEQawv.d.ts} +4 -4
- package/dist/{types-BjV-fqcA.d.ts → types-CeIscIKp.d.ts} +4 -4
- package/dist/{types-_sHy8BQf.d.ts → types-CfiKC5aN.d.ts} +1 -1
- package/dist/{types-ClZaWzfb.d.ts → types-D3A19w4A.d.ts} +8 -4
- package/dist/{types-C8-KNsFf.d.ts → types-DBHftjgS.d.ts} +15 -8
- package/dist/{types-CCpc2Dcv.d.ts → types-DC44-hqb.d.ts} +2 -2
- package/dist/{types-fbZ7JEXr.d.ts → types-DXmwu78Z.d.ts} +4 -4
- package/dist/{types-DiQjupG2.d.ts → types-cTos-H7p.d.ts} +2 -2
- package/dist/{types-DX0LaKTb.d.ts → types-g7Oy6Zg4.d.ts} +3 -3
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +2 -2
- package/dist/{usage-T5mXq98v.js → usage-Bybmck7f.js} +7 -7
- package/dist/{usage-DHtE8j4E.js → usage-CDaLXCfL.js} +2 -2
- package/dist/usage-y-kBNdqs.js +1 -0
- package/dist/{validation-error-Bg5Tpluk.d.ts → validation-error-CQpoquwj.d.ts} +1 -1
- package/dist/{verify-DbCL9bnB.js → verify-pe8rL_Qw.js} +350 -83
- package/dist/{views-DTIIO-oQ.js → views-BB8E9dTP.js} +1 -1
- package/dist/vocabulary-Bg3eeT2H.d.ts +67 -0
- package/dist/vue/index.d.ts +6 -5
- package/dist/vue/index.js +4 -4
- package/dist/{waiter-CKy4MQSY.d.ts → waiter-DwEyouan.d.ts} +2 -2
- package/dist/watch-Bd9mx3rd.js +1 -0
- package/dist/{watch-CszoTJMq.js → watch-C-4VmFlL.js} +3 -3
- package/dist/webmcp/index.d.ts +2 -2
- package/dist/webmcp/index.js +2 -2
- package/dist/{where-scope-Dv-WTP-w.js → where-scope-wq2coopv.js} +41 -21
- package/dist/{who-can-D0QoYbEb.js → who-can-BwNsI_Js.js} +6 -6
- package/dist/workflow/index.d.ts +3 -3
- package/dist/workflow/index.js +5 -5
- package/dist/workflow-sdk/index.d.ts +5 -5
- package/dist/{wrap-Bd1Tejur.d.ts → wrap-B6R0cMRL.d.ts} +2 -2
- package/dist/{wrap-gX61EtpB.js → wrap-CiSc_KCq.js} +8 -2
- package/package.json +22 -11
- package/schemas/cli-result-v1.json +1 -0
- package/schemas/cloud-api-v1.json +152 -7
- package/schemas/connector-v1.json +8 -0
- package/schemas/supabase-claims-v1.json +5 -0
- package/schemas/supabase-manifest-v1.json +276 -75
- package/skills/permdock/SKILL.md +1 -0
- package/skills/permdock-agents/references/runtimes.md +3 -1
- package/skills/permdock-approvals/references/stores-and-resume.md +2 -1
- package/skills/permdock-data/references/queries-and-rls.md +5 -2
- package/skills/permdock-tenancy/SKILL.md +1 -1
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +2 -0
- package/skills/permdock-wire/references/adapters.md +14 -0
- package/skills/permdock-wire/references/policy-options.md +1 -0
- package/dist/catalog-DjhMYYCq.js +0 -1
- package/dist/collect-FlXJlhus.js +0 -1
- package/dist/index-D8EovfsP.d.ts +0 -91
- package/dist/load-BxEtwlBS.js +0 -1
- package/dist/output-files-B3IZJksC.js +0 -1
- package/dist/parse-Cxq0WxrA.js +0 -53
- package/dist/peer-C8swJ2GH.js +0 -50
- package/dist/powersync-CbzkqL_M.js +0 -1
- package/dist/powersync-bYUDLOAO.js +0 -538
- package/dist/prompt-DfZLJLIn.js +0 -32
- package/dist/pull-ByOks_Tg.js +0 -1
- package/dist/run-CNsApDpd.js +0 -1
- package/dist/supabase-hook-CXcGpHqq.js +0 -1
- package/dist/supautils-B6HYL9eL.js +0 -91
- package/dist/text-diff-BQcUapOb.js +0 -15
- package/dist/usage-CHOtwL19.js +0 -1
- package/dist/watch-Cb-NcaUw.js +0 -1
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { a as sole, n as isReadonlyArray, r as isRecord } from "./compact-CAf557ZB.js";
|
|
2
2
|
import { n as isForbiddenKey } from "./paths-DXSHAMIa.js";
|
|
3
|
-
import {
|
|
3
|
+
import { u as isSqlFunctionField } from "./refs-CNJBiTFo.js";
|
|
4
|
+
import { c as listPermissions, h as formerKeys } from "./permission-registry-DpPtSKNZ.js";
|
|
4
5
|
import { u as isoAt } from "./time-DkWjdL98.js";
|
|
5
|
-
import {
|
|
6
|
+
import { f as scopeList, l as rootScope, s as resolveScope, u as scopeChain } from "./scopes-BHsc-wp-.js";
|
|
6
7
|
import { t as isoInstant } from "./instant-BluqDvyS.js";
|
|
7
|
-
import { m as findRole, v as listRoles } from "./grantee-
|
|
8
|
-
import { r as relationCondition, t as inheritCondition } from "./grantee-conditions-
|
|
9
|
-
import { l as partitionsOf, p as scopeField } from "./tenancy-
|
|
10
|
-
import {
|
|
11
|
-
import { i as
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
8
|
+
import { m as findRole, v as listRoles } from "./grantee-BrRANrpT.js";
|
|
9
|
+
import { r as relationCondition, t as inheritCondition } from "./grantee-conditions-C6P1Tvol.js";
|
|
10
|
+
import { l as partitionsOf, p as scopeField } from "./tenancy-CmeBQ6Nv.js";
|
|
11
|
+
import { a as delegatedScopeMap } from "./permdock-BfWEeMqk.js";
|
|
12
|
+
import { i as configError } from "./errors-DpP_U6Bf.js";
|
|
13
|
+
import { d as sqlTextArray, i as qualifySqlTable, t as SQL_IDENT } from "./sql-B96movIM.js";
|
|
14
|
+
import { i as scopeMembershipTable, o as relatedSql, r as scopeColumn } from "./compile-BAEO5bOk.js";
|
|
15
|
+
import { d as keptKeys, l as roleColumn, u as roleManifest } from "./sources-B-zYB4GS.js";
|
|
16
|
+
import { A as quoteQualified, D as qualified, E as helperSchema, F as scopeTypeOf, I as sqlType, L as tenantTypeOf, M as textArrayLiteral, O as quoteIdent, S as CLAIM, T as declaredArray, _ as subjectClaimJsonSql, b as tenantClaimSql, d as keyTenantSql, f as kindFilterSql, h as roleKindSql, i as allowEntry, j as quoteTable, k as quoteLiteral, m as memberVia, n as GLOBAL_ROLES, o as rootName, p as liveSessionSql, r as HELPERS, s as signedIn, t as CUSTOM_ROLES, u as globalKindFilterSql, v as subjectClaimSql, w as CLI, x as userIdHelperSql, y as subjectIdSql } from "./shared-fMKOv8hP.js";
|
|
15
17
|
//#region src/cli/sql/active.ts
|
|
16
18
|
function checkActiveRow(label, row) {
|
|
17
19
|
quoteTable(row.table);
|
|
@@ -699,6 +701,10 @@ function collectGrants(policy) {
|
|
|
699
701
|
};
|
|
700
702
|
});
|
|
701
703
|
}
|
|
704
|
+
/** The declared roles a custom role may draw grants from: the custom-role ceiling. */
|
|
705
|
+
function assignableRoles(policy) {
|
|
706
|
+
return [...roleNames(policy)].toSorted().filter((name) => policy.rolesByName.get(name)?.assignable ?? listRoles(policy.vocabulary.roles).some((leaf) => leaf.key === name && leaf.assignable));
|
|
707
|
+
}
|
|
702
708
|
/** Declared role names (bindings and `defineRoles` leaves) plus any role a top-level grant names. */
|
|
703
709
|
function roleNames(policy) {
|
|
704
710
|
const names = new Set(policy.roles.map((item) => item.name));
|
|
@@ -840,909 +846,1105 @@ function byKey([a], [b]) {
|
|
|
840
846
|
return byCodeUnit(a, b);
|
|
841
847
|
}
|
|
842
848
|
//#endregion
|
|
843
|
-
//#region src/cli/
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
return activeUserSql(ctx, user).map((part) => `${indent}and ${part}`);
|
|
902
|
-
}
|
|
903
|
-
/** `and exists (...)` lines for every suspendable instance on the chain of `scope`'s membership. */
|
|
904
|
-
function instancesActive(ctx, scope, idOf, indent, permission) {
|
|
905
|
-
return activeInstancesSql(ctx, scope, idOf, permission).map((part) => `${indent}and ${part}`);
|
|
906
|
-
}
|
|
907
|
-
function roleRows(ctx) {
|
|
908
|
-
const claim = ctx.roleClaim ?? "user_role";
|
|
909
|
-
switch (ctx.dialect) {
|
|
910
|
-
case "guc": return `unnest(string_to_array(nullif(current_setting(${quoteLiteral(`${ctx.gucPrefix}.${claim}`)}, true), ''), ',')) r(role)`;
|
|
911
|
-
case "supabase":
|
|
912
|
-
case "neon": {
|
|
913
|
-
const raw = subjectClaimJsonSql(ctx, claim);
|
|
914
|
-
const value = ctx.dialect === "supabase" ? `coalesce(nullif(${raw}, 'null'::jsonb), (select auth.jwt()) -> 'app_metadata' -> ${quoteLiteral(claim)})` : raw;
|
|
915
|
-
return `jsonb_array_elements_text(
|
|
916
|
-
case jsonb_typeof(${value})
|
|
917
|
-
when 'array' then ${value}
|
|
918
|
-
when 'string' then jsonb_build_array(${value})
|
|
919
|
-
else '[]'::jsonb
|
|
920
|
-
end
|
|
921
|
-
) r(role)`;
|
|
849
|
+
//#region src/cli/rls/custom-roles.ts
|
|
850
|
+
/** The readers every helpers file ends with. Names are part of the SQL contract. */
|
|
851
|
+
const READERS = {
|
|
852
|
+
keys: "permdock_permission_keys",
|
|
853
|
+
roles: "permdock_role_permissions",
|
|
854
|
+
trusted: "permdock_trusted_role_permissions"
|
|
855
|
+
};
|
|
856
|
+
/**
|
|
857
|
+
* The custom-role objects: the tables (`database` mode), the ceiling view
|
|
858
|
+
* over the `assignable` declared roles, and `permdock_custom_keys`, which
|
|
859
|
+
* resolves one custom role to the grant keys it holds in a scope. A row in
|
|
860
|
+
* the tables can never reach a key outside the view.
|
|
861
|
+
*/
|
|
862
|
+
function customRolesSql(ctx) {
|
|
863
|
+
const custom = ctx.customRoles;
|
|
864
|
+
if (custom === void 0) return "";
|
|
865
|
+
const rp = qualified(ctx, "role_permissions");
|
|
866
|
+
const ceiling = qualified(ctx, CUSTOM_ROLES.ceiling);
|
|
867
|
+
const keys = qualified(ctx, CUSTOM_ROLES.keys);
|
|
868
|
+
const chunks = [];
|
|
869
|
+
if (ctx.authorize === "database") {
|
|
870
|
+
const tenantType = tenantTypeOf(ctx);
|
|
871
|
+
for (const [name, column, check] of [[
|
|
872
|
+
CUSTOM_ROLES.permissions,
|
|
873
|
+
"permission",
|
|
874
|
+
`,
|
|
875
|
+
effect text not null default 'allow' check (effect in ('allow', 'deny'))`
|
|
876
|
+
], [
|
|
877
|
+
CUSTOM_ROLES.includes,
|
|
878
|
+
"include_role",
|
|
879
|
+
""
|
|
880
|
+
]]) {
|
|
881
|
+
const table = qualified(ctx, name);
|
|
882
|
+
const leveled = custom.levels === true && name === CUSTOM_ROLES.permissions;
|
|
883
|
+
const unique = `${column}${name === CUSTOM_ROLES.permissions ? ", effect" : ""}${leveled ? ", coalesce(level, '')" : ""}`;
|
|
884
|
+
const index = leveled ? `drop index if exists ${qualified(ctx, quoteIdent(`${name}_key`))};
|
|
885
|
+
alter table ${table} add column if not exists level text check (level ~ '^[a-z][a-z0-9_]*$');
|
|
886
|
+
create unique index if not exists ${quoteIdent(`${name}_level_key`)}` : `create unique index if not exists ${quoteIdent(`${name}_key`)}`;
|
|
887
|
+
chunks.push(`create table if not exists ${table} (
|
|
888
|
+
tenant_id ${tenantType},
|
|
889
|
+
scope text not null default ${quoteLiteral(rootName(ctx))},
|
|
890
|
+
scope_id text,
|
|
891
|
+
role text not null,
|
|
892
|
+
${column} text not null${check}
|
|
893
|
+
);
|
|
894
|
+
-- global: no tenant or instance; a tenant role: a tenant; a shared role: no tenant or instance
|
|
895
|
+
alter table ${table} drop constraint if exists ${quoteIdent(`${name}_check`)};
|
|
896
|
+
alter table ${table} drop constraint if exists ${quoteIdent(`${name}_shape`)};
|
|
897
|
+
alter table ${table} add constraint ${quoteIdent(`${name}_shape`)} check (
|
|
898
|
+
case when scope = 'global' then tenant_id is null and scope_id is null
|
|
899
|
+
else tenant_id is not null or scope_id is null end
|
|
900
|
+
);
|
|
901
|
+
${index}
|
|
902
|
+
on ${table} (coalesce(tenant_id::text, ''), scope, coalesce(scope_id, ''), role, ${unique});
|
|
903
|
+
-- a row identity for triggers the application adds, such as an audit trigger that reads the primary key
|
|
904
|
+
alter table ${table} add column if not exists id bigint generated always as identity primary key;
|
|
905
|
+
alter table ${table} enable row level security;
|
|
906
|
+
revoke all on table ${table} from anon, authenticated, public;`);
|
|
922
907
|
}
|
|
923
|
-
default: return ctx.dialect;
|
|
924
908
|
}
|
|
909
|
+
chunks.push(`-- the ceiling: keys of assignable declared roles, and the denies of those roles on the same permissions
|
|
910
|
+
create or replace view ${ceiling} with (security_invoker = true) as
|
|
911
|
+
select rp.scope, rp.role, rp.permission, rp.grant_key, rp.effect
|
|
912
|
+
from ${rp} rp
|
|
913
|
+
where rp.role = any(${sqlTextArray(custom.assignable)})
|
|
914
|
+
and exists (
|
|
915
|
+
select 1 from ${rp} a
|
|
916
|
+
where a.role = rp.role
|
|
917
|
+
and a.permission = rp.permission
|
|
918
|
+
and a.scope = rp.scope
|
|
919
|
+
and a.effect = 'allow'
|
|
920
|
+
);
|
|
921
|
+
revoke all on table ${ceiling} from anon, authenticated, public;`);
|
|
922
|
+
const renames = Object.entries(custom.renamed ?? {}).toSorted(byKey);
|
|
923
|
+
const shared = sharedRoleSql(ctx);
|
|
924
|
+
if (custom.levels === true) {
|
|
925
|
+
chunks.push(leveledKeysSql(ctx, renames));
|
|
926
|
+
if (shared !== "") chunks.push(shared);
|
|
927
|
+
return chunks.join("\n\n");
|
|
928
|
+
}
|
|
929
|
+
const rename = renamesSql(renames);
|
|
930
|
+
chunks.push(`-- one custom role: included ceiling keys and same-scope denies, plus every ceiling key of an allowed permission, minus denied permissions
|
|
931
|
+
create or replace function ${keys}(p_allow text[], p_deny text[], p_include text[], p_scope text)
|
|
932
|
+
returns setof text
|
|
933
|
+
language sql
|
|
934
|
+
stable
|
|
935
|
+
set search_path = ''
|
|
936
|
+
as $$
|
|
937
|
+
with ${rename.prelude}ceiling as (
|
|
938
|
+
select c.role, ${rename.permission("c")}, c.grant_key, c.effect
|
|
939
|
+
from ${ceiling} c${rename.join("c")}
|
|
940
|
+
where c.scope = p_scope
|
|
941
|
+
),
|
|
942
|
+
included as (
|
|
943
|
+
select rp.role, ${rename.permission("rp")}, rp.grant_key, rp.scope, rp.effect
|
|
944
|
+
from ${rp} rp${rename.join("rp")}
|
|
945
|
+
where rp.role = any(coalesce(p_include, '{}'::text[]))
|
|
946
|
+
),
|
|
947
|
+
kept as (
|
|
948
|
+
select i.permission, i.grant_key
|
|
949
|
+
from included i
|
|
950
|
+
where i.effect = 'allow'
|
|
951
|
+
and i.scope = p_scope
|
|
952
|
+
and exists (
|
|
953
|
+
select 1 from ceiling c
|
|
954
|
+
where c.role = i.role and c.grant_key = i.grant_key and c.effect = 'allow'
|
|
955
|
+
)
|
|
956
|
+
),
|
|
957
|
+
wanted as (
|
|
958
|
+
${rename.allow}
|
|
959
|
+
union
|
|
960
|
+
select i.permission
|
|
961
|
+
from included i
|
|
962
|
+
where i.effect = 'allow'
|
|
963
|
+
and not (i.scope = p_scope and exists (
|
|
964
|
+
select 1 from ceiling c
|
|
965
|
+
where c.role = i.role and c.grant_key = i.grant_key and c.effect = 'allow'
|
|
966
|
+
))
|
|
967
|
+
),
|
|
968
|
+
allowed as (
|
|
969
|
+
select w.permission
|
|
970
|
+
from wanted w
|
|
971
|
+
where not (${rename.denied("w")})
|
|
972
|
+
and not exists (select 1 from kept k where k.permission = w.permission)
|
|
973
|
+
)
|
|
974
|
+
select k.grant_key from kept k
|
|
975
|
+
where not (${rename.denied("k")})
|
|
976
|
+
union
|
|
977
|
+
select i.grant_key from included i
|
|
978
|
+
where i.effect = 'deny' and i.scope = p_scope
|
|
979
|
+
union
|
|
980
|
+
select c.grant_key from ceiling c
|
|
981
|
+
where c.permission in (select a.permission from allowed a)
|
|
982
|
+
$$;
|
|
983
|
+
revoke execute on function ${keys}(text[], text[], text[], text) from public, anon, authenticated;`);
|
|
984
|
+
if (shared !== "") chunks.push(shared);
|
|
985
|
+
return chunks.join("\n\n");
|
|
925
986
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
}
|
|
947
|
-
function customKeysJoin(ctx, scope, allows, denies, includes) {
|
|
948
|
-
return ` cross join lateral ${qualified(ctx, CUSTOM_ROLES.keys)}(
|
|
949
|
-
${allows},
|
|
950
|
-
${denies},
|
|
951
|
-
${includes},
|
|
952
|
-
${quoteLiteral(scope)}
|
|
953
|
-
) ck(grant_key)`;
|
|
987
|
+
/** `permdock_is_shared_role(p_scope, p_role)` (`database` mode): whether a shared custom role of that name exists at the scope. */
|
|
988
|
+
function sharedRoleSql(ctx) {
|
|
989
|
+
if (ctx.authorize !== "database") return "";
|
|
990
|
+
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
991
|
+
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
992
|
+
const shared = qualified(ctx, CUSTOM_ROLES.shared);
|
|
993
|
+
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
|
|
994
|
+
create or replace function ${shared}(p_scope text, p_role text)
|
|
995
|
+
returns boolean
|
|
996
|
+
language sql
|
|
997
|
+
stable
|
|
998
|
+
security definer
|
|
999
|
+
set search_path = ''
|
|
1000
|
+
as $$
|
|
1001
|
+
select p_scope <> 'global' and (
|
|
1002
|
+
exists (select 1 from ${perms} c where c.tenant_id is null and c.scope = p_scope and c.role = p_role)
|
|
1003
|
+
or exists (select 1 from ${includes} c where c.tenant_id is null and c.scope = p_scope and c.role = p_role)
|
|
1004
|
+
)
|
|
1005
|
+
$$;
|
|
1006
|
+
revoke execute on function ${shared}(text, text) from public, anon, authenticated;`;
|
|
954
1007
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1008
|
+
/**
|
|
1009
|
+
* `permdock_custom_keys` when resources declare levels. An allow entry is
|
|
1010
|
+
* `key` or `key@level`. A level reaches only the ceiling's `<grant key>@<level>`
|
|
1011
|
+
* keys, plus the ceiling denies of that permission; a level no ceiling key
|
|
1012
|
+
* carries removes the permission, as `resolveCustomRole` drops it with
|
|
1013
|
+
* `unknown-level`.
|
|
1014
|
+
*/
|
|
1015
|
+
function leveledKeysSql(ctx, renames) {
|
|
958
1016
|
const rp = qualified(ctx, "role_permissions");
|
|
959
|
-
const
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1017
|
+
const ceiling = qualified(ctx, CUSTOM_ROLES.ceiling);
|
|
1018
|
+
const keys = qualified(ctx, CUSTOM_ROLES.keys);
|
|
1019
|
+
const renamed = renames.length > 0;
|
|
1020
|
+
const prelude = renamed ? `renamed (former, key) as (values ${renames.map(([former, current]) => `(${quoteLiteral(former)}, ${quoteLiteral(current)})`).join(", ")}),
|
|
1021
|
+
` : "";
|
|
1022
|
+
const current = (raw, alias) => renamed ? `coalesce(r_${alias}.key, ${raw})` : raw;
|
|
1023
|
+
const join = (raw, alias) => renamed ? ` left join renamed r_${alias} on r_${alias}.former = ${raw}` : "";
|
|
1024
|
+
return `-- one custom role: as above, where an allow entry key@level reaches only the ceiling keys of that level
|
|
1025
|
+
create or replace function ${keys}(p_allow text[], p_deny text[], p_include text[], p_scope text)
|
|
1026
|
+
returns setof text
|
|
1027
|
+
language sql
|
|
1028
|
+
stable
|
|
1029
|
+
set search_path = ''
|
|
1030
|
+
as $$
|
|
1031
|
+
with ${prelude}entries as (
|
|
1032
|
+
select ${current("split_part(e.entry, '@', 1)", "e")} as permission, nullif(split_part(e.entry, '@', 2), '') as level
|
|
1033
|
+
from unnest(coalesce(p_allow, '{}'::text[])) as e(entry)${join("split_part(e.entry, '@', 1)", "e")}
|
|
1034
|
+
),
|
|
1035
|
+
ceiling as (
|
|
1036
|
+
select c.role, ${current("c.permission", "c")} as permission, c.grant_key, c.effect
|
|
1037
|
+
from ${ceiling} c${join("c.permission", "c")}
|
|
1038
|
+
where c.scope = p_scope
|
|
1039
|
+
),
|
|
1040
|
+
levels as (
|
|
1041
|
+
select distinct c.permission, split_part(c.grant_key, '@', 2) as level
|
|
1042
|
+
from ceiling c
|
|
1043
|
+
where c.effect = 'allow' and strpos(c.grant_key, '@') > 0
|
|
1044
|
+
),
|
|
1045
|
+
denied as (
|
|
1046
|
+
select ${current("d.permission", "d")} as permission
|
|
1047
|
+
from unnest(coalesce(p_deny, '{}'::text[])) as d(permission)${join("d.permission", "d")}
|
|
1048
|
+
union
|
|
1049
|
+
select e.permission from entries e
|
|
1050
|
+
where e.level is not null
|
|
1051
|
+
and not exists (select 1 from levels l where l.permission = e.permission and l.level = e.level)
|
|
1052
|
+
),
|
|
1053
|
+
included as (
|
|
1054
|
+
select rp.role, ${current("rp.permission", "rp")} as permission, rp.grant_key, rp.scope, rp.effect
|
|
1055
|
+
from ${rp} rp${join("rp.permission", "rp")}
|
|
1056
|
+
where rp.role = any(coalesce(p_include, '{}'::text[]))
|
|
1057
|
+
),
|
|
1058
|
+
kept as (
|
|
1059
|
+
select i.permission, i.grant_key
|
|
1060
|
+
from included i
|
|
1061
|
+
where i.effect = 'allow'
|
|
1062
|
+
and i.scope = p_scope
|
|
1063
|
+
and exists (
|
|
1064
|
+
select 1 from ceiling c
|
|
1065
|
+
where c.role = i.role and c.grant_key = i.grant_key and c.effect = 'allow'
|
|
1066
|
+
)
|
|
1067
|
+
),
|
|
1068
|
+
wanted as (
|
|
1069
|
+
select e.permission from entries e where e.level is null
|
|
1070
|
+
union
|
|
1071
|
+
select i.permission
|
|
1072
|
+
from included i
|
|
1073
|
+
where i.effect = 'allow'
|
|
1074
|
+
and not (i.scope = p_scope and exists (
|
|
1075
|
+
select 1 from ceiling c
|
|
1076
|
+
where c.role = i.role and c.grant_key = i.grant_key and c.effect = 'allow'
|
|
1077
|
+
))
|
|
1078
|
+
),
|
|
1079
|
+
allowed as (
|
|
1080
|
+
select w.permission
|
|
1081
|
+
from wanted w
|
|
1082
|
+
where w.permission not in (select d.permission from denied d)
|
|
1083
|
+
and not exists (select 1 from kept k where k.permission = w.permission)
|
|
1084
|
+
),
|
|
1085
|
+
allowed_levels as (
|
|
1086
|
+
select e.permission, e.level
|
|
1087
|
+
from entries e
|
|
1088
|
+
where e.level is not null
|
|
1089
|
+
and e.permission not in (select d.permission from denied d)
|
|
1090
|
+
and e.permission not in (select a.permission from allowed a)
|
|
1091
|
+
and not exists (select 1 from kept k where k.permission = e.permission)
|
|
1092
|
+
)
|
|
1093
|
+
select k.grant_key from kept k
|
|
1094
|
+
where k.permission not in (select d.permission from denied d)
|
|
972
1095
|
union
|
|
973
|
-
select
|
|
974
|
-
|
|
975
|
-
${customKeysJoin(ctx, "global", rows(CUSTOM_ROLES.permissions, allowEntry(ctx), " and c.effect = 'allow'"), rows(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), rows(CUSTOM_ROLES.includes, "include_role", ""))}
|
|
976
|
-
where ${ur.userSql} = ${subjectIdSql(ctx)}
|
|
977
|
-
and not (${ur.roleSql} = any(${sqlTextArray(custom.declared)}))${active}`;
|
|
978
|
-
}
|
|
979
|
-
const declared = ` select rp.grant_key
|
|
980
|
-
from ${roleRows(ctx)}
|
|
981
|
-
join ${rp} rp on rp.role = r.role
|
|
982
|
-
where ${signedIn(ctx)}
|
|
983
|
-
and rp.scope = 'global'${andLine(" ", globalKindFilterSql(ctx, "r.role"))}${active}`;
|
|
984
|
-
if (custom === void 0) return declared;
|
|
985
|
-
const claim = subjectClaimJsonSql(ctx, GLOBAL_GRANTS_CLAIM);
|
|
986
|
-
return `${declared}
|
|
1096
|
+
select i.grant_key from included i
|
|
1097
|
+
where i.effect = 'deny' and i.scope = p_scope
|
|
987
1098
|
union
|
|
988
|
-
select
|
|
989
|
-
from
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1099
|
+
select c.grant_key from ceiling c
|
|
1100
|
+
where c.permission in (select a.permission from allowed a)
|
|
1101
|
+
union
|
|
1102
|
+
select c.grant_key from ceiling c
|
|
1103
|
+
join allowed_levels a on a.permission = c.permission
|
|
1104
|
+
where c.effect = 'deny' or split_part(c.grant_key, '@', 2) = a.level
|
|
1105
|
+
$$;
|
|
1106
|
+
revoke execute on function ${keys}(text[], text[], text[], text) from public, anon, authenticated;`;
|
|
995
1107
|
}
|
|
996
|
-
function
|
|
997
|
-
|
|
1108
|
+
function currentKeysSql(array) {
|
|
1109
|
+
return `select coalesce(r.key, e.permission) as permission
|
|
1110
|
+
from unnest(coalesce(${array}, '{}'::text[])) as e(permission)
|
|
1111
|
+
left join renamed r on r.former = e.permission`;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* SQL fragments that read a stored former key as its current key inside
|
|
1115
|
+
* `permdock_custom_keys`. Without renames they are the plain column and
|
|
1116
|
+
* array reads.
|
|
1117
|
+
*/
|
|
1118
|
+
function renamesSql(renames) {
|
|
1119
|
+
if (renames.length === 0) return {
|
|
1120
|
+
prelude: "",
|
|
1121
|
+
permission: (alias) => `${alias}.permission`,
|
|
1122
|
+
join: () => "",
|
|
1123
|
+
allow: "select unnest(coalesce(p_allow, '{}'::text[])) as permission",
|
|
1124
|
+
denied: (alias) => `${alias}.permission = any(coalesce(p_deny, '{}'::text[]))`
|
|
1125
|
+
};
|
|
1126
|
+
return {
|
|
1127
|
+
prelude: `renamed (former, key) as (values ${renames.map(([former, current]) => `(${quoteLiteral(former)}, ${quoteLiteral(current)})`).join(", ")}),
|
|
1128
|
+
denied as (
|
|
1129
|
+
${currentKeysSql("p_deny")}
|
|
1130
|
+
),
|
|
1131
|
+
`,
|
|
1132
|
+
permission: (alias) => `coalesce(r_${alias}.key, ${alias}.permission) as permission`,
|
|
1133
|
+
join: (alias) => ` left join renamed r_${alias} on r_${alias}.former = ${alias}.permission`,
|
|
1134
|
+
allow: currentKeysSql("p_allow"),
|
|
1135
|
+
denied: (alias) => `${alias}.permission in (select d.permission from denied d)`
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* The catalog reader `permdock_permission_keys()` and the role reader
|
|
1140
|
+
* `permdock_role_permissions(p_role, p_scope, p_tenant, p_scope_id)`, both
|
|
1141
|
+
* for `authenticated`. A declared role answers from `role_permissions`, which
|
|
1142
|
+
* is the catalog. A custom role (`database` mode) answers from the tenant's
|
|
1143
|
+
* own rows through `permdock_custom_keys`, only to a member of the tenant or
|
|
1144
|
+
* a holder of a `meta.manageRoles` permission for a platform role; a shared
|
|
1145
|
+
* role (no tenant below global) to any signed-in caller. The
|
|
1146
|
+
* effect is `deny` for a deny the role carries after the ceiling, as
|
|
1147
|
+
* `resolveCustomRole` returns it, never for a deny that merely removes an allow.
|
|
1148
|
+
*/
|
|
1149
|
+
function roleReadersSql(ctx, permissions, trustedReaders) {
|
|
998
1150
|
const rp = qualified(ctx, "role_permissions");
|
|
999
|
-
const
|
|
1151
|
+
const reader = qualified(ctx, READERS.roles);
|
|
1152
|
+
const tenantType = tenantTypeOf(ctx);
|
|
1000
1153
|
const custom = ctx.customRoles;
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1154
|
+
const root = ctx.scopes[0]?.name;
|
|
1155
|
+
const database = ctx.authorize === "database";
|
|
1156
|
+
const chunks = [];
|
|
1157
|
+
if (permissions !== void 0) {
|
|
1158
|
+
const catalog = qualified(ctx, READERS.keys);
|
|
1159
|
+
chunks.push(`-- every declared permission key
|
|
1160
|
+
create or replace function ${catalog}()
|
|
1161
|
+
returns setof text
|
|
1162
|
+
language sql
|
|
1163
|
+
stable
|
|
1164
|
+
set search_path = ''
|
|
1165
|
+
as $$
|
|
1166
|
+
select k.permission from unnest(${sqlTextArray(permissions.toSorted())}) k(permission) order by 1
|
|
1167
|
+
$$;
|
|
1168
|
+
revoke execute on function ${catalog}() from public, anon;
|
|
1169
|
+
grant execute on function ${catalog}() to authenticated;
|
|
1170
|
+
|
|
1171
|
+
create or replace function ${catalog}(p_scope text)
|
|
1172
|
+
returns setof text
|
|
1173
|
+
language sql
|
|
1174
|
+
stable
|
|
1175
|
+
security definer
|
|
1176
|
+
set search_path = ''
|
|
1177
|
+
as $$
|
|
1178
|
+
select distinct rp.permission
|
|
1179
|
+
from ${rp} rp
|
|
1180
|
+
where rp.scope = p_scope
|
|
1181
|
+
and rp.effect = 'allow'
|
|
1182
|
+
and rp.permission = any(${sqlTextArray(permissions.toSorted())})
|
|
1183
|
+
order by 1
|
|
1184
|
+
$$;
|
|
1185
|
+
revoke execute on function ${catalog}(text) from public, anon;
|
|
1186
|
+
grant execute on function ${catalog}(text) to authenticated;`);
|
|
1022
1187
|
}
|
|
1023
|
-
const declared = ` select
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
const held = scopeColumn(table, ctx.scopes, name);
|
|
1083
|
-
return held === void 0 ? void 0 : memberColumn(held);
|
|
1084
|
-
}, " ", grantPermissionSql(grant)));
|
|
1085
|
-
return filters;
|
|
1086
|
-
};
|
|
1087
|
-
const filters = filtersFor(set ? "rp.grant_key" : "p_grant");
|
|
1088
|
-
const [owner, ...rest] = filters;
|
|
1089
|
-
const role = memberRoleOf(table, "m", " ");
|
|
1090
|
-
const kind = kindFilterSql(ctx, role.sql, memberVia(table));
|
|
1091
|
-
const lines = [
|
|
1092
|
-
` select${role.lateral ? " distinct" : ""} ${memberColumn(column)}::${type}${set ? ", rp.grant_key" : ""}`,
|
|
1093
|
-
` from ${quoteQualified(table.table)} m${role.join}`,
|
|
1094
|
-
` join ${qualified(ctx, "role_permissions")} rp on rp.role = ${role.sql}`,
|
|
1095
|
-
owner ?? "",
|
|
1096
|
-
...set ? [] : [" and rp.grant_key = p_grant"],
|
|
1097
|
-
` and rp.scope = ${quoteLiteral(scope)}`,
|
|
1098
|
-
...kind === void 0 ? [] : [` and ${kind}`],
|
|
1099
|
-
...rest
|
|
1100
|
-
];
|
|
1101
|
-
const custom = ctx.customRoles;
|
|
1102
|
-
if (custom === void 0) return lines.join("\n");
|
|
1103
|
-
const match = [
|
|
1104
|
-
`c.tenant_id::text = ${memberColumn(tenantColumn ?? column)}::text`,
|
|
1105
|
-
`c.scope = ${quoteLiteral(scope)}`,
|
|
1106
|
-
`(c.scope_id is null or c.scope_id = ${memberColumn(column)}::text)`,
|
|
1107
|
-
`c.role = ${role.sql}`
|
|
1108
|
-
].join(" and ");
|
|
1109
|
-
const rows = (source, value, extra) => `array(select c.${value} from ${qualified(ctx, source)} c where ${match}${extra})`;
|
|
1110
|
-
const entries = [
|
|
1111
|
-
rows(CUSTOM_ROLES.permissions, allowEntry(ctx), " and c.effect = 'allow'"),
|
|
1112
|
-
rows(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"),
|
|
1113
|
-
rows(CUSTOM_ROLES.includes, "include_role", "")
|
|
1114
|
-
];
|
|
1115
|
-
if (set) return [
|
|
1116
|
-
...lines,
|
|
1117
|
-
" union",
|
|
1118
|
-
` select ${memberColumn(column)}::${type}, ck.grant_key`,
|
|
1119
|
-
` from ${quoteQualified(table.table)} m${role.join}`,
|
|
1120
|
-
customKeysJoin(ctx, scope, ...entries),
|
|
1121
|
-
...filtersFor("ck.grant_key"),
|
|
1122
|
-
` and not (${role.sql} = any(${sqlTextArray(custom.declared)}))`
|
|
1123
|
-
].join("\n");
|
|
1124
|
-
return [
|
|
1125
|
-
...lines,
|
|
1126
|
-
" union",
|
|
1127
|
-
` select ${memberColumn(column)}::${type}`,
|
|
1128
|
-
` from ${quoteQualified(table.table)} m${role.join}`,
|
|
1129
|
-
...filters,
|
|
1130
|
-
` and not (${role.sql} = any(${sqlTextArray(custom.declared)}))`,
|
|
1131
|
-
customKeysSql(ctx, scope, ...entries)
|
|
1132
|
-
].join("\n");
|
|
1133
|
-
}
|
|
1134
|
-
/** The membership's instance of the first scope, from the canonical claim. */
|
|
1135
|
-
function claimTenant(ctx, scope) {
|
|
1136
|
-
const root = rootName(ctx);
|
|
1137
|
-
return scope === root ? `m ->> 'id'` : `m -> 'within' ->> ${quoteLiteral(root)}`;
|
|
1138
|
-
}
|
|
1139
|
-
function claimBody(ctx, scope, type) {
|
|
1140
|
-
const set = ctx.grantSet === true;
|
|
1141
|
-
const narrow = narrowsTo(ctx, scope) ? `
|
|
1142
|
-
and (${activeTenant(ctx)} is null or ${claimTenant(ctx, scope)} = ${activeTenant(ctx)})` : "";
|
|
1143
|
-
const filtersFor = (grant) => ` and m ->> 'scope' = ${quoteLiteral(scope)}
|
|
1144
|
-
and m ->> 'id' is not null${narrow}${keyTenantLine(ctx, scope, claimTenant(ctx, scope))}
|
|
1145
|
-
and case jsonb_typeof(m -> 'expires_at')
|
|
1146
|
-
when 'number' then (m ->> 'expires_at')::numeric > extract(epoch from now())
|
|
1147
|
-
else true
|
|
1148
|
-
end${[
|
|
1149
|
-
...claimKeptLines(ctx, " ", grantPermissionSql(grant)),
|
|
1150
|
-
...userActive(ctx, " "),
|
|
1151
|
-
...instancesActive(ctx, scope, (name) => name === scope ? `m ->> 'id'` : `m -> 'within' ->> ${quoteLiteral(name)}`, " ", grantPermissionSql(grant))
|
|
1152
|
-
].map((line) => `\n${line}`).join("")}`;
|
|
1153
|
-
const filters = filtersFor(set ? "rp.grant_key" : "p_grant");
|
|
1154
|
-
const declared = ` select (m ->> 'id')::${type}${set ? ", rp.grant_key" : ""}
|
|
1155
|
-
from ${membershipRows(ctx)}
|
|
1156
|
-
join ${qualified(ctx, "role_permissions")} rp on rp.role = r.role
|
|
1157
|
-
where ${signedIn(ctx)}${set ? "" : "\n and rp.grant_key = p_grant"}
|
|
1158
|
-
and rp.scope = ${quoteLiteral(scope)}${andLine(" ", kindFilterSql(ctx, "r.role", "m ->> 'via'"))}
|
|
1159
|
-
${filters}`;
|
|
1160
|
-
const custom = ctx.customRoles;
|
|
1161
|
-
if (custom === void 0) return declared;
|
|
1162
|
-
if (set) return `${declared}
|
|
1163
|
-
union
|
|
1164
|
-
select (m ->> 'id')::${type}, ck.grant_key
|
|
1165
|
-
from ${membershipRows(ctx)}
|
|
1166
|
-
cross join lateral (select m -> 'grants' -> r.role as g) cg
|
|
1167
|
-
${customKeysJoin(ctx, scope, claimEntries("left(e, 1) not in ('-', '@')", "e"), claimEntries("left(e, 1) = '-'", "substr(e, 2)"), claimEntries("left(e, 1) = '@'", "substr(e, 2)"))}
|
|
1168
|
-
where ${signedIn(ctx)}
|
|
1169
|
-
and jsonb_typeof(cg.g) = 'array'
|
|
1170
|
-
and not (r.role = any(${sqlTextArray(custom.declared)}))
|
|
1171
|
-
${filtersFor("ck.grant_key")}`;
|
|
1172
|
-
return `${declared}
|
|
1173
|
-
union
|
|
1174
|
-
select (m ->> 'id')::${type}
|
|
1175
|
-
from ${membershipRows(ctx)}
|
|
1176
|
-
cross join lateral (select m -> 'grants' -> r.role as g) cg
|
|
1177
|
-
where ${signedIn(ctx)}
|
|
1178
|
-
and jsonb_typeof(cg.g) = 'array'
|
|
1179
|
-
and not (r.role = any(${sqlTextArray(custom.declared)}))
|
|
1180
|
-
${filters}
|
|
1181
|
-
${customKeysSql(ctx, scope, claimEntries("left(e, 1) not in ('-', '@')", "e"), claimEntries("left(e, 1) = '-'", "substr(e, 2)"), claimEntries("left(e, 1) = '@'", "substr(e, 2)"))}`;
|
|
1182
|
-
}
|
|
1183
|
-
/**
|
|
1184
|
-
* The rows the membership sources select for the subject, one per instance
|
|
1185
|
-
* (`scope`, `id`, `within`, `roles`, `via`), with each source's own expiry
|
|
1186
|
-
* and suspension filters: the statements the token hook runs.
|
|
1187
|
-
*/
|
|
1188
|
-
function sourcesKeep(sources) {
|
|
1189
|
-
return sources.some((source) => source.sql.keeps);
|
|
1190
|
-
}
|
|
1191
|
-
function sourceRows(sources) {
|
|
1192
|
-
const keep = sourcesKeep(sources);
|
|
1193
|
-
return sources.map((source, index) => source.sql.select(sourceUser(index), keep).replaceAll(/^/gmu, " ")).join("\n union all\n");
|
|
1194
|
-
}
|
|
1195
|
-
function sourceUser(index) {
|
|
1196
|
-
return `v_user_${String(index)}`;
|
|
1197
|
-
}
|
|
1198
|
-
function sqlBody(sql) {
|
|
1199
|
-
return {
|
|
1200
|
-
sql,
|
|
1201
|
-
vars: []
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
function sourcesBodyOf(sql, sources, user) {
|
|
1205
|
-
return {
|
|
1206
|
-
sql,
|
|
1207
|
-
vars: sources.map((source, index) => `${sourceUser(index)} ${source.sql.userType} := ${user};`)
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1210
|
-
/** `language sql`, or `plpgsql` returning the query when the body declares variables. */
|
|
1211
|
-
function functionBody(body) {
|
|
1212
|
-
if (body.vars.length === 0) return `language sql
|
|
1188
|
+
const declared = ` select distinct rp.permission, rp.effect
|
|
1189
|
+
from ${rp} rp
|
|
1190
|
+
where rp.role = p_role and rp.scope = p_scope
|
|
1191
|
+
order by 1, 2`;
|
|
1192
|
+
let body = `begin
|
|
1193
|
+
perform p_tenant, p_scope_id; -- only a custom role reads them
|
|
1194
|
+
return query
|
|
1195
|
+
${declared};
|
|
1196
|
+
end;`;
|
|
1197
|
+
let trustedBody = body;
|
|
1198
|
+
if (custom !== void 0 && database && root !== void 0) {
|
|
1199
|
+
const tenant = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
1200
|
+
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
1201
|
+
const keys = qualified(ctx, CUSTOM_ROLES.keys);
|
|
1202
|
+
const shape = qualified(ctx, CUSTOM_ROLES.shape);
|
|
1203
|
+
const ids = (helper) => `(select x::text from ${qualified(ctx, helper)}() x)`;
|
|
1204
|
+
const memberOf = `p_tenant::text in ${ids(memberIdsHelper(root))}
|
|
1205
|
+
or ${perScope(ctx, (scope) => scope === root ? "false" : `p_scope_id in ${ids(memberIdsHelper(scope))}`)}`;
|
|
1206
|
+
const manage = custom.manage ?? [];
|
|
1207
|
+
const platform = manage.length === 0 ? "false" : `exists (
|
|
1208
|
+
select 1 from ${rp} m
|
|
1209
|
+
where m.effect = 'allow'
|
|
1210
|
+
and m.permission = any(${sqlTextArray(manage)})
|
|
1211
|
+
and ${qualified(ctx, HELPERS.has)}(m.grant_key)
|
|
1212
|
+
)`;
|
|
1213
|
+
const match = `c.tenant_id is not distinct from p_tenant and c.scope = p_scope and c.scope_id is not distinct from p_scope_id and c.role = p_role`;
|
|
1214
|
+
const allowEntryOf = `c.${allowEntry(ctx)}`;
|
|
1215
|
+
const checked = (gate) => `begin
|
|
1216
|
+
if p_role = any(${sqlTextArray(custom.declared)}) then
|
|
1217
|
+
return query
|
|
1218
|
+
${declared};
|
|
1219
|
+
return;
|
|
1220
|
+
end if;
|
|
1221
|
+
perform ${shape}(p_tenant, p_scope, p_scope_id, p_role);${gate}
|
|
1222
|
+
return query
|
|
1223
|
+
select distinct rp.permission, rp.effect
|
|
1224
|
+
from ${keys}(
|
|
1225
|
+
array(select ${allowEntryOf} from ${tenant} c where ${match} and c.effect = 'allow'),
|
|
1226
|
+
array(select c.permission from ${tenant} c where ${match} and c.effect = 'deny'),
|
|
1227
|
+
array(select c.include_role from ${includes} c where ${match}),
|
|
1228
|
+
p_scope
|
|
1229
|
+
) k(grant_key)
|
|
1230
|
+
join ${rp} rp on rp.grant_key = k.grant_key and rp.scope = p_scope
|
|
1231
|
+
order by 1, 2;
|
|
1232
|
+
end;`;
|
|
1233
|
+
body = checked(`
|
|
1234
|
+
if not (${signedIn(ctx)} and (
|
|
1235
|
+
${platform}
|
|
1236
|
+
or (p_scope <> 'global' and p_tenant is null and p_scope_id is null)
|
|
1237
|
+
or (p_scope <> 'global' and (${memberOf}))
|
|
1238
|
+
)) then
|
|
1239
|
+
${raiseSql(" ", "42501", `'permdock: the caller is not a member of ' || coalesce(p_tenant::text, 'the platform')`, "not-member")}
|
|
1240
|
+
end if;`);
|
|
1241
|
+
trustedBody = checked("");
|
|
1242
|
+
}
|
|
1243
|
+
chunks.push(`-- the permission keys a role holds on a scope, with effect allow or deny; a custom role is read for its tenant and, below the first scope, its instance
|
|
1244
|
+
create or replace function ${reader}(p_role text, p_scope text, p_tenant ${tenantType} default null, p_scope_id text default null)
|
|
1245
|
+
returns table (permission text, effect text)
|
|
1246
|
+
language plpgsql
|
|
1213
1247
|
stable
|
|
1214
1248
|
security definer
|
|
1215
1249
|
set search_path = ''
|
|
1216
1250
|
as $$
|
|
1217
|
-
${body
|
|
1218
|
-
|
|
1219
|
-
|
|
1251
|
+
${body}
|
|
1252
|
+
$$;
|
|
1253
|
+
revoke execute on function ${reader}(text, text, ${tenantType}, text) from public, anon;
|
|
1254
|
+
grant execute on function ${reader}(text, text, ${tenantType}, text) to authenticated;`);
|
|
1255
|
+
const trusted = qualified(ctx, READERS.trusted);
|
|
1256
|
+
const signature = `text, text, ${tenantType}, text`;
|
|
1257
|
+
const grants = trustedReaders === void 0 || trustedReaders.length === 0 ? "" : `\n${trustedReaderGrants(ctx, `${trusted}(${signature})`, trustedReaders)}`;
|
|
1258
|
+
chunks.push(`create or replace function ${trusted}(p_role text, p_scope text, p_tenant ${tenantType} default null, p_scope_id text default null)
|
|
1259
|
+
returns table (permission text, effect text)
|
|
1260
|
+
language plpgsql
|
|
1220
1261
|
stable
|
|
1221
1262
|
security definer
|
|
1222
1263
|
set search_path = ''
|
|
1223
1264
|
as $$
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1265
|
+
${trustedBody}
|
|
1266
|
+
$$;
|
|
1267
|
+
revoke execute on function ${trusted}(${signature}) from public, anon, authenticated;${grants}`);
|
|
1268
|
+
return chunks.join("\n\n");
|
|
1269
|
+
}
|
|
1270
|
+
/** The stored definition of `p_role` at `p_scope` for `p_tenant`: the shared rows when a shared role has the name. */
|
|
1271
|
+
function storedTenant(ctx) {
|
|
1272
|
+
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`;
|
|
1231
1273
|
}
|
|
1232
1274
|
/**
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1275
|
+
* The tenant half of a custom-role row match for a membership: the
|
|
1276
|
+
* membership's tenant, or no tenant when a shared role of that name exists at
|
|
1277
|
+
* the scope, so a shared role shadows a tenant role of the same name.
|
|
1235
1278
|
*/
|
|
1236
|
-
function
|
|
1237
|
-
return (
|
|
1279
|
+
function customTenantMatch(ctx, tenant, scope, role) {
|
|
1280
|
+
return `case when ${qualified(ctx, CUSTOM_ROLES.shared)}(${quoteLiteral(scope)}, ${role}) then c.tenant_id is null else c.tenant_id::text = ${tenant} end`;
|
|
1238
1281
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1282
|
+
/**
|
|
1283
|
+
* The scope of a row of the application's roles table: its scope column (or
|
|
1284
|
+
* the first scope) with a tenant; with no tenant, the same scope when the
|
|
1285
|
+
* `shared` column is true, else `'global'`.
|
|
1286
|
+
*/
|
|
1287
|
+
function rolesTableScope(table, tenant, root, column) {
|
|
1288
|
+
const own = table.scope === void 0 ? quoteLiteral(root) : `${column(table.scope)}::text`;
|
|
1289
|
+
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`;
|
|
1245
1290
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
const sources = scopeSources(ctx, scope);
|
|
1251
|
-
const set = ctx.grantSet === true;
|
|
1252
|
-
const rows = ` from (
|
|
1253
|
-
${sourceRows(sources)}
|
|
1254
|
-
) ms
|
|
1255
|
-
cross join lateral jsonb_array_elements_text(
|
|
1256
|
-
case jsonb_typeof(ms.roles) when 'array' then ms.roles else '[]'::jsonb end
|
|
1257
|
-
) r(role)`;
|
|
1258
|
-
const declared = ` select (ms.id)::${type}${set ? ", rp.grant_key" : ""}
|
|
1259
|
-
${rows}
|
|
1260
|
-
join ${qualified(ctx, "role_permissions")} rp on rp.role = r.role
|
|
1261
|
-
where ${signedIn(ctx)}
|
|
1262
|
-
and ms.scope = ${quoteLiteral(scope)}${set ? "" : "\n and rp.grant_key = p_grant"}
|
|
1263
|
-
and rp.scope = ${quoteLiteral(scope)}${andLine(" ", kindFilterSql(ctx, "r.role", "ms.via"))}${narrow}${sourceFilters(ctx, scope, subjectIdSql(ctx), grantPermissionSql(set ? "rp.grant_key" : "p_grant"), sourcesKeep(sources))}`;
|
|
1264
|
-
const custom = ctx.customRoles;
|
|
1265
|
-
if (custom === void 0) return sourcesBodyOf(declared, sources, subjectIdSql(ctx));
|
|
1266
|
-
const match = [
|
|
1267
|
-
`c.tenant_id::text = (${tenant})::text`,
|
|
1268
|
-
`c.scope = ${quoteLiteral(scope)}`,
|
|
1269
|
-
"(c.scope_id is null or c.scope_id = (ms.id)::text)",
|
|
1270
|
-
"c.role = r.role"
|
|
1271
|
-
].join(" and ");
|
|
1272
|
-
const entries = (source, value, extra) => `array(select c.${value} from ${qualified(ctx, source)} c where ${match}${extra})`;
|
|
1273
|
-
if (set) return sourcesBodyOf(`${declared}
|
|
1274
|
-
union
|
|
1275
|
-
select (ms.id)::${type}, ck.grant_key
|
|
1276
|
-
${rows}
|
|
1277
|
-
${customKeysJoin(ctx, scope, entries(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'allow'"), entries(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), entries(CUSTOM_ROLES.includes, "include_role", ""))}
|
|
1278
|
-
where ${signedIn(ctx)}
|
|
1279
|
-
and ms.scope = ${quoteLiteral(scope)}
|
|
1280
|
-
and not (r.role = any(${sqlTextArray(custom.declared)}))${narrow}${sourceFilters(ctx, scope, subjectIdSql(ctx), grantPermissionSql("ck.grant_key"), sourcesKeep(sources))}`, sources, subjectIdSql(ctx));
|
|
1281
|
-
return sourcesBodyOf(`${declared}
|
|
1282
|
-
union
|
|
1283
|
-
select (ms.id)::${type}
|
|
1284
|
-
${rows}
|
|
1285
|
-
where ${signedIn(ctx)}
|
|
1286
|
-
and ms.scope = ${quoteLiteral(scope)}
|
|
1287
|
-
and not (r.role = any(${sqlTextArray(custom.declared)}))${narrow}${sourceFilters(ctx, scope, subjectIdSql(ctx), grantPermissionSql("p_grant"), sourcesKeep(sources))}
|
|
1288
|
-
${customKeysSql(ctx, scope, entries(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'allow'"), entries(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), entries(CUSTOM_ROLES.includes, "include_role", ""))}`, sources, subjectIdSql(ctx));
|
|
1291
|
+
/** `expr` read as its current key: a former key maps to the key it was renamed to. */
|
|
1292
|
+
function currentKeyExpr(renames, expr) {
|
|
1293
|
+
if (renames.length === 0) return expr;
|
|
1294
|
+
return `(case ${expr} ${renames.map(([former, current]) => `when ${quoteLiteral(former)} then ${quoteLiteral(current)}`).join(" ")} else ${expr} end)`;
|
|
1289
1295
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1296
|
+
/** `case p_scope when '<scope>' then <sql for scope> ... else false end` over the declared scopes. */
|
|
1297
|
+
function perScope(ctx, sql) {
|
|
1298
|
+
const arms = ctx.scopes.map((scope) => `when ${quoteLiteral(scope.name)} then ${sql(scope.name)}`).join(" ");
|
|
1299
|
+
return arms === "" ? "false" : `(case p_scope ${arms} else false end)`;
|
|
1300
|
+
}
|
|
1301
|
+
function raiseSql(indent, errcode, message, hint) {
|
|
1302
|
+
return `${indent}raise exception using
|
|
1303
|
+
${indent} errcode = '${errcode}',
|
|
1304
|
+
${indent} message = ${message},
|
|
1305
|
+
${indent} hint = ${quoteLiteral(hint)};`;
|
|
1306
|
+
}
|
|
1307
|
+
function hasCustomRoleChecksFor(ctx) {
|
|
1308
|
+
return ctx.authorize === "database" && ctx.customRoles !== void 0 && ctx.scopes.length > 0 && ctx.scopes.every((scope) => hasMemberFor(ctx, scope.name));
|
|
1293
1309
|
}
|
|
1294
|
-
//#endregion
|
|
1295
|
-
//#region src/cli/rls/capabilities.ts
|
|
1296
|
-
/** The helper `--capabilities` adds: ids of one resource a link capability claim reaches. Part of the SQL contract. */
|
|
1297
|
-
const CAPABILITIES = { ids: "permdock_capability_ids" };
|
|
1298
1310
|
/**
|
|
1299
|
-
* The `
|
|
1300
|
-
*
|
|
1301
|
-
*
|
|
1302
|
-
* statement; the column is compared as text because the claim holds text.
|
|
1311
|
+
* The grants a `rls.trustedReaders` role needs for one reader: usage on the
|
|
1312
|
+
* helper schema, which no client role reaches through these lines, and
|
|
1313
|
+
* execute on the function.
|
|
1303
1314
|
*/
|
|
1304
|
-
function
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
permission
|
|
1309
|
-
].map(quoteLiteral).join(", ");
|
|
1310
|
-
return `${quoteIdent(field)}::text in (select ${qualified(ctx, CAPABILITIES.ids)}(${args}))`;
|
|
1315
|
+
function trustedReaderGrants(ctx, fn, readers) {
|
|
1316
|
+
const to = readers.map(quoteIdent).join(", ");
|
|
1317
|
+
return `grant usage on schema ${quoteIdent(helperSchema(ctx))} to ${to};
|
|
1318
|
+
grant execute on function ${fn} to ${to};`;
|
|
1311
1319
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
return
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1320
|
+
//#endregion
|
|
1321
|
+
//#region src/cli/sql/global-roles.ts
|
|
1322
|
+
function qualify(name, schema) {
|
|
1323
|
+
return name.includes(".") ? name : `${schema}.${name}`;
|
|
1324
|
+
}
|
|
1325
|
+
function throughTenant(through, roles) {
|
|
1326
|
+
if (through === void 0 || roles?.tenant === void 0) return;
|
|
1327
|
+
return qualify(roles.table, "public") === through.table ? roles.tenant : void 0;
|
|
1328
|
+
}
|
|
1329
|
+
/** Resolves `source` against `schema`, aliasing the table `alias` and the roles table `<alias>k`. */
|
|
1330
|
+
function globalRoleSource(source, schema, alias, roles) {
|
|
1331
|
+
const table = qualify(source.table, schema);
|
|
1332
|
+
const user = source.user ?? "user_id";
|
|
1333
|
+
const role = roleColumn(source.role ?? "role", table, alias, {
|
|
1334
|
+
label: "roles.role",
|
|
1335
|
+
prefix: "PermDock CLI",
|
|
1336
|
+
indent: " "
|
|
1337
|
+
});
|
|
1338
|
+
const tenant = role.lateral ? void 0 : throughTenant(role.through, roles);
|
|
1339
|
+
const shared = tenant === void 0 ? void 0 : roles?.shared;
|
|
1340
|
+
const global = tenant === void 0 ? "" : ` and ${alias}k.${quoteIdent(tenant)} is null${shared === void 0 ? "" : ` and ${alias}k.${quoteIdent(shared)} is not true`}`;
|
|
1341
|
+
return {
|
|
1342
|
+
table,
|
|
1343
|
+
user,
|
|
1344
|
+
from: `${quoteTable(table)} ${alias}${role.join}${global}`,
|
|
1345
|
+
userSql: `${alias}.${quoteIdent(user)}`,
|
|
1346
|
+
roleSql: role.sql,
|
|
1347
|
+
nullable: role.nullable,
|
|
1348
|
+
role: roleManifest(role),
|
|
1349
|
+
...role.through === void 0 ? {} : { through: role.through },
|
|
1350
|
+
...tenant === void 0 ? {} : { tenant },
|
|
1351
|
+
...shared === void 0 ? {} : { shared }
|
|
1352
|
+
};
|
|
1336
1353
|
}
|
|
1337
1354
|
//#endregion
|
|
1338
|
-
//#region src/cli/rls/
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
*/
|
|
1351
|
-
function
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1354
|
-
const
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
for (const [name, column, check] of [[
|
|
1361
|
-
CUSTOM_ROLES.permissions,
|
|
1362
|
-
"permission",
|
|
1363
|
-
`,
|
|
1364
|
-
effect text not null default 'allow' check (effect in ('allow', 'deny'))`
|
|
1365
|
-
], [
|
|
1366
|
-
CUSTOM_ROLES.includes,
|
|
1367
|
-
"include_role",
|
|
1368
|
-
""
|
|
1369
|
-
]]) {
|
|
1370
|
-
const table = qualified(ctx, name);
|
|
1371
|
-
const leveled = custom.levels === true && name === CUSTOM_ROLES.permissions;
|
|
1372
|
-
const unique = `${column}${name === CUSTOM_ROLES.permissions ? ", effect" : ""}${leveled ? ", coalesce(level, '')" : ""}`;
|
|
1373
|
-
const index = leveled ? `drop index if exists ${qualified(ctx, quoteIdent(`${name}_key`))};
|
|
1374
|
-
alter table ${table} add column if not exists level text check (level ~ '^[a-z][a-z0-9_]*$');
|
|
1375
|
-
create unique index if not exists ${quoteIdent(`${name}_level_key`)}` : `create unique index if not exists ${quoteIdent(`${name}_key`)}`;
|
|
1376
|
-
chunks.push(`create table if not exists ${table} (
|
|
1377
|
-
tenant_id ${tenantType},
|
|
1378
|
-
scope text not null default ${quoteLiteral(rootName(ctx))},
|
|
1379
|
-
scope_id text,
|
|
1380
|
-
role text not null,
|
|
1381
|
-
${column} text not null${check},
|
|
1382
|
-
check ((scope = 'global') = (tenant_id is null)),
|
|
1383
|
-
check (scope <> 'global' or scope_id is null)
|
|
1384
|
-
);
|
|
1385
|
-
${index}
|
|
1386
|
-
on ${table} (coalesce(tenant_id::text, ''), scope, coalesce(scope_id, ''), role, ${unique});
|
|
1387
|
-
-- a row identity for triggers the application adds, such as an audit trigger that reads the primary key
|
|
1388
|
-
alter table ${table} add column if not exists id bigint generated always as identity primary key;
|
|
1389
|
-
alter table ${table} enable row level security;
|
|
1390
|
-
revoke all on table ${table} from anon, authenticated, public;`);
|
|
1391
|
-
}
|
|
1355
|
+
//#region src/cli/rls/helper-rows.ts
|
|
1356
|
+
function activeTenant(ctx) {
|
|
1357
|
+
return `nullif(${subjectClaimSql(ctx, ctx.tenantClaim)}, '')`;
|
|
1358
|
+
}
|
|
1359
|
+
function keyTenantLine(ctx, scope, tenant) {
|
|
1360
|
+
const keyed = keyTenantSql(ctx, underRoot(ctx, scope) ? tenant : void 0);
|
|
1361
|
+
return keyed === void 0 ? "" : `\n and ${keyed}`;
|
|
1362
|
+
}
|
|
1363
|
+
/** Whether the helper for `scope` narrows to the tenant claim: unless `rls.tenants` is `'all'`, when the first scope is on its chain. */
|
|
1364
|
+
function narrowsTo(ctx, scope) {
|
|
1365
|
+
return ctx.tenants !== "all" && underRoot(ctx, scope);
|
|
1366
|
+
}
|
|
1367
|
+
/** Whether the active tenant narrows memberships of `scope`: the first scope is on its chain. */
|
|
1368
|
+
function underRoot(ctx, scope) {
|
|
1369
|
+
const root = ctx.scopes[0]?.name;
|
|
1370
|
+
let current = ctx.scopes.find((item) => item.name === scope);
|
|
1371
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1372
|
+
while (current !== void 0 && !seen.has(current.name)) {
|
|
1373
|
+
if (current.name === root) return true;
|
|
1374
|
+
seen.add(current.name);
|
|
1375
|
+
const parent = current.within;
|
|
1376
|
+
current = parent === void 0 ? void 0 : ctx.scopes.find((item) => item.name === parent);
|
|
1392
1377
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1378
|
+
return false;
|
|
1379
|
+
}
|
|
1380
|
+
/** `and exists (...)` lines for an active user; empty without `rls.suspension.users`. */
|
|
1381
|
+
function userActive(ctx, indent, user) {
|
|
1382
|
+
return activeUserSql(ctx, user).map((part) => `${indent}and ${part}`);
|
|
1383
|
+
}
|
|
1384
|
+
/** `and exists (...)` lines for every suspendable instance on the chain of `scope`'s membership. */
|
|
1385
|
+
function instancesActive(ctx, scope, idOf, indent, permission) {
|
|
1386
|
+
return activeInstancesSql(ctx, scope, idOf, permission).map((part) => `${indent}and ${part}`);
|
|
1387
|
+
}
|
|
1388
|
+
function roleRows(ctx) {
|
|
1389
|
+
const claim = ctx.roleClaim ?? "user_role";
|
|
1390
|
+
switch (ctx.dialect) {
|
|
1391
|
+
case "guc": return `unnest(string_to_array(nullif(current_setting(${quoteLiteral(`${ctx.gucPrefix}.${claim}`)}, true), ''), ',')) r(role)`;
|
|
1392
|
+
case "supabase":
|
|
1393
|
+
case "neon": {
|
|
1394
|
+
const raw = subjectClaimJsonSql(ctx, claim);
|
|
1395
|
+
const value = ctx.dialect === "supabase" ? `coalesce(nullif(${raw}, 'null'::jsonb), (select auth.jwt()) -> 'app_metadata' -> ${quoteLiteral(claim)})` : raw;
|
|
1396
|
+
return `jsonb_array_elements_text(
|
|
1397
|
+
case jsonb_typeof(${value})
|
|
1398
|
+
when 'array' then ${value}
|
|
1399
|
+
when 'string' then jsonb_build_array(${value})
|
|
1400
|
+
else '[]'::jsonb
|
|
1401
|
+
end
|
|
1402
|
+
) r(role)`;
|
|
1403
|
+
}
|
|
1404
|
+
default: return ctx.dialect;
|
|
1410
1405
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1406
|
+
}
|
|
1407
|
+
function membershipRows(ctx) {
|
|
1408
|
+
const raw = subjectClaimJsonSql(ctx, "memberships");
|
|
1409
|
+
const value = ctx.dialect === "supabase" ? `coalesce(${raw}, (select auth.jwt()) -> 'app_metadata' -> 'memberships')` : raw;
|
|
1410
|
+
return `jsonb_array_elements(
|
|
1411
|
+
case jsonb_typeof(${value}) when 'array' then ${value} else '[]'::jsonb end
|
|
1412
|
+
) m
|
|
1413
|
+
cross join lateral jsonb_array_elements_text(
|
|
1414
|
+
case jsonb_typeof(m -> 'roles') when 'array' then m -> 'roles' else '[]'::jsonb end
|
|
1415
|
+
) r(role)`;
|
|
1416
|
+
}
|
|
1417
|
+
function andLine(indent, condition) {
|
|
1418
|
+
return condition === void 0 ? "" : `\n${indent}and ${condition}`;
|
|
1419
|
+
}
|
|
1420
|
+
/** `rls.roles`, or the generated `user_roles (user_id, role)`, aliased `ur`. */
|
|
1421
|
+
function globalRoleRows(ctx) {
|
|
1422
|
+
return ctx.roles === void 0 ? {
|
|
1423
|
+
from: `${qualified(ctx, "user_roles")} ur`,
|
|
1424
|
+
userSql: "ur.user_id",
|
|
1425
|
+
roleSql: "ur.role::text"
|
|
1426
|
+
} : globalRoleSource(ctx.roles, "public", "ur", ctx.customRoles?.table);
|
|
1427
|
+
}
|
|
1428
|
+
function customKeysJoin(ctx, scope, allows, denies, includes) {
|
|
1429
|
+
return ` cross join lateral ${qualified(ctx, CUSTOM_ROLES.keys)}(
|
|
1430
|
+
${allows},
|
|
1431
|
+
${denies},
|
|
1432
|
+
${includes},
|
|
1433
|
+
${quoteLiteral(scope)}
|
|
1434
|
+
) ck(grant_key)`;
|
|
1435
|
+
}
|
|
1436
|
+
//#endregion
|
|
1437
|
+
//#region src/cli/rls/helper-bodies.ts
|
|
1438
|
+
function hasSetBody(ctx) {
|
|
1439
|
+
const rp = qualified(ctx, "role_permissions");
|
|
1440
|
+
const active = userActive(ctx, " ").map((line) => `\n${line}`).join("");
|
|
1441
|
+
const custom = ctx.customRoles;
|
|
1442
|
+
if (ctx.authorize === "database") {
|
|
1443
|
+
const ur = globalRoleRows(ctx);
|
|
1444
|
+
const declared = ` select rp.grant_key
|
|
1445
|
+
from ${ur.from}
|
|
1446
|
+
join ${rp} rp on rp.role = ${ur.roleSql}
|
|
1447
|
+
where ${ur.userSql} = ${subjectIdSql(ctx)}
|
|
1448
|
+
and rp.scope = 'global'${andLine(" ", globalKindFilterSql(ctx, ur.roleSql))}${active}`;
|
|
1449
|
+
if (custom === void 0) return declared;
|
|
1450
|
+
const match = `c.tenant_id is null and c.scope = 'global' and c.scope_id is null and c.role = ${ur.roleSql}`;
|
|
1451
|
+
const rows = (source, value, extra) => `array(select c.${value} from ${qualified(ctx, source)} c where ${match}${extra})`;
|
|
1452
|
+
return `${declared}
|
|
1458
1453
|
union
|
|
1459
|
-
select
|
|
1460
|
-
|
|
1454
|
+
select ck.grant_key
|
|
1455
|
+
from ${ur.from}
|
|
1456
|
+
${customKeysJoin(ctx, "global", rows(CUSTOM_ROLES.permissions, allowEntry(ctx), " and c.effect = 'allow'"), rows(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), rows(CUSTOM_ROLES.includes, "include_role", ""))}
|
|
1457
|
+
where ${ur.userSql} = ${subjectIdSql(ctx)}
|
|
1458
|
+
and not (${ur.roleSql} = any(${sqlTextArray(custom.declared)}))${active}`;
|
|
1459
|
+
}
|
|
1460
|
+
const declared = ` select rp.grant_key
|
|
1461
|
+
from ${roleRows(ctx)}
|
|
1462
|
+
join ${rp} rp on rp.role = r.role
|
|
1463
|
+
where ${signedIn(ctx)}
|
|
1464
|
+
and rp.scope = 'global'${andLine(" ", globalKindFilterSql(ctx, "r.role"))}${active}`;
|
|
1465
|
+
if (custom === void 0) return declared;
|
|
1466
|
+
const claim = subjectClaimJsonSql(ctx, GLOBAL_GRANTS_CLAIM);
|
|
1467
|
+
return `${declared}
|
|
1461
1468
|
union
|
|
1462
|
-
select
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1469
|
+
select ck.grant_key
|
|
1470
|
+
from ${roleRows(ctx)}
|
|
1471
|
+
cross join lateral (select ${claim} -> r.role as g) cg
|
|
1472
|
+
${customKeysJoin(ctx, "global", claimEntries("left(e, 1) not in ('-', '@')", "e"), claimEntries("left(e, 1) = '-'", "substr(e, 2)"), claimEntries("left(e, 1) = '@'", "substr(e, 2)"))}
|
|
1473
|
+
where ${signedIn(ctx)}
|
|
1474
|
+
and jsonb_typeof(cg.g) = 'array'
|
|
1475
|
+
and not (r.role = any(${sqlTextArray(custom.declared)}))${active}`;
|
|
1467
1476
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
* `key` or `key@level`. A level reaches only the ceiling's `<grant key>@<level>`
|
|
1471
|
-
* keys, plus the ceiling denies of that permission; a level no ceiling key
|
|
1472
|
-
* carries removes the permission, as `resolveCustomRole` drops it with
|
|
1473
|
-
* `unknown-level`.
|
|
1474
|
-
*/
|
|
1475
|
-
function leveledKeysSql(ctx, renames) {
|
|
1477
|
+
function hasBody(ctx) {
|
|
1478
|
+
if (ctx.grantSet === true) return hasSetBody(ctx);
|
|
1476
1479
|
const rp = qualified(ctx, "role_permissions");
|
|
1477
|
-
const
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
)
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
select
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1480
|
+
const active = userActive(ctx, " ").map((line) => `\n${line}`).join("");
|
|
1481
|
+
const custom = ctx.customRoles;
|
|
1482
|
+
if (ctx.authorize === "database") {
|
|
1483
|
+
const ur = globalRoleRows(ctx);
|
|
1484
|
+
const declared = ` select exists (
|
|
1485
|
+
select 1
|
|
1486
|
+
from ${ur.from}
|
|
1487
|
+
join ${rp} rp on rp.role = ${ur.roleSql}
|
|
1488
|
+
where ${ur.userSql} = ${subjectIdSql(ctx)}
|
|
1489
|
+
and rp.grant_key = p_grant
|
|
1490
|
+
and rp.scope = 'global'${andLine(" ", globalKindFilterSql(ctx, ur.roleSql))}${active}
|
|
1491
|
+
)`;
|
|
1492
|
+
if (custom === void 0) return declared;
|
|
1493
|
+
const match = `c.tenant_id is null and c.scope = 'global' and c.scope_id is null and c.role = ${ur.roleSql}`;
|
|
1494
|
+
const rows = (source, value, extra) => `array(select c.${value} from ${qualified(ctx, source)} c where ${match}${extra})`;
|
|
1495
|
+
return `${declared}
|
|
1496
|
+
or exists (
|
|
1497
|
+
select 1
|
|
1498
|
+
from ${ur.from}
|
|
1499
|
+
where ${ur.userSql} = ${subjectIdSql(ctx)}
|
|
1500
|
+
and not (${ur.roleSql} = any(${sqlTextArray(custom.declared)}))${active}
|
|
1501
|
+
${customKeysSql(ctx, "global", rows(CUSTOM_ROLES.permissions, allowEntry(ctx), " and c.effect = 'allow'"), rows(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), rows(CUSTOM_ROLES.includes, "include_role", "")).trimStart()}
|
|
1502
|
+
)`;
|
|
1503
|
+
}
|
|
1504
|
+
const declared = ` select ${signedIn(ctx)} and exists (
|
|
1505
|
+
select 1
|
|
1506
|
+
from ${roleRows(ctx)}
|
|
1507
|
+
join ${rp} rp on rp.role = r.role
|
|
1508
|
+
where rp.grant_key = p_grant
|
|
1509
|
+
and rp.scope = 'global'${andLine(" ", globalKindFilterSql(ctx, "r.role"))}${active}
|
|
1510
|
+
)`;
|
|
1511
|
+
if (custom === void 0) return declared;
|
|
1512
|
+
const claim = subjectClaimJsonSql(ctx, GLOBAL_GRANTS_CLAIM);
|
|
1513
|
+
return `${declared}
|
|
1514
|
+
or (${signedIn(ctx)} and exists (
|
|
1515
|
+
select 1
|
|
1516
|
+
from ${roleRows(ctx)}
|
|
1517
|
+
cross join lateral (select ${claim} -> r.role as g) cg
|
|
1518
|
+
where jsonb_typeof(cg.g) = 'array'
|
|
1519
|
+
and not (r.role = any(${sqlTextArray(custom.declared)}))${active}
|
|
1520
|
+
${customKeysSql(ctx, "global", claimEntries("left(e, 1) not in ('-', '@')", "e"), claimEntries("left(e, 1) = '-'", "substr(e, 2)"), claimEntries("left(e, 1) = '@'", "substr(e, 2)")).trimStart()}
|
|
1521
|
+
))`;
|
|
1522
|
+
}
|
|
1523
|
+
/** The claim `jwt` mode reads platform custom roles from: `customRoleClaim(globalRoles)`. */
|
|
1524
|
+
const GLOBAL_GRANTS_CLAIM = "role_grants";
|
|
1525
|
+
function memberColumn(name) {
|
|
1526
|
+
return `m.${quoteIdent(name)}`;
|
|
1527
|
+
}
|
|
1528
|
+
function disabledColumn(table) {
|
|
1529
|
+
return table.disabledAt === void 0 ? void 0 : memberColumn(table.disabledAt);
|
|
1530
|
+
}
|
|
1531
|
+
function claimKeptLines(ctx, indent, permission) {
|
|
1532
|
+
return disabledKeep(ctx).length === 0 ? [] : [`${indent}and ${keptRowSql("m -> 'keep'", permission)}`];
|
|
1533
|
+
}
|
|
1534
|
+
/** Entries of a compact custom-role claim (`cg.g`) that match `where`. */
|
|
1535
|
+
function claimEntries(where, value) {
|
|
1536
|
+
return `array(select ${value} from jsonb_array_elements_text(cg.g) e where ${where})`;
|
|
1537
|
+
}
|
|
1538
|
+
/** `p_grant in (select permdock_custom_keys(allows, denies, includes, scope))`. */
|
|
1539
|
+
function customKeysSql(ctx, scope, allows, denies, includes) {
|
|
1540
|
+
return ` and p_grant in (select ${qualified(ctx, CUSTOM_ROLES.keys)}(
|
|
1541
|
+
${allows},
|
|
1542
|
+
${denies},
|
|
1543
|
+
${includes},
|
|
1544
|
+
${quoteLiteral(scope)}
|
|
1545
|
+
))`;
|
|
1546
|
+
}
|
|
1547
|
+
function tableBody(ctx, scope, type) {
|
|
1548
|
+
const mapped = scopeTable(ctx, scope);
|
|
1549
|
+
if (mapped === void 0) return ` select null::${type} where false -- no ${scope} memberships table configured`;
|
|
1550
|
+
const { table, column, tenantColumn } = mapped;
|
|
1551
|
+
const set = ctx.grantSet === true;
|
|
1552
|
+
const filtersFor = (grant) => {
|
|
1553
|
+
const filters = [` where ${memberColumn(table.user)} = ${subjectIdSql(ctx)}`];
|
|
1554
|
+
if (table.expiresAt !== void 0) {
|
|
1555
|
+
const expires = memberColumn(table.expiresAt);
|
|
1556
|
+
filters.push(` and (${expires} is null or ${expires} > now())`);
|
|
1557
|
+
}
|
|
1558
|
+
filters.push(...activeMembershipSql(ctx, disabledColumn(table), grantPermissionSql(grant)).map((part) => ` and ${part}`));
|
|
1559
|
+
if (tenantColumn !== void 0 && ctx.tenants !== "all") filters.push(` and (${activeTenant(ctx)} is null or ${memberColumn(tenantColumn)}::text = ${activeTenant(ctx)})`);
|
|
1560
|
+
const keyed = keyTenantSql(ctx, tenantColumn === void 0 ? void 0 : memberColumn(tenantColumn));
|
|
1561
|
+
if (keyed !== void 0) filters.push(` and ${keyed}`);
|
|
1562
|
+
filters.push(...userActive(ctx, " "), ...instancesActive(ctx, scope, (name) => {
|
|
1563
|
+
const held = scopeColumn(table, ctx.scopes, name);
|
|
1564
|
+
return held === void 0 ? void 0 : memberColumn(held);
|
|
1565
|
+
}, " ", grantPermissionSql(grant)));
|
|
1566
|
+
return filters;
|
|
1567
|
+
};
|
|
1568
|
+
const filters = filtersFor(set ? "rp.grant_key" : "p_grant");
|
|
1569
|
+
const [owner, ...rest] = filters;
|
|
1570
|
+
const role = memberRoleOf(table, "m", " ");
|
|
1571
|
+
const kind = kindFilterSql(ctx, role.sql, memberVia(table));
|
|
1572
|
+
const lines = [
|
|
1573
|
+
` select${role.lateral ? " distinct" : ""} ${memberColumn(column)}::${type}${set ? ", rp.grant_key" : ""}`,
|
|
1574
|
+
` from ${quoteQualified(table.table)} m${role.join}`,
|
|
1575
|
+
` join ${qualified(ctx, "role_permissions")} rp on rp.role = ${role.sql}`,
|
|
1576
|
+
owner ?? "",
|
|
1577
|
+
...set ? [] : [" and rp.grant_key = p_grant"],
|
|
1578
|
+
` and rp.scope = ${quoteLiteral(scope)}`,
|
|
1579
|
+
...kind === void 0 ? [] : [` and ${kind}`],
|
|
1580
|
+
...rest
|
|
1581
|
+
];
|
|
1582
|
+
const custom = ctx.customRoles;
|
|
1583
|
+
if (custom === void 0) return lines.join("\n");
|
|
1584
|
+
const match = [
|
|
1585
|
+
customTenantMatch(ctx, `${memberColumn(tenantColumn ?? column)}::text`, scope, role.sql),
|
|
1586
|
+
`c.scope = ${quoteLiteral(scope)}`,
|
|
1587
|
+
`(c.scope_id is null or c.scope_id = ${memberColumn(column)}::text)`,
|
|
1588
|
+
`c.role = ${role.sql}`
|
|
1589
|
+
].join(" and ");
|
|
1590
|
+
const rows = (source, value, extra) => `array(select c.${value} from ${qualified(ctx, source)} c where ${match}${extra})`;
|
|
1591
|
+
const entries = [
|
|
1592
|
+
rows(CUSTOM_ROLES.permissions, allowEntry(ctx), " and c.effect = 'allow'"),
|
|
1593
|
+
rows(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"),
|
|
1594
|
+
rows(CUSTOM_ROLES.includes, "include_role", "")
|
|
1595
|
+
];
|
|
1596
|
+
if (set) return [
|
|
1597
|
+
...lines,
|
|
1598
|
+
" union",
|
|
1599
|
+
` select ${memberColumn(column)}::${type}, ck.grant_key`,
|
|
1600
|
+
` from ${quoteQualified(table.table)} m${role.join}`,
|
|
1601
|
+
customKeysJoin(ctx, scope, ...entries),
|
|
1602
|
+
...filtersFor("ck.grant_key"),
|
|
1603
|
+
` and not (${role.sql} = any(${sqlTextArray(custom.declared)}))`
|
|
1604
|
+
].join("\n");
|
|
1605
|
+
return [
|
|
1606
|
+
...lines,
|
|
1607
|
+
" union",
|
|
1608
|
+
` select ${memberColumn(column)}::${type}`,
|
|
1609
|
+
` from ${quoteQualified(table.table)} m${role.join}`,
|
|
1610
|
+
...filters,
|
|
1611
|
+
` and not (${role.sql} = any(${sqlTextArray(custom.declared)}))`,
|
|
1612
|
+
customKeysSql(ctx, scope, ...entries)
|
|
1613
|
+
].join("\n");
|
|
1614
|
+
}
|
|
1615
|
+
/** The membership's instance of the first scope, from the canonical claim. */
|
|
1616
|
+
function claimTenant(ctx, scope) {
|
|
1617
|
+
const root = rootName(ctx);
|
|
1618
|
+
return scope === root ? `m ->> 'id'` : `m -> 'within' ->> ${quoteLiteral(root)}`;
|
|
1619
|
+
}
|
|
1620
|
+
function claimBody(ctx, scope, type) {
|
|
1621
|
+
const set = ctx.grantSet === true;
|
|
1622
|
+
const narrow = narrowsTo(ctx, scope) ? `
|
|
1623
|
+
and (${activeTenant(ctx)} is null or ${claimTenant(ctx, scope)} = ${activeTenant(ctx)})` : "";
|
|
1624
|
+
const filtersFor = (grant) => ` and m ->> 'scope' = ${quoteLiteral(scope)}
|
|
1625
|
+
and m ->> 'id' is not null${narrow}${keyTenantLine(ctx, scope, claimTenant(ctx, scope))}
|
|
1626
|
+
and case jsonb_typeof(m -> 'expires_at')
|
|
1627
|
+
when 'number' then (m ->> 'expires_at')::numeric > extract(epoch from now())
|
|
1628
|
+
else true
|
|
1629
|
+
end${[
|
|
1630
|
+
...claimKeptLines(ctx, " ", grantPermissionSql(grant)),
|
|
1631
|
+
...userActive(ctx, " "),
|
|
1632
|
+
...instancesActive(ctx, scope, (name) => name === scope ? `m ->> 'id'` : `m -> 'within' ->> ${quoteLiteral(name)}`, " ", grantPermissionSql(grant))
|
|
1633
|
+
].map((line) => `\n${line}`).join("")}`;
|
|
1634
|
+
const filters = filtersFor(set ? "rp.grant_key" : "p_grant");
|
|
1635
|
+
const declared = ` select (m ->> 'id')::${type}${set ? ", rp.grant_key" : ""}
|
|
1636
|
+
from ${membershipRows(ctx)}
|
|
1637
|
+
join ${qualified(ctx, "role_permissions")} rp on rp.role = r.role
|
|
1638
|
+
where ${signedIn(ctx)}${set ? "" : "\n and rp.grant_key = p_grant"}
|
|
1639
|
+
and rp.scope = ${quoteLiteral(scope)}${andLine(" ", kindFilterSql(ctx, "r.role", "m ->> 'via'"))}
|
|
1640
|
+
${filters}`;
|
|
1641
|
+
const custom = ctx.customRoles;
|
|
1642
|
+
if (custom === void 0) return declared;
|
|
1643
|
+
if (set) return `${declared}
|
|
1558
1644
|
union
|
|
1559
|
-
select
|
|
1560
|
-
|
|
1645
|
+
select (m ->> 'id')::${type}, ck.grant_key
|
|
1646
|
+
from ${membershipRows(ctx)}
|
|
1647
|
+
cross join lateral (select m -> 'grants' -> r.role as g) cg
|
|
1648
|
+
${customKeysJoin(ctx, scope, claimEntries("left(e, 1) not in ('-', '@')", "e"), claimEntries("left(e, 1) = '-'", "substr(e, 2)"), claimEntries("left(e, 1) = '@'", "substr(e, 2)"))}
|
|
1649
|
+
where ${signedIn(ctx)}
|
|
1650
|
+
and jsonb_typeof(cg.g) = 'array'
|
|
1651
|
+
and not (r.role = any(${sqlTextArray(custom.declared)}))
|
|
1652
|
+
${filtersFor("ck.grant_key")}`;
|
|
1653
|
+
return `${declared}
|
|
1561
1654
|
union
|
|
1562
|
-
select
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
from unnest(coalesce(${array}, '{}'::text[])) as e(permission)
|
|
1571
|
-
left join renamed r on r.former = e.permission`;
|
|
1655
|
+
select (m ->> 'id')::${type}
|
|
1656
|
+
from ${membershipRows(ctx)}
|
|
1657
|
+
cross join lateral (select m -> 'grants' -> r.role as g) cg
|
|
1658
|
+
where ${signedIn(ctx)}
|
|
1659
|
+
and jsonb_typeof(cg.g) = 'array'
|
|
1660
|
+
and not (r.role = any(${sqlTextArray(custom.declared)}))
|
|
1661
|
+
${filters}
|
|
1662
|
+
${customKeysSql(ctx, scope, claimEntries("left(e, 1) not in ('-', '@')", "e"), claimEntries("left(e, 1) = '-'", "substr(e, 2)"), claimEntries("left(e, 1) = '@'", "substr(e, 2)"))}`;
|
|
1572
1663
|
}
|
|
1573
1664
|
/**
|
|
1574
|
-
*
|
|
1575
|
-
* `
|
|
1576
|
-
*
|
|
1665
|
+
* The rows the membership sources select for the subject, one per instance
|
|
1666
|
+
* (`scope`, `id`, `within`, `roles`, `via`), with each source's own expiry
|
|
1667
|
+
* and suspension filters: the statements the token hook runs.
|
|
1577
1668
|
*/
|
|
1578
|
-
function
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1669
|
+
function sourcesKeep(sources) {
|
|
1670
|
+
return sources.some((source) => source.sql.keeps);
|
|
1671
|
+
}
|
|
1672
|
+
function sourceRows(sources) {
|
|
1673
|
+
const keep = sourcesKeep(sources);
|
|
1674
|
+
return sources.map((source, index) => source.sql.select(sourceUser(index), keep).replaceAll(/^/gmu, " ")).join("\n union all\n");
|
|
1675
|
+
}
|
|
1676
|
+
function sourceUser(index) {
|
|
1677
|
+
return `v_user_${String(index)}`;
|
|
1678
|
+
}
|
|
1679
|
+
function sqlBody(sql) {
|
|
1680
|
+
return {
|
|
1681
|
+
sql,
|
|
1682
|
+
vars: []
|
|
1585
1683
|
};
|
|
1684
|
+
}
|
|
1685
|
+
function sourcesBodyOf(sql, sources, user) {
|
|
1586
1686
|
return {
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
${currentKeysSql("p_deny")}
|
|
1590
|
-
),
|
|
1591
|
-
`,
|
|
1592
|
-
permission: (alias) => `coalesce(r_${alias}.key, ${alias}.permission) as permission`,
|
|
1593
|
-
join: (alias) => ` left join renamed r_${alias} on r_${alias}.former = ${alias}.permission`,
|
|
1594
|
-
allow: currentKeysSql("p_allow"),
|
|
1595
|
-
denied: (alias) => `${alias}.permission in (select d.permission from denied d)`
|
|
1687
|
+
sql,
|
|
1688
|
+
vars: sources.map((source, index) => `${sourceUser(index)} ${source.sql.userType} := ${user};`)
|
|
1596
1689
|
};
|
|
1597
1690
|
}
|
|
1598
|
-
/**
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
* for `authenticated`. A declared role answers from `role_permissions`, which
|
|
1602
|
-
* is the catalog. A custom role (`database` mode) answers from the tenant's
|
|
1603
|
-
* own rows through `permdock_custom_keys`, only to a member of the tenant or
|
|
1604
|
-
* a holder of a `meta.manageRoles` permission for a platform role. The
|
|
1605
|
-
* effect is `deny` for a deny the role carries after the ceiling, as
|
|
1606
|
-
* `resolveCustomRole` returns it, never for a deny that merely removes an allow.
|
|
1607
|
-
*/
|
|
1608
|
-
function roleReadersSql(ctx, permissions, trustedReaders) {
|
|
1609
|
-
const rp = qualified(ctx, "role_permissions");
|
|
1610
|
-
const reader = qualified(ctx, READERS.roles);
|
|
1611
|
-
const tenantType = tenantTypeOf(ctx);
|
|
1612
|
-
const custom = ctx.customRoles;
|
|
1613
|
-
const root = ctx.scopes[0]?.name;
|
|
1614
|
-
const database = ctx.authorize === "database";
|
|
1615
|
-
const chunks = [];
|
|
1616
|
-
if (permissions !== void 0) {
|
|
1617
|
-
const catalog = qualified(ctx, READERS.keys);
|
|
1618
|
-
chunks.push(`-- every declared permission key
|
|
1619
|
-
create or replace function ${catalog}()
|
|
1620
|
-
returns setof text
|
|
1621
|
-
language sql
|
|
1691
|
+
/** `language sql`, or `plpgsql` returning the query when the body declares variables. */
|
|
1692
|
+
function functionBody(body) {
|
|
1693
|
+
if (body.vars.length === 0) return `language sql
|
|
1622
1694
|
stable
|
|
1695
|
+
security definer
|
|
1623
1696
|
set search_path = ''
|
|
1624
1697
|
as $$
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1698
|
+
${body.sql}
|
|
1699
|
+
$$;`;
|
|
1700
|
+
return `language plpgsql
|
|
1701
|
+
stable
|
|
1702
|
+
security definer
|
|
1703
|
+
set search_path = ''
|
|
1704
|
+
as $$
|
|
1705
|
+
declare
|
|
1706
|
+
${body.vars.map((line) => ` ${line}`).join("\n")}
|
|
1707
|
+
begin
|
|
1708
|
+
return query
|
|
1709
|
+
${body.sql};
|
|
1710
|
+
end;
|
|
1711
|
+
$$;`;
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* `ms.id` for the scope itself and `ms.within ->> name` for an ancestor. The
|
|
1715
|
+
* alias is not `s`, which `activeRowSql` binds inside its `exists`.
|
|
1716
|
+
*/
|
|
1717
|
+
function sourceIdOf(scope) {
|
|
1718
|
+
return (name) => name === scope ? "ms.id" : `ms.within ->> ${quoteLiteral(name)}`;
|
|
1719
|
+
}
|
|
1720
|
+
function sourceFilters(ctx, scope, user = subjectIdSql(ctx), permission, keeps = false) {
|
|
1721
|
+
return [
|
|
1722
|
+
...keeps ? [` and ${keptRowSql("ms.keep", permission)}`] : [],
|
|
1723
|
+
...userActive(ctx, " ", user),
|
|
1724
|
+
...instancesActive(ctx, scope, sourceIdOf(scope), " ", permission)
|
|
1725
|
+
].map((line) => `\n${line}`).join("");
|
|
1726
|
+
}
|
|
1727
|
+
function sourcesBody(ctx, scope, type) {
|
|
1728
|
+
const root = rootName(ctx);
|
|
1729
|
+
const tenant = sourceIdOf(scope)(root);
|
|
1730
|
+
const narrow = narrowsTo(ctx, scope) ? `\n and (${activeTenant(ctx)} is null or ${tenant} = ${activeTenant(ctx)})${keyTenantLine(ctx, scope, tenant)}` : keyTenantLine(ctx, scope, tenant);
|
|
1731
|
+
const sources = scopeSources(ctx, scope);
|
|
1732
|
+
const set = ctx.grantSet === true;
|
|
1733
|
+
const rows = ` from (
|
|
1734
|
+
${sourceRows(sources)}
|
|
1735
|
+
) ms
|
|
1736
|
+
cross join lateral jsonb_array_elements_text(
|
|
1737
|
+
case jsonb_typeof(ms.roles) when 'array' then ms.roles else '[]'::jsonb end
|
|
1738
|
+
) r(role)`;
|
|
1739
|
+
const declared = ` select (ms.id)::${type}${set ? ", rp.grant_key" : ""}
|
|
1740
|
+
${rows}
|
|
1741
|
+
join ${qualified(ctx, "role_permissions")} rp on rp.role = r.role
|
|
1742
|
+
where ${signedIn(ctx)}
|
|
1743
|
+
and ms.scope = ${quoteLiteral(scope)}${set ? "" : "\n and rp.grant_key = p_grant"}
|
|
1744
|
+
and rp.scope = ${quoteLiteral(scope)}${andLine(" ", kindFilterSql(ctx, "r.role", "ms.via"))}${narrow}${sourceFilters(ctx, scope, subjectIdSql(ctx), grantPermissionSql(set ? "rp.grant_key" : "p_grant"), sourcesKeep(sources))}`;
|
|
1745
|
+
const custom = ctx.customRoles;
|
|
1746
|
+
if (custom === void 0) return sourcesBodyOf(declared, sources, subjectIdSql(ctx));
|
|
1747
|
+
const match = [
|
|
1748
|
+
customTenantMatch(ctx, `(${tenant})::text`, scope, "r.role"),
|
|
1749
|
+
`c.scope = ${quoteLiteral(scope)}`,
|
|
1750
|
+
"(c.scope_id is null or c.scope_id = (ms.id)::text)",
|
|
1751
|
+
"c.role = r.role"
|
|
1752
|
+
].join(" and ");
|
|
1753
|
+
const entries = (source, value, extra) => `array(select c.${value} from ${qualified(ctx, source)} c where ${match}${extra})`;
|
|
1754
|
+
if (set) return sourcesBodyOf(`${declared}
|
|
1755
|
+
union
|
|
1756
|
+
select (ms.id)::${type}, ck.grant_key
|
|
1757
|
+
${rows}
|
|
1758
|
+
${customKeysJoin(ctx, scope, entries(CUSTOM_ROLES.permissions, allowEntry(ctx), " and c.effect = 'allow'"), entries(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), entries(CUSTOM_ROLES.includes, "include_role", ""))}
|
|
1759
|
+
where ${signedIn(ctx)}
|
|
1760
|
+
and ms.scope = ${quoteLiteral(scope)}
|
|
1761
|
+
and not (r.role = any(${sqlTextArray(custom.declared)}))${narrow}${sourceFilters(ctx, scope, subjectIdSql(ctx), grantPermissionSql("ck.grant_key"), sourcesKeep(sources))}`, sources, subjectIdSql(ctx));
|
|
1762
|
+
return sourcesBodyOf(`${declared}
|
|
1763
|
+
union
|
|
1764
|
+
select (ms.id)::${type}
|
|
1765
|
+
${rows}
|
|
1766
|
+
where ${signedIn(ctx)}
|
|
1767
|
+
and ms.scope = ${quoteLiteral(scope)}
|
|
1768
|
+
and not (r.role = any(${sqlTextArray(custom.declared)}))${narrow}${sourceFilters(ctx, scope, subjectIdSql(ctx), grantPermissionSql("p_grant"), sourcesKeep(sources))}
|
|
1769
|
+
${customKeysSql(ctx, scope, entries(CUSTOM_ROLES.permissions, allowEntry(ctx), " and c.effect = 'allow'"), entries(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"), entries(CUSTOM_ROLES.includes, "include_role", ""))}`, sources, subjectIdSql(ctx));
|
|
1770
|
+
}
|
|
1771
|
+
function scopedBody(ctx, scope, type) {
|
|
1772
|
+
if (ctx.authorize !== "database") return sqlBody(claimBody(ctx, scope, type));
|
|
1773
|
+
return scopeSources(ctx, scope).length > 0 ? sourcesBody(ctx, scope, type) : sqlBody(tableBody(ctx, scope, type));
|
|
1774
|
+
}
|
|
1775
|
+
//#endregion
|
|
1776
|
+
//#region src/cli/rls/capabilities.ts
|
|
1777
|
+
/** The helper `--capabilities` adds: ids of one resource a link capability claim reaches. Part of the SQL contract. */
|
|
1778
|
+
const CAPABILITIES = { ids: "permdock_capability_ids" };
|
|
1779
|
+
/**
|
|
1780
|
+
* The `anon` branch access for a resource-scoped grant reached through a link
|
|
1781
|
+
* capability: the row's `field` holds an id the capability claim names for
|
|
1782
|
+
* `resource`, `role` and `permission`. Uncorrelated, so it runs once per
|
|
1783
|
+
* statement; the column is compared as text because the claim holds text.
|
|
1784
|
+
*/
|
|
1785
|
+
function capabilityAccessSql(ctx, field, resource, role, permission) {
|
|
1786
|
+
const args = [
|
|
1787
|
+
resource,
|
|
1788
|
+
role,
|
|
1789
|
+
permission
|
|
1790
|
+
].map(quoteLiteral).join(", ");
|
|
1791
|
+
return `${quoteIdent(field)}::text in (select ${qualified(ctx, CAPABILITIES.ids)}(${args}))`;
|
|
1792
|
+
}
|
|
1793
|
+
function capabilitiesSql(ctx) {
|
|
1794
|
+
if (ctx.capabilities !== true) return "";
|
|
1795
|
+
const fn = qualified(ctx, CAPABILITIES.ids);
|
|
1796
|
+
return `-- link capabilities: the capability claim exchangeCapability mints (role anon, short-lived)
|
|
1797
|
+
create or replace function ${fn}(p_resource text, p_role text, p_permission text)
|
|
1631
1798
|
returns setof text
|
|
1632
1799
|
language sql
|
|
1633
1800
|
stable
|
|
1634
|
-
security definer
|
|
1635
1801
|
set search_path = ''
|
|
1636
1802
|
as $$
|
|
1637
|
-
select
|
|
1638
|
-
from ${
|
|
1639
|
-
where
|
|
1640
|
-
and
|
|
1641
|
-
and
|
|
1642
|
-
|
|
1803
|
+
select c -> 'on' ->> 'id'
|
|
1804
|
+
from (select ${subjectClaimJsonSql(ctx, "capability")} as c) capability
|
|
1805
|
+
where jsonb_typeof(c) = 'object'
|
|
1806
|
+
and c ->> 'v' = '1'
|
|
1807
|
+
and c ->> 'holder' = 'link'
|
|
1808
|
+
and c -> 'on' ->> 'resource' = p_resource
|
|
1809
|
+
and jsonb_typeof(c -> 'roles') = 'array'
|
|
1810
|
+
and c -> 'roles' @> jsonb_build_array(p_role)
|
|
1811
|
+
and (c -> 'permissions' is null or c -> 'permissions' @> jsonb_build_array(p_permission))
|
|
1812
|
+
and jsonb_typeof(c -> 'expires_at') = 'number'
|
|
1813
|
+
and (c ->> 'expires_at')::numeric > extract(epoch from now())
|
|
1643
1814
|
$$;
|
|
1644
|
-
revoke execute on function ${
|
|
1645
|
-
grant execute on function ${
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1815
|
+
revoke execute on function ${fn}(text, text, text) from public;
|
|
1816
|
+
grant execute on function ${fn}(text, text, text) to anon, authenticated;`;
|
|
1817
|
+
}
|
|
1818
|
+
//#endregion
|
|
1819
|
+
//#region src/cli/rls/custom-role-by-id.ts
|
|
1820
|
+
const PREFIX = "PermDock CLI";
|
|
1821
|
+
const col = (name) => `r.${quoteIdent(name)}`;
|
|
1822
|
+
/**
|
|
1823
|
+
* Custom-role functions keyed by the application's role row
|
|
1824
|
+
* (`rls.customRoleWrites.roles.primaryKey`): the tenant, scope, instance and
|
|
1825
|
+
* name come from the row, never from the caller. The writers delegate to
|
|
1826
|
+
* `permdock_replace_custom_role_grants` (or its trusted variant), so every
|
|
1827
|
+
* caller and definition check applies, and keep the stored denies and
|
|
1828
|
+
* includes. The reader returns declared allow keys only and is executable by
|
|
1829
|
+
* `rls.trustedReaders` alone.
|
|
1830
|
+
*/
|
|
1831
|
+
function customRoleByIdSql(ctx, trustedReaders) {
|
|
1832
|
+
const custom = ctx.customRoles;
|
|
1833
|
+
const roles = custom?.table;
|
|
1834
|
+
const root = ctx.scopes[0]?.name;
|
|
1835
|
+
if (custom === void 0 || roles?.primaryKey === void 0 || ctx.authorize !== "database" || root === void 0) return "";
|
|
1836
|
+
const pk = roles.primaryKey;
|
|
1837
|
+
if (pk === "") throw configError(`${PREFIX}: rls.customRoleWrites.roles.primaryKey names no column`);
|
|
1838
|
+
const leveled = custom.levels === true;
|
|
1839
|
+
const tenantType = tenantTypeOf(ctx);
|
|
1840
|
+
const table = quoteQualified(roles.table);
|
|
1841
|
+
const idType = `${table}.${quoteIdent(pk)}%type`;
|
|
1842
|
+
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
1843
|
+
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
1844
|
+
const replace = qualified(ctx, CUSTOM_ROLES.replace);
|
|
1845
|
+
const trustedReplace = qualified(ctx, CUSTOM_ROLES.trustedReplace);
|
|
1846
|
+
const byId = qualified(ctx, CUSTOM_ROLES.replaceById);
|
|
1847
|
+
const trustedById = qualified(ctx, CUSTOM_ROLES.trustedReplaceById);
|
|
1848
|
+
const reader = qualified(ctx, CUSTOM_ROLES.trustedReadById);
|
|
1849
|
+
const renames = Object.entries(custom.renamed ?? {}).toSorted(byKey);
|
|
1850
|
+
const current = (expr) => currentKeyExpr(renames, expr);
|
|
1851
|
+
const declaredKeys = sqlTextArray(custom.permissions ?? []);
|
|
1852
|
+
const declaredRoles = sqlTextArray(custom.declared);
|
|
1853
|
+
const tenant = roles.tenant === void 0 ? `null::${tenantType}` : `${col(roles.tenant)}::${tenantType}`;
|
|
1854
|
+
const scope = rolesTableScope(roles, tenant, root, col);
|
|
1855
|
+
const scopeId = roles.id === void 0 || roles.tenant === void 0 ? "null::text" : `case when ${tenant} is null then null else ${col(roles.id)}::text end`;
|
|
1856
|
+
const skipped = roles.skip === void 0 || roles.tenant === void 0 ? "false" : `(${tenant} is not null and coalesce(${col(roles.skip)}, false))`;
|
|
1857
|
+
const match = "c.tenant_id is not distinct from v_tenant and c.scope = v_scope and c.scope_id is not distinct from v_scope_id and c.role = v_key";
|
|
1858
|
+
const audit = custom.audit;
|
|
1859
|
+
const auditCall = audit === void 0 ? "" : `
|
|
1860
|
+
perform ${quoteQualified(audit.function)}(${[
|
|
1861
|
+
"event_type => 'permdock.custom_role.replaced'",
|
|
1862
|
+
"target_type => 'role'",
|
|
1863
|
+
"record_id => p_role_id::text",
|
|
1864
|
+
"tenant => v_tenant",
|
|
1865
|
+
"metadata => jsonb_build_object('operation', 'replace', 'role', v_key, 'permissions', p_permissions)",
|
|
1866
|
+
...Object.entries(audit.args ?? {}).toSorted(byKey).map(([name, value]) => `${quoteIdent(name)} => ${quoteLiteral(value)}`)
|
|
1867
|
+
].join(", ")});`;
|
|
1868
|
+
const body = (write) => `declare
|
|
1869
|
+
v_tenant ${tenantType};
|
|
1870
|
+
v_scope text;
|
|
1871
|
+
v_scope_id text;
|
|
1872
|
+
v_key text;
|
|
1873
|
+
v_skipped boolean;
|
|
1874
|
+
v_deny text[];
|
|
1875
|
+
v_include text[];
|
|
1876
|
+
begin
|
|
1877
|
+
select ${tenant}, ${scope}, ${scopeId}, ${col(roles.key)}::text, ${skipped}
|
|
1878
|
+
into v_tenant, v_scope, v_scope_id, v_key, v_skipped
|
|
1879
|
+
from ${table} r
|
|
1880
|
+
where ${col(pk)} = p_role_id;
|
|
1881
|
+
if not found then
|
|
1882
|
+
${raiseSql(" ", "P0002", "'permdock: no role row has id ' || coalesce(p_role_id::text, 'null')", "unknown-role")}
|
|
1679
1883
|
end if;
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
order by 1, 2;
|
|
1884
|
+
if v_skipped or v_key = any(${declaredRoles}) then
|
|
1885
|
+
${raiseSql(" ", "22023", "'permdock: ' || coalesce(v_key, 'null') || ' is a declared role, not a custom role'", "declared-role")}
|
|
1886
|
+
end if;
|
|
1887
|
+
select array_agg(c.permission) into v_deny
|
|
1888
|
+
from ${perms} c
|
|
1889
|
+
where ${match} and c.effect = 'deny';
|
|
1890
|
+
select array_agg(c.include_role) into v_include
|
|
1891
|
+
from ${includes} c
|
|
1892
|
+
where ${match};
|
|
1893
|
+
perform ${write}(v_tenant, v_scope, v_scope_id, v_key, p_permissions, v_deny, v_include);${auditCall}
|
|
1691
1894
|
end;`;
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
or (p_scope <> 'global' and (${memberOf}))
|
|
1696
|
-
)) then
|
|
1697
|
-
${raiseSql(" ", "42501", `'permdock: the caller is not a member of ' || coalesce(p_tenant::text, 'the platform')`, "not-member")}
|
|
1698
|
-
end if;`);
|
|
1699
|
-
trustedBody = checked("");
|
|
1700
|
-
}
|
|
1701
|
-
chunks.push(`-- the permission keys a role holds on a scope, with effect allow or deny; a custom role is read for its tenant and, below the first scope, its instance
|
|
1702
|
-
create or replace function ${reader}(p_role text, p_scope text, p_tenant ${tenantType} default null, p_scope_id text default null)
|
|
1703
|
-
returns table (permission text, effect text)
|
|
1895
|
+
const writer = (comment, name, write, member) => `-- ${comment}
|
|
1896
|
+
create or replace function ${name}(p_role_id ${idType}, p_permissions text[])
|
|
1897
|
+
returns void
|
|
1704
1898
|
language plpgsql
|
|
1705
|
-
|
|
1899
|
+
volatile
|
|
1706
1900
|
security definer
|
|
1707
1901
|
set search_path = ''
|
|
1708
1902
|
as $$
|
|
1709
|
-
${body}
|
|
1903
|
+
${body(write)}
|
|
1710
1904
|
$$;
|
|
1711
|
-
revoke execute on function ${
|
|
1712
|
-
grant execute on function ${
|
|
1713
|
-
const
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1905
|
+
${member ? `revoke execute on function ${name}(${idType}, text[]) from public, anon;
|
|
1906
|
+
grant execute on function ${name}(${idType}, text[]) to authenticated;` : `revoke execute on function ${name}(${idType}, text[]) from public, anon, authenticated;`}`;
|
|
1907
|
+
const readerEntry = leveled ? `${current("c.permission")} || coalesce('@' || c.level, '')` : current("c.permission");
|
|
1908
|
+
const readerGrant = trustedReaders === void 0 || trustedReaders.length === 0 ? "" : `\n${trustedReaderGrants(ctx, `${reader}(text[], ${tenantType})`, trustedReaders)}`;
|
|
1909
|
+
return `${writer("save a custom role by its row id: the row supplies tenant, scope, instance and name; the caller checks of permdock_replace_custom_role_grants apply, and stored denies and includes stay", byId, replace, true)}
|
|
1910
|
+
|
|
1911
|
+
${writer("save a custom role by its row id for a trusted caller: the definition checks without the caller checks", trustedById, trustedReplace, false)}
|
|
1912
|
+
|
|
1913
|
+
-- the declared allow keys of custom roles by their row ids as text, for a trusted reader; with p_tenant, an id of another tenant is refused
|
|
1914
|
+
create or replace function ${reader}(p_role_ids text[], p_tenant ${tenantType} default null)
|
|
1915
|
+
returns table (role_id text, permission text)
|
|
1718
1916
|
language plpgsql
|
|
1719
1917
|
stable
|
|
1720
1918
|
security definer
|
|
1721
1919
|
set search_path = ''
|
|
1722
1920
|
as $$
|
|
1723
|
-
|
|
1921
|
+
begin
|
|
1922
|
+
if p_tenant is not null and exists (
|
|
1923
|
+
select 1 from ${table} r
|
|
1924
|
+
where ${col(pk)}::text = any(p_role_ids) and ${tenant} is distinct from p_tenant
|
|
1925
|
+
) then
|
|
1926
|
+
${raiseSql(" ", "42501", "'permdock: a role id belongs to another tenant than ' || p_tenant::text", "not-member")}
|
|
1927
|
+
end if;
|
|
1928
|
+
return query
|
|
1929
|
+
select distinct v.role_id, ${readerEntry}
|
|
1930
|
+
from (
|
|
1931
|
+
select ${col(pk)}::text as role_id, ${tenant} as tenant_id, ${scope} as scope, ${scopeId} as scope_id, ${col(roles.key)}::text as role
|
|
1932
|
+
from ${table} r
|
|
1933
|
+
where ${col(pk)}::text = any(p_role_ids)
|
|
1934
|
+
and not ${skipped}
|
|
1935
|
+
and not (${col(roles.key)}::text = any(${declaredRoles}))
|
|
1936
|
+
) v
|
|
1937
|
+
join ${perms} c
|
|
1938
|
+
on c.tenant_id is not distinct from v.tenant_id
|
|
1939
|
+
and c.scope = v.scope
|
|
1940
|
+
and c.scope_id is not distinct from v.scope_id
|
|
1941
|
+
and c.role = v.role
|
|
1942
|
+
where c.effect = 'allow'
|
|
1943
|
+
and ${current("c.permission")} = any(${declaredKeys})
|
|
1944
|
+
order by 1, 2;
|
|
1945
|
+
end;
|
|
1724
1946
|
$$;
|
|
1725
|
-
revoke execute on function ${
|
|
1726
|
-
return chunks.join("\n\n");
|
|
1727
|
-
}
|
|
1728
|
-
/** `expr` read as its current key: a former key maps to the key it was renamed to. */
|
|
1729
|
-
function currentKeyExpr(renames, expr) {
|
|
1730
|
-
if (renames.length === 0) return expr;
|
|
1731
|
-
return `(case ${expr} ${renames.map(([former, current]) => `when ${quoteLiteral(former)} then ${quoteLiteral(current)}`).join(" ")} else ${expr} end)`;
|
|
1732
|
-
}
|
|
1733
|
-
/** `case p_scope when '<scope>' then <sql for scope> ... else false end` over the declared scopes. */
|
|
1734
|
-
function perScope(ctx, sql) {
|
|
1735
|
-
const arms = ctx.scopes.map((scope) => `when ${quoteLiteral(scope.name)} then ${sql(scope.name)}`).join(" ");
|
|
1736
|
-
return arms === "" ? "false" : `(case p_scope ${arms} else false end)`;
|
|
1737
|
-
}
|
|
1738
|
-
function raiseSql(indent, errcode, message, hint) {
|
|
1739
|
-
return `${indent}raise exception using
|
|
1740
|
-
${indent} errcode = '${errcode}',
|
|
1741
|
-
${indent} message = ${message},
|
|
1742
|
-
${indent} hint = ${quoteLiteral(hint)};`;
|
|
1743
|
-
}
|
|
1744
|
-
function hasCustomRoleChecksFor(ctx) {
|
|
1745
|
-
return ctx.authorize === "database" && ctx.customRoles !== void 0 && ctx.scopes.length > 0 && ctx.scopes.every((scope) => hasMemberFor(ctx, scope.name));
|
|
1947
|
+
revoke execute on function ${reader}(text[], ${tenantType}) from public, anon, authenticated;${readerGrant}`;
|
|
1746
1948
|
}
|
|
1747
1949
|
//#endregion
|
|
1748
1950
|
//#region src/cli/rls/custom-role-cascade.ts
|
|
@@ -1775,12 +1977,12 @@ function customRoleCascadeSql(ctx) {
|
|
|
1775
1977
|
const fn = qualified(ctx, CUSTOM_ROLES.cascade);
|
|
1776
1978
|
const target = quoteTable(qualifySqlTable(table.table));
|
|
1777
1979
|
const tenant = (row) => table.tenant === void 0 ? `null::${tenantType}` : `${rowColumn(row, table.tenant)}::${tenantType}`;
|
|
1778
|
-
const scope = (row) => table
|
|
1779
|
-
const id = (row) => table.id === void 0 ? "null::text" :
|
|
1780
|
-
const skipped = (row) => table.skip === void 0 ? "false" : `coalesce(${rowColumn(row, table.skip)}, false)`;
|
|
1980
|
+
const scope = (row) => rolesTableScope(table, tenant(row), root, (name) => rowColumn(row, name));
|
|
1981
|
+
const id = (row) => table.id === void 0 || table.tenant === void 0 ? "null::text" : `case when ${tenant(row)} is null then null else ${rowColumn(row, table.id)}::text end`;
|
|
1982
|
+
const skipped = (row) => table.skip === void 0 || table.tenant === void 0 ? "false" : `(${tenant(row)} is not null and coalesce(${rowColumn(row, table.skip)}, false))`;
|
|
1781
1983
|
const guard = qualified(ctx, CUSTOM_ROLES.guard);
|
|
1782
1984
|
const entries = qualified(ctx, CUSTOM_ROLES.entries);
|
|
1783
|
-
const allowEntryOf =
|
|
1985
|
+
const allowEntryOf = `c.${allowEntry(ctx)}`;
|
|
1784
1986
|
return `-- the application's roles table: renames, moves and deletes of a custom role carry its grants and includes
|
|
1785
1987
|
create or replace function ${fn}()
|
|
1786
1988
|
returns trigger
|
|
@@ -1859,6 +2061,58 @@ create trigger "permdock_custom_role_cascade"
|
|
|
1859
2061
|
`;
|
|
1860
2062
|
}
|
|
1861
2063
|
//#endregion
|
|
2064
|
+
//#region src/cli/rls/custom-role-guard.ts
|
|
2065
|
+
/** A column of the roles-table row a trigger sees as `new`. */
|
|
2066
|
+
function column(name) {
|
|
2067
|
+
return `new.${quoteIdent(name)}`;
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* The trigger on the application's roles table (`rls.customRoleWrites.roles`)
|
|
2071
|
+
* that refuses a tenant row named like a declared role (`declared-role`) or
|
|
2072
|
+
* like a shared custom role (`name-taken`). A row the `skip` column marks is
|
|
2073
|
+
* a system role of the tenant and passes. Empty without a tenant column: then
|
|
2074
|
+
* no row is a tenant row.
|
|
2075
|
+
*/
|
|
2076
|
+
function customRoleNameGuardSql(ctx) {
|
|
2077
|
+
const custom = ctx.customRoles;
|
|
2078
|
+
const table = custom?.table;
|
|
2079
|
+
const root = ctx.scopes[0]?.name;
|
|
2080
|
+
if (custom === void 0 || table?.tenant === void 0 || root === void 0) return "";
|
|
2081
|
+
const tenant = `${column(table.tenant)}::${tenantTypeOf(ctx)}`;
|
|
2082
|
+
const skipped = table.skip === void 0 ? "false" : `coalesce(${column(table.skip)}, false)`;
|
|
2083
|
+
const fn = qualified(ctx, CUSTOM_ROLES.nameGuard);
|
|
2084
|
+
const target = quoteTable(qualifySqlTable(table.table));
|
|
2085
|
+
const key = `${column(table.key)}::text`;
|
|
2086
|
+
const scope = rolesTableScope(table, tenant, root, column);
|
|
2087
|
+
return `-- the application's roles table: a tenant row cannot take the name of a declared or a shared role
|
|
2088
|
+
create or replace function ${fn}()
|
|
2089
|
+
returns trigger
|
|
2090
|
+
language plpgsql
|
|
2091
|
+
volatile
|
|
2092
|
+
security definer
|
|
2093
|
+
set search_path = ''
|
|
2094
|
+
as $$
|
|
2095
|
+
begin
|
|
2096
|
+
if ${tenant} is null or ${skipped} then
|
|
2097
|
+
return new;
|
|
2098
|
+
end if;
|
|
2099
|
+
if ${key} = any(${sqlTextArray(custom.declared)}) then
|
|
2100
|
+
${raiseSql(" ", "22023", `'permdock: ' || ${key} || ' is a declared role'`, "declared-role")}
|
|
2101
|
+
end if;
|
|
2102
|
+
if ${qualified(ctx, CUSTOM_ROLES.shared)}(${scope}, ${key}) then
|
|
2103
|
+
${raiseSql(" ", "22023", `'permdock: ' || ${key} || ' is a shared role'`, "name-taken")}
|
|
2104
|
+
end if;
|
|
2105
|
+
return new;
|
|
2106
|
+
end;
|
|
2107
|
+
$$;
|
|
2108
|
+
revoke execute on function ${fn}() from public, anon, authenticated;
|
|
2109
|
+
drop trigger if exists "permdock_custom_role_name_guard" on ${target};
|
|
2110
|
+
create trigger "permdock_custom_role_name_guard"
|
|
2111
|
+
before insert or update on ${target}
|
|
2112
|
+
for each row execute function ${fn}();
|
|
2113
|
+
`;
|
|
2114
|
+
}
|
|
2115
|
+
//#endregion
|
|
1862
2116
|
//#region src/cli/rls/custom-role-writes.ts
|
|
1863
2117
|
function callerFunctionsSql(ctx, custom, root, levelReach, forUser) {
|
|
1864
2118
|
const leveled = custom.levels === true;
|
|
@@ -1909,7 +2163,7 @@ ${raiseSql(" ", "42501", `'permdock: the caller may not manage roles in ' ||
|
|
|
1909
2163
|
)` : "";
|
|
1910
2164
|
const reachCte = leveled && reachRows !== "" ? `,
|
|
1911
2165
|
reach (grant_key, level) as (values ${reachRows})` : "";
|
|
1912
|
-
const match =
|
|
2166
|
+
const match = `${storedTenant(ctx)} and c.scope = p_scope and c.scope_id is not distinct from p_scope_id`;
|
|
1913
2167
|
const memberOf = `p_tenant::text in ${members(root)}
|
|
1914
2168
|
or ${perScope(ctx, (scope) => scope === root ? "false" : `p_scope_id in ${members(scope)}`)}`;
|
|
1915
2169
|
return {
|
|
@@ -1961,7 +2215,7 @@ begin
|
|
|
1961
2215
|
perform ${shape}(p_tenant, p_scope, p_scope_id, p_role);
|
|
1962
2216
|
if not (${signedIn(ctx, user)} and (
|
|
1963
2217
|
${platform}
|
|
1964
|
-
or (p_scope <> 'global' and (${memberOf}))
|
|
2218
|
+
or (p_scope <> 'global' and coalesce(${memberOf}, false))
|
|
1965
2219
|
)) then
|
|
1966
2220
|
${raiseSql(" ", "42501", `'permdock: the caller is not a member of ' || coalesce(p_tenant::text, 'the platform')`, "not-member")}
|
|
1967
2221
|
end if;${requires}
|
|
@@ -1989,8 +2243,9 @@ revoke execute on function ${guard}(${userArg}${tenantType}, text, text, text) f
|
|
|
1989
2243
|
* \`assignablePermissions\`: entries inside the ceiling of the role's scope,
|
|
1990
2244
|
* includes naming declared roles, and only permissions and levels the caller
|
|
1991
2245
|
* may hand out, for the new definition and the stored one alike. A platform
|
|
1992
|
-
* custom role (\`p_scope = 'global'\`, no tenant)
|
|
1993
|
-
*
|
|
2246
|
+
* custom role (\`p_scope = 'global'\`, no tenant) and a shared custom role (no
|
|
2247
|
+
* tenant at a lower scope) are written by a caller who holds a
|
|
2248
|
+
* \`meta.manageRoles\` permission through a global role. The
|
|
1994
2249
|
* \`permdock_trusted_*\` variants skip the caller checks and keep the
|
|
1995
2250
|
* definition checks; no client role may execute them.
|
|
1996
2251
|
*/
|
|
@@ -2008,6 +2263,7 @@ function customRoleWritesSql(ctx, levelReach) {
|
|
|
2008
2263
|
const guard = qualified(ctx, CUSTOM_ROLES.guard);
|
|
2009
2264
|
const shape = qualified(ctx, CUSTOM_ROLES.shape);
|
|
2010
2265
|
const entries = qualified(ctx, CUSTOM_ROLES.entries);
|
|
2266
|
+
const problem = qualified(ctx, CUSTOM_ROLES.entryProblem);
|
|
2011
2267
|
const renames = Object.entries(custom.renamed ?? {}).toSorted(byKey);
|
|
2012
2268
|
const current = (expr) => currentKeyExpr(renames, expr);
|
|
2013
2269
|
const known = sqlTextArray([...custom.permissions ?? [], ...renames.map(([former]) => former)]);
|
|
@@ -2016,9 +2272,15 @@ function customRoleWritesSql(ctx, levelReach) {
|
|
|
2016
2272
|
const scopes = sqlTextArray(ctx.scopes.map((scope) => scope.name));
|
|
2017
2273
|
const roleName = (param) => `coalesce(${param}, '') = '' or ${param} = any(${sqlTextArray(custom.declared)})`;
|
|
2018
2274
|
const levelOf = leveled ? ", nullif(split_part(e, '@', 2), '')" : "";
|
|
2019
|
-
const
|
|
2275
|
+
const levelColumn = leveled ? ", level" : "";
|
|
2276
|
+
const sharedRole = qualified(ctx, CUSTOM_ROLES.shared);
|
|
2277
|
+
const taken = (param) => ` if p_tenant is not null and ${sharedRole}(p_scope, ${param}) then
|
|
2278
|
+
${raiseSql(" ", "22023", `'permdock: ' || ${param} || ' is the name of a shared custom role'`, "name-taken")}
|
|
2279
|
+
end if;
|
|
2280
|
+
`;
|
|
2281
|
+
const write = `${taken("p_role")} delete from ${perms} c where ${match} and c.role = p_role;
|
|
2020
2282
|
delete from ${includes} c where ${match} and c.role = p_role;
|
|
2021
|
-
insert into ${perms} (tenant_id, scope, scope_id, role, permission, effect${
|
|
2283
|
+
insert into ${perms} (tenant_id, scope, scope_id, role, permission, effect${levelColumn})
|
|
2022
2284
|
select distinct p_tenant, p_scope, p_scope_id, p_role, ${current(leveled ? "split_part(e, '@', 1)" : "e")}, 'allow'${levelOf}
|
|
2023
2285
|
from unnest(coalesce(p_allow, '{}'::text[])) e
|
|
2024
2286
|
union
|
|
@@ -2027,7 +2289,7 @@ function customRoleWritesSql(ctx, levelReach) {
|
|
|
2027
2289
|
insert into ${includes} (tenant_id, scope, scope_id, role, include_role)
|
|
2028
2290
|
select distinct p_tenant, p_scope, p_scope_id, p_role, e
|
|
2029
2291
|
from unnest(coalesce(p_include, '{}'::text[])) e;`;
|
|
2030
|
-
const move =
|
|
2292
|
+
const move = `${taken("p_to")} if ${roleName("p_to")} then
|
|
2031
2293
|
${raiseSql(" ", "22023", `'permdock: ' || coalesce(p_to, 'null') || ' is not a custom role name'`, "declared-role")}
|
|
2032
2294
|
end if;
|
|
2033
2295
|
if exists (select 1 from ${perms} c where ${match} and c.role = p_to)
|
|
@@ -2058,7 +2320,7 @@ grant execute on function ${qualified(ctx, name)}(${args}) to authenticated;` :
|
|
|
2058
2320
|
const removeParams = `p_tenant ${tenantType}, p_scope text, p_scope_id text, p_role text`;
|
|
2059
2321
|
return `${caller.beyond}
|
|
2060
2322
|
|
|
2061
|
-
-- where a custom role may live: a scope of a tenant, or the platform (scope global, no tenant or instance), under an undeclared name
|
|
2323
|
+
-- 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
|
|
2062
2324
|
create or replace function ${shape}(p_tenant ${tenantType}, p_scope text, p_scope_id text, p_role text)
|
|
2063
2325
|
returns void
|
|
2064
2326
|
language plpgsql
|
|
@@ -2070,8 +2332,10 @@ begin
|
|
|
2070
2332
|
if p_tenant is not null or p_scope_id is not null then
|
|
2071
2333
|
${raiseSql(" ", "22023", `'permdock: a platform custom role has no tenant or instance'`, "unknown-scope")}
|
|
2072
2334
|
end if;
|
|
2073
|
-
elsif
|
|
2335
|
+
elsif not (p_scope = any(${scopes})) then
|
|
2074
2336
|
${raiseSql(" ", "22023", `'permdock: custom roles are held at a scope of a tenant or at global, not ' || coalesce(p_scope, 'null')`, "unknown-scope")}
|
|
2337
|
+
elsif p_tenant is null and p_scope_id is not null then
|
|
2338
|
+
${raiseSql(" ", "22023", `'permdock: a shared custom role has no instance'`, "unknown-scope")}
|
|
2075
2339
|
end if;
|
|
2076
2340
|
if ${roleName("p_role")} then
|
|
2077
2341
|
${raiseSql(" ", "22023", `'permdock: ' || coalesce(p_role, 'null') || ' is not a custom role name'`, "declared-role")}
|
|
@@ -2087,6 +2351,38 @@ ${callerFunctionsSql(ctx, custom, root, levelReach, true).beyond}
|
|
|
2087
2351
|
|
|
2088
2352
|
${callerFunctionsSql(ctx, custom, root, levelReach, true).guard}
|
|
2089
2353
|
` : ""}
|
|
2354
|
+
-- null when an allow entry is a declared permission inside the ceiling of the scope${leveled ? ", at a level the resource declares" : ""}; else unknown-permission, outside-ceiling${leveled ? " or unknown-level" : ""}
|
|
2355
|
+
create or replace function ${problem}(p_scope text, p_entry text)
|
|
2356
|
+
returns text
|
|
2357
|
+
language plpgsql
|
|
2358
|
+
stable
|
|
2359
|
+
security definer
|
|
2360
|
+
set search_path = ''
|
|
2361
|
+
as $$
|
|
2362
|
+
declare
|
|
2363
|
+
v_key text := ${current(leveled ? "split_part(p_entry, '@', 1)" : "p_entry")};
|
|
2364
|
+
begin
|
|
2365
|
+
if p_entry is null or not (v_key = any(${known})) then
|
|
2366
|
+
return 'unknown-permission';
|
|
2367
|
+
end if;
|
|
2368
|
+
if not exists (
|
|
2369
|
+
select 1 from ${ceiling} c
|
|
2370
|
+
where c.scope = p_scope and c.effect = 'allow' and ${current("c.permission")} = v_key
|
|
2371
|
+
) then
|
|
2372
|
+
return 'outside-ceiling';
|
|
2373
|
+
end if;${leveled ? `
|
|
2374
|
+
if strpos(p_entry, '@') > 0 and not exists (
|
|
2375
|
+
select 1 from ${ceiling} c
|
|
2376
|
+
where c.scope = p_scope and c.effect = 'allow' and ${current("c.permission")} = v_key
|
|
2377
|
+
and split_part(c.grant_key, '@', 2) = split_part(p_entry, '@', 2)
|
|
2378
|
+
) then
|
|
2379
|
+
return 'unknown-level';
|
|
2380
|
+
end if;` : ""}
|
|
2381
|
+
return null;
|
|
2382
|
+
end;
|
|
2383
|
+
$$;
|
|
2384
|
+
revoke execute on function ${problem}(text, text) from public, anon, authenticated;
|
|
2385
|
+
|
|
2090
2386
|
-- the entries of a definition: declared keys and levels inside the ceiling of the scope, and declared includes
|
|
2091
2387
|
create or replace function ${entries}(p_scope text, p_allow text[], p_deny text[], p_include text[])
|
|
2092
2388
|
returns void
|
|
@@ -2098,23 +2394,18 @@ as $$
|
|
|
2098
2394
|
declare
|
|
2099
2395
|
v_entry text;
|
|
2100
2396
|
v_key text;
|
|
2397
|
+
v_problem text;
|
|
2101
2398
|
begin
|
|
2102
2399
|
foreach v_entry in array coalesce(p_allow, '{}'::text[]) loop
|
|
2103
2400
|
v_key := ${current(leveled ? "split_part(v_entry, '@', 1)" : "v_entry")};
|
|
2104
|
-
|
|
2401
|
+
v_problem := ${problem}(p_scope, v_entry);
|
|
2402
|
+
if v_problem = 'unknown-permission' then
|
|
2105
2403
|
${raiseSql(" ", "22023", `'permdock: ' || v_entry || ' is not a declared permission'`, "unknown-permission")}
|
|
2106
2404
|
end if;
|
|
2107
|
-
if
|
|
2108
|
-
select 1 from ${ceiling} c
|
|
2109
|
-
where c.scope = p_scope and c.effect = 'allow' and ${current("c.permission")} = v_key
|
|
2110
|
-
) then
|
|
2405
|
+
if v_problem = 'outside-ceiling' then
|
|
2111
2406
|
${raiseSql(" ", "22023", `'permdock: ' || v_key || ' is outside the ceiling of ' || p_scope`, "outside-ceiling")}
|
|
2112
2407
|
end if;${leveled ? `
|
|
2113
|
-
if
|
|
2114
|
-
select 1 from ${ceiling} c
|
|
2115
|
-
where c.scope = p_scope and c.effect = 'allow' and ${current("c.permission")} = v_key
|
|
2116
|
-
and split_part(c.grant_key, '@', 2) = split_part(v_entry, '@', 2)
|
|
2117
|
-
) then
|
|
2408
|
+
if v_problem = 'unknown-level' then
|
|
2118
2409
|
${raiseSql(" ", "22023", `'permdock: ' || v_entry || ' names a level the resource does not declare'`, "unknown-level")}
|
|
2119
2410
|
end if;` : ""}
|
|
2120
2411
|
end loop;
|
|
@@ -2217,7 +2508,7 @@ function globalRolesSql(ctx) {
|
|
|
2217
2508
|
const id = quoteIdent(through.id);
|
|
2218
2509
|
const key = quoteIdent(through.key);
|
|
2219
2510
|
const ref = quoteIdent(through.ref);
|
|
2220
|
-
const global = source.tenant === void 0 ? "" : ` and k.${quoteIdent(source.tenant)} is null`;
|
|
2511
|
+
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`}`;
|
|
2221
2512
|
body = ` select string_agg(w.r, ', ' order by w.r) into v_unknown
|
|
2222
2513
|
from (${wanted}) w(r)
|
|
2223
2514
|
where not exists (select 1 from ${keys} k where k.${key}::text = w.r${global});
|
|
@@ -2375,17 +2666,103 @@ ${functionBody(memberRowsBody(ctx, scope, type, "p_user", memberForSources(ctx,
|
|
|
2375
2666
|
revoke execute on function ${fn}(uuid) from public, anon, authenticated;`;
|
|
2376
2667
|
}
|
|
2377
2668
|
//#endregion
|
|
2669
|
+
//#region src/cli/rls/token-scopes.ts
|
|
2670
|
+
const TOKEN_SCOPE_ALLOWS = "permdock_token_scope_allows";
|
|
2671
|
+
/** `rls.tokenScopes` as the SQL generator needs it, or `undefined` when it is off. */
|
|
2672
|
+
function tokenScopesPlan(setting, policy) {
|
|
2673
|
+
if (setting !== true) return;
|
|
2674
|
+
const permissions = listPermissions(policy.permissions).map((leaf) => ({
|
|
2675
|
+
key: leaf.key,
|
|
2676
|
+
scope: leaf.scope,
|
|
2677
|
+
renamedFrom: formerKeys(leaf)
|
|
2678
|
+
}));
|
|
2679
|
+
return { scopes: delegatedScopeMap(permissions, policy.oauthScopes) };
|
|
2680
|
+
}
|
|
2681
|
+
function tokenScopeAllowsCall(ctx, grant) {
|
|
2682
|
+
return `${qualified(ctx, TOKEN_SCOPE_ALLOWS)}(${grant})`;
|
|
2683
|
+
}
|
|
2684
|
+
/** The token's scopes as rows of `s(scope)`: a space-separated string or, outside `guc`, a JSON array. */
|
|
2685
|
+
function tokenScopeRows(ctx) {
|
|
2686
|
+
if (ctx.dialect === "guc") return `regexp_split_to_table(btrim(coalesce(${subjectClaimSql(ctx, "scope")}, '')), '\\s+') s(scope)`;
|
|
2687
|
+
const scope = subjectClaimJsonSql(ctx, "scope");
|
|
2688
|
+
return `unnest(case jsonb_typeof(${scope})
|
|
2689
|
+
when 'string' then regexp_split_to_array(btrim(${scope} #>> '{}'), '\\s+')
|
|
2690
|
+
when 'array' then array(select jsonb_array_elements_text(${scope}))
|
|
2691
|
+
else '{}'::text[]
|
|
2692
|
+
end) s(scope)`;
|
|
2693
|
+
}
|
|
2694
|
+
/**
|
|
2695
|
+
* `permdock_token_scope_allows(p_grant)`: a delegated token (a `client_id`, or
|
|
2696
|
+
* an `act` that is not a support or impersonation session) reaches a grant key
|
|
2697
|
+
* only when one of its scopes maps to the key's permission. better-supabase's
|
|
2698
|
+
* `cap_allows` reads the same tokens the same way.
|
|
2699
|
+
*/
|
|
2700
|
+
function tokenScopeAllowsSql(ctx, plan, anonExecute) {
|
|
2701
|
+
const fn = qualified(ctx, TOKEN_SCOPE_ALLOWS);
|
|
2702
|
+
const act = subjectClaimJsonSql(ctx, "act");
|
|
2703
|
+
const grants = anonExecute ? `revoke execute on function ${fn}(text) from public;
|
|
2704
|
+
grant execute on function ${fn}(text) to anon, authenticated;` : `revoke execute on function ${fn}(text) from public, anon;
|
|
2705
|
+
grant execute on function ${fn}(text) to authenticated;`;
|
|
2706
|
+
return `-- Token scopes: a delegated token holds only the permissions its scope claim maps to; denies always apply, and a token without client_id or an agent act is not limited
|
|
2707
|
+
create or replace function ${fn}(p_grant text)
|
|
2708
|
+
returns boolean
|
|
2709
|
+
language sql
|
|
2710
|
+
stable
|
|
2711
|
+
security definer
|
|
2712
|
+
set search_path = ''
|
|
2713
|
+
as $$
|
|
2714
|
+
select case
|
|
2715
|
+
when coalesce(${subjectClaimSql(ctx, "client_id")}, '') = ''
|
|
2716
|
+
and (${act} is null
|
|
2717
|
+
or ${act} = 'null'::jsonb
|
|
2718
|
+
or (jsonb_typeof(${act}) = 'object'
|
|
2719
|
+
and ${act} ->> 'kind' in ('support', 'impersonation'))) then true
|
|
2720
|
+
when exists (
|
|
2721
|
+
select 1 from ${qualified(ctx, "role_permissions")} rp
|
|
2722
|
+
where rp.grant_key = p_grant and rp.effect = 'deny'
|
|
2723
|
+
) then true
|
|
2724
|
+
else exists (
|
|
2725
|
+
select 1
|
|
2726
|
+
from ${tokenScopeRows(ctx)}
|
|
2727
|
+
where s.scope <> ''
|
|
2728
|
+
and coalesce(${quoteLiteral(JSON.stringify(plan.scopes))}::jsonb -> s.scope, '[]'::jsonb)
|
|
2729
|
+
? split_part(split_part(p_grant, '#', 1), '@', 1)
|
|
2730
|
+
)
|
|
2731
|
+
end
|
|
2732
|
+
$$;
|
|
2733
|
+
${grants}`;
|
|
2734
|
+
}
|
|
2735
|
+
//#endregion
|
|
2378
2736
|
//#region src/cli/rls/helpers.ts
|
|
2379
2737
|
/**
|
|
2380
2738
|
* The policy-side call for a grant key. Every form is uncorrelated, so
|
|
2381
2739
|
* Postgres runs the helper once per statement (an InitPlan, or a hashed
|
|
2382
2740
|
* SubPlan for `in`), never once per row.
|
|
2383
2741
|
*/
|
|
2384
|
-
function accessSql(ctx, scope, grantKey, column) {
|
|
2742
|
+
function accessSql(ctx, scope, grantKey, column, helper) {
|
|
2385
2743
|
const key = quoteLiteral(grantKey);
|
|
2386
2744
|
if (scope === "global") return `(select ${qualified(ctx, HELPERS.has)}(${key}))`;
|
|
2387
2745
|
if (column === void 0) throw configError(`PermDock CLI: ${scope}-scoped grant needs definePolicy({ scopes.${scope} })`);
|
|
2388
|
-
return `${quoteIdent(column)} in (select ${qualified(ctx, permittedIdsHelper(scope))}(${key}))`;
|
|
2746
|
+
return `${quoteIdent(column)} in (select ${qualified(ctx, helper ?? permittedIdsHelper(scope))}(${key}))`;
|
|
2747
|
+
}
|
|
2748
|
+
/**
|
|
2749
|
+
* `permitted_<parent>_ids(p_grant)` for each \`rls.tables\` \`via\`: the parent
|
|
2750
|
+
* rows whose scope instance the grant reaches. \`security definer\`, so the
|
|
2751
|
+
* caller needs no read on the parent, and called uncorrelated like
|
|
2752
|
+
* \`permitted_<scope>_ids\`.
|
|
2753
|
+
*/
|
|
2754
|
+
function viaHelpersSql(ctx, anonExecute) {
|
|
2755
|
+
const done = /* @__PURE__ */ new Set();
|
|
2756
|
+
const chunks = [];
|
|
2757
|
+
for (const via of Object.values(ctx.vias ?? {})) {
|
|
2758
|
+
if (done.has(via.helper)) continue;
|
|
2759
|
+
done.add(via.helper);
|
|
2760
|
+
chunks.push(`-- rows of ${via.table} whose ${via.column} is a ${via.scope} the grant reaches
|
|
2761
|
+
${helperFunction(ctx, via.helper, `setof ${via.type}`, sqlBody(` select p.${quoteIdent(via.id)}
|
|
2762
|
+
from ${quoteQualified(via.table)} p
|
|
2763
|
+
where p.${quoteIdent(via.column)} in (select ${qualified(ctx, permittedIdsHelper(via.scope))}(p_grant))`), anonExecute)}`);
|
|
2764
|
+
}
|
|
2765
|
+
return chunks;
|
|
2389
2766
|
}
|
|
2390
2767
|
function userIdSql(ctx, anonExecute) {
|
|
2391
2768
|
const fn = userIdHelperSql(ctx);
|
|
@@ -2473,6 +2850,40 @@ ${rows.map((row) => ` (${[
|
|
|
2473
2850
|
);`;
|
|
2474
2851
|
}
|
|
2475
2852
|
/**
|
|
2853
|
+
* `role_permissions` as a view over the compiled rows (`rls.seeds: 'view'`).
|
|
2854
|
+
* `security_invoker` makes the view read as its caller, so it adds no
|
|
2855
|
+
* privilege; the table's revoke applies to the view the same way.
|
|
2856
|
+
*/
|
|
2857
|
+
function seedViewSql(ctx, rows) {
|
|
2858
|
+
const view = qualified(ctx, "role_permissions");
|
|
2859
|
+
const columns = "role, permission, grant_key, scope, effect";
|
|
2860
|
+
return `-- rls.seeds: 'view': role_permissions is a view, so the rows travel with the schema
|
|
2861
|
+
create or replace view ${view} (${columns}) with (security_invoker = true) as
|
|
2862
|
+
${rows.length === 0 ? `select null::text, null::text, null::text, null::text, null::text where false` : `select * from (values\n${rows.map((row) => ` (${[
|
|
2863
|
+
row.role,
|
|
2864
|
+
row.permission,
|
|
2865
|
+
row.grantKey,
|
|
2866
|
+
row.scope,
|
|
2867
|
+
row.effect
|
|
2868
|
+
].map(quoteLiteral).join(", ")})`).join(",\n")}\n) as v(${columns})`};
|
|
2869
|
+
revoke all on table ${view} from anon, authenticated, public;`;
|
|
2870
|
+
}
|
|
2871
|
+
/** `<schema>.seed()`: the same upsert and delete as `seedSql`, for a declarative schema that carries no rows. */
|
|
2872
|
+
function seedFunctionSql(ctx, rows) {
|
|
2873
|
+
const fn = qualified(ctx, "seed");
|
|
2874
|
+
return `-- rls.seeds: 'function': the role_permissions rows are data, so a declarative diff leaves them out; run select ${fn}(); after each deploy
|
|
2875
|
+
create or replace function ${fn}()
|
|
2876
|
+
returns void
|
|
2877
|
+
language sql
|
|
2878
|
+
volatile
|
|
2879
|
+
security definer
|
|
2880
|
+
set search_path = ''
|
|
2881
|
+
as $permdock$
|
|
2882
|
+
${seedSql(ctx, rows)}
|
|
2883
|
+
$permdock$;
|
|
2884
|
+
revoke execute on function ${fn}() from public, anon, authenticated;`;
|
|
2885
|
+
}
|
|
2886
|
+
/**
|
|
2476
2887
|
* `role_permissions`, its seeds, and the three `security definer` helpers
|
|
2477
2888
|
* with `search_path = ''`. `execute` goes to `authenticated` only.
|
|
2478
2889
|
*/
|
|
@@ -2482,9 +2893,13 @@ function helpersSql(ctx, rows, options) {
|
|
|
2482
2893
|
const rp = qualified(ctx, "role_permissions");
|
|
2483
2894
|
const chunks = [`-- permdock helpers (${ctx.authorize === "database" ? "database: reads the membership and user_roles tables" : "jwt: reads the role and memberships claims"})
|
|
2484
2895
|
-- policies call them uncorrelated, so Postgres evaluates each once per statement`];
|
|
2485
|
-
if (schema !== "public")
|
|
2896
|
+
if (schema !== "public") {
|
|
2897
|
+
const anonUsage = options.anonExecute === true || ctx.capabilities === true;
|
|
2898
|
+
chunks.push(`create schema if not exists ${s};\nrevoke all on schema ${s} from public;\ngrant usage on schema ${s} to ${anonUsage ? "anon, authenticated" : "authenticated"};`);
|
|
2899
|
+
}
|
|
2486
2900
|
if (ctx.dialect === "supabase") chunks.push(userIdSql(ctx, options.anonExecute === true));
|
|
2487
|
-
|
|
2901
|
+
if (ctx.seeds === "view") chunks.push(seedViewSql(ctx, rows));
|
|
2902
|
+
else chunks.push(`create table if not exists ${rp} (
|
|
2488
2903
|
role text not null,
|
|
2489
2904
|
permission text not null,
|
|
2490
2905
|
grant_key text not null,
|
|
@@ -2494,7 +2909,8 @@ function helpersSql(ctx, rows, options) {
|
|
|
2494
2909
|
);
|
|
2495
2910
|
alter table ${rp} enable row level security;
|
|
2496
2911
|
revoke all on table ${rp} from anon, authenticated, public;`);
|
|
2497
|
-
if (
|
|
2912
|
+
if (ctx.seeds === "function") chunks.push(seedFunctionSql(ctx, rows));
|
|
2913
|
+
else if (options.withoutSeeds !== true && ctx.seeds !== "view") chunks.push(seedSql(ctx, rows));
|
|
2498
2914
|
if (ctx.authorize === "database" && options.userRoles && ctx.roles === void 0) {
|
|
2499
2915
|
const ur = qualified(ctx, "user_roles");
|
|
2500
2916
|
chunks.push(`create table if not exists ${ur} (
|
|
@@ -2510,6 +2926,7 @@ revoke all on table ${ur} from anon, authenticated, public;`);
|
|
|
2510
2926
|
const anon = options.anonExecute === true;
|
|
2511
2927
|
const keys = ctx.apiKeys;
|
|
2512
2928
|
if (keys !== void 0) chunks.push(apiKeyAllowsSql(ctx, keys, anon));
|
|
2929
|
+
if (ctx.tokenScopes !== void 0) chunks.push(tokenScopeAllowsSql(ctx, ctx.tokenScopes, anon));
|
|
2513
2930
|
const bodies = grantBodies(ctx);
|
|
2514
2931
|
chunks.push(helperFunction(ctx, HELPERS.has, "boolean", bodies.has, anon));
|
|
2515
2932
|
const capabilities = capabilitiesSql(ctx);
|
|
@@ -2521,6 +2938,7 @@ revoke all on table ${ur} from anon, authenticated, public;`);
|
|
|
2521
2938
|
if (ctx.roleMemberScopes?.includes(scope.name) === true) chunks.push(memberRolesFunction(ctx, scope.name, type, anon));
|
|
2522
2939
|
if (hasMemberFor(ctx, scope.name)) chunks.push(memberForFunction(ctx, scope.name, type));
|
|
2523
2940
|
}
|
|
2941
|
+
chunks.push(...viaHelpersSql(ctx, anon));
|
|
2524
2942
|
const forUser = forUserSql(ctx);
|
|
2525
2943
|
if (forUser !== "") chunks.push(forUser);
|
|
2526
2944
|
const writes = customRoleWritesSql(ctx, options.levelReach ?? []);
|
|
@@ -2528,6 +2946,12 @@ revoke all on table ${ur} from anon, authenticated, public;`);
|
|
|
2528
2946
|
chunks.push(writes);
|
|
2529
2947
|
const cascade = customRoleCascadeSql(ctx);
|
|
2530
2948
|
if (cascade !== "") chunks.push(cascade);
|
|
2949
|
+
const sync = ctx.customRoles?.sync;
|
|
2950
|
+
if (sync !== void 0) chunks.push(sync);
|
|
2951
|
+
const byId = customRoleByIdSql(ctx, options.trustedReaders);
|
|
2952
|
+
if (byId !== "") chunks.push(byId);
|
|
2953
|
+
const nameGuard = customRoleNameGuardSql(ctx);
|
|
2954
|
+
if (nameGuard !== "") chunks.push(nameGuard);
|
|
2531
2955
|
}
|
|
2532
2956
|
const globalRoles = globalRolesSql(ctx);
|
|
2533
2957
|
if (globalRoles !== "") chunks.push(globalRoles);
|
|
@@ -2537,20 +2961,26 @@ revoke all on table ${ur} from anon, authenticated, public;`);
|
|
|
2537
2961
|
}
|
|
2538
2962
|
function grantBodies(ctx) {
|
|
2539
2963
|
const keys = ctx.apiKeys;
|
|
2964
|
+
const scoped = ctx.tokenScopes !== void 0;
|
|
2540
2965
|
const set = ctx.grantSet === true;
|
|
2541
|
-
const
|
|
2542
|
-
|
|
2966
|
+
const ceiling = (grant) => {
|
|
2967
|
+
const calls = [...keys === void 0 ? [] : [apiKeyAllowsCall(ctx, grant)], ...scoped ? [tokenScopeAllowsCall(ctx, grant)] : []];
|
|
2968
|
+
return calls.length === 0 ? void 0 : calls.join(" and ");
|
|
2969
|
+
};
|
|
2970
|
+
const allows = ceiling(set ? "ids.grant_key" : "p_grant");
|
|
2971
|
+
const keyTenant = keys === void 0 ? "" : ` and ${keyTenantSql(ctx, void 0) ?? "true"}`;
|
|
2972
|
+
const hasAllows = ceiling(set ? "g.grant_key" : "p_grant");
|
|
2543
2973
|
return {
|
|
2544
|
-
has: sqlBody(
|
|
2974
|
+
has: sqlBody(hasAllows === void 0 ? hasBody(ctx) : set ? ` select g.grant_key
|
|
2545
2975
|
from (
|
|
2546
2976
|
${hasBody(ctx)}
|
|
2547
2977
|
) g(grant_key)
|
|
2548
|
-
where ${
|
|
2978
|
+
where ${hasAllows}${keyTenant}` : ceilingHasSql(hasBody(ctx), `${hasAllows}${keyTenant}`)),
|
|
2549
2979
|
ids: (scope) => {
|
|
2550
2980
|
const type = scopeTypeOf(ctx, scope);
|
|
2551
2981
|
const body = scopedBody(ctx, scope, type);
|
|
2552
|
-
if (
|
|
2553
|
-
const sql = scope === rootName(ctx) ? `${body.sql}\n union\n${serviceKeyIdsSql(ctx, keys, scope, type)}` : body.sql;
|
|
2982
|
+
if (allows === void 0) return body;
|
|
2983
|
+
const sql = keys !== void 0 && scope === rootName(ctx) ? `${body.sql}\n union\n${serviceKeyIdsSql(ctx, keys, scope, type)}` : body.sql;
|
|
2554
2984
|
return {
|
|
2555
2985
|
sql: set ? ` select ids.id, ids.grant_key
|
|
2556
2986
|
from (
|
|
@@ -2563,7 +2993,7 @@ ${sql}
|
|
|
2563
2993
|
};
|
|
2564
2994
|
}
|
|
2565
2995
|
function forUserContext(ctx) {
|
|
2566
|
-
const { apiKeys: _apiKeys, ...unkeyed } = ctx;
|
|
2996
|
+
const { apiKeys: _apiKeys, tokenScopes: _tokenScopes, ...unkeyed } = ctx;
|
|
2567
2997
|
return {
|
|
2568
2998
|
...unkeyed,
|
|
2569
2999
|
subjectId: "p_user",
|
|
@@ -2909,7 +3339,7 @@ function canAssignCustomSql(ctx, forUser = false) {
|
|
|
2909
3339
|
const guard = qualified(ctx, forUser ? CUSTOM_ROLES.guardFor : CUSTOM_ROLES.guard);
|
|
2910
3340
|
const userParam = forUser ? "p_user uuid, " : "";
|
|
2911
3341
|
const userArg = forUser ? "uuid, " : "";
|
|
2912
|
-
const stored =
|
|
3342
|
+
const stored = `${storedTenant(ctx)} and c.scope = p_scope and c.role = p_role and (c.scope_id is null or c.scope_id = p_scope_id)`;
|
|
2913
3343
|
const grants = forUser ? `revoke execute on function ${fn}(${userArg}${tenantType}, text, text, text) from public, anon, authenticated;` : `revoke execute on function ${fn}(${tenantType}, text, text, text) from public, anon;
|
|
2914
3344
|
grant execute on function ${fn}(${tenantType}, text, text, text) to authenticated;`;
|
|
2915
3345
|
return `-- whether ${forUser ? "user p_user" : "the caller"} may assign custom role p_role at instance p_scope_id (a platform role: null tenant, scope global): the custom-role write checks on its stored definition${forUser ? "; no client role may execute it" : ""}
|
|
@@ -3586,6 +4016,42 @@ function ownershipRules(policy, scopes) {
|
|
|
3586
4016
|
};
|
|
3587
4017
|
}
|
|
3588
4018
|
/**
|
|
4019
|
+
* The roles whose `for` lists kinds that no membership source of the role's
|
|
4020
|
+
* scope can hold: such a role resolves to no one. A `via` read from a column
|
|
4021
|
+
* can hold any kind, and so does a source with no `via` (it is not
|
|
4022
|
+
* compared) and a scope with no table or source.
|
|
4023
|
+
*/
|
|
4024
|
+
function unsatisfiedKinds(ctx, policy) {
|
|
4025
|
+
const out = [];
|
|
4026
|
+
for (const [role, kinds] of Object.entries(ctx.ownership?.kinds ?? {})) {
|
|
4027
|
+
const scope = roleScope(policy, ctx.scopes, role);
|
|
4028
|
+
if (scope === void 0) continue;
|
|
4029
|
+
const held = [];
|
|
4030
|
+
const mapped = scopeTable(ctx, scope)?.table;
|
|
4031
|
+
if (mapped !== void 0) {
|
|
4032
|
+
const via = memberVia(mapped);
|
|
4033
|
+
held.push({
|
|
4034
|
+
label: mapped.table,
|
|
4035
|
+
via: typeof via === "string" ? void 0 : via.value ?? void 0
|
|
4036
|
+
});
|
|
4037
|
+
} else for (const source of ctx.memberSources ?? ctx.sources ?? []) {
|
|
4038
|
+
if (source.sql.scope !== void 0 && source.sql.scope !== scope) continue;
|
|
4039
|
+
const via = source.sql.manifest.via;
|
|
4040
|
+
held.push({
|
|
4041
|
+
label: source.sql.table,
|
|
4042
|
+
via: via === void 0 ? void 0 : "value" in via && typeof via.value === "string" ? via.value : void 0
|
|
4043
|
+
});
|
|
4044
|
+
}
|
|
4045
|
+
if (held.length === 0 || held.some((entry) => entry.via === void 0 || kinds.includes(entry.via ?? ""))) continue;
|
|
4046
|
+
out.push({
|
|
4047
|
+
role,
|
|
4048
|
+
for: kinds,
|
|
4049
|
+
sources: held.map((entry) => `${entry.label} (via ${entry.via ?? ""})`)
|
|
4050
|
+
});
|
|
4051
|
+
}
|
|
4052
|
+
return out;
|
|
4053
|
+
}
|
|
4054
|
+
/**
|
|
3589
4055
|
* The ownership objects: holder-count and transfer-only triggers on each
|
|
3590
4056
|
* scope's membership table, and `permdock_can_assign` for the application's
|
|
3591
4057
|
* own policies on those tables. Empty when no role declares a rule.
|
|
@@ -3631,4 +4097,4 @@ function ownershipSql(ctx) {
|
|
|
3631
4097
|
return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
|
|
3632
4098
|
}
|
|
3633
4099
|
//#endregion
|
|
3634
|
-
export {
|
|
4100
|
+
export { permittedForHelper as $, apiKeyFields as A, sqlFunctionNames as B, globalRoleSource as C, byKey as D, byCodeUnit as E, collectGrants as F, hasMemberFor as G, graphHelper as H, roleNames as I, memberForHelper as J, hasPermittedUsers as K, andConditions as L, assignableRoles as M, breakGlassHolder as N, API_KEY_ALLOWS as O, breakGlassKey as P, memberRoleOf as Q, compileConditionSql as R, globalRoleRows as S, rolesTableScope as T, graphSqlName as U, CLOSURE as V, graphSqlText as W, memberForTable as X, memberForSources as Y, memberIdsHelper as Z, seedSql as _, assignmentSql as a, activeUserSql as at, tokenScopesPlan as b, OWNERSHIP as c, permissionHelpersSql as d, permittedIdsHelper as et, permissionKeysForHelper as f, helpersSql as g, accessSql as h, holderSources as i, activeRowSql as it, apiKeysPlan as j, apiKeyAllowsCall as k, PERMISSION_HELPERS as l, requiresSql as m, ownershipSql as n, scopeSources as nt, guardedTables as o, checkSuspension as ot, permittedByPermissionHelper as p, linkHelper as q, unsatisfiedKinds as r, scopeTable as rt, ASSIGNMENTS as s, ownershipRules as t, restrictedHelper as tt, checkName as u, TOKEN_SCOPE_ALLOWS as v, currentKeyExpr as w, capabilityAccessSql as x, tokenScopeAllowsCall as y, contextRefs as z };
|