permdock 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -35
- package/dist/{schema-Bol6gqXA.js → 2020-C9VN-x8c.js} +5 -130
- package/dist/a2a/index.d.ts +6 -6
- package/dist/a2a/index.js +14 -17
- package/dist/{adapter-context-mlWsVGGg.js → adapter-context-CRhqyzCk.js} +1 -1
- package/dist/adoption-scan-DCYe60Pi.js +107 -0
- package/dist/agent/index.d.ts +3 -0
- package/dist/agent/index.js +1 -0
- package/dist/ai-sdk/index.d.ts +18 -35
- package/dist/ai-sdk/index.js +37 -23
- package/dist/{app-data-CV8pqqMi.js → app-data-MuyzVqX8.js} +2 -1
- package/dist/approval-header-CJACtHkv.js +23 -0
- package/dist/approval-prompt-Dg5LeAcT.js +62 -0
- package/dist/approval-prompt-DvSSPQJX.d.ts +57 -0
- package/dist/approvals/index.d.ts +5 -3
- package/dist/approvals/index.js +8 -5
- package/dist/approvals-p1PGoVjb.js +995 -0
- package/dist/arazzo-41WuzHou.js +36 -0
- package/dist/{arazzo-C4BcskbX.js → arazzo-CaUVBn7D.js} +20 -14
- package/dist/args-IIolxxKA.js +197 -0
- package/dist/astro/index.d.ts +43 -0
- package/dist/astro/index.js +27 -0
- package/dist/audience-DabClFLQ.js +55 -0
- package/dist/auth-DRLEizbL.js +178 -0
- package/dist/authzen/index.d.ts +26 -6
- package/dist/authzen/index.js +94 -79
- package/dist/automation-BNM3TtDo.d.ts +114 -0
- package/dist/automation-DVjOckjV.js +157 -0
- package/dist/better-auth/index.d.ts +8 -6
- package/dist/better-auth/index.js +9 -8
- package/dist/better-supabase/index.d.ts +357 -30
- package/dist/better-supabase/index.js +936 -194
- package/dist/{bind-kk-uyCwH.js → bind-CAvgUuKs.js} +14 -10
- package/dist/{bind-BzqG8SMb.d.ts → bind-DOyrv6zi.d.ts} +12 -10
- package/dist/body-CH5of8Ne.js +10 -0
- package/dist/body-text-QcXok_SI.js +52 -0
- package/dist/{build-instance-DIIV9DbC.js → build-instance-Bu_eDqYw.js} +4 -3
- package/dist/{canonical-json-CFKyFOke.js → canonical-json-BC8_nMTw.js} +5 -3
- package/dist/{capability-CksyCWDH.d.ts → capability-Drg-1YhL.d.ts} +44 -25
- package/dist/{capability-txATtKV4.js → capability-N1K9IpqS.js} +68 -42
- package/dist/casbin-Cr7RLtA3.js +30 -0
- package/dist/casbin-Dyu2gnUD.js +234 -0
- package/dist/casl-64gpT-8x.js +24 -0
- package/dist/casl-DjEDhTCP.js +299 -0
- package/dist/catalog/index.d.ts +74 -2
- package/dist/catalog/index.js +51 -2
- package/dist/catalog-BoWglDPI.js +1 -0
- package/dist/catalog-DZVbrF-z.js +52 -0
- package/dist/{catalog-tZUyOfOv.js → catalog-HD2gf5mY.js} +51 -6
- package/dist/{catalog-doc-DWtg0gGR.js → catalog-doc-Cdn7-0pQ.js} +45 -23
- package/dist/catalog-fingerprint-CCP0-yQS.js +153 -0
- package/dist/chat/index.d.ts +2 -0
- package/dist/chat/index.js +224 -0
- package/dist/chat-sdk/index.d.ts +87 -0
- package/dist/chat-sdk/index.js +197 -0
- package/dist/claims-map-7oQ3ue4X.js +13 -0
- package/dist/claims-map-Ca-QX8C6.d.ts +32 -0
- package/dist/claude-agent/index.d.ts +7 -14
- package/dist/claude-agent/index.js +24 -27
- package/dist/clerk/index.d.ts +9 -8
- package/dist/clerk/index.js +9 -14
- package/dist/cli/bin.js +36 -17
- package/dist/cli/index.d.ts +19 -5
- package/dist/cli/index.js +164 -4
- package/dist/cloud/index.d.ts +102 -63
- package/dist/cloud/index.js +239 -340
- package/dist/cloud-ytQ3uviA.js +87 -0
- package/dist/codemod-5mpSpnUY.js +37 -0
- package/dist/codemod-ua2hHy7w.js +2084 -0
- package/dist/collect-Ci6akVwH.js +55 -0
- package/dist/collect-D7eQDffE.js +1 -0
- package/dist/{collect-2SrB7lfk.js → collect-mLTY4nwb.js} +75 -11
- package/dist/command-L8EstFNm.js +6 -0
- package/dist/compile/index.d.ts +1 -1
- package/dist/compile/index.js +1 -2
- package/dist/{compile-CtTP7eur.d.ts → compile-1bsAezL0.d.ts} +5 -3
- package/dist/{compile-CcEu2ZQ1.js → compile-CMqMx_IN.js} +11 -8
- package/dist/completion-BTFrHZhX.js +143 -0
- package/dist/config-Bj2Ixq3d.js +26 -0
- package/dist/{config-BIjREpyE.js → config-bs_kZeYq.js} +12 -7
- package/dist/{config-report--Xb9APJS.js → config-report-DSpsLpCE.js} +15 -5
- package/dist/{connection-DUsczpB5.js → connection-DjhIJSs2.js} +6 -5
- package/dist/{context-DieZVJXu.js → context-B_u3_bDJ.js} +2 -2
- package/dist/convex/index.d.ts +11 -10
- package/dist/convex/index.js +6 -4
- package/dist/create-B30OMUOJ.d.ts +107 -0
- package/dist/create-CBMmFie-.d.ts +138 -0
- package/dist/create-CGm28gyI.js +130 -0
- package/dist/create-CQWIpUG-.js +403 -0
- package/dist/{create-BBO24vj6.js → create-DCF2SvoT.js} +31 -32
- package/dist/create-D_NmPOdH.d.ts +110 -0
- package/dist/{credential-kue0qpbm.js → credential-BcoiejpY.js} +152 -31
- package/dist/credentials-DPyHmYz5.js +74 -0
- package/dist/delegation-j6-rc37o.js +286 -0
- package/dist/delivery/index.d.ts +5 -0
- package/dist/delivery/index.js +5 -0
- package/dist/delivery-DCAgNyCn.js +499 -0
- package/dist/delivery-event-xX4obB8F.js +40 -0
- package/dist/{denied-text-DfRk15uw.d.ts → denied-text-CiFQNuMV.d.ts} +5 -5
- package/dist/{describe-C3ldA9os.d.ts → describe-B5qgDvEW.d.ts} +2 -2
- package/dist/{describe-Cgkp7eP0.js → describe-BybMRY7A.js} +6 -2
- package/dist/device-BARQ8eYG.js +193 -0
- package/dist/{diff-rpqoe7Ag.js → diff-3jbx6Rm4.js} +60 -54
- package/dist/diff-CmuSD3vc.js +37 -0
- package/dist/doctor-3ZqNHX_G.js +44 -0
- package/dist/{doctor-owlv6ddh.js → doctor-BdZw53-k.js} +245 -50
- package/dist/drizzle/index.d.ts +5 -3
- package/dist/drizzle/index.js +2 -2
- package/dist/elysia/index.d.ts +16 -13
- package/dist/elysia/index.js +13 -12
- package/dist/env-DJZrPjRi.js +8 -0
- package/dist/errors-BDUwg2qC.js +27 -0
- package/dist/{errors-drnfxYtm.js → errors-BKy0EfhN.js} +5 -9
- package/dist/errors-BWXBMaKK.d.ts +11 -0
- package/dist/errors-CWTY6uih.js +143 -0
- package/dist/{errors-OD_4c__H.d.ts → errors-DH5QEGXh.d.ts} +2 -2
- package/dist/evaluate-CNClBvcj.js +1785 -0
- package/dist/evaluations-C_1vdSZc.d.ts +32 -0
- package/dist/{evaluations-C9YJsv3H.js → evaluations-beAooV0n.js} +145 -34
- package/dist/eve/index.d.ts +12 -16
- package/dist/eve/index.js +22 -28
- package/dist/event-time-CsegxXxB.js +23 -0
- package/dist/expiry-D4_YKRKD.js +52 -0
- package/dist/express/index.d.ts +13 -10
- package/dist/express/index.js +12 -11
- package/dist/fastify/index.d.ts +25 -22
- package/dist/fastify/index.js +13 -12
- package/dist/fetch-adapter-DD_gfmrC.js +120 -0
- package/dist/fetch-adapter-Dw0MTS3D.d.ts +31 -0
- package/dist/fields-OYG_JR78.js +203 -0
- package/dist/files-C5ZI27eV.js +124 -0
- package/dist/{fixtures-h2eG2OZV.js → fixtures-B7XrLeY7.js} +8 -7
- package/dist/flags-C3DktBDY.js +86 -0
- package/dist/{from-snapshot-7REQZe0H.js → from-snapshot-CTCMsFZk.js} +55 -52
- package/dist/{grantee-conditions-MQs5NIuX.js → grantee-conditions-CcXf8hMM.js} +4 -3
- package/dist/{grantee-CXY-0C87.js → grantee-hxrUsbpM.js} +16 -6
- package/dist/{grantee-match-fxYp9Nmt.js → grantee-match-DbENBHLB.js} +5 -4
- package/dist/h3/index.d.ts +37 -0
- package/dist/h3/index.js +35 -0
- package/dist/headers-BIjfMaoC.js +8 -0
- package/dist/headers-CIbDTUTK.d.ts +7 -0
- package/dist/{helpers-C9YmyBIa.js → helpers-CBYVMjwh.js} +45 -38
- package/dist/hono/index.d.ts +12 -9
- package/dist/hono/index.js +13 -12
- package/dist/hosted-B46-5LYE.js +315 -0
- package/dist/hosted-BPk51-kX.d.ts +78 -0
- package/dist/{http-DPuDaChb.d.ts → http-B_LphE5Y.d.ts} +1 -2
- package/dist/http-DXJ37RDA.js +46 -0
- package/dist/{http-BEbdMZ05.js → http-ujKm0JXE.js} +1 -2
- package/dist/{import-UHk1N7vH.js → import-CNV7I2Dw.js} +25 -20
- package/dist/{import-BFIlhetD.js → import-wyqXUyjm.js} +190 -53
- package/dist/index-B8YjkEZ4.d.ts +210 -0
- package/dist/index-C-6Letxg.d.ts +431 -0
- package/dist/index-CaxrMD1A.d.ts +1 -0
- package/dist/{index-BwMe94Xq.d.ts → index-Ch8LpNDp.d.ts} +34 -16
- package/dist/index-Cp_ilKi1.d.ts +91 -0
- package/dist/index-CueFnxQc.d.ts +110 -0
- package/dist/index-DuWxc5dt.d.ts +909 -0
- package/dist/index.d.ts +19 -15
- package/dist/index.js +31 -21
- package/dist/init-DeMYsnYJ.js +96 -0
- package/dist/init-GG6Jh8HD.js +95 -0
- package/dist/{instance-options-plkrsAog.d.ts → instance-options-BsVpyGIk.d.ts} +2 -2
- package/dist/instance-snapshot-BOEhcqIF.js +601 -0
- package/dist/instant-BluqDvyS.js +19 -0
- package/dist/internals-Dk8A8fX3.js +17 -0
- package/dist/{json-schema-DBkdUuCO.js → json-schema-B-vAwlde.js} +45 -6
- package/dist/json-schema-CN_iLFH2.d.ts +40 -0
- package/dist/jwt/index.d.ts +18 -11
- package/dist/jwt/index.js +156 -135
- package/dist/{kernel-CB3LrIJM.js → kernel-D0NVb1cK.js} +52 -24
- package/dist/kysely/index.d.ts +5 -3
- package/dist/kysely/index.js +2 -2
- package/dist/load-BxEtwlBS.js +1 -0
- package/dist/{load-C3_oTHyO.js → load-DFS_ry0W.js} +22 -6
- package/dist/{load-jose-BL6LH4bL.js → load-jose-DZ4dnjri.js} +1 -4
- package/dist/login-Dfe5RlYk.js +68 -0
- package/dist/login-imhuwvn3.js +254 -0
- package/dist/manifest-C8IzW3Do.js +42 -0
- package/dist/manifest-dvL2XEe1.js +370 -0
- package/dist/manifest-json-CAGzls-C.js +9 -0
- package/dist/{manifest-uDS9y5de.d.ts → manifest-mx5ghSFM.d.ts} +9 -2
- package/dist/manifest-schema-C0kK_8V0.js +399 -0
- package/dist/{map-claims-AVlBaJoa.js → map-claims-D3oRmTk0.js} +13 -10
- package/dist/{map-error-BE27PYkE.js → map-error-Dr70oA_3.js} +3 -3
- package/dist/{markers-CiwAOZtO.js → markers-C6joDI3o.js} +2 -1
- package/dist/mcp/index.d.ts +22 -12
- package/dist/mcp/index.js +57 -39
- package/dist/{migrate-BwlB5-Wk.js → migrate-CIq4-E9F.js} +53 -14
- package/dist/nest/index.d.ts +12 -9
- package/dist/nest/index.js +15 -14
- package/dist/next/index.d.ts +18 -21
- package/dist/next/index.js +13 -11
- package/dist/next/plugin.d.ts +1 -1
- package/dist/next/plugin.js +2 -2
- package/dist/node/index.d.ts +10 -10
- package/dist/node/index.js +7 -9
- package/dist/{ocsf-uYFXGVw2.js → ocsf-BFp-PZZA.js} +22 -11
- package/dist/openai/index.d.ts +9 -16
- package/dist/openai/index.js +17 -24
- package/dist/openapi/index.d.ts +3 -3
- package/dist/openapi/index.js +1 -1
- package/dist/{openapi-DBzHQbov.js → openapi-B7HZ0f-r.js} +8 -8
- package/dist/openapi-C4dDTVEd.js +138 -0
- package/dist/{openapi-BJdBmYDG.js → openapi-DD1SV-5-.js} +10 -9
- package/dist/operation-findings-DgATDpvh.js +36 -0
- package/dist/operations-X0VM7F3m.d.ts +59 -0
- package/dist/orpc/index.d.ts +13 -10
- package/dist/orpc/index.js +20 -15
- package/dist/otel/index.d.ts +3 -2
- package/dist/otel/index.js +30 -7
- package/dist/output-1im__-08.js +88 -0
- package/dist/output-files-B3IZJksC.js +1 -0
- package/dist/{output-files-CgIsvdSa.js → output-files-DhRBBE4n.js} +24 -26
- package/dist/{permission-keys-B26jeyUv.js → ownership-DqaJNJPn.js} +1045 -153
- package/dist/{package-root-DtNnpmUd.js → package-root-Dk_ETdkp.js} +1 -1
- package/dist/{parse-CVFQHk6c.js → parse-Cxq0WxrA.js} +2 -2
- package/dist/{parse-local-manifest-DvYUR0QL.d.ts → parse-local-manifest-5ODwqu8b.d.ts} +1 -1
- package/dist/{parse-local-manifest-C-q02Pal.js → parse-local-manifest-CVSs6J9I.js} +1 -1
- package/dist/parse-request-D_tUXG6M.js +26 -0
- package/dist/paths-DXSHAMIa.js +42 -0
- package/dist/pdp/index.d.ts +7 -3
- package/dist/pdp/index.js +45 -26
- package/dist/{peer-BOr_kJE1.js → peer-C8swJ2GH.js} +23 -4
- package/dist/peer-CRvC7Hg1.js +1 -0
- package/dist/permdock-BrAscV28.js +868 -0
- package/dist/permdock-SV8ZINYT.d.ts +502 -0
- package/dist/{permission-registry-BsblpB-z.js → permission-registry-Br4jSNVi.js} +3 -3
- package/dist/permissions-B-wCP5ZC.js +30 -0
- package/dist/{permissions-CCJ6lmxB.js → permissions-BPxlwVhN.js} +4 -3
- package/dist/{permissions-COH-KRgT.d.ts → permissions-u_KHg-sN.d.ts} +1 -2
- package/dist/policies-DP2nqRoF.js +295 -0
- package/dist/{policy-ur0BBK_e.d.ts → policy-77FSAq_V.d.ts} +2 -2
- package/dist/{hosted-CBHgkzDl.js → policy-DbM5YYtV.js} +17 -273
- package/dist/policy-from-catalog-C2gMa2Dj.js +450 -0
- package/dist/policy-module-CTlwMvLI.js +89 -0
- package/dist/{policy-types-sp9P1iIV.d.ts → policy-types-n0uRTsW8.d.ts} +460 -632
- package/dist/postgrest-CEgRbefE.d.ts +100 -0
- package/dist/postgrest-DAjUkF9w.js +575 -0
- package/dist/{powersync-cZe4kzhL.js → powersync-BkWWCS1K.js} +23 -24
- package/dist/powersync-DISt6SUe.js +62 -0
- package/dist/powersync-MMJ2mI2q.js +1 -0
- package/dist/preset/index.d.ts +146 -0
- package/dist/preset/index.js +1 -0
- package/dist/preset-DZdU0tne.js +1768 -0
- package/dist/principal-source-pHneukrx.js +103 -0
- package/dist/prisma/index.d.ts +6 -3
- package/dist/prisma/index.js +2 -2
- package/dist/{problem-CnWraVhC.js → problem-CTviyH5L.js} +13 -23
- package/dist/{problem-details-rIIo8B2C.d.ts → problem-details-B45V5d2b.d.ts} +1 -1
- package/dist/{problem-details-DECUnXAn.js → problem-details-cn5X30gZ.js} +43 -40
- package/dist/problem-types-CJ2325SZ.js +5 -0
- package/dist/{prompt-CXYhLfsS.js → prompt-DfZLJLIn.js} +2 -2
- package/dist/{protected-BN7BUdY1.js → protected-BlF0F6wL.js} +21 -4
- package/dist/{protected-B3_P7xuw.d.ts → protected-RFGpakN9.d.ts} +16 -8
- package/dist/{provider-Bi-0HNL3.js → provider-B1al-AZz.js} +2 -2
- package/dist/{provider-kwHC9CZD.d.ts → provider-DILL9oPK.d.ts} +1 -1
- package/dist/{provider-store-Ctt-81Ni.js → provider-store-DM6DNcta.js} +2 -2
- package/dist/pull-CvpINcMK.js +1 -0
- package/dist/pull-jEwbf80J.js +256 -0
- package/dist/push-BD4f15co.js +116 -0
- package/dist/pushed-policy-CqtxkV4G.js +238 -0
- package/dist/pushed-policy-DCW91KQU.d.ts +57 -0
- package/dist/react/index.d.ts +8 -8
- package/dist/react/index.js +6 -6
- package/dist/react/provider-client.d.ts +1 -1
- package/dist/react/provider-client.js +1 -2
- package/dist/react-native/index.d.ts +24 -72
- package/dist/react-native/index.js +29 -25
- package/dist/{refs-f3imMjNW.js → refs-K6eWUJ0H.js} +1 -1
- package/dist/{relation-walk-BXnlLtg2.js → relation-walk-3OyDBF0S.js} +2 -2
- package/dist/{relations-CvmSpG67.js → relations-BnCWnJGI.js} +3 -3
- package/dist/renames-CGj7YwcQ.js +175 -0
- package/dist/{replay-COpmZUfx.d.ts → replay-Bkm-Ne4l.d.ts} +1 -1
- package/dist/{replay-BsOyTrdV.js → replay-Du1tJjG7.js} +2 -1
- package/dist/{request-context-BVM7aWNs.js → request-context-C81Pip9n.js} +3 -3
- package/dist/request-schema-CMreVuFM.js +148 -0
- package/dist/resend/index.d.ts +126 -0
- package/dist/resend/index.js +379 -0
- package/dist/{resource-relations-D1Un3pcz.js → resource-relations-D_jw0BFx.js} +1 -1
- package/dist/{retire-BFcxL6un.js → retire-CALjIQ7e.js} +2 -2
- package/dist/{revocations-Cy06JNx2.d.ts → revocations-R07lFBnX.d.ts} +2 -0
- package/dist/rls-BAqP4q66.js +336 -0
- package/dist/{rls-BEktUaqg.js → rls-DTvYd-lI.js} +288 -690
- package/dist/{role-builder-B4GbKM-i.js → role-builder-uWQZ4v84.js} +14 -207
- package/dist/{rpc-nYC3cR_u.js → rpc-VSl5ylaQ.js} +7 -7
- package/dist/run-CUKuz0A8.js +723 -0
- package/dist/run-DDwUaJDU.js +1 -0
- package/dist/run-ticket-DocwaTKn.d.ts +124 -0
- package/dist/run-ticket-sxFI6LAk.js +337 -0
- package/dist/{runtime-D_tMXnsq.js → runtime-D3VTAiCA.js} +1 -1
- package/dist/{saas-DtaA4JuW.js → saas-BCXRuXca.js} +16 -14
- package/dist/scan-qvOe0hS3.js +29 -0
- package/dist/{schema-DjQBcAP9.js → schema-B4v0Qrac.js} +102 -8
- package/dist/schema-yLuBWWsh.js +130 -0
- package/dist/scim/index.d.ts +2 -2
- package/dist/scim/index.js +1 -2
- package/dist/{scim-vFR1O5oG.js → scim-ClHtpyV3.js} +208 -76
- package/dist/{scopes-tvpa34ep.js → scopes-CUPSjo9q.js} +12 -7
- package/dist/server/index.d.ts +12 -143
- package/dist/server/index.js +12 -9
- package/dist/server-C3WXAHZj.js +453 -0
- package/dist/{server-types-DdqBwZN_.d.ts → server-types-CKQiCnON.d.ts} +51 -68
- package/dist/{sha256-BO0swRXL.js → sha256-D9ihqmvd.js} +8 -1
- package/dist/{shared-1uhI-e-b.js → shared-BAK8t4Uo.js} +20 -9
- package/dist/{shared-JhMeOsDF.js → shared-DvvFzYXe.js} +8 -9
- package/dist/signals-D3NcTKUS.js +32 -0
- package/dist/sink-4-eQIRWf.d.ts +225 -0
- package/dist/sink-BEZSlMS_.js +957 -0
- package/dist/{skills-BZdRQkxs.js → skills-B6lVdD_W.js} +13 -6
- package/dist/skills-CTYwkl44.js +51 -0
- package/dist/snapshot-B7DkYg6g.js +269 -0
- package/dist/{snapshot-cache-Cod40D-X.js → snapshot-cache-DZHQ0KF6.js} +11 -5
- package/dist/{snapshot-cache-J5T7t_vD.d.ts → snapshot-cache-PXI-vGpY.d.ts} +7 -5
- package/dist/solid/index.d.ts +11 -12
- package/dist/solid/index.js +7 -7
- package/dist/solid-start/index.d.ts +52 -0
- package/dist/solid-start/index.js +49 -0
- package/dist/{sources-Dljy06zL.d.ts → sources-Xc4zkszI.d.ts} +7 -13
- package/dist/{sources-B1-b-R0t.js → sources-vs5dGOsS.js} +125 -119
- package/dist/{sql-t8teWOfo.js → sql-B5T1IL83.js} +13 -1
- package/dist/src-Cu2m75uo.js +1860 -0
- package/dist/ssf/index.d.ts +4 -4
- package/dist/ssf/index.js +384 -47
- package/dist/standard-webhooks-DgFFD-Ga.js +228 -0
- package/dist/standard-webhooks-rbD7LS6h.d.ts +88 -0
- package/dist/storage-C2bWdvJH.js +132 -0
- package/dist/{store-ILkLUj_o.js → store-BDutJkDL.js} +40 -53
- package/dist/{store-options-CbUHDoSe.js → store-options-u0ycEe8s.js} +26 -21
- package/dist/{stores-Cxz_80g-.d.ts → stores-BUGNbJTt.d.ts} +7 -8
- package/dist/{stores-BHXdSbw9.js → stores-CGhM8BN_.js} +2 -2
- package/dist/{stream-Dwsg6WwE.d.ts → stream-CeCMJmBB.d.ts} +2 -2
- package/dist/{stream-BMui98KK.js → stream-CvdUS9V1.js} +9 -2
- package/dist/style-DiKWnAvn.js +1 -0
- package/dist/{style-CTm1AXDT.js → style-Uao76wO7.js} +3 -2
- package/dist/subject-BGoM5o8B.js +17 -0
- package/dist/supabase/index.d.ts +23 -106
- package/dist/supabase/index.js +5 -4
- package/dist/supabase/middleware.d.ts +7 -6
- package/dist/supabase/middleware.js +13 -11
- package/dist/supabase-By_frk3U.js +92 -0
- package/dist/{supabase-hook-Cva3ikXz.js → supabase-hook-BMoEhZir.js} +960 -64
- package/dist/supabase-hook-CCw5m35C.js +1 -0
- package/dist/{supabase-C1TWlnYt.js → supabase-x5JtR2tc.js} +99 -206
- package/dist/support-session-C6xJQYut.js +158 -0
- package/dist/svelte/Protected.svelte +4 -4
- package/dist/svelte/index.d.ts +7 -7
- package/dist/svelte/index.js +10 -10
- package/dist/svelte/runtime.d.ts +3 -3
- package/dist/svelte/runtime.js +3 -3
- package/dist/svelte/source.d.ts +7 -7
- package/dist/svelte/source.js +4 -4
- package/dist/sveltekit/index.d.ts +51 -0
- package/dist/sveltekit/index.js +27 -0
- package/dist/tanstack-start/index.d.ts +51 -0
- package/dist/tanstack-start/index.js +40 -0
- package/dist/target-Bw37TFNA.js +51 -0
- package/dist/{tenancy-DshjqY0n.js → tenancy-DN2QAx9z.js} +5 -5
- package/dist/terminal/index.d.ts +11 -9
- package/dist/terminal/index.js +23 -287
- package/dist/testing/index.d.ts +311 -38
- package/dist/testing/index.js +3475 -201
- package/dist/testing/saas/index.d.ts +9 -8
- package/dist/testing/saas/index.js +2 -2
- package/dist/testing/saas/permissions.d.ts +1 -1
- package/dist/testing/saas/permissions.js +1 -2
- package/dist/text-BMFRfBqc.js +8 -0
- package/dist/{text-diff-BdkkYSbi.js → text-diff-BQcUapOb.js} +1 -1
- package/dist/thenable-yD7SabqG.js +31 -0
- package/dist/time-DkWjdL98.js +71 -0
- package/dist/{token-Bx9qy30p.js → token-BLZcjQgt.js} +15 -6
- package/dist/{tool-refusal-CQ7OEia9.js → tool-refusal-CMLN5xhn.js} +2 -2
- package/dist/trpc/index.d.ts +12 -9
- package/dist/trpc/index.js +16 -14
- package/dist/types-8RnwUAvE.d.ts +107 -0
- package/dist/{store-9KO4BlkB.d.ts → types-B3_ooZgZ.d.ts} +100 -75
- package/dist/{types-Br9IXUBG.d.ts → types-BdtSDbBO.d.ts} +77 -17
- package/dist/types-BtHtcKMr.d.ts +157 -0
- package/dist/{types-CqrjB9o6.d.ts → types-BzzaHxt3.d.ts} +23 -11
- package/dist/types-C791HFu9.d.ts +332 -0
- package/dist/types-C863OQiR.d.ts +57 -0
- package/dist/{types-ByUo56un.d.ts → types-CVZSNocA.d.ts} +23 -22
- package/dist/{types-DHrmBJLH.d.ts → types-CX7AvRqP.d.ts} +7 -7
- package/dist/types-C_IiEs64.d.ts +65 -0
- package/dist/{types-7rJVLWe5.d.ts → types-Dfv0aTER.d.ts} +63 -11
- package/dist/types-DqzsnlSA.js +67 -0
- package/dist/{types-DpWfQdSh.d.ts → types-Druu9wLM.d.ts} +46 -11
- package/dist/{types-CIlTojhq.d.ts → types-_WhUP7ud.d.ts} +9 -6
- package/dist/{types-CUcFZfpU.d.ts → types-lUlG3TVd.d.ts} +2 -1
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +2 -2
- package/dist/usage-CRVl9V3t.js +32 -0
- package/dist/usage-CZpSQP6L.js +1 -0
- package/dist/{usage-CG9nOYZw.js → usage-SveTRTyr.js} +54 -11
- package/dist/{validation-error-BSSw0l_8.d.ts → validation-error-DHUrAkku.d.ts} +1 -1
- package/dist/{validity-dVrUEPJt.js → validity-CZ7-y0Cj.js} +32 -15
- package/dist/{verifier-DMr_Zmb_.js → verifier-DHuh-okk.js} +18 -7
- package/dist/{verify-BztUB3GX.js → verify-BaAKx7t6.js} +645 -50
- package/dist/{version-BEogC-rB.js → version-CEbQ5UeM.js} +4 -3
- package/dist/version-DXkYUISx.js +1 -0
- package/dist/{views-CJsrBS1f.js → views-DTIIO-oQ.js} +22 -5
- package/dist/{permission-types-Dg39z9Kv.d.ts → vocabulary-CN8H_9UF.d.ts} +67 -2
- package/dist/vue/index.d.ts +9 -10
- package/dist/vue/index.js +8 -8
- package/dist/waiter-B986w1oK.d.ts +36 -0
- package/dist/waiter-BGUtGYoU.js +67 -0
- package/dist/watch--ovlPOc0.js +1 -0
- package/dist/{watch-zeucB7eF.js → watch-vea54PRw.js} +44 -5
- package/dist/webmcp/index.d.ts +2 -2
- package/dist/webmcp/index.js +3 -3
- package/dist/where-scope-C9vfEphl.js +306 -0
- package/dist/who-can-DWBAcaCQ.js +364 -0
- package/dist/wire-delegation-Cv3W2eaM.js +36 -0
- package/dist/wire-denial-C7PaVYEM.js +27 -0
- package/dist/workflow/index.d.ts +5 -0
- package/dist/workflow/index.js +223 -0
- package/dist/workflow-sdk/index.d.ts +56 -0
- package/dist/workflow-sdk/index.js +78 -0
- package/dist/{wrap-Cr93qqdP.d.ts → wrap-C5jC6sKI.d.ts} +2 -1
- package/dist/{wrap-DWsU_5Bd.js → wrap-gX61EtpB.js} +7 -6
- package/package.json +136 -37
- package/schemas/access-event-v1.json +118 -0
- package/schemas/approval-request-v1.json +34 -1
- package/schemas/approval-signature-v1.json +94 -0
- package/schemas/automation-event-v1.json +113 -0
- package/schemas/capability-v1.json +84 -0
- package/schemas/catalog-event-v1.json +87 -0
- package/schemas/catalog-v1.json +123 -12
- package/schemas/cli-commands-v1.json +164 -0
- package/schemas/cli-result-v1.json +96 -0
- package/schemas/cloud-api-v1.json +5407 -0
- package/schemas/connector-v1.json +408 -0
- package/schemas/credential-event-v1.json +81 -0
- package/schemas/credential-v1.json +134 -0
- package/schemas/decision-event-v1.json +288 -0
- package/schemas/delivery-event-v1.json +117 -0
- package/schemas/directory-event-v1.json +85 -0
- package/schemas/evidence-export-v1.json +54 -0
- package/schemas/membership-event-v1.json +93 -0
- package/schemas/policy-v1.json +68 -0
- package/schemas/pushed-policy-v1.json +236 -0
- package/schemas/run-v1.json +162 -0
- package/schemas/snapshot-v1.json +138 -0
- package/schemas/supabase-claims-v1.json +4 -4
- package/schemas/supabase-manifest-v1.json +5 -1
- package/schemas/support-session-v1.json +111 -0
- package/skills/permdock/SKILL.md +16 -12
- package/skills/permdock-agents/SKILL.md +40 -23
- package/skills/permdock-agents/references/operations-automations-chat.md +221 -0
- package/skills/permdock-agents/references/runtimes.md +22 -18
- package/skills/permdock-approvals/SKILL.md +29 -13
- package/skills/permdock-approvals/references/delivery-and-waiters.md +172 -0
- package/skills/permdock-approvals/references/stores-and-resume.md +21 -7
- package/skills/permdock-audit/SKILL.md +3 -3
- package/skills/permdock-credentials/SKILL.md +5 -5
- package/skills/permdock-credentials/references/tokens-keys-links.md +14 -10
- package/skills/permdock-data/SKILL.md +6 -6
- package/skills/permdock-data/references/queries-and-rls.md +10 -9
- package/skills/permdock-data/references/relationships.md +3 -3
- package/skills/permdock-tenancy/SKILL.md +20 -7
- package/skills/permdock-tenancy/references/contacts-and-support-sessions.md +122 -0
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +6 -4
- package/skills/permdock-wire/SKILL.md +17 -11
- package/skills/permdock-wire/references/adapters.md +139 -51
- package/skills/permdock-wire/references/policy-options.md +5 -5
- package/skills/permdock-wire/references/presets-and-recipes.md +58 -0
- package/dist/approvals-DZL76lKW.js +0 -240
- package/dist/arazzo-CD8d0WLb.js +0 -54
- package/dist/catalog-6yWAX6Ps.js +0 -2
- package/dist/catalog-WHG4kBq7.js +0 -48
- package/dist/cloud-C7bSDgX4.js +0 -50
- package/dist/cloud-PeBN3IEn.js +0 -118
- package/dist/collect-Bw2Jdq1q.js +0 -2
- package/dist/collect-CQx4hX4N.js +0 -83
- package/dist/commands-Cpip4ULx.js +0 -97
- package/dist/config-DLt6BvV7.js +0 -36
- package/dist/diff-Cnghh_Ip.js +0 -48
- package/dist/doctor-CuVJvhV_.js +0 -53
- package/dist/duration-xK2jGFVc.js +0 -25
- package/dist/errors-CWfoNg9N.js +0 -47
- package/dist/errors-DRnGZHzF.js +0 -30
- package/dist/expiry-BoBJ8Tvc.js +0 -7
- package/dist/fields-fwfXN01n.js +0 -1016
- package/dist/headers-BZSDieNT.js +0 -6
- package/dist/headers-Cf-zWvsO.d.ts +0 -6
- package/dist/ids-DC4V4CFu.js +0 -18
- package/dist/index-DgM7w96K.d.ts +0 -6
- package/dist/index-Dz7didpC.d.ts +0 -201
- package/dist/index-UdVQgq48.d.ts +0 -550
- package/dist/load-B_shHrmd.js +0 -2
- package/dist/manifest-BngWKryL.js +0 -770
- package/dist/may-use-BOn_dR_w.js +0 -78
- package/dist/openapi-DcQ1Kg40.js +0 -164
- package/dist/ownership-BgnbOJFT.js +0 -851
- package/dist/permdock-L26udHSk.js +0 -3646
- package/dist/powersync-BFSbSElQ.js +0 -62
- package/dist/powersync-DCDgZgfZ.js +0 -2
- package/dist/rls-55DQTuf5.js +0 -250
- package/dist/run-DMcNW_8T.js +0 -174
- package/dist/server-ZYEHtNHS.js +0 -268
- package/dist/sink-BTtmfPnE.js +0 -141
- package/dist/sink-CPN4zLfs.d.ts +0 -104
- package/dist/skills-Dle4ZoLF.js +0 -56
- package/dist/snapshot-Dc9CHPME.js +0 -1078
- package/dist/src-Bb7no-hS.js +0 -639
- package/dist/subject-C6dLxqHU.js +0 -24
- package/dist/supabase-Dbqr141s.js +0 -83
- package/dist/supabase-hook-BULg7Ut_.js +0 -2
- package/dist/thenable-QSmvt-vZ.js +0 -65
- package/dist/types-C0dee0Qf.d.ts +0 -197
- package/dist/types-DvupZE0w.d.ts +0 -9
- package/dist/usage-4G6t643F.js +0 -42
- package/dist/usage-iFLJ0-rr.js +0 -2
- package/dist/vocabulary-hhEFLFoL.d.ts +0 -65
- package/dist/watch-C8TpsAl4.js +0 -2
|
@@ -1,3646 +0,0 @@
|
|
|
1
|
-
import { a as sole, n as isReadonlyArray, r as isRecord, t as compact } from "./compact-CAf557ZB.js";
|
|
2
|
-
import { n as freezeDeep, t as freezeCopy } from "./freeze-CrU8nQeX.js";
|
|
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
|
-
import { n as sameId } from "./ids-DC4V4CFu.js";
|
|
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";
|
|
13
|
-
import { t as parseDuration } from "./duration-xK2jGFVc.js";
|
|
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";
|
|
22
|
-
import { t as requiresApproval } from "./approval-required-TzNB9oQA.js";
|
|
23
|
-
import { t as byCodePoint } from "./compare-pfyVnBjE.js";
|
|
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";
|
|
26
|
-
import { t as resourceIdOf } from "./resource-ref-CaS9yVPi.js";
|
|
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";
|
|
29
|
-
import { i as isSubject, r as isPrincipal, t as anonymousSubject } from "./subject-C6dLxqHU.js";
|
|
30
|
-
//#region src/core/approval-policies.ts
|
|
31
|
-
/** The denial detail when an `ApprovalPolicySource` fails: every call an allow would grant denies with reason `approval`. */
|
|
32
|
-
const APPROVAL_POLICY_UNAVAILABLE = "approval-policy-unavailable";
|
|
33
|
-
function memoryApprovalPolicies(policies) {
|
|
34
|
-
const entries = [...policies];
|
|
35
|
-
return Object.freeze({ approvalPoliciesFor(query) {
|
|
36
|
-
return entries.filter((entry) => entry.tenant === void 0 || query.tenants.includes(entry.tenant));
|
|
37
|
-
} });
|
|
38
|
-
}
|
|
39
|
-
function isStringList(value) {
|
|
40
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
41
|
-
}
|
|
42
|
-
function checkOne(policy, raw) {
|
|
43
|
-
if (!isRecord(raw) || typeof raw["permission"] !== "string") return { problem: "invalid" };
|
|
44
|
-
const permission = findPermission(policy.permissions, raw["permission"]);
|
|
45
|
-
if (permission === void 0) return { problem: "unknown-permission" };
|
|
46
|
-
const { tenant, actors, where, check, approval } = raw;
|
|
47
|
-
if (tenant !== void 0 && typeof tenant !== "string" || actors !== void 0 && !isStringList(actors) || where !== void 0 && !isPortableCondition(where) || check !== void 0 && !isPortableCondition(check) || approval !== "human" && !isRecord(approval)) return { problem: "invalid" };
|
|
48
|
-
if (where !== void 0 && permission.kind !== "instance") return { problem: "where-on-collection" };
|
|
49
|
-
let normalized;
|
|
50
|
-
try {
|
|
51
|
-
normalized = normalizeApproval(approval, permission.key);
|
|
52
|
-
if (normalized !== void 0) assertApprovalRelations([{
|
|
53
|
-
permission,
|
|
54
|
-
approval: normalized
|
|
55
|
-
}], policy.resources);
|
|
56
|
-
} catch {
|
|
57
|
-
return { problem: "invalid" };
|
|
58
|
-
}
|
|
59
|
-
if (normalized === void 0) return { problem: "invalid" };
|
|
60
|
-
if (normalized !== "human" && normalized.staleOn !== void 0 && (permission.kind !== "instance" || policy.resources.get(permission.resource)?.version === void 0)) return { problem: "stale-on-without-version" };
|
|
61
|
-
return { entry: compact({
|
|
62
|
-
permission: permission.key,
|
|
63
|
-
tenant,
|
|
64
|
-
actors: actors === void 0 ? void 0 : new Set(actors),
|
|
65
|
-
where: where === void 0 ? void 0 : normalizeWhere(where),
|
|
66
|
-
check: check === void 0 ? void 0 : normalizeWhere(check),
|
|
67
|
-
approval: normalized
|
|
68
|
-
}) };
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Checks one entry against the policy the way `approvalPoliciesFor` loads
|
|
72
|
-
* it, for an application to refuse a bad entry when it is saved instead of
|
|
73
|
-
* finding every call denied. Never throws.
|
|
74
|
-
*/
|
|
75
|
-
function validateApprovalPolicy(policy, entry) {
|
|
76
|
-
try {
|
|
77
|
-
const checked = checkOne(policy, entry);
|
|
78
|
-
return "problem" in checked ? {
|
|
79
|
-
ok: false,
|
|
80
|
-
problem: checked.problem
|
|
81
|
-
} : { ok: true };
|
|
82
|
-
} catch {
|
|
83
|
-
return {
|
|
84
|
-
ok: false,
|
|
85
|
-
problem: "invalid"
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* One raw entry checked against the policy: `null` when it names no
|
|
91
|
-
* permission the policy declares (it applies to nothing), `undefined` when
|
|
92
|
-
* it does not load.
|
|
93
|
-
*/
|
|
94
|
-
function loadOne(policy, raw) {
|
|
95
|
-
const checked = checkOne(policy, raw);
|
|
96
|
-
if ("entry" in checked) return checked.entry;
|
|
97
|
-
return checked.problem === "unknown-permission" ? null : void 0;
|
|
98
|
-
}
|
|
99
|
-
function loadAll(policy, raw) {
|
|
100
|
-
if (!Array.isArray(raw)) return "failed";
|
|
101
|
-
const out = [];
|
|
102
|
-
for (const item of raw) {
|
|
103
|
-
const loaded = loadOne(policy, item);
|
|
104
|
-
if (loaded === void 0) return "failed";
|
|
105
|
-
if (loaded !== null) out.push(loaded);
|
|
106
|
-
}
|
|
107
|
-
return out;
|
|
108
|
-
}
|
|
109
|
-
/** Reads the source once for `tenants`; a throw or an invalid entry loads as `"failed"`. */
|
|
110
|
-
function approvalPoliciesFor(policy, source, tenants, auth) {
|
|
111
|
-
if (source === void 0) return;
|
|
112
|
-
const threw = () => {
|
|
113
|
-
auth.push({
|
|
114
|
-
reason: "source-threw",
|
|
115
|
-
source: "approvalPolicies"
|
|
116
|
-
});
|
|
117
|
-
return "failed";
|
|
118
|
-
};
|
|
119
|
-
let raw;
|
|
120
|
-
try {
|
|
121
|
-
raw = source.approvalPoliciesFor({ tenants });
|
|
122
|
-
} catch {
|
|
123
|
-
return threw();
|
|
124
|
-
}
|
|
125
|
-
if (isThenable(raw)) return Promise.resolve(raw).then((value) => loadAll(policy, value), threw);
|
|
126
|
-
return loadAll(policy, raw);
|
|
127
|
-
}
|
|
128
|
-
function matches(entry, tenant, subject, current, next, now, scopes) {
|
|
129
|
-
if (entry.tenant !== void 0 && entry.tenant !== tenant) return false;
|
|
130
|
-
if (entry.actors !== void 0 && (subject.actor === void 0 || !entry.actors.has(subject.actor.kind))) return false;
|
|
131
|
-
if (entry.where !== void 0 && !evaluateCondition(entry.where, current, subject, now, scopes)) return false;
|
|
132
|
-
return entry.check === void 0 || evaluateCondition(entry.check, next ?? current, subject, now, scopes);
|
|
133
|
-
}
|
|
134
|
-
function asStages(approval) {
|
|
135
|
-
if (approval === "human") return [{ by: authenticated() }];
|
|
136
|
-
if (approval.stages !== void 0) return approval.stages;
|
|
137
|
-
return [compact({
|
|
138
|
-
by: approval.by ?? authenticated(),
|
|
139
|
-
quorum: approval.quorum
|
|
140
|
-
})];
|
|
141
|
-
}
|
|
142
|
-
/** An entry's stages, each carrying the entry's escalation unless it has its own. */
|
|
143
|
-
function entryStages(approval) {
|
|
144
|
-
const stages = asStages(approval);
|
|
145
|
-
if (approval === "human" || approval.escalation === void 0) return stages;
|
|
146
|
-
const escalation = approval.escalation;
|
|
147
|
-
return stages.map((stage) => stage.escalation === void 0 ? compact({
|
|
148
|
-
by: stage.by,
|
|
149
|
-
quorum: stage.quorum,
|
|
150
|
-
escalation
|
|
151
|
-
}) : stage);
|
|
152
|
-
}
|
|
153
|
-
function shorter(a, b) {
|
|
154
|
-
if (a === void 0) return b;
|
|
155
|
-
if (b === void 0) return a;
|
|
156
|
-
return (parseDuration(b) ?? Infinity) < (parseDuration(a) ?? Infinity) ? b : a;
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* `code` with every matching entry added as further stages: the result
|
|
160
|
-
* needs all of them (`'sequential'` when any part is ordered), keeps the
|
|
161
|
-
* code's escalation, the shortest `ttl`, and `distinct` unless every part
|
|
162
|
-
* opts out. Without a matching entry, `code` unchanged.
|
|
163
|
-
*/
|
|
164
|
-
function tightenApproval(code, entries, input) {
|
|
165
|
-
const extra = entries.filter((entry) => entry.permission === input.permission && matches(entry, input.tenant, input.subject, input.current, input.next, input.now, input.scopes));
|
|
166
|
-
if (extra.length === 0) return code;
|
|
167
|
-
const parts = [...code === void 0 ? [] : [code], ...extra.map((entry) => entry.approval)];
|
|
168
|
-
const stages = [];
|
|
169
|
-
const seen = /* @__PURE__ */ new Set();
|
|
170
|
-
let sequential = false;
|
|
171
|
-
let distinct = false;
|
|
172
|
-
let staleOn;
|
|
173
|
-
let ttl;
|
|
174
|
-
for (const [index, part] of parts.entries()) {
|
|
175
|
-
const fromCode = code !== void 0 && index === 0;
|
|
176
|
-
for (const stage of fromCode ? asStages(part) : entryStages(part)) {
|
|
177
|
-
const key = JSON.stringify(stage);
|
|
178
|
-
if (!seen.has(key)) {
|
|
179
|
-
seen.add(key);
|
|
180
|
-
stages.push(stage);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (part === "human") {
|
|
184
|
-
distinct = true;
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
sequential ||= part.mode === "sequential";
|
|
188
|
-
distinct ||= part.distinct !== false;
|
|
189
|
-
staleOn ??= part.staleOn;
|
|
190
|
-
ttl = shorter(ttl, part.ttl);
|
|
191
|
-
}
|
|
192
|
-
return compact({
|
|
193
|
-
mode: sequential ? "sequential" : "all",
|
|
194
|
-
stages,
|
|
195
|
-
distinct: distinct ? void 0 : false,
|
|
196
|
-
staleOn,
|
|
197
|
-
ttl,
|
|
198
|
-
escalation: code === void 0 || code === "human" ? void 0 : code.escalation
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
/** The tenant a matched allow applies in: its membership's root scope, else the active tenant. */
|
|
202
|
-
function decisionTenant(subject, membership) {
|
|
203
|
-
return (membership === void 0 ? void 0 : rootMembershipId(membership)) ?? subject.principal?.tenant;
|
|
204
|
-
}
|
|
205
|
-
//#endregion
|
|
206
|
-
//#region src/core/memberships.ts
|
|
207
|
-
/** Two memberships are the same row when everything but `roles` matches. */
|
|
208
|
-
function identity(membership) {
|
|
209
|
-
const within = Object.entries(membership.within ?? {}).toSorted(([a], [b]) => byCodePoint(a, b));
|
|
210
|
-
return JSON.stringify([
|
|
211
|
-
membership.scope ?? null,
|
|
212
|
-
membership.id ?? null,
|
|
213
|
-
within,
|
|
214
|
-
membership.on ?? null,
|
|
215
|
-
membership.tenant ?? null,
|
|
216
|
-
membership.team ?? null,
|
|
217
|
-
membership.via ?? null,
|
|
218
|
-
membership.expiresAt ?? null,
|
|
219
|
-
membership.grantedBy ?? null,
|
|
220
|
-
membership.reason ?? null,
|
|
221
|
-
membership.member?.group ?? null,
|
|
222
|
-
[...membership.eligible ?? []].toSorted(),
|
|
223
|
-
membership.managedBy ?? null,
|
|
224
|
-
[...membership.entitlements ?? []].toSorted(),
|
|
225
|
-
membership.keep === void 0 ? null : [...membership.keep].toSorted()
|
|
226
|
-
]);
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Merges memberships from several sources: entries naming the same instance
|
|
230
|
-
* with the same `via`, expiry, owner and seats become one entry with the union
|
|
231
|
-
* of their roles. Entries that differ in any of those stay separate, so a
|
|
232
|
-
* staff role and a contact role in one instance keep their own `via`.
|
|
233
|
-
*/
|
|
234
|
-
function mergeMemberships(lists) {
|
|
235
|
-
const merged = /* @__PURE__ */ new Map();
|
|
236
|
-
for (const list of lists) for (const membership of list) {
|
|
237
|
-
const key = identity(membership);
|
|
238
|
-
const found = merged.get(key);
|
|
239
|
-
merged.set(key, found === void 0 ? membership : {
|
|
240
|
-
...found,
|
|
241
|
-
roles: [.../* @__PURE__ */ new Set([...found.roles, ...membership.roles])]
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
return [...merged.values()];
|
|
245
|
-
}
|
|
246
|
-
function all(values) {
|
|
247
|
-
return values.some((value) => isThenable(value)) ? Promise.all(values) : values;
|
|
248
|
-
}
|
|
249
|
-
function then(value, next) {
|
|
250
|
-
return isThenable(value) ? Promise.resolve(value).then(next) : next(value);
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* One `MembershipSource` over several: memberships are merged and
|
|
254
|
-
* de-duplicated, `list` concatenates the sources that can list, and `version`
|
|
255
|
-
* is the highest version any source reports. A source that throws makes the
|
|
256
|
-
* whole lookup throw, which the subject resolver turns into no memberships.
|
|
257
|
-
*/
|
|
258
|
-
function composeMemberships(sources) {
|
|
259
|
-
const listing = sources.filter((source) => source.list !== void 0);
|
|
260
|
-
const versioned = sources.filter((source) => source.version !== void 0);
|
|
261
|
-
return {
|
|
262
|
-
membershipsFor(principal, options) {
|
|
263
|
-
return then(all(sources.map((source) => source.membershipsFor(principal, options))), (lists) => mergeMemberships(lists));
|
|
264
|
-
},
|
|
265
|
-
...listing.length === 0 ? {} : { list(query) {
|
|
266
|
-
return then(all(listing.map((source) => source.list?.(query) ?? [])), (lists) => {
|
|
267
|
-
const seen = /* @__PURE__ */ new Map();
|
|
268
|
-
for (const entry of lists.flat()) {
|
|
269
|
-
const key = `${entry.principal.id}\u0000${identity(entry.membership)}`;
|
|
270
|
-
const found = seen.get(key);
|
|
271
|
-
seen.set(key, found === void 0 ? entry : {
|
|
272
|
-
principal: entry.principal,
|
|
273
|
-
membership: {
|
|
274
|
-
...found.membership,
|
|
275
|
-
roles: [.../* @__PURE__ */ new Set([...found.membership.roles, ...entry.membership.roles])]
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
return [...seen.values()];
|
|
280
|
-
});
|
|
281
|
-
} },
|
|
282
|
-
...versioned.length === 0 ? {} : { version(principal) {
|
|
283
|
-
return then(all(versioned.map((source) => source.version?.(principal))), (versions) => {
|
|
284
|
-
const known = versions.filter((value) => typeof value === "number" && Number.isFinite(value));
|
|
285
|
-
return known.length === 0 ? void 0 : Math.max(...known);
|
|
286
|
-
});
|
|
287
|
-
} }
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
/** A single source, or several composed. */
|
|
291
|
-
function asMembershipSource(input) {
|
|
292
|
-
return Array.isArray(input) ? composeMemberships(input) : input;
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Trust the memberships the verified token carries, and read `source` only
|
|
296
|
-
* when the token marks them truncated. With `version`, a token minted before
|
|
297
|
-
* the latest membership change is stale for the policy's `fresh` permissions,
|
|
298
|
-
* or with `onStale: 'reread'` has its memberships read from `source` instead.
|
|
299
|
-
*/
|
|
300
|
-
function claimsFirst(source, options = {}) {
|
|
301
|
-
const inner = asMembershipSource(source);
|
|
302
|
-
const version = options.version ?? (inner.version === void 0 ? void 0 : (principal) => inner.version?.(principal));
|
|
303
|
-
return {
|
|
304
|
-
membershipsFor: (principal, query) => inner.membershipsFor(principal, query),
|
|
305
|
-
...inner.list === void 0 ? {} : { list: (query) => inner.list?.(query) ?? [] },
|
|
306
|
-
...version === void 0 ? {} : { version },
|
|
307
|
-
...options.onStale === "reread" ? { onStale: "reread" } : {},
|
|
308
|
-
claimsFirst: true
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
/** Whether the identity provider owns `membership`: the application must not add, change or remove it. */
|
|
312
|
-
function isExternallyManaged(membership) {
|
|
313
|
-
return membership.managedBy === "idp";
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* How many principals hold `role` in one scope instance now, from the
|
|
317
|
-
* source's member list (`MembershipSource.list`): live memberships of exactly
|
|
318
|
-
* that scope and instance, each principal counted once. The `holders` a
|
|
319
|
-
* `decideRoleChange` with `min`, `max` or `transferOnly` needs. `undefined`
|
|
320
|
-
* when the source cannot list or the read fails, which that check denies.
|
|
321
|
-
*/
|
|
322
|
-
async function countHolders(source, query) {
|
|
323
|
-
const composed = asMembershipSource(source);
|
|
324
|
-
if (composed.list === void 0) return;
|
|
325
|
-
const role = typeof query.role === "string" ? query.role : query.role.key;
|
|
326
|
-
try {
|
|
327
|
-
const entries = await composed.list({
|
|
328
|
-
scope: query.scope,
|
|
329
|
-
id: query.id
|
|
330
|
-
});
|
|
331
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
332
|
-
const holders = /* @__PURE__ */ new Set();
|
|
333
|
-
for (const entry of entries) {
|
|
334
|
-
const membership = entry.membership;
|
|
335
|
-
if (membership.scope === query.scope && membership.id === query.id && membership.roles.includes(role) && (membership.expiresAt === void 0 || membership.expiresAt > now)) holders.add(entry.principal.id);
|
|
336
|
-
}
|
|
337
|
-
return holders.size;
|
|
338
|
-
} catch {
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
//#endregion
|
|
343
|
-
//#region src/core/custom-roles.ts
|
|
344
|
-
/** The current key for a stored key, current or former; `undefined` when neither. */
|
|
345
|
-
function currentKey(policy, raw) {
|
|
346
|
-
const leaf = findPermission(policy.permissions, raw);
|
|
347
|
-
if (leaf === void 0) return;
|
|
348
|
-
return leaf.key === raw || formerKeys(leaf).includes(raw) ? leaf.key : void 0;
|
|
349
|
-
}
|
|
350
|
-
const GRANT_KEYS = /* @__PURE__ */ new Set([
|
|
351
|
-
"permission",
|
|
352
|
-
"effect",
|
|
353
|
-
"level"
|
|
354
|
-
]);
|
|
355
|
-
/** `where` narrowed by a level; the empty level (`{}`) leaves it as is. */
|
|
356
|
-
function andWhere(where, level) {
|
|
357
|
-
if (level.op === "and" && level.conditions.length === 0) return where;
|
|
358
|
-
return where === void 0 ? level : {
|
|
359
|
-
op: "and",
|
|
360
|
-
conditions: [where, level]
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
/** A ceiling grant narrowed by a level's condition and marked with the level. */
|
|
364
|
-
function leveled(grant, level, condition) {
|
|
365
|
-
const where = andWhere(grant.where, condition);
|
|
366
|
-
const check = grant.check === void 0 ? void 0 : andWhere(grant.check, condition);
|
|
367
|
-
return freezeDeep({
|
|
368
|
-
...grant,
|
|
369
|
-
level,
|
|
370
|
-
...where === void 0 ? {} : { where },
|
|
371
|
-
...check === void 0 ? {} : { check }
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* The scope a custom role is held at: `scope` (a name or alias), the second
|
|
376
|
-
* scope for the `team` input shape, else the first scope. `undefined` when
|
|
377
|
-
* the policy does not declare it; such a role resolves to nothing.
|
|
378
|
-
*/
|
|
379
|
-
function customRoleScope(role, scopes) {
|
|
380
|
-
if (role.scope === "global") return "global";
|
|
381
|
-
if (role.scope !== void 0) return resolveScope(scopes, role.scope);
|
|
382
|
-
return resolveScope(scopes, role.team === void 0 ? "tenant" : "team");
|
|
383
|
-
}
|
|
384
|
-
/** The instance of its scope a custom role is pinned to, if any. */
|
|
385
|
-
function customRoleId(role) {
|
|
386
|
-
return role.scope === void 0 ? role.team : role.id;
|
|
387
|
-
}
|
|
388
|
-
/** A platform custom role: held through `principal.roles`, capped by the assignable global roles. */
|
|
389
|
-
function isGlobalCustomRole(role) {
|
|
390
|
-
return role.scope === "global";
|
|
391
|
-
}
|
|
392
|
-
function isCeilingScope(scope) {
|
|
393
|
-
return typeof scope === "string";
|
|
394
|
-
}
|
|
395
|
-
function soleRole(grant) {
|
|
396
|
-
const roles = flattenGrantee(grant.to).filter((item) => item.kind === "role");
|
|
397
|
-
const first = roles[0];
|
|
398
|
-
return roles.length === 1 && first?.kind === "role" ? first.role : void 0;
|
|
399
|
-
}
|
|
400
|
-
/** Permission keys a declared role's allows reach, hosted grants included. */
|
|
401
|
-
function roleAllowKeys(policy, name) {
|
|
402
|
-
return new Set(grantList(policy).filter((grant) => grant.effect === "allow" && soleRole(grant) === name).map((grant) => grant.permission.key));
|
|
403
|
-
}
|
|
404
|
-
function isAssignableRole(policy, name) {
|
|
405
|
-
const binding = policy.rolesByName.get(name);
|
|
406
|
-
if (binding !== void 0) return binding.assignable;
|
|
407
|
-
return findRole(policy.vocabulary?.roles, name)?.assignable === true;
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Every scope's ceiling, keyed by scope name (`global` for platform custom
|
|
411
|
-
* roles): the code allows of declared `assignable` roles in that scope,
|
|
412
|
-
* everything a custom role of the scope may ever reach. Hosted grants never
|
|
413
|
-
* widen it. `assignable` narrows it to those role names (`RoleSource.assignable`).
|
|
414
|
-
*/
|
|
415
|
-
function ceilings(policy, assignable) {
|
|
416
|
-
const allowed = assignable === void 0 ? void 0 : new Set(assignable);
|
|
417
|
-
const byScope = /* @__PURE__ */ new Map();
|
|
418
|
-
for (const grant of grantList(policy)) {
|
|
419
|
-
const name = soleRole(grant);
|
|
420
|
-
if (grant.effect !== "allow" || grant.hosted !== void 0 || !isCeilingScope(grant.scope) || name === void 0 || !isAssignableRole(policy, name) || allowed !== void 0 && !allowed.has(name)) continue;
|
|
421
|
-
const list = byScope.get(grant.scope) ?? [];
|
|
422
|
-
list.push(grant);
|
|
423
|
-
byScope.set(grant.scope, list);
|
|
424
|
-
}
|
|
425
|
-
return byScope;
|
|
426
|
-
}
|
|
427
|
-
function ceilingGrants(policy, scope, assignable) {
|
|
428
|
-
return ceilings(policy, assignable).get(scope) ?? [];
|
|
429
|
-
}
|
|
430
|
-
function retarget(grant, role, scope) {
|
|
431
|
-
const grantee = {
|
|
432
|
-
kind: "role",
|
|
433
|
-
role: role.name,
|
|
434
|
-
scope
|
|
435
|
-
};
|
|
436
|
-
const others = flattenGrantee(grant.to).filter((item) => item.kind !== "role");
|
|
437
|
-
return freezeDeep({
|
|
438
|
-
...grant,
|
|
439
|
-
to: others.length === 0 ? grantee : [grantee, ...others],
|
|
440
|
-
role: role.name,
|
|
441
|
-
scope
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* The one resolver behind evaluation, snapshots and `validateCustomRole`.
|
|
446
|
-
* Included roles' grants plus the role's own allows, minus its denies, all
|
|
447
|
-
* intersected with the ceiling. An allow inherits the condition, approval and
|
|
448
|
-
* limit of the declared grant it comes from: an included assignable role's
|
|
449
|
-
* grant when there is one, otherwise every ceiling grant of that permission,
|
|
450
|
-
* together with the denies of the roles those grants belong to.
|
|
451
|
-
*/
|
|
452
|
-
function resolveCustomRole(policy, role) {
|
|
453
|
-
const scope = customRoleScope(role, scopeList(policy.scopes));
|
|
454
|
-
if (scope === void 0) return freezeDeep({
|
|
455
|
-
grants: [],
|
|
456
|
-
dropped: [],
|
|
457
|
-
renamed: []
|
|
458
|
-
});
|
|
459
|
-
const ceiling = ceilingGrants(policy, scope);
|
|
460
|
-
const ceilingSet = new Set(ceiling);
|
|
461
|
-
const ceilingKeys = new Set(ceiling.map((grant) => grant.permission.key));
|
|
462
|
-
const declared = declaredRoleNames(policy);
|
|
463
|
-
const all = grantList(policy).filter((grant) => grant.hosted === void 0);
|
|
464
|
-
const dropped = [];
|
|
465
|
-
const renamed = [];
|
|
466
|
-
const seen = /* @__PURE__ */ new Set();
|
|
467
|
-
const drop = (entry) => {
|
|
468
|
-
const id = "role" in entry ? `role:${entry.role}` : `${entry.reason}:${entry.permission}`;
|
|
469
|
-
if (!seen.has(id)) {
|
|
470
|
-
seen.add(id);
|
|
471
|
-
dropped.push(entry);
|
|
472
|
-
}
|
|
473
|
-
};
|
|
474
|
-
const included = [];
|
|
475
|
-
const includedKeys = /* @__PURE__ */ new Set();
|
|
476
|
-
const allowKeys = /* @__PURE__ */ new Map();
|
|
477
|
-
const denyKeys = /* @__PURE__ */ new Set();
|
|
478
|
-
const allowAll = (key) => {
|
|
479
|
-
allowKeys.set(key, "all");
|
|
480
|
-
};
|
|
481
|
-
for (const name of Array.isArray(role.includes) ? role.includes : []) {
|
|
482
|
-
if (typeof name !== "string" || !declared.has(name)) {
|
|
483
|
-
drop({
|
|
484
|
-
role: String(name),
|
|
485
|
-
reason: "unknown-role"
|
|
486
|
-
});
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
for (const grant of all) {
|
|
490
|
-
if (soleRole(grant) !== name) continue;
|
|
491
|
-
const key = grant.permission.key;
|
|
492
|
-
if (grant.effect === "deny") {
|
|
493
|
-
if (grant.scope === scope) included.push(grant);
|
|
494
|
-
} else if (ceilingSet.has(grant)) {
|
|
495
|
-
included.push(grant);
|
|
496
|
-
includedKeys.add(key);
|
|
497
|
-
} else if (ceilingKeys.has(key)) allowAll(key);
|
|
498
|
-
else drop({
|
|
499
|
-
permission: key,
|
|
500
|
-
reason: "outside-ceiling"
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
const entries = Array.isArray(role.grants) ? role.grants : [];
|
|
505
|
-
for (const item of entries) {
|
|
506
|
-
if (item === null || typeof item !== "object") {
|
|
507
|
-
drop({
|
|
508
|
-
permission: String(item),
|
|
509
|
-
reason: "unknown-permission"
|
|
510
|
-
});
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
const entry = item;
|
|
514
|
-
const raw = entry.permission;
|
|
515
|
-
const key = typeof raw === "string" ? currentKey(policy, raw) : void 0;
|
|
516
|
-
if (key === void 0) {
|
|
517
|
-
drop({
|
|
518
|
-
permission: String(raw),
|
|
519
|
-
reason: "unknown-permission"
|
|
520
|
-
});
|
|
521
|
-
continue;
|
|
522
|
-
}
|
|
523
|
-
if (key !== raw) renamed.push({
|
|
524
|
-
from: String(raw),
|
|
525
|
-
to: key
|
|
526
|
-
});
|
|
527
|
-
const effect = entry.effect ?? "allow";
|
|
528
|
-
const extra = Object.keys(entry).some((name) => !GRANT_KEYS.has(name));
|
|
529
|
-
const level = entry.level;
|
|
530
|
-
if (extra || effect !== "allow" && effect !== "deny" || effect === "deny" && level !== void 0) {
|
|
531
|
-
drop({
|
|
532
|
-
permission: key,
|
|
533
|
-
reason: "condition-not-allowed"
|
|
534
|
-
});
|
|
535
|
-
denyKeys.add(key);
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
if (effect === "deny") {
|
|
539
|
-
denyKeys.add(key);
|
|
540
|
-
continue;
|
|
541
|
-
}
|
|
542
|
-
let narrowing;
|
|
543
|
-
if (level !== void 0) {
|
|
544
|
-
const leaf = findPermission(policy.permissions, key);
|
|
545
|
-
const condition = typeof level === "string" && leaf?.kind === "instance" ? levelCondition(policy, leaf.resource, level) : void 0;
|
|
546
|
-
if (condition === void 0) {
|
|
547
|
-
drop({
|
|
548
|
-
permission: key,
|
|
549
|
-
reason: "unknown-level",
|
|
550
|
-
level: String(level)
|
|
551
|
-
});
|
|
552
|
-
denyKeys.add(key);
|
|
553
|
-
continue;
|
|
554
|
-
}
|
|
555
|
-
narrowing = [String(level), condition];
|
|
556
|
-
}
|
|
557
|
-
if (!ceilingKeys.has(key)) {
|
|
558
|
-
drop({
|
|
559
|
-
permission: key,
|
|
560
|
-
reason: "outside-ceiling"
|
|
561
|
-
});
|
|
562
|
-
continue;
|
|
563
|
-
}
|
|
564
|
-
const held = allowKeys.get(key);
|
|
565
|
-
if (narrowing === void 0) allowAll(key);
|
|
566
|
-
else if (held === void 0) allowKeys.set(key, new Map([narrowing]));
|
|
567
|
-
else if (held !== "all") held.set(...narrowing);
|
|
568
|
-
}
|
|
569
|
-
const out = [];
|
|
570
|
-
const push = (grant) => {
|
|
571
|
-
if (!out.includes(grant)) out.push(grant);
|
|
572
|
-
};
|
|
573
|
-
for (const grant of included) if (grant.effect === "deny" || !denyKeys.has(grant.permission.key)) push(grant);
|
|
574
|
-
for (const [key, levels] of allowKeys) {
|
|
575
|
-
if (denyKeys.has(key) || includedKeys.has(key)) continue;
|
|
576
|
-
const sources = /* @__PURE__ */ new Set();
|
|
577
|
-
for (const grant of ceiling) {
|
|
578
|
-
if (grant.permission.key !== key) continue;
|
|
579
|
-
sources.add(soleRole(grant) ?? "");
|
|
580
|
-
if (levels === "all") {
|
|
581
|
-
push(grant);
|
|
582
|
-
continue;
|
|
583
|
-
}
|
|
584
|
-
for (const [level, condition] of levels) push(leveled(grant, level, condition));
|
|
585
|
-
}
|
|
586
|
-
for (const grant of all) if (grant.effect === "deny" && grant.permission.key === key && sources.has(soleRole(grant) ?? "")) push(grant);
|
|
587
|
-
}
|
|
588
|
-
return freezeDeep({
|
|
589
|
-
grants: out.map((grant) => retarget(grant, role, scope)),
|
|
590
|
-
dropped,
|
|
591
|
-
renamed
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
function validateCustomRole(policy, role) {
|
|
595
|
-
const resolved = resolveCustomRole(policy, role);
|
|
596
|
-
const permissions = [...new Set(resolved.grants.filter((grant) => grant.effect === "allow").map((grant) => grant.permission.key))].toSorted();
|
|
597
|
-
return freezeDeep({
|
|
598
|
-
ok: resolved.dropped.length === 0,
|
|
599
|
-
permissions,
|
|
600
|
-
dropped: resolved.dropped,
|
|
601
|
-
renamed: resolved.renamed
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
function wellFormed(role) {
|
|
605
|
-
if (role === null || typeof role !== "object" || typeof role.name !== "string") return false;
|
|
606
|
-
if (role.scope === "global") return role.tenant === void 0 && role.team === void 0 && role.id === void 0;
|
|
607
|
-
return typeof role.tenant === "string" && (role.team === void 0 || typeof role.team === "string") && (role.scope === void 0 || typeof role.scope === "string") && (role.id === void 0 || typeof role.id === "string");
|
|
608
|
-
}
|
|
609
|
-
/** Every custom role's resolved grants, once per instance. Malformed roles resolve to nothing. */
|
|
610
|
-
function customGrantsFor(policy, roles) {
|
|
611
|
-
const declared = declaredRoleNames(policy);
|
|
612
|
-
const out = [];
|
|
613
|
-
for (const role of roles) {
|
|
614
|
-
if (!wellFormed(role) || declared.has(role.name)) continue;
|
|
615
|
-
for (const grant of resolveCustomRole(policy, role).grants) out.push({
|
|
616
|
-
grant,
|
|
617
|
-
role
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
return out;
|
|
621
|
-
}
|
|
622
|
-
/** The well-formed tenant (not platform) custom roles; a declared role name is never one. */
|
|
623
|
-
function tenantCustomRoles(policy, roles) {
|
|
624
|
-
const declared = declaredRoleNames(policy);
|
|
625
|
-
return roles.filter((role) => wellFormed(role) && !isGlobalCustomRole(role) && !declared.has(role.name));
|
|
626
|
-
}
|
|
627
|
-
/** A membership at the role's scope, inside its tenant (and instance, when pinned), naming it. */
|
|
628
|
-
function holdsCustomRole(membership, role, scopes) {
|
|
629
|
-
if (isGlobalCustomRole(role)) return false;
|
|
630
|
-
const scope = customRoleScope(role, scopes);
|
|
631
|
-
const id = customRoleId(role);
|
|
632
|
-
return scope !== void 0 && membership.scope === scope && sameId(tenantOf(membership, scopes), role.tenant) && (id === void 0 || sameId(membership.id, id)) && membership.roles.includes(role.name);
|
|
633
|
-
}
|
|
634
|
-
/** Whether `principal.roles` names the platform custom role. */
|
|
635
|
-
function holdsGlobalCustomRole(roles, role) {
|
|
636
|
-
return isGlobalCustomRole(role) && (roles ?? []).includes(role.name);
|
|
637
|
-
}
|
|
638
|
-
/** A tenant's custom role inside that tenant, or a platform custom role anywhere. */
|
|
639
|
-
function isCustomRoleName(name, roles, tenant) {
|
|
640
|
-
return roles.some((item) => wellFormed(item) && item.name === name && (isGlobalCustomRole(item) || tenant !== void 0 && sameId(item.tenant, tenant)));
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* The compact JWT form of custom roles for the `memberships[].grants` claim
|
|
644
|
-
* RLS reads in `jwt` mode: role name to entries, where `key` allows, `-key`
|
|
645
|
-
* denies and `@role` includes. Put a team custom role on its team membership.
|
|
646
|
-
* Pass `policy` to write keys stored under a former name as the current key.
|
|
647
|
-
*/
|
|
648
|
-
function customRoleClaim(roles, policy) {
|
|
649
|
-
const keyOf = (raw) => policy === void 0 ? raw : currentKey(policy, raw) ?? raw;
|
|
650
|
-
const claim = {};
|
|
651
|
-
for (const role of roles) {
|
|
652
|
-
if (!wellFormed(role) || isForbiddenKey(role.name)) continue;
|
|
653
|
-
const entries = claim[role.name] ?? [];
|
|
654
|
-
const includes = Array.isArray(role.includes) ? role.includes : [];
|
|
655
|
-
const grants = Array.isArray(role.grants) ? role.grants : [];
|
|
656
|
-
for (const name of includes) entries.push(`@${name}`);
|
|
657
|
-
for (const grant of grants) {
|
|
658
|
-
if (grant.effect === "deny") {
|
|
659
|
-
entries.push(`-${keyOf(grant.permission)}`);
|
|
660
|
-
continue;
|
|
661
|
-
}
|
|
662
|
-
const level = grant.level;
|
|
663
|
-
entries.push(typeof level === "string" && level !== "" ? `${keyOf(grant.permission)}@${level}` : keyOf(grant.permission));
|
|
664
|
-
}
|
|
665
|
-
claim[role.name] = entries;
|
|
666
|
-
}
|
|
667
|
-
return freezeDeep(claim);
|
|
668
|
-
}
|
|
669
|
-
/**
|
|
670
|
-
* The `memberships` claim RLS reads in `jwt` mode: each membership canonical,
|
|
671
|
-
* with the custom roles it holds as the compact `grants` map.
|
|
672
|
-
*/
|
|
673
|
-
function membershipsClaim(memberships, customRoles, scopes) {
|
|
674
|
-
const claim = [];
|
|
675
|
-
for (const membership of normalizeMemberships(memberships, scopes)) {
|
|
676
|
-
const held = customRoles.filter((role) => wellFormed(role) && holdsCustomRole(membership, role, scopes));
|
|
677
|
-
claim.push(held.length === 0 ? membership : Object.assign({}, membership, { grants: customRoleClaim(held) }));
|
|
678
|
-
}
|
|
679
|
-
return claim;
|
|
680
|
-
}
|
|
681
|
-
//#endregion
|
|
682
|
-
//#region src/core/elevated.ts
|
|
683
|
-
function flattenPermissions(input) {
|
|
684
|
-
if (input !== null && typeof input === "object" && "key" in input && "action" in input) return [input];
|
|
685
|
-
if (isReadonlyArray(input)) return input.flatMap((item) => flattenPermissions(item));
|
|
686
|
-
return [...listPermissions(input)];
|
|
687
|
-
}
|
|
688
|
-
/**
|
|
689
|
-
* The only deny override: a break-glass grant overrides deny grants whose
|
|
690
|
-
* `name` it lists, and nothing else. It is an allow that stays non-portable
|
|
691
|
-
* (RLS never compiles it) and grants only once its `requires` are met.
|
|
692
|
-
*/
|
|
693
|
-
function breakGlass(permission, options = {}) {
|
|
694
|
-
const purpose = options.requires?.purpose === void 0 || options.requires.purpose.length === 0 ? void 0 : Object.freeze([...new Set(options.requires.purpose)]);
|
|
695
|
-
const spec = compact({
|
|
696
|
-
overrides: Object.freeze([...new Set(options.overrides ?? [])]),
|
|
697
|
-
purpose,
|
|
698
|
-
reason: options.requires?.reason === true,
|
|
699
|
-
assurance: normalizeAssurance(options.requires?.assurance),
|
|
700
|
-
maxDuration: options.maxDuration,
|
|
701
|
-
obligations: Object.freeze([...new Set(options.obligations ?? [])])
|
|
702
|
-
});
|
|
703
|
-
const grants = flattenPermissions(permission).map((leaf) => freezeDeep(compact({
|
|
704
|
-
permission: leaf,
|
|
705
|
-
effect: "allow",
|
|
706
|
-
to: authenticated(),
|
|
707
|
-
portable: false,
|
|
708
|
-
breakGlass: spec
|
|
709
|
-
})));
|
|
710
|
-
return sole(grants) ?? grants;
|
|
711
|
-
}
|
|
712
|
-
function supportAccess(options) {
|
|
713
|
-
const via = options.role;
|
|
714
|
-
const forbidden = (options.forbid ?? []).flatMap((item) => flattenPermissions(item));
|
|
715
|
-
const binding = role(options.role, [], {
|
|
716
|
-
on: options.on ?? "tenant",
|
|
717
|
-
for: [via]
|
|
718
|
-
});
|
|
719
|
-
const denies = forbidden.map((leaf) => freezeDeep(compact({
|
|
720
|
-
permission: leaf,
|
|
721
|
-
effect: "deny",
|
|
722
|
-
to: authenticated(),
|
|
723
|
-
role: options.role,
|
|
724
|
-
scope: "global",
|
|
725
|
-
viaOnly: via,
|
|
726
|
-
portable: false
|
|
727
|
-
})));
|
|
728
|
-
const support = freezeDeep(compact({
|
|
729
|
-
role: options.role,
|
|
730
|
-
actorRequired: options.actorRequired === true,
|
|
731
|
-
consent: {
|
|
732
|
-
by: asGrantee(options.consent.by),
|
|
733
|
-
durations: Object.freeze([...new Set(options.consent.durations)])
|
|
734
|
-
},
|
|
735
|
-
group: options.group ?? "vendor-support"
|
|
736
|
-
}));
|
|
737
|
-
return freezeDeep({
|
|
738
|
-
...binding,
|
|
739
|
-
grants: [...binding.grants, ...denies],
|
|
740
|
-
support
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
/** Whether the subject's authentication is fresh enough for an elevated grant. */
|
|
744
|
-
function assuranceMet(requirement, subject, now) {
|
|
745
|
-
if (requirement === void 0) return true;
|
|
746
|
-
const held = subject.principal?.assurance;
|
|
747
|
-
if (requirement.acr !== void 0 && requirement.acr.length > 0) {
|
|
748
|
-
const acr = held?.acr;
|
|
749
|
-
if (acr === void 0 || !requirement.acr.includes(acr)) return false;
|
|
750
|
-
}
|
|
751
|
-
if (requirement.amr !== void 0 && requirement.amr.length > 0) {
|
|
752
|
-
const amr = held?.amr ?? [];
|
|
753
|
-
if (!requirement.amr.every((method) => amr.includes(method))) return false;
|
|
754
|
-
}
|
|
755
|
-
if (requirement.maxAge !== void 0) {
|
|
756
|
-
const authTime = held?.authTime;
|
|
757
|
-
if (authTime === void 0 || now - authTime > requirement.maxAge) return false;
|
|
758
|
-
}
|
|
759
|
-
return true;
|
|
760
|
-
}
|
|
761
|
-
/** `context.purpose` as a list of strings; fail-closed to empty. */
|
|
762
|
-
function purposesOf(subject) {
|
|
763
|
-
const value = subject.context["purpose"];
|
|
764
|
-
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" && value !== "" ? [value] : [];
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* Whether a break-glass grant fires for this subject. It engages when the
|
|
768
|
-
* caller asserts a purpose (`context.purpose`); once engaged, an unlisted
|
|
769
|
-
* purpose, a missing reason or stale authentication denies with the matching
|
|
770
|
-
* reason, and everything met grants with the declared obligations.
|
|
771
|
-
*/
|
|
772
|
-
function evaluateBreakGlass(spec, subject, now) {
|
|
773
|
-
const purposes = purposesOf(subject);
|
|
774
|
-
if (purposes.length === 0) return { kind: "inactive" };
|
|
775
|
-
const required = spec.purpose;
|
|
776
|
-
if (required !== void 0 && !purposes.some((purpose) => required.includes(purpose))) return {
|
|
777
|
-
kind: "denied",
|
|
778
|
-
reason: "purpose"
|
|
779
|
-
};
|
|
780
|
-
const reason = subject.context["reason"];
|
|
781
|
-
if (spec.reason && (typeof reason !== "string" || reason === "")) return {
|
|
782
|
-
kind: "denied",
|
|
783
|
-
reason: "reason-required"
|
|
784
|
-
};
|
|
785
|
-
if (spec.assurance !== void 0 && !assuranceMet(spec.assurance, subject, now)) return {
|
|
786
|
-
kind: "denied",
|
|
787
|
-
reason: "insufficient-user-authentication",
|
|
788
|
-
to: assurance(spec.assurance)
|
|
789
|
-
};
|
|
790
|
-
return {
|
|
791
|
-
kind: "granted",
|
|
792
|
-
obligations: [...spec.obligations.map((kind) => ({ kind })), ...typeof reason === "string" && reason !== "" ? [{
|
|
793
|
-
kind: "justify",
|
|
794
|
-
reason
|
|
795
|
-
}] : []]
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
/** Support memberships (`via: 'support'`) the subject holds that demand an actor. */
|
|
799
|
-
function actorRequiredVias(supports) {
|
|
800
|
-
return new Set(supports.filter((spec) => spec.actorRequired).map((spec) => spec.role));
|
|
801
|
-
}
|
|
802
|
-
/** Whether a membership is a support membership of one of the given kinds. */
|
|
803
|
-
function isSupportMembership(membership, vias) {
|
|
804
|
-
return membership.via !== void 0 && vias.has(membership.via);
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* Whether the caller's `within` names the same ancestors as the eligible
|
|
808
|
-
* membership. Omitted means "whatever the membership holds"; any entry that
|
|
809
|
-
* disagrees, or names an ancestor the membership lacks, refuses.
|
|
810
|
-
*/
|
|
811
|
-
function withinAgrees(scopes, held, requested) {
|
|
812
|
-
if (requested === void 0) return true;
|
|
813
|
-
return Object.entries(requested).every(([key, id]) => {
|
|
814
|
-
const name = resolveScope(scopes, key) ?? key;
|
|
815
|
-
return held !== void 0 && Object.hasOwn(held, name) && held[name] === id;
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
function activationDenied(reason, to) {
|
|
819
|
-
return freezeDeep({
|
|
820
|
-
outcome: "denied",
|
|
821
|
-
denials: [to === void 0 ? {
|
|
822
|
-
role: null,
|
|
823
|
-
reason
|
|
824
|
-
} : {
|
|
825
|
-
role: null,
|
|
826
|
-
reason,
|
|
827
|
-
to
|
|
828
|
-
}],
|
|
829
|
-
alternatives: []
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
/**
|
|
833
|
-
* Requests a just-in-time role activation. It never writes: a granted
|
|
834
|
-
* decision carries the elevated membership under `elevation` for the app to
|
|
835
|
-
* write. `approval-required` comes back first when the role's `activation`
|
|
836
|
-
* sets `approval`. Fail-closed: an unknown role, an ineligible subject, a
|
|
837
|
-
* missing justification or stale authentication all deny.
|
|
838
|
-
*/
|
|
839
|
-
function activate(policy, subject, input, now = nowSeconds()) {
|
|
840
|
-
const roleName = isRole(input.role) ? input.role.key : input.role;
|
|
841
|
-
const binding = policy.rolesByName.get(roleName);
|
|
842
|
-
const activation = binding?.activation;
|
|
843
|
-
const principal = subject.principal;
|
|
844
|
-
if (principal === null) return activationDenied("anonymous");
|
|
845
|
-
if (binding === void 0 || activation === void 0) return activationDenied("unknown-role");
|
|
846
|
-
const scopes = scopeList(policy.scopes);
|
|
847
|
-
const scope = resolveScope(scopes, input.scope) ?? input.scope;
|
|
848
|
-
const eligible = (principal.memberships ?? []).find((membership) => membership.scope === scope && sameId(membership.id, input.id) && !isMembershipExpired(membership, now) && (membership.eligible ?? []).includes(roleName) && withinAgrees(scopes, membership.within, input.within));
|
|
849
|
-
if (eligible === void 0) return activationDenied("no-membership");
|
|
850
|
-
const within = eligible.within;
|
|
851
|
-
if (activation.justification === "required") {
|
|
852
|
-
const reason = input.reason;
|
|
853
|
-
if (typeof reason !== "string" || reason === "") return activationDenied("reason-required");
|
|
854
|
-
}
|
|
855
|
-
if (activation.assurance !== void 0 && !assuranceMet(activation.assurance, subject, now)) return activationDenied("insufficient-user-authentication", assurance(activation.assurance));
|
|
856
|
-
const requested = parseDuration(input.duration);
|
|
857
|
-
const cap = parseDuration(activation.maxDuration);
|
|
858
|
-
const seconds = cap === void 0 ? requested : Math.min(requested ?? cap, cap);
|
|
859
|
-
const membership = freezeDeep(compact({
|
|
860
|
-
scope,
|
|
861
|
-
id: input.id,
|
|
862
|
-
within,
|
|
863
|
-
roles: [roleName],
|
|
864
|
-
via: "elevated",
|
|
865
|
-
expiresAt: seconds === void 0 ? void 0 : Math.floor(now) + seconds,
|
|
866
|
-
grantedBy: principal.id,
|
|
867
|
-
reason: input.reason
|
|
868
|
-
}));
|
|
869
|
-
const token = decisionToken({
|
|
870
|
-
key: `activate:${roleName}`,
|
|
871
|
-
resourceId: within === void 0 ? `${scope}:${input.id}` : `${scope}:${input.id}@${canonicalJson(within)}`,
|
|
872
|
-
principal,
|
|
873
|
-
actor: subject.actor,
|
|
874
|
-
fingerprint: policy.fingerprint
|
|
875
|
-
});
|
|
876
|
-
if (activation.approval !== void 0) return freezeDeep({
|
|
877
|
-
outcome: "approval-required",
|
|
878
|
-
grant: {
|
|
879
|
-
role: roleName,
|
|
880
|
-
permission: `activate:${roleName}`
|
|
881
|
-
},
|
|
882
|
-
reason: "human",
|
|
883
|
-
token
|
|
884
|
-
});
|
|
885
|
-
return freezeDeep({
|
|
886
|
-
outcome: "granted",
|
|
887
|
-
subject,
|
|
888
|
-
matched: {
|
|
889
|
-
role: roleName,
|
|
890
|
-
permission: `activate:${roleName}`
|
|
891
|
-
},
|
|
892
|
-
token,
|
|
893
|
-
elevation: membership
|
|
894
|
-
});
|
|
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
|
|
954
|
-
//#region src/core/events.ts
|
|
955
|
-
function emitSafe(listeners, payload, errors) {
|
|
956
|
-
for (const listener of listeners) try {
|
|
957
|
-
listener(payload);
|
|
958
|
-
} catch (error) {
|
|
959
|
-
for (const handler of errors.error) try {
|
|
960
|
-
handler(error);
|
|
961
|
-
} catch {}
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
function emptyListeners() {
|
|
965
|
-
return {
|
|
966
|
-
decision: /* @__PURE__ */ new Set(),
|
|
967
|
-
denied: /* @__PURE__ */ new Set(),
|
|
968
|
-
approval: /* @__PURE__ */ new Set(),
|
|
969
|
-
auth: /* @__PURE__ */ new Set(),
|
|
970
|
-
error: /* @__PURE__ */ new Set()
|
|
971
|
-
};
|
|
972
|
-
}
|
|
973
|
-
function credentialRef(subject) {
|
|
974
|
-
const value = subject.principal?.["credential"];
|
|
975
|
-
if (value === null || typeof value !== "object") return;
|
|
976
|
-
const { id, kind } = value;
|
|
977
|
-
return typeof id === "string" && (kind === "user" || kind === "service") ? {
|
|
978
|
-
id,
|
|
979
|
-
kind
|
|
980
|
-
} : void 0;
|
|
981
|
-
}
|
|
982
|
-
function finish(policy, subject, permission, data, decision, options, env, trusted, membership, counts) {
|
|
983
|
-
const heard = env.sink !== void 0 || env.listeners.decision.size > 0 || decision.outcome === "denied" && env.listeners.denied.size > 0 || decision.outcome === "approval-required" && env.listeners.approval.size > 0;
|
|
984
|
-
if (!env.emit || !heard) return;
|
|
985
|
-
const resource = getResource(policy.permissions, permission.resource);
|
|
986
|
-
const resourceId = data !== null && typeof data === "object" ? data[resource?.id ?? "id"] : void 0;
|
|
987
|
-
const event = freezeDeep(compact({
|
|
988
|
-
type: "decision",
|
|
989
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
990
|
-
outcome: decision.outcome,
|
|
991
|
-
permission: permission.key,
|
|
992
|
-
scope: permission.scope,
|
|
993
|
-
resource: compact({
|
|
994
|
-
type: permission.resource,
|
|
995
|
-
id: resourceId === void 0 ? void 0 : String(resourceId)
|
|
996
|
-
}),
|
|
997
|
-
subject: compact({
|
|
998
|
-
principal: subject.principal === null ? null : compact({
|
|
999
|
-
id: subject.principal.id,
|
|
1000
|
-
roles: subject.principal.roles ?? [],
|
|
1001
|
-
tenant: subject.principal.tenant
|
|
1002
|
-
}),
|
|
1003
|
-
actor: subject.actor === void 0 ? void 0 : {
|
|
1004
|
-
id: subject.actor.id,
|
|
1005
|
-
kind: subject.actor.kind
|
|
1006
|
-
},
|
|
1007
|
-
delegation: subject.delegation === void 0 ? void 0 : compact({
|
|
1008
|
-
scopes: subject.delegation.scopes,
|
|
1009
|
-
authorizationDetails: subject.delegation.authorizationDetails
|
|
1010
|
-
}),
|
|
1011
|
-
credential: credentialRef(subject)
|
|
1012
|
-
}),
|
|
1013
|
-
tenant: subject.principal?.tenant,
|
|
1014
|
-
membership,
|
|
1015
|
-
via: membership?.via ?? null,
|
|
1016
|
-
matched: decision.outcome === "granted" ? compact({
|
|
1017
|
-
role: decision.matched.role,
|
|
1018
|
-
permission: decision.matched.permission,
|
|
1019
|
-
name: decision.matched.name,
|
|
1020
|
-
meta: decision.matched.meta,
|
|
1021
|
-
to: decision.matched.to,
|
|
1022
|
-
hosted: decision.matched.hosted,
|
|
1023
|
-
breakGlass: decision.matched.breakGlass
|
|
1024
|
-
}) : decision.outcome === "approval-required" ? compact({
|
|
1025
|
-
role: decision.grant.role,
|
|
1026
|
-
permission: decision.grant.permission,
|
|
1027
|
-
name: decision.grant.name,
|
|
1028
|
-
meta: decision.grant.meta,
|
|
1029
|
-
to: decision.grant.to,
|
|
1030
|
-
hosted: decision.grant.hosted
|
|
1031
|
-
}) : void 0,
|
|
1032
|
-
obligations: decision.outcome === "granted" && (decision.obligations?.length ?? 0) > 0 ? decision.obligations : void 0,
|
|
1033
|
-
purpose: Array.isArray(subject.context["purpose"]) && subject.context["purpose"].length > 0 ? subject.context["purpose"] : void 0,
|
|
1034
|
-
reason: typeof subject.context["reason"] === "string" && subject.context["reason"] !== "" ? subject.context["reason"] : void 0,
|
|
1035
|
-
denials: decision.outcome === "denied" ? wireDenials(decision.denials) : void 0,
|
|
1036
|
-
alternatives: decision.outcome === "denied" ? decision.alternatives.map((leaf) => leaf.key) : void 0,
|
|
1037
|
-
token: decision.outcome === "granted" || decision.outcome === "approval-required" ? decision.token : void 0,
|
|
1038
|
-
trusted,
|
|
1039
|
-
source: options.source ?? "decide",
|
|
1040
|
-
adapter: options.adapter,
|
|
1041
|
-
counts
|
|
1042
|
-
}));
|
|
1043
|
-
emitSafe(env.listeners.decision, event, env.listeners);
|
|
1044
|
-
if (decision.outcome === "denied") emitSafe(env.listeners.denied, event, env.listeners);
|
|
1045
|
-
if (decision.outcome === "approval-required") emitSafe(env.listeners.approval, event, env.listeners);
|
|
1046
|
-
if (env.sink !== void 0) try {
|
|
1047
|
-
const written = env.sink.write([event]);
|
|
1048
|
-
if (isThenable(written)) written.catch((error) => {
|
|
1049
|
-
emitSafe(env.listeners.error, error, env.listeners);
|
|
1050
|
-
});
|
|
1051
|
-
} catch (error) {
|
|
1052
|
-
emitSafe(env.listeners.error, error, env.listeners);
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
//#endregion
|
|
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
|
|
1080
|
-
});
|
|
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
|
-
};
|
|
1093
|
-
}
|
|
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
|
-
};
|
|
1116
|
-
}
|
|
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 };
|
|
1138
|
-
}
|
|
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
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
|
-
//#endregion
|
|
1181
|
-
//#region src/core/role-loading.ts
|
|
1182
|
-
function isGlobalRole(value) {
|
|
1183
|
-
return value !== null && typeof value === "object" && Reflect.get(value, "scope") === "global";
|
|
1184
|
-
}
|
|
1185
|
-
function onlyGlobal(roles) {
|
|
1186
|
-
return Array.isArray(roles) ? roles.filter(isGlobalRole) : [];
|
|
1187
|
-
}
|
|
1188
|
-
/** The `customRoles` option as a source: a factory is called with the resolved subject, and a throw reads none. */
|
|
1189
|
-
function roleSourceFor(option, subject, auth) {
|
|
1190
|
-
if (typeof option !== "function") return option;
|
|
1191
|
-
try {
|
|
1192
|
-
return option(subject);
|
|
1193
|
-
} catch {
|
|
1194
|
-
auth.push({
|
|
1195
|
-
reason: "source-threw",
|
|
1196
|
-
source: "customRoles"
|
|
1197
|
-
});
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
function customRolesFor(source, tenants, auth, globalHeld, heldIn) {
|
|
1202
|
-
if (source === void 0) return [];
|
|
1203
|
-
const loaded = [];
|
|
1204
|
-
for (const tenant of tenants) try {
|
|
1205
|
-
loaded.push(source.rolesFor(tenant, { held: heldIn(tenant) }));
|
|
1206
|
-
} catch {
|
|
1207
|
-
auth.push({
|
|
1208
|
-
reason: "source-threw",
|
|
1209
|
-
source: "customRoles"
|
|
1210
|
-
});
|
|
1211
|
-
loaded.push([]);
|
|
1212
|
-
}
|
|
1213
|
-
if (globalHeld !== void 0 && source.globalRoles !== void 0) try {
|
|
1214
|
-
const roles = source.globalRoles({ held: globalHeld });
|
|
1215
|
-
loaded.push(isThenable(roles) ? roles.then(onlyGlobal) : onlyGlobal(roles));
|
|
1216
|
-
} catch {
|
|
1217
|
-
auth.push({
|
|
1218
|
-
reason: "source-threw",
|
|
1219
|
-
source: "customRoles"
|
|
1220
|
-
});
|
|
1221
|
-
loaded.push([]);
|
|
1222
|
-
}
|
|
1223
|
-
if (loaded.some((item) => isThenable(item))) return Promise.all(loaded.map((item) => Promise.resolve(item).catch(() => {
|
|
1224
|
-
auth.push({
|
|
1225
|
-
reason: "source-threw",
|
|
1226
|
-
source: "customRoles"
|
|
1227
|
-
});
|
|
1228
|
-
return [];
|
|
1229
|
-
}))).then((lists) => lists.flat());
|
|
1230
|
-
return loaded.flat();
|
|
1231
|
-
}
|
|
1232
|
-
function stringList(value) {
|
|
1233
|
-
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1234
|
-
}
|
|
1235
|
-
/**
|
|
1236
|
-
* `RoleSource.assignable` per tenant; `undefined` when the source has no
|
|
1237
|
-
* `assignable`. A tenant whose call throws or answers junk assigns nothing.
|
|
1238
|
-
*/
|
|
1239
|
-
function assignableNamesFor(source, tenants, auth) {
|
|
1240
|
-
if (source?.assignable === void 0) return;
|
|
1241
|
-
const failed = () => {
|
|
1242
|
-
auth.push({
|
|
1243
|
-
reason: "source-threw",
|
|
1244
|
-
source: "customRoles"
|
|
1245
|
-
});
|
|
1246
|
-
return [];
|
|
1247
|
-
};
|
|
1248
|
-
const loaded = tenants.map((tenant) => {
|
|
1249
|
-
try {
|
|
1250
|
-
return source.assignable?.(tenant) ?? [];
|
|
1251
|
-
} catch {
|
|
1252
|
-
return failed();
|
|
1253
|
-
}
|
|
1254
|
-
});
|
|
1255
|
-
const toMap = (lists) => new Map(tenants.map((tenant, index) => [tenant, stringList(lists[index])]));
|
|
1256
|
-
if (loaded.some((item) => isThenable(item))) return Promise.all(loaded.map((item) => Promise.resolve(item).catch(failed))).then(toMap);
|
|
1257
|
-
return toMap(loaded);
|
|
1258
|
-
}
|
|
1259
|
-
/**
|
|
1260
|
-
* The declared role names among `names`. A custom role of `tenant` is known
|
|
1261
|
-
* but contributes no declared name: its grants are resolved separately
|
|
1262
|
-
* (`customGrantsFor`), bounded by the ceiling. A tenant's custom role is
|
|
1263
|
-
* unknown inside another tenant and for a membership without one.
|
|
1264
|
-
*/
|
|
1265
|
-
function expandRoleNames(names, declared, custom, tenant) {
|
|
1266
|
-
const resolved = /* @__PURE__ */ new Set();
|
|
1267
|
-
const unknown = [];
|
|
1268
|
-
for (const name of names) if (declared.has(name)) resolved.add(name);
|
|
1269
|
-
else if (!isCustomRoleName(name, custom, tenant)) unknown.push(name);
|
|
1270
|
-
return {
|
|
1271
|
-
roles: [...resolved],
|
|
1272
|
-
unknown
|
|
1273
|
-
};
|
|
1274
|
-
}
|
|
1275
|
-
//#endregion
|
|
1276
|
-
//#region src/core/requires-conditions.ts
|
|
1277
|
-
const NEVER = freezeDeep({
|
|
1278
|
-
op: "or",
|
|
1279
|
-
conditions: []
|
|
1280
|
-
});
|
|
1281
|
-
function isUnconditionedAllow(grant, now) {
|
|
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);
|
|
1283
|
-
}
|
|
1284
|
-
/**
|
|
1285
|
-
* The row condition a grant's `requires` adds: the row's instance of some
|
|
1286
|
-
* scope is one where the subject holds `key` through a role grant without a
|
|
1287
|
-
* row condition, minus the instances a deny of `key` reaches at that scope.
|
|
1288
|
-
* `undefined` when the subject holds `key` globally that way, so every row
|
|
1289
|
-
* passes. The in-process mirror of the SQL `permdock_has_permission` and
|
|
1290
|
-
* `permitted_<scope>_ids_by_permission`.
|
|
1291
|
-
*/
|
|
1292
|
-
function requirementCondition(policy, subject, keys, resource, now, customRoles, customGrants) {
|
|
1293
|
-
let where;
|
|
1294
|
-
for (const key of keys) where = combineWhere(where, keyRequirement(policy, subject, key, resource, now, customRoles, customGrants));
|
|
1295
|
-
return where;
|
|
1296
|
-
}
|
|
1297
|
-
function keyRequirement(policy, subject, key, resource, now, customRoles, customGrants) {
|
|
1298
|
-
const principal = subject.principal;
|
|
1299
|
-
if (principal === null) return NEVER;
|
|
1300
|
-
const scopes = scopeList(policy.scopes);
|
|
1301
|
-
const declared = declaredRoleNames(policy);
|
|
1302
|
-
const global = new Set(expandRoleNames(principal.roles ?? [], declared, customRoles, principal.tenant).roles);
|
|
1303
|
-
const held = (principal.memberships ?? []).filter((membership) => !isMembershipExpired(membership, now) && keepsPermission(membership, key)).map((membership) => ({
|
|
1304
|
-
membership,
|
|
1305
|
-
roles: new Set(expandRoleNames(membership.roles, declared, customRoles, tenantOf(membership, scopes)).roles)
|
|
1306
|
-
}));
|
|
1307
|
-
const allowed = /* @__PURE__ */ new Map();
|
|
1308
|
-
const denied = /* @__PURE__ */ new Map();
|
|
1309
|
-
let globalAllow = false;
|
|
1310
|
-
let globalDeny = false;
|
|
1311
|
-
const note = (effect, membership) => {
|
|
1312
|
-
if (membership === void 0) {
|
|
1313
|
-
if (effect === "allow") globalAllow = true;
|
|
1314
|
-
else globalDeny = true;
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
if (membership.scope === void 0 || membership.id === void 0) return;
|
|
1318
|
-
const target = effect === "allow" ? allowed : denied;
|
|
1319
|
-
const ids = target.get(membership.scope) ?? /* @__PURE__ */ new Set();
|
|
1320
|
-
ids.add(membership.id);
|
|
1321
|
-
target.set(membership.scope, ids);
|
|
1322
|
-
};
|
|
1323
|
-
const applies = (grant) => grant.effect === "allow" ? isUnconditionedAllow(grant, now) : isActive(grant.validity, now);
|
|
1324
|
-
for (const grant of policy.index.grantsByKey.get(key) ?? []) {
|
|
1325
|
-
const items = flattenGrantee(grant.to);
|
|
1326
|
-
if (grant.breakGlass !== void 0 || !applies(grant) || items.length === 0 || !items.every((item) => item.kind === "role")) continue;
|
|
1327
|
-
const roles = items.flatMap((item) => item.kind === "role" ? [item] : []);
|
|
1328
|
-
if (!roles.every((item) => item.scope !== "global" || global.has(item.role))) continue;
|
|
1329
|
-
const scoped = roles.filter((item) => item.scope !== "global");
|
|
1330
|
-
if (scoped.length === 0) {
|
|
1331
|
-
note(grant.effect);
|
|
1332
|
-
continue;
|
|
1333
|
-
}
|
|
1334
|
-
for (const entry of held) if (scoped.every((item) => typeof item.scope === "string" && entry.membership.scope === item.scope && entry.roles.has(item.role))) note(grant.effect, entry.membership);
|
|
1335
|
-
}
|
|
1336
|
-
for (const { grant, role } of customGrants) {
|
|
1337
|
-
if (grant.permission.key !== key || !applies(grant)) continue;
|
|
1338
|
-
if (holdsGlobalCustomRole(principal.roles, role)) note(grant.effect);
|
|
1339
|
-
for (const entry of held) if (holdsCustomRole(entry.membership, role, scopes)) note(grant.effect, entry.membership);
|
|
1340
|
-
}
|
|
1341
|
-
if (globalAllow && !globalDeny) return;
|
|
1342
|
-
const parts = [];
|
|
1343
|
-
for (const name of resource === void 0 ? [] : partitionsOf(resource, scopes)) {
|
|
1344
|
-
const field = scopeField(resource, name, scopes);
|
|
1345
|
-
const deniedIds = denied.get(name);
|
|
1346
|
-
const ids = [...allowed.get(name) ?? []].filter((id) => deniedIds?.has(id) !== true).toSorted();
|
|
1347
|
-
if (field !== void 0 && ids.length > 0) parts.push({
|
|
1348
|
-
op: "in",
|
|
1349
|
-
field,
|
|
1350
|
-
value: ids
|
|
1351
|
-
});
|
|
1352
|
-
}
|
|
1353
|
-
return sole(parts) ?? {
|
|
1354
|
-
op: "or",
|
|
1355
|
-
conditions: parts
|
|
1356
|
-
};
|
|
1357
|
-
}
|
|
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}.`
|
|
1369
|
-
});
|
|
1370
|
-
return data;
|
|
1371
|
-
}
|
|
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
|
-
});
|
|
1382
|
-
}
|
|
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;
|
|
1392
|
-
}
|
|
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("; ")}.`;
|
|
1399
|
-
}
|
|
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;
|
|
1411
|
-
}
|
|
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)
|
|
1421
|
-
});
|
|
1422
|
-
finish(call.policy, call.given, permission, call.current, final, call.options, env, call.options.trusted === true, membership);
|
|
1423
|
-
return final;
|
|
1424
|
-
}
|
|
1425
|
-
function deniedNow(call, denial) {
|
|
1426
|
-
return complete(call, {
|
|
1427
|
-
outcome: "denied",
|
|
1428
|
-
denials: [denial],
|
|
1429
|
-
alternatives: []
|
|
1430
|
-
});
|
|
1431
|
-
}
|
|
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
|
-
});
|
|
1438
|
-
}
|
|
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;
|
|
1445
|
-
try {
|
|
1446
|
-
if (permission.kind === "instance" && isRowPair(data)) {
|
|
1447
|
-
current = check(data.current);
|
|
1448
|
-
return {
|
|
1449
|
-
current,
|
|
1450
|
-
next: check(data.next)
|
|
1451
|
-
};
|
|
1452
|
-
}
|
|
1453
|
-
const validated = check(data);
|
|
1454
|
-
return permission.kind === "instance" ? {
|
|
1455
|
-
current: validated,
|
|
1456
|
-
next: validated
|
|
1457
|
-
} : {
|
|
1458
|
-
current,
|
|
1459
|
-
next: validated
|
|
1460
|
-
};
|
|
1461
|
-
} catch (error) {
|
|
1462
|
-
if (error instanceof PermDockValidationError && error.code === "invalid-data") return {
|
|
1463
|
-
current,
|
|
1464
|
-
next: rows.next,
|
|
1465
|
-
invalid: error
|
|
1466
|
-
};
|
|
1467
|
-
throw error;
|
|
1468
|
-
}
|
|
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, {
|
|
1474
|
-
reason: "unknown-role",
|
|
1475
|
-
source: "roles"
|
|
1476
|
-
}, env.listeners);
|
|
1477
|
-
const matching = new Set(global.roles);
|
|
1478
|
-
const denials = [];
|
|
1479
|
-
for (const membership of subject.principal?.memberships ?? []) {
|
|
1480
|
-
if (!inTeam(membership, scopes, env.team)) continue;
|
|
1481
|
-
const expanded = expandRoleNames(membership.roles, declared, env.customRoles, tenantOf(membership, scopes));
|
|
1482
|
-
for (const name of expanded.roles) matching.add(name);
|
|
1483
|
-
for (const name of expanded.unknown) denials.push({
|
|
1484
|
-
role: name,
|
|
1485
|
-
reason: "unknown-role"
|
|
1486
|
-
});
|
|
1487
|
-
}
|
|
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({
|
|
1504
|
-
grant: item.grant,
|
|
1505
|
-
custom: item.role
|
|
1506
|
-
});
|
|
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) ?? []) {
|
|
1514
|
-
if (grant.breakGlass === void 0) continue;
|
|
1515
|
-
if (tracer !== void 0) tracer.evaluated += 1;
|
|
1516
|
-
if (!matchGrantee(grant.to, subject, now, call.resource, scopes).matched) {
|
|
1517
|
-
skip(tracer, grant, permission.key, "grantee");
|
|
1518
|
-
continue;
|
|
1519
|
-
}
|
|
1520
|
-
const result = evaluateBreakGlass(grant.breakGlass, subject, now);
|
|
1521
|
-
if (result.kind === "inactive") {
|
|
1522
|
-
skip(tracer, grant, permission.key, "break-glass-inactive");
|
|
1523
|
-
continue;
|
|
1524
|
-
}
|
|
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" ? {
|
|
1533
|
-
role: null,
|
|
1534
|
-
reason: result.reason,
|
|
1535
|
-
to: result.to
|
|
1536
|
-
} : {
|
|
1537
|
-
role: null,
|
|
1538
|
-
reason: result.reason
|
|
1539
|
-
};
|
|
1540
|
-
}
|
|
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) => {
|
|
1552
|
-
const on = membership.on;
|
|
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 };
|
|
1584
|
-
}
|
|
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
|
|
1594
|
-
});
|
|
1595
|
-
return { held: false };
|
|
1596
|
-
}
|
|
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
|
|
1646
|
-
});
|
|
1647
|
-
else skip(tracer, grant, permission.key, "validity");
|
|
1648
|
-
return;
|
|
1649
|
-
}
|
|
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"
|
|
1662
|
-
});
|
|
1663
|
-
}
|
|
1664
|
-
if (!condition.matched) {
|
|
1665
|
-
state.denials.push({
|
|
1666
|
-
role: displayRole,
|
|
1667
|
-
reason: condition.reason ?? "condition",
|
|
1668
|
-
detail: condition.cause
|
|
1669
|
-
});
|
|
1670
|
-
return;
|
|
1671
|
-
}
|
|
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
|
|
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
|
-
}]);
|
|
1717
|
-
const unscoped = subject.actor !== void 0 && ceiling === void 0;
|
|
1718
|
-
let delegationMiss = coveredByDelegation(permission, subject.delegation, resourceIdOf(current), unscoped);
|
|
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));
|
|
1721
|
-
if (delegationMiss === "not-delegated" && usable.length > 0) delegationMiss = void 0;
|
|
1722
|
-
if (delegationMiss === void 0 && usable.length === 0) delegationMiss = "not-delegated";
|
|
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
|
|
1739
|
-
});
|
|
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 = [];
|
|
1745
|
-
for (const candidate of usable) {
|
|
1746
|
-
const approval = approvalOf(state, candidate);
|
|
1747
|
-
const quota = applyQuota({
|
|
1748
|
-
store: env.limits,
|
|
1749
|
-
cache: env.limitCache,
|
|
1750
|
-
grant: candidate.grant,
|
|
1751
|
-
permissionKey: state.permission.key,
|
|
1752
|
-
subjectId: subject.principal?.id ?? "",
|
|
1753
|
-
tenant: subject.principal?.tenant,
|
|
1754
|
-
now: state.now,
|
|
1755
|
-
consume: !requiresApproval(approval) && shouldConsumeQuota(state.options.source, env.simulated)
|
|
1756
|
-
});
|
|
1757
|
-
if (quota.ok) return {
|
|
1758
|
-
allow: candidate,
|
|
1759
|
-
approval,
|
|
1760
|
-
quota: compact({
|
|
1761
|
-
quota: quota.quota,
|
|
1762
|
-
obligations: quota.obligations
|
|
1763
|
-
})
|
|
1764
|
-
};
|
|
1765
|
-
denials.push(quota.reason === "limit" ? {
|
|
1766
|
-
role: candidate.grant.role,
|
|
1767
|
-
reason: "limit",
|
|
1768
|
-
detail: quota.detail
|
|
1769
|
-
} : {
|
|
1770
|
-
role: candidate.grant.role,
|
|
1771
|
-
reason: quota.reason
|
|
1772
|
-
});
|
|
1773
|
-
}
|
|
1774
|
-
return { denials };
|
|
1775
|
-
}
|
|
1776
|
-
function allowedDecision(state, allow, approval, quotaState) {
|
|
1777
|
-
const { policy, subject, permission, resource, env, current, next } = state;
|
|
1778
|
-
const resourceId = permission.kind === "collection" ? "*" : rowIdOf(current, resource?.id);
|
|
1779
|
-
const version = approval !== void 0 && approval !== "human" && approval.staleOn === "resource-change" && resource?.version !== void 0 ? versionOf(current, resource.version) : void 0;
|
|
1780
|
-
const token = env.simulated ? "pd1.simulated" : env.outcomeOnly === true ? "pd1.outcome" : decisionToken({
|
|
1781
|
-
key: permission.key,
|
|
1782
|
-
resourceId,
|
|
1783
|
-
principal: subject.principal,
|
|
1784
|
-
actor: subject.actor,
|
|
1785
|
-
fingerprint: policy.fingerprint,
|
|
1786
|
-
version,
|
|
1787
|
-
payload: resourceId === "*" && (next ?? current) !== void 0 ? payloadDigest(next ?? current) : void 0
|
|
1788
|
-
});
|
|
1789
|
-
const matched = compact({
|
|
1790
|
-
...matchedOf(allow.grant, permission.key, allow.breakGlass),
|
|
1791
|
-
approval
|
|
1792
|
-
});
|
|
1793
|
-
const obligations = [
|
|
1794
|
-
...quotaState.obligations ?? [],
|
|
1795
|
-
...allow.obligations ?? [],
|
|
1796
|
-
...allow.grant.obligations ?? []
|
|
1797
|
-
];
|
|
1798
|
-
return complete(state, requiresApproval(approval) ? {
|
|
1799
|
-
outcome: "approval-required",
|
|
1800
|
-
grant: matched,
|
|
1801
|
-
reason: "human",
|
|
1802
|
-
token
|
|
1803
|
-
} : {
|
|
1804
|
-
outcome: "granted",
|
|
1805
|
-
subject,
|
|
1806
|
-
matched,
|
|
1807
|
-
token,
|
|
1808
|
-
...quotaState,
|
|
1809
|
-
...obligations.length === 0 ? {} : { obligations }
|
|
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);
|
|
1895
|
-
}
|
|
1896
|
-
//#endregion
|
|
1897
|
-
//#region src/core/ownership.ts
|
|
1898
|
-
function bindingOf(policy, name) {
|
|
1899
|
-
return policy.rolesByName.get(name);
|
|
1900
|
-
}
|
|
1901
|
-
function roleMeta(policy, name) {
|
|
1902
|
-
return bindingOf(policy, name)?.meta ?? findRole(policy.vocabulary?.roles, name)?.meta;
|
|
1903
|
-
}
|
|
1904
|
-
/** A role with `for` is held only through a membership of one of those kinds. */
|
|
1905
|
-
function mayHoldVia(policy, role, via) {
|
|
1906
|
-
const kinds = bindingOf(policy, role)?.for;
|
|
1907
|
-
return kinds === void 0 || via !== void 0 && kinds.includes(via);
|
|
1908
|
-
}
|
|
1909
|
-
function sameList(a, b) {
|
|
1910
|
-
return a.length === b.length && a.every((item, index) => item === b[index]);
|
|
1911
|
-
}
|
|
1912
|
-
/**
|
|
1913
|
-
* The identity provider may only hand out what the application could: a
|
|
1914
|
-
* declared role with `assignable: false` (an owner, a platform role) never
|
|
1915
|
-
* rides in on a `managedBy: 'idp'` membership. A name the policy does not
|
|
1916
|
-
* declare stays, because only a tenant's custom role can give it meaning.
|
|
1917
|
-
*/
|
|
1918
|
-
function mayHoldManaged(policy, role, membership) {
|
|
1919
|
-
return !isExternallyManaged(membership) || bindingOf(policy, role)?.assignable !== false;
|
|
1920
|
-
}
|
|
1921
|
-
/**
|
|
1922
|
-
* Drops every role a membership's kind may not hold (fail-closed: a missing
|
|
1923
|
-
* `via` is no kind) and every non-assignable role on an identity-provider
|
|
1924
|
-
* membership. Global roles have no kind, so a role with `for` held globally
|
|
1925
|
-
* is dropped too.
|
|
1926
|
-
*/
|
|
1927
|
-
function applyRoleKinds(policy, roles, memberships) {
|
|
1928
|
-
const kinds = policy.roles.some((binding) => binding.for !== void 0);
|
|
1929
|
-
if (!kinds && !memberships.some(isExternallyManaged)) return {
|
|
1930
|
-
roles,
|
|
1931
|
-
memberships
|
|
1932
|
-
};
|
|
1933
|
-
return {
|
|
1934
|
-
roles: kinds ? roles?.filter((name) => mayHoldVia(policy, name, void 0)) ?? roles : roles,
|
|
1935
|
-
memberships: memberships.map((membership) => {
|
|
1936
|
-
const allowed = membership.roles.filter((name) => mayHoldVia(policy, name, membership.via) && mayHoldManaged(policy, name, membership));
|
|
1937
|
-
return sameList(allowed, membership.roles) ? membership : freezeDeep({
|
|
1938
|
-
...membership,
|
|
1939
|
-
roles: allowed
|
|
1940
|
-
});
|
|
1941
|
-
})
|
|
1942
|
-
};
|
|
1943
|
-
}
|
|
1944
|
-
function usesAssigns(policy) {
|
|
1945
|
-
return policy.roles.some((binding) => binding.assigns !== void 0);
|
|
1946
|
-
}
|
|
1947
|
-
/** Declared role names in declaration order: bindings first, then vocabulary-only leaves. */
|
|
1948
|
-
function declarationOrder(policy) {
|
|
1949
|
-
const names = policy.roles.map((binding) => binding.name);
|
|
1950
|
-
const seen = new Set(names);
|
|
1951
|
-
for (const leaf of listRoles(policy.vocabulary?.roles)) if (!seen.has(leaf.key)) {
|
|
1952
|
-
names.push(leaf.key);
|
|
1953
|
-
seen.add(leaf.key);
|
|
1954
|
-
}
|
|
1955
|
-
return names;
|
|
1956
|
-
}
|
|
1957
|
-
/**
|
|
1958
|
-
* Each role's depth in the `assigns` graph: a role nobody else assigns is 0,
|
|
1959
|
-
* a role another assigns sits one below its deepest assigner. Cycles stop
|
|
1960
|
-
* after one pass per role.
|
|
1961
|
-
*/
|
|
1962
|
-
function assignDepths(policy) {
|
|
1963
|
-
const names = declarationOrder(policy);
|
|
1964
|
-
const depth = new Map(names.map((name) => [name, 0]));
|
|
1965
|
-
for (let pass = 0; pass < names.length; pass += 1) {
|
|
1966
|
-
let changed = false;
|
|
1967
|
-
for (const binding of policy.roles) {
|
|
1968
|
-
const from = depth.get(binding.name) ?? 0;
|
|
1969
|
-
for (const target of binding.assigns ?? []) if (target !== binding.name && (depth.get(target) ?? 0) < from + 1) {
|
|
1970
|
-
depth.set(target, from + 1);
|
|
1971
|
-
changed = true;
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
if (!changed) break;
|
|
1975
|
-
}
|
|
1976
|
-
return depth;
|
|
1977
|
-
}
|
|
1978
|
-
/**
|
|
1979
|
-
* `names` ordered by rank: assigners before the roles they assign, then
|
|
1980
|
-
* declaration order. Without an `assigns` graph the order is kept.
|
|
1981
|
-
*/
|
|
1982
|
-
function rankRoles(policy, names) {
|
|
1983
|
-
if (!usesAssigns(policy)) return names;
|
|
1984
|
-
const order = declarationOrder(policy);
|
|
1985
|
-
const depth = assignDepths(policy);
|
|
1986
|
-
const index = (name) => {
|
|
1987
|
-
const found = order.indexOf(name);
|
|
1988
|
-
return found === -1 ? order.length : found;
|
|
1989
|
-
};
|
|
1990
|
-
return [...names].toSorted((a, b) => (depth.get(a) ?? Number.MAX_SAFE_INTEGER) - (depth.get(b) ?? Number.MAX_SAFE_INTEGER) || index(a) - index(b));
|
|
1991
|
-
}
|
|
1992
|
-
/** The distinct `meta.audience` values of `names`, in rank order. */
|
|
1993
|
-
function audiencesOf(policy, names) {
|
|
1994
|
-
const out = [];
|
|
1995
|
-
for (const name of rankRoles(policy, names)) {
|
|
1996
|
-
const audience = roleMeta(policy, name)?.audience;
|
|
1997
|
-
if (typeof audience === "string" && !out.includes(audience)) out.push(audience);
|
|
1998
|
-
}
|
|
1999
|
-
return out;
|
|
2000
|
-
}
|
|
2001
|
-
/** Whether `membership` is the instance itself or one of its ancestors named in `within`. */
|
|
2002
|
-
function coversInstance(membership, scopes, scope, id, within) {
|
|
2003
|
-
if (membership.scope === void 0 || membership.id === void 0) return false;
|
|
2004
|
-
if (membership.scope === scope) return sameId(membership.id, id);
|
|
2005
|
-
if (!scopeChain(scopes, scope).slice(1).includes(membership.scope)) return false;
|
|
2006
|
-
const wanted = within === void 0 ? void 0 : Object.entries(within).find(([key]) => resolveScope(scopes, key) === membership.scope)?.[1] ?? void 0;
|
|
2007
|
-
return wanted !== void 0 && scopeIdOf(membership, membership.scope) === wanted;
|
|
2008
|
-
}
|
|
2009
|
-
/** Roles the principal holds at the instance, at an ancestor named in `within`, or globally. */
|
|
2010
|
-
function rolesAtInstance(policy, principal, scopes, scope, id, within, now) {
|
|
2011
|
-
const declared = declaredRoleNames(policy);
|
|
2012
|
-
const names = new Set((principal.roles ?? []).filter((name) => declared.has(name)));
|
|
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);
|
|
2014
|
-
return rankRoles(policy, [...names]);
|
|
2015
|
-
}
|
|
2016
|
-
/** Whether the principal holds `role` on a live membership of exactly this instance. */
|
|
2017
|
-
function holdsAt(principal, role, scope, id, now) {
|
|
2018
|
-
return (principal.memberships ?? []).some((membership) => membership.scope === scope && sameId(membership.id, id) && !isMembershipExpired(membership, now) && membership.roles.includes(role));
|
|
2019
|
-
}
|
|
2020
|
-
function includedRoles(role) {
|
|
2021
|
-
return isReadonlyArray(role.includes) ? role.includes.filter((name) => typeof name === "string") : [];
|
|
2022
|
-
}
|
|
2023
|
-
/**
|
|
2024
|
-
* The membership kinds a custom role may be held through: those every
|
|
2025
|
-
* included declared role with `for` allows. `undefined` when none restricts.
|
|
2026
|
-
*/
|
|
2027
|
-
function customForKinds(policy, role) {
|
|
2028
|
-
let kinds;
|
|
2029
|
-
for (const name of includedRoles(role)) {
|
|
2030
|
-
const listed = bindingOf(policy, name)?.for;
|
|
2031
|
-
if (listed !== void 0) kinds = kinds === void 0 ? listed : kinds.filter((kind) => listed.includes(kind));
|
|
2032
|
-
}
|
|
2033
|
-
return kinds;
|
|
2034
|
-
}
|
|
2035
|
-
function conflictsWith(policy, role) {
|
|
2036
|
-
const out = new Set(bindingOf(policy, role)?.exclusiveWith ?? []);
|
|
2037
|
-
for (const binding of policy.roles) if (binding.exclusiveWith?.includes(role) === true) out.add(binding.name);
|
|
2038
|
-
return out;
|
|
2039
|
-
}
|
|
2040
|
-
/** What the change does to the instance's holder count. */
|
|
2041
|
-
function countRules(binding, role, holders, delta, deny) {
|
|
2042
|
-
if (binding === void 0 || delta === 0) return;
|
|
2043
|
-
const min = binding.min ?? 0;
|
|
2044
|
-
if (!(min > 0 || binding.max !== void 0 || binding.transferOnly === true)) return;
|
|
2045
|
-
if (!(holders !== void 0 && Number.isInteger(holders) && holders >= 0)) {
|
|
2046
|
-
deny(delta < 0 && min > 0 ? "last-holder" : delta > 0 && binding.max !== void 0 ? "max-holders" : "transfer-only", {
|
|
2047
|
-
role,
|
|
2048
|
-
holders: null
|
|
2049
|
-
});
|
|
2050
|
-
return;
|
|
2051
|
-
}
|
|
2052
|
-
const after = holders + delta;
|
|
2053
|
-
if (delta < 0 && min > 0 && after < min) deny("last-holder", {
|
|
2054
|
-
role,
|
|
2055
|
-
min,
|
|
2056
|
-
holders
|
|
2057
|
-
});
|
|
2058
|
-
if (delta > 0 && binding.max !== void 0 && after > binding.max) deny("max-holders", {
|
|
2059
|
-
role,
|
|
2060
|
-
max: binding.max,
|
|
2061
|
-
holders
|
|
2062
|
-
});
|
|
2063
|
-
if (binding.transferOnly === true && holders > 0 && after > 0) deny("transfer-only", {
|
|
2064
|
-
role,
|
|
2065
|
-
holders
|
|
2066
|
-
});
|
|
2067
|
-
}
|
|
2068
|
-
/**
|
|
2069
|
-
* A live membership of the principal on the instance or on an instance under
|
|
2070
|
-
* it: the one in-band fact that ties a nested instance to a tenant.
|
|
2071
|
-
*/
|
|
2072
|
-
function heldAnchor(principal, scopes, scope, id, now) {
|
|
2073
|
-
return (principal.memberships ?? []).find((membership) => !isMembershipExpired(membership, now) && membership.scope !== void 0 && scopeChain(scopes, membership.scope).includes(scope) && scopeIdOf(membership, scope) === id && tenantOf(membership, scopes) !== void 0);
|
|
2074
|
-
}
|
|
2075
|
-
/**
|
|
2076
|
-
* The one rule check behind `permdock.decideRoleChange`. The actor is always
|
|
2077
|
-
* the instance's own principal; nothing in `change` can stand in for it.
|
|
2078
|
-
*/
|
|
2079
|
-
function decideRoleChange(policy, principal, scopes, change, customRoles, authority, now, options = {}) {
|
|
2080
|
-
const name = typeof change?.role === "string" ? change.role : change?.role?.key;
|
|
2081
|
-
const denials = [];
|
|
2082
|
-
const deny = (reason, detail) => {
|
|
2083
|
-
denials.push(compact({
|
|
2084
|
-
role: name ?? null,
|
|
2085
|
-
reason,
|
|
2086
|
-
detail
|
|
2087
|
-
}));
|
|
2088
|
-
};
|
|
2089
|
-
const done = (role) => freezeDeep(denials.length === 0 ? {
|
|
2090
|
-
outcome: "granted",
|
|
2091
|
-
change,
|
|
2092
|
-
role
|
|
2093
|
-
} : {
|
|
2094
|
-
outcome: "denied",
|
|
2095
|
-
change,
|
|
2096
|
-
denials
|
|
2097
|
-
});
|
|
2098
|
-
if (principal === null) {
|
|
2099
|
-
deny("anonymous");
|
|
2100
|
-
return done(null);
|
|
2101
|
-
}
|
|
2102
|
-
const kind = change?.kind;
|
|
2103
|
-
const target = change?.target;
|
|
2104
|
-
const global = change?.scope === "global";
|
|
2105
|
-
if (typeof name !== "string" || kind !== "assign" && kind !== "revoke" && kind !== "transfer" || (global ? change.id !== void 0 : typeof change.id !== "string" || change.id === "") || target === null || typeof target !== "object" || typeof target.id !== "string" || target.id === "") {
|
|
2106
|
-
deny("validation");
|
|
2107
|
-
return done(null);
|
|
2108
|
-
}
|
|
2109
|
-
const id = change.id ?? "";
|
|
2110
|
-
const binding = bindingOf(policy, name);
|
|
2111
|
-
const declared = declaredRoleNames(policy).has(name);
|
|
2112
|
-
const named = declared || global ? [] : tenantCustomRoles(policy, customRoles).filter((role) => role.name === name);
|
|
2113
|
-
if (!declared && named.length === 0) {
|
|
2114
|
-
deny("unknown-role");
|
|
2115
|
-
return done(null);
|
|
2116
|
-
}
|
|
2117
|
-
const scope = global ? "global" : resolveScope(scopes, change.scope);
|
|
2118
|
-
const customAt = named.map((role) => customRoleScope(role, scopes));
|
|
2119
|
-
const on = declared ? binding?.on ?? findRole(policy.vocabulary?.roles, name)?.on : customAt.includes(scope) ? scope : customAt[0];
|
|
2120
|
-
const heldAt = typeof on === "string" ? resolveScope(scopes, on) : global ? "global" : void 0;
|
|
2121
|
-
if (scope === void 0 || heldAt === void 0 || heldAt !== scope) {
|
|
2122
|
-
deny("scope", { expected: heldAt ?? null });
|
|
2123
|
-
return done(null);
|
|
2124
|
-
}
|
|
2125
|
-
let tenant;
|
|
2126
|
-
let within = change.within;
|
|
2127
|
-
let custom;
|
|
2128
|
-
if (!global) {
|
|
2129
|
-
const root = scopes[0]?.name;
|
|
2130
|
-
const supplied = change.within === void 0 ? void 0 : Object.entries(change.within).find(([key]) => resolveScope(scopes, key) === root)?.[1];
|
|
2131
|
-
if (scope === root) tenant = id;
|
|
2132
|
-
else {
|
|
2133
|
-
const anchor = heldAnchor(principal, scopes, scope, id, now);
|
|
2134
|
-
const held = anchor === void 0 ? void 0 : tenantOf(anchor, scopes);
|
|
2135
|
-
if (held !== void 0) {
|
|
2136
|
-
tenant = supplied === void 0 || supplied === held ? held : void 0;
|
|
2137
|
-
within = anchor?.within;
|
|
2138
|
-
} else if (options.trusted === true) tenant = supplied;
|
|
2139
|
-
}
|
|
2140
|
-
if (tenant === void 0) {
|
|
2141
|
-
deny("no-membership", {
|
|
2142
|
-
scope,
|
|
2143
|
-
id
|
|
2144
|
-
});
|
|
2145
|
-
return done(null);
|
|
2146
|
-
}
|
|
2147
|
-
const found = tenant;
|
|
2148
|
-
custom = named.find((role) => sameId(role.tenant, found) && customRoleScope(role, scopes) === scope && sameId(customRoleId(role) ?? id, id));
|
|
2149
|
-
}
|
|
2150
|
-
if (!declared && custom === void 0) {
|
|
2151
|
-
deny("unknown-role");
|
|
2152
|
-
return done(null);
|
|
2153
|
-
}
|
|
2154
|
-
if (target.managedBy === "idp") {
|
|
2155
|
-
deny("externally-managed");
|
|
2156
|
-
return done(null);
|
|
2157
|
-
}
|
|
2158
|
-
const self = target.id === principal.id;
|
|
2159
|
-
const targetRoles = isReadonlyArray(target.roles) ? target.roles : [];
|
|
2160
|
-
const already = targetRoles.includes(name);
|
|
2161
|
-
let by = null;
|
|
2162
|
-
if (kind === "transfer") {
|
|
2163
|
-
if (self) deny("self-demotion");
|
|
2164
|
-
if (global ? (principal.roles ?? []).includes(name) : holdsAt(principal, name, scope, id, now)) by = name;
|
|
2165
|
-
else deny("not-assignable-by", { holds: false });
|
|
2166
|
-
} else {
|
|
2167
|
-
if (self) deny(kind === "revoke" ? "self-demotion" : "not-assignable-by", { self: true });
|
|
2168
|
-
const allowed = authority(tenant);
|
|
2169
|
-
const revocable = kind === "revoke" && custom !== void 0 && allowed.inert?.has(name);
|
|
2170
|
-
if (!allowed.assignable.has(name) && revocable !== true) deny("not-assignable-by");
|
|
2171
|
-
else if (custom === void 0 && usesAssigns(policy) && !allowed.manage) {
|
|
2172
|
-
const held = global ? rankRoles(policy, (principal.roles ?? []).filter((role) => declaredRoleNames(policy).has(role))) : rolesAtInstance(policy, principal, scopes, scope, id, within, now);
|
|
2173
|
-
by = held.find((role) => bindingOf(policy, role)?.assigns?.includes(name) === true) ?? null;
|
|
2174
|
-
if (by === null) deny("not-assignable-by", { held });
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2177
|
-
if (kind !== "revoke" && !already) {
|
|
2178
|
-
const kinds = custom === void 0 ? binding?.for : customForKinds(policy, custom);
|
|
2179
|
-
if (kinds !== void 0 && (target.via === void 0 || !kinds.includes(target.via))) deny("not-allowed-for-membership", {
|
|
2180
|
-
via: target.via ?? null,
|
|
2181
|
-
for: kinds
|
|
2182
|
-
});
|
|
2183
|
-
const conflicts = custom === void 0 ? conflictsWith(policy, name) : new Set(includedRoles(custom).flatMap((include) => Array.from(conflictsWith(policy, include))));
|
|
2184
|
-
const clash = targetRoles.filter((role) => conflicts.has(role));
|
|
2185
|
-
if (clash.length > 0) deny("conflicting-role", { with: clash });
|
|
2186
|
-
}
|
|
2187
|
-
const delta = kind === "assign" ? already ? 0 : 1 : kind === "revoke" ? targetRoles.length > 0 && !already ? 0 : -1 : already ? -1 : 0;
|
|
2188
|
-
countRules(binding, name, change.holders, delta, deny);
|
|
2189
|
-
return done(by);
|
|
2190
|
-
}
|
|
2191
|
-
//#endregion
|
|
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);
|
|
2208
|
-
}
|
|
2209
|
-
return rankRoles(policy, [...names]);
|
|
2210
|
-
}
|
|
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
|
|
2217
|
-
}));
|
|
2218
|
-
}
|
|
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
|
-
});
|
|
2229
|
-
}
|
|
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(".");
|
|
2239
|
-
});
|
|
2240
|
-
}
|
|
2241
|
-
/**
|
|
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.
|
|
2245
|
-
*/
|
|
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
|
-
}));
|
|
2256
|
-
}
|
|
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 });
|
|
2296
|
-
continue;
|
|
2297
|
-
}
|
|
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
|
-
});
|
|
2302
|
-
}
|
|
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
|
-
});
|
|
2316
|
-
}
|
|
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;
|
|
2320
|
-
}
|
|
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));
|
|
2331
|
-
}
|
|
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
|
-
});
|
|
2345
|
-
}
|
|
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: []
|
|
2360
|
-
};
|
|
2361
|
-
const scopes = scopeList(policy.scopes);
|
|
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 = {
|
|
2373
|
-
emit: false,
|
|
2374
|
-
simulated: true,
|
|
2375
|
-
skipAlternatives: true,
|
|
2376
|
-
customRoles,
|
|
2377
|
-
customGrants,
|
|
2378
|
-
listeners: emptyListeners(),
|
|
2379
|
-
sink: void 0,
|
|
2380
|
-
limits: void 0,
|
|
2381
|
-
limitCache: /* @__PURE__ */ new Map(),
|
|
2382
|
-
team: void 0
|
|
2383
|
-
};
|
|
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));
|
|
2398
|
-
});
|
|
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);
|
|
2411
|
-
}
|
|
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);
|
|
2457
|
-
continue;
|
|
2458
|
-
}
|
|
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
|
|
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;
|
|
2548
|
-
}
|
|
2549
|
-
throw new PermDockDeniedError({
|
|
2550
|
-
decision,
|
|
2551
|
-
permission: permission.key,
|
|
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 }
|
|
2557
|
-
});
|
|
2558
|
-
}
|
|
2559
|
-
//#endregion
|
|
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: []
|
|
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);
|
|
2588
|
-
}
|
|
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
|
|
2603
|
-
});
|
|
2604
|
-
}
|
|
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);
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
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
|
|
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;
|
|
2666
|
-
}
|
|
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);
|
|
2685
|
-
}
|
|
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
|
-
});
|
|
2697
|
-
}
|
|
2698
|
-
/**
|
|
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.
|
|
2703
|
-
*/
|
|
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);
|
|
2728
|
-
continue;
|
|
2729
|
-
}
|
|
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);
|
|
2745
|
-
}
|
|
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);
|
|
2778
|
-
}
|
|
2779
|
-
}
|
|
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);
|
|
2814
|
-
}
|
|
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
|
-
}
|
|
2830
|
-
}
|
|
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;
|
|
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
|
-
});
|
|
2866
|
-
const scopes = scopeList(policy.scopes);
|
|
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 = {
|
|
2896
|
-
emit: false,
|
|
2897
|
-
simulated: true,
|
|
2898
|
-
skipAlternatives: true,
|
|
2899
|
-
customRoles: [],
|
|
2900
|
-
customGrants: [],
|
|
2901
|
-
listeners: emptyListeners(),
|
|
2902
|
-
sink: void 0,
|
|
2903
|
-
limits: void 0,
|
|
2904
|
-
limitCache: /* @__PURE__ */ new Map(),
|
|
2905
|
-
team: input.team,
|
|
2906
|
-
relations: input.reader
|
|
2907
|
-
};
|
|
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);
|
|
2925
|
-
});
|
|
2926
|
-
let decisions = decideAll();
|
|
2927
|
-
if (pendingRelations(input.cache).length > 0) {
|
|
2928
|
-
await settle$1(input.cache);
|
|
2929
|
-
decisions = decideAll();
|
|
2930
|
-
}
|
|
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;
|
|
2936
|
-
continue;
|
|
2937
|
-
}
|
|
2938
|
-
holders.push({
|
|
2939
|
-
principal: { id },
|
|
2940
|
-
via: entry.via
|
|
2941
|
-
});
|
|
2942
|
-
}
|
|
2943
|
-
return freezeDeep({
|
|
2944
|
-
permission: permission.key,
|
|
2945
|
-
holders,
|
|
2946
|
-
complete
|
|
2947
|
-
});
|
|
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;
|
|
2953
|
-
function buildInstance(policy, subject, envBase, team) {
|
|
2954
|
-
const listeners = emptyListeners();
|
|
2955
|
-
const queuedAuth = [...envBase.queuedAuth];
|
|
2956
|
-
const queuedErrors = [...envBase.queuedErrors ?? []];
|
|
2957
|
-
const customGrants = customGrantsFor(policy, envBase.customRoles);
|
|
2958
|
-
const relationCache = envBase.relationCache ?? /* @__PURE__ */ new Map();
|
|
2959
|
-
const relations = relationReader(envBase.relations, relationCache, policy.resources);
|
|
2960
|
-
const assignableAt = (tenant) => assignableIn(policy, subject, envBase.customRoles, customGrants, tenant, tenant === void 0 ? void 0 : envBase.assignable?.get(tenant));
|
|
2961
|
-
const assignableGlobal = () => assignableIn(policy, subject, envBase.customRoles, customGrants, void 0, void 0, nowSeconds(), true);
|
|
2962
|
-
const envFor = (emit, skipAlternatives = false) => ({
|
|
2963
|
-
emit,
|
|
2964
|
-
simulated: envBase.simulated,
|
|
2965
|
-
skipAlternatives,
|
|
2966
|
-
customRoles: envBase.customRoles,
|
|
2967
|
-
customGrants,
|
|
2968
|
-
listeners,
|
|
2969
|
-
sink: envBase.sink,
|
|
2970
|
-
limits: envBase.limits,
|
|
2971
|
-
limitCache: envBase.limitCache,
|
|
2972
|
-
team,
|
|
2973
|
-
relations,
|
|
2974
|
-
...envBase.approvalPolicies === void 0 ? {} : { approvalPolicies: envBase.approvalPolicies }
|
|
2975
|
-
});
|
|
2976
|
-
const decideImpl = (permission, data, options) => evaluate(policy, subject, permission, data, options ?? {}, envFor(options?.source !== "simulate", options?.source === "can"));
|
|
2977
|
-
const explainImpl = (permission, data, options) => {
|
|
2978
|
-
return decideImpl(permission, data, {
|
|
2979
|
-
...options,
|
|
2980
|
-
source: options?.source ?? "explain",
|
|
2981
|
-
explain: true
|
|
2982
|
-
});
|
|
2983
|
-
};
|
|
2984
|
-
const canImpl = (permission, data, options) => {
|
|
2985
|
-
try {
|
|
2986
|
-
return decideImpl(permission, data, {
|
|
2987
|
-
...options,
|
|
2988
|
-
source: options?.source ?? "can"
|
|
2989
|
-
}).outcome === "granted";
|
|
2990
|
-
} catch {
|
|
2991
|
-
return false;
|
|
2992
|
-
}
|
|
2993
|
-
};
|
|
2994
|
-
const assertImpl = (permission, data, options) => {
|
|
2995
|
-
return assertDecision(policy, subject, permission, data, decideImpl(permission, data, {
|
|
2996
|
-
...options,
|
|
2997
|
-
source: options?.source ?? "assert"
|
|
2998
|
-
}), options?.onDenied ?? policy.onDenied);
|
|
2999
|
-
};
|
|
3000
|
-
const instance = {
|
|
3001
|
-
can: canImpl,
|
|
3002
|
-
decide: decideImpl,
|
|
3003
|
-
assert: assertImpl,
|
|
3004
|
-
explain: explainImpl,
|
|
3005
|
-
permissions: policy.permissions,
|
|
3006
|
-
roles: policy.vocabulary?.roles ?? {},
|
|
3007
|
-
plans: policy.vocabulary?.plans ?? {},
|
|
3008
|
-
filter(permission, rows, options) {
|
|
3009
|
-
const allowed = [];
|
|
3010
|
-
let granted = 0;
|
|
3011
|
-
let denied = 0;
|
|
3012
|
-
let approvalRequired = 0;
|
|
3013
|
-
const quiet = {
|
|
3014
|
-
...envFor(false, true),
|
|
3015
|
-
outcomeOnly: true
|
|
3016
|
-
};
|
|
3017
|
-
const trusted = options?.trusted ?? true;
|
|
3018
|
-
const decideOptions = {
|
|
3019
|
-
...options,
|
|
3020
|
-
source: "filter",
|
|
3021
|
-
trusted
|
|
3022
|
-
};
|
|
3023
|
-
for (const row of rows) {
|
|
3024
|
-
const decision = evaluate(policy, subject, permission, row, decideOptions, quiet);
|
|
3025
|
-
if (decision.outcome === "granted") {
|
|
3026
|
-
allowed.push(row);
|
|
3027
|
-
granted += 1;
|
|
3028
|
-
} else if (decision.outcome === "approval-required") approvalRequired += 1;
|
|
3029
|
-
else denied += 1;
|
|
3030
|
-
}
|
|
3031
|
-
const summary = granted > 0 ? freezeDeep({
|
|
3032
|
-
outcome: "granted",
|
|
3033
|
-
subject,
|
|
3034
|
-
matched: {
|
|
3035
|
-
role: "*",
|
|
3036
|
-
permission: permission.key
|
|
3037
|
-
},
|
|
3038
|
-
token: "pd1.filter"
|
|
3039
|
-
}) : freezeDeep({
|
|
3040
|
-
outcome: "denied",
|
|
3041
|
-
permission: permission.key,
|
|
3042
|
-
denials: [{
|
|
3043
|
-
role: null,
|
|
3044
|
-
reason: "no-grant"
|
|
3045
|
-
}],
|
|
3046
|
-
alternatives: []
|
|
3047
|
-
});
|
|
3048
|
-
finish(policy, subject, permission, rows[0], summary, decideOptions, {
|
|
3049
|
-
...quiet,
|
|
3050
|
-
emit: true
|
|
3051
|
-
}, trusted, void 0, {
|
|
3052
|
-
granted,
|
|
3053
|
-
denied,
|
|
3054
|
-
approvalRequired
|
|
3055
|
-
});
|
|
3056
|
-
return allowed;
|
|
3057
|
-
},
|
|
3058
|
-
pick(permission, row, options) {
|
|
3059
|
-
if (row === null || typeof row !== "object") return {};
|
|
3060
|
-
if (canImpl(permission, row, options) !== true) return {};
|
|
3061
|
-
return pickVisible(row, (field) => {
|
|
3062
|
-
const next = compact({
|
|
3063
|
-
...options,
|
|
3064
|
-
field
|
|
3065
|
-
});
|
|
3066
|
-
return canImpl(permission, row, next) === true;
|
|
3067
|
-
});
|
|
3068
|
-
},
|
|
3069
|
-
where(permission) {
|
|
3070
|
-
const grants = collectSnapshotGrants(policy, subject, envBase.customRoles, nowSeconds(), customGrants).filter((item) => item.grant.permission.key === permission.key).map((item) => snapshotGrant(whereGrant(item.grant), item.membership));
|
|
3071
|
-
return whereFromGrants(grants, compact({
|
|
3072
|
-
resource: permission.resource,
|
|
3073
|
-
resources: policy.resources,
|
|
3074
|
-
graph: envBase.relations !== void 0,
|
|
3075
|
-
scopes: scopeList(policy.scopes),
|
|
3076
|
-
partitioned: (name, key) => relatesTo(getResource(policy.permissions, permission.resource), key, name, scopeList(policy.scopes)),
|
|
3077
|
-
fieldOf: (name) => scopeField(getResource(policy.permissions, permission.resource), name, scopeList(policy.scopes)),
|
|
3078
|
-
tenant: subject.principal?.tenant,
|
|
3079
|
-
team,
|
|
3080
|
-
now: nowSeconds(),
|
|
3081
|
-
subject
|
|
3082
|
-
}));
|
|
3083
|
-
},
|
|
3084
|
-
actions(resource, data, options) {
|
|
3085
|
-
const fromTree = listPermissions(resource);
|
|
3086
|
-
return (fromTree.length > 0 ? fromTree : "resource" in resource && typeof resource.resource === "string" ? listPermissions(policy.permissions).filter((item) => item.resource === resource.resource) : []).filter((item) => canImpl(item, data, options) === true);
|
|
3087
|
-
},
|
|
3088
|
-
async loadRelations(permission, rows) {
|
|
3089
|
-
if (envBase.relations === void 0) return;
|
|
3090
|
-
const quiet = {
|
|
3091
|
-
...envFor(false),
|
|
3092
|
-
simulated: true,
|
|
3093
|
-
skipAlternatives: true
|
|
3094
|
-
};
|
|
3095
|
-
for (let round = 0; round < LOAD_ROUNDS; round += 1) {
|
|
3096
|
-
for (const row of rows) evaluate(policy, subject, permission, row, {
|
|
3097
|
-
source: "simulate",
|
|
3098
|
-
trusted: true
|
|
3099
|
-
}, quiet);
|
|
3100
|
-
const pending = pendingRelations(relationCache);
|
|
3101
|
-
if (pending.length === 0) return;
|
|
3102
|
-
await Promise.all(pending);
|
|
3103
|
-
}
|
|
3104
|
-
},
|
|
3105
|
-
whoCan(permission, row) {
|
|
3106
|
-
return whoCan({
|
|
3107
|
-
policy,
|
|
3108
|
-
permission,
|
|
3109
|
-
row,
|
|
3110
|
-
memberships: envBase.memberships,
|
|
3111
|
-
reader: relations,
|
|
3112
|
-
cache: relationCache,
|
|
3113
|
-
customGrants,
|
|
3114
|
-
team
|
|
3115
|
-
});
|
|
3116
|
-
},
|
|
3117
|
-
simulate: ((input, options) => {
|
|
3118
|
-
const evaluated = [];
|
|
3119
|
-
const quietly = (permission, data) => {
|
|
3120
|
-
const decision = evaluate(policy, subject, permission, data, compact({
|
|
3121
|
-
source: "simulate",
|
|
3122
|
-
trusted: true,
|
|
3123
|
-
now: options?.now
|
|
3124
|
-
}), envFor(false));
|
|
3125
|
-
evaluated.push([
|
|
3126
|
-
permission,
|
|
3127
|
-
data,
|
|
3128
|
-
decision
|
|
3129
|
-
]);
|
|
3130
|
-
return decision;
|
|
3131
|
-
};
|
|
3132
|
-
if (isReadonlyArray(input)) {
|
|
3133
|
-
const decisions = input.map(([permission, data]) => quietly(permission, data));
|
|
3134
|
-
simulateEvent(policy, subject, envFor(false), evaluated);
|
|
3135
|
-
return decisions;
|
|
3136
|
-
}
|
|
3137
|
-
if (isArazzoSimulateInput(input)) {
|
|
3138
|
-
const plan = simulateArazzo(input, input.permissions ?? policy.permissions, quietly);
|
|
3139
|
-
simulateEvent(policy, subject, envFor(false), evaluated);
|
|
3140
|
-
return plan;
|
|
3141
|
-
}
|
|
3142
|
-
return buildInstance(policy, previewSubject(policy, subject, input), {
|
|
3143
|
-
...envBase,
|
|
3144
|
-
simulated: true,
|
|
3145
|
-
relationCache
|
|
3146
|
-
}, team);
|
|
3147
|
-
}),
|
|
3148
|
-
snapshot: ((options) => {
|
|
3149
|
-
const snapshot = snapshotOf(policy, subject, compact({
|
|
3150
|
-
customRoles: envBase.customRoles,
|
|
3151
|
-
customGrants,
|
|
3152
|
-
assignable: envBase.assignable,
|
|
3153
|
-
include: options?.include,
|
|
3154
|
-
tenants: options?.tenants,
|
|
3155
|
-
simulated: envBase.simulated
|
|
3156
|
-
}));
|
|
3157
|
-
if (options?.signer !== void 0) return signSnapshot(snapshot, options.signer, options.audience);
|
|
3158
|
-
return snapshot;
|
|
3159
|
-
}),
|
|
3160
|
-
on(event, handler) {
|
|
3161
|
-
const set = listeners[event];
|
|
3162
|
-
set.add(handler);
|
|
3163
|
-
if (event === "auth") for (const queued of queuedAuth) try {
|
|
3164
|
-
handler(queued);
|
|
3165
|
-
} catch (error) {
|
|
3166
|
-
emitSafe(listeners.error, error, listeners);
|
|
3167
|
-
}
|
|
3168
|
-
if (event === "error") for (const queued of queuedErrors) try {
|
|
3169
|
-
handler(queued);
|
|
3170
|
-
} catch {}
|
|
3171
|
-
return () => {
|
|
3172
|
-
set.delete(handler);
|
|
3173
|
-
};
|
|
3174
|
-
},
|
|
3175
|
-
tenant(id) {
|
|
3176
|
-
if (subject.principal === null) return buildInstance(policy, subject, {
|
|
3177
|
-
...envBase,
|
|
3178
|
-
relationCache
|
|
3179
|
-
}, team);
|
|
3180
|
-
return buildInstance(policy, freezeDeep(compact({
|
|
3181
|
-
...subject,
|
|
3182
|
-
principal: compact({
|
|
3183
|
-
...subject.principal,
|
|
3184
|
-
tenant: resolveActiveTenant(subject.principal, id, scopeList(policy.scopes))
|
|
3185
|
-
})
|
|
3186
|
-
})), {
|
|
3187
|
-
...envBase,
|
|
3188
|
-
relationCache
|
|
3189
|
-
}, team);
|
|
3190
|
-
},
|
|
3191
|
-
team(id) {
|
|
3192
|
-
return buildInstance(policy, subject, {
|
|
3193
|
-
...envBase,
|
|
3194
|
-
relationCache
|
|
3195
|
-
}, id);
|
|
3196
|
-
},
|
|
3197
|
-
memberships() {
|
|
3198
|
-
return subject.principal?.memberships ?? [];
|
|
3199
|
-
},
|
|
3200
|
-
tenants() {
|
|
3201
|
-
return tenantsOf(subject.principal, scopeList(policy.scopes));
|
|
3202
|
-
},
|
|
3203
|
-
heldRoles(options) {
|
|
3204
|
-
return heldRoleNames(policy, subject, options?.tenant ?? subject.principal?.tenant, options?.scope === void 0 ? void 0 : compact({
|
|
3205
|
-
scope: options.scope,
|
|
3206
|
-
id: options.id
|
|
3207
|
-
})).map((name) => roleLeaf(policy, name));
|
|
3208
|
-
},
|
|
3209
|
-
audiences() {
|
|
3210
|
-
return audiencesOf(policy, heldRoleNames(policy, subject, subject.principal?.tenant));
|
|
3211
|
-
},
|
|
3212
|
-
assignableRoles(options) {
|
|
3213
|
-
if (options?.scope === "global") return assignableGlobal().roles.filter((leaf) => leaf.on === void 0);
|
|
3214
|
-
return assignableAt(options?.tenant ?? subject.principal?.tenant).roles;
|
|
3215
|
-
},
|
|
3216
|
-
assignablePermissions(options) {
|
|
3217
|
-
if (options?.scope === "global") return assignableGlobal().permissions;
|
|
3218
|
-
const tenant = options?.tenant ?? subject.principal?.tenant;
|
|
3219
|
-
return tenant === void 0 ? [] : assignableAt(tenant).permissions;
|
|
3220
|
-
},
|
|
3221
|
-
assignableLevels(permission, options) {
|
|
3222
|
-
const of = (found) => (Object.hasOwn(found.levels, permission.key) ? found.levels[permission.key] : void 0) ?? [];
|
|
3223
|
-
if (options?.scope === "global") return of(assignableGlobal());
|
|
3224
|
-
const tenant = options?.tenant ?? subject.principal?.tenant;
|
|
3225
|
-
return tenant === void 0 ? [] : of(assignableAt(tenant));
|
|
3226
|
-
},
|
|
3227
|
-
decideRoleChange(change, options) {
|
|
3228
|
-
return decideRoleChange(policy, subject.principal, scopeList(policy.scopes), change, envBase.customRoles, (tenant) => {
|
|
3229
|
-
const found = tenant === void 0 ? assignableGlobal() : assignableAt(tenant);
|
|
3230
|
-
return {
|
|
3231
|
-
assignable: new Set(found.roles.map((leaf) => leaf.key)),
|
|
3232
|
-
manage: found.manage,
|
|
3233
|
-
inert: new Set(found.inert)
|
|
3234
|
-
};
|
|
3235
|
-
}, nowSeconds(), options);
|
|
3236
|
-
},
|
|
3237
|
-
activate(input) {
|
|
3238
|
-
return activate(policy, subject, input, nowSeconds());
|
|
3239
|
-
},
|
|
3240
|
-
derive(options) {
|
|
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);
|
|
3243
|
-
},
|
|
3244
|
-
subject
|
|
3245
|
-
};
|
|
3246
|
-
return Object.freeze(instance);
|
|
3247
|
-
}
|
|
3248
|
-
//#endregion
|
|
3249
|
-
//#region src/core/oauth-scopes.ts
|
|
3250
|
-
/**
|
|
3251
|
-
* `delegation` with every coarse OAuth scope the policy declares in
|
|
3252
|
-
* `oauthScopes` followed by the scopes of the permissions it covers, so the
|
|
3253
|
-
* delegation checks downstream match them. The token's scopes stay first and
|
|
3254
|
-
* unchanged; an undeclared scope expands to nothing.
|
|
3255
|
-
*/
|
|
3256
|
-
function expandOAuthScopes(policy, delegation) {
|
|
3257
|
-
const declared = policy.oauthScopes;
|
|
3258
|
-
const scopes = delegation?.scopes;
|
|
3259
|
-
if (declared === void 0 || delegation === void 0 || scopes === void 0) return delegation;
|
|
3260
|
-
const out = new Set(scopes);
|
|
3261
|
-
for (const entry of declared) if (scopes.includes(entry.scope)) for (const scope of entry.scopes) out.add(scope);
|
|
3262
|
-
return out.size === scopes.length ? delegation : {
|
|
3263
|
-
...delegation,
|
|
3264
|
-
scopes: [...out]
|
|
3265
|
-
};
|
|
3266
|
-
}
|
|
3267
|
-
/** The coarse scopes the policy declares that cover `permission`, in declaration order. */
|
|
3268
|
-
function coarseScopesFor(policy, permission) {
|
|
3269
|
-
return (policy.oauthScopes ?? []).filter((entry) => entry.permissions.includes(permission.key)).map((entry) => entry.scope);
|
|
3270
|
-
}
|
|
3271
|
-
/** Every token scope that reaches `permission`: its own, any it was renamed from, and the coarse scopes covering it. */
|
|
3272
|
-
function scopesReaching(policy, permission) {
|
|
3273
|
-
return [
|
|
3274
|
-
permission.scope,
|
|
3275
|
-
...formerScopes(permission),
|
|
3276
|
-
...coarseScopesFor(policy, permission)
|
|
3277
|
-
];
|
|
3278
|
-
}
|
|
3279
|
-
/**
|
|
3280
|
-
* The scope an `insufficient_scope` challenge names for `permission`: the
|
|
3281
|
-
* first coarse scope that covers it, which the authorization server issues,
|
|
3282
|
-
* else the permission's own scope.
|
|
3283
|
-
*/
|
|
3284
|
-
function challengeScope(policy, permission) {
|
|
3285
|
-
return coarseScopesFor(policy, permission)[0] ?? permission.scope;
|
|
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
|
-
}
|
|
3291
|
-
//#endregion
|
|
3292
|
-
//#region src/core/resolve-subject.ts
|
|
3293
|
-
/**
|
|
3294
|
-
* The tenant a user API key holds its owner to (`Credential.tenant`): only
|
|
3295
|
-
* the memberships inside it count, and no global role.
|
|
3296
|
-
*/
|
|
3297
|
-
function heldTenantOf(principal) {
|
|
3298
|
-
const credential = principal["credential"];
|
|
3299
|
-
if (credential === null || typeof credential !== "object" || !Object.hasOwn(credential, "tenant") || !Object.hasOwn(credential, "kind")) return;
|
|
3300
|
-
const record = credential;
|
|
3301
|
-
return record.kind === "user" && typeof record.tenant === "string" && record.tenant !== "" ? record.tenant : void 0;
|
|
3302
|
-
}
|
|
3303
|
-
function outsideHeld(held, options) {
|
|
3304
|
-
return held !== void 0 && options.tenant !== void 0 && options.tenant !== held;
|
|
3305
|
-
}
|
|
3306
|
-
function assemblePrincipal(policy, user, options, auth) {
|
|
3307
|
-
let principal;
|
|
3308
|
-
let context = {};
|
|
3309
|
-
let actor = options.actor;
|
|
3310
|
-
let delegation = options.delegation;
|
|
3311
|
-
let session = options.session;
|
|
3312
|
-
let expiresAt = options.expiresAt;
|
|
3313
|
-
let liveSession;
|
|
3314
|
-
try {
|
|
3315
|
-
if (user === null || user === void 0) principal = policy.subject(user);
|
|
3316
|
-
else if (isSubject(user)) {
|
|
3317
|
-
principal = user.principal;
|
|
3318
|
-
context = user.context;
|
|
3319
|
-
actor = user.actor ?? actor;
|
|
3320
|
-
delegation = user.delegation ?? delegation;
|
|
3321
|
-
session = user.session ?? session;
|
|
3322
|
-
expiresAt = user.expiresAt ?? expiresAt;
|
|
3323
|
-
liveSession = user.liveSession === true ? true : void 0;
|
|
3324
|
-
} else if (isPrincipal(user)) principal = user;
|
|
3325
|
-
else principal = policy.subject(user);
|
|
3326
|
-
} catch {
|
|
3327
|
-
principal = null;
|
|
3328
|
-
}
|
|
3329
|
-
const contextResult = withRequestContext(options.context, resolveContext(policy, user, context, auth));
|
|
3330
|
-
let memberships = principal?.memberships ?? [];
|
|
3331
|
-
let live = false;
|
|
3332
|
-
const source = options.memberships === void 0 ? void 0 : asMembershipSource(options.memberships);
|
|
3333
|
-
const fromClaims = source?.claimsFirst === true && principal?.memberships !== void 0 && principal.membershipsTruncated !== true;
|
|
3334
|
-
const read = (from, of) => {
|
|
3335
|
-
try {
|
|
3336
|
-
return from.membershipsFor(compact({
|
|
3337
|
-
id: of.id,
|
|
3338
|
-
kind: of.kind
|
|
3339
|
-
}), compact({ tenant: options.tenant }));
|
|
3340
|
-
} catch {
|
|
3341
|
-
auth.push({
|
|
3342
|
-
reason: "source-threw",
|
|
3343
|
-
source: "memberships"
|
|
3344
|
-
});
|
|
3345
|
-
return [];
|
|
3346
|
-
}
|
|
3347
|
-
};
|
|
3348
|
-
let stale;
|
|
3349
|
-
if (principal !== null && source !== void 0 && !fromClaims) {
|
|
3350
|
-
live = true;
|
|
3351
|
-
memberships = read(source, principal);
|
|
3352
|
-
} else if (principal?.memberships !== void 0 && fromClaims && source.onStale === "reread" && source.version !== void 0) {
|
|
3353
|
-
const reread = rereadStale(source, principal, principal.memberships, () => read(source, principal), auth);
|
|
3354
|
-
memberships = reread.memberships;
|
|
3355
|
-
stale = reread.stale;
|
|
3356
|
-
}
|
|
3357
|
-
return compact({
|
|
3358
|
-
principal,
|
|
3359
|
-
context: contextResult,
|
|
3360
|
-
actor,
|
|
3361
|
-
delegation: expandOAuthScopes(policy, delegation),
|
|
3362
|
-
session,
|
|
3363
|
-
expiresAt,
|
|
3364
|
-
memberships,
|
|
3365
|
-
live,
|
|
3366
|
-
stale,
|
|
3367
|
-
liveSession: principal === null ? void 0 : liveSession
|
|
3368
|
-
});
|
|
3369
|
-
}
|
|
3370
|
-
function versionQuery(principal) {
|
|
3371
|
-
return compact({
|
|
3372
|
-
id: principal.id,
|
|
3373
|
-
roles: principal.roles,
|
|
3374
|
-
memberships: principal.memberships
|
|
3375
|
-
});
|
|
3376
|
-
}
|
|
3377
|
-
function rereadStale(source, principal, claims, read, auth) {
|
|
3378
|
-
const claimed = principal.authzVersion;
|
|
3379
|
-
const judge = (current) => typeof claimed === "number" && current !== void 0 && claimed >= current ? "fresh" : "reread";
|
|
3380
|
-
const unknown = () => {
|
|
3381
|
-
auth.push({
|
|
3382
|
-
reason: "source-threw",
|
|
3383
|
-
source: "memberships"
|
|
3384
|
-
});
|
|
3385
|
-
return "unknown";
|
|
3386
|
-
};
|
|
3387
|
-
let freshness;
|
|
3388
|
-
try {
|
|
3389
|
-
const current = source.version?.(versionQuery(principal));
|
|
3390
|
-
freshness = isThenable(current) ? Promise.resolve(current).then(judge, unknown) : judge(current);
|
|
3391
|
-
} catch {
|
|
3392
|
-
freshness = unknown();
|
|
3393
|
-
}
|
|
3394
|
-
if (isThenable(freshness)) return {
|
|
3395
|
-
memberships: freshness.then((found) => found === "reread" ? read() : claims),
|
|
3396
|
-
stale: freshness.then((found) => found === "unknown")
|
|
3397
|
-
};
|
|
3398
|
-
return {
|
|
3399
|
-
memberships: freshness === "reread" ? read() : claims,
|
|
3400
|
-
stale: freshness === "unknown"
|
|
3401
|
-
};
|
|
3402
|
-
}
|
|
3403
|
-
function finishSubject(policy, assembled, context, input, options, extra) {
|
|
3404
|
-
if (assembled.principal === null) return freezeCopy(compact({
|
|
3405
|
-
...anonymousSubject(context),
|
|
3406
|
-
actor: assembled.actor,
|
|
3407
|
-
delegation: assembled.delegation,
|
|
3408
|
-
session: assembled.session,
|
|
3409
|
-
expiresAt: assembled.expiresAt
|
|
3410
|
-
}));
|
|
3411
|
-
const scopes = scopeList(policy.scopes);
|
|
3412
|
-
const held = heldTenantOf(assembled.principal);
|
|
3413
|
-
const kinds = applyRoleKinds(policy, assembled.principal.roles, checkMembershipX(normalizeMemberships(input, scopes), policyX(policy)?.membership, extra.auth));
|
|
3414
|
-
const roles = held === void 0 ? kinds.roles : [];
|
|
3415
|
-
const memberships = held === void 0 ? kinds.memberships : outsideHeld(held, options) ? [] : kinds.memberships.filter((membership) => tenantOf(membership, scopes) === held);
|
|
3416
|
-
const plans = [.../* @__PURE__ */ new Set([...assembled.principal.plans ?? [], ...extra.plans])];
|
|
3417
|
-
const withMemberships = freezeCopy(compact({
|
|
3418
|
-
...assembled.principal,
|
|
3419
|
-
roles,
|
|
3420
|
-
memberships,
|
|
3421
|
-
plans: plans.length === 0 ? assembled.principal.plans : plans,
|
|
3422
|
-
tenant: activeTenantOf(policy, assembled, memberships, options)
|
|
3423
|
-
}));
|
|
3424
|
-
return freezeCopy(compact({
|
|
3425
|
-
principal: withMemberships,
|
|
3426
|
-
actor: assembled.actor,
|
|
3427
|
-
delegation: assembled.delegation,
|
|
3428
|
-
context: freezeCopy(context),
|
|
3429
|
-
session: assembled.session,
|
|
3430
|
-
expiresAt: assembled.expiresAt,
|
|
3431
|
-
stale: extra.stale ? true : void 0,
|
|
3432
|
-
liveSession: assembled.liveSession
|
|
3433
|
-
}));
|
|
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
|
-
}
|
|
3459
|
-
function activeTenantOf(policy, assembled, memberships, options) {
|
|
3460
|
-
if (assembled.principal === null) return;
|
|
3461
|
-
const held = heldTenantOf(assembled.principal);
|
|
3462
|
-
if (held !== void 0) return outsideHeld(held, options) ? void 0 : held;
|
|
3463
|
-
return resolveActiveTenant({
|
|
3464
|
-
...assembled.principal,
|
|
3465
|
-
memberships
|
|
3466
|
-
}, options.tenant ?? assembled.principal.tenant, scopeList(policy.scopes));
|
|
3467
|
-
}
|
|
3468
|
-
function settle(value, fallback, onError) {
|
|
3469
|
-
if (isThenable(value)) return Promise.resolve(value).catch(() => {
|
|
3470
|
-
onError();
|
|
3471
|
-
return fallback;
|
|
3472
|
-
});
|
|
3473
|
-
return value;
|
|
3474
|
-
}
|
|
3475
|
-
/**
|
|
3476
|
-
* Whether the token's memberships are behind the source for `fresh`
|
|
3477
|
-
* permissions. Live memberships are never stale; token memberships are stale
|
|
3478
|
-
* unless a source reports a version the token's `authzVersion` has reached.
|
|
3479
|
-
*/
|
|
3480
|
-
function staleness(policy, assembled, options, auth) {
|
|
3481
|
-
const principal = assembled.principal;
|
|
3482
|
-
if ((policy.fresh ?? []).length === 0 || principal === null || assembled.live) return false;
|
|
3483
|
-
if (assembled.stale !== void 0) return assembled.stale;
|
|
3484
|
-
const source = options.memberships === void 0 ? void 0 : asMembershipSource(options.memberships);
|
|
3485
|
-
const claimed = principal.authzVersion;
|
|
3486
|
-
if (source?.version === void 0 || typeof claimed !== "number") return true;
|
|
3487
|
-
const compare = (current) => current === void 0 || claimed < current;
|
|
3488
|
-
try {
|
|
3489
|
-
const current = source.version(versionQuery(principal));
|
|
3490
|
-
return isThenable(current) ? settle(Promise.resolve(current).then(compare), true, () => {
|
|
3491
|
-
auth.push({
|
|
3492
|
-
reason: "source-threw",
|
|
3493
|
-
source: "memberships"
|
|
3494
|
-
});
|
|
3495
|
-
}) : compare(current);
|
|
3496
|
-
} catch {
|
|
3497
|
-
auth.push({
|
|
3498
|
-
reason: "source-threw",
|
|
3499
|
-
source: "memberships"
|
|
3500
|
-
});
|
|
3501
|
-
return true;
|
|
3502
|
-
}
|
|
3503
|
-
}
|
|
3504
|
-
function cleanNames(list) {
|
|
3505
|
-
return Array.isArray(list) ? list.filter((item) => typeof item === "string" && item !== "") : [];
|
|
3506
|
-
}
|
|
3507
|
-
function entitlementsOf(assembled, tenant, options, auth) {
|
|
3508
|
-
const source = options.entitlements;
|
|
3509
|
-
if (source === void 0 || assembled.principal === null || outsideHeld(heldTenantOf(assembled.principal), options)) return [];
|
|
3510
|
-
try {
|
|
3511
|
-
const found = source.entitlementsFor({ id: assembled.principal.id }, compact({ tenant }));
|
|
3512
|
-
return isThenable(found) ? settle(Promise.resolve(found).then(cleanNames), [], () => {
|
|
3513
|
-
auth.push({
|
|
3514
|
-
reason: "source-threw",
|
|
3515
|
-
source: "entitlements"
|
|
3516
|
-
});
|
|
3517
|
-
}) : cleanNames(found);
|
|
3518
|
-
} catch {
|
|
3519
|
-
auth.push({
|
|
3520
|
-
reason: "source-threw",
|
|
3521
|
-
source: "entitlements"
|
|
3522
|
-
});
|
|
3523
|
-
return [];
|
|
3524
|
-
}
|
|
3525
|
-
}
|
|
3526
|
-
function resolveContext(policy, user, fallback, auth) {
|
|
3527
|
-
if (policy.context === void 0) return sanitizeContext(fallback);
|
|
3528
|
-
try {
|
|
3529
|
-
const loaded = policy.context(user);
|
|
3530
|
-
if (isThenable(loaded)) return loaded.then((value) => sanitizeContext(value), () => {
|
|
3531
|
-
auth.push({
|
|
3532
|
-
reason: "source-threw",
|
|
3533
|
-
source: "context"
|
|
3534
|
-
});
|
|
3535
|
-
return {};
|
|
3536
|
-
});
|
|
3537
|
-
return sanitizeContext(loaded);
|
|
3538
|
-
} catch {
|
|
3539
|
-
auth.push({
|
|
3540
|
-
reason: "source-threw",
|
|
3541
|
-
source: "context"
|
|
3542
|
-
});
|
|
3543
|
-
return {};
|
|
3544
|
-
}
|
|
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
|
-
}
|
|
3557
|
-
function resolveSubject(policy, user, options, auth) {
|
|
3558
|
-
const assembled = assemblePrincipal(policy, user, options, auth);
|
|
3559
|
-
const memberships = settle(assembled.memberships, [], () => {
|
|
3560
|
-
auth.push({
|
|
3561
|
-
reason: "source-threw",
|
|
3562
|
-
source: "memberships"
|
|
3563
|
-
});
|
|
3564
|
-
});
|
|
3565
|
-
const stale = staleness(policy, assembled, options, auth);
|
|
3566
|
-
const withPlans = (resolved) => entitlementsOf(assembled, activeTenantOf(policy, assembled, applyRoleKinds(policy, assembled.principal?.roles, normalizeMemberships(resolved, scopeList(policy.scopes))).memberships, options), options, auth);
|
|
3567
|
-
if (isThenable(assembled.context) || isThenable(memberships) || isThenable(stale)) return Promise.all([
|
|
3568
|
-
Promise.resolve(assembled.context),
|
|
3569
|
-
Promise.resolve(memberships),
|
|
3570
|
-
Promise.resolve(stale)
|
|
3571
|
-
]).then(async ([context, resolved, isStale]) => finishSubject(policy, assembled, context, resolved, options, {
|
|
3572
|
-
stale: isStale,
|
|
3573
|
-
plans: await withPlans(resolved),
|
|
3574
|
-
auth
|
|
3575
|
-
}));
|
|
3576
|
-
const plans = withPlans(memberships);
|
|
3577
|
-
if (isThenable(plans)) return Promise.resolve(plans).then((resolved) => finishSubject(policy, assembled, assembled.context, memberships, options, {
|
|
3578
|
-
stale,
|
|
3579
|
-
plans: resolved,
|
|
3580
|
-
auth
|
|
3581
|
-
}));
|
|
3582
|
-
return finishSubject(policy, assembled, assembled.context, memberships, options, {
|
|
3583
|
-
stale,
|
|
3584
|
-
plans,
|
|
3585
|
-
auth
|
|
3586
|
-
});
|
|
3587
|
-
}
|
|
3588
|
-
//#endregion
|
|
3589
|
-
//#region src/core/permdock.ts
|
|
3590
|
-
function hostedPolicy(policy, source) {
|
|
3591
|
-
if (source === void 0 || policy.hostable.length === 0) return {
|
|
3592
|
-
policy,
|
|
3593
|
-
errors: []
|
|
3594
|
-
};
|
|
3595
|
-
let document;
|
|
3596
|
-
try {
|
|
3597
|
-
document = source.current();
|
|
3598
|
-
} catch (error) {
|
|
3599
|
-
return {
|
|
3600
|
-
policy,
|
|
3601
|
-
errors: [error]
|
|
3602
|
-
};
|
|
3603
|
-
}
|
|
3604
|
-
const merged = mergeHostedGrants(policy, document);
|
|
3605
|
-
return {
|
|
3606
|
-
policy: merged.policy,
|
|
3607
|
-
errors: merged.dropped
|
|
3608
|
-
};
|
|
3609
|
-
}
|
|
3610
|
-
function instantiate(codePolicy, subject, options, auth) {
|
|
3611
|
-
const { policy, errors } = hostedPolicy(codePolicy, options.policies);
|
|
3612
|
-
const scopes = scopeList(policy.scopes);
|
|
3613
|
-
const tenants = tenantsOf(subject.principal, scopes);
|
|
3614
|
-
const roleSource = roleSourceFor(options.customRoles, subject, auth);
|
|
3615
|
-
const customRoles = customRolesFor(roleSource, tenants, auth, subject.principal === null ? void 0 : subject.principal.roles ?? [], (tenant) => heldRoleNamesIn(subject.principal, scopes, tenant));
|
|
3616
|
-
const assignable = assignableNamesFor(roleSource, tenants, auth);
|
|
3617
|
-
const approvals = approvalPoliciesFor(policy, options.approvalPolicies, tenants, auth);
|
|
3618
|
-
const build = (roles, names, approvalPolicies) => buildInstance(policy, subject, compact({
|
|
3619
|
-
customRoles: roles,
|
|
3620
|
-
sink: options.sink,
|
|
3621
|
-
limits: options.limits,
|
|
3622
|
-
limitCache: /* @__PURE__ */ new Map(),
|
|
3623
|
-
simulated: false,
|
|
3624
|
-
roleSource,
|
|
3625
|
-
assignable: names,
|
|
3626
|
-
queuedAuth: auth,
|
|
3627
|
-
queuedErrors: errors,
|
|
3628
|
-
relations: options.relations,
|
|
3629
|
-
approvalPolicies,
|
|
3630
|
-
memberships: options.memberships === void 0 ? void 0 : asMembershipSource(options.memberships)
|
|
3631
|
-
}));
|
|
3632
|
-
if (isThenable(customRoles) || isThenable(assignable) || isThenable(approvals)) return Promise.all([
|
|
3633
|
-
customRoles,
|
|
3634
|
-
assignable,
|
|
3635
|
-
approvals
|
|
3636
|
-
]).then(([roles, names, loaded]) => build(roles, names, loaded));
|
|
3637
|
-
return build(customRoles, assignable, approvals);
|
|
3638
|
-
}
|
|
3639
|
-
function createPermDock(policy, user, options = {}) {
|
|
3640
|
-
const auth = [];
|
|
3641
|
-
const subject = resolveSubject(policy, user, options, auth);
|
|
3642
|
-
if (isThenable(subject)) return subject.then((resolved) => instantiate(policy, resolved, options, auth));
|
|
3643
|
-
return instantiate(policy, subject, options, auth);
|
|
3644
|
-
}
|
|
3645
|
-
//#endregion
|
|
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 };
|