permdock 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a2a/index.d.ts +5 -5
- package/dist/a2a/index.js +3 -3
- package/dist/{adoption-scan-B4R5fnMU.js → adoption-scan-DCYe60Pi.js} +1 -1
- package/dist/agent/index.d.ts +2 -2
- package/dist/agent/index.js +1 -1
- package/dist/ai-sdk/index.d.ts +4 -4
- package/dist/ai-sdk/index.js +3 -3
- package/dist/{approval-prompt-Djo_PyZc.d.ts → approval-prompt-DvSSPQJX.d.ts} +2 -2
- package/dist/approvals/index.d.ts +3 -3
- package/dist/approvals/index.js +3 -3
- package/dist/{approvals-DJ6Yd5z1.js → approvals-p1PGoVjb.js} +3 -3
- package/dist/{arazzo-B6lebmiQ.js → arazzo-41WuzHou.js} +1 -1
- package/dist/{arazzo-BWhNF-k0.js → arazzo-CaUVBn7D.js} +2 -2
- package/dist/astro/index.d.ts +6 -6
- package/dist/astro/index.js +2 -2
- package/dist/{audience-Dg8XsRaO.js → audience-DabClFLQ.js} +1 -1
- package/dist/authzen/index.d.ts +4 -4
- package/dist/authzen/index.js +2 -2
- package/dist/{automation-BMZPIVUM.d.ts → automation-BNM3TtDo.d.ts} +3 -3
- package/dist/better-auth/index.d.ts +2 -2
- package/dist/better-auth/index.js +1 -1
- package/dist/better-supabase/index.d.ts +7 -7
- package/dist/better-supabase/index.js +6 -6
- package/dist/{bind-CZzhx-Vj.js → bind-CAvgUuKs.js} +4 -4
- package/dist/{bind--9EFVYWF.d.ts → bind-DOyrv6zi.d.ts} +7 -7
- package/dist/{build-instance-BMnAudHn.js → build-instance-Bu_eDqYw.js} +1 -1
- package/dist/{capability-BSslw1QV.d.ts → capability-Drg-1YhL.d.ts} +2 -2
- package/dist/catalog/index.d.ts +5 -5
- package/dist/catalog/index.js +3 -3
- package/dist/catalog-BoWglDPI.js +1 -0
- package/dist/{catalog-R_A__RsH.js → catalog-DZVbrF-z.js} +1 -1
- package/dist/{catalog-BECMPrwc.js → catalog-HD2gf5mY.js} +2 -2
- package/dist/{catalog-doc-C9wjsfNY.js → catalog-doc-Cdn7-0pQ.js} +1 -1
- 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/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/cli/bin.js +1 -1
- package/dist/cli/index.d.ts +3 -3
- package/dist/cli/index.js +2 -2
- package/dist/cloud/index.d.ts +8 -8
- package/dist/cloud/index.js +1 -1
- package/dist/{cloud-CIoHNevm.js → cloud-ytQ3uviA.js} +2 -2
- package/dist/{codemod-Bmj9dWMH.js → codemod-5mpSpnUY.js} +2 -2
- package/dist/{codemod-Cxvvc70T.js → codemod-ua2hHy7w.js} +1 -1
- package/dist/{collect-DloHbDVa.js → collect-Ci6akVwH.js} +2 -2
- package/dist/collect-D7eQDffE.js +1 -0
- package/dist/{collect-CrZJJnEF.js → collect-mLTY4nwb.js} +2 -2
- package/dist/compile/index.d.ts +1 -1
- package/dist/{compile-Ckm2HqBZ.d.ts → compile-1bsAezL0.d.ts} +3 -3
- package/dist/{completion-BqhRBJD8.js → completion-BTFrHZhX.js} +1 -1
- package/dist/{config-B46rm9sc.js → config-Bj2Ixq3d.js} +1 -1
- package/dist/{config-report-DUgj62Ai.js → config-report-DSpsLpCE.js} +2 -2
- package/dist/{context-bbFHR-0D.js → context-B_u3_bDJ.js} +2 -2
- package/dist/convex/index.d.ts +5 -5
- package/dist/convex/index.js +2 -2
- package/dist/{create-0VZ7Pdiu.d.ts → create-B30OMUOJ.d.ts} +5 -5
- package/dist/{create-BDS7wc3y.d.ts → create-CBMmFie-.d.ts} +9 -9
- package/dist/{create-C3b4B5eF.js → create-CGm28gyI.js} +4 -4
- package/dist/{create-BWRSXgOF.js → create-CQWIpUG-.js} +1 -1
- package/dist/{create-DnrmLnLW.js → create-DCF2SvoT.js} +4 -4
- package/dist/{create-BFfm17UA.d.ts → create-D_NmPOdH.d.ts} +7 -7
- package/dist/{credential-DM2um3ha.js → credential-BcoiejpY.js} +2 -2
- package/dist/{delegation-DaXVdDhM.js → delegation-j6-rc37o.js} +2 -2
- package/dist/delivery/index.d.ts +4 -4
- package/dist/delivery/index.js +1 -1
- package/dist/{denied-text-D8jDHgsl.d.ts → denied-text-CiFQNuMV.d.ts} +2 -2
- package/dist/{describe-BZUsixiL.d.ts → describe-B5qgDvEW.d.ts} +2 -2
- package/dist/{diff-DCePiN6M.js → diff-3jbx6Rm4.js} +3 -3
- package/dist/{diff-CxZo9pTq.js → diff-CmuSD3vc.js} +1 -1
- package/dist/{doctor-CxiwSfqH.js → doctor-3ZqNHX_G.js} +2 -2
- package/dist/{doctor-DFjUxf2f.js → doctor-BdZw53-k.js} +14 -14
- package/dist/drizzle/index.d.ts +3 -3
- package/dist/elysia/index.d.ts +5 -5
- package/dist/elysia/index.js +3 -3
- package/dist/{errors-C2PXIv4c.d.ts → errors-DH5QEGXh.d.ts} +2 -2
- package/dist/{evaluate-_R8QkWaf.js → evaluate-CNClBvcj.js} +81 -28
- package/dist/{evaluations-16UqqQ6W.d.ts → evaluations-C_1vdSZc.d.ts} +3 -3
- package/dist/{evaluations-DZ2CbV5k.js → evaluations-beAooV0n.js} +1 -1
- 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-g0ypLkID.js → fetch-adapter-DD_gfmrC.js} +2 -2
- package/dist/{fetch-adapter-qKJBuRqy.d.ts → fetch-adapter-Dw0MTS3D.d.ts} +4 -4
- package/dist/{fields-D9tPVUUS.js → fields-OYG_JR78.js} +3 -3
- package/dist/{from-snapshot-D2lZt4Dd.js → from-snapshot-CTCMsFZk.js} +4 -4
- package/dist/{grantee-Cn1HyUsI.js → grantee-hxrUsbpM.js} +13 -3
- package/dist/{grantee-match-DHArqY82.js → grantee-match-DbENBHLB.js} +1 -1
- package/dist/h3/index.d.ts +4 -4
- package/dist/h3/index.js +3 -3
- package/dist/{helpers-DTcZOzhF.js → helpers-CBYVMjwh.js} +1 -1
- package/dist/hono/index.d.ts +5 -5
- package/dist/hono/index.js +3 -3
- package/dist/{hosted-BZIAYvon.js → hosted-B46-5LYE.js} +3 -3
- package/dist/{hosted-DBDX0M-P.d.ts → hosted-BPk51-kX.d.ts} +1 -1
- package/dist/{import-DMmOG27O.js → import-wyqXUyjm.js} +3 -3
- package/dist/{index-Chrs5fQ-.d.ts → index-B8YjkEZ4.d.ts} +6 -6
- package/dist/{index-CxGUswIC.d.ts → index-C-6Letxg.d.ts} +5 -5
- package/dist/{index-SGdIQyzc.d.ts → index-Ch8LpNDp.d.ts} +2 -2
- package/dist/{index-D8EovfsP.d.ts → index-Cp_ilKi1.d.ts} +2 -2
- package/dist/{index-BcHcPyIo.d.ts → index-CueFnxQc.d.ts} +6 -6
- package/dist/{index-DOVf7RJv.d.ts → index-DuWxc5dt.d.ts} +21 -12
- package/dist/index.d.ts +17 -17
- package/dist/index.js +14 -14
- package/dist/{instance-options-Omfd793B.d.ts → instance-options-BsVpyGIk.d.ts} +1 -1
- package/dist/{instance-snapshot-Cxq4ygFc.js → instance-snapshot-BOEhcqIF.js} +10 -9
- package/dist/jwt/index.d.ts +4 -4
- package/dist/{kernel-5JatXyND.js → kernel-D0NVb1cK.js} +5 -5
- package/dist/kysely/index.d.ts +3 -3
- package/dist/{manifest-CtKR3GXL.js → manifest-dvL2XEe1.js} +23 -23
- package/dist/{map-error-TCAdw-eW.js → map-error-Dr70oA_3.js} +1 -1
- package/dist/mcp/index.d.ts +9 -9
- package/dist/mcp/index.js +4 -4
- package/dist/{migrate-BShP7Vf4.js → migrate-CIq4-E9F.js} +1 -1
- 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 +3 -3
- package/dist/next/plugin.d.ts +1 -1
- package/dist/next/plugin.js +1 -1
- 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 +3 -3
- package/dist/{openapi-ahr8rHu3.js → openapi-C4dDTVEd.js} +1 -1
- package/dist/{openapi-DTaEmTSW.js → openapi-DD1SV-5-.js} +1 -1
- package/dist/{operations-CxybZErY.d.ts → operations-X0VM7F3m.d.ts} +1 -1
- package/dist/orpc/index.d.ts +6 -6
- package/dist/orpc/index.js +3 -3
- package/dist/otel/index.d.ts +3 -3
- package/dist/{output-Dyw8fyPF.js → output-1im__-08.js} +1 -1
- package/dist/{ownership-BatFgAA4.js → ownership-DqaJNJPn.js} +921 -858
- package/dist/{parse-local-manifest-DgqtruPn.d.ts → parse-local-manifest-5ODwqu8b.d.ts} +1 -1
- package/dist/pdp/index.d.ts +3 -3
- package/dist/pdp/index.js +1 -1
- package/dist/{permdock-B9JxkBwC.js → permdock-BrAscV28.js} +5 -5
- package/dist/{permdock-D7eeLJER.d.ts → permdock-SV8ZINYT.d.ts} +4 -4
- package/dist/{permissions-CZKiUfFM.js → permissions-BPxlwVhN.js} +2 -2
- package/dist/{permissions-oEWfl-kl.d.ts → permissions-u_KHg-sN.d.ts} +1 -1
- package/dist/{policies-CO1oOymN.js → policies-DP2nqRoF.js} +1 -1
- package/dist/{policy-Czo4n3Hs.d.ts → policy-77FSAq_V.d.ts} +2 -2
- package/dist/{policy-DLZJ5gAp.js → policy-DbM5YYtV.js} +2 -2
- package/dist/{policy-from-catalog-DzhEc3Y9.js → policy-from-catalog-C2gMa2Dj.js} +5 -5
- package/dist/{policy-types-DzwivBgR.d.ts → policy-types-n0uRTsW8.d.ts} +28 -3
- package/dist/{postgrest-NUqZT-RB.d.ts → postgrest-CEgRbefE.d.ts} +1 -1
- package/dist/{postgrest-CW0IqC-U.js → postgrest-DAjUkF9w.js} +10 -1
- package/dist/{powersync-bYUDLOAO.js → powersync-BkWWCS1K.js} +6 -6
- package/dist/{powersync-BHfuehf3.js → powersync-DISt6SUe.js} +2 -2
- package/dist/powersync-MMJ2mI2q.js +1 -0
- package/dist/prisma/index.d.ts +2 -2
- package/dist/{problem-details-DXlt-vks.d.ts → problem-details-B45V5d2b.d.ts} +1 -1
- package/dist/{protected-DfjY5mLA.js → protected-BlF0F6wL.js} +1 -1
- package/dist/{protected-DEPHc2xV.d.ts → protected-RFGpakN9.d.ts} +5 -5
- package/dist/{provider-CC-u3bYP.js → provider-B1al-AZz.js} +2 -2
- package/dist/{provider-DMVt6yYn.d.ts → provider-DILL9oPK.d.ts} +1 -1
- package/dist/{provider-store-B5Yrkhzr.js → provider-store-DM6DNcta.js} +2 -2
- package/dist/pull-CvpINcMK.js +1 -0
- package/dist/{pull-BgIfno6w.js → pull-jEwbf80J.js} +1 -1
- package/dist/{push-CmFVBOeI.js → push-BD4f15co.js} +2 -2
- package/dist/{pushed-policy-DobP7AHI.js → pushed-policy-CqtxkV4G.js} +1 -1
- package/dist/{pushed-policy-BImMTTIM.d.ts → pushed-policy-DCW91KQU.d.ts} +1 -1
- package/dist/react/index.d.ts +6 -6
- package/dist/react/index.js +2 -2
- package/dist/react/provider-client.d.ts +1 -1
- package/dist/react/provider-client.js +1 -1
- package/dist/react-native/index.d.ts +9 -9
- package/dist/react-native/index.js +10 -8
- package/dist/{replay-D7iKcLFe.d.ts → replay-Bkm-Ne4l.d.ts} +1 -1
- package/dist/{request-context-BTRTG7OT.js → request-context-C81Pip9n.js} +1 -1
- package/dist/resend/index.d.ts +4 -4
- package/dist/{rls-BA8an5Ap.js → rls-BAqP4q66.js} +1 -1
- package/dist/{rls-j6t43t7n.js → rls-DTvYd-lI.js} +12 -12
- package/dist/{role-builder-CXbis5-X.js → role-builder-uWQZ4v84.js} +1 -1
- package/dist/{rpc-aPyNi6tA.js → rpc-VSl5ylaQ.js} +1 -1
- package/dist/{run-DOfMtc9V.js → run-CUKuz0A8.js} +2 -2
- package/dist/run-DDwUaJDU.js +1 -0
- package/dist/{run-ticket-BjhTOi6u.d.ts → run-ticket-DocwaTKn.d.ts} +2 -2
- package/dist/{saas-FO-Q8oj0.js → saas-BCXRuXca.js} +5 -5
- package/dist/{scan-BOI5tJ-6.js → scan-qvOe0hS3.js} +1 -1
- package/dist/scim/index.d.ts +1 -1
- package/dist/server/index.d.ts +10 -10
- package/dist/server/index.js +4 -4
- package/dist/{server-DqCT4CgZ.js → server-C3WXAHZj.js} +1 -1
- package/dist/{server-types-BLd4MnBX.d.ts → server-types-CKQiCnON.d.ts} +9 -9
- package/dist/{shared-CD11nBbC.js → shared-BAK8t4Uo.js} +2 -1
- package/dist/{sink-Bj_lDUDq.d.ts → sink-4-eQIRWf.d.ts} +4 -4
- package/dist/{snapshot-CXUWfsNy.js → snapshot-B7DkYg6g.js} +1 -1
- package/dist/{snapshot-cache-DqK9eDmM.d.ts → snapshot-cache-PXI-vGpY.d.ts} +1 -1
- package/dist/solid/index.d.ts +5 -5
- package/dist/solid/index.js +2 -2
- package/dist/solid-start/index.d.ts +6 -6
- package/dist/solid-start/index.js +3 -3
- package/dist/{sources-BV9j0DeZ.d.ts → sources-Xc4zkszI.d.ts} +2 -2
- package/dist/{src-DLfUWFAI.js → src-Cu2m75uo.js} +24 -15
- package/dist/ssf/index.d.ts +3 -3
- package/dist/ssf/index.js +1 -1
- package/dist/{standard-webhooks-6lg8v_is.d.ts → standard-webhooks-rbD7LS6h.d.ts} +2 -2
- package/dist/{store-4u_hxjWa.js → store-BDutJkDL.js} +1 -1
- package/dist/{store-options-BqEakYRa.js → store-options-u0ycEe8s.js} +4 -4
- package/dist/{stores-ClZUgeoP.d.ts → stores-BUGNbJTt.d.ts} +5 -5
- package/dist/{stores-BbsD4fAR.js → stores-CGhM8BN_.js} +1 -1
- package/dist/{stream-7-xmnf3U.d.ts → stream-CeCMJmBB.d.ts} +2 -2
- package/dist/supabase/index.d.ts +5 -5
- package/dist/supabase/index.js +2 -2
- package/dist/supabase/middleware.d.ts +6 -6
- package/dist/supabase/middleware.js +5 -5
- package/dist/{supabase-9pqI57pW.js → supabase-By_frk3U.js} +2 -2
- package/dist/{supabase-hook-DdDQhMlw.js → supabase-hook-BMoEhZir.js} +14 -9
- package/dist/supabase-hook-CCw5m35C.js +1 -0
- package/dist/{supabase-Dxlp8A94.js → supabase-x5JtR2tc.js} +3 -3
- package/dist/svelte/index.d.ts +5 -5
- package/dist/svelte/index.js +2 -2
- package/dist/svelte/runtime.d.ts +3 -3
- package/dist/svelte/runtime.js +1 -1
- package/dist/svelte/source.d.ts +5 -5
- package/dist/svelte/source.js +1 -1
- 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/terminal/index.d.ts +6 -6
- package/dist/terminal/index.js +3 -3
- package/dist/testing/index.d.ts +20 -20
- package/dist/testing/index.js +18 -18
- 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/trpc/index.d.ts +6 -6
- package/dist/trpc/index.js +3 -3
- package/dist/{types-C8-KNsFf.d.ts → types-8RnwUAvE.d.ts} +5 -5
- package/dist/{types-BNORmDM8.d.ts → types-B3_ooZgZ.d.ts} +4 -4
- package/dist/{types-CCpc2Dcv.d.ts → types-BdtSDbBO.d.ts} +2 -2
- package/dist/{types-DX0LaKTb.d.ts → types-BtHtcKMr.d.ts} +3 -3
- package/dist/{types-CdlxsYQh.d.ts → types-BzzaHxt3.d.ts} +1 -1
- package/dist/{types-DiQjupG2.d.ts → types-C791HFu9.d.ts} +2 -2
- package/dist/{types-BjV-fqcA.d.ts → types-C863OQiR.d.ts} +4 -4
- package/dist/{types-fbZ7JEXr.d.ts → types-CVZSNocA.d.ts} +4 -4
- package/dist/{types-tIgIeIKJ.d.ts → types-CX7AvRqP.d.ts} +4 -4
- package/dist/{types-_sHy8BQf.d.ts → types-C_IiEs64.d.ts} +1 -1
- package/dist/{types-5Ut_pbKy.d.ts → types-Dfv0aTER.d.ts} +4 -2
- package/dist/{types-CZTnPFzM.d.ts → types-Druu9wLM.d.ts} +2 -2
- package/dist/{types-ClZaWzfb.d.ts → types-_WhUP7ud.d.ts} +3 -3
- package/dist/{types-BqAKcRNW.d.ts → types-lUlG3TVd.d.ts} +2 -2
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/{usage-DHtE8j4E.js → usage-CRVl9V3t.js} +1 -1
- package/dist/usage-CZpSQP6L.js +1 -0
- package/dist/{usage-T5mXq98v.js → usage-SveTRTyr.js} +4 -4
- package/dist/{validation-error-Bg5Tpluk.d.ts → validation-error-DHUrAkku.d.ts} +1 -1
- package/dist/{verify-DbCL9bnB.js → verify-BaAKx7t6.js} +11 -11
- package/dist/{vocabulary-D6lPdb4Z.d.ts → vocabulary-CN8H_9UF.d.ts} +2 -0
- package/dist/vue/index.d.ts +5 -5
- package/dist/vue/index.js +2 -2
- package/dist/{waiter-CKy4MQSY.d.ts → waiter-B986w1oK.d.ts} +2 -2
- package/dist/watch--ovlPOc0.js +1 -0
- package/dist/{watch-CszoTJMq.js → watch-vea54PRw.js} +1 -1
- package/dist/webmcp/index.d.ts +2 -2
- package/dist/{where-scope-Dv-WTP-w.js → where-scope-C9vfEphl.js} +2 -2
- package/dist/{who-can-D0QoYbEb.js → who-can-DWBAcaCQ.js} +2 -2
- package/dist/workflow/index.d.ts +3 -3
- package/dist/workflow/index.js +2 -2
- package/dist/workflow-sdk/index.d.ts +5 -5
- package/dist/{wrap-Bd1Tejur.d.ts → wrap-C5jC6sKI.d.ts} +2 -2
- package/package.json +2 -2
- package/skills/permdock-data/references/queries-and-rls.md +1 -1
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +1 -0
- package/dist/catalog-DjhMYYCq.js +0 -1
- package/dist/collect-FlXJlhus.js +0 -1
- package/dist/powersync-CbzkqL_M.js +0 -1
- 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/usage-CHOtwL19.js +0 -1
- package/dist/watch-Cb-NcaUw.js +0 -1
|
@@ -4,14 +4,14 @@ import { l as isSqlFunctionField } from "./refs-K6eWUJ0H.js";
|
|
|
4
4
|
import { u as isoAt } from "./time-DkWjdL98.js";
|
|
5
5
|
import { c as rootScope, d as scopeList, l as scopeChain, o as resolveScope } from "./scopes-CUPSjo9q.js";
|
|
6
6
|
import { t as isoInstant } from "./instant-BluqDvyS.js";
|
|
7
|
-
import { m as findRole, v as listRoles } from "./grantee-
|
|
7
|
+
import { m as findRole, v as listRoles } from "./grantee-hxrUsbpM.js";
|
|
8
8
|
import { r as relationCondition, t as inheritCondition } from "./grantee-conditions-CcXf8hMM.js";
|
|
9
9
|
import { l as partitionsOf, p as scopeField } from "./tenancy-DN2QAx9z.js";
|
|
10
10
|
import { i as configError } from "./errors-CWTY6uih.js";
|
|
11
11
|
import { i as scopeMembershipTable, o as relatedSql, r as scopeColumn } from "./compile-CMqMx_IN.js";
|
|
12
12
|
import { i as qualifySqlTable, t as SQL_IDENT, u as sqlTextArray } from "./sql-B5T1IL83.js";
|
|
13
13
|
import { d as keptKeys, l as roleColumn, u as roleManifest } from "./sources-vs5dGOsS.js";
|
|
14
|
-
import { A as declaredArray, D as sqlType, E as scopeTypeOf, F as quoteQualified, I as quoteTable, L as textArrayLiteral, M as qualified, N as quoteIdent, O as tenantTypeOf, P as quoteLiteral, S as CLAIM, _ as subjectClaimJsonSql, b as tenantClaimSql, d as keyTenantSql, f as kindFilterSql, h as roleKindSql, i as allowEntry, j as helperSchema, k as CLI, m as memberVia, n as GLOBAL_ROLES, o as rootName, p as liveSessionSql, r as HELPERS, s as signedIn, t as CUSTOM_ROLES, u as globalKindFilterSql, v as subjectClaimSql, x as userIdHelperSql, y as subjectIdSql } from "./shared-
|
|
14
|
+
import { A as declaredArray, D as sqlType, E as scopeTypeOf, F as quoteQualified, I as quoteTable, L as textArrayLiteral, M as qualified, N as quoteIdent, O as tenantTypeOf, P as quoteLiteral, S as CLAIM, _ as subjectClaimJsonSql, b as tenantClaimSql, d as keyTenantSql, f as kindFilterSql, h as roleKindSql, i as allowEntry, j as helperSchema, k as CLI, m as memberVia, n as GLOBAL_ROLES, o as rootName, p as liveSessionSql, r as HELPERS, s as signedIn, t as CUSTOM_ROLES, u as globalKindFilterSql, v as subjectClaimSql, x as userIdHelperSql, y as subjectIdSql } from "./shared-BAK8t4Uo.js";
|
|
15
15
|
//#region src/cli/sql/active.ts
|
|
16
16
|
function checkActiveRow(label, row) {
|
|
17
17
|
quoteTable(row.table);
|
|
@@ -840,909 +840,963 @@ function byKey([a], [b]) {
|
|
|
840
840
|
return byCodeUnit(a, b);
|
|
841
841
|
}
|
|
842
842
|
//#endregion
|
|
843
|
-
//#region src/cli/
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
prefix: "PermDock CLI",
|
|
858
|
-
indent: " "
|
|
859
|
-
});
|
|
860
|
-
const tenant = role.lateral ? void 0 : throughTenant(role.through, roles);
|
|
861
|
-
const global = tenant === void 0 ? "" : ` and ${alias}k.${quoteIdent(tenant)} is null`;
|
|
862
|
-
return {
|
|
863
|
-
table,
|
|
864
|
-
user,
|
|
865
|
-
from: `${quoteTable(table)} ${alias}${role.join}${global}`,
|
|
866
|
-
userSql: `${alias}.${quoteIdent(user)}`,
|
|
867
|
-
roleSql: role.sql,
|
|
868
|
-
role: roleManifest(role),
|
|
869
|
-
...role.through === void 0 ? {} : { through: role.through },
|
|
870
|
-
...tenant === void 0 ? {} : { tenant }
|
|
871
|
-
};
|
|
872
|
-
}
|
|
873
|
-
//#endregion
|
|
874
|
-
//#region src/cli/rls/helper-rows.ts
|
|
875
|
-
function activeTenant(ctx) {
|
|
876
|
-
return `nullif(${subjectClaimSql(ctx, ctx.tenantClaim)}, '')`;
|
|
877
|
-
}
|
|
878
|
-
function keyTenantLine(ctx, scope, tenant) {
|
|
879
|
-
const keyed = keyTenantSql(ctx, underRoot(ctx, scope) ? tenant : void 0);
|
|
880
|
-
return keyed === void 0 ? "" : `\n and ${keyed}`;
|
|
881
|
-
}
|
|
882
|
-
/** Whether the helper for `scope` narrows to the tenant claim: unless `rls.tenants` is `'all'`, when the first scope is on its chain. */
|
|
883
|
-
function narrowsTo(ctx, scope) {
|
|
884
|
-
return ctx.tenants !== "all" && underRoot(ctx, scope);
|
|
885
|
-
}
|
|
886
|
-
/** Whether the active tenant narrows memberships of `scope`: the first scope is on its chain. */
|
|
887
|
-
function underRoot(ctx, scope) {
|
|
888
|
-
const root = ctx.scopes[0]?.name;
|
|
889
|
-
let current = ctx.scopes.find((item) => item.name === scope);
|
|
890
|
-
const seen = /* @__PURE__ */ new Set();
|
|
891
|
-
while (current !== void 0 && !seen.has(current.name)) {
|
|
892
|
-
if (current.name === root) return true;
|
|
893
|
-
seen.add(current.name);
|
|
894
|
-
const parent = current.within;
|
|
895
|
-
current = parent === void 0 ? void 0 : ctx.scopes.find((item) => item.name === parent);
|
|
896
|
-
}
|
|
897
|
-
return false;
|
|
898
|
-
}
|
|
899
|
-
/** `and exists (...)` lines for an active user; empty without `rls.suspension.users`. */
|
|
900
|
-
function userActive(ctx, indent, user) {
|
|
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)`;
|
|
922
|
-
}
|
|
923
|
-
default: return ctx.dialect;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
function membershipRows(ctx) {
|
|
927
|
-
const raw = subjectClaimJsonSql(ctx, "memberships");
|
|
928
|
-
const value = ctx.dialect === "supabase" ? `coalesce(${raw}, (select auth.jwt()) -> 'app_metadata' -> 'memberships')` : raw;
|
|
929
|
-
return `jsonb_array_elements(
|
|
930
|
-
case jsonb_typeof(${value}) when 'array' then ${value} else '[]'::jsonb end
|
|
931
|
-
) m
|
|
932
|
-
cross join lateral jsonb_array_elements_text(
|
|
933
|
-
case jsonb_typeof(m -> 'roles') when 'array' then m -> 'roles' else '[]'::jsonb end
|
|
934
|
-
) r(role)`;
|
|
935
|
-
}
|
|
936
|
-
function andLine(indent, condition) {
|
|
937
|
-
return condition === void 0 ? "" : `\n${indent}and ${condition}`;
|
|
938
|
-
}
|
|
939
|
-
/** `rls.roles`, or the generated `user_roles (user_id, role)`, aliased `ur`. */
|
|
940
|
-
function globalRoleRows(ctx) {
|
|
941
|
-
return ctx.roles === void 0 ? {
|
|
942
|
-
from: `${qualified(ctx, "user_roles")} ur`,
|
|
943
|
-
userSql: "ur.user_id",
|
|
944
|
-
roleSql: "ur.role::text"
|
|
945
|
-
} : globalRoleSource(ctx.roles, "public", "ur", ctx.customRoles?.table);
|
|
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)`;
|
|
954
|
-
}
|
|
955
|
-
//#endregion
|
|
956
|
-
//#region src/cli/rls/helper-bodies.ts
|
|
957
|
-
function hasSetBody(ctx) {
|
|
958
|
-
const rp = qualified(ctx, "role_permissions");
|
|
959
|
-
const active = userActive(ctx, " ").map((line) => `\n${line}`).join("");
|
|
843
|
+
//#region src/cli/rls/custom-roles.ts
|
|
844
|
+
/** The readers every helpers file ends with. Names are part of the SQL contract. */
|
|
845
|
+
const READERS = {
|
|
846
|
+
keys: "permdock_permission_keys",
|
|
847
|
+
roles: "permdock_role_permissions",
|
|
848
|
+
trusted: "permdock_trusted_role_permissions"
|
|
849
|
+
};
|
|
850
|
+
/**
|
|
851
|
+
* The custom-role objects: the tables (`database` mode), the ceiling view
|
|
852
|
+
* over the `assignable` declared roles, and `permdock_custom_keys`, which
|
|
853
|
+
* resolves one custom role to the grant keys it holds in a scope. A row in
|
|
854
|
+
* the tables can never reach a key outside the view.
|
|
855
|
+
*/
|
|
856
|
+
function customRolesSql(ctx) {
|
|
960
857
|
const custom = ctx.customRoles;
|
|
961
|
-
if (
|
|
962
|
-
const ur = globalRoleRows(ctx);
|
|
963
|
-
const declared = ` select rp.grant_key
|
|
964
|
-
from ${ur.from}
|
|
965
|
-
join ${rp} rp on rp.role = ${ur.roleSql}
|
|
966
|
-
where ${ur.userSql} = ${subjectIdSql(ctx)}
|
|
967
|
-
and rp.scope = 'global'${andLine(" ", globalKindFilterSql(ctx, ur.roleSql))}${active}`;
|
|
968
|
-
if (custom === void 0) return declared;
|
|
969
|
-
const match = `c.tenant_id is null and c.scope = 'global' and c.scope_id is null and c.role = ${ur.roleSql}`;
|
|
970
|
-
const rows = (source, value, extra) => `array(select c.${value} from ${qualified(ctx, source)} c where ${match}${extra})`;
|
|
971
|
-
return `${declared}
|
|
972
|
-
union
|
|
973
|
-
select ck.grant_key
|
|
974
|
-
from ${ur.from}
|
|
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}
|
|
987
|
-
union
|
|
988
|
-
select ck.grant_key
|
|
989
|
-
from ${roleRows(ctx)}
|
|
990
|
-
cross join lateral (select ${claim} -> r.role as g) cg
|
|
991
|
-
${customKeysJoin(ctx, "global", claimEntries("left(e, 1) not in ('-', '@')", "e"), claimEntries("left(e, 1) = '-'", "substr(e, 2)"), claimEntries("left(e, 1) = '@'", "substr(e, 2)"))}
|
|
992
|
-
where ${signedIn(ctx)}
|
|
993
|
-
and jsonb_typeof(cg.g) = 'array'
|
|
994
|
-
and not (r.role = any(${sqlTextArray(custom.declared)}))${active}`;
|
|
995
|
-
}
|
|
996
|
-
function hasBody(ctx) {
|
|
997
|
-
if (ctx.grantSet === true) return hasSetBody(ctx);
|
|
858
|
+
if (custom === void 0) return "";
|
|
998
859
|
const rp = qualified(ctx, "role_permissions");
|
|
999
|
-
const
|
|
1000
|
-
const
|
|
860
|
+
const ceiling = qualified(ctx, CUSTOM_ROLES.ceiling);
|
|
861
|
+
const keys = qualified(ctx, CUSTOM_ROLES.keys);
|
|
862
|
+
const chunks = [];
|
|
1001
863
|
if (ctx.authorize === "database") {
|
|
1002
|
-
const
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
)
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
${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()}
|
|
1040
|
-
))`;
|
|
1041
|
-
}
|
|
1042
|
-
/** The claim `jwt` mode reads platform custom roles from: `customRoleClaim(globalRoles)`. */
|
|
1043
|
-
const GLOBAL_GRANTS_CLAIM = "role_grants";
|
|
1044
|
-
function memberColumn(name) {
|
|
1045
|
-
return `m.${quoteIdent(name)}`;
|
|
1046
|
-
}
|
|
1047
|
-
function disabledColumn(table) {
|
|
1048
|
-
return table.disabledAt === void 0 ? void 0 : memberColumn(table.disabledAt);
|
|
1049
|
-
}
|
|
1050
|
-
function claimKeptLines(ctx, indent, permission) {
|
|
1051
|
-
return disabledKeep(ctx).length === 0 ? [] : [`${indent}and ${keptRowSql("m -> 'keep'", permission)}`];
|
|
1052
|
-
}
|
|
1053
|
-
/** Entries of a compact custom-role claim (`cg.g`) that match `where`. */
|
|
1054
|
-
function claimEntries(where, value) {
|
|
1055
|
-
return `array(select ${value} from jsonb_array_elements_text(cg.g) e where ${where})`;
|
|
1056
|
-
}
|
|
1057
|
-
/** `p_grant in (select permdock_custom_keys(allows, denies, includes, scope))`. */
|
|
1058
|
-
function customKeysSql(ctx, scope, allows, denies, includes) {
|
|
1059
|
-
return ` and p_grant in (select ${qualified(ctx, CUSTOM_ROLES.keys)}(
|
|
1060
|
-
${allows},
|
|
1061
|
-
${denies},
|
|
1062
|
-
${includes},
|
|
1063
|
-
${quoteLiteral(scope)}
|
|
1064
|
-
))`;
|
|
1065
|
-
}
|
|
1066
|
-
function tableBody(ctx, scope, type) {
|
|
1067
|
-
const mapped = scopeTable(ctx, scope);
|
|
1068
|
-
if (mapped === void 0) return ` select null::${type} where false -- no ${scope} memberships table configured`;
|
|
1069
|
-
const { table, column, tenantColumn } = mapped;
|
|
1070
|
-
const set = ctx.grantSet === true;
|
|
1071
|
-
const filtersFor = (grant) => {
|
|
1072
|
-
const filters = [` where ${memberColumn(table.user)} = ${subjectIdSql(ctx)}`];
|
|
1073
|
-
if (table.expiresAt !== void 0) {
|
|
1074
|
-
const expires = memberColumn(table.expiresAt);
|
|
1075
|
-
filters.push(` and (${expires} is null or ${expires} > now())`);
|
|
864
|
+
const tenantType = tenantTypeOf(ctx);
|
|
865
|
+
for (const [name, column, check] of [[
|
|
866
|
+
CUSTOM_ROLES.permissions,
|
|
867
|
+
"permission",
|
|
868
|
+
`,
|
|
869
|
+
effect text not null default 'allow' check (effect in ('allow', 'deny'))`
|
|
870
|
+
], [
|
|
871
|
+
CUSTOM_ROLES.includes,
|
|
872
|
+
"include_role",
|
|
873
|
+
""
|
|
874
|
+
]]) {
|
|
875
|
+
const table = qualified(ctx, name);
|
|
876
|
+
const leveled = custom.levels === true && name === CUSTOM_ROLES.permissions;
|
|
877
|
+
const unique = `${column}${name === CUSTOM_ROLES.permissions ? ", effect" : ""}${leveled ? ", coalesce(level, '')" : ""}`;
|
|
878
|
+
const index = leveled ? `drop index if exists ${qualified(ctx, quoteIdent(`${name}_key`))};
|
|
879
|
+
alter table ${table} add column if not exists level text check (level ~ '^[a-z][a-z0-9_]*$');
|
|
880
|
+
create unique index if not exists ${quoteIdent(`${name}_level_key`)}` : `create unique index if not exists ${quoteIdent(`${name}_key`)}`;
|
|
881
|
+
chunks.push(`create table if not exists ${table} (
|
|
882
|
+
tenant_id ${tenantType},
|
|
883
|
+
scope text not null default ${quoteLiteral(rootName(ctx))},
|
|
884
|
+
scope_id text,
|
|
885
|
+
role text not null,
|
|
886
|
+
${column} text not null${check}
|
|
887
|
+
);
|
|
888
|
+
-- global: no tenant or instance; a tenant role: a tenant; a shared role: no tenant or instance
|
|
889
|
+
alter table ${table} drop constraint if exists ${quoteIdent(`${name}_check`)};
|
|
890
|
+
alter table ${table} drop constraint if exists ${quoteIdent(`${name}_shape`)};
|
|
891
|
+
alter table ${table} add constraint ${quoteIdent(`${name}_shape`)} check (
|
|
892
|
+
case when scope = 'global' then tenant_id is null and scope_id is null
|
|
893
|
+
else tenant_id is not null or scope_id is null end
|
|
894
|
+
);
|
|
895
|
+
${index}
|
|
896
|
+
on ${table} (coalesce(tenant_id::text, ''), scope, coalesce(scope_id, ''), role, ${unique});
|
|
897
|
+
-- a row identity for triggers the application adds, such as an audit trigger that reads the primary key
|
|
898
|
+
alter table ${table} add column if not exists id bigint generated always as identity primary key;
|
|
899
|
+
alter table ${table} enable row level security;
|
|
900
|
+
revoke all on table ${table} from anon, authenticated, public;`);
|
|
1076
901
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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}
|
|
902
|
+
}
|
|
903
|
+
chunks.push(`-- the ceiling: keys of assignable declared roles, and the denies of those roles on the same permissions
|
|
904
|
+
create or replace view ${ceiling} with (security_invoker = true) as
|
|
905
|
+
select rp.scope, rp.role, rp.permission, rp.grant_key, rp.effect
|
|
906
|
+
from ${rp} rp
|
|
907
|
+
where rp.role = any(${sqlTextArray(custom.assignable)})
|
|
908
|
+
and exists (
|
|
909
|
+
select 1 from ${rp} a
|
|
910
|
+
where a.role = rp.role
|
|
911
|
+
and a.permission = rp.permission
|
|
912
|
+
and a.scope = rp.scope
|
|
913
|
+
and a.effect = 'allow'
|
|
914
|
+
);
|
|
915
|
+
revoke all on table ${ceiling} from anon, authenticated, public;`);
|
|
916
|
+
const renames = Object.entries(custom.renamed ?? {}).toSorted(byKey);
|
|
917
|
+
const shared = sharedRoleSql(ctx);
|
|
918
|
+
if (custom.levels === true) {
|
|
919
|
+
chunks.push(leveledKeysSql(ctx, renames));
|
|
920
|
+
if (shared !== "") chunks.push(shared);
|
|
921
|
+
return chunks.join("\n\n");
|
|
922
|
+
}
|
|
923
|
+
const rename = renamesSql(renames);
|
|
924
|
+
chunks.push(`-- one custom role: included ceiling keys and same-scope denies, plus every ceiling key of an allowed permission, minus denied permissions
|
|
925
|
+
create or replace function ${keys}(p_allow text[], p_deny text[], p_include text[], p_scope text)
|
|
926
|
+
returns setof text
|
|
927
|
+
language sql
|
|
928
|
+
stable
|
|
929
|
+
set search_path = ''
|
|
930
|
+
as $$
|
|
931
|
+
with ${rename.prelude}ceiling as (
|
|
932
|
+
select c.role, ${rename.permission("c")}, c.grant_key, c.effect
|
|
933
|
+
from ${ceiling} c${rename.join("c")}
|
|
934
|
+
where c.scope = p_scope
|
|
935
|
+
),
|
|
936
|
+
included as (
|
|
937
|
+
select rp.role, ${rename.permission("rp")}, rp.grant_key, rp.scope, rp.effect
|
|
938
|
+
from ${rp} rp${rename.join("rp")}
|
|
939
|
+
where rp.role = any(coalesce(p_include, '{}'::text[]))
|
|
940
|
+
),
|
|
941
|
+
kept as (
|
|
942
|
+
select i.permission, i.grant_key
|
|
943
|
+
from included i
|
|
944
|
+
where i.effect = 'allow'
|
|
945
|
+
and i.scope = p_scope
|
|
946
|
+
and exists (
|
|
947
|
+
select 1 from ceiling c
|
|
948
|
+
where c.role = i.role and c.grant_key = i.grant_key and c.effect = 'allow'
|
|
949
|
+
)
|
|
950
|
+
),
|
|
951
|
+
wanted as (
|
|
952
|
+
${rename.allow}
|
|
953
|
+
union
|
|
954
|
+
select i.permission
|
|
955
|
+
from included i
|
|
956
|
+
where i.effect = 'allow'
|
|
957
|
+
and not (i.scope = p_scope and exists (
|
|
958
|
+
select 1 from ceiling c
|
|
959
|
+
where c.role = i.role and c.grant_key = i.grant_key and c.effect = 'allow'
|
|
960
|
+
))
|
|
961
|
+
),
|
|
962
|
+
allowed as (
|
|
963
|
+
select w.permission
|
|
964
|
+
from wanted w
|
|
965
|
+
where not (${rename.denied("w")})
|
|
966
|
+
and not exists (select 1 from kept k where k.permission = w.permission)
|
|
967
|
+
)
|
|
968
|
+
select k.grant_key from kept k
|
|
969
|
+
where not (${rename.denied("k")})
|
|
1163
970
|
union
|
|
1164
|
-
select
|
|
1165
|
-
|
|
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}
|
|
971
|
+
select i.grant_key from included i
|
|
972
|
+
where i.effect = 'deny' and i.scope = p_scope
|
|
1173
973
|
union
|
|
1174
|
-
select
|
|
1175
|
-
from
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
974
|
+
select c.grant_key from ceiling c
|
|
975
|
+
where c.permission in (select a.permission from allowed a)
|
|
976
|
+
$$;
|
|
977
|
+
revoke execute on function ${keys}(text[], text[], text[], text) from public, anon, authenticated;`);
|
|
978
|
+
if (shared !== "") chunks.push(shared);
|
|
979
|
+
return chunks.join("\n\n");
|
|
980
|
+
}
|
|
981
|
+
/** `permdock_is_shared_role(p_scope, p_role)` (`database` mode): whether a shared custom role of that name exists at the scope. */
|
|
982
|
+
function sharedRoleSql(ctx) {
|
|
983
|
+
if (ctx.authorize !== "database") return "";
|
|
984
|
+
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
985
|
+
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
986
|
+
const shared = qualified(ctx, CUSTOM_ROLES.shared);
|
|
987
|
+
return `-- a shared custom role: rows with no tenant at a scope below global; its name wins over a tenant role's in every tenant
|
|
988
|
+
create or replace function ${shared}(p_scope text, p_role text)
|
|
989
|
+
returns boolean
|
|
990
|
+
language sql
|
|
991
|
+
stable
|
|
992
|
+
security definer
|
|
993
|
+
set search_path = ''
|
|
994
|
+
as $$
|
|
995
|
+
select p_scope <> 'global' and (
|
|
996
|
+
exists (select 1 from ${perms} c where c.tenant_id is null and c.scope = p_scope and c.role = p_role)
|
|
997
|
+
or exists (select 1 from ${includes} c where c.tenant_id is null and c.scope = p_scope and c.role = p_role)
|
|
998
|
+
)
|
|
999
|
+
$$;
|
|
1000
|
+
revoke execute on function ${shared}(text, text) from public, anon, authenticated;`;
|
|
1182
1001
|
}
|
|
1183
1002
|
/**
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1003
|
+
* `permdock_custom_keys` when resources declare levels. An allow entry is
|
|
1004
|
+
* `key` or `key@level`. A level reaches only the ceiling's `<grant key>@<level>`
|
|
1005
|
+
* keys, plus the ceiling denies of that permission; a level no ceiling key
|
|
1006
|
+
* carries removes the permission, as `resolveCustomRole` drops it with
|
|
1007
|
+
* `unknown-level`.
|
|
1187
1008
|
*/
|
|
1188
|
-
function
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1009
|
+
function leveledKeysSql(ctx, renames) {
|
|
1010
|
+
const rp = qualified(ctx, "role_permissions");
|
|
1011
|
+
const ceiling = qualified(ctx, CUSTOM_ROLES.ceiling);
|
|
1012
|
+
const keys = qualified(ctx, CUSTOM_ROLES.keys);
|
|
1013
|
+
const renamed = renames.length > 0;
|
|
1014
|
+
const prelude = renamed ? `renamed (former, key) as (values ${renames.map(([former, current]) => `(${quoteLiteral(former)}, ${quoteLiteral(current)})`).join(", ")}),
|
|
1015
|
+
` : "";
|
|
1016
|
+
const current = (raw, alias) => renamed ? `coalesce(r_${alias}.key, ${raw})` : raw;
|
|
1017
|
+
const join = (raw, alias) => renamed ? ` left join renamed r_${alias} on r_${alias}.former = ${raw}` : "";
|
|
1018
|
+
return `-- one custom role: as above, where an allow entry key@level reaches only the ceiling keys of that level
|
|
1019
|
+
create or replace function ${keys}(p_allow text[], p_deny text[], p_include text[], p_scope text)
|
|
1020
|
+
returns setof text
|
|
1021
|
+
language sql
|
|
1022
|
+
stable
|
|
1023
|
+
set search_path = ''
|
|
1024
|
+
as $$
|
|
1025
|
+
with ${prelude}entries as (
|
|
1026
|
+
select ${current("split_part(e.entry, '@', 1)", "e")} as permission, nullif(split_part(e.entry, '@', 2), '') as level
|
|
1027
|
+
from unnest(coalesce(p_allow, '{}'::text[])) as e(entry)${join("split_part(e.entry, '@', 1)", "e")}
|
|
1028
|
+
),
|
|
1029
|
+
ceiling as (
|
|
1030
|
+
select c.role, ${current("c.permission", "c")} as permission, c.grant_key, c.effect
|
|
1031
|
+
from ${ceiling} c${join("c.permission", "c")}
|
|
1032
|
+
where c.scope = p_scope
|
|
1033
|
+
),
|
|
1034
|
+
levels as (
|
|
1035
|
+
select distinct c.permission, split_part(c.grant_key, '@', 2) as level
|
|
1036
|
+
from ceiling c
|
|
1037
|
+
where c.effect = 'allow' and strpos(c.grant_key, '@') > 0
|
|
1038
|
+
),
|
|
1039
|
+
denied as (
|
|
1040
|
+
select ${current("d.permission", "d")} as permission
|
|
1041
|
+
from unnest(coalesce(p_deny, '{}'::text[])) as d(permission)${join("d.permission", "d")}
|
|
1042
|
+
union
|
|
1043
|
+
select e.permission from entries e
|
|
1044
|
+
where e.level is not null
|
|
1045
|
+
and not exists (select 1 from levels l where l.permission = e.permission and l.level = e.level)
|
|
1046
|
+
),
|
|
1047
|
+
included as (
|
|
1048
|
+
select rp.role, ${current("rp.permission", "rp")} as permission, rp.grant_key, rp.scope, rp.effect
|
|
1049
|
+
from ${rp} rp${join("rp.permission", "rp")}
|
|
1050
|
+
where rp.role = any(coalesce(p_include, '{}'::text[]))
|
|
1051
|
+
),
|
|
1052
|
+
kept as (
|
|
1053
|
+
select i.permission, i.grant_key
|
|
1054
|
+
from included i
|
|
1055
|
+
where i.effect = 'allow'
|
|
1056
|
+
and i.scope = p_scope
|
|
1057
|
+
and exists (
|
|
1058
|
+
select 1 from ceiling c
|
|
1059
|
+
where c.role = i.role and c.grant_key = i.grant_key and c.effect = 'allow'
|
|
1060
|
+
)
|
|
1061
|
+
),
|
|
1062
|
+
wanted as (
|
|
1063
|
+
select e.permission from entries e where e.level is null
|
|
1064
|
+
union
|
|
1065
|
+
select i.permission
|
|
1066
|
+
from included i
|
|
1067
|
+
where i.effect = 'allow'
|
|
1068
|
+
and not (i.scope = p_scope and exists (
|
|
1069
|
+
select 1 from ceiling c
|
|
1070
|
+
where c.role = i.role and c.grant_key = i.grant_key and c.effect = 'allow'
|
|
1071
|
+
))
|
|
1072
|
+
),
|
|
1073
|
+
allowed as (
|
|
1074
|
+
select w.permission
|
|
1075
|
+
from wanted w
|
|
1076
|
+
where w.permission not in (select d.permission from denied d)
|
|
1077
|
+
and not exists (select 1 from kept k where k.permission = w.permission)
|
|
1078
|
+
),
|
|
1079
|
+
allowed_levels as (
|
|
1080
|
+
select e.permission, e.level
|
|
1081
|
+
from entries e
|
|
1082
|
+
where e.level is not null
|
|
1083
|
+
and e.permission not in (select d.permission from denied d)
|
|
1084
|
+
and e.permission not in (select a.permission from allowed a)
|
|
1085
|
+
and not exists (select 1 from kept k where k.permission = e.permission)
|
|
1086
|
+
)
|
|
1087
|
+
select k.grant_key from kept k
|
|
1088
|
+
where k.permission not in (select d.permission from denied d)
|
|
1089
|
+
union
|
|
1090
|
+
select i.grant_key from included i
|
|
1091
|
+
where i.effect = 'deny' and i.scope = p_scope
|
|
1092
|
+
union
|
|
1093
|
+
select c.grant_key from ceiling c
|
|
1094
|
+
where c.permission in (select a.permission from allowed a)
|
|
1095
|
+
union
|
|
1096
|
+
select c.grant_key from ceiling c
|
|
1097
|
+
join allowed_levels a on a.permission = c.permission
|
|
1098
|
+
where c.effect = 'deny' or split_part(c.grant_key, '@', 2) = a.level
|
|
1099
|
+
$$;
|
|
1100
|
+
revoke execute on function ${keys}(text[], text[], text[], text) from public, anon, authenticated;`;
|
|
1194
1101
|
}
|
|
1195
|
-
function
|
|
1196
|
-
return `
|
|
1102
|
+
function currentKeysSql(array) {
|
|
1103
|
+
return `select coalesce(r.key, e.permission) as permission
|
|
1104
|
+
from unnest(coalesce(${array}, '{}'::text[])) as e(permission)
|
|
1105
|
+
left join renamed r on r.former = e.permission`;
|
|
1197
1106
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1107
|
+
/**
|
|
1108
|
+
* SQL fragments that read a stored former key as its current key inside
|
|
1109
|
+
* `permdock_custom_keys`. Without renames they are the plain column and
|
|
1110
|
+
* array reads.
|
|
1111
|
+
*/
|
|
1112
|
+
function renamesSql(renames) {
|
|
1113
|
+
if (renames.length === 0) return {
|
|
1114
|
+
prelude: "",
|
|
1115
|
+
permission: (alias) => `${alias}.permission`,
|
|
1116
|
+
join: () => "",
|
|
1117
|
+
allow: "select unnest(coalesce(p_allow, '{}'::text[])) as permission",
|
|
1118
|
+
denied: (alias) => `${alias}.permission = any(coalesce(p_deny, '{}'::text[]))`
|
|
1202
1119
|
};
|
|
1203
|
-
}
|
|
1204
|
-
function sourcesBodyOf(sql, sources, user) {
|
|
1205
1120
|
return {
|
|
1206
|
-
|
|
1207
|
-
|
|
1121
|
+
prelude: `renamed (former, key) as (values ${renames.map(([former, current]) => `(${quoteLiteral(former)}, ${quoteLiteral(current)})`).join(", ")}),
|
|
1122
|
+
denied as (
|
|
1123
|
+
${currentKeysSql("p_deny")}
|
|
1124
|
+
),
|
|
1125
|
+
`,
|
|
1126
|
+
permission: (alias) => `coalesce(r_${alias}.key, ${alias}.permission) as permission`,
|
|
1127
|
+
join: (alias) => ` left join renamed r_${alias} on r_${alias}.former = ${alias}.permission`,
|
|
1128
|
+
allow: currentKeysSql("p_allow"),
|
|
1129
|
+
denied: (alias) => `${alias}.permission in (select d.permission from denied d)`
|
|
1208
1130
|
};
|
|
1209
1131
|
}
|
|
1210
|
-
/**
|
|
1211
|
-
|
|
1212
|
-
|
|
1132
|
+
/**
|
|
1133
|
+
* The catalog reader `permdock_permission_keys()` and the role reader
|
|
1134
|
+
* `permdock_role_permissions(p_role, p_scope, p_tenant, p_scope_id)`, both
|
|
1135
|
+
* for `authenticated`. A declared role answers from `role_permissions`, which
|
|
1136
|
+
* is the catalog. A custom role (`database` mode) answers from the tenant's
|
|
1137
|
+
* own rows through `permdock_custom_keys`, only to a member of the tenant or
|
|
1138
|
+
* a holder of a `meta.manageRoles` permission for a platform role; a shared
|
|
1139
|
+
* role (no tenant below global) to any signed-in caller. The
|
|
1140
|
+
* effect is `deny` for a deny the role carries after the ceiling, as
|
|
1141
|
+
* `resolveCustomRole` returns it, never for a deny that merely removes an allow.
|
|
1142
|
+
*/
|
|
1143
|
+
function roleReadersSql(ctx, permissions, trustedReaders) {
|
|
1144
|
+
const rp = qualified(ctx, "role_permissions");
|
|
1145
|
+
const reader = qualified(ctx, READERS.roles);
|
|
1146
|
+
const tenantType = tenantTypeOf(ctx);
|
|
1147
|
+
const custom = ctx.customRoles;
|
|
1148
|
+
const root = ctx.scopes[0]?.name;
|
|
1149
|
+
const database = ctx.authorize === "database";
|
|
1150
|
+
const chunks = [];
|
|
1151
|
+
if (permissions !== void 0) {
|
|
1152
|
+
const catalog = qualified(ctx, READERS.keys);
|
|
1153
|
+
chunks.push(`-- every declared permission key
|
|
1154
|
+
create or replace function ${catalog}()
|
|
1155
|
+
returns setof text
|
|
1156
|
+
language sql
|
|
1157
|
+
stable
|
|
1158
|
+
set search_path = ''
|
|
1159
|
+
as $$
|
|
1160
|
+
select k.permission from unnest(${sqlTextArray(permissions.toSorted())}) k(permission) order by 1
|
|
1161
|
+
$$;
|
|
1162
|
+
revoke execute on function ${catalog}() from public, anon;
|
|
1163
|
+
grant execute on function ${catalog}() to authenticated;
|
|
1164
|
+
|
|
1165
|
+
create or replace function ${catalog}(p_scope text)
|
|
1166
|
+
returns setof text
|
|
1167
|
+
language sql
|
|
1168
|
+
stable
|
|
1169
|
+
security definer
|
|
1170
|
+
set search_path = ''
|
|
1171
|
+
as $$
|
|
1172
|
+
select distinct rp.permission
|
|
1173
|
+
from ${rp} rp
|
|
1174
|
+
where rp.scope = p_scope
|
|
1175
|
+
and rp.effect = 'allow'
|
|
1176
|
+
and rp.permission = any(${sqlTextArray(permissions.toSorted())})
|
|
1177
|
+
order by 1
|
|
1178
|
+
$$;
|
|
1179
|
+
revoke execute on function ${catalog}(text) from public, anon;
|
|
1180
|
+
grant execute on function ${catalog}(text) to authenticated;`);
|
|
1181
|
+
}
|
|
1182
|
+
const declared = ` select distinct rp.permission, rp.effect
|
|
1183
|
+
from ${rp} rp
|
|
1184
|
+
where rp.role = p_role and rp.scope = p_scope
|
|
1185
|
+
order by 1, 2`;
|
|
1186
|
+
let body = `begin
|
|
1187
|
+
perform p_tenant, p_scope_id; -- only a custom role reads them
|
|
1188
|
+
return query
|
|
1189
|
+
${declared};
|
|
1190
|
+
end;`;
|
|
1191
|
+
let trustedBody = body;
|
|
1192
|
+
if (custom !== void 0 && database && root !== void 0) {
|
|
1193
|
+
const tenant = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
1194
|
+
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
1195
|
+
const keys = qualified(ctx, CUSTOM_ROLES.keys);
|
|
1196
|
+
const shape = qualified(ctx, CUSTOM_ROLES.shape);
|
|
1197
|
+
const ids = (helper) => `(select x::text from ${qualified(ctx, helper)}() x)`;
|
|
1198
|
+
const memberOf = `p_tenant::text in ${ids(memberIdsHelper(root))}
|
|
1199
|
+
or ${perScope(ctx, (scope) => scope === root ? "false" : `p_scope_id in ${ids(memberIdsHelper(scope))}`)}`;
|
|
1200
|
+
const manage = custom.manage ?? [];
|
|
1201
|
+
const platform = manage.length === 0 ? "false" : `exists (
|
|
1202
|
+
select 1 from ${rp} m
|
|
1203
|
+
where m.effect = 'allow'
|
|
1204
|
+
and m.permission = any(${sqlTextArray(manage)})
|
|
1205
|
+
and ${qualified(ctx, HELPERS.has)}(m.grant_key)
|
|
1206
|
+
)`;
|
|
1207
|
+
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`;
|
|
1208
|
+
const allowEntryOf = custom.levels === true ? "c.permission || coalesce('@' || c.level, '')" : "c.permission";
|
|
1209
|
+
const checked = (gate) => `begin
|
|
1210
|
+
if p_role = any(${sqlTextArray(custom.declared)}) then
|
|
1211
|
+
return query
|
|
1212
|
+
${declared};
|
|
1213
|
+
return;
|
|
1214
|
+
end if;
|
|
1215
|
+
perform ${shape}(p_tenant, p_scope, p_scope_id, p_role);${gate}
|
|
1216
|
+
return query
|
|
1217
|
+
select distinct rp.permission, rp.effect
|
|
1218
|
+
from ${keys}(
|
|
1219
|
+
array(select ${allowEntryOf} from ${tenant} c where ${match} and c.effect = 'allow'),
|
|
1220
|
+
array(select c.permission from ${tenant} c where ${match} and c.effect = 'deny'),
|
|
1221
|
+
array(select c.include_role from ${includes} c where ${match}),
|
|
1222
|
+
p_scope
|
|
1223
|
+
) k(grant_key)
|
|
1224
|
+
join ${rp} rp on rp.grant_key = k.grant_key and rp.scope = p_scope
|
|
1225
|
+
order by 1, 2;
|
|
1226
|
+
end;`;
|
|
1227
|
+
body = checked(`
|
|
1228
|
+
if not (${signedIn(ctx)} and (
|
|
1229
|
+
${platform}
|
|
1230
|
+
or (p_scope <> 'global' and p_tenant is null and p_scope_id is null)
|
|
1231
|
+
or (p_scope <> 'global' and (${memberOf}))
|
|
1232
|
+
)) then
|
|
1233
|
+
${raiseSql(" ", "42501", `'permdock: the caller is not a member of ' || coalesce(p_tenant::text, 'the platform')`, "not-member")}
|
|
1234
|
+
end if;`);
|
|
1235
|
+
trustedBody = checked("");
|
|
1236
|
+
}
|
|
1237
|
+
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
|
|
1238
|
+
create or replace function ${reader}(p_role text, p_scope text, p_tenant ${tenantType} default null, p_scope_id text default null)
|
|
1239
|
+
returns table (permission text, effect text)
|
|
1240
|
+
language plpgsql
|
|
1213
1241
|
stable
|
|
1214
1242
|
security definer
|
|
1215
1243
|
set search_path = ''
|
|
1216
1244
|
as $$
|
|
1217
|
-
${body
|
|
1218
|
-
|
|
1219
|
-
|
|
1245
|
+
${body}
|
|
1246
|
+
$$;
|
|
1247
|
+
revoke execute on function ${reader}(text, text, ${tenantType}, text) from public, anon;
|
|
1248
|
+
grant execute on function ${reader}(text, text, ${tenantType}, text) to authenticated;`);
|
|
1249
|
+
const trusted = qualified(ctx, READERS.trusted);
|
|
1250
|
+
const signature = `text, text, ${tenantType}, text`;
|
|
1251
|
+
const grants = trustedReaders === void 0 || trustedReaders.length === 0 ? "" : `\ngrant execute on function ${trusted}(${signature}) to ${trustedReaders.map(quoteIdent).join(", ")};`;
|
|
1252
|
+
chunks.push(`create or replace function ${trusted}(p_role text, p_scope text, p_tenant ${tenantType} default null, p_scope_id text default null)
|
|
1253
|
+
returns table (permission text, effect text)
|
|
1254
|
+
language plpgsql
|
|
1220
1255
|
stable
|
|
1221
1256
|
security definer
|
|
1222
1257
|
set search_path = ''
|
|
1223
1258
|
as $$
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1259
|
+
${trustedBody}
|
|
1260
|
+
$$;
|
|
1261
|
+
revoke execute on function ${trusted}(${signature}) from public, anon, authenticated;${grants}`);
|
|
1262
|
+
return chunks.join("\n\n");
|
|
1263
|
+
}
|
|
1264
|
+
/** The stored definition of `p_role` at `p_scope` for `p_tenant`: the shared rows when a shared role has the name. */
|
|
1265
|
+
function storedTenant(ctx) {
|
|
1266
|
+
return `case when ${qualified(ctx, CUSTOM_ROLES.shared)}(p_scope, p_role) then c.tenant_id is null else c.tenant_id is not distinct from p_tenant end`;
|
|
1231
1267
|
}
|
|
1232
1268
|
/**
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1269
|
+
* The tenant half of a custom-role row match for a membership: the
|
|
1270
|
+
* membership's tenant, or no tenant when a shared role of that name exists at
|
|
1271
|
+
* the scope, so a shared role shadows a tenant role of the same name.
|
|
1235
1272
|
*/
|
|
1236
|
-
function
|
|
1237
|
-
return (
|
|
1273
|
+
function customTenantMatch(ctx, tenant, scope, role) {
|
|
1274
|
+
return `case when ${qualified(ctx, CUSTOM_ROLES.shared)}(${quoteLiteral(scope)}, ${role}) then c.tenant_id is null else c.tenant_id::text = ${tenant} end`;
|
|
1238
1275
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1276
|
+
/**
|
|
1277
|
+
* The scope of a row of the application's roles table: its scope column (or
|
|
1278
|
+
* the first scope) with a tenant; with no tenant, the same scope when the
|
|
1279
|
+
* `shared` column is true, else `'global'`.
|
|
1280
|
+
*/
|
|
1281
|
+
function rolesTableScope(table, tenant, root, column) {
|
|
1282
|
+
const own = table.scope === void 0 ? quoteLiteral(root) : `${column(table.scope)}::text`;
|
|
1283
|
+
return `case when ${tenant} is null then ${table.shared === void 0 ? "'global'" : `case when coalesce(${column(table.shared)}, false) then ${own} else 'global' end`} else ${own} end`;
|
|
1245
1284
|
}
|
|
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));
|
|
1285
|
+
/** `expr` read as its current key: a former key maps to the key it was renamed to. */
|
|
1286
|
+
function currentKeyExpr(renames, expr) {
|
|
1287
|
+
if (renames.length === 0) return expr;
|
|
1288
|
+
return `(case ${expr} ${renames.map(([former, current]) => `when ${quoteLiteral(former)} then ${quoteLiteral(current)}`).join(" ")} else ${expr} end)`;
|
|
1289
1289
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1290
|
+
/** `case p_scope when '<scope>' then <sql for scope> ... else false end` over the declared scopes. */
|
|
1291
|
+
function perScope(ctx, sql) {
|
|
1292
|
+
const arms = ctx.scopes.map((scope) => `when ${quoteLiteral(scope.name)} then ${sql(scope.name)}`).join(" ");
|
|
1293
|
+
return arms === "" ? "false" : `(case p_scope ${arms} else false end)`;
|
|
1294
|
+
}
|
|
1295
|
+
function raiseSql(indent, errcode, message, hint) {
|
|
1296
|
+
return `${indent}raise exception using
|
|
1297
|
+
${indent} errcode = '${errcode}',
|
|
1298
|
+
${indent} message = ${message},
|
|
1299
|
+
${indent} hint = ${quoteLiteral(hint)};`;
|
|
1300
|
+
}
|
|
1301
|
+
function hasCustomRoleChecksFor(ctx) {
|
|
1302
|
+
return ctx.authorize === "database" && ctx.customRoles !== void 0 && ctx.scopes.length > 0 && ctx.scopes.every((scope) => hasMemberFor(ctx, scope.name));
|
|
1293
1303
|
}
|
|
1294
1304
|
//#endregion
|
|
1295
|
-
//#region src/cli/
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
*/
|
|
1304
|
-
function
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1305
|
+
//#region src/cli/sql/global-roles.ts
|
|
1306
|
+
function qualify(name, schema) {
|
|
1307
|
+
return name.includes(".") ? name : `${schema}.${name}`;
|
|
1308
|
+
}
|
|
1309
|
+
function throughTenant(through, roles) {
|
|
1310
|
+
if (through === void 0 || roles?.tenant === void 0) return;
|
|
1311
|
+
return qualify(roles.table, "public") === through.table ? roles.tenant : void 0;
|
|
1312
|
+
}
|
|
1313
|
+
/** Resolves `source` against `schema`, aliasing the table `alias` and the roles table `<alias>k`. */
|
|
1314
|
+
function globalRoleSource(source, schema, alias, roles) {
|
|
1315
|
+
const table = qualify(source.table, schema);
|
|
1316
|
+
const user = source.user ?? "user_id";
|
|
1317
|
+
const role = roleColumn(source.role ?? "role", table, alias, {
|
|
1318
|
+
label: "roles.role",
|
|
1319
|
+
prefix: "PermDock CLI",
|
|
1320
|
+
indent: " "
|
|
1321
|
+
});
|
|
1322
|
+
const tenant = role.lateral ? void 0 : throughTenant(role.through, roles);
|
|
1323
|
+
const shared = tenant === void 0 ? void 0 : roles?.shared;
|
|
1324
|
+
const global = tenant === void 0 ? "" : ` and ${alias}k.${quoteIdent(tenant)} is null${shared === void 0 ? "" : ` and ${alias}k.${quoteIdent(shared)} is not true`}`;
|
|
1325
|
+
return {
|
|
1326
|
+
table,
|
|
1327
|
+
user,
|
|
1328
|
+
from: `${quoteTable(table)} ${alias}${role.join}${global}`,
|
|
1329
|
+
userSql: `${alias}.${quoteIdent(user)}`,
|
|
1330
|
+
roleSql: role.sql,
|
|
1331
|
+
role: roleManifest(role),
|
|
1332
|
+
...role.through === void 0 ? {} : { through: role.through },
|
|
1333
|
+
...tenant === void 0 ? {} : { tenant },
|
|
1334
|
+
...shared === void 0 ? {} : { shared }
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
//#endregion
|
|
1338
|
+
//#region src/cli/rls/helper-rows.ts
|
|
1339
|
+
function activeTenant(ctx) {
|
|
1340
|
+
return `nullif(${subjectClaimSql(ctx, ctx.tenantClaim)}, '')`;
|
|
1341
|
+
}
|
|
1342
|
+
function keyTenantLine(ctx, scope, tenant) {
|
|
1343
|
+
const keyed = keyTenantSql(ctx, underRoot(ctx, scope) ? tenant : void 0);
|
|
1344
|
+
return keyed === void 0 ? "" : `\n and ${keyed}`;
|
|
1345
|
+
}
|
|
1346
|
+
/** Whether the helper for `scope` narrows to the tenant claim: unless `rls.tenants` is `'all'`, when the first scope is on its chain. */
|
|
1347
|
+
function narrowsTo(ctx, scope) {
|
|
1348
|
+
return ctx.tenants !== "all" && underRoot(ctx, scope);
|
|
1349
|
+
}
|
|
1350
|
+
/** Whether the active tenant narrows memberships of `scope`: the first scope is on its chain. */
|
|
1351
|
+
function underRoot(ctx, scope) {
|
|
1352
|
+
const root = ctx.scopes[0]?.name;
|
|
1353
|
+
let current = ctx.scopes.find((item) => item.name === scope);
|
|
1354
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1355
|
+
while (current !== void 0 && !seen.has(current.name)) {
|
|
1356
|
+
if (current.name === root) return true;
|
|
1357
|
+
seen.add(current.name);
|
|
1358
|
+
const parent = current.within;
|
|
1359
|
+
current = parent === void 0 ? void 0 : ctx.scopes.find((item) => item.name === parent);
|
|
1360
|
+
}
|
|
1361
|
+
return false;
|
|
1362
|
+
}
|
|
1363
|
+
/** `and exists (...)` lines for an active user; empty without `rls.suspension.users`. */
|
|
1364
|
+
function userActive(ctx, indent, user) {
|
|
1365
|
+
return activeUserSql(ctx, user).map((part) => `${indent}and ${part}`);
|
|
1366
|
+
}
|
|
1367
|
+
/** `and exists (...)` lines for every suspendable instance on the chain of `scope`'s membership. */
|
|
1368
|
+
function instancesActive(ctx, scope, idOf, indent, permission) {
|
|
1369
|
+
return activeInstancesSql(ctx, scope, idOf, permission).map((part) => `${indent}and ${part}`);
|
|
1370
|
+
}
|
|
1371
|
+
function roleRows(ctx) {
|
|
1372
|
+
const claim = ctx.roleClaim ?? "user_role";
|
|
1373
|
+
switch (ctx.dialect) {
|
|
1374
|
+
case "guc": return `unnest(string_to_array(nullif(current_setting(${quoteLiteral(`${ctx.gucPrefix}.${claim}`)}, true), ''), ',')) r(role)`;
|
|
1375
|
+
case "supabase":
|
|
1376
|
+
case "neon": {
|
|
1377
|
+
const raw = subjectClaimJsonSql(ctx, claim);
|
|
1378
|
+
const value = ctx.dialect === "supabase" ? `coalesce(nullif(${raw}, 'null'::jsonb), (select auth.jwt()) -> 'app_metadata' -> ${quoteLiteral(claim)})` : raw;
|
|
1379
|
+
return `jsonb_array_elements_text(
|
|
1380
|
+
case jsonb_typeof(${value})
|
|
1381
|
+
when 'array' then ${value}
|
|
1382
|
+
when 'string' then jsonb_build_array(${value})
|
|
1383
|
+
else '[]'::jsonb
|
|
1384
|
+
end
|
|
1385
|
+
) r(role)`;
|
|
1386
|
+
}
|
|
1387
|
+
default: return ctx.dialect;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
function membershipRows(ctx) {
|
|
1391
|
+
const raw = subjectClaimJsonSql(ctx, "memberships");
|
|
1392
|
+
const value = ctx.dialect === "supabase" ? `coalesce(${raw}, (select auth.jwt()) -> 'app_metadata' -> 'memberships')` : raw;
|
|
1393
|
+
return `jsonb_array_elements(
|
|
1394
|
+
case jsonb_typeof(${value}) when 'array' then ${value} else '[]'::jsonb end
|
|
1395
|
+
) m
|
|
1396
|
+
cross join lateral jsonb_array_elements_text(
|
|
1397
|
+
case jsonb_typeof(m -> 'roles') when 'array' then m -> 'roles' else '[]'::jsonb end
|
|
1398
|
+
) r(role)`;
|
|
1399
|
+
}
|
|
1400
|
+
function andLine(indent, condition) {
|
|
1401
|
+
return condition === void 0 ? "" : `\n${indent}and ${condition}`;
|
|
1402
|
+
}
|
|
1403
|
+
/** `rls.roles`, or the generated `user_roles (user_id, role)`, aliased `ur`. */
|
|
1404
|
+
function globalRoleRows(ctx) {
|
|
1405
|
+
return ctx.roles === void 0 ? {
|
|
1406
|
+
from: `${qualified(ctx, "user_roles")} ur`,
|
|
1407
|
+
userSql: "ur.user_id",
|
|
1408
|
+
roleSql: "ur.role::text"
|
|
1409
|
+
} : globalRoleSource(ctx.roles, "public", "ur", ctx.customRoles?.table);
|
|
1311
1410
|
}
|
|
1312
|
-
function
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
stable
|
|
1320
|
-
set search_path = ''
|
|
1321
|
-
as $$
|
|
1322
|
-
select c -> 'on' ->> 'id'
|
|
1323
|
-
from (select ${subjectClaimJsonSql(ctx, "capability")} as c) capability
|
|
1324
|
-
where jsonb_typeof(c) = 'object'
|
|
1325
|
-
and c ->> 'v' = '1'
|
|
1326
|
-
and c ->> 'holder' = 'link'
|
|
1327
|
-
and c -> 'on' ->> 'resource' = p_resource
|
|
1328
|
-
and jsonb_typeof(c -> 'roles') = 'array'
|
|
1329
|
-
and c -> 'roles' @> jsonb_build_array(p_role)
|
|
1330
|
-
and (c -> 'permissions' is null or c -> 'permissions' @> jsonb_build_array(p_permission))
|
|
1331
|
-
and jsonb_typeof(c -> 'expires_at') = 'number'
|
|
1332
|
-
and (c ->> 'expires_at')::numeric > extract(epoch from now())
|
|
1333
|
-
$$;
|
|
1334
|
-
revoke execute on function ${fn}(text, text, text) from public;
|
|
1335
|
-
grant execute on function ${fn}(text, text, text) to anon, authenticated;`;
|
|
1411
|
+
function customKeysJoin(ctx, scope, allows, denies, includes) {
|
|
1412
|
+
return ` cross join lateral ${qualified(ctx, CUSTOM_ROLES.keys)}(
|
|
1413
|
+
${allows},
|
|
1414
|
+
${denies},
|
|
1415
|
+
${includes},
|
|
1416
|
+
${quoteLiteral(scope)}
|
|
1417
|
+
) ck(grant_key)`;
|
|
1336
1418
|
}
|
|
1337
1419
|
//#endregion
|
|
1338
|
-
//#region src/cli/rls/
|
|
1339
|
-
|
|
1340
|
-
const READERS = {
|
|
1341
|
-
keys: "permdock_permission_keys",
|
|
1342
|
-
roles: "permdock_role_permissions",
|
|
1343
|
-
trusted: "permdock_trusted_role_permissions"
|
|
1344
|
-
};
|
|
1345
|
-
/**
|
|
1346
|
-
* The custom-role objects: the tables (`database` mode), the ceiling view
|
|
1347
|
-
* over the `assignable` declared roles, and `permdock_custom_keys`, which
|
|
1348
|
-
* resolves one custom role to the grant keys it holds in a scope. A row in
|
|
1349
|
-
* the tables can never reach a key outside the view.
|
|
1350
|
-
*/
|
|
1351
|
-
function customRolesSql(ctx) {
|
|
1352
|
-
const custom = ctx.customRoles;
|
|
1353
|
-
if (custom === void 0) return "";
|
|
1420
|
+
//#region src/cli/rls/helper-bodies.ts
|
|
1421
|
+
function hasSetBody(ctx) {
|
|
1354
1422
|
const rp = qualified(ctx, "role_permissions");
|
|
1355
|
-
const
|
|
1356
|
-
const
|
|
1357
|
-
const chunks = [];
|
|
1423
|
+
const active = userActive(ctx, " ").map((line) => `\n${line}`).join("");
|
|
1424
|
+
const custom = ctx.customRoles;
|
|
1358
1425
|
if (ctx.authorize === "database") {
|
|
1359
|
-
const
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
-
}
|
|
1426
|
+
const ur = globalRoleRows(ctx);
|
|
1427
|
+
const declared = ` select rp.grant_key
|
|
1428
|
+
from ${ur.from}
|
|
1429
|
+
join ${rp} rp on rp.role = ${ur.roleSql}
|
|
1430
|
+
where ${ur.userSql} = ${subjectIdSql(ctx)}
|
|
1431
|
+
and rp.scope = 'global'${andLine(" ", globalKindFilterSql(ctx, ur.roleSql))}${active}`;
|
|
1432
|
+
if (custom === void 0) return declared;
|
|
1433
|
+
const match = `c.tenant_id is null and c.scope = 'global' and c.scope_id is null and c.role = ${ur.roleSql}`;
|
|
1434
|
+
const rows = (source, value, extra) => `array(select c.${value} from ${qualified(ctx, source)} c where ${match}${extra})`;
|
|
1435
|
+
return `${declared}
|
|
1436
|
+
union
|
|
1437
|
+
select ck.grant_key
|
|
1438
|
+
from ${ur.from}
|
|
1439
|
+
${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", ""))}
|
|
1440
|
+
where ${ur.userSql} = ${subjectIdSql(ctx)}
|
|
1441
|
+
and not (${ur.roleSql} = any(${sqlTextArray(custom.declared)}))${active}`;
|
|
1392
1442
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
)
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1443
|
+
const declared = ` select rp.grant_key
|
|
1444
|
+
from ${roleRows(ctx)}
|
|
1445
|
+
join ${rp} rp on rp.role = r.role
|
|
1446
|
+
where ${signedIn(ctx)}
|
|
1447
|
+
and rp.scope = 'global'${andLine(" ", globalKindFilterSql(ctx, "r.role"))}${active}`;
|
|
1448
|
+
if (custom === void 0) return declared;
|
|
1449
|
+
const claim = subjectClaimJsonSql(ctx, GLOBAL_GRANTS_CLAIM);
|
|
1450
|
+
return `${declared}
|
|
1451
|
+
union
|
|
1452
|
+
select ck.grant_key
|
|
1453
|
+
from ${roleRows(ctx)}
|
|
1454
|
+
cross join lateral (select ${claim} -> r.role as g) cg
|
|
1455
|
+
${customKeysJoin(ctx, "global", claimEntries("left(e, 1) not in ('-', '@')", "e"), claimEntries("left(e, 1) = '-'", "substr(e, 2)"), claimEntries("left(e, 1) = '@'", "substr(e, 2)"))}
|
|
1456
|
+
where ${signedIn(ctx)}
|
|
1457
|
+
and jsonb_typeof(cg.g) = 'array'
|
|
1458
|
+
and not (r.role = any(${sqlTextArray(custom.declared)}))${active}`;
|
|
1459
|
+
}
|
|
1460
|
+
function hasBody(ctx) {
|
|
1461
|
+
if (ctx.grantSet === true) return hasSetBody(ctx);
|
|
1462
|
+
const rp = qualified(ctx, "role_permissions");
|
|
1463
|
+
const active = userActive(ctx, " ").map((line) => `\n${line}`).join("");
|
|
1464
|
+
const custom = ctx.customRoles;
|
|
1465
|
+
if (ctx.authorize === "database") {
|
|
1466
|
+
const ur = globalRoleRows(ctx);
|
|
1467
|
+
const declared = ` select exists (
|
|
1468
|
+
select 1
|
|
1469
|
+
from ${ur.from}
|
|
1470
|
+
join ${rp} rp on rp.role = ${ur.roleSql}
|
|
1471
|
+
where ${ur.userSql} = ${subjectIdSql(ctx)}
|
|
1472
|
+
and rp.grant_key = p_grant
|
|
1473
|
+
and rp.scope = 'global'${andLine(" ", globalKindFilterSql(ctx, ur.roleSql))}${active}
|
|
1474
|
+
)`;
|
|
1475
|
+
if (custom === void 0) return declared;
|
|
1476
|
+
const match = `c.tenant_id is null and c.scope = 'global' and c.scope_id is null and c.role = ${ur.roleSql}`;
|
|
1477
|
+
const rows = (source, value, extra) => `array(select c.${value} from ${qualified(ctx, source)} c where ${match}${extra})`;
|
|
1478
|
+
return `${declared}
|
|
1479
|
+
or exists (
|
|
1480
|
+
select 1
|
|
1481
|
+
from ${ur.from}
|
|
1482
|
+
where ${ur.userSql} = ${subjectIdSql(ctx)}
|
|
1483
|
+
and not (${ur.roleSql} = any(${sqlTextArray(custom.declared)}))${active}
|
|
1484
|
+
${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()}
|
|
1485
|
+
)`;
|
|
1410
1486
|
}
|
|
1411
|
-
const
|
|
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
|
-
select
|
|
1457
|
-
|
|
1487
|
+
const declared = ` select ${signedIn(ctx)} and exists (
|
|
1488
|
+
select 1
|
|
1489
|
+
from ${roleRows(ctx)}
|
|
1490
|
+
join ${rp} rp on rp.role = r.role
|
|
1491
|
+
where rp.grant_key = p_grant
|
|
1492
|
+
and rp.scope = 'global'${andLine(" ", globalKindFilterSql(ctx, "r.role"))}${active}
|
|
1493
|
+
)`;
|
|
1494
|
+
if (custom === void 0) return declared;
|
|
1495
|
+
const claim = subjectClaimJsonSql(ctx, GLOBAL_GRANTS_CLAIM);
|
|
1496
|
+
return `${declared}
|
|
1497
|
+
or (${signedIn(ctx)} and exists (
|
|
1498
|
+
select 1
|
|
1499
|
+
from ${roleRows(ctx)}
|
|
1500
|
+
cross join lateral (select ${claim} -> r.role as g) cg
|
|
1501
|
+
where jsonb_typeof(cg.g) = 'array'
|
|
1502
|
+
and not (r.role = any(${sqlTextArray(custom.declared)}))${active}
|
|
1503
|
+
${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()}
|
|
1504
|
+
))`;
|
|
1505
|
+
}
|
|
1506
|
+
/** The claim `jwt` mode reads platform custom roles from: `customRoleClaim(globalRoles)`. */
|
|
1507
|
+
const GLOBAL_GRANTS_CLAIM = "role_grants";
|
|
1508
|
+
function memberColumn(name) {
|
|
1509
|
+
return `m.${quoteIdent(name)}`;
|
|
1510
|
+
}
|
|
1511
|
+
function disabledColumn(table) {
|
|
1512
|
+
return table.disabledAt === void 0 ? void 0 : memberColumn(table.disabledAt);
|
|
1513
|
+
}
|
|
1514
|
+
function claimKeptLines(ctx, indent, permission) {
|
|
1515
|
+
return disabledKeep(ctx).length === 0 ? [] : [`${indent}and ${keptRowSql("m -> 'keep'", permission)}`];
|
|
1516
|
+
}
|
|
1517
|
+
/** Entries of a compact custom-role claim (`cg.g`) that match `where`. */
|
|
1518
|
+
function claimEntries(where, value) {
|
|
1519
|
+
return `array(select ${value} from jsonb_array_elements_text(cg.g) e where ${where})`;
|
|
1520
|
+
}
|
|
1521
|
+
/** `p_grant in (select permdock_custom_keys(allows, denies, includes, scope))`. */
|
|
1522
|
+
function customKeysSql(ctx, scope, allows, denies, includes) {
|
|
1523
|
+
return ` and p_grant in (select ${qualified(ctx, CUSTOM_ROLES.keys)}(
|
|
1524
|
+
${allows},
|
|
1525
|
+
${denies},
|
|
1526
|
+
${includes},
|
|
1527
|
+
${quoteLiteral(scope)}
|
|
1528
|
+
))`;
|
|
1529
|
+
}
|
|
1530
|
+
function tableBody(ctx, scope, type) {
|
|
1531
|
+
const mapped = scopeTable(ctx, scope);
|
|
1532
|
+
if (mapped === void 0) return ` select null::${type} where false -- no ${scope} memberships table configured`;
|
|
1533
|
+
const { table, column, tenantColumn } = mapped;
|
|
1534
|
+
const set = ctx.grantSet === true;
|
|
1535
|
+
const filtersFor = (grant) => {
|
|
1536
|
+
const filters = [` where ${memberColumn(table.user)} = ${subjectIdSql(ctx)}`];
|
|
1537
|
+
if (table.expiresAt !== void 0) {
|
|
1538
|
+
const expires = memberColumn(table.expiresAt);
|
|
1539
|
+
filters.push(` and (${expires} is null or ${expires} > now())`);
|
|
1540
|
+
}
|
|
1541
|
+
filters.push(...activeMembershipSql(ctx, disabledColumn(table), grantPermissionSql(grant)).map((part) => ` and ${part}`));
|
|
1542
|
+
if (tenantColumn !== void 0 && ctx.tenants !== "all") filters.push(` and (${activeTenant(ctx)} is null or ${memberColumn(tenantColumn)}::text = ${activeTenant(ctx)})`);
|
|
1543
|
+
const keyed = keyTenantSql(ctx, tenantColumn === void 0 ? void 0 : memberColumn(tenantColumn));
|
|
1544
|
+
if (keyed !== void 0) filters.push(` and ${keyed}`);
|
|
1545
|
+
filters.push(...userActive(ctx, " "), ...instancesActive(ctx, scope, (name) => {
|
|
1546
|
+
const held = scopeColumn(table, ctx.scopes, name);
|
|
1547
|
+
return held === void 0 ? void 0 : memberColumn(held);
|
|
1548
|
+
}, " ", grantPermissionSql(grant)));
|
|
1549
|
+
return filters;
|
|
1550
|
+
};
|
|
1551
|
+
const filters = filtersFor(set ? "rp.grant_key" : "p_grant");
|
|
1552
|
+
const [owner, ...rest] = filters;
|
|
1553
|
+
const role = memberRoleOf(table, "m", " ");
|
|
1554
|
+
const kind = kindFilterSql(ctx, role.sql, memberVia(table));
|
|
1555
|
+
const lines = [
|
|
1556
|
+
` select${role.lateral ? " distinct" : ""} ${memberColumn(column)}::${type}${set ? ", rp.grant_key" : ""}`,
|
|
1557
|
+
` from ${quoteQualified(table.table)} m${role.join}`,
|
|
1558
|
+
` join ${qualified(ctx, "role_permissions")} rp on rp.role = ${role.sql}`,
|
|
1559
|
+
owner ?? "",
|
|
1560
|
+
...set ? [] : [" and rp.grant_key = p_grant"],
|
|
1561
|
+
` and rp.scope = ${quoteLiteral(scope)}`,
|
|
1562
|
+
...kind === void 0 ? [] : [` and ${kind}`],
|
|
1563
|
+
...rest
|
|
1564
|
+
];
|
|
1565
|
+
const custom = ctx.customRoles;
|
|
1566
|
+
if (custom === void 0) return lines.join("\n");
|
|
1567
|
+
const match = [
|
|
1568
|
+
customTenantMatch(ctx, `${memberColumn(tenantColumn ?? column)}::text`, scope, role.sql),
|
|
1569
|
+
`c.scope = ${quoteLiteral(scope)}`,
|
|
1570
|
+
`(c.scope_id is null or c.scope_id = ${memberColumn(column)}::text)`,
|
|
1571
|
+
`c.role = ${role.sql}`
|
|
1572
|
+
].join(" and ");
|
|
1573
|
+
const rows = (source, value, extra) => `array(select c.${value} from ${qualified(ctx, source)} c where ${match}${extra})`;
|
|
1574
|
+
const entries = [
|
|
1575
|
+
rows(CUSTOM_ROLES.permissions, allowEntry(ctx), " and c.effect = 'allow'"),
|
|
1576
|
+
rows(CUSTOM_ROLES.permissions, "permission", " and c.effect = 'deny'"),
|
|
1577
|
+
rows(CUSTOM_ROLES.includes, "include_role", "")
|
|
1578
|
+
];
|
|
1579
|
+
if (set) return [
|
|
1580
|
+
...lines,
|
|
1581
|
+
" union",
|
|
1582
|
+
` select ${memberColumn(column)}::${type}, ck.grant_key`,
|
|
1583
|
+
` from ${quoteQualified(table.table)} m${role.join}`,
|
|
1584
|
+
customKeysJoin(ctx, scope, ...entries),
|
|
1585
|
+
...filtersFor("ck.grant_key"),
|
|
1586
|
+
` and not (${role.sql} = any(${sqlTextArray(custom.declared)}))`
|
|
1587
|
+
].join("\n");
|
|
1588
|
+
return [
|
|
1589
|
+
...lines,
|
|
1590
|
+
" union",
|
|
1591
|
+
` select ${memberColumn(column)}::${type}`,
|
|
1592
|
+
` from ${quoteQualified(table.table)} m${role.join}`,
|
|
1593
|
+
...filters,
|
|
1594
|
+
` and not (${role.sql} = any(${sqlTextArray(custom.declared)}))`,
|
|
1595
|
+
customKeysSql(ctx, scope, ...entries)
|
|
1596
|
+
].join("\n");
|
|
1597
|
+
}
|
|
1598
|
+
/** The membership's instance of the first scope, from the canonical claim. */
|
|
1599
|
+
function claimTenant(ctx, scope) {
|
|
1600
|
+
const root = rootName(ctx);
|
|
1601
|
+
return scope === root ? `m ->> 'id'` : `m -> 'within' ->> ${quoteLiteral(root)}`;
|
|
1602
|
+
}
|
|
1603
|
+
function claimBody(ctx, scope, type) {
|
|
1604
|
+
const set = ctx.grantSet === true;
|
|
1605
|
+
const narrow = narrowsTo(ctx, scope) ? `
|
|
1606
|
+
and (${activeTenant(ctx)} is null or ${claimTenant(ctx, scope)} = ${activeTenant(ctx)})` : "";
|
|
1607
|
+
const filtersFor = (grant) => ` and m ->> 'scope' = ${quoteLiteral(scope)}
|
|
1608
|
+
and m ->> 'id' is not null${narrow}${keyTenantLine(ctx, scope, claimTenant(ctx, scope))}
|
|
1609
|
+
and case jsonb_typeof(m -> 'expires_at')
|
|
1610
|
+
when 'number' then (m ->> 'expires_at')::numeric > extract(epoch from now())
|
|
1611
|
+
else true
|
|
1612
|
+
end${[
|
|
1613
|
+
...claimKeptLines(ctx, " ", grantPermissionSql(grant)),
|
|
1614
|
+
...userActive(ctx, " "),
|
|
1615
|
+
...instancesActive(ctx, scope, (name) => name === scope ? `m ->> 'id'` : `m -> 'within' ->> ${quoteLiteral(name)}`, " ", grantPermissionSql(grant))
|
|
1616
|
+
].map((line) => `\n${line}`).join("")}`;
|
|
1617
|
+
const filters = filtersFor(set ? "rp.grant_key" : "p_grant");
|
|
1618
|
+
const declared = ` select (m ->> 'id')::${type}${set ? ", rp.grant_key" : ""}
|
|
1619
|
+
from ${membershipRows(ctx)}
|
|
1620
|
+
join ${qualified(ctx, "role_permissions")} rp on rp.role = r.role
|
|
1621
|
+
where ${signedIn(ctx)}${set ? "" : "\n and rp.grant_key = p_grant"}
|
|
1622
|
+
and rp.scope = ${quoteLiteral(scope)}${andLine(" ", kindFilterSql(ctx, "r.role", "m ->> 'via'"))}
|
|
1623
|
+
${filters}`;
|
|
1624
|
+
const custom = ctx.customRoles;
|
|
1625
|
+
if (custom === void 0) return declared;
|
|
1626
|
+
if (set) return `${declared}
|
|
1458
1627
|
union
|
|
1459
|
-
select
|
|
1460
|
-
|
|
1628
|
+
select (m ->> 'id')::${type}, ck.grant_key
|
|
1629
|
+
from ${membershipRows(ctx)}
|
|
1630
|
+
cross join lateral (select m -> 'grants' -> r.role as g) cg
|
|
1631
|
+
${customKeysJoin(ctx, scope, claimEntries("left(e, 1) not in ('-', '@')", "e"), claimEntries("left(e, 1) = '-'", "substr(e, 2)"), claimEntries("left(e, 1) = '@'", "substr(e, 2)"))}
|
|
1632
|
+
where ${signedIn(ctx)}
|
|
1633
|
+
and jsonb_typeof(cg.g) = 'array'
|
|
1634
|
+
and not (r.role = any(${sqlTextArray(custom.declared)}))
|
|
1635
|
+
${filtersFor("ck.grant_key")}`;
|
|
1636
|
+
return `${declared}
|
|
1461
1637
|
union
|
|
1462
|
-
select
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1638
|
+
select (m ->> 'id')::${type}
|
|
1639
|
+
from ${membershipRows(ctx)}
|
|
1640
|
+
cross join lateral (select m -> 'grants' -> r.role as g) cg
|
|
1641
|
+
where ${signedIn(ctx)}
|
|
1642
|
+
and jsonb_typeof(cg.g) = 'array'
|
|
1643
|
+
and not (r.role = any(${sqlTextArray(custom.declared)}))
|
|
1644
|
+
${filters}
|
|
1645
|
+
${customKeysSql(ctx, scope, claimEntries("left(e, 1) not in ('-', '@')", "e"), claimEntries("left(e, 1) = '-'", "substr(e, 2)"), claimEntries("left(e, 1) = '@'", "substr(e, 2)"))}`;
|
|
1467
1646
|
}
|
|
1468
1647
|
/**
|
|
1469
|
-
*
|
|
1470
|
-
* `
|
|
1471
|
-
*
|
|
1472
|
-
* carries removes the permission, as `resolveCustomRole` drops it with
|
|
1473
|
-
* `unknown-level`.
|
|
1648
|
+
* The rows the membership sources select for the subject, one per instance
|
|
1649
|
+
* (`scope`, `id`, `within`, `roles`, `via`), with each source's own expiry
|
|
1650
|
+
* and suspension filters: the statements the token hook runs.
|
|
1474
1651
|
*/
|
|
1475
|
-
function
|
|
1476
|
-
|
|
1477
|
-
const ceiling = qualified(ctx, CUSTOM_ROLES.ceiling);
|
|
1478
|
-
const keys = qualified(ctx, CUSTOM_ROLES.keys);
|
|
1479
|
-
const renamed = renames.length > 0;
|
|
1480
|
-
const prelude = renamed ? `renamed (former, key) as (values ${renames.map(([former, current]) => `(${quoteLiteral(former)}, ${quoteLiteral(current)})`).join(", ")}),
|
|
1481
|
-
` : "";
|
|
1482
|
-
const current = (raw, alias) => renamed ? `coalesce(r_${alias}.key, ${raw})` : raw;
|
|
1483
|
-
const join = (raw, alias) => renamed ? ` left join renamed r_${alias} on r_${alias}.former = ${raw}` : "";
|
|
1484
|
-
return `-- one custom role: as above, where an allow entry key@level reaches only the ceiling keys of that level
|
|
1485
|
-
create or replace function ${keys}(p_allow text[], p_deny text[], p_include text[], p_scope text)
|
|
1486
|
-
returns setof text
|
|
1487
|
-
language sql
|
|
1488
|
-
stable
|
|
1489
|
-
set search_path = ''
|
|
1490
|
-
as $$
|
|
1491
|
-
with ${prelude}entries as (
|
|
1492
|
-
select ${current("split_part(e.entry, '@', 1)", "e")} as permission, nullif(split_part(e.entry, '@', 2), '') as level
|
|
1493
|
-
from unnest(coalesce(p_allow, '{}'::text[])) as e(entry)${join("split_part(e.entry, '@', 1)", "e")}
|
|
1494
|
-
),
|
|
1495
|
-
ceiling as (
|
|
1496
|
-
select c.role, ${current("c.permission", "c")} as permission, c.grant_key, c.effect
|
|
1497
|
-
from ${ceiling} c${join("c.permission", "c")}
|
|
1498
|
-
where c.scope = p_scope
|
|
1499
|
-
),
|
|
1500
|
-
levels as (
|
|
1501
|
-
select distinct c.permission, split_part(c.grant_key, '@', 2) as level
|
|
1502
|
-
from ceiling c
|
|
1503
|
-
where c.effect = 'allow' and strpos(c.grant_key, '@') > 0
|
|
1504
|
-
),
|
|
1505
|
-
denied as (
|
|
1506
|
-
select ${current("d.permission", "d")} as permission
|
|
1507
|
-
from unnest(coalesce(p_deny, '{}'::text[])) as d(permission)${join("d.permission", "d")}
|
|
1508
|
-
union
|
|
1509
|
-
select e.permission from entries e
|
|
1510
|
-
where e.level is not null
|
|
1511
|
-
and not exists (select 1 from levels l where l.permission = e.permission and l.level = e.level)
|
|
1512
|
-
),
|
|
1513
|
-
included as (
|
|
1514
|
-
select rp.role, ${current("rp.permission", "rp")} as permission, rp.grant_key, rp.scope, rp.effect
|
|
1515
|
-
from ${rp} rp${join("rp.permission", "rp")}
|
|
1516
|
-
where rp.role = any(coalesce(p_include, '{}'::text[]))
|
|
1517
|
-
),
|
|
1518
|
-
kept as (
|
|
1519
|
-
select i.permission, i.grant_key
|
|
1520
|
-
from included i
|
|
1521
|
-
where i.effect = 'allow'
|
|
1522
|
-
and i.scope = p_scope
|
|
1523
|
-
and exists (
|
|
1524
|
-
select 1 from ceiling c
|
|
1525
|
-
where c.role = i.role and c.grant_key = i.grant_key and c.effect = 'allow'
|
|
1526
|
-
)
|
|
1527
|
-
),
|
|
1528
|
-
wanted as (
|
|
1529
|
-
select e.permission from entries e where e.level is null
|
|
1530
|
-
union
|
|
1531
|
-
select i.permission
|
|
1532
|
-
from included i
|
|
1533
|
-
where i.effect = 'allow'
|
|
1534
|
-
and not (i.scope = p_scope and exists (
|
|
1535
|
-
select 1 from ceiling c
|
|
1536
|
-
where c.role = i.role and c.grant_key = i.grant_key and c.effect = 'allow'
|
|
1537
|
-
))
|
|
1538
|
-
),
|
|
1539
|
-
allowed as (
|
|
1540
|
-
select w.permission
|
|
1541
|
-
from wanted w
|
|
1542
|
-
where w.permission not in (select d.permission from denied d)
|
|
1543
|
-
and not exists (select 1 from kept k where k.permission = w.permission)
|
|
1544
|
-
),
|
|
1545
|
-
allowed_levels as (
|
|
1546
|
-
select e.permission, e.level
|
|
1547
|
-
from entries e
|
|
1548
|
-
where e.level is not null
|
|
1549
|
-
and e.permission not in (select d.permission from denied d)
|
|
1550
|
-
and e.permission not in (select a.permission from allowed a)
|
|
1551
|
-
and not exists (select 1 from kept k where k.permission = e.permission)
|
|
1552
|
-
)
|
|
1553
|
-
select k.grant_key from kept k
|
|
1554
|
-
where k.permission not in (select d.permission from denied d)
|
|
1555
|
-
union
|
|
1556
|
-
select i.grant_key from included i
|
|
1557
|
-
where i.effect = 'deny' and i.scope = p_scope
|
|
1558
|
-
union
|
|
1559
|
-
select c.grant_key from ceiling c
|
|
1560
|
-
where c.permission in (select a.permission from allowed a)
|
|
1561
|
-
union
|
|
1562
|
-
select c.grant_key from ceiling c
|
|
1563
|
-
join allowed_levels a on a.permission = c.permission
|
|
1564
|
-
where c.effect = 'deny' or split_part(c.grant_key, '@', 2) = a.level
|
|
1565
|
-
$$;
|
|
1566
|
-
revoke execute on function ${keys}(text[], text[], text[], text) from public, anon, authenticated;`;
|
|
1652
|
+
function sourcesKeep(sources) {
|
|
1653
|
+
return sources.some((source) => source.sql.keeps);
|
|
1567
1654
|
}
|
|
1568
|
-
function
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
left join renamed r on r.former = e.permission`;
|
|
1655
|
+
function sourceRows(sources) {
|
|
1656
|
+
const keep = sourcesKeep(sources);
|
|
1657
|
+
return sources.map((source, index) => source.sql.select(sourceUser(index), keep).replaceAll(/^/gmu, " ")).join("\n union all\n");
|
|
1572
1658
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
*/
|
|
1578
|
-
function renamesSql(renames) {
|
|
1579
|
-
if (renames.length === 0) return {
|
|
1580
|
-
prelude: "",
|
|
1581
|
-
permission: (alias) => `${alias}.permission`,
|
|
1582
|
-
join: () => "",
|
|
1583
|
-
allow: "select unnest(coalesce(p_allow, '{}'::text[])) as permission",
|
|
1584
|
-
denied: (alias) => `${alias}.permission = any(coalesce(p_deny, '{}'::text[]))`
|
|
1585
|
-
};
|
|
1659
|
+
function sourceUser(index) {
|
|
1660
|
+
return `v_user_${String(index)}`;
|
|
1661
|
+
}
|
|
1662
|
+
function sqlBody(sql) {
|
|
1586
1663
|
return {
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
denied: (alias) => `${alias}.permission in (select d.permission from denied d)`
|
|
1664
|
+
sql,
|
|
1665
|
+
vars: []
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
function sourcesBodyOf(sql, sources, user) {
|
|
1669
|
+
return {
|
|
1670
|
+
sql,
|
|
1671
|
+
vars: sources.map((source, index) => `${sourceUser(index)} ${source.sql.userType} := ${user};`)
|
|
1596
1672
|
};
|
|
1597
1673
|
}
|
|
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
|
|
1622
|
-
stable
|
|
1623
|
-
set search_path = ''
|
|
1624
|
-
as $$
|
|
1625
|
-
select k.permission from unnest(${sqlTextArray(permissions.toSorted())}) k(permission) order by 1
|
|
1626
|
-
$$;
|
|
1627
|
-
revoke execute on function ${catalog}() from public, anon;
|
|
1628
|
-
grant execute on function ${catalog}() to authenticated;
|
|
1629
|
-
|
|
1630
|
-
create or replace function ${catalog}(p_scope text)
|
|
1631
|
-
returns setof text
|
|
1632
|
-
language sql
|
|
1633
|
-
stable
|
|
1634
|
-
security definer
|
|
1635
|
-
set search_path = ''
|
|
1636
|
-
as $$
|
|
1637
|
-
select distinct rp.permission
|
|
1638
|
-
from ${rp} rp
|
|
1639
|
-
where rp.scope = p_scope
|
|
1640
|
-
and rp.effect = 'allow'
|
|
1641
|
-
and rp.permission = any(${sqlTextArray(permissions.toSorted())})
|
|
1642
|
-
order by 1
|
|
1643
|
-
$$;
|
|
1644
|
-
revoke execute on function ${catalog}(text) from public, anon;
|
|
1645
|
-
grant execute on function ${catalog}(text) to authenticated;`);
|
|
1646
|
-
}
|
|
1647
|
-
const declared = ` select distinct rp.permission, rp.effect
|
|
1648
|
-
from ${rp} rp
|
|
1649
|
-
where rp.role = p_role and rp.scope = p_scope
|
|
1650
|
-
order by 1, 2`;
|
|
1651
|
-
let body = `begin
|
|
1652
|
-
perform p_tenant, p_scope_id; -- only a custom role reads them
|
|
1653
|
-
return query
|
|
1654
|
-
${declared};
|
|
1655
|
-
end;`;
|
|
1656
|
-
let trustedBody = body;
|
|
1657
|
-
if (custom !== void 0 && database && root !== void 0) {
|
|
1658
|
-
const tenant = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
1659
|
-
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
1660
|
-
const keys = qualified(ctx, CUSTOM_ROLES.keys);
|
|
1661
|
-
const shape = qualified(ctx, CUSTOM_ROLES.shape);
|
|
1662
|
-
const ids = (helper) => `(select x::text from ${qualified(ctx, helper)}() x)`;
|
|
1663
|
-
const memberOf = `p_tenant::text in ${ids(memberIdsHelper(root))}
|
|
1664
|
-
or ${perScope(ctx, (scope) => scope === root ? "false" : `p_scope_id in ${ids(memberIdsHelper(scope))}`)}`;
|
|
1665
|
-
const manage = custom.manage ?? [];
|
|
1666
|
-
const platform = manage.length === 0 ? "false" : `exists (
|
|
1667
|
-
select 1 from ${rp} m
|
|
1668
|
-
where m.effect = 'allow'
|
|
1669
|
-
and m.permission = any(${sqlTextArray(manage)})
|
|
1670
|
-
and ${qualified(ctx, HELPERS.has)}(m.grant_key)
|
|
1671
|
-
)`;
|
|
1672
|
-
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`;
|
|
1673
|
-
const allowEntryOf = custom.levels === true ? "c.permission || coalesce('@' || c.level, '')" : "c.permission";
|
|
1674
|
-
const checked = (gate) => `begin
|
|
1675
|
-
if p_role = any(${sqlTextArray(custom.declared)}) then
|
|
1676
|
-
return query
|
|
1677
|
-
${declared};
|
|
1678
|
-
return;
|
|
1679
|
-
end if;
|
|
1680
|
-
perform ${shape}(p_tenant, p_scope, p_scope_id, p_role);${gate}
|
|
1681
|
-
return query
|
|
1682
|
-
select distinct rp.permission, rp.effect
|
|
1683
|
-
from ${keys}(
|
|
1684
|
-
array(select ${allowEntryOf} from ${tenant} c where ${match} and c.effect = 'allow'),
|
|
1685
|
-
array(select c.permission from ${tenant} c where ${match} and c.effect = 'deny'),
|
|
1686
|
-
array(select c.include_role from ${includes} c where ${match}),
|
|
1687
|
-
p_scope
|
|
1688
|
-
) k(grant_key)
|
|
1689
|
-
join ${rp} rp on rp.grant_key = k.grant_key and rp.scope = p_scope
|
|
1690
|
-
order by 1, 2;
|
|
1691
|
-
end;`;
|
|
1692
|
-
body = checked(`
|
|
1693
|
-
if not (${signedIn(ctx)} and (
|
|
1694
|
-
${platform}
|
|
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)
|
|
1704
|
-
language plpgsql
|
|
1674
|
+
/** `language sql`, or `plpgsql` returning the query when the body declares variables. */
|
|
1675
|
+
function functionBody(body) {
|
|
1676
|
+
if (body.vars.length === 0) return `language sql
|
|
1705
1677
|
stable
|
|
1706
1678
|
security definer
|
|
1707
1679
|
set search_path = ''
|
|
1708
1680
|
as $$
|
|
1709
|
-
${body}
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
grant execute on function ${reader}(text, text, ${tenantType}, text) to authenticated;`);
|
|
1713
|
-
const trusted = qualified(ctx, READERS.trusted);
|
|
1714
|
-
const signature = `text, text, ${tenantType}, text`;
|
|
1715
|
-
const grants = trustedReaders === void 0 || trustedReaders.length === 0 ? "" : `\ngrant execute on function ${trusted}(${signature}) to ${trustedReaders.map(quoteIdent).join(", ")};`;
|
|
1716
|
-
chunks.push(`create or replace function ${trusted}(p_role text, p_scope text, p_tenant ${tenantType} default null, p_scope_id text default null)
|
|
1717
|
-
returns table (permission text, effect text)
|
|
1718
|
-
language plpgsql
|
|
1681
|
+
${body.sql}
|
|
1682
|
+
$$;`;
|
|
1683
|
+
return `language plpgsql
|
|
1719
1684
|
stable
|
|
1720
1685
|
security definer
|
|
1721
1686
|
set search_path = ''
|
|
1722
1687
|
as $$
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1688
|
+
declare
|
|
1689
|
+
${body.vars.map((line) => ` ${line}`).join("\n")}
|
|
1690
|
+
begin
|
|
1691
|
+
return query
|
|
1692
|
+
${body.sql};
|
|
1693
|
+
end;
|
|
1694
|
+
$$;`;
|
|
1727
1695
|
}
|
|
1728
|
-
/**
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1696
|
+
/**
|
|
1697
|
+
* `ms.id` for the scope itself and `ms.within ->> name` for an ancestor. The
|
|
1698
|
+
* alias is not `s`, which `activeRowSql` binds inside its `exists`.
|
|
1699
|
+
*/
|
|
1700
|
+
function sourceIdOf(scope) {
|
|
1701
|
+
return (name) => name === scope ? "ms.id" : `ms.within ->> ${quoteLiteral(name)}`;
|
|
1732
1702
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1703
|
+
function sourceFilters(ctx, scope, user = subjectIdSql(ctx), permission, keeps = false) {
|
|
1704
|
+
return [
|
|
1705
|
+
...keeps ? [` and ${keptRowSql("ms.keep", permission)}`] : [],
|
|
1706
|
+
...userActive(ctx, " ", user),
|
|
1707
|
+
...instancesActive(ctx, scope, sourceIdOf(scope), " ", permission)
|
|
1708
|
+
].map((line) => `\n${line}`).join("");
|
|
1737
1709
|
}
|
|
1738
|
-
function
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
${
|
|
1742
|
-
|
|
1710
|
+
function sourcesBody(ctx, scope, type) {
|
|
1711
|
+
const root = rootName(ctx);
|
|
1712
|
+
const tenant = sourceIdOf(scope)(root);
|
|
1713
|
+
const narrow = narrowsTo(ctx, scope) ? `\n and (${activeTenant(ctx)} is null or ${tenant} = ${activeTenant(ctx)})${keyTenantLine(ctx, scope, tenant)}` : keyTenantLine(ctx, scope, tenant);
|
|
1714
|
+
const sources = scopeSources(ctx, scope);
|
|
1715
|
+
const set = ctx.grantSet === true;
|
|
1716
|
+
const rows = ` from (
|
|
1717
|
+
${sourceRows(sources)}
|
|
1718
|
+
) ms
|
|
1719
|
+
cross join lateral jsonb_array_elements_text(
|
|
1720
|
+
case jsonb_typeof(ms.roles) when 'array' then ms.roles else '[]'::jsonb end
|
|
1721
|
+
) r(role)`;
|
|
1722
|
+
const declared = ` select (ms.id)::${type}${set ? ", rp.grant_key" : ""}
|
|
1723
|
+
${rows}
|
|
1724
|
+
join ${qualified(ctx, "role_permissions")} rp on rp.role = r.role
|
|
1725
|
+
where ${signedIn(ctx)}
|
|
1726
|
+
and ms.scope = ${quoteLiteral(scope)}${set ? "" : "\n and rp.grant_key = p_grant"}
|
|
1727
|
+
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))}`;
|
|
1728
|
+
const custom = ctx.customRoles;
|
|
1729
|
+
if (custom === void 0) return sourcesBodyOf(declared, sources, subjectIdSql(ctx));
|
|
1730
|
+
const match = [
|
|
1731
|
+
customTenantMatch(ctx, `(${tenant})::text`, scope, "r.role"),
|
|
1732
|
+
`c.scope = ${quoteLiteral(scope)}`,
|
|
1733
|
+
"(c.scope_id is null or c.scope_id = (ms.id)::text)",
|
|
1734
|
+
"c.role = r.role"
|
|
1735
|
+
].join(" and ");
|
|
1736
|
+
const entries = (source, value, extra) => `array(select c.${value} from ${qualified(ctx, source)} c where ${match}${extra})`;
|
|
1737
|
+
if (set) return sourcesBodyOf(`${declared}
|
|
1738
|
+
union
|
|
1739
|
+
select (ms.id)::${type}, ck.grant_key
|
|
1740
|
+
${rows}
|
|
1741
|
+
${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", ""))}
|
|
1742
|
+
where ${signedIn(ctx)}
|
|
1743
|
+
and ms.scope = ${quoteLiteral(scope)}
|
|
1744
|
+
and not (r.role = any(${sqlTextArray(custom.declared)}))${narrow}${sourceFilters(ctx, scope, subjectIdSql(ctx), grantPermissionSql("ck.grant_key"), sourcesKeep(sources))}`, sources, subjectIdSql(ctx));
|
|
1745
|
+
return sourcesBodyOf(`${declared}
|
|
1746
|
+
union
|
|
1747
|
+
select (ms.id)::${type}
|
|
1748
|
+
${rows}
|
|
1749
|
+
where ${signedIn(ctx)}
|
|
1750
|
+
and ms.scope = ${quoteLiteral(scope)}
|
|
1751
|
+
and not (r.role = any(${sqlTextArray(custom.declared)}))${narrow}${sourceFilters(ctx, scope, subjectIdSql(ctx), grantPermissionSql("p_grant"), sourcesKeep(sources))}
|
|
1752
|
+
${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));
|
|
1743
1753
|
}
|
|
1744
|
-
function
|
|
1745
|
-
|
|
1754
|
+
function scopedBody(ctx, scope, type) {
|
|
1755
|
+
if (ctx.authorize !== "database") return sqlBody(claimBody(ctx, scope, type));
|
|
1756
|
+
return scopeSources(ctx, scope).length > 0 ? sourcesBody(ctx, scope, type) : sqlBody(tableBody(ctx, scope, type));
|
|
1757
|
+
}
|
|
1758
|
+
//#endregion
|
|
1759
|
+
//#region src/cli/rls/capabilities.ts
|
|
1760
|
+
/** The helper `--capabilities` adds: ids of one resource a link capability claim reaches. Part of the SQL contract. */
|
|
1761
|
+
const CAPABILITIES = { ids: "permdock_capability_ids" };
|
|
1762
|
+
/**
|
|
1763
|
+
* The `anon` branch access for a resource-scoped grant reached through a link
|
|
1764
|
+
* capability: the row's `field` holds an id the capability claim names for
|
|
1765
|
+
* `resource`, `role` and `permission`. Uncorrelated, so it runs once per
|
|
1766
|
+
* statement; the column is compared as text because the claim holds text.
|
|
1767
|
+
*/
|
|
1768
|
+
function capabilityAccessSql(ctx, field, resource, role, permission) {
|
|
1769
|
+
const args = [
|
|
1770
|
+
resource,
|
|
1771
|
+
role,
|
|
1772
|
+
permission
|
|
1773
|
+
].map(quoteLiteral).join(", ");
|
|
1774
|
+
return `${quoteIdent(field)}::text in (select ${qualified(ctx, CAPABILITIES.ids)}(${args}))`;
|
|
1775
|
+
}
|
|
1776
|
+
function capabilitiesSql(ctx) {
|
|
1777
|
+
if (ctx.capabilities !== true) return "";
|
|
1778
|
+
const fn = qualified(ctx, CAPABILITIES.ids);
|
|
1779
|
+
return `-- link capabilities: the capability claim exchangeCapability mints (role anon, short-lived)
|
|
1780
|
+
create or replace function ${fn}(p_resource text, p_role text, p_permission text)
|
|
1781
|
+
returns setof text
|
|
1782
|
+
language sql
|
|
1783
|
+
stable
|
|
1784
|
+
set search_path = ''
|
|
1785
|
+
as $$
|
|
1786
|
+
select c -> 'on' ->> 'id'
|
|
1787
|
+
from (select ${subjectClaimJsonSql(ctx, "capability")} as c) capability
|
|
1788
|
+
where jsonb_typeof(c) = 'object'
|
|
1789
|
+
and c ->> 'v' = '1'
|
|
1790
|
+
and c ->> 'holder' = 'link'
|
|
1791
|
+
and c -> 'on' ->> 'resource' = p_resource
|
|
1792
|
+
and jsonb_typeof(c -> 'roles') = 'array'
|
|
1793
|
+
and c -> 'roles' @> jsonb_build_array(p_role)
|
|
1794
|
+
and (c -> 'permissions' is null or c -> 'permissions' @> jsonb_build_array(p_permission))
|
|
1795
|
+
and jsonb_typeof(c -> 'expires_at') = 'number'
|
|
1796
|
+
and (c ->> 'expires_at')::numeric > extract(epoch from now())
|
|
1797
|
+
$$;
|
|
1798
|
+
revoke execute on function ${fn}(text, text, text) from public;
|
|
1799
|
+
grant execute on function ${fn}(text, text, text) to anon, authenticated;`;
|
|
1746
1800
|
}
|
|
1747
1801
|
//#endregion
|
|
1748
1802
|
//#region src/cli/rls/custom-role-cascade.ts
|
|
@@ -1775,7 +1829,7 @@ function customRoleCascadeSql(ctx) {
|
|
|
1775
1829
|
const fn = qualified(ctx, CUSTOM_ROLES.cascade);
|
|
1776
1830
|
const target = quoteTable(qualifySqlTable(table.table));
|
|
1777
1831
|
const tenant = (row) => table.tenant === void 0 ? `null::${tenantType}` : `${rowColumn(row, table.tenant)}::${tenantType}`;
|
|
1778
|
-
const scope = (row) => table
|
|
1832
|
+
const scope = (row) => rolesTableScope(table, tenant(row), root, (name) => rowColumn(row, name));
|
|
1779
1833
|
const id = (row) => table.id === void 0 ? "null::text" : `${rowColumn(row, table.id)}::text`;
|
|
1780
1834
|
const skipped = (row) => table.skip === void 0 ? "false" : `coalesce(${rowColumn(row, table.skip)}, false)`;
|
|
1781
1835
|
const guard = qualified(ctx, CUSTOM_ROLES.guard);
|
|
@@ -1909,7 +1963,7 @@ ${raiseSql(" ", "42501", `'permdock: the caller may not manage roles in ' ||
|
|
|
1909
1963
|
)` : "";
|
|
1910
1964
|
const reachCte = leveled && reachRows !== "" ? `,
|
|
1911
1965
|
reach (grant_key, level) as (values ${reachRows})` : "";
|
|
1912
|
-
const match =
|
|
1966
|
+
const match = `${storedTenant(ctx)} and c.scope = p_scope and c.scope_id is not distinct from p_scope_id`;
|
|
1913
1967
|
const memberOf = `p_tenant::text in ${members(root)}
|
|
1914
1968
|
or ${perScope(ctx, (scope) => scope === root ? "false" : `p_scope_id in ${members(scope)}`)}`;
|
|
1915
1969
|
return {
|
|
@@ -1961,7 +2015,7 @@ begin
|
|
|
1961
2015
|
perform ${shape}(p_tenant, p_scope, p_scope_id, p_role);
|
|
1962
2016
|
if not (${signedIn(ctx, user)} and (
|
|
1963
2017
|
${platform}
|
|
1964
|
-
or (p_scope <> 'global' and (${memberOf}))
|
|
2018
|
+
or (p_scope <> 'global' and coalesce(${memberOf}, false))
|
|
1965
2019
|
)) then
|
|
1966
2020
|
${raiseSql(" ", "42501", `'permdock: the caller is not a member of ' || coalesce(p_tenant::text, 'the platform')`, "not-member")}
|
|
1967
2021
|
end if;${requires}
|
|
@@ -1989,8 +2043,9 @@ revoke execute on function ${guard}(${userArg}${tenantType}, text, text, text) f
|
|
|
1989
2043
|
* \`assignablePermissions\`: entries inside the ceiling of the role's scope,
|
|
1990
2044
|
* includes naming declared roles, and only permissions and levels the caller
|
|
1991
2045
|
* may hand out, for the new definition and the stored one alike. A platform
|
|
1992
|
-
* custom role (\`p_scope = 'global'\`, no tenant)
|
|
1993
|
-
*
|
|
2046
|
+
* custom role (\`p_scope = 'global'\`, no tenant) and a shared custom role (no
|
|
2047
|
+
* tenant at a lower scope) are written by a caller who holds a
|
|
2048
|
+
* \`meta.manageRoles\` permission through a global role. The
|
|
1994
2049
|
* \`permdock_trusted_*\` variants skip the caller checks and keep the
|
|
1995
2050
|
* definition checks; no client role may execute them.
|
|
1996
2051
|
*/
|
|
@@ -2016,9 +2071,15 @@ function customRoleWritesSql(ctx, levelReach) {
|
|
|
2016
2071
|
const scopes = sqlTextArray(ctx.scopes.map((scope) => scope.name));
|
|
2017
2072
|
const roleName = (param) => `coalesce(${param}, '') = '' or ${param} = any(${sqlTextArray(custom.declared)})`;
|
|
2018
2073
|
const levelOf = leveled ? ", nullif(split_part(e, '@', 2), '')" : "";
|
|
2019
|
-
const
|
|
2074
|
+
const levelColumn = leveled ? ", level" : "";
|
|
2075
|
+
const sharedRole = qualified(ctx, CUSTOM_ROLES.shared);
|
|
2076
|
+
const taken = (param) => ` if p_tenant is not null and ${sharedRole}(p_scope, ${param}) then
|
|
2077
|
+
${raiseSql(" ", "22023", `'permdock: ' || ${param} || ' is the name of a shared custom role'`, "name-taken")}
|
|
2078
|
+
end if;
|
|
2079
|
+
`;
|
|
2080
|
+
const write = `${taken("p_role")} delete from ${perms} c where ${match} and c.role = p_role;
|
|
2020
2081
|
delete from ${includes} c where ${match} and c.role = p_role;
|
|
2021
|
-
insert into ${perms} (tenant_id, scope, scope_id, role, permission, effect${
|
|
2082
|
+
insert into ${perms} (tenant_id, scope, scope_id, role, permission, effect${levelColumn})
|
|
2022
2083
|
select distinct p_tenant, p_scope, p_scope_id, p_role, ${current(leveled ? "split_part(e, '@', 1)" : "e")}, 'allow'${levelOf}
|
|
2023
2084
|
from unnest(coalesce(p_allow, '{}'::text[])) e
|
|
2024
2085
|
union
|
|
@@ -2027,7 +2088,7 @@ function customRoleWritesSql(ctx, levelReach) {
|
|
|
2027
2088
|
insert into ${includes} (tenant_id, scope, scope_id, role, include_role)
|
|
2028
2089
|
select distinct p_tenant, p_scope, p_scope_id, p_role, e
|
|
2029
2090
|
from unnest(coalesce(p_include, '{}'::text[])) e;`;
|
|
2030
|
-
const move =
|
|
2091
|
+
const move = `${taken("p_to")} if ${roleName("p_to")} then
|
|
2031
2092
|
${raiseSql(" ", "22023", `'permdock: ' || coalesce(p_to, 'null') || ' is not a custom role name'`, "declared-role")}
|
|
2032
2093
|
end if;
|
|
2033
2094
|
if exists (select 1 from ${perms} c where ${match} and c.role = p_to)
|
|
@@ -2058,7 +2119,7 @@ grant execute on function ${qualified(ctx, name)}(${args}) to authenticated;` :
|
|
|
2058
2119
|
const removeParams = `p_tenant ${tenantType}, p_scope text, p_scope_id text, p_role text`;
|
|
2059
2120
|
return `${caller.beyond}
|
|
2060
2121
|
|
|
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
|
|
2122
|
+
-- where a custom role may live: a scope of a tenant, a scope of every tenant (shared: no tenant or instance), or the platform (scope global, no tenant or instance), under an undeclared name
|
|
2062
2123
|
create or replace function ${shape}(p_tenant ${tenantType}, p_scope text, p_scope_id text, p_role text)
|
|
2063
2124
|
returns void
|
|
2064
2125
|
language plpgsql
|
|
@@ -2070,8 +2131,10 @@ begin
|
|
|
2070
2131
|
if p_tenant is not null or p_scope_id is not null then
|
|
2071
2132
|
${raiseSql(" ", "22023", `'permdock: a platform custom role has no tenant or instance'`, "unknown-scope")}
|
|
2072
2133
|
end if;
|
|
2073
|
-
elsif
|
|
2134
|
+
elsif not (p_scope = any(${scopes})) then
|
|
2074
2135
|
${raiseSql(" ", "22023", `'permdock: custom roles are held at a scope of a tenant or at global, not ' || coalesce(p_scope, 'null')`, "unknown-scope")}
|
|
2136
|
+
elsif p_tenant is null and p_scope_id is not null then
|
|
2137
|
+
${raiseSql(" ", "22023", `'permdock: a shared custom role has no instance'`, "unknown-scope")}
|
|
2075
2138
|
end if;
|
|
2076
2139
|
if ${roleName("p_role")} then
|
|
2077
2140
|
${raiseSql(" ", "22023", `'permdock: ' || coalesce(p_role, 'null') || ' is not a custom role name'`, "declared-role")}
|
|
@@ -2217,7 +2280,7 @@ function globalRolesSql(ctx) {
|
|
|
2217
2280
|
const id = quoteIdent(through.id);
|
|
2218
2281
|
const key = quoteIdent(through.key);
|
|
2219
2282
|
const ref = quoteIdent(through.ref);
|
|
2220
|
-
const global = source.tenant === void 0 ? "" : ` and k.${quoteIdent(source.tenant)} is null`;
|
|
2283
|
+
const global = source.tenant === void 0 ? "" : ` and k.${quoteIdent(source.tenant)} is null${source.shared === void 0 ? "" : ` and k.${quoteIdent(source.shared)} is not true`}`;
|
|
2221
2284
|
body = ` select string_agg(w.r, ', ' order by w.r) into v_unknown
|
|
2222
2285
|
from (${wanted}) w(r)
|
|
2223
2286
|
where not exists (select 1 from ${keys} k where k.${key}::text = w.r${global});
|
|
@@ -2909,7 +2972,7 @@ function canAssignCustomSql(ctx, forUser = false) {
|
|
|
2909
2972
|
const guard = qualified(ctx, forUser ? CUSTOM_ROLES.guardFor : CUSTOM_ROLES.guard);
|
|
2910
2973
|
const userParam = forUser ? "p_user uuid, " : "";
|
|
2911
2974
|
const userArg = forUser ? "uuid, " : "";
|
|
2912
|
-
const stored =
|
|
2975
|
+
const stored = `${storedTenant(ctx)} and c.scope = p_scope and c.role = p_role and (c.scope_id is null or c.scope_id = p_scope_id)`;
|
|
2913
2976
|
const grants = forUser ? `revoke execute on function ${fn}(${userArg}${tenantType}, text, text, text) from public, anon, authenticated;` : `revoke execute on function ${fn}(${tenantType}, text, text, text) from public, anon;
|
|
2914
2977
|
grant execute on function ${fn}(${tenantType}, text, text, text) to authenticated;`;
|
|
2915
2978
|
return `-- whether ${forUser ? "user p_user" : "the caller"} may assign custom role p_role at instance p_scope_id (a platform role: null tenant, scope global): the custom-role write checks on its stored definition${forUser ? "; no client role may execute it" : ""}
|
|
@@ -3631,4 +3694,4 @@ function ownershipSql(ctx) {
|
|
|
3631
3694
|
return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
|
|
3632
3695
|
}
|
|
3633
3696
|
//#endregion
|
|
3634
|
-
export {
|
|
3697
|
+
export { activeUserSql as $, collectGrants as A, hasPermittedUsers as B, byKey as C, apiKeysPlan as D, apiKeyFields as E, CLOSURE as F, memberIdsHelper as G, memberForHelper as H, graphHelper as I, permittedIdsHelper as J, memberRoleOf as K, graphSqlName as L, compileConditionSql as M, contextRefs as N, breakGlassHolder as O, sqlFunctionNames as P, activeRowSql as Q, graphSqlText as R, byCodeUnit as S, apiKeyAllowsCall as T, memberForSources as U, linkHelper as V, memberForTable as W, scopeSources as X, restrictedHelper as Y, scopeTable as Z, capabilityAccessSql as _, guardedTables as a, currentKeyExpr as b, PERMISSION_HELPERS as c, permissionKeysForHelper as d, checkSuspension as et, permittedByPermissionHelper as f, seedSql as g, helpersSql as h, assignmentSql as i, roleNames as j, breakGlassKey as k, checkName as l, accessSql as m, ownershipSql as n, ASSIGNMENTS as o, requiresSql as p, permittedForHelper as q, holderSources as r, OWNERSHIP as s, ownershipRules as t, permissionHelpersSql as u, globalRoleRows as v, API_KEY_ALLOWS as w, rolesTableScope as x, globalRoleSource as y, hasMemberFor as z };
|