permdock 0.3.0 → 0.4.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 +7 -5
- package/dist/a2a/index.js +14 -21
- package/dist/ai-sdk/index.d.ts +18 -6
- package/dist/ai-sdk/index.js +8 -5
- package/dist/app-data-CV8pqqMi.js +59 -0
- package/dist/approvals/index.d.ts +2 -2
- package/dist/approvals/index.js +3 -3
- package/dist/{approvals-DbYAuNkG.js → approvals-DZL76lKW.js} +35 -19
- package/dist/{approvers-DT4AVyIg.js → approvers-CqPOLD-o.js} +1 -1
- package/dist/{arazzo-BAgw9ap6.js → arazzo-C4BcskbX.js} +6 -5
- package/dist/{arazzo-CKwwqFWz.js → arazzo-CD8d0WLb.js} +2 -2
- package/dist/authzen/index.d.ts +3 -3
- package/dist/authzen/index.js +161 -23
- package/dist/better-auth/index.d.ts +2 -2
- package/dist/better-auth/index.js +6 -19
- package/dist/better-supabase/index.d.ts +112 -87
- package/dist/better-supabase/index.js +257 -156
- package/dist/{bind-C_ob3LKa.d.ts → bind-BzqG8SMb.d.ts} +16 -5
- package/dist/{bind-CuzGytRf.js → bind-kk-uyCwH.js} +14 -6
- package/dist/build-instance-DIIV9DbC.js +51 -0
- package/dist/{canonical-json-DemiK4Vp.js → canonical-json-CFKyFOke.js} +1 -3
- package/dist/{capability-2kYTMWDv.d.ts → capability-CksyCWDH.d.ts} +2 -2
- package/dist/{capability-4qPdO9gk.js → capability-txATtKV4.js} +3 -2
- package/dist/catalog/index.d.ts +1 -1
- package/dist/catalog/index.js +1 -1
- package/dist/catalog-6yWAX6Ps.js +2 -0
- package/dist/{catalog-dFZdWCGN.js → catalog-WHG4kBq7.js} +2 -2
- package/dist/{catalog-doc-DdGm82XT.js → catalog-doc-DWtg0gGR.js} +17 -10
- package/dist/{catalog-DWF5cYqB.js → catalog-tZUyOfOv.js} +4 -4
- package/dist/claude-agent/index.d.ts +4 -4
- package/dist/claude-agent/index.js +4 -5
- package/dist/clerk/index.d.ts +3 -3
- package/dist/clerk/index.js +8 -33
- package/dist/cli/bin.js +1 -1
- package/dist/cli/index.d.ts +3 -3
- package/dist/cli/index.js +3 -3
- package/dist/cloud/index.d.ts +4 -4
- package/dist/cloud/index.js +9 -15
- package/dist/{cloud-CB_ELYuX.js → cloud-C7bSDgX4.js} +2 -2
- package/dist/{cloud-DWLXO2af.js → cloud-PeBN3IEn.js} +5 -5
- package/dist/{collect-DSTE3ml9.js → collect-2SrB7lfk.js} +6 -8
- package/dist/collect-Bw2Jdq1q.js +2 -0
- package/dist/{collect-DClJsheJ.js → collect-CQx4hX4N.js} +3 -3
- package/dist/{commands-DSxCqG9A.js → commands-Cpip4ULx.js} +13 -13
- package/dist/{compact-y6vfu2_f.js → compact-CAf557ZB.js} +11 -1
- package/dist/compile/index.d.ts +1 -1
- package/dist/compile/index.js +1 -1
- package/dist/{compile-Ba4blROR.js → compile-CcEu2ZQ1.js} +6 -5
- package/dist/{compile-BkRyBDBw.d.ts → compile-CtTP7eur.d.ts} +2 -2
- package/dist/{config-BJiM-xAm.js → config-BIjREpyE.js} +4 -4
- package/dist/{config-Dnszbv-6.js → config-DLt6BvV7.js} +2 -2
- package/dist/{config-report-DMvWKUxL.js → config-report--Xb9APJS.js} +5 -6
- package/dist/{connection-CU8MQxRi.js → connection-DUsczpB5.js} +3 -2
- package/dist/constants-DcJABbLW.js +20 -0
- package/dist/{context-Bcy85rGi.js → context-DieZVJXu.js} +16 -5
- package/dist/convex/index.d.ts +4 -4
- package/dist/convex/index.js +14 -16
- package/dist/create-BBO24vj6.js +324 -0
- package/dist/{credential-8P81ff4u.js → credential-kue0qpbm.js} +4 -4
- package/dist/deciding-columns-Sq12mEA6.js +123 -0
- package/dist/delegation-input-FTxMK3ji.js +19 -0
- package/dist/denied-text-DfRk15uw.d.ts +19 -0
- package/dist/{describe-DMYiCjJV.d.ts → describe-C3ldA9os.d.ts} +11 -2
- package/dist/{describe-B_gVZEVi.js → describe-Cgkp7eP0.js} +5 -4
- package/dist/{diff-DBIhz_f2.js → diff-Cnghh_Ip.js} +2 -2
- package/dist/{diff-2VaeChzX.js → diff-rpqoe7Ag.js} +9 -9
- package/dist/{doctor-BwCAMLx_.js → doctor-CuVJvhV_.js} +2 -2
- package/dist/doctor-owlv6ddh.js +2166 -0
- package/dist/drizzle/index.d.ts +2 -2
- package/dist/drizzle/index.js +6 -14
- package/dist/elysia/index.d.ts +4 -4
- package/dist/elysia/index.js +7 -7
- package/dist/{errors-DPdqI50v.d.ts → errors-OD_4c__H.d.ts} +2 -2
- package/dist/{errors-Cz5zn0mW.js → errors-drnfxYtm.js} +3 -3
- package/dist/{evaluations-tz6D9A2G.js → evaluations-C9YJsv3H.js} +45 -7
- package/dist/eve/index.d.ts +5 -5
- package/dist/eve/index.js +5 -5
- package/dist/express/index.d.ts +4 -4
- package/dist/express/index.js +8 -7
- package/dist/fastify/index.d.ts +15 -5
- package/dist/fastify/index.js +9 -29
- package/dist/fields-fwfXN01n.js +1016 -0
- package/dist/{files-DWUN9PzI.js → files-D_JIzUrP.js} +12 -3
- package/dist/{fixtures-ht28Cplr.js → fixtures-h2eG2OZV.js} +5 -6
- package/dist/{from-snapshot-CTLijvdm.js → from-snapshot-7REQZe0H.js} +32 -22
- package/dist/{generate-CH7ThJ14.js → generate-CNU1pIOo.js} +1 -3
- package/dist/grantee-CXY-0C87.js +254 -0
- package/dist/grantee-conditions-MQs5NIuX.js +270 -0
- package/dist/grantee-match-fxYp9Nmt.js +141 -0
- package/dist/{helpers-H_KARDVC.js → helpers-C9YmyBIa.js} +7 -6
- package/dist/hono/index.d.ts +4 -4
- package/dist/hono/index.js +8 -8
- package/dist/hosted-CBHgkzDl.js +655 -0
- package/dist/{http-SO7qGttf.js → http-BBoKefeS.js} +35 -2
- package/dist/{http-BIhmLfCc.js → http-BEbdMZ05.js} +2 -24
- package/dist/{rls-import-BTFVk3jH.js → import-BFIlhetD.js} +86 -80
- package/dist/{openapi-import-cmwetbzw.js → import-UHk1N7vH.js} +6 -7
- package/dist/{index-C8z4RjQV.d.ts → index-BwMe94Xq.d.ts} +1 -1
- package/dist/{index-0nNqHspG.d.ts → index-DgM7w96K.d.ts} +2 -2
- package/dist/{index-C7Tw7MaW.d.ts → index-Dz7didpC.d.ts} +3 -3
- package/dist/{index-BNc7Wer_.d.ts → index-UdVQgq48.d.ts} +105 -38
- package/dist/index.d.ts +15 -13
- package/dist/index.js +22 -22
- package/dist/{instance-options-jARdwfPN.d.ts → instance-options-plkrsAog.d.ts} +1 -1
- package/dist/{json-schema-BsAflUGW.js → json-schema-DBkdUuCO.js} +1 -3
- package/dist/jwt/index.d.ts +4 -4
- package/dist/jwt/index.js +13 -13
- package/dist/{kernel-_RRHmnMY.js → kernel-CB3LrIJM.js} +81 -32
- package/dist/kysely/index.d.ts +2 -2
- package/dist/kysely/index.js +6 -14
- package/dist/{live-options-H2eG8dfJ.js → live-options-BaAFCIiu.js} +16 -3
- package/dist/load-B_shHrmd.js +2 -0
- package/dist/{load-DhWEB9P1.js → load-C3_oTHyO.js} +11 -3
- package/dist/{manifest-iaoBXHKc.js → manifest-BngWKryL.js} +5 -18
- package/dist/{map-claims-Z4QZOiFF.js → map-claims-AVlBaJoa.js} +7 -16
- package/dist/{map-error-pgUWNNtG.js → map-error-BE27PYkE.js} +5 -5
- package/dist/{markers-B0hYLvMD.js → markers-CiwAOZtO.js} +6 -2
- package/dist/{may-use-DndQ5tQ-.js → may-use-BOn_dR_w.js} +4 -3
- package/dist/mcp/index.d.ts +26 -9
- package/dist/mcp/index.js +456 -446
- package/dist/{rls-migrate-CN4TlJTO.js → migrate-BwlB5-Wk.js} +75 -15
- package/dist/nest/index.d.ts +4 -4
- package/dist/nest/index.js +11 -12
- package/dist/next/index.d.ts +31 -8
- package/dist/next/index.js +29 -33
- package/dist/next/plugin.d.ts +1 -1
- package/dist/next/plugin.js +1 -1
- package/dist/node/index.d.ts +4 -4
- package/dist/node/index.js +4 -4
- package/dist/ocsf-uYFXGVw2.js +115 -0
- package/dist/openai/index.d.ts +5 -5
- package/dist/openai/index.js +4 -4
- package/dist/openapi/index.d.ts +3 -3
- package/dist/openapi/index.js +1 -1
- package/dist/{openapi-CqqEbHsL.js → openapi-BJdBmYDG.js} +19 -23
- package/dist/{openapi-BqRBvAng.js → openapi-DBzHQbov.js} +2 -5
- package/dist/{openapi-l0xYHkcv.js → openapi-DcQ1Kg40.js} +3 -3
- package/dist/orpc/index.d.ts +5 -5
- package/dist/orpc/index.js +9 -11
- package/dist/otel/index.d.ts +2 -2
- package/dist/otel/index.js +26 -23
- package/dist/output-files-CgIsvdSa.js +140 -0
- package/dist/{rls-ownership-RuFqaqGz.js → ownership-BgnbOJFT.js} +387 -379
- package/dist/{package-root-zGkxO3oN.js → package-root-DtNnpmUd.js} +3 -2
- package/dist/{parse-BuyjxhZV.js → parse-CVFQHk6c.js} +3 -3
- package/dist/{parse-local-manifest-BrMozJCo.js → parse-local-manifest-C-q02Pal.js} +2 -4
- package/dist/{parse-local-manifest-JLnaBH0S.d.ts → parse-local-manifest-DvYUR0QL.d.ts} +1 -1
- package/dist/pdp/index.d.ts +2 -2
- package/dist/pdp/index.js +8 -9
- package/dist/{permdock-CbgvLnuo.js → permdock-L26udHSk.js} +1387 -1220
- package/dist/{rls-fields-74oZPXWL.js → permission-keys-B26jeyUv.js} +1289 -2066
- package/dist/permission-registry-BsblpB-z.js +385 -0
- package/dist/{permissions-DttnqqxC.d.ts → permission-types-Dg39z9Kv.d.ts} +59 -31
- package/dist/{permissions-RJrovw98.js → permissions-CCJ6lmxB.js} +3 -3
- package/dist/{permissions-CDgvKG5L.d.ts → permissions-COH-KRgT.d.ts} +2 -2
- package/dist/{policy-IB6yxpC3.d.ts → policy-types-sp9P1iIV.d.ts} +661 -582
- package/dist/{policy-D2W7uv19.d.ts → policy-ur0BBK_e.d.ts} +2 -2
- package/dist/{powersync-DliZK_-y.js → powersync-BFSbSElQ.js} +2 -2
- package/dist/powersync-DCDgZgfZ.js +2 -0
- package/dist/{powersync-BylawD_U.js → powersync-cZe4kzhL.js} +26 -27
- package/dist/prisma/index.d.ts +1 -1
- package/dist/prisma/index.js +6 -12
- package/dist/{problem-B43ztKIm.js → problem-CnWraVhC.js} +8 -7
- package/dist/{problem-details-_AMOYz-w.js → problem-details-DECUnXAn.js} +2 -2
- package/dist/{problem-details-DX075l9V.d.ts → problem-details-rIIo8B2C.d.ts} +1 -1
- package/dist/{protected-BdnIEFkY.d.ts → protected-B3_P7xuw.d.ts} +9 -6
- package/dist/{protected-utU-SLuy.js → protected-BN7BUdY1.js} +20 -8
- package/dist/{provider-CSQd7m92.js → provider-Bi-0HNL3.js} +23 -10
- package/dist/{provider-DZY281jO.d.ts → provider-kwHC9CZD.d.ts} +1 -1
- package/dist/{provider-store-Cfbvo4DE.js → provider-store-Ctt-81Ni.js} +6 -3
- package/dist/react/index.d.ts +7 -7
- package/dist/react/index.js +5 -5
- package/dist/react/provider-client.d.ts +1 -1
- package/dist/react/provider-client.js +1 -1
- package/dist/react-native/index.d.ts +20 -11
- package/dist/react-native/index.js +33 -17
- package/dist/{refs-KYb4ntaz.js → refs-f3imMjNW.js} +1 -1
- package/dist/relation-walk-BXnlLtg2.js +186 -0
- package/dist/relations-CvmSpG67.js +163 -0
- package/dist/{replay-CYyiLiwO.d.ts → replay-COpmZUfx.d.ts} +1 -1
- package/dist/request-context-BVM7aWNs.js +71 -0
- package/dist/request-context-C2_1lchh.d.ts +5 -0
- package/dist/resource-relations-D1Un3pcz.js +243 -0
- package/dist/retire-BFcxL6un.js +240 -0
- package/dist/{rls-B4ur8nKQ.js → rls-55DQTuf5.js} +14 -2
- package/dist/{rls-DQtdgTHB.js → rls-BEktUaqg.js} +884 -484
- package/dist/{policy-xyWg7QdR.js → role-builder-B4GbKM-i.js} +235 -541
- package/dist/{rpc-DL9Sqvb1.js → rpc-nYC3cR_u.js} +15 -5
- package/dist/{run-DKDbM4Du.js → run-DMcNW_8T.js} +3 -3
- package/dist/{runtime-CB8gQFvS.js → runtime-D_tMXnsq.js} +1 -1
- package/dist/{saas-DhpJjnBA.js → saas-DtaA4JuW.js} +9 -8
- package/dist/{openapi-schema--ARjyfQr.js → schema-Bol6gqXA.js} +3 -5
- package/dist/{schema-D3MnDMxa.js → schema-DjQBcAP9.js} +4 -2
- package/dist/scim/index.d.ts +1 -1
- package/dist/scim/index.js +1 -1
- package/dist/{scim-BFGBBAqR.js → scim-vFR1O5oG.js} +6 -12
- package/dist/{scopes-CVdPrWN-.js → scopes-tvpa34ep.js} +10 -2
- package/dist/server/index.d.ts +18 -8
- package/dist/server/index.js +8 -8
- package/dist/{server-DlZ28Oz5.js → server-ZYEHtNHS.js} +4 -4
- package/dist/{create-B1d519XK.d.ts → server-types-DdqBwZN_.d.ts} +69 -33
- package/dist/shared-1uhI-e-b.js +294 -0
- package/dist/{shared-DRu8VFVM.js → shared-JhMeOsDF.js} +1 -1
- package/dist/{sink-Bd9PSyfq.js → sink-BTtmfPnE.js} +1 -1
- package/dist/{sink-B1fZzIMf.d.ts → sink-CPN4zLfs.d.ts} +1 -1
- package/dist/{skills-Biq5uup3.js → skills-BZdRQkxs.js} +4 -3
- package/dist/{skills-Bx-U0irG.js → skills-Dle4ZoLF.js} +2 -2
- package/dist/{snapshot-Enxd0n-s.js → snapshot-Dc9CHPME.js} +116 -13
- package/dist/{snapshot-cache-D6jLelln.js → snapshot-cache-Cod40D-X.js} +1 -1
- package/dist/{snapshot-cache-B-LxUahj.d.ts → snapshot-cache-J5T7t_vD.d.ts} +1 -1
- package/dist/solid/index.d.ts +22 -22
- package/dist/solid/index.js +40 -10
- package/dist/{sources-4p0-4xQm.js → sources-B1-b-R0t.js} +25 -26
- package/dist/{sources-BdOYLw9j.d.ts → sources-Dljy06zL.d.ts} +6 -2
- package/dist/{sql-xdg5f4Fi.js → sql-t8teWOfo.js} +19 -3
- package/dist/{src-BDwEZWrZ.js → src-Bb7no-hS.js} +194 -123
- package/dist/ssf/index.d.ts +3 -3
- package/dist/ssf/index.js +58 -61
- package/dist/store-9KO4BlkB.d.ts +147 -0
- package/dist/{store-DMAWwAJV.js → store-ILkLUj_o.js} +4 -4
- package/dist/{store-options-ByhZq1jV.js → store-options-CbUHDoSe.js} +16 -7
- package/dist/{stores-BT2pSCub.js → stores-BHXdSbw9.js} +7 -3
- package/dist/stores-Cxz_80g-.d.ts +31 -0
- package/dist/{stream-DDpvynSB.js → stream-BMui98KK.js} +2 -1
- package/dist/{stream-D1RXubw2.d.ts → stream-Dwsg6WwE.d.ts} +2 -2
- package/dist/{subject-settings-BwBmCdTR.js → subject-settings-CkEwrw3W.js} +23 -5
- package/dist/supabase/index.d.ts +4 -4
- package/dist/supabase/index.js +3 -3
- package/dist/supabase/middleware.d.ts +11 -5
- package/dist/supabase/middleware.js +10 -7
- package/dist/{supabase-C0qESOa-.js → supabase-C1TWlnYt.js} +36 -52
- package/dist/{supabase-Boqb9a_Y.js → supabase-Dbqr141s.js} +2 -2
- package/dist/supabase-hook-BULg7Ut_.js +2 -0
- package/dist/{supabase-hook-CSLQ4zJ3.js → supabase-hook-Cva3ikXz.js} +731 -782
- package/dist/{supautils-Cn4R4cJ4.js → supautils-B6HYL9eL.js} +2 -2
- package/dist/svelte/Protected.svelte +9 -2
- package/dist/svelte/index.d.ts +6 -6
- package/dist/svelte/index.js +25 -11
- package/dist/svelte/runtime.d.ts +10 -3
- package/dist/svelte/runtime.js +4 -3
- package/dist/svelte/source.d.ts +6 -6
- package/dist/svelte/source.js +4 -4
- package/dist/{validity-uOp8UpYn.js → tenancy-DshjqY0n.js} +3 -39
- package/dist/terminal/index.d.ts +5 -5
- package/dist/terminal/index.js +18 -34
- package/dist/testing/index.d.ts +89 -77
- package/dist/testing/index.js +1276 -1262
- package/dist/testing/saas/index.d.ts +4 -4
- 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/thenable-QSmvt-vZ.js +65 -0
- package/dist/{token-CIWxJOCh.js → token-Bx9qy30p.js} +1 -1
- package/dist/tool-refusal-CQ7OEia9.js +41 -0
- package/dist/trpc/index.d.ts +5 -5
- package/dist/trpc/index.js +7 -8
- package/dist/{types-DbjRN55U.d.ts → types-7rJVLWe5.d.ts} +82 -4
- package/dist/{types-aqOHTHcp.d.ts → types-Br9IXUBG.d.ts} +1 -1
- package/dist/types-ByUo56un.d.ts +88 -0
- package/dist/{types-Cq_H8N_X.d.ts → types-C0dee0Qf.d.ts} +2 -2
- package/dist/{types-DxiCGmmd.d.ts → types-CIlTojhq.d.ts} +2 -2
- package/dist/{types-QCuYJh4q.d.ts → types-CUcFZfpU.d.ts} +1 -1
- package/dist/{types-C97IskBw.d.ts → types-CqrjB9o6.d.ts} +1 -1
- package/dist/{types-BnIEz1sU.d.ts → types-DHrmBJLH.d.ts} +24 -6
- package/dist/{types-Dnemwd77.d.ts → types-DpWfQdSh.d.ts} +5 -1
- package/dist/{types-Bvc4oWT6.d.ts → types-DvupZE0w.d.ts} +1 -1
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/{usage-GTixvDDU.js → usage-4G6t643F.js} +2 -2
- package/dist/{usage-ItS7nLcl.js → usage-CG9nOYZw.js} +7 -7
- package/dist/usage-iFLJ0-rr.js +2 -0
- package/dist/{validation-error-Ba264OaJ.d.ts → validation-error-BSSw0l_8.d.ts} +1 -1
- package/dist/{validation-error-CnjZ4t92.js → validation-error-ZfUHbKvf.js} +1 -1
- package/dist/validity-dVrUEPJt.js +62 -0
- package/dist/{verifier-BxBo6I44.js → verifier-DMr_Zmb_.js} +51 -15
- package/dist/{rls-verify-CVQTHTh5.js → verify-BztUB3GX.js} +668 -239
- package/dist/{version-BvacVTZR.js → version-BEogC-rB.js} +1 -1
- package/dist/{views-Soe7bGSC.js → views-CJsrBS1f.js} +23 -1
- package/dist/{vocabulary-DA8a8zif.d.ts → vocabulary-hhEFLFoL.d.ts} +15 -6
- package/dist/vue/index.d.ts +24 -19
- package/dist/vue/index.js +24 -8
- package/dist/watch-C8TpsAl4.js +2 -0
- package/dist/{watch-DVBJ9cBq.js → watch-zeucB7eF.js} +6 -6
- package/dist/webmcp/index.d.ts +2 -2
- package/dist/webmcp/index.js +13 -37
- package/dist/wrap-Cr93qqdP.d.ts +14 -0
- package/dist/wrap-DWsU_5Bd.js +26 -0
- package/package.json +8 -8
- package/schemas/catalog-v1.json +6 -0
- package/skills/permdock-credentials/SKILL.md +1 -1
- package/skills/permdock-credentials/references/tokens-keys-links.md +7 -2
- package/skills/permdock-data/references/queries-and-rls.md +4 -0
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +1 -0
- package/skills/permdock-wire/SKILL.md +2 -2
- package/skills/permdock-wire/references/adapters.md +9 -1
- package/skills/permdock-wire/references/policy-options.md +10 -1
- package/dist/catalog-CRW5Mx_B.js +0 -2
- package/dist/collect-DSWOk19R.js +0 -2
- package/dist/create-CMwIl-w2.js +0 -253
- package/dist/delegation-D8pUEuce.js +0 -103
- package/dist/doctor-collect-CHk6XWOA.js +0 -665
- package/dist/doctor-igIiM_Hl.js +0 -1194
- package/dist/evaluation-items-CfCCejQd.js +0 -40
- package/dist/fields-BMl1RxoX.js +0 -26
- package/dist/grantee-Cv5Sogfv.js +0 -602
- package/dist/hosted-DJQw46hn.js +0 -266
- package/dist/instance-options-Dhhcnm3y.js +0 -22
- package/dist/load-DwJUafYN.js +0 -2
- package/dist/map-AcxK9o_D.js +0 -147
- package/dist/permissions-DVoF6dMt.js +0 -662
- package/dist/powersync-e_AZrKbg.js +0 -2
- package/dist/protected-xAr24FEC.d.ts +0 -40
- package/dist/relations-cjOL01hZ.js +0 -505
- package/dist/rls-graph-CHwx0V8q.js +0 -435
- package/dist/rls-shared-Cewd_-zI.js +0 -495
- package/dist/store-jOBYpiXC.d.ts +0 -36
- package/dist/stores-C0IMQrGI.d.ts +0 -52
- package/dist/supabase-config-BpPXt3x1.js +0 -472
- package/dist/supabase-hook-6sYLXg_u.js +0 -2
- package/dist/thenable-okc2xDsj.js +0 -10
- package/dist/types-CKNSyIf_.d.ts +0 -52
- package/dist/usage-DorAJiU_.js +0 -2
- package/dist/watch-BwxLTYMZ.js +0 -2
|
@@ -1,41 +1,652 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as hasConditionOp } from "./refs-
|
|
3
|
-
import {
|
|
4
|
-
import { d as scopeList } from "./scopes-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { r as isRecord } from "./compact-CAf557ZB.js";
|
|
2
|
+
import { a as hasConditionOp } from "./refs-f3imMjNW.js";
|
|
3
|
+
import { f as memoryRelations, g as memoryRoleSource } from "./src-Bb7no-hS.js";
|
|
4
|
+
import { d as scopeList } from "./scopes-tvpa34ep.js";
|
|
5
|
+
import { c as listPermissions, n as findPermission } from "./permission-registry-BsblpB-z.js";
|
|
6
|
+
import { a as expandRelation, c as isEdgeRelation, d as isSelfParented, l as isFieldRelation, o as groupEntries, p as restrictedFor, u as isPrincipalRelation } from "./resource-relations-D1Un3pcz.js";
|
|
7
|
+
import { o as flattenGrantee } from "./grantee-CXY-0C87.js";
|
|
8
|
+
import { r as relationCondition, t as inheritCondition } from "./grantee-conditions-MQs5NIuX.js";
|
|
9
|
+
import { g as customRoleScope, t as createPermDock, v as membershipsClaim } from "./permdock-L26udHSk.js";
|
|
7
10
|
import { i as describeError, o as usageResult } from "./errors-CWfoNg9N.js";
|
|
8
11
|
import { t as policyRowConditionKeys } from "./row-conditions-h2HythXQ.js";
|
|
9
|
-
import { o as jsonSchemaOf } from "./catalog-doc-
|
|
10
|
-
import {
|
|
12
|
+
import { o as jsonSchemaOf } from "./catalog-doc-DWtg0gGR.js";
|
|
13
|
+
import { o as loadPolicyAt } from "./load-C3_oTHyO.js";
|
|
14
|
+
import { c as relationArmSql, u as restrictedRowsSql } from "./compile-CcEu2ZQ1.js";
|
|
11
15
|
import "./budget-NbEtbJC_.js";
|
|
12
|
-
import "./
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import { r as qualifySqlTable } from "./sql-t8teWOfo.js";
|
|
17
|
+
import "./sources-B1-b-R0t.js";
|
|
18
|
+
import { n as fixtureSubject, r as loadFixtures, t as fixtureRow } from "./fixtures-h2eG2OZV.js";
|
|
19
|
+
import { F as quoteQualified, I as quoteTable, M as qualified, N as quoteIdent, P as quoteLiteral, T as columnTypesOf, a as customRolesOn, j as helperSchema, r as HELPERS } from "./shared-1uhI-e-b.js";
|
|
20
|
+
import { I as linkHelper, J as activeUserSql, M as graphHelper, N as graphSqlName, P as graphSqlText, W as restrictedHelper, Z as pgConnector, j as CLOSURE } from "./permission-keys-B26jeyUv.js";
|
|
21
|
+
import { p as commandFor, s as viewName, t as FIELD_VIEWS } from "./fields-fwfXN01n.js";
|
|
18
22
|
import { randomUUID } from "node:crypto";
|
|
19
|
-
//#region src/cli/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
//#region src/cli/sql/helper-calls.ts
|
|
24
|
+
/**
|
|
25
|
+
* Tables whose policies other packages write by calling the SQL helpers
|
|
26
|
+
* directly (storage buckets and realtime topics).
|
|
27
|
+
*/
|
|
28
|
+
const HELPER_TABLES = ["storage.objects", "realtime.messages"];
|
|
29
|
+
const HELPER_CALL = new RegExp(String.raw`\b(${HELPERS.has}(?:_permission)?|permitted_[a-z][a-z0-9_]*_ids(?:_by_permission)?)\s*\(\s*'((?:[^']|'')+)'(\s*,\s*(false\b)?)?`, "giu");
|
|
30
|
+
const ANY_HELPER = new RegExp(String.raw`\b(?:${HELPERS.has}(?:_permission)?(?:_for)?|permitted_[a-z][a-z0-9_]*_ids(?:_by_permission)?(?:_for)?|member_[a-z][a-z0-9_]*_ids(?:_for)?)\s*\(`, "iu");
|
|
31
|
+
/** Whether a policy expression calls any generated helper, the key-less `member_<scope>_ids` included. */
|
|
32
|
+
function callsHelper(sql) {
|
|
33
|
+
return ANY_HELPER.test(sql);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The permission keys a policy expression passes to `permdock_has`,
|
|
37
|
+
* `permitted_<scope>_ids` or their permission-key forms, `#n` stripped. With
|
|
38
|
+
* `blind`, only the calls that ignore row conditions: the grant-key forms,
|
|
39
|
+
* and a permission-key form asked for conditioned allows. A plain
|
|
40
|
+
* `permdock_has_permission(key)` or `permitted_<scope>_ids_by_permission(key)`
|
|
41
|
+
* counts only allows without a row condition, so it never grants more than
|
|
42
|
+
* the application does.
|
|
43
|
+
*/
|
|
44
|
+
function helperCallKeys(sql, blind = false) {
|
|
45
|
+
const keys = /* @__PURE__ */ new Set();
|
|
46
|
+
for (const [, name = "", literal = "", extra, falsy] of sql.matchAll(HELPER_CALL)) {
|
|
47
|
+
const byPermission = /_permission$/iu.test(name);
|
|
48
|
+
if (blind && byPermission && (extra === void 0 || falsy !== void 0)) continue;
|
|
49
|
+
const key = literal.replaceAll("''", "'").split("#")[0] ?? "";
|
|
50
|
+
if (key !== "") keys.add(key);
|
|
51
|
+
}
|
|
52
|
+
return [...keys];
|
|
53
|
+
}
|
|
54
|
+
const CREATE_POLICY = /\bcreate\s+policy\s+("(?:[^"]|"")+"|\w+)\s+on\s+((?:"[^"]+"|\w+)\.(?:"[^"]+"|\w+))([\s\S]*?);/giu;
|
|
55
|
+
function tableName(name) {
|
|
56
|
+
return name.replaceAll("\"", "").toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
function helperTable(name) {
|
|
59
|
+
const table = tableName(name);
|
|
60
|
+
return HELPER_TABLES.find((candidate) => candidate === table);
|
|
61
|
+
}
|
|
62
|
+
/** `create policy` statements on {@link HELPER_TABLES} in migration SQL, with the keys their condition-blind helper calls pass. */
|
|
63
|
+
function helperTablePolicies(sql) {
|
|
64
|
+
const text = sql.replaceAll(/--[^\n]*/gu, "").replaceAll(/\/\*[\s\S]*?\*\//gu, "");
|
|
65
|
+
const found = [];
|
|
66
|
+
for (const [, name = "", target = "", body = ""] of text.matchAll(CREATE_POLICY)) {
|
|
67
|
+
const table = helperTable(target);
|
|
68
|
+
const keys = helperCallKeys(body, true);
|
|
69
|
+
if (table !== void 0 && keys.length > 0) found.push({
|
|
70
|
+
table,
|
|
71
|
+
name: name.replaceAll("\"", ""),
|
|
72
|
+
keys
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return found;
|
|
76
|
+
}
|
|
77
|
+
/** `pg_policies` rows on {@link HELPER_TABLES}, read by `rls verify --db`. */
|
|
78
|
+
const HELPER_TABLE_POLICIES_SQL = `select schemaname || '.' || tablename as target, policyname, coalesce(qual, '') || ' ' || coalesce(with_check, '') as body
|
|
79
|
+
from pg_policies
|
|
80
|
+
where (schemaname, tablename) in (('storage', 'objects'), ('realtime', 'messages'))`;
|
|
81
|
+
/** The same rows as {@link helperTablePolicies}, from `pg_policies`. */
|
|
82
|
+
function helperTablePoliciesFromRows(rows) {
|
|
83
|
+
const found = [];
|
|
84
|
+
for (const row of rows) {
|
|
85
|
+
const table = helperTable(String(row["target"] ?? ""));
|
|
86
|
+
const keys = helperCallKeys(String(row["body"] ?? ""), true);
|
|
87
|
+
if (table !== void 0 && keys.length > 0) found.push({
|
|
88
|
+
table,
|
|
89
|
+
name: String(row["policyname"] ?? ""),
|
|
90
|
+
keys
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return found;
|
|
23
94
|
}
|
|
24
|
-
|
|
95
|
+
/** The message PD037 and `rls verify --db` report for one policy. */
|
|
96
|
+
function rowConditionMessage(policy, keys) {
|
|
97
|
+
return `${policy.table} policy '${policy.name}' calls the SQL helpers with ${keys.join(", ")}, whose grants carry row conditions the helpers do not check: the policy grants more than the application does`;
|
|
98
|
+
}
|
|
99
|
+
const ROW_CONDITION_FIX = "call permitted_<scope>_ids_by_permission('<key>') or permdock_has_permission('<key>'), which count only the allows without a row condition (a relationship grant never reaches a topic or a folder), or use a permission whose catalog entry has rowConditions: false";
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/cli/rls/verify-pgtap.ts
|
|
102
|
+
/** A custom role's scope and pinned instance, as the `custom_role_*` tables store them. */
|
|
103
|
+
function customRoleAt(role, scopes) {
|
|
104
|
+
return {
|
|
105
|
+
scope: customRoleScope(role, scopes),
|
|
106
|
+
id: role.scope === void 0 ? role.team : role.id
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function columnsOf(row) {
|
|
110
|
+
return isRecord(row) ? Object.entries(row).filter(([, value]) => value !== void 0 && typeof value !== "object") : [];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The statement a fixture runs: `create` inserts the whole row, `update` writes
|
|
114
|
+
* `newRow` when given. Each reads back only the key, so `--revoke-columns`
|
|
115
|
+
* (which keeps only unrestricted columns readable) does not reject it.
|
|
116
|
+
*/
|
|
117
|
+
function statementFor(fixture, action, table) {
|
|
118
|
+
const quoted = quoteTable(table);
|
|
119
|
+
const id = quoteIdent("id");
|
|
120
|
+
const key = rowId(fixture.row);
|
|
121
|
+
switch (action) {
|
|
122
|
+
case "read":
|
|
123
|
+
case "list":
|
|
124
|
+
case "get": return {
|
|
125
|
+
sql: `select ${id} from ${quoted} where ${id} = $1`,
|
|
126
|
+
values: [key]
|
|
127
|
+
};
|
|
128
|
+
case "update": {
|
|
129
|
+
const next = columnsOf(fixture.newRow).filter(([name]) => name !== "id");
|
|
130
|
+
return {
|
|
131
|
+
sql: `update ${quoted} set ${next.length === 0 ? `${id} = ${id}` : next.map(([name], index) => `${quoteIdent(name)} = $${index + 2}`).join(", ")} where ${id} = $1 returning ${id}`,
|
|
132
|
+
values: [key, ...next.map(([, value]) => value)]
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
case "create": {
|
|
136
|
+
const cols = columnsOf(fixture.row);
|
|
137
|
+
if (cols.length === 0) return {
|
|
138
|
+
sql: `insert into ${quoted} (${id}) values ($1) returning ${id}`,
|
|
139
|
+
values: [key]
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
sql: `insert into ${quoted} (${cols.map(([name]) => quoteIdent(name)).join(", ")}) values (${cols.map((_, index) => `$${index + 1}`).join(", ")}) returning ${id}`,
|
|
143
|
+
values: cols.map(([, value]) => value)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
case "delete": return {
|
|
147
|
+
sql: `delete from ${quoted} where ${id} = $1 returning ${id}`,
|
|
148
|
+
values: [key]
|
|
149
|
+
};
|
|
150
|
+
default: return {
|
|
151
|
+
sql: `select ${id} from ${quoted} where ${id} = $1`,
|
|
152
|
+
values: [key]
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function rowId(row) {
|
|
157
|
+
if (isRecord(row) && "id" in row) return row["id"];
|
|
158
|
+
}
|
|
159
|
+
/** A fixture value as a SQL literal; untyped, so it coerces to the column like a bound parameter. */
|
|
160
|
+
function sqlValue(value) {
|
|
161
|
+
if (value === null || value === void 0) return "null";
|
|
162
|
+
return quoteLiteral(typeof value === "string" ? value : JSON.stringify(value));
|
|
163
|
+
}
|
|
164
|
+
function inlineStatement(statement) {
|
|
165
|
+
return statement.sql.replaceAll(/\$(\d+)/gu, (_, index) => sqlValue(statement.values[Number(index) - 1]));
|
|
166
|
+
}
|
|
167
|
+
const PGTAP_DECISION = "pg_temp.permdock_decision";
|
|
168
|
+
const PGTAP_DECISION_SQL = `create function ${PGTAP_DECISION}(statement text) returns text
|
|
169
|
+
language plpgsql
|
|
170
|
+
as $$
|
|
171
|
+
declare
|
|
172
|
+
affected bigint;
|
|
173
|
+
begin
|
|
174
|
+
execute statement;
|
|
175
|
+
get diagnostics affected = row_count;
|
|
176
|
+
return case when affected > 0 then 'granted' else 'denied' end;
|
|
177
|
+
exception
|
|
178
|
+
when others then
|
|
179
|
+
return 'denied';
|
|
180
|
+
end;
|
|
181
|
+
$$;`;
|
|
182
|
+
function subjectOf(config, customRoles, scopes) {
|
|
183
|
+
return {
|
|
184
|
+
dialect: config.rls?.dialect ?? "supabase",
|
|
185
|
+
gucPrefix: config.rls?.gucPrefix ?? "app",
|
|
186
|
+
tenantClaim: config.rls?.tenantClaim ?? "tenant_id",
|
|
187
|
+
roleClaim: config.rls?.roleClaim ?? "user_role",
|
|
188
|
+
customRoles,
|
|
189
|
+
scopes
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** The settings that bind a fixture's subject: the GUCs, or the Supabase JWT claims. */
|
|
193
|
+
function subjectSettings(fixture, subject) {
|
|
194
|
+
const roles = fixture.subject.roles ?? [];
|
|
195
|
+
const memberships = membershipsClaim(fixture.subject.memberships ?? [], subject.customRoles, subject.scopes);
|
|
196
|
+
if (subject.dialect === "guc") {
|
|
197
|
+
const settings = [
|
|
198
|
+
[`${subject.gucPrefix}.user_id`, fixture.subject.id],
|
|
199
|
+
[`${subject.gucPrefix}.${subject.roleClaim}`, roles.join(",")],
|
|
200
|
+
[`${subject.gucPrefix}.memberships`, JSON.stringify(memberships)]
|
|
201
|
+
];
|
|
202
|
+
if (fixture.subject.tenant !== void 0) settings.push([`${subject.gucPrefix}.${subject.tenantClaim}`, fixture.subject.tenant]);
|
|
203
|
+
return settings;
|
|
204
|
+
}
|
|
205
|
+
const claims = {
|
|
206
|
+
...fixture.subject.claims,
|
|
207
|
+
sub: fixture.subject.id,
|
|
208
|
+
role: "authenticated",
|
|
209
|
+
[subject.roleClaim]: roles.length === 1 ? roles[0] : roles,
|
|
210
|
+
[subject.tenantClaim]: fixture.subject.tenant,
|
|
211
|
+
memberships
|
|
212
|
+
};
|
|
213
|
+
return [["request.jwt.claims", JSON.stringify(claims)]];
|
|
214
|
+
}
|
|
215
|
+
/** The `custom_role_*` rows for the fixtures' custom roles; existing rows are kept. */
|
|
216
|
+
function customRoleRows(schema, customRoles, scopes) {
|
|
217
|
+
const table = (name) => `${quoteIdent(schema)}.${quoteIdent(name)}`;
|
|
218
|
+
const rows = [];
|
|
219
|
+
for (const role of customRoles) {
|
|
220
|
+
const at = customRoleAt(role, scopes);
|
|
221
|
+
if (at.scope === void 0) continue;
|
|
222
|
+
for (const grant of role.grants ?? []) rows.push({
|
|
223
|
+
sql: grant.level === void 0 ? `insert into ${table("custom_role_permissions")} (tenant_id, scope, scope_id, role, permission, effect) values ($1, $2, $3, $4, $5, $6) on conflict do nothing` : `insert into ${table("custom_role_permissions")} (tenant_id, scope, scope_id, role, permission, effect, level) values ($1, $2, $3, $4, $5, $6, $7) on conflict do nothing`,
|
|
224
|
+
values: [
|
|
225
|
+
role.tenant ?? null,
|
|
226
|
+
at.scope,
|
|
227
|
+
at.id ?? null,
|
|
228
|
+
role.name,
|
|
229
|
+
grant.permission,
|
|
230
|
+
grant.effect ?? "allow",
|
|
231
|
+
...grant.level === void 0 ? [] : [grant.level]
|
|
232
|
+
]
|
|
233
|
+
});
|
|
234
|
+
for (const name of role.includes ?? []) rows.push({
|
|
235
|
+
sql: `insert into ${table("custom_role_includes")} (tenant_id, scope, scope_id, role, include_role) values ($1, $2, $3, $4, $5) on conflict do nothing`,
|
|
236
|
+
values: [
|
|
237
|
+
role.tenant ?? null,
|
|
238
|
+
at.scope,
|
|
239
|
+
at.id ?? null,
|
|
240
|
+
role.name,
|
|
241
|
+
name
|
|
242
|
+
]
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return rows;
|
|
246
|
+
}
|
|
247
|
+
function seedsCustomRoleTables(config) {
|
|
248
|
+
const rls = config.rls;
|
|
249
|
+
return customRolesOn(rls) && (rls?.authorize ?? rls?.rbac?.authorize ?? "jwt") === "database";
|
|
250
|
+
}
|
|
251
|
+
function customRoleSchema(config) {
|
|
252
|
+
return config.rls?.schema ?? config.rls?.rbac?.schema ?? "permdock";
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* A pgTAP script that runs each fixture's statement under the fixture's
|
|
256
|
+
* subject and asserts the outcome `can()` gave in-process.
|
|
257
|
+
*/
|
|
258
|
+
function emitPgtap(input) {
|
|
259
|
+
const scopes = scopeList(input.policy.scopes);
|
|
260
|
+
const subject = subjectOf(input.config, input.customRoles, scopes);
|
|
261
|
+
const lines = [
|
|
262
|
+
"begin;",
|
|
263
|
+
`select plan(${String(input.fixtures.length)});`,
|
|
264
|
+
PGTAP_DECISION_SQL
|
|
265
|
+
];
|
|
266
|
+
if (seedsCustomRoleTables(input.config)) for (const row of customRoleRows(customRoleSchema(input.config), input.customRoles, scopes)) lines.push(`${inlineStatement(row)};`);
|
|
267
|
+
for (const [index, fixture] of input.fixtures.entries()) {
|
|
268
|
+
const permission = findPermission(input.policy.permissions, fixture.action);
|
|
269
|
+
const status = input.inProcess[index];
|
|
270
|
+
const label = `fixture ${String(index)} ${fixture.action}`;
|
|
271
|
+
if (permission === void 0 || status === void 0) continue;
|
|
272
|
+
if (status.kind === "opaque") {
|
|
273
|
+
lines.push(`select skip(${quoteLiteral(`${label}: opaque grant untestable app-side`)}, 1);`);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const outcome = status.granted ? "granted" : "denied";
|
|
277
|
+
const table = input.config.rls?.tables?.[permission.resource] ?? permission.resource;
|
|
278
|
+
const statement = inlineStatement(statementFor(fixture, permission.action, table));
|
|
279
|
+
lines.push(`-- ${label}`, "savepoint permdock_fixture;", "set local role \"authenticated\";", ...subjectSettings(fixture, subject).map(([name, value]) => `select set_config(${quoteLiteral(name)}, ${quoteLiteral(value)}, true);`), `select is(${PGTAP_DECISION}(${quoteLiteral(statement)}), ${quoteLiteral(outcome)}, ${quoteLiteral(`${label} ${outcome}`)});`, "rollback to savepoint permdock_fixture;");
|
|
280
|
+
}
|
|
281
|
+
lines.push("select * from finish();", "rollback;");
|
|
282
|
+
return `${lines.join("\n")}\n`;
|
|
283
|
+
}
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/cli/rls/graph.ts
|
|
286
|
+
function tableFor(resource, tables) {
|
|
287
|
+
return tables?.[resource] ?? resource;
|
|
288
|
+
}
|
|
289
|
+
/** Every `related` condition the policy's grants compile to, with the resource graph they need, groups included. */
|
|
290
|
+
function graphPlan(policy) {
|
|
291
|
+
const scopes = scopeList(policy.scopes);
|
|
292
|
+
const plan = /* @__PURE__ */ new Map();
|
|
293
|
+
const entryFor = (node) => {
|
|
294
|
+
const entry = plan.get(node.name) ?? {
|
|
295
|
+
node,
|
|
296
|
+
relations: /* @__PURE__ */ new Set(),
|
|
297
|
+
links: /* @__PURE__ */ new Set()
|
|
298
|
+
};
|
|
299
|
+
plan.set(node.name, entry);
|
|
300
|
+
return entry;
|
|
301
|
+
};
|
|
302
|
+
const addRelation = (node, relation) => {
|
|
303
|
+
const entry = entryFor(node);
|
|
304
|
+
if (entry.relations.has(relation)) return;
|
|
305
|
+
entry.relations.add(relation);
|
|
306
|
+
for (const name of expandRelation(node, relation)) {
|
|
307
|
+
const spec = node.relations[name];
|
|
308
|
+
if (!isEdgeRelation(spec) || spec.groups === void 0) continue;
|
|
309
|
+
for (const { resource, relation: groupRelation } of groupEntries(spec)) {
|
|
310
|
+
const target = policy.resources.get(resource);
|
|
311
|
+
if (target !== void 0 && target.name !== node.name) addRelation(target, groupRelation);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const addLinks = (hops) => {
|
|
316
|
+
for (let index = 1; index < hops.length; index += 1) {
|
|
317
|
+
const from = policy.resources.get(hops[index - 1]?.resource ?? "");
|
|
318
|
+
const hop = hops[index];
|
|
319
|
+
if (from !== void 0 && hop !== void 0) entryFor(from).links.add(hop.link);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
const addAbove = (above) => {
|
|
323
|
+
const node = above === void 0 ? void 0 : policy.resources.get(above.resource);
|
|
324
|
+
if (above === void 0 || node === void 0) return;
|
|
325
|
+
const entry = entryFor(node);
|
|
326
|
+
entry.closure = Math.max(entry.closure ?? 0, above.depth);
|
|
327
|
+
entry.restrictedAncestors = above;
|
|
328
|
+
};
|
|
329
|
+
for (const grant of policy.grants) {
|
|
330
|
+
const row = policy.resources.get(grant.permission.resource);
|
|
331
|
+
for (const item of flattenGrantee(grant.to)) {
|
|
332
|
+
if (item.kind === "inherit") {
|
|
333
|
+
const inherited = inheritCondition(item, row, policy.resources);
|
|
334
|
+
addLinks(inherited?.hops ?? []);
|
|
335
|
+
addAbove(inherited?.restrictedAncestors);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (item.kind !== "relation") continue;
|
|
339
|
+
const condition = relationCondition(item, row, scopes, { resources: policy.resources });
|
|
340
|
+
if (condition?.op !== "related") continue;
|
|
341
|
+
const node = policy.resources.get(condition.resource);
|
|
342
|
+
if (node === void 0) continue;
|
|
343
|
+
addRelation(node, condition.relation);
|
|
344
|
+
const entry = entryFor(node);
|
|
345
|
+
if (condition.depth > 0 && isSelfParented(node)) entry.closure = Math.max(entry.closure ?? 0, condition.depth);
|
|
346
|
+
addLinks(condition.hops ?? []);
|
|
347
|
+
addAbove(condition.restrictedAncestors);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return plan;
|
|
351
|
+
}
|
|
352
|
+
/** The closure depth per walked resource, as `RlsSqlContext.graph` carries it. */
|
|
353
|
+
function closureDepths(plan) {
|
|
354
|
+
const out = {};
|
|
355
|
+
for (const [name, entry] of plan) if (entry.closure !== void 0) out[name] = entry.closure;
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* The `permitted_<resource>_ids(relation)` arm for one concrete relation:
|
|
360
|
+
* ids of `resource` the subject holds it on, directly or through a group.
|
|
361
|
+
* `asked` are the relation names that include it, so a call for any of them
|
|
362
|
+
* (or for all, with null) reads this arm.
|
|
363
|
+
*/
|
|
364
|
+
function relationArm(ctx, plan, node, relation, asked, tables) {
|
|
365
|
+
const spec = node.relations[relation];
|
|
366
|
+
if (!isEdgeRelation(spec) && !isPrincipalRelation(spec) && (!isFieldRelation(spec) || spec.memberOf !== void 0)) throw new Error(`PermDock CLI: relation '${relation}' on ${node.name} is not a graph relation RLS can compile`);
|
|
367
|
+
const guard = asked.length === 1 ? `(p_relation is null or p_relation = ${quoteLiteral(asked[0] ?? relation)})` : `(p_relation is null or p_relation in (${asked.map(quoteLiteral).join(", ")}))`;
|
|
368
|
+
return ` select a.id from (${graphSqlText(relationArmSql(node.name, relation, {
|
|
369
|
+
resources: new Map([...plan].map(([name, item]) => [name, item.node])),
|
|
370
|
+
...tables === void 0 ? {} : { tables },
|
|
371
|
+
qualify: qualifySqlTable,
|
|
372
|
+
holders: (resource, groupRelation) => [{ text: `select ${qualified(ctx, graphHelper(resource))}(${quoteLiteral(groupRelation)})` }]
|
|
373
|
+
}), ctx)}) a\n where ${guard}${activeUserSql(ctx).map((part) => ` and ${part}`).join("")}`;
|
|
374
|
+
}
|
|
375
|
+
function permittedSql(ctx, plan, entry, tables) {
|
|
376
|
+
const fn = qualified(ctx, graphHelper(entry.node.name));
|
|
377
|
+
const asked = [...entry.relations].toSorted();
|
|
378
|
+
const arms = [...new Set(asked.flatMap((name) => expandRelation(entry.node, name)))].toSorted().map((relation) => relationArm(ctx, plan, entry.node, relation, asked.filter((name) => expandRelation(entry.node, name).includes(relation)), tables));
|
|
379
|
+
if (arms.length === 0) arms.push(" select null::text where false");
|
|
380
|
+
return `-- ${entry.node.name}: ids the subject holds a relation on (all relations when p_relation is null)
|
|
381
|
+
create or replace function ${fn}(p_relation text)
|
|
382
|
+
returns setof text
|
|
383
|
+
language sql
|
|
384
|
+
stable
|
|
385
|
+
security definer
|
|
386
|
+
set search_path = ''
|
|
387
|
+
as $$
|
|
388
|
+
${arms.join("\n union\n")}
|
|
389
|
+
$$;
|
|
390
|
+
revoke execute on function ${fn}(text) from public, anon;
|
|
391
|
+
grant execute on function ${fn}(text) to authenticated;`;
|
|
392
|
+
}
|
|
393
|
+
/** `permdock_link_<resource>_<link>(ids)`: ids of `resource` whose link points into `ids`, read past the table's own policies. */
|
|
394
|
+
function linkSql(ctx, node, link, tables) {
|
|
395
|
+
const spec = Object.hasOwn(node.links, link) ? node.links[link] : void 0;
|
|
396
|
+
if (spec === void 0) throw new Error(`PermDock CLI: ${node.name} has no link '${link}'`);
|
|
397
|
+
const fn = qualified(ctx, linkHelper(node.name, link));
|
|
398
|
+
return `-- ${node.name}.${link}: rows whose ${spec.field} points at one of the given ${spec.resource} ids
|
|
399
|
+
create or replace function ${fn}(p_ids text[])
|
|
400
|
+
returns setof text
|
|
401
|
+
language sql
|
|
402
|
+
stable
|
|
403
|
+
security definer
|
|
404
|
+
set search_path = ''
|
|
405
|
+
as $$
|
|
406
|
+
select t.${quoteIdent(node.id)}::text from ${quoteQualified(tableFor(node.name, tables))} t
|
|
407
|
+
where t.${quoteIdent(spec.field)}::text = any(p_ids)
|
|
408
|
+
$$;
|
|
409
|
+
revoke execute on function ${fn}(text[]) from public, anon;
|
|
410
|
+
grant execute on function ${fn}(text[]) to authenticated;`;
|
|
411
|
+
}
|
|
412
|
+
function closureTableSql(ctx) {
|
|
413
|
+
const table = qualified(ctx, CLOSURE.table);
|
|
414
|
+
return `-- the object graph: one row per (ancestor, descendant) within each walked resource's depth,
|
|
415
|
+
-- never crossing a restricted row; kept current by statement triggers on each self-parented table
|
|
416
|
+
create table if not exists ${table} (
|
|
417
|
+
resource text not null,
|
|
418
|
+
ancestor text not null,
|
|
419
|
+
descendant text not null,
|
|
420
|
+
depth integer not null check (depth >= 0),
|
|
421
|
+
primary key (resource, descendant, ancestor)
|
|
422
|
+
);
|
|
423
|
+
create index if not exists ${quoteIdent(`${CLOSURE.table}_ancestor`)} on ${table} (resource, ancestor);
|
|
424
|
+
alter table ${table} enable row level security;
|
|
425
|
+
revoke all on table ${table} from anon, authenticated, public;
|
|
426
|
+
grant select on table ${table} to authenticated;`;
|
|
427
|
+
}
|
|
428
|
+
function closureObjectsSql(ctx, entry, depth, tables) {
|
|
429
|
+
const { node } = entry;
|
|
430
|
+
const parent = node.parent;
|
|
431
|
+
if (parent === void 0) return "";
|
|
432
|
+
const name = node.name;
|
|
433
|
+
const sqlName = graphSqlName(name) ?? name;
|
|
434
|
+
const closure = qualified(ctx, CLOSURE.table);
|
|
435
|
+
const table = quoteQualified(tableFor(name, tables));
|
|
436
|
+
const id = quoteIdent(node.id);
|
|
437
|
+
const up = quoteIdent(parent.field);
|
|
438
|
+
const restricted = restrictedFor(node, "parent");
|
|
439
|
+
const stop = restricted === void 0 ? "false" : `coalesce(t.${quoteIdent(restricted)}, false)`;
|
|
440
|
+
const stopParent = restricted === void 0 ? "false" : `coalesce(p.${quoteIdent(restricted)}, false)`;
|
|
441
|
+
const refresh = qualified(ctx, `${CLOSURE.table}_${sqlName}`);
|
|
442
|
+
const changed = qualified(ctx, `${CLOSURE.table}_${sqlName}_changed`);
|
|
443
|
+
const literal = quoteLiteral(name);
|
|
444
|
+
const restrictedChanged = restricted === void 0 ? "" : ` or n.${quoteIdent(restricted)} is distinct from o.${quoteIdent(restricted)}`;
|
|
445
|
+
const trigger = (suffix) => quoteIdent(`${CLOSURE.table}_${sqlName}_${suffix}`);
|
|
446
|
+
return `-- ${name}: ${restricted === void 0 ? `rows reach their ancestors up to depth ${String(depth)}` : `rows reach their ancestors up to depth ${String(depth)}, stopping at a restricted row`}
|
|
447
|
+
drop policy if exists ${quoteIdent(`${CLOSURE.table}_${sqlName}`)} on ${closure};
|
|
448
|
+
create policy ${quoteIdent(`${CLOSURE.table}_${sqlName}`)} on ${closure}
|
|
449
|
+
for select to authenticated
|
|
450
|
+
using (resource = ${literal} and ancestor in (select ${qualified(ctx, graphHelper(name))}(null)));
|
|
451
|
+
create or replace function ${refresh}(p_ids text[])
|
|
452
|
+
returns void
|
|
453
|
+
language plpgsql
|
|
454
|
+
security definer
|
|
455
|
+
set search_path = ''
|
|
456
|
+
as $$
|
|
457
|
+
declare
|
|
458
|
+
v_cycle text;
|
|
459
|
+
v_affected text[];
|
|
460
|
+
begin
|
|
461
|
+
with recursive walk(start, next, path) as (
|
|
462
|
+
select t.${id}, t.${up}, array[t.${id}::text]
|
|
463
|
+
from ${table} t
|
|
464
|
+
where t.${id}::text = any(p_ids)
|
|
465
|
+
union all
|
|
466
|
+
select walk.start, t.${up}, walk.path || t.${id}::text
|
|
467
|
+
from walk join ${table} t on t.${id} = walk.next
|
|
468
|
+
where not (walk.next::text = any(walk.path))
|
|
469
|
+
)
|
|
470
|
+
select walk.start::text into v_cycle
|
|
471
|
+
from walk
|
|
472
|
+
where walk.next::text = any(walk.path)
|
|
473
|
+
limit 1;
|
|
474
|
+
if v_cycle is not null then
|
|
475
|
+
raise exception 'permdock: % % is its own ancestor', ${literal}, v_cycle
|
|
476
|
+
using errcode = 'check_violation';
|
|
477
|
+
end if;
|
|
478
|
+
with recursive down(id, level) as (
|
|
479
|
+
select t.${id}, 0
|
|
480
|
+
from ${table} t
|
|
481
|
+
where t.${id}::text = any(p_ids) or t.${up}::text = any(p_ids)
|
|
482
|
+
union
|
|
483
|
+
select t.${id}, down.level + 1
|
|
484
|
+
from down join ${table} t on t.${up} = down.id
|
|
485
|
+
where down.level < ${String(depth)}
|
|
486
|
+
)
|
|
487
|
+
select coalesce(array_agg(distinct down.id::text), '{}') into v_affected from down;
|
|
488
|
+
delete from ${closure} c
|
|
489
|
+
where c.resource = ${literal}
|
|
490
|
+
and (c.descendant = any(p_ids) or c.descendant = any(v_affected));
|
|
491
|
+
insert into ${closure} (resource, ancestor, descendant, depth)
|
|
492
|
+
with recursive walk(descendant, ancestor, parent, depth, stop) as (
|
|
493
|
+
select t.${id}::text, t.${id}, t.${up}, 0, ${stop}
|
|
494
|
+
from ${table} t
|
|
495
|
+
where t.${id}::text = any(v_affected)
|
|
496
|
+
union all
|
|
497
|
+
select walk.descendant, p.${id}, p.${up}, walk.depth + 1, ${stopParent}
|
|
498
|
+
from walk join ${table} p on p.${id} = walk.parent
|
|
499
|
+
where not walk.stop and walk.depth < ${String(depth)}
|
|
500
|
+
)
|
|
501
|
+
select ${literal}, walk.ancestor::text, walk.descendant, walk.depth from walk
|
|
502
|
+
on conflict do nothing;
|
|
503
|
+
end
|
|
504
|
+
$$;
|
|
505
|
+
revoke execute on function ${refresh}(text[]) from public, anon, authenticated;
|
|
506
|
+
create or replace function ${changed}()
|
|
507
|
+
returns trigger
|
|
508
|
+
language plpgsql
|
|
509
|
+
security definer
|
|
510
|
+
set search_path = ''
|
|
511
|
+
as $$
|
|
512
|
+
begin
|
|
513
|
+
if tg_op = 'TRUNCATE' then
|
|
514
|
+
delete from ${closure} where resource = ${literal};
|
|
515
|
+
elsif tg_op = 'INSERT' then
|
|
516
|
+
perform ${refresh}(array(select n.${id}::text from new_rows n));
|
|
517
|
+
elsif tg_op = 'DELETE' then
|
|
518
|
+
perform ${refresh}(array(select o.${id}::text from old_rows o));
|
|
519
|
+
else
|
|
520
|
+
perform ${refresh}(array(
|
|
521
|
+
select n.${id}::text
|
|
522
|
+
from new_rows n left join old_rows o on o.${id} = n.${id}
|
|
523
|
+
where o.${id} is null or n.${up} is distinct from o.${up}${restrictedChanged}
|
|
524
|
+
union
|
|
525
|
+
select o.${id}::text
|
|
526
|
+
from old_rows o left join new_rows n on n.${id} = o.${id}
|
|
527
|
+
where n.${id} is null
|
|
528
|
+
));
|
|
529
|
+
end if;
|
|
530
|
+
return null;
|
|
531
|
+
end
|
|
532
|
+
$$;
|
|
533
|
+
revoke execute on function ${changed}() from public, anon, authenticated;
|
|
534
|
+
drop trigger if exists ${trigger("insert")} on ${table};
|
|
535
|
+
create trigger ${trigger("insert")} after insert on ${table}
|
|
536
|
+
referencing new table as new_rows
|
|
537
|
+
for each statement execute function ${changed}();
|
|
538
|
+
drop trigger if exists ${trigger("update")} on ${table};
|
|
539
|
+
create trigger ${trigger("update")} after update on ${table}
|
|
540
|
+
referencing old table as old_rows new table as new_rows
|
|
541
|
+
for each statement execute function ${changed}();
|
|
542
|
+
drop trigger if exists ${trigger("delete")} on ${table};
|
|
543
|
+
create trigger ${trigger("delete")} after delete on ${table}
|
|
544
|
+
referencing old table as old_rows
|
|
545
|
+
for each statement execute function ${changed}();
|
|
546
|
+
drop trigger if exists ${trigger("truncate")} on ${table};
|
|
547
|
+
create trigger ${trigger("truncate")} after truncate on ${table}
|
|
548
|
+
for each statement execute function ${changed}();
|
|
549
|
+
select ${refresh}(array(select t.${id}::text from ${table} t));`;
|
|
550
|
+
}
|
|
551
|
+
function restrictedRowsHelperSql(ctx, plan, entry, tables) {
|
|
552
|
+
const above = entry.restrictedAncestors;
|
|
553
|
+
if (above === void 0) return "";
|
|
554
|
+
const fn = qualified(ctx, restrictedHelper(entry.node.name));
|
|
555
|
+
const body = graphSqlText(restrictedRowsSql(above, {
|
|
556
|
+
resources: new Map([...plan].map(([name, item]) => [name, item.node])),
|
|
557
|
+
...tables === void 0 ? {} : { tables },
|
|
558
|
+
qualify: qualifySqlTable,
|
|
559
|
+
closure: `${helperSchema(ctx)}.${CLOSURE.table}`,
|
|
560
|
+
closureDepths: closureDepths(plan)
|
|
561
|
+
}), ctx);
|
|
562
|
+
return `-- ${entry.node.name}: rows a stopped link does not reach below: restricted, under a restricted row within ${String(above.depth - 1)} levels, or deeper
|
|
563
|
+
create or replace function ${fn}()
|
|
564
|
+
returns setof text
|
|
565
|
+
language sql
|
|
566
|
+
stable
|
|
567
|
+
security definer
|
|
568
|
+
set search_path = ''
|
|
569
|
+
as $$
|
|
570
|
+
${body}
|
|
571
|
+
$$;
|
|
572
|
+
revoke execute on function ${fn}() from public, anon;
|
|
573
|
+
grant execute on function ${fn}() to authenticated;`;
|
|
574
|
+
}
|
|
575
|
+
/** Group resources a resource's asked relations name, other than itself. */
|
|
576
|
+
function groupTargets(entry) {
|
|
577
|
+
const out = /* @__PURE__ */ new Set();
|
|
578
|
+
for (const asked of entry.relations) for (const name of expandRelation(entry.node, asked)) {
|
|
579
|
+
const spec = entry.node.relations[name];
|
|
580
|
+
const resources = isEdgeRelation(spec) ? groupEntries(spec).map((group) => group.resource) : [];
|
|
581
|
+
for (const resource of resources) if (resource !== entry.node.name) out.add(resource);
|
|
582
|
+
}
|
|
583
|
+
return [...out].toSorted();
|
|
584
|
+
}
|
|
585
|
+
/** Plan entries by name, each after the group resources its helper calls (SQL function bodies are checked on create). */
|
|
586
|
+
function dependencyOrder(plan) {
|
|
587
|
+
const out = [];
|
|
588
|
+
const done = /* @__PURE__ */ new Set();
|
|
589
|
+
const visit = (name) => {
|
|
590
|
+
const entry = plan.get(name);
|
|
591
|
+
if (entry === void 0 || done.has(name)) return;
|
|
592
|
+
done.add(name);
|
|
593
|
+
for (const target of groupTargets(entry)) visit(target);
|
|
594
|
+
out.push(entry);
|
|
595
|
+
};
|
|
596
|
+
for (const name of [...plan.keys()].toSorted()) visit(name);
|
|
597
|
+
return out;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* The graph preamble: `permitted_<resource>_ids` for every resource a graph
|
|
601
|
+
* grant reads, and the closure table with its triggers for every
|
|
602
|
+
* self-parented resource a grant walks. Empty when no grant is a graph grant.
|
|
603
|
+
*/
|
|
604
|
+
function graphSql(ctx, plan, tables) {
|
|
605
|
+
if (plan.size === 0) return "";
|
|
606
|
+
const sqlNames = /* @__PURE__ */ new Map();
|
|
607
|
+
for (const name of plan.keys()) {
|
|
608
|
+
const helper = graphHelper(name);
|
|
609
|
+
const sql = graphSqlName(name) ?? name;
|
|
610
|
+
if (ctx.scopes.some((scope) => scope.name === sql)) throw new Error(`PermDock CLI: resource '${name}' has graph relations and shares its SQL name with the ${sql} scope, so ${helper} would replace the scope helper; rename the resource (permdock doctor PD032)`);
|
|
611
|
+
const other = sqlNames.get(sql);
|
|
612
|
+
if (other !== void 0) throw new Error(`PermDock CLI: resources '${other}' and '${name}' both name the SQL helper ${helper}; rename one (permdock doctor PD032)`);
|
|
613
|
+
sqlNames.set(sql, name);
|
|
614
|
+
}
|
|
615
|
+
const entries = dependencyOrder(plan);
|
|
616
|
+
const chunks = entries.flatMap((entry) => [...entry.relations.size === 0 && entry.closure === void 0 ? [] : [permittedSql(ctx, plan, entry, tables)], ...[...entry.links].toSorted().map((link) => linkSql(ctx, entry.node, link, tables))]);
|
|
617
|
+
const walked = entries.filter((entry) => entry.closure !== void 0);
|
|
618
|
+
if (walked.length > 0) {
|
|
619
|
+
chunks.push(closureTableSql(ctx));
|
|
620
|
+
for (const entry of walked) chunks.push(closureObjectsSql(ctx, entry, entry.closure ?? 0, tables));
|
|
621
|
+
for (const entry of walked) {
|
|
622
|
+
const helper = restrictedRowsHelperSql(ctx, plan, entry, tables);
|
|
623
|
+
if (helper !== "") chunks.push(helper);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return `${chunks.join("\n\n")}\n`;
|
|
627
|
+
}
|
|
628
|
+
//#endregion
|
|
629
|
+
//#region src/cli/rls/verify-tree.ts
|
|
630
|
+
const SYSTEM_ENTROPY = Object.freeze({
|
|
631
|
+
now: () => Date.now(),
|
|
632
|
+
randomUUID: () => randomUUID()
|
|
633
|
+
});
|
|
634
|
+
const SUBJECTS = 3;
|
|
635
|
+
function idFactory(node, entropy) {
|
|
25
636
|
const type = columnTypesOf(jsonSchemaOf(node))[node.id];
|
|
26
637
|
let next = 9e5;
|
|
27
638
|
return type === "numeric" ? () => {
|
|
28
639
|
next += 1;
|
|
29
640
|
return next;
|
|
30
|
-
} : () => randomUUID();
|
|
641
|
+
} : () => entropy.randomUUID();
|
|
31
642
|
}
|
|
32
643
|
/**
|
|
33
644
|
* Two spines one level deeper than the walk depth, so the depth cap is
|
|
34
645
|
* crossed; side branches under the first levels alternate restricted, and
|
|
35
646
|
* the second spine is restricted two levels down.
|
|
36
647
|
*/
|
|
37
|
-
function generateTree(node, depth) {
|
|
38
|
-
const id = idFactory(node);
|
|
648
|
+
function generateTree(node, depth, entropy) {
|
|
649
|
+
const id = idFactory(node, entropy);
|
|
39
650
|
const nodes = [];
|
|
40
651
|
for (const spine of [0, 1]) {
|
|
41
652
|
let parent = null;
|
|
@@ -72,7 +683,7 @@ function insertSql(table, rows) {
|
|
|
72
683
|
return `$${String(values.length)}`;
|
|
73
684
|
}).join(", ")})`);
|
|
74
685
|
return {
|
|
75
|
-
sql: `insert into ${
|
|
686
|
+
sql: `insert into ${quoteQualified(table)} (${columns.map(quoteIdent).join(", ")}) values ${tuples.join(", ")} returning *`,
|
|
76
687
|
values
|
|
77
688
|
};
|
|
78
689
|
}
|
|
@@ -119,20 +730,20 @@ function checkedValue(checks) {
|
|
|
119
730
|
}
|
|
120
731
|
}
|
|
121
732
|
}
|
|
122
|
-
function placeholder(column) {
|
|
733
|
+
function placeholder(column, entropy) {
|
|
123
734
|
if (column.enumLabel !== null) return column.enumLabel;
|
|
124
735
|
const checked = checkedValue(column.checks);
|
|
125
736
|
if (checked !== void 0) return checked;
|
|
126
737
|
const type = column.type;
|
|
127
|
-
if (type === "uuid") return randomUUID();
|
|
738
|
+
if (type === "uuid") return entropy.randomUUID();
|
|
128
739
|
if (type === "boolean") return false;
|
|
129
740
|
if (/^(smallint|integer|bigint|numeric|real|double precision)/u.test(type)) return 0;
|
|
130
|
-
if (type.startsWith("timestamp") || type === "date") return
|
|
741
|
+
if (type.startsWith("timestamp") || type === "date") return new Date(entropy.now()).toISOString();
|
|
131
742
|
if (type === "json" || type === "jsonb") return {};
|
|
132
743
|
return "permdock-tree";
|
|
133
744
|
}
|
|
134
|
-
function distinctPlaceholder(column, index) {
|
|
135
|
-
const value = placeholder(column);
|
|
745
|
+
function distinctPlaceholder(column, index, entropy) {
|
|
746
|
+
const value = placeholder(column, entropy);
|
|
136
747
|
if (column.enumLabel !== null) return value;
|
|
137
748
|
if (typeof value === "number") return value + index;
|
|
138
749
|
if (typeof value !== "string" || checkedValue(column.checks) !== void 0) return value;
|
|
@@ -157,7 +768,7 @@ function asRequiredColumn(row) {
|
|
|
157
768
|
* tenant column or a name: an existing value of the referenced table for a
|
|
158
769
|
* foreign key, a placeholder of the column's type otherwise.
|
|
159
770
|
*/
|
|
160
|
-
async function requiredValues(query, table, set) {
|
|
771
|
+
async function requiredValues(query, table, set, entropy) {
|
|
161
772
|
const columns = (await query(REQUIRED_COLUMNS_SQL, [table])).rows.map(asRequiredColumn).filter((column) => column !== void 0 && !set.has(column.name));
|
|
162
773
|
const unique = new Set(columns.length === 0 ? [] : (await query(UNIQUE_COLUMNS_SQL, [table])).rows.map((row) => String(row["name"])));
|
|
163
774
|
const values = {};
|
|
@@ -166,16 +777,16 @@ async function requiredValues(query, table, set) {
|
|
|
166
777
|
const existing = await query(`select ${quoteIdent(column.refColumn)} as value from ${column.refTable} limit 1`);
|
|
167
778
|
values[column.name] = existing.rows[0]?.["value"] ?? null;
|
|
168
779
|
} else if (unique.has(column.name)) distinct.push(column);
|
|
169
|
-
else values[column.name] = placeholder(column);
|
|
780
|
+
else values[column.name] = placeholder(column, entropy);
|
|
170
781
|
return (index) => {
|
|
171
782
|
const row = { ...values };
|
|
172
|
-
for (const column of distinct) row[column.name] = distinctPlaceholder(column, index);
|
|
783
|
+
for (const column of distinct) row[column.name] = distinctPlaceholder(column, index, entropy);
|
|
173
784
|
return row;
|
|
174
785
|
};
|
|
175
786
|
}
|
|
176
787
|
/** Inserts `rows` after filling the required columns they leave out. */
|
|
177
|
-
async function seedRows(query, table, rows, given
|
|
178
|
-
const fill = await requiredValues(query,
|
|
788
|
+
async function seedRows(query, table, rows, given, entropy) {
|
|
789
|
+
const fill = await requiredValues(query, quoteQualified(table), /* @__PURE__ */ new Set([...Object.keys(rows[0] ?? {}), ...Object.keys(given)]), entropy);
|
|
179
790
|
const inserted = insertSql(table, rows.map((row, index) => ({
|
|
180
791
|
...fill(index),
|
|
181
792
|
...given,
|
|
@@ -192,9 +803,10 @@ async function seedRows(query, table, rows, given = {}) {
|
|
|
192
803
|
*/
|
|
193
804
|
async function verifyTree(input) {
|
|
194
805
|
const { policy, query } = input;
|
|
806
|
+
const entropy = input.entropy ?? SYSTEM_ENTROPY;
|
|
195
807
|
const tables = input.config.rls?.tables;
|
|
196
808
|
const treeValues = input.config.rls?.treeValues ?? {};
|
|
197
|
-
const seed = (table, list) => seedRows(query, table, list, treeValues[table] ?? {});
|
|
809
|
+
const seed = (table, list) => seedRows(query, table, list, treeValues[table] ?? {}, entropy);
|
|
198
810
|
const walked = [...graphPlan(policy).values()].filter((entry) => entry.closure !== void 0);
|
|
199
811
|
if (walked.length === 0) return {
|
|
200
812
|
checked: 0,
|
|
@@ -202,15 +814,15 @@ async function verifyTree(input) {
|
|
|
202
814
|
mismatches: [],
|
|
203
815
|
notes: ["no graph grant walks a self-parented resource"]
|
|
204
816
|
};
|
|
205
|
-
const subjects = Array.from({ length: SUBJECTS }, () => randomUUID());
|
|
817
|
+
const subjects = Array.from({ length: SUBJECTS }, () => entropy.randomUUID());
|
|
206
818
|
const rows = {};
|
|
207
819
|
const edges = {};
|
|
208
|
-
const now =
|
|
820
|
+
const now = entropy.now();
|
|
209
821
|
for (const entry of walked) {
|
|
210
822
|
const node = entry.node;
|
|
211
823
|
const parent = node.parent;
|
|
212
824
|
if (parent === void 0) continue;
|
|
213
|
-
const tree = generateTree(node, entry.closure ?? 0);
|
|
825
|
+
const tree = generateTree(node, entry.closure ?? 0, entropy);
|
|
214
826
|
const seeded = tree.map((item, index) => {
|
|
215
827
|
const row = {
|
|
216
828
|
[node.id]: item.id,
|
|
@@ -220,7 +832,7 @@ async function verifyTree(input) {
|
|
|
220
832
|
for (const name of entry.relations) {
|
|
221
833
|
const spec = node.relations[name];
|
|
222
834
|
const column = isFieldRelation(spec) ? spec.memberOf === void 0 ? spec.field : void 0 : isPrincipalRelation(spec) ? spec.principal : void 0;
|
|
223
|
-
if (column !== void 0 && column !== parent.field) row[column] = index % 4 === 0 ? subjects[index % SUBJECTS] : randomUUID();
|
|
835
|
+
if (column !== void 0 && column !== parent.field) row[column] = index % 4 === 0 ? subjects[index % SUBJECTS] : entropy.randomUUID();
|
|
224
836
|
}
|
|
225
837
|
return row;
|
|
226
838
|
});
|
|
@@ -259,7 +871,7 @@ async function verifyTree(input) {
|
|
|
259
871
|
}
|
|
260
872
|
for (const child of policy.resources.values()) {
|
|
261
873
|
if (child.name === node.name || child.parent?.resource !== node.name) continue;
|
|
262
|
-
const make = idFactory(child);
|
|
874
|
+
const make = idFactory(child, entropy);
|
|
263
875
|
const childRows = tree.map((item, index) => {
|
|
264
876
|
const row = {
|
|
265
877
|
[child.id]: make(),
|
|
@@ -293,7 +905,7 @@ async function verifyTree(input) {
|
|
|
293
905
|
const list = rows[permission.resource] ?? [];
|
|
294
906
|
if (node === void 0 || list.length === 0) continue;
|
|
295
907
|
const ids = list.map((row) => String(row[node.id]));
|
|
296
|
-
const visible = await query(`select ${quoteIdent(node.id)}::text as id from ${
|
|
908
|
+
const visible = await query(`select ${quoteIdent(node.id)}::text as id from ${quoteQualified(tables?.[node.name] ?? node.name)} where ${quoteIdent(node.id)} = any($1)`, [ids]);
|
|
297
909
|
const seen = new Set(visible.rows.map((row) => String(row["id"])));
|
|
298
910
|
for (const row of list) {
|
|
299
911
|
checked += 1;
|
|
@@ -312,202 +924,7 @@ async function verifyTree(input) {
|
|
|
312
924
|
};
|
|
313
925
|
}
|
|
314
926
|
//#endregion
|
|
315
|
-
//#region src/cli/rls-
|
|
316
|
-
function isRecord(value) {
|
|
317
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
318
|
-
}
|
|
319
|
-
/** A custom role's scope and pinned instance, as the `custom_role_*` tables store them. */
|
|
320
|
-
function customRoleAt(role, scopes) {
|
|
321
|
-
return {
|
|
322
|
-
scope: customRoleScope(role, scopes),
|
|
323
|
-
id: role.scope === void 0 ? role.team : role.id
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
function canFixture(permdock, permission, row) {
|
|
327
|
-
if (permission.kind === "collection") return permdock.can(permission, row);
|
|
328
|
-
return permdock.can(permission, row);
|
|
329
|
-
}
|
|
330
|
-
function grantKind(policy, key) {
|
|
331
|
-
const grants = policy.grants.filter((grant) => grant.permission.key === key);
|
|
332
|
-
if (grants.some((grant) => hasConditionOp(grant.where, "opaque") || hasConditionOp(grant.check, "opaque"))) return "opaque";
|
|
333
|
-
if (grants.some((grant) => hasConditionOp(grant.where, "sqlFunction") || hasConditionOp(grant.check, "sqlFunction"))) return "sqlFunction";
|
|
334
|
-
return "portable";
|
|
335
|
-
}
|
|
336
|
-
function columnsOf(row) {
|
|
337
|
-
return isRecord(row) ? Object.entries(row).filter(([, value]) => value !== void 0 && typeof value !== "object") : [];
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* The statement a fixture runs: `create` inserts the whole row, `update` writes
|
|
341
|
-
* `newRow` when given. Each reads back only the key, so `--revoke-columns`
|
|
342
|
-
* (which keeps only unrestricted columns readable) does not reject it.
|
|
343
|
-
*/
|
|
344
|
-
function statementFor(fixture, action, table) {
|
|
345
|
-
const quoted = quoteTable(table);
|
|
346
|
-
const id = quoteIdent("id");
|
|
347
|
-
const key = rowId(fixture.row);
|
|
348
|
-
switch (action) {
|
|
349
|
-
case "read":
|
|
350
|
-
case "list":
|
|
351
|
-
case "get": return {
|
|
352
|
-
sql: `select ${id} from ${quoted} where ${id} = $1`,
|
|
353
|
-
values: [key]
|
|
354
|
-
};
|
|
355
|
-
case "update": {
|
|
356
|
-
const next = columnsOf(fixture.newRow).filter(([name]) => name !== "id");
|
|
357
|
-
return {
|
|
358
|
-
sql: `update ${quoted} set ${next.length === 0 ? `${id} = ${id}` : next.map(([name], index) => `${quoteIdent(name)} = $${index + 2}`).join(", ")} where ${id} = $1 returning ${id}`,
|
|
359
|
-
values: [key, ...next.map(([, value]) => value)]
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
case "create": {
|
|
363
|
-
const cols = columnsOf(fixture.row);
|
|
364
|
-
if (cols.length === 0) return {
|
|
365
|
-
sql: `insert into ${quoted} (${id}) values ($1) returning ${id}`,
|
|
366
|
-
values: [key]
|
|
367
|
-
};
|
|
368
|
-
return {
|
|
369
|
-
sql: `insert into ${quoted} (${cols.map(([name]) => quoteIdent(name)).join(", ")}) values (${cols.map((_, index) => `$${index + 1}`).join(", ")}) returning ${id}`,
|
|
370
|
-
values: cols.map(([, value]) => value)
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
case "delete": return {
|
|
374
|
-
sql: `delete from ${quoted} where ${id} = $1 returning ${id}`,
|
|
375
|
-
values: [key]
|
|
376
|
-
};
|
|
377
|
-
default: return {
|
|
378
|
-
sql: `select ${id} from ${quoted} where ${id} = $1`,
|
|
379
|
-
values: [key]
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
function rowId(row) {
|
|
384
|
-
if (isRecord(row) && "id" in row) return row["id"];
|
|
385
|
-
}
|
|
386
|
-
/** A fixture value as a SQL literal; untyped, so it coerces to the column like a bound parameter. */
|
|
387
|
-
function sqlValue(value) {
|
|
388
|
-
if (value === null || value === void 0) return "null";
|
|
389
|
-
return quoteLiteral(typeof value === "string" ? value : JSON.stringify(value));
|
|
390
|
-
}
|
|
391
|
-
function inlineStatement(statement) {
|
|
392
|
-
return statement.sql.replaceAll(/\$(\d+)/gu, (_, index) => sqlValue(statement.values[Number(index) - 1]));
|
|
393
|
-
}
|
|
394
|
-
const PGTAP_DECISION = "pg_temp.permdock_decision";
|
|
395
|
-
const PGTAP_DECISION_SQL = `create function ${PGTAP_DECISION}(statement text) returns text
|
|
396
|
-
language plpgsql
|
|
397
|
-
as $$
|
|
398
|
-
declare
|
|
399
|
-
affected bigint;
|
|
400
|
-
begin
|
|
401
|
-
execute statement;
|
|
402
|
-
get diagnostics affected = row_count;
|
|
403
|
-
return case when affected > 0 then 'granted' else 'denied' end;
|
|
404
|
-
exception
|
|
405
|
-
when others then
|
|
406
|
-
return 'denied';
|
|
407
|
-
end;
|
|
408
|
-
$$;`;
|
|
409
|
-
function subjectOf(config, customRoles, scopes) {
|
|
410
|
-
return {
|
|
411
|
-
dialect: config.rls?.dialect ?? "supabase",
|
|
412
|
-
gucPrefix: config.rls?.gucPrefix ?? "app",
|
|
413
|
-
tenantClaim: config.rls?.tenantClaim ?? "tenant_id",
|
|
414
|
-
roleClaim: config.rls?.roleClaim ?? "user_role",
|
|
415
|
-
customRoles,
|
|
416
|
-
scopes
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
/** The settings that bind a fixture's subject: the GUCs, or the Supabase JWT claims. */
|
|
420
|
-
function subjectSettings(fixture, subject) {
|
|
421
|
-
const roles = fixture.subject.roles ?? [];
|
|
422
|
-
const memberships = membershipsClaim(fixture.subject.memberships ?? [], subject.customRoles, subject.scopes);
|
|
423
|
-
if (subject.dialect === "guc") {
|
|
424
|
-
const settings = [
|
|
425
|
-
[`${subject.gucPrefix}.user_id`, fixture.subject.id],
|
|
426
|
-
[`${subject.gucPrefix}.${subject.roleClaim}`, roles.join(",")],
|
|
427
|
-
[`${subject.gucPrefix}.memberships`, JSON.stringify(memberships)]
|
|
428
|
-
];
|
|
429
|
-
if (fixture.subject.tenant !== void 0) settings.push([`${subject.gucPrefix}.${subject.tenantClaim}`, fixture.subject.tenant]);
|
|
430
|
-
return settings;
|
|
431
|
-
}
|
|
432
|
-
const claims = {
|
|
433
|
-
...fixture.subject.claims,
|
|
434
|
-
sub: fixture.subject.id,
|
|
435
|
-
role: "authenticated",
|
|
436
|
-
[subject.roleClaim]: roles.length === 1 ? roles[0] : roles,
|
|
437
|
-
[subject.tenantClaim]: fixture.subject.tenant,
|
|
438
|
-
memberships
|
|
439
|
-
};
|
|
440
|
-
return [["request.jwt.claims", JSON.stringify(claims)]];
|
|
441
|
-
}
|
|
442
|
-
/** The `custom_role_*` rows for the fixtures' custom roles; existing rows are kept. */
|
|
443
|
-
function customRoleRows(schema, customRoles, scopes) {
|
|
444
|
-
const table = (name) => `${quoteIdent(schema)}.${quoteIdent(name)}`;
|
|
445
|
-
const rows = [];
|
|
446
|
-
for (const role of customRoles) {
|
|
447
|
-
const at = customRoleAt(role, scopes);
|
|
448
|
-
if (at.scope === void 0) continue;
|
|
449
|
-
for (const grant of role.grants ?? []) rows.push({
|
|
450
|
-
sql: grant.level === void 0 ? `insert into ${table("custom_role_permissions")} (tenant_id, scope, scope_id, role, permission, effect) values ($1, $2, $3, $4, $5, $6) on conflict do nothing` : `insert into ${table("custom_role_permissions")} (tenant_id, scope, scope_id, role, permission, effect, level) values ($1, $2, $3, $4, $5, $6, $7) on conflict do nothing`,
|
|
451
|
-
values: [
|
|
452
|
-
role.tenant ?? null,
|
|
453
|
-
at.scope,
|
|
454
|
-
at.id ?? null,
|
|
455
|
-
role.name,
|
|
456
|
-
grant.permission,
|
|
457
|
-
grant.effect ?? "allow",
|
|
458
|
-
...grant.level === void 0 ? [] : [grant.level]
|
|
459
|
-
]
|
|
460
|
-
});
|
|
461
|
-
for (const name of role.includes ?? []) rows.push({
|
|
462
|
-
sql: `insert into ${table("custom_role_includes")} (tenant_id, scope, scope_id, role, include_role) values ($1, $2, $3, $4, $5) on conflict do nothing`,
|
|
463
|
-
values: [
|
|
464
|
-
role.tenant ?? null,
|
|
465
|
-
at.scope,
|
|
466
|
-
at.id ?? null,
|
|
467
|
-
role.name,
|
|
468
|
-
name
|
|
469
|
-
]
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
return rows;
|
|
473
|
-
}
|
|
474
|
-
function seedsCustomRoleTables(config) {
|
|
475
|
-
const rls = config.rls;
|
|
476
|
-
return rls?.customRoles === true && (rls.authorize ?? rls.rbac?.authorize ?? "jwt") === "database";
|
|
477
|
-
}
|
|
478
|
-
function customRoleSchema(config) {
|
|
479
|
-
return config.rls?.schema ?? config.rls?.rbac?.schema ?? "permdock";
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* A pgTAP script that runs each fixture's statement under the fixture's
|
|
483
|
-
* subject and asserts the outcome `can()` gave in-process.
|
|
484
|
-
*/
|
|
485
|
-
function emitPgtap(input) {
|
|
486
|
-
const scopes = scopeList(input.policy.scopes);
|
|
487
|
-
const subject = subjectOf(input.config, input.customRoles, scopes);
|
|
488
|
-
const lines = [
|
|
489
|
-
"begin;",
|
|
490
|
-
`select plan(${String(input.fixtures.length)});`,
|
|
491
|
-
PGTAP_DECISION_SQL
|
|
492
|
-
];
|
|
493
|
-
if (seedsCustomRoleTables(input.config)) for (const row of customRoleRows(customRoleSchema(input.config), input.customRoles, scopes)) lines.push(`${inlineStatement(row)};`);
|
|
494
|
-
for (const [index, fixture] of input.fixtures.entries()) {
|
|
495
|
-
const permission = findPermission(input.policy.permissions, fixture.action);
|
|
496
|
-
const status = input.inProcess[index];
|
|
497
|
-
const label = `fixture ${String(index)} ${fixture.action}`;
|
|
498
|
-
if (permission === void 0 || status === void 0) continue;
|
|
499
|
-
if (status.kind === "opaque") {
|
|
500
|
-
lines.push(`select skip(${quoteLiteral(`${label}: opaque grant untestable app-side`)}, 1);`);
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
const outcome = status.granted ? "granted" : "denied";
|
|
504
|
-
const table = input.config.rls?.tables?.[permission.resource] ?? permission.resource;
|
|
505
|
-
const statement = inlineStatement(statementFor(fixture, permission.action, table));
|
|
506
|
-
lines.push(`-- ${label}`, "savepoint permdock_fixture;", "set local role \"authenticated\";", ...subjectSettings(fixture, subject).map(([name, value]) => `select set_config(${quoteLiteral(name)}, ${quoteLiteral(value)}, true);`), `select is(${PGTAP_DECISION}(${quoteLiteral(statement)}), ${quoteLiteral(outcome)}, ${quoteLiteral(`${label} ${outcome}`)});`, "rollback to savepoint permdock_fixture;");
|
|
507
|
-
}
|
|
508
|
-
lines.push("select * from finish();", "rollback;");
|
|
509
|
-
return `${lines.join("\n")}\n`;
|
|
510
|
-
}
|
|
927
|
+
//#region src/cli/rls/verify-database.ts
|
|
511
928
|
/** Columns of `row` that hold a value; a masked column reads as null. */
|
|
512
929
|
function valuedColumns(row) {
|
|
513
930
|
return isRecord(row) ? Object.keys(row).filter((name) => row[name] !== null && row[name] !== void 0).toSorted() : [];
|
|
@@ -654,14 +1071,26 @@ async function verifyTreeAgainstDatabase(db, policy, config, connect) {
|
|
|
654
1071
|
await client.end();
|
|
655
1072
|
}
|
|
656
1073
|
}
|
|
1074
|
+
//#endregion
|
|
1075
|
+
//#region src/cli/rls/verify.ts
|
|
1076
|
+
function canFixture(permdock, permission, row) {
|
|
1077
|
+
if (permission.kind === "collection") return permdock.can(permission, row);
|
|
1078
|
+
return permdock.can(permission, row);
|
|
1079
|
+
}
|
|
1080
|
+
function grantKind(policy, key) {
|
|
1081
|
+
const grants = policy.grants.filter((grant) => grant.permission.key === key);
|
|
1082
|
+
if (grants.some((grant) => hasConditionOp(grant.where, "opaque") || hasConditionOp(grant.check, "opaque"))) return "opaque";
|
|
1083
|
+
if (grants.some((grant) => hasConditionOp(grant.where, "sqlFunction") || hasConditionOp(grant.check, "sqlFunction"))) return "sqlFunction";
|
|
1084
|
+
return "portable";
|
|
1085
|
+
}
|
|
657
1086
|
async function runRlsVerify(input) {
|
|
658
|
-
const connect = input.connect ?? (
|
|
1087
|
+
const connect = input.connect ?? pgConnector("permdock rls verify --db");
|
|
659
1088
|
const policyPath = input.from ?? input.config.policy;
|
|
660
1089
|
if (policyPath === void 0) return {
|
|
661
1090
|
code: 2,
|
|
662
1091
|
output: "PermDock CLI: rls verify needs policy in the config or --from"
|
|
663
1092
|
};
|
|
664
|
-
const policy =
|
|
1093
|
+
const policy = await loadPolicyAt(input.cwd, policyPath);
|
|
665
1094
|
if (input.tree === true) {
|
|
666
1095
|
if (input.db === void 0) return {
|
|
667
1096
|
code: 2,
|
|
@@ -743,4 +1172,4 @@ async function runRlsVerify(input) {
|
|
|
743
1172
|
};
|
|
744
1173
|
}
|
|
745
1174
|
//#endregion
|
|
746
|
-
export { runRlsVerify as n, canFixture as t };
|
|
1175
|
+
export { graphSql as a, callsHelper as c, rowConditionMessage as d, graphPlan as i, helperCallKeys as l, runRlsVerify as n, HELPER_TABLES as o, closureDepths as r, ROW_CONDITION_FIX as s, canFixture as t, helperTablePolicies as u };
|