@bajustone/fortress 1.0.0-rc.1
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/CHANGELOG.md +1011 -0
- package/LICENSE +21 -0
- package/README.md +760 -0
- package/SECURITY.md +197 -0
- package/bin/fortress.ts +667 -0
- package/dist/auth-service-BcyQ2PuZ.d.cts +307 -0
- package/dist/auth-service-BkzZkWfH.d.ts +307 -0
- package/dist/config-B2366s_G.d.ts +665 -0
- package/dist/config-GtlVt6ZD.d.cts +665 -0
- package/dist/convert-routes-BLCQT57f.d.ts +72 -0
- package/dist/convert-routes-BdMvP2_X.d.cts +72 -0
- package/dist/crypto.cjs +61 -0
- package/dist/crypto.d.cts +36 -0
- package/dist/crypto.d.ts +36 -0
- package/dist/crypto.js +35 -0
- package/dist/drift-BT1wtMDJ.d.cts +22 -0
- package/dist/drift-k9LiYpRP.d.ts +22 -0
- package/dist/drizzle/pg.cjs +481 -0
- package/dist/drizzle/pg.d.cts +15 -0
- package/dist/drizzle/pg.d.ts +15 -0
- package/dist/drizzle/pg.js +454 -0
- package/dist/drizzle.cjs +1442 -0
- package/dist/drizzle.d.cts +85 -0
- package/dist/drizzle.d.ts +85 -0
- package/dist/drizzle.js +1411 -0
- package/dist/express.cjs +1357 -0
- package/dist/express.d.cts +333 -0
- package/dist/express.d.ts +333 -0
- package/dist/express.js +1314 -0
- package/dist/fetcher.cjs +77 -0
- package/dist/fetcher.d.cts +7 -0
- package/dist/fetcher.d.ts +7 -0
- package/dist/fetcher.js +41 -0
- package/dist/fortress-BmSvFfqK.d.cts +1089 -0
- package/dist/fortress-uA-_cheR.d.ts +1089 -0
- package/dist/hono.cjs +1530 -0
- package/dist/hono.d.cts +514 -0
- package/dist/hono.d.ts +514 -0
- package/dist/hono.js +1483 -0
- package/dist/index-CxEkfCA0.d.cts +77 -0
- package/dist/index-CxEkfCA0.d.ts +77 -0
- package/dist/index-D054pcb-.d.cts +146 -0
- package/dist/index-jAMbbUAY.d.ts +146 -0
- package/dist/index.cjs +9046 -0
- package/dist/index.d.cts +717 -0
- package/dist/index.d.ts +717 -0
- package/dist/index.js +8935 -0
- package/dist/jwt.cjs +255 -0
- package/dist/jwt.d.cts +90 -0
- package/dist/jwt.d.ts +90 -0
- package/dist/jwt.js +227 -0
- package/dist/otel.cjs +108 -0
- package/dist/otel.d.cts +62 -0
- package/dist/otel.d.ts +62 -0
- package/dist/otel.js +73 -0
- package/dist/plugins/account-lockout.cjs +322 -0
- package/dist/plugins/account-lockout.d.cts +42 -0
- package/dist/plugins/account-lockout.d.ts +42 -0
- package/dist/plugins/account-lockout.js +295 -0
- package/dist/plugins/admin.cjs +2378 -0
- package/dist/plugins/admin.d.cts +72 -0
- package/dist/plugins/admin.d.ts +72 -0
- package/dist/plugins/admin.js +2351 -0
- package/dist/plugins/api-key.cjs +792 -0
- package/dist/plugins/api-key.d.cts +121 -0
- package/dist/plugins/api-key.d.ts +121 -0
- package/dist/plugins/api-key.js +765 -0
- package/dist/plugins/audit-log.cjs +493 -0
- package/dist/plugins/audit-log.d.cts +86 -0
- package/dist/plugins/audit-log.d.ts +86 -0
- package/dist/plugins/audit-log.js +468 -0
- package/dist/plugins/data-isolation.cjs +211 -0
- package/dist/plugins/data-isolation.d.cts +49 -0
- package/dist/plugins/data-isolation.d.ts +49 -0
- package/dist/plugins/data-isolation.js +186 -0
- package/dist/plugins/email-verification.cjs +273 -0
- package/dist/plugins/email-verification.d.cts +44 -0
- package/dist/plugins/email-verification.d.ts +44 -0
- package/dist/plugins/email-verification.js +246 -0
- package/dist/plugins/magic-link.cjs +231 -0
- package/dist/plugins/magic-link.d.cts +39 -0
- package/dist/plugins/magic-link.d.ts +39 -0
- package/dist/plugins/magic-link.js +204 -0
- package/dist/plugins/oauth.cjs +1696 -0
- package/dist/plugins/oauth.d.cts +406 -0
- package/dist/plugins/oauth.d.ts +406 -0
- package/dist/plugins/oauth.js +1669 -0
- package/dist/plugins/openapi.cjs +1185 -0
- package/dist/plugins/openapi.d.cts +6 -0
- package/dist/plugins/openapi.d.ts +6 -0
- package/dist/plugins/openapi.js +1158 -0
- package/dist/plugins/rate-limit/express.cjs +55 -0
- package/dist/plugins/rate-limit/express.d.cts +64 -0
- package/dist/plugins/rate-limit/express.d.ts +64 -0
- package/dist/plugins/rate-limit/express.js +30 -0
- package/dist/plugins/rate-limit/hono.cjs +51 -0
- package/dist/plugins/rate-limit/hono.d.cts +59 -0
- package/dist/plugins/rate-limit/hono.d.ts +59 -0
- package/dist/plugins/rate-limit/hono.js +26 -0
- package/dist/plugins/rate-limit/sveltekit.cjs +49 -0
- package/dist/plugins/rate-limit/sveltekit.d.cts +59 -0
- package/dist/plugins/rate-limit/sveltekit.d.ts +59 -0
- package/dist/plugins/rate-limit/sveltekit.js +24 -0
- package/dist/plugins/rate-limit.cjs +354 -0
- package/dist/plugins/rate-limit.d.cts +140 -0
- package/dist/plugins/rate-limit.d.ts +140 -0
- package/dist/plugins/rate-limit.js +325 -0
- package/dist/plugins/social-login.cjs +905 -0
- package/dist/plugins/social-login.d.cts +114 -0
- package/dist/plugins/social-login.d.ts +114 -0
- package/dist/plugins/social-login.js +880 -0
- package/dist/plugins/tenancy.cjs +780 -0
- package/dist/plugins/tenancy.d.cts +108 -0
- package/dist/plugins/tenancy.d.ts +108 -0
- package/dist/plugins/tenancy.js +753 -0
- package/dist/plugins/two-factor.cjs +555 -0
- package/dist/plugins/two-factor.d.cts +67 -0
- package/dist/plugins/two-factor.d.ts +67 -0
- package/dist/plugins/two-factor.js +526 -0
- package/dist/plugins/webauthn.cjs +786 -0
- package/dist/plugins/webauthn.d.cts +72 -0
- package/dist/plugins/webauthn.d.ts +72 -0
- package/dist/plugins/webauthn.js +766 -0
- package/dist/plugins/webhook.cjs +819 -0
- package/dist/plugins/webhook.d.cts +254 -0
- package/dist/plugins/webhook.d.ts +254 -0
- package/dist/plugins/webhook.js +789 -0
- package/dist/protect-D1v_0upK.d.cts +123 -0
- package/dist/protect-DsxV_-dJ.d.ts +123 -0
- package/dist/sveltekit.cjs +1258 -0
- package/dist/sveltekit.d.cts +420 -0
- package/dist/sveltekit.d.ts +420 -0
- package/dist/sveltekit.js +1207 -0
- package/dist/testing.cjs +4294 -0
- package/dist/testing.d.cts +197 -0
- package/dist/testing.d.ts +197 -0
- package/dist/testing.js +4264 -0
- package/dist/types-et0R8tsC.d.cts +217 -0
- package/dist/types-et0R8tsC.d.ts +217 -0
- package/docs/adapter-typed-helpers.md +192 -0
- package/docs/admin-recipes.md +227 -0
- package/docs/architecture.md +434 -0
- package/docs/ci/github-actions.yml +59 -0
- package/docs/ci.md +109 -0
- package/docs/compatibility.md +115 -0
- package/docs/deployment.md +305 -0
- package/docs/hardening.md +118 -0
- package/docs/host-owned-routes.md +154 -0
- package/docs/migrations/0001-schema-version.md +54 -0
- package/docs/migrations/0002-initial-schema.md +84 -0
- package/docs/migrations/upgrade-guide.md +160 -0
- package/docs/observability.md +394 -0
- package/docs/plugins/account-lockout.md +131 -0
- package/docs/plugins/admin.md +402 -0
- package/docs/plugins/api-key.md +327 -0
- package/docs/plugins/audit-log.md +261 -0
- package/docs/plugins/data-isolation.md +186 -0
- package/docs/plugins/email-verification.md +121 -0
- package/docs/plugins/magic-link.md +123 -0
- package/docs/plugins/oauth.md +544 -0
- package/docs/plugins/openapi.md +250 -0
- package/docs/plugins/rate-limit.md +223 -0
- package/docs/plugins/social-login.md +337 -0
- package/docs/plugins/tenancy.md +122 -0
- package/docs/plugins/two-factor.md +191 -0
- package/docs/plugins/webauthn.md +188 -0
- package/docs/plugins/webhook.md +242 -0
- package/docs/policy-as-code.md +156 -0
- package/docs/route-manifest.md +46 -0
- package/docs/security.md +261 -0
- package/docs/threat-model.md +161 -0
- package/examples/README.md +119 -0
- package/examples/express-app/index.ts +747 -0
- package/examples/hono-app/docker-compose.yml +14 -0
- package/examples/hono-app/index.ts +1009 -0
- package/examples/policy/fortress.policy.json +47 -0
- package/examples/sveltekit-app/README.md +125 -0
- package/examples/sveltekit-app/src/app.d.ts +16 -0
- package/examples/sveltekit-app/src/hooks.server.ts +23 -0
- package/examples/sveltekit-app/src/lib/server/fortress.ts +81 -0
- package/examples/sveltekit-app/src/routes/api/echo/[id]/+server.ts +32 -0
- package/examples/sveltekit-app/src/routes/api/fortress/[...path]/+server.ts +15 -0
- package/examples/sveltekit-app/src/routes/dashboard/+page.server.ts +20 -0
- package/examples/sveltekit-app/src/routes/login/+page.server.ts +48 -0
- package/examples/sveltekit-app/src/routes/oauth/consent/+page.server.ts +69 -0
- package/examples/sveltekit-app/src/routes/oauth/consent/+page.svelte +83 -0
- package/migrations/pg/0001_schema_version.down.sql +2 -0
- package/migrations/pg/0001_schema_version.sql +8 -0
- package/migrations/pg/0002_initial_schema.down.sql +36 -0
- package/migrations/pg/0002_initial_schema.sql +376 -0
- package/migrations/pg/0003_auth_continuation.down.sql +6 -0
- package/migrations/pg/0003_auth_continuation.sql +30 -0
- package/migrations/pg/0004_tenant_default_unique.down.sql +1 -0
- package/migrations/pg/0004_tenant_default_unique.sql +14 -0
- package/migrations/pg/0005_hot_indexes_timestamptz.down.sql +71 -0
- package/migrations/pg/0005_hot_indexes_timestamptz.sql +71 -0
- package/migrations/pg/0006_canonical_email.down.sql +3 -0
- package/migrations/pg/0006_canonical_email.sql +8 -0
- package/migrations/pg/0007_audit_chain_anchor.down.sql +1 -0
- package/migrations/pg/0007_audit_chain_anchor.sql +8 -0
- package/migrations/pg/0008_two_factor_hardening.down.sql +8 -0
- package/migrations/pg/0008_two_factor_hardening.sql +8 -0
- package/migrations/pg/0009_encrypt_totp_secrets.down.sql +2 -0
- package/migrations/pg/0009_encrypt_totp_secrets.sql +5 -0
- package/migrations/pg/0010_bigint_append_only_ids.down.sql +2 -0
- package/migrations/pg/0010_bigint_append_only_ids.sql +23 -0
- package/migrations/sqlite/0001_schema_version.down.sql +2 -0
- package/migrations/sqlite/0001_schema_version.sql +8 -0
- package/migrations/sqlite/0002_initial_schema.down.sql +36 -0
- package/migrations/sqlite/0002_initial_schema.sql +376 -0
- package/migrations/sqlite/0003_auth_continuation.down.sql +27 -0
- package/migrations/sqlite/0003_auth_continuation.sql +45 -0
- package/migrations/sqlite/0004_tenant_default_unique.down.sql +1 -0
- package/migrations/sqlite/0004_tenant_default_unique.sql +13 -0
- package/migrations/sqlite/0005_hot_indexes_timestamptz.down.sql +10 -0
- package/migrations/sqlite/0005_hot_indexes_timestamptz.sql +10 -0
- package/migrations/sqlite/0006_canonical_email.down.sql +3 -0
- package/migrations/sqlite/0006_canonical_email.sql +8 -0
- package/migrations/sqlite/0007_audit_chain_anchor.down.sql +1 -0
- package/migrations/sqlite/0007_audit_chain_anchor.sql +8 -0
- package/migrations/sqlite/0008_two_factor_hardening.down.sql +14 -0
- package/migrations/sqlite/0008_two_factor_hardening.sql +7 -0
- package/migrations/sqlite/0009_encrypt_totp_secrets.down.sql +2 -0
- package/migrations/sqlite/0009_encrypt_totp_secrets.sql +5 -0
- package/migrations/sqlite/0010_bigint_append_only_ids.down.sql +1 -0
- package/migrations/sqlite/0010_bigint_append_only_ids.sql +2 -0
- package/package.json +398 -0
- package/src/adapters/database/index.ts +63 -0
- package/src/adapters/database/types.ts +22 -0
- package/src/changelog-script.test.ts +21 -0
- package/src/cli.test.ts +79 -0
- package/src/core/auth/auth-admin.test.ts +247 -0
- package/src/core/auth/auth-endpoints.ts +558 -0
- package/src/core/auth/auth-observer.test.ts +191 -0
- package/src/core/auth/auth-service.ts +1464 -0
- package/src/core/auth/email.test.ts +17 -0
- package/src/core/auth/email.ts +11 -0
- package/src/core/auth/hooks.test.ts +251 -0
- package/src/core/auth/impersonation.test.ts +179 -0
- package/src/core/auth/jwt.test.ts +184 -0
- package/src/core/auth/jwt.ts +239 -0
- package/src/core/auth/login-timing.test.ts +77 -0
- package/src/core/auth/password-policy.test.ts +253 -0
- package/src/core/auth/password-policy.ts +220 -0
- package/src/core/auth/password.test.ts +42 -0
- package/src/core/auth/password.ts +62 -0
- package/src/core/auth/post-auth-gate.test.ts +258 -0
- package/src/core/auth/post-auth-gate.ts +228 -0
- package/src/core/auth/refresh-token.test.ts +86 -0
- package/src/core/auth/refresh-token.ts +105 -0
- package/src/core/auth/timing-safe.ts +23 -0
- package/src/core/config.ts +212 -0
- package/src/core/endpoint.ts +149 -0
- package/src/core/errors.ts +199 -0
- package/src/core/fetcher-interop.test.ts +106 -0
- package/src/core/fortress.test.ts +354 -0
- package/src/core/fortress.ts +550 -0
- package/src/core/http/call.test.ts +148 -0
- package/src/core/http/call.ts +149 -0
- package/src/core/http/cookie-serialize.test.ts +198 -0
- package/src/core/http/cookie-serialize.ts +107 -0
- package/src/core/http/csrf.test.ts +140 -0
- package/src/core/http/csrf.ts +173 -0
- package/src/core/http/dispatch.ts +652 -0
- package/src/core/http/error-response.test.ts +69 -0
- package/src/core/http/error-response.ts +93 -0
- package/src/core/http/fortress-rbac.test.ts +43 -0
- package/src/core/http/fortress-rbac.ts +127 -0
- package/src/core/http/handle-request.test.ts +441 -0
- package/src/core/http/handle-request.ts +354 -0
- package/src/core/http/match.test.ts +122 -0
- package/src/core/http/match.ts +126 -0
- package/src/core/http/outbound.test.ts +34 -0
- package/src/core/http/outbound.ts +105 -0
- package/src/core/http/plugin-middleware.ts +67 -0
- package/src/core/http/principal.test.ts +345 -0
- package/src/core/http/principal.ts +86 -0
- package/src/core/http/protect.test.ts +220 -0
- package/src/core/http/protect.ts +394 -0
- package/src/core/http/token-extraction.test.ts +59 -0
- package/src/core/http/token-extraction.ts +53 -0
- package/src/core/iam/explain.test.ts +177 -0
- package/src/core/iam/explain.ts +302 -0
- package/src/core/iam/iam-endpoints.ts +779 -0
- package/src/core/iam/iam-service.test.ts +829 -0
- package/src/core/iam/iam-service.ts +1137 -0
- package/src/core/iam/permission-cache.test.ts +115 -0
- package/src/core/iam/permission-cache.ts +71 -0
- package/src/core/iam/permission-check-observer.test.ts +90 -0
- package/src/core/iam/permission-evaluator.test.ts +291 -0
- package/src/core/iam/permission-evaluator.ts +198 -0
- package/src/core/iam/permission-sync.test.ts +166 -0
- package/src/core/iam/permission-sync.ts +192 -0
- package/src/core/iam/resource-sync.test.ts +70 -0
- package/src/core/iam/resource-sync.ts +182 -0
- package/src/core/iam/service-account-http.test.ts +529 -0
- package/src/core/iam/service-account.test.ts +282 -0
- package/src/core/internal-adapter.test.ts +389 -0
- package/src/core/internal-adapter.ts +435 -0
- package/src/core/json-schema.ts +50 -0
- package/src/core/manifest/__snapshots__/route-manifest.test.ts.snap +192 -0
- package/src/core/manifest/drift.ts +103 -0
- package/src/core/manifest/route-manifest.test.ts +142 -0
- package/src/core/manifest/route-manifest.ts +154 -0
- package/src/core/migrate.test.ts +72 -0
- package/src/core/migrations/engine.test.ts +308 -0
- package/src/core/migrations/engine.ts +548 -0
- package/src/core/migrations/migrations.ts +1771 -0
- package/src/core/migrations/pg-migration.integration-test.ts +353 -0
- package/src/core/migrations/upgrade-fixture.test.ts +378 -0
- package/src/core/observability/db-instrumentation.ts +205 -0
- package/src/core/observability/listener-list.test.ts +124 -0
- package/src/core/observability/listener-list.ts +73 -0
- package/src/core/observability/logger.test.ts +43 -0
- package/src/core/observability/logger.ts +49 -0
- package/src/core/observability/spans.test.ts +193 -0
- package/src/core/observability/types.ts +80 -0
- package/src/core/openapi-drift.test.ts +41 -0
- package/src/core/openapi.ts +47 -0
- package/src/core/plugin-methods-map.ts +75 -0
- package/src/core/plugin-runner.test.ts +233 -0
- package/src/core/plugin-runner.ts +276 -0
- package/src/core/plugin.ts +210 -0
- package/src/core/policy/apply.ts +272 -0
- package/src/core/policy/diff.ts +288 -0
- package/src/core/policy/loader.test.ts +63 -0
- package/src/core/policy/loader.ts +214 -0
- package/src/core/policy/policy.test.ts +232 -0
- package/src/core/policy/types.ts +105 -0
- package/src/core/schema-builder.test.ts +660 -0
- package/src/core/schema-builder.ts +881 -0
- package/src/core/standard-schema.ts +56 -0
- package/src/core/types.ts +258 -0
- package/src/core/validation.test.ts +195 -0
- package/src/core/validation.ts +192 -0
- package/src/drizzle/adapter.test.ts +425 -0
- package/src/drizzle/adapter.ts +474 -0
- package/src/drizzle/index.ts +31 -0
- package/src/drizzle/pg/adapter.integration-test.ts +456 -0
- package/src/drizzle/pg/index.ts +13 -0
- package/src/drizzle/pg/pg.integration-test.ts +1539 -0
- package/src/drizzle/pg/schema.ts +539 -0
- package/src/drizzle/pg-error-map.test.ts +218 -0
- package/src/drizzle/pg-error-map.ts +151 -0
- package/src/drizzle/schema.ts +544 -0
- package/src/drizzle/sqlite-serialization.test.ts +106 -0
- package/src/express/express-api-key-user-routes.test.ts +241 -0
- package/src/express/express.test.ts +442 -0
- package/src/express/handle.ts +191 -0
- package/src/express/index.ts +62 -0
- package/src/express/middleware.ts +551 -0
- package/src/express/protect.ts +154 -0
- package/src/express/validated.test.ts +86 -0
- package/src/express/validated.ts +99 -0
- package/src/fetcher/index.ts +81 -0
- package/src/hono/convert-routes.test.ts +220 -0
- package/src/hono/convert-routes.ts +196 -0
- package/src/hono/converters.test.ts +50 -0
- package/src/hono/converters.ts +43 -0
- package/src/hono/handle.ts +79 -0
- package/src/hono/helpers.ts +2 -0
- package/src/hono/hono-api-key-user-routes.test.ts +225 -0
- package/src/hono/hono-handlers.test.ts +861 -0
- package/src/hono/hono.test.ts +454 -0
- package/src/hono/index.ts +101 -0
- package/src/hono/middleware/auth.ts +236 -0
- package/src/hono/middleware/auth.types.test.ts +94 -0
- package/src/hono/middleware/csrf.test.ts +127 -0
- package/src/hono/middleware/csrf.ts +68 -0
- package/src/hono/middleware/error-handler.ts +12 -0
- package/src/hono/middleware/plugin-middleware.ts +35 -0
- package/src/hono/middleware/rbac.ts +131 -0
- package/src/hono/middleware/security-headers.test.ts +88 -0
- package/src/hono/middleware/security-headers.ts +68 -0
- package/src/hono/openapi.ts +237 -0
- package/src/hono/protect.ts +87 -0
- package/src/hono/validated.test.ts +123 -0
- package/src/hono/validated.ts +62 -0
- package/src/index.ts +309 -0
- package/src/integration.test.ts +718 -0
- package/src/internal/changelog.ts +56 -0
- package/src/otel/index.ts +158 -0
- package/src/otel/otel-types.test.ts +35 -0
- package/src/otel/otel.test.ts +235 -0
- package/src/plugins/account-lockout/account-lockout.test.ts +367 -0
- package/src/plugins/account-lockout/index.ts +267 -0
- package/src/plugins/admin/admin-api-key.test.ts +438 -0
- package/src/plugins/admin/index.ts +1612 -0
- package/src/plugins/api-key/api-key.test.ts +684 -0
- package/src/plugins/api-key/core.ts +336 -0
- package/src/plugins/api-key/index.ts +315 -0
- package/src/plugins/audit-log/audit-log.test.ts +749 -0
- package/src/plugins/audit-log/index.ts +680 -0
- package/src/plugins/data-isolation/data-isolation.test.ts +197 -0
- package/src/plugins/data-isolation/index.ts +157 -0
- package/src/plugins/email-verification/email-verification.test.ts +199 -0
- package/src/plugins/email-verification/index.ts +190 -0
- package/src/plugins/magic-link/index.ts +129 -0
- package/src/plugins/magic-link/magic-link.test.ts +156 -0
- package/src/plugins/oauth/id-token.ts +86 -0
- package/src/plugins/oauth/index.ts +2145 -0
- package/src/plugins/oauth/jwks.test.ts +32 -0
- package/src/plugins/oauth/jwks.ts +182 -0
- package/src/plugins/oauth/oauth.test.ts +2220 -0
- package/src/plugins/oauth/pkce.ts +58 -0
- package/src/plugins/openapi/index.ts +298 -0
- package/src/plugins/openapi/openapi.test.ts +425 -0
- package/src/plugins/openapi/spec-builder.ts +287 -0
- package/src/plugins/rate-limit/express.test.ts +89 -0
- package/src/plugins/rate-limit/express.ts +86 -0
- package/src/plugins/rate-limit/hono.test.ts +60 -0
- package/src/plugins/rate-limit/hono.ts +74 -0
- package/src/plugins/rate-limit/index.ts +383 -0
- package/src/plugins/rate-limit/memory-store.ts +61 -0
- package/src/plugins/rate-limit/rate-limit.test.ts +756 -0
- package/src/plugins/rate-limit/sveltekit.test.ts +58 -0
- package/src/plugins/rate-limit/sveltekit.ts +66 -0
- package/src/plugins/social-login/index.ts +715 -0
- package/src/plugins/social-login/providers/apple.ts +34 -0
- package/src/plugins/social-login/providers/discord.ts +26 -0
- package/src/plugins/social-login/providers/github.ts +54 -0
- package/src/plugins/social-login/providers/google.ts +23 -0
- package/src/plugins/social-login/providers/index.ts +22 -0
- package/src/plugins/social-login/providers/microsoft.ts +35 -0
- package/src/plugins/social-login/providers/oidc.ts +37 -0
- package/src/plugins/social-login/providers/providers.test.ts +211 -0
- package/src/plugins/social-login/social-login.test.ts +675 -0
- package/src/plugins/social-login/types.ts +80 -0
- package/src/plugins/tenancy/index.ts +544 -0
- package/src/plugins/tenancy/tenancy.test.ts +323 -0
- package/src/plugins/two-factor/index.ts +569 -0
- package/src/plugins/two-factor/two-factor.test.ts +235 -0
- package/src/plugins/webauthn/index.ts +554 -0
- package/src/plugins/webauthn/webauthn.test.ts +472 -0
- package/src/plugins/webhook/builtin-events.ts +29 -0
- package/src/plugins/webhook/delivery.test.ts +51 -0
- package/src/plugins/webhook/delivery.ts +154 -0
- package/src/plugins/webhook/hooks.ts +89 -0
- package/src/plugins/webhook/index.ts +445 -0
- package/src/plugins/webhook/queue/database.ts +67 -0
- package/src/plugins/webhook/queue/in-memory.test.ts +48 -0
- package/src/plugins/webhook/queue/in-memory.ts +71 -0
- package/src/plugins/webhook/queue/types.ts +51 -0
- package/src/plugins/webhook/registry.test.ts +48 -0
- package/src/plugins/webhook/registry.ts +64 -0
- package/src/plugins/webhook/signing.ts +45 -0
- package/src/plugins/webhook/ssrf.ts +198 -0
- package/src/plugins/webhook/types.ts +138 -0
- package/src/plugins/webhook/webhook.test.ts +324 -0
- package/src/sveltekit/actions.ts +233 -0
- package/src/sveltekit/catch-all.ts +43 -0
- package/src/sveltekit/cookies.ts +136 -0
- package/src/sveltekit/handle.ts +356 -0
- package/src/sveltekit/helpers.ts +69 -0
- package/src/sveltekit/index.ts +74 -0
- package/src/sveltekit/protect.ts +80 -0
- package/src/sveltekit/sveltekit-types.test.ts +31 -0
- package/src/sveltekit/sveltekit.test.ts +799 -0
- package/src/sveltekit/types.ts +134 -0
- package/src/sveltekit/validated.test.ts +117 -0
- package/src/sveltekit/validated.ts +78 -0
- package/src/testing/adapter-conformance.test.ts +408 -0
- package/src/testing/checks.test.ts +124 -0
- package/src/testing/checks.ts +304 -0
- package/src/testing/index.ts +107 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token extraction from a web-standard Request.
|
|
3
|
+
*
|
|
4
|
+
* Reads the access token from the configured cookie first, then falls back
|
|
5
|
+
* to the `Authorization: Bearer` header. Pure function — no side effects,
|
|
6
|
+
* no framework dependencies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ResolvedCookieConfig } from '../config';
|
|
10
|
+
import { parseCookieHeader } from './cookie-serialize';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract the access token from a request, preferring the
|
|
14
|
+
* `Authorization: Bearer <token>` header and falling back to the cookie
|
|
15
|
+
* configured via {@link ResolvedCookieConfig.accessName}.
|
|
16
|
+
*
|
|
17
|
+
* P3.7 fix: the previous implementation read the cookie first, which on a
|
|
18
|
+
* shared cookie domain could let a same-origin attacker's cookie shadow
|
|
19
|
+
* the caller's intended bearer token. The Authorization header is an
|
|
20
|
+
* explicit, intentional credential and takes precedence; the cookie is
|
|
21
|
+
* only consulted when no bearer header is present.
|
|
22
|
+
*
|
|
23
|
+
* Returns `null` if neither source carries a token.
|
|
24
|
+
*/
|
|
25
|
+
export function extractAccessToken(
|
|
26
|
+
request: Request,
|
|
27
|
+
cookies: ResolvedCookieConfig,
|
|
28
|
+
): string | null {
|
|
29
|
+
const auth = request.headers.get('authorization');
|
|
30
|
+
if (auth) {
|
|
31
|
+
const separator = auth.indexOf(' ');
|
|
32
|
+
if (separator > 0 && auth.slice(0, separator).toLowerCase() === 'bearer') {
|
|
33
|
+
const fromHeader = auth.slice(separator + 1).trim();
|
|
34
|
+
if (fromHeader)
|
|
35
|
+
return fromHeader;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const jar = parseCookieHeader(request.headers.get('cookie'));
|
|
40
|
+
return jar[cookies.accessName] ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract the refresh token from a request's cookie jar. Refresh tokens are
|
|
45
|
+
* never read from headers — they live in `httpOnly` cookies only.
|
|
46
|
+
*/
|
|
47
|
+
export function extractRefreshToken(
|
|
48
|
+
request: Request,
|
|
49
|
+
cookies: ResolvedCookieConfig,
|
|
50
|
+
): string | null {
|
|
51
|
+
const jar = parseCookieHeader(request.headers.get('cookie'));
|
|
52
|
+
return jar[cookies.refreshName] ?? null;
|
|
53
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `explainPermission` (P1-8 operator-debugging helper).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { createTestAdapter } from '../../testing';
|
|
7
|
+
import { createFortress } from '../fortress';
|
|
8
|
+
import { explainPermission } from './explain';
|
|
9
|
+
|
|
10
|
+
const SECRET = 'explain-test-secret-at-least-32-bytes!';
|
|
11
|
+
|
|
12
|
+
function setup(evaluationMode: 'allow-only' | 'deny-overrides' = 'allow-only') {
|
|
13
|
+
const fortress = createFortress({
|
|
14
|
+
jwt: { key: SECRET },
|
|
15
|
+
database: createTestAdapter(),
|
|
16
|
+
rbac: { evaluationMode },
|
|
17
|
+
});
|
|
18
|
+
return { fortress };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('explainPermission', () => {
|
|
22
|
+
it('returns allowed=false with no sources for a user with no bindings', async () => {
|
|
23
|
+
const { fortress } = setup();
|
|
24
|
+
const user = await fortress.auth.createUser({
|
|
25
|
+
email: 'noperm@example.com',
|
|
26
|
+
name: 'Noperm',
|
|
27
|
+
password: 'noperm-password-1234564',
|
|
28
|
+
});
|
|
29
|
+
const result = await explainPermission(
|
|
30
|
+
fortress.config.database,
|
|
31
|
+
fortress.iam,
|
|
32
|
+
{ type: 'USER', id: user.id },
|
|
33
|
+
'article',
|
|
34
|
+
'read',
|
|
35
|
+
);
|
|
36
|
+
expect(result.allowed).toBe(false);
|
|
37
|
+
expect(result.sources).toEqual([]);
|
|
38
|
+
expect(result.roleBindings).toEqual([]);
|
|
39
|
+
expect(result.groupMemberships).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('attributes a permission granted via a role binding', async () => {
|
|
43
|
+
const { fortress } = setup();
|
|
44
|
+
const user = await fortress.auth.createUser({
|
|
45
|
+
email: 'role@example.com',
|
|
46
|
+
name: 'Role User',
|
|
47
|
+
password: 'role-password-1234564',
|
|
48
|
+
});
|
|
49
|
+
const role = await fortress.iam.createRole('editor', [
|
|
50
|
+
{ resource: 'article', action: 'create' },
|
|
51
|
+
{ resource: 'article', action: 'update' },
|
|
52
|
+
]);
|
|
53
|
+
await fortress.iam.bindRoleToUser(user.id, role.id);
|
|
54
|
+
|
|
55
|
+
const explain = await explainPermission(
|
|
56
|
+
fortress.config.database,
|
|
57
|
+
fortress.iam,
|
|
58
|
+
{ type: 'USER', id: user.id },
|
|
59
|
+
'article',
|
|
60
|
+
'create',
|
|
61
|
+
);
|
|
62
|
+
expect(explain.allowed).toBe(true);
|
|
63
|
+
expect(explain.sources).toHaveLength(1);
|
|
64
|
+
expect(explain.sources[0].via).toBe('role');
|
|
65
|
+
expect(explain.sources[0].role).toBe('editor');
|
|
66
|
+
expect(explain.sources[0].permission.resource).toBe('article');
|
|
67
|
+
expect(explain.sources[0].permission.action).toBe('create');
|
|
68
|
+
expect(explain.roleBindings.map(rb => rb.role)).toEqual(['editor']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('attributes a permission granted via a direct binding', async () => {
|
|
72
|
+
const { fortress } = setup();
|
|
73
|
+
const user = await fortress.auth.createUser({
|
|
74
|
+
email: 'direct@example.com',
|
|
75
|
+
name: 'Direct',
|
|
76
|
+
password: 'direct-password-1234564',
|
|
77
|
+
});
|
|
78
|
+
await fortress.iam.bindPermissionToUser(user.id, { resource: 'article', action: 'publish' });
|
|
79
|
+
|
|
80
|
+
const explain = await explainPermission(
|
|
81
|
+
fortress.config.database,
|
|
82
|
+
fortress.iam,
|
|
83
|
+
{ type: 'USER', id: user.id },
|
|
84
|
+
'article',
|
|
85
|
+
'publish',
|
|
86
|
+
);
|
|
87
|
+
expect(explain.allowed).toBe(true);
|
|
88
|
+
expect(explain.sources).toHaveLength(1);
|
|
89
|
+
expect(explain.sources[0].via).toBe('direct-user');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('attributes a permission inherited via a group role binding', async () => {
|
|
93
|
+
const { fortress } = setup();
|
|
94
|
+
const user = await fortress.auth.createUser({
|
|
95
|
+
email: 'group@example.com',
|
|
96
|
+
name: 'Group',
|
|
97
|
+
password: 'group-password-1234564',
|
|
98
|
+
});
|
|
99
|
+
const group = await fortress.iam.createGroup('eng', 'Engineering');
|
|
100
|
+
await fortress.iam.addUserToGroup(group.id, user.id);
|
|
101
|
+
const role = await fortress.iam.createRole('viewer', [
|
|
102
|
+
{ resource: 'article', action: 'read' },
|
|
103
|
+
]);
|
|
104
|
+
await fortress.iam.bindRoleToGroup(group.id, role.id);
|
|
105
|
+
|
|
106
|
+
const explain = await explainPermission(
|
|
107
|
+
fortress.config.database,
|
|
108
|
+
fortress.iam,
|
|
109
|
+
{ type: 'USER', id: user.id },
|
|
110
|
+
'article',
|
|
111
|
+
'read',
|
|
112
|
+
);
|
|
113
|
+
expect(explain.allowed).toBe(true);
|
|
114
|
+
expect(explain.sources).toHaveLength(1);
|
|
115
|
+
expect(explain.sources[0].via).toBe('role');
|
|
116
|
+
expect(explain.sources[0].role).toBe('viewer');
|
|
117
|
+
expect(explain.sources[0].group).toEqual({ id: group.id, name: 'eng' });
|
|
118
|
+
expect(explain.groupMemberships).toEqual([{ id: group.id, name: 'eng' }]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('uses the same condition context as checkPermission', async () => {
|
|
122
|
+
const { fortress } = setup();
|
|
123
|
+
const user = await fortress.auth.createUser({
|
|
124
|
+
email: 'condition@example.com',
|
|
125
|
+
name: 'Condition',
|
|
126
|
+
password: 'condition-password-1234564',
|
|
127
|
+
});
|
|
128
|
+
const role = await fortress.iam.createRole('owner', [{
|
|
129
|
+
resource: 'article',
|
|
130
|
+
action: 'edit',
|
|
131
|
+
conditions: [{ field: 'resource.ownerId', operator: 'eq', value: { ref: 'user.id' } }],
|
|
132
|
+
}]);
|
|
133
|
+
await fortress.iam.bindRoleToUser(user.id, role.id);
|
|
134
|
+
|
|
135
|
+
const explanation = await explainPermission(
|
|
136
|
+
fortress.config.database,
|
|
137
|
+
fortress.iam,
|
|
138
|
+
{ type: 'USER', id: user.id },
|
|
139
|
+
'article',
|
|
140
|
+
'edit',
|
|
141
|
+
{ resource: { ownerId: 'someone-else' } },
|
|
142
|
+
);
|
|
143
|
+
expect(explanation.sources).toHaveLength(0);
|
|
144
|
+
expect(explanation.allowed).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('uses the configured deny-overrides evaluator', async () => {
|
|
148
|
+
const { fortress } = setup('deny-overrides');
|
|
149
|
+
const user = await fortress.auth.createUser({
|
|
150
|
+
email: 'deny@example.com',
|
|
151
|
+
name: 'Deny',
|
|
152
|
+
password: 'deny-password-1234564',
|
|
153
|
+
});
|
|
154
|
+
const allowRole = await fortress.iam.createRole('allow-articles', [
|
|
155
|
+
{ resource: 'article', action: 'delete' },
|
|
156
|
+
]);
|
|
157
|
+
await fortress.iam.bindRoleToUser(user.id, allowRole.id);
|
|
158
|
+
// Direct DENY overrides the role grant.
|
|
159
|
+
await fortress.iam.bindPermissionToUser(user.id, {
|
|
160
|
+
resource: 'article',
|
|
161
|
+
action: 'delete',
|
|
162
|
+
effect: 'DENY',
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const explain = await explainPermission(
|
|
166
|
+
fortress.config.database,
|
|
167
|
+
fortress.iam,
|
|
168
|
+
{ type: 'USER', id: user.id },
|
|
169
|
+
'article',
|
|
170
|
+
'delete',
|
|
171
|
+
);
|
|
172
|
+
expect(explain.allowed).toBe(false);
|
|
173
|
+
expect(explain.sources.length).toBeGreaterThanOrEqual(2);
|
|
174
|
+
expect(explain.sources.some(s => s.permission.effect === 'DENY')).toBe(true);
|
|
175
|
+
expect(explain.sources.some(s => (s.permission.effect ?? 'ALLOW') === 'ALLOW')).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission debugging helper (P1-8).
|
|
3
|
+
*
|
|
4
|
+
* Answers the operator-console question "why does subject X have (or not
|
|
5
|
+
* have) permission `resource:action`?" by walking the same data the
|
|
6
|
+
* permission evaluator uses — direct bindings, role bindings, group
|
|
7
|
+
* memberships (for USER subjects) — and emitting a structured
|
|
8
|
+
* explanation listing every grant + its source.
|
|
9
|
+
*
|
|
10
|
+
* Designed as a free function rather than a new {@link IamService} method
|
|
11
|
+
* to keep the surface narrow and the DB queries explicit.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { DatabaseAdapter } from '../../adapters/database';
|
|
17
|
+
import type { Permission, PermissionContext, Role, Subject } from '../types';
|
|
18
|
+
import type { IamService } from './iam-service';
|
|
19
|
+
import { evaluateConditions } from './permission-evaluator';
|
|
20
|
+
|
|
21
|
+
export interface PermissionExplanationSource {
|
|
22
|
+
/** Where the grant came from. */
|
|
23
|
+
via: 'direct-user' | 'direct-group' | 'direct-service-account' | 'role';
|
|
24
|
+
/** Role name when `via === 'role'`. */
|
|
25
|
+
role?: string;
|
|
26
|
+
/** Group id/name when the grant flows through a group (USER subjects only). */
|
|
27
|
+
group?: { id: string; name: string };
|
|
28
|
+
/** The matching permission row. */
|
|
29
|
+
permission: Permission;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PermissionExplanation {
|
|
33
|
+
subject: Subject;
|
|
34
|
+
resource: string;
|
|
35
|
+
action: string;
|
|
36
|
+
/** Final verdict from the configured IAM evaluation mode and condition context. */
|
|
37
|
+
allowed: boolean;
|
|
38
|
+
/** Every grant source that matched, including DENYs. */
|
|
39
|
+
sources: PermissionExplanationSource[];
|
|
40
|
+
/** Roles the subject is bound to (whether or not their permissions matched). */
|
|
41
|
+
roleBindings: Array<{ role: string; tenantId?: string | null }>;
|
|
42
|
+
/** Groups the subject belongs to (USER only). */
|
|
43
|
+
groupMemberships: Array<{ id: string; name: string }>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface RoleBindingRow {
|
|
47
|
+
roleId: string;
|
|
48
|
+
tenantId: string | null;
|
|
49
|
+
subjectType: 'USER' | 'GROUP' | 'SERVICE_ACCOUNT';
|
|
50
|
+
subjectId: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface DirectPermissionBindingRow {
|
|
54
|
+
permissionId: string;
|
|
55
|
+
tenantId: string | null;
|
|
56
|
+
subjectType: 'USER' | 'GROUP' | 'SERVICE_ACCOUNT';
|
|
57
|
+
subjectId: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface GroupMembershipRow {
|
|
61
|
+
groupId: string;
|
|
62
|
+
userId: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface GroupRow {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function permissionMatches(perm: Permission, resource: string, action: string): boolean {
|
|
71
|
+
const matchPart = (declared: string, actual: string): boolean => declared === actual || declared === '*';
|
|
72
|
+
return matchPart(perm.resource, resource) && matchPart(perm.action, action);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function loadGroupMemberships(
|
|
76
|
+
db: DatabaseAdapter,
|
|
77
|
+
userId: string,
|
|
78
|
+
): Promise<GroupRow[]> {
|
|
79
|
+
const memberships = await db.findMany<GroupMembershipRow>({
|
|
80
|
+
model: 'group_user',
|
|
81
|
+
where: [{ field: 'userId', operator: '=', value: userId }],
|
|
82
|
+
});
|
|
83
|
+
if (memberships.length === 0)
|
|
84
|
+
return [];
|
|
85
|
+
return db.findMany<GroupRow>({
|
|
86
|
+
model: 'group',
|
|
87
|
+
where: [{ field: 'id', operator: 'in', value: memberships.map(m => m.groupId) }],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function loadRoleBindings(
|
|
92
|
+
db: DatabaseAdapter,
|
|
93
|
+
subject: { type: 'USER' | 'GROUP' | 'SERVICE_ACCOUNT'; id: string },
|
|
94
|
+
): Promise<RoleBindingRow[]> {
|
|
95
|
+
return db.findMany<RoleBindingRow>({
|
|
96
|
+
model: 'role_binding',
|
|
97
|
+
where: [
|
|
98
|
+
{ field: 'subjectType', operator: '=', value: subject.type },
|
|
99
|
+
{ field: 'subjectId', operator: '=', value: subject.id },
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function loadDirectBindings(
|
|
105
|
+
db: DatabaseAdapter,
|
|
106
|
+
subject: { type: 'USER' | 'GROUP' | 'SERVICE_ACCOUNT'; id: string },
|
|
107
|
+
): Promise<DirectPermissionBindingRow[]> {
|
|
108
|
+
return db.findMany<DirectPermissionBindingRow>({
|
|
109
|
+
model: 'direct_permission_binding',
|
|
110
|
+
where: [
|
|
111
|
+
{ field: 'subjectType', operator: '=', value: subject.type },
|
|
112
|
+
{ field: 'subjectId', operator: '=', value: subject.id },
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function loadPermissionsByIds(
|
|
118
|
+
db: DatabaseAdapter,
|
|
119
|
+
ids: string[],
|
|
120
|
+
): Promise<Map<string, Permission>> {
|
|
121
|
+
if (ids.length === 0)
|
|
122
|
+
return new Map();
|
|
123
|
+
const rows = await db.findMany<Permission>({
|
|
124
|
+
model: 'permission',
|
|
125
|
+
where: [{ field: 'id', operator: 'in', value: ids }],
|
|
126
|
+
});
|
|
127
|
+
return new Map(rows.map(row => [row.id, row]));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function loadRolePermissions(
|
|
131
|
+
db: DatabaseAdapter,
|
|
132
|
+
roleIds: string[],
|
|
133
|
+
): Promise<Map<string, string[]>> {
|
|
134
|
+
if (roleIds.length === 0)
|
|
135
|
+
return new Map();
|
|
136
|
+
const links = await db.findMany<{ roleId: string; permissionId: string }>({
|
|
137
|
+
model: 'role_permission',
|
|
138
|
+
where: [{ field: 'roleId', operator: 'in', value: roleIds }],
|
|
139
|
+
});
|
|
140
|
+
const out = new Map<string, string[]>();
|
|
141
|
+
for (const link of links) {
|
|
142
|
+
const list = out.get(link.roleId) ?? [];
|
|
143
|
+
list.push(link.permissionId);
|
|
144
|
+
out.set(link.roleId, list);
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function loadRoles(db: DatabaseAdapter, ids: string[]): Promise<Map<string, Role>> {
|
|
150
|
+
if (ids.length === 0)
|
|
151
|
+
return new Map();
|
|
152
|
+
const rows = await db.findMany<Role>({
|
|
153
|
+
model: 'role',
|
|
154
|
+
where: [{ field: 'id', operator: 'in', value: ids }],
|
|
155
|
+
});
|
|
156
|
+
return new Map(rows.map(row => [row.id, row]));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Explain why a subject does (or does not) have a permission. Walks
|
|
161
|
+
* direct bindings, role bindings, and group memberships (USER only) and
|
|
162
|
+
* lists every matching grant with its source so operators can debug
|
|
163
|
+
* unexpected allow/deny results.
|
|
164
|
+
*
|
|
165
|
+
* @param db - Database adapter (typically `fortress.config.database`).
|
|
166
|
+
* @param iam - IAM service used for the authoritative verdict, including
|
|
167
|
+
* configured evaluation mode, conditions, cache semantics, and inactive
|
|
168
|
+
* service-account handling.
|
|
169
|
+
*/
|
|
170
|
+
export async function explainPermission(
|
|
171
|
+
db: DatabaseAdapter,
|
|
172
|
+
iam: IamService,
|
|
173
|
+
subject: Subject,
|
|
174
|
+
resource: string,
|
|
175
|
+
action: string,
|
|
176
|
+
context?: PermissionContext,
|
|
177
|
+
): Promise<PermissionExplanation> {
|
|
178
|
+
const sources: PermissionExplanationSource[] = [];
|
|
179
|
+
|
|
180
|
+
// ── Group memberships (USER only) ──────────────────────────────────
|
|
181
|
+
const groupMemberships: GroupRow[] = subject.type === 'USER'
|
|
182
|
+
? await loadGroupMemberships(db, subject.id)
|
|
183
|
+
: [];
|
|
184
|
+
|
|
185
|
+
// ── Role bindings: subject + inherited via groups ──────────────────
|
|
186
|
+
const directRoleBindings = await loadRoleBindings(db, subject);
|
|
187
|
+
const groupRoleBindings = await Promise.all(
|
|
188
|
+
groupMemberships.map(group => loadRoleBindings(db, { type: 'GROUP', id: group.id })),
|
|
189
|
+
);
|
|
190
|
+
// Map of roleId → { binding, viaGroup? }
|
|
191
|
+
const allRoleSources: Array<{ binding: RoleBindingRow; viaGroup?: GroupRow }> = [
|
|
192
|
+
...directRoleBindings.map(binding => ({ binding })),
|
|
193
|
+
...groupRoleBindings.flatMap((bindings, idx) =>
|
|
194
|
+
bindings.map(binding => ({ binding, viaGroup: groupMemberships[idx] })),
|
|
195
|
+
),
|
|
196
|
+
];
|
|
197
|
+
const roleIds = [...new Set(allRoleSources.map(s => s.binding.roleId))];
|
|
198
|
+
const rolesById = await loadRoles(db, roleIds);
|
|
199
|
+
const rolePermIds = await loadRolePermissions(db, roleIds);
|
|
200
|
+
|
|
201
|
+
// ── Direct permission bindings: subject + inherited via groups ─────
|
|
202
|
+
const directBindings = await loadDirectBindings(db, subject);
|
|
203
|
+
const groupDirectBindings = await Promise.all(
|
|
204
|
+
groupMemberships.map(group => loadDirectBindings(db, { type: 'GROUP', id: group.id })),
|
|
205
|
+
);
|
|
206
|
+
const allDirectSources: Array<{ binding: DirectPermissionBindingRow; viaGroup?: GroupRow }> = [
|
|
207
|
+
...directBindings.map(binding => ({ binding })),
|
|
208
|
+
...groupDirectBindings.flatMap((bindings, idx) =>
|
|
209
|
+
bindings.map(binding => ({ binding, viaGroup: groupMemberships[idx] })),
|
|
210
|
+
),
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
// ── Resolve every referenced permission row in one query ───────────
|
|
214
|
+
const permissionIds = new Set<string>();
|
|
215
|
+
for (const list of rolePermIds.values()) {
|
|
216
|
+
for (const id of list) permissionIds.add(id);
|
|
217
|
+
}
|
|
218
|
+
for (const source of allDirectSources) permissionIds.add(source.binding.permissionId);
|
|
219
|
+
const permsById = await loadPermissionsByIds(db, [...permissionIds]);
|
|
220
|
+
|
|
221
|
+
const tenantMatches = (tenantId: string | null): boolean => context?.tenantId
|
|
222
|
+
? tenantId === null || tenantId === context.tenantId
|
|
223
|
+
: tenantId === null;
|
|
224
|
+
const conditionContext: PermissionContext = {
|
|
225
|
+
...context,
|
|
226
|
+
user: {
|
|
227
|
+
...context?.user,
|
|
228
|
+
id: subject.id,
|
|
229
|
+
subjectType: subject.type,
|
|
230
|
+
subjectId: subject.id,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
const conditionsMatch = (permission: Permission): boolean => {
|
|
234
|
+
const raw = permission.conditions;
|
|
235
|
+
const conditions = typeof raw === 'string' ? JSON.parse(raw) as Permission['conditions'] : raw;
|
|
236
|
+
return !conditions?.length || evaluateConditions(conditions, conditionContext);
|
|
237
|
+
};
|
|
238
|
+
const subjectIsActive = subject.type !== 'SERVICE_ACCOUNT' || (await db.findOne<{ isActive: boolean }>({
|
|
239
|
+
model: 'service_account',
|
|
240
|
+
where: [{ field: 'id', operator: '=', value: subject.id }],
|
|
241
|
+
}))?.isActive === true;
|
|
242
|
+
|
|
243
|
+
// ── Direct grants ──────────────────────────────────────────────────
|
|
244
|
+
for (const { binding, viaGroup } of allDirectSources) {
|
|
245
|
+
const perm = permsById.get(binding.permissionId);
|
|
246
|
+
if (!subjectIsActive || !tenantMatches(binding.tenantId)
|
|
247
|
+
|| !perm || !permissionMatches(perm, resource, action)
|
|
248
|
+
|| !conditionsMatch(perm)) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
sources.push({
|
|
252
|
+
via: viaGroup
|
|
253
|
+
? 'direct-group'
|
|
254
|
+
: subject.type === 'SERVICE_ACCOUNT'
|
|
255
|
+
? 'direct-service-account'
|
|
256
|
+
: 'direct-user',
|
|
257
|
+
...(viaGroup ? { group: { id: viaGroup.id, name: viaGroup.name } } : {}),
|
|
258
|
+
permission: perm,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Role-based grants ──────────────────────────────────────────────
|
|
263
|
+
for (const { binding, viaGroup } of allRoleSources) {
|
|
264
|
+
const role = rolesById.get(binding.roleId);
|
|
265
|
+
if (!subjectIsActive || !tenantMatches(binding.tenantId) || !role)
|
|
266
|
+
continue;
|
|
267
|
+
const permIds = rolePermIds.get(binding.roleId) ?? [];
|
|
268
|
+
for (const permId of permIds) {
|
|
269
|
+
const perm = permsById.get(permId);
|
|
270
|
+
if (!perm || !permissionMatches(perm, resource, action)
|
|
271
|
+
|| !conditionsMatch(perm)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
sources.push({
|
|
275
|
+
via: 'role',
|
|
276
|
+
role: role.name,
|
|
277
|
+
...(viaGroup ? { group: { id: viaGroup.id, name: viaGroup.name } } : {}),
|
|
278
|
+
permission: perm,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Reuse the authoritative evaluator so explanation verdicts cannot drift
|
|
284
|
+
// from checkPermission's configured mode, conditions, or subject gates.
|
|
285
|
+
const allowed = await iam.checkPermission(subject, resource, action, context);
|
|
286
|
+
|
|
287
|
+
// Flatten role bindings for the result.
|
|
288
|
+
const roleBindings = allRoleSources
|
|
289
|
+
.filter(source => subjectIsActive && tenantMatches(source.binding.tenantId))
|
|
290
|
+
.map(s => ({ role: rolesById.get(s.binding.roleId)?.name ?? `role#${s.binding.roleId}`, tenantId: s.binding.tenantId }))
|
|
291
|
+
.filter((entry, idx, arr) => arr.findIndex(e => e.role === entry.role && e.tenantId === entry.tenantId) === idx);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
subject,
|
|
295
|
+
resource,
|
|
296
|
+
action,
|
|
297
|
+
allowed,
|
|
298
|
+
sources,
|
|
299
|
+
roleBindings,
|
|
300
|
+
groupMemberships: groupMemberships.map(group => ({ id: group.id, name: group.name })),
|
|
301
|
+
};
|
|
302
|
+
}
|