permdock 0.3.1 → 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-C9chp3ST.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-CYj-mX2O.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-CfwZ39Rl.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-CHB22Sx4.js → collect-CQx4hX4N.js} +3 -3
- package/dist/{commands-BeaGNj_o.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-ir40y8m0.js → config-DLt6BvV7.js} +2 -2
- package/dist/{config-report-CdCgbRnV.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-C9RDjjhs.js → diff-Cnghh_Ip.js} +2 -2
- package/dist/{diff-2VaeChzX.js → diff-rpqoe7Ag.js} +9 -9
- package/dist/{doctor-Djh6JDEk.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-4lLW_pbb.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-CQTsgKoB.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-DcYlZ09B.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-CZOMlwHl.js → permission-keys-B26jeyUv.js} +1268 -2053
- 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-DE19b9dT.js → powersync-BFSbSElQ.js} +2 -2
- package/dist/powersync-DCDgZgfZ.js +2 -0
- package/dist/{powersync-Dv1_aWF7.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-P-BeBx0n.js → rls-55DQTuf5.js} +14 -2
- package/dist/{rls-DiinlRML.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-BWc9qqvE.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-C5CbhhpG.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-s4o9s1O1.js → supabase-Dbqr141s.js} +2 -2
- package/dist/supabase-hook-BULg7Ut_.js +2 -0
- package/dist/{supabase-hook-ntETYCQu.js → supabase-hook-Cva3ikXz.js} +730 -781
- package/dist/{supautils-u8jXTGrt.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-B0YuTQRO.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-CMv9H3wV.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-DvH0IQff.js +0 -1194
- package/dist/doctor-collect-CjdO0wQX.js +0 -665
- 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-CrBX8hY6.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-Bj3iDH2I.js +0 -472
- package/dist/supabase-hook-gd3s8bxB.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,27 +1,31 @@
|
|
|
1
|
-
import { n as isReadonlyArray, r as
|
|
1
|
+
import { a as sole, n as isReadonlyArray, r as isRecord, t as compact } from "./compact-CAf557ZB.js";
|
|
2
2
|
import { n as freezeDeep, t as freezeCopy } from "./freeze-CrU8nQeX.js";
|
|
3
|
-
import {
|
|
4
|
-
import { a as hasConditionOp } from "./refs-
|
|
5
|
-
import { d as
|
|
6
|
-
import { n as PermDockValidationError } from "./validation-error-
|
|
7
|
-
import { a as wireDenials } from "./problem-details-
|
|
8
|
-
import { a as deniedMessage, i as approvalMessage, n as PermDockDeniedError, t as PermDockApprovalRequiredError } from "./errors-
|
|
3
|
+
import { n as isThenable, o as isForbiddenKey, s as ownGet, t as ignoreRejection } from "./thenable-QSmvt-vZ.js";
|
|
4
|
+
import { a as hasConditionOp } from "./refs-f3imMjNW.js";
|
|
5
|
+
import { d as assertApprovalRelations, l as applyQuota, m as normalizeWhere, n as role, p as normalizeApproval, t as normalizeAssurance } from "./role-builder-B4GbKM-i.js";
|
|
6
|
+
import { n as PermDockValidationError } from "./validation-error-ZfUHbKvf.js";
|
|
7
|
+
import { a as wireDenials } from "./problem-details-DECUnXAn.js";
|
|
8
|
+
import { a as deniedMessage, i as approvalMessage, n as PermDockDeniedError, t as PermDockApprovalRequiredError } from "./errors-drnfxYtm.js";
|
|
9
9
|
import { n as sameId } from "./ids-DC4V4CFu.js";
|
|
10
|
-
import {
|
|
11
|
-
import { a as
|
|
10
|
+
import { i as readAppData } from "./app-data-CV8pqqMi.js";
|
|
11
|
+
import { a as normalizeMemberships, c as rootScope, d as scopeList, f as subjectForPermission, i as keepsPermission, l as scopeChain, m as tenantOf, o as resolveScope, s as rootMembershipId, u as scopeIdOf } from "./scopes-tvpa34ep.js";
|
|
12
|
+
import { _ as evaluateCondition, a as whereFromGrants, c as coversRequirements, d as isRowPair, f as rowIdOf, g as simulateArazzo, h as isArazzoSimulateInput, i as signSnapshot, l as delegatedPermissions, p as rowValues, r as snapshotGrant, s as coveredByDelegation, t as buildSnapshot, u as requiresStandIn } from "./snapshot-Dc9CHPME.js";
|
|
12
13
|
import { t as parseDuration } from "./duration-xK2jGFVc.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { a as
|
|
14
|
+
import { a as getResource, c as listPermissions, g as formerScopes, h as formerKeys, n as findPermission } from "./permission-registry-BsblpB-z.js";
|
|
15
|
+
import { a as expandRelation, c as isEdgeRelation, l as isFieldRelation, u as isPrincipalRelation } from "./resource-relations-D1Un3pcz.js";
|
|
16
|
+
import { a as authenticated, b as synthesiseRole, g as isRole, i as assurance, m as findRole, o as flattenGrantee, r as asGrantee, v as listRoles, x as treeXSchema, y as sourceRoleMeta } from "./grantee-CXY-0C87.js";
|
|
17
|
+
import { a as sanitizeContext, i as pickVisible, r as grantCoversField, t as isActive } from "./validity-dVrUEPJt.js";
|
|
18
|
+
import { t as canonicalJson } from "./canonical-json-CFKyFOke.js";
|
|
19
|
+
import { o as resourceRoleCondition, r as relationCondition } from "./grantee-conditions-MQs5NIuX.js";
|
|
20
|
+
import { c as nowSeconds, d as resolveActiveTenant, i as matchScopedMembership, l as partitionsOf, m as tenantsOf, n as inTeam, p as scopeField, r as isMembershipExpired, t as heldRoleNamesIn, u as relatesTo } from "./tenancy-DshjqY0n.js";
|
|
21
|
+
import { a as declaredRoleNames, c as levelCondition, f as policyX, l as levelNames, r as mergeHostedGrants, s as grantList, t as isPortableCondition } from "./hosted-CBHgkzDl.js";
|
|
17
22
|
import { t as requiresApproval } from "./approval-required-TzNB9oQA.js";
|
|
18
|
-
import { r as mergeHostedGrants, t as isPortableCondition } from "./hosted-DJQw46hn.js";
|
|
19
|
-
import { n as isThenable, t as ignoreRejection } from "./thenable-okc2xDsj.js";
|
|
20
23
|
import { t as byCodePoint } from "./compare-pfyVnBjE.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
24
|
+
import { n as relationId, r as relationReader, t as pendingRelations } from "./relations-CvmSpG67.js";
|
|
25
|
+
import { n as matchGrantee, t as combineWhere } from "./grantee-match-fxYp9Nmt.js";
|
|
23
26
|
import { t as resourceIdOf } from "./resource-ref-CaS9yVPi.js";
|
|
24
|
-
import { n as payloadDigest, r as versionOf, t as decisionToken } from "./token-
|
|
27
|
+
import { n as payloadDigest, r as versionOf, t as decisionToken } from "./token-Bx9qy30p.js";
|
|
28
|
+
import { n as resolveRelated, t as relationWalk } from "./relation-walk-BXnlLtg2.js";
|
|
25
29
|
import { i as isSubject, r as isPrincipal, t as anonymousSubject } from "./subject-C6dLxqHU.js";
|
|
26
30
|
//#region src/core/approval-policies.ts
|
|
27
31
|
/** The denial detail when an `ApprovalPolicySource` fails: every call an allow would grant denies with reason `approval`. */
|
|
@@ -32,9 +36,6 @@ function memoryApprovalPolicies(policies) {
|
|
|
32
36
|
return entries.filter((entry) => entry.tenant === void 0 || query.tenants.includes(entry.tenant));
|
|
33
37
|
} });
|
|
34
38
|
}
|
|
35
|
-
function isRecord(value) {
|
|
36
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
37
|
-
}
|
|
38
39
|
function isStringList(value) {
|
|
39
40
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
40
41
|
}
|
|
@@ -458,7 +459,7 @@ function resolveCustomRole(policy, role) {
|
|
|
458
459
|
const ceiling = ceilingGrants(policy, scope);
|
|
459
460
|
const ceilingSet = new Set(ceiling);
|
|
460
461
|
const ceilingKeys = new Set(ceiling.map((grant) => grant.permission.key));
|
|
461
|
-
const declared = declaredRoleNames
|
|
462
|
+
const declared = declaredRoleNames(policy);
|
|
462
463
|
const all = grantList(policy).filter((grant) => grant.hosted === void 0);
|
|
463
464
|
const dropped = [];
|
|
464
465
|
const renamed = [];
|
|
@@ -607,7 +608,7 @@ function wellFormed(role) {
|
|
|
607
608
|
}
|
|
608
609
|
/** Every custom role's resolved grants, once per instance. Malformed roles resolve to nothing. */
|
|
609
610
|
function customGrantsFor(policy, roles) {
|
|
610
|
-
const declared = declaredRoleNames
|
|
611
|
+
const declared = declaredRoleNames(policy);
|
|
611
612
|
const out = [];
|
|
612
613
|
for (const role of roles) {
|
|
613
614
|
if (!wellFormed(role) || declared.has(role.name)) continue;
|
|
@@ -620,7 +621,7 @@ function customGrantsFor(policy, roles) {
|
|
|
620
621
|
}
|
|
621
622
|
/** The well-formed tenant (not platform) custom roles; a declared role name is never one. */
|
|
622
623
|
function tenantCustomRoles(policy, roles) {
|
|
623
|
-
const declared = declaredRoleNames
|
|
624
|
+
const declared = declaredRoleNames(policy);
|
|
624
625
|
return roles.filter((role) => wellFormed(role) && !isGlobalCustomRole(role) && !declared.has(role.name));
|
|
625
626
|
}
|
|
626
627
|
/** A membership at the role's scope, inside its tenant (and instance, when pinned), naming it. */
|
|
@@ -893,6 +894,63 @@ function activate(policy, subject, input, now = nowSeconds()) {
|
|
|
893
894
|
});
|
|
894
895
|
}
|
|
895
896
|
//#endregion
|
|
897
|
+
//#region src/core/evaluation-alternatives.ts
|
|
898
|
+
function alternativesFor(evaluate, policy, permission, subject, now, env) {
|
|
899
|
+
return listPermissions(policy.permissions).filter((leaf) => leaf.resource === permission.resource && leaf.key !== permission.key).filter((leaf) => {
|
|
900
|
+
return evaluate(policy, subject, leaf, void 0, {
|
|
901
|
+
trusted: true,
|
|
902
|
+
source: "simulate",
|
|
903
|
+
now
|
|
904
|
+
}, {
|
|
905
|
+
...env,
|
|
906
|
+
emit: false,
|
|
907
|
+
skipAlternatives: true,
|
|
908
|
+
outcomeOnly: true
|
|
909
|
+
}).outcome === "granted";
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
function inheritedVerdict(evaluate, policy, subject, resource, id, key, now, env) {
|
|
913
|
+
const leaf = findPermission(policy.permissions, key);
|
|
914
|
+
const loaded = env.relations?.row({
|
|
915
|
+
resource,
|
|
916
|
+
id
|
|
917
|
+
});
|
|
918
|
+
if (leaf === void 0 || loaded === void 0) return "relation-unavailable";
|
|
919
|
+
if (loaded === "pending" || loaded === "failed") return "relation-unavailable";
|
|
920
|
+
if (loaded.row === null) return false;
|
|
921
|
+
const decision = evaluate(policy, subject, leaf, loaded.row, {
|
|
922
|
+
trusted: true,
|
|
923
|
+
source: "simulate",
|
|
924
|
+
now
|
|
925
|
+
}, {
|
|
926
|
+
...env,
|
|
927
|
+
emit: false,
|
|
928
|
+
skipAlternatives: true,
|
|
929
|
+
outcomeOnly: true
|
|
930
|
+
});
|
|
931
|
+
if (decision.outcome === "granted") return true;
|
|
932
|
+
const reasons = decision.outcome === "denied" ? decision.denials.map((denial) => denial.reason) : [];
|
|
933
|
+
return reasons.includes("relation-unavailable") ? "relation-unavailable" : reasons.includes("relation-depth") ? "relation-depth" : false;
|
|
934
|
+
}
|
|
935
|
+
//#endregion
|
|
936
|
+
//#region src/core/evaluation-trace.ts
|
|
937
|
+
function skip(tracer, grant, permissionKey, why) {
|
|
938
|
+
tracer?.skipped.push({
|
|
939
|
+
role: grant.role,
|
|
940
|
+
permission: permissionKey,
|
|
941
|
+
effect: grant.effect,
|
|
942
|
+
why
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
function traceOf(tracer) {
|
|
946
|
+
return {
|
|
947
|
+
evaluated: tracer.evaluated,
|
|
948
|
+
allows: tracer.allows,
|
|
949
|
+
denies: tracer.denies,
|
|
950
|
+
skipped: tracer.skipped
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
//#endregion
|
|
896
954
|
//#region src/core/events.ts
|
|
897
955
|
function emitSafe(listeners, payload, errors) {
|
|
898
956
|
for (const listener of listeners) try {
|
|
@@ -958,15 +1016,20 @@ function finish(policy, subject, permission, data, decision, options, env, trust
|
|
|
958
1016
|
matched: decision.outcome === "granted" ? compact({
|
|
959
1017
|
role: decision.matched.role,
|
|
960
1018
|
permission: decision.matched.permission,
|
|
1019
|
+
name: decision.matched.name,
|
|
1020
|
+
meta: decision.matched.meta,
|
|
961
1021
|
to: decision.matched.to,
|
|
962
1022
|
hosted: decision.matched.hosted,
|
|
963
1023
|
breakGlass: decision.matched.breakGlass
|
|
964
1024
|
}) : decision.outcome === "approval-required" ? compact({
|
|
965
1025
|
role: decision.grant.role,
|
|
966
1026
|
permission: decision.grant.permission,
|
|
1027
|
+
name: decision.grant.name,
|
|
1028
|
+
meta: decision.grant.meta,
|
|
967
1029
|
to: decision.grant.to,
|
|
968
1030
|
hosted: decision.grant.hosted
|
|
969
1031
|
}) : void 0,
|
|
1032
|
+
obligations: decision.outcome === "granted" && (decision.obligations?.length ?? 0) > 0 ? decision.obligations : void 0,
|
|
970
1033
|
purpose: Array.isArray(subject.context["purpose"]) && subject.context["purpose"].length > 0 ? subject.context["purpose"] : void 0,
|
|
971
1034
|
reason: typeof subject.context["reason"] === "string" && subject.context["reason"] !== "" ? subject.context["reason"] : void 0,
|
|
972
1035
|
denials: decision.outcome === "denied" ? wireDenials(decision.denials) : void 0,
|
|
@@ -990,59 +1053,132 @@ function finish(policy, subject, permission, data, decision, options, env, trust
|
|
|
990
1053
|
}
|
|
991
1054
|
}
|
|
992
1055
|
//#endregion
|
|
993
|
-
//#region src/core/
|
|
994
|
-
function
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1056
|
+
//#region src/core/grant-conditions.ts
|
|
1057
|
+
function evaluateGrantCondition(grant, permission, current, next, subject, now, scopes, relations, permitted) {
|
|
1058
|
+
let unknown;
|
|
1059
|
+
const onOpaque = () => {
|
|
1060
|
+
unknown ??= "opaque-condition";
|
|
1061
|
+
};
|
|
1062
|
+
const related = (condition, row) => {
|
|
1063
|
+
if (relations === void 0) {
|
|
1064
|
+
unknown ??= "relation-unavailable";
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
const verdict = resolveRelated(condition, row, subject, now, relations, permitted);
|
|
1068
|
+
if (verdict === "relation-depth" || verdict === "relation-unavailable") {
|
|
1069
|
+
unknown ??= verdict;
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
return verdict;
|
|
1073
|
+
};
|
|
1074
|
+
if (!grant.portable && grant.closure !== void 0) try {
|
|
1075
|
+
const result = grant.closure(next ?? current, {
|
|
1076
|
+
subject,
|
|
1077
|
+
actor: subject.actor,
|
|
1078
|
+
delegation: subject.delegation,
|
|
1079
|
+
context: subject.context
|
|
1003
1080
|
});
|
|
1004
|
-
return
|
|
1081
|
+
if (isThenable(result)) return {
|
|
1082
|
+
matched: false,
|
|
1083
|
+
reason: "closure-error",
|
|
1084
|
+
cause: result
|
|
1085
|
+
};
|
|
1086
|
+
return { matched: result === true };
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
return {
|
|
1089
|
+
matched: false,
|
|
1090
|
+
reason: "closure-error",
|
|
1091
|
+
cause: error
|
|
1092
|
+
};
|
|
1005
1093
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1094
|
+
if (grant.where !== void 0) {
|
|
1095
|
+
if (permission.kind === "collection") return {
|
|
1096
|
+
matched: false,
|
|
1097
|
+
reason: "condition"
|
|
1098
|
+
};
|
|
1099
|
+
if (current === void 0) return {
|
|
1100
|
+
matched: false,
|
|
1101
|
+
reason: "condition"
|
|
1102
|
+
};
|
|
1103
|
+
if (grant.where.op === "opaque" || grant.check?.op === "opaque") return {
|
|
1104
|
+
matched: false,
|
|
1105
|
+
reason: "opaque-condition"
|
|
1106
|
+
};
|
|
1107
|
+
const matched = evaluateCondition(grant.where, current, subject, now, scopes, related, onOpaque);
|
|
1108
|
+
if (unknown !== void 0) return {
|
|
1109
|
+
matched: false,
|
|
1110
|
+
reason: unknown
|
|
1111
|
+
};
|
|
1112
|
+
if (!matched) return {
|
|
1113
|
+
matched: false,
|
|
1114
|
+
reason: "condition"
|
|
1115
|
+
};
|
|
1016
1116
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1117
|
+
const checkCondition = grant.check ?? (permission.action === "update" ? grant.where : void 0);
|
|
1118
|
+
if (checkCondition !== void 0) {
|
|
1119
|
+
if (checkCondition.op === "opaque") return {
|
|
1120
|
+
matched: false,
|
|
1121
|
+
reason: "opaque-condition"
|
|
1122
|
+
};
|
|
1123
|
+
if (next === void 0) return {
|
|
1124
|
+
matched: false,
|
|
1125
|
+
reason: "condition"
|
|
1126
|
+
};
|
|
1127
|
+
const matched = evaluateCondition(checkCondition, next, subject, now, scopes, related, onOpaque);
|
|
1128
|
+
if (unknown !== void 0) return {
|
|
1129
|
+
matched: false,
|
|
1130
|
+
reason: unknown
|
|
1131
|
+
};
|
|
1132
|
+
if (!matched) return {
|
|
1133
|
+
matched: false,
|
|
1134
|
+
reason: "condition"
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
return { matched: true };
|
|
1026
1138
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1139
|
+
/** A grant whose condition could not be answered; a deny with one denies. */
|
|
1140
|
+
function isUnevaluable(reason) {
|
|
1141
|
+
return reason === "relation-depth" || reason === "relation-unavailable" || reason === "closure-error" || reason === "opaque-condition";
|
|
1142
|
+
}
|
|
1143
|
+
function shouldConsumeQuota(source, simulated) {
|
|
1144
|
+
if (simulated) return false;
|
|
1145
|
+
switch (source) {
|
|
1146
|
+
case "can":
|
|
1147
|
+
case "filter":
|
|
1148
|
+
case "simulate":
|
|
1149
|
+
case "explain": return false;
|
|
1150
|
+
case "decide":
|
|
1151
|
+
case "assert":
|
|
1152
|
+
case "endpoint":
|
|
1153
|
+
case "adapter":
|
|
1154
|
+
case "approval":
|
|
1155
|
+
case void 0: return true;
|
|
1156
|
+
default: return source;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
function isDelegatedPermission(policy, permission) {
|
|
1160
|
+
const providers = policy.providers;
|
|
1161
|
+
if (providers === void 0 || providers.length === 0) return false;
|
|
1162
|
+
for (const provider of providers) if (provider.handles(permission)) return true;
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
/** The `MatchedGrant` view of a grant, shared by `matched` and the trace. */
|
|
1166
|
+
function matchedOf(grant, permissionKey, breakGlass) {
|
|
1167
|
+
return compact({
|
|
1168
|
+
role: grant.role,
|
|
1169
|
+
permission: permissionKey,
|
|
1170
|
+
name: grant.name,
|
|
1171
|
+
meta: grant.meta,
|
|
1172
|
+
to: grant.to,
|
|
1173
|
+
where: grant.where,
|
|
1174
|
+
check: grant.check,
|
|
1175
|
+
approval: grant.approval,
|
|
1176
|
+
hosted: grant.hosted,
|
|
1177
|
+
breakGlass
|
|
1031
1178
|
});
|
|
1032
|
-
return `${permission.key}: ${parts.join("; ")}.`;
|
|
1033
1179
|
}
|
|
1034
1180
|
//#endregion
|
|
1035
|
-
//#region src/core/
|
|
1036
|
-
/**
|
|
1037
|
-
* A collection write proposes `next`; an update pair may move `next` out of
|
|
1038
|
-
* the scope `current` is in. Both must stay in scope.
|
|
1039
|
-
*/
|
|
1040
|
-
function matchWriteScope(match, instance, current, next) {
|
|
1041
|
-
const first = match(instance ? current : next);
|
|
1042
|
-
if (!first.ok || !instance || next === current) return first;
|
|
1043
|
-
const moved = match(next);
|
|
1044
|
-
return moved.ok ? first : moved;
|
|
1045
|
-
}
|
|
1181
|
+
//#region src/core/role-loading.ts
|
|
1046
1182
|
function isGlobalRole(value) {
|
|
1047
1183
|
return value !== null && typeof value === "object" && Reflect.get(value, "scope") === "global";
|
|
1048
1184
|
}
|
|
@@ -1136,13 +1272,12 @@ function expandRoleNames(names, declared, custom, tenant) {
|
|
|
1136
1272
|
unknown
|
|
1137
1273
|
};
|
|
1138
1274
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
const NEVER = {
|
|
1275
|
+
//#endregion
|
|
1276
|
+
//#region src/core/requires-conditions.ts
|
|
1277
|
+
const NEVER = freezeDeep({
|
|
1143
1278
|
op: "or",
|
|
1144
1279
|
conditions: []
|
|
1145
|
-
};
|
|
1280
|
+
});
|
|
1146
1281
|
function isUnconditionedAllow(grant, now) {
|
|
1147
1282
|
return grant.where === void 0 && grant.check === void 0 && grant.closure === void 0 && grant.portable && grant.purpose === void 0 && grant.viaOnly === void 0 && grant.requires === void 0 && isActive(grant.validity, now);
|
|
1148
1283
|
}
|
|
@@ -1220,277 +1355,165 @@ function keyRequirement(policy, subject, key, resource, now, customRoles, custom
|
|
|
1220
1355
|
conditions: parts
|
|
1221
1356
|
};
|
|
1222
1357
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
}).outcome === "granted";
|
|
1235
|
-
});
|
|
1236
|
-
}
|
|
1237
|
-
function inheritedVerdict(policy, subject, resource, id, key, now, env) {
|
|
1238
|
-
const leaf = findPermission(policy.permissions, key);
|
|
1239
|
-
const loaded = env.relations?.row({
|
|
1240
|
-
resource,
|
|
1241
|
-
id
|
|
1242
|
-
});
|
|
1243
|
-
if (leaf === void 0 || loaded === void 0) return "relation-unavailable";
|
|
1244
|
-
if (loaded === "pending" || loaded === "failed") return "relation-unavailable";
|
|
1245
|
-
if (loaded.row === null) return false;
|
|
1246
|
-
const decision = evaluate(policy, subject, leaf, loaded.row, {
|
|
1247
|
-
trusted: true,
|
|
1248
|
-
source: "simulate",
|
|
1249
|
-
now
|
|
1250
|
-
}, {
|
|
1251
|
-
...env,
|
|
1252
|
-
emit: false,
|
|
1253
|
-
skipAlternatives: true,
|
|
1254
|
-
outcomeOnly: true
|
|
1255
|
-
});
|
|
1256
|
-
if (decision.outcome === "granted") return true;
|
|
1257
|
-
const reasons = decision.outcome === "denied" ? decision.denials.map((denial) => denial.reason) : [];
|
|
1258
|
-
return reasons.includes("relation-unavailable") ? "relation-unavailable" : reasons.includes("relation-depth") ? "relation-depth" : false;
|
|
1259
|
-
}
|
|
1260
|
-
function evaluateGrantCondition(grant, permission, current, next, subject, now, scopes, relations, permitted) {
|
|
1261
|
-
let unknown;
|
|
1262
|
-
const onOpaque = () => {
|
|
1263
|
-
unknown ??= "opaque-condition";
|
|
1264
|
-
};
|
|
1265
|
-
const related = (condition, row) => {
|
|
1266
|
-
if (relations === void 0) {
|
|
1267
|
-
unknown ??= "relation-unavailable";
|
|
1268
|
-
return false;
|
|
1269
|
-
}
|
|
1270
|
-
const verdict = resolveRelated(condition, row, subject, now, relations, permitted);
|
|
1271
|
-
if (verdict === "relation-depth" || verdict === "relation-unavailable") {
|
|
1272
|
-
unknown ??= verdict;
|
|
1273
|
-
return false;
|
|
1274
|
-
}
|
|
1275
|
-
return verdict;
|
|
1276
|
-
};
|
|
1277
|
-
if (!grant.portable && grant.closure !== void 0) try {
|
|
1278
|
-
const result = grant.closure(next ?? current, {
|
|
1279
|
-
subject,
|
|
1280
|
-
actor: subject.actor,
|
|
1281
|
-
delegation: subject.delegation,
|
|
1282
|
-
context: subject.context
|
|
1358
|
+
//#endregion
|
|
1359
|
+
//#region src/core/validation.ts
|
|
1360
|
+
function validateBoundary(permission, resource, data, mode, trusted, boundary = "manual") {
|
|
1361
|
+
if (!(mode === "always" || mode === "boundary" && !trusted)) return data;
|
|
1362
|
+
if (resource?.schema === void 0) {
|
|
1363
|
+
if (mode === "always") throw new PermDockValidationError({
|
|
1364
|
+
code: "no-schema",
|
|
1365
|
+
permission: permission.key,
|
|
1366
|
+
resource: permission.resource,
|
|
1367
|
+
boundary,
|
|
1368
|
+
message: `${permission.key}: no schema for resource ${permission.resource}.`
|
|
1283
1369
|
});
|
|
1284
|
-
|
|
1285
|
-
matched: false,
|
|
1286
|
-
reason: "closure-error",
|
|
1287
|
-
cause: result
|
|
1288
|
-
};
|
|
1289
|
-
return { matched: result === true };
|
|
1290
|
-
} catch (error) {
|
|
1291
|
-
return {
|
|
1292
|
-
matched: false,
|
|
1293
|
-
reason: "closure-error",
|
|
1294
|
-
cause: error
|
|
1295
|
-
};
|
|
1296
|
-
}
|
|
1297
|
-
if (grant.where !== void 0) {
|
|
1298
|
-
if (permission.kind === "collection") return {
|
|
1299
|
-
matched: false,
|
|
1300
|
-
reason: "condition"
|
|
1301
|
-
};
|
|
1302
|
-
if (current === void 0) return {
|
|
1303
|
-
matched: false,
|
|
1304
|
-
reason: "condition"
|
|
1305
|
-
};
|
|
1306
|
-
if (grant.where.op === "opaque" || grant.check?.op === "opaque") return {
|
|
1307
|
-
matched: false,
|
|
1308
|
-
reason: "opaque-condition"
|
|
1309
|
-
};
|
|
1310
|
-
const matched = evaluateCondition(grant.where, current, subject, now, scopes, related, onOpaque);
|
|
1311
|
-
if (unknown !== void 0) return {
|
|
1312
|
-
matched: false,
|
|
1313
|
-
reason: unknown
|
|
1314
|
-
};
|
|
1315
|
-
if (!matched) return {
|
|
1316
|
-
matched: false,
|
|
1317
|
-
reason: "condition"
|
|
1318
|
-
};
|
|
1370
|
+
return data;
|
|
1319
1371
|
}
|
|
1320
|
-
const
|
|
1321
|
-
if (
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
};
|
|
1330
|
-
const matched = evaluateCondition(checkCondition, next, subject, now, scopes, related, onOpaque);
|
|
1331
|
-
if (unknown !== void 0) return {
|
|
1332
|
-
matched: false,
|
|
1333
|
-
reason: unknown
|
|
1334
|
-
};
|
|
1335
|
-
if (!matched) return {
|
|
1336
|
-
matched: false,
|
|
1337
|
-
reason: "condition"
|
|
1338
|
-
};
|
|
1372
|
+
const result = resource.schema["~standard"].validate(data);
|
|
1373
|
+
if (isThenable(result)) {
|
|
1374
|
+
ignoreRejection(result);
|
|
1375
|
+
throw new PermDockValidationError({
|
|
1376
|
+
code: "async-schema",
|
|
1377
|
+
permission: permission.key,
|
|
1378
|
+
resource: permission.resource,
|
|
1379
|
+
boundary,
|
|
1380
|
+
message: `${permission.key}: schema for ${permission.resource} is async.`
|
|
1381
|
+
});
|
|
1339
1382
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1383
|
+
if ("issues" in result && result.issues !== void 0) throw new PermDockValidationError({
|
|
1384
|
+
code: "invalid-data",
|
|
1385
|
+
permission: permission.key,
|
|
1386
|
+
resource: permission.resource,
|
|
1387
|
+
issues: result.issues,
|
|
1388
|
+
boundary,
|
|
1389
|
+
message: validationMessage(permission, resource, result.issues)
|
|
1390
|
+
});
|
|
1391
|
+
return result.value;
|
|
1345
1392
|
}
|
|
1346
|
-
function
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
case "explain": return false;
|
|
1353
|
-
case "decide":
|
|
1354
|
-
case "assert":
|
|
1355
|
-
case "endpoint":
|
|
1356
|
-
case "adapter":
|
|
1357
|
-
case "approval":
|
|
1358
|
-
case void 0: return true;
|
|
1359
|
-
default: return source;
|
|
1360
|
-
}
|
|
1393
|
+
function validationMessage(permission, resource, issues) {
|
|
1394
|
+
const parts = issues.map((issue) => {
|
|
1395
|
+
const path = issue.path?.map((item) => typeof item === "object" && "key" in item ? String(item.key) : String(item)).join(".") ?? "/";
|
|
1396
|
+
return `invalid ${resource.name} data at ${path}: ${issue.message}`;
|
|
1397
|
+
});
|
|
1398
|
+
return `${permission.key}: ${parts.join("; ")}.`;
|
|
1361
1399
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1400
|
+
//#endregion
|
|
1401
|
+
//#region src/core/evaluate.ts
|
|
1402
|
+
/**
|
|
1403
|
+
* A collection write proposes `next`; an update pair may move `next` out of
|
|
1404
|
+
* the scope `current` is in. Both must stay in scope.
|
|
1405
|
+
*/
|
|
1406
|
+
function matchWriteScope(match, instance, current, next) {
|
|
1407
|
+
const first = match(instance ? current : next);
|
|
1408
|
+
if (!first.ok || !instance || next === current) return first;
|
|
1409
|
+
const moved = match(next);
|
|
1410
|
+
return moved.ok ? first : moved;
|
|
1367
1411
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
permission:
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
approval: grant.approval,
|
|
1378
|
-
hosted: grant.hosted,
|
|
1379
|
-
breakGlass
|
|
1412
|
+
function complete(call, raw, membership) {
|
|
1413
|
+
const { env, permission, tracer } = call;
|
|
1414
|
+
const decision = env.outcomeOnly !== true && raw.outcome === "denied" && raw.permission === void 0 ? {
|
|
1415
|
+
...raw,
|
|
1416
|
+
permission: permission.key
|
|
1417
|
+
} : raw;
|
|
1418
|
+
const final = env.outcomeOnly === true ? decision : freezeDeep(tracer === void 0 ? decision : {
|
|
1419
|
+
...decision,
|
|
1420
|
+
trace: traceOf(tracer)
|
|
1380
1421
|
});
|
|
1422
|
+
finish(call.policy, call.given, permission, call.current, final, call.options, env, call.options.trusted === true, membership);
|
|
1423
|
+
return final;
|
|
1381
1424
|
}
|
|
1382
|
-
function
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
why
|
|
1425
|
+
function deniedNow(call, denial) {
|
|
1426
|
+
return complete(call, {
|
|
1427
|
+
outcome: "denied",
|
|
1428
|
+
denials: [denial],
|
|
1429
|
+
alternatives: []
|
|
1388
1430
|
});
|
|
1389
1431
|
}
|
|
1390
|
-
function
|
|
1391
|
-
return {
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
};
|
|
1432
|
+
function deniedWithAlternatives(state, denials) {
|
|
1433
|
+
return complete(state, {
|
|
1434
|
+
outcome: "denied",
|
|
1435
|
+
denials,
|
|
1436
|
+
alternatives: state.env.skipAlternatives ? [] : alternativesFor(evaluate, state.policy, state.permission, state.subject, state.now, state.env)
|
|
1437
|
+
});
|
|
1397
1438
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
const
|
|
1401
|
-
|
|
1402
|
-
const
|
|
1403
|
-
let
|
|
1404
|
-
const tracer = options.explain === true ? {
|
|
1405
|
-
evaluated: 0,
|
|
1406
|
-
allows: [],
|
|
1407
|
-
denies: [],
|
|
1408
|
-
skipped: []
|
|
1409
|
-
} : void 0;
|
|
1410
|
-
const complete = (decision, membership) => {
|
|
1411
|
-
const final = env.outcomeOnly === true ? decision : freezeDeep(tracer === void 0 ? decision : {
|
|
1412
|
-
...decision,
|
|
1413
|
-
trace: traceOf(tracer)
|
|
1414
|
-
});
|
|
1415
|
-
finish(policy, given, permission, current, final, options, env, trusted, membership);
|
|
1416
|
-
return final;
|
|
1417
|
-
};
|
|
1439
|
+
/** The rows after boundary validation; on `invalid`, the rows validated before the failure. */
|
|
1440
|
+
function validatedRows(call, data, rows) {
|
|
1441
|
+
const { permission, policy, options } = call;
|
|
1442
|
+
if (data === void 0 && !(permission.kind === "instance" && policy.validate === "always")) return rows;
|
|
1443
|
+
const check = (value) => validateBoundary(permission, call.resource, value, policy.validate, options.trusted === true, options.boundary ?? "manual");
|
|
1444
|
+
let current = rows.current;
|
|
1418
1445
|
try {
|
|
1419
|
-
if (
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
current
|
|
1423
|
-
next
|
|
1424
|
-
}
|
|
1425
|
-
current = validated;
|
|
1426
|
-
next = validated;
|
|
1427
|
-
} else next = validated;
|
|
1446
|
+
if (permission.kind === "instance" && isRowPair(data)) {
|
|
1447
|
+
current = check(data.current);
|
|
1448
|
+
return {
|
|
1449
|
+
current,
|
|
1450
|
+
next: check(data.next)
|
|
1451
|
+
};
|
|
1428
1452
|
}
|
|
1453
|
+
const validated = check(data);
|
|
1454
|
+
return permission.kind === "instance" ? {
|
|
1455
|
+
current: validated,
|
|
1456
|
+
next: validated
|
|
1457
|
+
} : {
|
|
1458
|
+
current,
|
|
1459
|
+
next: validated
|
|
1460
|
+
};
|
|
1429
1461
|
} catch (error) {
|
|
1430
|
-
if (error instanceof PermDockValidationError && error.code === "invalid-data") return
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
detail: error
|
|
1436
|
-
}],
|
|
1437
|
-
alternatives: []
|
|
1438
|
-
});
|
|
1462
|
+
if (error instanceof PermDockValidationError && error.code === "invalid-data") return {
|
|
1463
|
+
current,
|
|
1464
|
+
next: rows.next,
|
|
1465
|
+
invalid: error
|
|
1466
|
+
};
|
|
1439
1467
|
throw error;
|
|
1440
1468
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
detail: "use permdock/pdp createPermDock"
|
|
1447
|
-
}],
|
|
1448
|
-
alternatives: []
|
|
1449
|
-
});
|
|
1450
|
-
const scopes = scopeList(policy.scopes);
|
|
1451
|
-
const declared = declaredRoleNames(policy);
|
|
1452
|
-
const principalRoles = subject.principal?.roles ?? [];
|
|
1453
|
-
const globalNames = expandRoleNames(principalRoles, declared, env.customRoles, subject.principal?.tenant);
|
|
1454
|
-
if (subject.principal !== null && globalNames.unknown.length > 0) emitSafe(env.listeners.auth, {
|
|
1469
|
+
}
|
|
1470
|
+
function heldRoles(call, scopes, declared) {
|
|
1471
|
+
const { subject, env } = call;
|
|
1472
|
+
const global = expandRoleNames(subject.principal?.roles ?? [], declared, env.customRoles, subject.principal?.tenant);
|
|
1473
|
+
if (subject.principal !== null && global.unknown.length > 0) emitSafe(env.listeners.auth, {
|
|
1455
1474
|
reason: "unknown-role",
|
|
1456
1475
|
source: "roles"
|
|
1457
1476
|
}, env.listeners);
|
|
1477
|
+
const matching = new Set(global.roles);
|
|
1458
1478
|
const denials = [];
|
|
1459
|
-
const allows = [];
|
|
1460
|
-
const matchingRoles = new Set(globalNames.roles);
|
|
1461
1479
|
for (const membership of subject.principal?.memberships ?? []) {
|
|
1462
1480
|
if (!inTeam(membership, scopes, env.team)) continue;
|
|
1463
1481
|
const expanded = expandRoleNames(membership.roles, declared, env.customRoles, tenantOf(membership, scopes));
|
|
1464
|
-
for (const name of expanded.roles)
|
|
1482
|
+
for (const name of expanded.roles) matching.add(name);
|
|
1465
1483
|
for (const name of expanded.unknown) denials.push({
|
|
1466
1484
|
role: name,
|
|
1467
1485
|
reason: "unknown-role"
|
|
1468
1486
|
});
|
|
1469
1487
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
denials
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
}
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1488
|
+
return {
|
|
1489
|
+
global,
|
|
1490
|
+
matching,
|
|
1491
|
+
denials
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
/** A support membership the policy says needs an actor, held without one. */
|
|
1495
|
+
function lacksActor(call, scopes) {
|
|
1496
|
+
const { subject, now, env } = call;
|
|
1497
|
+
const actorVias = actorRequiredVias(call.policy.index.supports);
|
|
1498
|
+
return actorVias.size > 0 && subject.actor === void 0 && (subject.principal?.memberships ?? []).some((membership) => isSupportMembership(membership, actorVias) && !isMembershipExpired(membership, now) && inTeam(membership, scopes, env.team));
|
|
1499
|
+
}
|
|
1500
|
+
function candidatesFor(call) {
|
|
1501
|
+
const { permission } = call;
|
|
1502
|
+
const candidates = (call.policy.index.grantsByKey.get(permission.key) ?? []).filter((grant) => grant.breakGlass === void 0).map((grant) => ({ grant }));
|
|
1503
|
+
for (const item of call.env.customGrants) if (item.grant.permission.key === permission.key) candidates.push({
|
|
1483
1504
|
grant: item.grant,
|
|
1484
1505
|
custom: item.role
|
|
1485
1506
|
});
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1507
|
+
return candidates;
|
|
1508
|
+
}
|
|
1509
|
+
function breakGlassFor(call, scopes) {
|
|
1510
|
+
const { permission, subject, now, tracer } = call;
|
|
1511
|
+
const overrides = /* @__PURE__ */ new Set();
|
|
1512
|
+
let denial;
|
|
1513
|
+
for (const grant of call.policy.index.grantsByKey.get(permission.key) ?? []) {
|
|
1491
1514
|
if (grant.breakGlass === void 0) continue;
|
|
1492
1515
|
if (tracer !== void 0) tracer.evaluated += 1;
|
|
1493
|
-
if (!matchGrantee(grant.to, subject, now, resource, scopes).matched) {
|
|
1516
|
+
if (!matchGrantee(grant.to, subject, now, call.resource, scopes).matched) {
|
|
1494
1517
|
skip(tracer, grant, permission.key, "grantee");
|
|
1495
1518
|
continue;
|
|
1496
1519
|
}
|
|
@@ -1499,13 +1522,14 @@ function evaluate(policy, given, permission, data, options, env) {
|
|
|
1499
1522
|
skip(tracer, grant, permission.key, "break-glass-inactive");
|
|
1500
1523
|
continue;
|
|
1501
1524
|
}
|
|
1502
|
-
for (const name of grant.breakGlass.overrides)
|
|
1503
|
-
if (result.kind === "granted") {
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1525
|
+
for (const name of grant.breakGlass.overrides) overrides.add(name);
|
|
1526
|
+
if (result.kind === "granted") return {
|
|
1527
|
+
grant,
|
|
1528
|
+
obligations: result.obligations,
|
|
1529
|
+
overrides,
|
|
1530
|
+
denial
|
|
1531
|
+
};
|
|
1532
|
+
denial ??= result.reason === "insufficient-user-authentication" ? {
|
|
1509
1533
|
role: null,
|
|
1510
1534
|
reason: result.reason,
|
|
1511
1535
|
to: result.to
|
|
@@ -1514,241 +1538,231 @@ function evaluate(policy, given, permission, data, options, env) {
|
|
|
1514
1538
|
reason: result.reason
|
|
1515
1539
|
};
|
|
1516
1540
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1541
|
+
return {
|
|
1542
|
+
grant: void 0,
|
|
1543
|
+
obligations: [],
|
|
1544
|
+
overrides,
|
|
1545
|
+
denial
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function walkRoleFor(call) {
|
|
1549
|
+
const { env } = call;
|
|
1550
|
+
if (env.relations?.available !== true) return;
|
|
1551
|
+
return (membership, row) => {
|
|
1519
1552
|
const on = membership.on;
|
|
1520
|
-
const condition = on === void 0 ? void 0 : resourceRoleCondition(resource, on.resource, [on.id], policy.resources);
|
|
1521
|
-
return condition === void 0 || env.relations === void 0 ? false : resolveRelated(condition, row, subject, now, env.relations);
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
}
|
|
1552
|
-
const roleItems = flattenGrantee(grant.to).filter((item) => item.kind === "role");
|
|
1553
|
-
let scopeMembership;
|
|
1554
|
-
let roleOk = roleItems.length === 0;
|
|
1555
|
-
if (roleItems.length > 0) {
|
|
1556
|
-
if (subject.principal === null) {
|
|
1557
|
-
denials.push({
|
|
1558
|
-
role: displayRole,
|
|
1559
|
-
reason: "anonymous"
|
|
1560
|
-
});
|
|
1561
|
-
continue;
|
|
1562
|
-
}
|
|
1563
|
-
let allHeld = true;
|
|
1564
|
-
for (const roleItem of roleItems) {
|
|
1565
|
-
if (!(custom === void 0 ? roleItem.scope === "global" ? globalNames.roles.includes(roleItem.role) : matchingRoles.has(roleItem.role) : holdsCustom(custom))) {
|
|
1566
|
-
skip(tracer, grant, permission.key, "role");
|
|
1567
|
-
allHeld = false;
|
|
1568
|
-
break;
|
|
1569
|
-
}
|
|
1570
|
-
const matchRow = (row) => matchScopedMembership(subject, roleItem.scope, roleItem.role, row, scopes, resource, policy.resources, now, (membership) => {
|
|
1571
|
-
if (custom === void 0) return expandRoleNames(membership.roles, declared, env.customRoles, tenantOf(membership, scopes)).roles;
|
|
1572
|
-
return holdsCustomRole(membership, custom, scopes) ? [custom.name] : [];
|
|
1573
|
-
}, env.team, walkRole, permission.kind === "instance", options.scope);
|
|
1574
|
-
const scopeMatch = matchWriteScope(matchRow, permission.kind === "instance", current, next);
|
|
1575
|
-
if (!scopeMatch.ok) {
|
|
1576
|
-
denials.push({
|
|
1577
|
-
role: roleItem.role,
|
|
1578
|
-
reason: scopeMatch.reason
|
|
1579
|
-
});
|
|
1580
|
-
allHeld = false;
|
|
1581
|
-
break;
|
|
1582
|
-
}
|
|
1583
|
-
scopeMembership = scopeMatch.membership ?? scopeMembership;
|
|
1584
|
-
}
|
|
1585
|
-
roleOk = allHeld;
|
|
1586
|
-
}
|
|
1587
|
-
if (!roleOk) continue;
|
|
1588
|
-
if (!isActive(grant.validity, now)) {
|
|
1589
|
-
if (grant.effect === "allow") denials.push({
|
|
1590
|
-
role: displayRole,
|
|
1591
|
-
reason: "inactive-grant",
|
|
1592
|
-
detail: grant.validity
|
|
1593
|
-
});
|
|
1594
|
-
else skip(tracer, grant, permission.key, "validity");
|
|
1595
|
-
continue;
|
|
1596
|
-
}
|
|
1597
|
-
const requirement = grant.requires === void 0 || grant.effect !== "allow" ? void 0 : requirementCondition(policy, subject, grant.requires, resource, now, env.customRoles, env.customGrants);
|
|
1598
|
-
const merged = freezeDeep(compact({
|
|
1599
|
-
...grant,
|
|
1600
|
-
where: combineWhere(combineWhere(grant.where, granteeMatch.where), requirement)
|
|
1601
|
-
}));
|
|
1602
|
-
const condition = evaluateGrantCondition(merged, permission, current, next, subject, now, scopes, env.relations, (target, id, key) => inheritedVerdict(policy, given, target, id, key, now, env));
|
|
1603
|
-
if (!condition.matched && grant.effect === "deny" && isUnevaluable(condition.reason) && grantCoversField(grant.fields, options.field, grant.effect)) {
|
|
1604
|
-
if (condition.reason === "closure-error") emitSafe(env.listeners.error, condition.cause ?? /* @__PURE__ */ new Error("closure-error"), env.listeners);
|
|
1605
|
-
tracer?.denies.push(matchedOf(merged, permission.key));
|
|
1606
|
-
return complete({
|
|
1607
|
-
outcome: "denied",
|
|
1608
|
-
denials: [{
|
|
1609
|
-
role: displayRole,
|
|
1610
|
-
reason: condition.reason ?? "relation-unavailable"
|
|
1611
|
-
}],
|
|
1612
|
-
alternatives: []
|
|
1613
|
-
});
|
|
1553
|
+
const condition = on === void 0 ? void 0 : resourceRoleCondition(call.resource, on.resource, [on.id], call.policy.resources);
|
|
1554
|
+
return condition === void 0 || env.relations === void 0 ? false : resolveRelated(condition, row, call.subject, call.now, env.relations);
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
function holdsCustom(state, custom) {
|
|
1558
|
+
return isGlobalCustomRole(custom) ? holdsGlobalCustomRole(state.subject.principal?.roles ?? [], custom) : (state.subject.principal?.memberships ?? []).some((membership) => inTeam(membership, state.scopes, state.env.team) && holdsCustomRole(membership, custom, state.scopes));
|
|
1559
|
+
}
|
|
1560
|
+
function holdsRole(state, item, custom) {
|
|
1561
|
+
if (custom !== void 0) return holdsCustom(state, custom);
|
|
1562
|
+
return item.scope === "global" ? state.roles.global.roles.includes(item.role) : state.roles.matching.has(item.role);
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Whether the subject holds every role the grantee names, at a scope the row
|
|
1566
|
+
* (and a write's next row) is in. `held: false` when a denial or skip was recorded.
|
|
1567
|
+
*/
|
|
1568
|
+
function roleScope(state, grant, custom) {
|
|
1569
|
+
const { subject, permission, tracer } = state;
|
|
1570
|
+
const roleItems = flattenGrantee(grant.to).filter((item) => item.kind === "role");
|
|
1571
|
+
if (roleItems.length === 0) return { held: true };
|
|
1572
|
+
if (subject.principal === null) {
|
|
1573
|
+
state.denials.push({
|
|
1574
|
+
role: grant.role,
|
|
1575
|
+
reason: "anonymous"
|
|
1576
|
+
});
|
|
1577
|
+
return { held: false };
|
|
1578
|
+
}
|
|
1579
|
+
let membership;
|
|
1580
|
+
for (const roleItem of roleItems) {
|
|
1581
|
+
if (!holdsRole(state, roleItem, custom)) {
|
|
1582
|
+
skip(tracer, grant, permission.key, "role");
|
|
1583
|
+
return { held: false };
|
|
1614
1584
|
}
|
|
1615
|
-
|
|
1616
|
-
if (
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1585
|
+
const matchRow = (row) => matchScopedMembership(subject, roleItem.scope, roleItem.role, row, state.scopes, state.resource, state.policy.resources, state.now, (held) => {
|
|
1586
|
+
if (custom === void 0) return expandRoleNames(held.roles, state.declared, state.env.customRoles, tenantOf(held, state.scopes)).roles;
|
|
1587
|
+
return holdsCustomRole(held, custom, state.scopes) ? [custom.name] : [];
|
|
1588
|
+
}, state.env.team, state.walkRole, permission.kind === "instance", state.options.scope);
|
|
1589
|
+
const scopeMatch = matchWriteScope(matchRow, permission.kind === "instance", state.current, state.next);
|
|
1590
|
+
if (!scopeMatch.ok) {
|
|
1591
|
+
state.denials.push({
|
|
1592
|
+
role: roleItem.role,
|
|
1593
|
+
reason: scopeMatch.reason
|
|
1621
1594
|
});
|
|
1622
|
-
|
|
1623
|
-
}
|
|
1624
|
-
if (!grantCoversField(grant.fields, options.field, grant.effect)) {
|
|
1625
|
-
skip(tracer, grant, permission.key, "field");
|
|
1626
|
-
continue;
|
|
1595
|
+
return { held: false };
|
|
1627
1596
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
tracer
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1597
|
+
membership = scopeMatch.membership ?? membership;
|
|
1598
|
+
}
|
|
1599
|
+
return membership === void 0 ? { held: true } : {
|
|
1600
|
+
held: true,
|
|
1601
|
+
membership
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
/** Whether the grantee matches; records the denial or skip when it does not. */
|
|
1605
|
+
function granteeOf(state, grant, custom) {
|
|
1606
|
+
const { subject, now, permission } = state;
|
|
1607
|
+
if (grant.viaOnly !== void 0 && !(subject.principal?.memberships ?? []).some((membership) => membership.via === grant.viaOnly && !isMembershipExpired(membership, now) && inTeam(membership, state.scopes, state.env.team))) {
|
|
1608
|
+
skip(state.tracer, grant, permission.key, "via-only");
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
const required = grant.purpose;
|
|
1612
|
+
if (required !== void 0 && !state.purposes.some((purpose) => required.includes(purpose))) {
|
|
1613
|
+
skip(state.tracer, grant, permission.key, "purpose");
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
const granteeMatch = matchGrantee(grant.to, subject, now, state.resource, state.scopes, state.policy.resources, grant.effect === "deny");
|
|
1617
|
+
if (granteeMatch.matched) return granteeMatch;
|
|
1618
|
+
const reason = granteeMatch.reason ?? "no-grant";
|
|
1619
|
+
if (reason === "not-entitled" && !flattenGrantee(grant.to).every((item) => item.kind !== "role" || holdsRole(state, item, custom))) {
|
|
1620
|
+
skip(state.tracer, grant, permission.key, "grantee");
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
state.denials.push((reason === "insufficient-user-authentication" || reason === "not-entitled") && grant.to !== void 0 ? {
|
|
1624
|
+
role: grant.role,
|
|
1625
|
+
reason,
|
|
1626
|
+
to: grant.to
|
|
1627
|
+
} : {
|
|
1628
|
+
role: grant.role,
|
|
1629
|
+
reason
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
/** One candidate grant: records an allow, a denial or a skip, or returns the decision a deny ends with. */
|
|
1633
|
+
function evaluateCandidate(state, { grant, custom }, breakGlass) {
|
|
1634
|
+
const { policy, subject, permission, now, env, tracer } = state;
|
|
1635
|
+
const displayRole = grant.role;
|
|
1636
|
+
if (tracer !== void 0) tracer.evaluated += 1;
|
|
1637
|
+
const granteeMatch = granteeOf(state, grant, custom);
|
|
1638
|
+
if (granteeMatch === void 0) return;
|
|
1639
|
+
const scoped = roleScope(state, grant, custom);
|
|
1640
|
+
if (!scoped.held) return;
|
|
1641
|
+
if (!isActive(grant.validity, now)) {
|
|
1642
|
+
if (grant.effect === "allow") state.denials.push({
|
|
1643
|
+
role: displayRole,
|
|
1644
|
+
reason: "inactive-grant",
|
|
1645
|
+
detail: grant.validity
|
|
1655
1646
|
});
|
|
1647
|
+
else skip(tracer, grant, permission.key, "validity");
|
|
1648
|
+
return;
|
|
1656
1649
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1650
|
+
const requirement = grant.requires === void 0 || grant.effect !== "allow" ? void 0 : requirementCondition(policy, subject, grant.requires, state.resource, now, env.customRoles, env.customGrants);
|
|
1651
|
+
const merged = freezeDeep(compact({
|
|
1652
|
+
...grant,
|
|
1653
|
+
where: combineWhere(combineWhere(grant.where, granteeMatch.where), requirement)
|
|
1654
|
+
}));
|
|
1655
|
+
const condition = evaluateGrantCondition(merged, permission, state.current, state.next, subject, now, state.scopes, env.relations, (target, id, key) => inheritedVerdict(evaluate, policy, state.given, target, id, key, now, env));
|
|
1656
|
+
if (!condition.matched && condition.reason === "closure-error") emitSafe(env.listeners.error, condition.cause ?? /* @__PURE__ */ new Error("closure-error"), env.listeners);
|
|
1657
|
+
if (!condition.matched && grant.effect === "deny" && isUnevaluable(condition.reason) && grantCoversField(grant.fields, state.options.field, grant.effect)) {
|
|
1658
|
+
tracer?.denies.push(matchedOf(merged, permission.key));
|
|
1659
|
+
return deniedNow(state, {
|
|
1660
|
+
role: displayRole,
|
|
1661
|
+
reason: condition.reason ?? "relation-unavailable"
|
|
1663
1662
|
});
|
|
1664
|
-
}
|
|
1665
|
-
if (
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
role: null,
|
|
1671
|
-
reason
|
|
1672
|
-
}],
|
|
1673
|
-
alternatives: env.skipAlternatives ? [] : alternativesFor(policy, permission, subject, now, env)
|
|
1663
|
+
}
|
|
1664
|
+
if (!condition.matched) {
|
|
1665
|
+
state.denials.push({
|
|
1666
|
+
role: displayRole,
|
|
1667
|
+
reason: condition.reason ?? "condition",
|
|
1668
|
+
detail: condition.cause
|
|
1674
1669
|
});
|
|
1670
|
+
return;
|
|
1675
1671
|
}
|
|
1676
|
-
if (
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1672
|
+
if (!grantCoversField(grant.fields, state.options.field, grant.effect)) {
|
|
1673
|
+
skip(tracer, grant, permission.key, "field");
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
if (grant.effect === "deny") {
|
|
1677
|
+
tracer?.denies.push(matchedOf(merged, permission.key));
|
|
1678
|
+
if (grant.name !== void 0 && breakGlass.overrides.has(grant.name)) {
|
|
1679
|
+
if (breakGlass.grant !== void 0) return;
|
|
1680
|
+
if (breakGlass.denial !== void 0) return deniedWithAlternatives(state, [breakGlass.denial]);
|
|
1681
|
+
}
|
|
1682
|
+
return deniedWithAlternatives(state, [grant.name === void 0 ? {
|
|
1683
|
+
role: displayRole,
|
|
1684
|
+
reason: "deny"
|
|
1685
|
+
} : {
|
|
1686
|
+
role: displayRole,
|
|
1687
|
+
reason: "deny",
|
|
1688
|
+
detail: { name: grant.name }
|
|
1689
|
+
}]);
|
|
1690
|
+
}
|
|
1691
|
+
tracer?.allows.push(matchedOf(merged, permission.key));
|
|
1692
|
+
state.allows.push(scoped.membership === void 0 ? { grant: merged } : {
|
|
1693
|
+
grant: merged,
|
|
1694
|
+
membership: scoped.membership
|
|
1692
1695
|
});
|
|
1696
|
+
}
|
|
1697
|
+
function noAllowDecision(state) {
|
|
1698
|
+
const { denials, subject, roles } = state;
|
|
1699
|
+
const reason = denials[0]?.reason ?? (subject.principal === null ? "anonymous" : roles.global.unknown.length > 0 && roles.global.roles.length === 0 ? "unknown-role" : "no-grant");
|
|
1700
|
+
return deniedWithAlternatives(state, denials.length > 0 ? denials : [{
|
|
1701
|
+
role: null,
|
|
1702
|
+
reason
|
|
1703
|
+
}]);
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* The allows a delegation leaves usable. A policy delegation is a ceiling for
|
|
1707
|
+
* the actor: outside it, nothing is delegated; inside it, a token delegation
|
|
1708
|
+
* on the call must still cover.
|
|
1709
|
+
*/
|
|
1710
|
+
function delegatedAllows(state) {
|
|
1711
|
+
const { policy, subject, permission, current } = state;
|
|
1712
|
+
const ceiling = delegatedPermissions(policy.delegations, subject, state.roles.matching, state.now);
|
|
1713
|
+
if (ceiling !== void 0 && !ceiling.has(permission.key)) return deniedWithAlternatives(state, [{
|
|
1714
|
+
role: null,
|
|
1715
|
+
reason: "not-delegated"
|
|
1716
|
+
}]);
|
|
1693
1717
|
const unscoped = subject.actor !== void 0 && ceiling === void 0;
|
|
1694
1718
|
let delegationMiss = coveredByDelegation(permission, subject.delegation, resourceIdOf(current), unscoped);
|
|
1695
|
-
const requirementsCovered = allows.filter((candidate) => coversRequirements(policy.permissions, candidate.grant.requires, subject.delegation, resourceIdOf(current), unscoped));
|
|
1696
|
-
|
|
1719
|
+
const requirementsCovered = state.allows.filter((candidate) => coversRequirements(policy.permissions, candidate.grant.requires, subject.delegation, resourceIdOf(current), unscoped));
|
|
1720
|
+
const usable = delegationMiss === void 0 ? requirementsCovered : requirementsCovered.filter((candidate) => requiresStandIn(candidate.grant));
|
|
1697
1721
|
if (delegationMiss === "not-delegated" && usable.length > 0) delegationMiss = void 0;
|
|
1698
1722
|
if (delegationMiss === void 0 && usable.length === 0) delegationMiss = "not-delegated";
|
|
1699
|
-
if (delegationMiss !== void 0) return
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
alternatives: []
|
|
1716
|
-
});
|
|
1717
|
-
const approvalOf = (candidate) => approvalPolicies === void 0 || approvalPolicies === "failed" ? candidate.grant.approval : tightenApproval(candidate.grant.approval, approvalPolicies, {
|
|
1718
|
-
permission: permission.key,
|
|
1719
|
-
tenant: decisionTenant(subject, candidate.membership),
|
|
1720
|
-
subject,
|
|
1721
|
-
current,
|
|
1722
|
-
next,
|
|
1723
|
-
now,
|
|
1724
|
-
scopes: scopeList(policy.scopes)
|
|
1723
|
+
if (delegationMiss !== void 0) return deniedWithAlternatives(state, [{
|
|
1724
|
+
role: null,
|
|
1725
|
+
reason: delegationMiss
|
|
1726
|
+
}]);
|
|
1727
|
+
return usable;
|
|
1728
|
+
}
|
|
1729
|
+
function approvalOf(state, candidate) {
|
|
1730
|
+
const approvalPolicies = state.env.approvalPolicies;
|
|
1731
|
+
return approvalPolicies === void 0 || approvalPolicies === "failed" ? candidate.grant.approval : tightenApproval(candidate.grant.approval, approvalPolicies, {
|
|
1732
|
+
permission: state.permission.key,
|
|
1733
|
+
tenant: decisionTenant(state.subject, candidate.membership),
|
|
1734
|
+
subject: state.subject,
|
|
1735
|
+
current: state.current,
|
|
1736
|
+
next: state.next,
|
|
1737
|
+
now: state.now,
|
|
1738
|
+
scopes: state.scopes
|
|
1725
1739
|
});
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1740
|
+
}
|
|
1741
|
+
/** The first usable allow whose quota admits the call, or the quota denials. */
|
|
1742
|
+
function quotaAllow(state, usable) {
|
|
1743
|
+
const { subject, env } = state;
|
|
1744
|
+
const denials = [];
|
|
1730
1745
|
for (const candidate of usable) {
|
|
1731
|
-
approval = approvalOf(candidate);
|
|
1732
|
-
const consume = !requiresApproval(approval) && shouldConsumeQuota(options.source, env.simulated);
|
|
1746
|
+
const approval = approvalOf(state, candidate);
|
|
1733
1747
|
const quota = applyQuota({
|
|
1734
1748
|
store: env.limits,
|
|
1735
1749
|
cache: env.limitCache,
|
|
1736
1750
|
grant: candidate.grant,
|
|
1737
|
-
permissionKey: permission.key,
|
|
1751
|
+
permissionKey: state.permission.key,
|
|
1738
1752
|
subjectId: subject.principal?.id ?? "",
|
|
1739
1753
|
tenant: subject.principal?.tenant,
|
|
1740
|
-
now,
|
|
1741
|
-
consume
|
|
1754
|
+
now: state.now,
|
|
1755
|
+
consume: !requiresApproval(approval) && shouldConsumeQuota(state.options.source, env.simulated)
|
|
1742
1756
|
});
|
|
1743
|
-
if (quota.ok) {
|
|
1744
|
-
|
|
1745
|
-
|
|
1757
|
+
if (quota.ok) return {
|
|
1758
|
+
allow: candidate,
|
|
1759
|
+
approval,
|
|
1760
|
+
quota: compact({
|
|
1746
1761
|
quota: quota.quota,
|
|
1747
1762
|
obligations: quota.obligations
|
|
1748
|
-
})
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
quotaDenials.push(quota.reason === "limit" ? {
|
|
1763
|
+
})
|
|
1764
|
+
};
|
|
1765
|
+
denials.push(quota.reason === "limit" ? {
|
|
1752
1766
|
role: candidate.grant.role,
|
|
1753
1767
|
reason: "limit",
|
|
1754
1768
|
detail: quota.detail
|
|
@@ -1757,11 +1771,10 @@ function evaluate(policy, given, permission, data, options, env) {
|
|
|
1757
1771
|
reason: quota.reason
|
|
1758
1772
|
});
|
|
1759
1773
|
}
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
});
|
|
1774
|
+
return { denials };
|
|
1775
|
+
}
|
|
1776
|
+
function allowedDecision(state, allow, approval, quotaState) {
|
|
1777
|
+
const { policy, subject, permission, resource, env, current, next } = state;
|
|
1765
1778
|
const resourceId = permission.kind === "collection" ? "*" : rowIdOf(current, resource?.id);
|
|
1766
1779
|
const version = approval !== void 0 && approval !== "human" && approval.staleOn === "resource-change" && resource?.version !== void 0 ? versionOf(current, resource.version) : void 0;
|
|
1767
1780
|
const token = env.simulated ? "pd1.simulated" : env.outcomeOnly === true ? "pd1.outcome" : decisionToken({
|
|
@@ -1774,11 +1787,15 @@ function evaluate(policy, given, permission, data, options, env) {
|
|
|
1774
1787
|
payload: resourceId === "*" && (next ?? current) !== void 0 ? payloadDigest(next ?? current) : void 0
|
|
1775
1788
|
});
|
|
1776
1789
|
const matched = compact({
|
|
1777
|
-
...matchedOf(
|
|
1790
|
+
...matchedOf(allow.grant, permission.key, allow.breakGlass),
|
|
1778
1791
|
approval
|
|
1779
1792
|
});
|
|
1780
|
-
const obligations = [
|
|
1781
|
-
|
|
1793
|
+
const obligations = [
|
|
1794
|
+
...quotaState.obligations ?? [],
|
|
1795
|
+
...allow.obligations ?? [],
|
|
1796
|
+
...allow.grant.obligations ?? []
|
|
1797
|
+
];
|
|
1798
|
+
return complete(state, requiresApproval(approval) ? {
|
|
1782
1799
|
outcome: "approval-required",
|
|
1783
1800
|
grant: matched,
|
|
1784
1801
|
reason: "human",
|
|
@@ -1790,7 +1807,91 @@ function evaluate(policy, given, permission, data, options, env) {
|
|
|
1790
1807
|
token,
|
|
1791
1808
|
...quotaState,
|
|
1792
1809
|
...obligations.length === 0 ? {} : { obligations }
|
|
1793
|
-
},
|
|
1810
|
+
}, allow.membership);
|
|
1811
|
+
}
|
|
1812
|
+
function evaluate(policy, given, permission, data, options, env) {
|
|
1813
|
+
const call = {
|
|
1814
|
+
policy,
|
|
1815
|
+
given,
|
|
1816
|
+
subject: subjectForPermission(given, permission.key),
|
|
1817
|
+
permission,
|
|
1818
|
+
options,
|
|
1819
|
+
env,
|
|
1820
|
+
now: options.now ?? nowSeconds(),
|
|
1821
|
+
resource: getResource(policy.permissions, permission.resource),
|
|
1822
|
+
tracer: options.explain === true ? {
|
|
1823
|
+
evaluated: 0,
|
|
1824
|
+
allows: [],
|
|
1825
|
+
denies: [],
|
|
1826
|
+
skipped: []
|
|
1827
|
+
} : void 0
|
|
1828
|
+
};
|
|
1829
|
+
const rows = validatedRows(call, data, rowValues(permission, data));
|
|
1830
|
+
if (rows.invalid !== void 0) return deniedNow({
|
|
1831
|
+
...call,
|
|
1832
|
+
current: rows.current
|
|
1833
|
+
}, {
|
|
1834
|
+
role: null,
|
|
1835
|
+
reason: "validation",
|
|
1836
|
+
detail: rows.invalid
|
|
1837
|
+
});
|
|
1838
|
+
const settled = {
|
|
1839
|
+
...call,
|
|
1840
|
+
current: rows.current
|
|
1841
|
+
};
|
|
1842
|
+
if (isDelegatedPermission(policy, permission)) return deniedNow(settled, {
|
|
1843
|
+
role: null,
|
|
1844
|
+
reason: "pdp-unavailable",
|
|
1845
|
+
detail: "use permdock/pdp createPermDock"
|
|
1846
|
+
});
|
|
1847
|
+
const scopes = scopeList(policy.scopes);
|
|
1848
|
+
const declared = declaredRoleNames(policy);
|
|
1849
|
+
const roles = heldRoles(call, scopes, declared);
|
|
1850
|
+
if (lacksActor(call, scopes)) return deniedNow(settled, {
|
|
1851
|
+
role: null,
|
|
1852
|
+
reason: "actor-required"
|
|
1853
|
+
});
|
|
1854
|
+
const state = {
|
|
1855
|
+
...call,
|
|
1856
|
+
current: rows.current,
|
|
1857
|
+
next: rows.next,
|
|
1858
|
+
scopes,
|
|
1859
|
+
declared,
|
|
1860
|
+
roles,
|
|
1861
|
+
purposes: purposesOf(call.subject),
|
|
1862
|
+
walkRole: walkRoleFor(call),
|
|
1863
|
+
denials: roles.denials,
|
|
1864
|
+
allows: []
|
|
1865
|
+
};
|
|
1866
|
+
const candidates = candidatesFor(call);
|
|
1867
|
+
const breakGlass = breakGlassFor(call, scopes);
|
|
1868
|
+
for (const candidate of candidates) {
|
|
1869
|
+
const decision = evaluateCandidate(state, candidate, breakGlass);
|
|
1870
|
+
if (decision !== void 0) return decision;
|
|
1871
|
+
}
|
|
1872
|
+
if (breakGlass.grant !== void 0) {
|
|
1873
|
+
state.tracer?.allows.push(matchedOf(breakGlass.grant, permission.key, true));
|
|
1874
|
+
state.allows.push({
|
|
1875
|
+
grant: breakGlass.grant,
|
|
1876
|
+
obligations: breakGlass.obligations,
|
|
1877
|
+
breakGlass: true
|
|
1878
|
+
});
|
|
1879
|
+
} else if (breakGlass.denial !== void 0) state.denials.unshift(breakGlass.denial);
|
|
1880
|
+
if (state.allows.length === 0) return noAllowDecision(state);
|
|
1881
|
+
if (state.subject.stale === true && (policy.fresh ?? []).includes(permission.key)) return deniedNow(state, {
|
|
1882
|
+
role: null,
|
|
1883
|
+
reason: "stale-credentials"
|
|
1884
|
+
});
|
|
1885
|
+
const usable = delegatedAllows(state);
|
|
1886
|
+
if (!Array.isArray(usable)) return usable;
|
|
1887
|
+
if (env.approvalPolicies === "failed" && usable.length > 0) return deniedNow(state, {
|
|
1888
|
+
role: null,
|
|
1889
|
+
reason: "approval",
|
|
1890
|
+
detail: APPROVAL_POLICY_UNAVAILABLE
|
|
1891
|
+
});
|
|
1892
|
+
const chosen = quotaAllow(state, usable);
|
|
1893
|
+
if ("denials" in chosen) return deniedWithAlternatives(state, chosen.denials.length > 0 ? chosen.denials : state.denials);
|
|
1894
|
+
return allowedDecision(state, chosen.allow, chosen.approval, chosen.quota);
|
|
1794
1895
|
}
|
|
1795
1896
|
//#endregion
|
|
1796
1897
|
//#region src/core/ownership.ts
|
|
@@ -1907,7 +2008,7 @@ function coversInstance(membership, scopes, scope, id, within) {
|
|
|
1907
2008
|
}
|
|
1908
2009
|
/** Roles the principal holds at the instance, at an ancestor named in `within`, or globally. */
|
|
1909
2010
|
function rolesAtInstance(policy, principal, scopes, scope, id, within, now) {
|
|
1910
|
-
const declared = declaredRoleNames
|
|
2011
|
+
const declared = declaredRoleNames(policy);
|
|
1911
2012
|
const names = new Set((principal.roles ?? []).filter((name) => declared.has(name)));
|
|
1912
2013
|
for (const membership of principal.memberships ?? []) if (!isMembershipExpired(membership, now) && coversInstance(membership, scopes, scope, id, within)) for (const name of membership.roles) names.add(name);
|
|
1913
2014
|
return rankRoles(policy, [...names]);
|
|
@@ -2007,7 +2108,7 @@ function decideRoleChange(policy, principal, scopes, change, customRoles, author
|
|
|
2007
2108
|
}
|
|
2008
2109
|
const id = change.id ?? "";
|
|
2009
2110
|
const binding = bindingOf(policy, name);
|
|
2010
|
-
const declared = declaredRoleNames
|
|
2111
|
+
const declared = declaredRoleNames(policy).has(name);
|
|
2011
2112
|
const named = declared || global ? [] : tenantCustomRoles(policy, customRoles).filter((role) => role.name === name);
|
|
2012
2113
|
if (!declared && named.length === 0) {
|
|
2013
2114
|
deny("unknown-role");
|
|
@@ -2068,7 +2169,7 @@ function decideRoleChange(policy, principal, scopes, change, customRoles, author
|
|
|
2068
2169
|
const revocable = kind === "revoke" && custom !== void 0 && allowed.inert?.has(name);
|
|
2069
2170
|
if (!allowed.assignable.has(name) && revocable !== true) deny("not-assignable-by");
|
|
2070
2171
|
else if (custom === void 0 && usesAssigns(policy) && !allowed.manage) {
|
|
2071
|
-
const held = global ? rankRoles(policy, (principal.roles ?? []).filter((role) => declaredRoleNames
|
|
2172
|
+
const held = global ? rankRoles(policy, (principal.roles ?? []).filter((role) => declaredRoleNames(policy).has(role))) : rolesAtInstance(policy, principal, scopes, scope, id, within, now);
|
|
2072
2173
|
by = held.find((role) => bindingOf(policy, role)?.assigns?.includes(name) === true) ?? null;
|
|
2073
2174
|
if (by === null) deny("not-assignable-by", { held });
|
|
2074
2175
|
}
|
|
@@ -2088,658 +2189,767 @@ function decideRoleChange(policy, principal, scopes, change, customRoles, author
|
|
|
2088
2189
|
return done(by);
|
|
2089
2190
|
}
|
|
2090
2191
|
//#endregion
|
|
2091
|
-
//#region src/core/
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2192
|
+
//#region src/core/held-roles.ts
|
|
2193
|
+
/**
|
|
2194
|
+
* Role names held in `tenant` on live memberships, global roles included, in
|
|
2195
|
+
* rank order. With `scope`, only the roles of that scope's memberships (and
|
|
2196
|
+
* instance `id`).
|
|
2197
|
+
*/
|
|
2198
|
+
function heldRoleNames(policy, subject, tenant, only, now) {
|
|
2199
|
+
if (subject.principal === null) return [];
|
|
2200
|
+
const scopes = scopeList(policy.scopes);
|
|
2201
|
+
const scope = only?.scope === void 0 ? void 0 : resolveScope(scopes, only.scope);
|
|
2202
|
+
if (only?.scope !== void 0 && scope === void 0) return [];
|
|
2203
|
+
const names = new Set(scope === void 0 ? subject.principal.roles ?? [] : []);
|
|
2204
|
+
for (const membership of subject.principal.memberships ?? []) {
|
|
2205
|
+
if (isMembershipExpired(membership, now) || membership.keep !== void 0) continue;
|
|
2206
|
+
if (!(scope === void 0 ? tenantOf(membership, scopes) === tenant : membership.scope === scope && (only?.id === void 0 || sameId(membership.id, only.id)))) continue;
|
|
2207
|
+
for (const role of membership.roles) names.add(role);
|
|
2098
2208
|
}
|
|
2209
|
+
return rankRoles(policy, [...names]);
|
|
2099
2210
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
}
|
|
2107
|
-
if (walk === void 0) return [];
|
|
2108
|
-
if (walk === "relation-depth" || walk === "relation-unavailable") return;
|
|
2109
|
-
const { ids } = walk;
|
|
2110
|
-
const read = () => ids.map((id) => reader.holders({
|
|
2111
|
-
resource: condition.resource,
|
|
2112
|
-
id,
|
|
2113
|
-
relation: condition.relation
|
|
2211
|
+
function roleLeaf(policy, name) {
|
|
2212
|
+
const binding = policy.rolesByName.get(name);
|
|
2213
|
+
return findRole(policy.vocabulary?.roles, name) ?? synthesiseRole(name, compact({
|
|
2214
|
+
on: typeof binding?.on === "string" ? binding.on : void 0,
|
|
2215
|
+
assignable: binding?.assignable,
|
|
2216
|
+
meta: binding?.meta
|
|
2114
2217
|
}));
|
|
2115
|
-
let answers = read();
|
|
2116
|
-
if (answers.includes("pending")) {
|
|
2117
|
-
await settle$1(cache);
|
|
2118
|
-
answers = read();
|
|
2119
|
-
}
|
|
2120
|
-
const out = [];
|
|
2121
|
-
for (const [index, answer] of answers.entries()) {
|
|
2122
|
-
const id = ids[index];
|
|
2123
|
-
if (answer === "pending" || answer === "failed" || id === void 0) return;
|
|
2124
|
-
out.push({
|
|
2125
|
-
id,
|
|
2126
|
-
holders: answer
|
|
2127
|
-
});
|
|
2128
|
-
}
|
|
2129
|
-
return out;
|
|
2130
2218
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
ctx.incomplete();
|
|
2142
|
-
return;
|
|
2143
|
-
}
|
|
2144
|
-
for (const member of members.filter((entry) => entry.membership.roles.includes(item.role))) ctx.add(member.id, allow ? {
|
|
2145
|
-
kind: "role",
|
|
2146
|
-
role: item.role,
|
|
2147
|
-
membership: member.membership
|
|
2148
|
-
} : void 0, member.membership);
|
|
2219
|
+
//#endregion
|
|
2220
|
+
//#region src/core/snapshot-grants.ts
|
|
2221
|
+
/** A grant on a relation with a `period` is decided on the server, where the clock is. */
|
|
2222
|
+
function periodBound(policy, grant) {
|
|
2223
|
+
const relations = policy.resources.get(grant.permission.resource)?.relations;
|
|
2224
|
+
return flattenGrantee(grant.to).some((item) => {
|
|
2225
|
+
if (item.kind !== "relation") return false;
|
|
2226
|
+
const spec = relations?.[item.relation];
|
|
2227
|
+
return isPrincipalRelation(spec) && spec.period !== void 0;
|
|
2228
|
+
});
|
|
2149
2229
|
}
|
|
2150
|
-
function
|
|
2151
|
-
if (
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
group
|
|
2230
|
+
function includePrefixes(include) {
|
|
2231
|
+
if (include === void 0) return;
|
|
2232
|
+
return include.map((item) => {
|
|
2233
|
+
if ("key" in item && typeof item.key === "string") return item.key;
|
|
2234
|
+
const first = listPermissions(item)[0];
|
|
2235
|
+
if (first === void 0) return "";
|
|
2236
|
+
const parts = first.key.split(".");
|
|
2237
|
+
parts.pop();
|
|
2238
|
+
return parts.join(".");
|
|
2160
2239
|
});
|
|
2161
2240
|
}
|
|
2162
2241
|
/**
|
|
2163
|
-
*
|
|
2164
|
-
*
|
|
2165
|
-
*
|
|
2166
|
-
* read once; a result cut short by a cycle is path-specific and not kept.
|
|
2242
|
+
* The grant with its grantee's row condition. A graph relation needs the
|
|
2243
|
+
* `RelationSource`, which snapshots and `where()` do not carry, so the grant
|
|
2244
|
+
* becomes server-only (`portable: false`) there.
|
|
2167
2245
|
*/
|
|
2168
|
-
|
|
2169
|
-
const
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
cut: false
|
|
2179
|
-
};
|
|
2180
|
-
let holders = ctx.reader.holders(group);
|
|
2181
|
-
if (holders === "pending") {
|
|
2182
|
-
await settle$1(ctx.cache);
|
|
2183
|
-
holders = ctx.reader.holders(group);
|
|
2184
|
-
}
|
|
2185
|
-
if (holders === "pending" || holders === "failed") return;
|
|
2186
|
-
const path = new Set(seen).add(key);
|
|
2187
|
-
const out = /* @__PURE__ */ new Set();
|
|
2188
|
-
let cut = false;
|
|
2189
|
-
for (const holder of holders) {
|
|
2190
|
-
if ("principal" in holder) {
|
|
2191
|
-
out.add(holder.principal.id);
|
|
2192
|
-
continue;
|
|
2193
|
-
}
|
|
2194
|
-
const nested = JSON.stringify([
|
|
2195
|
-
holder.group.resource,
|
|
2196
|
-
holder.group.id,
|
|
2197
|
-
holder.group.relation
|
|
2198
|
-
]);
|
|
2199
|
-
const same = holder.group.resource === group.resource;
|
|
2200
|
-
if (path.has(nested)) {
|
|
2201
|
-
cut = true;
|
|
2202
|
-
continue;
|
|
2203
|
-
}
|
|
2204
|
-
if (same && budget <= 0) continue;
|
|
2205
|
-
const walk = await groupMembers(holder.group, ctx, same ? budget - 1 : 16, path, memo);
|
|
2206
|
-
if (walk === void 0) return;
|
|
2207
|
-
cut ||= walk.cut;
|
|
2208
|
-
for (const member of walk.members) out.add(member);
|
|
2209
|
-
}
|
|
2210
|
-
const members = [...out];
|
|
2211
|
-
if (!cut) memo.set(memoKey, members);
|
|
2212
|
-
return {
|
|
2213
|
-
members,
|
|
2214
|
-
cut
|
|
2215
|
-
};
|
|
2246
|
+
function graphAware(grant, where) {
|
|
2247
|
+
const combined = combineWhere(grant.where, where);
|
|
2248
|
+
const graph = hasConditionOp(combined, "related");
|
|
2249
|
+
const inherits = flattenGrantee(grant.to).some((item) => item.kind === "inherit");
|
|
2250
|
+
return freezeDeep(compact({
|
|
2251
|
+
...grant,
|
|
2252
|
+
where: combined,
|
|
2253
|
+
portable: graph ? false : grant.portable,
|
|
2254
|
+
graph: graph && grant.portable && !inherits ? true : void 0
|
|
2255
|
+
}));
|
|
2216
2256
|
}
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2257
|
+
/**
|
|
2258
|
+
* `where()` keeps a grant that is server-only because it reads the graph:
|
|
2259
|
+
* its `related` nodes compile to subqueries given relation mappings.
|
|
2260
|
+
*/
|
|
2261
|
+
function whereGrant(grant) {
|
|
2262
|
+
return grant.graph === true ? freezeDeep({
|
|
2263
|
+
...grant,
|
|
2264
|
+
portable: true
|
|
2265
|
+
}) : grant;
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Every grant the subject may reach, once per membership that holds all of
|
|
2269
|
+
* its scoped roles, so a client can check that membership's tenant, team or
|
|
2270
|
+
* resource. A scoped role held only globally reaches nothing.
|
|
2271
|
+
*/
|
|
2272
|
+
function collectSnapshotGrants(policy, subject, customRoles, now = nowSeconds(), customGrants = customGrantsFor(policy, customRoles), mode = "held") {
|
|
2273
|
+
const skip = (grant, match) => mode === "held" ? !match.matched : match.matched || match.reason !== "not-entitled" || grant.effect !== "allow";
|
|
2274
|
+
const scopes = scopeList(policy.scopes);
|
|
2275
|
+
const declared = declaredRoleNames(policy);
|
|
2276
|
+
const global = expandRoleNames(subject.principal?.roles ?? [], declared, customRoles, subject.principal?.tenant);
|
|
2277
|
+
const held = (subject.principal?.memberships ?? []).map((membership) => ({
|
|
2278
|
+
membership,
|
|
2279
|
+
roles: new Set(expandRoleNames(membership.roles, declared, customRoles, tenantOf(membership, scopes)).roles)
|
|
2280
|
+
}));
|
|
2281
|
+
const holds = (entry, item) => entry.roles.has(item.role) && (typeof item.scope === "string" ? entry.membership.scope === item.scope : entry.membership.on !== void 0);
|
|
2282
|
+
const requirementOf = (grant, resource) => grant.requires === void 0 || grant.effect !== "allow" ? void 0 : requirementCondition(policy, subject, grant.requires, resource, now, customRoles, customGrants);
|
|
2283
|
+
const out = [];
|
|
2284
|
+
for (const grant of grantList(policy)) {
|
|
2285
|
+
if (mode === "held" && !coversRequirements(policy.permissions, grant.requires, subject.delegation)) continue;
|
|
2286
|
+
const resource = getResource(policy.permissions, grant.permission.resource);
|
|
2287
|
+
const match = matchGrantee(grant.to, subject, now, resource, scopes, policy.resources);
|
|
2288
|
+
if (skip(grant, match)) continue;
|
|
2289
|
+
const roleItems = flattenGrantee(grant.to).filter((item) => item.kind === "role");
|
|
2290
|
+
if (roleItems.length > 0 && subject.principal === null) continue;
|
|
2291
|
+
if (!roleItems.every((item) => item.scope !== "global" || global.roles.includes(item.role))) continue;
|
|
2292
|
+
const merged = graphAware(grant, combineWhere(match.where, requirementOf(grant, resource)));
|
|
2293
|
+
const scoped = roleItems.filter((item) => item.scope !== "global");
|
|
2294
|
+
if (scoped.length === 0) {
|
|
2295
|
+
out.push({ grant: merged });
|
|
2239
2296
|
continue;
|
|
2240
2297
|
}
|
|
2241
|
-
for (const
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
const condition = relationCondition(item, ctx.resource, ctx.scopes, { resources: ctx.policy.resources });
|
|
2246
|
-
if (condition?.op === "related") {
|
|
2247
|
-
await discoverGraph(condition, allow, ctx);
|
|
2248
|
-
return;
|
|
2249
|
-
}
|
|
2250
|
-
const spec = ctx.resource?.relations[item.relation];
|
|
2251
|
-
if (spec?.includes !== void 0 && !expanded) {
|
|
2252
|
-
for (const name of expandRelation(ctx.resource, item.relation)) await discoverRelation({
|
|
2253
|
-
...item,
|
|
2254
|
-
relation: name
|
|
2255
|
-
}, allow, ctx, true);
|
|
2256
|
-
return;
|
|
2298
|
+
for (const entry of held) if (keepsPermission(entry.membership, grant.permission.key) && scoped.every((item) => item.kind === "role" && holds(entry, item))) out.push({
|
|
2299
|
+
grant: merged,
|
|
2300
|
+
membership: entry.membership
|
|
2301
|
+
});
|
|
2257
2302
|
}
|
|
2258
|
-
const
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
const
|
|
2266
|
-
|
|
2267
|
-
const
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
}
|
|
2272
|
-
for (const member of members) ctx.add(member.id, via, member.membership);
|
|
2273
|
-
return;
|
|
2303
|
+
for (const { grant, role } of customGrants) {
|
|
2304
|
+
const globalHeld = holdsGlobalCustomRole(subject.principal?.roles, role);
|
|
2305
|
+
const holders = held.filter((entry) => keepsPermission(entry.membership, grant.permission.key) && holdsCustomRole(entry.membership, role, scopes));
|
|
2306
|
+
if (holders.length === 0 && !globalHeld) continue;
|
|
2307
|
+
const resource = getResource(policy.permissions, grant.permission.resource);
|
|
2308
|
+
const match = matchGrantee(grant.to, subject, now, resource, scopes, policy.resources);
|
|
2309
|
+
if (skip(grant, match)) continue;
|
|
2310
|
+
const merged = graphAware(grant, combineWhere(match.where, requirementOf(grant, resource)));
|
|
2311
|
+
if (globalHeld) out.push({ grant: merged });
|
|
2312
|
+
for (const entry of holders) out.push({
|
|
2313
|
+
grant: merged,
|
|
2314
|
+
membership: entry.membership
|
|
2315
|
+
});
|
|
2274
2316
|
}
|
|
2275
|
-
const
|
|
2276
|
-
|
|
2277
|
-
|
|
2317
|
+
const fresh = policy.fresh ?? [];
|
|
2318
|
+
if (mode === "held" && subject.stale === true && fresh.length > 0) return out.filter(({ grant }) => grant.effect === "deny" || !fresh.includes(grant.permission.key));
|
|
2319
|
+
return out;
|
|
2278
2320
|
}
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
case "inherit":
|
|
2290
|
-
ctx.incomplete();
|
|
2291
|
-
return Promise.resolve();
|
|
2292
|
-
default: return unknownKind(item, ctx);
|
|
2293
|
-
}
|
|
2321
|
+
//#endregion
|
|
2322
|
+
//#region src/core/assignable.ts
|
|
2323
|
+
/**
|
|
2324
|
+
* Declared roles a tenant admin may hand out or compose, before any
|
|
2325
|
+
* intersection: `assignable` roles, plus every role some `assigns` lists.
|
|
2326
|
+
*/
|
|
2327
|
+
function assignableCandidates(policy) {
|
|
2328
|
+
const listed = new Set(policy.roles.flatMap((binding) => binding.assigns ?? []));
|
|
2329
|
+
const names = [...listRoles(policy.vocabulary?.roles).filter((leaf) => leaf.assignable || listed.has(leaf.key)).map((leaf) => leaf.key), ...policy.roles.filter((binding) => binding.assignable || listed.has(binding.name)).map((binding) => binding.name)];
|
|
2330
|
+
return rankRoles(policy, [...new Set(names)]).map((name) => roleLeaf(policy, name));
|
|
2294
2331
|
}
|
|
2295
|
-
/**
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2332
|
+
/**
|
|
2333
|
+
* The levels a held allow of a permission covers: its own `level`, every
|
|
2334
|
+
* level when it has no row condition, or the levels whose condition equals
|
|
2335
|
+
* its condition.
|
|
2336
|
+
*/
|
|
2337
|
+
function heldLevels(policy, grant, names) {
|
|
2338
|
+
if (grant.level !== void 0) return [grant.level];
|
|
2339
|
+
if (grant.where === void 0) return names;
|
|
2340
|
+
const held = canonicalJson(grant.where);
|
|
2341
|
+
return names.filter((name) => {
|
|
2342
|
+
const condition = levelCondition(policy, grant.permission.resource, name);
|
|
2343
|
+
return condition !== void 0 && canonicalJson(condition) === held;
|
|
2344
|
+
});
|
|
2299
2345
|
}
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
}
|
|
2315
|
-
};
|
|
2316
|
-
return (scope, id) => {
|
|
2317
|
-
const key = JSON.stringify([scope, id]);
|
|
2318
|
-
const known = listed.get(key) ?? load(scope, id);
|
|
2319
|
-
listed.set(key, known);
|
|
2320
|
-
return known;
|
|
2346
|
+
/**
|
|
2347
|
+
* What the subject may hand out in `tenant`: declared assignable roles and the
|
|
2348
|
+
* tenant custom-role ceiling, narrowed by `allowed` (`RoleSource.assignable`),
|
|
2349
|
+
* then intersected with the permissions the subject holds there. Holding a
|
|
2350
|
+
* role or a granted permission with `meta.manageRoles` lifts the intersection.
|
|
2351
|
+
*/
|
|
2352
|
+
function assignableIn(policy, subject, customRoles, customGrants, tenant, allowed, now = nowSeconds(), global = false) {
|
|
2353
|
+
const principal = subject.principal;
|
|
2354
|
+
if (principal === null) return {
|
|
2355
|
+
roles: [],
|
|
2356
|
+
permissions: [],
|
|
2357
|
+
levels: {},
|
|
2358
|
+
manage: false,
|
|
2359
|
+
inert: []
|
|
2321
2360
|
};
|
|
2322
|
-
}
|
|
2323
|
-
async function whoCan(input) {
|
|
2324
|
-
const { policy, permission, row } = input;
|
|
2325
|
-
if (permission.kind !== "instance" || row === null || typeof row !== "object") return freezeDeep({
|
|
2326
|
-
permission: permission.key,
|
|
2327
|
-
holders: [],
|
|
2328
|
-
complete: false
|
|
2329
|
-
});
|
|
2330
2361
|
const scopes = scopeList(policy.scopes);
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
add(id, via, membership) {
|
|
2343
|
-
const entry = found.get(id) ?? {
|
|
2344
|
-
memberships: [],
|
|
2345
|
-
via: []
|
|
2346
|
-
};
|
|
2347
|
-
found.set(id, entry);
|
|
2348
|
-
if (via !== void 0) entry.via.push(via);
|
|
2349
|
-
if (membership !== void 0) entry.memberships.push(membership);
|
|
2350
|
-
},
|
|
2351
|
-
incomplete() {
|
|
2352
|
-
complete = false;
|
|
2353
|
-
}
|
|
2354
|
-
};
|
|
2355
|
-
await Promise.all((policy.index.grantsByKey.get(permission.key) ?? []).flatMap((grant) => flattenGrantee(grant.to).map((item) => discover(grant, item, ctx))));
|
|
2356
|
-
const root = rootScope(scopes);
|
|
2357
|
-
const tenantKey = root === void 0 ? void 0 : scopeField(getResource(policy.permissions, permission.resource), root, scopes);
|
|
2358
|
-
const tenant = tenantKey === void 0 ? void 0 : relationId(ownGet(row, tenantKey));
|
|
2359
|
-
const env = {
|
|
2362
|
+
const scoped = tenant === void 0 ? subject : freezeDeep(compact({
|
|
2363
|
+
...subject,
|
|
2364
|
+
principal: compact({
|
|
2365
|
+
...principal,
|
|
2366
|
+
tenant: resolveActiveTenant(principal, tenant, scopes, now)
|
|
2367
|
+
})
|
|
2368
|
+
}));
|
|
2369
|
+
const heldGrants = collectSnapshotGrants(policy, scoped, customRoles, now, customGrants).filter((item) => item.grant.effect === "allow" && (item.membership === void 0 || !global && tenantOf(item.membership, scopes) === tenant && !isMembershipExpired(item.membership, now))).map((item) => item.grant);
|
|
2370
|
+
const heldKeys = new Set(heldGrants.map((grant) => grant.permission.key));
|
|
2371
|
+
const heldNames = heldRoleNames(policy, subject, tenant, void 0, now);
|
|
2372
|
+
const quiet = {
|
|
2360
2373
|
emit: false,
|
|
2361
2374
|
simulated: true,
|
|
2362
2375
|
skipAlternatives: true,
|
|
2363
|
-
customRoles
|
|
2364
|
-
customGrants
|
|
2376
|
+
customRoles,
|
|
2377
|
+
customGrants,
|
|
2365
2378
|
listeners: emptyListeners(),
|
|
2366
2379
|
sink: void 0,
|
|
2367
2380
|
limits: void 0,
|
|
2368
2381
|
limitCache: /* @__PURE__ */ new Map(),
|
|
2369
|
-
team:
|
|
2370
|
-
relations: input.reader
|
|
2382
|
+
team: void 0
|
|
2371
2383
|
};
|
|
2372
|
-
const
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
trusted: true,
|
|
2387
|
-
now
|
|
2388
|
-
}, env);
|
|
2384
|
+
const manage = heldNames.some((name) => roleMeta(policy, name)?.manageRoles === true) || listPermissions(policy.permissions).some((leaf) => leaf.meta.manageRoles === true && evaluate(policy, scoped, leaf, void 0, {
|
|
2385
|
+
source: "simulate",
|
|
2386
|
+
trusted: true,
|
|
2387
|
+
now
|
|
2388
|
+
}, quiet).outcome === "granted");
|
|
2389
|
+
const narrowed = allowed === void 0 ? void 0 : new Set(allowed);
|
|
2390
|
+
const candidates = assignableCandidates(policy).filter((leaf) => narrowed === void 0 || narrowed.has(leaf.key));
|
|
2391
|
+
const held = new Set(heldNames);
|
|
2392
|
+
const listed = usesAssigns(policy) ? new Set(heldNames.flatMap((name) => policy.rolesByName.get(name)?.assigns ?? [])) : void 0;
|
|
2393
|
+
const roles = manage ? candidates : candidates.filter((leaf) => {
|
|
2394
|
+
if (listed !== void 0) return listed.has(leaf.key);
|
|
2395
|
+
if (held.has(leaf.key)) return true;
|
|
2396
|
+
const keys = [...roleAllowKeys(policy, leaf.key)];
|
|
2397
|
+
return keys.length > 0 && keys.every((key) => heldKeys.has(key));
|
|
2389
2398
|
});
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2399
|
+
const root = global ? "global" : rootScope(scopes);
|
|
2400
|
+
const ceiling = new Set((root === void 0 ? [] : ceilingGrants(policy, root, allowed)).map((grant) => grant.permission.key));
|
|
2401
|
+
const permissions = listPermissions(policy.permissions).filter((leaf) => ceiling.has(leaf.key) && (manage || heldKeys.has(leaf.key)));
|
|
2402
|
+
const offeredLevels = (key, names) => {
|
|
2403
|
+
if (manage) return names;
|
|
2404
|
+
const reached = new Set(heldGrants.filter((grant) => grant.permission.key === key).flatMap((grant) => heldLevels(policy, grant, names)));
|
|
2405
|
+
return names.filter((name) => reached.has(name));
|
|
2406
|
+
};
|
|
2407
|
+
const levels = {};
|
|
2408
|
+
for (const leaf of permissions) {
|
|
2409
|
+
const names = leaf.kind === "instance" ? levelNames(policy, leaf.resource) : [];
|
|
2410
|
+
if (names.length > 0) levels[leaf.key] = offeredLevels(leaf.key, names);
|
|
2394
2411
|
}
|
|
2395
|
-
const
|
|
2396
|
-
|
|
2397
|
-
const
|
|
2398
|
-
|
|
2399
|
-
|
|
2412
|
+
const ceilingKeys = /* @__PURE__ */ new Map();
|
|
2413
|
+
const mayHandOut = (scope, grant) => {
|
|
2414
|
+
const key = grant.permission.key;
|
|
2415
|
+
let keys = ceilingKeys.get(scope);
|
|
2416
|
+
if (keys === void 0) {
|
|
2417
|
+
keys = new Set(ceilingGrants(policy, scope, allowed).map((item) => item.permission.key));
|
|
2418
|
+
ceilingKeys.set(scope, keys);
|
|
2419
|
+
}
|
|
2420
|
+
if (!keys.has(key) || !(manage || heldKeys.has(key))) return false;
|
|
2421
|
+
const names = grant.permission.kind === "instance" ? levelNames(policy, grant.permission.resource) : [];
|
|
2422
|
+
if (names.length === 0) return true;
|
|
2423
|
+
const offered = offeredLevels(key, names);
|
|
2424
|
+
return heldLevels(policy, grant, names).every((name) => offered.includes(name));
|
|
2425
|
+
};
|
|
2426
|
+
const custom = global || tenant === void 0 ? {
|
|
2427
|
+
roles: [],
|
|
2428
|
+
inert: []
|
|
2429
|
+
} : assignableCustomRoles(policy, customRoles, tenant, manage ? void 0 : roles, mayHandOut);
|
|
2430
|
+
return {
|
|
2431
|
+
roles: [...roles, ...custom.roles],
|
|
2432
|
+
permissions,
|
|
2433
|
+
levels,
|
|
2434
|
+
manage,
|
|
2435
|
+
inert: custom.inert
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* The tenant's custom roles the subject may hand out: held at a scope it may
|
|
2440
|
+
* assign declared roles at (any scope with `meta.manageRoles`), allowing at
|
|
2441
|
+
* least one permission after the ceiling, and only permissions and levels it
|
|
2442
|
+
* may hand out at that scope. A role that allows nothing is `inert`.
|
|
2443
|
+
*/
|
|
2444
|
+
function assignableCustomRoles(policy, customRoles, tenant, declared, mayHandOut) {
|
|
2445
|
+
const scopes = scopeList(policy.scopes);
|
|
2446
|
+
const root = rootScope(scopes);
|
|
2447
|
+
const assignsAt = declared === void 0 ? void 0 : new Set(declared.map((leaf) => leaf.on === void 0 ? root : resolveScope(scopes, leaf.on)));
|
|
2448
|
+
const out = /* @__PURE__ */ new Map();
|
|
2449
|
+
const inert = /* @__PURE__ */ new Set();
|
|
2450
|
+
for (const role of tenantCustomRoles(policy, customRoles)) {
|
|
2451
|
+
if (role.tenant !== tenant || out.has(role.name)) continue;
|
|
2452
|
+
const at = customRoleScope(role, scopes);
|
|
2453
|
+
if (at === void 0 || assignsAt !== void 0 && !assignsAt.has(at)) continue;
|
|
2454
|
+
const { grants } = resolveCustomRole(policy, role);
|
|
2455
|
+
if (!grants.some((grant) => grant.effect === "allow")) {
|
|
2456
|
+
inert.add(role.name);
|
|
2400
2457
|
continue;
|
|
2401
2458
|
}
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2459
|
+
if (grants.every((grant) => grant.effect === "deny" || mayHandOut(at, grant))) out.set(role.name, synthesiseRole(role.name, compact({
|
|
2460
|
+
on: at,
|
|
2461
|
+
assignable: true,
|
|
2462
|
+
meta: sourceRoleMeta(role.meta, treeXSchema(policy.vocabulary?.roles))
|
|
2463
|
+
})));
|
|
2464
|
+
}
|
|
2465
|
+
return {
|
|
2466
|
+
roles: [...out.values()].toSorted((a, b) => a.key.localeCompare(b.key)),
|
|
2467
|
+
inert: [...inert].filter((name) => !out.has(name)).toSorted()
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
//#endregion
|
|
2471
|
+
//#region src/core/instance-snapshot.ts
|
|
2472
|
+
/** The one path from a resolved subject to a `Snapshot`; pure apart from the default clock. */
|
|
2473
|
+
function snapshotOf(policy, subject, options) {
|
|
2474
|
+
const now = options.now ?? nowSeconds();
|
|
2475
|
+
const customGrants = options.customGrants ?? customGrantsFor(policy, options.customRoles);
|
|
2476
|
+
const roles = heldRoleNames(policy, subject, subject.principal?.tenant, void 0, now);
|
|
2477
|
+
const audiences = audiencesOf(policy, roles);
|
|
2478
|
+
return buildSnapshot(compact({
|
|
2479
|
+
subject,
|
|
2480
|
+
roles,
|
|
2481
|
+
audiences: audiences.length === 0 ? void 0 : audiences,
|
|
2482
|
+
notEntitled: collectSnapshotGrants(policy, subject, options.customRoles, now, customGrants, "not-entitled"),
|
|
2483
|
+
grants: collectSnapshotGrants(policy, subject, options.customRoles, now, customGrants).map((item) => periodBound(policy, item.grant) ? Object.assign({}, item, { grant: freezeDeep({
|
|
2484
|
+
...item.grant,
|
|
2485
|
+
portable: false
|
|
2486
|
+
}) }) : item),
|
|
2487
|
+
include: includePrefixes(options.include),
|
|
2488
|
+
tenants: options.tenants,
|
|
2489
|
+
simulated: options.simulated,
|
|
2490
|
+
now: Math.floor(now),
|
|
2491
|
+
vocabulary: policy.vocabulary,
|
|
2492
|
+
scopes: snapshotScopes(policy),
|
|
2493
|
+
idOf: (name) => getResource(policy.permissions, name)?.id,
|
|
2494
|
+
delegated: delegatedPermissions(policy.delegations, subject, new Set(roles), now),
|
|
2495
|
+
assignable: (tenant) => {
|
|
2496
|
+
const found = assignableIn(policy, subject, options.customRoles, customGrants, tenant, options.assignable?.get(tenant), now);
|
|
2497
|
+
return compact({
|
|
2498
|
+
tenant,
|
|
2499
|
+
roles: found.roles.map((leaf) => leaf.key),
|
|
2500
|
+
permissions: found.permissions,
|
|
2501
|
+
levels: Object.keys(found.levels).length === 0 ? void 0 : found.levels
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
}));
|
|
2505
|
+
}
|
|
2506
|
+
/** The policy's scopes as snapshots and `permdock cloud push` carry them, with the resources each partitions. */
|
|
2507
|
+
function snapshotScopes(policy) {
|
|
2508
|
+
if (policy.scopes.length === 0) return;
|
|
2509
|
+
const resources = [...policy.resources.values()];
|
|
2510
|
+
return policy.scopes.map((scope) => {
|
|
2511
|
+
const nodes = resources.filter((node) => partitionsOf(node, policy.scopes).includes(scope.name));
|
|
2512
|
+
const partitioned = nodes.map((node) => node.name);
|
|
2513
|
+
const fields = Object.fromEntries(nodes.flatMap((node) => {
|
|
2514
|
+
const field = scopeField(node, scope.name, policy.scopes);
|
|
2515
|
+
return field === void 0 || field === scope.key ? [] : [[node.name, field]];
|
|
2516
|
+
}));
|
|
2517
|
+
return compact({
|
|
2518
|
+
name: scope.name,
|
|
2519
|
+
key: scope.key ?? "",
|
|
2520
|
+
within: scope.within,
|
|
2521
|
+
resources: partitioned.length === 0 ? void 0 : partitioned,
|
|
2522
|
+
fields: Object.keys(fields).length === 0 ? void 0 : fields
|
|
2405
2523
|
});
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
//#endregion
|
|
2527
|
+
//#region src/core/assert-decision.ts
|
|
2528
|
+
/** `assert`'s outcome: the granted decision, or the error a refusal throws after `onDenied` runs. */
|
|
2529
|
+
function assertDecision(policy, subject, permission, data, decision, onDenied) {
|
|
2530
|
+
if (decision.outcome === "granted") return decision;
|
|
2531
|
+
if (onDenied !== void 0) onDenied(decision);
|
|
2532
|
+
const resource = getResource(policy.permissions, permission.resource);
|
|
2533
|
+
const resourceId = data !== null && typeof data === "object" ? data[resource?.id ?? "id"] : void 0;
|
|
2534
|
+
const resourceRef = compact({
|
|
2535
|
+
type: permission.resource,
|
|
2536
|
+
id: resourceId === void 0 ? void 0 : String(resourceId)
|
|
2537
|
+
});
|
|
2538
|
+
if (decision.outcome === "approval-required") throw new PermDockApprovalRequiredError({
|
|
2539
|
+
decision,
|
|
2540
|
+
permission: permission.key,
|
|
2541
|
+
scope: permission.scope,
|
|
2542
|
+
resource: resourceRef,
|
|
2543
|
+
message: approvalMessage(permission.key, decision.reason, decision.token)
|
|
2544
|
+
});
|
|
2545
|
+
if (decision.denials.some((denial) => denial.reason === "validation")) {
|
|
2546
|
+
const detail = decision.denials[0]?.detail;
|
|
2547
|
+
if (detail instanceof PermDockValidationError) throw detail;
|
|
2406
2548
|
}
|
|
2407
|
-
|
|
2549
|
+
throw new PermDockDeniedError({
|
|
2550
|
+
decision,
|
|
2408
2551
|
permission: permission.key,
|
|
2409
|
-
|
|
2410
|
-
|
|
2552
|
+
scope: permission.scope,
|
|
2553
|
+
resource: resourceRef,
|
|
2554
|
+
subject,
|
|
2555
|
+
message: deniedMessage(permission.key, subject.principal?.id, decision.denials, decision.alternatives.map((leaf) => leaf.key)),
|
|
2556
|
+
...data === void 0 || resource === void 0 ? {} : { disclosure: resource.disclosure }
|
|
2411
2557
|
});
|
|
2412
2558
|
}
|
|
2413
2559
|
//#endregion
|
|
2414
|
-
//#region src/core/instance.ts
|
|
2415
|
-
/**
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
const
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2560
|
+
//#region src/core/instance-env.ts
|
|
2561
|
+
/** The environment `derive(options)` builds on: `base` with the sources `options` replaces loaded again. */
|
|
2562
|
+
function deriveEnv(policy, subject, base, relationCache, options) {
|
|
2563
|
+
const scopes = scopeList(policy.scopes);
|
|
2564
|
+
const tenants = tenantsOf(subject.principal, scopes);
|
|
2565
|
+
const auth = [];
|
|
2566
|
+
const named = options.customRoles === void 0 ? void 0 : roleSourceFor(options.customRoles, subject, auth);
|
|
2567
|
+
const roleSource = options.customRoles === void 0 ? base.roleSource : named;
|
|
2568
|
+
const roles = options.customRoles === void 0 ? base.customRoles : customRolesFor(named, tenants, auth, subject.principal === null ? void 0 : subject.principal.roles ?? [], (tenant) => heldRoleNamesIn(subject.principal, scopes, tenant));
|
|
2569
|
+
const names = options.customRoles === void 0 ? base.assignable : assignableNamesFor(named, tenants, auth);
|
|
2570
|
+
const approvals = options.approvalPolicies === void 0 ? base.approvalPolicies : approvalPoliciesFor(policy, options.approvalPolicies, tenants, auth);
|
|
2571
|
+
const envWith = (loadedRoles, loadedNames, loadedApprovals) => compact({
|
|
2572
|
+
...base,
|
|
2573
|
+
customRoles: loadedRoles,
|
|
2574
|
+
roleSource,
|
|
2575
|
+
assignable: loadedNames,
|
|
2576
|
+
approvalPolicies: loadedApprovals,
|
|
2577
|
+
relations: options.relations ?? base.relations,
|
|
2578
|
+
relationCache: options.relations === void 0 ? relationCache : void 0,
|
|
2579
|
+
queuedAuth: auth,
|
|
2580
|
+
queuedErrors: []
|
|
2424
2581
|
});
|
|
2582
|
+
if (isThenable(roles) || isThenable(names) || isThenable(approvals)) return Promise.all([
|
|
2583
|
+
roles,
|
|
2584
|
+
names,
|
|
2585
|
+
approvals
|
|
2586
|
+
]).then(([loadedRoles, loadedNames, loadedApprovals]) => envWith(loadedRoles, loadedNames, loadedApprovals));
|
|
2587
|
+
return envWith(roles, names, approvals);
|
|
2425
2588
|
}
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2589
|
+
//#endregion
|
|
2590
|
+
//#region src/core/simulate-preview.ts
|
|
2591
|
+
function previewSubject(policy, subject, preview) {
|
|
2592
|
+
const previewRoles = preview.roles?.map((item) => typeof item === "string" ? item : item.key);
|
|
2593
|
+
const kinds = subject.principal === null ? void 0 : applyRoleKinds(policy, previewRoles ?? subject.principal.roles, preview.memberships === void 0 ? subject.principal.memberships ?? [] : normalizeMemberships(preview.memberships, scopeList(policy.scopes)));
|
|
2594
|
+
const principal = subject.principal === null || kinds === void 0 ? null : freezeDeep(compact({
|
|
2595
|
+
...subject.principal,
|
|
2596
|
+
roles: kinds.roles,
|
|
2597
|
+
memberships: preview.memberships === void 0 && subject.principal.memberships === void 0 ? void 0 : kinds.memberships,
|
|
2598
|
+
tenant: preview.tenant ?? subject.principal.tenant
|
|
2599
|
+
}));
|
|
2600
|
+
return freezeDeep({
|
|
2601
|
+
...subject,
|
|
2602
|
+
principal
|
|
2435
2603
|
});
|
|
2436
2604
|
}
|
|
2437
|
-
/**
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
const
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
const
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2605
|
+
/** The one `simulate` event for a batch: the worst decision, with the counts. */
|
|
2606
|
+
function simulateEvent(policy, subject, env, evaluated) {
|
|
2607
|
+
const counts = {
|
|
2608
|
+
granted: 0,
|
|
2609
|
+
denied: 0,
|
|
2610
|
+
approvalRequired: 0
|
|
2611
|
+
};
|
|
2612
|
+
for (const [, , decision] of evaluated) if (decision.outcome === "granted") counts.granted += 1;
|
|
2613
|
+
else if (decision.outcome === "approval-required") counts.approvalRequired += 1;
|
|
2614
|
+
else counts.denied += 1;
|
|
2615
|
+
const worst = evaluated.find(([, , decision]) => decision.outcome === "denied") ?? evaluated.find(([, , decision]) => decision.outcome === "approval-required") ?? evaluated[0];
|
|
2616
|
+
if (worst === void 0) return;
|
|
2617
|
+
const [permission, data, decision] = worst;
|
|
2618
|
+
finish(policy, subject, permission, data, decision, {
|
|
2619
|
+
source: "simulate",
|
|
2620
|
+
trusted: true
|
|
2621
|
+
}, {
|
|
2622
|
+
...env,
|
|
2623
|
+
emit: true
|
|
2624
|
+
}, true, void 0, counts);
|
|
2625
|
+
}
|
|
2626
|
+
//#endregion
|
|
2627
|
+
//#region src/core/who-can.ts
|
|
2628
|
+
const MAX_ROUNDS = 48;
|
|
2629
|
+
async function settle$1(cache) {
|
|
2630
|
+
for (let round = 0; round < MAX_ROUNDS; round += 1) {
|
|
2631
|
+
const pending = pendingRelations(cache);
|
|
2632
|
+
if (pending.length === 0) return;
|
|
2633
|
+
await Promise.all(pending);
|
|
2452
2634
|
}
|
|
2453
|
-
return rankRoles(policy, [...names]);
|
|
2454
2635
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2636
|
+
/** The holders on each instance a graph relation reads, or `undefined` when the graph cannot say. */
|
|
2637
|
+
async function walkHolders(condition, row, reader, cache) {
|
|
2638
|
+
let walk = relationWalk(condition, row, reader);
|
|
2639
|
+
if (walk === "relation-unavailable") {
|
|
2640
|
+
await settle$1(cache);
|
|
2641
|
+
walk = relationWalk(condition, row, reader);
|
|
2642
|
+
}
|
|
2643
|
+
if (walk === void 0) return [];
|
|
2644
|
+
if (walk === "relation-depth" || walk === "relation-unavailable") return;
|
|
2645
|
+
const { ids } = walk;
|
|
2646
|
+
const read = () => ids.map((id) => reader.holders({
|
|
2647
|
+
resource: condition.resource,
|
|
2648
|
+
id,
|
|
2649
|
+
relation: condition.relation
|
|
2461
2650
|
}));
|
|
2651
|
+
let answers = read();
|
|
2652
|
+
if (answers.includes("pending")) {
|
|
2653
|
+
await settle$1(cache);
|
|
2654
|
+
answers = read();
|
|
2655
|
+
}
|
|
2656
|
+
const out = [];
|
|
2657
|
+
for (const [index, answer] of answers.entries()) {
|
|
2658
|
+
const id = ids[index];
|
|
2659
|
+
if (answer === "pending" || answer === "failed" || id === void 0) return;
|
|
2660
|
+
out.push({
|
|
2661
|
+
id,
|
|
2662
|
+
holders: answer
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
return out;
|
|
2462
2666
|
}
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
const
|
|
2470
|
-
|
|
2471
|
-
const
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2667
|
+
async function discoverRole(item, allow, ctx) {
|
|
2668
|
+
if (typeof item.scope !== "string" || item.scope === "global") {
|
|
2669
|
+
ctx.incomplete();
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
const key = scopeField(ctx.resource, item.scope, ctx.scopes);
|
|
2673
|
+
const id = key === void 0 ? void 0 : relationId(ownGet(ctx.row, key));
|
|
2674
|
+
if (id === void 0) return;
|
|
2675
|
+
const members = await ctx.list(item.scope, id);
|
|
2676
|
+
if (members === null) {
|
|
2677
|
+
ctx.incomplete();
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
for (const member of members.filter((entry) => entry.membership.roles.includes(item.role))) ctx.add(member.id, allow ? {
|
|
2681
|
+
kind: "role",
|
|
2682
|
+
role: item.role,
|
|
2683
|
+
membership: member.membership
|
|
2684
|
+
} : void 0, member.membership);
|
|
2478
2685
|
}
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
return
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2686
|
+
function holderVia(holder, at, share, group) {
|
|
2687
|
+
if (!share) return {
|
|
2688
|
+
kind: "relation",
|
|
2689
|
+
...at
|
|
2690
|
+
};
|
|
2691
|
+
return compact({
|
|
2692
|
+
kind: "share",
|
|
2693
|
+
...at,
|
|
2694
|
+
expiresAt: holder.expiresAt,
|
|
2695
|
+
group
|
|
2696
|
+
});
|
|
2488
2697
|
}
|
|
2489
2698
|
/**
|
|
2490
|
-
*
|
|
2491
|
-
* its
|
|
2492
|
-
*
|
|
2699
|
+
* Principals holding `group`'s relation, through at most `budget` more groups
|
|
2700
|
+
* of its resource; `undefined` when a read failed. `memo` keeps each group's
|
|
2701
|
+
* members per budget across one discovery, so a group shared by many paths is
|
|
2702
|
+
* read once; a result cut short by a cycle is path-specific and not kept.
|
|
2493
2703
|
*/
|
|
2494
|
-
function
|
|
2495
|
-
const
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
if (
|
|
2517
|
-
out.
|
|
2704
|
+
async function groupMembers(group, ctx, budget, seen, memo) {
|
|
2705
|
+
const key = JSON.stringify([
|
|
2706
|
+
group.resource,
|
|
2707
|
+
group.id,
|
|
2708
|
+
group.relation
|
|
2709
|
+
]);
|
|
2710
|
+
const memoKey = `${budget}:${key}`;
|
|
2711
|
+
const known = memo.get(memoKey);
|
|
2712
|
+
if (known !== void 0) return {
|
|
2713
|
+
members: known,
|
|
2714
|
+
cut: false
|
|
2715
|
+
};
|
|
2716
|
+
let holders = ctx.reader.holders(group);
|
|
2717
|
+
if (holders === "pending") {
|
|
2718
|
+
await settle$1(ctx.cache);
|
|
2719
|
+
holders = ctx.reader.holders(group);
|
|
2720
|
+
}
|
|
2721
|
+
if (holders === "pending" || holders === "failed") return;
|
|
2722
|
+
const path = new Set(seen).add(key);
|
|
2723
|
+
const out = /* @__PURE__ */ new Set();
|
|
2724
|
+
let cut = false;
|
|
2725
|
+
for (const holder of holders) {
|
|
2726
|
+
if ("principal" in holder) {
|
|
2727
|
+
out.add(holder.principal.id);
|
|
2518
2728
|
continue;
|
|
2519
2729
|
}
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2730
|
+
const nested = JSON.stringify([
|
|
2731
|
+
holder.group.resource,
|
|
2732
|
+
holder.group.id,
|
|
2733
|
+
holder.group.relation
|
|
2734
|
+
]);
|
|
2735
|
+
const same = holder.group.resource === group.resource;
|
|
2736
|
+
if (path.has(nested)) {
|
|
2737
|
+
cut = true;
|
|
2738
|
+
continue;
|
|
2739
|
+
}
|
|
2740
|
+
if (same && budget <= 0) continue;
|
|
2741
|
+
const walk = await groupMembers(holder.group, ctx, same ? budget - 1 : 16, path, memo);
|
|
2742
|
+
if (walk === void 0) return;
|
|
2743
|
+
cut ||= walk.cut;
|
|
2744
|
+
for (const member of walk.members) out.add(member);
|
|
2524
2745
|
}
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2746
|
+
const members = [...out];
|
|
2747
|
+
if (!cut) memo.set(memoKey, members);
|
|
2748
|
+
return {
|
|
2749
|
+
members,
|
|
2750
|
+
cut
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
async function discoverGraph(condition, allow, ctx) {
|
|
2754
|
+
const nodes = await walkHolders(condition, ctx.row, ctx.reader, ctx.cache);
|
|
2755
|
+
if (nodes === void 0) {
|
|
2756
|
+
ctx.incomplete();
|
|
2757
|
+
return;
|
|
2758
|
+
}
|
|
2759
|
+
const node = ctx.policy.resources.get(condition.resource);
|
|
2760
|
+
const memo = /* @__PURE__ */ new Map();
|
|
2761
|
+
for (const entry of nodes) for (const holder of entry.holders) {
|
|
2762
|
+
const at = {
|
|
2763
|
+
resource: condition.resource,
|
|
2764
|
+
relation: holder.relation,
|
|
2765
|
+
id: entry.id
|
|
2766
|
+
};
|
|
2767
|
+
const share = isEdgeRelation(node?.relations[holder.relation]);
|
|
2768
|
+
if ("principal" in holder) {
|
|
2769
|
+
ctx.add(holder.principal.id, allow ? holderVia(holder, at, share) : void 0);
|
|
2770
|
+
continue;
|
|
2771
|
+
}
|
|
2772
|
+
const walk = await groupMembers(holder.group, ctx, holder.group.resource === condition.resource ? 15 : 16, /* @__PURE__ */ new Set(), memo);
|
|
2773
|
+
if (walk === void 0) {
|
|
2774
|
+
ctx.incomplete();
|
|
2775
|
+
continue;
|
|
2776
|
+
}
|
|
2777
|
+
for (const member of walk.members) ctx.add(member, allow ? holderVia(holder, at, share, holder.group) : void 0);
|
|
2538
2778
|
}
|
|
2539
|
-
const fresh = policy.fresh ?? [];
|
|
2540
|
-
if (mode === "held" && subject.stale === true && fresh.length > 0) return out.filter(({ grant }) => grant.effect === "deny" || !fresh.includes(grant.permission.key));
|
|
2541
|
-
return out;
|
|
2542
2779
|
}
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2780
|
+
async function discoverRelation(item, allow, ctx, expanded = false) {
|
|
2781
|
+
const condition = relationCondition(item, ctx.resource, ctx.scopes, { resources: ctx.policy.resources });
|
|
2782
|
+
if (condition?.op === "related") {
|
|
2783
|
+
await discoverGraph(condition, allow, ctx);
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
const spec = ctx.resource?.relations[item.relation];
|
|
2787
|
+
if (spec?.includes !== void 0 && !expanded) {
|
|
2788
|
+
for (const name of expandRelation(ctx.resource, item.relation)) await discoverRelation({
|
|
2789
|
+
...item,
|
|
2790
|
+
relation: name
|
|
2791
|
+
}, allow, ctx, true);
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
const via = allow ? {
|
|
2795
|
+
kind: "relation",
|
|
2796
|
+
resource: ctx.permission.resource,
|
|
2797
|
+
relation: item.relation,
|
|
2798
|
+
id: relationId(ownGet(ctx.row, ctx.resource?.id ?? "id")) ?? ""
|
|
2799
|
+
} : void 0;
|
|
2800
|
+
if (isFieldRelation(spec) && spec.memberOf !== void 0) {
|
|
2801
|
+
const scope = resolveScope(ctx.scopes, spec.memberOf);
|
|
2802
|
+
const id = relationId(ownGet(ctx.row, spec.field));
|
|
2803
|
+
const members = scope === void 0 || id === void 0 ? [] : await ctx.list(scope, id);
|
|
2804
|
+
if (members === null) {
|
|
2805
|
+
ctx.incomplete();
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
for (const member of members) ctx.add(member.id, via, member.membership);
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
const column = isFieldRelation(spec) ? spec.field : isPrincipalRelation(spec) ? spec.principal : void 0;
|
|
2812
|
+
const holder = column === void 0 ? void 0 : relationId(ownGet(ctx.row, column));
|
|
2813
|
+
if (holder !== void 0) ctx.add(holder, via);
|
|
2551
2814
|
}
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2815
|
+
function discover(grant, item, ctx) {
|
|
2816
|
+
const allow = grant.effect === "allow";
|
|
2817
|
+
switch (item.kind) {
|
|
2818
|
+
case "role": return discoverRole(item, allow, ctx);
|
|
2819
|
+
case "relation": return discoverRelation(item, allow, ctx);
|
|
2820
|
+
case "anyone":
|
|
2821
|
+
case "authenticated":
|
|
2822
|
+
case "plan":
|
|
2823
|
+
case "actor":
|
|
2824
|
+
case "assurance":
|
|
2825
|
+
case "inherit":
|
|
2826
|
+
ctx.incomplete();
|
|
2827
|
+
return Promise.resolve();
|
|
2828
|
+
default: return unknownKind(item, ctx);
|
|
2829
|
+
}
|
|
2565
2830
|
}
|
|
2566
|
-
/**
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
const
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2831
|
+
/** Holders of a grantee kind this build does not know cannot be listed. */
|
|
2832
|
+
function unknownKind(_item, ctx) {
|
|
2833
|
+
ctx.incomplete();
|
|
2834
|
+
return Promise.resolve();
|
|
2835
|
+
}
|
|
2836
|
+
function memberLister(source, scopes) {
|
|
2837
|
+
const listed = /* @__PURE__ */ new Map();
|
|
2838
|
+
const load = async (scope, id) => {
|
|
2839
|
+
if (source?.list === void 0) return null;
|
|
2840
|
+
try {
|
|
2841
|
+
return (await source.list({
|
|
2842
|
+
scope,
|
|
2843
|
+
id
|
|
2844
|
+
})).flatMap((entry) => typeof entry.principal?.id === "string" ? normalizeMemberships([entry.membership], scopes).map((membership) => ({
|
|
2845
|
+
id: entry.principal.id,
|
|
2846
|
+
membership
|
|
2847
|
+
})) : []);
|
|
2848
|
+
} catch {
|
|
2849
|
+
return null;
|
|
2850
|
+
}
|
|
2851
|
+
};
|
|
2852
|
+
return (scope, id) => {
|
|
2853
|
+
const key = JSON.stringify([scope, id]);
|
|
2854
|
+
const known = listed.get(key) ?? load(scope, id);
|
|
2855
|
+
listed.set(key, known);
|
|
2856
|
+
return known;
|
|
2580
2857
|
};
|
|
2858
|
+
}
|
|
2859
|
+
async function whoCan(input) {
|
|
2860
|
+
const { policy, permission, row } = input;
|
|
2861
|
+
if (permission.kind !== "instance" || row === null || typeof row !== "object") return freezeDeep({
|
|
2862
|
+
permission: permission.key,
|
|
2863
|
+
holders: [],
|
|
2864
|
+
complete: false
|
|
2865
|
+
});
|
|
2581
2866
|
const scopes = scopeList(policy.scopes);
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2867
|
+
let complete = !input.customGrants.some((item) => item.grant.permission.key === permission.key);
|
|
2868
|
+
const found = /* @__PURE__ */ new Map();
|
|
2869
|
+
const ctx = {
|
|
2870
|
+
policy,
|
|
2871
|
+
scopes,
|
|
2872
|
+
resource: getResource(policy.permissions, permission.resource),
|
|
2873
|
+
permission,
|
|
2874
|
+
row,
|
|
2875
|
+
reader: input.reader,
|
|
2876
|
+
cache: input.cache,
|
|
2877
|
+
list: memberLister(input.memberships, scopes),
|
|
2878
|
+
add(id, via, membership) {
|
|
2879
|
+
const entry = found.get(id) ?? {
|
|
2880
|
+
memberships: [],
|
|
2881
|
+
via: []
|
|
2882
|
+
};
|
|
2883
|
+
found.set(id, entry);
|
|
2884
|
+
if (via !== void 0) entry.via.push(via);
|
|
2885
|
+
if (membership !== void 0) entry.memberships.push(membership);
|
|
2886
|
+
},
|
|
2887
|
+
incomplete() {
|
|
2888
|
+
complete = false;
|
|
2889
|
+
}
|
|
2890
|
+
};
|
|
2891
|
+
await Promise.all((policy.index.grantsByKey.get(permission.key) ?? []).flatMap((grant) => flattenGrantee(grant.to).map((item) => discover(grant, item, ctx))));
|
|
2892
|
+
const root = rootScope(scopes);
|
|
2893
|
+
const tenantKey = root === void 0 ? void 0 : scopeField(getResource(policy.permissions, permission.resource), root, scopes);
|
|
2894
|
+
const tenant = tenantKey === void 0 ? void 0 : relationId(ownGet(row, tenantKey));
|
|
2895
|
+
const env = {
|
|
2593
2896
|
emit: false,
|
|
2594
2897
|
simulated: true,
|
|
2595
2898
|
skipAlternatives: true,
|
|
2596
|
-
customRoles,
|
|
2597
|
-
customGrants,
|
|
2899
|
+
customRoles: [],
|
|
2900
|
+
customGrants: [],
|
|
2598
2901
|
listeners: emptyListeners(),
|
|
2599
2902
|
sink: void 0,
|
|
2600
2903
|
limits: void 0,
|
|
2601
2904
|
limitCache: /* @__PURE__ */ new Map(),
|
|
2602
|
-
team:
|
|
2905
|
+
team: input.team,
|
|
2906
|
+
relations: input.reader
|
|
2603
2907
|
};
|
|
2604
|
-
const
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2908
|
+
const now = nowSeconds();
|
|
2909
|
+
const ordered = [...found.entries()].toSorted(([a], [b]) => byCodePoint(a, b));
|
|
2910
|
+
const decideAll = () => ordered.map(([id, entry]) => {
|
|
2911
|
+
const subject = freezeDeep({
|
|
2912
|
+
principal: {
|
|
2913
|
+
id,
|
|
2914
|
+
roles: [],
|
|
2915
|
+
memberships: entry.memberships,
|
|
2916
|
+
...tenant === void 0 ? {} : { tenant }
|
|
2917
|
+
},
|
|
2918
|
+
context: {}
|
|
2919
|
+
});
|
|
2920
|
+
return evaluate(policy, subject, permission, row, {
|
|
2921
|
+
source: "simulate",
|
|
2922
|
+
trusted: true,
|
|
2923
|
+
now
|
|
2924
|
+
}, env);
|
|
2618
2925
|
});
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
if (manage) return names;
|
|
2624
|
-
const reached = new Set(heldGrants.filter((grant) => grant.permission.key === key).flatMap((grant) => heldLevels(policy, grant, names)));
|
|
2625
|
-
return names.filter((name) => reached.has(name));
|
|
2626
|
-
};
|
|
2627
|
-
const levels = {};
|
|
2628
|
-
for (const leaf of permissions) {
|
|
2629
|
-
const names = leaf.kind === "instance" ? levelNames(policy, leaf.resource) : [];
|
|
2630
|
-
if (names.length > 0) levels[leaf.key] = offeredLevels(leaf.key, names);
|
|
2926
|
+
let decisions = decideAll();
|
|
2927
|
+
if (pendingRelations(input.cache).length > 0) {
|
|
2928
|
+
await settle$1(input.cache);
|
|
2929
|
+
decisions = decideAll();
|
|
2631
2930
|
}
|
|
2632
|
-
const
|
|
2633
|
-
const
|
|
2634
|
-
const
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
keys = new Set(ceilingGrants(policy, scope, allowed).map((item) => item.permission.key));
|
|
2638
|
-
ceilingKeys.set(scope, keys);
|
|
2639
|
-
}
|
|
2640
|
-
if (!keys.has(key) || !(manage || heldKeys.has(key))) return false;
|
|
2641
|
-
const names = grant.permission.kind === "instance" ? levelNames(policy, grant.permission.resource) : [];
|
|
2642
|
-
if (names.length === 0) return true;
|
|
2643
|
-
const offered = offeredLevels(key, names);
|
|
2644
|
-
return heldLevels(policy, grant, names).every((name) => offered.includes(name));
|
|
2645
|
-
};
|
|
2646
|
-
const custom = global || tenant === void 0 ? {
|
|
2647
|
-
roles: [],
|
|
2648
|
-
inert: []
|
|
2649
|
-
} : assignableCustomRoles(policy, customRoles, tenant, manage ? void 0 : roles, mayHandOut);
|
|
2650
|
-
return {
|
|
2651
|
-
roles: [...roles, ...custom.roles],
|
|
2652
|
-
permissions,
|
|
2653
|
-
levels,
|
|
2654
|
-
manage,
|
|
2655
|
-
inert: custom.inert
|
|
2656
|
-
};
|
|
2657
|
-
}
|
|
2658
|
-
/**
|
|
2659
|
-
* The tenant's custom roles the subject may hand out: held at a scope it may
|
|
2660
|
-
* assign declared roles at (any scope with `meta.manageRoles`), allowing at
|
|
2661
|
-
* least one permission after the ceiling, and only permissions and levels it
|
|
2662
|
-
* may hand out at that scope. A role that allows nothing is `inert`.
|
|
2663
|
-
*/
|
|
2664
|
-
function assignableCustomRoles(policy, customRoles, tenant, declared, mayHandOut) {
|
|
2665
|
-
const scopes = scopeList(policy.scopes);
|
|
2666
|
-
const root = rootScope(scopes);
|
|
2667
|
-
const assignsAt = declared === void 0 ? void 0 : new Set(declared.map((leaf) => leaf.on === void 0 ? root : resolveScope(scopes, leaf.on)));
|
|
2668
|
-
const out = /* @__PURE__ */ new Map();
|
|
2669
|
-
const inert = /* @__PURE__ */ new Set();
|
|
2670
|
-
for (const role of tenantCustomRoles(policy, customRoles)) {
|
|
2671
|
-
if (role.tenant !== tenant || out.has(role.name)) continue;
|
|
2672
|
-
const at = customRoleScope(role, scopes);
|
|
2673
|
-
if (at === void 0 || assignsAt !== void 0 && !assignsAt.has(at)) continue;
|
|
2674
|
-
const { grants } = resolveCustomRole(policy, role);
|
|
2675
|
-
if (!grants.some((grant) => grant.effect === "allow")) {
|
|
2676
|
-
inert.add(role.name);
|
|
2931
|
+
const holders = [];
|
|
2932
|
+
for (const [index, [id, entry]] of ordered.entries()) {
|
|
2933
|
+
const decision = decisions[index];
|
|
2934
|
+
if (decision === void 0 || decision.outcome === "denied") {
|
|
2935
|
+
if (decision?.outcome === "denied" && decision.denials.some((denial) => denial.reason === "relation-unavailable" || denial.reason === "relation-depth")) complete = false;
|
|
2677
2936
|
continue;
|
|
2678
2937
|
}
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
}));
|
|
2683
|
-
}
|
|
2684
|
-
return {
|
|
2685
|
-
roles: [...out.values()].toSorted((a, b) => a.key.localeCompare(b.key)),
|
|
2686
|
-
inert: [...inert].filter((name) => !out.has(name)).toSorted()
|
|
2687
|
-
};
|
|
2688
|
-
}
|
|
2689
|
-
/** The one path from a resolved subject to a `Snapshot`; pure apart from the default clock. */
|
|
2690
|
-
function snapshotOf(policy, subject, options) {
|
|
2691
|
-
const now = options.now ?? nowSeconds();
|
|
2692
|
-
const customGrants = options.customGrants ?? customGrantsFor(policy, options.customRoles);
|
|
2693
|
-
const roles = heldRoleNames(policy, subject, subject.principal?.tenant, void 0, now);
|
|
2694
|
-
const audiences = audiencesOf(policy, roles);
|
|
2695
|
-
return buildSnapshot(compact({
|
|
2696
|
-
subject,
|
|
2697
|
-
roles,
|
|
2698
|
-
audiences: audiences.length === 0 ? void 0 : audiences,
|
|
2699
|
-
notEntitled: collectSnapshotGrants(policy, subject, options.customRoles, now, customGrants, "not-entitled"),
|
|
2700
|
-
grants: collectSnapshotGrants(policy, subject, options.customRoles, now, customGrants).map((item) => periodBound(policy, item.grant) ? Object.assign({}, item, { grant: freezeDeep({
|
|
2701
|
-
...item.grant,
|
|
2702
|
-
portable: false
|
|
2703
|
-
}) }) : item),
|
|
2704
|
-
include: includePrefixes(options.include),
|
|
2705
|
-
tenants: options.tenants,
|
|
2706
|
-
simulated: options.simulated,
|
|
2707
|
-
now: Math.floor(now),
|
|
2708
|
-
vocabulary: policy.vocabulary,
|
|
2709
|
-
scopes: snapshotScopes(policy),
|
|
2710
|
-
idOf: (name) => getResource(policy.permissions, name)?.id,
|
|
2711
|
-
delegated: delegatedPermissions(policy.delegations, subject, new Set(roles), now),
|
|
2712
|
-
assignable: (tenant) => {
|
|
2713
|
-
const found = assignableIn(policy, subject, options.customRoles, customGrants, tenant, options.assignable?.get(tenant), now);
|
|
2714
|
-
return compact({
|
|
2715
|
-
tenant,
|
|
2716
|
-
roles: found.roles.map((leaf) => leaf.key),
|
|
2717
|
-
permissions: found.permissions,
|
|
2718
|
-
levels: Object.keys(found.levels).length === 0 ? void 0 : found.levels
|
|
2719
|
-
});
|
|
2720
|
-
}
|
|
2721
|
-
}));
|
|
2722
|
-
}
|
|
2723
|
-
/** The policy's scopes as snapshots and `permdock cloud push` carry them, with the resources each partitions. */
|
|
2724
|
-
function snapshotScopes(policy) {
|
|
2725
|
-
if (policy.scopes.length === 0) return;
|
|
2726
|
-
const resources = [...policy.resources.values()];
|
|
2727
|
-
return policy.scopes.map((scope) => {
|
|
2728
|
-
const nodes = resources.filter((node) => partitionsOf(node, policy.scopes).includes(scope.name));
|
|
2729
|
-
const partitioned = nodes.map((node) => node.name);
|
|
2730
|
-
const fields = Object.fromEntries(nodes.flatMap((node) => {
|
|
2731
|
-
const field = scopeField(node, scope.name, policy.scopes);
|
|
2732
|
-
return field === void 0 || field === scope.key ? [] : [[node.name, field]];
|
|
2733
|
-
}));
|
|
2734
|
-
return compact({
|
|
2735
|
-
name: scope.name,
|
|
2736
|
-
key: scope.key ?? "",
|
|
2737
|
-
within: scope.within,
|
|
2738
|
-
resources: partitioned.length === 0 ? void 0 : partitioned,
|
|
2739
|
-
fields: Object.keys(fields).length === 0 ? void 0 : fields
|
|
2938
|
+
holders.push({
|
|
2939
|
+
principal: { id },
|
|
2940
|
+
via: entry.via
|
|
2740
2941
|
});
|
|
2942
|
+
}
|
|
2943
|
+
return freezeDeep({
|
|
2944
|
+
permission: permission.key,
|
|
2945
|
+
holders,
|
|
2946
|
+
complete
|
|
2741
2947
|
});
|
|
2742
2948
|
}
|
|
2949
|
+
//#endregion
|
|
2950
|
+
//#region src/core/instance.ts
|
|
2951
|
+
/** Each round reads one more layer: link hops, then the chain, then nested groups. */
|
|
2952
|
+
const LOAD_ROUNDS = 48;
|
|
2743
2953
|
function buildInstance(policy, subject, envBase, team) {
|
|
2744
2954
|
const listeners = emptyListeners();
|
|
2745
2955
|
const queuedAuth = [...envBase.queuedAuth];
|
|
@@ -2763,27 +2973,6 @@ function buildInstance(policy, subject, envBase, team) {
|
|
|
2763
2973
|
relations,
|
|
2764
2974
|
...envBase.approvalPolicies === void 0 ? {} : { approvalPolicies: envBase.approvalPolicies }
|
|
2765
2975
|
});
|
|
2766
|
-
/** The one `simulate` event for a batch: the worst decision, with the counts. */
|
|
2767
|
-
const simulateEvent = (evaluated) => {
|
|
2768
|
-
const counts = {
|
|
2769
|
-
granted: 0,
|
|
2770
|
-
denied: 0,
|
|
2771
|
-
approvalRequired: 0
|
|
2772
|
-
};
|
|
2773
|
-
for (const [, , decision] of evaluated) if (decision.outcome === "granted") counts.granted += 1;
|
|
2774
|
-
else if (decision.outcome === "approval-required") counts.approvalRequired += 1;
|
|
2775
|
-
else counts.denied += 1;
|
|
2776
|
-
const worst = evaluated.find(([, , decision]) => decision.outcome === "denied") ?? evaluated.find(([, , decision]) => decision.outcome === "approval-required") ?? evaluated[0];
|
|
2777
|
-
if (worst === void 0) return;
|
|
2778
|
-
const [permission, data, decision] = worst;
|
|
2779
|
-
finish(policy, subject, permission, data, decision, {
|
|
2780
|
-
source: "simulate",
|
|
2781
|
-
trusted: true
|
|
2782
|
-
}, {
|
|
2783
|
-
...envFor(false),
|
|
2784
|
-
emit: true
|
|
2785
|
-
}, true, void 0, counts);
|
|
2786
|
-
};
|
|
2787
2976
|
const decideImpl = (permission, data, options) => evaluate(policy, subject, permission, data, options ?? {}, envFor(options?.source !== "simulate", options?.source === "can"));
|
|
2788
2977
|
const explainImpl = (permission, data, options) => {
|
|
2789
2978
|
return decideImpl(permission, data, {
|
|
@@ -2803,39 +2992,10 @@ function buildInstance(policy, subject, envBase, team) {
|
|
|
2803
2992
|
}
|
|
2804
2993
|
};
|
|
2805
2994
|
const assertImpl = (permission, data, options) => {
|
|
2806
|
-
|
|
2995
|
+
return assertDecision(policy, subject, permission, data, decideImpl(permission, data, {
|
|
2807
2996
|
...options,
|
|
2808
2997
|
source: options?.source ?? "assert"
|
|
2809
|
-
});
|
|
2810
|
-
if (decision.outcome === "granted") return decision;
|
|
2811
|
-
const onDenied = options?.onDenied ?? policy.onDenied;
|
|
2812
|
-
if (onDenied !== void 0) onDenied(decision);
|
|
2813
|
-
const resource = getResource(policy.permissions, permission.resource);
|
|
2814
|
-
const resourceId = data !== null && typeof data === "object" ? data[resource?.id ?? "id"] : void 0;
|
|
2815
|
-
const resourceRef = compact({
|
|
2816
|
-
type: permission.resource,
|
|
2817
|
-
id: resourceId === void 0 ? void 0 : String(resourceId)
|
|
2818
|
-
});
|
|
2819
|
-
if (decision.outcome === "approval-required") throw new PermDockApprovalRequiredError({
|
|
2820
|
-
decision,
|
|
2821
|
-
permission: permission.key,
|
|
2822
|
-
scope: permission.scope,
|
|
2823
|
-
resource: resourceRef,
|
|
2824
|
-
message: approvalMessage(permission.key, decision.reason, decision.token)
|
|
2825
|
-
});
|
|
2826
|
-
if (decision.denials.some((denial) => denial.reason === "validation")) {
|
|
2827
|
-
const detail = decision.denials[0]?.detail;
|
|
2828
|
-
if (detail instanceof PermDockValidationError) throw detail;
|
|
2829
|
-
}
|
|
2830
|
-
throw new PermDockDeniedError({
|
|
2831
|
-
decision,
|
|
2832
|
-
permission: permission.key,
|
|
2833
|
-
scope: permission.scope,
|
|
2834
|
-
resource: resourceRef,
|
|
2835
|
-
subject,
|
|
2836
|
-
message: deniedMessage(permission.key, subject.principal?.id, decision.denials, decision.alternatives.map((leaf) => leaf.key)),
|
|
2837
|
-
...data === void 0 || resource === void 0 ? {} : { disclosure: resource.disclosure }
|
|
2838
|
-
});
|
|
2998
|
+
}), options?.onDenied ?? policy.onDenied);
|
|
2839
2999
|
};
|
|
2840
3000
|
const instance = {
|
|
2841
3001
|
can: canImpl,
|
|
@@ -2878,6 +3038,7 @@ function buildInstance(policy, subject, envBase, team) {
|
|
|
2878
3038
|
token: "pd1.filter"
|
|
2879
3039
|
}) : freezeDeep({
|
|
2880
3040
|
outcome: "denied",
|
|
3041
|
+
permission: permission.key,
|
|
2881
3042
|
denials: [{
|
|
2882
3043
|
role: null,
|
|
2883
3044
|
reason: "no-grant"
|
|
@@ -2970,27 +3131,15 @@ function buildInstance(policy, subject, envBase, team) {
|
|
|
2970
3131
|
};
|
|
2971
3132
|
if (isReadonlyArray(input)) {
|
|
2972
3133
|
const decisions = input.map(([permission, data]) => quietly(permission, data));
|
|
2973
|
-
simulateEvent(evaluated);
|
|
3134
|
+
simulateEvent(policy, subject, envFor(false), evaluated);
|
|
2974
3135
|
return decisions;
|
|
2975
3136
|
}
|
|
2976
3137
|
if (isArazzoSimulateInput(input)) {
|
|
2977
3138
|
const plan = simulateArazzo(input, input.permissions ?? policy.permissions, quietly);
|
|
2978
|
-
simulateEvent(evaluated);
|
|
3139
|
+
simulateEvent(policy, subject, envFor(false), evaluated);
|
|
2979
3140
|
return plan;
|
|
2980
3141
|
}
|
|
2981
|
-
|
|
2982
|
-
const previewRoles = preview.roles?.map((item) => typeof item === "string" ? item : item.key);
|
|
2983
|
-
const kinds = subject.principal === null ? void 0 : applyRoleKinds(policy, previewRoles ?? subject.principal.roles, preview.memberships === void 0 ? subject.principal.memberships ?? [] : normalizeMemberships(preview.memberships, scopeList(policy.scopes)));
|
|
2984
|
-
const previewPrincipal = subject.principal === null || kinds === void 0 ? null : freezeDeep(compact({
|
|
2985
|
-
...subject.principal,
|
|
2986
|
-
roles: kinds.roles,
|
|
2987
|
-
memberships: preview.memberships === void 0 && subject.principal.memberships === void 0 ? void 0 : kinds.memberships,
|
|
2988
|
-
tenant: preview.tenant ?? subject.principal.tenant
|
|
2989
|
-
}));
|
|
2990
|
-
return buildInstance(policy, freezeDeep({
|
|
2991
|
-
...subject,
|
|
2992
|
-
principal: previewPrincipal
|
|
2993
|
-
}), {
|
|
3142
|
+
return buildInstance(policy, previewSubject(policy, subject, input), {
|
|
2994
3143
|
...envBase,
|
|
2995
3144
|
simulated: true,
|
|
2996
3145
|
relationCache
|
|
@@ -3089,32 +3238,8 @@ function buildInstance(policy, subject, envBase, team) {
|
|
|
3089
3238
|
return activate(policy, subject, input, nowSeconds());
|
|
3090
3239
|
},
|
|
3091
3240
|
derive(options) {
|
|
3092
|
-
const
|
|
3093
|
-
|
|
3094
|
-
const auth = [];
|
|
3095
|
-
const named = options.customRoles === void 0 ? void 0 : roleSourceFor(options.customRoles, subject, auth);
|
|
3096
|
-
const roleSource = options.customRoles === void 0 ? envBase.roleSource : named;
|
|
3097
|
-
const roles = options.customRoles === void 0 ? envBase.customRoles : customRolesFor(named, tenants, auth, subject.principal === null ? void 0 : subject.principal.roles ?? [], (tenant) => heldRoleNamesIn(subject.principal, scopes, tenant));
|
|
3098
|
-
const names = options.customRoles === void 0 ? envBase.assignable : assignableNamesFor(named, tenants, auth);
|
|
3099
|
-
const approvals = options.approvalPolicies === void 0 ? envBase.approvalPolicies : approvalPoliciesFor(policy, options.approvalPolicies, tenants, auth);
|
|
3100
|
-
const relationSource = options.relations ?? envBase.relations;
|
|
3101
|
-
const build = (loadedRoles, loadedNames, loadedApprovals) => buildInstance(policy, subject, compact({
|
|
3102
|
-
...envBase,
|
|
3103
|
-
customRoles: loadedRoles,
|
|
3104
|
-
roleSource,
|
|
3105
|
-
assignable: loadedNames,
|
|
3106
|
-
approvalPolicies: loadedApprovals,
|
|
3107
|
-
relations: relationSource,
|
|
3108
|
-
relationCache: options.relations === void 0 ? relationCache : void 0,
|
|
3109
|
-
queuedAuth: auth,
|
|
3110
|
-
queuedErrors: []
|
|
3111
|
-
}), team);
|
|
3112
|
-
if (isThenable(roles) || isThenable(names) || isThenable(approvals)) return Promise.all([
|
|
3113
|
-
roles,
|
|
3114
|
-
names,
|
|
3115
|
-
approvals
|
|
3116
|
-
]).then(([loadedRoles, loadedNames, loadedApprovals]) => build(loadedRoles, loadedNames, loadedApprovals));
|
|
3117
|
-
return build(roles, names, approvals);
|
|
3241
|
+
const derived = deriveEnv(policy, subject, envBase, relationCache, options);
|
|
3242
|
+
return isThenable(derived) ? Promise.resolve(derived).then((env) => buildInstance(policy, subject, env, team)) : buildInstance(policy, subject, derived, team);
|
|
3118
3243
|
},
|
|
3119
3244
|
subject
|
|
3120
3245
|
};
|
|
@@ -3159,6 +3284,10 @@ function scopesReaching(policy, permission) {
|
|
|
3159
3284
|
function challengeScope(policy, permission) {
|
|
3160
3285
|
return coarseScopesFor(policy, permission)[0] ?? permission.scope;
|
|
3161
3286
|
}
|
|
3287
|
+
/** Whether a token holding `granted` carries any of `scopes`. */
|
|
3288
|
+
function hasScope(granted, scopes) {
|
|
3289
|
+
return scopes.some((scope) => granted?.includes(scope) === true);
|
|
3290
|
+
}
|
|
3162
3291
|
//#endregion
|
|
3163
3292
|
//#region src/core/resolve-subject.ts
|
|
3164
3293
|
/**
|
|
@@ -3197,7 +3326,7 @@ function assemblePrincipal(policy, user, options, auth) {
|
|
|
3197
3326
|
} catch {
|
|
3198
3327
|
principal = null;
|
|
3199
3328
|
}
|
|
3200
|
-
const contextResult = resolveContext(policy, user, context, auth);
|
|
3329
|
+
const contextResult = withRequestContext(options.context, resolveContext(policy, user, context, auth));
|
|
3201
3330
|
let memberships = principal?.memberships ?? [];
|
|
3202
3331
|
let live = false;
|
|
3203
3332
|
const source = options.memberships === void 0 ? void 0 : asMembershipSource(options.memberships);
|
|
@@ -3281,7 +3410,7 @@ function finishSubject(policy, assembled, context, input, options, extra) {
|
|
|
3281
3410
|
}));
|
|
3282
3411
|
const scopes = scopeList(policy.scopes);
|
|
3283
3412
|
const held = heldTenantOf(assembled.principal);
|
|
3284
|
-
const kinds = applyRoleKinds(policy, assembled.principal.roles, normalizeMemberships(input, scopes));
|
|
3413
|
+
const kinds = applyRoleKinds(policy, assembled.principal.roles, checkMembershipX(normalizeMemberships(input, scopes), policyX(policy)?.membership, extra.auth));
|
|
3285
3414
|
const roles = held === void 0 ? kinds.roles : [];
|
|
3286
3415
|
const memberships = held === void 0 ? kinds.memberships : outsideHeld(held, options) ? [] : kinds.memberships.filter((membership) => tenantOf(membership, scopes) === held);
|
|
3287
3416
|
const plans = [.../* @__PURE__ */ new Set([...assembled.principal.plans ?? [], ...extra.plans])];
|
|
@@ -3303,6 +3432,30 @@ function finishSubject(policy, assembled, context, input, options, extra) {
|
|
|
3303
3432
|
liveSession: assembled.liveSession
|
|
3304
3433
|
}));
|
|
3305
3434
|
}
|
|
3435
|
+
/** Runs the policy's membership schema over each `x`; an invalid one is dropped and reported, never the membership. */
|
|
3436
|
+
function checkMembershipX(memberships, schema, auth) {
|
|
3437
|
+
if (schema === void 0) return memberships;
|
|
3438
|
+
let dropped = false;
|
|
3439
|
+
const checked = memberships.map((membership) => {
|
|
3440
|
+
if (membership.x === void 0) return membership;
|
|
3441
|
+
const x = readAppData(schema, membership.x);
|
|
3442
|
+
if (x === void 0) {
|
|
3443
|
+
dropped = true;
|
|
3444
|
+
const { x: _dropped, ...rest } = membership;
|
|
3445
|
+
return Object.freeze(rest);
|
|
3446
|
+
}
|
|
3447
|
+
return Object.freeze({
|
|
3448
|
+
...membership,
|
|
3449
|
+
x: freezeCopy(x)
|
|
3450
|
+
});
|
|
3451
|
+
});
|
|
3452
|
+
if (dropped) auth.push({
|
|
3453
|
+
reason: "schema",
|
|
3454
|
+
source: "memberships",
|
|
3455
|
+
cause: "x"
|
|
3456
|
+
});
|
|
3457
|
+
return checked;
|
|
3458
|
+
}
|
|
3306
3459
|
function activeTenantOf(policy, assembled, memberships, options) {
|
|
3307
3460
|
if (assembled.principal === null) return;
|
|
3308
3461
|
const held = heldTenantOf(assembled.principal);
|
|
@@ -3390,6 +3543,17 @@ function resolveContext(policy, user, fallback, auth) {
|
|
|
3390
3543
|
return {};
|
|
3391
3544
|
}
|
|
3392
3545
|
}
|
|
3546
|
+
function withRequestContext(request, resolved) {
|
|
3547
|
+
if (request === void 0) return resolved;
|
|
3548
|
+
const base = sanitizeContext(request);
|
|
3549
|
+
return isThenable(resolved) ? resolved.then((value) => ({
|
|
3550
|
+
...base,
|
|
3551
|
+
...value
|
|
3552
|
+
})) : {
|
|
3553
|
+
...base,
|
|
3554
|
+
...resolved
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3393
3557
|
function resolveSubject(policy, user, options, auth) {
|
|
3394
3558
|
const assembled = assemblePrincipal(policy, user, options, auth);
|
|
3395
3559
|
const memberships = settle(assembled.memberships, [], () => {
|
|
@@ -3406,16 +3570,19 @@ function resolveSubject(policy, user, options, auth) {
|
|
|
3406
3570
|
Promise.resolve(stale)
|
|
3407
3571
|
]).then(async ([context, resolved, isStale]) => finishSubject(policy, assembled, context, resolved, options, {
|
|
3408
3572
|
stale: isStale,
|
|
3409
|
-
plans: await withPlans(resolved)
|
|
3573
|
+
plans: await withPlans(resolved),
|
|
3574
|
+
auth
|
|
3410
3575
|
}));
|
|
3411
3576
|
const plans = withPlans(memberships);
|
|
3412
3577
|
if (isThenable(plans)) return Promise.resolve(plans).then((resolved) => finishSubject(policy, assembled, assembled.context, memberships, options, {
|
|
3413
3578
|
stale,
|
|
3414
|
-
plans: resolved
|
|
3579
|
+
plans: resolved,
|
|
3580
|
+
auth
|
|
3415
3581
|
}));
|
|
3416
3582
|
return finishSubject(policy, assembled, assembled.context, memberships, options, {
|
|
3417
3583
|
stale,
|
|
3418
|
-
plans
|
|
3584
|
+
plans,
|
|
3585
|
+
auth
|
|
3419
3586
|
});
|
|
3420
3587
|
}
|
|
3421
3588
|
//#endregion
|
|
@@ -3476,4 +3643,4 @@ function createPermDock(policy, user, options = {}) {
|
|
|
3476
3643
|
return instantiate(policy, subject, options, auth);
|
|
3477
3644
|
}
|
|
3478
3645
|
//#endregion
|
|
3479
|
-
export { countHolders as C, memoryApprovalPolicies as D, approvalPoliciesFor as E, validateApprovalPolicy as O, composeMemberships as S, APPROVAL_POLICY_UNAVAILABLE as T, leveled as _,
|
|
3646
|
+
export { countHolders as C, memoryApprovalPolicies as D, approvalPoliciesFor as E, validateApprovalPolicy as O, composeMemberships as S, APPROVAL_POLICY_UNAVAILABLE as T, leveled as _, scopesReaching as a, validateCustomRole as b, collectSnapshotGrants as c, breakGlass as d, supportAccess as f, customRoleScope as g, customRoleClaim as h, hasScope as i, rankRoles as l, ceilingGrants as m, resolveSubject as n, snapshotOf as o, andWhere as p, challengeScope as r, snapshotScopes as s, createPermDock as t, usesAssigns as u, membershipsClaim as v, isExternallyManaged as w, claimsFirst as x, resolveCustomRole as y };
|