@hilbras/keystone 1.6.0 → 1.9.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/CHANGELOG.md +239 -0
- package/README.md +55 -25
- package/dist/cli.js +25 -27
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +15 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +45 -8
- package/dist/config.js.map +1 -1
- package/dist/container.d.ts +4 -1
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js.map +1 -1
- package/dist/db/migrations/0008_flashy_firestar.sql +5 -0
- package/dist/db/migrations/0009_daffy_changeling.sql +1 -0
- package/dist/db/migrations/0010_wealthy_shadow_king.sql +37 -0
- package/dist/db/migrations/0011_overrated_vector.sql +18 -0
- package/dist/db/migrations/0012_brave_darkhawk.sql +28 -0
- package/dist/db/migrations/0013_wonderful_chimera.sql +5 -0
- package/dist/db/migrations/0014_magical_morph.sql +48 -0
- package/{src/db/migrations/meta/0007_snapshot.json → dist/db/migrations/meta/0008_snapshot.json} +136 -32
- package/{src/db/migrations/meta/0006_snapshot.json → dist/db/migrations/meta/0009_snapshot.json} +359 -30
- package/{src/db/migrations/meta/0005_snapshot.json → dist/db/migrations/meta/0010_snapshot.json} +459 -29
- package/{src/db/migrations/meta/0004_snapshot.json → dist/db/migrations/meta/0011_snapshot.json} +594 -30
- package/dist/db/migrations/meta/0012_snapshot.json +3891 -0
- package/dist/db/migrations/meta/0013_snapshot.json +3919 -0
- package/dist/db/migrations/meta/0014_snapshot.json +4311 -0
- package/dist/db/migrations/meta/_journal.json +49 -0
- package/dist/db/reencryptOidcSecrets.d.ts +2 -0
- package/dist/db/reencryptOidcSecrets.d.ts.map +1 -0
- package/dist/db/reencryptOidcSecrets.js +41 -0
- package/dist/db/reencryptOidcSecrets.js.map +1 -0
- package/dist/db/schema.d.ts +2521 -53
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +118 -3
- package/dist/db/schema.js.map +1 -1
- package/dist/db/seed.js +67 -40
- package/dist/db/seed.js.map +1 -1
- package/dist/di.d.ts.map +1 -1
- package/dist/di.js +11 -3
- package/dist/di.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/errors.d.ts +2 -1
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +10 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/plugins/appContext.d.ts.map +1 -1
- package/dist/plugins/appContext.js +3 -12
- package/dist/plugins/appContext.js.map +1 -1
- package/dist/plugins/audit.d.ts.map +1 -1
- package/dist/plugins/audit.js +26 -6
- package/dist/plugins/audit.js.map +1 -1
- package/dist/plugins/auth.d.ts.map +1 -1
- package/dist/plugins/auth.js +32 -7
- package/dist/plugins/auth.js.map +1 -1
- package/dist/plugins/metrics.d.ts.map +1 -1
- package/dist/plugins/mtls.d.ts.map +1 -1
- package/dist/plugins/permissions.d.ts.map +1 -1
- package/dist/plugins/permissions.js +29 -9
- package/dist/plugins/permissions.js.map +1 -1
- package/dist/plugins/rateLimit.d.ts +15 -0
- package/dist/plugins/rateLimit.d.ts.map +1 -1
- package/dist/plugins/rateLimit.js +11 -1
- package/dist/plugins/rateLimit.js.map +1 -1
- package/dist/plugins/requestLogger.d.ts.map +1 -1
- package/dist/repositories/apiKey.d.ts.map +1 -1
- package/dist/repositories/application.d.ts.map +1 -1
- package/dist/repositories/application.js +1 -1
- package/dist/repositories/application.js.map +1 -1
- package/dist/repositories/audit.d.ts.map +1 -1
- package/dist/repositories/index.d.ts +3 -0
- package/dist/repositories/index.d.ts.map +1 -1
- package/dist/repositories/index.js +3 -0
- package/dist/repositories/index.js.map +1 -1
- package/dist/repositories/mfaChallenge.d.ts +23 -0
- package/dist/repositories/mfaChallenge.d.ts.map +1 -0
- package/dist/repositories/mfaChallenge.js +71 -0
- package/dist/repositories/mfaChallenge.js.map +1 -0
- package/dist/repositories/oidcConnection.d.ts +27 -43
- package/dist/repositories/oidcConnection.d.ts.map +1 -1
- package/dist/repositories/oidcConnection.js +15 -6
- package/dist/repositories/oidcConnection.js.map +1 -1
- package/dist/repositories/organization.d.ts +9 -9
- package/dist/repositories/organization.d.ts.map +1 -1
- package/dist/repositories/organization.js +91 -37
- package/dist/repositories/organization.js.map +1 -1
- package/dist/repositories/permission.d.ts.map +1 -1
- package/dist/repositories/permission.js +27 -5
- package/dist/repositories/permission.js.map +1 -1
- package/dist/repositories/samlConnection.d.ts +28 -28
- package/dist/repositories/samlConnection.d.ts.map +1 -1
- package/dist/repositories/samlConnection.js +12 -4
- package/dist/repositories/samlConnection.js.map +1 -1
- package/dist/repositories/scimConnection.d.ts +38 -0
- package/dist/repositories/scimConnection.d.ts.map +1 -0
- package/dist/repositories/scimConnection.js +123 -0
- package/dist/repositories/scimConnection.js.map +1 -0
- package/dist/repositories/scimGroup.d.ts +36 -0
- package/dist/repositories/scimGroup.d.ts.map +1 -0
- package/dist/repositories/scimGroup.js +113 -0
- package/dist/repositories/scimGroup.js.map +1 -0
- package/dist/repositories/session.d.ts +23 -13
- package/dist/repositories/session.d.ts.map +1 -1
- package/dist/repositories/session.js +13 -1
- package/dist/repositories/session.js.map +1 -1
- package/dist/repositories/types.d.ts +112 -5
- package/dist/repositories/types.d.ts.map +1 -1
- package/dist/repositories/types.js +7 -1
- package/dist/repositories/types.js.map +1 -1
- package/dist/repositories/user.d.ts +23 -1
- package/dist/repositories/user.d.ts.map +1 -1
- package/dist/repositories/user.js +180 -4
- package/dist/repositories/user.js.map +1 -1
- package/dist/routes/admin/billing.js +4 -4
- package/dist/routes/admin/billing.js.map +1 -1
- package/dist/routes/admin/helpers.d.ts +16 -20
- package/dist/routes/admin/helpers.d.ts.map +1 -1
- package/dist/routes/admin/helpers.js +53 -10
- package/dist/routes/admin/helpers.js.map +1 -1
- package/dist/routes/admin/organizations.d.ts.map +1 -1
- package/dist/routes/admin/organizations.js +76 -69
- package/dist/routes/admin/organizations.js.map +1 -1
- package/dist/routes/admin/permissions.d.ts.map +1 -1
- package/dist/routes/admin/permissions.js +48 -14
- package/dist/routes/admin/permissions.js.map +1 -1
- package/dist/routes/admin/platform.d.ts.map +1 -1
- package/dist/routes/admin/platform.js +86 -35
- package/dist/routes/admin/platform.js.map +1 -1
- package/dist/routes/admin/sso.d.ts.map +1 -1
- package/dist/routes/admin/sso.js +143 -12
- package/dist/routes/admin/sso.js.map +1 -1
- package/dist/routes/admin/webhooks.js +8 -8
- package/dist/routes/admin/webhooks.js.map +1 -1
- package/dist/routes/apiKeys.d.ts.map +1 -1
- package/dist/routes/apiKeys.js +23 -5
- package/dist/routes/apiKeys.js.map +1 -1
- package/dist/routes/auth.d.ts.map +1 -1
- package/dist/routes/auth.js +112 -22
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/authz.d.ts.map +1 -1
- package/dist/routes/authz.js +16 -4
- package/dist/routes/authz.js.map +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +16 -14
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/federation.d.ts.map +1 -1
- package/dist/routes/federation.js +17 -3
- package/dist/routes/federation.js.map +1 -1
- package/dist/routes/magicLinks.d.ts.map +1 -1
- package/dist/routes/magicLinks.js +15 -3
- package/dist/routes/magicLinks.js.map +1 -1
- package/dist/routes/oauth.d.ts.map +1 -1
- package/dist/routes/oauth.js +22 -3
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/oauth2.d.ts.map +1 -1
- package/dist/routes/oauth2.js +104 -11
- package/dist/routes/oauth2.js.map +1 -1
- package/dist/routes/oidcEnterprise.d.ts.map +1 -1
- package/dist/routes/oidcEnterprise.js +67 -21
- package/dist/routes/oidcEnterprise.js.map +1 -1
- package/dist/routes/password.d.ts.map +1 -1
- package/dist/routes/password.js.map +1 -1
- package/dist/routes/profile.js +4 -4
- package/dist/routes/profile.js.map +1 -1
- package/dist/routes/saml.d.ts +9 -0
- package/dist/routes/saml.d.ts.map +1 -1
- package/dist/routes/saml.js +187 -11
- package/dist/routes/saml.js.map +1 -1
- package/dist/routes/scim.d.ts +13 -0
- package/dist/routes/scim.d.ts.map +1 -1
- package/dist/routes/scim.js +885 -110
- package/dist/routes/scim.js.map +1 -1
- package/dist/routes/sdk.d.ts.map +1 -1
- package/dist/routes/sdk.js +24 -8
- package/dist/routes/sdk.js.map +1 -1
- package/dist/routes/serviceAccounts.d.ts.map +1 -1
- package/dist/routes/serviceAccounts.js +2 -1
- package/dist/routes/serviceAccounts.js.map +1 -1
- package/dist/routes/setup.d.ts.map +1 -1
- package/dist/routes/setup.js +15 -16
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/smsOtp.js +1 -1
- package/dist/routes/smsOtp.js.map +1 -1
- package/dist/routes/totp.d.ts.map +1 -1
- package/dist/routes/totp.js +113 -23
- package/dist/routes/totp.js.map +1 -1
- package/dist/routes/webauthn.d.ts.map +1 -1
- package/dist/routes/webauthn.js +44 -8
- package/dist/routes/webauthn.js.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/routes/workflows.js +58 -25
- package/dist/routes/workflows.js.map +1 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +141 -40
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/types.d.ts +60 -29
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/services/application/authentication.d.ts +39 -1
- package/dist/services/application/authentication.d.ts.map +1 -1
- package/dist/services/application/authentication.js +43 -1
- package/dist/services/application/authentication.js.map +1 -1
- package/dist/services/application/identity.d.ts +10 -7
- package/dist/services/application/identity.d.ts.map +1 -1
- package/dist/services/application/identity.js +13 -10
- package/dist/services/application/identity.js.map +1 -1
- package/dist/services/application/organization.d.ts +11 -7
- package/dist/services/application/organization.d.ts.map +1 -1
- package/dist/services/application/organization.js +160 -12
- package/dist/services/application/organization.js.map +1 -1
- package/dist/services/applications.js +2 -2
- package/dist/services/applications.js.map +1 -1
- package/dist/services/audit.d.ts +1 -0
- package/dist/services/audit.d.ts.map +1 -1
- package/dist/services/audit.js +13 -0
- package/dist/services/audit.js.map +1 -1
- package/dist/services/cache.d.ts.map +1 -1
- package/dist/services/configuration/profiles.d.ts +4 -0
- package/dist/services/configuration/profiles.d.ts.map +1 -1
- package/dist/services/configuration/profiles.js +50 -0
- package/dist/services/configuration/profiles.js.map +1 -1
- package/dist/services/configuration.d.ts.map +1 -1
- package/dist/services/configuration.js.map +1 -1
- package/dist/services/connectors/google.d.ts.map +1 -1
- package/dist/services/connectors/google.js +3 -2
- package/dist/services/connectors/google.js.map +1 -1
- package/dist/services/connectors/oidc.d.ts.map +1 -1
- package/dist/services/connectors/oidc.js +6 -3
- package/dist/services/connectors/oidc.js.map +1 -1
- package/dist/services/connectors/zitadel.d.ts.map +1 -1
- package/dist/services/connectors/zitadel.js +4 -3
- package/dist/services/connectors/zitadel.js.map +1 -1
- package/dist/services/domain/authentication.d.ts +43 -4
- package/dist/services/domain/authentication.d.ts.map +1 -1
- package/dist/services/domain/authentication.js +82 -20
- package/dist/services/domain/authentication.js.map +1 -1
- package/dist/services/domain/authorization.d.ts +11 -0
- package/dist/services/domain/authorization.d.ts.map +1 -1
- package/dist/services/domain/authorization.js +36 -1
- package/dist/services/domain/authorization.js.map +1 -1
- package/dist/services/domain/identity.d.ts +21 -9
- package/dist/services/domain/identity.d.ts.map +1 -1
- package/dist/services/domain/identity.js +187 -10
- package/dist/services/domain/identity.js.map +1 -1
- package/dist/services/domain/organization.d.ts +6 -9
- package/dist/services/domain/organization.d.ts.map +1 -1
- package/dist/services/domain/organization.js +48 -30
- package/dist/services/domain/organization.js.map +1 -1
- package/dist/services/emailVerification.d.ts +2 -2
- package/dist/services/enterpriseSso.d.ts +13 -6
- package/dist/services/enterpriseSso.d.ts.map +1 -1
- package/dist/services/enterpriseSso.js +75 -6
- package/dist/services/enterpriseSso.js.map +1 -1
- package/dist/services/events/subscribers/auditLog.js +1 -1
- package/dist/services/events/subscribers/auditLog.js.map +1 -1
- package/dist/services/events/types.d.ts +7 -1
- package/dist/services/events/types.d.ts.map +1 -1
- package/dist/services/events/types.js.map +1 -1
- package/dist/services/events/validate.d.ts.map +1 -1
- package/dist/services/events/validate.js +27 -0
- package/dist/services/events/validate.js.map +1 -1
- package/dist/services/features.d.ts.map +1 -1
- package/dist/services/features.js.map +1 -1
- package/dist/services/federation.d.ts +6 -1
- package/dist/services/federation.d.ts.map +1 -1
- package/dist/services/federation.js +31 -4
- package/dist/services/federation.js.map +1 -1
- package/dist/services/ipControls.js +2 -2
- package/dist/services/ipControls.js.map +1 -1
- package/dist/services/magicLinks.d.ts +2 -2
- package/dist/services/mfa.d.ts +53 -0
- package/dist/services/mfa.d.ts.map +1 -0
- package/dist/services/mfa.js +157 -0
- package/dist/services/mfa.js.map +1 -0
- package/dist/services/oauth2.d.ts +16 -12
- package/dist/services/oauth2.d.ts.map +1 -1
- package/dist/services/oauth2.js +22 -13
- package/dist/services/oauth2.js.map +1 -1
- package/dist/services/oidcSecretFormat.d.ts +3 -0
- package/dist/services/oidcSecretFormat.d.ts.map +1 -0
- package/dist/services/oidcSecretFormat.js +5 -0
- package/dist/services/oidcSecretFormat.js.map +1 -0
- package/dist/services/organizations.d.ts +4 -16
- package/dist/services/organizations.d.ts.map +1 -1
- package/dist/services/organizations.js +0 -62
- package/dist/services/organizations.js.map +1 -1
- package/dist/services/permissions.d.ts.map +1 -1
- package/dist/services/permissions.js +9 -1
- package/dist/services/permissions.js.map +1 -1
- package/dist/services/plugins/registry.d.ts.map +1 -1
- package/dist/services/plugins/registry.js.map +1 -1
- package/dist/services/plugins/types.d.ts.map +1 -1
- package/dist/services/queue/bullmq.d.ts.map +1 -1
- package/dist/services/queue/inProcess.d.ts.map +1 -1
- package/dist/services/redis.d.ts +1 -1
- package/dist/services/redis.d.ts.map +1 -1
- package/dist/services/scimCredentials.d.ts +46 -0
- package/dist/services/scimCredentials.d.ts.map +1 -0
- package/dist/services/scimCredentials.js +123 -0
- package/dist/services/scimCredentials.js.map +1 -0
- package/dist/services/scimLegacyMigration.d.ts +12 -0
- package/dist/services/scimLegacyMigration.d.ts.map +1 -0
- package/dist/services/scimLegacyMigration.js +44 -0
- package/dist/services/scimLegacyMigration.js.map +1 -0
- package/dist/services/secrets/awsKms.d.ts +2 -2
- package/dist/services/secrets/awsKms.d.ts.map +1 -1
- package/dist/services/secrets/awsKms.js +3 -5
- package/dist/services/secrets/awsKms.js.map +1 -1
- package/dist/services/secrets/azureKeyVault.d.ts.map +1 -1
- package/dist/services/secrets/database.d.ts.map +1 -1
- package/dist/services/secrets/database.js.map +1 -1
- package/dist/services/secrets/environment.d.ts.map +1 -1
- package/dist/services/secrets/environment.js.map +1 -1
- package/dist/services/secrets/index.d.ts.map +1 -1
- package/dist/services/secrets/password.d.ts.map +1 -1
- package/dist/services/secrets/password.js +0 -4
- package/dist/services/secrets/password.js.map +1 -1
- package/dist/services/secrets/provider.d.ts +2 -3
- package/dist/services/secrets/provider.d.ts.map +1 -1
- package/dist/services/secrets/vault.d.ts.map +1 -1
- package/dist/services/setup/configWriter.d.ts.map +1 -1
- package/dist/services/setup/configWriter.js.map +1 -1
- package/dist/services/ssoEndpointPolicy.d.ts +8 -0
- package/dist/services/ssoEndpointPolicy.d.ts.map +1 -0
- package/dist/services/ssoEndpointPolicy.js +136 -0
- package/dist/services/ssoEndpointPolicy.js.map +1 -0
- package/dist/services/stepUp.d.ts +35 -0
- package/dist/services/stepUp.d.ts.map +1 -0
- package/dist/services/stepUp.js +65 -0
- package/dist/services/stepUp.js.map +1 -0
- package/dist/services/tokens.d.ts +37 -11
- package/dist/services/tokens.d.ts.map +1 -1
- package/dist/services/tokens.js +75 -14
- package/dist/services/tokens.js.map +1 -1
- package/dist/services/totp.d.ts +46 -0
- package/dist/services/totp.d.ts.map +1 -1
- package/dist/services/totp.js +178 -41
- package/dist/services/totp.js.map +1 -1
- package/dist/services/users.d.ts +20 -12
- package/dist/services/users.d.ts.map +1 -1
- package/dist/services/users.js +24 -51
- package/dist/services/users.js.map +1 -1
- package/dist/services/webauthn.d.ts +8 -4
- package/dist/services/webauthn.d.ts.map +1 -1
- package/dist/services/webauthn.js +4 -4
- package/dist/services/webauthn.js.map +1 -1
- package/dist/services/webhooks.d.ts +20 -20
- package/dist/services/workflows/engine.d.ts +4 -2
- package/dist/services/workflows/engine.d.ts.map +1 -1
- package/dist/services/workflows/engine.js +181 -12
- package/dist/services/workflows/engine.js.map +1 -1
- package/dist/services/workflows/steps.d.ts +2 -0
- package/dist/services/workflows/steps.d.ts.map +1 -1
- package/dist/services/workflows/steps.js +8 -75
- package/dist/services/workflows/steps.js.map +1 -1
- package/dist/setup-server.js.map +1 -1
- package/dist/types.d.ts +33 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +26 -2
- package/dist/types.js.map +1 -1
- package/package.json +26 -3
- package/dist/tests/errors.test.d.ts +0 -2
- package/dist/tests/errors.test.d.ts.map +0 -1
- package/dist/tests/errors.test.js +0 -64
- package/dist/tests/errors.test.js.map +0 -1
- package/dist/tests/events.test.d.ts +0 -2
- package/dist/tests/events.test.d.ts.map +0 -1
- package/dist/tests/events.test.js +0 -45
- package/dist/tests/events.test.js.map +0 -1
- package/dist/tests/featureFlags.test.d.ts +0 -2
- package/dist/tests/featureFlags.test.d.ts.map +0 -1
- package/dist/tests/featureFlags.test.js +0 -24
- package/dist/tests/featureFlags.test.js.map +0 -1
- package/dist/tests/integration/queue.test.d.ts +0 -2
- package/dist/tests/integration/queue.test.d.ts.map +0 -1
- package/dist/tests/integration/queue.test.js +0 -49
- package/dist/tests/integration/queue.test.js.map +0 -1
- package/dist/tests/plugins.test.d.ts +0 -2
- package/dist/tests/plugins.test.d.ts.map +0 -1
- package/dist/tests/plugins.test.js +0 -56
- package/dist/tests/plugins.test.js.map +0 -1
- package/dist/tests/result.test.d.ts +0 -2
- package/dist/tests/result.test.d.ts.map +0 -1
- package/dist/tests/result.test.js +0 -46
- package/dist/tests/result.test.js.map +0 -1
- package/dist/tests/setup/configWriter.test.d.ts +0 -2
- package/dist/tests/setup/configWriter.test.d.ts.map +0 -1
- package/dist/tests/setup/configWriter.test.js +0 -55
- package/dist/tests/setup/configWriter.test.js.map +0 -1
- package/dist/tests/setup/validation.test.d.ts +0 -2
- package/dist/tests/setup/validation.test.d.ts.map +0 -1
- package/dist/tests/setup/validation.test.js +0 -26
- package/dist/tests/setup/validation.test.js.map +0 -1
- package/dist/tests/smoke.test.d.ts +0 -2
- package/dist/tests/smoke.test.d.ts.map +0 -1
- package/dist/tests/smoke.test.js +0 -124
- package/dist/tests/smoke.test.js.map +0 -1
- package/src/cli.ts +0 -172
- package/src/config.ts +0 -143
- package/src/container.ts +0 -48
- package/src/db/index.ts +0 -53
- package/src/db/migrations/0000_skinny_ben_parker.sql +0 -553
- package/src/db/migrations/0001_brief_joseph.sql +0 -10
- package/src/db/migrations/0002_add_account_lockout.sql +0 -3
- package/src/db/migrations/0003_steep_rachel_grey.sql +0 -1
- package/src/db/migrations/0004_clever_bill_hollister.sql +0 -20
- package/src/db/migrations/0005_steep_the_call.sql +0 -2
- package/src/db/migrations/0006_ambitious_boomer.sql +0 -17
- package/src/db/migrations/0007_greedy_vindicator.sql +0 -40
- package/src/db/migrations/meta/0000_snapshot.json +0 -2964
- package/src/db/migrations/meta/0001_snapshot.json +0 -3031
- package/src/db/migrations/meta/0003_snapshot.json +0 -3051
- package/src/db/migrations/meta/_journal.json +0 -62
- package/src/db/schema.ts +0 -676
- package/src/db/seed.ts +0 -146
- package/src/di.ts +0 -81
- package/src/index.ts +0 -326
- package/src/lib/errors.ts +0 -139
- package/src/lib/result.ts +0 -44
- package/src/lib/webhookSignature.ts +0 -41
- package/src/plugins/appContext.ts +0 -76
- package/src/plugins/audit.ts +0 -42
- package/src/plugins/auth.ts +0 -230
- package/src/plugins/metrics.ts +0 -41
- package/src/plugins/mtls.ts +0 -94
- package/src/plugins/permissions.ts +0 -29
- package/src/plugins/rateLimit.ts +0 -133
- package/src/plugins/requestLogger.ts +0 -25
- package/src/plugins/tracing.ts +0 -22
- package/src/repositories/apiKey.ts +0 -158
- package/src/repositories/application.ts +0 -70
- package/src/repositories/audit.ts +0 -32
- package/src/repositories/index.ts +0 -9
- package/src/repositories/oidcConnection.ts +0 -111
- package/src/repositories/organization.ts +0 -167
- package/src/repositories/permission.ts +0 -153
- package/src/repositories/samlConnection.ts +0 -96
- package/src/repositories/session.ts +0 -86
- package/src/repositories/types.ts +0 -134
- package/src/repositories/user.ts +0 -161
- package/src/routes/admin/billing.ts +0 -48
- package/src/routes/admin/helpers.ts +0 -50
- package/src/routes/admin/index.ts +0 -16
- package/src/routes/admin/organizations.ts +0 -299
- package/src/routes/admin/permissions.ts +0 -80
- package/src/routes/admin/platform.ts +0 -335
- package/src/routes/admin/sso.ts +0 -138
- package/src/routes/admin/webhooks.ts +0 -73
- package/src/routes/admin.ts +0 -1
- package/src/routes/apiKeys.ts +0 -60
- package/src/routes/auth.ts +0 -214
- package/src/routes/authz.ts +0 -38
- package/src/routes/config.ts +0 -67
- package/src/routes/emailVerification.ts +0 -73
- package/src/routes/federation.ts +0 -156
- package/src/routes/helpers.ts +0 -17
- package/src/routes/magicLinks.ts +0 -107
- package/src/routes/oauth.ts +0 -167
- package/src/routes/oauth2.ts +0 -284
- package/src/routes/oidcEnterprise.ts +0 -166
- package/src/routes/password.ts +0 -90
- package/src/routes/profile.ts +0 -44
- package/src/routes/saml.ts +0 -209
- package/src/routes/scim.ts +0 -199
- package/src/routes/sdk.ts +0 -141
- package/src/routes/serviceAccounts.ts +0 -124
- package/src/routes/sessions.ts +0 -62
- package/src/routes/setup.ts +0 -315
- package/src/routes/smsOtp.ts +0 -67
- package/src/routes/totp.ts +0 -122
- package/src/routes/webauthn.ts +0 -147
- package/src/routes/workflows.ts +0 -157
- package/src/sdk/index.ts +0 -152
- package/src/sdk/types.ts +0 -54
- package/src/services/anomalyDetection.ts +0 -73
- package/src/services/application/authentication.ts +0 -75
- package/src/services/application/identity.ts +0 -56
- package/src/services/application/index.ts +0 -3
- package/src/services/application/organization.ts +0 -77
- package/src/services/applications.ts +0 -89
- package/src/services/audit.ts +0 -41
- package/src/services/auditExport.ts +0 -59
- package/src/services/billing.ts +0 -87
- package/src/services/cache.ts +0 -131
- package/src/services/configuration/profiles.ts +0 -115
- package/src/services/configuration.ts +0 -120
- package/src/services/connectors/google.ts +0 -92
- package/src/services/connectors/oidc.ts +0 -111
- package/src/services/connectors/registry.ts +0 -124
- package/src/services/connectors/types.ts +0 -38
- package/src/services/connectors/zitadel.ts +0 -77
- package/src/services/devices.ts +0 -76
- package/src/services/domain/authentication.ts +0 -279
- package/src/services/domain/authorization.ts +0 -49
- package/src/services/domain/identity.ts +0 -108
- package/src/services/domain/index.ts +0 -4
- package/src/services/domain/organization.ts +0 -114
- package/src/services/email.ts +0 -167
- package/src/services/emailVerification.ts +0 -75
- package/src/services/enterpriseSso.ts +0 -58
- package/src/services/events/bus.ts +0 -43
- package/src/services/events/subscribers/anomaly.ts +0 -13
- package/src/services/events/subscribers/auditLog.ts +0 -21
- package/src/services/events/subscribers/webhook.ts +0 -39
- package/src/services/events/subscribers/webhookDispatch.ts +0 -14
- package/src/services/events/types.ts +0 -106
- package/src/services/events/validate.ts +0 -118
- package/src/services/featureFlags.ts +0 -90
- package/src/services/features.ts +0 -22
- package/src/services/federation.ts +0 -30
- package/src/services/hibp.ts +0 -28
- package/src/services/ipControls.ts +0 -99
- package/src/services/magicLinks.ts +0 -51
- package/src/services/oauth2.ts +0 -216
- package/src/services/organizations.ts +0 -171
- package/src/services/permissions.ts +0 -146
- package/src/services/plugins/registry.ts +0 -242
- package/src/services/plugins/types.ts +0 -98
- package/src/services/queue/bullmq.ts +0 -104
- package/src/services/queue/inProcess.ts +0 -75
- package/src/services/queue/index.ts +0 -19
- package/src/services/queue/types.ts +0 -26
- package/src/services/redis.ts +0 -16
- package/src/services/secrets/awsKms.ts +0 -97
- package/src/services/secrets/azureKeyVault.ts +0 -171
- package/src/services/secrets/database.ts +0 -166
- package/src/services/secrets/environment.ts +0 -117
- package/src/services/secrets/index.ts +0 -50
- package/src/services/secrets/password.ts +0 -73
- package/src/services/secrets/provider.ts +0 -36
- package/src/services/secrets/vault.ts +0 -149
- package/src/services/secrets.ts +0 -2
- package/src/services/serviceAccounts.ts +0 -74
- package/src/services/setup/configWriter.ts +0 -200
- package/src/services/setup/diagnostics.ts +0 -76
- package/src/services/setup/token.ts +0 -42
- package/src/services/setup/types.ts +0 -40
- package/src/services/setup/validation.ts +0 -205
- package/src/services/sms.ts +0 -81
- package/src/services/smsOtp.ts +0 -74
- package/src/services/tokens.ts +0 -287
- package/src/services/totp.ts +0 -113
- package/src/services/users.ts +0 -239
- package/src/services/webauthn.ts +0 -193
- package/src/services/webhooks.ts +0 -190
- package/src/services/workflows/engine.ts +0 -125
- package/src/services/workflows/steps.ts +0 -129
- package/src/services/zitadel.ts +0 -277
- package/src/setup-server.ts +0 -71
- package/src/tests/errors.test.ts +0 -74
- package/src/tests/events.test.ts +0 -52
- package/src/tests/featureFlags.test.ts +0 -26
- package/src/tests/integration/queue.test.ts +0 -54
- package/src/tests/plugins.test.ts +0 -73
- package/src/tests/result.test.ts +0 -52
- package/src/tests/setup/configWriter.test.ts +0 -69
- package/src/tests/setup/validation.test.ts +0 -29
- package/src/tests/smoke.test.ts +0 -136
- package/src/types.ts +0 -70
package/dist/routes/scim.js
CHANGED
|
@@ -1,16 +1,111 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { hashScimToken } from "../services/scimCredentials.js";
|
|
3
|
+
import { rateLimit, isAllowed, clientAddress } from "../plugins/rateLimit.js";
|
|
4
|
+
import { config } from "../config.js";
|
|
5
|
+
import { emit } from "../services/events/bus.js";
|
|
6
|
+
const SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User";
|
|
7
|
+
const SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group";
|
|
8
|
+
const SCIM_LIST_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:ListResponse";
|
|
9
|
+
const SCIM_ERROR_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:Error";
|
|
10
|
+
/**
|
|
11
|
+
* SCIM 2.0 provisioning.
|
|
12
|
+
*
|
|
13
|
+
* Every request resolves to exactly one organization through its bearer
|
|
14
|
+
* credential, and every read and write below is filtered by that organization.
|
|
15
|
+
* There is no global SCIM configuration and no code path that looks a user or
|
|
16
|
+
* group up by id alone.
|
|
17
|
+
*
|
|
18
|
+
* Tenant rule for shared users: a user row is global, so an organization may
|
|
19
|
+
* only mutate global attributes for a user whose *only* membership is that
|
|
20
|
+
* organization. For a user who belongs elsewhere, SCIM manages the membership
|
|
21
|
+
* and refuses to touch the shared account. See `removeFromOrg`.
|
|
22
|
+
*/
|
|
23
|
+
const UserBodySchema = z.object({
|
|
24
|
+
userName: z.string().email().max(255),
|
|
25
|
+
name: z
|
|
26
|
+
.object({ givenName: z.string().max(255).optional(), familyName: z.string().max(255).optional() })
|
|
27
|
+
.optional(),
|
|
8
28
|
emails: z.array(z.object({ value: z.string().email(), primary: z.boolean().optional() })).optional(),
|
|
9
29
|
active: z.boolean().optional(),
|
|
30
|
+
externalId: z.string().max(255).optional(),
|
|
10
31
|
});
|
|
32
|
+
/** PATCH with `active` / `name.*` / `userName` operations, per RFC 7644 §3.5.2. */
|
|
33
|
+
const UserPatchSchema = z.object({
|
|
34
|
+
schemas: z.array(z.string()).optional(),
|
|
35
|
+
Operations: z
|
|
36
|
+
.array(z.object({
|
|
37
|
+
op: z.enum(["add", "replace", "remove"]),
|
|
38
|
+
path: z.string().optional(),
|
|
39
|
+
value: z.unknown().optional(),
|
|
40
|
+
}))
|
|
41
|
+
.min(1),
|
|
42
|
+
});
|
|
43
|
+
const GroupBodySchema = z.object({
|
|
44
|
+
displayName: z.string().min(1).max(255),
|
|
45
|
+
description: z.string().max(1024).optional(),
|
|
46
|
+
externalId: z.string().max(255).optional(),
|
|
47
|
+
members: z
|
|
48
|
+
.array(z.object({ value: z.string().min(1) }))
|
|
49
|
+
.optional(),
|
|
50
|
+
});
|
|
51
|
+
const GroupPatchSchema = z.object({
|
|
52
|
+
Operations: z
|
|
53
|
+
.array(z.object({
|
|
54
|
+
op: z.enum(["add", "replace", "remove"]),
|
|
55
|
+
path: z.string().optional(),
|
|
56
|
+
value: z.unknown().optional(),
|
|
57
|
+
}))
|
|
58
|
+
.min(1),
|
|
59
|
+
});
|
|
60
|
+
const MemberBodySchema = z.object({
|
|
61
|
+
value: z.string().min(1),
|
|
62
|
+
});
|
|
63
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
64
|
+
/**
|
|
65
|
+
* Read a path id, rejecting anything that is not a uuid before it reaches the
|
|
66
|
+
* database. A malformed id is reported as "not found" rather than "bad
|
|
67
|
+
* request", so the endpoint does not advertise how it validates identifiers.
|
|
68
|
+
*/
|
|
69
|
+
function pathId(request, key = "id") {
|
|
70
|
+
const value = request.params[key];
|
|
71
|
+
if (!value || !UUID_PATTERN.test(value))
|
|
72
|
+
throw new ScimNotFound("Resource not found");
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
const ListQuerySchema = z.object({
|
|
76
|
+
filter: z.string().optional(),
|
|
77
|
+
startIndex: z.coerce.number().int().min(1).optional(),
|
|
78
|
+
count: z.coerce.number().int().min(0).max(500).optional(),
|
|
79
|
+
});
|
|
80
|
+
/**
|
|
81
|
+
* Only attributes each handler genuinely implements. Adding one here without
|
|
82
|
+
* implementing it downstream would make clients believe a filter was honoured
|
|
83
|
+
* when it had been silently dropped.
|
|
84
|
+
*/
|
|
85
|
+
const USER_FILTER_ATTRIBUTES = new Set(["username"]);
|
|
86
|
+
const GROUP_FILTER_ATTRIBUTES = new Set(["displayname", "externalid"]);
|
|
87
|
+
/**
|
|
88
|
+
* Parse the single-attribute `eq` form that IdMs actually send, e.g.
|
|
89
|
+
* `userName eq "a@b.com"`.
|
|
90
|
+
*
|
|
91
|
+
* Anything outside `supported` — including operators this parser does not
|
|
92
|
+
* understand — is rejected, so a client is never handed an unfiltered listing
|
|
93
|
+
* while believing its filter was applied.
|
|
94
|
+
*/
|
|
95
|
+
export function parseEqFilter(filter, supported) {
|
|
96
|
+
if (!filter)
|
|
97
|
+
return { attribute: "", value: "" };
|
|
98
|
+
const match = /^\s*([A-Za-z][A-Za-z0-9_]*)\s+eq\s+"?([^"]*)"?\s*$/i.exec(filter);
|
|
99
|
+
if (!match)
|
|
100
|
+
return "unsupported";
|
|
101
|
+
const attribute = match[1].toLowerCase();
|
|
102
|
+
if (!supported.has(attribute))
|
|
103
|
+
return "unsupported";
|
|
104
|
+
return { attribute, value: match[2] };
|
|
105
|
+
}
|
|
11
106
|
function scimUserResponse(user) {
|
|
12
107
|
return {
|
|
13
|
-
schemas: [
|
|
108
|
+
schemas: [SCIM_USER_SCHEMA],
|
|
14
109
|
id: user.id,
|
|
15
110
|
userName: user.email,
|
|
16
111
|
name: {
|
|
@@ -18,66 +113,254 @@ function scimUserResponse(user) {
|
|
|
18
113
|
familyName: user.name?.split(" ").slice(1).join(" ") || "",
|
|
19
114
|
},
|
|
20
115
|
emails: [{ value: user.email, primary: true }],
|
|
21
|
-
active:
|
|
22
|
-
meta: {
|
|
23
|
-
resourceType: "User",
|
|
24
|
-
},
|
|
116
|
+
active: user.isActive !== false,
|
|
117
|
+
meta: { resourceType: "User" },
|
|
25
118
|
};
|
|
26
119
|
}
|
|
27
|
-
function scimGroupResponse(group) {
|
|
120
|
+
function scimGroupResponse(group, members) {
|
|
28
121
|
return {
|
|
29
|
-
schemas: [
|
|
122
|
+
schemas: [SCIM_GROUP_SCHEMA],
|
|
30
123
|
id: group.id,
|
|
31
124
|
displayName: group.displayName,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
})),
|
|
37
|
-
meta: {
|
|
38
|
-
resourceType: "Group",
|
|
39
|
-
},
|
|
125
|
+
...(group.description ? { description: group.description } : {}),
|
|
126
|
+
...(group.externalId ? { externalId: group.externalId } : {}),
|
|
127
|
+
members: members.map((m) => ({ value: m.value, display: m.display, $ref: `Users/${m.value}` })),
|
|
128
|
+
meta: { resourceType: "Group" },
|
|
40
129
|
};
|
|
41
130
|
}
|
|
42
|
-
function scimError(status, detail) {
|
|
131
|
+
function scimError(status, detail, scimType) {
|
|
43
132
|
return {
|
|
44
|
-
schemas: [
|
|
133
|
+
schemas: [SCIM_ERROR_SCHEMA],
|
|
45
134
|
status: String(status),
|
|
135
|
+
...(scimType ? { scimType } : {}),
|
|
46
136
|
detail,
|
|
47
137
|
};
|
|
48
138
|
}
|
|
139
|
+
/** Thrown for a cross-tenant or otherwise unauthorized target; surfaces as 404. */
|
|
140
|
+
class ScimNotFound extends Error {
|
|
141
|
+
}
|
|
142
|
+
/** Thrown when the target is real but this credential may not act on it. */
|
|
143
|
+
class ScimForbidden extends Error {
|
|
144
|
+
scimType;
|
|
145
|
+
constructor(scimType, message) {
|
|
146
|
+
super(message);
|
|
147
|
+
this.scimType = scimType;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
49
150
|
export default async function scimRoutes(app) {
|
|
151
|
+
// SCIM clients expect a SCIM `Error` object for every failure. Without this
|
|
152
|
+
// scoped handler a ZodError or a driver error would escape as a generic
|
|
153
|
+
// Fastify payload, and outside production the raw message would be returned.
|
|
154
|
+
app.setErrorHandler((error, _request, reply) => {
|
|
155
|
+
if (error instanceof ScimNotFound) {
|
|
156
|
+
return reply.status(404).send(scimError(404, error.message));
|
|
157
|
+
}
|
|
158
|
+
if (error instanceof ScimForbidden) {
|
|
159
|
+
return reply.status(409).send(scimError(409, error.message, error.scimType));
|
|
160
|
+
}
|
|
161
|
+
if (error && typeof error === "object" && error.name === "ZodError") {
|
|
162
|
+
return reply.status(400).send(scimError(400, "Invalid request", "invalidValue"));
|
|
163
|
+
}
|
|
164
|
+
// Postgres uuid cast failures are caused by a malformed path parameter.
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
if (message.includes("invalid input syntax for type uuid")) {
|
|
167
|
+
return reply.status(404).send(scimError(404, "Resource not found"));
|
|
168
|
+
}
|
|
169
|
+
console.error("[scim] unhandled error:", error);
|
|
170
|
+
return reply.status(500).send(scimError(500, "Internal error"));
|
|
171
|
+
});
|
|
172
|
+
/**
|
|
173
|
+
* Authenticate the credential and pin the request to one organization.
|
|
174
|
+
*
|
|
175
|
+
* The presented token is hashed and looked up by digest, so the comparison
|
|
176
|
+
* happens inside the database index rather than in application code and
|
|
177
|
+
* carries no timing signal about the secret.
|
|
178
|
+
*/
|
|
50
179
|
app.addHook("onRequest", async (request, reply) => {
|
|
180
|
+
// This hook runs before the per-credential limiter, and it emits audit and
|
|
181
|
+
// webhook events on failure. Unbounded unauthenticated traffic would
|
|
182
|
+
// therefore amplify writes, so budget it by client address first.
|
|
183
|
+
const guard = await isAllowed(`scim-auth:${clientAddress(request)}`, config.SCIM_AUTH_FAILURE_MAX, config.SCIM_AUTH_FAILURE_WINDOW_SECONDS);
|
|
184
|
+
if (!guard) {
|
|
185
|
+
return reply
|
|
186
|
+
.header("Retry-After", String(config.SCIM_AUTH_FAILURE_WINDOW_SECONDS))
|
|
187
|
+
.status(429)
|
|
188
|
+
.send(scimError(429, "Too many requests"));
|
|
189
|
+
}
|
|
51
190
|
const auth = request.headers.authorization;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return reply.status(
|
|
191
|
+
if (!auth || !auth.startsWith("Bearer ")) {
|
|
192
|
+
await emit({ type: "scim_authentication_failed", payload: { reason: "missing_bearer" } });
|
|
193
|
+
return reply.status(401).send(scimError(401, "Unauthorized"));
|
|
194
|
+
}
|
|
195
|
+
const presented = auth.slice(7).trim();
|
|
196
|
+
if (presented.length < 16) {
|
|
197
|
+
await emit({ type: "scim_authentication_failed", payload: { reason: "malformed_token" } });
|
|
198
|
+
return reply.status(401).send(scimError(401, "Unauthorized"));
|
|
55
199
|
}
|
|
56
|
-
|
|
200
|
+
const now = new Date();
|
|
201
|
+
const connection = await app.container.scimConnectionRepository.findByTokenHash(hashScimToken(presented), now);
|
|
202
|
+
if (!connection) {
|
|
203
|
+
await emit({ type: "scim_authentication_failed", payload: { reason: "unknown_or_revoked_token" } });
|
|
57
204
|
return reply.status(401).send(scimError(401, "Unauthorized"));
|
|
58
205
|
}
|
|
206
|
+
if (connection.expiresAt && connection.expiresAt <= now) {
|
|
207
|
+
await emit({
|
|
208
|
+
type: "scim_authentication_failed",
|
|
209
|
+
payload: { orgId: connection.orgId, connectionId: connection.id, reason: "expired" },
|
|
210
|
+
});
|
|
211
|
+
return reply.status(401).send(scimError(401, "Unauthorized"));
|
|
212
|
+
}
|
|
213
|
+
const organization = await app.container.organizationRepository.findById(connection.orgId);
|
|
214
|
+
if (!organization) {
|
|
215
|
+
await emit({
|
|
216
|
+
type: "scim_authentication_failed",
|
|
217
|
+
payload: { orgId: connection.orgId, connectionId: connection.id, reason: "org_missing" },
|
|
218
|
+
});
|
|
219
|
+
return reply.status(503).send(scimError(503, "SCIM organization not found"));
|
|
220
|
+
}
|
|
221
|
+
request.state.scimOrgId = connection.orgId;
|
|
222
|
+
request.state.scimConnectionId = connection.id;
|
|
223
|
+
request.state.org = organization;
|
|
224
|
+
await app.container.scimConnectionRepository.touch(connection.id, now);
|
|
225
|
+
});
|
|
226
|
+
// A dedicated budget, keyed on the credential, applied to every SCIM route.
|
|
227
|
+
const scimRateLimit = rateLimit({
|
|
228
|
+
keyPrefix: "scim",
|
|
229
|
+
maxAttempts: config.SCIM_RATE_LIMIT_MAX,
|
|
230
|
+
windowSeconds: config.SCIM_RATE_LIMIT_WINDOW_SECONDS,
|
|
231
|
+
keyFrom: (request) => request.state.scimConnectionId ?? request.ip,
|
|
59
232
|
});
|
|
60
|
-
|
|
61
|
-
|
|
233
|
+
/**
|
|
234
|
+
* Resolve a target inside the credential's organization, or fail closed.
|
|
235
|
+
*
|
|
236
|
+
* `forWrite` additionally refuses accounts SCIM must never change. Reading a
|
|
237
|
+
* platform owner who happens to be a member of the organization is
|
|
238
|
+
* legitimate; mutating one is not.
|
|
239
|
+
*/
|
|
240
|
+
async function requireOrgUser(request, userId, { forWrite = false } = {}) {
|
|
241
|
+
const orgId = request.state.scimOrgId;
|
|
242
|
+
const user = await app.container.userRepository.findByIdInOrg(orgId, userId);
|
|
243
|
+
if (!user) {
|
|
244
|
+
// Deliberately the same response for "does not exist" and "belongs to
|
|
245
|
+
// another organization", so the endpoint is not a tenant oracle.
|
|
246
|
+
throw new ScimNotFound("User not found");
|
|
247
|
+
}
|
|
248
|
+
if (!forWrite)
|
|
249
|
+
return user;
|
|
250
|
+
if (user.role === "owner") {
|
|
251
|
+
throw new ScimForbidden("mutability", "Platform owners cannot be modified through SCIM");
|
|
252
|
+
}
|
|
253
|
+
if (user.accountReviewRequired) {
|
|
254
|
+
throw new ScimForbidden("mutability", "This account is pending platform review and cannot be modified through SCIM");
|
|
255
|
+
}
|
|
256
|
+
return user;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* A user row is global. Writing global attributes for someone who also
|
|
260
|
+
* belongs to another organization would change that other tenant's view
|
|
261
|
+
* without any authorization from it, so SCIM refuses.
|
|
262
|
+
*/
|
|
263
|
+
async function assertSoleMembership(request, user, action) {
|
|
264
|
+
const orgIds = await app.container.userRepository.listOrgIdsForUser(user.id);
|
|
265
|
+
if (orgIds.length > 1) {
|
|
266
|
+
await emit({
|
|
267
|
+
type: "scim_access_denied",
|
|
268
|
+
payload: {
|
|
269
|
+
orgId: request.state.scimOrgId,
|
|
270
|
+
targetUserId: user.id,
|
|
271
|
+
reason: "shared_user",
|
|
272
|
+
action,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
throw new ScimForbidden("mutability", "This user also belongs to another organization; remove the membership from this organization instead of modifying the shared account");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function handleError(reply, error) {
|
|
279
|
+
if (error instanceof ScimNotFound) {
|
|
280
|
+
return reply.status(404).send(scimError(404, error.message));
|
|
281
|
+
}
|
|
282
|
+
if (error instanceof ScimForbidden) {
|
|
283
|
+
return reply.status(409).send(scimError(409, error.message, error.scimType));
|
|
284
|
+
}
|
|
285
|
+
if (error && typeof error === "object" && error.name === "ZodError") {
|
|
286
|
+
return reply.status(400).send(scimError(400, "Invalid request body"));
|
|
287
|
+
}
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
// --- Users -------------------------------------------------------------
|
|
291
|
+
app.get("/scim/v2/Users", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
292
|
+
const query = ListQuerySchema.parse(request.query ?? {});
|
|
293
|
+
const filter = parseEqFilter(query.filter, USER_FILTER_ATTRIBUTES);
|
|
294
|
+
if (filter === "unsupported") {
|
|
295
|
+
return reply.status(400).send(scimError(400, "Unsupported filter expression", "invalidFilter"));
|
|
296
|
+
}
|
|
297
|
+
const all = (await app.container.userRepository.listByOrg(request.state.scimOrgId)).filter((u) => u.isActive);
|
|
298
|
+
const matching = filter.attribute === "username"
|
|
299
|
+
? all.filter((u) => u.email.toLowerCase() === filter.value.toLowerCase())
|
|
300
|
+
: all;
|
|
301
|
+
const start = (query.startIndex ?? 1) - 1;
|
|
302
|
+
const count = query.count ?? matching.length;
|
|
62
303
|
return {
|
|
63
|
-
schemas: [
|
|
64
|
-
totalResults:
|
|
65
|
-
|
|
304
|
+
schemas: [SCIM_LIST_SCHEMA],
|
|
305
|
+
totalResults: matching.length,
|
|
306
|
+
startIndex: start + 1,
|
|
307
|
+
itemsPerPage: Math.max(0, Math.min(count, matching.length - start)),
|
|
308
|
+
Resources: matching.slice(start, start + count).map(scimUserResponse),
|
|
66
309
|
};
|
|
67
310
|
});
|
|
68
|
-
app.get("/scim/v2/Users/:userId", async (request, reply) => {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
311
|
+
app.get("/scim/v2/Users/:userId", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
312
|
+
const userId = pathId(request, "userId");
|
|
313
|
+
try {
|
|
314
|
+
return scimUserResponse(await requireOrgUser(request, userId));
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
return handleError(reply, error);
|
|
73
318
|
}
|
|
74
|
-
return scimUserResponse(user);
|
|
75
319
|
});
|
|
76
|
-
app.post("/scim/v2/Users", async (request, reply) => {
|
|
77
|
-
const body =
|
|
320
|
+
app.post("/scim/v2/Users", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
321
|
+
const body = UserBodySchema.parse(request.body);
|
|
322
|
+
const orgId = request.state.scimOrgId;
|
|
78
323
|
const email = body.userName.toLowerCase().trim();
|
|
79
|
-
|
|
324
|
+
// Resolve the target through the organization. `userName` is globally
|
|
325
|
+
// unique, so a user that exists elsewhere has to be classified before we
|
|
326
|
+
// can act: one with no memberships left was removed from *this*
|
|
327
|
+
// organization and can be re-provisioned, while one that still belongs
|
|
328
|
+
// to another organization must not be adopted.
|
|
329
|
+
const global = await app.container.userRepository.findByEmail(email);
|
|
330
|
+
const existing = global
|
|
331
|
+
? await app.container.userRepository.findByIdInOrg(orgId, global.id)
|
|
332
|
+
: undefined;
|
|
333
|
+
let user = existing;
|
|
80
334
|
let isNewUser = false;
|
|
335
|
+
let isReattached = false;
|
|
336
|
+
if (global && !existing) {
|
|
337
|
+
const otherOrgIds = await app.container.userRepository.listOrgIdsForUser(global.id);
|
|
338
|
+
if (otherOrgIds.length > 0) {
|
|
339
|
+
await emit({
|
|
340
|
+
type: "scim_access_denied",
|
|
341
|
+
payload: { orgId, reason: "user_exists_outside_organization" },
|
|
342
|
+
});
|
|
343
|
+
// Deliberately does not say which organization holds the account.
|
|
344
|
+
return reply
|
|
345
|
+
.status(409)
|
|
346
|
+
.send(scimError(409, "A user with that userName already exists", "uniqueness"));
|
|
347
|
+
}
|
|
348
|
+
// No memberships remain anywhere: this user was removed from this
|
|
349
|
+
// organization, so re-provisioning must be able to bring them back
|
|
350
|
+
// rather than 409 forever.
|
|
351
|
+
if (global.accountReviewRequired) {
|
|
352
|
+
return reply.status(409).send(scimError(409, "Account review must be completed by a platform owner", "mutability"));
|
|
353
|
+
}
|
|
354
|
+
if (global.role === "owner") {
|
|
355
|
+
return reply.status(409).send(scimError(409, "Platform owners cannot be provisioned through SCIM", "mutability"));
|
|
356
|
+
}
|
|
357
|
+
await app.container.organizationRepository.addMembership({ orgId, userId: global.id, role: "member" });
|
|
358
|
+
if (!global.isActive) {
|
|
359
|
+
await app.container.userRepository.updateInOrg(orgId, global.id, { isActive: true });
|
|
360
|
+
}
|
|
361
|
+
user = (await app.container.userRepository.findByIdInOrg(orgId, global.id));
|
|
362
|
+
isReattached = true;
|
|
363
|
+
}
|
|
81
364
|
if (!user) {
|
|
82
365
|
const base = email.split("@")[0];
|
|
83
366
|
const username = await app.container.userRepository.ensureUniqueUsername(base);
|
|
@@ -91,85 +374,577 @@ export default async function scimRoutes(app) {
|
|
|
91
374
|
provider: "scim",
|
|
92
375
|
emailVerified: true,
|
|
93
376
|
});
|
|
377
|
+
await app.container.organizationRepository.addMembership({
|
|
378
|
+
orgId,
|
|
379
|
+
userId: user.id,
|
|
380
|
+
role: "member",
|
|
381
|
+
});
|
|
94
382
|
isNewUser = true;
|
|
95
383
|
}
|
|
96
|
-
|
|
384
|
+
if (!isNewUser && !isReattached) {
|
|
385
|
+
// IdMs reconcile with POST, so an existing member must be updated too.
|
|
386
|
+
await assertSoleMembership(request, user, "update");
|
|
387
|
+
user =
|
|
388
|
+
(await app.container.userRepository.updateInOrg(orgId, user.id, {
|
|
389
|
+
...(body.name
|
|
390
|
+
? { name: `${body.name.givenName || ""} ${body.name.familyName || ""}`.trim() }
|
|
391
|
+
: {}),
|
|
392
|
+
...(body.active === undefined ? {} : { isActive: body.active }),
|
|
393
|
+
})) ?? user;
|
|
394
|
+
}
|
|
395
|
+
if (body.active === false && user.isActive) {
|
|
396
|
+
const removed = await app.container.userRepository.removeFromOrg(orgId, user.id);
|
|
397
|
+
if (!removed || removed.outcome === "not_found") {
|
|
398
|
+
return reply.status(404).send(scimError(404, "User not found"));
|
|
399
|
+
}
|
|
400
|
+
if (removed.outcome === "last_owner") {
|
|
401
|
+
return reply.status(409).send(scimError(409, "The last platform owner cannot be deactivated", "mutability"));
|
|
402
|
+
}
|
|
403
|
+
user = { ...user, isActive: false };
|
|
404
|
+
}
|
|
405
|
+
else if (body.active === true && !user.isActive) {
|
|
406
|
+
if (user.accountReviewRequired) {
|
|
407
|
+
return reply.status(409).send(scimError(409, "Account review must be completed by a platform owner", "mutability"));
|
|
408
|
+
}
|
|
409
|
+
// Reactivating a shared account would hand someone access to the other
|
|
410
|
+
// organizations they belong to, without those organizations' consent.
|
|
411
|
+
await assertSoleMembership(request, user, "reactivate");
|
|
412
|
+
user = (await app.container.userRepository.updateInOrg(orgId, user.id, { isActive: true })) ?? user;
|
|
413
|
+
}
|
|
414
|
+
await request.audit(isNewUser ? "scim_user_created" : "scim_user_updated", {
|
|
415
|
+
targetUserId: user.id,
|
|
416
|
+
email: user.email,
|
|
417
|
+
actorType: "scim",
|
|
418
|
+
...(isReattached ? { reattached: true } : {}),
|
|
419
|
+
scimConnectionId: request.state.scimConnectionId,
|
|
420
|
+
scimOrgId: orgId,
|
|
421
|
+
});
|
|
97
422
|
return reply.status(201).send(scimUserResponse(user));
|
|
98
423
|
});
|
|
99
|
-
app.put("/scim/v2/Users/:userId", async (request, reply) => {
|
|
100
|
-
const
|
|
101
|
-
const body =
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
424
|
+
app.put("/scim/v2/Users/:userId", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
425
|
+
const userId = pathId(request, "userId");
|
|
426
|
+
const body = UserBodySchema.parse(request.body);
|
|
427
|
+
const orgId = request.state.scimOrgId;
|
|
428
|
+
try {
|
|
429
|
+
const existing = await requireOrgUser(request, userId, { forWrite: true });
|
|
430
|
+
if (body.active === false) {
|
|
431
|
+
const removed = await app.container.userRepository.removeFromOrg(orgId, existing.id);
|
|
432
|
+
if (!removed || removed.outcome === "not_found") {
|
|
433
|
+
return reply.status(404).send(scimError(404, "User not found"));
|
|
434
|
+
}
|
|
435
|
+
if (removed.outcome === "last_owner") {
|
|
436
|
+
return reply.status(409).send(scimError(409, "The last platform owner cannot be deactivated", "mutability"));
|
|
437
|
+
}
|
|
438
|
+
// The membership is gone, so re-reading through the organization
|
|
439
|
+
// would 404. Report the deactivation from the resolved user instead.
|
|
440
|
+
await request.audit("scim_user_updated", {
|
|
441
|
+
targetUserId: existing.id,
|
|
442
|
+
email: existing.email,
|
|
443
|
+
actorType: "scim",
|
|
444
|
+
operation: "put",
|
|
445
|
+
outcome: removed.outcome,
|
|
446
|
+
scimConnectionId: request.state.scimConnectionId,
|
|
447
|
+
scimOrgId: orgId,
|
|
448
|
+
});
|
|
449
|
+
return scimUserResponse({ ...existing, isActive: false });
|
|
450
|
+
}
|
|
451
|
+
{
|
|
452
|
+
await assertSoleMembership(request, existing, "update");
|
|
453
|
+
const updated = await app.container.userRepository.updateInOrg(orgId, existing.id, {
|
|
454
|
+
email: body.userName.toLowerCase().trim(),
|
|
455
|
+
name: body.name
|
|
456
|
+
? `${body.name.givenName || ""} ${body.name.familyName || ""}`.trim()
|
|
457
|
+
: undefined,
|
|
458
|
+
isActive: body.active,
|
|
459
|
+
});
|
|
460
|
+
if (!updated)
|
|
461
|
+
return reply.status(404).send(scimError(404, "User not found"));
|
|
462
|
+
}
|
|
463
|
+
const updated = await app.container.userRepository.findByIdInOrg(orgId, userId);
|
|
464
|
+
if (!updated)
|
|
465
|
+
return reply.status(404).send(scimError(404, "User not found"));
|
|
466
|
+
await request.audit("scim_user_updated", {
|
|
467
|
+
targetUserId: updated.id,
|
|
468
|
+
email: updated.email,
|
|
469
|
+
actorType: "scim",
|
|
470
|
+
scimConnectionId: request.state.scimConnectionId,
|
|
471
|
+
scimOrgId: orgId,
|
|
472
|
+
});
|
|
473
|
+
return scimUserResponse(updated);
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
return handleError(reply, error);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
app.patch("/scim/v2/Users/:userId", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
480
|
+
const userId = pathId(request, "userId");
|
|
481
|
+
const body = UserPatchSchema.parse(request.body);
|
|
482
|
+
const orgId = request.state.scimOrgId;
|
|
483
|
+
try {
|
|
484
|
+
const existing = await requireOrgUser(request, userId, { forWrite: true });
|
|
485
|
+
const patch = collapsePatch(body.Operations);
|
|
486
|
+
const nameValue = patch.name;
|
|
487
|
+
const nameParts = nameValue && typeof nameValue === "object" && !Array.isArray(nameValue)
|
|
488
|
+
? nameValue
|
|
489
|
+
: undefined;
|
|
490
|
+
const name = nameParts
|
|
491
|
+
? {
|
|
492
|
+
givenName: typeof nameParts.givenName === "string" ? nameParts.givenName : undefined,
|
|
493
|
+
familyName: typeof nameParts.familyName === "string" ? nameParts.familyName : undefined,
|
|
494
|
+
}
|
|
495
|
+
: undefined;
|
|
496
|
+
const activePatch = typeof patch.active === "boolean" ? patch.active : undefined;
|
|
497
|
+
if (activePatch === false) {
|
|
498
|
+
const removed = await app.container.userRepository.removeFromOrg(orgId, existing.id);
|
|
499
|
+
if (!removed || removed.outcome === "not_found") {
|
|
500
|
+
return reply.status(404).send(scimError(404, "User not found"));
|
|
501
|
+
}
|
|
502
|
+
if (removed.outcome === "last_owner") {
|
|
503
|
+
return reply.status(409).send(scimError(409, "The last platform owner cannot be deactivated", "mutability"));
|
|
504
|
+
}
|
|
505
|
+
await request.audit("scim_user_updated", {
|
|
506
|
+
targetUserId: existing.id,
|
|
507
|
+
email: existing.email,
|
|
508
|
+
actorType: "scim",
|
|
509
|
+
operation: "patch",
|
|
510
|
+
outcome: removed.outcome,
|
|
511
|
+
scimConnectionId: request.state.scimConnectionId,
|
|
512
|
+
scimOrgId: orgId,
|
|
513
|
+
});
|
|
514
|
+
return scimUserResponse({ ...existing, isActive: false });
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
await assertSoleMembership(request, existing, "patch");
|
|
518
|
+
const updated = await app.container.userRepository.updateInOrg(orgId, existing.id, {
|
|
519
|
+
...(patch.userName ? { email: String(patch.userName).toLowerCase().trim() } : {}),
|
|
520
|
+
...(name ? { name: `${name.givenName || ""} ${name.familyName || ""}`.trim() } : {}),
|
|
521
|
+
...(activePatch === undefined ? {} : { isActive: activePatch }),
|
|
522
|
+
});
|
|
523
|
+
if (!updated)
|
|
524
|
+
return reply.status(404).send(scimError(404, "User not found"));
|
|
525
|
+
}
|
|
526
|
+
const updated = await app.container.userRepository.findByIdInOrg(orgId, userId);
|
|
527
|
+
if (!updated)
|
|
528
|
+
return reply.status(404).send(scimError(404, "User not found"));
|
|
529
|
+
await request.audit("scim_user_updated", {
|
|
530
|
+
targetUserId: updated.id,
|
|
531
|
+
email: updated.email,
|
|
532
|
+
actorType: "scim",
|
|
533
|
+
operation: "patch",
|
|
534
|
+
scimConnectionId: request.state.scimConnectionId,
|
|
535
|
+
scimOrgId: orgId,
|
|
145
536
|
});
|
|
537
|
+
return scimUserResponse(updated);
|
|
146
538
|
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
return handleError(reply, error);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
app.delete("/scim/v2/Users/:userId", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
544
|
+
const userId = pathId(request, "userId");
|
|
545
|
+
const orgId = request.state.scimOrgId;
|
|
546
|
+
try {
|
|
547
|
+
const user = await requireOrgUser(request, userId, { forWrite: true });
|
|
548
|
+
const result = await app.container.userRepository.removeFromOrg(orgId, user.id);
|
|
549
|
+
if (!result || result.outcome === "not_found") {
|
|
550
|
+
return reply.status(404).send(scimError(404, "User not found"));
|
|
551
|
+
}
|
|
552
|
+
if (result.outcome === "last_owner") {
|
|
553
|
+
return reply.status(409).send(scimError(409, "The last platform owner cannot be deactivated", "mutability"));
|
|
554
|
+
}
|
|
555
|
+
await request.audit("scim_user_deleted", {
|
|
556
|
+
targetUserId: user.id,
|
|
557
|
+
email: user.email,
|
|
558
|
+
actorType: "scim",
|
|
559
|
+
outcome: result.outcome,
|
|
560
|
+
scimConnectionId: request.state.scimConnectionId,
|
|
561
|
+
scimOrgId: orgId,
|
|
562
|
+
});
|
|
563
|
+
return reply.status(204).send();
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
return handleError(reply, error);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
// --- Groups ------------------------------------------------------------
|
|
570
|
+
async function loadGroup(request, groupId) {
|
|
571
|
+
const group = await app.container.scimGroupRepository.findByIdInOrg(request.state.scimOrgId, groupId);
|
|
572
|
+
// A group from another organization is reported as missing, not forbidden.
|
|
573
|
+
if (!group)
|
|
574
|
+
throw new ScimNotFound("Group not found");
|
|
575
|
+
return group;
|
|
576
|
+
}
|
|
577
|
+
async function groupWithMembers(request, groupId) {
|
|
578
|
+
const group = await loadGroup(request, groupId);
|
|
579
|
+
const members = await app.container.scimGroupRepository.listMembers(request.state.scimOrgId, group.id);
|
|
580
|
+
return scimGroupResponse(group, members.map((m) => ({ value: m.userId, display: m.email })));
|
|
581
|
+
}
|
|
582
|
+
/** Only users who are members of this credential's organization can join. */
|
|
583
|
+
async function assertMemberOfOrg(request, userId) {
|
|
584
|
+
const user = await app.container.userRepository.findByIdInOrg(request.state.scimOrgId, userId);
|
|
585
|
+
if (!user)
|
|
586
|
+
throw new ScimNotFound("User not found");
|
|
587
|
+
}
|
|
588
|
+
app.get("/scim/v2/Groups", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
589
|
+
const query = ListQuerySchema.parse(request.query ?? {});
|
|
590
|
+
const filter = parseEqFilter(query.filter, GROUP_FILTER_ATTRIBUTES);
|
|
591
|
+
if (filter === "unsupported") {
|
|
592
|
+
return reply.status(400).send(scimError(400, "Unsupported filter expression", "invalidFilter"));
|
|
593
|
+
}
|
|
594
|
+
const orgId = request.state.scimOrgId;
|
|
595
|
+
let groups = await app.container.scimGroupRepository.listByOrg(orgId);
|
|
596
|
+
if (filter.attribute === "displayname") {
|
|
597
|
+
const wanted = filter.value.toLowerCase();
|
|
598
|
+
groups = groups.filter((g) => g.displayName.toLowerCase() === wanted);
|
|
599
|
+
}
|
|
600
|
+
else if (filter.attribute === "externalid") {
|
|
601
|
+
const wanted = filter.value;
|
|
602
|
+
groups = groups.filter((g) => g.externalId === wanted);
|
|
603
|
+
}
|
|
604
|
+
const start = (query.startIndex ?? 1) - 1;
|
|
605
|
+
const count = query.count ?? groups.length;
|
|
606
|
+
const page = groups.slice(start, start + count);
|
|
607
|
+
const Resources = await Promise.all(page.map(async (group) => {
|
|
608
|
+
const members = await app.container.scimGroupRepository.listMembers(orgId, group.id);
|
|
609
|
+
return scimGroupResponse(group, members.map((m) => ({ value: m.userId, display: m.email })));
|
|
610
|
+
}));
|
|
147
611
|
return {
|
|
148
|
-
schemas: [
|
|
612
|
+
schemas: [SCIM_LIST_SCHEMA],
|
|
149
613
|
totalResults: groups.length,
|
|
150
|
-
|
|
614
|
+
startIndex: start + 1,
|
|
615
|
+
itemsPerPage: Resources.length,
|
|
616
|
+
Resources,
|
|
151
617
|
};
|
|
152
618
|
});
|
|
153
|
-
app.get("/scim/v2/Groups/:groupId", async (request, reply) => {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
619
|
+
app.get("/scim/v2/Groups/:groupId", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
620
|
+
const groupId = pathId(request, "groupId");
|
|
621
|
+
try {
|
|
622
|
+
return await groupWithMembers(request, groupId);
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
return handleError(reply, error);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
app.post("/scim/v2/Groups", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
629
|
+
const body = GroupBodySchema.parse(request.body);
|
|
630
|
+
const orgId = request.state.scimOrgId;
|
|
631
|
+
try {
|
|
632
|
+
const group = await app.container.scimGroupRepository.create({
|
|
633
|
+
orgId,
|
|
634
|
+
displayName: body.displayName,
|
|
635
|
+
description: body.description ?? null,
|
|
636
|
+
externalId: body.externalId ?? null,
|
|
637
|
+
});
|
|
638
|
+
for (const member of body.members ?? []) {
|
|
639
|
+
await assertMemberOfOrg(request, member.value);
|
|
640
|
+
await app.container.scimGroupRepository.addMember(orgId, group.id, member.value);
|
|
641
|
+
}
|
|
642
|
+
await request.audit("scim_group_created", {
|
|
643
|
+
groupId: group.id,
|
|
644
|
+
displayName: group.displayName,
|
|
645
|
+
actorType: "scim",
|
|
646
|
+
scimConnectionId: request.state.scimConnectionId,
|
|
647
|
+
scimOrgId: orgId,
|
|
648
|
+
});
|
|
649
|
+
return reply.status(201).send(await groupWithMembers(request, group.id));
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
return handleError(reply, error);
|
|
653
|
+
}
|
|
173
654
|
});
|
|
655
|
+
app.put("/scim/v2/Groups/:groupId", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
656
|
+
const groupId = pathId(request, "groupId");
|
|
657
|
+
const body = GroupBodySchema.parse(request.body);
|
|
658
|
+
const orgId = request.state.scimOrgId;
|
|
659
|
+
try {
|
|
660
|
+
const existing = await loadGroup(request, groupId);
|
|
661
|
+
await app.container.scimGroupRepository.updateInOrg(orgId, existing.id, {
|
|
662
|
+
displayName: body.displayName,
|
|
663
|
+
description: body.description ?? null,
|
|
664
|
+
externalId: body.externalId ?? null,
|
|
665
|
+
});
|
|
666
|
+
if (body.members) {
|
|
667
|
+
for (const member of body.members) {
|
|
668
|
+
await assertMemberOfOrg(request, member.value);
|
|
669
|
+
await app.container.scimGroupRepository.addMember(orgId, existing.id, member.value);
|
|
670
|
+
}
|
|
671
|
+
const current = await app.container.scimGroupRepository.listMembers(orgId, existing.id);
|
|
672
|
+
const wanted = new Set(body.members.map((m) => m.value));
|
|
673
|
+
for (const member of current) {
|
|
674
|
+
if (!wanted.has(member.userId)) {
|
|
675
|
+
await app.container.scimGroupRepository.removeMember(orgId, existing.id, member.userId);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
await request.audit("scim_group_updated", {
|
|
680
|
+
groupId: existing.id,
|
|
681
|
+
actorType: "scim",
|
|
682
|
+
operation: "put",
|
|
683
|
+
scimConnectionId: request.state.scimConnectionId,
|
|
684
|
+
scimOrgId: orgId,
|
|
685
|
+
});
|
|
686
|
+
return await groupWithMembers(request, existing.id);
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
return handleError(reply, error);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
app.patch("/scim/v2/Groups/:groupId", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
693
|
+
const groupId = pathId(request, "groupId");
|
|
694
|
+
const body = GroupPatchSchema.parse(request.body);
|
|
695
|
+
const orgId = request.state.scimOrgId;
|
|
696
|
+
try {
|
|
697
|
+
const existing = await loadGroup(request, groupId);
|
|
698
|
+
const patch = collapsePatch(body.Operations);
|
|
699
|
+
if (typeof patch.displayName === "string") {
|
|
700
|
+
await app.container.scimGroupRepository.updateInOrg(orgId, existing.id, {
|
|
701
|
+
displayName: patch.displayName,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
if (typeof patch.description === "string") {
|
|
705
|
+
await app.container.scimGroupRepository.updateInOrg(orgId, existing.id, {
|
|
706
|
+
description: patch.description,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
const members = Array.isArray(patch.members) ? patch.members : undefined;
|
|
710
|
+
if (members) {
|
|
711
|
+
for (const entry of members) {
|
|
712
|
+
const value = typeof entry === "string" ? entry : entry?.value;
|
|
713
|
+
if (typeof value !== "string")
|
|
714
|
+
continue;
|
|
715
|
+
await assertMemberOfOrg(request, value);
|
|
716
|
+
await app.container.scimGroupRepository.addMember(orgId, existing.id, value);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const removeMembers = Array.isArray(patch["members.remove"])
|
|
720
|
+
? patch["members.remove"].map((entry) => (typeof entry === "string" ? entry : entry?.value))
|
|
721
|
+
: undefined;
|
|
722
|
+
for (const value of removeMembers ?? []) {
|
|
723
|
+
if (typeof value === "string") {
|
|
724
|
+
await app.container.scimGroupRepository.removeMember(orgId, existing.id, value);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
await request.audit("scim_group_updated", {
|
|
728
|
+
groupId: existing.id,
|
|
729
|
+
actorType: "scim",
|
|
730
|
+
operation: "patch",
|
|
731
|
+
scimConnectionId: request.state.scimConnectionId,
|
|
732
|
+
scimOrgId: orgId,
|
|
733
|
+
});
|
|
734
|
+
return await groupWithMembers(request, existing.id);
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
return handleError(reply, error);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
app.delete("/scim/v2/Groups/:groupId", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
741
|
+
const groupId = pathId(request, "groupId");
|
|
742
|
+
const orgId = request.state.scimOrgId;
|
|
743
|
+
try {
|
|
744
|
+
const group = await loadGroup(request, groupId);
|
|
745
|
+
const removed = await app.container.scimGroupRepository.deleteInOrg(orgId, group.id);
|
|
746
|
+
if (!removed)
|
|
747
|
+
return reply.status(404).send(scimError(404, "Group not found"));
|
|
748
|
+
await request.audit("scim_group_deleted", {
|
|
749
|
+
groupId: group.id,
|
|
750
|
+
displayName: group.displayName,
|
|
751
|
+
actorType: "scim",
|
|
752
|
+
scimConnectionId: request.state.scimConnectionId,
|
|
753
|
+
scimOrgId: orgId,
|
|
754
|
+
});
|
|
755
|
+
return reply.status(204).send();
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
return handleError(reply, error);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
app.get("/scim/v2/Groups/:groupId/members", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
762
|
+
const groupId = pathId(request, "groupId");
|
|
763
|
+
try {
|
|
764
|
+
const group = await loadGroup(request, groupId);
|
|
765
|
+
const members = await app.container.scimGroupRepository.listMembers(request.state.scimOrgId, group.id);
|
|
766
|
+
return {
|
|
767
|
+
schemas: [SCIM_LIST_SCHEMA],
|
|
768
|
+
totalResults: members.length,
|
|
769
|
+
Resources: members.map((m) => ({ value: m.userId, display: m.email, $ref: `Users/${m.userId}` })),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
return handleError(reply, error);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
app.post("/scim/v2/Groups/:groupId/members", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
777
|
+
const groupId = pathId(request, "groupId");
|
|
778
|
+
const body = z.union([MemberBodySchema, z.object({ members: z.array(MemberBodySchema) })]).parse(request.body);
|
|
779
|
+
const orgId = request.state.scimOrgId;
|
|
780
|
+
try {
|
|
781
|
+
const group = await loadGroup(request, groupId);
|
|
782
|
+
const values = "value" in body ? [body.value] : body.members.map((m) => m.value);
|
|
783
|
+
let added = 0;
|
|
784
|
+
for (const value of values) {
|
|
785
|
+
await assertMemberOfOrg(request, value);
|
|
786
|
+
if (await app.container.scimGroupRepository.addMember(orgId, group.id, value))
|
|
787
|
+
added++;
|
|
788
|
+
}
|
|
789
|
+
if (added === 0)
|
|
790
|
+
return reply.status(409).send(scimError(409, "No new members were added"));
|
|
791
|
+
await request.audit("scim_group_updated", {
|
|
792
|
+
groupId: group.id,
|
|
793
|
+
actorType: "scim",
|
|
794
|
+
operation: "add_members",
|
|
795
|
+
added,
|
|
796
|
+
scimConnectionId: request.state.scimConnectionId,
|
|
797
|
+
scimOrgId: orgId,
|
|
798
|
+
});
|
|
799
|
+
return reply.status(201).send(await groupWithMembers(request, group.id));
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
return handleError(reply, error);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
app.delete("/scim/v2/Groups/:groupId/members/:userId", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
806
|
+
const groupId = pathId(request, "groupId");
|
|
807
|
+
const userId = pathId(request, "userId");
|
|
808
|
+
const orgId = request.state.scimOrgId;
|
|
809
|
+
try {
|
|
810
|
+
const group = await loadGroup(request, groupId);
|
|
811
|
+
const removed = await app.container.scimGroupRepository.removeMember(orgId, group.id, userId);
|
|
812
|
+
if (!removed)
|
|
813
|
+
return reply.status(404).send(scimError(404, "Member not found"));
|
|
814
|
+
await request.audit("scim_group_updated", {
|
|
815
|
+
groupId: group.id,
|
|
816
|
+
actorType: "scim",
|
|
817
|
+
operation: "remove_member",
|
|
818
|
+
targetUserId: userId,
|
|
819
|
+
scimConnectionId: request.state.scimConnectionId,
|
|
820
|
+
scimOrgId: orgId,
|
|
821
|
+
});
|
|
822
|
+
return reply.status(204).send();
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
return handleError(reply, error);
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
// SCIM service provider configuration, scoped to the authenticated tenant.
|
|
829
|
+
app.get("/scim/v2/ServiceProviderConfig", { preHandler: [scimRateLimit] }, async (request) => {
|
|
830
|
+
const base = `${request.protocol}://${request.hostname}`;
|
|
831
|
+
return {
|
|
832
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
|
|
833
|
+
documentationUri: "https://docs.hilbras.ai/keystone/scim",
|
|
834
|
+
patch: { supported: true },
|
|
835
|
+
bulk: { supported: false, maxOperations: 0, maxPayloadSize: 0 },
|
|
836
|
+
filter: { supported: true, maxResults: 500 },
|
|
837
|
+
changePassword: { supported: false },
|
|
838
|
+
sort: { supported: false },
|
|
839
|
+
etag: { supported: false },
|
|
840
|
+
authenticationSchemes: [
|
|
841
|
+
{
|
|
842
|
+
type: "oauthbearertoken",
|
|
843
|
+
name: "OAuth Bearer Token",
|
|
844
|
+
description: "Per-organization SCIM credential",
|
|
845
|
+
specUri: "http://www.rfc-editor.org/info/rfc6750",
|
|
846
|
+
primary: true,
|
|
847
|
+
},
|
|
848
|
+
],
|
|
849
|
+
meta: {
|
|
850
|
+
resourceType: "ServiceProviderConfig",
|
|
851
|
+
organization: `${base} (${request.state.scimOrgId})`,
|
|
852
|
+
location: `${base}/scim/v2/ServiceProviderConfig`,
|
|
853
|
+
},
|
|
854
|
+
};
|
|
855
|
+
});
|
|
856
|
+
app.get("/scim/v2/ResourceTypes", { preHandler: [scimRateLimit] }, async () => [
|
|
857
|
+
{
|
|
858
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
|
|
859
|
+
id: "User",
|
|
860
|
+
name: "User",
|
|
861
|
+
endpoint: "/Users",
|
|
862
|
+
schema: SCIM_USER_SCHEMA,
|
|
863
|
+
meta: { resourceType: "ResourceType", location: "/ResourceTypes/User" },
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
|
|
867
|
+
id: "Group",
|
|
868
|
+
name: "Group",
|
|
869
|
+
endpoint: "/Groups",
|
|
870
|
+
schema: SCIM_GROUP_SCHEMA,
|
|
871
|
+
meta: { resourceType: "ResourceType", location: "/ResourceTypes/Group" },
|
|
872
|
+
},
|
|
873
|
+
]);
|
|
874
|
+
// `.search` is the POST form of the list endpoints, required by RFC 7644 §3.4.3.
|
|
875
|
+
app.post("/scim/v2/Users/.search", { preHandler: [scimRateLimit] }, async (request, reply) => {
|
|
876
|
+
const body = z
|
|
877
|
+
.object({
|
|
878
|
+
filter: z.string().optional(),
|
|
879
|
+
startIndex: z.number().int().min(1).optional(),
|
|
880
|
+
count: z.number().int().min(0).max(500).optional(),
|
|
881
|
+
})
|
|
882
|
+
.parse(request.body ?? {});
|
|
883
|
+
const filter = parseEqFilter(body.filter, USER_FILTER_ATTRIBUTES);
|
|
884
|
+
if (filter === "unsupported") {
|
|
885
|
+
return reply.status(400).send(scimError(400, "Unsupported filter expression", "invalidFilter"));
|
|
886
|
+
}
|
|
887
|
+
const all = (await app.container.userRepository.listByOrg(request.state.scimOrgId)).filter((u) => u.isActive);
|
|
888
|
+
const matching = filter.attribute === "username"
|
|
889
|
+
? all.filter((u) => u.email.toLowerCase() === filter.value.toLowerCase())
|
|
890
|
+
: all;
|
|
891
|
+
const start = (body.startIndex ?? 1) - 1;
|
|
892
|
+
const count = body.count ?? matching.length;
|
|
893
|
+
return {
|
|
894
|
+
schemas: [SCIM_LIST_SCHEMA],
|
|
895
|
+
totalResults: matching.length,
|
|
896
|
+
startIndex: start + 1,
|
|
897
|
+
itemsPerPage: Math.max(0, Math.min(count, matching.length - start)),
|
|
898
|
+
Resources: matching.slice(start, start + count).map(scimUserResponse),
|
|
899
|
+
};
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Flatten a SCIM PATCH body into a single attribute map. Operations without a
|
|
904
|
+
* `path` contribute their `value` object directly; `remove` clears the
|
|
905
|
+
* attribute.
|
|
906
|
+
*/
|
|
907
|
+
function collapsePatch(operations) {
|
|
908
|
+
const result = {};
|
|
909
|
+
for (const operation of operations) {
|
|
910
|
+
if (operation.op === "remove") {
|
|
911
|
+
if (operation.path) {
|
|
912
|
+
result[normalizePatchPath(operation.path)] = undefined;
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
if (operation.value && typeof operation.value === "object" && !Array.isArray(operation.value)) {
|
|
916
|
+
for (const key of Object.keys(operation.value))
|
|
917
|
+
result[key] = undefined;
|
|
918
|
+
}
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (!operation.path) {
|
|
922
|
+
if (operation.value && typeof operation.value === "object" && !Array.isArray(operation.value)) {
|
|
923
|
+
Object.assign(result, operation.value);
|
|
924
|
+
}
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
const path = normalizePatchPath(operation.path);
|
|
928
|
+
const value = operation.value;
|
|
929
|
+
if (path.includes(".")) {
|
|
930
|
+
const [head, tail] = path.split(".");
|
|
931
|
+
const current = (result[head] && typeof result[head] === "object" ? result[head] : {});
|
|
932
|
+
current[tail] = value;
|
|
933
|
+
result[head] = current;
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
result[path] = value;
|
|
937
|
+
}
|
|
938
|
+
return result;
|
|
939
|
+
}
|
|
940
|
+
/** `active` -> `active`, `name.givenName` -> `name.givenName`, `members` -> `members`. */
|
|
941
|
+
function normalizePatchPath(path) {
|
|
942
|
+
const [head, ...rest] = path.replace(/^\//, "").split(".");
|
|
943
|
+
const headKey = head.toLowerCase();
|
|
944
|
+
if (headKey === "members.remove")
|
|
945
|
+
return "members.remove";
|
|
946
|
+
if (headKey === "members")
|
|
947
|
+
return rest.length ? "members" : "members";
|
|
948
|
+
return rest.length ? `${headKey}.${rest.join(".")}` : headKey;
|
|
174
949
|
}
|
|
175
950
|
//# sourceMappingURL=scim.js.map
|