permdock 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +327 -1
- package/dist/a2a/index.d.ts +169 -0
- package/dist/a2a/index.js +232 -0
- package/dist/adapter-context-mlWsVGGg.js +23 -0
- package/dist/ai-sdk/index.d.ts +95 -0
- package/dist/ai-sdk/index.js +154 -0
- package/dist/approval-required-TzNB9oQA.js +6 -0
- package/dist/approvals/index.d.ts +3 -0
- package/dist/approvals/index.js +5 -0
- package/dist/approvals-DbYAuNkG.js +224 -0
- package/dist/approvers-DT4AVyIg.js +52 -0
- package/dist/arazzo-BAgw9ap6.js +70 -0
- package/dist/arazzo-cmvX2jDe.js +54 -0
- package/dist/authzen/index.d.ts +39 -0
- package/dist/authzen/index.js +302 -0
- package/dist/better-auth/index.d.ts +109 -0
- package/dist/better-auth/index.js +344 -0
- package/dist/better-supabase/index.d.ts +206 -0
- package/dist/better-supabase/index.js +620 -0
- package/dist/bind-CVy3Espv.d.ts +25 -0
- package/dist/bind-VnYn2HKq.js +97 -0
- package/dist/boundary-BM8a_P6E.js +13 -0
- package/dist/boundary-BmHxVH1J.d.ts +15 -0
- package/dist/budget-JqcRqyhK.d.ts +11 -0
- package/dist/budget-NbEtbJC_.js +11 -0
- package/dist/canonical-json-DemiK4Vp.js +26 -0
- package/dist/capability-4qPdO9gk.js +173 -0
- package/dist/capability-Dq43tT1u.d.ts +99 -0
- package/dist/catalog/index.d.ts +12 -0
- package/dist/catalog/index.js +2 -0
- package/dist/catalog-BjZSovXQ.js +2 -0
- package/dist/catalog-doc-Dsc9y7sd.js +309 -0
- package/dist/catalog-h2tlKFkq.js +48 -0
- package/dist/catalog-path-CnI0XTFK.js +12 -0
- package/dist/catalog-z8q6bi_r.js +55 -0
- package/dist/claude-agent/index.d.ts +72 -0
- package/dist/claude-agent/index.js +77 -0
- package/dist/clerk/index.d.ts +65 -0
- package/dist/clerk/index.js +301 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +20 -0
- package/dist/cli/index.d.ts +46 -0
- package/dist/cli/index.js +5 -0
- package/dist/clients-C7zLFuWT.d.ts +12 -0
- package/dist/clients-DjU5ABMy.js +23 -0
- package/dist/cloud/index.d.ts +90 -0
- package/dist/cloud/index.js +404 -0
- package/dist/cloud-BGhHS-lY.js +50 -0
- package/dist/cloud-Bo3e9Ak3.js +118 -0
- package/dist/collect-DDDv_ESs.js +502 -0
- package/dist/collect-DLlujdHC.js +83 -0
- package/dist/collect-rG2IQ3t0.js +2 -0
- package/dist/commands-DWOrSQic.js +97 -0
- package/dist/compact-y6vfu2_f.js +19 -0
- package/dist/compare-pfyVnBjE.js +12 -0
- package/dist/compile/index.d.ts +3 -0
- package/dist/compile/index.js +2 -0
- package/dist/compile-Ba4blROR.js +613 -0
- package/dist/compile-CNnl6uIt.d.ts +94 -0
- package/dist/config-BJiM-xAm.js +157 -0
- package/dist/config-Bn00Z3ks.js +36 -0
- package/dist/config-report-Dw_JcUqT.js +44 -0
- package/dist/connection-CU8MQxRi.js +194 -0
- package/dist/context-Ttp0ekMT.js +82 -0
- package/dist/convex/index.d.ts +45 -0
- package/dist/convex/index.js +65 -0
- package/dist/create-BWyx3jua.d.ts +245 -0
- package/dist/create-CPhDL6qx.js +253 -0
- package/dist/credential-8P81ff4u.js +306 -0
- package/dist/delegation-D8pUEuce.js +103 -0
- package/dist/describe-B_gVZEVi.js +94 -0
- package/dist/describe-PdoqESvg.d.ts +36 -0
- package/dist/diff-B8Tunc3o.js +451 -0
- package/dist/diff-CzqLxZ77.js +48 -0
- package/dist/digest-B3kc_PgQ.d.ts +19 -0
- package/dist/digest-BI-6aa6d.js +27 -0
- package/dist/doctor-WpvG6p4k.js +1194 -0
- package/dist/doctor-collect-ByfYlNv1.js +664 -0
- package/dist/doctor-rOr-4ZfS.js +53 -0
- package/dist/drizzle/index.d.ts +73 -0
- package/dist/drizzle/index.js +160 -0
- package/dist/duration-xK2jGFVc.js +25 -0
- package/dist/elysia/index.d.ts +49 -0
- package/dist/elysia/index.js +59 -0
- package/dist/errors-CWfoNg9N.js +47 -0
- package/dist/errors-Cz5zn0mW.js +123 -0
- package/dist/errors-DAqevpNk.d.ts +125 -0
- package/dist/errors-DRnGZHzF.js +30 -0
- package/dist/evaluation-items-CfCCejQd.js +40 -0
- package/dist/evaluations-tz6D9A2G.js +461 -0
- package/dist/eve/index.d.ts +86 -0
- package/dist/eve/index.js +176 -0
- package/dist/expiry-BoBJ8Tvc.js +7 -0
- package/dist/express/index.d.ts +23 -0
- package/dist/express/index.js +78 -0
- package/dist/fastify/index.d.ts +33 -0
- package/dist/fastify/index.js +92 -0
- package/dist/fields-BMl1RxoX.js +26 -0
- package/dist/files-DWUN9PzI.js +125 -0
- package/dist/fixtures-ht28Cplr.js +67 -0
- package/dist/freeze-CrU8nQeX.js +60 -0
- package/dist/from-snapshot-C3sHpZNS.js +569 -0
- package/dist/generate-CH7ThJ14.js +173 -0
- package/dist/grantee-Cv5Sogfv.js +602 -0
- package/dist/graph-sql-B-dsAT6j.d.ts +29 -0
- package/dist/headers-BZSDieNT.js +6 -0
- package/dist/headers-Cf-zWvsO.d.ts +6 -0
- package/dist/helpers-H_KARDVC.js +421 -0
- package/dist/hono/index.d.ts +48 -0
- package/dist/hono/index.js +99 -0
- package/dist/hosted-DJQw46hn.js +266 -0
- package/dist/http-BIhmLfCc.js +95 -0
- package/dist/http-DPuDaChb.d.ts +13 -0
- package/dist/http-SO7qGttf.js +44 -0
- package/dist/ids-DC4V4CFu.js +18 -0
- package/dist/index-A0YNL8jZ.d.ts +177 -0
- package/dist/index-CtE07CBF.d.ts +6 -0
- package/dist/index-D3vbkoSI.d.ts +483 -0
- package/dist/index-D6_cSa1z.d.ts +201 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +26 -0
- package/dist/instance-options-BK_rNOSC.d.ts +11 -0
- package/dist/instance-options-Dhhcnm3y.js +22 -0
- package/dist/json-schema-BsAflUGW.js +106 -0
- package/dist/jwt/index.d.ts +91 -0
- package/dist/jwt/index.js +590 -0
- package/dist/kernel-GSQnMz3O.js +208 -0
- package/dist/kysely/index.d.ts +78 -0
- package/dist/kysely/index.js +129 -0
- package/dist/live-options-H2eG8dfJ.js +60 -0
- package/dist/load-DhWEB9P1.js +75 -0
- package/dist/load-DwJUafYN.js +2 -0
- package/dist/load-jose-BL6LH4bL.js +11 -0
- package/dist/lru-8q4WZjBV.js +28 -0
- package/dist/manifest-iaoBXHKc.js +783 -0
- package/dist/manifest-uDS9y5de.d.ts +206 -0
- package/dist/map-AcxK9o_D.js +147 -0
- package/dist/map-claims-Z4QZOiFF.js +369 -0
- package/dist/map-error-pgUWNNtG.js +23 -0
- package/dist/markers-B0hYLvMD.js +60 -0
- package/dist/may-use-DndQ5tQ-.js +77 -0
- package/dist/mcp/index.d.ts +159 -0
- package/dist/mcp/index.js +603 -0
- package/dist/nest/index.d.ts +75 -0
- package/dist/nest/index.js +261 -0
- package/dist/next/client.d.ts +23 -0
- package/dist/next/client.js +33 -0
- package/dist/next/index.d.ts +93 -0
- package/dist/next/index.js +180 -0
- package/dist/next/plugin.d.ts +14 -0
- package/dist/next/plugin.js +33 -0
- package/dist/node/index.d.ts +24 -0
- package/dist/node/index.js +34 -0
- package/dist/openai/index.d.ts +62 -0
- package/dist/openai/index.js +92 -0
- package/dist/openapi/index.d.ts +176 -0
- package/dist/openapi/index.js +3 -0
- package/dist/openapi-Bbg4wGVL.js +164 -0
- package/dist/openapi-BqRBvAng.js +538 -0
- package/dist/openapi-CqqEbHsL.js +250 -0
- package/dist/openapi-import-cmwetbzw.js +5959 -0
- package/dist/openapi-schema--ARjyfQr.js +6902 -0
- package/dist/orpc/index.d.ts +51 -0
- package/dist/orpc/index.js +145 -0
- package/dist/otel/index.d.ts +13 -0
- package/dist/otel/index.js +202 -0
- package/dist/package-root-zGkxO3oN.js +30 -0
- package/dist/parse-BuyjxhZV.js +53 -0
- package/dist/parse-local-manifest-BkGQNzAh.d.ts +10 -0
- package/dist/parse-local-manifest-BrMozJCo.js +36 -0
- package/dist/pdp/index.d.ts +22 -0
- package/dist/pdp/index.js +682 -0
- package/dist/peer-BOr_kJE1.js +31 -0
- package/dist/permdock-CzqXjCpU.js +3471 -0
- package/dist/permissions-CDgvKG5L.d.ts +52 -0
- package/dist/permissions-CpZrivYv.js +142 -0
- package/dist/permissions-DVoF6dMt.js +662 -0
- package/dist/permissions-DttnqqxC.d.ts +214 -0
- package/dist/pins-8ILTftC9.js +12 -0
- package/dist/policy-CFZAr_Al.d.ts +2252 -0
- package/dist/policy-D8UFyERw.d.ts +20 -0
- package/dist/policy-xyWg7QdR.js +1122 -0
- package/dist/powersync-A8x_Yyvh.js +2 -0
- package/dist/powersync-C7kLglqX.js +540 -0
- package/dist/powersync-yW1UUFzp.js +62 -0
- package/dist/prisma/index.d.ts +136 -0
- package/dist/prisma/index.js +455 -0
- package/dist/problem-B43ztKIm.js +191 -0
- package/dist/problem-details-CkL50EEf.d.ts +7 -0
- package/dist/problem-details-_AMOYz-w.js +144 -0
- package/dist/prompt-CXYhLfsS.js +32 -0
- package/dist/protected-CNsMzrVd.d.ts +27 -0
- package/dist/protected-Cs6kTaHo.js +100 -0
- package/dist/protected-bZs6rbT1.d.ts +40 -0
- package/dist/provider-Cjr62YMV.d.ts +6 -0
- package/dist/provider-DLzpb2Xz.js +52 -0
- package/dist/provider-store-DNyC1y48.js +43 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.js +7 -0
- package/dist/react/provider-client.d.ts +2 -0
- package/dist/react/provider-client.js +3 -0
- package/dist/react-native/index.d.ts +198 -0
- package/dist/react-native/index.js +696 -0
- package/dist/refs-KYb4ntaz.js +91 -0
- package/dist/relations-cjOL01hZ.js +505 -0
- package/dist/replay-BsOyTrdV.js +28 -0
- package/dist/replay-sqBGMnNs.d.ts +5 -0
- package/dist/resource-ref-CaS9yVPi.js +20 -0
- package/dist/revocations-Bhr0aMIC.js +27 -0
- package/dist/revocations-Cy06JNx2.d.ts +21 -0
- package/dist/rls-BMNydpHk.js +2521 -0
- package/dist/rls-CEvs64Kj.js +238 -0
- package/dist/rls-fields-BMasREhR.js +3502 -0
- package/dist/rls-graph-pc9fuXYm.js +435 -0
- package/dist/rls-import-D8F7j35Q.js +943 -0
- package/dist/rls-migrate-7wikfrVI.js +367 -0
- package/dist/rls-ownership-B5Lkq62W.js +843 -0
- package/dist/rls-shared-aOQ4hGTO.js +493 -0
- package/dist/rls-verify-D8B7dOTh.js +745 -0
- package/dist/row-check-Dt35czLy.d.ts +40 -0
- package/dist/rpc-DOK54_CS.js +89 -0
- package/dist/run-YXCJDNYk.js +174 -0
- package/dist/runtime-CB8gQFvS.js +11 -0
- package/dist/saas-Bj_FFkjN.js +1203 -0
- package/dist/schema-D3MnDMxa.js +300 -0
- package/dist/scim/index.d.ts +2 -0
- package/dist/scim/index.js +2 -0
- package/dist/scim-BFGBBAqR.js +1445 -0
- package/dist/scopes-CVdPrWN-.js +239 -0
- package/dist/server/index.d.ts +134 -0
- package/dist/server/index.js +11 -0
- package/dist/server-DlZ28Oz5.js +268 -0
- package/dist/sha256-BO0swRXL.js +150 -0
- package/dist/shared-DRu8VFVM.js +123 -0
- package/dist/sink-Bd9PSyfq.js +141 -0
- package/dist/sink-Gs6TTVa-.d.ts +104 -0
- package/dist/skills-Biq5uup3.js +192 -0
- package/dist/skills-JaymNCdK.js +56 -0
- package/dist/snapshot-Enxd0n-s.js +975 -0
- package/dist/snapshot-cache-D6jLelln.js +44 -0
- package/dist/snapshot-cache-DwQH8ufx.d.ts +45 -0
- package/dist/solid/index.d.ts +93 -0
- package/dist/solid/index.js +188 -0
- package/dist/sources-B0UvrJ21.js +534 -0
- package/dist/sources-BcJgw0lt.d.ts +192 -0
- package/dist/sql-xdg5f4Fi.js +27 -0
- package/dist/src-CSbUiozF.js +568 -0
- package/dist/ssf/index.d.ts +4 -0
- package/dist/ssf/index.js +596 -0
- package/dist/store-DMAWwAJV.js +371 -0
- package/dist/store-D_pjn91-.d.ts +36 -0
- package/dist/store-options-yO0V6gRn.js +716 -0
- package/dist/stores-4_sqiOA1.js +87 -0
- package/dist/stores-DVR5NHLE.d.ts +52 -0
- package/dist/stream-DDpvynSB.js +66 -0
- package/dist/stream-DlvG2hs-.d.ts +12 -0
- package/dist/style-CTm1AXDT.js +22 -0
- package/dist/subject-C6dLxqHU.js +24 -0
- package/dist/subject-settings-BwBmCdTR.js +128 -0
- package/dist/supabase/index.d.ts +183 -0
- package/dist/supabase/index.js +5 -0
- package/dist/supabase/middleware.d.ts +82 -0
- package/dist/supabase/middleware.js +102 -0
- package/dist/supabase-CwafwxxF.js +83 -0
- package/dist/supabase-DTBaC20O.js +774 -0
- package/dist/supabase-config-Bs44dare.js +472 -0
- package/dist/supabase-hook-BuxLQLpU.js +1814 -0
- package/dist/supabase-hook-D5qVd_D8.js +2 -0
- package/dist/supautils-DYZnzRWV.js +91 -0
- package/dist/svelte/PermissionBoundary.svelte +37 -0
- package/dist/svelte/Protected.svelte +40 -0
- package/dist/svelte/index.d.ts +13 -0
- package/dist/svelte/index.js +107 -0
- package/dist/svelte/runtime.d.ts +7 -0
- package/dist/svelte/runtime.js +4 -0
- package/dist/svelte/source.d.ts +13 -0
- package/dist/svelte/source.js +11 -0
- package/dist/terminal/index.d.ts +163 -0
- package/dist/terminal/index.js +833 -0
- package/dist/testing/index.d.ts +745 -0
- package/dist/testing/index.js +3672 -0
- package/dist/testing/saas/index.d.ts +194 -0
- package/dist/testing/saas/index.js +3 -0
- package/dist/testing/saas/permissions.d.ts +2 -0
- package/dist/testing/saas/permissions.js +2 -0
- package/dist/text-diff-BdkkYSbi.js +15 -0
- package/dist/thenable-okc2xDsj.js +10 -0
- package/dist/timeout-D51niuQP.js +8 -0
- package/dist/token-CIWxJOCh.js +50 -0
- package/dist/trpc/index.d.ts +61 -0
- package/dist/trpc/index.js +122 -0
- package/dist/types-9AWO9EGt.d.ts +197 -0
- package/dist/types-BPxgszlT.d.ts +550 -0
- package/dist/types-BVyImy5I.d.ts +120 -0
- package/dist/types-Bvc4oWT6.d.ts +9 -0
- package/dist/types-C3s9xVJG.d.ts +48 -0
- package/dist/types-Dnemwd77.d.ts +183 -0
- package/dist/types-kxznc7cn.d.ts +52 -0
- package/dist/types-vKyAMtcX.d.ts +157 -0
- package/dist/types-vNSDF7Dq.d.ts +66 -0
- package/dist/types-yF5LmQVD.d.ts +306 -0
- package/dist/unplugin/index.d.ts +7 -0
- package/dist/unplugin/index.js +31 -0
- package/dist/usage-D07N9QJ3.js +42 -0
- package/dist/usage-D82l1CwK.js +2 -0
- package/dist/usage-DG1oeCca.js +693 -0
- package/dist/validation-error-CnjZ4t92.js +33 -0
- package/dist/validation-error-CtF23SrC.d.ts +26 -0
- package/dist/validity-uOp8UpYn.js +245 -0
- package/dist/verifier-BxBo6I44.js +466 -0
- package/dist/version-BvacVTZR.js +17 -0
- package/dist/views-Soe7bGSC.js +95 -0
- package/dist/vocabulary-DA8a8zif.d.ts +56 -0
- package/dist/vue/index.d.ts +79 -0
- package/dist/vue/index.js +196 -0
- package/dist/watch-Etoa_Mvr.js +2 -0
- package/dist/watch-UihESpsJ.js +128 -0
- package/dist/webmcp/index.d.ts +75 -0
- package/dist/webmcp/index.js +307 -0
- package/package.json +450 -5
- package/rulesets/permdock.yaml +29 -0
- package/schemas/approval-request-v1.json +175 -0
- package/schemas/catalog-v1.json +535 -0
- package/schemas/openapi/oas-3.1.json +1298 -0
- package/schemas/openapi/oas-3.2.json +1516 -0
- package/schemas/openapi/overlay-1.1.json +79 -0
- package/schemas/supabase-claims-v1.json +144 -0
- package/schemas/supabase-manifest-v1.json +361 -0
- package/skills/permdock/SKILL.md +66 -0
- package/skills/permdock-agents/SKILL.md +77 -0
- package/skills/permdock-agents/references/runtimes.md +213 -0
- package/skills/permdock-approvals/SKILL.md +80 -0
- package/skills/permdock-approvals/references/stores-and-resume.md +94 -0
- package/skills/permdock-audit/SKILL.md +67 -0
- package/skills/permdock-credentials/SKILL.md +55 -0
- package/skills/permdock-credentials/references/tokens-keys-links.md +132 -0
- package/skills/permdock-data/SKILL.md +57 -0
- package/skills/permdock-data/references/queries-and-rls.md +62 -0
- package/skills/permdock-data/references/relationships.md +67 -0
- package/skills/permdock-tenancy/SKILL.md +80 -0
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +89 -0
- package/skills/permdock-wire/SKILL.md +116 -0
- package/skills/permdock-wire/references/adapters.md +492 -0
- package/skills/permdock-wire/references/policy-options.md +45 -0
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
import { _ as listPermissions, d as isEdgeRelation, f as isFieldRelation, i as findPermission, m as isPrincipalRelation } from "./permissions-DVoF6dMt.js";
|
|
2
|
+
import { a as hasConditionOp } from "./refs-KYb4ntaz.js";
|
|
3
|
+
import { _ as memoryRoleSource } from "./src-CSbUiozF.js";
|
|
4
|
+
import { d as scopeList } from "./scopes-CVdPrWN-.js";
|
|
5
|
+
import { g as customRoleScope, t as createPermDock, v as membershipsClaim } from "./permdock-CzqXjCpU.js";
|
|
6
|
+
import { t as memoryRelations } from "./relations-cjOL01hZ.js";
|
|
7
|
+
import { i as describeError, o as usageResult } from "./errors-CWfoNg9N.js";
|
|
8
|
+
import { o as jsonSchemaOf, s as policyRowConditionKeys } from "./catalog-doc-Dsc9y7sd.js";
|
|
9
|
+
import { a as loadModule, n as asPolicy, o as pickNamed } from "./load-DhWEB9P1.js";
|
|
10
|
+
import "./budget-NbEtbJC_.js";
|
|
11
|
+
import "./sources-B0UvrJ21.js";
|
|
12
|
+
import { G as quoteLiteral, K as quoteTable, W as quoteIdent, y as columnTypesOf } from "./rls-shared-aOQ4hGTO.js";
|
|
13
|
+
import { p as commandFor, s as viewName, t as FIELD_VIEWS, w as connectPg } from "./rls-fields-BMasREhR.js";
|
|
14
|
+
import { a as HELPER_TABLE_POLICIES_SQL, d as rowConditionMessage, n as graphPlan, u as helperTablePoliciesFromRows } from "./rls-graph-pc9fuXYm.js";
|
|
15
|
+
import { n as fixtureSubject, r as loadFixtures, t as fixtureRow } from "./fixtures-ht28Cplr.js";
|
|
16
|
+
import { resolve } from "node:path";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
//#region src/cli/rls-verify-tree.ts
|
|
19
|
+
const SUBJECTS = 3;
|
|
20
|
+
function tableSql(name) {
|
|
21
|
+
return quoteTable(name.includes(".") ? name : `public.${name}`);
|
|
22
|
+
}
|
|
23
|
+
function idFactory(node) {
|
|
24
|
+
const type = columnTypesOf(jsonSchemaOf(node))[node.id];
|
|
25
|
+
let next = 9e5;
|
|
26
|
+
return type === "numeric" ? () => {
|
|
27
|
+
next += 1;
|
|
28
|
+
return next;
|
|
29
|
+
} : () => randomUUID();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Two spines one level deeper than the walk depth, so the depth cap is
|
|
33
|
+
* crossed; side branches under the first levels alternate restricted, and
|
|
34
|
+
* the second spine is restricted two levels down.
|
|
35
|
+
*/
|
|
36
|
+
function generateTree(node, depth) {
|
|
37
|
+
const id = idFactory(node);
|
|
38
|
+
const nodes = [];
|
|
39
|
+
for (const spine of [0, 1]) {
|
|
40
|
+
let parent = null;
|
|
41
|
+
for (let level = 0; level <= depth + 1; level += 1) {
|
|
42
|
+
const current = id();
|
|
43
|
+
nodes.push({
|
|
44
|
+
id: current,
|
|
45
|
+
parent,
|
|
46
|
+
restricted: node.restricted !== void 0 && spine === 1 && level === 2
|
|
47
|
+
});
|
|
48
|
+
if (spine === 0 && level >= 1 && level <= 3) {
|
|
49
|
+
const side = id();
|
|
50
|
+
nodes.push({
|
|
51
|
+
id: side,
|
|
52
|
+
parent: current,
|
|
53
|
+
restricted: node.restricted !== void 0 && level % 2 === 1
|
|
54
|
+
});
|
|
55
|
+
nodes.push({
|
|
56
|
+
id: id(),
|
|
57
|
+
parent: side,
|
|
58
|
+
restricted: false
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
parent = current;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return nodes;
|
|
65
|
+
}
|
|
66
|
+
function insertSql(table, rows) {
|
|
67
|
+
const columns = Object.keys(rows[0] ?? {});
|
|
68
|
+
const values = [];
|
|
69
|
+
const tuples = rows.map((row) => `(${columns.map((column) => {
|
|
70
|
+
values.push(row[column]);
|
|
71
|
+
return `$${String(values.length)}`;
|
|
72
|
+
}).join(", ")})`);
|
|
73
|
+
return {
|
|
74
|
+
sql: `insert into ${tableSql(table)} (${columns.map(quoteIdent).join(", ")}) values ${tuples.join(", ")} returning *`,
|
|
75
|
+
values
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const REQUIRED_COLUMNS_SQL = `select a.attname as name,
|
|
79
|
+
format_type(a.atttypid, a.atttypmod) as type,
|
|
80
|
+
k.confrelid::regclass::text as "refTable",
|
|
81
|
+
r.attname as "refColumn",
|
|
82
|
+
(select e.enumlabel from pg_enum e where e.enumtypid = a.atttypid order by e.enumsortorder limit 1) as "enumLabel",
|
|
83
|
+
array(
|
|
84
|
+
select pg_get_constraintdef(c.oid) from pg_constraint c
|
|
85
|
+
where c.conrelid = a.attrelid and c.contype = 'c' and c.conkey = array[a.attnum]
|
|
86
|
+
order by c.conname
|
|
87
|
+
) as checks
|
|
88
|
+
from pg_attribute a
|
|
89
|
+
left join pg_constraint k
|
|
90
|
+
on k.conrelid = a.attrelid and k.contype = 'f' and k.conkey = array[a.attnum]
|
|
91
|
+
left join pg_attribute r
|
|
92
|
+
on r.attrelid = k.confrelid and r.attnum = k.confkey[1]
|
|
93
|
+
where a.attrelid = $1::regclass and a.attnum > 0 and not a.attisdropped
|
|
94
|
+
and a.attnotnull and not a.atthasdef
|
|
95
|
+
and a.attidentity = '' and a.attgenerated = ''`;
|
|
96
|
+
const UNIQUE_COLUMNS_SQL = `select distinct a.attname as name
|
|
97
|
+
from pg_index i
|
|
98
|
+
join pg_attribute a on a.attrelid = i.indrelid
|
|
99
|
+
where i.indrelid = $1::regclass and i.indisunique
|
|
100
|
+
and a.attnum > 0 and not a.attisdropped
|
|
101
|
+
and (
|
|
102
|
+
a.attnum = any(i.indkey)
|
|
103
|
+
or exists (
|
|
104
|
+
select 1 from pg_depend d
|
|
105
|
+
where d.classid = 'pg_class'::regclass and d.objid = i.indexrelid
|
|
106
|
+
and d.refclassid = 'pg_class'::regclass and d.refobjid = i.indrelid
|
|
107
|
+
and d.refobjsubid = a.attnum
|
|
108
|
+
)
|
|
109
|
+
)`;
|
|
110
|
+
function checkedValue(checks) {
|
|
111
|
+
for (const check of checks) {
|
|
112
|
+
const literal = /'((?:[^']|'')*)'/u.exec(check);
|
|
113
|
+
if (literal?.[1] !== void 0 && /[=]|\bin\b/iu.test(check)) return literal[1].replaceAll("''", "'");
|
|
114
|
+
const bound = /(>=?)\s*\(?(-?\d+(?:\.\d+)?)/u.exec(check);
|
|
115
|
+
if (bound?.[1] !== void 0 && bound[2] !== void 0) {
|
|
116
|
+
const value = Number(bound[2]);
|
|
117
|
+
return bound[1] === ">" ? Math.floor(value) + 1 : value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function placeholder(column) {
|
|
122
|
+
if (column.enumLabel !== null) return column.enumLabel;
|
|
123
|
+
const checked = checkedValue(column.checks);
|
|
124
|
+
if (checked !== void 0) return checked;
|
|
125
|
+
const type = column.type;
|
|
126
|
+
if (type === "uuid") return randomUUID();
|
|
127
|
+
if (type === "boolean") return false;
|
|
128
|
+
if (/^(smallint|integer|bigint|numeric|real|double precision)/u.test(type)) return 0;
|
|
129
|
+
if (type.startsWith("timestamp") || type === "date") return (/* @__PURE__ */ new Date()).toISOString();
|
|
130
|
+
if (type === "json" || type === "jsonb") return {};
|
|
131
|
+
return "permdock-tree";
|
|
132
|
+
}
|
|
133
|
+
function distinctPlaceholder(column, index) {
|
|
134
|
+
const value = placeholder(column);
|
|
135
|
+
if (column.enumLabel !== null) return value;
|
|
136
|
+
if (typeof value === "number") return value + index;
|
|
137
|
+
if (typeof value !== "string" || checkedValue(column.checks) !== void 0) return value;
|
|
138
|
+
if (column.type === "uuid") return value;
|
|
139
|
+
if (column.type.startsWith("timestamp") || column.type === "date") return new Date(Date.parse(value) + index * 864e5).toISOString();
|
|
140
|
+
return `${value}-${String(index + 1)}`;
|
|
141
|
+
}
|
|
142
|
+
function asRequiredColumn(row) {
|
|
143
|
+
const { name, type, refTable, refColumn, enumLabel, checks } = row;
|
|
144
|
+
if (typeof name !== "string" || typeof type !== "string") return;
|
|
145
|
+
return {
|
|
146
|
+
name,
|
|
147
|
+
type,
|
|
148
|
+
refTable: typeof refTable === "string" ? refTable : null,
|
|
149
|
+
refColumn: typeof refColumn === "string" ? refColumn : null,
|
|
150
|
+
enumLabel: typeof enumLabel === "string" ? enumLabel : null,
|
|
151
|
+
checks: Array.isArray(checks) ? checks.filter((check) => typeof check === "string") : []
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Values for the required columns a generated row leaves out, such as a
|
|
156
|
+
* tenant column or a name: an existing value of the referenced table for a
|
|
157
|
+
* foreign key, a placeholder of the column's type otherwise.
|
|
158
|
+
*/
|
|
159
|
+
async function requiredValues(query, table, set) {
|
|
160
|
+
const columns = (await query(REQUIRED_COLUMNS_SQL, [table])).rows.map(asRequiredColumn).filter((column) => column !== void 0 && !set.has(column.name));
|
|
161
|
+
const unique = new Set(columns.length === 0 ? [] : (await query(UNIQUE_COLUMNS_SQL, [table])).rows.map((row) => String(row["name"])));
|
|
162
|
+
const values = {};
|
|
163
|
+
const distinct = [];
|
|
164
|
+
for (const column of columns) if (column.refTable !== null && column.refColumn !== null) {
|
|
165
|
+
const existing = await query(`select ${quoteIdent(column.refColumn)} as value from ${column.refTable} limit 1`);
|
|
166
|
+
values[column.name] = existing.rows[0]?.["value"] ?? null;
|
|
167
|
+
} else if (unique.has(column.name)) distinct.push(column);
|
|
168
|
+
else values[column.name] = placeholder(column);
|
|
169
|
+
return (index) => {
|
|
170
|
+
const row = { ...values };
|
|
171
|
+
for (const column of distinct) row[column.name] = distinctPlaceholder(column, index);
|
|
172
|
+
return row;
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** Inserts `rows` after filling the required columns they leave out. */
|
|
176
|
+
async function seedRows(query, table, rows, given = {}) {
|
|
177
|
+
const fill = await requiredValues(query, tableSql(table), /* @__PURE__ */ new Set([...Object.keys(rows[0] ?? {}), ...Object.keys(given)]));
|
|
178
|
+
const inserted = insertSql(table, rows.map((row, index) => ({
|
|
179
|
+
...fill(index),
|
|
180
|
+
...given,
|
|
181
|
+
...row
|
|
182
|
+
})));
|
|
183
|
+
return (await query(inserted.sql, inserted.values)).rows;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Parity between `decide` and RLS on a generated object graph: for every
|
|
187
|
+
* self-parented resource a graph grant walks, a tree with restricted
|
|
188
|
+
* branches and a chain past the walk depth, child rows under it, and edges
|
|
189
|
+
* for a few generated principals. Everything runs in one transaction that
|
|
190
|
+
* is rolled back.
|
|
191
|
+
*/
|
|
192
|
+
async function verifyTree(input) {
|
|
193
|
+
const { policy, query } = input;
|
|
194
|
+
const tables = input.config.rls?.tables;
|
|
195
|
+
const treeValues = input.config.rls?.treeValues ?? {};
|
|
196
|
+
const seed = (table, list) => seedRows(query, table, list, treeValues[table] ?? {});
|
|
197
|
+
const walked = [...graphPlan(policy).values()].filter((entry) => entry.closure !== void 0);
|
|
198
|
+
if (walked.length === 0) return {
|
|
199
|
+
checked: 0,
|
|
200
|
+
granted: 0,
|
|
201
|
+
mismatches: [],
|
|
202
|
+
notes: ["no graph grant walks a self-parented resource"]
|
|
203
|
+
};
|
|
204
|
+
const subjects = Array.from({ length: SUBJECTS }, () => randomUUID());
|
|
205
|
+
const rows = {};
|
|
206
|
+
const edges = {};
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
for (const entry of walked) {
|
|
209
|
+
const node = entry.node;
|
|
210
|
+
const parent = node.parent;
|
|
211
|
+
if (parent === void 0) continue;
|
|
212
|
+
const tree = generateTree(node, entry.closure ?? 0);
|
|
213
|
+
const seeded = tree.map((item, index) => {
|
|
214
|
+
const row = {
|
|
215
|
+
[node.id]: item.id,
|
|
216
|
+
[parent.field]: item.parent
|
|
217
|
+
};
|
|
218
|
+
if (node.restricted !== void 0) row[node.restricted] = item.restricted;
|
|
219
|
+
for (const name of entry.relations) {
|
|
220
|
+
const spec = node.relations[name];
|
|
221
|
+
const column = isFieldRelation(spec) ? spec.memberOf === void 0 ? spec.field : void 0 : isPrincipalRelation(spec) ? spec.principal : void 0;
|
|
222
|
+
if (column !== void 0 && column !== parent.field) row[column] = index % 4 === 0 ? subjects[index % SUBJECTS] : randomUUID();
|
|
223
|
+
}
|
|
224
|
+
return row;
|
|
225
|
+
});
|
|
226
|
+
if ([...entry.relations].some((name) => {
|
|
227
|
+
const spec = node.relations[name];
|
|
228
|
+
return isPrincipalRelation(spec) && spec.principal === parent.field || isFieldRelation(spec) && spec.field === parent.field;
|
|
229
|
+
})) subjects.push(...tree.filter((_, index) => index % 3 === 0).map((item) => String(item.id)));
|
|
230
|
+
rows[node.name] = [...await seed(tables?.[node.name] ?? node.name, seeded)];
|
|
231
|
+
for (const name of entry.relations) {
|
|
232
|
+
const spec = node.relations[name];
|
|
233
|
+
if (!isEdgeRelation(spec)) continue;
|
|
234
|
+
const object = spec.object ?? `${node.name}_id`;
|
|
235
|
+
const subject = spec.subject ?? "user_id";
|
|
236
|
+
const list = [];
|
|
237
|
+
const edgeRows = [];
|
|
238
|
+
for (const [index, item] of tree.entries()) {
|
|
239
|
+
const holder = subjects[index % SUBJECTS];
|
|
240
|
+
if (holder === void 0 || index % 5 !== 1) continue;
|
|
241
|
+
const expiresAt = spec.expiresAt !== void 0 && index % 10 === 6 ? /* @__PURE__ */ new Date(now - 864e5) : void 0;
|
|
242
|
+
edgeRows.push({
|
|
243
|
+
[object]: item.id,
|
|
244
|
+
[subject]: holder,
|
|
245
|
+
...spec.expiresAt === void 0 ? {} : { [spec.expiresAt]: expiresAt ?? null }
|
|
246
|
+
});
|
|
247
|
+
list.push({
|
|
248
|
+
id: String(item.id),
|
|
249
|
+
principal: holder,
|
|
250
|
+
...expiresAt === void 0 ? {} : { expiresAt: expiresAt.getTime() / 1e3 }
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (edgeRows.length > 0) await seed(spec.edge, edgeRows);
|
|
254
|
+
edges[node.name] = {
|
|
255
|
+
...edges[node.name],
|
|
256
|
+
[name]: list
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
for (const child of policy.resources.values()) {
|
|
260
|
+
if (child.name === node.name || child.parent?.resource !== node.name) continue;
|
|
261
|
+
const make = idFactory(child);
|
|
262
|
+
const childRows = tree.map((item, index) => {
|
|
263
|
+
const row = {
|
|
264
|
+
[child.id]: make(),
|
|
265
|
+
[child.parent?.field ?? ""]: item.id
|
|
266
|
+
};
|
|
267
|
+
if (child.restricted !== void 0) row[child.restricted] = index % 7 === 3;
|
|
268
|
+
return row;
|
|
269
|
+
});
|
|
270
|
+
rows[child.name] = [...await seed(tables?.[child.name] ?? child.name, childRows)];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const relations = memoryRelations(policy.permissions, {
|
|
274
|
+
rows,
|
|
275
|
+
edges
|
|
276
|
+
});
|
|
277
|
+
const mismatches = [];
|
|
278
|
+
let checked = 0;
|
|
279
|
+
let granted = 0;
|
|
280
|
+
const reads = listPermissions(policy.permissions).filter((leaf) => leaf.kind === "instance" && commandFor(leaf.action, input.config.rls?.actions) === "select" && rows[leaf.resource] !== void 0);
|
|
281
|
+
for (const subject of subjects) {
|
|
282
|
+
const permdock = await createPermDock(policy, {
|
|
283
|
+
principal: {
|
|
284
|
+
id: subject,
|
|
285
|
+
roles: []
|
|
286
|
+
},
|
|
287
|
+
context: {}
|
|
288
|
+
}, { relations });
|
|
289
|
+
await input.bind(subject);
|
|
290
|
+
for (const permission of reads) {
|
|
291
|
+
const node = policy.resources.get(permission.resource);
|
|
292
|
+
const list = rows[permission.resource] ?? [];
|
|
293
|
+
if (node === void 0 || list.length === 0) continue;
|
|
294
|
+
const ids = list.map((row) => String(row[node.id]));
|
|
295
|
+
const visible = await query(`select ${quoteIdent(node.id)}::text as id from ${tableSql(tables?.[node.name] ?? node.name)} where ${quoteIdent(node.id)} = any($1)`, [ids]);
|
|
296
|
+
const seen = new Set(visible.rows.map((row) => String(row["id"])));
|
|
297
|
+
for (const row of list) {
|
|
298
|
+
checked += 1;
|
|
299
|
+
const id = String(row[node.id]);
|
|
300
|
+
const allowed = permdock.can(permission, row);
|
|
301
|
+
if (allowed !== seen.has(id)) mismatches.push(`${permission.key} ${id}: in-process ${allowed ? "granted" : "denied"}, database ${seen.has(id) ? "allowed" : "filtered"}`);
|
|
302
|
+
else if (allowed) granted += 1;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
checked,
|
|
308
|
+
granted,
|
|
309
|
+
mismatches,
|
|
310
|
+
notes: []
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/cli/rls-verify.ts
|
|
315
|
+
function isRecord(value) {
|
|
316
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
317
|
+
}
|
|
318
|
+
/** A custom role's scope and pinned instance, as the `custom_role_*` tables store them. */
|
|
319
|
+
function customRoleAt(role, scopes) {
|
|
320
|
+
return {
|
|
321
|
+
scope: customRoleScope(role, scopes),
|
|
322
|
+
id: role.scope === void 0 ? role.team : role.id
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function canFixture(permdock, permission, row) {
|
|
326
|
+
if (permission.kind === "collection") return permdock.can(permission, row);
|
|
327
|
+
return permdock.can(permission, row);
|
|
328
|
+
}
|
|
329
|
+
function grantKind(policy, key) {
|
|
330
|
+
const grants = policy.grants.filter((grant) => grant.permission.key === key);
|
|
331
|
+
if (grants.some((grant) => hasConditionOp(grant.where, "opaque") || hasConditionOp(grant.check, "opaque"))) return "opaque";
|
|
332
|
+
if (grants.some((grant) => hasConditionOp(grant.where, "sqlFunction") || hasConditionOp(grant.check, "sqlFunction"))) return "sqlFunction";
|
|
333
|
+
return "portable";
|
|
334
|
+
}
|
|
335
|
+
function columnsOf(row) {
|
|
336
|
+
return isRecord(row) ? Object.entries(row).filter(([, value]) => value !== void 0 && typeof value !== "object") : [];
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* The statement a fixture runs: `create` inserts the whole row, `update` writes
|
|
340
|
+
* `newRow` when given. Each reads back only the key, so `--revoke-columns`
|
|
341
|
+
* (which keeps only unrestricted columns readable) does not reject it.
|
|
342
|
+
*/
|
|
343
|
+
function statementFor(fixture, action, table) {
|
|
344
|
+
const quoted = quoteTable(table);
|
|
345
|
+
const id = quoteIdent("id");
|
|
346
|
+
const key = rowId(fixture.row);
|
|
347
|
+
switch (action) {
|
|
348
|
+
case "read":
|
|
349
|
+
case "list":
|
|
350
|
+
case "get": return {
|
|
351
|
+
sql: `select ${id} from ${quoted} where ${id} = $1`,
|
|
352
|
+
values: [key]
|
|
353
|
+
};
|
|
354
|
+
case "update": {
|
|
355
|
+
const next = columnsOf(fixture.newRow).filter(([name]) => name !== "id");
|
|
356
|
+
return {
|
|
357
|
+
sql: `update ${quoted} set ${next.length === 0 ? `${id} = ${id}` : next.map(([name], index) => `${quoteIdent(name)} = $${index + 2}`).join(", ")} where ${id} = $1 returning ${id}`,
|
|
358
|
+
values: [key, ...next.map(([, value]) => value)]
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
case "create": {
|
|
362
|
+
const cols = columnsOf(fixture.row);
|
|
363
|
+
if (cols.length === 0) return {
|
|
364
|
+
sql: `insert into ${quoted} (${id}) values ($1) returning ${id}`,
|
|
365
|
+
values: [key]
|
|
366
|
+
};
|
|
367
|
+
return {
|
|
368
|
+
sql: `insert into ${quoted} (${cols.map(([name]) => quoteIdent(name)).join(", ")}) values (${cols.map((_, index) => `$${index + 1}`).join(", ")}) returning ${id}`,
|
|
369
|
+
values: cols.map(([, value]) => value)
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
case "delete": return {
|
|
373
|
+
sql: `delete from ${quoted} where ${id} = $1 returning ${id}`,
|
|
374
|
+
values: [key]
|
|
375
|
+
};
|
|
376
|
+
default: return {
|
|
377
|
+
sql: `select ${id} from ${quoted} where ${id} = $1`,
|
|
378
|
+
values: [key]
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function rowId(row) {
|
|
383
|
+
if (isRecord(row) && "id" in row) return row["id"];
|
|
384
|
+
}
|
|
385
|
+
/** A fixture value as a SQL literal; untyped, so it coerces to the column like a bound parameter. */
|
|
386
|
+
function sqlValue(value) {
|
|
387
|
+
if (value === null || value === void 0) return "null";
|
|
388
|
+
return quoteLiteral(typeof value === "string" ? value : JSON.stringify(value));
|
|
389
|
+
}
|
|
390
|
+
function inlineStatement(statement) {
|
|
391
|
+
return statement.sql.replaceAll(/\$(\d+)/gu, (_, index) => sqlValue(statement.values[Number(index) - 1]));
|
|
392
|
+
}
|
|
393
|
+
const PGTAP_DECISION = "pg_temp.permdock_decision";
|
|
394
|
+
const PGTAP_DECISION_SQL = `create function ${PGTAP_DECISION}(statement text) returns text
|
|
395
|
+
language plpgsql
|
|
396
|
+
as $$
|
|
397
|
+
declare
|
|
398
|
+
affected bigint;
|
|
399
|
+
begin
|
|
400
|
+
execute statement;
|
|
401
|
+
get diagnostics affected = row_count;
|
|
402
|
+
return case when affected > 0 then 'granted' else 'denied' end;
|
|
403
|
+
exception
|
|
404
|
+
when others then
|
|
405
|
+
return 'denied';
|
|
406
|
+
end;
|
|
407
|
+
$$;`;
|
|
408
|
+
function subjectOf(config, customRoles, scopes) {
|
|
409
|
+
return {
|
|
410
|
+
dialect: config.rls?.dialect ?? "supabase",
|
|
411
|
+
gucPrefix: config.rls?.gucPrefix ?? "app",
|
|
412
|
+
tenantClaim: config.rls?.tenantClaim ?? "tenant_id",
|
|
413
|
+
roleClaim: config.rls?.roleClaim ?? "user_role",
|
|
414
|
+
customRoles,
|
|
415
|
+
scopes
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
/** The settings that bind a fixture's subject: the GUCs, or the Supabase JWT claims. */
|
|
419
|
+
function subjectSettings(fixture, subject) {
|
|
420
|
+
const roles = fixture.subject.roles ?? [];
|
|
421
|
+
const memberships = membershipsClaim(fixture.subject.memberships ?? [], subject.customRoles, subject.scopes);
|
|
422
|
+
if (subject.dialect === "guc") {
|
|
423
|
+
const settings = [
|
|
424
|
+
[`${subject.gucPrefix}.user_id`, fixture.subject.id],
|
|
425
|
+
[`${subject.gucPrefix}.${subject.roleClaim}`, roles.join(",")],
|
|
426
|
+
[`${subject.gucPrefix}.memberships`, JSON.stringify(memberships)]
|
|
427
|
+
];
|
|
428
|
+
if (fixture.subject.tenant !== void 0) settings.push([`${subject.gucPrefix}.${subject.tenantClaim}`, fixture.subject.tenant]);
|
|
429
|
+
return settings;
|
|
430
|
+
}
|
|
431
|
+
const claims = {
|
|
432
|
+
...fixture.subject.claims,
|
|
433
|
+
sub: fixture.subject.id,
|
|
434
|
+
role: "authenticated",
|
|
435
|
+
[subject.roleClaim]: roles.length === 1 ? roles[0] : roles,
|
|
436
|
+
[subject.tenantClaim]: fixture.subject.tenant,
|
|
437
|
+
memberships
|
|
438
|
+
};
|
|
439
|
+
return [["request.jwt.claims", JSON.stringify(claims)]];
|
|
440
|
+
}
|
|
441
|
+
/** The `custom_role_*` rows for the fixtures' custom roles; existing rows are kept. */
|
|
442
|
+
function customRoleRows(schema, customRoles, scopes) {
|
|
443
|
+
const table = (name) => `${quoteIdent(schema)}.${quoteIdent(name)}`;
|
|
444
|
+
const rows = [];
|
|
445
|
+
for (const role of customRoles) {
|
|
446
|
+
const at = customRoleAt(role, scopes);
|
|
447
|
+
if (at.scope === void 0) continue;
|
|
448
|
+
for (const grant of role.grants ?? []) rows.push({
|
|
449
|
+
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`,
|
|
450
|
+
values: [
|
|
451
|
+
role.tenant ?? null,
|
|
452
|
+
at.scope,
|
|
453
|
+
at.id ?? null,
|
|
454
|
+
role.name,
|
|
455
|
+
grant.permission,
|
|
456
|
+
grant.effect ?? "allow",
|
|
457
|
+
...grant.level === void 0 ? [] : [grant.level]
|
|
458
|
+
]
|
|
459
|
+
});
|
|
460
|
+
for (const name of role.includes ?? []) rows.push({
|
|
461
|
+
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`,
|
|
462
|
+
values: [
|
|
463
|
+
role.tenant ?? null,
|
|
464
|
+
at.scope,
|
|
465
|
+
at.id ?? null,
|
|
466
|
+
role.name,
|
|
467
|
+
name
|
|
468
|
+
]
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
return rows;
|
|
472
|
+
}
|
|
473
|
+
function seedsCustomRoleTables(config) {
|
|
474
|
+
const rls = config.rls;
|
|
475
|
+
return rls?.customRoles === true && (rls.authorize ?? rls.rbac?.authorize ?? "jwt") === "database";
|
|
476
|
+
}
|
|
477
|
+
function customRoleSchema(config) {
|
|
478
|
+
return config.rls?.schema ?? config.rls?.rbac?.schema ?? "permdock";
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* A pgTAP script that runs each fixture's statement under the fixture's
|
|
482
|
+
* subject and asserts the outcome `can()` gave in-process.
|
|
483
|
+
*/
|
|
484
|
+
function emitPgtap(input) {
|
|
485
|
+
const scopes = scopeList(input.policy.scopes);
|
|
486
|
+
const subject = subjectOf(input.config, input.customRoles, scopes);
|
|
487
|
+
const lines = [
|
|
488
|
+
"begin;",
|
|
489
|
+
`select plan(${String(input.fixtures.length)});`,
|
|
490
|
+
PGTAP_DECISION_SQL
|
|
491
|
+
];
|
|
492
|
+
if (seedsCustomRoleTables(input.config)) for (const row of customRoleRows(customRoleSchema(input.config), input.customRoles, scopes)) lines.push(`${inlineStatement(row)};`);
|
|
493
|
+
for (const [index, fixture] of input.fixtures.entries()) {
|
|
494
|
+
const permission = findPermission(input.policy.permissions, fixture.action);
|
|
495
|
+
const status = input.inProcess[index];
|
|
496
|
+
const label = `fixture ${String(index)} ${fixture.action}`;
|
|
497
|
+
if (permission === void 0 || status === void 0) continue;
|
|
498
|
+
if (status.kind === "opaque") {
|
|
499
|
+
lines.push(`select skip(${quoteLiteral(`${label}: opaque grant untestable app-side`)}, 1);`);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const outcome = status.granted ? "granted" : "denied";
|
|
503
|
+
const table = input.config.rls?.tables?.[permission.resource] ?? permission.resource;
|
|
504
|
+
const statement = inlineStatement(statementFor(fixture, permission.action, table));
|
|
505
|
+
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;");
|
|
506
|
+
}
|
|
507
|
+
lines.push("select * from finish();", "rollback;");
|
|
508
|
+
return `${lines.join("\n")}\n`;
|
|
509
|
+
}
|
|
510
|
+
/** Columns of `row` that hold a value; a masked column reads as null. */
|
|
511
|
+
function valuedColumns(row) {
|
|
512
|
+
return isRecord(row) ? Object.keys(row).filter((name) => row[name] !== null && row[name] !== void 0).toSorted() : [];
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* What a field view returns for a row: nothing for a denied row, otherwise
|
|
516
|
+
* `pick`'s columns with a value plus the key, which always passes through.
|
|
517
|
+
*/
|
|
518
|
+
function expectedFields(granted, picked, key, row) {
|
|
519
|
+
if (!granted) return [];
|
|
520
|
+
const kept = new Set(valuedColumns(picked));
|
|
521
|
+
if (isRecord(row) && row[key] !== null && row[key] !== void 0) kept.add(key);
|
|
522
|
+
return [...kept].toSorted();
|
|
523
|
+
}
|
|
524
|
+
function fieldsMismatch(fixture, want, seen) {
|
|
525
|
+
const kept = want.join(", ");
|
|
526
|
+
if (typeof seen === "string") return [`${fixture.action}: field view read failed (${seen}), pick keeps [${kept}]`];
|
|
527
|
+
return seen.join(", ") === kept ? [] : [`${fixture.action} ${String(rowId(fixture.row))}: field view returns [${seen.join(", ")}], pick keeps [${kept}]`];
|
|
528
|
+
}
|
|
529
|
+
/** The field view's columns for the fixture's row: the view, or the table itself when no grant limits its fields. */
|
|
530
|
+
async function viewFields(query, table, key) {
|
|
531
|
+
const view = quoteTable(viewName(table, FIELD_VIEWS.view));
|
|
532
|
+
const id = quoteIdent("id");
|
|
533
|
+
await query("savepoint permdock_fields");
|
|
534
|
+
const result = await query(`select * from ${view} where ${id} = $1`, [key]);
|
|
535
|
+
if (result.code === void 0) return valuedColumns(result.rows[0]);
|
|
536
|
+
await query("rollback to savepoint permdock_fields");
|
|
537
|
+
if (result.code !== "42P01") return result.code;
|
|
538
|
+
const base = await query(`select * from ${quoteTable(table)} where ${id} = $1`, [key]);
|
|
539
|
+
return base.code ?? valuedColumns(base.rows[0]);
|
|
540
|
+
}
|
|
541
|
+
/** Writes the custom roles into the `database`-mode tables; the fixture transaction rolls them back. */
|
|
542
|
+
async function seedCustomRoles(query, schema, customRoles, scopes) {
|
|
543
|
+
for (const row of customRoleRows(schema, customRoles, scopes)) {
|
|
544
|
+
const result = await query(row.sql, row.values);
|
|
545
|
+
if (result.code !== void 0) throw new Error(`PermDock CLI: rls verify --db could not seed custom roles (${result.code}); connect as a role that owns the custom_role_* tables`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function bindSubject(query, fixture, subject) {
|
|
549
|
+
await query("set local role \"authenticated\"");
|
|
550
|
+
for (const [name, value] of subjectSettings(fixture, subject)) await query("select set_config($1, $2, true)", [name, value]);
|
|
551
|
+
}
|
|
552
|
+
async function verifyAgainstDatabase(input) {
|
|
553
|
+
const client = await input.connect(input.db);
|
|
554
|
+
const seedsTables = seedsCustomRoleTables(input.config);
|
|
555
|
+
const schema = customRoleSchema(input.config);
|
|
556
|
+
const scopes = scopeList(input.policy.scopes);
|
|
557
|
+
const subject = subjectOf(input.config, input.customRoles, scopes);
|
|
558
|
+
const query = async (sql, values) => {
|
|
559
|
+
try {
|
|
560
|
+
const result = await client.query(sql, values === void 0 ? [] : [...values]);
|
|
561
|
+
return {
|
|
562
|
+
rows: result.rows,
|
|
563
|
+
rowCount: result.rowCount ?? result.rows.length
|
|
564
|
+
};
|
|
565
|
+
} catch (cause) {
|
|
566
|
+
const code = cause !== null && typeof cause === "object" && "code" in cause && typeof cause.code === "string" ? cause.code : void 0;
|
|
567
|
+
return {
|
|
568
|
+
rows: [],
|
|
569
|
+
rowCount: 0,
|
|
570
|
+
...code === void 0 ? {} : { code }
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
const mismatches = [];
|
|
575
|
+
const notes = [];
|
|
576
|
+
try {
|
|
577
|
+
const conditioned = policyRowConditionKeys(input.policy);
|
|
578
|
+
for (const found of helperTablePoliciesFromRows((await query(HELPER_TABLE_POLICIES_SQL)).rows)) {
|
|
579
|
+
const keys = found.keys.filter((key) => conditioned.has(key));
|
|
580
|
+
if (keys.length > 0) mismatches.push(`PD037 ${rowConditionMessage(found, keys)}`);
|
|
581
|
+
}
|
|
582
|
+
for (const [index, fixture] of input.fixtures.entries()) {
|
|
583
|
+
const permission = findPermission(input.policy.permissions, fixture.action);
|
|
584
|
+
const status = input.inProcess[index];
|
|
585
|
+
if (permission === void 0 || status === void 0) continue;
|
|
586
|
+
if (status.kind === "opaque") {
|
|
587
|
+
notes.push(`${fixture.action}: opaque grant untestable app-side`);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const table = input.config.rls?.tables?.[permission.resource] ?? permission.resource;
|
|
591
|
+
await query("begin");
|
|
592
|
+
try {
|
|
593
|
+
if (seedsTables) await seedCustomRoles(query, schema, input.customRoles, scopes);
|
|
594
|
+
await bindSubject(query, fixture, subject);
|
|
595
|
+
const statement = statementFor(fixture, permission.action, table);
|
|
596
|
+
const result = await query(statement.sql, statement.values);
|
|
597
|
+
const count = result.rowCount ?? result.rows.length;
|
|
598
|
+
const database = result.code === "42501" ? "rejected" : count > 0 ? "allowed" : "filtered";
|
|
599
|
+
if (!(status.granted ? database === "allowed" : database === "filtered" || database === "rejected")) mismatches.push(`${fixture.action}: in-process ${status.granted ? "granted" : "denied"}, database ${database}`);
|
|
600
|
+
else if (status.kind === "sqlFunction") notes.push(`${fixture.action}: verified through twin`);
|
|
601
|
+
mismatches.push(...status.fields === void 0 ? [] : fieldsMismatch(fixture, status.fields, await viewFields(query, table, rowId(fixture.row))));
|
|
602
|
+
} finally {
|
|
603
|
+
await query("rollback");
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
} finally {
|
|
607
|
+
await client.end();
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
mismatches,
|
|
611
|
+
notes
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
/** `--tree`: seeds a generated object graph in a rolled-back transaction and compares `can()` with RLS. */
|
|
615
|
+
async function verifyTreeAgainstDatabase(db, policy, config, connect) {
|
|
616
|
+
const client = await connect(db);
|
|
617
|
+
const scopes = scopeList(policy.scopes);
|
|
618
|
+
const query = async (sql, values) => {
|
|
619
|
+
return { rows: (await client.query(sql, values === void 0 ? [] : [...values])).rows };
|
|
620
|
+
};
|
|
621
|
+
const loose = async (sql, values) => {
|
|
622
|
+
await query(sql, values);
|
|
623
|
+
return { rows: [] };
|
|
624
|
+
};
|
|
625
|
+
try {
|
|
626
|
+
await query("begin");
|
|
627
|
+
const result = await verifyTree({
|
|
628
|
+
policy,
|
|
629
|
+
config,
|
|
630
|
+
query,
|
|
631
|
+
bind: (subject) => bindSubject(loose, {
|
|
632
|
+
subject: { id: subject },
|
|
633
|
+
row: {},
|
|
634
|
+
action: "read"
|
|
635
|
+
}, subjectOf(config, [], scopes))
|
|
636
|
+
});
|
|
637
|
+
if (result.mismatches.length > 0) return {
|
|
638
|
+
code: 1,
|
|
639
|
+
output: [...result.mismatches, ...result.notes].join("\n")
|
|
640
|
+
};
|
|
641
|
+
const verified = `verified ${String(result.checked)} tree check(s) against the database (${String(result.granted)} granted)`;
|
|
642
|
+
return {
|
|
643
|
+
code: 0,
|
|
644
|
+
output: result.notes.length === 0 ? verified : `${verified}\n${result.notes.join("\n")}`
|
|
645
|
+
};
|
|
646
|
+
} catch (cause) {
|
|
647
|
+
return {
|
|
648
|
+
code: 2,
|
|
649
|
+
output: `PermDock CLI: rls verify --tree could not seed the tree (${describeError(cause)}); connect as a role that owns the tables and is a member of authenticated`
|
|
650
|
+
};
|
|
651
|
+
} finally {
|
|
652
|
+
await client.query("rollback", []).catch(() => void 0);
|
|
653
|
+
await client.end();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function runRlsVerify(input) {
|
|
657
|
+
const connect = input.connect ?? ((db) => connectPg(db, "permdock rls verify --db"));
|
|
658
|
+
const policyPath = input.from ?? input.config.policy;
|
|
659
|
+
if (policyPath === void 0) return {
|
|
660
|
+
code: 2,
|
|
661
|
+
output: "PermDock CLI: rls verify needs policy in the config or --from"
|
|
662
|
+
};
|
|
663
|
+
const policy = asPolicy(pickNamed(await loadModule(resolve(input.cwd, policyPath)), ["policy"]));
|
|
664
|
+
if (input.tree === true) {
|
|
665
|
+
if (input.db === void 0) return {
|
|
666
|
+
code: 2,
|
|
667
|
+
output: "PermDock CLI: rls verify --tree needs --db"
|
|
668
|
+
};
|
|
669
|
+
try {
|
|
670
|
+
return await verifyTreeAgainstDatabase(input.db, policy, input.config, connect);
|
|
671
|
+
} catch (cause) {
|
|
672
|
+
return usageResult(cause);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const fixturesPath = input.fixtures ?? input.config.rls?.fixtures ?? "rls.fixtures.json";
|
|
676
|
+
const { fixtures, customRoles } = await loadFixtures(input.cwd, fixturesPath);
|
|
677
|
+
const mismatches = [];
|
|
678
|
+
const notes = [];
|
|
679
|
+
const inProcess = [];
|
|
680
|
+
const fieldsMode = input.config.rls?.fields === "views";
|
|
681
|
+
for (const fixture of fixtures) {
|
|
682
|
+
const permission = findPermission(policy.permissions, fixture.action);
|
|
683
|
+
if (permission === void 0) {
|
|
684
|
+
mismatches.push(`${fixture.action}: unknown permission`);
|
|
685
|
+
inProcess.push({
|
|
686
|
+
action: fixture.action,
|
|
687
|
+
granted: false,
|
|
688
|
+
kind: "portable"
|
|
689
|
+
});
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const kind = grantKind(policy, fixture.action);
|
|
693
|
+
const permdock = await createPermDock(policy, fixtureSubject(fixture.subject), { customRoles: memoryRoleSource(customRoles) });
|
|
694
|
+
const granted = canFixture(permdock, permission, fixtureRow(fixture, permission.kind));
|
|
695
|
+
const outcome = granted ? "granted" : "denied";
|
|
696
|
+
if (fixture.expected !== void 0 && fixture.expected !== outcome) mismatches.push(`${fixture.action}: in-process ${outcome}, expected ${fixture.expected}`);
|
|
697
|
+
if (kind === "opaque") notes.push(`${fixture.action}: opaque grant untestable app-side`);
|
|
698
|
+
const fields = fieldsMode && permission.kind === "instance" && commandFor(permission.action, input.config.rls?.actions) === "select" ? expectedFields(granted, permdock.pick(permission, fixture.row), policy.resources.get(permission.resource)?.id ?? "id", fixture.row) : void 0;
|
|
699
|
+
inProcess.push({
|
|
700
|
+
action: fixture.action,
|
|
701
|
+
granted,
|
|
702
|
+
kind,
|
|
703
|
+
...fields === void 0 ? {} : { fields }
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
if (input.format === "pgtap") return mismatches.length > 0 ? {
|
|
707
|
+
code: 1,
|
|
708
|
+
output: mismatches.join("\n")
|
|
709
|
+
} : {
|
|
710
|
+
code: 0,
|
|
711
|
+
output: emitPgtap({
|
|
712
|
+
fixtures,
|
|
713
|
+
inProcess,
|
|
714
|
+
policy,
|
|
715
|
+
config: input.config,
|
|
716
|
+
customRoles
|
|
717
|
+
})
|
|
718
|
+
};
|
|
719
|
+
if (input.db !== void 0) try {
|
|
720
|
+
const database = await verifyAgainstDatabase({
|
|
721
|
+
db: input.db,
|
|
722
|
+
policy,
|
|
723
|
+
fixtures,
|
|
724
|
+
customRoles,
|
|
725
|
+
config: input.config,
|
|
726
|
+
inProcess,
|
|
727
|
+
connect
|
|
728
|
+
});
|
|
729
|
+
mismatches.push(...database.mismatches);
|
|
730
|
+
notes.push(...database.notes);
|
|
731
|
+
} catch (cause) {
|
|
732
|
+
return usageResult(cause);
|
|
733
|
+
}
|
|
734
|
+
if (mismatches.length > 0) return {
|
|
735
|
+
code: 1,
|
|
736
|
+
output: [...mismatches, ...notes].join("\n")
|
|
737
|
+
};
|
|
738
|
+
const verified = `verified ${String(fixtures.length)} fixture(s) ${input.db === void 0 ? "in-process" : "in-process and against the database"}`;
|
|
739
|
+
return {
|
|
740
|
+
code: 0,
|
|
741
|
+
output: notes.length === 0 ? verified : `${verified}\n${notes.join("\n")}`
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
//#endregion
|
|
745
|
+
export { runRlsVerify as n, canFixture as t };
|