diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 90dbb52ba7..5d49f92cf4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,9 +7,7 @@ "ghcr.io/devcontainers/features/go:1": { "version": "1.22" }, - "ghcr.io/devcontainers/features/node:1": { - "version": "18" - }, + "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/guiyomh/features/golangci-lint:0": {}, "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, "ghcr.io/devcontainers/features/github-cli:1": {}, @@ -21,4 +19,4 @@ 8080 ], "onCreateCommand": "npm install -g sass@1.64.1" -} \ No newline at end of file +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a8f673c2e8..ac7d909589 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,9 +50,19 @@ jobs: console_cache_path: ${{ needs.console.outputs.cache_path }} version: ${{ needs.version.outputs.version }} - core-test: + core-unit-test: needs: core - uses: ./.github/workflows/core-test.yml + uses: ./.github/workflows/core-unit-test.yml + with: + go_version: "1.22" + core_cache_key: ${{ needs.core.outputs.cache_key }} + core_cache_path: ${{ needs.core.outputs.cache_path }} + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + core-integration-test: + needs: core + uses: ./.github/workflows/core-integration-test.yml with: go_version: "1.22" core_cache_key: ${{ needs.core.outputs.cache_key }} @@ -93,7 +103,7 @@ jobs: issues: write pull-requests: write needs: - [version, core-test, lint, container, e2e] + [version, core-unit-test, core-integration-test, lint, container, e2e] if: ${{ github.event_name == 'workflow_dispatch' }} secrets: GCR_JSON_KEY_BASE64: ${{ secrets.GCR_JSON_KEY_BASE64 }} diff --git a/.github/workflows/core-test.yml b/.github/workflows/core-integration-test.yml similarity index 94% rename from .github/workflows/core-test.yml rename to .github/workflows/core-integration-test.yml index 4d8d978b60..2673d4addf 100644 --- a/.github/workflows/core-test.yml +++ b/.github/workflows/core-integration-test.yml @@ -18,11 +18,8 @@ on: jobs: postgres: - runs-on: ubuntu-latest - # TODO: use runner group as soon as integration tests run in parallel - # Currently it only consumes time and adds no value - # runs-on: - # group: zitadel-public + runs-on: + group: zitadel-public services: postgres: image: postgres @@ -77,6 +74,15 @@ jobs: ZITADEL_MASTERKEY: MasterkeyNeedsToHave32Characters INTEGRATION_DB_FLAVOR: postgres run: make core_integration_test + - + name: upload server logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: integration-test-server-logs + path: | + tmp/zitadel.log + tmp/race.log.* - name: publish coverage uses: codecov/codecov-action@v4.3.0 diff --git a/.github/workflows/core-unit-test.yml b/.github/workflows/core-unit-test.yml new file mode 100644 index 0000000000..0b1467ff5d --- /dev/null +++ b/.github/workflows/core-unit-test.yml @@ -0,0 +1,76 @@ +name: Unit test core + +on: + workflow_call: + inputs: + go_version: + required: true + type: string + core_cache_key: + required: true + type: string + core_cache_path: + required: true + type: string + crdb_version: + required: false + type: string + secrets: + CODECOV_TOKEN: + required: true + +jobs: + test: + runs-on: + group: zitadel-public + steps: + - + uses: actions/checkout@v3 + - + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go_version }} + - + uses: actions/cache/restore@v4 + timeout-minutes: 1 + name: restore core + id: restore-core + with: + path: ${{ inputs.core_cache_path }} + key: ${{ inputs.core_cache_key }} + fail-on-cache-miss: true + - + id: go-cache-path + name: set cache path + run: echo "GO_CACHE_PATH=$(go env GOCACHE)" >> $GITHUB_OUTPUT + - + uses: actions/cache/restore@v4 + id: cache + timeout-minutes: 1 + continue-on-error: true + name: restore previous results + with: + key: unit-test-${{ inputs.core_cache_key }} + restore-keys: | + unit-test-core- + path: ${{ steps.go-cache-path.outputs.GO_CACHE_PATH }} + - + name: test + if: ${{ steps.cache.outputs.cache-hit != 'true' }} + run: make core_unit_test + - + name: publish coverage + uses: codecov/codecov-action@v4.3.0 + with: + file: profile.cov + name: core-unit-tests + flags: core-unit-tests + token: ${{ secrets.CODECOV_TOKEN }} + - + uses: actions/cache/save@v4 + name: cache results + if: ${{ steps.cache.outputs.cache-hit != 'true' }} + with: + key: unit-test-${{ inputs.core_cache_key }} + path: ${{ steps.go-cache-path.outputs.GO_CACHE_PATH }} + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d58908786..e56ca307d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -205,16 +205,50 @@ make core_unit_test #### Run Local Integration Tests -To test the database-connected gRPC API, run PostgreSQL and CockroachDB, set up a ZITADEL instance and run the tests including integration tests: +Integration tests are run as gRPC clients against a running ZITADEL server binary. +The server binary is typically [build with coverage enabled](https://go.dev/doc/build-cover). +It is also possible to run a ZITADEL sever in a debugger and run the integrations tests like that. In order to run the server, a database is required. + +The database flavor can **optionally** be set in the environment to `cockroach` or `postgres`. The default is `postgres`. ```bash -export INTEGRATION_DB_FLAVOR="cockroach" ZITADEL_MASTERKEY="MasterkeyNeedsToHave32Characters" -docker compose -f internal/integration/config/docker-compose.yaml up --pull always --wait ${INTEGRATION_DB_FLAVOR} -make core_integration_test -docker compose -f internal/integration/config/docker-compose.yaml down +export INTEGRATION_DB_FLAVOR="cockroach" ``` -Repeat the above with `INTEGRATION_DB_FLAVOR="postgres"`. +In order to prepare the local system, the following will bring up the database, builds a coverage binary, initializes the database and starts the sever. + +```bash +make core_integration_db_up core_integration_server_start +``` + +When this job is finished, you can run individual package integration test through your IDE or command-line. The actual integration test clients reside in the `integration_test` subdirectory of the package they aim to test. Integration test files use the `integration` build tag, in order to be excluded from regular unit tests. +Because of the server-client split, Go is usually unaware of changes in server code and tends to cache test results. Pas `-count 1` to disable test caching. + +Example command to run a single package integration test: + +```bash +go test -count 1 -tags integration ./internal/api/grpc/management/integration_test +``` + +To run all available integration tests: + +```bash +make core_integration_test_packages +``` + +When you change any ZITADEL server code, be sure to rebuild and restart the server before the next test run. + +```bash +make core_integration_server_stop core_integration_server_start +``` + +To cleanup after testing (deletes the database!): + +```bash +make core_integration_server_stop core_integration_db_down +``` + +The test binary has the race detector enabled. `core_core_integration_server_stop` checks for any race logs reported by Go and will print them along a `66` exit code when found. Note that the actual race condition may have happened anywhere during the server lifetime, including start, stop or serving gRPC requests during tests. #### Run Local End-to-End Tests diff --git a/Makefile b/Makefile index 8ef48623f6..17e1bbd9b7 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,12 @@ VERSION ?= development-$(now) COMMIT_SHA ?= $(shell git rev-parse HEAD) ZITADEL_IMAGE ?= zitadel:local +GOCOVERDIR = tmp/coverage +INTEGRATION_DB_FLAVOR ?= postgres +ZITADEL_MASTERKEY ?= MasterkeyNeedsToHave32Characters + +export GOCOVERDIR INTEGRATION_DB_FLAVOR ZITADEL_MASTERKEY + .PHONY: compile compile: core_build console_build compile_pipeline @@ -99,25 +105,54 @@ clean: $(RM) -r .artifacts/grpc $(RM) $(gen_authopt_path) $(RM) $(gen_zitadel_path) + $(RM) -r tmp/ .PHONY: core_unit_test core_unit_test: - go test -race -coverprofile=profile.cov ./... + go test -race -coverprofile=profile.cov -coverpkg=./internal/... ./... + +.PHONY: core_integration_db_up +core_integration_db_up: + docker compose -f internal/integration/config/docker-compose.yaml up --pull always --wait $${INTEGRATION_DB_FLAVOR} + +.PHONY: core_integration_db_down +core_integration_db_down: + docker compose -f internal/integration/config/docker-compose.yaml down .PHONY: core_integration_setup core_integration_setup: - go build -o zitadel main.go - ./zitadel init --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml - ./zitadel setup --masterkeyFromEnv --init-projections --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml --steps internal/integration/config/zitadel.yaml --steps internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml - $(RM) zitadel + go build -cover -race -tags integration -o zitadel.test main.go + mkdir -p $${GOCOVERDIR} + GORACE="halt_on_error=1" ./zitadel.test init --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml + GORACE="halt_on_error=1" ./zitadel.test setup --masterkeyFromEnv --init-projections --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml --steps internal/integration/config/steps.yaml + +.PHONY: core_integration_server_start +core_integration_server_start: core_integration_setup + GORACE="log_path=tmp/race.log" \ + ./zitadel.test start --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml \ + > tmp/zitadel.log 2>&1 \ + & printf $$! > tmp/zitadel.pid + +.PHONY: core_integration_test_packages +core_integration_test_packages: + go test -count 1 -tags integration -timeout 30m $$(go list -tags integration ./... | grep "integration_test") + +.PHONY: core_integration_server_stop +core_integration_server_stop: + pid=$$(cat tmp/zitadel.pid); \ + $(RM) tmp/zitadel.pid; \ + kill $$pid; \ + if [ -s tmp/race.log.$$pid ]; then \ + cat tmp/race.log.$$pid; \ + exit 66; \ + fi + +.PHONY: core_integration_reports +core_integration_reports: + go tool covdata textfmt -i=tmp/coverage -pkg=github.com/zitadel/zitadel/internal/...,github.com/zitadel/zitadel/cmd/... -o profile.cov .PHONY: core_integration_test -core_integration_test: core_integration_setup - go test -tags=integration -race -p 1 -coverprofile=profile.cov -coverpkg=./internal/...,./cmd/... ./... - -.PHONY: core_integration_test_fast -core_integration_test_fast: core_integration_setup - go test -tags=integration -p 1 ./... +core_integration_test: core_integration_server_start core_integration_test_packages core_integration_server_stop core_integration_reports .PHONY: console_lint console_lint: diff --git a/README.md b/README.md index 1840218eb7..27c66dbf81 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,11 @@ See all guides [here](https://zitadel.com/docs/self-hosting/deploy/overview) ### Setup ZITADEL Cloud (SaaS) If you want to experience a hands-free ZITADEL, you should use [ZITADEL Cloud](https://zitadel.com). +Available data regions are: +* 🇺🇸 United States +* 🇪🇺 European Union +* 🇦🇺 Australia +* 🇨🇭 Switzerland ZITADEL Cloud comes with a free tier, providing you with all the same features as the open-source version. Learn more about the [pay-as-you-go pricing](https://zitadel.com/pricing). @@ -149,11 +154,12 @@ Deployment - [Zero Downtime Updates](https://zitadel.com/docs/concepts/architecture/solution#zero-downtime-updates) - [High scalability](https://zitadel.com/docs/self-hosting/manage/production) -Track upcoming features on our [roadmap](https://zitadel.com/roadmap). +Track upcoming features on our [roadmap](https://zitadel.com/roadmap) and follow our [changelog](https://zitadel.com/changelog) for recent updates. ## How To Contribute -Find details about how you can contribute in our [Contribution Guide](./CONTRIBUTING.md) +Find details about how you can contribute in our [Contribution Guide](./CONTRIBUTING.md). +Join our [Discord Chat](https://zitadel.com/chat) to get help. ## Contributors diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 76f2fb7fbb..a81a1ff126 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -712,6 +712,13 @@ DefaultInstance: IncludeUpperLetters: false # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_OTPEMAIL_INCLUDEUPPERLETTERS IncludeDigits: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_OTPEMAIL_INCLUDEDIGITS IncludeSymbols: false # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_OTPEMAIL_INCLUDESYMBOLS + InviteCode: + Length: 6 # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_LENGTH + Expiry: "72h" # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_EXPIRY + IncludeLowerLetters: false # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDELOWERLETTERS + IncludeUpperLetters: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDEUPPERLETTERS + IncludeDigits: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDEDIGITS + IncludeSymbols: false # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDESYMBOLS PasswordComplexityPolicy: MinLength: 8 # ZITADEL_DEFAULTINSTANCE_PASSWORDCOMPLEXITYPOLICY_MINLENGTH HasLowercase: true # ZITADEL_DEFAULTINSTANCE_PASSWORDCOMPLEXITYPOLICY_HASLOWERCASE @@ -1033,6 +1040,8 @@ InternalAuthZ: - "iam.web_key.write" - "iam.web_key.delete" - "iam.web_key.read" + - "iam.debug.write" + - "iam.debug.read" - "org.read" - "org.global.read" - "org.create" @@ -1110,6 +1119,7 @@ InternalAuthZ: - "iam.restrictions.read" - "iam.feature.read" - "iam.web_key.read" + - "iam.debug.read" - "org.read" - "org.member.read" - "org.idp.read" diff --git a/cmd/mirror/event.go b/cmd/mirror/event.go index 2bb0d52f45..af0ac25e27 100644 --- a/cmd/mirror/event.go +++ b/cmd/mirror/event.go @@ -3,6 +3,8 @@ package mirror import ( "context" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/v2/projection" "github.com/zitadel/zitadel/internal/v2/readmodel" @@ -30,12 +32,12 @@ func queryLastSuccessfulMigration(ctx context.Context, destinationES *eventstore return lastSuccess, nil } -func writeMigrationStart(ctx context.Context, sourceES *eventstore.EventStore, id string, destination string) (_ float64, err error) { +func writeMigrationStart(ctx context.Context, sourceES *eventstore.EventStore, id string, destination string) (_ decimal.Decimal, err error) { var cmd *eventstore.Command if len(instanceIDs) > 0 { cmd, err = mirror_event.NewStartedInstancesCommand(destination, instanceIDs) if err != nil { - return 0, err + return decimal.Decimal{}, err } } else { cmd = mirror_event.NewStartedSystemCommand(destination) @@ -58,12 +60,12 @@ func writeMigrationStart(ctx context.Context, sourceES *eventstore.EventStore, i ), ) if err != nil { - return 0, err + return decimal.Decimal{}, err } return position.Position, nil } -func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position float64) error { +func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position decimal.Decimal) error { return destinationES.Push( ctx, eventstore.NewPushIntent( diff --git a/cmd/mirror/event_store.go b/cmd/mirror/event_store.go index 23145bdc37..2eab4eb0da 100644 --- a/cmd/mirror/event_store.go +++ b/cmd/mirror/event_store.go @@ -9,6 +9,7 @@ import ( "time" "github.com/jackc/pgx/v5/stdlib" + "github.com/shopspring/decimal" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zitadel/logging" @@ -180,7 +181,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) { logging.WithFields("took", time.Since(start), "count", eventCount).Info("events migrated") } -func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, source string, position float64, errs <-chan error) { +func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, source string, position decimal.Decimal, errs <-chan error) { joinedErrs := make([]error, 0, len(errs)) for err := range errs { joinedErrs = append(joinedErrs, err) diff --git a/cmd/start/start.go b/cmd/start/start.go index 47bc33ca42..5fc4ba936a 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -45,6 +45,7 @@ import ( org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta" action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/action/v3alpha" + "github.com/zitadel/zitadel/internal/api/grpc/resources/debug_events/debug_events" user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha" userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha" "github.com/zitadel/zitadel/internal/api/grpc/resources/webkey/v3" @@ -79,6 +80,7 @@ import ( new_es "github.com/zitadel/zitadel/internal/eventstore/v3" "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/id" + "github.com/zitadel/zitadel/internal/integration/sink" "github.com/zitadel/zitadel/internal/logstore" "github.com/zitadel/zitadel/internal/logstore/emitters/access" "github.com/zitadel/zitadel/internal/logstore/emitters/execution" @@ -138,6 +140,10 @@ type Server struct { func startZitadel(ctx context.Context, config *Config, masterKey string, server chan<- *Server) error { showBasicInformation(config) + // sink Server is stubbed out in production builds, see function's godoc. + closeSink := sink.StartServer() + defer closeSink() + i18n.MustLoadSupportedLanguagesFromDir() queryDBClient, err := database.Connect(config.Database, false, dialect.DBPurposeQuery) @@ -454,6 +460,9 @@ func startAPIs( if err := apis.RegisterService(ctx, webkey.CreateServer(commands, queries)); err != nil { return nil, err } + if err := apis.RegisterService(ctx, debug_events.CreateServer(commands, queries)); err != nil { + return nil, err + } instanceInterceptor := middleware.InstanceInterceptor(queries, config.ExternalDomain, login.IgnoreInstanceEndpoints...) assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge) apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) diff --git a/console/src/app/modules/changes/changes.component.html b/console/src/app/modules/changes/changes.component.html index 1cf65b3db8..e78fb0fbb3 100644 --- a/console/src/app/modules/changes/changes.component.html +++ b/console/src/app/modules/changes/changes.component.html @@ -28,7 +28,7 @@ {{ action.localizedMessage }} {{ dayelement.dates[j] | timestampToDate | localizedDate: 'HH:mm' }} diff --git a/console/src/app/modules/header/header.component.html b/console/src/app/modules/header/header.component.html index 2a167731ec..e4ef76dd2a 100644 --- a/console/src/app/modules/header/header.component.html +++ b/console/src/app/modules/header/header.component.html @@ -172,11 +172,11 @@ {{ pP.customLinkText }} - - - {{ 'MENU.DOCUMENTATION' | translate }} - + + {{ 'MENU.DOCUMENTATION' | translate }} + +
= new Subject(); public BreadcrumbType: any = BreadcrumbType; public ActionKeysType: any = ActionKeysType; - public docsLink = 'https://zitadel.com/docs'; public positions: ConnectedPosition[] = [ new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }, 0, 10), diff --git a/console/src/app/modules/policies/message-texts/message-texts.component.ts b/console/src/app/modules/policies/message-texts/message-texts.component.ts index 2042130399..b3be74c17f 100644 --- a/console/src/app/modules/policies/message-texts/message-texts.component.ts +++ b/console/src/app/modules/policies/message-texts/message-texts.component.ts @@ -7,6 +7,7 @@ import { GetDefaultInitMessageTextRequest as AdminGetDefaultInitMessageTextRequest, GetDefaultPasswordChangeMessageTextRequest as AdminGetDefaultPasswordChangeMessageTextRequest, GetDefaultPasswordlessRegistrationMessageTextRequest as AdminGetDefaultPasswordlessRegistrationMessageTextRequest, + GetDefaultInviteUserMessageTextRequest as AdminGetDefaultInviteUserMessageTextRequest, GetDefaultPasswordResetMessageTextRequest as AdminGetDefaultPasswordResetMessageTextRequest, GetDefaultVerifyEmailMessageTextRequest as AdminGetDefaultVerifyEmailMessageTextRequest, GetDefaultVerifyEmailOTPMessageTextRequest as AdminGetDefaultVerifyEmailOTPMessageTextRequest, @@ -16,6 +17,7 @@ import { SetDefaultInitMessageTextRequest, SetDefaultPasswordChangeMessageTextRequest, SetDefaultPasswordlessRegistrationMessageTextRequest, + SetDefaultInviteUserMessageTextRequest, SetDefaultPasswordResetMessageTextRequest, SetDefaultVerifyEmailMessageTextRequest, SetDefaultVerifyEmailOTPMessageTextRequest, @@ -27,6 +29,7 @@ import { GetCustomInitMessageTextRequest, GetCustomPasswordChangeMessageTextRequest, GetCustomPasswordlessRegistrationMessageTextRequest, + GetCustomInviteUserMessageTextRequest, GetCustomPasswordResetMessageTextRequest, GetCustomVerifyEmailMessageTextRequest, GetCustomVerifyEmailOTPMessageTextRequest, @@ -36,6 +39,7 @@ import { GetDefaultInitMessageTextRequest, GetDefaultPasswordChangeMessageTextRequest, GetDefaultPasswordlessRegistrationMessageTextRequest, + GetDefaultInviteUserMessageTextRequest, GetDefaultPasswordResetMessageTextRequest, GetDefaultVerifyEmailMessageTextRequest, GetDefaultVerifyEmailOTPMessageTextRequest, @@ -45,6 +49,7 @@ import { SetCustomInitMessageTextRequest, SetCustomPasswordChangeMessageTextRequest, SetCustomPasswordlessRegistrationMessageTextRequest, + SetCustomInviteUserMessageTextRequest, SetCustomPasswordResetMessageTextRequest, SetCustomVerifyEmailMessageTextRequest, SetCustomVerifyEmailOTPMessageTextRequest, @@ -73,6 +78,7 @@ enum MESSAGETYPES { PASSWORDCHANGE = 'PC', VERIFYSMSOTP = 'VSO', VERIFYEMAILOTP = 'VEO', + INVITEUSER = 'IU', } const REQUESTMAP = { @@ -226,6 +232,23 @@ const REQUESTMAP = { req.setText(map.text ?? ''); req.setTitle(map.title ?? ''); + return req; + }, + }, + [MESSAGETYPES.INVITEUSER]: { + get: new GetCustomInviteUserMessageTextRequest(), + set: new SetCustomInviteUserMessageTextRequest(), + getDefault: new GetDefaultInviteUserMessageTextRequest(), + setFcn: (map: Partial): SetCustomInviteUserMessageTextRequest => { + const req = new SetCustomInviteUserMessageTextRequest(); + req.setButtonText(map.buttonText ?? ''); + req.setFooterText(map.footerText ?? ''); + req.setGreeting(map.greeting ?? ''); + req.setPreHeader(map.preHeader ?? ''); + req.setSubject(map.subject ?? ''); + req.setText(map.text ?? ''); + req.setTitle(map.title ?? ''); + return req; }, }, @@ -371,6 +394,22 @@ const REQUESTMAP = { req.setText(map.text ?? ''); req.setTitle(map.title ?? ''); + return req; + }, + }, + [MESSAGETYPES.INVITEUSER]: { + get: new AdminGetDefaultInviteUserMessageTextRequest(), + set: new SetDefaultInviteUserMessageTextRequest(), + setFcn: (map: Partial): SetDefaultInviteUserMessageTextRequest => { + const req = new SetDefaultInviteUserMessageTextRequest(); + req.setButtonText(map.buttonText ?? ''); + req.setFooterText(map.footerText ?? ''); + req.setGreeting(map.greeting ?? ''); + req.setPreHeader(map.preHeader ?? ''); + req.setSubject(map.subject ?? ''); + req.setText(map.text ?? ''); + req.setTitle(map.title ?? ''); + return req; }, }, @@ -540,6 +579,21 @@ export class MessageTextsComponent implements OnInit, OnDestroy { { key: 'POLICY.MESSAGE_TEXTS.CHIPS.loginnames', value: '{{.LoginNames}}' }, { key: 'POLICY.MESSAGE_TEXTS.CHIPS.changedate', value: '{{.ChangeDate}}' }, ], + [MESSAGETYPES.INVITEUSER]: [ + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.preferredLoginName', value: '{{.PreferredLoginName}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.username', value: '{{.UserName}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.firstname', value: '{{.FirstName}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.LastName}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.nickName', value: '{{.NickName}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.displayName', value: '{{.DisplayName}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastEmail', value: '{{.LastEmail}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.verifiedEmail', value: '{{.VerifiedEmail}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastPhone', value: '{{.LastPhone}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.verifiedPhone', value: '{{.VerifiedPhone}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.loginnames', value: '{{.LoginNames}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.changedate', value: '{{.ChangeDate}}' }, + { key: 'POLICY.MESSAGE_TEXTS.CHIPS.applicationName', value: '{{.ApplicationName}}' }, + ], }; public language: string = 'en'; @@ -599,6 +653,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy { return this.stripEmail(this.service.getDefaultPasswordlessRegistrationMessageText(req)); case MESSAGETYPES.PASSWORDCHANGE: return this.stripEmail(this.service.getDefaultPasswordChangeMessageText(req)); + case MESSAGETYPES.INVITEUSER: + return this.stripEmail(this.service.getDefaultInviteUserMessageText(req)); } } @@ -622,6 +678,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy { return this.stripEmail(this.service.getCustomPasswordlessRegistrationMessageText(req)); case MESSAGETYPES.PASSWORDCHANGE: return this.stripEmail(this.service.getCustomPasswordChangeMessageText(req)); + case MESSAGETYPES.INVITEUSER: + return this.stripEmail(this.service.getCustomInviteUserMessageText(req)); default: return undefined; } @@ -690,6 +748,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy { ); case MESSAGETYPES.PASSWORDCHANGE: return handler((this.service as ManagementService).setCustomPasswordChangeMessageText(this.updateRequest)); + case MESSAGETYPES.INVITEUSER: + return handler((this.service as ManagementService).setCustomInviteUserMessageText(this.updateRequest)); } } else if (this.serviceType === PolicyComponentServiceType.ADMIN) { switch (this.currentType) { @@ -711,6 +771,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy { return handler((this.service as AdminService).setDefaultPasswordlessRegistrationMessageText(this.updateRequest)); case MESSAGETYPES.PASSWORDCHANGE: return handler((this.service as AdminService).setDefaultPasswordChangeMessageText(this.updateRequest)); + case MESSAGETYPES.INVITEUSER: + return handler((this.service as AdminService).setDefaultInviteUserMessageText(this.updateRequest)); } } } @@ -763,6 +825,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy { return handler(this.service.resetCustomPasswordlessRegistrationMessageTextToDefault(this.language)); case MESSAGETYPES.PASSWORDCHANGE: return handler(this.service.resetCustomPasswordChangeMessageTextToDefault(this.language)); + case MESSAGETYPES.INVITEUSER: + return handler(this.service.resetCustomInviteUserMessageTextToDefault(this.language)); default: return Promise.reject(); } diff --git a/console/src/app/modules/smtp-table/smtp-table.component.html b/console/src/app/modules/smtp-table/smtp-table.component.html index 3359eda596..b71a7fcefd 100644 --- a/console/src/app/modules/smtp-table/smtp-table.component.html +++ b/console/src/app/modules/smtp-table/smtp-table.component.html @@ -24,7 +24,7 @@ {{ 'SMTP.LIST.ACTIVATED' | translate }} - + {{ 'SETTING.SMTP.DESCRIPTION' | translate }} - + {{ config?.description }} @@ -45,22 +45,22 @@ TLS - + - + {{ 'SETTING.SMTP.HOSTANDPORT' | translate }} - + {{ config?.host }} {{ 'SETTING.SMTP.SENDERADDRESS' | translate }} - + {{ config?.senderAddress }} @@ -95,7 +95,7 @@ + + + + +
+ + + + + + +{{template "main-bottom" .}} diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index ca1cc5a03c..c6ae475536 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -106,6 +106,7 @@ type idpUserLinksProvider interface { type userEventProvider interface { UserEventsByID(ctx context.Context, id string, changeDate time.Time, eventTypes []eventstore.EventType) ([]eventstore.Event, error) PasswordCodeExists(ctx context.Context, userID string) (exists bool, err error) + InviteCodeExists(ctx context.Context, userID string) (exists bool, err error) } type userCommandProvider interface { @@ -1060,8 +1061,12 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth if err != nil { return nil, err } - if (!isInternalLogin || len(idps.Links) > 0) && len(request.LinkingUsers) == 0 { - step := repo.idpChecked(request, idps.Links, userSession) + noLocalAuth := request.LoginPolicy != nil && !request.LoginPolicy.AllowUsernamePassword + if (!isInternalLogin || len(idps.Links) > 0 || noLocalAuth) && len(request.LinkingUsers) == 0 { + step, err := repo.idpChecked(request, idps.Links, userSession) + if err != nil { + return nil, err + } if step != nil { return append(steps, step), nil } @@ -1087,7 +1092,7 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth } if !user.IsEmailVerified { steps = append(steps, &domain.VerifyEMailStep{ - InitPassword: !user.PasswordSet, + InitPassword: !user.PasswordSet && len(idps.Links) == 0, }) } if user.UsernameChangeRequired { @@ -1250,8 +1255,18 @@ func (repo *AuthRequestRepo) firstFactorChecked(ctx context.Context, request *do if user.PasswordInitRequired { if !user.IsEmailVerified { + // If the user was created through the user resource API, + // they can either have an invite code... + exists, err := repo.UserEventProvider.InviteCodeExists(ctx, user.ID) + logging.WithFields("userID", user.ID).OnError(err).Error("unable to check if invite code exists") + if err == nil && exists { + return &domain.VerifyInviteStep{} + } + // or were created with an explicit email verification mail return &domain.VerifyEMailStep{InitPassword: true} } + // If they were created with a verified mail, they might have never received mail to set their password, + // e.g. when created through a user resource API. In this case we'll just create and send one now. exists, err := repo.UserEventProvider.PasswordCodeExists(ctx, user.ID) logging.WithFields("userID", user.ID).OnError(err).Error("unable to check if password code exists") if err == nil && !exists { @@ -1272,20 +1287,29 @@ func (repo *AuthRequestRepo) firstFactorChecked(ctx context.Context, request *do return &domain.PasswordStep{} } -func (repo *AuthRequestRepo) idpChecked(request *domain.AuthRequest, idps []*query.IDPUserLink, userSession *user_model.UserSessionView) domain.NextStep { +func (repo *AuthRequestRepo) idpChecked(request *domain.AuthRequest, idps []*query.IDPUserLink, userSession *user_model.UserSessionView) (domain.NextStep, error) { if checkVerificationTimeMaxAge(userSession.ExternalLoginVerification, request.LoginPolicy.ExternalLoginCheckLifetime, request) { request.IDPLoginChecked = true request.AuthTime = userSession.ExternalLoginVerification - return nil + return nil, nil } - selectedIDPConfigID := request.SelectedIDPConfigID - if selectedIDPConfigID == "" { - selectedIDPConfigID = userSession.SelectedIDPConfigID + // use the explicitly set IdP first + if request.SelectedIDPConfigID != "" { + return &domain.ExternalLoginStep{SelectedIDPConfigID: request.SelectedIDPConfigID}, nil } - if selectedIDPConfigID == "" && len(idps) > 0 { - selectedIDPConfigID = idps[0].IDPID + // reuse the previously used IdP from the session + if userSession.SelectedIDPConfigID != "" { + return &domain.ExternalLoginStep{SelectedIDPConfigID: userSession.SelectedIDPConfigID}, nil } - return &domain.ExternalLoginStep{SelectedIDPConfigID: selectedIDPConfigID} + // then use an existing linked IdP of the user + if len(idps) > 0 { + return &domain.ExternalLoginStep{SelectedIDPConfigID: idps[0].IDPID}, nil + } + // if the user did not link one, then just use one of the configured IdPs of the org + if len(request.AllowedExternalIDPs) > 0 { + return &domain.ExternalLoginStep{SelectedIDPConfigID: request.AllowedExternalIDPs[0].IDPConfigID}, nil + } + return nil, zerrors.ThrowPreconditionFailed(nil, "LOGIN-5Hm8s", "Errors.Org.IdpNotExisting") } func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView, request *domain.AuthRequest, user *user_model.UserView, isInternalAuthentication bool) (domain.NextStep, bool, error) { diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index 9ed47762b7..ccd53e06a1 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -110,8 +110,9 @@ func (m *mockViewNoUser) UserByID(context.Context, string, string) (*user_view_m } type mockEventUser struct { - Events []eventstore.Event - CodeExists bool + Events []eventstore.Event + PwCodeExists bool + InvitationCodeExists bool } func (m *mockEventUser) UserEventsByID(ctx context.Context, id string, changeDate time.Time, types []eventstore.EventType) ([]eventstore.Event, error) { @@ -119,7 +120,11 @@ func (m *mockEventUser) UserEventsByID(ctx context.Context, id string, changeDat } func (m *mockEventUser) PasswordCodeExists(ctx context.Context, userID string) (bool, error) { - return m.CodeExists, nil + return m.PwCodeExists, nil +} + +func (m *mockEventUser) InviteCodeExists(ctx context.Context, userID string) (bool, error) { + return m.InvitationCodeExists, nil } func (m *mockEventUser) GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*query.CurrentState, error) { @@ -140,6 +145,10 @@ func (m *mockEventErrUser) PasswordCodeExists(ctx context.Context, userID string return false, zerrors.ThrowInternal(nil, "id", "internal error") } +func (m *mockEventErrUser) InviteCodeExists(ctx context.Context, userID string) (bool, error) { + return false, zerrors.ThrowInternal(nil, "id", "internal error") +} + type mockViewUser struct { InitRequired bool PasswordInitRequired bool @@ -563,6 +572,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { idpUserLinksProvider: &mockIDPUserLinks{}, loginPolicyProvider: &mockLoginPolicy{ policy: &query.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: database.Duration(10 * 24 * time.Hour), SecondFactorCheckLifetime: database.Duration(18 * time.Hour), @@ -584,6 +594,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { args{&domain.AuthRequest{ Request: &domain.AuthRequestOIDC{}, LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, @@ -812,7 +823,15 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }, idpUserLinksProvider: &mockIDPUserLinks{}, }, - args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false}, + args{ + &domain.AuthRequest{ + UserID: "UserID", + LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, + }, + }, + false, + }, []domain.NextStep{&domain.PasswordStep{}}, nil, }, @@ -849,9 +868,22 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { ShowFailures: true, }, }, + loginPolicyProvider: &mockLoginPolicy{ + policy: &query.LoginPolicy{ + AllowUsernamePassword: true, + }, + }, idpUserLinksProvider: &mockIDPUserLinks{}, }, - args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false}, + args{ + &domain.AuthRequest{ + UserID: "UserID", + LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, + }, + }, + false, + }, []domain.NextStep{&domain.InitUserStep{ PasswordSet: true, }}, @@ -878,7 +910,16 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }, idpUserLinksProvider: &mockIDPUserLinks{}, }, - args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{PasswordlessType: domain.PasswordlessTypeAllowed}}, false}, + args{ + &domain.AuthRequest{ + UserID: "UserID", + LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + }, + }, + false, + }, []domain.NextStep{&domain.PasswordlessRegistrationPromptStep{}}, nil, }, @@ -903,7 +944,15 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }, idpUserLinksProvider: &mockIDPUserLinks{}, }, - args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{PasswordlessType: domain.PasswordlessTypeAllowed}}, false}, + args{ + &domain.AuthRequest{ + UserID: "UserID", + LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + }, + }, false, + }, []domain.NextStep{&domain.PasswordlessStep{}}, nil, }, @@ -929,7 +978,15 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }, idpUserLinksProvider: &mockIDPUserLinks{}, }, - args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{PasswordlessType: domain.PasswordlessTypeAllowed}}, false}, + args{ + &domain.AuthRequest{ + UserID: "UserID", + LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + }, + }, false, + }, []domain.NextStep{&domain.PasswordlessStep{PasswordSet: true}}, nil, }, @@ -956,14 +1013,116 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, idpUserLinksProvider: &mockIDPUserLinks{}, }, - args{&domain.AuthRequest{ - UserID: "UserID", - LoginPolicy: &domain.LoginPolicy{ - PasswordlessType: domain.PasswordlessTypeAllowed, - MultiFactors: []domain.MultiFactorType{domain.MultiFactorTypeU2FWithPIN}, - MultiFactorCheckLifetime: 10 * time.Hour, + args{ + &domain.AuthRequest{ + UserID: "UserID", + LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + MultiFactors: []domain.MultiFactorType{domain.MultiFactorTypeU2FWithPIN}, + MultiFactorCheckLifetime: 10 * time.Hour, + }, }, - }, false}, + false, + }, + []domain.NextStep{&domain.VerifyEMailStep{}}, + nil, + }, + { + "password not set (email not verified), invite code exists, invite step", + fields{ + userSessionViewProvider: &mockViewUserSession{}, + userViewProvider: &mockViewUser{ + PasswordInitRequired: true, + }, + userEventProvider: &mockEventUser{ + InvitationCodeExists: true, + }, + lockoutPolicyProvider: &mockLockoutPolicy{ + policy: &query.LockoutPolicy{ + ShowFailures: true, + }, + }, + orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + idpUserLinksProvider: &mockIDPUserLinks{}, + }, + args{ + &domain.AuthRequest{ + UserID: "UserID", + LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, + }, + }, + false, + }, + []domain.NextStep{&domain.VerifyInviteStep{}}, + nil, + }, + { + "password not set (email not verified), verify email with password step", + fields{ + userSessionViewProvider: &mockViewUserSession{}, + userViewProvider: &mockViewUser{ + PasswordInitRequired: true, + }, + userEventProvider: &mockEventUser{}, + lockoutPolicyProvider: &mockLockoutPolicy{ + policy: &query.LockoutPolicy{ + ShowFailures: true, + }, + }, + orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + idpUserLinksProvider: &mockIDPUserLinks{}, + }, + args{ + &domain.AuthRequest{ + UserID: "UserID", + LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, + }, + }, + false, + }, + []domain.NextStep{&domain.VerifyEMailStep{InitPassword: true}}, + nil, + }, + { + "password not set, but idp, email not verified, verify email step", + fields{ + userSessionViewProvider: &mockViewUserSession{ + ExternalLoginVerification: testNow.Add(-5 * time.Minute), + }, + userViewProvider: &mockViewUser{}, + userEventProvider: &mockEventUser{}, + lockoutPolicyProvider: &mockLockoutPolicy{ + policy: &query.LockoutPolicy{ + ShowFailures: true, + }, + }, + orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + idpUserLinksProvider: &mockIDPUserLinks{ + []*query.IDPUserLink{ + { + IDPID: "idpID", + UserID: "userID", + IDPName: "idpName", + ProvidedUserID: "providedUserID", + ProvidedUsername: "providedUsername", + }, + }, + }, + }, + args{ + &domain.AuthRequest{ + UserID: "UserID", + LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, + ExternalLoginCheckLifetime: 10 * 24 * time.Hour, + }, + SelectedIDPConfigID: "idpID", + }, + false, + }, []domain.NextStep{&domain.VerifyEMailStep{}}, nil, }, @@ -983,7 +1142,15 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, idpUserLinksProvider: &mockIDPUserLinks{}, }, - args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false}, + args{ + &domain.AuthRequest{ + UserID: "UserID", + LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, + }, + }, + false, + }, []domain.NextStep{&domain.VerifyEMailStep{InitPassword: true}}, nil, }, @@ -996,7 +1163,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { IsEmailVerified: true, }, userEventProvider: &mockEventUser{ - CodeExists: true, + PwCodeExists: true, }, lockoutPolicyProvider: &mockLockoutPolicy{ policy: &query.LockoutPolicy{ @@ -1007,7 +1174,15 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { idpUserLinksProvider: &mockIDPUserLinks{}, passwordReset: newMockPasswordReset(false), }, - args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false}, + args{ + &domain.AuthRequest{ + UserID: "UserID", + LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, + }, + }, + false, + }, []domain.NextStep{&domain.InitPasswordStep{}}, nil, }, @@ -1020,7 +1195,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { IsEmailVerified: true, }, userEventProvider: &mockEventUser{ - CodeExists: false, + PwCodeExists: false, }, lockoutPolicyProvider: &mockLockoutPolicy{ policy: &query.LockoutPolicy{ @@ -1031,7 +1206,15 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { idpUserLinksProvider: &mockIDPUserLinks{}, passwordReset: newMockPasswordReset(true), }, - args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false}, + args{ + &domain.AuthRequest{ + UserID: "UserID", + LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, + }, + }, + false, + }, []domain.NextStep{&domain.InitPasswordStep{}}, nil, }, @@ -1063,6 +1246,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { UserID: "UserID", SelectedIDPConfigID: "IDPConfigID", LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: false, SecondFactorCheckLifetime: 18 * time.Hour, }}, false}, []domain.NextStep{&domain.ExternalLoginStep{SelectedIDPConfigID: "IDPConfigID"}}, @@ -1097,6 +1281,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { args{&domain.AuthRequest{ UserID: "UserID", LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: false, SecondFactorCheckLifetime: 18 * time.Hour, }}, false}, []domain.NextStep{&domain.ExternalLoginStep{SelectedIDPConfigID: "IDPConfigID"}}, @@ -1131,6 +1316,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { SelectedIDPConfigID: "IDPConfigID", Request: &domain.AuthRequestOIDC{}, LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: false, ExternalLoginCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, }, @@ -1160,7 +1346,15 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }, idpUserLinksProvider: &mockIDPUserLinks{}, }, - args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false}, + args{ + &domain.AuthRequest{ + UserID: "UserID", + LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, + }, + }, + false, + }, []domain.NextStep{&domain.PasswordStep{}}, nil, }, @@ -1194,6 +1388,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { SelectedIDPConfigID: "IDPConfigID", Request: &domain.AuthRequestOIDC{}, LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactorCheckLifetime: 18 * time.Hour, ExternalLoginCheckLifetime: 10 * 24 * time.Hour, }, @@ -1226,6 +1421,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { &domain.AuthRequest{ UserID: "UserID", LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, @@ -1260,6 +1456,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { &domain.AuthRequest{ UserID: "UserID", LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, @@ -1296,6 +1493,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { UserID: "UserID", SelectedIDPConfigID: "IDPConfigID", LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, ExternalLoginCheckLifetime: 10 * 24 * time.Hour, @@ -1333,6 +1531,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { &domain.AuthRequest{ UserID: "UserID", LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, @@ -1364,6 +1563,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { args{&domain.AuthRequest{ UserID: "UserID", LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, @@ -1396,6 +1596,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { args{&domain.AuthRequest{ UserID: "UserID", LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, @@ -1435,6 +1636,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { args{&domain.AuthRequest{ UserID: "UserID", LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, @@ -1474,6 +1676,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { UserID: "UserID", Request: &domain.AuthRequestOIDC{}, LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, @@ -1511,6 +1714,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { Prompt: []domain.Prompt{domain.PromptNone}, Request: &domain.AuthRequestOIDC{}, LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, @@ -1548,6 +1752,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { Prompt: []domain.Prompt{domain.PromptNone}, Request: &domain.AuthRequestOIDC{}, LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, @@ -1587,6 +1792,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { Prompt: []domain.Prompt{domain.PromptNone}, Request: &domain.AuthRequestOIDC{}, LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, @@ -1627,6 +1833,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { Prompt: []domain.Prompt{domain.PromptNone}, Request: &domain.AuthRequestOIDC{}, LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, @@ -1667,6 +1874,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { Prompt: []domain.Prompt{domain.PromptNone}, Request: &domain.AuthRequestOIDC{}, LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, @@ -1708,6 +1916,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { Prompt: []domain.Prompt{domain.PromptNone}, Request: &domain.AuthRequestOIDC{}, LoginPolicy: &domain.LoginPolicy{ + AllowUsernamePassword: true, SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, PasswordCheckLifetime: 10 * 24 * time.Hour, SecondFactorCheckLifetime: 18 * time.Hour, diff --git a/internal/auth/repository/eventsourcing/eventstore/user.go b/internal/auth/repository/eventsourcing/eventstore/user.go index 0a33199b0a..b11f770d77 100644 --- a/internal/auth/repository/eventsourcing/eventstore/user.go +++ b/internal/auth/repository/eventsourcing/eventstore/user.go @@ -93,3 +93,41 @@ func (repo *UserRepo) PasswordCodeExists(ctx context.Context, userID string) (ex } return model.exists, nil } + +type inviteCodeCheck struct { + userID string + + exists bool + events int +} + +func (p *inviteCodeCheck) Reduce() error { + p.exists = p.events > 0 + return nil +} + +func (p *inviteCodeCheck) AppendEvents(events ...eventstore.Event) { + p.events += len(events) +} + +func (p *inviteCodeCheck) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + AddQuery(). + AggregateTypes(user.AggregateType). + AggregateIDs(p.userID). + EventTypes( + user.HumanInviteCodeAddedType, + user.HumanInviteCodeSentType). + Builder() +} + +func (repo *UserRepo) InviteCodeExists(ctx context.Context, userID string) (exists bool, err error) { + model := &inviteCodeCheck{ + userID: userID, + } + err = repo.Eventstore.FilterToQueryReducer(ctx, model) + if err != nil { + return false, zerrors.ThrowPermissionDenied(err, "EVENT-GJ2os", "Errors.Internal") + } + return model.exists, nil +} diff --git a/internal/command/crypto.go b/internal/command/crypto.go index 5db4764f28..45c597fd95 100644 --- a/internal/command/crypto.go +++ b/internal/command/crypto.go @@ -41,14 +41,6 @@ func newEncryptedCodeWithDefaultConfig(ctx context.Context, filter preparation.F }, nil } -func verifyEncryptedCode(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm, creation time.Time, expiry time.Duration, crypted *crypto.CryptoValue, plain string) error { - gen, _, err := encryptedCodeGenerator(ctx, filter, typ, alg, emptyConfig) - if err != nil { - return err - } - return crypto.VerifyCode(creation, expiry, crypted, plain, gen.Alg()) -} - func encryptedCodeGenerator(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm, defaultConfig *crypto.GeneratorConfig) (crypto.Generator, *crypto.GeneratorConfig, error) { config, err := cryptoGeneratorConfigWithDefault(ctx, filter, typ, defaultConfig) if err != nil { diff --git a/internal/command/crypto_test.go b/internal/command/crypto_test.go index 07db20e2ec..815539120a 100644 --- a/internal/command/crypto_test.go +++ b/internal/command/crypto_test.go @@ -123,78 +123,6 @@ func Test_newCryptoCode(t *testing.T) { } } -func Test_verifyCryptoCode(t *testing.T) { - es := eventstoreExpect(t, expectFilter( - eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)), - )) - code, err := newEncryptedCode(context.Background(), es.Filter, domain.SecretGeneratorTypeVerifyEmailCode, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) //nolint:staticcheck - require.NoError(t, err) - - type args struct { - typ domain.SecretGeneratorType - alg crypto.EncryptionAlgorithm - expiry time.Duration - crypted *crypto.CryptoValue - plain string - } - tests := []struct { - name string - eventsore *eventstore.Eventstore - args args - wantErr bool - }{ - { - name: "filter config error", - eventsore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)), - args: args{ - typ: domain.SecretGeneratorTypeVerifyEmailCode, - alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - expiry: code.Expiry, - crypted: code.Crypted, - plain: code.Plain, - }, - wantErr: true, - }, - { - name: "success", - eventsore: eventstoreExpect(t, expectFilter( - eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)), - )), - args: args{ - typ: domain.SecretGeneratorTypeVerifyEmailCode, - alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - expiry: code.Expiry, - crypted: code.Crypted, - plain: code.Plain, - }, - }, - { - name: "wrong plain", - eventsore: eventstoreExpect(t, expectFilter( - eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)), - )), - args: args{ - typ: domain.SecretGeneratorTypeVerifyEmailCode, - alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - expiry: code.Expiry, - crypted: code.Crypted, - plain: "wrong", - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := verifyEncryptedCode(context.Background(), tt.eventsore.Filter, tt.args.typ, tt.args.alg, time.Now(), tt.args.expiry, tt.args.crypted, tt.args.plain) //nolint:staticcheck - if tt.wantErr { - assert.Error(t, err) - return - } - require.NoError(t, err) - }) - } -} - func Test_cryptoCodeGenerator(t *testing.T) { type args struct { typ domain.SecretGeneratorType diff --git a/internal/command/debug_events.go b/internal/command/debug_events.go new file mode 100644 index 0000000000..50f87bd2fd --- /dev/null +++ b/internal/command/debug_events.go @@ -0,0 +1,82 @@ +package command + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/debug_events" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type DebugEvents struct { + AggregateID string + Events []DebugEvent +} + +type DebugEvent interface { + isADebugEvent() +} + +type DebugEventAdded struct { + ProjectionSleep time.Duration + Blob *string +} + +type DebugEventChanged struct { + ProjectionSleep time.Duration + Blob *string +} + +type DebugEventRemoved struct { + ProjectionSleep time.Duration +} + +func (DebugEventAdded) isADebugEvent() {} +func (DebugEventChanged) isADebugEvent() {} +func (DebugEventRemoved) isADebugEvent() {} + +func (c *Commands) CreateDebugEvents(ctx context.Context, dbe *DebugEvents) (_ *domain.ObjectDetails, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + model := NewDebugEventsWriteModel(dbe.AggregateID, authz.GetInstance(ctx).InstanceID()) + if err = c.eventstore.FilterToQueryReducer(ctx, model); err != nil { + return nil, err + } + aggr := debug_events.AggregateFromWriteModel(ctx, &model.WriteModel) + + cmds := make([]eventstore.Command, len(dbe.Events)) + for i, event := range dbe.Events { + var cmd eventstore.Command + switch e := event.(type) { + case DebugEventAdded: + if model.State.Exists() { + return nil, zerrors.ThrowAlreadyExists(nil, "COMMAND-Aex6j", "debug aggregate already exists") + } + cmd = debug_events.NewAddedEvent(ctx, aggr, e.ProjectionSleep, e.Blob) + case DebugEventChanged: + if !model.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-Thie6", "debug aggregate not found") + } + cmd = debug_events.NewChangedEvent(ctx, aggr, e.ProjectionSleep, e.Blob) + case DebugEventRemoved: + if !model.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-Ohna9", "debug aggregate not found") + } + cmd = debug_events.NewRemovedEvent(ctx, aggr, e.ProjectionSleep) + } + + cmds[i] = cmd + // be sure the state of the last event is reduced before handling the next one. + model.reduceEvent(cmd.(eventstore.Event)) + } + events, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(events), nil +} diff --git a/internal/command/debug_events_model.go b/internal/command/debug_events_model.go new file mode 100644 index 0000000000..81d0aca6f9 --- /dev/null +++ b/internal/command/debug_events_model.go @@ -0,0 +1,68 @@ +package command + +import ( + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + debug "github.com/zitadel/zitadel/internal/repository/debug_events" +) + +type DebugEventsWriteModel struct { + eventstore.WriteModel + State domain.DebugEventsState + Blob string +} + +func NewDebugEventsWriteModel(aggregateID, resourceOwner string) *DebugEventsWriteModel { + return &DebugEventsWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: aggregateID, + ResourceOwner: resourceOwner, + }, + } +} + +func (wm *DebugEventsWriteModel) AppendEvents(events ...eventstore.Event) { + wm.WriteModel.AppendEvents(events...) +} + +func (wm *DebugEventsWriteModel) Reduce() error { + for _, event := range wm.Events { + wm.reduceEvent(event) + } + return wm.WriteModel.Reduce() +} + +func (wm *DebugEventsWriteModel) reduceEvent(event eventstore.Event) { + if event.Aggregate().ID != wm.AggregateID { + return + } + switch e := event.(type) { + case *debug.AddedEvent: + wm.State = domain.DebugEventsStateInitial + if e.Blob != nil { + wm.Blob = *e.Blob + } + case *debug.ChangedEvent: + wm.State = domain.DebugEventsStateChanged + if e.Blob != nil { + wm.Blob = *e.Blob + } + case *debug.RemovedEvent: + wm.State = domain.DebugEventsStateRemoved + wm.Blob = "" + } +} + +func (wm *DebugEventsWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(debug.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + debug.AddedEventType, + debug.ChangedEventType, + debug.RemovedEventType, + ). + Builder() +} diff --git a/internal/command/debug_events_test.go b/internal/command/debug_events_test.go new file mode 100644 index 0000000000..e740de8574 --- /dev/null +++ b/internal/command/debug_events_test.go @@ -0,0 +1,340 @@ +package command + +import ( + "io" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/debug_events" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestCommands_CreateDebugEvents(t *testing.T) { + ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil) + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + } + type args struct { + dbe *DebugEvents + } + tests := []struct { + name string + fields fields + args args + want *domain.ObjectDetails + wantErr error + }{ + { + name: "filter error", + fields: fields{ + eventstore: expectEventstore( + expectFilterError(io.ErrClosedPipe), + ), + }, + args: args{&DebugEvents{ + AggregateID: "dbg1", + Events: []DebugEvent{ + DebugEventAdded{ + ProjectionSleep: time.Millisecond, + Blob: gu.Ptr("a"), + }, + }, + }}, + wantErr: io.ErrClosedPipe, + }, + { + name: "already exists", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + debug_events.NewAddedEvent( + ctx, debug_events.NewAggregate("dbg1", "instance1"), + time.Millisecond, gu.Ptr("a"), + ), + ), + ), + ), + }, + args: args{&DebugEvents{ + AggregateID: "dbg1", + Events: []DebugEvent{ + DebugEventAdded{ + ProjectionSleep: time.Millisecond, + Blob: gu.Ptr("a"), + }, + }, + }}, + wantErr: zerrors.ThrowAlreadyExists(nil, "COMMAND-Aex6j", "debug aggregate already exists"), + }, + { + name: "double added event, already exists", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{&DebugEvents{ + AggregateID: "dbg1", + Events: []DebugEvent{ + DebugEventAdded{ + ProjectionSleep: time.Millisecond, + Blob: gu.Ptr("a"), + }, + DebugEventAdded{ + ProjectionSleep: time.Millisecond, + Blob: gu.Ptr("a"), + }, + }, + }}, + wantErr: zerrors.ThrowAlreadyExists(nil, "COMMAND-Aex6j", "debug aggregate already exists"), + }, + { + name: "changed event, not found", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{&DebugEvents{ + AggregateID: "dbg1", + Events: []DebugEvent{ + DebugEventChanged{ + ProjectionSleep: time.Millisecond, + Blob: gu.Ptr("a"), + }, + }, + }}, + wantErr: zerrors.ThrowNotFound(nil, "COMMAND-Thie6", "debug aggregate not found"), + }, + { + name: "removed event, not found", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{&DebugEvents{ + AggregateID: "dbg1", + Events: []DebugEvent{ + DebugEventRemoved{ + ProjectionSleep: time.Millisecond, + }, + }, + }}, + wantErr: zerrors.ThrowNotFound(nil, "COMMAND-Ohna9", "debug aggregate not found"), + }, + { + name: "changed after removed event, not found", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + debug_events.NewAddedEvent( + ctx, debug_events.NewAggregate("dbg1", "instance1"), + time.Millisecond, gu.Ptr("a"), + ), + ), + ), + ), + }, + args: args{&DebugEvents{ + AggregateID: "dbg1", + Events: []DebugEvent{ + DebugEventRemoved{ + ProjectionSleep: time.Millisecond, + }, + DebugEventChanged{ + ProjectionSleep: time.Millisecond, + Blob: gu.Ptr("a"), + }, + }, + }}, + wantErr: zerrors.ThrowNotFound(nil, "COMMAND-Thie6", "debug aggregate not found"), + }, + { + name: "double removed event, not found", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + debug_events.NewAddedEvent( + ctx, debug_events.NewAggregate("dbg1", "instance1"), + time.Millisecond, gu.Ptr("a"), + ), + ), + ), + ), + }, + args: args{&DebugEvents{ + AggregateID: "dbg1", + Events: []DebugEvent{ + DebugEventRemoved{ + ProjectionSleep: time.Millisecond, + }, + DebugEventRemoved{ + ProjectionSleep: time.Millisecond, + }, + }, + }}, + wantErr: zerrors.ThrowNotFound(nil, "COMMAND-Ohna9", "debug aggregate not found"), + }, + { + name: "added, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + debug_events.NewAddedEvent( + ctx, debug_events.NewAggregate("dbg1", "instance1"), + time.Millisecond, gu.Ptr("a"), + ), + ), + ), + }, + args: args{&DebugEvents{ + AggregateID: "dbg1", + Events: []DebugEvent{ + DebugEventAdded{ + ProjectionSleep: time.Millisecond, + Blob: gu.Ptr("a"), + }, + }, + }}, + want: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + { + name: "changed, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + debug_events.NewAddedEvent( + ctx, debug_events.NewAggregate("dbg1", "instance1"), + time.Millisecond, gu.Ptr("a"), + ), + ), + ), + expectPush( + debug_events.NewChangedEvent( + ctx, debug_events.NewAggregate("dbg1", "instance1"), + time.Millisecond, gu.Ptr("b"), + ), + ), + ), + }, + args: args{&DebugEvents{ + AggregateID: "dbg1", + Events: []DebugEvent{ + DebugEventChanged{ + ProjectionSleep: time.Millisecond, + Blob: gu.Ptr("b"), + }, + }, + }}, + want: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + { + name: "removed, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + debug_events.NewAddedEvent( + ctx, debug_events.NewAggregate("dbg1", "instance1"), + time.Millisecond, gu.Ptr("a"), + ), + ), + ), + expectPush( + debug_events.NewRemovedEvent( + ctx, debug_events.NewAggregate("dbg1", "instance1"), + time.Millisecond, + ), + ), + ), + }, + args: args{&DebugEvents{ + AggregateID: "dbg1", + Events: []DebugEvent{ + DebugEventRemoved{ + ProjectionSleep: time.Millisecond, + }, + }, + }}, + want: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + { + name: "added, changed, changed, removed ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + debug_events.NewAddedEvent( + ctx, debug_events.NewAggregate("dbg1", "instance1"), + time.Millisecond, gu.Ptr("a"), + ), + debug_events.NewChangedEvent( + ctx, debug_events.NewAggregate("dbg1", "instance1"), + time.Millisecond, gu.Ptr("b"), + ), + debug_events.NewChangedEvent( + ctx, debug_events.NewAggregate("dbg1", "instance1"), + time.Millisecond, gu.Ptr("c"), + ), + debug_events.NewRemovedEvent( + ctx, debug_events.NewAggregate("dbg1", "instance1"), + time.Millisecond, + ), + ), + ), + }, + args: args{&DebugEvents{ + AggregateID: "dbg1", + Events: []DebugEvent{ + DebugEventAdded{ + ProjectionSleep: time.Millisecond, + Blob: gu.Ptr("a"), + }, + DebugEventChanged{ + ProjectionSleep: time.Millisecond, + Blob: gu.Ptr("b"), + }, + DebugEventChanged{ + ProjectionSleep: time.Millisecond, + Blob: gu.Ptr("c"), + }, + DebugEventRemoved{ + ProjectionSleep: time.Millisecond, + }, + }, + }}, + want: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + } + got, err := c.CreateDebugEvents(ctx, tt.args.dbe) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/command/email.go b/internal/command/email.go index bb7dfc3f7d..85314f27b3 100644 --- a/internal/command/email.go +++ b/internal/command/email.go @@ -12,6 +12,9 @@ type Email struct { Address domain.EmailAddress Verified bool + // NoEmailVerification is used Verified field is false + NoEmailVerification bool + // ReturnCode is used if the Verified field is false ReturnCode bool diff --git a/internal/command/instance.go b/internal/command/instance.go index a0cc773019..f220c0c961 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -116,7 +116,7 @@ type InstanceSetup struct { } EmailTemplate []byte MessageTexts []*domain.CustomMessageText - SMTPConfiguration *smtp.Config + SMTPConfiguration *SMTPConfiguration OIDCSettings *OIDCSettings Quotas *SetQuotas Features *InstanceFeatures @@ -124,6 +124,15 @@ type InstanceSetup struct { Restrictions *SetRestrictions } +type SMTPConfiguration struct { + Description string + SMTP smtp.SMTP + Tls bool + From string + FromName string + ReplyToAddress string +} + type OIDCSettings struct { AccessTokenLifetime time.Duration IdTokenLifetime time.Duration @@ -145,6 +154,7 @@ type SecretGenerators struct { DomainVerification *crypto.GeneratorConfig OTPSMS *crypto.GeneratorConfig OTPEmail *crypto.GeneratorConfig + InviteCode *crypto.GeneratorConfig } type ZitadelConfig struct { @@ -439,7 +449,7 @@ func setupOIDCSettings(commands *Commands, validations *[]preparation.Validation ) } -func setupSMTPSettings(commands *Commands, validations *[]preparation.Validation, smtpConfig *smtp.Config, instanceAgg *instance.Aggregate) { +func setupSMTPSettings(commands *Commands, validations *[]preparation.Validation, smtpConfig *SMTPConfiguration, instanceAgg *instance.Aggregate) { if smtpConfig == nil { return } diff --git a/internal/command/instance_smtp_config_model.go b/internal/command/instance_smtp_config_model.go index e165419093..187ccdc0ee 100644 --- a/internal/command/instance_smtp_config_model.go +++ b/internal/command/instance_smtp_config_model.go @@ -12,8 +12,20 @@ import ( type IAMSMTPConfigWriteModel struct { eventstore.WriteModel - ID string - Description string + ID string + Description string + + SMTPConfig *SMTPConfig + HTTPConfig *HTTPConfig + + State domain.SMTPConfigState + + domain string + domainState domain.InstanceDomainState + smtpSenderAddressMatchesInstanceDomain bool +} + +type SMTPConfig struct { TLS bool Host string User string @@ -21,11 +33,6 @@ type IAMSMTPConfigWriteModel struct { SenderAddress string SenderName string ReplyToAddress string - State domain.SMTPConfigState - - domain string - domainState domain.InstanceDomainState - smtpSenderAddressMatchesInstanceDomain bool } func NewIAMSMTPConfigWriteModel(instanceID, id, domain string) *IAMSMTPConfigWriteModel { @@ -73,6 +80,23 @@ func (wm *IAMSMTPConfigWriteModel) Reduce() error { continue } wm.reduceSMTPConfigChangedEvent(e) + case *instance.SMTPConfigPasswordChangedEvent: + if wm.ID != e.ID { + continue + } + if e.Password != nil { + wm.SMTPConfig.Password = e.Password + } + case *instance.SMTPConfigHTTPAddedEvent: + if wm.ID != e.ID { + continue + } + wm.reduceSMTPConfigHTTPAddedEvent(e) + case *instance.SMTPConfigHTTPChangedEvent: + if wm.ID != e.ID { + continue + } + wm.reduceSMTPConfigHTTPChangedEvent(e) case *instance.SMTPConfigRemovedEvent: if wm.ID != e.ID { continue @@ -80,6 +104,7 @@ func (wm *IAMSMTPConfigWriteModel) Reduce() error { wm.reduceSMTPConfigRemovedEvent(e) case *instance.SMTPConfigActivatedEvent: if wm.ID != e.ID { + wm.State = domain.SMTPConfigStateInactive continue } wm.State = domain.SMTPConfigStateActive @@ -120,6 +145,8 @@ func (wm *IAMSMTPConfigWriteModel) Query() *eventstore.SearchQueryBuilder { instance.SMTPConfigRemovedEventType, instance.SMTPConfigChangedEventType, instance.SMTPConfigPasswordChangedEventType, + instance.SMTPConfigHTTPAddedEventType, + instance.SMTPConfigHTTPChangedEventType, instance.SMTPConfigActivatedEventType, instance.SMTPConfigDeactivatedEventType, instance.SMTPConfigRemovedEventType, @@ -133,6 +160,9 @@ func (wm *IAMSMTPConfigWriteModel) Query() *eventstore.SearchQueryBuilder { func (wm *IAMSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id, description string, tls bool, fromAddress, fromName, replyToAddress, smtpHost, smtpUser string, smtpPassword *crypto.CryptoValue) (*instance.SMTPConfigChangedEvent, bool, error) { changes := make([]instance.SMTPConfigChanges, 0) var err error + if wm.SMTPConfig == nil { + return nil, false, nil + } if wm.ID != id { changes = append(changes, instance.ChangeSMTPConfigID(id)) @@ -140,22 +170,22 @@ func (wm *IAMSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregat if wm.Description != description { changes = append(changes, instance.ChangeSMTPConfigDescription(description)) } - if wm.TLS != tls { + if wm.SMTPConfig.TLS != tls { changes = append(changes, instance.ChangeSMTPConfigTLS(tls)) } - if wm.SenderAddress != fromAddress { + if wm.SMTPConfig.SenderAddress != fromAddress { changes = append(changes, instance.ChangeSMTPConfigFromAddress(fromAddress)) } - if wm.SenderName != fromName { + if wm.SMTPConfig.SenderName != fromName { changes = append(changes, instance.ChangeSMTPConfigFromName(fromName)) } - if wm.ReplyToAddress != replyToAddress { + if wm.SMTPConfig.ReplyToAddress != replyToAddress { changes = append(changes, instance.ChangeSMTPConfigReplyToAddress(replyToAddress)) } - if wm.Host != smtpHost { + if wm.SMTPConfig.Host != smtpHost { changes = append(changes, instance.ChangeSMTPConfigSMTPHost(smtpHost)) } - if wm.User != smtpUser { + if wm.SMTPConfig.User != smtpUser { changes = append(changes, instance.ChangeSMTPConfigSMTPUser(smtpUser)) } if smtpPassword != nil { @@ -171,15 +201,58 @@ func (wm *IAMSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregat return changeEvent, true, nil } +func (wm *IAMSMTPConfigWriteModel) NewHTTPChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id, description, endpoint string) (*instance.SMTPConfigHTTPChangedEvent, bool, error) { + changes := make([]instance.SMTPConfigHTTPChanges, 0) + var err error + if wm.HTTPConfig == nil { + return nil, false, nil + } + + if wm.ID != id { + changes = append(changes, instance.ChangeSMTPConfigHTTPID(id)) + } + if wm.Description != description { + changes = append(changes, instance.ChangeSMTPConfigHTTPDescription(description)) + } + if wm.HTTPConfig.Endpoint != endpoint { + changes = append(changes, instance.ChangeSMTPConfigHTTPEndpoint(endpoint)) + } + if len(changes) == 0 { + return nil, false, nil + } + changeEvent, err := instance.NewSMTPConfigHTTPChangeEvent(ctx, aggregate, id, changes) + if err != nil { + return nil, false, err + } + return changeEvent, true, nil +} + func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigAddedEvent(e *instance.SMTPConfigAddedEvent) { wm.Description = e.Description - wm.TLS = e.TLS - wm.Host = e.Host - wm.User = e.User - wm.Password = e.Password - wm.SenderAddress = e.SenderAddress - wm.SenderName = e.SenderName - wm.ReplyToAddress = e.ReplyToAddress + wm.SMTPConfig = &SMTPConfig{ + TLS: e.TLS, + Host: e.Host, + User: e.User, + Password: e.Password, + SenderName: e.SenderName, + SenderAddress: e.SenderAddress, + ReplyToAddress: e.ReplyToAddress, + } + wm.State = domain.SMTPConfigStateInactive + // If ID has empty value we're dealing with the old and unique smtp settings + // These would be the default values for ID and State + if e.ID == "" { + wm.Description = "generic" + wm.ID = e.Aggregate().ResourceOwner + wm.State = domain.SMTPConfigStateActive + } +} + +func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigHTTPAddedEvent(e *instance.SMTPConfigHTTPAddedEvent) { + wm.Description = e.Description + wm.HTTPConfig = &HTTPConfig{ + Endpoint: e.Endpoint, + } wm.State = domain.SMTPConfigStateInactive // If ID has empty value we're dealing with the old and unique smtp settings // These would be the default values for ID and State @@ -191,29 +264,54 @@ func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigAddedEvent(e *instance.SMTPCo } func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigChangedEvent(e *instance.SMTPConfigChangedEvent) { + if wm.SMTPConfig == nil { + return + } + if e.Description != nil { wm.Description = *e.Description } if e.TLS != nil { - wm.TLS = *e.TLS + wm.SMTPConfig.TLS = *e.TLS } if e.Host != nil { - wm.Host = *e.Host + wm.SMTPConfig.Host = *e.Host } if e.User != nil { - wm.User = *e.User + wm.SMTPConfig.User = *e.User } if e.Password != nil { - wm.Password = e.Password + wm.SMTPConfig.Password = e.Password } if e.FromAddress != nil { - wm.SenderAddress = *e.FromAddress + wm.SMTPConfig.SenderAddress = *e.FromAddress } if e.FromName != nil { - wm.SenderName = *e.FromName + wm.SMTPConfig.SenderName = *e.FromName } if e.ReplyToAddress != nil { - wm.ReplyToAddress = *e.ReplyToAddress + wm.SMTPConfig.ReplyToAddress = *e.ReplyToAddress + } + + // If ID has empty value we're dealing with the old and unique smtp settings + // These would be the default values for ID and State + if e.ID == "" { + wm.Description = "generic" + wm.ID = e.Aggregate().ResourceOwner + wm.State = domain.SMTPConfigStateActive + } +} + +func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigHTTPChangedEvent(e *instance.SMTPConfigHTTPChangedEvent) { + if wm.HTTPConfig == nil { + return + } + + if e.Description != nil { + wm.Description = *e.Description + } + if e.Endpoint != nil { + wm.HTTPConfig.Endpoint = *e.Endpoint } // If ID has empty value we're dealing with the old and unique smtp settings @@ -227,13 +325,8 @@ func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigChangedEvent(e *instance.SMTP func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigRemovedEvent(e *instance.SMTPConfigRemovedEvent) { wm.Description = "" - wm.TLS = false - wm.SenderName = "" - wm.SenderAddress = "" - wm.ReplyToAddress = "" - wm.Host = "" - wm.User = "" - wm.Password = nil + wm.HTTPConfig = nil + wm.SMTPConfig = nil wm.State = domain.SMTPConfigStateRemoved // If ID has empty value we're dealing with the old and unique smtp settings diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index e1fa43e879..b2b4632cf6 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -21,7 +21,7 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func TestCommandSide_AddOrgGenericOAuthIDP(t *testing.T) { +func TestCommandSide_AddOrgGenericOAuthProvider(t *testing.T) { type fields struct { eventstore func(*testing.T) *eventstore.Eventstore idGenerator id.Generator diff --git a/internal/command/sms_config.go b/internal/command/sms_config.go index cd5e38e518..82eae763df 100644 --- a/internal/command/sms_config.go +++ b/internal/command/sms_config.go @@ -5,203 +5,328 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddSMSConfigTwilio(ctx context.Context, instanceID string, config *twilio.Config) (string, *domain.ObjectDetails, error) { - id, err := c.idGenerator.Next() - if err != nil { - return "", nil, err +type AddTwilioConfig struct { + Details *domain.ObjectDetails + ResourceOwner string + ID string + + Description string + SID string + Token string + SenderNumber string +} + +func (c *Commands) AddSMSConfigTwilio(ctx context.Context, config *AddTwilioConfig) (err error) { + if config.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-ZLrZhKSKq0", "Errors.ResourceOwnerMissing") } - smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id) + if config.ID == "" { + config.ID, err = c.idGenerator.Next() + if err != nil { + return err + } + } + smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID) if err != nil { - return "", nil, err + return err } var token *crypto.CryptoValue if config.Token != "" { token, err = crypto.Encrypt([]byte(config.Token), c.smsEncryption) if err != nil { - return "", nil, err + return err } } - - iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigTwilioAddedEvent( - ctx, - iamAgg, - id, - config.SID, - config.SenderNumber, - token)) + err = c.pushAppendAndReduce(ctx, + smsConfigWriteModel, + instance.NewSMSConfigTwilioAddedEvent( + ctx, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + config.ID, + config.Description, + config.SID, + config.SenderNumber, + token, + ), + ) if err != nil { - return "", nil, err + return err } - err = AppendAndReduce(smsConfigWriteModel, pushedEvents...) - if err != nil { - return "", nil, err - } - return id, writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil + config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel) + return nil } -func (c *Commands) ChangeSMSConfigTwilio(ctx context.Context, instanceID, id string, config *twilio.Config) (*domain.ObjectDetails, error) { - if id == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "SMS-e9jwf", "Errors.IDMissing") +type ChangeTwilioConfig struct { + Details *domain.ObjectDetails + ResourceOwner string + ID string + + Description *string + SID *string + Token *string + SenderNumber *string +} + +func (c *Commands) ChangeSMSConfigTwilio(ctx context.Context, config *ChangeTwilioConfig) (err error) { + if config.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-RHXryJwmFG", "Errors.ResourceOwnerMissing") } - smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id) + if config.ID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-gMr93iNhTR", "Errors.IDMissing") + } + smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID) if err != nil { - return nil, err + return err } if !smsConfigWriteModel.State.Exists() || smsConfigWriteModel.Twilio == nil { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-2m9fw", "Errors.SMSConfig.NotFound") + return zerrors.ThrowNotFound(nil, "COMMAND-MUY0IFAf8O", "Errors.SMSConfig.NotFound") } - iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel) - - changedEvent, hasChanged, err := smsConfigWriteModel.NewChangedEvent( + changedEvent, hasChanged, err := smsConfigWriteModel.NewTwilioChangedEvent( ctx, - iamAgg, - id, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + config.ID, + config.Description, config.SID, config.SenderNumber) if err != nil { - return nil, err + return err } if !hasChanged { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-jf9wk", "Errors.NoChangesFound") + config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel) + return nil } - pushedEvents, err := c.eventstore.Push(ctx, changedEvent) + err = c.pushAppendAndReduce(ctx, + smsConfigWriteModel, + changedEvent, + ) if err != nil { - return nil, err + return err } - err = AppendAndReduce(smsConfigWriteModel, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil + config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel) + return nil } -func (c *Commands) ChangeSMSConfigTwilioToken(ctx context.Context, instanceID, id, token string) (*domain.ObjectDetails, error) { - smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id) +func (c *Commands) ChangeSMSConfigTwilioToken(ctx context.Context, resourceOwner, id, token string) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-sLLA1HnMzj", "Errors.ResourceOwnerMissing") + } + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "SMS-PeNaqbC0r0", "Errors.IDMissing") + } + + smsConfigWriteModel, err := c.getSMSConfig(ctx, resourceOwner, id) if err != nil { return nil, err } if !smsConfigWriteModel.State.Exists() || smsConfigWriteModel.Twilio == nil { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-fj9wf", "Errors.SMSConfig.NotFound") + return nil, zerrors.ThrowNotFound(nil, "COMMAND-ij3NhEHATp", "Errors.SMSConfig.NotFound") } - iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel) newtoken, err := crypto.Encrypt([]byte(token), c.smsEncryption) if err != nil { return nil, err } - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigTokenChangedEvent( - ctx, - iamAgg, - id, - newtoken)) - if err != nil { - return nil, err - } - err = AppendAndReduce(smsConfigWriteModel, pushedEvents...) + err = c.pushAppendAndReduce(ctx, + smsConfigWriteModel, + instance.NewSMSConfigTokenChangedEvent( + ctx, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + id, + newtoken, + ), + ) if err != nil { return nil, err } return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil } -func (c *Commands) ActivateSMSConfig(ctx context.Context, instanceID, id string) (*domain.ObjectDetails, error) { - if id == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "SMS-dn93n", "Errors.IDMissing") +type AddSMSHTTP struct { + Details *domain.ObjectDetails + ResourceOwner string + ID string + + Description string + Endpoint string +} + +func (c *Commands) AddSMSConfigHTTP(ctx context.Context, config *AddSMSHTTP) (err error) { + if config.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-huy99qWjX4", "Errors.ResourceOwnerMissing") } - smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id) + if config.ID == "" { + config.ID, err = c.idGenerator.Next() + if err != nil { + return err + } + } + smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID) + if err != nil { + return err + } + + err = c.pushAppendAndReduce(ctx, + smsConfigWriteModel, + instance.NewSMSConfigHTTPAddedEvent( + ctx, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + config.ID, + config.Description, + config.Endpoint, + ), + ) + if err != nil { + return err + } + config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel) + return nil +} + +type ChangeSMSHTTP struct { + Details *domain.ObjectDetails + ResourceOwner string + ID string + + Description *string + Endpoint *string +} + +func (c *Commands) ChangeSMSConfigHTTP(ctx context.Context, config *ChangeSMSHTTP) (err error) { + if config.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-M622CFQnwK", "Errors.ResourceOwnerMissing") + } + if config.ID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-phyb2e4Kll", "Errors.IDMissing") + } + smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID) + if err != nil { + return err + } + if !smsConfigWriteModel.State.Exists() || smsConfigWriteModel.HTTP == nil { + return zerrors.ThrowNotFound(nil, "COMMAND-6NW4I5Kqzj", "Errors.SMSConfig.NotFound") + } + changedEvent, hasChanged, err := smsConfigWriteModel.NewHTTPChangedEvent( + ctx, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + config.ID, + config.Description, + config.Endpoint) + if err != nil { + return err + } + if !hasChanged { + config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel) + return nil + } + err = c.pushAppendAndReduce(ctx, smsConfigWriteModel, changedEvent) + if err != nil { + return err + } + config.Details = writeModelToObjectDetails(&smsConfigWriteModel.WriteModel) + return nil +} + +func (c *Commands) ActivateSMSConfig(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-EFgoOg997V", "Errors.ResourceOwnerMissing") + } + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-jJ6TVqzvjp", "Errors.IDMissing") + } + + smsConfigWriteModel, err := c.getSMSConfig(ctx, resourceOwner, id) if err != nil { return nil, err } if !smsConfigWriteModel.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-sn9we", "Errors.SMSConfig.NotFound") + return nil, zerrors.ThrowNotFound(nil, "COMMAND-9ULtp9PH5E", "Errors.SMSConfig.NotFound") } if smsConfigWriteModel.State == domain.SMSConfigStateActive { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-sn9we", "Errors.SMSConfig.AlreadyActive") + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-B25GFeIvRi", "Errors.SMSConfig.AlreadyActive") } - iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigTwilioActivatedEvent( - ctx, - iamAgg, - id)) - if err != nil { - return nil, err - } - err = AppendAndReduce(smsConfigWriteModel, pushedEvents...) + err = c.pushAppendAndReduce(ctx, smsConfigWriteModel, + instance.NewSMSConfigActivatedEvent( + ctx, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + id, + ), + ) if err != nil { return nil, err } return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil } -func (c *Commands) DeactivateSMSConfig(ctx context.Context, instanceID, id string) (*domain.ObjectDetails, error) { - if id == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "SMS-frkwf", "Errors.IDMissing") +func (c *Commands) DeactivateSMSConfig(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-V9NWOZj8Gi", "Errors.ResourceOwnerMissing") } - smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id) + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xs1ah1v1CL", "Errors.IDMissing") + } + smsConfigWriteModel, err := c.getSMSConfig(ctx, resourceOwner, id) if err != nil { return nil, err } if !smsConfigWriteModel.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-s39Kg", "Errors.SMSConfig.NotFound") + return nil, zerrors.ThrowNotFound(nil, "COMMAND-La91dGNhbM", "Errors.SMSConfig.NotFound") } if smsConfigWriteModel.State == domain.SMSConfigStateInactive { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-dm9e3", "Errors.SMSConfig.AlreadyDeactivated") + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-OSZAEkYvk7", "Errors.SMSConfig.AlreadyDeactivated") } - - iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigDeactivatedEvent( - ctx, - iamAgg, - id)) - if err != nil { - return nil, err - } - err = AppendAndReduce(smsConfigWriteModel, pushedEvents...) + err = c.pushAppendAndReduce(ctx, + smsConfigWriteModel, + instance.NewSMSConfigDeactivatedEvent( + ctx, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + id, + ), + ) if err != nil { return nil, err } return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil } -func (c *Commands) RemoveSMSConfig(ctx context.Context, instanceID, id string) (*domain.ObjectDetails, error) { - if id == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "SMS-3j9fs", "Errors.IDMissing") +func (c *Commands) RemoveSMSConfig(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-cw0NSJsn1v", "Errors.ResourceOwnerMissing") } - smsConfigWriteModel, err := c.getSMSConfig(ctx, instanceID, id) + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Qrz7lvdC4c", "Errors.IDMissing") + } + smsConfigWriteModel, err := c.getSMSConfig(ctx, resourceOwner, id) if err != nil { return nil, err } if !smsConfigWriteModel.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-sn9we", "Errors.SMSConfig.NotFound") + return nil, zerrors.ThrowNotFound(nil, "COMMAND-povEVHPCkV", "Errors.SMSConfig.NotFound") } - iamAgg := InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMSConfigRemovedEvent( - ctx, - iamAgg, - id)) - if err != nil { - return nil, err - } - err = AppendAndReduce(smsConfigWriteModel, pushedEvents...) + err = c.pushAppendAndReduce(ctx, + smsConfigWriteModel, + instance.NewSMSConfigRemovedEvent( + ctx, + InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), + id, + ), + ) if err != nil { return nil, err } return writeModelToObjectDetails(&smsConfigWriteModel.WriteModel), nil } + func (c *Commands) getSMSConfig(ctx context.Context, instanceID, id string) (_ *IAMSMSConfigWriteModel, err error) { writeModel := NewIAMSMSConfigWriteModel(instanceID, id) err = c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err } - return writeModel, nil } diff --git a/internal/command/sms_config_model.go b/internal/command/sms_config_model.go index 2c215ebf6b..06360a5cfd 100644 --- a/internal/command/sms_config_model.go +++ b/internal/command/sms_config_model.go @@ -12,9 +12,11 @@ import ( type IAMSMSConfigWriteModel struct { eventstore.WriteModel - ID string - Twilio *TwilioConfig - State domain.SMSConfigState + ID string + Description string + Twilio *TwilioConfig + HTTP *HTTPConfig + State domain.SMSConfigState } type TwilioConfig struct { @@ -23,6 +25,10 @@ type TwilioConfig struct { SenderNumber string } +type HTTPConfig struct { + Endpoint string +} + func NewIAMSMSConfigWriteModel(instanceID, id string) *IAMSMSConfigWriteModel { return &IAMSMSConfigWriteModel{ WriteModel: eventstore.WriteModel{ @@ -46,11 +52,15 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error { Token: e.Token, SenderNumber: e.SenderNumber, } + wm.Description = e.Description wm.State = domain.SMSConfigStateInactive case *instance.SMSConfigTwilioChangedEvent: if wm.ID != e.ID { continue } + if e.Description != nil { + wm.Description = *e.Description + } if e.SID != nil { wm.Twilio.SID = *e.SID } @@ -62,8 +72,46 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error { continue } wm.Twilio.Token = e.Token + case *instance.SMSConfigHTTPAddedEvent: + if wm.ID != e.ID { + continue + } + wm.HTTP = &HTTPConfig{ + Endpoint: e.Endpoint, + } + wm.Description = e.Description + wm.State = domain.SMSConfigStateInactive + case *instance.SMSConfigHTTPChangedEvent: + if wm.ID != e.ID { + continue + } + if e.Description != nil { + wm.Description = *e.Description + } + if e.Endpoint != nil { + wm.HTTP.Endpoint = *e.Endpoint + } + case *instance.SMSConfigTwilioActivatedEvent: + if wm.ID != e.ID { + wm.State = domain.SMSConfigStateInactive + continue + } + wm.State = domain.SMSConfigStateActive + case *instance.SMSConfigTwilioDeactivatedEvent: + if wm.ID != e.ID { + continue + } + wm.State = domain.SMSConfigStateInactive + case *instance.SMSConfigTwilioRemovedEvent: + if wm.ID != e.ID { + continue + } + wm.Twilio = nil + wm.HTTP = nil + wm.State = domain.SMSConfigStateRemoved case *instance.SMSConfigActivatedEvent: if wm.ID != e.ID { + wm.State = domain.SMSConfigStateInactive continue } wm.State = domain.SMSConfigStateActive @@ -77,6 +125,7 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error { continue } wm.Twilio = nil + wm.HTTP = nil wm.State = domain.SMSConfigStateRemoved } } @@ -92,21 +141,33 @@ func (wm *IAMSMSConfigWriteModel) Query() *eventstore.SearchQueryBuilder { instance.SMSConfigTwilioAddedEventType, instance.SMSConfigTwilioChangedEventType, instance.SMSConfigTwilioTokenChangedEventType, + instance.SMSConfigHTTPAddedEventType, + instance.SMSConfigHTTPChangedEventType, + instance.SMSConfigTwilioActivatedEventType, + instance.SMSConfigTwilioDeactivatedEventType, + instance.SMSConfigTwilioRemovedEventType, instance.SMSConfigActivatedEventType, instance.SMSConfigDeactivatedEventType, instance.SMSConfigRemovedEventType). Builder() } -func (wm *IAMSMSConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id, sid, senderNumber string) (*instance.SMSConfigTwilioChangedEvent, bool, error) { +func (wm *IAMSMSConfigWriteModel) NewTwilioChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id string, description, sid, senderNumber *string) (*instance.SMSConfigTwilioChangedEvent, bool, error) { changes := make([]instance.SMSConfigTwilioChanges, 0) var err error - if wm.Twilio.SID != sid { - changes = append(changes, instance.ChangeSMSConfigTwilioSID(sid)) + if wm.Twilio == nil { + return nil, false, nil } - if wm.Twilio.SenderNumber != senderNumber { - changes = append(changes, instance.ChangeSMSConfigTwilioSenderNumber(senderNumber)) + + if description != nil && wm.Description != *description { + changes = append(changes, instance.ChangeSMSConfigTwilioDescription(*description)) + } + if sid != nil && wm.Twilio.SID != *sid { + changes = append(changes, instance.ChangeSMSConfigTwilioSID(*sid)) + } + if senderNumber != nil && wm.Twilio.SenderNumber != *senderNumber { + changes = append(changes, instance.ChangeSMSConfigTwilioSenderNumber(*senderNumber)) } if len(changes) == 0 { @@ -118,3 +179,28 @@ func (wm *IAMSMSConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate } return changeEvent, true, nil } + +func (wm *IAMSMSConfigWriteModel) NewHTTPChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id string, description, endpoint *string) (*instance.SMSConfigHTTPChangedEvent, bool, error) { + changes := make([]instance.SMSConfigHTTPChanges, 0) + var err error + + if wm.HTTP == nil { + return nil, false, nil + } + + if description != nil && wm.Description != *description { + changes = append(changes, instance.ChangeSMSConfigHTTPDescription(*description)) + } + if endpoint != nil && wm.HTTP.Endpoint != *endpoint { + changes = append(changes, instance.ChangeSMSConfigHTTPEndpoint(*endpoint)) + } + + if len(changes) == 0 { + return nil, false, nil + } + changeEvent, err := instance.NewSMSConfigHTTPChangedEvent(ctx, aggregate, id, changes) + if err != nil { + return nil, false, err + } + return changeEvent, true, nil +} diff --git a/internal/command/sms_config_test.go b/internal/command/sms_config_test.go index 8d96751944..b0936ab8f1 100644 --- a/internal/command/sms_config_test.go +++ b/internal/command/sms_config_test.go @@ -2,8 +2,10 @@ package command import ( "context" + "errors" "testing" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -12,21 +14,19 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" id_mock "github.com/zitadel/zitadel/internal/id/mock" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/zerrors" ) func TestCommandSide_AddSMSConfigTwilio(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore idGenerator id.Generator alg crypto.EncryptionAlgorithm } type args struct { - ctx context.Context - instanceID string - sms *twilio.Config + ctx context.Context + sms *AddTwilioConfig } type res struct { want *domain.ObjectDetails @@ -38,17 +38,32 @@ func TestCommandSide_AddSMSConfigTwilio(t *testing.T) { args args res res }{ + { + name: "add sms config twilio, missing resourceowner", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + sms: &AddTwilioConfig{}, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-ZLrZhKSKq0", "Errors.ResourceOwnerMissing")) + }, + }, + }, { name: "add sms config twilio, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), expectPush( instance.NewSMSConfigTwilioAddedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", + "description", "sid", "senderName", &crypto.CryptoValue{ @@ -64,12 +79,13 @@ func TestCommandSide_AddSMSConfigTwilio(t *testing.T) { alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: context.Background(), - instanceID: "INSTANCE", - sms: &twilio.Config{ - SID: "sid", - Token: "token", - SenderNumber: "senderName", + ctx: context.Background(), + sms: &AddTwilioConfig{ + ResourceOwner: "INSTANCE", + Description: "description", + SID: "sid", + Token: "token", + SenderNumber: "senderName", }, }, res: res{ @@ -82,11 +98,11 @@ func TestCommandSide_AddSMSConfigTwilio(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, smsEncryption: tt.fields.alg, } - _, got, err := r.AddSMSConfigTwilio(tt.args.ctx, tt.args.instanceID, tt.args.sms) + err := r.AddSMSConfigTwilio(tt.args.ctx, tt.args.sms) if tt.res.err == nil { assert.NoError(t, err) } @@ -94,7 +110,7 @@ func TestCommandSide_AddSMSConfigTwilio(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assertObjectDetails(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, tt.args.sms.Details) } }) } @@ -102,13 +118,11 @@ func TestCommandSide_AddSMSConfigTwilio(t *testing.T) { func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { - ctx context.Context - instanceID string - id string - sms *twilio.Config + ctx context.Context + sms *ChangeTwilioConfig } type res struct { want *domain.ObjectDetails @@ -120,50 +134,70 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { args args res res }{ + { - name: "id empty, precondition error", + name: "resourceowner empty, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), - sms: &twilio.Config{}, + sms: &ChangeTwilioConfig{}, }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-RHXryJwmFG", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "id empty, invalid argument error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + sms: &ChangeTwilioConfig{ + ResourceOwner: "INSTANCE", + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-gMr93iNhTR", "Errors.IDMissing")) + }, }, }, { name: "sms not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, args: args{ - ctx: context.Background(), - sms: &twilio.Config{}, - instanceID: "INSTANCE", - id: "id", + ctx: context.Background(), + sms: &ChangeTwilioConfig{ + ResourceOwner: "INSTANCE", + ID: "id", + }, }, res: res{ - err: zerrors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-MUY0IFAf8O", "Errors.SMSConfig.NotFound")) + }, }, }, { - name: "no changes, precondition error", + name: "no changes", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMSConfigTwilioAddedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", + "description", "sid", "senderName", &crypto.CryptoValue{ @@ -179,29 +213,31 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { }, args: args{ ctx: context.Background(), - sms: &twilio.Config{ - SID: "sid", - Token: "token", - SenderNumber: "senderName", + sms: &ChangeTwilioConfig{ + ResourceOwner: "INSTANCE", + ID: "providerid", + SID: gu.Ptr("sid"), + Token: gu.Ptr("token"), + SenderNumber: gu.Ptr("senderName"), }, - instanceID: "INSTANCE", - id: "providerid", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, }, }, { name: "sms config twilio change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMSConfigTwilioAddedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", + "description", "sid", "token", &crypto.CryptoValue{ @@ -219,19 +255,21 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { "providerid", "sid2", "senderName2", + "description2", ), ), ), }, args: args{ ctx: context.Background(), - sms: &twilio.Config{ - SID: "sid2", - Token: "token2", - SenderNumber: "senderName2", + sms: &ChangeTwilioConfig{ + ResourceOwner: "INSTANCE", + ID: "providerid", + Description: gu.Ptr("description2"), + SID: gu.Ptr("sid2"), + Token: gu.Ptr("token2"), + SenderNumber: gu.Ptr("senderName2"), }, - instanceID: "INSTANCE", - id: "providerid", }, res: res{ want: &domain.ObjectDetails{ @@ -243,9 +281,9 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } - got, err := r.ChangeSMSConfigTwilio(tt.args.ctx, tt.args.instanceID, tt.args.id, tt.args.sms) + err := r.ChangeSMSConfigTwilio(tt.args.ctx, tt.args.sms) if tt.res.err == nil { assert.NoError(t, err) } @@ -253,15 +291,264 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assertObjectDetails(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, tt.args.sms.Details) } }) } } -func TestCommandSide_ActivateSMSConfigTwilio(t *testing.T) { +func TestCommandSide_AddSMSConfigHTTP(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + alg crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + http *AddSMSHTTP + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "add sms config http, resource owner missing", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + http: &AddSMSHTTP{}, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-huy99qWjX4", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "add sms config http, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + instance.NewSMSConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", + "endpoint", + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "providerid"), + }, + args: args{ + ctx: context.Background(), + http: &AddSMSHTTP{ + ResourceOwner: "INSTANCE", + Description: "description", + Endpoint: "endpoint", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + smsEncryption: tt.fields.alg, + } + err := r.AddSMSConfigHTTP(tt.args.ctx, tt.args.http) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, tt.args.http.Details) + } + }) + } +} + +func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + } + type args struct { + ctx context.Context + http *ChangeSMSHTTP + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "resourceowner empty, precondition error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + http: &ChangeSMSHTTP{}, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-M622CFQnwK", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "id empty, precondition error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + http: &ChangeSMSHTTP{ + ResourceOwner: "INSTANCE", + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-phyb2e4Kll", "Errors.IDMissing")) + }, + }, + }, + { + name: "sms not existing, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + http: &ChangeSMSHTTP{ + ResourceOwner: "INSTANCE", + ID: "id", + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-6NW4I5Kqzj", "Errors.SMSConfig.NotFound")) + }, + }, + }, + { + name: "no changes", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", + "endpoint", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + http: &ChangeSMSHTTP{ + ResourceOwner: "INSTANCE", + ID: "providerid", + Endpoint: gu.Ptr("endpoint"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "sms config http change, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", + "endpoint", + ), + ), + ), + expectPush( + newSMSConfigHTTPChangedEvent( + context.Background(), + "providerid", + "endpoint2", + "description2", + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + http: &ChangeSMSHTTP{ + ResourceOwner: "INSTANCE", + ID: "providerid", + Description: gu.Ptr("description2"), + Endpoint: gu.Ptr("endpoint2"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + } + err := r.ChangeSMSConfigHTTP(tt.args.ctx, tt.args.http) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, tt.args.http.Details) + } + }) + } +} + +func TestCommandSide_ActivateSMSConfig(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -279,24 +566,38 @@ func TestCommandSide_ActivateSMSConfigTwilio(t *testing.T) { res res }{ { - name: "id empty, invalid error", + name: "resourceowner empty, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-EFgoOg997V", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "id empty, invalid error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-jJ6TVqzvjp", "Errors.IDMissing")) + }, }, }, { name: "sms not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -306,20 +607,59 @@ func TestCommandSide_ActivateSMSConfigTwilio(t *testing.T) { id: "id", }, res: res{ - err: zerrors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-9ULtp9PH5E", "Errors.SMSConfig.NotFound")) + }, }, }, { - name: "sms config twilio activate, ok", + name: "sms existing, already active", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMSConfigTwilioAddedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", + "description", + "sid", + "sender-name", + &crypto.CryptoValue{}, + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + id: "providerid", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-B25GFeIvRi", "Errors.SMSConfig.AlreadyActive")) + }, + }, + }, + { + name: "sms config twilio activate, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", "sid", "sender-name", &crypto.CryptoValue{}, @@ -327,7 +667,42 @@ func TestCommandSide_ActivateSMSConfigTwilio(t *testing.T) { ), ), expectPush( - instance.NewSMSConfigTwilioActivatedEvent( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + id: "providerid", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "sms config http activate, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", + "endpoint", + ), + ), + ), + expectPush( + instance.NewSMSConfigActivatedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", @@ -350,7 +725,7 @@ func TestCommandSide_ActivateSMSConfigTwilio(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.ActivateSMSConfig(tt.args.ctx, tt.args.instanceID, tt.args.id) if tt.res.err == nil { @@ -366,9 +741,9 @@ func TestCommandSide_ActivateSMSConfigTwilio(t *testing.T) { } } -func TestCommandSide_DeactivateSMSConfigTwilio(t *testing.T) { +func TestCommandSide_DeactivateSMSConfig(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -384,26 +759,39 @@ func TestCommandSide_DeactivateSMSConfigTwilio(t *testing.T) { fields fields args args res res - }{ + }{{ + name: "resourceowner empty, invalid error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-V9NWOZj8Gi", "Errors.ResourceOwnerMissing")) + }, + }, + }, { name: "id empty, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ - ctx: context.Background(), + ctx: context.Background(), + instanceID: "INSTANCE", }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-xs1ah1v1CL", "Errors.IDMissing")) + }, }, }, { name: "sms not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -413,27 +801,115 @@ func TestCommandSide_DeactivateSMSConfigTwilio(t *testing.T) { id: "id", }, res: res{ - err: zerrors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-La91dGNhbM", "Errors.SMSConfig.NotFound")) + }, }, }, { - name: "sms config twilio deactivate, ok", + name: "sms config twilio deactivate, already deactivated", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMSConfigTwilioAddedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", + "description", "sid", "sender-name", &crypto.CryptoValue{}, ), ), eventFromEventPusher( - instance.NewSMSConfigTwilioActivatedEvent( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigDeactivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + id: "providerid", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-OSZAEkYvk7", "Errors.SMSConfig.AlreadyDeactivated")) + }, + }, + }, + { + name: "sms config twilio deactivate, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigTwilioAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", + "sid", + "sender-name", + &crypto.CryptoValue{}, + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + ), + ), + ), + expectPush( + instance.NewSMSConfigDeactivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + id: "providerid", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "sms config http deactivate, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", + "endpoint", + ), + ), + eventFromEventPusher( + instance.NewSMSConfigActivatedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", @@ -464,7 +940,7 @@ func TestCommandSide_DeactivateSMSConfigTwilio(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.DeactivateSMSConfig(tt.args.ctx, tt.args.instanceID, tt.args.id) if tt.res.err == nil { @@ -482,7 +958,7 @@ func TestCommandSide_DeactivateSMSConfigTwilio(t *testing.T) { func TestCommandSide_RemoveSMSConfig(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -500,24 +976,38 @@ func TestCommandSide_RemoveSMSConfig(t *testing.T) { res res }{ { - name: "id empty, invalid error", + name: "resourceowner empty, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-cw0NSJsn1v", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "id empty, invalid error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-Qrz7lvdC4c", "Errors.IDMissing")) + }, }, }, { name: "sms not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -527,20 +1017,22 @@ func TestCommandSide_RemoveSMSConfig(t *testing.T) { id: "id", }, res: res{ - err: zerrors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-povEVHPCkV", "Errors.SMSConfig.NotFound")) + }, }, }, { - name: "sms config remove, ok", + name: "sms config remove, twilio, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMSConfigTwilioAddedEvent( context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, "providerid", + "description", "sid", "sender-name", &crypto.CryptoValue{}, @@ -567,11 +1059,46 @@ func TestCommandSide_RemoveSMSConfig(t *testing.T) { }, }, }, + { + name: "sms config remove, http, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMSConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + "description", + "endpoint", + ), + ), + ), + expectPush( + instance.NewSMSConfigRemovedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "providerid", + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + id: "providerid", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.RemoveSMSConfig(tt.args.ctx, tt.args.instanceID, tt.args.id) if tt.res.err == nil { @@ -587,10 +1114,11 @@ func TestCommandSide_RemoveSMSConfig(t *testing.T) { } } -func newSMSConfigTwilioChangedEvent(ctx context.Context, id, sid, senderName string) *instance.SMSConfigTwilioChangedEvent { +func newSMSConfigTwilioChangedEvent(ctx context.Context, id, sid, senderName, description string) *instance.SMSConfigTwilioChangedEvent { changes := []instance.SMSConfigTwilioChanges{ instance.ChangeSMSConfigTwilioSID(sid), instance.ChangeSMSConfigTwilioSenderNumber(senderName), + instance.ChangeSMSConfigTwilioDescription(description), } event, _ := instance.NewSMSConfigTwilioChangedEvent(ctx, &instance.NewAggregate("INSTANCE").Aggregate, @@ -599,3 +1127,16 @@ func newSMSConfigTwilioChangedEvent(ctx context.Context, id, sid, senderName str ) return event } + +func newSMSConfigHTTPChangedEvent(ctx context.Context, id, endpoint, description string) *instance.SMSConfigHTTPChangedEvent { + changes := []instance.SMSConfigHTTPChanges{ + instance.ChangeSMSConfigHTTPEndpoint(endpoint), + instance.ChangeSMSConfigHTTPDescription(description), + } + event, _ := instance.NewSMSConfigHTTPChangedEvent(ctx, + &instance.NewAggregate("INSTANCE").Aggregate, + id, + changes, + ) + return event +} diff --git a/internal/command/smtp.go b/internal/command/smtp.go index f51bef9b53..d856b9a3da 100644 --- a/internal/command/smtp.go +++ b/internal/command/smtp.go @@ -15,151 +15,189 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddSMTPConfig(ctx context.Context, instanceID string, config *smtp.Config) (string, *domain.ObjectDetails, error) { - id, err := c.idGenerator.Next() - if err != nil { - return "", nil, err +type AddSMTPConfig struct { + Details *domain.ObjectDetails + ResourceOwner string + ID string + + Description string + Host string + User string + Password string + Tls bool + From string + FromName string + ReplyToAddress string +} + +func (c *Commands) AddSMTPConfig(ctx context.Context, config *AddSMTPConfig) (err error) { + if config.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-PQN0wsqSyi", "Errors.ResourceOwnerMissing") + } + if config.ID == "" { + config.ID, err = c.idGenerator.Next() + if err != nil { + return err + } } from := strings.TrimSpace(config.From) if from == "" { - return "", nil, zerrors.ThrowInvalidArgument(nil, "INST-ASv2d", "Errors.Invalid.Argument") + return zerrors.ThrowInvalidArgument(nil, "COMMAND-SAAFpV8VKV", "Errors.Invalid.Argument") } fromSplitted := strings.Split(from, "@") senderDomain := fromSplitted[len(fromSplitted)-1] description := strings.TrimSpace(config.Description) replyTo := strings.TrimSpace(config.ReplyToAddress) - hostAndPort := strings.TrimSpace(config.SMTP.Host) + hostAndPort := strings.TrimSpace(config.Host) if _, _, err := net.SplitHostPort(hostAndPort); err != nil { - return "", nil, zerrors.ThrowInvalidArgument(nil, "INST-9JdRe", "Errors.Invalid.Argument") + return zerrors.ThrowInvalidArgument(nil, "COMMAND-EvAtufIinh", "Errors.Invalid.Argument") } var smtpPassword *crypto.CryptoValue - if config.SMTP.Password != "" { - smtpPassword, err = crypto.Encrypt([]byte(config.SMTP.Password), c.smtpEncryption) + if config.Password != "" { + smtpPassword, err = crypto.Encrypt([]byte(config.Password), c.smtpEncryption) if err != nil { - return "", nil, err + return err } } - smtpConfigWriteModel, err := c.getSMTPConfig(ctx, instanceID, id, senderDomain) + smtpConfigWriteModel, err := c.getSMTPConfig(ctx, config.ResourceOwner, config.ID, senderDomain) if err != nil { - return "", nil, err + return err } err = checkSenderAddress(smtpConfigWriteModel) if err != nil { - return "", nil, err + return err } - iamAgg := InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMTPConfigAddedEvent( - ctx, - iamAgg, - id, - description, - config.Tls, - config.From, - config.FromName, - replyTo, - hostAndPort, - config.SMTP.User, - smtpPassword, - )) + err = c.pushAppendAndReduce(ctx, + smtpConfigWriteModel, + instance.NewSMTPConfigAddedEvent( + ctx, + InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel), + config.ID, + description, + config.Tls, + config.From, + config.FromName, + replyTo, + hostAndPort, + config.User, + smtpPassword, + ), + ) if err != nil { - return "", nil, err + return err } - - err = AppendAndReduce(smtpConfigWriteModel, pushedEvents...) - if err != nil { - return "", nil, err - } - return id, writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel), nil + config.Details = writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel) + return nil } -func (c *Commands) ChangeSMTPConfig(ctx context.Context, instanceID string, id string, config *smtp.Config) (*domain.ObjectDetails, error) { - if id == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "SMTP-x8vo9", "Errors.IDMissing") +type ChangeSMTPConfig struct { + Details *domain.ObjectDetails + ResourceOwner string + ID string + + Description string + Host string + User string + Password string + Tls bool + From string + FromName string + ReplyToAddress string +} + +func (c *Commands) ChangeSMTPConfig(ctx context.Context, config *ChangeSMTPConfig) error { + if config.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-jwA8gxldy3", "Errors.ResourceOwnerMissing") + } + if config.ID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-2JPlSRzuHy", "Errors.IDMissing") } from := strings.TrimSpace(config.From) if from == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "INST-HSv2d", "Errors.Invalid.Argument") + return zerrors.ThrowInvalidArgument(nil, "COMMAND-gyPUXOTA4N", "Errors.Invalid.Argument") } fromSplitted := strings.Split(from, "@") senderDomain := fromSplitted[len(fromSplitted)-1] description := strings.TrimSpace(config.Description) replyTo := strings.TrimSpace(config.ReplyToAddress) - hostAndPort := strings.TrimSpace(config.SMTP.Host) + hostAndPort := strings.TrimSpace(config.Host) if _, _, err := net.SplitHostPort(hostAndPort); err != nil { - return nil, zerrors.ThrowInvalidArgument(nil, "INST-Kv875", "Errors.Invalid.Argument") + return zerrors.ThrowInvalidArgument(nil, "COMMAND-kZNVkuL32L", "Errors.Invalid.Argument") } var smtpPassword *crypto.CryptoValue var err error - if config.SMTP.Password != "" { - smtpPassword, err = crypto.Encrypt([]byte(config.SMTP.Password), c.smtpEncryption) + if config.Password != "" { + smtpPassword, err = crypto.Encrypt([]byte(config.Password), c.smtpEncryption) if err != nil { - return nil, err + return err } } - smtpConfigWriteModel, err := c.getSMTPConfig(ctx, instanceID, id, senderDomain) + smtpConfigWriteModel, err := c.getSMTPConfig(ctx, config.ResourceOwner, config.ID, senderDomain) if err != nil { - return nil, err + return err } if !smtpConfigWriteModel.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-7j8gv", "Errors.SMTPConfig.NotFound") + return zerrors.ThrowNotFound(nil, "COMMAND-j5IDFtt3T1", "Errors.SMTPConfig.NotFound") } err = checkSenderAddress(smtpConfigWriteModel) if err != nil { - return nil, err + return err } - iamAgg := InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel) - changedEvent, hasChanged, err := smtpConfigWriteModel.NewChangedEvent( ctx, - iamAgg, - id, + InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel), + config.ID, description, config.Tls, from, config.FromName, replyTo, hostAndPort, - config.SMTP.User, + config.User, smtpPassword, ) if err != nil { - return nil, err + return err } if !hasChanged { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-lh3op", "Errors.NoChangesFound") + config.Details = writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel) + return nil } - pushedEvents, err := c.eventstore.Push(ctx, changedEvent) + err = c.pushAppendAndReduce(ctx, smtpConfigWriteModel, changedEvent) if err != nil { - return nil, err + return err } - err = AppendAndReduce(smtpConfigWriteModel, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel), nil + config.Details = writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel) + return nil } -func (c *Commands) ChangeSMTPConfigPassword(ctx context.Context, instanceID, id string, password string) (*domain.ObjectDetails, error) { - instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) - smtpConfigWriteModel, err := c.getSMTPConfig(ctx, instanceID, id, "") +func (c *Commands) ChangeSMTPConfigPassword(ctx context.Context, resourceOwner, id string, password string) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-gHAyvUXCAF", "Errors.ResourceOwnerMissing") + } + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-BCkAf7LcJA", "Errors.IDMissing") + } + + smtpConfigWriteModel, err := c.getSMTPConfig(ctx, resourceOwner, id, "") if err != nil { return nil, err } if smtpConfigWriteModel.State != domain.SMTPConfigStateActive { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-3n9ls", "Errors.SMTPConfig.NotFound") + return nil, zerrors.ThrowNotFound(nil, "COMMAND-rDHzqjGuKQ", "Errors.SMTPConfig.NotFound") } var smtpPassword *crypto.CryptoValue @@ -170,68 +208,152 @@ func (c *Commands) ChangeSMTPConfigPassword(ctx context.Context, instanceID, id } } - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMTPConfigPasswordChangedEvent( - ctx, - &instanceAgg.Aggregate, - id, - smtpPassword)) + err = c.pushAppendAndReduce(ctx, + smtpConfigWriteModel, + instance.NewSMTPConfigPasswordChangedEvent( + ctx, + InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel), + id, + smtpPassword, + ), + ) if err != nil { return nil, err } - err = AppendAndReduce(smtpConfigWriteModel, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel), nil } -func (c *Commands) ActivateSMTPConfig(ctx context.Context, instanceID, id, activatedId string) (*domain.ObjectDetails, error) { - if id == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "SMTP-nm56k", "Errors.IDMissing") - } +type AddSMTPConfigHTTP struct { + Details *domain.ObjectDetails + ResourceOwner string + ID string - if len(activatedId) > 0 { - _, err := c.DeactivateSMTPConfig(ctx, instanceID, activatedId) + Description string + Endpoint string +} + +func (c *Commands) AddSMTPConfigHTTP(ctx context.Context, config *AddSMTPConfigHTTP) (err error) { + if config.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-FTNDXc8ACS", "Errors.ResourceOwnerMissing") + } + if config.ID == "" { + config.ID, err = c.idGenerator.Next() if err != nil { - return nil, err + return err } } - smtpConfigWriteModel, err := c.getSMTPConfig(ctx, instanceID, id, "") + smtpConfigWriteModel, err := c.getSMTPConfig(ctx, config.ResourceOwner, config.ID, "") + if err != nil { + return err + } + + err = c.pushAppendAndReduce(ctx, smtpConfigWriteModel, instance.NewSMTPConfigHTTPAddedEvent( + ctx, + InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel), + config.ID, + config.Description, + config.Endpoint, + )) + if err != nil { + return err + } + config.Details = writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel) + return nil +} + +type ChangeSMTPConfigHTTP struct { + Details *domain.ObjectDetails + ResourceOwner string + ID string + + Description string + Endpoint string +} + +func (c *Commands) ChangeSMTPConfigHTTP(ctx context.Context, config *ChangeSMTPConfigHTTP) (err error) { + if config.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-k7QCGOWyJA", "Errors.ResourceOwnerMissing") + } + if config.ID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-2MHkV8ObWo", "Errors.IDMissing") + } + + smtpConfigWriteModel, err := c.getSMTPConfig(ctx, config.ResourceOwner, config.ID, "") + if err != nil { + return err + } + + if !smtpConfigWriteModel.State.Exists() || smtpConfigWriteModel.HTTPConfig == nil { + return zerrors.ThrowNotFound(nil, "COMMAND-xIrdledqv4", "Errors.SMTPConfig.NotFound") + } + + changedEvent, hasChanged, err := smtpConfigWriteModel.NewHTTPChangedEvent( + ctx, + InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel), + config.ID, + config.Description, + config.Endpoint, + ) + if err != nil { + return err + } + if !hasChanged { + config.Details = writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel) + return nil + } + + err = c.pushAppendAndReduce(ctx, smtpConfigWriteModel, changedEvent) + if err != nil { + return err + } + config.Details = writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel) + return nil +} + +func (c *Commands) ActivateSMTPConfig(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-h5htMCebv3", "Errors.ResourceOwnerMissing") + } + if id == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-1hPl6oVMJa", "Errors.IDMissing") + } + + smtpConfigWriteModel, err := c.getSMTPConfig(ctx, resourceOwner, id, "") if err != nil { return nil, err } if !smtpConfigWriteModel.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-kg8yr", "Errors.SMTPConfig.NotFound") + return nil, zerrors.ThrowNotFound(nil, "COMMAND-E9K20hxOS9", "Errors.SMTPConfig.NotFound") } - if smtpConfigWriteModel.State == domain.SMTPConfigStateActive { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-ed3lr", "Errors.SMTPConfig.AlreadyActive") + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-vUHBSmBzaw", "Errors.SMTPConfig.AlreadyActive") } - iamAgg := InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMTPConfigActivatedEvent( - ctx, - iamAgg, - id)) - if err != nil { - return nil, err - } - err = AppendAndReduce(smtpConfigWriteModel, pushedEvents...) + err = c.pushAppendAndReduce(ctx, + smtpConfigWriteModel, + instance.NewSMTPConfigActivatedEvent( + ctx, + InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel), + id, + ), + ) if err != nil { return nil, err } return writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel), nil } -func (c *Commands) DeactivateSMTPConfig(ctx context.Context, instanceID, id string) (*domain.ObjectDetails, error) { +func (c *Commands) DeactivateSMTPConfig(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-pvNHou89Tw", "Errors.ResourceOwnerMissing") + } if id == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "SMTP-98ikl", "Errors.IDMissing") + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-jLTIMrtApO", "Errors.IDMissing") } - smtpConfigWriteModel, err := c.getSMTPConfig(ctx, instanceID, id, "") + smtpConfigWriteModel, err := c.getSMTPConfig(ctx, resourceOwner, id, "") if err != nil { return nil, err } @@ -239,46 +361,47 @@ func (c *Commands) DeactivateSMTPConfig(ctx context.Context, instanceID, id stri return nil, zerrors.ThrowNotFound(nil, "COMMAND-k39PJ", "Errors.SMTPConfig.NotFound") } if smtpConfigWriteModel.State == domain.SMTPConfigStateInactive { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-km8g3", "Errors.SMTPConfig.AlreadyDeactivated") + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-km8g3", "Errors.SMTPConfig.AlreadyDeactivated") } - iamAgg := InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMTPConfigDeactivatedEvent( - ctx, - iamAgg, - id)) - if err != nil { - return nil, err - } - err = AppendAndReduce(smtpConfigWriteModel, pushedEvents...) + err = c.pushAppendAndReduce(ctx, + smtpConfigWriteModel, + instance.NewSMTPConfigDeactivatedEvent( + ctx, + InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel), + id, + ), + ) if err != nil { return nil, err } return writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel), nil } -func (c *Commands) RemoveSMTPConfig(ctx context.Context, instanceID, id string) (*domain.ObjectDetails, error) { +func (c *Commands) RemoveSMTPConfig(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-t2WsPRgGaK", "Errors.ResourceOwnerMissing") + } if id == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "SMTP-7f5cv", "Errors.IDMissing") + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-0ZV5whuUfu", "Errors.IDMissing") } - smtpConfigWriteModel, err := c.getSMTPConfig(ctx, instanceID, id, "") + smtpConfigWriteModel, err := c.getSMTPConfig(ctx, resourceOwner, id, "") if err != nil { return nil, err } if !smtpConfigWriteModel.State.Exists() { - return nil, zerrors.ThrowNotFound(nil, "COMMAND-kg8rt", "Errors.SMTPConfig.NotFound") + return nil, zerrors.ThrowNotFound(nil, "COMMAND-09CXlTDL6w", "Errors.SMTPConfig.NotFound") } - iamAgg := InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewSMTPConfigRemovedEvent( - ctx, - iamAgg, - id)) - if err != nil { - return nil, err - } - err = AppendAndReduce(smtpConfigWriteModel, pushedEvents...) + err = c.pushAppendAndReduce(ctx, + smtpConfigWriteModel, + instance.NewSMTPConfigRemovedEvent( + ctx, + InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel), + id, + ), + ) if err != nil { return nil, err } @@ -303,11 +426,11 @@ func (c *Commands) TestSMTPConfig(ctx context.Context, instanceID, id, email str if err != nil { return err } - if !smtpConfigWriteModel.State.Exists() { + if !smtpConfigWriteModel.State.Exists() || smtpConfigWriteModel.SMTPConfig == nil { return zerrors.ThrowNotFound(nil, "SMTP-p9cc", "Errors.SMTPConfig.NotFound") } - password, err = crypto.DecryptString(smtpConfigWriteModel.Password, c.smtpEncryption) + password, err = crypto.DecryptString(smtpConfigWriteModel.SMTPConfig.Password, c.smtpEncryption) if err != nil { return err } @@ -338,23 +461,22 @@ func (c *Commands) TestSMTPConfigById(ctx context.Context, instanceID, id, email return err } - if !smtpConfigWriteModel.State.Exists() { + if !smtpConfigWriteModel.State.Exists() || smtpConfigWriteModel.SMTPConfig == nil { return zerrors.ThrowNotFound(nil, "SMTP-99klw", "Errors.SMTPConfig.NotFound") } - password, err := crypto.DecryptString(smtpConfigWriteModel.Password, c.smtpEncryption) + password, err := crypto.DecryptString(smtpConfigWriteModel.SMTPConfig.Password, c.smtpEncryption) if err != nil { return err } smtpConfig := &smtp.Config{ - Description: smtpConfigWriteModel.Description, - Tls: smtpConfigWriteModel.TLS, - From: smtpConfigWriteModel.SenderAddress, - FromName: smtpConfigWriteModel.SenderName, + Tls: smtpConfigWriteModel.SMTPConfig.TLS, + From: smtpConfigWriteModel.SMTPConfig.SenderAddress, + FromName: smtpConfigWriteModel.SMTPConfig.SenderName, SMTP: smtp.SMTP{ - Host: smtpConfigWriteModel.Host, - User: smtpConfigWriteModel.User, + Host: smtpConfigWriteModel.SMTPConfig.Host, + User: smtpConfigWriteModel.SMTPConfig.User, Password: password, }, } @@ -373,7 +495,7 @@ func checkSenderAddress(writeModel *IAMSMTPConfigWriteModel) error { return nil } if !writeModel.domainState.Exists() { - return zerrors.ThrowInvalidArgument(nil, "INST-83nl8", "Errors.SMTPConfig.SenderAdressNotCustomDomain") + return zerrors.ThrowInvalidArgument(nil, "INST-xtWIiR2ZbR", "Errors.SMTPConfig.SenderAdressNotCustomDomain") } return nil } diff --git a/internal/command/smtp_test.go b/internal/command/smtp_test.go index b6bb7d98a7..7aa224f251 100644 --- a/internal/command/smtp_test.go +++ b/internal/command/smtp_test.go @@ -2,6 +2,7 @@ package command import ( "context" + "errors" "testing" "github.com/stretchr/testify/assert" @@ -20,14 +21,13 @@ import ( func TestCommandSide_AddSMTPConfig(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator alg crypto.EncryptionAlgorithm } type args struct { - ctx context.Context instanceID string - smtp *smtp.Config + smtp *AddSMTPConfig } type res struct { want *domain.ObjectDetails @@ -39,11 +39,24 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { args args res res }{ + { + name: "resourceowner empty, invalid argument", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + smtp: &AddSMTPConfig{}, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-PQN0wsqSyi", "Errors.ResourceOwnerMissing")) + }, + }, + }, { name: "smtp config, custom domain not existing", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewDomainPolicyAddedEvent(context.Background(), @@ -59,25 +72,42 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - smtp: &smtp.Config{ - From: "from@domain.ch", - SMTP: smtp.SMTP{ - Host: "host:587", - User: "user", - Password: "password", - }, + smtp: &AddSMTPConfig{ + ResourceOwner: "INSTANCE", + From: "from@domain.ch", + Host: "host:587", + User: "user", + Password: "password", }, }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-xtWIiR2ZbR", "Errors.SMTPConfig.SenderAdressNotCustomDomain")) + }, + }, + }, + { + name: "add smtp config, from empty", + fields: fields{ + eventstore: expectEventstore(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"), + }, + args: args{ + smtp: &AddSMTPConfig{ + ResourceOwner: "INSTANCE", + From: " ", + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-SAAFpV8VKV", "Errors.Invalid.Argument")) + }, }, }, { name: "add smtp config, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewDomainAddedEvent(context.Background(), @@ -118,17 +148,15 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - smtp: &smtp.Config{ - Description: "test", - Tls: true, - From: "from@domain.ch", - FromName: "name", - SMTP: smtp.SMTP{ - Host: "host:587", - User: "user", - Password: "password", - }, + smtp: &AddSMTPConfig{ + ResourceOwner: "INSTANCE", + Description: "test", + Tls: true, + From: "from@domain.ch", + FromName: "name", + Host: "host:587", + User: "user", + Password: "password", }, }, res: res{ @@ -140,8 +168,7 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { { name: "add smtp config with reply to address, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewDomainAddedEvent(context.Background(), @@ -182,18 +209,16 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - smtp: &smtp.Config{ + smtp: &AddSMTPConfig{ + ResourceOwner: "INSTANCE", Description: "test", Tls: true, From: "from@domain.ch", FromName: "name", ReplyToAddress: "replyto@domain.ch", - SMTP: smtp.SMTP{ - Host: "host:587", - User: "user", - Password: "password", - }, + Host: "host:587", + User: "user", + Password: "password", }, }, res: res{ @@ -205,58 +230,57 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { { name: "smtp config, port is missing", fields: fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"), alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - smtp: &smtp.Config{ - Description: "test", - Tls: true, - From: "from@domain.ch", - FromName: "name", - SMTP: smtp.SMTP{ - Host: "host", - User: "user", - Password: "password", - }, + smtp: &AddSMTPConfig{ + ResourceOwner: "INSTANCE", + Description: "test", + Tls: true, + From: "from@domain.ch", + FromName: "name", + Host: "host", + User: "user", + Password: "password", }, }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-EvAtufIinh", "Errors.Invalid.Argument")) + }, }, }, { name: "smtp config, host is empty", fields: fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"), alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - smtp: &smtp.Config{ - Description: "test", - Tls: true, - From: "from@domain.ch", - FromName: "name", - SMTP: smtp.SMTP{ - Host: " ", - User: "user", - Password: "password", - }, + smtp: &AddSMTPConfig{ + ResourceOwner: "INSTANCE", + Description: "test", + Tls: true, + From: "from@domain.ch", + FromName: "name", + Host: " ", + User: "user", + Password: "password", }, }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-EvAtufIinh", "Errors.Invalid.Argument")) + }, }, }, { name: "add smtp config, ipv6 works", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewDomainAddedEvent(context.Background(), @@ -297,17 +321,15 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - smtp: &smtp.Config{ - Description: "test", - Tls: true, - From: "from@domain.ch", - FromName: "name", - SMTP: smtp.SMTP{ - Host: "[2001:db8::1]:2525", - User: "user", - Password: "password", - }, + smtp: &AddSMTPConfig{ + ResourceOwner: "INSTANCE", + Description: "test", + Tls: true, + From: "from@domain.ch", + FromName: "name", + Host: "[2001:db8::1]:2525", + User: "user", + Password: "password", }, }, res: res{ @@ -320,11 +342,11 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, smtpEncryption: tt.fields.alg, } - _, got, err := r.AddSMTPConfig(tt.args.ctx, tt.args.instanceID, tt.args.smtp) + err := r.AddSMTPConfig(context.Background(), tt.args.smtp) if tt.res.err == nil { assert.NoError(t, err) } @@ -332,7 +354,8 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assertObjectDetails(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, tt.args.smtp.Details) + assert.NotEmpty(t, tt.args.smtp.ID) } }) } @@ -340,13 +363,10 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { func TestCommandSide_ChangeSMTPConfig(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { - ctx context.Context - instanceID string - id string - smtp *smtp.Config + smtp *ChangeSMTPConfig } type res struct { want *domain.ObjectDetails @@ -359,68 +379,81 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { res res }{ { - name: "id empty, precondition error", + name: "resourceowner empty, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - smtp: &smtp.Config{}, + smtp: &ChangeSMTPConfig{}, }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-jwA8gxldy3", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "id empty, precondition error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + smtp: &ChangeSMTPConfig{ + ResourceOwner: "INSTANCE", + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-2JPlSRzuHy", "Errors.IDMissing")) + }, }, }, { name: "empty config, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - smtp: &smtp.Config{}, - id: "configID", + smtp: &ChangeSMTPConfig{ + ResourceOwner: "INSTANCE", + ID: "configID", + }, }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-gyPUXOTA4N", "Errors.Invalid.Argument")) + }, }, }, { name: "smtp not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - smtp: &smtp.Config{ - Description: "test", - Tls: true, - From: "from@domain.ch", - FromName: "name", - SMTP: smtp.SMTP{ - Host: "host:587", - User: "user", - }, + smtp: &ChangeSMTPConfig{ + ResourceOwner: "INSTANCE", + ID: "ID", + Description: "test", + Tls: true, + From: "from@domain.ch", + FromName: "name", + Host: "host:587", + User: "user", }, - instanceID: "INSTANCE", - id: "ID", }, res: res{ - err: zerrors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-j5IDFtt3T1", "Errors.SMTPConfig.NotFound")) + }, }, }, { name: "smtp domain not matched", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewDomainAddedEvent(context.Background(), @@ -454,18 +487,15 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { ), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - instanceID: "INSTANCE", - id: "ID", - smtp: &smtp.Config{ - Description: "test", - Tls: true, - From: "from@wrongdomain.ch", - FromName: "name", - SMTP: smtp.SMTP{ - Host: "host:587", - User: "user", - }, + smtp: &ChangeSMTPConfig{ + ResourceOwner: "INSTANCE", + ID: "ID", + Description: "test", + Tls: true, + From: "from@wrongdomain.ch", + FromName: "name", + Host: "host:587", + User: "user", }, }, res: res{ @@ -475,8 +505,7 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { { name: "no changes, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewDomainAddedEvent(context.Background(), @@ -510,29 +539,27 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { ), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - smtp: &smtp.Config{ - Description: "test", - Tls: true, - From: "from@domain.ch", - FromName: "name", - SMTP: smtp.SMTP{ - Host: "host:587", - User: "user", - }, + smtp: &ChangeSMTPConfig{ + ResourceOwner: "INSTANCE", + ID: "ID", + Description: "test", + Tls: true, + From: "from@domain.ch", + FromName: "name", + Host: "host:587", + User: "user", }, - instanceID: "INSTANCE", - id: "ID", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, }, }, { name: "smtp config change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewDomainAddedEvent(context.Background(), @@ -579,20 +606,17 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { ), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - smtp: &smtp.Config{ + smtp: &ChangeSMTPConfig{ + ResourceOwner: "INSTANCE", + ID: "ID", Description: "test", Tls: false, From: "from2@domain.ch", FromName: "name2", ReplyToAddress: "replyto@domain.ch", - SMTP: smtp.SMTP{ - Host: "host2:587", - User: "user2", - }, + Host: "host2:587", + User: "user2", }, - id: "ID", - instanceID: "INSTANCE", }, res: res{ want: &domain.ObjectDetails{ @@ -603,58 +627,55 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { { name: "smtp config, port is missing", fields: fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - smtp: &smtp.Config{ - Description: "test", - Tls: true, - From: "from@domain.ch", - FromName: "name", - SMTP: smtp.SMTP{ - Host: "host", - User: "user", - Password: "password", - }, + smtp: &ChangeSMTPConfig{ + ResourceOwner: "INSTANCE", + ID: "ID", + Description: "test", + Tls: true, + From: "from@domain.ch", + FromName: "name", + Host: "host", + User: "user", + Password: "password", }, - instanceID: "INSTANCE", - id: "ID", }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-kZNVkuL32L", "Errors.Invalid.Argument")) + }, }, }, { name: "smtp config, host is empty", fields: fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - smtp: &smtp.Config{ - Description: "test", - Tls: true, - From: "from@domain.ch", - FromName: "name", - SMTP: smtp.SMTP{ - Host: " ", - User: "user", - Password: "password", - }, + smtp: &ChangeSMTPConfig{ + ResourceOwner: "INSTANCE", + ID: "ID", + Description: "test", + Tls: true, + From: "from@domain.ch", + FromName: "name", + Host: " ", + User: "user", + Password: "password", }, - instanceID: "INSTANCE", - id: "ID", }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-kZNVkuL32L", "Errors.Invalid.Argument")) + }, }, }, { name: "smtp config change, ipv6 works", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewDomainAddedEvent(context.Background(), @@ -701,20 +722,17 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { ), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - smtp: &smtp.Config{ + smtp: &ChangeSMTPConfig{ + ResourceOwner: "INSTANCE", + ID: "ID", Description: "test", Tls: false, From: "from2@domain.ch", FromName: "name2", ReplyToAddress: "replyto@domain.ch", - SMTP: smtp.SMTP{ - Host: "[2001:db8::1]:2525", - User: "user2", - }, + Host: "[2001:db8::1]:2525", + User: "user2", }, - instanceID: "INSTANCE", - id: "ID", }, res: res{ want: &domain.ObjectDetails{ @@ -726,9 +744,9 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } - got, err := r.ChangeSMTPConfig(tt.args.ctx, tt.args.instanceID, tt.args.id, tt.args.smtp) + err := r.ChangeSMTPConfig(context.Background(), tt.args.smtp) if tt.res.err == nil { assert.NoError(t, err) } @@ -736,7 +754,7 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assertObjectDetails(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, tt.args.smtp.Details) } }) } @@ -744,11 +762,10 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { func TestCommandSide_ChangeSMTPConfigPassword(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore alg crypto.EncryptionAlgorithm } type args struct { - ctx context.Context instanceID string id string password string @@ -763,28 +780,54 @@ func TestCommandSide_ChangeSMTPConfigPassword(t *testing.T) { args args res res }{ + { + name: "smtp config, error resourceOwner empty", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{}, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-gHAyvUXCAF", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "smtp config, error id empty", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + instanceID: "INSTANCE", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-BCkAf7LcJA", "Errors.IDMissing")) + }, + }, + }, { name: "smtp config, error not found", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, args: args{ - ctx: context.Background(), - password: "", - id: "ID", + instanceID: "INSTANCE", + password: "", + id: "ID", }, res: res{ - err: zerrors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-rDHzqjGuKQ", "Errors.SMTPConfig.NotFound")) + }, }, }, { name: "change smtp config password, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMTPConfigAddedEvent( @@ -826,7 +869,6 @@ func TestCommandSide_ChangeSMTPConfigPassword(t *testing.T) { alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), password: "password", id: "ID", instanceID: "INSTANCE", @@ -841,10 +883,10 @@ func TestCommandSide_ChangeSMTPConfigPassword(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), smtpEncryption: tt.fields.alg, } - got, err := r.ChangeSMTPConfigPassword(tt.args.ctx, tt.args.instanceID, tt.args.id, tt.args.password) + got, err := r.ChangeSMTPConfigPassword(context.Background(), tt.args.instanceID, tt.args.id, tt.args.password) if tt.res.err == nil { assert.NoError(t, err) } @@ -858,16 +900,13 @@ func TestCommandSide_ChangeSMTPConfigPassword(t *testing.T) { } } -func TestCommandSide_ActivateSMTPConfig(t *testing.T) { +func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - alg crypto.EncryptionAlgorithm + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator } type args struct { - ctx context.Context - instanceID string - id string - activatedId string + http *AddSMTPConfigHTTP } type res struct { want *domain.ObjectDetails @@ -880,24 +919,279 @@ func TestCommandSide_ActivateSMTPConfig(t *testing.T) { res res }{ { - name: "id empty, precondition error", + name: "add smtp config, resourceowner empty", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + http: &AddSMTPConfigHTTP{}, }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-FTNDXc8ACS", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "add smtp config, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + instance.NewSMTPConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "configid", + "test", + "endpoint", + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"), + }, + args: args{ + http: &AddSMTPConfigHTTP{ + ResourceOwner: "INSTANCE", + Description: "test", + Endpoint: "endpoint", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + } + err := r.AddSMTPConfigHTTP(context.Background(), tt.args.http) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, tt.args.http.Details) + assert.NotEmpty(t, tt.args.http.ID) + } + }) + } +} + +func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + } + type args struct { + http *ChangeSMTPConfigHTTP + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{{ + name: "resourceowner empty, precondition error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + http: &ChangeSMTPConfigHTTP{}, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-k7QCGOWyJA", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "id empty, precondition error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + http: &ChangeSMTPConfigHTTP{ + ResourceOwner: "INSTANCE", + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-2MHkV8ObWo", "Errors.IDMissing")) + }, }, }, { name: "smtp not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{ + http: &ChangeSMTPConfigHTTP{ + ResourceOwner: "INSTANCE", + ID: "ID", + Description: "test", + Endpoint: "endpoint", + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-xIrdledqv4", "Errors.SMTPConfig.NotFound")) + }, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMTPConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + "test", + "endpoint", + ), + ), + ), + ), + }, + args: args{ + http: &ChangeSMTPConfigHTTP{ + ResourceOwner: "INSTANCE", + ID: "ID", + Description: "test", + Endpoint: "endpoint", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "smtp config change, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMTPConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + "", + "endpoint", + ), + ), + ), + expectPush( + newSMTPConfigHTTPChangedEvent( + context.Background(), + "ID", + "test", + "endpoint2", + ), + ), + ), + }, + args: args{ + http: &ChangeSMTPConfigHTTP{ + ResourceOwner: "INSTANCE", + ID: "ID", + Description: "test", + Endpoint: "endpoint2", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + } + err := r.ChangeSMTPConfigHTTP(context.Background(), tt.args.http) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, tt.args.http.Details) + } + }) + } +} + +func TestCommandSide_ActivateSMTPConfig(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + alg crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + instanceID string + id string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{{ + name: "resourceowner empty, precondition error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + instanceID: "", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-h5htMCebv3", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "id empty, precondition error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + instanceID: "INSTANCE", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-1hPl6oVMJa", "Errors.IDMissing")) + }, + }, + }, + { + name: "smtp not existing, not found error", + fields: fields{ + eventstore: expectEventstore( expectFilter(), ), }, @@ -907,14 +1201,15 @@ func TestCommandSide_ActivateSMTPConfig(t *testing.T) { id: "id", }, res: res{ - err: zerrors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-E9K20hxOS9", "Errors.SMTPConfig.NotFound")) + }, }, }, { name: "activate smtp config, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMTPConfigAddedEvent( @@ -942,10 +1237,85 @@ func TestCommandSide_ActivateSMTPConfig(t *testing.T) { ), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - id: "ID", - instanceID: "INSTANCE", - activatedId: "", + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + id: "ID", + instanceID: "INSTANCE", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "activate smtp config, already active", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMTPConfigAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + "test", + true, + "from", + "name", + "", + "host:587", + "user", + &crypto.CryptoValue{}, + ), + ), + eventFromEventPusher( + instance.NewSMTPConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + ), + ), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + id: "ID", + instanceID: "INSTANCE", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-vUHBSmBzaw", "Errors.SMTPConfig.AlreadyActive")) + }, + }, + }, + { + name: "activate smtp config http, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMTPConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + "test", + "endpoint", + ), + ), + ), + expectPush( + instance.NewSMTPConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + ), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + id: "ID", + instanceID: "INSTANCE", }, res: res{ want: &domain.ObjectDetails{ @@ -957,10 +1327,10 @@ func TestCommandSide_ActivateSMTPConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), smtpEncryption: tt.fields.alg, } - got, err := r.ActivateSMTPConfig(tt.args.ctx, tt.args.instanceID, tt.args.id, tt.args.activatedId) + got, err := r.ActivateSMTPConfig(tt.args.ctx, tt.args.instanceID, tt.args.id) if tt.res.err == nil { assert.NoError(t, err) } @@ -976,14 +1346,12 @@ func TestCommandSide_ActivateSMTPConfig(t *testing.T) { func TestCommandSide_DeactivateSMTPConfig(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore alg crypto.EncryptionAlgorithm } type args struct { - ctx context.Context - instanceID string - id string - activatedId string + instanceID string + id string } type res struct { want *domain.ObjectDetails @@ -994,43 +1362,53 @@ func TestCommandSide_DeactivateSMTPConfig(t *testing.T) { fields fields args args res res - }{ + }{{ + name: "resourceOwner empty, precondition error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{}, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-pvNHou89Tw", "Errors.ResourceOwnerMissing")) + }, + }, + }, { name: "id empty, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + instanceID: "INSTANCE", }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-jLTIMrtApO", "Errors.IDMissing")) + }, }, }, { name: "smtp not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, args: args{ - ctx: context.Background(), instanceID: "INSTANCE", id: "id", }, res: res{ - err: zerrors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-k39PJ", "Errors.SMTPConfig.NotFound")) + }, }, }, { name: "deactivate smtp config, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMTPConfigAddedEvent( @@ -1065,10 +1443,96 @@ func TestCommandSide_DeactivateSMTPConfig(t *testing.T) { ), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - id: "ID", - instanceID: "INSTANCE", - activatedId: "", + id: "ID", + instanceID: "INSTANCE", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "deactivate smtp config, already deactivated", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMTPConfigAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + "test", + true, + "from", + "name", + "", + "host:587", + "user", + &crypto.CryptoValue{}, + ), + ), + eventFromEventPusher( + instance.NewSMTPConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + ), + ), + eventFromEventPusher( + instance.NewSMTPConfigDeactivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + ), + ), + ), + ), + }, + args: args{ + id: "ID", + instanceID: "INSTANCE", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "COMMAND-km8g3", "Errors.SMTPConfig.AlreadyDeactivated")) + }, + }, + }, + { + name: "deactivate smtp config http, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMTPConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + "test", + "endpoint", + ), + ), + eventFromEventPusher( + instance.NewSMTPConfigActivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + ), + ), + ), + expectPush( + instance.NewSMTPConfigDeactivatedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + ), + ), + ), + }, + args: args{ + id: "ID", + instanceID: "INSTANCE", }, res: res{ want: &domain.ObjectDetails{ @@ -1080,10 +1544,10 @@ func TestCommandSide_DeactivateSMTPConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), smtpEncryption: tt.fields.alg, } - got, err := r.DeactivateSMTPConfig(tt.args.ctx, tt.args.instanceID, tt.args.id) + got, err := r.DeactivateSMTPConfig(context.Background(), tt.args.instanceID, tt.args.id) if tt.res.err == nil { assert.NoError(t, err) } @@ -1099,11 +1563,10 @@ func TestCommandSide_DeactivateSMTPConfig(t *testing.T) { func TestCommandSide_RemoveSMTPConfig(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore alg crypto.EncryptionAlgorithm } type args struct { - ctx context.Context instanceID string id string } @@ -1117,27 +1580,55 @@ func TestCommandSide_RemoveSMTPConfig(t *testing.T) { args args res res }{ + { + name: "resourceowner empty, invalid argument error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + id: "ID", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-t2WsPRgGaK", "Errors.ResourceOwnerMissing")) + }, + }, + }, + { + name: "id empty, invalid argument error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + instanceID: "INSTANCE", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-0ZV5whuUfu", "Errors.IDMissing")) + }, + }, + }, { name: "smtp config, error not found", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, args: args{ - ctx: context.Background(), - id: "ID", + instanceID: "INSTANCE", + id: "ID", }, res: res{ - err: zerrors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-09CXlTDL6w", "Errors.SMTPConfig.NotFound")) + }, }, }, { name: "remove smtp config, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMTPConfigAddedEvent( @@ -1165,7 +1656,40 @@ func TestCommandSide_RemoveSMTPConfig(t *testing.T) { ), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + id: "ID", + instanceID: "INSTANCE", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "remove smtp config http, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMTPConfigHTTPAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + "test", + "endpoint", + ), + ), + ), + expectPush( + instance.NewSMTPConfigRemovedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + ), + ), + ), + }, + args: args{ id: "ID", instanceID: "INSTANCE", }, @@ -1179,10 +1703,10 @@ func TestCommandSide_RemoveSMTPConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), smtpEncryption: tt.fields.alg, } - got, err := r.RemoveSMTPConfig(tt.args.ctx, tt.args.instanceID, tt.args.id) + got, err := r.RemoveSMTPConfig(context.Background(), tt.args.instanceID, tt.args.id) if tt.res.err == nil { assert.NoError(t, err) } @@ -1198,7 +1722,7 @@ func TestCommandSide_RemoveSMTPConfig(t *testing.T) { func TestCommandSide_TestSMTPConfig(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore alg crypto.EncryptionAlgorithm } type args struct { @@ -1220,9 +1744,7 @@ func TestCommandSide_TestSMTPConfig(t *testing.T) { { name: "id empty, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), @@ -1234,9 +1756,7 @@ func TestCommandSide_TestSMTPConfig(t *testing.T) { { name: "email empty, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), @@ -1250,9 +1770,7 @@ func TestCommandSide_TestSMTPConfig(t *testing.T) { { name: "if password is empty, smtp id must not", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -1276,8 +1794,7 @@ func TestCommandSide_TestSMTPConfig(t *testing.T) { { name: "password empty and smtp config not found, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -1303,10 +1820,8 @@ func TestCommandSide_TestSMTPConfig(t *testing.T) { { name: "valid new smtp config, wrong auth, ok", fields: fields{ - eventstore: eventstoreExpect( - t, - ), - alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + eventstore: expectEventstore(), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ ctx: context.Background(), @@ -1329,8 +1844,7 @@ func TestCommandSide_TestSMTPConfig(t *testing.T) { { name: "valid smtp config using stored password, wrong auth, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewSMTPConfigAddedEvent( @@ -1379,7 +1893,7 @@ func TestCommandSide_TestSMTPConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), smtpEncryption: tt.fields.alg, } err := r.TestSMTPConfig(tt.args.ctx, tt.args.instanceID, tt.args.id, tt.args.email, &tt.args.config) @@ -1536,3 +2050,16 @@ func newSMTPConfigChangedEvent(ctx context.Context, id, description string, tls ) return event } + +func newSMTPConfigHTTPChangedEvent(ctx context.Context, id, description, endpoint string) *instance.SMTPConfigHTTPChangedEvent { + changes := []instance.SMTPConfigHTTPChanges{ + instance.ChangeSMTPConfigHTTPDescription(description), + instance.ChangeSMTPConfigHTTPEndpoint(endpoint), + } + event, _ := instance.NewSMTPConfigHTTPChangeEvent(ctx, + &instance.NewAggregate("INSTANCE").Aggregate, + id, + changes, + ) + return event +} diff --git a/internal/command/user.go b/internal/command/user.go index bb3c75775a..6b65aa83ec 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -388,6 +388,10 @@ func (c *Commands) newUserInitCode(ctx context.Context, filter preparation.Filte return c.newEncryptedCode(ctx, filter, domain.SecretGeneratorTypeInitCode, alg) } +func (c *Commands) newUserInviteCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*EncryptedCode, error) { + return c.newEncryptedCodeWithDefault(ctx, filter, domain.SecretGeneratorTypeInviteCode, alg, c.defaultSecretGenerators.InviteCode) +} + func userWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, resourceOwner string) (*UserWriteModel, error) { user := NewUserWriteModel(userID, resourceOwner) events, err := filter(ctx, user.Query()) diff --git a/internal/command/user_human.go b/internal/command/user_human.go index b1abf50b33..1208296159 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -284,6 +284,9 @@ func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation. } return append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, initCode.Crypted, initCode.Expiry, human.AuthRequestID)), nil } + if human.Email.NoEmailVerification { + return cmds, nil + } if !human.Email.Verified { emailCode, err := c.newEmailCode(ctx, filter, codeAlg) if err != nil { diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index ffde2480c1..468a27e8b8 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -626,6 +626,67 @@ func TestCommandSide_AddHuman(t *testing.T) { wantEmailCode: "emailCode", }, }, + { + name: "add human (with password and unverified email), ok (no email code)", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + newAddHumanEvent("$plain$x$password", false, true, "", AllowedLanguage), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + newCode: mockEncryptedCode("emailCode", time.Hour), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &AddHuman{ + Username: "username", + Password: "password", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", + Verified: false, + NoEmailVerification: true, + }, + PreferredLanguage: AllowedLanguage, + }, + secretGenerator: GetMockSecretGenerator(t), + allowInitMail: false, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + wantID: "user1", + }, + }, { name: "add human email verified, ok", fields: fields{ diff --git a/internal/command/user_model.go b/internal/command/user_model.go index 10c6dd2cf8..ad625756f6 100644 --- a/internal/command/user_model.go +++ b/internal/command/user_model.go @@ -1,6 +1,8 @@ package command import ( + "context" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/user" @@ -122,6 +124,10 @@ func UserAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregat return eventstore.AggregateFromWriteModel(wm, user.AggregateType, user.AggregateVersion) } +func UserAggregateFromWriteModelCtx(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate { + return eventstore.AggregateFromWriteModelCtx(ctx, wm, user.AggregateType, user.AggregateVersion) +} + func isUserStateExists(state domain.UserState) bool { return !hasUserState(state, domain.UserStateDeleted, domain.UserStateUnspecified) } diff --git a/internal/command/user_v2_invite.go b/internal/command/user_v2_invite.go new file mode 100644 index 0000000000..78b46a530e --- /dev/null +++ b/internal/command/user_v2_invite.go @@ -0,0 +1,193 @@ +package command + +import ( + "context" + "strings" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type CreateUserInvite struct { + UserID string + URLTemplate string + ReturnCode bool + ApplicationName string +} + +func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvite) (details *domain.ObjectDetails, returnCode *string, err error) { + invite.UserID = strings.TrimSpace(invite.UserID) + if invite.UserID == "" { + return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4jio3", "Errors.User.UserIDMissing") + } + wm, err := c.userInviteCodeWriteModel(ctx, invite.UserID, "") + if err != nil { + return nil, nil, err + } + if err := c.checkPermission(ctx, domain.PermissionUserWrite, wm.ResourceOwner, wm.AggregateID); err != nil { + return nil, nil, err + } + if !wm.UserState.Exists() { + return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wgvn4", "Errors.User.NotFound") + } + if !wm.CreationAllowed() { + return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-EF34g", "Errors.User.AlreadyInitialised") + } + code, err := c.newUserInviteCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint + if err != nil { + return nil, nil, err + } + err = c.pushAppendAndReduce(ctx, wm, user.NewHumanInviteCodeAddedEvent( + ctx, + UserAggregateFromWriteModelCtx(ctx, &wm.WriteModel), + code.Crypted, + code.Expiry, + invite.URLTemplate, + invite.ReturnCode, + invite.ApplicationName, + "", + )) + if err != nil { + return nil, nil, err + } + if invite.ReturnCode { + returnCode = &code.Plain + } + return writeModelToObjectDetails(&wm.WriteModel), returnCode, nil +} + +// ResendInviteCode resends the invite mail with a new code and an optional authRequestID. +// It will reuse the applicationName from the previous code. +func (c *Commands) ResendInviteCode(ctx context.Context, userID, resourceOwner, authRequestID string) (objectDetails *domain.ObjectDetails, err error) { + if userID == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing") + } + + existingCode, err := c.userInviteCodeWriteModel(ctx, userID, resourceOwner) + if err != nil { + return nil, err + } + if authz.GetCtxData(ctx).UserID != userID { + if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingCode.ResourceOwner, userID); err != nil { + return nil, err + } + } + if !existingCode.UserState.Exists() { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-H3b2a", "Errors.User.NotFound") + } + if !existingCode.CreationAllowed() { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Gg42s", "Errors.User.AlreadyInitialised") + } + if existingCode.InviteCode == nil || existingCode.CodeReturned { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound") + } + code, err := c.newUserInviteCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint + if err != nil { + return nil, err + } + if authRequestID == "" { + authRequestID = existingCode.AuthRequestID + } + err = c.pushAppendAndReduce(ctx, existingCode, + user.NewHumanInviteCodeAddedEvent( + ctx, + UserAggregateFromWriteModelCtx(ctx, &existingCode.WriteModel), + code.Crypted, + code.Expiry, + existingCode.URLTemplate, + false, + existingCode.ApplicationName, + authRequestID, + )) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&existingCode.WriteModel), nil +} + +func (c *Commands) InviteCodeSent(ctx context.Context, userID, orgID string) (err error) { + if userID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-Sgf31", "Errors.User.UserIDMissing") + } + existingCode, err := c.userInviteCodeWriteModel(ctx, userID, orgID) + if err != nil { + return err + } + if !existingCode.UserState.Exists() { + return zerrors.ThrowPreconditionFailed(nil, "COMMAND-HN34a", "Errors.User.NotFound") + } + if existingCode.InviteCode == nil || existingCode.CodeReturned { + return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound") + } + userAgg := UserAggregateFromWriteModelCtx(ctx, &existingCode.WriteModel) + _, err = c.eventstore.Push(ctx, user.NewHumanInviteCodeSentEvent(ctx, userAgg)) + return err +} + +func (c *Commands) VerifyInviteCode(ctx context.Context, userID, code string) (details *domain.ObjectDetails, err error) { + return c.VerifyInviteCodeSetPassword(ctx, userID, code, "", "") +} + +func (c *Commands) VerifyInviteCodeSetPassword(ctx context.Context, userID, code, password, userAgentID string) (details *domain.ObjectDetails, err error) { + if userID == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Gk3f2", "Errors.User.UserIDMissing") + } + wm, err := c.userInviteCodeWriteModel(ctx, userID, "") + if err != nil { + return nil, err + } + if !wm.UserState.Exists() { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-F5g2h", "Errors.User.NotFound") + } + userAgg := UserAggregateFromWriteModelCtx(ctx, &wm.WriteModel) + err = crypto.VerifyCode(wm.InviteCodeCreationDate, wm.InviteCodeExpiry, wm.InviteCode, code, c.userEncryption) + if err != nil { + _, err = c.eventstore.Push(ctx, user.NewHumanInviteCheckFailedEvent(ctx, userAgg)) + logging.WithFields("userID", userAgg.ID).OnError(err).Error("NewHumanInviteCheckFailedEvent push failed") + return nil, zerrors.ThrowInvalidArgument(err, "COMMAND-Wgn4q", "Errors.User.Code.Invalid") + } + commands := []eventstore.Command{ + user.NewHumanInviteCheckSucceededEvent(ctx, userAgg), + user.NewHumanEmailVerifiedEvent(ctx, userAgg), + } + if password != "" { + passwordCommand, err := c.setPasswordCommand( + ctx, + userAgg, + wm.UserState, + password, + "", + userAgentID, + false, + nil, + ) + if err != nil { + return nil, err + } + commands = append(commands, passwordCommand) + } + err = c.pushAppendAndReduce(ctx, wm, commands...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&wm.WriteModel), nil +} + +func (c *Commands) userInviteCodeWriteModel(ctx context.Context, userID, orgID string) (writeModel *UserV2InviteWriteModel, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + writeModel = newUserV2InviteWriteModel(userID, orgID) + err = c.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + return writeModel, nil +} diff --git a/internal/command/user_v2_invite_model.go b/internal/command/user_v2_invite_model.go new file mode 100644 index 0000000000..23f6322a19 --- /dev/null +++ b/internal/command/user_v2_invite_model.go @@ -0,0 +1,141 @@ +package command + +import ( + "time" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user" +) + +type UserV2InviteWriteModel struct { + eventstore.WriteModel + + InviteCode *crypto.CryptoValue + InviteCodeCreationDate time.Time + InviteCodeExpiry time.Duration + InviteCheckFailureCount uint8 + + ApplicationName string + AuthRequestID string + URLTemplate string + CodeReturned bool + EmailVerified bool + AuthMethodSet bool + + UserState domain.UserState +} + +func (wm *UserV2InviteWriteModel) CreationAllowed() bool { + return !wm.EmailVerified && !wm.AuthMethodSet +} + +func newUserV2InviteWriteModel(userID, orgID string) *UserV2InviteWriteModel { + return &UserV2InviteWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: userID, + ResourceOwner: orgID, + }, + } +} + +func (wm *UserV2InviteWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *user.HumanAddedEvent: + wm.UserState = domain.UserStateActive + wm.AuthMethodSet = crypto.SecretOrEncodedHash(e.Secret, e.EncodedHash) != "" + wm.EmptyInviteCode() + wm.ApplicationName = "" + wm.AuthRequestID = "" + case *user.HumanRegisteredEvent: + wm.UserState = domain.UserStateActive + wm.AuthMethodSet = crypto.SecretOrEncodedHash(e.Secret, e.EncodedHash) != "" + wm.EmptyInviteCode() + wm.ApplicationName = "" + wm.AuthRequestID = "" + case *user.HumanInviteCodeAddedEvent: + wm.SetInviteCode(e.Code, e.Expiry, e.CreationDate()) + wm.URLTemplate = e.URLTemplate + wm.CodeReturned = e.CodeReturned + wm.ApplicationName = e.ApplicationName + wm.AuthRequestID = e.AuthRequestID + case *user.HumanInviteCheckSucceededEvent: + wm.EmptyInviteCode() + case *user.HumanInviteCheckFailedEvent: + wm.InviteCheckFailureCount++ + if wm.InviteCheckFailureCount >= 3 { //TODO: config? + wm.UserState = domain.UserStateDeleted + } + case *user.HumanEmailVerifiedEvent: + wm.EmailVerified = true + wm.EmptyInviteCode() + case *user.UserLockedEvent: + wm.UserState = domain.UserStateLocked + case *user.UserUnlockedEvent: + wm.UserState = domain.UserStateActive + case *user.UserDeactivatedEvent: + wm.UserState = domain.UserStateInactive + case *user.UserReactivatedEvent: + wm.UserState = domain.UserStateActive + case *user.UserRemovedEvent: + wm.UserState = domain.UserStateDeleted + case *user.HumanPasswordChangedEvent: + wm.AuthMethodSet = true + case *user.UserIDPLinkAddedEvent: + wm.AuthMethodSet = true + case *user.HumanPasswordlessVerifiedEvent: + wm.AuthMethodSet = true + } + } + return wm.WriteModel.Reduce() +} + +func (wm *UserV2InviteWriteModel) SetInviteCode(code *crypto.CryptoValue, expiry time.Duration, creationDate time.Time) { + wm.InviteCode = code + wm.InviteCodeExpiry = expiry + wm.InviteCodeCreationDate = creationDate + wm.InviteCheckFailureCount = 0 +} + +func (wm *UserV2InviteWriteModel) EmptyInviteCode() { + wm.InviteCode = nil + wm.InviteCodeExpiry = 0 + wm.InviteCodeCreationDate = time.Time{} + wm.InviteCheckFailureCount = 0 +} +func (wm *UserV2InviteWriteModel) Query() *eventstore.SearchQueryBuilder { + query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + AddQuery(). + AggregateTypes(user.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + user.UserV1AddedType, + user.HumanAddedType, + user.UserV1RegisteredType, + user.HumanRegisteredType, + user.HumanInviteCodeAddedType, + user.HumanInviteCheckSucceededType, + user.HumanInviteCheckFailedType, + user.UserV1EmailVerifiedType, + user.HumanEmailVerifiedType, + user.UserLockedType, + user.UserUnlockedType, + user.UserDeactivatedType, + user.UserReactivatedType, + user.UserRemovedType, + user.HumanPasswordChangedType, + user.UserV1PasswordChangedType, + user.UserIDPLinkAddedType, + user.HumanPasswordlessTokenVerifiedType, + ).Builder() + if wm.ResourceOwner != "" { + query.ResourceOwner(wm.ResourceOwner) + } + return query +} + +func (wm *UserV2InviteWriteModel) Aggregate() *user.Aggregate { + return user.NewAggregate(wm.AggregateID, wm.ResourceOwner) +} diff --git a/internal/command/user_v2_invite_test.go b/internal/command/user_v2_invite_test.go new file mode 100644 index 0000000000..efb57d86ad --- /dev/null +++ b/internal/command/user_v2_invite_test.go @@ -0,0 +1,1207 @@ +package command + +import ( + "context" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestCommands_CreateInviteCode(t *testing.T) { + type fields struct { + checkPermission domain.PermissionCheck + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + eventstore func(*testing.T) *eventstore.Eventstore + defaultSecretGenerators *SecretGenerators + } + type args struct { + ctx context.Context + invite *CreateUserInvite + } + type want struct { + details *domain.ObjectDetails + returnCode *string + err error + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + "user id missing", + fields{ + eventstore: expectEventstore(), + }, + args{ + ctx: context.Background(), + invite: &CreateUserInvite{ + UserID: "", + }, + }, + want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMAND-4jio3", "Errors.User.UserIDMissing"), + }, + }, + { + "missing permission", + fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args{ + ctx: context.Background(), + invite: &CreateUserInvite{ + UserID: "userID", + }, + }, + want{ + err: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + }, + { + "user does not exist", + fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: context.Background(), + invite: &CreateUserInvite{ + UserID: "unknown", + }, + }, + want{ + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wgvn4", "Errors.User.NotFound"), + }, + }, + { + "create ok", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + ctx: context.Background(), + invite: &CreateUserInvite{ + UserID: "userID", + }, + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + returnCode: nil, + }, + }, + { + "return ok", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + true, + "", + "", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + ctx: context.Background(), + invite: &CreateUserInvite{ + UserID: "userID", + ReturnCode: true, + }, + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + returnCode: gu.Ptr("code"), + }, + }, + { + "with template and application name ok", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "https://example.com/invite?userID={{.UserID}}", + false, + "applicationName", + "", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + ctx: context.Background(), + invite: &CreateUserInvite{ + UserID: "userID", + URLTemplate: "https://example.com/invite?userID={{.UserID}}", + ReturnCode: false, + ApplicationName: "applicationName", + }, + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + returnCode: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + checkPermission: tt.fields.checkPermission, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + eventstore: tt.fields.eventstore(t), + defaultSecretGenerators: tt.fields.defaultSecretGenerators, + } + gotDetails, gotReturnCode, err := c.CreateInviteCode(tt.args.ctx, tt.args.invite) + + require.ErrorIs(t, err, tt.want.err) + assert.Equal(t, tt.want.details, gotDetails) + assert.Equal(t, tt.want.returnCode, gotReturnCode) + }) + } +} + +func TestCommands_ResendInviteCode(t *testing.T) { + type fields struct { + checkPermission domain.PermissionCheck + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + eventstore func(*testing.T) *eventstore.Eventstore + defaultSecretGenerators *SecretGenerators + } + type args struct { + ctx context.Context + userID string + orgID string + authRequestID string + } + type want struct { + details *domain.ObjectDetails + err error + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + "missing user id", + fields{ + eventstore: expectEventstore(), + }, + args{ + ctx: context.Background(), + userID: "", + }, + want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing"), + }, + }, + { + "missing permission", + fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args{ + ctx: context.Background(), + userID: "userID", + }, + want{ + err: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + }, + { + "user does not exist", + fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: context.Background(), + userID: "unknown", + }, + want{ + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-H3b2a", "Errors.User.NotFound"), + }, + }, + { + "no previous code", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: context.Background(), + userID: "userID", + }, + want{ + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound"), + }, + }, + { + "previous code returned", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + true, + "", + "", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args{ + ctx: context.Background(), + userID: "userID", + }, + want{ + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound"), + }, + }, + { + "resend ok", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID", + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + ctx: context.Background(), + userID: "userID", + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + }, + }, + { + "resend with new auth requestID ok", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID", + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID2", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + ctx: context.Background(), + userID: "userID", + authRequestID: "authRequestID2", + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + }, + }, + { + "resend with own user ok", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID", + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(authz.NewMockContext("instanceID", "org1", "userID"), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID2", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), // user does not have permission, is allowed in the own context + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + ctx: authz.NewMockContext("instanceID", "org1", "userID"), + userID: "userID", + authRequestID: "authRequestID2", + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + checkPermission: tt.fields.checkPermission, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + eventstore: tt.fields.eventstore(t), + defaultSecretGenerators: tt.fields.defaultSecretGenerators, + } + details, err := c.ResendInviteCode(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.authRequestID) + assert.ErrorIs(t, err, tt.want.err) + assert.Equal(t, tt.want.details, details) + }) + } +} + +func TestCommands_InviteCodeSent(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + } + type args struct { + ctx context.Context + userID string + orgID string + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + "missing user id", + fields{ + eventstore: expectEventstore(), + }, + args{ + ctx: context.Background(), + userID: "", + }, + zerrors.ThrowInvalidArgument(nil, "COMMAND-Sgf31", "Errors.User.UserIDMissing"), + }, + { + "user does not exist", + fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args{ + ctx: context.Background(), + userID: "unknown", + }, + zerrors.ThrowPreconditionFailed(nil, "COMMAND-HN34a", "Errors.User.NotFound"), + }, + { + "code does not exist", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + userID: "unknown", + }, + zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound"), + }, + { + "sent ok", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID", + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeSentEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + userID: "userID", + }, + nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + } + err := c.InviteCodeSent(tt.args.ctx, tt.args.userID, tt.args.orgID) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestCommands_VerifyInviteCode(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + userEncryption crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + userID string + code string + } + type want struct { + details *domain.ObjectDetails + err error + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + "code ok", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID", + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCheckSucceededEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + ), + ), + userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args{ + ctx: context.Background(), + userID: "userID", + code: "code", + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + }, + }, + // all other cases are tested in TestCommands_VerifyInviteCodeSetPassword + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + userEncryption: tt.fields.userEncryption, + } + gotDetails, err := c.VerifyInviteCode(tt.args.ctx, tt.args.userID, tt.args.code) + assert.ErrorIs(t, err, tt.want.err) + assert.Equal(t, tt.want.details, gotDetails) + }) + } +} + +func TestCommands_VerifyInviteCodeSetPassword(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + userEncryption crypto.EncryptionAlgorithm + userPasswordHasher *crypto.Hasher + } + type args struct { + ctx context.Context + userID string + code string + password string + userAgentID string + } + type want struct { + details *domain.ObjectDetails + err error + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + "missing user id", + fields{ + eventstore: expectEventstore(), + }, + args{ + ctx: context.Background(), + userID: "", + }, + want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMAND-Gk3f2", "Errors.User.UserIDMissing"), + }, + }, + { + "user does not exist", + fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args{ + ctx: context.Background(), + userID: "unknown", + }, + want{ + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-F5g2h", "Errors.User.NotFound"), + }, + }, + { + "invalid code", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID", + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCheckFailedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + ), + ), + userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args{ + ctx: context.Background(), + userID: "userID", + code: "invalid", + }, + want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMAND-Wgn4q", "Errors.User.Code.Invalid"), + }, + }, + { + "code ok", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID", + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCheckSucceededEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + ), + ), + userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args{ + ctx: context.Background(), + userID: "userID", + code: "code", + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + }, + }, + { + "code ok, with password and user agent", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID", + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + 6, + true, + true, + true, + true, + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCheckSucceededEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "$plain$x$Password1!", + false, + "userAgentID", + ), + ), + ), + ), + userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + userID: "userID", + code: "code", + password: "Password1!", + userAgentID: "userAgentID", + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + }, + }, + { + "code ok, with non compliant password", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID", + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + 6, + true, + true, + true, + true, + ), + ), + ), + ), + userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + userID: "userID", + code: "code", + password: "pw", + userAgentID: "userAgentID", + }, + want{ + err: zerrors.ThrowInvalidArgument(nil, "DOMAIN-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + userEncryption: tt.fields.userEncryption, + userPasswordHasher: tt.fields.userPasswordHasher, + } + gotDetails, err := c.VerifyInviteCodeSetPassword(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.password, tt.args.userAgentID) + assert.ErrorIs(t, err, tt.want.err) + assert.Equal(t, tt.want.details, gotDetails) + }) + } +} diff --git a/internal/command/user_v2_passkey.go b/internal/command/user_v2_passkey.go index 4d42d105dd..897a1ab41d 100644 --- a/internal/command/user_v2_passkey.go +++ b/internal/command/user_v2_passkey.go @@ -48,7 +48,7 @@ func (c *Commands) verifyUserPasskeyCode(ctx context.Context, userID, resourceOw if err != nil { return nil, err } - err = verifyEncryptedCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg, wm.ChangeDate, wm.Expiration, wm.CryptoCode, code) //nolint:staticcheck + err = crypto.VerifyCode(wm.ChangeDate, wm.Expiration, wm.CryptoCode, code, alg) if err != nil || wm.State != domain.PasswordlessInitCodeStateActive { c.verifyUserPasskeyCodeFailed(ctx, wm) return nil, zerrors.ThrowInvalidArgument(err, "COMMAND-Eeb2a", "Errors.User.Code.Invalid") diff --git a/internal/command/user_v2_passkey_test.go b/internal/command/user_v2_passkey_test.go index aa2ded6d7a..a6ba470d2b 100644 --- a/internal/command/user_v2_passkey_test.go +++ b/internal/command/user_v2_passkey_test.go @@ -143,7 +143,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) { require.NoError(t, err) userAgg := &user.NewAggregate("user1", "org1").Aggregate type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore idGenerator id.Generator } type args struct { @@ -163,7 +163,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) { { name: "code verification error", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusherWithCreationDateNow( user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(), @@ -174,7 +174,6 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) { user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"), ), ), - expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))), expectPush( user.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, "123"), ), @@ -192,7 +191,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) { { name: "code verification ok, get human passwordless error", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusherWithCreationDateNow( user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(), @@ -203,7 +202,6 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) { user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"), ), ), - expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))), expectFilterError(io.ErrClosedPipe), ), }, @@ -220,7 +218,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, webauthnConfig: webauthnConfig, } @@ -242,7 +240,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) { userAgg := &user.NewAggregate("user1", "org1").Aggregate type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { userID string @@ -260,7 +258,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) { { name: "filter error", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilterError(io.ErrClosedPipe), ), }, @@ -274,7 +272,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) { { name: "code verification error", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusherWithCreationDateNow( user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(), @@ -285,7 +283,6 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) { user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"), ), ), - expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))), expectPush( user.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, "123"), ), @@ -302,7 +299,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) { { name: "success", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusherWithCreationDateNow( user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(), @@ -313,7 +310,6 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) { user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"), ), ), - expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))), ), }, args: args{ @@ -328,7 +324,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := c.verifyUserPasskeyCode(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.codeID, tt.args.code, alg) require.ErrorIs(t, err, tt.wantErr) diff --git a/internal/database/cockroach/crdb.go b/internal/database/cockroach/crdb.go index 2f685f6d92..988f678e16 100644 --- a/internal/database/cockroach/crdb.go +++ b/internal/database/cockroach/crdb.go @@ -7,6 +7,8 @@ import ( "strings" "time" + pgxdecimal "github.com/jackc/pgx-shopspring-decimal" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" "github.com/mitchellh/mapstructure" @@ -82,6 +84,11 @@ func (c *Config) Connect(useAdmin bool, pusherRatio, spoolerRatio float64, purpo return nil, err } + config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxdecimal.Register(conn.TypeMap()) + return nil + } + if connConfig.MaxOpenConns != 0 { config.MaxConns = int32(connConfig.MaxOpenConns) } diff --git a/internal/database/postgres/pg.go b/internal/database/postgres/pg.go index ecafbe877a..24b31a55fb 100644 --- a/internal/database/postgres/pg.go +++ b/internal/database/postgres/pg.go @@ -7,6 +7,8 @@ import ( "strings" "time" + pgxdecimal "github.com/jackc/pgx-shopspring-decimal" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" "github.com/mitchellh/mapstructure" @@ -82,6 +84,10 @@ func (c *Config) Connect(useAdmin bool, pusherRatio, spoolerRatio float64, purpo if err != nil { return nil, err } + config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxdecimal.Register(conn.TypeMap()) + return nil + } if connConfig.MaxOpenConns != 0 { config.MaxConns = int32(connConfig.MaxOpenConns) diff --git a/internal/domain/custom_message_text.go b/internal/domain/custom_message_text.go index 4ae0a3925a..aa73615451 100644 --- a/internal/domain/custom_message_text.go +++ b/internal/domain/custom_message_text.go @@ -17,6 +17,7 @@ const ( DomainClaimedMessageType = "DomainClaimed" PasswordlessRegistrationMessageType = "PasswordlessRegistration" PasswordChangeMessageType = "PasswordChange" + InviteUserMessageType = "InviteUser" MessageTitle = "Title" MessagePreHeader = "PreHeader" MessageSubject = "Subject" @@ -26,16 +27,6 @@ const ( MessageFooterText = "Footer" ) -type MessageTexts struct { - InitCode CustomMessageText - PasswordReset CustomMessageText - VerifyEmail CustomMessageText - VerifyPhone CustomMessageText - DomainClaimed CustomMessageText - PasswordlessRegistration CustomMessageText - PasswordChange CustomMessageText -} - type CustomMessageText struct { models.ObjectRoot @@ -71,5 +62,6 @@ func IsMessageTextType(textType string) bool { textType == VerifyEmailOTPMessageType || textType == DomainClaimedMessageType || textType == PasswordlessRegistrationMessageType || - textType == PasswordChangeMessageType + textType == PasswordChangeMessageType || + textType == InviteUserMessageType } diff --git a/internal/domain/debug_events.go b/internal/domain/debug_events.go new file mode 100644 index 0000000000..627c4e54b2 --- /dev/null +++ b/internal/domain/debug_events.go @@ -0,0 +1,14 @@ +package domain + +type DebugEventsState int + +const ( + DebugEventsStateUnspecified DebugEventsState = iota + DebugEventsStateInitial + DebugEventsStateChanged + DebugEventsStateRemoved +) + +func (state DebugEventsState) Exists() bool { + return state == DebugEventsStateInitial || state == DebugEventsStateChanged +} diff --git a/internal/domain/next_step.go b/internal/domain/next_step.go index 009989956f..7242d6eb1d 100644 --- a/internal/domain/next_step.go +++ b/internal/domain/next_step.go @@ -29,6 +29,7 @@ const ( NextStepProjectRequired NextStepRedirectToExternalIDP NextStepLoginSucceeded + NextStepVerifyInvite ) type LoginStep struct{} @@ -191,3 +192,9 @@ type LoginSucceededStep struct{} func (s *LoginSucceededStep) Type() NextStepType { return NextStepLoginSucceeded } + +type VerifyInviteStep struct{} + +func (s *VerifyInviteStep) Type() NextStepType { + return NextStepVerifyInvite +} diff --git a/internal/domain/secret_generator.go b/internal/domain/secret_generator.go index 10afb774a7..855e3447c1 100644 --- a/internal/domain/secret_generator.go +++ b/internal/domain/secret_generator.go @@ -14,6 +14,7 @@ const ( SecretGeneratorTypeAppSecret SecretGeneratorTypeOTPSMS SecretGeneratorTypeOTPEmail + SecretGeneratorTypeInviteCode secretGeneratorTypeCount ) diff --git a/internal/domain/secretgeneratortype_enumer.go b/internal/domain/secretgeneratortype_enumer.go index 92e0ead334..f819bafc1f 100644 --- a/internal/domain/secretgeneratortype_enumer.go +++ b/internal/domain/secretgeneratortype_enumer.go @@ -4,14 +4,11 @@ package domain import ( "fmt" - "strings" ) -const _SecretGeneratorTypeName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailsecret_generator_type_count" +const _SecretGeneratorTypeName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesecret_generator_type_count" -var _SecretGeneratorTypeIndex = [...]uint8{0, 11, 20, 37, 54, 67, 86, 108, 118, 124, 133, 160} - -const _SecretGeneratorTypeLowerName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailsecret_generator_type_count" +var _SecretGeneratorTypeIndex = [...]uint8{0, 11, 20, 37, 54, 67, 86, 108, 118, 124, 133, 144, 171} func (i SecretGeneratorType) String() string { if i < 0 || i >= SecretGeneratorType(len(_SecretGeneratorTypeIndex)-1) { @@ -20,62 +17,21 @@ func (i SecretGeneratorType) String() string { return _SecretGeneratorTypeName[_SecretGeneratorTypeIndex[i]:_SecretGeneratorTypeIndex[i+1]] } -// An "invalid array index" compiler error signifies that the constant values have changed. -// Re-run the stringer command to generate them again. -func _SecretGeneratorTypeNoOp() { - var x [1]struct{} - _ = x[SecretGeneratorTypeUnspecified-(0)] - _ = x[SecretGeneratorTypeInitCode-(1)] - _ = x[SecretGeneratorTypeVerifyEmailCode-(2)] - _ = x[SecretGeneratorTypeVerifyPhoneCode-(3)] - _ = x[SecretGeneratorTypeVerifyDomain-(4)] - _ = x[SecretGeneratorTypePasswordResetCode-(5)] - _ = x[SecretGeneratorTypePasswordlessInitCode-(6)] - _ = x[SecretGeneratorTypeAppSecret-(7)] - _ = x[SecretGeneratorTypeOTPSMS-(8)] - _ = x[SecretGeneratorTypeOTPEmail-(9)] - _ = x[secretGeneratorTypeCount-(10)] -} - -var _SecretGeneratorTypeValues = []SecretGeneratorType{SecretGeneratorTypeUnspecified, SecretGeneratorTypeInitCode, SecretGeneratorTypeVerifyEmailCode, SecretGeneratorTypeVerifyPhoneCode, SecretGeneratorTypeVerifyDomain, SecretGeneratorTypePasswordResetCode, SecretGeneratorTypePasswordlessInitCode, SecretGeneratorTypeAppSecret, SecretGeneratorTypeOTPSMS, SecretGeneratorTypeOTPEmail, secretGeneratorTypeCount} +var _SecretGeneratorTypeValues = []SecretGeneratorType{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} var _SecretGeneratorTypeNameToValueMap = map[string]SecretGeneratorType{ - _SecretGeneratorTypeName[0:11]: SecretGeneratorTypeUnspecified, - _SecretGeneratorTypeLowerName[0:11]: SecretGeneratorTypeUnspecified, - _SecretGeneratorTypeName[11:20]: SecretGeneratorTypeInitCode, - _SecretGeneratorTypeLowerName[11:20]: SecretGeneratorTypeInitCode, - _SecretGeneratorTypeName[20:37]: SecretGeneratorTypeVerifyEmailCode, - _SecretGeneratorTypeLowerName[20:37]: SecretGeneratorTypeVerifyEmailCode, - _SecretGeneratorTypeName[37:54]: SecretGeneratorTypeVerifyPhoneCode, - _SecretGeneratorTypeLowerName[37:54]: SecretGeneratorTypeVerifyPhoneCode, - _SecretGeneratorTypeName[54:67]: SecretGeneratorTypeVerifyDomain, - _SecretGeneratorTypeLowerName[54:67]: SecretGeneratorTypeVerifyDomain, - _SecretGeneratorTypeName[67:86]: SecretGeneratorTypePasswordResetCode, - _SecretGeneratorTypeLowerName[67:86]: SecretGeneratorTypePasswordResetCode, - _SecretGeneratorTypeName[86:108]: SecretGeneratorTypePasswordlessInitCode, - _SecretGeneratorTypeLowerName[86:108]: SecretGeneratorTypePasswordlessInitCode, - _SecretGeneratorTypeName[108:118]: SecretGeneratorTypeAppSecret, - _SecretGeneratorTypeLowerName[108:118]: SecretGeneratorTypeAppSecret, - _SecretGeneratorTypeName[118:124]: SecretGeneratorTypeOTPSMS, - _SecretGeneratorTypeLowerName[118:124]: SecretGeneratorTypeOTPSMS, - _SecretGeneratorTypeName[124:133]: SecretGeneratorTypeOTPEmail, - _SecretGeneratorTypeLowerName[124:133]: SecretGeneratorTypeOTPEmail, - _SecretGeneratorTypeName[133:160]: secretGeneratorTypeCount, - _SecretGeneratorTypeLowerName[133:160]: secretGeneratorTypeCount, -} - -var _SecretGeneratorTypeNames = []string{ - _SecretGeneratorTypeName[0:11], - _SecretGeneratorTypeName[11:20], - _SecretGeneratorTypeName[20:37], - _SecretGeneratorTypeName[37:54], - _SecretGeneratorTypeName[54:67], - _SecretGeneratorTypeName[67:86], - _SecretGeneratorTypeName[86:108], - _SecretGeneratorTypeName[108:118], - _SecretGeneratorTypeName[118:124], - _SecretGeneratorTypeName[124:133], - _SecretGeneratorTypeName[133:160], + _SecretGeneratorTypeName[0:11]: 0, + _SecretGeneratorTypeName[11:20]: 1, + _SecretGeneratorTypeName[20:37]: 2, + _SecretGeneratorTypeName[37:54]: 3, + _SecretGeneratorTypeName[54:67]: 4, + _SecretGeneratorTypeName[67:86]: 5, + _SecretGeneratorTypeName[86:108]: 6, + _SecretGeneratorTypeName[108:118]: 7, + _SecretGeneratorTypeName[118:124]: 8, + _SecretGeneratorTypeName[124:133]: 9, + _SecretGeneratorTypeName[133:144]: 10, + _SecretGeneratorTypeName[144:171]: 11, } // SecretGeneratorTypeString retrieves an enum value from the enum constants string name. @@ -84,10 +40,6 @@ func SecretGeneratorTypeString(s string) (SecretGeneratorType, error) { if val, ok := _SecretGeneratorTypeNameToValueMap[s]; ok { return val, nil } - - if val, ok := _SecretGeneratorTypeNameToValueMap[strings.ToLower(s)]; ok { - return val, nil - } return 0, fmt.Errorf("%s does not belong to SecretGeneratorType values", s) } @@ -96,13 +48,6 @@ func SecretGeneratorTypeValues() []SecretGeneratorType { return _SecretGeneratorTypeValues } -// SecretGeneratorTypeStrings returns a slice of all String values of the enum -func SecretGeneratorTypeStrings() []string { - strs := make([]string, len(_SecretGeneratorTypeNames)) - copy(strs, _SecretGeneratorTypeNames) - return strs -} - // IsASecretGeneratorType returns "true" if the value is listed in the enum definition. "false" otherwise func (i SecretGeneratorType) IsASecretGeneratorType() bool { for _, v := range _SecretGeneratorTypeValues { diff --git a/internal/eventstore/event.go b/internal/eventstore/event.go index 3df096f069..656a02f33d 100644 --- a/internal/eventstore/event.go +++ b/internal/eventstore/event.go @@ -5,6 +5,8 @@ import ( "reflect" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/zerrors" ) @@ -44,7 +46,7 @@ type Event interface { // CreatedAt is the time the event was created at CreatedAt() time.Time // Position is the global position of the event - Position() float64 + Position() decimal.Decimal // Unmarshal parses the payload and stores the result // in the value pointed to by ptr. If ptr is nil or not a pointer, diff --git a/internal/eventstore/event_base.go b/internal/eventstore/event_base.go index c2b56128a8..0cd5b6440c 100644 --- a/internal/eventstore/event_base.go +++ b/internal/eventstore/event_base.go @@ -5,6 +5,8 @@ import ( "encoding/json" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/service" ) @@ -21,7 +23,7 @@ type BaseEvent struct { Agg *Aggregate Seq uint64 - Pos float64 + Pos decimal.Decimal Creation time.Time previousAggregateSequence uint64 previousAggregateTypeSequence uint64 @@ -34,7 +36,7 @@ type BaseEvent struct { } // Position implements Event. -func (e *BaseEvent) Position() float64 { +func (e *BaseEvent) Position() decimal.Decimal { return e.Pos } diff --git a/internal/eventstore/eventstore.go b/internal/eventstore/eventstore.go index e456135828..066a876da3 100644 --- a/internal/eventstore/eventstore.go +++ b/internal/eventstore/eventstore.go @@ -8,6 +8,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -217,11 +218,11 @@ func (es *Eventstore) FilterToReducer(ctx context.Context, searchQuery *SearchQu }) } -// LatestSequence filters the latest sequence for the given search query -func (es *Eventstore) LatestSequence(ctx context.Context, queryFactory *SearchQueryBuilder) (float64, error) { +// LatestPosition filters the latest position for the given search query +func (es *Eventstore) LatestPosition(ctx context.Context, queryFactory *SearchQueryBuilder) (decimal.Decimal, error) { queryFactory.InstanceID(authz.GetInstance(ctx).InstanceID()) - return es.querier.LatestSequence(ctx, queryFactory) + return es.querier.LatestPosition(ctx, queryFactory) } // InstanceIDs returns the instance ids found by the search query @@ -266,8 +267,8 @@ type Querier interface { Health(ctx context.Context) error // FilterToReducer calls r for every event returned from the storage FilterToReducer(ctx context.Context, searchQuery *SearchQueryBuilder, r Reducer) error - // LatestSequence returns the latest sequence found by the search query - LatestSequence(ctx context.Context, queryFactory *SearchQueryBuilder) (float64, error) + // LatestPosition returns the latest position found by the search query + LatestPosition(ctx context.Context, queryFactory *SearchQueryBuilder) (decimal.Decimal, error) // InstanceIDs returns the instance ids found by the search query InstanceIDs(ctx context.Context, queryFactory *SearchQueryBuilder) ([]string, error) } diff --git a/internal/eventstore/eventstore_querier_test.go b/internal/eventstore/eventstore_querier_test.go index 856bb4a20e..6a01c6fbf0 100644 --- a/internal/eventstore/eventstore_querier_test.go +++ b/internal/eventstore/eventstore_querier_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/eventstore" ) @@ -98,7 +100,7 @@ func TestCRDB_Filter(t *testing.T) { } } -func TestCRDB_LatestSequence(t *testing.T) { +func TestCRDB_LatestPosition(t *testing.T) { type args struct { searchQuery *eventstore.SearchQueryBuilder } @@ -106,7 +108,7 @@ func TestCRDB_LatestSequence(t *testing.T) { existingEvents []eventstore.Command } type res struct { - sequence float64 + position decimal.Decimal } tests := []struct { name string @@ -118,7 +120,7 @@ func TestCRDB_LatestSequence(t *testing.T) { { name: "aggregate type filter no sequence", args: args{ - searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). + searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). AddQuery(). AggregateTypes("not found"). Builder(), @@ -135,7 +137,7 @@ func TestCRDB_LatestSequence(t *testing.T) { { name: "aggregate type filter sequence", args: args{ - searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). + searchQuery: eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). AddQuery(). AggregateTypes(eventstore.AggregateType(t.Name())). Builder(), @@ -169,12 +171,12 @@ func TestCRDB_LatestSequence(t *testing.T) { return } - sequence, err := db.LatestSequence(context.Background(), tt.args.searchQuery) + position, err := db.LatestPosition(context.Background(), tt.args.searchQuery) if (err != nil) != tt.wantErr { t.Errorf("CRDB.query() error = %v, wantErr %v", err, tt.wantErr) } - if tt.res.sequence > sequence { - t.Errorf("CRDB.query() expected sequence: %v got %v", tt.res.sequence, sequence) + if tt.res.position.GreaterThan(position) { + t.Errorf("CRDB.query() expected sequence: %v got %v", tt.res.position, position) } }) } diff --git a/internal/eventstore/eventstore_test.go b/internal/eventstore/eventstore_test.go index 53ef4e54cf..86f7809f24 100644 --- a/internal/eventstore/eventstore_test.go +++ b/internal/eventstore/eventstore_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/service" @@ -390,7 +391,7 @@ func (repo *testPusher) Push(ctx context.Context, commands ...Command) (events [ type testQuerier struct { events []Event - sequence float64 + sequence decimal.Decimal instances []string err error t *testing.T @@ -423,9 +424,9 @@ func (repo *testQuerier) FilterToReducer(ctx context.Context, searchQuery *Searc return nil } -func (repo *testQuerier) LatestSequence(ctx context.Context, queryFactory *SearchQueryBuilder) (float64, error) { +func (repo *testQuerier) LatestPosition(ctx context.Context, queryFactory *SearchQueryBuilder) (decimal.Decimal, error) { if repo.err != nil { - return 0, repo.err + return decimal.Decimal{}, repo.err } return repo.sequence, nil } @@ -1055,7 +1056,7 @@ func TestEventstore_FilterEvents(t *testing.T) { } } -func TestEventstore_LatestSequence(t *testing.T) { +func TestEventstore_LatestPosition(t *testing.T) { type args struct { query *SearchQueryBuilder } @@ -1075,7 +1076,7 @@ func TestEventstore_LatestSequence(t *testing.T) { name: "no events", args: args{ query: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, queries: []*SearchQuery{ { builder: &SearchQueryBuilder{}, @@ -1098,7 +1099,7 @@ func TestEventstore_LatestSequence(t *testing.T) { name: "repo error", args: args{ query: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, queries: []*SearchQuery{ { builder: &SearchQueryBuilder{}, @@ -1121,7 +1122,7 @@ func TestEventstore_LatestSequence(t *testing.T) { name: "found events", args: args{ query: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, queries: []*SearchQuery{ { builder: &SearchQueryBuilder{}, @@ -1147,7 +1148,7 @@ func TestEventstore_LatestSequence(t *testing.T) { querier: tt.fields.repo, } - _, err := es.LatestSequence(context.Background(), tt.args.query) + _, err := es.LatestPosition(context.Background(), tt.args.query) if (err != nil) != tt.res.wantErr { t.Errorf("Eventstore.aggregatesToEvents() error = %v, wantErr %v", err, tt.res.wantErr) } diff --git a/internal/eventstore/handler/v2/field_handler.go b/internal/eventstore/handler/v2/field_handler.go index 8b71f32519..2c371d67ec 100644 --- a/internal/eventstore/handler/v2/field_handler.go +++ b/internal/eventstore/handler/v2/field_handler.go @@ -8,6 +8,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/eventstore" ) @@ -123,7 +124,7 @@ func (h *FieldHandler) processEvents(ctx context.Context, config *triggerConfig) return additionalIteration, err } // stop execution if currentState.eventTimestamp >= config.maxCreatedAt - if config.maxPosition != 0 && currentState.position >= config.maxPosition { + if !config.maxPosition.IsZero() && currentState.position.GreaterThanOrEqual(config.maxPosition) { return false, nil } @@ -156,7 +157,7 @@ func (h *FieldHandler) fetchEvents(ctx context.Context, tx *sql.Tx, currentState idx, offset := skipPreviouslyReducedEvents(events, currentState) - if currentState.position == events[len(events)-1].Position() { + if currentState.position.Equal(events[len(events)-1].Position()) { offset += currentState.offset } currentState.position = events[len(events)-1].Position() @@ -186,9 +187,9 @@ func (h *FieldHandler) fetchEvents(ctx context.Context, tx *sql.Tx, currentState } func skipPreviouslyReducedEvents(events []eventstore.Event, currentState *state) (index int, offset uint32) { - var position float64 + var position decimal.Decimal for i, event := range events { - if event.Position() != position { + if !event.Position().Equal(position) { offset = 0 position = event.Position() } diff --git a/internal/eventstore/handler/v2/handler.go b/internal/eventstore/handler/v2/handler.go index b395035b8f..aaefec2e9b 100644 --- a/internal/eventstore/handler/v2/handler.go +++ b/internal/eventstore/handler/v2/handler.go @@ -4,13 +4,13 @@ import ( "context" "database/sql" "errors" - "math" "math/rand" "slices" "sync" "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -379,7 +379,7 @@ func (h *Handler) existingInstances(ctx context.Context) ([]string, error) { type triggerConfig struct { awaitRunning bool - maxPosition float64 + maxPosition decimal.Decimal } type TriggerOpt func(conf *triggerConfig) @@ -390,7 +390,7 @@ func WithAwaitRunning() TriggerOpt { } } -func WithMaxPosition(position float64) TriggerOpt { +func WithMaxPosition(position decimal.Decimal) TriggerOpt { return func(conf *triggerConfig) { conf.maxPosition = position } @@ -500,7 +500,7 @@ func (h *Handler) processEvents(ctx context.Context, config *triggerConfig) (add return additionalIteration, err } // stop execution if currentState.eventTimestamp >= config.maxCreatedAt - if config.maxPosition != 0 && currentState.position >= config.maxPosition { + if !config.maxPosition.Equal(decimal.Decimal{}) && currentState.position.GreaterThanOrEqual(config.maxPosition) { return false, nil } @@ -576,7 +576,7 @@ func (h *Handler) generateStatements(ctx context.Context, tx *sql.Tx, currentSta func skipPreviouslyReducedStatements(statements []*Statement, currentState *state) int { for i, statement := range statements { - if statement.Position == currentState.position && + if statement.Position.Equal(currentState.position) && statement.AggregateID == currentState.aggregateID && statement.AggregateType == currentState.aggregateType && statement.Sequence == currentState.sequence { @@ -609,14 +609,14 @@ func (h *Handler) executeStatement(ctx context.Context, tx *sql.Tx, currentState return nil } - _, err = tx.Exec("SAVEPOINT exec") + _, err = tx.ExecContext(ctx, "SAVEPOINT exec") if err != nil { h.log().WithError(err).Debug("create savepoint failed") return err } var shouldContinue bool defer func() { - _, errSave := tx.Exec("RELEASE SAVEPOINT exec") + _, errSave := tx.ExecContext(ctx, "RELEASE SAVEPOINT exec") if err == nil { err = errSave } @@ -644,9 +644,8 @@ func (h *Handler) eventQuery(currentState *state) *eventstore.SearchQueryBuilder OrderAsc(). InstanceID(currentState.instanceID) - if currentState.position > 0 { - // decrease position by 10 because builder.PositionAfter filters for position > and we need position >= - builder = builder.PositionAfter(math.Float64frombits(math.Float64bits(currentState.position) - 10)) + if currentState.position.GreaterThan(decimal.Decimal{}) { + builder = builder.PositionGreaterEqual(currentState.position) if currentState.offset > 0 { builder = builder.Offset(currentState.offset) } diff --git a/internal/eventstore/handler/v2/state.go b/internal/eventstore/handler/v2/state.go index d3b6953488..c4afaed204 100644 --- a/internal/eventstore/handler/v2/state.go +++ b/internal/eventstore/handler/v2/state.go @@ -7,6 +7,8 @@ import ( "errors" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" @@ -14,7 +16,7 @@ import ( type state struct { instanceID string - position float64 + position decimal.Decimal eventTimestamp time.Time aggregateType eventstore.AggregateType aggregateID string @@ -45,7 +47,7 @@ func (h *Handler) currentState(ctx context.Context, tx *sql.Tx, config *triggerC aggregateType = new(sql.NullString) sequence = new(sql.NullInt64) timestamp = new(sql.NullTime) - position = new(sql.NullFloat64) + position = new(decimal.NullDecimal) offset = new(sql.NullInt64) ) @@ -75,7 +77,7 @@ func (h *Handler) currentState(ctx context.Context, tx *sql.Tx, config *triggerC currentState.aggregateType = eventstore.AggregateType(aggregateType.String) currentState.sequence = uint64(sequence.Int64) currentState.eventTimestamp = timestamp.Time - currentState.position = position.Float64 + currentState.position = position.Decimal // psql does not provide unsigned numbers so we work around it currentState.offset = uint32(offset.Int64) return currentState, nil diff --git a/internal/eventstore/handler/v2/state_test.go b/internal/eventstore/handler/v2/state_test.go index cc5fb1fbab..ef91d78e55 100644 --- a/internal/eventstore/handler/v2/state_test.go +++ b/internal/eventstore/handler/v2/state_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database/mock" @@ -166,7 +167,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { updatedState: &state{ instanceID: "instance", eventTimestamp: time.Now(), - position: 42, + position: decimal.NewFromInt(42), }, }, isErr: func(t *testing.T, err error) { @@ -192,7 +193,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { updatedState: &state{ instanceID: "instance", eventTimestamp: time.Now(), - position: 42, + position: decimal.NewFromInt(42), }, }, isErr: func(t *testing.T, err error) { @@ -217,7 +218,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { eventstore.AggregateType("aggregate type"), uint64(42), mock.AnyType[time.Time]{}, - float64(42), + decimal.NewFromInt(42), uint32(0), ), mock.WithExecRowsAffected(1), @@ -228,7 +229,7 @@ func TestHandler_updateLastUpdated(t *testing.T) { updatedState: &state{ instanceID: "instance", eventTimestamp: time.Now(), - position: 42, + position: decimal.NewFromInt(42), aggregateType: "aggregate type", aggregateID: "aggregate id", sequence: 42, @@ -397,7 +398,7 @@ func TestHandler_currentState(t *testing.T) { "aggregate type", int64(42), testTime, - float64(42), + decimal.NewFromInt(42).String(), uint16(10), }, }, @@ -412,7 +413,7 @@ func TestHandler_currentState(t *testing.T) { currentState: &state{ instanceID: "instance", eventTimestamp: testTime, - position: 42, + position: decimal.NewFromInt(42), aggregateType: "aggregate type", aggregateID: "aggregate id", sequence: 42, diff --git a/internal/eventstore/handler/v2/statement.go b/internal/eventstore/handler/v2/statement.go index 43d1818487..4bc660d9f9 100644 --- a/internal/eventstore/handler/v2/statement.go +++ b/internal/eventstore/handler/v2/statement.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "golang.org/x/exp/constraints" @@ -83,7 +84,7 @@ type Statement struct { AggregateType eventstore.AggregateType AggregateID string Sequence uint64 - Position float64 + Position decimal.Decimal CreationDate time.Time InstanceID string @@ -337,6 +338,21 @@ func NewNoOpStatement(event eventstore.Event) *Statement { return NewStatement(event, nil) } +func NewSleepStatement(event eventstore.Event, d time.Duration, opts ...execOption) *Statement { + return NewStatement( + event, + exec( + execConfig{ + args: []any{float64(d) / float64(time.Second)}, + }, + func(_ execConfig) string { + return "SELECT pg_sleep($1);" + }, + opts, + ), + ) +} + func NewMultiStatement(event eventstore.Event, opts ...func(eventstore.Event) Exec) *Statement { if len(opts) == 0 { return NewNoOpStatement(event) @@ -384,6 +400,12 @@ func AddCopyStatement(conflict, from, to []Column, conditions []NamespacedCondit } } +func AddSleepStatement(d time.Duration, opts ...execOption) func(eventstore.Event) Exec { + return func(event eventstore.Event) Exec { + return NewSleepStatement(event, d, opts...).Execute + } +} + func NewArrayAppendCol(column string, value interface{}) Column { return Column{ Name: column, diff --git a/internal/eventstore/local_crdb_test.go b/internal/eventstore/local_crdb_test.go index 6df9e9fd29..e46509ba9f 100644 --- a/internal/eventstore/local_crdb_test.go +++ b/internal/eventstore/local_crdb_test.go @@ -2,13 +2,16 @@ package eventstore_test import ( "context" - "database/sql" "encoding/json" "os" "testing" "time" "github.com/cockroachdb/cockroach-go/v2/testserver" + pgxdecimal "github.com/jackc/pgx-shopspring-decimal" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/jackc/pgx/v5/stdlib" "github.com/zitadel/logging" "github.com/zitadel/zitadel/cmd/initialise" @@ -39,10 +42,19 @@ func TestMain(m *testing.M) { testCRDBClient = &database.DB{ Database: new(testDB), } - testCRDBClient.DB, err = sql.Open("postgres", ts.PGURL().String()) + config, err := pgxpool.ParseConfig(ts.PGURL().String()) if err != nil { - logging.WithFields("error", err).Fatal("unable to connect to db") + logging.WithFields("error", err).Fatal("unable to parse db config") } + config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxdecimal.Register(conn.TypeMap()) + return nil + } + pool, err := pgxpool.NewWithConfig(context.Background(), config) + if err != nil { + logging.WithFields("error", err).Fatal("unable to create db pool") + } + testCRDBClient.DB = stdlib.OpenDBFromPool(pool) if err = testCRDBClient.Ping(); err != nil { logging.WithFields("error", err).Fatal("unable to ping db") } @@ -103,10 +115,19 @@ func initDB(db *database.DB) error { } func connectLocalhost() (*database.DB, error) { - client, err := sql.Open("pgx", "postgresql://root@localhost:26257/defaultdb?sslmode=disable") + config, err := pgxpool.ParseConfig("postgresql://root@localhost:26257/defaultdb?sslmode=disable") if err != nil { return nil, err } + config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxdecimal.Register(conn.TypeMap()) + return nil + } + pool, err := pgxpool.NewWithConfig(context.Background(), config) + if err != nil { + return nil, err + } + client := stdlib.OpenDBFromPool(pool) if err = client.Ping(); err != nil { return nil, err } diff --git a/internal/eventstore/read_model.go b/internal/eventstore/read_model.go index d2c755cc3a..ae77275732 100644 --- a/internal/eventstore/read_model.go +++ b/internal/eventstore/read_model.go @@ -1,19 +1,23 @@ package eventstore -import "time" +import ( + "time" + + "github.com/shopspring/decimal" +) // ReadModel is the minimum representation of a read model. // It implements a basic reducer // it might be saved in a database or in memory type ReadModel struct { - AggregateID string `json:"-"` - ProcessedSequence uint64 `json:"-"` - CreationDate time.Time `json:"-"` - ChangeDate time.Time `json:"-"` - Events []Event `json:"-"` - ResourceOwner string `json:"-"` - InstanceID string `json:"-"` - Position float64 `json:"-"` + AggregateID string `json:"-"` + ProcessedSequence uint64 `json:"-"` + CreationDate time.Time `json:"-"` + ChangeDate time.Time `json:"-"` + Events []Event `json:"-"` + ResourceOwner string `json:"-"` + InstanceID string `json:"-"` + Position decimal.Decimal `json:"-"` } // AppendEvents adds all the events to the read model. diff --git a/internal/eventstore/repository/event.go b/internal/eventstore/repository/event.go index 57b85f15ba..2f4c1d8843 100644 --- a/internal/eventstore/repository/event.go +++ b/internal/eventstore/repository/event.go @@ -5,6 +5,8 @@ import ( "encoding/json" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/eventstore" ) @@ -18,7 +20,7 @@ type Event struct { // Seq is the sequence of the event Seq uint64 // Pos is the global sequence of the event multiple events can have the same sequence - Pos float64 + Pos decimal.Decimal //CreationDate is the time the event is created // it's used for human readability. @@ -91,7 +93,7 @@ func (e *Event) Sequence() uint64 { } // Position implements [eventstore.Event] -func (e *Event) Position() float64 { +func (e *Event) Position() decimal.Decimal { return e.Pos } diff --git a/internal/eventstore/repository/mock/repository.mock.go b/internal/eventstore/repository/mock/repository.mock.go index a854de2995..ebd5f501a2 100644 --- a/internal/eventstore/repository/mock/repository.mock.go +++ b/internal/eventstore/repository/mock/repository.mock.go @@ -13,6 +13,7 @@ import ( context "context" reflect "reflect" + decimal "github.com/shopspring/decimal" eventstore "github.com/zitadel/zitadel/internal/eventstore" gomock "go.uber.org/mock/gomock" ) @@ -83,19 +84,19 @@ func (mr *MockQuerierMockRecorder) InstanceIDs(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceIDs", reflect.TypeOf((*MockQuerier)(nil).InstanceIDs), arg0, arg1) } -// LatestSequence mocks base method. -func (m *MockQuerier) LatestSequence(arg0 context.Context, arg1 *eventstore.SearchQueryBuilder) (float64, error) { +// LatestPosition mocks base method. +func (m *MockQuerier) LatestPosition(arg0 context.Context, arg1 *eventstore.SearchQueryBuilder) (decimal.Decimal, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LatestSequence", arg0, arg1) - ret0, _ := ret[0].(float64) + ret := m.ctrl.Call(m, "LatestPosition", arg0, arg1) + ret0, _ := ret[0].(decimal.Decimal) ret1, _ := ret[1].(error) return ret0, ret1 } -// LatestSequence indicates an expected call of LatestSequence. -func (mr *MockQuerierMockRecorder) LatestSequence(arg0, arg1 any) *gomock.Call { +// LatestPosition indicates an expected call of LatestPosition. +func (mr *MockQuerierMockRecorder) LatestPosition(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatestSequence", reflect.TypeOf((*MockQuerier)(nil).LatestSequence), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatestPosition", reflect.TypeOf((*MockQuerier)(nil).LatestPosition), arg0, arg1) } // MockPusher is a mock of Pusher interface. diff --git a/internal/eventstore/repository/mock/repository.mock.impl.go b/internal/eventstore/repository/mock/repository.mock.impl.go index d41521ad8f..41ad4befd3 100644 --- a/internal/eventstore/repository/mock/repository.mock.impl.go +++ b/internal/eventstore/repository/mock/repository.mock.impl.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -186,8 +187,8 @@ func (e *mockEvent) Sequence() uint64 { return e.sequence } -func (e *mockEvent) Position() float64 { - return 0 +func (e *mockEvent) Position() decimal.Decimal { + return decimal.Decimal{} } func (e *mockEvent) CreatedAt() time.Time { diff --git a/internal/eventstore/repository/search_query.go b/internal/eventstore/repository/search_query.go index 39cca8b149..b28574cc84 100644 --- a/internal/eventstore/repository/search_query.go +++ b/internal/eventstore/repository/search_query.go @@ -55,6 +55,8 @@ const ( //OperationNotIn checks if a stored value does not match one of the passed value list OperationNotIn + OperationGreaterEqual + operationCount ) @@ -232,10 +234,10 @@ func instanceIDsFilter(builder *eventstore.SearchQueryBuilder, query *SearchQuer } func positionAfterFilter(builder *eventstore.SearchQueryBuilder, query *SearchQuery) *Filter { - if builder.GetPositionAfter() == 0 { + if builder.GetPositionAfter().IsZero() { return nil } - query.Position = NewFilter(FieldPosition, builder.GetPositionAfter(), OperationGreater) + query.Position = NewFilter(FieldPosition, builder.GetPositionAfter(), OperationGreaterEqual) return query.Position } diff --git a/internal/eventstore/repository/sql/crdb.go b/internal/eventstore/repository/sql/crdb.go index a60a2ef7b8..c778015497 100644 --- a/internal/eventstore/repository/sql/crdb.go +++ b/internal/eventstore/repository/sql/crdb.go @@ -11,6 +11,7 @@ import ( "github.com/cockroachdb/cockroach-go/v2/crdb" "github.com/jackc/pgx/v5/pgconn" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -265,11 +266,11 @@ func (crdb *CRDB) FilterToReducer(ctx context.Context, searchQuery *eventstore.S return err } -// LatestSequence returns the latest sequence found by the search query -func (db *CRDB) LatestSequence(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) (float64, error) { - var position sql.NullFloat64 +// LatestPosition returns the latest position found by the search query +func (db *CRDB) LatestPosition(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) (decimal.Decimal, error) { + var position decimal.Decimal err := query(ctx, db, searchQuery, &position, false) - return position.Float64, err + return position, err } // InstanceIDs returns the instance ids found by the search query @@ -336,7 +337,7 @@ func (db *CRDB) eventQuery(useV1 bool) string { " FROM eventstore.events2" } -func (db *CRDB) maxSequenceQuery(useV1 bool) string { +func (db *CRDB) maxPositionQuery(useV1 bool) string { if useV1 { return `SELECT event_sequence FROM eventstore.events` } @@ -414,6 +415,8 @@ func (db *CRDB) operation(operation repository.Operation) string { return "=" case repository.OperationGreater: return ">" + case repository.OperationGreaterEqual: + return ">=" case repository.OperationLess: return "<" case repository.OperationJSONContains: diff --git a/internal/eventstore/repository/sql/crdb_test.go b/internal/eventstore/repository/sql/crdb_test.go index a3f3331a82..aae2fde78d 100644 --- a/internal/eventstore/repository/sql/crdb_test.go +++ b/internal/eventstore/repository/sql/crdb_test.go @@ -4,6 +4,8 @@ import ( "database/sql" "testing" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" ) @@ -312,7 +314,7 @@ func generateEvent(t *testing.T, aggregateID string, opts ...func(*repository.Ev ResourceOwner: sql.NullString{String: "ro", Valid: true}, Typ: "test.created", Version: "v1", - Pos: 42, + Pos: decimal.NewFromInt(42), } for _, opt := range opts { diff --git a/internal/eventstore/repository/sql/query.go b/internal/eventstore/repository/sql/query.go index 3cddcb7924..c20bb62275 100644 --- a/internal/eventstore/repository/sql/query.go +++ b/internal/eventstore/repository/sql/query.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/call" @@ -25,7 +26,7 @@ type querier interface { conditionFormat(repository.Operation) string placeholder(query string) string eventQuery(useV1 bool) string - maxSequenceQuery(useV1 bool) string + maxPositionQuery(useV1 bool) string instanceIDsQuery(useV1 bool) string db() *database.DB orderByEventSequence(desc, shouldOrderBySequence, useV1 bool) string @@ -74,7 +75,7 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search // instead of using the max function of the database (which doesn't work for postgres) // we select the most recent row - if q.Columns == eventstore.ColumnsMaxSequence { + if q.Columns == eventstore.ColumnsMaxPosition { q.Limit = 1 q.Desc = true } @@ -91,7 +92,7 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search switch q.Columns { case eventstore.ColumnsEvent, - eventstore.ColumnsMaxSequence: + eventstore.ColumnsMaxPosition: query += criteria.orderByEventSequence(q.Desc, shouldOrderBySequence, useV1) } @@ -135,8 +136,8 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search func prepareColumns(criteria querier, columns eventstore.Columns, useV1 bool) (string, func(s scan, dest interface{}) error) { switch columns { - case eventstore.ColumnsMaxSequence: - return criteria.maxSequenceQuery(useV1), maxSequenceScanner + case eventstore.ColumnsMaxPosition: + return criteria.maxPositionQuery(useV1), maxPositionScanner case eventstore.ColumnsInstanceIDs: return criteria.instanceIDsQuery(useV1), instanceIDsScanner case eventstore.ColumnsEvent: @@ -154,13 +155,15 @@ func prepareTimeTravel(ctx context.Context, criteria querier, allow bool) string return criteria.Timetravel(took) } -func maxSequenceScanner(row scan, dest interface{}) (err error) { - position, ok := dest.(*sql.NullFloat64) +func maxPositionScanner(row scan, dest interface{}) (err error) { + position, ok := dest.(*decimal.Decimal) if !ok { - return zerrors.ThrowInvalidArgumentf(nil, "SQL-NBjA9", "type must be sql.NullInt64 got: %T", dest) + return zerrors.ThrowInvalidArgumentf(nil, "SQL-NBjA9", "type must be decimal.Decimal got: %T", dest) } - err = row(position) + var res decimal.NullDecimal + err = row(&res) if err == nil || errors.Is(err, sql.ErrNoRows) { + *position = res.Decimal return nil } return zerrors.ThrowInternal(err, "SQL-bN5xg", "something went wrong") @@ -189,7 +192,7 @@ func eventsScanner(useV1 bool) func(scanner scan, dest interface{}) (err error) return zerrors.ThrowInvalidArgumentf(nil, "SQL-4GP6F", "events scanner: invalid type %T", dest) } event := new(repository.Event) - position := new(sql.NullFloat64) + position := new(decimal.NullDecimal) if useV1 { err = scanner( @@ -226,7 +229,7 @@ func eventsScanner(useV1 bool) func(scanner scan, dest interface{}) (err error) logging.New().WithError(err).Warn("unable to scan row") return zerrors.ThrowInternal(err, "SQL-M0dsf", "unable to scan row") } - event.Pos = position.Float64 + event.Pos = position.Decimal return reduce(event) } } diff --git a/internal/eventstore/repository/sql/query_test.go b/internal/eventstore/repository/sql/query_test.go index 5d54b27c21..654fa6d0b5 100644 --- a/internal/eventstore/repository/sql/query_test.go +++ b/internal/eventstore/repository/sql/query_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/DATA-DOG/go-sqlmock" + "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/database" @@ -109,36 +110,36 @@ func Test_prepareColumns(t *testing.T) { { name: "max column", args: args{ - columns: eventstore.ColumnsMaxSequence, - dest: new(sql.NullFloat64), + columns: eventstore.ColumnsMaxPosition, + dest: new(decimal.Decimal), useV1: true, }, res: res{ query: `SELECT event_sequence FROM eventstore.events`, - expected: sql.NullFloat64{Float64: 43, Valid: true}, + expected: decimal.NewFromInt(42), }, fields: fields{ - dbRow: []interface{}{sql.NullFloat64{Float64: 43, Valid: true}}, + dbRow: []interface{}{decimal.NewNullDecimal(decimal.NewFromInt(42))}, }, }, { name: "max column v2", args: args{ - columns: eventstore.ColumnsMaxSequence, - dest: new(sql.NullFloat64), + columns: eventstore.ColumnsMaxPosition, + dest: new(decimal.Decimal), }, res: res{ query: `SELECT "position" FROM eventstore.events2`, - expected: sql.NullFloat64{Float64: 43, Valid: true}, + expected: decimal.NewFromInt(42), }, fields: fields{ - dbRow: []interface{}{sql.NullFloat64{Float64: 43, Valid: true}}, + dbRow: []interface{}{decimal.NewNullDecimal(decimal.NewFromInt(42))}, }, }, { name: "max sequence wrong dest type", args: args{ - columns: eventstore.ColumnsMaxSequence, + columns: eventstore.ColumnsMaxPosition, dest: new(uint64), }, res: res{ @@ -178,11 +179,11 @@ func Test_prepareColumns(t *testing.T) { res: res{ query: `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2`, expected: []eventstore.Event{ - &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: 42, Data: nil, Version: "v1"}, + &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: decimal.NewFromInt(42), Data: nil, Version: "v1"}, }, }, fields: fields{ - dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), sql.NullFloat64{Float64: 42, Valid: true}, sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, + dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), decimal.NewNullDecimal(decimal.NewFromInt(42)), sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, }, }, { @@ -197,11 +198,11 @@ func Test_prepareColumns(t *testing.T) { res: res{ query: `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2`, expected: []eventstore.Event{ - &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: 0, Data: nil, Version: "v1"}, + &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: decimal.Decimal{}, Data: nil, Version: "v1"}, }, }, fields: fields{ - dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), sql.NullFloat64{Float64: 0, Valid: false}, sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, + dbRow: []interface{}{time.Time{}, eventstore.EventType(""), uint64(5), decimal.NullDecimal{}, sql.RawBytes(nil), "", sql.NullString{}, "", eventstore.AggregateType("user"), "hodor", uint8(1)}, }, }, { diff --git a/internal/eventstore/search_query.go b/internal/eventstore/search_query.go index 0c08a260eb..c7f9a65da3 100644 --- a/internal/eventstore/search_query.go +++ b/internal/eventstore/search_query.go @@ -5,6 +5,8 @@ import ( "database/sql" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -23,7 +25,7 @@ type SearchQueryBuilder struct { queries []*SearchQuery tx *sql.Tx allowTimeTravel bool - positionAfter float64 + positionGreaterEqual decimal.Decimal awaitOpenTransactions bool creationDateAfter time.Time creationDateBefore time.Time @@ -74,8 +76,8 @@ func (b *SearchQueryBuilder) GetAllowTimeTravel() bool { return b.allowTimeTravel } -func (b SearchQueryBuilder) GetPositionAfter() float64 { - return b.positionAfter +func (b SearchQueryBuilder) GetPositionAfter() decimal.Decimal { + return b.positionGreaterEqual } func (b SearchQueryBuilder) GetAwaitOpenTransactions() bool { @@ -131,8 +133,8 @@ type Columns int8 const ( //ColumnsEvent represents all fields of an event ColumnsEvent = iota + 1 - // ColumnsMaxSequence represents the latest sequence of the filtered events - ColumnsMaxSequence + // ColumnsMaxPosition represents the latest sequence of the filtered events + ColumnsMaxPosition // ColumnsInstanceIDs represents the instance ids of the filtered events ColumnsInstanceIDs @@ -267,8 +269,8 @@ func (builder *SearchQueryBuilder) AllowTimeTravel() *SearchQueryBuilder { } // PositionAfter filters for events which happened after the specified time -func (builder *SearchQueryBuilder) PositionAfter(position float64) *SearchQueryBuilder { - builder.positionAfter = position +func (builder *SearchQueryBuilder) PositionGreaterEqual(position decimal.Decimal) *SearchQueryBuilder { + builder.positionGreaterEqual = position return builder } diff --git a/internal/eventstore/search_query_test.go b/internal/eventstore/search_query_test.go index 8c654911ea..da8acf878d 100644 --- a/internal/eventstore/search_query_test.go +++ b/internal/eventstore/search_query_test.go @@ -116,10 +116,10 @@ func TestSearchQuerybuilderSetters(t *testing.T) { { name: "set columns", args: args{ - setters: []func(*SearchQueryBuilder) *SearchQueryBuilder{testSetColumns(ColumnsMaxSequence)}, + setters: []func(*SearchQueryBuilder) *SearchQueryBuilder{testSetColumns(ColumnsMaxPosition)}, }, res: &SearchQueryBuilder{ - columns: ColumnsMaxSequence, + columns: ColumnsMaxPosition, }, }, { diff --git a/internal/eventstore/v1/models/event.go b/internal/eventstore/v1/models/event.go index 8c50d64da0..ab2b608872 100644 --- a/internal/eventstore/v1/models/event.go +++ b/internal/eventstore/v1/models/event.go @@ -5,6 +5,8 @@ import ( "reflect" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -20,7 +22,7 @@ var _ eventstore.Event = (*Event)(nil) type Event struct { ID string Seq uint64 - Pos float64 + Pos decimal.Decimal CreationDate time.Time Typ eventstore.EventType PreviousSequence uint64 @@ -80,7 +82,7 @@ func (e *Event) Sequence() uint64 { } // Position implements [eventstore.Event] -func (e *Event) Position() float64 { +func (e *Event) Position() decimal.Decimal { return e.Pos } diff --git a/internal/eventstore/v3/event.go b/internal/eventstore/v3/event.go index e1c95f13ff..f489d98396 100644 --- a/internal/eventstore/v3/event.go +++ b/internal/eventstore/v3/event.go @@ -4,6 +4,7 @@ import ( "encoding/json" "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/eventstore" @@ -21,7 +22,7 @@ type event struct { typ eventstore.EventType createdAt time.Time sequence uint64 - position float64 + position decimal.Decimal payload Payload } @@ -84,8 +85,8 @@ func (e *event) Sequence() uint64 { return e.sequence } -// Sequence implements [eventstore.Event] -func (e *event) Position() float64 { +// Position implements [eventstore.Event] +func (e *event) Position() decimal.Decimal { return e.position } diff --git a/internal/integration/assert.go b/internal/integration/assert.go index b3e63fd29e..3c5fadb373 100644 --- a/internal/integration/assert.go +++ b/internal/integration/assert.go @@ -50,7 +50,7 @@ type ResourceListDetailsMsg interface { // If the change date is populated, it is checked with a tolerance of 1 minute around Now. // // The resource owner is compared with expected. -func AssertDetails[D Details, M DetailsMsg[D]](t testing.TB, expected, actual M) { +func AssertDetails[D Details, M DetailsMsg[D]](t assert.TestingT, expected, actual M) { wantDetails, gotDetails := expected.GetDetails(), actual.GetDetails() var nilDetails D if wantDetails == nilDetails { @@ -69,7 +69,7 @@ func AssertDetails[D Details, M DetailsMsg[D]](t testing.TB, expected, actual M) assert.Equal(t, wantDetails.GetResourceOwner(), gotDetails.GetResourceOwner()) } -func AssertResourceDetails(t testing.TB, expected *resources_object.Details, actual *resources_object.Details) { +func AssertResourceDetails(t assert.TestingT, expected *resources_object.Details, actual *resources_object.Details) { if expected.GetChanged() != nil { wantChangeDate := time.Now() gotChangeDate := actual.GetChanged().AsTime() @@ -87,7 +87,7 @@ func AssertResourceDetails(t testing.TB, expected *resources_object.Details, act } } -func AssertListDetails[L ListDetails, D ListDetailsMsg[L]](t testing.TB, expected, actual D) { +func AssertListDetails[L ListDetails, D ListDetailsMsg[L]](t assert.TestingT, expected, actual D) { wantDetails, gotDetails := expected.GetDetails(), actual.GetDetails() var nilDetails L if wantDetails == nilDetails { @@ -99,11 +99,11 @@ func AssertListDetails[L ListDetails, D ListDetailsMsg[L]](t testing.TB, expecte if wantDetails.GetTimestamp() != nil { gotCD := gotDetails.GetTimestamp().AsTime() wantCD := time.Now() - assert.WithinRange(t, gotCD, wantCD.Add(-time.Minute), wantCD.Add(time.Minute)) + assert.WithinRange(t, gotCD, wantCD.Add(-10*time.Minute), wantCD.Add(time.Minute)) } } -func AssertResourceListDetails[D ResourceListDetailsMsg](t testing.TB, expected, actual D) { +func AssertResourceListDetails[D ResourceListDetailsMsg](t assert.TestingT, expected, actual D) { wantDetails, gotDetails := expected.GetDetails(), actual.GetDetails() if wantDetails == nil { assert.Nil(t, gotDetails) @@ -116,7 +116,7 @@ func AssertResourceListDetails[D ResourceListDetailsMsg](t testing.TB, expected, if wantDetails.GetTimestamp() != nil { gotCD := gotDetails.GetTimestamp().AsTime() wantCD := time.Now() - assert.WithinRange(t, gotCD, wantCD.Add(-time.Minute), wantCD.Add(time.Minute)) + assert.WithinRange(t, gotCD, wantCD.Add(-10*time.Minute), wantCD.Add(time.Minute)) } } diff --git a/internal/integration/client.go b/internal/integration/client.go index ed5f0620c4..82b8ab3b6e 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -6,26 +6,17 @@ import ( "testing" "time" - crewjam_saml "github.com/crewjam/saml" + "github.com/brianvoe/gofakeit/v6" "github.com/muhlemmer/gu" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v3/pkg/oidc" - "golang.org/x/oauth2" - "golang.org/x/text/language" "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/idp/providers/ldap" - openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" - "github.com/zitadel/zitadel/internal/idp/providers/saml" - idp_rp "github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/pkg/grpc/admin" "github.com/zitadel/zitadel/pkg/grpc/auth" "github.com/zitadel/zitadel/pkg/grpc/feature/v2" @@ -37,7 +28,7 @@ import ( object_v3alpha "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" - "github.com/zitadel/zitadel/pkg/grpc/org/v2" + org "github.com/zitadel/zitadel/pkg/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha" user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" @@ -47,7 +38,6 @@ import ( session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" "github.com/zitadel/zitadel/pkg/grpc/settings/v2" settings_v2beta "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" - "github.com/zitadel/zitadel/pkg/grpc/system" user_pb "github.com/zitadel/zitadel/pkg/grpc/user" user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" @@ -68,7 +58,6 @@ type Client struct { OIDCv2 oidc_pb.OIDCServiceClient OrgV2beta org_v2beta.OrganizationServiceClient OrgV2 org.OrganizationServiceClient - System system.SystemServiceClient ActionV3Alpha action.ZITADELActionsClient FeatureV2beta feature_v2beta.FeatureServiceClient FeatureV2 feature.FeatureServiceClient @@ -78,8 +67,14 @@ type Client struct { UserV3Alpha user_v3alpha.ZITADELUsersClient } -func newClient(cc *grpc.ClientConn) Client { - return Client{ +func newClient(ctx context.Context, target string) (*Client, error) { + cc, err := grpc.NewClient(target, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return nil, err + } + client := &Client{ CC: cc, Admin: admin.NewAdminServiceClient(cc), Mgmt: mgmt.NewManagementServiceClient(cc), @@ -94,7 +89,6 @@ func newClient(cc *grpc.ClientConn) Client { OIDCv2: oidc_pb.NewOIDCServiceClient(cc), OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc), OrgV2: org.NewOrganizationServiceClient(cc), - System: system.NewSystemServiceClient(cc), ActionV3Alpha: action.NewZITADELActionsClient(cc), FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc), FeatureV2: feature.NewFeatureServiceClient(cc), @@ -103,60 +97,38 @@ func newClient(cc *grpc.ClientConn) Client { IDPv2: idp_pb.NewIdentityProviderServiceClient(cc), UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc), } + return client, client.pollHealth(ctx) } -func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx context.Context) (primaryDomain, instanceId, adminID string, authenticatedIamOwnerCtx context.Context) { - primaryDomain = RandString(5) + ".integration.localhost" - instance, err := t.Client.System.CreateInstance(systemCtx, &system.CreateInstanceRequest{ - InstanceName: "testinstance", - CustomDomain: primaryDomain, - Owner: &system.CreateInstanceRequest_Machine_{ - Machine: &system.CreateInstanceRequest_Machine{ - UserName: "owner", - Name: "owner", - PersonalAccessToken: &system.CreateInstanceRequest_PersonalAccessToken{}, - }, - }, - }) - require.NoError(tt, err) - t.createClientConn(iamOwnerCtx, fmt.Sprintf("%s:%d", primaryDomain, t.Config.Port)) - instanceId = instance.GetInstanceId() - owner, err := t.Queries.GetUserByLoginName(authz.WithInstanceID(iamOwnerCtx, instanceId), true, "owner@"+primaryDomain) - require.NoError(tt, err) - t.Users.Set(instanceId, IAMOwner, &User{ - User: owner, - Token: instance.GetPat(), - }) - newCtx := t.WithInstanceAuthorization(iamOwnerCtx, IAMOwner, instanceId) - var adminUser *mgmt.ImportHumanUserResponse - // the following serves two purposes: - // 1. it ensures that the instance is ready to be used - // 2. it enables a normal login with the default admin user credentials - require.EventuallyWithT(tt, func(collectT *assert.CollectT) { - var importErr error - adminUser, importErr = t.Client.Mgmt.ImportHumanUser(newCtx, &mgmt.ImportHumanUserRequest{ - UserName: "zitadel-admin@zitadel.localhost", - Email: &mgmt.ImportHumanUserRequest_Email{ - Email: "zitadel-admin@zitadel.localhost", - IsEmailVerified: true, - }, - Password: "Password1!", - Profile: &mgmt.ImportHumanUserRequest_Profile{ - FirstName: "hodor", - LastName: "hodor", - NickName: "hodor", - }, - }) - assert.NoError(collectT, importErr) - }, 2*time.Minute, 100*time.Millisecond, "instance not ready") - return primaryDomain, instanceId, adminUser.GetUserId(), t.updateInstanceAndOrg(newCtx, fmt.Sprintf("%s:%d", primaryDomain, t.Config.ExternalPort)) +// pollHealth waits until a healthy status is reported. +func (c *Client) pollHealth(ctx context.Context) (err error) { + for { + err = func(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + _, err := c.Admin.Healthz(ctx, &admin.HealthzRequest{}) + return err + }(ctx) + if err == nil { + return nil + } + logging.WithError(err).Debug("poll healthz") + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(time.Second): + continue + } + } } -func (s *Tester) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserResponse { - resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ +func (i *Instance) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserResponse { + resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ Org: &object.Organization_OrgId{ - OrgId: s.Organisation.ID, + OrgId: i.DefaultOrg.GetId(), }, }, Profile: &user_v2.SetHumanProfile{ @@ -178,15 +150,15 @@ func (s *Tester) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserRespo }, }, }) - logging.OnError(err).Fatal("create human user") + logging.OnError(err).Panic("create human user") return resp } -func (s *Tester) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHumanUserResponse { - resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ +func (i *Instance) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHumanUserResponse { + resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ Org: &object.Organization_OrgId{ - OrgId: s.Organisation.ID, + OrgId: i.DefaultOrg.GetId(), }, }, Profile: &user_v2.SetHumanProfile{ @@ -202,15 +174,15 @@ func (s *Tester) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHumanUs }, }, }) - logging.OnError(err).Fatal("create human user") + logging.OnError(err).Panic("create human user") return resp } -func (s *Tester) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user_v2.AddHumanUserResponse { - resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ +func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user_v2.AddHumanUserResponse { + resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ Org: &object.Organization_OrgId{ - OrgId: s.Organisation.ID, + OrgId: i.DefaultOrg.GetId(), }, }, Profile: &user_v2.SetHumanProfile{ @@ -233,12 +205,12 @@ func (s *Tester) CreateHumanUserWithTOTP(ctx context.Context, secret string) *us }, TotpSecret: gu.Ptr(secret), }) - logging.OnError(err).Fatal("create human user") + logging.OnError(err).Panic("create human user") return resp } -func (s *Tester) CreateOrganization(ctx context.Context, name, adminEmail string) *org.AddOrganizationResponse { - resp, err := s.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ +func (i *Instance) CreateOrganization(ctx context.Context, name, adminEmail string) *org.AddOrganizationResponse { + resp, err := i.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ Name: name, Admins: []*org.AddOrganizationRequest_Admin{ { @@ -259,12 +231,12 @@ func (s *Tester) CreateOrganization(ctx context.Context, name, adminEmail string }, }, }) - logging.OnError(err).Fatal("create org") + logging.OnError(err).Panic("create org") return resp } -func (s *Tester) DeactivateOrganization(ctx context.Context, orgID string) *mgmt.DeactivateOrgResponse { - resp, err := s.Client.Mgmt.DeactivateOrg( +func (i *Instance) DeactivateOrganization(ctx context.Context, orgID string) *mgmt.DeactivateOrgResponse { + resp, err := i.Client.Mgmt.DeactivateOrg( SetOrgID(ctx, orgID), &mgmt.DeactivateOrgRequest{}, ) @@ -281,8 +253,8 @@ func SetOrgID(ctx context.Context, orgID string) context.Context { return metadata.NewOutgoingContext(ctx, md) } -func (s *Tester) CreateOrganizationWithUserID(ctx context.Context, name, userID string) *org.AddOrganizationResponse { - resp, err := s.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ +func (i *Instance) CreateOrganizationWithUserID(ctx context.Context, name, userID string) *org.AddOrganizationResponse { + resp, err := i.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ Name: name, Admins: []*org.AddOrganizationRequest_Admin{ { @@ -296,8 +268,8 @@ func (s *Tester) CreateOrganizationWithUserID(ctx context.Context, name, userID return resp } -func (s *Tester) CreateHumanUserVerified(ctx context.Context, org, email string) *user_v2.AddHumanUserResponse { - resp, err := s.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ +func (i *Instance) CreateHumanUserVerified(ctx context.Context, org, email string) *user_v2.AddHumanUserResponse { + resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ Org: &object.Organization_OrgId{ OrgId: org, @@ -323,23 +295,23 @@ func (s *Tester) CreateHumanUserVerified(ctx context.Context, org, email string) }, }, }) - logging.OnError(err).Fatal("create human user") + logging.OnError(err).Panic("create human user") return resp } -func (s *Tester) CreateMachineUser(ctx context.Context) *mgmt.AddMachineUserResponse { - resp, err := s.Client.Mgmt.AddMachineUser(ctx, &mgmt.AddMachineUserRequest{ +func (i *Instance) CreateMachineUser(ctx context.Context) *mgmt.AddMachineUserResponse { + resp, err := i.Client.Mgmt.AddMachineUser(ctx, &mgmt.AddMachineUserRequest{ UserName: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()), Name: "Mickey", Description: "Mickey Mouse", AccessTokenType: user_pb.AccessTokenType_ACCESS_TOKEN_TYPE_BEARER, }) - logging.OnError(err).Fatal("create human user") + logging.OnError(err).Panic("create human user") return resp } -func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpID, username string) *user_v2.AddIDPLinkResponse { - resp, err := s.Client.UserV2.AddIDPLink( +func (i *Instance) CreateUserIDPlink(ctx context.Context, userID, externalID, idpID, username string) (*user_v2.AddIDPLinkResponse, error) { + return i.Client.UserV2.AddIDPLink( ctx, &user_v2.AddIDPLinkRequest{ UserId: userID, @@ -350,67 +322,65 @@ func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpI }, }, ) - logging.OnError(err).Fatal("create human user link") - return resp } -func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) { - reg, err := s.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user_v2.CreatePasskeyRegistrationLinkRequest{ +func (i *Instance) RegisterUserPasskey(ctx context.Context, userID string) { + reg, err := i.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user_v2.CreatePasskeyRegistrationLinkRequest{ UserId: userID, Medium: &user_v2.CreatePasskeyRegistrationLinkRequest_ReturnCode{}, }) - logging.OnError(err).Fatal("create user passkey") + logging.OnError(err).Panic("create user passkey") - pkr, err := s.Client.UserV2.RegisterPasskey(ctx, &user_v2.RegisterPasskeyRequest{ + pkr, err := i.Client.UserV2.RegisterPasskey(ctx, &user_v2.RegisterPasskeyRequest{ UserId: userID, Code: reg.GetCode(), - Domain: s.Config.ExternalDomain, + Domain: i.Domain, }) - logging.OnError(err).Fatal("create user passkey") - attestationResponse, err := s.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) - logging.OnError(err).Fatal("create user passkey") + logging.OnError(err).Panic("create user passkey") + attestationResponse, err := i.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) + logging.OnError(err).Panic("create user passkey") - _, err = s.Client.UserV2.VerifyPasskeyRegistration(ctx, &user_v2.VerifyPasskeyRegistrationRequest{ + _, err = i.Client.UserV2.VerifyPasskeyRegistration(ctx, &user_v2.VerifyPasskeyRegistrationRequest{ UserId: userID, PasskeyId: pkr.GetPasskeyId(), PublicKeyCredential: attestationResponse, PasskeyName: "nice name", }) - logging.OnError(err).Fatal("create user passkey") + logging.OnError(err).Panic("create user passkey") } -func (s *Tester) RegisterUserU2F(ctx context.Context, userID string) { - pkr, err := s.Client.UserV2.RegisterU2F(ctx, &user_v2.RegisterU2FRequest{ +func (i *Instance) RegisterUserU2F(ctx context.Context, userID string) { + pkr, err := i.Client.UserV2.RegisterU2F(ctx, &user_v2.RegisterU2FRequest{ UserId: userID, - Domain: s.Config.ExternalDomain, + Domain: i.Domain, }) - logging.OnError(err).Fatal("create user u2f") - attestationResponse, err := s.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) - logging.OnError(err).Fatal("create user u2f") + logging.OnError(err).Panic("create user u2f") + attestationResponse, err := i.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) + logging.OnError(err).Panic("create user u2f") - _, err = s.Client.UserV2.VerifyU2FRegistration(ctx, &user_v2.VerifyU2FRegistrationRequest{ + _, err = i.Client.UserV2.VerifyU2FRegistration(ctx, &user_v2.VerifyU2FRegistrationRequest{ UserId: userID, U2FId: pkr.GetU2FId(), PublicKeyCredential: attestationResponse, TokenName: "nice name", }) - logging.OnError(err).Fatal("create user u2f") + logging.OnError(err).Panic("create user u2f") } -func (s *Tester) SetUserPassword(ctx context.Context, userID, password string, changeRequired bool) *object.Details { - resp, err := s.Client.UserV2.SetPassword(ctx, &user_v2.SetPasswordRequest{ +func (i *Instance) SetUserPassword(ctx context.Context, userID, password string, changeRequired bool) *object.Details { + resp, err := i.Client.UserV2.SetPassword(ctx, &user_v2.SetPasswordRequest{ UserId: userID, NewPassword: &user_v2.Password{ Password: password, ChangeRequired: changeRequired, }, }) - logging.OnError(err).Fatal("set user password") + logging.OnError(err).Panic("set user password") return resp.GetDetails() } -func (s *Tester) AddGenericOAuthIDP(ctx context.Context, name string) *admin.AddGenericOAuthProviderResponse { - resp, err := s.Client.Admin.AddGenericOAuthProvider(ctx, &admin.AddGenericOAuthProviderRequest{ +func (i *Instance) AddGenericOAuthProvider(ctx context.Context, name string) *admin.AddGenericOAuthProviderResponse { + resp, err := i.Client.Admin.AddGenericOAuthProvider(ctx, &admin.AddGenericOAuthProviderRequest{ Name: name, ClientId: "clientID", ClientSecret: "clientSecret", @@ -427,136 +397,126 @@ func (s *Tester) AddGenericOAuthIDP(ctx context.Context, name string) *admin.Add AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, }, }) - logging.OnError(err).Fatal("create generic OAuth idp") - return resp -} + logging.OnError(err).Panic("create generic OAuth idp") -func (s *Tester) AddGenericOAuthProvider(t *testing.T, ctx context.Context) string { - ctx = authz.WithInstance(ctx, s.Instance) - id, _, err := s.Commands.AddInstanceGenericOAuthProvider(ctx, command.GenericOAuthProvider{ - Name: "idp", - ClientID: "clientID", - ClientSecret: "clientSecret", - AuthorizationEndpoint: "https://example.com/oauth/v2/authorize", - TokenEndpoint: "https://example.com/oauth/v2/token", - UserEndpoint: "https://api.example.com/user", - Scopes: []string{"openid", "profile", "email"}, - IDAttribute: "id", - IDPOptions: idp_rp.Options{ - IsLinkingAllowed: true, - IsCreationAllowed: true, - IsAutoCreation: true, - IsAutoUpdate: true, - }, - }) - require.NoError(t, err) - return id -} - -func (s *Tester) AddOrgGenericOAuthIDP(ctx context.Context, name string) *mgmt.AddGenericOAuthProviderResponse { - resp, err := s.Client.Mgmt.AddGenericOAuthProvider(ctx, &mgmt.AddGenericOAuthProviderRequest{ - Name: name, - ClientId: "clientID", - ClientSecret: "clientSecret", - AuthorizationEndpoint: "https://example.com/oauth/v2/authorize", - TokenEndpoint: "https://example.com/oauth/v2/token", - UserEndpoint: "https://api.example.com/user", - Scopes: []string{"openid", "profile", "email"}, - IdAttribute: "id", - ProviderOptions: &idp.Options{ - IsLinkingAllowed: true, - IsCreationAllowed: true, - IsAutoCreation: true, - IsAutoUpdate: true, - AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, - }, - }) - logging.OnError(err).Fatal("create generic OAuth idp") - return resp -} - -func (s *Tester) AddOrgGenericOAuthProvider(t *testing.T, ctx context.Context, orgID string) string { - ctx = authz.WithInstance(ctx, s.Instance) - id, _, err := s.Commands.AddOrgGenericOAuthProvider(ctx, orgID, - command.GenericOAuthProvider{ - Name: "idp", - ClientID: "clientID", - ClientSecret: "clientSecret", - AuthorizationEndpoint: "https://example.com/oauth/v2/authorize", - TokenEndpoint: "https://example.com/oauth/v2/token", - UserEndpoint: "https://api.example.com/user", - Scopes: []string{"openid", "profile", "email"}, - IDAttribute: "id", - IDPOptions: idp_rp.Options{ - IsLinkingAllowed: true, - IsCreationAllowed: true, - IsAutoCreation: true, - IsAutoUpdate: true, - }, + mustAwait(func() error { + _, err := i.Client.Admin.GetProviderByID(ctx, &admin.GetProviderByIDRequest{ + Id: resp.GetId(), }) - require.NoError(t, err) - return id + return err + }) + + return resp } -func (s *Tester) AddSAMLProvider(t *testing.T, ctx context.Context) string { - ctx = authz.WithInstance(ctx, s.Instance) - id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{ - Name: "saml-idp", - Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), - IDPOptions: idp_rp.Options{ +func (i *Instance) AddOrgGenericOAuthProvider(ctx context.Context, name string) *mgmt.AddGenericOAuthProviderResponse { + resp, err := i.Client.Mgmt.AddGenericOAuthProvider(ctx, &mgmt.AddGenericOAuthProviderRequest{ + Name: name, + ClientId: "clientID", + ClientSecret: "clientSecret", + AuthorizationEndpoint: "https://example.com/oauth/v2/authorize", + TokenEndpoint: "https://example.com/oauth/v2/token", + UserEndpoint: "https://api.example.com/user", + Scopes: []string{"openid", "profile", "email"}, + IdAttribute: "id", + ProviderOptions: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, + }, + }) + logging.OnError(err).Panic("create generic OAuth idp") + /* + mustAwait(func() error { + _, err := i.Client.Mgmt.GetProviderByID(ctx, &mgmt.GetProviderByIDRequest{ + Id: resp.GetId(), + }) + return err + }) + */ + return resp +} + +func (i *Instance) AddSAMLProvider(ctx context.Context) string { + resp, err := i.Client.Admin.AddSAMLProvider(ctx, &admin.AddSAMLProviderRequest{ + Name: "saml-idp", + Metadata: &admin.AddSAMLProviderRequest_MetadataXml{ + MetadataXml: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + }, + ProviderOptions: &idp.Options{ IsLinkingAllowed: true, IsCreationAllowed: true, IsAutoCreation: true, IsAutoUpdate: true, }, }) - require.NoError(t, err) - return id + logging.OnError(err).Panic("create saml idp") + return resp.GetId() } -func (s *Tester) AddSAMLRedirectProvider(t *testing.T, ctx context.Context, transientMappingAttributeName string) string { - ctx = authz.WithInstance(ctx, s.Instance) - id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{ - Name: "saml-idp-redirect", - Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", - Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n"), - TransientMappingAttributeName: transientMappingAttributeName, - IDPOptions: idp_rp.Options{ +func (i *Instance) AddSAMLRedirectProvider(ctx context.Context, transientMappingAttributeName string) string { + resp, err := i.Client.Admin.AddSAMLProvider(ctx, &admin.AddSAMLProviderRequest{ + Name: "saml-idp-redirect", + Binding: idp.SAMLBinding_SAML_BINDING_REDIRECT, + Metadata: &admin.AddSAMLProviderRequest_MetadataXml{ + MetadataXml: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n"), + }, + TransientMappingAttributeName: &transientMappingAttributeName, + ProviderOptions: &idp.Options{ IsLinkingAllowed: true, IsCreationAllowed: true, IsAutoCreation: true, IsAutoUpdate: true, }, }) - require.NoError(t, err) - return id + logging.OnError(err).Panic("create saml idp") + return resp.GetId() } -func (s *Tester) AddSAMLPostProvider(t *testing.T, ctx context.Context) string { - ctx = authz.WithInstance(ctx, s.Instance) - id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{ - Name: "saml-idp-post", - Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", - Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n"), - IDPOptions: idp_rp.Options{ +func (i *Instance) AddSAMLPostProvider(ctx context.Context) string { + resp, err := i.Client.Admin.AddSAMLProvider(ctx, &admin.AddSAMLProviderRequest{ + Name: "saml-idp-post", + Binding: idp.SAMLBinding_SAML_BINDING_POST, + Metadata: &admin.AddSAMLProviderRequest_MetadataXml{ + MetadataXml: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n"), + }, + ProviderOptions: &idp.Options{ IsLinkingAllowed: true, IsCreationAllowed: true, IsAutoCreation: true, IsAutoUpdate: true, }, }) - require.NoError(t, err) - return id + logging.OnError(err).Panic("create saml idp") + return resp.GetId() } -func (s *Tester) CreateIntent(t *testing.T, ctx context.Context, idpID string) string { +/* +func (s *Instance) CreateIntent(t *testing.T, ctx context.Context, idpID string) string { + resp, err := i.Client.UserV2.StartIdentityProviderIntent(ctx, &user.StartIdentityProviderIntentRequest{ + IdpId: idpID, + Content: &user.StartIdentityProviderIntentRequest_Urls{ + Urls: &user.RedirectURLs{ + SuccessUrl: "https://example.com/success", + FailureUrl: "https://example.com/failure", + }, + AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, + }, + }) + logging.OnError(err).Fatal("create generic OAuth idp") + return resp +} + +func (i *Instance) CreateIntent(t *testing.T, ctx context.Context, idpID string) string { ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance) writeModel, _, err := s.Commands.CreateIntent(ctx, idpID, "https://example.com/success", "https://example.com/failure", s.Instance.InstanceID()) require.NoError(t, err) return writeModel.AggregateID } -func (s *Tester) CreateSuccessfulOAuthIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { +func (i *Instance) CreateSuccessfulOAuthIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance) intentID := s.CreateIntent(t, ctx, idpID) writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID()) @@ -582,7 +542,7 @@ func (s *Tester) CreateSuccessfulOAuthIntent(t *testing.T, ctx context.Context, return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence } -func (s *Tester) CreateSuccessfulLDAPIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { +func (s *Instance) CreateSuccessfulLDAPIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance) intentID := s.CreateIntent(t, ctx, idpID) writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID()) @@ -610,7 +570,7 @@ func (s *Tester) CreateSuccessfulLDAPIntent(t *testing.T, ctx context.Context, i return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence } -func (s *Tester) CreateSuccessfulSAMLIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { +func (s *Instance) CreateSuccessfulSAMLIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance) intentID := s.CreateIntent(t, ctx, idpID) writeModel, err := s.Server.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID()) @@ -626,17 +586,18 @@ func (s *Tester) CreateSuccessfulSAMLIntent(t *testing.T, ctx context.Context, i require.NoError(t, err) return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence } +*/ -func (s *Tester) CreateVerifiedWebAuthNSession(t *testing.T, ctx context.Context, userID string) (id, token string, start, change time.Time) { - return s.CreateVerifiedWebAuthNSessionWithLifetime(t, ctx, userID, 0) +func (i *Instance) CreateVerifiedWebAuthNSession(t *testing.T, ctx context.Context, userID string) (id, token string, start, change time.Time) { + return i.CreateVerifiedWebAuthNSessionWithLifetime(t, ctx, userID, 0) } -func (s *Tester) CreateVerifiedWebAuthNSessionWithLifetime(t *testing.T, ctx context.Context, userID string, lifetime time.Duration) (id, token string, start, change time.Time) { +func (i *Instance) CreateVerifiedWebAuthNSessionWithLifetime(t *testing.T, ctx context.Context, userID string, lifetime time.Duration) (id, token string, start, change time.Time) { var sessionLifetime *durationpb.Duration if lifetime > 0 { sessionLifetime = durationpb.New(lifetime) } - createResp, err := s.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{ + createResp, err := i.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{ Checks: &session.Checks{ User: &session.CheckUser{ Search: &session.CheckUser_UserId{UserId: userID}, @@ -644,7 +605,7 @@ func (s *Tester) CreateVerifiedWebAuthNSessionWithLifetime(t *testing.T, ctx con }, Challenges: &session.RequestChallenges{ WebAuthN: &session.RequestChallenges_WebAuthN{ - Domain: s.Config.ExternalDomain, + Domain: i.Domain, UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED, }, }, @@ -652,10 +613,10 @@ func (s *Tester) CreateVerifiedWebAuthNSessionWithLifetime(t *testing.T, ctx con }) require.NoError(t, err) - assertion, err := s.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true) + assertion, err := i.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true) require.NoError(t, err) - updateResp, err := s.Client.SessionV2.SetSession(ctx, &session.SetSessionRequest{ + updateResp, err := i.Client.SessionV2.SetSession(ctx, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), Checks: &session.Checks{ WebAuthN: &session.CheckWebAuthN{ @@ -668,8 +629,8 @@ func (s *Tester) CreateVerifiedWebAuthNSessionWithLifetime(t *testing.T, ctx con createResp.GetDetails().GetChangeDate().AsTime(), updateResp.GetDetails().GetChangeDate().AsTime() } -func (s *Tester) CreatePasswordSession(t *testing.T, ctx context.Context, userID, password string) (id, token string, start, change time.Time) { - createResp, err := s.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{ +func (i *Instance) CreatePasswordSession(t *testing.T, ctx context.Context, userID, password string) (id, token string, start, change time.Time) { + createResp, err := i.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{ Checks: &session.Checks{ User: &session.CheckUser{ Search: &session.CheckUser_UserId{UserId: userID}, @@ -684,8 +645,8 @@ func (s *Tester) CreatePasswordSession(t *testing.T, ctx context.Context, userID createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime() } -func (s *Tester) CreateProjectUserGrant(t *testing.T, ctx context.Context, projectID, userID string) string { - resp, err := s.Client.Mgmt.AddUserGrant(ctx, &mgmt.AddUserGrantRequest{ +func (i *Instance) CreateProjectUserGrant(t *testing.T, ctx context.Context, projectID, userID string) string { + resp, err := i.Client.Mgmt.AddUserGrant(ctx, &mgmt.AddUserGrantRequest{ UserId: userID, ProjectId: projectID, }) @@ -693,16 +654,16 @@ func (s *Tester) CreateProjectUserGrant(t *testing.T, ctx context.Context, proje return resp.GetUserGrantId() } -func (s *Tester) CreateOrgMembership(t *testing.T, ctx context.Context, userID string) { - _, err := s.Client.Mgmt.AddOrgMember(ctx, &mgmt.AddOrgMemberRequest{ +func (i *Instance) CreateOrgMembership(t *testing.T, ctx context.Context, userID string) { + _, err := i.Client.Mgmt.AddOrgMember(ctx, &mgmt.AddOrgMemberRequest{ UserId: userID, Roles: []string{domain.RoleOrgOwner}, }) require.NoError(t, err) } -func (s *Tester) CreateProjectMembership(t *testing.T, ctx context.Context, projectID, userID string) { - _, err := s.Client.Mgmt.AddProjectMember(ctx, &mgmt.AddProjectMemberRequest{ +func (i *Instance) CreateProjectMembership(t *testing.T, ctx context.Context, projectID, userID string) { + _, err := i.Client.Mgmt.AddProjectMember(ctx, &mgmt.AddProjectMemberRequest{ ProjectId: projectID, UserId: userID, Roles: []string{domain.RoleProjectOwner}, @@ -710,13 +671,12 @@ func (s *Tester) CreateProjectMembership(t *testing.T, ctx context.Context, proj require.NoError(t, err) } -func (s *Tester) CreateTarget(ctx context.Context, t *testing.T, name, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse { - nameSet := fmt.Sprint(time.Now().UnixNano() + 1) - if name != "" { - nameSet = name +func (i *Instance) CreateTarget(ctx context.Context, t *testing.T, name, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse { + if name == "" { + name = gofakeit.Name() } reqTarget := &action.Target{ - Name: nameSet, + Name: name, Endpoint: endpoint, Timeout: durationpb.New(10 * time.Second), } @@ -738,20 +698,20 @@ func (s *Tester) CreateTarget(ctx context.Context, t *testing.T, name, endpoint RestAsync: &action.SetRESTAsync{}, } } - target, err := s.Client.ActionV3Alpha.CreateTarget(ctx, &action.CreateTargetRequest{Target: reqTarget}) + target, err := i.Client.ActionV3Alpha.CreateTarget(ctx, &action.CreateTargetRequest{Target: reqTarget}) require.NoError(t, err) return target } -func (s *Tester) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) { - _, err := s.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{ +func (i *Instance) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) { + _, err := i.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{ Condition: cond, }) require.NoError(t, err) } -func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse { - target, err := s.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{ +func (i *Instance) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse { + target, err := i.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{ Condition: cond, Execution: &action.Execution{ Targets: targets, @@ -761,15 +721,15 @@ func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Co return target } -func (s *Tester) CreateUserSchemaEmpty(ctx context.Context) *userschema_v3alpha.CreateUserSchemaResponse { - return s.CreateUserSchemaEmptyWithType(ctx, fmt.Sprint(time.Now().UnixNano()+1)) +func (i *Instance) CreateUserSchemaEmpty(ctx context.Context) *userschema_v3alpha.CreateUserSchemaResponse { + return i.CreateUserSchemaEmptyWithType(ctx, fmt.Sprint(time.Now().UnixNano()+1)) } -func (s *Tester) CreateUserSchema(ctx context.Context, schemaData []byte) *userschema_v3alpha.CreateUserSchemaResponse { +func (i *Instance) CreateUserSchema(ctx context.Context, schemaData []byte) *userschema_v3alpha.CreateUserSchemaResponse { userSchema := new(structpb.Struct) err := userSchema.UnmarshalJSON(schemaData) logging.OnError(err).Fatal("create userschema unmarshal") - schema, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &userschema_v3alpha.CreateUserSchemaRequest{ + schema, err := i.Client.UserSchemaV3.CreateUserSchema(ctx, &userschema_v3alpha.CreateUserSchemaRequest{ UserSchema: &userschema_v3alpha.UserSchema{ Type: fmt.Sprint(time.Now().UnixNano() + 1), DataType: &userschema_v3alpha.UserSchema_Schema{ @@ -781,7 +741,7 @@ func (s *Tester) CreateUserSchema(ctx context.Context, schemaData []byte) *users return schema } -func (s *Tester) CreateUserSchemaEmptyWithType(ctx context.Context, schemaType string) *userschema_v3alpha.CreateUserSchemaResponse { +func (i *Instance) CreateUserSchemaEmptyWithType(ctx context.Context, schemaType string) *userschema_v3alpha.CreateUserSchemaResponse { userSchema := new(structpb.Struct) err := userSchema.UnmarshalJSON([]byte(`{ "$schema": "urn:zitadel:schema:v1", @@ -789,7 +749,7 @@ func (s *Tester) CreateUserSchemaEmptyWithType(ctx context.Context, schemaType s "properties": {} }`)) logging.OnError(err).Fatal("create userschema unmarshal") - schema, err := s.Client.UserSchemaV3.CreateUserSchema(ctx, &userschema_v3alpha.CreateUserSchemaRequest{ + schema, err := i.Client.UserSchemaV3.CreateUserSchema(ctx, &userschema_v3alpha.CreateUserSchemaRequest{ UserSchema: &userschema_v3alpha.UserSchema{ Type: schemaType, DataType: &userschema_v3alpha.UserSchema_Schema{ @@ -801,11 +761,11 @@ func (s *Tester) CreateUserSchemaEmptyWithType(ctx context.Context, schemaType s return schema } -func (s *Tester) CreateSchemaUser(ctx context.Context, orgID string, schemaID string, data []byte) *user_v3alpha.CreateUserResponse { +func (i *Instance) CreateSchemaUser(ctx context.Context, orgID string, schemaID string, data []byte) *user_v3alpha.CreateUserResponse { userData := new(structpb.Struct) err := userData.UnmarshalJSON(data) logging.OnError(err).Fatal("create user unmarshal") - user, err := s.Client.UserV3Alpha.CreateUser(ctx, &user_v3alpha.CreateUserRequest{ + user, err := i.Client.UserV3Alpha.CreateUser(ctx, &user_v3alpha.CreateUserRequest{ Organization: &object_v3alpha.Organization{Property: &object_v3alpha.Organization_OrgId{OrgId: orgID}}, User: &user_v3alpha.CreateUser{ SchemaId: schemaID, @@ -815,3 +775,12 @@ func (s *Tester) CreateSchemaUser(ctx context.Context, orgID string, schemaID st logging.OnError(err).Fatal("create user") return user } + +func (i *Instance) CreateInviteCode(ctx context.Context, userID string) *user_v2.CreateInviteCodeResponse { + user, err := i.Client.UserV2.CreateInviteCode(ctx, &user_v2.CreateInviteCodeRequest{ + UserId: userID, + Verification: &user_v2.CreateInviteCodeRequest_ReturnCode{ReturnCode: &user_v2.ReturnInviteCode{}}, + }) + logging.OnError(err).Fatal("create invite code") + return user +} diff --git a/internal/integration/config.go b/internal/integration/config.go new file mode 100644 index 0000000000..5aea740752 --- /dev/null +++ b/internal/integration/config.go @@ -0,0 +1,53 @@ +package integration + +import ( + "bytes" + _ "embed" + "os/exec" + "path/filepath" + + "github.com/zitadel/logging" + "sigs.k8s.io/yaml" +) + +type Config struct { + Log *logging.Config + Hostname string + Port uint16 + Secure bool + LoginURLV2 string + LogoutURLV2 string + WebAuthNName string +} + +var ( + //go:embed config/client.yaml + clientYAML []byte +) + +var ( + tmpDir string + loadedConfig Config +) + +// TmpDir returns the absolute path to the projects's temp directory. +func TmpDir() string { + return tmpDir +} + +func init() { + cmd := exec.Command("git", "rev-parse", "--show-toplevel") + out, err := cmd.Output() + if err != nil { + panic(err) + } + tmpDir = filepath.Join(string(bytes.TrimSpace(out)), "tmp") + + if err := yaml.Unmarshal(clientYAML, &loadedConfig); err != nil { + panic(err) + } + if err := loadedConfig.Log.SetLogger(); err != nil { + panic(err) + } + SystemToken = systemUserToken() +} diff --git a/internal/integration/config/client.yaml b/internal/integration/config/client.yaml new file mode 100644 index 0000000000..43e417d4d6 --- /dev/null +++ b/internal/integration/config/client.yaml @@ -0,0 +1,10 @@ +Log: + Level: info + Formatter: + Format: text +Hostname: localhost +Port: 8080 +Secure: false +LoginURLV2: "/login?authRequest=" +LogoutURLV2: "/logout?post_logout_redirect=" +WebAuthNName: ZITADEL diff --git a/internal/integration/config/docker-compose.yaml b/internal/integration/config/docker-compose.yaml index b7f065fad7..1749b9f0ab 100644 --- a/internal/integration/config/docker-compose.yaml +++ b/internal/integration/config/docker-compose.yaml @@ -14,7 +14,7 @@ services: - PGUSER=zitadel - POSTGRES_DB=zitadel - POSTGRES_HOST_AUTH_METHOD=trust - command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all + command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c shared_buffers=1GB -c work_mem=16MB -c effective_io_concurrency=100 -c wal_level=minimal -c archive_mode=off -c max_wal_senders=0 healthcheck: test: ["CMD-SHELL", "pg_isready"] interval: '10s' diff --git a/internal/integration/config/postgres.yaml b/internal/integration/config/postgres.yaml index c75187d7fb..df1d08d3bc 100644 --- a/internal/integration/config/postgres.yaml +++ b/internal/integration/config/postgres.yaml @@ -1,10 +1,14 @@ Database: + EventPushConnRatio: 0.2 # 4 + ProjectionSpoolerConnRatio: 0.3 # 6 postgres: Host: localhost Port: 5432 Database: zitadel - MaxOpenConns: 15 - MaxIdleConns: 10 + MaxOpenConns: 20 + MaxIdleConns: 20 + MaxConnLifetime: 1h + MaxConnIdleTime: 5m User: Username: zitadel SSL: diff --git a/internal/integration/config/steps.yaml b/internal/integration/config/steps.yaml new file mode 100644 index 0000000000..fea8441e2b --- /dev/null +++ b/internal/integration/config/steps.yaml @@ -0,0 +1,13 @@ +FirstInstance: + Skip: false + PatPath: tmp/admin-pat.txt + InstanceName: ZITADEL + DefaultLanguage: en + Org: + Name: ZITADEL + Machine: + Machine: + Username: boss + Name: boss + Pat: + ExpirationDate: 2099-01-01T00:00:00Z diff --git a/internal/integration/config/zitadel.yaml b/internal/integration/config/zitadel.yaml index d58529b7a8..68e0b43f9c 100644 --- a/internal/integration/config/zitadel.yaml +++ b/internal/integration/config/zitadel.yaml @@ -1,15 +1,19 @@ Log: - Level: debug + Level: info ExternalSecure: false TLS: Enabled: false +Quotas: + Access: + Enabled: true + Telemetry: Enabled: true Endpoints: - - http://localhost:8081 + - http://localhost:8081/milestone Headers: single-value: "single-value" multi-value: @@ -27,14 +31,15 @@ LogStore: Enabled: true Projections: - HandleActiveInstances: 60s + HandleActiveInstances: 30m + RequeueEvery: 5s + TransactionDuration: 1m Customizations: NotificationsQuotas: RequeueEvery: 1s + telemetry: HandleActiveInstances: 60s - Telemetry: - RequeueEvery: 5s - HandleActiveInstances: 60s + RequeueEvery: 1s DefaultInstance: LoginPolicy: diff --git a/internal/integration/instance.go b/internal/integration/instance.go new file mode 100644 index 0000000000..c6e0b9737c --- /dev/null +++ b/internal/integration/instance.go @@ -0,0 +1,354 @@ +// Package integration provides helpers for integration testing. +package integration + +import ( + "bytes" + "context" + _ "embed" + "errors" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/zitadel/logging" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" + + http_util "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/webauthn" + "github.com/zitadel/zitadel/pkg/grpc/admin" + "github.com/zitadel/zitadel/pkg/grpc/auth" + "github.com/zitadel/zitadel/pkg/grpc/instance" + "github.com/zitadel/zitadel/pkg/grpc/management" + "github.com/zitadel/zitadel/pkg/grpc/org" + "github.com/zitadel/zitadel/pkg/grpc/system" + "github.com/zitadel/zitadel/pkg/grpc/user" + user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +// NotEmpty can be used as placeholder, when the returned values is unknown. +// It can be used in tests to assert whether a value should be empty or not. +const NotEmpty = "not empty" + +const ( + adminPATFile = "admin-pat.txt" +) + +// UserType provides constants that give +// a short explanation with the purpose +// a service user. +// This allows to pre-create users with +// different permissions and reuse them. +type UserType int + +//go:generate enumer -type UserType -transform snake -trimprefix UserType +const ( + UserTypeUnspecified UserType = iota + UserTypeIAMOwner + UserTypeOrgOwner + UserTypeLogin +) + +const ( + UserPassword = "VeryS3cret!" +) + +const ( + PortMilestoneServer = "8081" + PortQuotaServer = "8082" +) + +// User information with a Personal Access Token. +type User struct { + ID string + Username string + Token string +} + +type UserMap map[UserType]*User + +func (m UserMap) Set(typ UserType, user *User) { + m[typ] = user +} + +func (m UserMap) Get(typ UserType) *User { + return m[typ] +} + +// Host returns the primary host of zitadel, on which the first instance is served. +// http://localhost:8080 by default +func (c *Config) Host() string { + return fmt.Sprintf("%s:%d", c.Hostname, c.Port) +} + +// Instance is a Zitadel server and client with all resources available for testing. +type Instance struct { + Config Config + Domain string + Instance *instance.InstanceDetail + DefaultOrg *org.Org + Users UserMap + AdminUserID string // First human user for password login + + Client *Client + WebAuthN *webauthn.Client +} + +// GetFirstInstance returns the default instance and org information, +// with authorized machine users. +// Using the first instance is not recommended as parallel test might +// interfere with each other. +// It is recommended to use [NewInstance] instead. +func GetFirstInstance(ctx context.Context) *Instance { + i := &Instance{ + Config: loadedConfig, + Domain: loadedConfig.Hostname, + } + token := loadInstanceOwnerPAT() + i.setClient(ctx) + i.setupInstance(ctx, token) + return i +} + +// NewInstance returns a new instance that can be used for integration tests. +// The instance contains a gRPC client connected to the domain of this instance. +// The included users are the IAM_OWNER, ORG_OWNER of the default org and +// a Login client user. +// +// The instance is isolated and is safe for parallel testing. +func NewInstance(ctx context.Context) *Instance { + primaryDomain := RandString(5) + ".integration.localhost" + + ctx = WithSystemAuthorization(ctx) + resp, err := SystemClient().CreateInstance(ctx, &system.CreateInstanceRequest{ + InstanceName: "testinstance", + CustomDomain: primaryDomain, + Owner: &system.CreateInstanceRequest_Machine_{ + Machine: &system.CreateInstanceRequest_Machine{ + UserName: "owner", + Name: "owner", + PersonalAccessToken: &system.CreateInstanceRequest_PersonalAccessToken{}, + }, + }, + }) + if err != nil { + panic(err) + } + i := &Instance{ + Config: loadedConfig, + Domain: primaryDomain, + } + i.setClient(ctx) + i.awaitFirstUser(WithAuthorizationToken(ctx, resp.GetPat())) + i.setupInstance(ctx, resp.GetPat()) + return i +} + +func (i *Instance) ID() string { + return i.Instance.GetId() +} + +func (i *Instance) awaitFirstUser(ctx context.Context) { + var allErrs []error + for { + resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ + Username: proto.String("zitadel-admin@zitadel.localhost"), + Profile: &user_v2.SetHumanProfile{ + GivenName: "hodor", + FamilyName: "hodor", + NickName: proto.String("hodor"), + }, + Email: &user_v2.SetHumanEmail{ + Email: "zitadel-admin@zitadel.localhost", + Verification: &user_v2.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + PasswordType: &user_v2.AddHumanUserRequest_Password{ + Password: &user_v2.Password{ + Password: "Password1!", + ChangeRequired: false, + }, + }, + }) + if err == nil { + i.AdminUserID = resp.GetUserId() + return + } + logging.WithError(err).Debug("await first instance user") + allErrs = append(allErrs, err) + select { + case <-ctx.Done(): + panic(errors.Join(append(allErrs, ctx.Err())...)) + case <-time.After(time.Second): + continue + } + } +} + +func (i *Instance) setupInstance(ctx context.Context, token string) { + i.Users = make(UserMap) + ctx = WithAuthorizationToken(ctx, token) + i.setInstance(ctx) + i.setOrganization(ctx) + i.createMachineUserInstanceOwner(ctx, token) + i.createMachineUserOrgOwner(ctx) + i.createLoginClient(ctx) + i.createWebAuthNClient() +} + +// Host returns the primary Domain of the instance with the port. +func (i *Instance) Host() string { + return fmt.Sprintf("%s:%d", i.Domain, i.Config.Port) +} + +func loadInstanceOwnerPAT() string { + data, err := os.ReadFile(filepath.Join(tmpDir, adminPATFile)) + if err != nil { + panic(err) + } + return string(bytes.TrimSpace(data)) +} + +func (i *Instance) createMachineUserInstanceOwner(ctx context.Context, token string) { + mustAwait(func() error { + user, err := i.Client.Auth.GetMyUser(WithAuthorizationToken(ctx, token), &auth.GetMyUserRequest{}) + if err != nil { + return err + } + i.Users.Set(UserTypeIAMOwner, &User{ + ID: user.GetUser().GetId(), + Username: user.GetUser().GetUserName(), + Token: token, + }) + return nil + }) +} + +func (i *Instance) createMachineUserOrgOwner(ctx context.Context) { + _, err := i.Client.Mgmt.AddOrgMember(ctx, &management.AddOrgMemberRequest{ + UserId: i.createMachineUser(ctx, UserTypeOrgOwner), + Roles: []string{"ORG_OWNER"}, + }) + if err != nil { + panic(err) + } +} + +func (i *Instance) createLoginClient(ctx context.Context) { + i.createMachineUser(ctx, UserTypeLogin) +} + +func (i *Instance) setClient(ctx context.Context) { + client, err := newClient(ctx, i.Host()) + if err != nil { + panic(err) + } + i.Client = client +} + +func (i *Instance) setInstance(ctx context.Context) { + mustAwait(func() error { + instance, err := i.Client.Admin.GetMyInstance(ctx, &admin.GetMyInstanceRequest{}) + i.Instance = instance.GetInstance() + return err + }) +} + +func (i *Instance) setOrganization(ctx context.Context) { + mustAwait(func() error { + resp, err := i.Client.Mgmt.GetMyOrg(ctx, &management.GetMyOrgRequest{}) + i.DefaultOrg = resp.GetOrg() + return err + }) +} + +func (i *Instance) createMachineUser(ctx context.Context, userType UserType) (userID string) { + mustAwait(func() error { + username := gofakeit.Username() + userResp, err := i.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{ + UserName: username, + Name: username, + Description: userType.String(), + AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, + }) + if err != nil { + return err + } + userID = userResp.GetUserId() + patResp, err := i.Client.Mgmt.AddPersonalAccessToken(ctx, &management.AddPersonalAccessTokenRequest{ + UserId: userID, + }) + if err != nil { + return err + } + i.Users.Set(userType, &User{ + ID: userID, + Username: username, + Token: patResp.GetToken(), + }) + return nil + }) + return userID +} + +func (i *Instance) createWebAuthNClient() { + i.WebAuthN = webauthn.NewClient(i.Config.WebAuthNName, i.Domain, http_util.BuildOrigin(i.Host(), i.Config.Secure)) +} + +func (i *Instance) WithAuthorization(ctx context.Context, u UserType) context.Context { + return i.WithInstanceAuthorization(ctx, u) +} + +func (i *Instance) WithInstanceAuthorization(ctx context.Context, u UserType) context.Context { + return WithAuthorizationToken(ctx, i.Users.Get(u).Token) +} + +func (i *Instance) GetUserID(u UserType) string { + return i.Users.Get(u).ID +} + +func WithAuthorizationToken(ctx context.Context, token string) context.Context { + md, ok := metadata.FromOutgoingContext(ctx) + if !ok { + md = make(metadata.MD) + } + md.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + return metadata.NewOutgoingContext(ctx, md) +} + +func (i *Instance) BearerToken(ctx context.Context) string { + md, ok := metadata.FromOutgoingContext(ctx) + if !ok { + return "" + } + return md.Get("Authorization")[0] +} + +func (i *Instance) WithSystemAuthorizationHTTP(u UserType) map[string]string { + return map[string]string{"Authorization": fmt.Sprintf("Bearer %s", i.Users.Get(u).Token)} +} + +func await(af func() error) error { + maxTimer := time.NewTimer(15 * time.Minute) + for { + err := af() + if err == nil { + return nil + } + select { + case <-maxTimer.C: + return err + case <-time.After(time.Second): + continue + } + } +} + +func mustAwait(af func() error) { + if err := await(af); err != nil { + panic(err) + } +} diff --git a/internal/integration/integration.go b/internal/integration/integration.go deleted file mode 100644 index 20af65993b..0000000000 --- a/internal/integration/integration.go +++ /dev/null @@ -1,449 +0,0 @@ -// Package integration provides helpers for integration testing. -package integration - -import ( - "bytes" - "context" - "database/sql" - _ "embed" - "errors" - "fmt" - "io" - "net/http" - "net/http/httptest" - "os" - "reflect" - "strings" - "sync" - "time" - - "github.com/spf13/viper" - "github.com/zitadel/logging" - "github.com/zitadel/oidc/v3/pkg/client" - "github.com/zitadel/oidc/v3/pkg/oidc" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/metadata" - - "github.com/zitadel/zitadel/cmd" - "github.com/zitadel/zitadel/cmd/start" - "github.com/zitadel/zitadel/internal/api/authz" - http_util "github.com/zitadel/zitadel/internal/api/http" - z_oidc "github.com/zitadel/zitadel/internal/api/oidc" - "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" - "github.com/zitadel/zitadel/internal/net" - "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/webauthn" - "github.com/zitadel/zitadel/internal/zerrors" - "github.com/zitadel/zitadel/pkg/grpc/admin" -) - -var ( - //go:embed config/zitadel.yaml - zitadelYAML []byte - //go:embed config/cockroach.yaml - cockroachYAML []byte - //go:embed config/postgres.yaml - postgresYAML []byte - //go:embed config/system-user-key.pem - systemUserKey []byte -) - -// NotEmpty can be used as placeholder, when the returned values is unknown. -// It can be used in tests to assert whether a value should be empty or not. -const NotEmpty = "not empty" - -// UserType provides constants that give -// a short explinanation with the purpose -// a serverice user. -// This allows to pre-create users with -// different permissions and reuse them. -type UserType int - -//go:generate stringer -type=UserType -const ( - Unspecified UserType = iota - OrgOwner - Login - IAMOwner - SystemUser // SystemUser is a user with access to the system service. -) - -const ( - FirstInstanceUsersKey = "first" - UserPassword = "VeryS3cret!" -) - -const ( - PortMilestoneServer = "8081" - PortQuotaServer = "8082" -) - -// User information with a Personal Access Token. -type User struct { - *query.User - Token string -} - -type InstanceUserMap map[string]map[UserType]*User - -func (m InstanceUserMap) Set(instanceID string, typ UserType, user *User) { - if m[instanceID] == nil { - m[instanceID] = make(map[UserType]*User) - } - m[instanceID][typ] = user -} - -func (m InstanceUserMap) Get(instanceID string, typ UserType) *User { - if users, ok := m[instanceID]; ok { - return users[typ] - } - return nil -} - -// Tester is a Zitadel server and client with all resources available for testing. -type Tester struct { - *start.Server - - Instance authz.Instance - Organisation *query.Org - Users InstanceUserMap - - MilestoneChan chan []byte - milestoneServer *httptest.Server - QuotaNotificationChan chan []byte - quotaNotificationServer *httptest.Server - - Client Client - WebAuthN *webauthn.Client - wg sync.WaitGroup // used for shutdown -} - -const commandLine = `start --masterkeyFromEnv` - -func (s *Tester) Host() string { - return fmt.Sprintf("%s:%d", s.Config.ExternalDomain, s.Config.Port) -} - -func (s *Tester) createClientConn(ctx context.Context, target string) { - cc, err := grpc.DialContext(ctx, target, - grpc.WithBlock(), - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) - if err != nil { - s.Shutdown <- os.Interrupt - s.wg.Wait() - } - logging.OnError(err).Fatal("integration tester client dial") - logging.New().WithField("target", target).Info("finished dialing grpc client conn") - - s.Client = newClient(cc) - err = s.pollHealth(ctx) - logging.OnError(err).Fatal("integration tester health") -} - -// pollHealth waits until a healthy status is reported. -// TODO: remove when we make the setup blocking on all -// projections completed. -func (s *Tester) pollHealth(ctx context.Context) (err error) { - for { - err = func(ctx context.Context) error { - ctx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - - _, err := s.Client.Admin.Healthz(ctx, &admin.HealthzRequest{}) - return err - }(ctx) - if err == nil { - return nil - } - logging.WithError(err).Info("poll healthz") - - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(time.Second): - continue - } - } -} - -const ( - LoginUser = "loginClient" - MachineUserOrgOwner = "integrationOrgOwner" - MachineUserInstanceOwner = "integrationInstanceOwner" -) - -func (s *Tester) createMachineUserOrgOwner(ctx context.Context) { - var err error - - ctx, user := s.createMachineUser(ctx, MachineUserOrgOwner, OrgOwner) - _, err = s.Commands.AddOrgMember(ctx, user.ResourceOwner, user.ID, "ORG_OWNER") - target := new(zerrors.AlreadyExistsError) - if !errors.As(err, &target) { - logging.OnError(err).Fatal("add org member") - } -} - -func (s *Tester) createMachineUserInstanceOwner(ctx context.Context) { - var err error - - ctx, user := s.createMachineUser(ctx, MachineUserInstanceOwner, IAMOwner) - _, err = s.Commands.AddInstanceMember(ctx, user.ID, "IAM_OWNER") - target := new(zerrors.AlreadyExistsError) - if !errors.As(err, &target) { - logging.OnError(err).Fatal("add instance member") - } -} - -func (s *Tester) createLoginClient(ctx context.Context) { - s.createMachineUser(ctx, LoginUser, Login) -} - -func (s *Tester) createMachineUser(ctx context.Context, username string, userType UserType) (context.Context, *query.User) { - var err error - ctx = s.updateInstanceAndOrg(ctx, s.Host()) - usernameQuery, err := query.NewUserUsernameSearchQuery(username, query.TextEquals) - logging.OnError(err).Fatal("user query") - user, err := s.Queries.GetUser(ctx, true, usernameQuery) - if errors.Is(err, sql.ErrNoRows) { - _, err = s.Commands.AddMachine(ctx, &command.Machine{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: s.Organisation.ID, - }, - Username: username, - Name: username, - Description: "who cares?", - AccessTokenType: domain.OIDCTokenTypeJWT, - }) - logging.WithFields("username", username).OnError(err).Fatal("add machine user") - user, err = s.Queries.GetUser(ctx, true, usernameQuery) - } - logging.WithFields("username", username).OnError(err).Fatal("get user") - - scopes := []string{oidc.ScopeOpenID, oidc.ScopeProfile, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner} - pat := command.NewPersonalAccessToken(user.ResourceOwner, user.ID, time.Now().Add(time.Hour), scopes, domain.UserTypeMachine) - _, err = s.Commands.AddPersonalAccessToken(ctx, pat) - logging.WithFields("username", SystemUser).OnError(err).Fatal("add pat") - s.Users.Set(FirstInstanceUsersKey, userType, &User{ - User: user, - Token: pat.Token, - }) - return ctx, user -} - -func (s *Tester) WithAuthorization(ctx context.Context, u UserType) context.Context { - return s.WithInstanceAuthorization(ctx, u, FirstInstanceUsersKey) -} - -func (s *Tester) WithInstanceAuthorization(ctx context.Context, u UserType, instanceID string) context.Context { - if u == SystemUser { - s.ensureSystemUser() - } - return s.WithAuthorizationToken(ctx, s.Users.Get(instanceID, u).Token) -} - -func (s *Tester) GetUserID(u UserType) string { - if u == SystemUser { - s.ensureSystemUser() - } - return s.Users.Get(FirstInstanceUsersKey, u).ID -} - -func (s *Tester) WithAuthorizationToken(ctx context.Context, token string) context.Context { - md, ok := metadata.FromOutgoingContext(ctx) - if !ok { - md = make(metadata.MD) - } - md.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - return metadata.NewOutgoingContext(ctx, md) -} - -func (s *Tester) BearerToken(ctx context.Context) string { - md, ok := metadata.FromOutgoingContext(ctx) - if !ok { - return "" - } - return md.Get("Authorization")[0] -} - -func (s *Tester) ensureSystemUser() { - const ISSUER = "tester" - if s.Users.Get(FirstInstanceUsersKey, SystemUser) != nil { - return - } - audience := http_util.BuildOrigin(s.Host(), s.Server.Config.ExternalSecure) - signer, err := client.NewSignerFromPrivateKeyByte(systemUserKey, "") - logging.OnError(err).Fatal("system key signer") - jwt, err := client.SignedJWTProfileAssertion(ISSUER, []string{audience}, time.Hour, signer) - logging.OnError(err).Fatal("system key jwt") - s.Users.Set(FirstInstanceUsersKey, SystemUser, &User{Token: jwt}) -} - -func (s *Tester) WithSystemAuthorizationHTTP(u UserType) map[string]string { - return map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.Users.Get(FirstInstanceUsersKey, u).Token)} -} - -// Done send an interrupt signal to cleanly shutdown the server. -func (s *Tester) Done() { - err := s.Client.CC.Close() - logging.OnError(err).Error("integration tester client close") - - s.Shutdown <- os.Interrupt - s.wg.Wait() - s.milestoneServer.Close() - s.quotaNotificationServer.Close() -} - -// NewTester start a new Zitadel server by passing the default commandline. -// The server will listen on the configured port. -// The database configuration that will be used can be set by the -// INTEGRATION_DB_FLAVOR environment variable and can have the values "cockroach" -// or "postgres". Defaults to "cockroach". -// -// The default Instance and Organisation are read from the DB and system -// users are created as needed. -// -// After the server is started, a [grpc.ClientConn] will be created and -// the server is polled for it's health status. -// -// Note: the database must already be setup and initialized before -// using NewTester. See the CONTRIBUTING.md document for details. - -func NewTester(ctx context.Context, zitadelConfigYAML ...string) *Tester { - args := strings.Split(commandLine, " ") - - sc := make(chan *start.Server) - //nolint:contextcheck - cmd := cmd.New(os.Stdout, os.Stdin, args, sc) - cmd.SetArgs(args) - for _, yaml := range append([]string{string(zitadelYAML)}, zitadelConfigYAML...) { - err := viper.MergeConfig(bytes.NewBuffer([]byte(yaml))) - logging.OnError(err).Fatal() - } - var err error - flavor := os.Getenv("INTEGRATION_DB_FLAVOR") - switch flavor { - case "cockroach", "": - err = viper.MergeConfig(bytes.NewBuffer(cockroachYAML)) - case "postgres": - err = viper.MergeConfig(bytes.NewBuffer(postgresYAML)) - default: - logging.New().WithField("flavor", flavor).Fatal("unknown db flavor set in INTEGRATION_DB_FLAVOR") - } - logging.OnError(err).Fatal() - - tester := Tester{ - Users: make(InstanceUserMap), - } - tester.MilestoneChan = make(chan []byte, 100) - tester.milestoneServer, err = runMilestoneServer(ctx, tester.MilestoneChan) - logging.OnError(err).Fatal() - tester.QuotaNotificationChan = make(chan []byte, 100) - tester.quotaNotificationServer, err = runQuotaServer(ctx, tester.QuotaNotificationChan) - logging.OnError(err).Fatal() - - tester.wg.Add(1) - go func(wg *sync.WaitGroup) { - logging.OnError(cmd.Execute()).Fatal() - wg.Done() - }(&tester.wg) - - select { - case tester.Server = <-sc: - case <-ctx.Done(): - logging.OnError(ctx.Err()).Fatal("waiting for integration tester server") - } - host := tester.Host() - tester.createClientConn(ctx, host) - tester.createLoginClient(ctx) - tester.WebAuthN = webauthn.NewClient(tester.Config.WebAuthNName, tester.Config.ExternalDomain, http_util.BuildOrigin(host, tester.Config.ExternalSecure)) - tester.createMachineUserOrgOwner(ctx) - tester.createMachineUserInstanceOwner(ctx) - tester.WebAuthN = webauthn.NewClient(tester.Config.WebAuthNName, tester.Config.ExternalDomain, "https://"+tester.Host()) - return &tester -} - -func Contexts(timeout time.Duration) (ctx, errCtx context.Context, cancel context.CancelFunc) { - errCtx, cancel = context.WithCancel(context.Background()) - cancel() - ctx, cancel = context.WithTimeout(context.Background(), timeout) - return ctx, errCtx, cancel -} - -func runMilestoneServer(ctx context.Context, bodies chan []byte) (*httptest.Server, error) { - mockServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - if r.Header.Get("single-value") != "single-value" { - http.Error(w, "single-value header not set", http.StatusInternalServerError) - return - } - if reflect.DeepEqual(r.Header.Get("multi-value"), "multi-value-1,multi-value-2") { - http.Error(w, "single-value header not set", http.StatusInternalServerError) - return - } - bodies <- body - w.WriteHeader(http.StatusOK) - })) - config := net.ListenConfig() - listener, err := config.Listen(ctx, "tcp", ":"+PortMilestoneServer) - if err != nil { - return nil, err - } - mockServer.Listener = listener - mockServer.Start() - return mockServer, nil -} - -func runQuotaServer(ctx context.Context, bodies chan []byte) (*httptest.Server, error) { - mockServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - bodies <- body - w.WriteHeader(http.StatusOK) - })) - config := net.ListenConfig() - listener, err := config.Listen(ctx, "tcp", ":"+PortQuotaServer) - if err != nil { - return nil, err - } - mockServer.Listener = listener - mockServer.Start() - return mockServer, nil -} - -func (s *Tester) updateInstanceAndOrg(ctx context.Context, domain string) context.Context { - var err error - s.Instance, err = s.Queries.InstanceByHost(ctx, domain, "") - logging.OnError(err).Fatal("query instance") - ctx = authz.WithInstance(ctx, s.Instance) - - s.Organisation, err = s.Queries.OrgByID(ctx, true, s.Instance.DefaultOrganisationID()) - logging.OnError(err).Fatal("query organisation") - return ctx -} - -func await(af func() error) error { - maxTimer := time.NewTimer(15 * time.Minute) - for { - err := af() - if err == nil { - return nil - } - select { - case <-maxTimer.C: - return err - case <-time.After(time.Second / 10): - continue - } - } -} diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go deleted file mode 100644 index 416602ea25..0000000000 --- a/internal/integration/integration_test.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build integration - -package integration - -import ( - "testing" - "time" -) - -func TestNewTester(t *testing.T) { - ctx, _, cancel := Contexts(time.Hour) - defer cancel() - - s := NewTester(ctx) - defer s.Done() -} diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index 1d15d25f29..9f394e2c2c 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -24,11 +24,11 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/user" ) -func (s *Tester) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, appType app.OIDCAppType, authMethod app.OIDCAuthMethodType, devMode bool, grantTypes ...app.OIDCGrantType) (*management.AddOIDCAppResponse, error) { +func (i *Instance) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, appType app.OIDCAppType, authMethod app.OIDCAuthMethodType, devMode bool, grantTypes ...app.OIDCGrantType) (*management.AddOIDCAppResponse, error) { if len(grantTypes) == 0 { grantTypes = []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN} } - resp, err := s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{ + resp, err := i.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{ ProjectId: projectID, Name: fmt.Sprintf("app-%d", time.Now().UnixNano()), RedirectUris: []string{redirectURI}, @@ -51,7 +51,7 @@ func (s *Tester) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedire return nil, err } return resp, await(func() error { - _, err := s.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{ + _, err := i.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{ ProjectId: projectID, AppId: resp.GetAppId(), }) @@ -59,20 +59,20 @@ func (s *Tester) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedire }) } -func (s *Tester) CreateOIDCNativeClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, devMode bool) (*management.AddOIDCAppResponse, error) { - return s.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, devMode) +func (i *Instance) CreateOIDCNativeClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, devMode bool) (*management.AddOIDCAppResponse, error) { + return i.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, devMode) } -func (s *Tester) CreateOIDCWebClientBasic(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) { - return s.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_WEB, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, false) +func (i *Instance) CreateOIDCWebClientBasic(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) { + return i.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_WEB, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, false) } -func (s *Tester) CreateOIDCWebClientJWT(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, grantTypes ...app.OIDCGrantType) (client *management.AddOIDCAppResponse, keyData []byte, err error) { - client, err = s.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_WEB, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, false, grantTypes...) +func (i *Instance) CreateOIDCWebClientJWT(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, grantTypes ...app.OIDCGrantType) (client *management.AddOIDCAppResponse, keyData []byte, err error) { + client, err = i.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_WEB, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, false, grantTypes...) if err != nil { return nil, nil, err } - key, err := s.Client.Mgmt.AddAppKey(ctx, &management.AddAppKeyRequest{ + key, err := i.Client.Mgmt.AddAppKey(ctx, &management.AddAppKeyRequest{ ProjectId: projectID, AppId: client.GetAppId(), Type: authn.KeyType_KEY_TYPE_JSON, @@ -81,15 +81,23 @@ func (s *Tester) CreateOIDCWebClientJWT(ctx context.Context, redirectURI, logout if err != nil { return nil, nil, err } + mustAwait(func() error { + _, err := i.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{ + ProjectId: projectID, + AppId: client.GetAppId(), + }) + return err + }) + return client, key.GetKeyDetails(), nil } -func (s *Tester) CreateOIDCInactivateClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) { - client, err := s.CreateOIDCNativeClient(ctx, redirectURI, logoutRedirectURI, projectID, false) +func (i *Instance) CreateOIDCInactivateClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) { + client, err := i.CreateOIDCNativeClient(ctx, redirectURI, logoutRedirectURI, projectID, false) if err != nil { return nil, err } - _, err = s.Client.Mgmt.DeactivateApp(ctx, &management.DeactivateAppRequest{ + _, err = i.Client.Mgmt.DeactivateApp(ctx, &management.DeactivateAppRequest{ ProjectId: projectID, AppId: client.GetAppId(), }) @@ -99,14 +107,14 @@ func (s *Tester) CreateOIDCInactivateClient(ctx context.Context, redirectURI, lo return client, err } -func (s *Tester) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI string) (*management.AddOIDCAppResponse, error) { - project, err := s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ +func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI string) (*management.AddOIDCAppResponse, error) { + project, err := i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), }) if err != nil { return nil, err } - resp, err := s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{ + resp, err := i.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{ ProjectId: project.GetId(), Name: fmt.Sprintf("app-%d", time.Now().UnixNano()), RedirectUris: []string{redirectURI}, @@ -129,7 +137,7 @@ func (s *Tester) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI s return nil, err } return resp, await(func() error { - _, err := s.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{ + _, err := i.Client.Mgmt.GetAppByID(ctx, &management.GetAppByIDRequest{ ProjectId: project.GetId(), AppId: resp.GetAppId(), }) @@ -137,32 +145,32 @@ func (s *Tester) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI s }) } -func (s *Tester) CreateOIDCTokenExchangeClient(ctx context.Context) (client *management.AddOIDCAppResponse, keyData []byte, err error) { - project, err := s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ +func (i *Instance) CreateOIDCTokenExchangeClient(ctx context.Context) (client *management.AddOIDCAppResponse, keyData []byte, err error) { + project, err := i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), }) if err != nil { return nil, nil, err } - return s.CreateOIDCWebClientJWT(ctx, "", "", project.GetId(), app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE, app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN) + return i.CreateOIDCWebClientJWT(ctx, "", "", project.GetId(), app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE, app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN) } -func (s *Tester) CreateProject(ctx context.Context) (*management.AddProjectResponse, error) { - return s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ +func (i *Instance) CreateProject(ctx context.Context) (*management.AddProjectResponse, error) { + return i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), }) } -func (s *Tester) CreateAPIClientJWT(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) { - return s.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{ +func (i *Instance) CreateAPIClientJWT(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) { + return i.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{ ProjectId: projectID, Name: fmt.Sprintf("api-%d", time.Now().UnixNano()), AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, }) } -func (s *Tester) CreateAPIClientBasic(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) { - return s.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{ +func (i *Instance) CreateAPIClientBasic(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) { + return i.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{ ProjectId: projectID, Name: fmt.Sprintf("api-%d", time.Now().UnixNano()), AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, @@ -171,36 +179,36 @@ func (s *Tester) CreateAPIClientBasic(ctx context.Context, projectID string) (*m const CodeVerifier = "codeVerifier" -func (s *Tester) CreateOIDCAuthRequest(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { - return s.CreateOIDCAuthRequestWithDomain(ctx, s.Config.ExternalDomain, clientID, loginClient, redirectURI, scope...) +func (i *Instance) CreateOIDCAuthRequest(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { + return i.CreateOIDCAuthRequestWithDomain(ctx, i.Domain, clientID, loginClient, redirectURI, scope...) } -func (s *Tester) CreateOIDCAuthRequestWithDomain(ctx context.Context, domain, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { - provider, err := s.CreateRelyingPartyForDomain(ctx, domain, clientID, redirectURI, scope...) +func (i *Instance) CreateOIDCAuthRequestWithDomain(ctx context.Context, domain, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { + provider, err := i.CreateRelyingPartyForDomain(ctx, domain, clientID, redirectURI, scope...) if err != nil { - return "", err + return "", fmt.Errorf("create relying party: %w", err) } codeChallenge := oidc.NewSHACodeChallenge(CodeVerifier) authURL := rp.AuthURL("state", provider, rp.WithCodeChallenge(codeChallenge)) req, err := GetRequest(authURL, map[string]string{oidc_internal.LoginClientHeader: loginClient}) if err != nil { - return "", err + return "", fmt.Errorf("get request: %w", err) } loc, err := CheckRedirect(req) if err != nil { - return "", err + return "", fmt.Errorf("check redirect: %w", err) } - prefixWithHost := provider.Issuer() + s.Config.OIDC.DefaultLoginURLV2 + prefixWithHost := provider.Issuer() + i.Config.LoginURLV2 if !strings.HasPrefix(loc.String(), prefixWithHost) { return "", fmt.Errorf("login location has not prefix %s, but is %s", prefixWithHost, loc.String()) } return strings.TrimPrefix(loc.String(), prefixWithHost), nil } -func (s *Tester) CreateOIDCAuthRequestImplicit(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { - provider, err := s.CreateRelyingParty(ctx, clientID, redirectURI, scope...) +func (i *Instance) CreateOIDCAuthRequestImplicit(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { + provider, err := i.CreateRelyingParty(ctx, clientID, redirectURI, scope...) if err != nil { return "", err } @@ -224,48 +232,49 @@ func (s *Tester) CreateOIDCAuthRequestImplicit(ctx context.Context, clientID, lo return "", err } - prefixWithHost := provider.Issuer() + s.Config.OIDC.DefaultLoginURLV2 + prefixWithHost := provider.Issuer() + i.Config.LoginURLV2 if !strings.HasPrefix(loc.String(), prefixWithHost) { return "", fmt.Errorf("login location has not prefix %s, but is %s", prefixWithHost, loc.String()) } return strings.TrimPrefix(loc.String(), prefixWithHost), nil } -func (s *Tester) OIDCIssuer() string { - return http_util.BuildHTTP(s.Config.ExternalDomain, s.Config.Port, s.Config.ExternalSecure) +func (i *Instance) OIDCIssuer() string { + return http_util.BuildHTTP(i.Domain, i.Config.Port, i.Config.Secure) } -func (s *Tester) CreateRelyingParty(ctx context.Context, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) { - return s.CreateRelyingPartyForDomain(ctx, s.Config.ExternalDomain, clientID, redirectURI, scope...) +func (i *Instance) CreateRelyingParty(ctx context.Context, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) { + return i.CreateRelyingPartyForDomain(ctx, i.Domain, clientID, redirectURI, scope...) } -func (s *Tester) CreateRelyingPartyForDomain(ctx context.Context, domain, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) { +func (i *Instance) CreateRelyingPartyForDomain(ctx context.Context, domain, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) { if len(scope) == 0 { scope = []string{oidc.ScopeOpenID} } - loginClient := &http.Client{Transport: &loginRoundTripper{http.DefaultTransport}} - return rp.NewRelyingPartyOIDC(ctx, http_util.BuildHTTP(domain, s.Config.Port, s.Config.ExternalSecure), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient)) + loginClient := &http.Client{Transport: &loginRoundTripper{http.DefaultTransport, i.Users.Get(UserTypeLogin).Username}} + return rp.NewRelyingPartyOIDC(ctx, http_util.BuildHTTP(domain, i.Config.Port, i.Config.Secure), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient)) } type loginRoundTripper struct { http.RoundTripper + loginUsername string } func (c *loginRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - req.Header.Set(oidc_internal.LoginClientHeader, LoginUser) + req.Header.Set(oidc_internal.LoginClientHeader, c.loginUsername) return c.RoundTripper.RoundTrip(req) } -func (s *Tester) CreateResourceServerJWTProfile(ctx context.Context, keyFileData []byte) (rs.ResourceServer, error) { +func (i *Instance) CreateResourceServerJWTProfile(ctx context.Context, keyFileData []byte) (rs.ResourceServer, error) { keyFile, err := client.ConfigFromKeyFileData(keyFileData) if err != nil { return nil, err } - return rs.NewResourceServerJWTProfile(ctx, s.OIDCIssuer(), keyFile.ClientID, keyFile.KeyID, []byte(keyFile.Key)) + return rs.NewResourceServerJWTProfile(ctx, i.OIDCIssuer(), keyFile.ClientID, keyFile.KeyID, []byte(keyFile.Key)) } -func (s *Tester) CreateResourceServerClientCredentials(ctx context.Context, clientID, clientSecret string) (rs.ResourceServer, error) { - return rs.NewResourceServerClientCredentials(ctx, s.OIDCIssuer(), clientID, clientSecret) +func (i *Instance) CreateResourceServerClientCredentials(ctx context.Context, clientID, clientSecret string) (rs.ResourceServer, error) { + return rs.NewResourceServerClientCredentials(ctx, i.OIDCIssuer(), clientID, clientSecret) } func GetRequest(url string, headers map[string]string) (*http.Request, error) { @@ -313,9 +322,9 @@ func CheckRedirect(req *http.Request) (*url.URL, error) { return resp.Location() } -func (s *Tester) CreateOIDCCredentialsClient(ctx context.Context) (machine *management.AddMachineUserResponse, name, clientID, clientSecret string, err error) { +func (i *Instance) CreateOIDCCredentialsClient(ctx context.Context) (machine *management.AddMachineUserResponse, name, clientID, clientSecret string, err error) { name = gofakeit.Username() - machine, err = s.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{ + machine, err = i.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{ Name: name, UserName: name, AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, @@ -323,7 +332,7 @@ func (s *Tester) CreateOIDCCredentialsClient(ctx context.Context) (machine *mana if err != nil { return nil, "", "", "", err } - secret, err := s.Client.Mgmt.GenerateMachineSecret(ctx, &management.GenerateMachineSecretRequest{ + secret, err := i.Client.Mgmt.GenerateMachineSecret(ctx, &management.GenerateMachineSecretRequest{ UserId: machine.GetUserId(), }) if err != nil { @@ -332,9 +341,9 @@ func (s *Tester) CreateOIDCCredentialsClient(ctx context.Context) (machine *mana return machine, name, secret.GetClientId(), secret.GetClientSecret(), nil } -func (s *Tester) CreateOIDCJWTProfileClient(ctx context.Context) (machine *management.AddMachineUserResponse, name string, keyData []byte, err error) { +func (i *Instance) CreateOIDCJWTProfileClient(ctx context.Context) (machine *management.AddMachineUserResponse, name string, keyData []byte, err error) { name = gofakeit.Username() - machine, err = s.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{ + machine, err = i.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{ Name: name, UserName: name, AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, @@ -342,7 +351,7 @@ func (s *Tester) CreateOIDCJWTProfileClient(ctx context.Context) (machine *manag if err != nil { return nil, "", nil, err } - keyResp, err := s.Client.Mgmt.AddMachineKey(ctx, &management.AddMachineKeyRequest{ + keyResp, err := i.Client.Mgmt.AddMachineKey(ctx, &management.AddMachineKeyRequest{ UserId: machine.GetUserId(), Type: authn.KeyType_KEY_TYPE_JSON, ExpirationDate: timestamppb.New(time.Now().Add(time.Hour)), diff --git a/internal/integration/sink/channel.go b/internal/integration/sink/channel.go new file mode 100644 index 0000000000..e25ae4d70c --- /dev/null +++ b/internal/integration/sink/channel.go @@ -0,0 +1,9 @@ +package sink + +//go:generate enumer -type Channel -trimprefix Channel -transform snake +type Channel int + +const ( + ChannelMilestone Channel = iota + ChannelQuota +) diff --git a/internal/integration/sink/channel_enumer.go b/internal/integration/sink/channel_enumer.go new file mode 100644 index 0000000000..85792ffec7 --- /dev/null +++ b/internal/integration/sink/channel_enumer.go @@ -0,0 +1,78 @@ +// Code generated by "enumer -type Channel -trimprefix Channel -transform snake"; DO NOT EDIT. + +package sink + +import ( + "fmt" + "strings" +) + +const _ChannelName = "milestonequota" + +var _ChannelIndex = [...]uint8{0, 9, 14} + +const _ChannelLowerName = "milestonequota" + +func (i Channel) String() string { + if i < 0 || i >= Channel(len(_ChannelIndex)-1) { + return fmt.Sprintf("Channel(%d)", i) + } + return _ChannelName[_ChannelIndex[i]:_ChannelIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _ChannelNoOp() { + var x [1]struct{} + _ = x[ChannelMilestone-(0)] + _ = x[ChannelQuota-(1)] +} + +var _ChannelValues = []Channel{ChannelMilestone, ChannelQuota} + +var _ChannelNameToValueMap = map[string]Channel{ + _ChannelName[0:9]: ChannelMilestone, + _ChannelLowerName[0:9]: ChannelMilestone, + _ChannelName[9:14]: ChannelQuota, + _ChannelLowerName[9:14]: ChannelQuota, +} + +var _ChannelNames = []string{ + _ChannelName[0:9], + _ChannelName[9:14], +} + +// ChannelString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func ChannelString(s string) (Channel, error) { + if val, ok := _ChannelNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _ChannelNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to Channel values", s) +} + +// ChannelValues returns all values of the enum +func ChannelValues() []Channel { + return _ChannelValues +} + +// ChannelStrings returns a slice of all String values of the enum +func ChannelStrings() []string { + strs := make([]string, len(_ChannelNames)) + copy(strs, _ChannelNames) + return strs +} + +// IsAChannel returns "true" if the value is listed in the enum definition. "false" otherwise +func (i Channel) IsAChannel() bool { + for _, v := range _ChannelValues { + if i == v { + return true + } + } + return false +} diff --git a/internal/integration/sink/server.go b/internal/integration/sink/server.go new file mode 100644 index 0000000000..959353ae5f --- /dev/null +++ b/internal/integration/sink/server.go @@ -0,0 +1,167 @@ +//go:build integration + +package sink + +import ( + "errors" + "io" + "net/http" + "net/url" + "path" + "sync" + "sync/atomic" + + "github.com/go-chi/chi/v5" + "github.com/gorilla/websocket" + "github.com/sirupsen/logrus" + "github.com/zitadel/logging" +) + +const ( + port = "8081" + listenAddr = "127.0.0.1:" + port + host = "localhost:" + port +) + +// CallURL returns the full URL to the handler of a [Channel]. +func CallURL(ch Channel) string { + u := url.URL{ + Scheme: "http", + Host: host, + Path: rootPath(ch), + } + return u.String() +} + +// StartServer starts a simple HTTP server on localhost:8081 +// ZITADEL can use the server to send HTTP requests which can be +// used to validate tests through [Subscribe]rs. +// For each [Channel] a route is registered on http://localhost:8081/. +// The route must be used to send the HTTP request to be validated. +// [CallURL] can be used to obtain the full URL for a given Channel. +// +// This function is only active when the `integration` build tag is enabled +func StartServer() (close func()) { + router := chi.NewRouter() + for _, ch := range ChannelValues() { + fwd := &forwarder{ + channelID: ch, + subscribers: make(map[int64]chan<- *Request), + } + router.HandleFunc(rootPath(ch), fwd.receiveHandler) + router.HandleFunc(subscribePath(ch), fwd.subscriptionHandler) + } + s := &http.Server{ + Addr: listenAddr, + Handler: router, + } + + logging.WithFields("listen_addr", listenAddr).Warn("!!!! A sink server is started which may expose sensitive data on a public endpoint. Make sure the `integration` build tag is disabled for production builds. !!!!") + go func() { + err := s.ListenAndServe() + if !errors.Is(err, http.ErrServerClosed) { + logging.WithError(err).Fatal("sink server") + } + }() + return func() { + logging.OnError(s.Close()).Error("sink server") + } +} + +func rootPath(c Channel) string { + return path.Join("/", c.String()) +} + +func subscribePath(c Channel) string { + return path.Join("/", c.String(), "subscribe") +} + +// forwarder handles incoming HTTP requests from ZITADEL and +// forwards them to all subscribed web sockets. +type forwarder struct { + channelID Channel + id atomic.Int64 + mtx sync.RWMutex + subscribers map[int64]chan<- *Request + upgrader websocket.Upgrader +} + +// receiveHandler receives a simple HTTP for a single [Channel] +// and forwards them on all active subscribers of that Channel. +func (c *forwarder) receiveHandler(w http.ResponseWriter, r *http.Request) { + req := &Request{ + Header: r.Header.Clone(), + } + var err error + req.Body, err = io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + c.mtx.RLock() + for _, reqChan := range c.subscribers { + reqChan <- req + } + c.mtx.RUnlock() + w.WriteHeader(http.StatusOK) +} + +// subscriptionHandler upgrades HTTP request to a websocket connection for subscribers. +// All received HTTP requests on a subscriber's channel are send on the websocket to the client. +func (c *forwarder) subscriptionHandler(w http.ResponseWriter, r *http.Request) { + ws, err := c.upgrader.Upgrade(w, r, nil) + logging.OnError(err).Error("websocket upgrade") + if err != nil { + return + } + done := readLoop(ws) + + id := c.id.Add(1) + reqChannel := make(chan *Request, 100) + + c.mtx.Lock() + c.subscribers[id] = reqChannel + c.mtx.Unlock() + + logging.WithFields("id", id, "channel", c.channelID).Info("websocket opened") + + defer func() { + c.mtx.Lock() + delete(c.subscribers, id) + c.mtx.Unlock() + + ws.Close() + close(reqChannel) + }() + + for { + select { + case err := <-done: + logging.WithError(err).WithFields(logrus.Fields{"id": id, "channel": c.channelID}).Info("websocket closed") + return + case req := <-reqChannel: + if err := ws.WriteJSON(req); err != nil { + logging.WithError(err).WithFields(logrus.Fields{"id": id, "channel": c.channelID}).Error("websocket write json") + return + } + } + } +} + +// readLoop makes sure we can receive close messages +func readLoop(ws *websocket.Conn) (done chan error) { + done = make(chan error, 1) + + go func(done chan<- error) { + for { + _, _, err := ws.NextReader() + if err != nil { + done <- err + break + } + } + close(done) + }(done) + + return done +} diff --git a/internal/integration/sink/sink.go b/internal/integration/sink/sink.go new file mode 100644 index 0000000000..fa339e4763 --- /dev/null +++ b/internal/integration/sink/sink.go @@ -0,0 +1,4 @@ +// Package sink provides a simple HTTP server where Zitadel can send HTTP based messages, +// which are then possible to be observed using observers on websockets. +// The contents of this package become available when the `integration` build tag is enabled. +package sink diff --git a/internal/integration/sink/stub.go b/internal/integration/sink/stub.go new file mode 100644 index 0000000000..01d1047f34 --- /dev/null +++ b/internal/integration/sink/stub.go @@ -0,0 +1,9 @@ +//go:build !integration + +package sink + +// StartServer and its returned close function are a no-op +// when the `integration` build tag is disabled. +func StartServer() (close func()) { + return func() {} +} diff --git a/internal/integration/sink/subscription.go b/internal/integration/sink/subscription.go new file mode 100644 index 0000000000..7d5ca36e91 --- /dev/null +++ b/internal/integration/sink/subscription.go @@ -0,0 +1,90 @@ +//go:build integration + +package sink + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "sync/atomic" + + "github.com/gorilla/websocket" + "github.com/zitadel/logging" +) + +// Request is a message forwarded from the handler to [Subscription]s. +type Request struct { + Header http.Header + Body json.RawMessage +} + +// Subscription is a websocket client to which [Request]s are forwarded by the server. +type Subscription struct { + conn *websocket.Conn + closed atomic.Bool + reqChannel chan *Request +} + +// Subscribe to a channel. +// The subscription forwards all requests it received on the channel's +// handler, after Subscribe has returned. +// Multiple subscription may be active on a single channel. +// Each request is always forwarded to each Subscription. +// Close must be called to cleanup up the Subscription's channel and go routine. +func Subscribe(ctx context.Context, ch Channel) *Subscription { + u := url.URL{ + Scheme: "ws", + Host: listenAddr, + Path: subscribePath(ch), + } + conn, resp, err := websocket.DefaultDialer.DialContext(ctx, u.String(), nil) + if err != nil { + if resp != nil { + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + err = fmt.Errorf("subscribe: %w, status: %s, body: %s", err, resp.Status, body) + } + panic(err) + } + + sub := &Subscription{ + conn: conn, + reqChannel: make(chan *Request, 10), + } + go sub.readToChan() + return sub +} + +func (s *Subscription) readToChan() { + for { + if s.closed.Load() { + break + } + req := new(Request) + if err := s.conn.ReadJSON(req); err != nil { + opErr := new(net.OpError) + if errors.As(err, &opErr) { + break + } + logging.WithError(err).Error("subscription read") + break + } + s.reqChannel <- req + } + close(s.reqChannel) +} + +// Recv returns the channel over which [Request]s are send. +func (s *Subscription) Recv() <-chan *Request { + return s.reqChannel +} + +func (s *Subscription) Close() error { + s.closed.Store(true) + return s.conn.Close() +} diff --git a/internal/integration/system.go b/internal/integration/system.go new file mode 100644 index 0000000000..a9673a40ae --- /dev/null +++ b/internal/integration/system.go @@ -0,0 +1,59 @@ +package integration + +import ( + "context" + _ "embed" + "sync" + "time" + + "github.com/zitadel/oidc/v3/pkg/client" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + http_util "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/pkg/grpc/system" +) + +var ( + //go:embed config/system-user-key.pem + systemUserKey []byte +) + +var ( + // SystemClient creates a system connection once and reuses it on every use. + // Each client call automatically gets the authorization context for the system user. + SystemClient = sync.OnceValue[system.SystemServiceClient](systemClient) + SystemToken string +) + +func systemClient() system.SystemServiceClient { + cc, err := grpc.NewClient(loadedConfig.Host(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithChainUnaryInterceptor(func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + ctx = WithSystemAuthorization(ctx) + return invoker(ctx, method, req, reply, cc, opts...) + }), + ) + if err != nil { + panic(err) + } + return system.NewSystemServiceClient(cc) +} + +func systemUserToken() string { + const ISSUER = "tester" + audience := http_util.BuildOrigin(loadedConfig.Host(), loadedConfig.Secure) + signer, err := client.NewSignerFromPrivateKeyByte(systemUserKey, "") + if err != nil { + panic(err) + } + token, err := client.SignedJWTProfileAssertion(ISSUER, []string{audience}, time.Hour, signer) + if err != nil { + panic(err) + } + return token +} + +func WithSystemAuthorization(ctx context.Context) context.Context { + return WithAuthorizationToken(ctx, SystemToken) +} diff --git a/internal/integration/user.go b/internal/integration/user.go index 60a6e41318..6eb26fa5a7 100644 --- a/internal/integration/user.go +++ b/internal/integration/user.go @@ -11,10 +11,10 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/management" ) -func (s *Tester) CreateMachineUserPATWithMembership(ctx context.Context, roles ...string) (id, pat string, err error) { - user := s.CreateMachineUser(ctx) +func (i *Instance) CreateMachineUserPATWithMembership(ctx context.Context, roles ...string) (id, pat string, err error) { + user := i.CreateMachineUser(ctx) - patResp, err := s.Client.Mgmt.AddPersonalAccessToken(ctx, &management.AddPersonalAccessTokenRequest{ + patResp, err := i.Client.Mgmt.AddPersonalAccessToken(ctx, &management.AddPersonalAccessTokenRequest{ UserId: user.GetUserId(), ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour)), }) @@ -35,7 +35,7 @@ func (s *Tester) CreateMachineUserPATWithMembership(ctx context.Context, roles . } if len(orgRoles) > 0 { - _, err := s.Client.Mgmt.AddOrgMember(ctx, &management.AddOrgMemberRequest{ + _, err := i.Client.Mgmt.AddOrgMember(ctx, &management.AddOrgMemberRequest{ UserId: user.GetUserId(), Roles: orgRoles, }) @@ -44,7 +44,7 @@ func (s *Tester) CreateMachineUserPATWithMembership(ctx context.Context, roles . } } if len(iamRoles) > 0 { - _, err := s.Client.Admin.AddIAMMember(ctx, &admin.AddIAMMemberRequest{ + _, err := i.Client.Admin.AddIAMMember(ctx, &admin.AddIAMMemberRequest{ UserId: user.GetUserId(), Roles: iamRoles, }) diff --git a/internal/integration/usertype_enumer.go b/internal/integration/usertype_enumer.go new file mode 100644 index 0000000000..66d49ced4d --- /dev/null +++ b/internal/integration/usertype_enumer.go @@ -0,0 +1,86 @@ +// Code generated by "enumer -type UserType -transform snake -trimprefix UserType"; DO NOT EDIT. + +package integration + +import ( + "fmt" + "strings" +) + +const _UserTypeName = "unspecifiediam_ownerorg_ownerlogin" + +var _UserTypeIndex = [...]uint8{0, 11, 20, 29, 34} + +const _UserTypeLowerName = "unspecifiediam_ownerorg_ownerlogin" + +func (i UserType) String() string { + if i < 0 || i >= UserType(len(_UserTypeIndex)-1) { + return fmt.Sprintf("UserType(%d)", i) + } + return _UserTypeName[_UserTypeIndex[i]:_UserTypeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _UserTypeNoOp() { + var x [1]struct{} + _ = x[UserTypeUnspecified-(0)] + _ = x[UserTypeIAMOwner-(1)] + _ = x[UserTypeOrgOwner-(2)] + _ = x[UserTypeLogin-(3)] +} + +var _UserTypeValues = []UserType{UserTypeUnspecified, UserTypeIAMOwner, UserTypeOrgOwner, UserTypeLogin} + +var _UserTypeNameToValueMap = map[string]UserType{ + _UserTypeName[0:11]: UserTypeUnspecified, + _UserTypeLowerName[0:11]: UserTypeUnspecified, + _UserTypeName[11:20]: UserTypeIAMOwner, + _UserTypeLowerName[11:20]: UserTypeIAMOwner, + _UserTypeName[20:29]: UserTypeOrgOwner, + _UserTypeLowerName[20:29]: UserTypeOrgOwner, + _UserTypeName[29:34]: UserTypeLogin, + _UserTypeLowerName[29:34]: UserTypeLogin, +} + +var _UserTypeNames = []string{ + _UserTypeName[0:11], + _UserTypeName[11:20], + _UserTypeName[20:29], + _UserTypeName[29:34], +} + +// UserTypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func UserTypeString(s string) (UserType, error) { + if val, ok := _UserTypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _UserTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to UserType values", s) +} + +// UserTypeValues returns all values of the enum +func UserTypeValues() []UserType { + return _UserTypeValues +} + +// UserTypeStrings returns a slice of all String values of the enum +func UserTypeStrings() []string { + strs := make([]string, len(_UserTypeNames)) + copy(strs, _UserTypeNames) + return strs +} + +// IsAUserType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i UserType) IsAUserType() bool { + for _, v := range _UserTypeValues { + if i == v { + return true + } + } + return false +} diff --git a/internal/integration/usertype_string.go b/internal/integration/usertype_string.go deleted file mode 100644 index 6477630986..0000000000 --- a/internal/integration/usertype_string.go +++ /dev/null @@ -1,27 +0,0 @@ -// Code generated by "stringer -type=UserType"; DO NOT EDIT. - -package integration - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[Unspecified-0] - _ = x[OrgOwner-1] - _ = x[Login-2] - _ = x[IAMOwner-3] - _ = x[SystemUser-4] -} - -const _UserType_name = "UnspecifiedOrgOwnerLoginIAMOwnerSystemUser" - -var _UserType_index = [...]uint8{0, 11, 19, 24, 32, 42} - -func (i UserType) String() string { - if i < 0 || i >= UserType(len(_UserType_index)-1) { - return "UserType(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _UserType_name[_UserType_index[i]:_UserType_index[i+1]] -} diff --git a/internal/notification/channels.go b/internal/notification/channels.go index 68ad673472..c70eaecbcc 100644 --- a/internal/notification/channels.go +++ b/internal/notification/channels.go @@ -5,8 +5,8 @@ import ( "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/notification/channels/smtp" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" + "github.com/zitadel/zitadel/internal/notification/channels/email" + "github.com/zitadel/zitadel/internal/notification/channels/sms" "github.com/zitadel/zitadel/internal/notification/channels/webhook" "github.com/zitadel/zitadel/internal/notification/handlers" "github.com/zitadel/zitadel/internal/notification/senders" @@ -62,36 +62,36 @@ func registerCounter(counter, desc string) { logging.WithFields("metric", counter).OnError(err).Panic("unable to register counter") } -func (c *channels) Email(ctx context.Context) (*senders.Chain, *smtp.Config, error) { - smtpCfg, err := c.q.GetSMTPConfig(ctx) +func (c *channels) Email(ctx context.Context) (*senders.Chain, *email.Config, error) { + emailCfg, err := c.q.GetActiveEmailConfig(ctx) if err != nil { return nil, nil, err } chain, err := senders.EmailChannels( ctx, - smtpCfg, + emailCfg, c.q.GetFileSystemProvider, c.q.GetLogProvider, c.counters.success.email, c.counters.failed.email, ) - return chain, smtpCfg, err + return chain, emailCfg, err } -func (c *channels) SMS(ctx context.Context) (*senders.Chain, *twilio.Config, error) { - twilioCfg, err := c.q.GetTwilioConfig(ctx) +func (c *channels) SMS(ctx context.Context) (*senders.Chain, *sms.Config, error) { + smsCfg, err := c.q.GetActiveSMSConfig(ctx) if err != nil { return nil, nil, err } chain, err := senders.SMSChannels( ctx, - twilioCfg, + smsCfg, c.q.GetFileSystemProvider, c.q.GetLogProvider, c.counters.success.sms, c.counters.failed.sms, ) - return chain, twilioCfg, err + return chain, smsCfg, err } func (c *channels) Webhook(ctx context.Context, cfg webhook.Config) (*senders.Chain, error) { diff --git a/internal/notification/channels/email/config.go b/internal/notification/channels/email/config.go new file mode 100644 index 0000000000..d06029f8c2 --- /dev/null +++ b/internal/notification/channels/email/config.go @@ -0,0 +1,17 @@ +package email + +import ( + "github.com/zitadel/zitadel/internal/notification/channels/smtp" + "github.com/zitadel/zitadel/internal/notification/channels/webhook" +) + +type Config struct { + ProviderConfig *Provider + SMTPConfig *smtp.Config + WebhookConfig *webhook.Config +} + +type Provider struct { + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` +} diff --git a/internal/notification/channels/sms/config.go b/internal/notification/channels/sms/config.go new file mode 100644 index 0000000000..d759272084 --- /dev/null +++ b/internal/notification/channels/sms/config.go @@ -0,0 +1,17 @@ +package sms + +import ( + "github.com/zitadel/zitadel/internal/notification/channels/twilio" + "github.com/zitadel/zitadel/internal/notification/channels/webhook" +) + +type Config struct { + ProviderConfig *Provider + TwilioConfig *twilio.Config + WebhookConfig *webhook.Config +} + +type Provider struct { + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` +} diff --git a/internal/notification/channels/smtp/config.go b/internal/notification/channels/smtp/config.go index 865a2f4cd1..d8323ae574 100644 --- a/internal/notification/channels/smtp/config.go +++ b/internal/notification/channels/smtp/config.go @@ -1,7 +1,6 @@ package smtp type Config struct { - Description string SMTP SMTP Tls bool From string @@ -18,3 +17,7 @@ type SMTP struct { func (smtp *SMTP) HasAuth() bool { return smtp.User != "" && smtp.Password != "" } + +type ConfigHTTP struct { + Endpoint string +} diff --git a/internal/notification/handlers/commands.go b/internal/notification/handlers/commands.go index 5308a27c44..3d8546f800 100644 --- a/internal/notification/handlers/commands.go +++ b/internal/notification/handlers/commands.go @@ -19,6 +19,7 @@ type Commands interface { HumanPasswordlessInitCodeSent(ctx context.Context, userID, resourceOwner, codeID string) error PasswordChangeSent(ctx context.Context, orgID, userID string) error HumanPhoneVerificationCodeSent(ctx context.Context, orgID, userID string) error + InviteCodeSent(ctx context.Context, orgID, userID string) error UsageNotificationSent(ctx context.Context, dueEvent *quota.NotificationDueEvent) error MilestonePushed(ctx context.Context, msType milestone.Type, endpoints []string, primaryDomain string) error } diff --git a/internal/notification/handlers/config_email.go b/internal/notification/handlers/config_email.go new file mode 100644 index 0000000000..b78540a423 --- /dev/null +++ b/internal/notification/handlers/config_email.go @@ -0,0 +1,56 @@ +package handlers + +import ( + "context" + "net/http" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/notification/channels/email" + "github.com/zitadel/zitadel/internal/notification/channels/smtp" + "github.com/zitadel/zitadel/internal/notification/channels/webhook" + "github.com/zitadel/zitadel/internal/zerrors" +) + +// GetSMTPConfig reads the iam SMTP provider config +func (n *NotificationQueries) GetActiveEmailConfig(ctx context.Context) (*email.Config, error) { + config, err := n.SMTPConfigActive(ctx, authz.GetInstance(ctx).InstanceID()) + if err != nil { + return nil, err + } + provider := &email.Provider{ + ID: config.ID, + Description: config.Description, + } + if config.SMTPConfig != nil { + password, err := crypto.DecryptString(config.SMTPConfig.Password, n.SMTPPasswordCrypto) + if err != nil { + return nil, err + } + return &email.Config{ + ProviderConfig: provider, + SMTPConfig: &smtp.Config{ + From: config.SMTPConfig.SenderAddress, + FromName: config.SMTPConfig.SenderName, + ReplyToAddress: config.SMTPConfig.ReplyToAddress, + Tls: config.SMTPConfig.TLS, + SMTP: smtp.SMTP{ + Host: config.SMTPConfig.Host, + User: config.SMTPConfig.User, + Password: password, + }, + }, + }, nil + } + if config.HTTPConfig != nil { + return &email.Config{ + ProviderConfig: provider, + WebhookConfig: &webhook.Config{ + CallURL: config.HTTPConfig.Endpoint, + Method: http.MethodPost, + Headers: nil, + }, + }, nil + } + return nil, zerrors.ThrowNotFound(err, "QUERY-KPQleOckOV", "Errors.SMTPConfig.NotFound") +} diff --git a/internal/notification/handlers/config_sms.go b/internal/notification/handlers/config_sms.go new file mode 100644 index 0000000000..4698772eae --- /dev/null +++ b/internal/notification/handlers/config_sms.go @@ -0,0 +1,52 @@ +package handlers + +import ( + "context" + "net/http" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/notification/channels/sms" + "github.com/zitadel/zitadel/internal/notification/channels/twilio" + "github.com/zitadel/zitadel/internal/notification/channels/webhook" + "github.com/zitadel/zitadel/internal/zerrors" +) + +// GetActiveSMSConfig reads the active iam sms provider config +func (n *NotificationQueries) GetActiveSMSConfig(ctx context.Context) (*sms.Config, error) { + config, err := n.SMSProviderConfigActive(ctx, authz.GetInstance(ctx).InstanceID()) + if err != nil { + return nil, err + } + + provider := &sms.Provider{ + ID: config.ID, + Description: config.Description, + } + if config.TwilioConfig != nil { + token, err := crypto.DecryptString(config.TwilioConfig.Token, n.SMSTokenCrypto) + if err != nil { + return nil, err + } + return &sms.Config{ + ProviderConfig: provider, + TwilioConfig: &twilio.Config{ + SID: config.TwilioConfig.SID, + Token: token, + SenderNumber: config.TwilioConfig.SenderNumber, + }, + }, nil + } + if config.HTTPConfig != nil { + return &sms.Config{ + ProviderConfig: provider, + WebhookConfig: &webhook.Config{ + CallURL: config.HTTPConfig.Endpoint, + Method: http.MethodPost, + Headers: nil, + }, + }, nil + } + + return nil, zerrors.ThrowNotFound(nil, "HANDLER-8nfow", "Errors.SMS.Twilio.NotFound") +} diff --git a/internal/notification/handlers/config_smtp.go b/internal/notification/handlers/config_smtp.go deleted file mode 100644 index 8cce47dec6..0000000000 --- a/internal/notification/handlers/config_smtp.go +++ /dev/null @@ -1,33 +0,0 @@ -package handlers - -import ( - "context" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/notification/channels/smtp" -) - -// GetSMTPConfig reads the iam SMTP provider config -func (n *NotificationQueries) GetSMTPConfig(ctx context.Context) (*smtp.Config, error) { - config, err := n.SMTPConfigActive(ctx, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - password, err := crypto.DecryptString(config.Password, n.SMTPPasswordCrypto) - if err != nil { - return nil, err - } - return &smtp.Config{ - Description: config.Description, - From: config.SenderAddress, - FromName: config.SenderName, - ReplyToAddress: config.ReplyToAddress, - Tls: config.TLS, - SMTP: smtp.SMTP{ - Host: config.Host, - User: config.User, - Password: password, - }, - }, nil -} diff --git a/internal/notification/handlers/config_twilio.go b/internal/notification/handlers/config_twilio.go deleted file mode 100644 index 5391553690..0000000000 --- a/internal/notification/handlers/config_twilio.go +++ /dev/null @@ -1,35 +0,0 @@ -package handlers - -import ( - "context" - - "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" - "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/zerrors" -) - -// GetTwilioConfig reads the iam Twilio provider config -func (n *NotificationQueries) GetTwilioConfig(ctx context.Context) (*twilio.Config, error) { - active, err := query.NewSMSProviderStateQuery(domain.SMSConfigStateActive) - if err != nil { - return nil, err - } - config, err := n.SMSProviderConfig(ctx, active) - if err != nil { - return nil, err - } - if config.TwilioConfig == nil { - return nil, zerrors.ThrowNotFound(nil, "HANDLER-8nfow", "Errors.SMS.Twilio.NotFound") - } - token, err := crypto.DecryptString(config.TwilioConfig.Token, n.SMSTokenCrypto) - if err != nil { - return nil, err - } - return &twilio.Config{ - SID: config.TwilioConfig.SID, - Token: token, - SenderNumber: config.TwilioConfig.SenderNumber, - }, nil -} diff --git a/internal/notification/handlers/handlers_integration_test.go b/internal/notification/handlers/handlers_integration_test.go deleted file mode 100644 index 329bba1310..0000000000 --- a/internal/notification/handlers/handlers_integration_test.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build integration - -package handlers_test - -import ( - "context" - "os" - "testing" - "time" - - "github.com/zitadel/zitadel/internal/integration" -) - -var ( - CTX context.Context - SystemCTX context.Context - Tester *integration.Tester -) - -func TestMain(m *testing.M) { - os.Exit(func() int { - ctx, _, cancel := integration.Contexts(5 * time.Minute) - defer cancel() - CTX = ctx - - Tester = integration.NewTester(ctx, ` -Quotas: - Access: - Enabled: true -`) - defer Tester.Done() - - SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser) - return m.Run() - }()) -} diff --git a/internal/notification/handlers/integration_test/handlers_test.go b/internal/notification/handlers/integration_test/handlers_test.go new file mode 100644 index 0000000000..3b2dfe2406 --- /dev/null +++ b/internal/notification/handlers/integration_test/handlers_test.go @@ -0,0 +1,23 @@ +//go:build integration + +package handlers_test + +import ( + "context" + "os" + "testing" + "time" +) + +var ( + CTX context.Context +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + CTX = ctx + return m.Run() + }()) +} diff --git a/internal/notification/handlers/telemetry_pusher_integration_test.go b/internal/notification/handlers/integration_test/telemetry_pusher_test.go similarity index 53% rename from internal/notification/handlers/telemetry_pusher_integration_test.go rename to internal/notification/handlers/integration_test/telemetry_pusher_test.go index 8f207b9de3..c12ab64f35 100644 --- a/internal/notification/handlers/telemetry_pusher_integration_test.go +++ b/internal/notification/handlers/integration_test/telemetry_pusher_test.go @@ -4,7 +4,6 @@ package handlers_test import ( "bytes" - "context" "encoding/json" "net/url" "testing" @@ -16,6 +15,7 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/internal/integration/sink" "github.com/zitadel/zitadel/pkg/grpc/app" "github.com/zitadel/zitadel/pkg/grpc/management" "github.com/zitadel/zitadel/pkg/grpc/object" @@ -25,16 +25,20 @@ import ( ) func TestServer_TelemetryPushMilestones(t *testing.T) { - primaryDomain, instanceID, adminID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX) - t.Log("testing against instance with primary domain", primaryDomain) - awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "InstanceCreated") + sub := sink.Subscribe(CTX, sink.ChannelMilestone) + defer sub.Close() - projectAdded, err := Tester.Client.Mgmt.AddProject(iamOwnerCtx, &management.AddProjectRequest{Name: "integration"}) + instance := integration.NewInstance(CTX) + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + t.Log("testing against instance with primary domain", instance.Domain) + awaitMilestone(t, sub, instance.Domain, "InstanceCreated") + + projectAdded, err := instance.Client.Mgmt.AddProject(iamOwnerCtx, &management.AddProjectRequest{Name: "integration"}) require.NoError(t, err) - awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "ProjectCreated") + awaitMilestone(t, sub, instance.Domain, "ProjectCreated") redirectURI := "http://localhost:8888" - application, err := Tester.Client.Mgmt.AddOIDCApp(iamOwnerCtx, &management.AddOIDCAppRequest{ + application, err := instance.Client.Mgmt.AddOIDCApp(iamOwnerCtx, &management.AddOIDCAppRequest{ ProjectId: projectAdded.GetId(), Name: "integration", RedirectUris: []string{redirectURI}, @@ -46,35 +50,37 @@ func TestServer_TelemetryPushMilestones(t *testing.T) { AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, }) require.NoError(t, err) - awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "ApplicationCreated") + awaitMilestone(t, sub, instance.Domain, "ApplicationCreated") // create the session to be used for the authN of the clients - sessionID, sessionToken, _, _ := Tester.CreatePasswordSession(t, iamOwnerCtx, adminID, "Password1!") + sessionID, sessionToken, _, _ := instance.CreatePasswordSession(t, iamOwnerCtx, instance.AdminUserID, "Password1!") - console := consoleOIDCConfig(iamOwnerCtx, t) - loginToClient(iamOwnerCtx, t, primaryDomain, console.GetClientId(), instanceID, console.GetRedirectUris()[0], sessionID, sessionToken) - awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "AuthenticationSucceededOnInstance") + console := consoleOIDCConfig(t, instance) + loginToClient(t, instance, console.GetClientId(), console.GetRedirectUris()[0], sessionID, sessionToken) + awaitMilestone(t, sub, instance.Domain, "AuthenticationSucceededOnInstance") // make sure the client has been projected require.EventuallyWithT(t, func(collectT *assert.CollectT) { - _, err := Tester.Client.Mgmt.GetAppByID(iamOwnerCtx, &management.GetAppByIDRequest{ + _, err := instance.Client.Mgmt.GetAppByID(iamOwnerCtx, &management.GetAppByIDRequest{ ProjectId: projectAdded.GetId(), AppId: application.GetAppId(), }) assert.NoError(collectT, err) - }, 1*time.Minute, 100*time.Millisecond, "app not found") - loginToClient(iamOwnerCtx, t, primaryDomain, application.GetClientId(), instanceID, redirectURI, sessionID, sessionToken) - awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "AuthenticationSucceededOnApplication") + }, time.Minute, time.Second, "app not found") + loginToClient(t, instance, application.GetClientId(), redirectURI, sessionID, sessionToken) + awaitMilestone(t, sub, instance.Domain, "AuthenticationSucceededOnApplication") - _, err = Tester.Client.System.RemoveInstance(SystemCTX, &system.RemoveInstanceRequest{InstanceId: instanceID}) + _, err = integration.SystemClient().RemoveInstance(CTX, &system.RemoveInstanceRequest{InstanceId: instance.ID()}) require.NoError(t, err) - awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "InstanceDeleted") + awaitMilestone(t, sub, instance.Domain, "InstanceDeleted") } -func loginToClient(iamOwnerCtx context.Context, t *testing.T, primaryDomain, clientID, instanceID, redirectURI, sessionID, sessionToken string) { - authRequestID, err := Tester.CreateOIDCAuthRequestWithDomain(iamOwnerCtx, primaryDomain, clientID, Tester.Users.Get(instanceID, integration.IAMOwner).ID, redirectURI, "openid") +func loginToClient(t *testing.T, instance *integration.Instance, clientID, redirectURI, sessionID, sessionToken string) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + authRequestID, err := instance.CreateOIDCAuthRequestWithDomain(iamOwnerCtx, instance.Domain, clientID, instance.Users.Get(integration.UserTypeIAMOwner).ID, redirectURI, "openid") require.NoError(t, err) - callback, err := Tester.Client.OIDCv2.CreateCallback(iamOwnerCtx, &oidc_v2.CreateCallbackRequest{ + callback, err := instance.Client.OIDCv2.CreateCallback(iamOwnerCtx, &oidc_v2.CreateCallbackRequest{ AuthRequestId: authRequestID, CallbackKind: &oidc_v2.CreateCallbackRequest_Session{Session: &oidc_v2.Session{ SessionId: sessionID, @@ -82,7 +88,7 @@ func loginToClient(iamOwnerCtx context.Context, t *testing.T, primaryDomain, cli }}, }) require.NoError(t, err) - provider, err := Tester.CreateRelyingPartyForDomain(iamOwnerCtx, primaryDomain, clientID, redirectURI) + provider, err := instance.CreateRelyingPartyForDomain(iamOwnerCtx, instance.Domain, clientID, redirectURI) require.NoError(t, err) callbackURL, err := url.Parse(callback.GetCallbackUrl()) require.NoError(t, err) @@ -91,8 +97,10 @@ func loginToClient(iamOwnerCtx context.Context, t *testing.T, primaryDomain, cli require.NoError(t, err) } -func consoleOIDCConfig(iamOwnerCtx context.Context, t *testing.T) *app.OIDCConfig { - projects, err := Tester.Client.Mgmt.ListProjects(iamOwnerCtx, &management.ListProjectsRequest{ +func consoleOIDCConfig(t *testing.T, instance *integration.Instance) *app.OIDCConfig { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + projects, err := instance.Client.Mgmt.ListProjects(iamOwnerCtx, &management.ListProjectsRequest{ Queries: []*project.ProjectQuery{ { Query: &project.ProjectQuery_NameQuery{ @@ -106,7 +114,7 @@ func consoleOIDCConfig(iamOwnerCtx context.Context, t *testing.T) *app.OIDCConfi }) require.NoError(t, err) require.Len(t, projects.GetResult(), 1) - apps, err := Tester.Client.Mgmt.ListApps(iamOwnerCtx, &management.ListAppsRequest{ + apps, err := instance.Client.Mgmt.ListApps(iamOwnerCtx, &management.ListAppsRequest{ ProjectId: projects.GetResult()[0].GetId(), Queries: []*app.AppQuery{ { @@ -124,12 +132,12 @@ func consoleOIDCConfig(iamOwnerCtx context.Context, t *testing.T) *app.OIDCConfi return apps.GetResult()[0].GetOidcConfig() } -func awaitMilestone(t *testing.T, bodies chan []byte, primaryDomain, expectMilestoneType string) { +func awaitMilestone(t *testing.T, sub *sink.Subscription, primaryDomain, expectMilestoneType string) { for { select { - case body := <-bodies: + case req := <-sub.Recv(): plain := new(bytes.Buffer) - if err := json.Indent(plain, body, "", " "); err != nil { + if err := json.Indent(plain, req.Body, "", " "); err != nil { t.Fatal(err) } t.Log("received milestone", plain.String()) @@ -137,13 +145,13 @@ func awaitMilestone(t *testing.T, bodies chan []byte, primaryDomain, expectMiles Type string `json:"type"` PrimaryDomain string `json:"primaryDomain"` }{} - if err := json.Unmarshal(body, &milestone); err != nil { + if err := json.Unmarshal(req.Body, &milestone); err != nil { t.Error(err) } if milestone.Type == expectMilestoneType && milestone.PrimaryDomain == primaryDomain { return } - case <-time.After(60 * time.Second): + case <-time.After(2 * time.Minute): // why does it take so long to get a milestone !? t.Fatalf("timed out waiting for milestone %s in domain %s", expectMilestoneType, primaryDomain) } } diff --git a/internal/notification/handlers/mock/commands.mock.go b/internal/notification/handlers/mock/commands.mock.go index 3105dcdf60..ab94eda2cc 100644 --- a/internal/notification/handlers/mock/commands.mock.go +++ b/internal/notification/handlers/mock/commands.mock.go @@ -18,30 +18,30 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockCommands is a mock of Commands interface. +// MockCommands is a mock of Commands interface type MockCommands struct { ctrl *gomock.Controller recorder *MockCommandsMockRecorder } -// MockCommandsMockRecorder is the mock recorder for MockCommands. +// MockCommandsMockRecorder is the mock recorder for MockCommands type MockCommandsMockRecorder struct { mock *MockCommands } -// NewMockCommands creates a new mock instance. +// NewMockCommands creates a new mock instance func NewMockCommands(ctrl *gomock.Controller) *MockCommands { mock := &MockCommands{ctrl: ctrl} mock.recorder = &MockCommandsMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use. +// EXPECT returns an object that allows the caller to indicate expected use func (m *MockCommands) EXPECT() *MockCommandsMockRecorder { return m.recorder } -// HumanEmailVerificationCodeSent mocks base method. +// HumanEmailVerificationCodeSent mocks base method func (m *MockCommands) HumanEmailVerificationCodeSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "HumanEmailVerificationCodeSent", arg0, arg1, arg2) @@ -49,13 +49,13 @@ func (m *MockCommands) HumanEmailVerificationCodeSent(arg0 context.Context, arg1 return ret0 } -// HumanEmailVerificationCodeSent indicates an expected call of HumanEmailVerificationCodeSent. -func (mr *MockCommandsMockRecorder) HumanEmailVerificationCodeSent(arg0, arg1, arg2 any) *gomock.Call { +// HumanEmailVerificationCodeSent indicates an expected call of HumanEmailVerificationCodeSent +func (mr *MockCommandsMockRecorder) HumanEmailVerificationCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanEmailVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanEmailVerificationCodeSent), arg0, arg1, arg2) } -// HumanInitCodeSent mocks base method. +// HumanInitCodeSent mocks base method func (m *MockCommands) HumanInitCodeSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "HumanInitCodeSent", arg0, arg1, arg2) @@ -63,13 +63,13 @@ func (m *MockCommands) HumanInitCodeSent(arg0 context.Context, arg1, arg2 string return ret0 } -// HumanInitCodeSent indicates an expected call of HumanInitCodeSent. -func (mr *MockCommandsMockRecorder) HumanInitCodeSent(arg0, arg1, arg2 any) *gomock.Call { +// HumanInitCodeSent indicates an expected call of HumanInitCodeSent +func (mr *MockCommandsMockRecorder) HumanInitCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanInitCodeSent), arg0, arg1, arg2) } -// HumanOTPEmailCodeSent mocks base method. +// HumanOTPEmailCodeSent mocks base method func (m *MockCommands) HumanOTPEmailCodeSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "HumanOTPEmailCodeSent", arg0, arg1, arg2) @@ -77,13 +77,13 @@ func (m *MockCommands) HumanOTPEmailCodeSent(arg0 context.Context, arg1, arg2 st return ret0 } -// HumanOTPEmailCodeSent indicates an expected call of HumanOTPEmailCodeSent. -func (mr *MockCommandsMockRecorder) HumanOTPEmailCodeSent(arg0, arg1, arg2 any) *gomock.Call { +// HumanOTPEmailCodeSent indicates an expected call of HumanOTPEmailCodeSent +func (mr *MockCommandsMockRecorder) HumanOTPEmailCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPEmailCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPEmailCodeSent), arg0, arg1, arg2) } -// HumanOTPSMSCodeSent mocks base method. +// HumanOTPSMSCodeSent mocks base method func (m *MockCommands) HumanOTPSMSCodeSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "HumanOTPSMSCodeSent", arg0, arg1, arg2) @@ -91,13 +91,13 @@ func (m *MockCommands) HumanOTPSMSCodeSent(arg0 context.Context, arg1, arg2 stri return ret0 } -// HumanOTPSMSCodeSent indicates an expected call of HumanOTPSMSCodeSent. -func (mr *MockCommandsMockRecorder) HumanOTPSMSCodeSent(arg0, arg1, arg2 any) *gomock.Call { +// HumanOTPSMSCodeSent indicates an expected call of HumanOTPSMSCodeSent +func (mr *MockCommandsMockRecorder) HumanOTPSMSCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPSMSCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPSMSCodeSent), arg0, arg1, arg2) } -// HumanPasswordlessInitCodeSent mocks base method. +// HumanPasswordlessInitCodeSent mocks base method func (m *MockCommands) HumanPasswordlessInitCodeSent(arg0 context.Context, arg1, arg2, arg3 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "HumanPasswordlessInitCodeSent", arg0, arg1, arg2, arg3) @@ -105,13 +105,13 @@ func (m *MockCommands) HumanPasswordlessInitCodeSent(arg0 context.Context, arg1, return ret0 } -// HumanPasswordlessInitCodeSent indicates an expected call of HumanPasswordlessInitCodeSent. -func (mr *MockCommandsMockRecorder) HumanPasswordlessInitCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call { +// HumanPasswordlessInitCodeSent indicates an expected call of HumanPasswordlessInitCodeSent +func (mr *MockCommandsMockRecorder) HumanPasswordlessInitCodeSent(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPasswordlessInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPasswordlessInitCodeSent), arg0, arg1, arg2, arg3) } -// HumanPhoneVerificationCodeSent mocks base method. +// HumanPhoneVerificationCodeSent mocks base method func (m *MockCommands) HumanPhoneVerificationCodeSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "HumanPhoneVerificationCodeSent", arg0, arg1, arg2) @@ -119,13 +119,27 @@ func (m *MockCommands) HumanPhoneVerificationCodeSent(arg0 context.Context, arg1 return ret0 } -// HumanPhoneVerificationCodeSent indicates an expected call of HumanPhoneVerificationCodeSent. -func (mr *MockCommandsMockRecorder) HumanPhoneVerificationCodeSent(arg0, arg1, arg2 any) *gomock.Call { +// HumanPhoneVerificationCodeSent indicates an expected call of HumanPhoneVerificationCodeSent +func (mr *MockCommandsMockRecorder) HumanPhoneVerificationCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPhoneVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPhoneVerificationCodeSent), arg0, arg1, arg2) } -// MilestonePushed mocks base method. +// InviteCodeSent mocks base method +func (m *MockCommands) InviteCodeSent(arg0 context.Context, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InviteCodeSent", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// InviteCodeSent indicates an expected call of InviteCodeSent +func (mr *MockCommandsMockRecorder) InviteCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InviteCodeSent", reflect.TypeOf((*MockCommands)(nil).InviteCodeSent), arg0, arg1, arg2) +} + +// MilestonePushed mocks base method func (m *MockCommands) MilestonePushed(arg0 context.Context, arg1 milestone.Type, arg2 []string, arg3 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "MilestonePushed", arg0, arg1, arg2, arg3) @@ -133,13 +147,13 @@ func (m *MockCommands) MilestonePushed(arg0 context.Context, arg1 milestone.Type return ret0 } -// MilestonePushed indicates an expected call of MilestonePushed. -func (mr *MockCommandsMockRecorder) MilestonePushed(arg0, arg1, arg2, arg3 any) *gomock.Call { +// MilestonePushed indicates an expected call of MilestonePushed +func (mr *MockCommandsMockRecorder) MilestonePushed(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MilestonePushed", reflect.TypeOf((*MockCommands)(nil).MilestonePushed), arg0, arg1, arg2, arg3) } -// OTPEmailSent mocks base method. +// OTPEmailSent mocks base method func (m *MockCommands) OTPEmailSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "OTPEmailSent", arg0, arg1, arg2) @@ -147,13 +161,13 @@ func (m *MockCommands) OTPEmailSent(arg0 context.Context, arg1, arg2 string) err return ret0 } -// OTPEmailSent indicates an expected call of OTPEmailSent. -func (mr *MockCommandsMockRecorder) OTPEmailSent(arg0, arg1, arg2 any) *gomock.Call { +// OTPEmailSent indicates an expected call of OTPEmailSent +func (mr *MockCommandsMockRecorder) OTPEmailSent(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPEmailSent", reflect.TypeOf((*MockCommands)(nil).OTPEmailSent), arg0, arg1, arg2) } -// OTPSMSSent mocks base method. +// OTPSMSSent mocks base method func (m *MockCommands) OTPSMSSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "OTPSMSSent", arg0, arg1, arg2) @@ -161,13 +175,13 @@ func (m *MockCommands) OTPSMSSent(arg0 context.Context, arg1, arg2 string) error return ret0 } -// OTPSMSSent indicates an expected call of OTPSMSSent. -func (mr *MockCommandsMockRecorder) OTPSMSSent(arg0, arg1, arg2 any) *gomock.Call { +// OTPSMSSent indicates an expected call of OTPSMSSent +func (mr *MockCommandsMockRecorder) OTPSMSSent(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPSMSSent", reflect.TypeOf((*MockCommands)(nil).OTPSMSSent), arg0, arg1, arg2) } -// PasswordChangeSent mocks base method. +// PasswordChangeSent mocks base method func (m *MockCommands) PasswordChangeSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PasswordChangeSent", arg0, arg1, arg2) @@ -175,13 +189,13 @@ func (m *MockCommands) PasswordChangeSent(arg0 context.Context, arg1, arg2 strin return ret0 } -// PasswordChangeSent indicates an expected call of PasswordChangeSent. -func (mr *MockCommandsMockRecorder) PasswordChangeSent(arg0, arg1, arg2 any) *gomock.Call { +// PasswordChangeSent indicates an expected call of PasswordChangeSent +func (mr *MockCommandsMockRecorder) PasswordChangeSent(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordChangeSent", reflect.TypeOf((*MockCommands)(nil).PasswordChangeSent), arg0, arg1, arg2) } -// PasswordCodeSent mocks base method. +// PasswordCodeSent mocks base method func (m *MockCommands) PasswordCodeSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PasswordCodeSent", arg0, arg1, arg2) @@ -189,13 +203,13 @@ func (m *MockCommands) PasswordCodeSent(arg0 context.Context, arg1, arg2 string) return ret0 } -// PasswordCodeSent indicates an expected call of PasswordCodeSent. -func (mr *MockCommandsMockRecorder) PasswordCodeSent(arg0, arg1, arg2 any) *gomock.Call { +// PasswordCodeSent indicates an expected call of PasswordCodeSent +func (mr *MockCommandsMockRecorder) PasswordCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCodeSent", reflect.TypeOf((*MockCommands)(nil).PasswordCodeSent), arg0, arg1, arg2) } -// UsageNotificationSent mocks base method. +// UsageNotificationSent mocks base method func (m *MockCommands) UsageNotificationSent(arg0 context.Context, arg1 *quota.NotificationDueEvent) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UsageNotificationSent", arg0, arg1) @@ -203,13 +217,13 @@ func (m *MockCommands) UsageNotificationSent(arg0 context.Context, arg1 *quota.N return ret0 } -// UsageNotificationSent indicates an expected call of UsageNotificationSent. -func (mr *MockCommandsMockRecorder) UsageNotificationSent(arg0, arg1 any) *gomock.Call { +// UsageNotificationSent indicates an expected call of UsageNotificationSent +func (mr *MockCommandsMockRecorder) UsageNotificationSent(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UsageNotificationSent", reflect.TypeOf((*MockCommands)(nil).UsageNotificationSent), arg0, arg1) } -// UserDomainClaimedSent mocks base method. +// UserDomainClaimedSent mocks base method func (m *MockCommands) UserDomainClaimedSent(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UserDomainClaimedSent", arg0, arg1, arg2) @@ -217,8 +231,8 @@ func (m *MockCommands) UserDomainClaimedSent(arg0 context.Context, arg1, arg2 st return ret0 } -// UserDomainClaimedSent indicates an expected call of UserDomainClaimedSent. -func (mr *MockCommandsMockRecorder) UserDomainClaimedSent(arg0, arg1, arg2 any) *gomock.Call { +// UserDomainClaimedSent indicates an expected call of UserDomainClaimedSent +func (mr *MockCommandsMockRecorder) UserDomainClaimedSent(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserDomainClaimedSent", reflect.TypeOf((*MockCommands)(nil).UserDomainClaimedSent), arg0, arg1, arg2) } diff --git a/internal/notification/handlers/mock/queries.mock.go b/internal/notification/handlers/mock/queries.mock.go index 210493d875..48d7ec21ec 100644 --- a/internal/notification/handlers/mock/queries.mock.go +++ b/internal/notification/handlers/mock/queries.mock.go @@ -161,24 +161,19 @@ func (mr *MockQueriesMockRecorder) NotificationProviderByIDAndType(arg0, arg1, a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationProviderByIDAndType", reflect.TypeOf((*MockQueries)(nil).NotificationProviderByIDAndType), arg0, arg1, arg2) } -// SMSProviderConfig mocks base method. -func (m *MockQueries) SMSProviderConfig(arg0 context.Context, arg1 ...query.SearchQuery) (*query.SMSConfig, error) { +// SMSProviderConfigActive mocks base method. +func (m *MockQueries) SMSProviderConfigActive(arg0 context.Context, arg1 string) (*query.SMSConfig, error) { m.ctrl.T.Helper() - varargs := []any{arg0} - for _, a := range arg1 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "SMSProviderConfig", varargs...) + ret := m.ctrl.Call(m, "SMSProviderConfigActive", arg0, arg1) ret0, _ := ret[0].(*query.SMSConfig) ret1, _ := ret[1].(error) return ret0, ret1 } -// SMSProviderConfig indicates an expected call of SMSProviderConfig. -func (mr *MockQueriesMockRecorder) SMSProviderConfig(arg0 any, arg1 ...any) *gomock.Call { +// SMSProviderConfigActive indicates an expected call of SMSProviderConfigActive. +func (mr *MockQueriesMockRecorder) SMSProviderConfigActive(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]any{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMSProviderConfig", reflect.TypeOf((*MockQueries)(nil).SMSProviderConfig), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMSProviderConfigActive", reflect.TypeOf((*MockQueries)(nil).SMSProviderConfigActive), arg0, arg1) } // SMTPConfigActive mocks base method. diff --git a/internal/notification/handlers/queries.go b/internal/notification/handlers/queries.go index ce7597ead8..49cffc5e49 100644 --- a/internal/notification/handlers/queries.go +++ b/internal/notification/handlers/queries.go @@ -21,7 +21,7 @@ type Queries interface { NotificationPolicyByOrg(ctx context.Context, shouldTriggerBulk bool, orgID string, withOwnerRemoved bool) (*query.NotificationPolicy, error) SearchMilestones(ctx context.Context, instanceIDs []string, queries *query.MilestonesSearchQueries) (*query.Milestones, error) NotificationProviderByIDAndType(ctx context.Context, aggID string, providerType domain.NotificationProviderType) (*query.DebugNotificationProvider, error) - SMSProviderConfig(ctx context.Context, queries ...query.SearchQuery) (*query.SMSConfig, error) + SMSProviderConfigActive(ctx context.Context, resourceOwner string) (config *query.SMSConfig, err error) SMTPConfigActive(ctx context.Context, resourceOwner string) (*query.SMTPConfig, error) GetDefaultLanguage(ctx context.Context) language.Tag GetInstanceRestrictions(ctx context.Context) (restrictions query.Restrictions, err error) diff --git a/internal/notification/handlers/quota_notifier_test.go b/internal/notification/handlers/quota_notifier_test.go deleted file mode 100644 index 14de4e369c..0000000000 --- a/internal/notification/handlers/quota_notifier_test.go +++ /dev/null @@ -1,155 +0,0 @@ -//go:build integration - -package handlers_test - -import ( - "bytes" - "encoding/json" - "fmt" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/zitadel/zitadel/internal/repository/quota" - "github.com/zitadel/zitadel/pkg/grpc/admin" - quota_pb "github.com/zitadel/zitadel/pkg/grpc/quota" - "github.com/zitadel/zitadel/pkg/grpc/system" -) - -func TestServer_QuotaNotification_Limit(t *testing.T) { - _, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX) - amount := 10 - percent := 50 - percentAmount := amount * percent / 100 - - _, err := Tester.Client.System.AddQuota(SystemCTX, &system.AddQuotaRequest{ - InstanceId: instanceID, - Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED, - From: timestamppb.Now(), - ResetInterval: durationpb.New(time.Minute * 5), - Amount: uint64(amount), - Limit: true, - Notifications: []*quota_pb.Notification{ - { - Percent: uint32(percent), - Repeat: true, - CallUrl: "http://localhost:8082", - }, - { - Percent: 100, - Repeat: true, - CallUrl: "http://localhost:8082", - }, - }, - }) - require.NoError(t, err) - - for i := 0; i < percentAmount; i++ { - _, err := Tester.Client.Admin.GetDefaultOrg(iamOwnerCtx, &admin.GetDefaultOrgRequest{}) - if err != nil { - require.NoError(t, fmt.Errorf("error in %d call of %d: %f", i, percentAmount, err)) - } - } - awaitNotification(t, time.Now(), Tester.QuotaNotificationChan, quota.RequestsAllAuthenticated, percent) - - for i := 0; i < (amount - percentAmount); i++ { - _, err := Tester.Client.Admin.GetDefaultOrg(iamOwnerCtx, &admin.GetDefaultOrgRequest{}) - require.NoError(t, err) - } - awaitNotification(t, time.Now(), Tester.QuotaNotificationChan, quota.RequestsAllAuthenticated, 100) - - _, limitErr := Tester.Client.Admin.GetDefaultOrg(iamOwnerCtx, &admin.GetDefaultOrgRequest{}) - require.Error(t, limitErr) -} - -func TestServer_QuotaNotification_NoLimit(t *testing.T) { - _, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX) - amount := 10 - percent := 50 - percentAmount := amount * percent / 100 - - _, err := Tester.Client.System.AddQuota(SystemCTX, &system.AddQuotaRequest{ - InstanceId: instanceID, - Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED, - From: timestamppb.Now(), - ResetInterval: durationpb.New(time.Minute * 5), - Amount: uint64(amount), - Limit: false, - Notifications: []*quota_pb.Notification{ - { - Percent: uint32(percent), - Repeat: false, - CallUrl: "http://localhost:8082", - }, - { - Percent: 100, - Repeat: true, - CallUrl: "http://localhost:8082", - }, - }, - }) - require.NoError(t, err) - - for i := 0; i < percentAmount; i++ { - _, err := Tester.Client.Admin.GetDefaultOrg(iamOwnerCtx, &admin.GetDefaultOrgRequest{}) - if err != nil { - require.NoError(t, fmt.Errorf("error in %d call of %d: %f", i, percentAmount, err)) - } - } - awaitNotification(t, time.Now(), Tester.QuotaNotificationChan, quota.RequestsAllAuthenticated, percent) - - for i := 0; i < (amount - percentAmount); i++ { - _, err := Tester.Client.Admin.GetDefaultOrg(iamOwnerCtx, &admin.GetDefaultOrgRequest{}) - if err != nil { - require.NoError(t, fmt.Errorf("error in %d call of %d: %f", percentAmount+i, amount, err)) - } - } - awaitNotification(t, time.Now(), Tester.QuotaNotificationChan, quota.RequestsAllAuthenticated, 100) - - for i := 0; i < amount; i++ { - _, err := Tester.Client.Admin.GetDefaultOrg(iamOwnerCtx, &admin.GetDefaultOrgRequest{}) - if err != nil { - require.NoError(t, fmt.Errorf("error in %d call of %d over limit: %f", i, amount, err)) - } - } - awaitNotification(t, time.Now(), Tester.QuotaNotificationChan, quota.RequestsAllAuthenticated, 200) - - _, limitErr := Tester.Client.Admin.GetDefaultOrg(iamOwnerCtx, &admin.GetDefaultOrgRequest{}) - require.NoError(t, limitErr) -} - -func awaitNotification(t *testing.T, start time.Time, bodies chan []byte, unit quota.Unit, percent int) { - for { - select { - case body := <-bodies: - plain := new(bytes.Buffer) - if err := json.Indent(plain, body, "", " "); err != nil { - t.Fatal(err) - } - t.Log("received notificationDueEvent", plain.String()) - event := struct { - Unit quota.Unit `json:"unit"` - ID string `json:"id"` - CallURL string `json:"callURL"` - PeriodStart time.Time `json:"periodStart"` - Threshold uint16 `json:"threshold"` - Usage uint64 `json:"usage"` - }{} - if err := json.Unmarshal(body, &event); err != nil { - t.Error(err) - } - if event.ID == "" { - continue - } - if event.Unit == unit && event.Threshold == uint16(percent) { - return - } - case <-time.After(20 * time.Second): - t.Fatalf("start %s stop %s timed out waiting for unit %s and percent %d", start.Format(time.RFC3339), time.Now().Format(time.RFC3339), strconv.Itoa(int(unit)), percent) - } - } -} diff --git a/internal/notification/handlers/user_notifier.go b/internal/notification/handlers/user_notifier.go index 066796ae3b..57aa3a9251 100644 --- a/internal/notification/handlers/user_notifier.go +++ b/internal/notification/handlers/user_notifier.go @@ -106,6 +106,10 @@ func (u *userNotifier) Reducers() []handler.AggregateReducer { Event: user.HumanOTPEmailCodeAddedType, Reduce: u.reduceOTPEmailCodeAdded, }, + { + Event: user.HumanInviteCodeAddedType, + Reduce: u.reduceInviteCodeAdded, + }, }, }, { @@ -283,7 +287,7 @@ func (u *userNotifier) reducePasswordCodeAdded(event eventstore.Event) (*handler } notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e) if e.NotificationType == domain.NotificationTypeSms { - notify = types.SendSMSTwilio(ctx, u.channels, translator, notifyUser, colors, e) + notify = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e) } err = notify.SendPasswordCode(ctx, notifyUser, code, e.URLTemplate, e.AuthRequestID) if err != nil { @@ -373,7 +377,7 @@ func (u *userNotifier) reduceOTPSMS( if err != nil { return nil, err } - notify := types.SendSMSTwilio(ctx, u.channels, translator, notifyUser, colors, event) + notify := types.SendSMS(ctx, u.channels, translator, notifyUser, colors, event) err = notify.SendOTPSMSCode(ctx, plainCode, expiry) if err != nil { return nil, err @@ -709,7 +713,7 @@ func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.St if err != nil { return err } - err = types.SendSMSTwilio(ctx, u.channels, translator, notifyUser, colors, e). + err = types.SendSMS(ctx, u.channels, translator, notifyUser, colors, e). SendPhoneVerificationCode(ctx, code) if err != nil { return err @@ -718,6 +722,61 @@ func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.St }), nil } +func (u *userNotifier) reduceInviteCodeAdded(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*user.HumanInviteCodeAddedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Eeg3s", "reduce.wrong.event.type %s", user.HumanInviteCodeAddedType) + } + if e.CodeReturned { + return handler.NewNoOpStatement(e), nil + } + + return handler.NewStatement(event, func(ex handler.Executer, projectionName string) error { + ctx := HandlerContext(event.Aggregate()) + alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil, + user.HumanInviteCodeAddedType, user.HumanInviteCodeSentType) + if err != nil { + return err + } + if alreadyHandled { + return nil + } + code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto) + if err != nil { + return err + } + colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false) + if err != nil { + return err + } + + template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false) + if err != nil { + return err + } + + notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) + if err != nil { + return err + } + translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.InviteUserMessageType) + if err != nil { + return err + } + + ctx, err = u.queries.Origin(ctx, e) + if err != nil { + return err + } + notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e) + err = notify.SendInviteCode(ctx, notifyUser, code, e.ApplicationName, e.URLTemplate, e.AuthRequestID) + if err != nil { + return err + } + return u.commands.InviteCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner) + }), nil +} + func (u *userNotifier) checkIfCodeAlreadyHandledOrExpired(ctx context.Context, event eventstore.Event, expiry time.Duration, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) { if event.CreatedAt().Add(expiry).Before(time.Now().UTC()) { return true, nil diff --git a/internal/notification/handlers/user_notifier_test.go b/internal/notification/handlers/user_notifier_test.go index 8b2155cc27..c18ddc1df3 100644 --- a/internal/notification/handlers/user_notifier_test.go +++ b/internal/notification/handlers/user_notifier_test.go @@ -15,9 +15,10 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" es_repo_mock "github.com/zitadel/zitadel/internal/eventstore/repository/mock" + "github.com/zitadel/zitadel/internal/notification/channels/email" channel_mock "github.com/zitadel/zitadel/internal/notification/channels/mock" + "github.com/zitadel/zitadel/internal/notification/channels/sms" "github.com/zitadel/zitadel/internal/notification/channels/smtp" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" "github.com/zitadel/zitadel/internal/notification/channels/webhook" "github.com/zitadel/zitadel/internal/notification/handlers/mock" "github.com/zitadel/zitadel/internal/notification/messages" @@ -1449,7 +1450,27 @@ func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQu f.SMSTokenCrypto, ), otpEmailTmpl: defaultOTPEmailTemplate, - channels: &channels{Chain: *senders.ChainChannels(channel)}, + channels: &channels{ + Chain: *senders.ChainChannels(channel), + EmailConfig: &email.Config{ + ProviderConfig: &email.Provider{ + ID: "ID", + Description: "Description", + }, + SMTPConfig: &smtp.Config{ + SMTP: smtp.SMTP{ + Host: "host", + User: "user", + Password: "password", + }, + Tls: true, + From: "from", + FromName: "fromName", + ReplyToAddress: "replyToAddress", + }, + WebhookConfig: nil, + }, + }, } } @@ -1457,13 +1478,14 @@ var _ types.ChannelChains = (*channels)(nil) type channels struct { senders.Chain + EmailConfig *email.Config } -func (c *channels) Email(context.Context) (*senders.Chain, *smtp.Config, error) { - return &c.Chain, nil, nil +func (c *channels) Email(context.Context) (*senders.Chain, *email.Config, error) { + return &c.Chain, c.EmailConfig, nil } -func (c *channels) SMS(context.Context) (*senders.Chain, *twilio.Config, error) { +func (c *channels) SMS(context.Context) (*senders.Chain, *sms.Config, error) { return &c.Chain, nil, nil } diff --git a/internal/notification/senders/email.go b/internal/notification/senders/email.go index ea93c0911e..4dfc815919 100644 --- a/internal/notification/senders/email.go +++ b/internal/notification/senders/email.go @@ -7,38 +7,61 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/notification/channels" + "github.com/zitadel/zitadel/internal/notification/channels/email" "github.com/zitadel/zitadel/internal/notification/channels/fs" "github.com/zitadel/zitadel/internal/notification/channels/instrumenting" "github.com/zitadel/zitadel/internal/notification/channels/log" "github.com/zitadel/zitadel/internal/notification/channels/smtp" + "github.com/zitadel/zitadel/internal/notification/channels/webhook" ) const smtpSpanName = "smtp.NotificationChannel" func EmailChannels( ctx context.Context, - emailConfig *smtp.Config, + emailConfig *email.Config, getFileSystemProvider func(ctx context.Context) (*fs.Config, error), getLogProvider func(ctx context.Context) (*log.Config, error), successMetricName, failureMetricName string, ) (chain *Chain, err error) { channels := make([]channels.NotificationChannel, 0, 3) - p, err := smtp.InitChannel(emailConfig) - logging.WithFields( - "instance", authz.GetInstance(ctx).InstanceID(), - ).OnError(err).Debug("initializing SMTP channel failed") - if err == nil { - channels = append( - channels, - instrumenting.Wrap( - ctx, - p, - smtpSpanName, - successMetricName, - failureMetricName, - ), - ) + if emailConfig.SMTPConfig != nil { + p, err := smtp.InitChannel(emailConfig.SMTPConfig) + logging.WithFields( + "instance", authz.GetInstance(ctx).InstanceID(), + ).OnError(err).Debug("initializing SMTP channel failed") + if err == nil { + channels = append( + channels, + instrumenting.Wrap( + ctx, + p, + smtpSpanName, + successMetricName, + failureMetricName, + ), + ) + } + } + if emailConfig.WebhookConfig != nil { + webhookChannel, err := webhook.InitChannel(ctx, *emailConfig.WebhookConfig) + logging.WithFields( + "instance", authz.GetInstance(ctx).InstanceID(), + "callurl", emailConfig.WebhookConfig.CallURL, + ).OnError(err).Debug("initializing JSON channel failed") + if err == nil { + channels = append( + channels, + instrumenting.Wrap( + ctx, + webhookChannel, + webhookSpanName, + successMetricName, + failureMetricName, + ), + ) + } } channels = append(channels, debugChannels(ctx, getFileSystemProvider, getLogProvider)...) return ChainChannels(channels...), nil diff --git a/internal/notification/senders/sms.go b/internal/notification/senders/sms.go index 361fc56509..7ce2adc71c 100644 --- a/internal/notification/senders/sms.go +++ b/internal/notification/senders/sms.go @@ -3,36 +3,60 @@ package senders import ( "context" + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/notification/channels" "github.com/zitadel/zitadel/internal/notification/channels/fs" "github.com/zitadel/zitadel/internal/notification/channels/instrumenting" "github.com/zitadel/zitadel/internal/notification/channels/log" + "github.com/zitadel/zitadel/internal/notification/channels/sms" "github.com/zitadel/zitadel/internal/notification/channels/twilio" + "github.com/zitadel/zitadel/internal/notification/channels/webhook" ) const twilioSpanName = "twilio.NotificationChannel" func SMSChannels( ctx context.Context, - twilioConfig *twilio.Config, + smsConfig *sms.Config, getFileSystemProvider func(ctx context.Context) (*fs.Config, error), getLogProvider func(ctx context.Context) (*log.Config, error), successMetricName, failureMetricName string, ) (chain *Chain, err error) { channels := make([]channels.NotificationChannel, 0, 3) - if twilioConfig != nil { + if smsConfig.TwilioConfig != nil { channels = append( channels, instrumenting.Wrap( ctx, - twilio.InitChannel(*twilioConfig), + twilio.InitChannel(*smsConfig.TwilioConfig), twilioSpanName, successMetricName, failureMetricName, ), ) } + if smsConfig.WebhookConfig != nil { + webhookChannel, err := webhook.InitChannel(ctx, *smsConfig.WebhookConfig) + logging.WithFields( + "instance", authz.GetInstance(ctx).InstanceID(), + "callurl", smsConfig.WebhookConfig.CallURL, + ).OnError(err).Debug("initializing JSON channel failed") + if err == nil { + channels = append( + channels, + instrumenting.Wrap( + ctx, + webhookChannel, + webhookSpanName, + successMetricName, + failureMetricName, + ), + ) + } + } channels = append(channels, debugChannels(ctx, getFileSystemProvider, getLogProvider)...) return ChainChannels(channels...), nil } diff --git a/internal/notification/static/i18n/bg.yaml b/internal/notification/static/i18n/bg.yaml index 7f67c24dfb..47bed1cb5d 100644 --- a/internal/notification/static/i18n/bg.yaml +++ b/internal/notification/static/i18n/bg.yaml @@ -69,3 +69,10 @@ PasswordChange: Паролата на вашия потребител е променена, ако тази промяна не е направена от вас, моля, незабавно нулирайте паролата си. ButtonText: Влизам +InviteUser: + Title: Покана за {{.ApplicationName}} + PreHeader: Покана за {{.ApplicationName}} + Subject: Покана за {{.ApplicationName}} + Greeting: 'Здравейте {{.DisplayName}},' + Text: Вашият потребител е бил поканен за {{.ApplicationName}}. Моля, кликнете върху бутона по-долу, за да завършите процеса на покана. Ако не сте поискали този имейл, моля, игнорирайте го. + ButtonText: Приеми поканата \ No newline at end of file diff --git a/internal/notification/static/i18n/cs.yaml b/internal/notification/static/i18n/cs.yaml index b3d4cf18f5..95e897998e 100644 --- a/internal/notification/static/i18n/cs.yaml +++ b/internal/notification/static/i18n/cs.yaml @@ -59,3 +59,10 @@ PasswordChange: Greeting: Dobrý den, {{.DisplayName}}, Text: Heslo vašeho uživatele bylo změněno. Pokud tato změna nebyla provedena Vámi pak doporučujeme okamžitě resetovat/změnit vaše heslo. ButtonText: Přihlásit se +InviteUser: + Title: Pozvánka do {{.ApplicationName}} + PreHeader: Pozvánka do {{.ApplicationName}} + Subject: Pozvánka do {{.ApplicationName}} + Greeting: Dobrý den, {{.DisplayName}}, + Text: Váš uživatel byl pozván do {{.ApplicationName}}. Klikněte prosím na tlačítko níže, abyste dokončili proces pozvání. Pokud jste o tento e-mail nepožádali, prosím, ignorujte ho. + ButtonText: Přijmout pozvání \ No newline at end of file diff --git a/internal/notification/static/i18n/de.yaml b/internal/notification/static/i18n/de.yaml index 673b207ef2..88ca694754 100644 --- a/internal/notification/static/i18n/de.yaml +++ b/internal/notification/static/i18n/de.yaml @@ -59,3 +59,10 @@ PasswordChange: Greeting: Hallo {{.DisplayName}}, Text: Dein Passwort wurde geändert. Wenn diese Änderung nicht von dir gemacht wurde, empfehlen wir das sofortige Zurücksetzen deines Passworts. ButtonText: Login +InviteUser: + Title: Einladung zu {{.ApplicationName}} + PreHeader: Einladung zu {{.ApplicationName}} + Subject: Einladung zu {{.ApplicationName}} + Greeting: Hallo {{.DisplayName}}, + Text: Ihr Benutzer wurde zu {{.ApplicationName}} eingeladen. Bitte klicken Sie auf die Schaltfläche unten, um den Einladungsprozess abzuschließen. Wenn Sie diese E-Mail nicht angefordert haben, ignorieren Sie sie bitte. + ButtonText: Einladung annehmen \ No newline at end of file diff --git a/internal/notification/static/i18n/en.yaml b/internal/notification/static/i18n/en.yaml index 20e187aa0b..a431fc999d 100644 --- a/internal/notification/static/i18n/en.yaml +++ b/internal/notification/static/i18n/en.yaml @@ -59,3 +59,10 @@ PasswordChange: Greeting: Hello {{.DisplayName}}, Text: The password of your user has changed. If this change was not done by you, please be advised to immediately reset your password. ButtonText: Login +InviteUser: + Title: Invitation to {{.ApplicationName}} + PreHeader: Invitation to {{.ApplicationName}} + Subject: Invitation to {{.ApplicationName}} + Greeting: Hello {{.DisplayName}}, + Text: Your user has been invited to {{.ApplicationName}}. Please click the button below to finish the invite process. If you didn't ask for this mail, please ignore it. + ButtonText: Accept invite \ No newline at end of file diff --git a/internal/notification/static/i18n/es.yaml b/internal/notification/static/i18n/es.yaml index 1e92e30be2..caf138f1f9 100644 --- a/internal/notification/static/i18n/es.yaml +++ b/internal/notification/static/i18n/es.yaml @@ -59,3 +59,10 @@ PasswordChange: Greeting: Hola {{.DisplayName}}, Text: La contraseña de tu usuario ha sido cambiada, si este cambio no fue hecho por ti, por favor proceder a restablecer inmediatamente tu contraseña. ButtonText: Iniciar sesión +InviteUser: + Title: Invitación a {{.ApplicationName}} + PreHeader: Invitación a {{.ApplicationName}} + Subject: Invitación a {{.ApplicationName}} + Greeting: Hola {{.DisplayName}}, + Text: Tu usuario ha sido invitado a {{.ApplicationName}}. Haz clic en el botón de abajo para finalizar el proceso de invitación. Si no solicitaste este correo electrónico, por favor ignóralo. + ButtonText: Aceptar invitación \ No newline at end of file diff --git a/internal/notification/static/i18n/fr.yaml b/internal/notification/static/i18n/fr.yaml index a313343ba5..8fab61808b 100644 --- a/internal/notification/static/i18n/fr.yaml +++ b/internal/notification/static/i18n/fr.yaml @@ -59,3 +59,10 @@ PasswordChange: Greeting: Bonjour {{.DisplayName}}, Text: Le mot de passe de votre utilisateur a changé, si ce changement n'a pas été fait par vous, nous vous conseillons de réinitialiser immédiatement votre mot de passe. ButtonText: Login +InviteUser: + Title: Invitation à {{.ApplicationName}} + PreHeader: Invitation à {{.ApplicationName}} + Subject: Invitation à {{.ApplicationName}} + Greeting: Bonjour {{.DisplayName}}, + Text: Votre utilisateur a été invité à {{.ApplicationName}}. Veuillez cliquer sur le bouton ci-dessous pour terminer le processus d'invitation. Si vous n'avez pas demandé cet e-mail, veuillez l'ignorer. + ButtonText: Accepter l'invitation \ No newline at end of file diff --git a/internal/notification/static/i18n/id.yaml b/internal/notification/static/i18n/id.yaml index e5bd60d218..928c94215f 100644 --- a/internal/notification/static/i18n/id.yaml +++ b/internal/notification/static/i18n/id.yaml @@ -59,3 +59,10 @@ PasswordChange: Greeting: 'Halo {{.DisplayName}},' Text: 'Kata sandi pengguna Anda telah berubah. ' ButtonText: Login +InviteUser: + Title: Undangan ke {{.ApplicationName}} + PreHeader: Undangan ke {{.ApplicationName}} + Subject: Undangan ke {{.ApplicationName}} + Greeting: 'Halo {{.DisplayName}},' + Text: Pengguna Anda telah diundang ke {{.ApplicationName}}. Silakan klik tombol di bawah ini untuk menyelesaikan proses undangan. Jika Anda tidak meminta email ini, harap abaikan. + ButtonText: Terima undangan \ No newline at end of file diff --git a/internal/notification/static/i18n/it.yaml b/internal/notification/static/i18n/it.yaml index 58538b36b7..6be2f16a5a 100644 --- a/internal/notification/static/i18n/it.yaml +++ b/internal/notification/static/i18n/it.yaml @@ -59,3 +59,10 @@ PasswordChange: Greeting: Ciao {{.DisplayName}}, Text: La password del vostro utente è cambiata; se questa modifica non è stata fatta da voi, vi consigliamo di reimpostare immediatamente la vostra password. ButtonText: Login +InviteUser: + Title: Invito a {{.ApplicationName}} + PreHeader: Invito a {{.ApplicationName}} + Subject: Invito a {{.ApplicationName}} + Greeting: 'Ciao {{.DisplayName}},' + Text: Il tuo utente è stato invitato a {{.ApplicationName}}. Clicca sul pulsante qui sotto per completare il processo di invito. Se non hai richiesto questa email, ignorala. + ButtonText: Accetta invito \ No newline at end of file diff --git a/internal/notification/static/i18n/ja.yaml b/internal/notification/static/i18n/ja.yaml index 0696be71d8..4419f51b83 100644 --- a/internal/notification/static/i18n/ja.yaml +++ b/internal/notification/static/i18n/ja.yaml @@ -59,3 +59,10 @@ PasswordChange: Greeting: こんにちは {{.DisplayName}} さん、 Text: ユーザーのパスワードが変更されました。この変更があなたによって行われなかった場合は、すぐにパスワードをリセットすることをお勧めします。 ButtonText: ログイン +InviteUser: + Title: '{{.ApplicationName}}への招待' + PreHeader: '{{.ApplicationName}}への招待' + Subject: '{{.ApplicationName}}への招待' + Greeting: こんにちは {{.DisplayName}} さん、 + Text: あなたのユーザーは{{.ApplicationName}}に招待されました。下のボタンをクリックして、招待プロセスを完了してください。このメールをリクエストしていない場合は、無視してください。 + ButtonText: 招待を受け入れる \ No newline at end of file diff --git a/internal/notification/static/i18n/mk.yaml b/internal/notification/static/i18n/mk.yaml index 1d9d19b71d..a89b91227f 100644 --- a/internal/notification/static/i18n/mk.yaml +++ b/internal/notification/static/i18n/mk.yaml @@ -59,3 +59,10 @@ PasswordChange: Greeting: Здраво {{.DisplayName}}, Text: Лозинката на вашиот корисник е променета. Ако оваа промена не е извршена од вас, ве молиме веднаш ресетирајте ја вашата лозинка. ButtonText: Најава +InviteUser: + Title: Покана за {{.ApplicationName}} + PreHeader: Покана за {{.ApplicationName}} + Subject: Покана за {{.ApplicationName}} + Greeting: Здраво {{.DisplayName}}, + Text: Вашиот корисник е бил поканет за {{.ApplicationName}}. Ве молиме кликнете на копчето подолу за да го завршите процесот на покана. Ако не сте побарале овој мејл, ве молиме игнорирајте го. + ButtonText: Прифати покана \ No newline at end of file diff --git a/internal/notification/static/i18n/nl.yaml b/internal/notification/static/i18n/nl.yaml index 2c2c3632aa..46e247b64c 100644 --- a/internal/notification/static/i18n/nl.yaml +++ b/internal/notification/static/i18n/nl.yaml @@ -59,3 +59,10 @@ PasswordChange: Greeting: Hallo {{.DisplayName}}, Text: Het wachtwoord van uw gebruiker is veranderd. Als deze wijziging niet door u is gedaan, wordt u geadviseerd om direct uw wachtwoord te resetten. ButtonText: Inloggen +InviteUser: + Title: Uitnodiging voor {{.ApplicationName}} + PreHeader: Uitnodiging voor {{.ApplicationName}} + Subject: Uitnodiging voor {{.ApplicationName}} + Greeting: Hallo {{.DisplayName}}, + Text: Uw gebruiker is uitgenodigd voor {{.ApplicationName}}. Klik op de onderstaande knop om het uitnodigingsproces te voltooien. Als u deze e-mail niet hebt aangevraagd, negeer deze dan. + ButtonText: Uitnodiging accepteren \ No newline at end of file diff --git a/internal/notification/static/i18n/pl.yaml b/internal/notification/static/i18n/pl.yaml index e67b178c29..b2e9f268bb 100644 --- a/internal/notification/static/i18n/pl.yaml +++ b/internal/notification/static/i18n/pl.yaml @@ -59,3 +59,10 @@ PasswordChange: Greeting: Witaj {{.DisplayName}}, Text: Hasło Twojego użytkownika zostało zmienione, jeśli ta zmiana nie została dokonana przez Ciebie, zalecamy natychmiastowe zresetowanie hasła. ButtonText: Zaloguj się +InviteUser: + Title: Zaproszenie do {{.ApplicationName}} + PreHeader: Zaproszenie do {{.ApplicationName}} + Subject: Zaproszenie do {{.ApplicationName}} + Greeting: Witaj {{.DisplayName}}, + Text: Twój użytkownik został zaproszony do {{.ApplicationName}}. Kliknij poniższy przycisk, aby zakończyć proces zaproszenia. Jeśli nie zażądałeś tego e-maila, zignoruj go. + ButtonText: Akceptuj zaproszenie \ No newline at end of file diff --git a/internal/notification/static/i18n/pt.yaml b/internal/notification/static/i18n/pt.yaml index a7a2480b81..c45c20b8f7 100644 --- a/internal/notification/static/i18n/pt.yaml +++ b/internal/notification/static/i18n/pt.yaml @@ -59,3 +59,10 @@ PasswordChange: Greeting: Olá {{.DisplayName}}, Text: A senha do seu usuário foi alterada. Se esta alteração não foi feita por você, recomendamos que você redefina sua senha imediatamente. ButtonText: Fazer login +InviteUser: + Title: Convite para {{.ApplicationName}} + PreHeader: Convite para {{.ApplicationName}} + Subject: Convite para {{.ApplicationName}} + Greeting: Olá {{.DisplayName}}, + Text: Seu usuário foi convidado para {{.ApplicationName}}. Clique no botão abaixo para concluir o processo de convite. Se você não solicitou este e-mail, por favor, ignore-o. + ButtonText: Aceitar convite \ No newline at end of file diff --git a/internal/notification/static/i18n/ru.yaml b/internal/notification/static/i18n/ru.yaml index ec187554d0..af700dc975 100644 --- a/internal/notification/static/i18n/ru.yaml +++ b/internal/notification/static/i18n/ru.yaml @@ -2,28 +2,28 @@ InitCode: Title: Регистрация пользователя PreHeader: Регистрация пользователя Subject: Регистрация пользователя - Greeting: Здравствуйте {{.FirstName}} {{.LastName}}, + Greeting: Здравствуйте {{.DisplayName}}, Text: Используйте логин {{.PreferredLoginName}} для входа. Пожалуйста, нажмите кнопку ниже для завершения процесса регистрации. (Код {{.Code}}) Если вы не запрашивали это письмо, пожалуйста, проигнорируйте его. ButtonText: Завершить регистрацию PasswordReset: Title: Сброс пароля PreHeader: Сброс пароля Subject: Сброс пароля - Greeting: Здравствуйте {{.FirstName}} {{.LastName}}, + Greeting: Здравствуйте {{.DisplayName}}, Text: Мы получили запрос на сброс пароля. Пожалуйста, нажмите кнопку ниже для сброса вашего пароля. (Код {{.Code}}) Если вы не запрашивали это письмо, пожалуйста, проигнорируйте его. ButtonText: Сбросить пароль VerifyEmail: Title: Подтверждение email PreHeader: Подтверждение email Subject: Подтверждение email - Greeting: Здравствуйте {{.FirstName}} {{.LastName}}, + Greeting: Здравствуйте {{.DisplayName}}, Text: Добавлен новый email. Пожалуйста, нажмите кнопку ниже для подтверждения вашего email. (Код {{.Code}}) Если вы не запрашивали это письмо, пожалуйста, проигнорируйте его. ButtonText: Подтвердить email VerifyPhone: Title: Подтверждение телефона PreHeader: Подтверждение телефона Subject: Подтверждение телефона - Greeting: Здравствуйте {{.FirstName}} {{.LastName}}, + Greeting: Здравствуйте {{.DisplayName}}, Text: Добавлен новый номер телефона. Пожалуйста, используйте следующий код, чтобы подтвердить его. Код {{.Code}} ButtonText: Подтвердить телефон VerifyEmailOTP: @@ -42,20 +42,27 @@ DomainClaimed: Title: Утверждение домена PreHeader: Изменение email / логина Subject: Домен был утвержден - Greeting: Здравствуйте {{.FirstName}} {{.LastName}}, + Greeting: Здравствуйте {{.DisplayName}}, Text: Домен {{.Domain}} был утвержден организацией. Ваш текущий пользователь {{.Username}} не является частью этой организации. Вам необходимо изменить свой email при входе в систему. Мы создали временный логин ({{.TempUsername}}) для входа. ButtonText: Вход PasswordlessRegistration: Title: Добавление входа без пароля PreHeader: Добавление входа без пароля Subject: Добавление входа без пароля - Greeting: Здравствуйте {{.FirstName}} {{.LastName}}, + Greeting: Здравствуйте {{.DisplayName}}, Text: Мы получили запрос на добавление токена для входа без пароля. Пожалуйста, используйте кнопку ниже, чтобы добавить свой токен или устройство для входа без пароля. ButtonText: Добавить вход без пароля PasswordChange: Title: Смена пароля пользователя PreHeader: Смена пароля Subject: Пароль пользователя изменен - Greeting: Здравствуйте {{.FirstName}} {{.LastName}}, + Greeting: Здравствуйте {{.DisplayName}}, Text: Пароль пользователя был изменен. Если это изменение сделано не вами, советуем немедленно сбросить пароль. ButtonText: Вход +InviteUser: + Title: Приглашение в {{.ApplicationName}} + PreHeader: Приглашение в {{.ApplicationName}} + Subject: Приглашение в {{.ApplicationName}} + Greeting: Здравствуйте, {{.DisplayName}}, + Text: Ваш пользователь был приглашен в {{.ApplicationName}}. Пожалуйста, нажмите кнопку ниже, чтобы завершить процесс приглашения. Если вы не запрашивали это письмо, пожалуйста, игнорируйте его. + ButtonText: Принять приглашение \ No newline at end of file diff --git a/internal/notification/static/i18n/sv.yaml b/internal/notification/static/i18n/sv.yaml index 4788644c79..49c79a6790 100644 --- a/internal/notification/static/i18n/sv.yaml +++ b/internal/notification/static/i18n/sv.yaml @@ -59,3 +59,10 @@ PasswordChange: Greeting: Hej {{.DisplayName}}, Text: Lösenordet för din användare har ändrats. Om denna ändring inte gjordes av dig, vänligen återställ ditt lösenord omedelbart. ButtonText: Logga in +InviteUser: + Title: Inbjudan till {{.ApplicationName}} + PreHeader: Inbjudan till {{.ApplicationName}} + Subject: Inbjudan till {{.ApplicationName}} + Greeting: Hej {{.DisplayName}}, + Text: Din användare har blivit inbjuden till {{.ApplicationName}}. Klicka på knappen nedan för att slutföra inbjudansprocessen. Om du inte har begärt detta e-postmeddelande, ignorera det. + ButtonText: Acceptera inbjudan \ No newline at end of file diff --git a/internal/notification/static/i18n/zh.yaml b/internal/notification/static/i18n/zh.yaml index b0dd21cad3..4b83ebac79 100644 --- a/internal/notification/static/i18n/zh.yaml +++ b/internal/notification/static/i18n/zh.yaml @@ -59,3 +59,10 @@ PasswordChange: Greeting: 你好 {{.DisplayName}}, Text: 您的用户的密码已经改变,如果这个改变不是由您做的,请注意立即重新设置您的密码。 ButtonText: 登录 +InviteUser: + Title: '{{.ApplicationName}}邀请' + PreHeader: '{{.ApplicationName}}邀请' + Subject: '{{.ApplicationName}}邀请' + Greeting: 您好,{{.DisplayName}}, + Text: 您的用户已被邀请加入{{.ApplicationName}}。请点击下面的按钮完成邀请过程。如果您没有请求此邮件,请忽略它。 + ButtonText: 接受邀请 \ No newline at end of file diff --git a/internal/notification/templates/templateData.go b/internal/notification/templates/templateData.go index 8ff750da53..2c3885ef75 100644 --- a/internal/notification/templates/templateData.go +++ b/internal/notification/templates/templateData.go @@ -15,23 +15,23 @@ const ( ) type TemplateData struct { - Title string - PreHeader string - Subject string - Greeting string - Text string - URL string - ButtonText string - PrimaryColor string - BackgroundColor string - FontColor string - LogoURL string - FontURL string - FontFaceFamily string - FontFamily string + Title string `json:"title,omitempty"` + PreHeader string `json:"preHeader,omitempty"` + Subject string `json:"subject,omitempty"` + Greeting string `json:"greeting,omitempty"` + Text string `json:"text,omitempty"` + URL string `json:"url,omitempty"` + ButtonText string `json:"buttonText,omitempty"` + PrimaryColor string `json:"primaryColor,omitempty"` + BackgroundColor string `json:"backgroundColor,omitempty"` + FontColor string `json:"fontColor,omitempty"` + LogoURL string `json:"logoUrl,omitempty"` + FontURL string `json:"fontUrl,omitempty"` + FontFaceFamily string `json:"fontFaceFamily,omitempty"` + FontFamily string `json:"fontFamily,omitempty"` - IncludeFooter bool - FooterText string + IncludeFooter bool `json:"includeFooter,omitempty"` + FooterText string `json:"footerText,omitempty"` } func (data *TemplateData) Translate(translator *i18n.Translator, msgType string, args map[string]interface{}, langs ...string) { diff --git a/internal/notification/types/invite_code.go b/internal/notification/types/invite_code.go new file mode 100644 index 0000000000..953124a553 --- /dev/null +++ b/internal/notification/types/invite_code.go @@ -0,0 +1,31 @@ +package types + +import ( + "context" + "strings" + + http_utils "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/api/ui/login" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" +) + +func (notify Notify) SendInviteCode(ctx context.Context, user *query.NotifyUser, code, applicationName, urlTmpl, authRequestID string) error { + var url string + if applicationName == "" { + applicationName = "ZITADEL" + } + if urlTmpl == "" { + url = login.InviteUserLink(http_utils.DomainContext(ctx).Origin(), user.ID, user.PreferredLoginName, code, user.ResourceOwner, authRequestID) + } else { + var buf strings.Builder + if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil { + return err + } + url = buf.String() + } + args := make(map[string]interface{}) + args["Code"] = code + args["ApplicationName"] = applicationName + return notify(url, args, domain.InviteUserMessageType, true) +} diff --git a/internal/notification/types/notification.go b/internal/notification/types/notification.go index 9bbfb66c8f..8d1b013164 100644 --- a/internal/notification/types/notification.go +++ b/internal/notification/types/notification.go @@ -7,8 +7,8 @@ import ( "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/notification/channels/smtp" - "github.com/zitadel/zitadel/internal/notification/channels/twilio" + "github.com/zitadel/zitadel/internal/notification/channels/email" + "github.com/zitadel/zitadel/internal/notification/channels/sms" "github.com/zitadel/zitadel/internal/notification/channels/webhook" "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/notification/templates" @@ -23,8 +23,8 @@ type Notify func( ) error type ChannelChains interface { - Email(context.Context) (*senders.Chain, *smtp.Config, error) - SMS(context.Context) (*senders.Chain, *twilio.Config, error) + Email(context.Context) (*senders.Chain, *email.Config, error) + SMS(context.Context) (*senders.Chain, *sms.Config, error) Webhook(context.Context, webhook.Config) (*senders.Chain, error) } @@ -54,8 +54,9 @@ func SendEmail( ctx, channels, user, - data.Subject, template, + data, + args, allowUnverifiedNotificationChannel, triggeringEvent, ) @@ -79,7 +80,7 @@ func sanitizeArgsForHTML(args map[string]any) { } } -func SendSMSTwilio( +func SendSMS( ctx context.Context, channels ChannelChains, translator *i18n.Translator, @@ -99,7 +100,8 @@ func SendSMSTwilio( ctx, channels, user, - data.Text, + data, + args, allowUnverifiedNotificationChannel, triggeringEvent, ) diff --git a/internal/notification/types/user_email.go b/internal/notification/types/user_email.go index d3c3bfdd4a..210ca14cf8 100644 --- a/internal/notification/types/user_email.go +++ b/internal/notification/types/user_email.go @@ -3,9 +3,13 @@ package types import ( "context" "html" + "strings" + + "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/notification/messages" + "github.com/zitadel/zitadel/internal/notification/templates" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -14,29 +18,56 @@ func generateEmail( ctx context.Context, channels ChannelChains, user *query.NotifyUser, - subject, - content string, + template string, + data templates.TemplateData, + args map[string]interface{}, lastEmail bool, triggeringEvent eventstore.Event, ) error { - content = html.UnescapeString(content) - message := &messages.Email{ - Recipients: []string{user.VerifiedEmail}, - Subject: subject, - Content: content, - TriggeringEvent: triggeringEvent, - } - if lastEmail { - message.Recipients = []string{user.LastEmail} - } - emailChannels, _, err := channels.Email(ctx) - if err != nil { - return err - } + emailChannels, config, err := channels.Email(ctx) + logging.OnError(err).Error("could not create email channel") if emailChannels == nil || emailChannels.Len() == 0 { - return zerrors.ThrowPreconditionFailed(nil, "MAIL-83nof", "Errors.Notification.Channels.NotPresent") + return zerrors.ThrowPreconditionFailed(nil, "PHONE-w8nfow", "Errors.Notification.Channels.NotPresent") } - return emailChannels.HandleMessage(message) + recipient := user.VerifiedEmail + if lastEmail { + recipient = user.LastEmail + } + if config.SMTPConfig != nil { + message := &messages.Email{ + Recipients: []string{recipient}, + Subject: data.Subject, + Content: html.UnescapeString(template), + TriggeringEvent: triggeringEvent, + } + return emailChannels.HandleMessage(message) + } + if config.WebhookConfig != nil { + caseArgs := make(map[string]interface{}, len(args)) + for k, v := range args { + caseArgs[strings.ToLower(string(k[0]))+k[1:]] = v + } + contextInfo := map[string]interface{}{ + "recipientEmailAddress": recipient, + "eventType": triggeringEvent.Type(), + "provider": config.ProviderConfig, + } + + message := &messages.JSON{ + Serializable: &serializableData{ + ContextInfo: contextInfo, + TemplateData: data, + Args: caseArgs, + }, + TriggeringEvent: triggeringEvent, + } + webhookChannels, err := channels.Webhook(ctx, *config.WebhookConfig) + if err != nil { + return err + } + return webhookChannels.HandleMessage(message) + } + return zerrors.ThrowPreconditionFailed(nil, "MAIL-83nof", "Errors.Notification.Channels.NotPresent") } func mapNotifyUserToArgs(user *query.NotifyUser, args map[string]interface{}) map[string]interface{} { diff --git a/internal/notification/types/user_phone.go b/internal/notification/types/user_phone.go index c9adfeac83..8e79f73718 100644 --- a/internal/notification/types/user_phone.go +++ b/internal/notification/types/user_phone.go @@ -2,40 +2,78 @@ package types import ( "context" + "strings" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/notification/messages" + "github.com/zitadel/zitadel/internal/notification/templates" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" ) +type serializableData struct { + ContextInfo map[string]interface{} `json:"contextInfo,omitempty"` + TemplateData templates.TemplateData `json:"templateData,omitempty"` + Args map[string]interface{} `json:"args,omitempty"` +} + func generateSms( ctx context.Context, channels ChannelChains, user *query.NotifyUser, - content string, + data templates.TemplateData, + args map[string]interface{}, lastPhone bool, triggeringEvent eventstore.Event, ) error { - number := "" - smsChannels, twilioConfig, err := channels.SMS(ctx) + smsChannels, config, err := channels.SMS(ctx) logging.OnError(err).Error("could not create sms channel") if smsChannels == nil || smsChannels.Len() == 0 { return zerrors.ThrowPreconditionFailed(nil, "PHONE-w8nfow", "Errors.Notification.Channels.NotPresent") } - if err == nil { - number = twilioConfig.SenderNumber - } - message := &messages.SMS{ - SenderPhoneNumber: number, - RecipientPhoneNumber: user.VerifiedPhone, - Content: content, - TriggeringEvent: triggeringEvent, - } + recipient := user.VerifiedPhone if lastPhone { - message.RecipientPhoneNumber = user.LastPhone + recipient = user.LastPhone } - return smsChannels.HandleMessage(message) + if config.TwilioConfig != nil { + number := "" + if err == nil { + number = config.TwilioConfig.SenderNumber + } + message := &messages.SMS{ + SenderPhoneNumber: number, + RecipientPhoneNumber: recipient, + Content: data.Text, + TriggeringEvent: triggeringEvent, + } + return smsChannels.HandleMessage(message) + } + if config.WebhookConfig != nil { + caseArgs := make(map[string]interface{}, len(args)) + for k, v := range args { + caseArgs[strings.ToLower(string(k[0]))+k[1:]] = v + } + contextInfo := map[string]interface{}{ + "recipientPhoneNumber": recipient, + "eventType": triggeringEvent.Type(), + "provider": config.ProviderConfig, + } + + message := &messages.JSON{ + Serializable: &serializableData{ + TemplateData: data, + Args: caseArgs, + ContextInfo: contextInfo, + }, + TriggeringEvent: triggeringEvent, + } + webhookChannels, err := channels.Webhook(ctx, *config.WebhookConfig) + if err != nil { + return err + } + return webhookChannels.HandleMessage(message) + } + return zerrors.ThrowPreconditionFailed(nil, "PHONE-w8nfow", "Errors.Notification.Channels.NotPresent") } diff --git a/internal/query/access_token.go b/internal/query/access_token.go index 4180a6ad5e..a5edc068cd 100644 --- a/internal/query/access_token.go +++ b/internal/query/access_token.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/shopspring/decimal" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" @@ -140,7 +141,7 @@ func (q *Queries) accessTokenByOIDCSessionAndTokenID(ctx context.Context, oidcSe // checkSessionNotTerminatedAfter checks if a [session.TerminateType] event (or user events leading to a session termination) // occurred after a certain time and will return an error if so. -func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, userID string, position float64, fingerprintID string) (err error) { +func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, userID string, position decimal.Decimal, fingerprintID string) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -162,7 +163,7 @@ func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, } type sessionTerminatedModel struct { - position float64 + position decimal.Decimal sessionID string userID string fingerPrintID string @@ -182,7 +183,7 @@ func (s *sessionTerminatedModel) AppendEvents(events ...eventstore.Event) { func (s *sessionTerminatedModel) Query() *eventstore.SearchQueryBuilder { query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - PositionAfter(s.position). + PositionGreaterEqual(s.position). AddQuery(). AggregateTypes(session.AggregateType). AggregateIDs(s.sessionID). diff --git a/internal/query/authn_key.go b/internal/query/authn_key.go index 679f444327..6c05a03f6f 100644 --- a/internal/query/authn_key.go +++ b/internal/query/authn_key.go @@ -3,6 +3,7 @@ package query import ( "context" "database/sql" + _ "embed" "errors" "time" @@ -249,6 +250,44 @@ func NewAuthNKeyObjectIDQuery(id string) (SearchQuery, error) { return NewTextQuery(AuthNKeyColumnObjectID, id, TextEquals) } +//go:embed authn_key_user.sql +var authNKeyUserQuery string + +type AuthNKeyUser struct { + UserID string + ResourceOwner string + Username string + TokenType domain.OIDCTokenType + PublicKey []byte +} + +func (q *Queries) GetAuthNKeyUser(ctx context.Context, keyID, userID string) (_ *AuthNKeyUser, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + dst := new(AuthNKeyUser) + err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { + return row.Scan( + &dst.UserID, + &dst.ResourceOwner, + &dst.Username, + &dst.TokenType, + &dst.PublicKey, + ) + }, + authNKeyUserQuery, + authz.GetInstance(ctx).InstanceID(), + keyID, userID, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-Tha6f", "Errors.AuthNKey.NotFound") + } + return nil, zerrors.ThrowInternal(err, "QUERY-aen2A", "Errors.Internal") + } + return dst, nil +} + func prepareAuthNKeysQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys, error)) { return sq.Select( AuthNKeyColumnID.identifier(), diff --git a/internal/query/authn_key_test.go b/internal/query/authn_key_test.go index 5f5084de35..19005893f8 100644 --- a/internal/query/authn_key_test.go +++ b/internal/query/authn_key_test.go @@ -8,6 +8,11 @@ import ( "regexp" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -470,3 +475,65 @@ func Test_AuthNKeyPrepares(t *testing.T) { }) } } + +func TestQueries_GetAuthNKeyUser(t *testing.T) { + expQuery := regexp.QuoteMeta(authNKeyUserQuery) + cols := []string{"user_id", "resource_owner", "username", "access_token_type", "public_key"} + pubkey := []byte(`-----BEGIN RSA PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2ufAL1b72bIy1ar+Ws6b +GohJJQFB7dfRapDqeqM8Ukp6CVdPzq/pOz1viAq50yzWZJryF+2wshFAKGF9A2/B +2Yf9bJXPZ/KbkFrYT3NTvYDkvlaSTl9mMnzrU29s48F1PTWKfB+C3aMsOEG1BufV +s63qF4nrEPjSbhljIco9FZq4XppIzhMQ0fDdA/+XygCJqvuaL0LibM1KrlUdnu71 +YekhSJjEPnvOisXIk4IXywoGIOwtjxkDvNItQvaMVldr4/kb6uvbgdWwq5EwBZXq +low2kyJov38V4Uk2I8kuXpLcnrpw5Tio2ooiUE27b0vHZqBKOei9Uo88qCrn3EKx +6QIDAQAB +-----END RSA PUBLIC KEY-----`) + + tests := []struct { + name string + mock sqlExpectation + want *AuthNKeyUser + wantErr error + }{ + { + name: "no rows", + mock: mockQueryErr(expQuery, sql.ErrNoRows, "instanceID", "keyID", "userID"), + wantErr: zerrors.ThrowNotFound(sql.ErrNoRows, "QUERY-Tha6f", "Errors.AuthNKey.NotFound"), + }, + { + name: "internal error", + mock: mockQueryErr(expQuery, sql.ErrConnDone, "instanceID", "keyID", "userID"), + wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-aen2A", "Errors.Internal"), + }, + { + name: "success", + mock: mockQuery(expQuery, cols, + []driver.Value{"userID", "orgID", "username", domain.OIDCTokenTypeJWT, pubkey}, + "instanceID", "keyID", "userID", + ), + want: &AuthNKeyUser{ + UserID: "userID", + ResourceOwner: "orgID", + Username: "username", + TokenType: domain.OIDCTokenTypeJWT, + PublicKey: pubkey, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + execMock(t, tt.mock, func(db *sql.DB) { + q := &Queries{ + client: &database.DB{ + DB: db, + Database: &prepareDB{}, + }, + } + ctx := authz.NewMockContext("instanceID", "orgID", "userID") + got, err := q.GetAuthNKeyUser(ctx, "keyID", "userID") + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + }) + } +} diff --git a/internal/query/authn_key_user.sql b/internal/query/authn_key_user.sql new file mode 100644 index 0000000000..e8eb4c3a1b --- /dev/null +++ b/internal/query/authn_key_user.sql @@ -0,0 +1,11 @@ +select u.id as user_id, u.resource_owner, u.username, m.access_token_type, k.public_key +from projections.authn_keys2 k +join projections.users13 u + on k.instance_id = u.instance_id + and k.identifier = u.id +join projections.users13_machines m + on u.instance_id = m.instance_id + and u.id = m.user_id +where k.instance_id = $1 + and k.id = $2 + and u.id = $3; diff --git a/internal/query/current_state.go b/internal/query/current_state.go index 29497e6eec..790b594c2d 100644 --- a/internal/query/current_state.go +++ b/internal/query/current_state.go @@ -10,6 +10,7 @@ import ( "time" sq "github.com/Masterminds/squirrel" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/call" @@ -26,7 +27,7 @@ type Stateful interface { type State struct { LastRun time.Time - Position float64 + Position decimal.Decimal EventCreatedAt time.Time AggregateID string AggregateType eventstore.AggregateType @@ -221,7 +222,7 @@ func prepareLatestState(ctx context.Context, db prepareDatabase) (sq.SelectBuild var ( creationDate sql.NullTime lastUpdated sql.NullTime - position sql.NullFloat64 + position decimal.NullDecimal ) err := row.Scan( &creationDate, @@ -234,7 +235,7 @@ func prepareLatestState(ctx context.Context, db prepareDatabase) (sq.SelectBuild return &State{ EventCreatedAt: creationDate.Time, LastRun: lastUpdated.Time, - Position: position.Float64, + Position: position.Decimal, }, nil } } @@ -259,7 +260,7 @@ func prepareCurrentStateQuery(ctx context.Context, db prepareDatabase) (sq.Selec var ( lastRun sql.NullTime eventDate sql.NullTime - currentPosition sql.NullFloat64 + currentPosition decimal.NullDecimal aggregateType sql.NullString aggregateID sql.NullString sequence sql.NullInt64 @@ -280,7 +281,7 @@ func prepareCurrentStateQuery(ctx context.Context, db prepareDatabase) (sq.Selec } currentState.State.EventCreatedAt = eventDate.Time currentState.State.LastRun = lastRun.Time - currentState.Position = currentPosition.Float64 + currentState.Position = currentPosition.Decimal currentState.AggregateType = eventstore.AggregateType(aggregateType.String) currentState.AggregateID = aggregateID.String currentState.Sequence = uint64(sequence.Int64) diff --git a/internal/query/current_state_test.go b/internal/query/current_state_test.go index c76dae710e..f17509aa9c 100644 --- a/internal/query/current_state_test.go +++ b/internal/query/current_state_test.go @@ -7,6 +7,8 @@ import ( "fmt" "regexp" "testing" + + "github.com/shopspring/decimal" ) var ( @@ -87,7 +89,7 @@ func Test_CurrentSequencesPrepares(t *testing.T) { State: State{ EventCreatedAt: testNow, LastRun: testNow, - Position: 20211108, + Position: decimal.NewFromInt(20211108), AggregateID: "agg-id", AggregateType: "agg-type", Sequence: 20211108, @@ -134,7 +136,7 @@ func Test_CurrentSequencesPrepares(t *testing.T) { ProjectionName: "projection-name", State: State{ EventCreatedAt: testNow, - Position: 20211108, + Position: decimal.NewFromInt(20211108), LastRun: testNow, AggregateID: "agg-id", AggregateType: "agg-type", @@ -145,7 +147,7 @@ func Test_CurrentSequencesPrepares(t *testing.T) { ProjectionName: "projection-name2", State: State{ EventCreatedAt: testNow, - Position: 20211108, + Position: decimal.NewFromInt(20211108), LastRun: testNow, AggregateID: "agg-id", AggregateType: "agg-type", diff --git a/internal/query/debug_events.go b/internal/query/debug_events.go new file mode 100644 index 0000000000..cc3e6e6cd6 --- /dev/null +++ b/internal/query/debug_events.go @@ -0,0 +1,106 @@ +package query + +import ( + "context" + "database/sql" + _ "embed" + "errors" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type DebugEventState struct { + domain.ObjectDetails + Blob string +} + +var ( + //go:embed debug_events_state_by_id.sql + debugEventsStateByIdQuery string + //go:embed debug_events_states.sql + debugEventsStatesQuery string +) + +func (q *Queries) GetDebugEventsStateByID(ctx context.Context, id string, triggerBulk bool) (_ *DebugEventState, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + ctx, err = triggerDebugEventsProjection(ctx, triggerBulk) + if err != nil { + return nil, err + } + + dst := new(DebugEventState) + err = q.client.QueryRowContext(ctx, + func(row *sql.Row) error { + return row.Scan( + &dst.ID, + &dst.CreationDate, + &dst.EventDate, + &dst.ResourceOwner, + &dst.Sequence, + &dst.Blob, + ) + }, + debugEventsStateByIdQuery, + authz.GetInstance(ctx).InstanceID(), + id, + ) + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-Eeth5", "debug event state not found") + } + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-oe0Ae", "Errors.Internal") + } + return dst, err +} + +func (q *Queries) ListDebugEventsStates(ctx context.Context, triggerBulk bool) (out []DebugEventState, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + ctx, err = triggerDebugEventsProjection(ctx, triggerBulk) + if err != nil { + return nil, err + } + + err = q.client.QueryContext(ctx, + func(rows *sql.Rows) error { + for rows.Next() { + var dst DebugEventState + err := rows.Scan( + &dst.ID, + &dst.CreationDate, + &dst.EventDate, + &dst.ResourceOwner, + &dst.Sequence, + &dst.Blob, + ) + if err != nil { + return err + } + out = append(out, dst) + } + return rows.Err() + }, + debugEventsStatesQuery, + authz.GetInstance(ctx).InstanceID(), + ) + + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-nooZ2", "Errors.Internal") + } + return out, nil +} + +func triggerDebugEventsProjection(ctx context.Context, trigger bool) (_ context.Context, err error) { + if trigger { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + return projection.DebugEventsProjection.Trigger(ctx, handler.WithAwaitRunning()) + } + return ctx, nil +} diff --git a/internal/query/debug_events_state_by_id.sql b/internal/query/debug_events_state_by_id.sql new file mode 100644 index 0000000000..a91f38f9d1 --- /dev/null +++ b/internal/query/debug_events_state_by_id.sql @@ -0,0 +1,5 @@ +select id, creation_date, change_date, resource_owner, sequence, blob +from projections.debug_events +where instance_id = $1 +and id = $2 +limit 1; diff --git a/internal/query/debug_events_states.sql b/internal/query/debug_events_states.sql new file mode 100644 index 0000000000..ec3e66bb89 --- /dev/null +++ b/internal/query/debug_events_states.sql @@ -0,0 +1,4 @@ +select id, creation_date, change_date, resource_owner, sequence, blob +from projections.debug_events +where instance_id = $1 +order by creation_date asc; diff --git a/internal/query/message_text.go b/internal/query/message_text.go index dd2d9cd50b..cb524d289a 100644 --- a/internal/query/message_text.go +++ b/internal/query/message_text.go @@ -33,6 +33,7 @@ type MessageTexts struct { DomainClaimed MessageText PasswordlessRegistration MessageText PasswordChange MessageText + InviteUser MessageText } type MessageText struct { @@ -346,6 +347,8 @@ func (m *MessageTexts) GetMessageTextByType(msgType string) *MessageText { return &m.PasswordlessRegistration case domain.PasswordChangeMessageType: return &m.PasswordChange + case domain.InviteUserMessageType: + return &m.InviteUser } return nil } diff --git a/internal/query/projection/debug_events.go b/internal/query/projection/debug_events.go new file mode 100644 index 0000000000..687bcc2c95 --- /dev/null +++ b/internal/query/projection/debug_events.go @@ -0,0 +1,146 @@ +package projection + +import ( + "context" + + "github.com/muhlemmer/gu" + + "github.com/zitadel/zitadel/internal/eventstore" + old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/debug_events" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const ( + DebugEventsProjectionTable = "projections.debug_events" + + DebugEventsColumnID = "id" + DebugEventsColumnCreationDate = "creation_date" + DebugEventsColumnChangeDate = "change_date" + DebugEventsColumnResourceOwner = "resource_owner" + DebugEventsColumnInstanceID = "instance_id" + DebugEventsColumnSequence = "sequence" + DebugEventsColumnBlob = "blob" +) + +type debugEventsProjection struct{} + +func (*debugEventsProjection) Name() string { + return DebugEventsProjectionTable +} + +func newDebugEventsProjection(ctx context.Context, config handler.Config) *handler.Handler { + return handler.NewHandler(ctx, &config, new(debugEventsProjection)) +} + +// Init implements [handler.initializer] +func (p *debugEventsProjection) Init() *old_handler.Check { + return handler.NewTableCheck( + handler.NewTable([]*handler.InitColumn{ + handler.NewColumn(DebugEventsColumnID, handler.ColumnTypeText), + handler.NewColumn(DebugEventsColumnCreationDate, handler.ColumnTypeTimestamp), + handler.NewColumn(DebugEventsColumnChangeDate, handler.ColumnTypeTimestamp), + handler.NewColumn(DebugEventsColumnResourceOwner, handler.ColumnTypeText), + handler.NewColumn(DebugEventsColumnInstanceID, handler.ColumnTypeText), + handler.NewColumn(DebugEventsColumnSequence, handler.ColumnTypeInt64), + handler.NewColumn(DebugEventsColumnBlob, handler.ColumnTypeText), + }, + handler.NewPrimaryKey(DebugEventsColumnInstanceID, DebugEventsColumnID), + ), + ) +} + +func (p *debugEventsProjection) Reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: debug_events.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: debug_events.AddedEventType, + Reduce: p.reduceDebugEventAdded, + }, + { + Event: debug_events.ChangedEventType, + Reduce: p.reduceDebugEventChanged, + }, + { + Event: debug_events.RemovedEventType, + Reduce: p.reduceDebugEventRemoved, + }, + }, + }, + { + Aggregate: instance.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: instance.InstanceRemovedEventType, + Reduce: reduceInstanceRemovedHelper(DebugEventsColumnInstanceID), + }, + }, + }, + } +} + +func (p *debugEventsProjection) reduceDebugEventAdded(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*debug_events.AddedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-uYq4r", "reduce.wrong.event.type %s", debug_events.AddedEventType) + } + return handler.NewMultiStatement( + e, + handler.AddSleepStatement(e.ProjectionSleep), + handler.AddCreateStatement([]handler.Column{ + handler.NewCol(DebugEventsColumnID, e.Aggregate().ID), + handler.NewCol(DebugEventsColumnCreationDate, e.CreationDate()), + handler.NewCol(DebugEventsColumnChangeDate, e.CreationDate()), + handler.NewCol(DebugEventsColumnResourceOwner, e.Aggregate().ResourceOwner), + handler.NewCol(DebugEventsColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(DebugEventsColumnSequence, e.Sequence()), + handler.NewCol(DebugEventsColumnBlob, gu.Value(e.Blob)), + }), + ), nil +} + +func (p *debugEventsProjection) reduceDebugEventChanged(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*debug_events.ChangedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Bg8oM", "reduce.wrong.event.type %s", debug_events.ChangedEventType) + } + updateCols := []handler.Column{ + handler.NewCol(DebugEventsColumnChangeDate, e.CreationDate()), + handler.NewCol(DebugEventsColumnSequence, e.Sequence()), + } + if e.Blob != nil { + updateCols = append(updateCols, + handler.NewCol(DebugEventsColumnBlob, *e.Blob), + ) + } + + return handler.NewMultiStatement( + e, + handler.AddSleepStatement(e.ProjectionSleep), + handler.AddUpdateStatement(updateCols, + []handler.Condition{ + handler.NewCond(DebugEventsColumnID, e.Aggregate().ID), + handler.NewCond(DebugEventsColumnInstanceID, e.Aggregate().InstanceID), + }), + ), nil +} + +func (p *debugEventsProjection) reduceDebugEventRemoved(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*debug_events.RemovedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-DgMSg", "reduce.wrong.event.type %s", debug_events.RemovedEventType) + } + return handler.NewMultiStatement( + e, + handler.AddSleepStatement(e.ProjectionSleep), + handler.AddDeleteStatement( + []handler.Condition{ + handler.NewCond(DebugEventsColumnID, e.Aggregate().ID), + handler.NewCond(DebugEventsColumnInstanceID, e.Aggregate().InstanceID), + }), + ), nil +} diff --git a/internal/query/projection/message_texts.go b/internal/query/projection/message_texts.go index a588e7e98a..8a001db905 100644 --- a/internal/query/projection/message_texts.go +++ b/internal/query/projection/message_texts.go @@ -272,7 +272,8 @@ func isMessageTemplate(template string) bool { template == domain.VerifyEmailOTPMessageType || template == domain.DomainClaimedMessageType || template == domain.PasswordlessRegistrationMessageType || - template == domain.PasswordChangeMessageType + template == domain.PasswordChangeMessageType || + template == domain.InviteUserMessageType } func isTitle(key string) bool { return key == domain.MessageTitle diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 0151a9953b..c4660c6c38 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -79,6 +79,7 @@ var ( ExecutionProjection *handler.Handler UserSchemaProjection *handler.Handler WebKeyProjection *handler.Handler + DebugEventsProjection *handler.Handler ProjectGrantFields *handler.FieldHandler OrgDomainVerifiedFields *handler.FieldHandler @@ -165,6 +166,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, ExecutionProjection = newExecutionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["executions"])) UserSchemaProjection = newUserSchemaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_schemas"])) WebKeyProjection = newWebKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["web_keys"])) + DebugEventsProjection = newDebugEventsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["debug_events"])) ProjectGrantFields = newFillProjectGrantFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsProjectGrant])) OrgDomainVerifiedFields = newFillOrgDomainVerifiedFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsOrgDomainVerified])) @@ -295,5 +297,6 @@ func newProjectionsList() { ExecutionProjection, UserSchemaProjection, WebKeyProjection, + DebugEventsProjection, } } diff --git a/internal/query/projection/sms.go b/internal/query/projection/sms.go index 13059203f9..9b157ff992 100644 --- a/internal/query/projection/sms.go +++ b/internal/query/projection/sms.go @@ -8,12 +8,12 @@ import ( old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/repository/instance" - "github.com/zitadel/zitadel/internal/zerrors" ) const ( - SMSConfigProjectionTable = "projections.sms_configs2" + SMSConfigProjectionTable = "projections.sms_configs3" SMSTwilioTable = SMSConfigProjectionTable + "_" + smsTwilioTableSuffix + SMSHTTPTable = SMSConfigProjectionTable + "_" + smsHTTPTableSuffix SMSColumnID = "id" SMSColumnAggregateID = "aggregate_id" @@ -23,13 +23,19 @@ const ( SMSColumnState = "state" SMSColumnResourceOwner = "resource_owner" SMSColumnInstanceID = "instance_id" + SMSColumnDescription = "description" - smsTwilioTableSuffix = "twilio" - SMSTwilioConfigColumnSMSID = "sms_id" - SMSTwilioColumnInstanceID = "instance_id" - SMSTwilioConfigColumnSID = "sid" - SMSTwilioConfigColumnSenderNumber = "sender_number" - SMSTwilioConfigColumnToken = "token" + smsTwilioTableSuffix = "twilio" + SMSTwilioColumnSMSID = "sms_id" + SMSTwilioColumnInstanceID = "instance_id" + SMSTwilioColumnSID = "sid" + SMSTwilioColumnSenderNumber = "sender_number" + SMSTwilioColumnToken = "token" + + smsHTTPTableSuffix = "http" + SMSHTTPColumnSMSID = "sms_id" + SMSHTTPColumnInstanceID = "instance_id" + SMSHTTPColumnEndpoint = "endpoint" ) type smsConfigProjection struct{} @@ -53,20 +59,30 @@ func (*smsConfigProjection) Init() *old_handler.Check { handler.NewColumn(SMSColumnState, handler.ColumnTypeEnum), handler.NewColumn(SMSColumnResourceOwner, handler.ColumnTypeText), handler.NewColumn(SMSColumnInstanceID, handler.ColumnTypeText), + handler.NewColumn(SMSColumnDescription, handler.ColumnTypeText), }, handler.NewPrimaryKey(SMSColumnInstanceID, SMSColumnID), ), handler.NewSuffixedTable([]*handler.InitColumn{ - handler.NewColumn(SMSTwilioConfigColumnSMSID, handler.ColumnTypeText), + handler.NewColumn(SMSTwilioColumnSMSID, handler.ColumnTypeText), handler.NewColumn(SMSTwilioColumnInstanceID, handler.ColumnTypeText), - handler.NewColumn(SMSTwilioConfigColumnSID, handler.ColumnTypeText), - handler.NewColumn(SMSTwilioConfigColumnSenderNumber, handler.ColumnTypeText), - handler.NewColumn(SMSTwilioConfigColumnToken, handler.ColumnTypeJSONB), + handler.NewColumn(SMSTwilioColumnSID, handler.ColumnTypeText), + handler.NewColumn(SMSTwilioColumnSenderNumber, handler.ColumnTypeText), + handler.NewColumn(SMSTwilioColumnToken, handler.ColumnTypeJSONB), }, - handler.NewPrimaryKey(SMSTwilioColumnInstanceID, SMSTwilioConfigColumnSMSID), + handler.NewPrimaryKey(SMSTwilioColumnInstanceID, SMSTwilioColumnSMSID), smsTwilioTableSuffix, handler.WithForeignKey(handler.NewForeignKeyOfPublicKeys()), ), + handler.NewSuffixedTable([]*handler.InitColumn{ + handler.NewColumn(SMSHTTPColumnSMSID, handler.ColumnTypeText), + handler.NewColumn(SMSHTTPColumnInstanceID, handler.ColumnTypeText), + handler.NewColumn(SMSHTTPColumnEndpoint, handler.ColumnTypeText), + }, + handler.NewPrimaryKey(SMSHTTPColumnInstanceID, SMSHTTPColumnSMSID), + smsHTTPTableSuffix, + handler.WithForeignKey(handler.NewForeignKeyOfPublicKeys()), + ), ) } @@ -87,6 +103,26 @@ func (p *smsConfigProjection) Reducers() []handler.AggregateReducer { Event: instance.SMSConfigTwilioTokenChangedEventType, Reduce: p.reduceSMSConfigTwilioTokenChanged, }, + { + Event: instance.SMSConfigHTTPAddedEventType, + Reduce: p.reduceSMSConfigHTTPAdded, + }, + { + Event: instance.SMSConfigHTTPChangedEventType, + Reduce: p.reduceSMSConfigHTTPChanged, + }, + { + Event: instance.SMSConfigTwilioActivatedEventType, + Reduce: p.reduceSMSConfigTwilioActivated, + }, + { + Event: instance.SMSConfigTwilioDeactivatedEventType, + Reduce: p.reduceSMSConfigTwilioDeactivated, + }, + { + Event: instance.SMSConfigTwilioRemovedEventType, + Reduce: p.reduceSMSConfigTwilioRemoved, + }, { Event: instance.SMSConfigActivatedEventType, Reduce: p.reduceSMSConfigActivated, @@ -109,9 +145,9 @@ func (p *smsConfigProjection) Reducers() []handler.AggregateReducer { } func (p *smsConfigProjection) reduceSMSConfigTwilioAdded(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMSConfigTwilioAddedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-s8efs", "reduce.wrong.event.type %s", instance.SMSConfigTwilioAddedEventType) + e, err := assertEvent[*instance.SMSConfigTwilioAddedEvent](event) + if err != nil { + return nil, err } return handler.NewMultiStatement( @@ -126,15 +162,16 @@ func (p *smsConfigProjection) reduceSMSConfigTwilioAdded(event eventstore.Event) handler.NewCol(SMSColumnInstanceID, e.Aggregate().InstanceID), handler.NewCol(SMSColumnState, domain.SMSConfigStateInactive), handler.NewCol(SMSColumnSequence, e.Sequence()), + handler.NewCol(SMSColumnDescription, e.Description), }, ), handler.AddCreateStatement( []handler.Column{ - handler.NewCol(SMSTwilioConfigColumnSMSID, e.ID), + handler.NewCol(SMSTwilioColumnSMSID, e.ID), handler.NewCol(SMSTwilioColumnInstanceID, e.Aggregate().InstanceID), - handler.NewCol(SMSTwilioConfigColumnSID, e.SID), - handler.NewCol(SMSTwilioConfigColumnToken, e.Token), - handler.NewCol(SMSTwilioConfigColumnSenderNumber, e.SenderNumber), + handler.NewCol(SMSTwilioColumnSID, e.SID), + handler.NewCol(SMSTwilioColumnToken, e.Token), + handler.NewCol(SMSTwilioColumnSenderNumber, e.SenderNumber), }, handler.WithTableSuffix(smsTwilioTableSuffix), ), @@ -142,57 +179,64 @@ func (p *smsConfigProjection) reduceSMSConfigTwilioAdded(event eventstore.Event) } func (p *smsConfigProjection) reduceSMSConfigTwilioChanged(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMSConfigTwilioChangedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-fi99F", "reduce.wrong.event.type %s", instance.SMSConfigTwilioChangedEventType) - } - columns := make([]handler.Column, 0) - if e.SID != nil { - columns = append(columns, handler.NewCol(SMSTwilioConfigColumnSID, *e.SID)) - } - if e.SenderNumber != nil { - columns = append(columns, handler.NewCol(SMSTwilioConfigColumnSenderNumber, *e.SenderNumber)) + e, err := assertEvent[*instance.SMSConfigTwilioChangedEvent](event) + if err != nil { + return nil, err } - return handler.NewMultiStatement( - e, - handler.AddUpdateStatement( + stmts := make([]func(eventstore.Event) handler.Exec, 0, 3) + columns := []handler.Column{ + handler.NewCol(SMSColumnChangeDate, e.CreationDate()), + handler.NewCol(SMSColumnSequence, e.Sequence()), + } + if e.Description != nil { + columns = append(columns, handler.NewCol(SMSColumnDescription, *e.Description)) + } + if len(columns) > 0 { + stmts = append(stmts, handler.AddUpdateStatement( columns, - []handler.Condition{ - handler.NewCond(SMSTwilioConfigColumnSMSID, e.ID), - handler.NewCond(SMSTwilioColumnInstanceID, e.Aggregate().InstanceID), - }, - handler.WithTableSuffix(smsTwilioTableSuffix), - ), - handler.AddUpdateStatement( - []handler.Column{ - handler.NewCol(SMSColumnChangeDate, e.CreationDate()), - handler.NewCol(SMSColumnSequence, e.Sequence()), - }, []handler.Condition{ handler.NewCond(SMSColumnID, e.ID), handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), }, - ), - ), nil + )) + } + + twilioColumns := make([]handler.Column, 0) + if e.SID != nil { + twilioColumns = append(twilioColumns, handler.NewCol(SMSTwilioColumnSID, *e.SID)) + } + if e.SenderNumber != nil { + twilioColumns = append(twilioColumns, handler.NewCol(SMSTwilioColumnSenderNumber, *e.SenderNumber)) + } + if len(twilioColumns) > 0 { + stmts = append(stmts, handler.AddUpdateStatement( + twilioColumns, + []handler.Condition{ + handler.NewCond(SMSTwilioColumnSMSID, e.ID), + handler.NewCond(SMSTwilioColumnInstanceID, e.Aggregate().InstanceID), + }, + handler.WithTableSuffix(smsTwilioTableSuffix), + )) + } + + return handler.NewMultiStatement(e, stmts...), nil } func (p *smsConfigProjection) reduceSMSConfigTwilioTokenChanged(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMSConfigTwilioTokenChangedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-fi99F", "reduce.wrong.event.type %s", instance.SMSConfigTwilioTokenChangedEventType) - } - columns := make([]handler.Column, 0) - if e.Token != nil { - columns = append(columns, handler.NewCol(SMSTwilioConfigColumnToken, e.Token)) + e, err := assertEvent[*instance.SMSConfigTwilioTokenChangedEvent](event) + if err != nil { + return nil, err } return handler.NewMultiStatement( e, handler.AddUpdateStatement( - columns, + []handler.Column{ + handler.NewCol(SMSTwilioColumnToken, e.Token), + }, []handler.Condition{ - handler.NewCond(SMSTwilioConfigColumnSMSID, e.ID), + handler.NewCond(SMSTwilioColumnSMSID, e.ID), handler.NewCond(SMSTwilioColumnInstanceID, e.Aggregate().InstanceID), }, handler.WithTableSuffix(smsTwilioTableSuffix), @@ -210,15 +254,122 @@ func (p *smsConfigProjection) reduceSMSConfigTwilioTokenChanged(event eventstore ), nil } -func (p *smsConfigProjection) reduceSMSConfigActivated(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMSConfigActivatedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-fj9Ef", "reduce.wrong.event.type %s", instance.SMSConfigActivatedEventType) +func (p *smsConfigProjection) reduceSMSConfigHTTPAdded(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMSConfigHTTPAddedEvent](event) + if err != nil { + return nil, err } + + return handler.NewMultiStatement( + e, + handler.AddCreateStatement( + []handler.Column{ + handler.NewCol(SMSColumnID, e.ID), + handler.NewCol(SMSColumnAggregateID, e.Aggregate().ID), + handler.NewCol(SMSColumnCreationDate, e.CreationDate()), + handler.NewCol(SMSColumnChangeDate, e.CreationDate()), + handler.NewCol(SMSColumnResourceOwner, e.Aggregate().ResourceOwner), + handler.NewCol(SMSColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(SMSColumnState, domain.SMSConfigStateInactive), + handler.NewCol(SMSColumnSequence, e.Sequence()), + handler.NewCol(SMSColumnDescription, e.Description), + }, + ), + handler.AddCreateStatement( + []handler.Column{ + handler.NewCol(SMSHTTPColumnSMSID, e.ID), + handler.NewCol(SMSHTTPColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(SMSHTTPColumnEndpoint, e.Endpoint), + }, + handler.WithTableSuffix(smsHTTPTableSuffix), + ), + ), nil +} + +func (p *smsConfigProjection) reduceSMSConfigHTTPChanged(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMSConfigHTTPChangedEvent](event) + if err != nil { + return nil, err + } + + stmts := make([]func(eventstore.Event) handler.Exec, 0, 3) + columns := []handler.Column{ + handler.NewCol(SMSColumnChangeDate, e.CreationDate()), + handler.NewCol(SMSColumnSequence, e.Sequence()), + } + if e.Description != nil { + columns = append(columns, handler.NewCol(SMSColumnDescription, *e.Description)) + } + if len(columns) > 0 { + stmts = append(stmts, handler.AddUpdateStatement( + columns, + []handler.Condition{ + handler.NewCond(SMSColumnID, e.ID), + handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), + }, + )) + } + + if e.Endpoint != nil { + stmts = append(stmts, handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(SMSHTTPColumnEndpoint, *e.Endpoint), + }, + []handler.Condition{ + handler.NewCond(SMSHTTPColumnSMSID, e.ID), + handler.NewCond(SMSHTTPColumnInstanceID, e.Aggregate().InstanceID), + }, + handler.WithTableSuffix(smsHTTPTableSuffix), + )) + } + + return handler.NewMultiStatement(e, stmts...), nil +} + +func (p *smsConfigProjection) reduceSMSConfigTwilioActivated(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMSConfigTwilioActivatedEvent](event) + if err != nil { + return nil, err + } + + return handler.NewMultiStatement( + e, + handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(SMSColumnState, domain.SMSConfigStateInactive), + handler.NewCol(SMSColumnChangeDate, e.CreationDate()), + handler.NewCol(SMSColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.Not(handler.NewCond(SMSColumnID, e.ID)), + handler.NewCond(SMSColumnState, domain.SMSConfigStateActive), + handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), + }, + ), + handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(SMSColumnState, domain.SMSConfigStateActive), + handler.NewCol(SMSColumnChangeDate, e.CreationDate()), + handler.NewCol(SMSColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(SMSColumnID, e.ID), + handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), + }, + ), + ), nil +} + +func (p *smsConfigProjection) reduceSMSConfigTwilioDeactivated(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMSConfigTwilioDeactivatedEvent](event) + if err != nil { + return nil, err + } + return handler.NewUpdateStatement( e, []handler.Column{ - handler.NewCol(SMSColumnState, domain.SMSConfigStateActive), + handler.NewCol(SMSColumnState, domain.SMSConfigStateInactive), handler.NewCol(SMSColumnChangeDate, e.CreationDate()), handler.NewCol(SMSColumnSequence, e.Sequence()), }, @@ -229,11 +380,61 @@ func (p *smsConfigProjection) reduceSMSConfigActivated(event eventstore.Event) ( ), nil } -func (p *smsConfigProjection) reduceSMSConfigDeactivated(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMSConfigDeactivatedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-dj9Js", "reduce.wrong.event.type %s", instance.SMSConfigDeactivatedEventType) +func (p *smsConfigProjection) reduceSMSConfigTwilioRemoved(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMSConfigTwilioRemovedEvent](event) + if err != nil { + return nil, err } + + return handler.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(SMSColumnID, e.ID), + handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), + }, + ), nil +} + +func (p *smsConfigProjection) reduceSMSConfigActivated(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMSConfigActivatedEvent](event) + if err != nil { + return nil, err + } + + return handler.NewMultiStatement( + e, + handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(SMSColumnState, domain.SMSConfigStateInactive), + handler.NewCol(SMSColumnChangeDate, e.CreationDate()), + handler.NewCol(SMSColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.Not(handler.NewCond(SMSColumnID, e.ID)), + handler.NewCond(SMSColumnState, domain.SMSConfigStateActive), + handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), + }, + ), + handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(SMSColumnState, domain.SMSConfigStateActive), + handler.NewCol(SMSColumnChangeDate, e.CreationDate()), + handler.NewCol(SMSColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(SMSColumnID, e.ID), + handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), + }, + ), + ), nil +} + +func (p *smsConfigProjection) reduceSMSConfigDeactivated(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMSConfigDeactivatedEvent](event) + if err != nil { + return nil, err + } + return handler.NewUpdateStatement( e, []handler.Column{ @@ -249,10 +450,11 @@ func (p *smsConfigProjection) reduceSMSConfigDeactivated(event eventstore.Event) } func (p *smsConfigProjection) reduceSMSConfigRemoved(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMSConfigRemovedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-s9JJf", "reduce.wrong.event.type %s", instance.SMSConfigRemovedEventType) + e, err := assertEvent[*instance.SMSConfigRemovedEvent](event) + if err != nil { + return nil, err } + return handler.NewDeleteStatement( e, []handler.Condition{ diff --git a/internal/query/projection/sms_test.go b/internal/query/projection/sms_test.go index 1e1a384ed2..88ce6e4417 100644 --- a/internal/query/projection/sms_test.go +++ b/internal/query/projection/sms_test.go @@ -37,9 +37,10 @@ func TestSMSProjection_reduces(t *testing.T) { "keyId": "key-id", "crypted": "Y3J5cHRlZA==" }, - "senderNumber": "sender-number" + "senderNumber": "sender-number", + "description": "description" }`), - ), instance.SMSConfigTwilioAddedEventMapper), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioAddedEvent]), }, reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioAdded, want: wantReduce{ @@ -48,7 +49,7 @@ func TestSMSProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.sms_configs2 (id, aggregate_id, creation_date, change_date, resource_owner, instance_id, state, sequence) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + expectedStmt: "INSERT INTO projections.sms_configs3 (id, aggregate_id, creation_date, change_date, resource_owner, instance_id, state, sequence, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "id", "agg-id", @@ -58,10 +59,11 @@ func TestSMSProjection_reduces(t *testing.T) { "instance-id", domain.SMSConfigStateInactive, uint64(15), + "description", }, }, { - expectedStmt: "INSERT INTO projections.sms_configs2_twilio (sms_id, instance_id, sid, token, sender_number) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.sms_configs3_twilio (sms_id, instance_id, sid, token, sender_number) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "id", "instance-id", @@ -89,9 +91,10 @@ func TestSMSProjection_reduces(t *testing.T) { []byte(`{ "id": "id", "sid": "sid", - "senderNumber": "sender-number" + "senderNumber": "sender-number", + "description": "description" }`), - ), instance.SMSConfigTwilioChangedEventMapper), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioChangedEvent]), }, reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioChanged, want: wantReduce{ @@ -100,7 +103,17 @@ func TestSMSProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sms_configs2_twilio SET (sid, sender_number) = ($1, $2) WHERE (sms_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "description", + "id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.sms_configs3_twilio SET (sid, sender_number) = ($1, $2) WHERE (sms_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "sid", "sender-number", @@ -108,11 +121,75 @@ func TestSMSProjection_reduces(t *testing.T) { "instance-id", }, }, + }, + }, + }, + }, + { + name: "instance reduceSMSConfigTwilioChanged, only description", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigTwilioChangedEventType, + instance.AggregateType, + []byte(`{ + "id": "id", + "description": "description" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioChangedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ { - expectedStmt: "UPDATE projections.sms_configs2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), + "description", + "id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceSMSConfigTwilioChanged, only sid", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigTwilioChangedEventType, + instance.AggregateType, + []byte(`{ + "id": "id", + "sid": "sid" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioChangedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.sms_configs3_twilio SET sid = $1 WHERE (sms_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "sid", "id", "instance-id", }, @@ -137,7 +214,7 @@ func TestSMSProjection_reduces(t *testing.T) { "crypted": "Y3J5cHRlZA==" } }`), - ), instance.SMSConfigTwilioTokenChangedEventMapper), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioTokenChangedEvent]), }, reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioTokenChanged, want: wantReduce{ @@ -146,7 +223,7 @@ func TestSMSProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sms_configs2_twilio SET token = $1 WHERE (sms_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.sms_configs3_twilio SET token = $1 WHERE (sms_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, @@ -159,7 +236,7 @@ func TestSMSProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.sms_configs2 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -171,6 +248,270 @@ func TestSMSProjection_reduces(t *testing.T) { }, }, }, + { + name: "instance reduceSMSHTTPAdded", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigHTTPAddedEventType, + instance.AggregateType, + []byte(`{ + "id": "id", + "description": "description", + "endpoint": "endpoint" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigHTTPAddedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigHTTPAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.sms_configs3 (id, aggregate_id, creation_date, change_date, resource_owner, instance_id, state, sequence, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedArgs: []interface{}{ + "id", + "agg-id", + anyArg{}, + anyArg{}, + "ro-id", + "instance-id", + domain.SMSConfigStateInactive, + uint64(15), + "description", + }, + }, + { + expectedStmt: "INSERT INTO projections.sms_configs3_http (sms_id, instance_id, endpoint) VALUES ($1, $2, $3)", + expectedArgs: []interface{}{ + "id", + "instance-id", + "endpoint", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceSMSConfigHTTPChanged", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigHTTPChangedEventType, + instance.AggregateType, + []byte(`{ + "id": "id", + "endpoint": "endpoint", + "description": "description" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigHTTPChangedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigHTTPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "description", + "id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.sms_configs3_http SET endpoint = $1 WHERE (sms_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "endpoint", + "id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceSMSConfigHTTPChanged, only description", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigHTTPChangedEventType, + instance.AggregateType, + []byte(`{ + "id": "id", + "description": "description" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigHTTPChangedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigHTTPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "description", + "id", + "instance-id", + }, + }, + }, + }, + }, + }, { + name: "instance reduceSMSConfigHTTPChanged, only endpoint", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigHTTPChangedEventType, + instance.AggregateType, + []byte(`{ + "id": "id", + "endpoint": "endpoint" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigHTTPChangedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigHTTPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.sms_configs3_http SET endpoint = $1 WHERE (sms_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "endpoint", + "id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceSMSConfigTwilioActivated", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigTwilioActivatedEventType, + instance.AggregateType, + []byte(`{ + "id": "id" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioActivatedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioActivated, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.sms_configs3 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (NOT (id = $4)) AND (state = $5) AND (instance_id = $6)", + expectedArgs: []interface{}{ + domain.SMSConfigStateInactive, + anyArg{}, + uint64(15), + "id", + domain.SMSConfigStateActive, + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.sms_configs3 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + domain.SMSConfigStateActive, + anyArg{}, + uint64(15), + "id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceSMSConfigTwilioDeactivated", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigTwilioDeactivatedEventType, + instance.AggregateType, + []byte(`{ + "id": "id" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioDeactivatedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioDeactivated, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.sms_configs3 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + domain.SMSConfigStateInactive, + anyArg{}, + uint64(15), + "id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "instance reduceSMSConfigTwilioRemoved", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigTwilioRemovedEventType, + instance.AggregateType, + []byte(`{ + "id": "id" + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigTwilioRemovedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigTwilioRemoved, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.sms_configs3 WHERE (id = $1) AND (instance_id = $2)", + expectedArgs: []interface{}{ + "id", + "instance-id", + }, + }, + }, + }, + }, + }, { name: "instance reduceSMSConfigActivated", args: args{ @@ -181,7 +522,7 @@ func TestSMSProjection_reduces(t *testing.T) { []byte(`{ "id": "id" }`), - ), instance.SMSConfigActivatedEventMapper), + ), eventstore.GenericEventMapper[instance.SMSConfigActivatedEvent]), }, reduce: (&smsConfigProjection{}).reduceSMSConfigActivated, want: wantReduce{ @@ -190,7 +531,18 @@ func TestSMSProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sms_configs2 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sms_configs3 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (NOT (id = $4)) AND (state = $5) AND (instance_id = $6)", + expectedArgs: []interface{}{ + domain.SMSConfigStateInactive, + anyArg{}, + uint64(15), + "id", + domain.SMSConfigStateActive, + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.sms_configs3 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ domain.SMSConfigStateActive, anyArg{}, @@ -213,7 +565,7 @@ func TestSMSProjection_reduces(t *testing.T) { []byte(`{ "id": "id" }`), - ), instance.SMSConfigDeactivatedEventMapper), + ), eventstore.GenericEventMapper[instance.SMSConfigDeactivatedEvent]), }, reduce: (&smsConfigProjection{}).reduceSMSConfigDeactivated, want: wantReduce{ @@ -222,7 +574,7 @@ func TestSMSProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sms_configs2 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sms_configs3 SET (state, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ domain.SMSConfigStateInactive, anyArg{}, @@ -245,7 +597,7 @@ func TestSMSProjection_reduces(t *testing.T) { []byte(`{ "id": "id" }`), - ), instance.SMSConfigRemovedEventMapper), + ), eventstore.GenericEventMapper[instance.SMSConfigRemovedEvent]), }, reduce: (&smsConfigProjection{}).reduceSMSConfigRemoved, want: wantReduce{ @@ -254,7 +606,7 @@ func TestSMSProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.sms_configs2 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.sms_configs3 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "id", "instance-id", @@ -281,7 +633,7 @@ func TestSMSProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.sms_configs2 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.sms_configs3 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, diff --git a/internal/query/projection/smtp.go b/internal/query/projection/smtp.go index 9b53da4150..df8ba50471 100644 --- a/internal/query/projection/smtp.go +++ b/internal/query/projection/smtp.go @@ -8,26 +8,38 @@ import ( old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/repository/instance" - "github.com/zitadel/zitadel/internal/zerrors" ) const ( - SMTPConfigProjectionTable = "projections.smtp_configs2" - SMTPConfigColumnInstanceID = "instance_id" - SMTPConfigColumnResourceOwner = "resource_owner" - SMTPConfigColumnID = "id" - SMTPConfigColumnCreationDate = "creation_date" - SMTPConfigColumnChangeDate = "change_date" - SMTPConfigColumnSequence = "sequence" - SMTPConfigColumnTLS = "tls" - SMTPConfigColumnSenderAddress = "sender_address" - SMTPConfigColumnSenderName = "sender_name" - SMTPConfigColumnReplyToAddress = "reply_to_address" - SMTPConfigColumnSMTPHost = "host" - SMTPConfigColumnSMTPUser = "username" - SMTPConfigColumnSMTPPassword = "password" - SMTPConfigColumnState = "state" - SMTPConfigColumnDescription = "description" + SMTPConfigProjectionTable = "projections.smtp_configs4" + SMTPConfigTable = SMTPConfigProjectionTable + "_" + smtpConfigSMTPTableSuffix + SMTPConfigHTTPTable = SMTPConfigProjectionTable + "_" + smtpConfigHTTPTableSuffix + + SMTPConfigColumnInstanceID = "instance_id" + SMTPConfigColumnResourceOwner = "resource_owner" + SMTPConfigColumnAggregateID = "aggregate_id" + SMTPConfigColumnID = "id" + SMTPConfigColumnCreationDate = "creation_date" + SMTPConfigColumnChangeDate = "change_date" + SMTPConfigColumnSequence = "sequence" + SMTPConfigColumnState = "state" + SMTPConfigColumnDescription = "description" + + smtpConfigSMTPTableSuffix = "smtp" + SMTPConfigSMTPColumnInstanceID = "instance_id" + SMTPConfigSMTPColumnID = "id" + SMTPConfigSMTPColumnTLS = "tls" + SMTPConfigSMTPColumnSenderAddress = "sender_address" + SMTPConfigSMTPColumnSenderName = "sender_name" + SMTPConfigSMTPColumnReplyToAddress = "reply_to_address" + SMTPConfigSMTPColumnHost = "host" + SMTPConfigSMTPColumnUser = "username" + SMTPConfigSMTPColumnPassword = "password" + + smtpConfigHTTPTableSuffix = "http" + SMTPConfigHTTPColumnInstanceID = "instance_id" + SMTPConfigHTTPColumnID = "id" + SMTPConfigHTTPColumnEndpoint = "endpoint" ) type smtpConfigProjection struct{} @@ -41,25 +53,43 @@ func (*smtpConfigProjection) Name() string { } func (*smtpConfigProjection) Init() *old_handler.Check { - return handler.NewTableCheck( + return handler.NewMultiTableCheck( handler.NewTable([]*handler.InitColumn{ handler.NewColumn(SMTPConfigColumnID, handler.ColumnTypeText), + handler.NewColumn(SMTPConfigColumnAggregateID, handler.ColumnTypeText), handler.NewColumn(SMTPConfigColumnCreationDate, handler.ColumnTypeTimestamp), handler.NewColumn(SMTPConfigColumnChangeDate, handler.ColumnTypeTimestamp), handler.NewColumn(SMTPConfigColumnSequence, handler.ColumnTypeInt64), handler.NewColumn(SMTPConfigColumnResourceOwner, handler.ColumnTypeText), handler.NewColumn(SMTPConfigColumnInstanceID, handler.ColumnTypeText), - handler.NewColumn(SMTPConfigColumnTLS, handler.ColumnTypeBool), - handler.NewColumn(SMTPConfigColumnSenderAddress, handler.ColumnTypeText), - handler.NewColumn(SMTPConfigColumnSenderName, handler.ColumnTypeText), - handler.NewColumn(SMTPConfigColumnReplyToAddress, handler.ColumnTypeText), - handler.NewColumn(SMTPConfigColumnSMTPHost, handler.ColumnTypeText), - handler.NewColumn(SMTPConfigColumnSMTPUser, handler.ColumnTypeText), - handler.NewColumn(SMTPConfigColumnSMTPPassword, handler.ColumnTypeJSONB, handler.Nullable()), - handler.NewColumn(SMTPConfigColumnState, handler.ColumnTypeEnum), handler.NewColumn(SMTPConfigColumnDescription, handler.ColumnTypeText), + handler.NewColumn(SMTPConfigColumnState, handler.ColumnTypeEnum), }, - handler.NewPrimaryKey(SMTPConfigColumnInstanceID, SMTPConfigColumnResourceOwner, SMTPConfigColumnID), + handler.NewPrimaryKey(SMTPConfigColumnInstanceID, SMTPConfigColumnID), + ), + handler.NewSuffixedTable([]*handler.InitColumn{ + handler.NewColumn(SMTPConfigSMTPColumnID, handler.ColumnTypeText), + handler.NewColumn(SMTPConfigSMTPColumnInstanceID, handler.ColumnTypeText), + handler.NewColumn(SMTPConfigSMTPColumnTLS, handler.ColumnTypeBool), + handler.NewColumn(SMTPConfigSMTPColumnSenderAddress, handler.ColumnTypeText), + handler.NewColumn(SMTPConfigSMTPColumnSenderName, handler.ColumnTypeText), + handler.NewColumn(SMTPConfigSMTPColumnReplyToAddress, handler.ColumnTypeText), + handler.NewColumn(SMTPConfigSMTPColumnHost, handler.ColumnTypeText), + handler.NewColumn(SMTPConfigSMTPColumnUser, handler.ColumnTypeText), + handler.NewColumn(SMTPConfigSMTPColumnPassword, handler.ColumnTypeJSONB, handler.Nullable()), + }, + handler.NewPrimaryKey(SMTPConfigSMTPColumnInstanceID, SMTPConfigSMTPColumnID), + smtpConfigSMTPTableSuffix, + handler.WithForeignKey(handler.NewForeignKeyOfPublicKeys()), + ), + handler.NewSuffixedTable([]*handler.InitColumn{ + handler.NewColumn(SMTPConfigHTTPColumnID, handler.ColumnTypeText), + handler.NewColumn(SMTPConfigHTTPColumnInstanceID, handler.ColumnTypeText), + handler.NewColumn(SMTPConfigHTTPColumnEndpoint, handler.ColumnTypeText), + }, + handler.NewPrimaryKey(SMTPConfigHTTPColumnInstanceID, SMTPConfigHTTPColumnID), + smtpConfigHTTPTableSuffix, + handler.WithForeignKey(handler.NewForeignKeyOfPublicKeys()), ), ) } @@ -81,6 +111,14 @@ func (p *smtpConfigProjection) Reducers() []handler.AggregateReducer { Event: instance.SMTPConfigPasswordChangedEventType, Reduce: p.reduceSMTPConfigPasswordChanged, }, + { + Event: instance.SMTPConfigHTTPAddedEventType, + Reduce: p.reduceSMTPConfigHTTPAdded, + }, + { + Event: instance.SMTPConfigHTTPChangedEventType, + Reduce: p.reduceSMTPConfigHTTPChanged, + }, { Event: instance.SMTPConfigActivatedEventType, Reduce: p.reduceSMTPConfigActivated, @@ -103,9 +141,9 @@ func (p *smtpConfigProjection) Reducers() []handler.AggregateReducer { } func (p *smtpConfigProjection) reduceSMTPConfigAdded(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMTPConfigAddedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-sk99F", "reduce.wrong.event.type %s", instance.SMTPConfigAddedEventType) + e, err := assertEvent[*instance.SMTPConfigAddedEvent](event) + if err != nil { + return nil, err } // Deal with old and unique SMTP settings (empty ID) @@ -118,83 +156,116 @@ func (p *smtpConfigProjection) reduceSMTPConfigAdded(event eventstore.Event) (*h state = domain.SMTPConfigStateActive } - return handler.NewCreateStatement( + return handler.NewMultiStatement( e, - []handler.Column{ - handler.NewCol(SMTPConfigColumnCreationDate, e.CreationDate()), - handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()), - handler.NewCol(SMTPConfigColumnResourceOwner, e.Aggregate().ResourceOwner), - handler.NewCol(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), - handler.NewCol(SMTPConfigColumnSequence, e.Sequence()), - handler.NewCol(SMTPConfigColumnID, id), - handler.NewCol(SMTPConfigColumnTLS, e.TLS), - handler.NewCol(SMTPConfigColumnSenderAddress, e.SenderAddress), - handler.NewCol(SMTPConfigColumnSenderName, e.SenderName), - handler.NewCol(SMTPConfigColumnReplyToAddress, e.ReplyToAddress), - handler.NewCol(SMTPConfigColumnSMTPHost, e.Host), - handler.NewCol(SMTPConfigColumnSMTPUser, e.User), - handler.NewCol(SMTPConfigColumnSMTPPassword, e.Password), - handler.NewCol(SMTPConfigColumnState, state), - handler.NewCol(SMTPConfigColumnDescription, description), - }, + handler.AddCreateStatement( + []handler.Column{ + handler.NewCol(SMTPConfigColumnCreationDate, e.CreationDate()), + handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()), + handler.NewCol(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(SMTPConfigColumnResourceOwner, e.Aggregate().ResourceOwner), + handler.NewCol(SMTPConfigColumnAggregateID, e.Aggregate().ID), + handler.NewCol(SMTPConfigColumnID, id), + handler.NewCol(SMTPConfigColumnSequence, e.Sequence()), + handler.NewCol(SMTPConfigColumnState, state), + handler.NewCol(SMTPConfigColumnDescription, description), + }, + ), + handler.AddCreateStatement( + []handler.Column{ + handler.NewCol(SMTPConfigSMTPColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(SMTPConfigSMTPColumnID, id), + handler.NewCol(SMTPConfigSMTPColumnTLS, e.TLS), + handler.NewCol(SMTPConfigSMTPColumnSenderAddress, e.SenderAddress), + handler.NewCol(SMTPConfigSMTPColumnSenderName, e.SenderName), + handler.NewCol(SMTPConfigSMTPColumnReplyToAddress, e.ReplyToAddress), + handler.NewCol(SMTPConfigSMTPColumnHost, e.Host), + handler.NewCol(SMTPConfigSMTPColumnUser, e.User), + handler.NewCol(SMTPConfigSMTPColumnPassword, e.Password), + }, + handler.WithTableSuffix(smtpConfigSMTPTableSuffix), + ), ), nil } -func (p *smtpConfigProjection) reduceSMTPConfigChanged(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMTPConfigChangedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-wl0wd", "reduce.wrong.event.type %s", instance.SMTPConfigChangedEventType) +func (p *smtpConfigProjection) reduceSMTPConfigHTTPAdded(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMTPConfigHTTPAddedEvent](event) + if err != nil { + return nil, err } - columns := make([]handler.Column, 0, 8) - columns = append(columns, handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()), - handler.NewCol(SMTPConfigColumnSequence, e.Sequence())) + return handler.NewMultiStatement( + e, + handler.AddCreateStatement( + []handler.Column{ + handler.NewCol(SMTPConfigColumnCreationDate, e.CreationDate()), + handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()), + handler.NewCol(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(SMTPConfigColumnResourceOwner, e.Aggregate().ResourceOwner), + handler.NewCol(SMTPConfigColumnAggregateID, e.Aggregate().ID), + handler.NewCol(SMTPConfigColumnID, e.ID), + handler.NewCol(SMTPConfigColumnSequence, e.Sequence()), + handler.NewCol(SMTPConfigColumnState, domain.SMTPConfigStateInactive), + handler.NewCol(SMTPConfigColumnDescription, e.Description), + }, + ), + handler.AddCreateStatement( + []handler.Column{ + handler.NewCol(SMTPConfigSMTPColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(SMTPConfigSMTPColumnID, e.ID), + handler.NewCol(SMTPConfigHTTPColumnEndpoint, e.Endpoint), + }, + handler.WithTableSuffix(smtpConfigHTTPTableSuffix), + ), + ), nil +} - // Deal with old and unique SMTP settings (empty ID) - id := e.ID - if e.ID == "" { - id = e.Aggregate().ResourceOwner +func (p *smtpConfigProjection) reduceSMTPConfigHTTPChanged(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMTPConfigHTTPChangedEvent](event) + if err != nil { + return nil, err } - if e.TLS != nil { - columns = append(columns, handler.NewCol(SMTPConfigColumnTLS, *e.TLS)) - } - if e.FromAddress != nil { - columns = append(columns, handler.NewCol(SMTPConfigColumnSenderAddress, *e.FromAddress)) - } - if e.FromName != nil { - columns = append(columns, handler.NewCol(SMTPConfigColumnSenderName, *e.FromName)) - } - if e.ReplyToAddress != nil { - columns = append(columns, handler.NewCol(SMTPConfigColumnReplyToAddress, *e.ReplyToAddress)) - } - if e.Host != nil { - columns = append(columns, handler.NewCol(SMTPConfigColumnSMTPHost, *e.Host)) - } - if e.User != nil { - columns = append(columns, handler.NewCol(SMTPConfigColumnSMTPUser, *e.User)) - } - if e.Password != nil { - columns = append(columns, handler.NewCol(SMTPConfigColumnSMTPPassword, *e.Password)) + stmts := make([]func(eventstore.Event) handler.Exec, 0, 3) + columns := []handler.Column{ + handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()), + handler.NewCol(SMTPConfigColumnSequence, e.Sequence()), } if e.Description != nil { columns = append(columns, handler.NewCol(SMTPConfigColumnDescription, *e.Description)) } - return handler.NewUpdateStatement( - e, - columns, - []handler.Condition{ - handler.NewCond(SMTPConfigColumnID, id), - handler.NewCond(SMTPConfigColumnResourceOwner, e.Aggregate().ResourceOwner), - handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), - }, - ), nil + if len(columns) > 0 { + stmts = append(stmts, handler.AddUpdateStatement( + columns, + []handler.Condition{ + handler.NewCond(SMTPConfigColumnID, e.ID), + handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), + }, + )) + } + + smtpColumns := make([]handler.Column, 0, 1) + if e.Endpoint != nil { + smtpColumns = append(smtpColumns, handler.NewCol(SMTPConfigHTTPColumnEndpoint, *e.Endpoint)) + } + if len(smtpColumns) > 0 { + stmts = append(stmts, handler.AddUpdateStatement( + smtpColumns, + []handler.Condition{ + handler.NewCond(SMTPConfigHTTPColumnID, e.ID), + handler.NewCond(SMTPConfigHTTPColumnInstanceID, e.Aggregate().InstanceID), + }, + handler.WithTableSuffix(smtpConfigHTTPTableSuffix), + )) + } + + return handler.NewMultiStatement(e, stmts...), nil } -func (p *smtpConfigProjection) reduceSMTPConfigPasswordChanged(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMTPConfigPasswordChangedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-fk02f", "reduce.wrong.event.type %s", instance.SMTPConfigChangedEventType) +func (p *smtpConfigProjection) reduceSMTPConfigChanged(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMTPConfigChangedEvent](event) + if err != nil { + return nil, err } // Deal with old and unique SMTP settings (empty ID) @@ -203,25 +274,101 @@ func (p *smtpConfigProjection) reduceSMTPConfigPasswordChanged(event eventstore. id = e.Aggregate().ResourceOwner } - return handler.NewUpdateStatement( + stmts := make([]func(eventstore.Event) handler.Exec, 0, 3) + columns := []handler.Column{ + handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()), + handler.NewCol(SMTPConfigColumnSequence, e.Sequence()), + } + if e.Description != nil { + columns = append(columns, handler.NewCol(SMTPConfigColumnDescription, *e.Description)) + } + if len(columns) > 0 { + stmts = append(stmts, handler.AddUpdateStatement( + columns, + []handler.Condition{ + handler.NewCond(SMTPConfigColumnID, id), + handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), + }, + )) + } + + httpColumns := make([]handler.Column, 0, 7) + if e.TLS != nil { + httpColumns = append(httpColumns, handler.NewCol(SMTPConfigSMTPColumnTLS, *e.TLS)) + } + if e.FromAddress != nil { + httpColumns = append(httpColumns, handler.NewCol(SMTPConfigSMTPColumnSenderAddress, *e.FromAddress)) + } + if e.FromName != nil { + httpColumns = append(httpColumns, handler.NewCol(SMTPConfigSMTPColumnSenderName, *e.FromName)) + } + if e.ReplyToAddress != nil { + httpColumns = append(httpColumns, handler.NewCol(SMTPConfigSMTPColumnReplyToAddress, *e.ReplyToAddress)) + } + if e.Host != nil { + httpColumns = append(httpColumns, handler.NewCol(SMTPConfigSMTPColumnHost, *e.Host)) + } + if e.User != nil { + httpColumns = append(httpColumns, handler.NewCol(SMTPConfigSMTPColumnUser, *e.User)) + } + if e.Password != nil { + httpColumns = append(httpColumns, handler.NewCol(SMTPConfigSMTPColumnPassword, *e.Password)) + } + if len(httpColumns) > 0 { + stmts = append(stmts, handler.AddUpdateStatement( + httpColumns, + []handler.Condition{ + handler.NewCond(SMTPConfigSMTPColumnID, e.ID), + handler.NewCond(SMTPConfigSMTPColumnInstanceID, e.Aggregate().InstanceID), + }, + handler.WithTableSuffix(smtpConfigSMTPTableSuffix), + )) + } + + return handler.NewMultiStatement(e, stmts...), nil +} + +func (p *smtpConfigProjection) reduceSMTPConfigPasswordChanged(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.SMTPConfigPasswordChangedEvent](event) + if err != nil { + return nil, err + } + + // Deal with old and unique SMTP settings (empty ID) + id := e.ID + if e.ID == "" { + id = e.Aggregate().ResourceOwner + } + + return handler.NewMultiStatement( e, - []handler.Column{ - handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()), - handler.NewCol(SMTPConfigColumnSequence, e.Sequence()), - handler.NewCol(SMTPConfigColumnSMTPPassword, e.Password), - }, - []handler.Condition{ - handler.NewCond(SMTPConfigColumnID, id), - handler.NewCond(SMTPConfigColumnResourceOwner, e.Aggregate().ResourceOwner), - handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), - }, + handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(SMTPConfigSMTPColumnPassword, e.Password), + }, + []handler.Condition{ + handler.NewCond(SMTPConfigColumnID, id), + handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), + }, + handler.WithTableSuffix(smtpConfigSMTPTableSuffix), + ), + handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()), + handler.NewCol(SMTPConfigColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(SMTPConfigColumnID, id), + handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), + }, + ), ), nil } func (p *smtpConfigProjection) reduceSMTPConfigActivated(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMTPConfigActivatedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-fq92r", "reduce.wrong.event.type %s", instance.SMTPConfigActivatedEventType) + e, err := assertEvent[*instance.SMTPConfigActivatedEvent](event) + if err != nil { + return nil, err } // Deal with old and unique SMTP settings (empty ID) @@ -230,25 +377,38 @@ func (p *smtpConfigProjection) reduceSMTPConfigActivated(event eventstore.Event) id = e.Aggregate().ResourceOwner } - return handler.NewUpdateStatement( + return handler.NewMultiStatement( e, - []handler.Column{ - handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()), - handler.NewCol(SMTPConfigColumnSequence, e.Sequence()), - handler.NewCol(SMTPConfigColumnState, domain.SMTPConfigStateActive), - }, - []handler.Condition{ - handler.NewCond(SMTPConfigColumnID, id), - handler.NewCond(SMTPConfigColumnResourceOwner, e.Aggregate().ResourceOwner), - handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), - }, + handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()), + handler.NewCol(SMTPConfigColumnSequence, e.Sequence()), + handler.NewCol(SMTPConfigColumnState, domain.SMTPConfigStateInactive), + }, + []handler.Condition{ + handler.Not(handler.NewCond(SMTPConfigColumnID, e.ID)), + handler.NewCond(SMTPConfigColumnState, domain.SMTPConfigStateActive), + handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), + }, + ), + handler.AddUpdateStatement( + []handler.Column{ + handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()), + handler.NewCol(SMTPConfigColumnSequence, e.Sequence()), + handler.NewCol(SMTPConfigColumnState, domain.SMTPConfigStateActive), + }, + []handler.Condition{ + handler.NewCond(SMTPConfigColumnID, id), + handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), + }, + ), ), nil } func (p *smtpConfigProjection) reduceSMTPConfigDeactivated(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*instance.SMTPConfigDeactivatedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-hv89j", "reduce.wrong.event.type %s", instance.SMTPConfigDeactivatedEventType) + e, err := assertEvent[*instance.SMTPConfigDeactivatedEvent](event) + if err != nil { + return nil, err } // Deal with old and unique SMTP settings (empty ID) @@ -266,7 +426,6 @@ func (p *smtpConfigProjection) reduceSMTPConfigDeactivated(event eventstore.Even }, []handler.Condition{ handler.NewCond(SMTPConfigColumnID, id), - handler.NewCond(SMTPConfigColumnResourceOwner, e.Aggregate().ResourceOwner), handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), }, ), nil @@ -288,7 +447,6 @@ func (p *smtpConfigProjection) reduceSMTPConfigRemoved(event eventstore.Event) ( e, []handler.Condition{ handler.NewCond(SMTPConfigColumnID, id), - handler.NewCond(SMTPConfigColumnResourceOwner, e.Aggregate().ResourceOwner), handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), }, ), nil diff --git a/internal/query/projection/smtp_test.go b/internal/query/projection/smtp_test.go index 4d7b5e4a99..d080d37344 100644 --- a/internal/query/projection/smtp_test.go +++ b/internal/query/projection/smtp_test.go @@ -28,19 +28,20 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { instance.SMTPConfigChangedEventType, instance.AggregateType, []byte(`{ + "instance_id": "instance-id", + "resource_owner": "ro-id", + "aggregate_id": "agg-id", + "id": "config-id", "description": "test", "tls": true, "senderAddress": "sender", "senderName": "name", "replyToAddress": "reply-to", "host": "host", - "user": "user", - "id": "44444", - "resource_owner": "ro-id", - "instance_id": "instance-id" + "user": "user" }`, ), - ), instance.SMTPConfigChangedEventMapper), + ), eventstore.GenericEventMapper[instance.SMTPConfigChangedEvent]), }, reduce: (&smtpConfigProjection{}).reduceSMTPConfigChanged, want: wantReduce{ @@ -49,19 +50,25 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs2 SET (change_date, sequence, tls, sender_address, sender_name, reply_to_address, host, username, description) = ($1, $2, $3, $4, $5, $6, $7, $8, $9) WHERE (id = $10) AND (resource_owner = $11) AND (instance_id = $12)", + expectedStmt: "UPDATE projections.smtp_configs4 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), + "test", + "config-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.smtp_configs4_smtp SET (tls, sender_address, sender_name, reply_to_address, host, username) = ($1, $2, $3, $4, $5, $6) WHERE (id = $7) AND (instance_id = $8)", + expectedArgs: []interface{}{ true, "sender", "name", "reply-to", "host", "user", - "test", - "44444", - "ro-id", + "config-id", "instance-id", }, }, @@ -69,6 +76,277 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, }, + { + name: "reduceSMTPConfigChanged, description", + args: args{ + event: getEvent( + testEvent( + instance.SMTPConfigChangedEventType, + instance.AggregateType, + []byte(`{ + "instance_id": "instance-id", + "resource_owner": "ro-id", + "aggregate_id": "agg-id", + "id": "config-id", + "description": "test" + }`, + ), + ), eventstore.GenericEventMapper[instance.SMTPConfigChangedEvent]), + }, + reduce: (&smtpConfigProjection{}).reduceSMTPConfigChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.smtp_configs4 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "test", + "config-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceSMTPConfigChanged, senderAddress", + args: args{ + event: getEvent( + testEvent( + instance.SMTPConfigChangedEventType, + instance.AggregateType, + []byte(`{ + "instance_id": "instance-id", + "resource_owner": "ro-id", + "aggregate_id": "agg-id", + "id": "config-id", + "senderAddress": "sender" + }`, + ), + ), eventstore.GenericEventMapper[instance.SMTPConfigChangedEvent]), + }, + reduce: (&smtpConfigProjection{}).reduceSMTPConfigChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.smtp_configs4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "config-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.smtp_configs4_smtp SET sender_address = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "sender", + "config-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceSMTPConfigHTTPChanged", + args: args{ + event: getEvent( + testEvent( + instance.SMTPConfigHTTPChangedEventType, + instance.AggregateType, + []byte(`{ + "instance_id": "instance-id", + "resource_owner": "ro-id", + "aggregate_id": "agg-id", + "id": "config-id", + "description": "test", + "endpoint": "endpoint" + }`, + ), + ), eventstore.GenericEventMapper[instance.SMTPConfigHTTPChangedEvent]), + }, + reduce: (&smtpConfigProjection{}).reduceSMTPConfigHTTPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.smtp_configs4 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "test", + "config-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.smtp_configs4_http SET endpoint = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "endpoint", + "config-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceSMTPConfigHTTPChanged, description", + args: args{ + event: getEvent( + testEvent( + instance.SMTPConfigHTTPChangedEventType, + instance.AggregateType, + []byte(`{ + "instance_id": "instance-id", + "resource_owner": "ro-id", + "aggregate_id": "agg-id", + "id": "config-id", + "description": "test" + }`, + ), + ), eventstore.GenericEventMapper[instance.SMTPConfigHTTPChangedEvent]), + }, + reduce: (&smtpConfigProjection{}).reduceSMTPConfigHTTPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.smtp_configs4 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "test", + "config-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceSMTPConfigHTTPChanged, endpoint", + args: args{ + event: getEvent( + testEvent( + instance.SMTPConfigHTTPChangedEventType, + instance.AggregateType, + []byte(`{ + "instance_id": "instance-id", + "resource_owner": "ro-id", + "aggregate_id": "agg-id", + "id": "config-id", + "endpoint": "endpoint" + }`, + ), + ), eventstore.GenericEventMapper[instance.SMTPConfigHTTPChangedEvent]), + }, + reduce: (&smtpConfigProjection{}).reduceSMTPConfigHTTPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.smtp_configs4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "config-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.smtp_configs4_http SET endpoint = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + "endpoint", + "config-id", + "instance-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceSMTPConfigAdded (no id)", + args: args{ + event: getEvent( + testEvent( + instance.SMTPConfigAddedEventType, + instance.AggregateType, + []byte(`{ + "instance_id": "instance-id", + "resource_owner": "ro-id", + "aggregate_id": "agg-id", + "tls": true, + "senderAddress": "sender", + "senderName": "name", + "replyToAddress": "reply-to", + "host": "host", + "user": "user", + "password": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + } + }`), + ), eventstore.GenericEventMapper[instance.SMTPConfigAddedEvent]), + }, + reduce: (&smtpConfigProjection{}).reduceSMTPConfigAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.smtp_configs4 (creation_date, change_date, instance_id, resource_owner, aggregate_id, id, sequence, state, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedArgs: []interface{}{ + anyArg{}, + anyArg{}, + "instance-id", + "ro-id", + "agg-id", + "ro-id", + uint64(15), + domain.SMTPConfigStateActive, + "generic", + }, + }, + { + expectedStmt: "INSERT INTO projections.smtp_configs4_smtp (instance_id, id, tls, sender_address, sender_name, reply_to_address, host, username, password) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedArgs: []interface{}{ + "instance-id", + "ro-id", + true, + "sender", + "name", + "reply-to", + "host", + "user", + anyArg{}, + }, + }, + }, + }, + }, + }, { name: "reduceSMTPConfigAdded", args: args{ @@ -77,9 +355,12 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { instance.SMTPConfigAddedEventType, instance.AggregateType, []byte(`{ - "tls": true, - "id": "id", + "instance_id": "instance-id", + "resource_owner": "ro-id", + "aggregate_id": "agg-id", + "id": "config-id", "description": "test", + "tls": true, "senderAddress": "sender", "senderName": "name", "replyToAddress": "reply-to", @@ -91,7 +372,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { "keyId": "key-id" } }`), - ), instance.SMTPConfigAddedEventMapper), + ), eventstore.GenericEventMapper[instance.SMTPConfigAddedEvent]), }, reduce: (&smtpConfigProjection{}).reduceSMTPConfigAdded, want: wantReduce{ @@ -100,14 +381,24 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.smtp_configs2 (creation_date, change_date, resource_owner, instance_id, sequence, id, tls, sender_address, sender_name, reply_to_address, host, username, password, state, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)", + expectedStmt: "INSERT INTO projections.smtp_configs4 (creation_date, change_date, instance_id, resource_owner, aggregate_id, id, sequence, state, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, - "ro-id", "instance-id", + "ro-id", + "agg-id", + "config-id", uint64(15), - "id", + domain.SMTPConfigStateInactive, + "test", + }, + }, + { + expectedStmt: "INSERT INTO projections.smtp_configs4_smtp (instance_id, id, tls, sender_address, sender_name, reply_to_address, host, username, password) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedArgs: []interface{}{ + "instance-id", + "config-id", true, "sender", "name", @@ -115,10 +406,58 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { "host", "user", anyArg{}, - domain.SMTPConfigState(3), + }, + }, + }, + }, + }, + }, + { + name: "reduceSMTPConfigHTTPAdded", + args: args{ + event: getEvent( + testEvent( + instance.SMTPConfigHTTPAddedEventType, + instance.AggregateType, + []byte(`{ + "instance_id": "instance-id", + "resource_owner": "ro-id", + "aggregate_id": "agg-id", + "id": "config-id", + "description": "test", + "senderAddress": "sender", + "endpoint": "endpoint" + }`), + ), eventstore.GenericEventMapper[instance.SMTPConfigHTTPAddedEvent]), + }, + reduce: (&smtpConfigProjection{}).reduceSMTPConfigHTTPAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.smtp_configs4 (creation_date, change_date, instance_id, resource_owner, aggregate_id, id, sequence, state, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedArgs: []interface{}{ + anyArg{}, + anyArg{}, + "instance-id", + "ro-id", + "agg-id", + "config-id", + uint64(15), + domain.SMTPConfigStateInactive, "test", }, }, + { + expectedStmt: "INSERT INTO projections.smtp_configs4_http (instance_id, id, endpoint) VALUES ($1, $2, $3)", + expectedArgs: []interface{}{ + "instance-id", + "config-id", + "endpoint", + }, + }, }, }, }, @@ -130,9 +469,12 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { instance.SMTPConfigActivatedEventType, instance.AggregateType, []byte(`{ - "id": "config-id" + "instance_id": "instance-id", + "resource_owner": "ro-id", + "aggregate_id": "agg-id", + "id": "config-id" }`), - ), instance.SMTPConfigActivatedEventMapper), + ), eventstore.GenericEventMapper[instance.SMTPConfigActivatedEvent]), }, reduce: (&smtpConfigProjection{}).reduceSMTPConfigActivated, want: wantReduce{ @@ -141,13 +483,23 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs2 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (resource_owner = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.smtp_configs4 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (NOT (id = $4)) AND (state = $5) AND (instance_id = $6)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + domain.SMTPConfigStateInactive, + "config-id", + domain.SMTPConfigStateActive, + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.smtp_configs4 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), domain.SMTPConfigStateActive, "config-id", - "ro-id", "instance-id", }, }, @@ -162,9 +514,12 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { instance.SMTPConfigDeactivatedEventType, instance.AggregateType, []byte(`{ - "id": "config-id" + "instance_id": "instance-id", + "resource_owner": "ro-id", + "aggregate_id": "agg-id", + "id": "config-id" }`), - ), instance.SMTPConfigDeactivatedEventMapper), + ), eventstore.GenericEventMapper[instance.SMTPConfigDeactivatedEvent]), }, reduce: (&smtpConfigProjection{}).reduceSMTPConfigDeactivated, want: wantReduce{ @@ -173,13 +528,12 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs2 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (resource_owner = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.smtp_configs4 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), domain.SMTPConfigStateInactive, "config-id", - "ro-id", "instance-id", }, }, @@ -195,14 +549,17 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { instance.SMTPConfigPasswordChangedEventType, instance.AggregateType, []byte(`{ - "id": "config-id", + "instance_id": "instance-id", + "resource_owner": "ro-id", + "aggregate_id": "agg-id", + "id": "config-id", "password": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" } }`), - ), instance.SMTPConfigPasswordChangedEventMapper), + ), eventstore.GenericEventMapper[instance.SMTPConfigPasswordChangedEvent]), }, reduce: (&smtpConfigProjection{}).reduceSMTPConfigPasswordChanged, want: wantReduce{ @@ -211,13 +568,19 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs2 SET (change_date, sequence, password) = ($1, $2, $3) WHERE (id = $4) AND (resource_owner = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.smtp_configs4_smtp SET password = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + "config-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.smtp_configs4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), - anyArg{}, "config-id", - "ro-id", "instance-id", }, }, @@ -231,8 +594,13 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { event: getEvent(testEvent( instance.SMTPConfigRemovedEventType, instance.AggregateType, - []byte(`{ "id": "config-id"}`), - ), instance.SMTPConfigRemovedEventMapper), + []byte(`{ + "instance_id": "instance-id", + "resource_owner": "ro-id", + "aggregate_id": "agg-id", + "id": "config-id" +}`), + ), eventstore.GenericEventMapper[instance.SMTPConfigRemovedEvent]), }, reduce: (&smtpConfigProjection{}).reduceSMTPConfigRemoved, want: wantReduce{ @@ -241,10 +609,9 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.smtp_configs2 WHERE (id = $1) AND (resource_owner = $2) AND (instance_id = $3)", + expectedStmt: "DELETE FROM projections.smtp_configs4 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "config-id", - "ro-id", "instance-id", }, }, @@ -269,7 +636,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.smtp_configs2 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.smtp_configs4 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, diff --git a/internal/query/sms.go b/internal/query/sms.go index 38c798ba17..6f0555634f 100644 --- a/internal/query/sms.go +++ b/internal/query/sms.go @@ -30,8 +30,10 @@ type SMSConfig struct { ResourceOwner string State domain.SMSConfigState Sequence uint64 + Description string TwilioConfig *Twilio + HTTPConfig *HTTP } type Twilio struct { @@ -40,6 +42,10 @@ type Twilio struct { SenderNumber string } +type HTTP struct { + Endpoint string +} + type SMSConfigsSearchQueries struct { SearchRequest Queries []SearchQuery @@ -58,60 +64,79 @@ var ( name: projection.SMSConfigProjectionTable, instanceIDCol: projection.SMSColumnInstanceID, } - SMSConfigColumnID = Column{ + SMSColumnID = Column{ name: projection.SMSColumnID, table: smsConfigsTable, } - SMSConfigColumnAggregateID = Column{ + SMSColumnAggregateID = Column{ name: projection.SMSColumnAggregateID, table: smsConfigsTable, } - SMSConfigColumnCreationDate = Column{ + SMSColumnCreationDate = Column{ name: projection.SMSColumnCreationDate, table: smsConfigsTable, } - SMSConfigColumnChangeDate = Column{ + SMSColumnChangeDate = Column{ name: projection.SMSColumnChangeDate, table: smsConfigsTable, } - SMSConfigColumnResourceOwner = Column{ + SMSColumnResourceOwner = Column{ name: projection.SMSColumnResourceOwner, table: smsConfigsTable, } - SMSConfigColumnInstanceID = Column{ + SMSColumnInstanceID = Column{ name: projection.SMSColumnInstanceID, table: smsConfigsTable, } - SMSConfigColumnState = Column{ + SMSColumnState = Column{ name: projection.SMSColumnState, table: smsConfigsTable, } - SMSConfigColumnSequence = Column{ + SMSColumnSequence = Column{ name: projection.SMSColumnSequence, table: smsConfigsTable, } + SMSColumnDescription = Column{ + name: projection.SMSColumnDescription, + table: smsConfigsTable, + } ) var ( - smsTwilioConfigsTable = table{ + smsTwilioTable = table{ name: projection.SMSTwilioTable, instanceIDCol: projection.SMSTwilioColumnInstanceID, } - SMSTwilioConfigColumnSMSID = Column{ - name: projection.SMSTwilioConfigColumnSMSID, - table: smsTwilioConfigsTable, + SMSTwilioColumnSMSID = Column{ + name: projection.SMSTwilioColumnSMSID, + table: smsTwilioTable, } - SMSTwilioConfigColumnSID = Column{ - name: projection.SMSTwilioConfigColumnSID, - table: smsTwilioConfigsTable, + SMSTwilioColumnSID = Column{ + name: projection.SMSTwilioColumnSID, + table: smsTwilioTable, } - SMSTwilioConfigColumnToken = Column{ - name: projection.SMSTwilioConfigColumnToken, - table: smsTwilioConfigsTable, + SMSTwilioColumnToken = Column{ + name: projection.SMSTwilioColumnToken, + table: smsTwilioTable, } - SMSTwilioConfigColumnSenderNumber = Column{ - name: projection.SMSTwilioConfigColumnSenderNumber, - table: smsTwilioConfigsTable, + SMSTwilioColumnSenderNumber = Column{ + name: projection.SMSTwilioColumnSenderNumber, + table: smsTwilioTable, + } +) + +var ( + smsHTTPTable = table{ + name: projection.SMSHTTPTable, + instanceIDCol: projection.SMSHTTPColumnInstanceID, + } + SMSHTTPColumnSMSID = Column{ + name: projection.SMSHTTPColumnSMSID, + table: smsHTTPTable, + } + SMSHTTPColumnEndpoint = Column{ + name: projection.SMSHTTPColumnEndpoint, + table: smsHTTPTable, } ) @@ -122,8 +147,8 @@ func (q *Queries) SMSProviderConfigByID(ctx context.Context, id string) (config query, scan := prepareSMSConfigQuery(ctx, q.client) stmt, args, err := query.Where( sq.Eq{ - SMSConfigColumnID.identifier(): id, - SMSConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + SMSColumnID.identifier(): id, + SMSColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }, ).ToSql() if err != nil { @@ -137,17 +162,15 @@ func (q *Queries) SMSProviderConfigByID(ctx context.Context, id string) (config return config, err } -func (q *Queries) SMSProviderConfig(ctx context.Context, queries ...SearchQuery) (config *SMSConfig, err error) { +func (q *Queries) SMSProviderConfigActive(ctx context.Context, instanceID string) (config *SMSConfig, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareSMSConfigQuery(ctx, q.client) - for _, searchQuery := range queries { - query = searchQuery.toQuery(query) - } stmt, args, err := query.Where( sq.Eq{ - SMSConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + SMSColumnInstanceID.identifier(): instanceID, + SMSColumnState.identifier(): domain.SMSConfigStateActive, }, ).ToSql() if err != nil { @@ -168,7 +191,7 @@ func (q *Queries) SearchSMSConfigs(ctx context.Context, queries *SMSConfigsSearc query, scan := prepareSMSConfigsQuery(ctx, q.client) stmt, args, err := queries.toQuery(query). Where(sq.Eq{ - SMSConfigColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + SMSColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-sn9Jf", "Errors.Query.InvalidRequest") @@ -186,30 +209,36 @@ func (q *Queries) SearchSMSConfigs(ctx context.Context, queries *SMSConfigsSearc } func NewSMSProviderStateQuery(state domain.SMSConfigState) (SearchQuery, error) { - return NewNumberQuery(SMSConfigColumnState, state, NumberEquals) + return NewNumberQuery(SMSColumnState, state, NumberEquals) } func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*SMSConfig, error)) { return sq.Select( - SMSConfigColumnID.identifier(), - SMSConfigColumnAggregateID.identifier(), - SMSConfigColumnCreationDate.identifier(), - SMSConfigColumnChangeDate.identifier(), - SMSConfigColumnResourceOwner.identifier(), - SMSConfigColumnState.identifier(), - SMSConfigColumnSequence.identifier(), + SMSColumnID.identifier(), + SMSColumnAggregateID.identifier(), + SMSColumnCreationDate.identifier(), + SMSColumnChangeDate.identifier(), + SMSColumnResourceOwner.identifier(), + SMSColumnState.identifier(), + SMSColumnSequence.identifier(), + SMSColumnDescription.identifier(), - SMSTwilioConfigColumnSMSID.identifier(), - SMSTwilioConfigColumnSID.identifier(), - SMSTwilioConfigColumnToken.identifier(), - SMSTwilioConfigColumnSenderNumber.identifier(), + SMSTwilioColumnSMSID.identifier(), + SMSTwilioColumnSID.identifier(), + SMSTwilioColumnToken.identifier(), + SMSTwilioColumnSenderNumber.identifier(), + + SMSHTTPColumnSMSID.identifier(), + SMSHTTPColumnEndpoint.identifier(), ).From(smsConfigsTable.identifier()). - LeftJoin(join(SMSTwilioConfigColumnSMSID, SMSConfigColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(SMSTwilioColumnSMSID, SMSColumnID)). + LeftJoin(join(SMSHTTPColumnSMSID, SMSColumnID) + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*SMSConfig, error) { config := new(SMSConfig) var ( twilioConfig = sqlTwilioConfig{} + httpConfig = sqlHTTPConfig{} ) err := row.Scan( @@ -220,11 +249,15 @@ func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu &config.ResourceOwner, &config.State, &config.Sequence, + &config.Description, &twilioConfig.smsID, &twilioConfig.sid, &twilioConfig.token, &twilioConfig.senderNumber, + + &httpConfig.id, + &httpConfig.endpoint, ) if err != nil { @@ -235,6 +268,7 @@ func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu } twilioConfig.set(config) + httpConfig.setSMS(config) return config, nil } @@ -242,21 +276,27 @@ func prepareSMSConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*SMSConfigs, error)) { return sq.Select( - SMSConfigColumnID.identifier(), - SMSConfigColumnAggregateID.identifier(), - SMSConfigColumnCreationDate.identifier(), - SMSConfigColumnChangeDate.identifier(), - SMSConfigColumnResourceOwner.identifier(), - SMSConfigColumnState.identifier(), - SMSConfigColumnSequence.identifier(), + SMSColumnID.identifier(), + SMSColumnAggregateID.identifier(), + SMSColumnCreationDate.identifier(), + SMSColumnChangeDate.identifier(), + SMSColumnResourceOwner.identifier(), + SMSColumnState.identifier(), + SMSColumnSequence.identifier(), + SMSColumnDescription.identifier(), + + SMSTwilioColumnSMSID.identifier(), + SMSTwilioColumnSID.identifier(), + SMSTwilioColumnToken.identifier(), + SMSTwilioColumnSenderNumber.identifier(), + + SMSHTTPColumnSMSID.identifier(), + SMSHTTPColumnEndpoint.identifier(), - SMSTwilioConfigColumnSMSID.identifier(), - SMSTwilioConfigColumnSID.identifier(), - SMSTwilioConfigColumnToken.identifier(), - SMSTwilioConfigColumnSenderNumber.identifier(), countColumn.identifier(), ).From(smsConfigsTable.identifier()). - LeftJoin(join(SMSTwilioConfigColumnSMSID, SMSConfigColumnID) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(SMSTwilioColumnSMSID, SMSColumnID)). + LeftJoin(join(SMSHTTPColumnSMSID, SMSColumnID) + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), func(row *sql.Rows) (*SMSConfigs, error) { configs := &SMSConfigs{Configs: []*SMSConfig{}} @@ -264,6 +304,7 @@ func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB config := new(SMSConfig) var ( twilioConfig = sqlTwilioConfig{} + httpConfig = sqlHTTPConfig{} ) err := row.Scan( @@ -274,11 +315,16 @@ func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB &config.ResourceOwner, &config.State, &config.Sequence, + &config.Description, &twilioConfig.smsID, &twilioConfig.sid, &twilioConfig.token, &twilioConfig.senderNumber, + + &httpConfig.id, + &httpConfig.endpoint, + &configs.Count, ) @@ -287,6 +333,7 @@ func prepareSMSConfigsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB } twilioConfig.set(config) + httpConfig.setSMS(config) configs.Configs = append(configs.Configs, config) } @@ -312,3 +359,17 @@ func (c sqlTwilioConfig) set(smsConfig *SMSConfig) { SenderNumber: c.senderNumber.String, } } + +type sqlHTTPConfig struct { + id sql.NullString + endpoint sql.NullString +} + +func (c sqlHTTPConfig) setSMS(smsConfig *SMSConfig) { + if !c.id.Valid { + return + } + smsConfig.HTTPConfig = &HTTP{ + Endpoint: c.endpoint.String, + } +} diff --git a/internal/query/sms_test.go b/internal/query/sms_test.go index 56bb97e2eb..20cf62f8cb 100644 --- a/internal/query/sms_test.go +++ b/internal/query/sms_test.go @@ -14,38 +14,50 @@ import ( ) var ( - expectedSMSConfigQuery = regexp.QuoteMeta(`SELECT projections.sms_configs2.id,` + - ` projections.sms_configs2.aggregate_id,` + - ` projections.sms_configs2.creation_date,` + - ` projections.sms_configs2.change_date,` + - ` projections.sms_configs2.resource_owner,` + - ` projections.sms_configs2.state,` + - ` projections.sms_configs2.sequence,` + + expectedSMSConfigQuery = regexp.QuoteMeta(`SELECT projections.sms_configs3.id,` + + ` projections.sms_configs3.aggregate_id,` + + ` projections.sms_configs3.creation_date,` + + ` projections.sms_configs3.change_date,` + + ` projections.sms_configs3.resource_owner,` + + ` projections.sms_configs3.state,` + + ` projections.sms_configs3.sequence,` + + ` projections.sms_configs3.description,` + // twilio config - ` projections.sms_configs2_twilio.sms_id,` + - ` projections.sms_configs2_twilio.sid,` + - ` projections.sms_configs2_twilio.token,` + - ` projections.sms_configs2_twilio.sender_number` + - ` FROM projections.sms_configs2` + - ` LEFT JOIN projections.sms_configs2_twilio ON projections.sms_configs2.id = projections.sms_configs2_twilio.sms_id AND projections.sms_configs2.instance_id = projections.sms_configs2_twilio.instance_id` + + ` projections.sms_configs3_twilio.sms_id,` + + ` projections.sms_configs3_twilio.sid,` + + ` projections.sms_configs3_twilio.token,` + + ` projections.sms_configs3_twilio.sender_number,` + + + // http config + ` projections.sms_configs3_http.sms_id,` + + ` projections.sms_configs3_http.endpoint` + + ` FROM projections.sms_configs3` + + ` LEFT JOIN projections.sms_configs3_twilio ON projections.sms_configs3.id = projections.sms_configs3_twilio.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_twilio.instance_id` + + ` LEFT JOIN projections.sms_configs3_http ON projections.sms_configs3.id = projections.sms_configs3_http.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_http.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) - expectedSMSConfigsQuery = regexp.QuoteMeta(`SELECT projections.sms_configs2.id,` + - ` projections.sms_configs2.aggregate_id,` + - ` projections.sms_configs2.creation_date,` + - ` projections.sms_configs2.change_date,` + - ` projections.sms_configs2.resource_owner,` + - ` projections.sms_configs2.state,` + - ` projections.sms_configs2.sequence,` + + expectedSMSConfigsQuery = regexp.QuoteMeta(`SELECT projections.sms_configs3.id,` + + ` projections.sms_configs3.aggregate_id,` + + ` projections.sms_configs3.creation_date,` + + ` projections.sms_configs3.change_date,` + + ` projections.sms_configs3.resource_owner,` + + ` projections.sms_configs3.state,` + + ` projections.sms_configs3.sequence,` + + ` projections.sms_configs3.description,` + // twilio config - ` projections.sms_configs2_twilio.sms_id,` + - ` projections.sms_configs2_twilio.sid,` + - ` projections.sms_configs2_twilio.token,` + - ` projections.sms_configs2_twilio.sender_number,` + + ` projections.sms_configs3_twilio.sms_id,` + + ` projections.sms_configs3_twilio.sid,` + + ` projections.sms_configs3_twilio.token,` + + ` projections.sms_configs3_twilio.sender_number,` + + + // http config + ` projections.sms_configs3_http.sms_id,` + + ` projections.sms_configs3_http.endpoint,` + ` COUNT(*) OVER ()` + - ` FROM projections.sms_configs2` + - ` LEFT JOIN projections.sms_configs2_twilio ON projections.sms_configs2.id = projections.sms_configs2_twilio.sms_id AND projections.sms_configs2.instance_id = projections.sms_configs2_twilio.instance_id` + + ` FROM projections.sms_configs3` + + ` LEFT JOIN projections.sms_configs3_twilio ON projections.sms_configs3.id = projections.sms_configs3_twilio.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_twilio.instance_id` + + ` LEFT JOIN projections.sms_configs3_http ON projections.sms_configs3.id = projections.sms_configs3_http.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_http.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) smsConfigCols = []string{ @@ -56,16 +68,20 @@ var ( "resource_owner", "state", "sequence", + "description", // twilio config "sms_id", "sid", "token", "sender-number", + // http config + "sms_id", + "endpoint", } smsConfigsCols = append(smsConfigCols, "count") ) -func Test_SMSConfigssPrepare(t *testing.T) { +func Test_SMSConfigsPrepare(t *testing.T) { type want struct { sqlExpectations sqlExpectation err checkErr @@ -104,11 +120,15 @@ func Test_SMSConfigssPrepare(t *testing.T) { "ro", domain.SMSConfigStateInactive, uint64(20211109), + "description", // twilio config "sms-id", "sid", &crypto.CryptoValue{}, "sender-number", + // http config + nil, + nil, }, }, ), @@ -126,6 +146,7 @@ func Test_SMSConfigssPrepare(t *testing.T) { ResourceOwner: "ro", State: domain.SMSConfigStateInactive, Sequence: 20211109, + Description: "description", TwilioConfig: &Twilio{ SID: "sid", Token: &crypto.CryptoValue{}, @@ -135,6 +156,56 @@ func Test_SMSConfigssPrepare(t *testing.T) { }, }, }, + { + name: "prepareSMSQuery http config", + prepare: prepareSMSConfigsQuery, + want: want{ + sqlExpectations: mockQueries( + expectedSMSConfigsQuery, + smsConfigsCols, + [][]driver.Value{ + { + "sms-id", + "agg-id", + testNow, + testNow, + "ro", + domain.SMSConfigStateInactive, + uint64(20211109), + "description", + // twilio config + nil, + nil, + nil, + nil, + // http config + "sms-id", + "endpoint", + }, + }, + ), + }, + object: &SMSConfigs{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Configs: []*SMSConfig{ + { + ID: "sms-id", + AggregateID: "agg-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.SMSConfigStateInactive, + Sequence: 20211109, + Description: "description", + HTTPConfig: &HTTP{ + Endpoint: "endpoint", + }, + }, + }, + }, + }, { name: "prepareSMSConfigsQuery multiple result", prepare: prepareSMSConfigsQuery, @@ -149,13 +220,17 @@ func Test_SMSConfigssPrepare(t *testing.T) { testNow, testNow, "ro", - domain.SMSConfigStateInactive, + domain.SMSConfigStateActive, uint64(20211109), + "description", // twilio config "sms-id", "sid", &crypto.CryptoValue{}, "sender-number", + // http config + nil, + nil, }, { "sms-id2", @@ -165,18 +240,40 @@ func Test_SMSConfigssPrepare(t *testing.T) { "ro", domain.SMSConfigStateInactive, uint64(20211109), + "description", // twilio config "sms-id2", "sid2", &crypto.CryptoValue{}, "sender-number2", + // http config + nil, + nil, + }, + { + "sms-id3", + "agg-id", + testNow, + testNow, + "ro", + domain.SMSConfigStateInactive, + uint64(20211109), + "description", + // twilio config + nil, + nil, + nil, + nil, + // http config + "sms-id3", + "endpoint3", }, }, ), }, object: &SMSConfigs{ SearchResponse: SearchResponse{ - Count: 2, + Count: 3, }, Configs: []*SMSConfig{ { @@ -185,8 +282,9 @@ func Test_SMSConfigssPrepare(t *testing.T) { CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", - State: domain.SMSConfigStateInactive, + State: domain.SMSConfigStateActive, Sequence: 20211109, + Description: "description", TwilioConfig: &Twilio{ SID: "sid", Token: &crypto.CryptoValue{}, @@ -201,12 +299,26 @@ func Test_SMSConfigssPrepare(t *testing.T) { ResourceOwner: "ro", State: domain.SMSConfigStateInactive, Sequence: 20211109, + Description: "description", TwilioConfig: &Twilio{ SID: "sid2", Token: &crypto.CryptoValue{}, SenderNumber: "sender-number2", }, }, + { + ID: "sms-id3", + AggregateID: "agg-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.SMSConfigStateInactive, + Sequence: 20211109, + Description: "description", + HTTPConfig: &HTTP{ + Endpoint: "endpoint3", + }, + }, }, }, }, @@ -265,7 +377,50 @@ func Test_SMSConfigPrepare(t *testing.T) { object: (*SMSConfig)(nil), }, { - name: "prepareSMSConfigQuery found", + name: "prepareSMSConfigQuery, twilio, found", + prepare: prepareSMSConfigQuery, + want: want{ + sqlExpectations: mockQuery( + expectedSMSConfigQuery, + smsConfigCols, + []driver.Value{ + "sms-id", + "agg-id", + testNow, + testNow, + "ro", + domain.SMSConfigStateActive, + uint64(20211109), + "description", + // twilio config + "sms-id", + "sid", + &crypto.CryptoValue{}, + "sender-number", + // http config + nil, + nil, + }, + ), + }, + object: &SMSConfig{ + ID: "sms-id", + AggregateID: "agg-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + State: domain.SMSConfigStateActive, + Sequence: 20211109, + Description: "description", + TwilioConfig: &Twilio{ + SID: "sid", + SenderNumber: "sender-number", + Token: &crypto.CryptoValue{}, + }, + }, + }, + { + name: "prepareSMSConfigQuery, http, found", prepare: prepareSMSConfigQuery, want: want{ sqlExpectations: mockQuery( @@ -279,11 +434,15 @@ func Test_SMSConfigPrepare(t *testing.T) { "ro", domain.SMSConfigStateInactive, uint64(20211109), + "description", // twilio config + nil, + nil, + nil, + nil, + // http config "sms-id", - "sid", - &crypto.CryptoValue{}, - "sender-number", + "endpoint", }, ), }, @@ -295,10 +454,9 @@ func Test_SMSConfigPrepare(t *testing.T) { ResourceOwner: "ro", State: domain.SMSConfigStateInactive, Sequence: 20211109, - TwilioConfig: &Twilio{ - SID: "sid", - SenderNumber: "sender-number", - Token: &crypto.CryptoValue{}, + Description: "description", + HTTPConfig: &HTTP{ + Endpoint: "endpoint", }, }, }, diff --git a/internal/query/smtp.go b/internal/query/smtp.go index b9dd3e0bff..7c45fe33fe 100644 --- a/internal/query/smtp.go +++ b/internal/query/smtp.go @@ -52,34 +52,6 @@ var ( name: projection.SMTPConfigColumnSequence, table: smtpConfigsTable, } - SMTPConfigColumnTLS = Column{ - name: projection.SMTPConfigColumnTLS, - table: smtpConfigsTable, - } - SMTPConfigColumnSenderAddress = Column{ - name: projection.SMTPConfigColumnSenderAddress, - table: smtpConfigsTable, - } - SMTPConfigColumnSenderName = Column{ - name: projection.SMTPConfigColumnSenderName, - table: smtpConfigsTable, - } - SMTPConfigColumnReplyToAddress = Column{ - name: projection.SMTPConfigColumnReplyToAddress, - table: smtpConfigsTable, - } - SMTPConfigColumnSMTPHost = Column{ - name: projection.SMTPConfigColumnSMTPHost, - table: smtpConfigsTable, - } - SMTPConfigColumnSMTPUser = Column{ - name: projection.SMTPConfigColumnSMTPUser, - table: smtpConfigsTable, - } - SMTPConfigColumnSMTPPassword = Column{ - name: projection.SMTPConfigColumnSMTPPassword, - table: smtpConfigsTable, - } SMTPConfigColumnID = Column{ name: projection.SMTPConfigColumnID, table: smtpConfigsTable, @@ -92,13 +64,82 @@ var ( name: projection.SMTPConfigColumnDescription, table: smtpConfigsTable, } + + smtpConfigsSMTPTable = table{ + name: projection.SMTPConfigTable, + instanceIDCol: projection.SMTPConfigColumnInstanceID, + } + SMTPConfigSMTPColumnInstanceID = Column{ + name: projection.SMTPConfigColumnInstanceID, + table: smtpConfigsSMTPTable, + } + SMTPConfigSMTPColumnID = Column{ + name: projection.SMTPConfigColumnID, + table: smtpConfigsSMTPTable, + } + SMTPConfigSMTPColumnTLS = Column{ + name: projection.SMTPConfigSMTPColumnTLS, + table: smtpConfigsSMTPTable, + } + SMTPConfigSMTPColumnSenderAddress = Column{ + name: projection.SMTPConfigSMTPColumnSenderAddress, + table: smtpConfigsSMTPTable, + } + SMTPConfigSMTPColumnSenderName = Column{ + name: projection.SMTPConfigSMTPColumnSenderName, + table: smtpConfigsSMTPTable, + } + SMTPConfigSMTPColumnReplyToAddress = Column{ + name: projection.SMTPConfigSMTPColumnReplyToAddress, + table: smtpConfigsSMTPTable, + } + SMTPConfigSMTPColumnHost = Column{ + name: projection.SMTPConfigSMTPColumnHost, + table: smtpConfigsSMTPTable, + } + SMTPConfigSMTPColumnUser = Column{ + name: projection.SMTPConfigSMTPColumnUser, + table: smtpConfigsSMTPTable, + } + SMTPConfigSMTPColumnPassword = Column{ + name: projection.SMTPConfigSMTPColumnPassword, + table: smtpConfigsSMTPTable, + } + + smtpConfigsHTTPTable = table{ + name: projection.SMTPConfigHTTPTable, + instanceIDCol: projection.SMTPConfigHTTPColumnInstanceID, + } + SMTPConfigHTTPColumnInstanceID = Column{ + name: projection.SMTPConfigHTTPColumnInstanceID, + table: smtpConfigsHTTPTable, + } + SMTPConfigHTTPColumnID = Column{ + name: projection.SMTPConfigHTTPColumnID, + table: smtpConfigsHTTPTable, + } + SMTPConfigHTTPColumnEndpoint = Column{ + name: projection.SMTPConfigHTTPColumnEndpoint, + table: smtpConfigsHTTPTable, + } ) type SMTPConfig struct { - CreationDate time.Time - ChangeDate time.Time - ResourceOwner string - Sequence uint64 + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + AggregateID string + ID string + Sequence uint64 + Description string + + SMTPConfig *SMTP + HTTPConfig *HTTP + + State domain.SMTPConfigState +} + +type SMTP struct { TLS bool SenderAddress string SenderName string @@ -106,9 +147,6 @@ type SMTPConfig struct { Host string User string Password *crypto.CryptoValue - ID string - State domain.SMTPConfigState - Description string } func (q *Queries) SMTPConfigActive(ctx context.Context, resourceOwner string) (config *SMTPConfig, err error) { @@ -132,15 +170,14 @@ func (q *Queries) SMTPConfigActive(ctx context.Context, resourceOwner string) (c return config, err } -func (q *Queries) SMTPConfigByID(ctx context.Context, instanceID, resourceOwner, id string) (config *SMTPConfig, err error) { +func (q *Queries) SMTPConfigByID(ctx context.Context, instanceID, id string) (config *SMTPConfig, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() stmt, scan := prepareSMTPConfigQuery(ctx, q.client) query, args, err := stmt.Where(sq.Eq{ - SMTPConfigColumnResourceOwner.identifier(): resourceOwner, - SMTPConfigColumnInstanceID.identifier(): instanceID, - SMTPConfigColumnID.identifier(): id, + SMTPConfigColumnInstanceID.identifier(): instanceID, + SMTPConfigColumnID.identifier(): id, }).ToSql() if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-8f8gw", "Errors.Query.SQLStatement") @@ -161,35 +198,49 @@ func prepareSMTPConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectB SMTPConfigColumnChangeDate.identifier(), SMTPConfigColumnResourceOwner.identifier(), SMTPConfigColumnSequence.identifier(), - SMTPConfigColumnTLS.identifier(), - SMTPConfigColumnSenderAddress.identifier(), - SMTPConfigColumnSenderName.identifier(), - SMTPConfigColumnReplyToAddress.identifier(), - SMTPConfigColumnSMTPHost.identifier(), - SMTPConfigColumnSMTPUser.identifier(), - SMTPConfigColumnSMTPPassword.identifier(), SMTPConfigColumnID.identifier(), SMTPConfigColumnState.identifier(), - SMTPConfigColumnDescription.identifier()). - From(smtpConfigsTable.identifier() + db.Timetravel(call.Took(ctx))). + SMTPConfigColumnDescription.identifier(), + + SMTPConfigSMTPColumnID.identifier(), + SMTPConfigSMTPColumnTLS.identifier(), + SMTPConfigSMTPColumnSenderAddress.identifier(), + SMTPConfigSMTPColumnSenderName.identifier(), + SMTPConfigSMTPColumnReplyToAddress.identifier(), + SMTPConfigSMTPColumnHost.identifier(), + SMTPConfigSMTPColumnUser.identifier(), + SMTPConfigSMTPColumnPassword.identifier(), + + SMTPConfigHTTPColumnID.identifier(), + SMTPConfigHTTPColumnEndpoint.identifier()). + From(smtpConfigsTable.identifier()). + LeftJoin(join(SMTPConfigSMTPColumnID, SMTPConfigColumnID)). + LeftJoin(join(SMTPConfigHTTPColumnID, SMTPConfigColumnID) + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*SMTPConfig, error) { config := new(SMTPConfig) + var ( + smtpConfig = sqlSmtpConfig{} + httpConfig = sqlHTTPConfig{} + ) err := row.Scan( &config.CreationDate, &config.ChangeDate, &config.ResourceOwner, &config.Sequence, - &config.TLS, - &config.SenderAddress, - &config.SenderName, - &config.ReplyToAddress, - &config.Host, - &config.User, - &password, &config.ID, &config.State, &config.Description, + &smtpConfig.id, + &smtpConfig.tls, + &smtpConfig.senderAddress, + &smtpConfig.senderName, + &smtpConfig.replyToAddress, + &smtpConfig.host, + &smtpConfig.user, + &password, + &httpConfig.id, + &httpConfig.endpoint, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -197,7 +248,9 @@ func prepareSMTPConfigQuery(ctx context.Context, db prepareDatabase) (sq.SelectB } return nil, zerrors.ThrowInternal(err, "QUERY-9k87F", "Errors.Internal") } - config.Password = password + smtpConfig.password = password + smtpConfig.set(config) + httpConfig.setSMTP(config) return config, nil } } @@ -208,38 +261,53 @@ func prepareSMTPConfigsQuery(ctx context.Context, db prepareDatabase) (sq.Select SMTPConfigColumnChangeDate.identifier(), SMTPConfigColumnResourceOwner.identifier(), SMTPConfigColumnSequence.identifier(), - SMTPConfigColumnTLS.identifier(), - SMTPConfigColumnSenderAddress.identifier(), - SMTPConfigColumnSenderName.identifier(), - SMTPConfigColumnReplyToAddress.identifier(), - SMTPConfigColumnSMTPHost.identifier(), - SMTPConfigColumnSMTPUser.identifier(), - SMTPConfigColumnSMTPPassword.identifier(), SMTPConfigColumnID.identifier(), SMTPConfigColumnState.identifier(), SMTPConfigColumnDescription.identifier(), - countColumn.identifier()). - From(smtpConfigsTable.identifier() + db.Timetravel(call.Took(ctx))). + + SMTPConfigSMTPColumnID.identifier(), + SMTPConfigSMTPColumnTLS.identifier(), + SMTPConfigSMTPColumnSenderAddress.identifier(), + SMTPConfigSMTPColumnSenderName.identifier(), + SMTPConfigSMTPColumnReplyToAddress.identifier(), + SMTPConfigSMTPColumnHost.identifier(), + SMTPConfigSMTPColumnUser.identifier(), + SMTPConfigSMTPColumnPassword.identifier(), + + SMTPConfigHTTPColumnID.identifier(), + SMTPConfigHTTPColumnEndpoint.identifier(), + countColumn.identifier(), + ).From(smtpConfigsTable.identifier()). + LeftJoin(join(SMTPConfigSMTPColumnID, SMTPConfigColumnID)). + LeftJoin(join(SMTPConfigHTTPColumnID, SMTPConfigColumnID) + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*SMTPConfigs, error) { configs := &SMTPConfigs{Configs: []*SMTPConfig{}} for rows.Next() { config := new(SMTPConfig) + password := new(crypto.CryptoValue) + var ( + smtpConfig = sqlSmtpConfig{} + httpConfig = sqlHTTPConfig{} + ) err := rows.Scan( &config.CreationDate, &config.ChangeDate, &config.ResourceOwner, &config.Sequence, - &config.TLS, - &config.SenderAddress, - &config.SenderName, - &config.ReplyToAddress, - &config.Host, - &config.User, - &config.Password, &config.ID, &config.State, &config.Description, + &smtpConfig.id, + &smtpConfig.tls, + &smtpConfig.senderAddress, + &smtpConfig.senderName, + &smtpConfig.replyToAddress, + &smtpConfig.host, + &smtpConfig.user, + &password, + &httpConfig.id, + &httpConfig.endpoint, &configs.Count, ) if err != nil { @@ -248,6 +316,9 @@ func prepareSMTPConfigsQuery(ctx context.Context, db prepareDatabase) (sq.Select } return nil, zerrors.ThrowInternal(err, "QUERY-9k87F", "Errors.Internal") } + smtpConfig.password = password + smtpConfig.set(config) + httpConfig.setSMTP(config) configs.Configs = append(configs.Configs, config) } return configs, nil @@ -277,3 +348,38 @@ func (q *Queries) SearchSMTPConfigs(ctx context.Context, queries *SMTPConfigsSea configs.State, err = q.latestState(ctx, smsConfigsTable) return configs, err } + +type sqlSmtpConfig struct { + id sql.NullString + tls sql.NullBool + senderAddress sql.NullString + senderName sql.NullString + replyToAddress sql.NullString + host sql.NullString + user sql.NullString + password *crypto.CryptoValue +} + +func (c sqlSmtpConfig) set(smtpConfig *SMTPConfig) { + if !c.id.Valid { + return + } + smtpConfig.SMTPConfig = &SMTP{ + TLS: c.tls.Bool, + SenderAddress: c.senderAddress.String, + SenderName: c.senderName.String, + ReplyToAddress: c.replyToAddress.String, + Host: c.host.String, + User: c.user.String, + Password: c.password, + } +} + +func (c sqlHTTPConfig) setSMTP(smtpConfig *SMTPConfig) { + if !c.id.Valid { + return + } + smtpConfig.HTTPConfig = &HTTP{ + Endpoint: c.endpoint.String, + } +} diff --git a/internal/query/smtp_test.go b/internal/query/smtp_test.go index e1824fd277..0769c3e152 100644 --- a/internal/query/smtp_test.go +++ b/internal/query/smtp_test.go @@ -14,27 +14,36 @@ import ( ) var ( - prepareSMTPConfigStmt = `SELECT projections.smtp_configs2.creation_date,` + - ` projections.smtp_configs2.change_date,` + - ` projections.smtp_configs2.resource_owner,` + - ` projections.smtp_configs2.sequence,` + - ` projections.smtp_configs2.tls,` + - ` projections.smtp_configs2.sender_address,` + - ` projections.smtp_configs2.sender_name,` + - ` projections.smtp_configs2.reply_to_address,` + - ` projections.smtp_configs2.host,` + - ` projections.smtp_configs2.username,` + - ` projections.smtp_configs2.password,` + - ` projections.smtp_configs2.id,` + - ` projections.smtp_configs2.state,` + - ` projections.smtp_configs2.description` + - ` FROM projections.smtp_configs2` + + prepareSMTPConfigStmt = `SELECT projections.smtp_configs4.creation_date,` + + ` projections.smtp_configs4.change_date,` + + ` projections.smtp_configs4.resource_owner,` + + ` projections.smtp_configs4.sequence,` + + ` projections.smtp_configs4.id,` + + ` projections.smtp_configs4.state,` + + ` projections.smtp_configs4.description,` + + ` projections.smtp_configs4_smtp.id,` + + ` projections.smtp_configs4_smtp.tls,` + + ` projections.smtp_configs4_smtp.sender_address,` + + ` projections.smtp_configs4_smtp.sender_name,` + + ` projections.smtp_configs4_smtp.reply_to_address,` + + ` projections.smtp_configs4_smtp.host,` + + ` projections.smtp_configs4_smtp.username,` + + ` projections.smtp_configs4_smtp.password,` + + ` projections.smtp_configs4_http.id,` + + ` projections.smtp_configs4_http.endpoint` + + ` FROM projections.smtp_configs4` + + ` LEFT JOIN projections.smtp_configs4_smtp ON projections.smtp_configs4.id = projections.smtp_configs4_smtp.id AND projections.smtp_configs4.instance_id = projections.smtp_configs4_smtp.instance_id` + + ` LEFT JOIN projections.smtp_configs4_http ON projections.smtp_configs4.id = projections.smtp_configs4_http.id AND projections.smtp_configs4.instance_id = projections.smtp_configs4_http.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` prepareSMTPConfigCols = []string{ "creation_date", "change_date", "resource_owner", "sequence", + "id", + "state", + "description", + "id", "tls", "sender_address", "sender_name", @@ -43,8 +52,7 @@ var ( "smtp_user", "smtp_password", "id", - "state", - "description", + "endpoint", } ) @@ -89,6 +97,10 @@ func Test_SMTPConfigsPrepares(t *testing.T) { testNow, "ro", uint64(20211108), + "2232323", + domain.SMTPConfigStateActive, + "test", + "2232323", true, "sender", "name", @@ -96,27 +108,69 @@ func Test_SMTPConfigsPrepares(t *testing.T) { "host", "user", &crypto.CryptoValue{}, - "2232323", - domain.SMTPConfigStateActive, - "test", + nil, + nil, }, ), }, object: &SMTPConfig{ - CreationDate: testNow, - ChangeDate: testNow, - ResourceOwner: "ro", - Sequence: 20211108, - TLS: true, - SenderAddress: "sender", - SenderName: "name", - ReplyToAddress: "reply-to", - Host: "host", - User: "user", - Password: &crypto.CryptoValue{}, - ID: "2232323", - State: domain.SMTPConfigStateActive, - Description: "test", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211108, + SMTPConfig: &SMTP{ + TLS: true, + SenderAddress: "sender", + SenderName: "name", + ReplyToAddress: "reply-to", + Host: "host", + User: "user", + Password: &crypto.CryptoValue{}, + }, + ID: "2232323", + State: domain.SMTPConfigStateActive, + Description: "test", + }, + }, + { + name: "prepareSMTPConfigQuery found, http", + prepare: prepareSMTPConfigQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(prepareSMTPConfigStmt), + prepareSMTPConfigCols, + []driver.Value{ + testNow, + testNow, + "ro", + uint64(20211108), + "2232323", + domain.SMTPConfigStateActive, + "test", + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + "2232323", + "endpoint", + }, + ), + }, + object: &SMTPConfig{ + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211108, + HTTPConfig: &HTTP{ + Endpoint: "endpoint", + }, + ID: "2232323", + State: domain.SMTPConfigStateActive, + Description: "test", }, }, { @@ -131,6 +185,10 @@ func Test_SMTPConfigsPrepares(t *testing.T) { testNow, "ro", uint64(20211109), + "44442323", + domain.SMTPConfigStateInactive, + "test2", + "44442323", true, "sender2", "name2", @@ -138,27 +196,28 @@ func Test_SMTPConfigsPrepares(t *testing.T) { "host2", "user2", &crypto.CryptoValue{}, - "44442323", - domain.SMTPConfigStateInactive, - "test2", + nil, + nil, }, ), }, object: &SMTPConfig{ - CreationDate: testNow, - ChangeDate: testNow, - ResourceOwner: "ro", - Sequence: 20211109, - TLS: true, - SenderAddress: "sender2", - SenderName: "name2", - ReplyToAddress: "reply-to2", - Host: "host2", - User: "user2", - Password: &crypto.CryptoValue{}, - ID: "44442323", - State: domain.SMTPConfigStateInactive, - Description: "test2", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211109, + SMTPConfig: &SMTP{ + TLS: true, + SenderAddress: "sender2", + SenderName: "name2", + ReplyToAddress: "reply-to2", + Host: "host2", + User: "user2", + Password: &crypto.CryptoValue{}, + }, + ID: "44442323", + State: domain.SMTPConfigStateInactive, + Description: "test2", }, }, { @@ -173,6 +232,10 @@ func Test_SMTPConfigsPrepares(t *testing.T) { testNow, "ro", uint64(20211109), + "23234444", + domain.SMTPConfigStateInactive, + "test3", + "23234444", true, "sender3", "name3", @@ -180,27 +243,28 @@ func Test_SMTPConfigsPrepares(t *testing.T) { "host3", "user3", &crypto.CryptoValue{}, - "23234444", - domain.SMTPConfigStateInactive, - "test3", + nil, + nil, }, ), }, object: &SMTPConfig{ - CreationDate: testNow, - ChangeDate: testNow, - ResourceOwner: "ro", - Sequence: 20211109, - TLS: true, - SenderAddress: "sender3", - SenderName: "name3", - ReplyToAddress: "reply-to3", - Host: "host3", - User: "user3", - Password: &crypto.CryptoValue{}, - ID: "23234444", - State: domain.SMTPConfigStateInactive, - Description: "test3", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211109, + SMTPConfig: &SMTP{ + TLS: true, + SenderAddress: "sender3", + SenderName: "name3", + ReplyToAddress: "reply-to3", + Host: "host3", + User: "user3", + Password: &crypto.CryptoValue{}, + }, + ID: "23234444", + State: domain.SMTPConfigStateInactive, + Description: "test3", }, }, { diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index 71031b5136..c8e1dc05a7 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -277,7 +277,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh return nil, zerrors.ThrowInternal(err, "QUERY-wXnQR", "Errors.Query.SQLStatement") } - latestSequence, err := q.latestState(ctx, userGrantTable) + latestState, err := q.latestState(ctx, userGrantTable) if err != nil { return nil, err } @@ -290,7 +290,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh return nil, err } - grants.State = latestSequence + grants.State = latestState return grants, nil } diff --git a/internal/query/user_membership.go b/internal/query/user_membership.go index 7ba2629cfa..a08617d57b 100644 --- a/internal/query/user_membership.go +++ b/internal/query/user_membership.go @@ -144,7 +144,7 @@ func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuer if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-T84X9", "Errors.Query.InvalidRequest") } - latestSequence, err := q.latestState(ctx, orgMemberTable, instanceMemberTable, projectMemberTable, projectGrantMemberTable) + latestState, err := q.latestState(ctx, orgMemberTable, instanceMemberTable, projectMemberTable, projectGrantMemberTable) if err != nil { return nil, err } @@ -157,7 +157,7 @@ func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuer if err != nil { return nil, err } - memberships.State = latestSequence + memberships.State = latestState return memberships, nil } diff --git a/internal/repository/debug_events/aggregate.go b/internal/repository/debug_events/aggregate.go new file mode 100644 index 0000000000..f2abd17b56 --- /dev/null +++ b/internal/repository/debug_events/aggregate.go @@ -0,0 +1,27 @@ +package debug_events + +import ( + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + eventTypePrefix = eventstore.EventType("debug.") +) + +const ( + AggregateType = "debug" + AggregateVersion = "v1" +) + +type Aggregate struct { + eventstore.Aggregate +} + +func NewAggregate(id, resourceOwner string) *eventstore.Aggregate { + return &eventstore.Aggregate{ + Type: AggregateType, + Version: AggregateVersion, + ID: id, + ResourceOwner: resourceOwner, + } +} diff --git a/internal/repository/debug_events/debug.go b/internal/repository/debug_events/debug.go new file mode 100644 index 0000000000..f56d995d6a --- /dev/null +++ b/internal/repository/debug_events/debug.go @@ -0,0 +1,125 @@ +package debug_events + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const ( + AddedEventType = eventTypePrefix + "added" + ChangedEventType = eventTypePrefix + "changed" + RemovedEventType = eventTypePrefix + "removed" +) + +type AddedEvent struct { + eventstore.BaseEvent `json:"-"` + ProjectionSleep time.Duration `json:"projectionSleep,omitempty"` + Blob *string `json:"blob,omitempty"` +} + +func (e *AddedEvent) Payload() interface{} { + return e +} + +func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewAddedEvent(ctx context.Context, aggregate *eventstore.Aggregate, projectionSleep time.Duration, blob *string) *AddedEvent { + return &AddedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + AddedEventType, + ), + Blob: blob, + ProjectionSleep: projectionSleep, + } +} + +func DebugAddedEventMapper(event eventstore.Event) (eventstore.Event, error) { + debugAdded := &AddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := event.Unmarshal(debugAdded) + if err != nil { + return nil, zerrors.ThrowInternal(err, "ORG-Bren2", "unable to unmarshal debug added") + } + + return debugAdded, nil +} + +type ChangedEvent struct { + eventstore.BaseEvent `json:"-"` + ProjectionSleep time.Duration `json:"projectionSleep,omitempty"` + Blob *string `json:"blob,omitempty"` +} + +func (e *ChangedEvent) Payload() interface{} { + return e +} + +func (e *ChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, projectionSleep time.Duration, blob *string) *ChangedEvent { + return &ChangedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + ChangedEventType, + ), + ProjectionSleep: projectionSleep, + Blob: blob, + } +} + +func DebugChangedEventMapper(event eventstore.Event) (eventstore.Event, error) { + debugChanged := &ChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := event.Unmarshal(debugChanged) + if err != nil { + return nil, zerrors.ThrowInternal(err, "ORG-Bren2", "unable to unmarshal debug added") + } + + return debugChanged, nil +} + +type RemovedEvent struct { + eventstore.BaseEvent `json:"-"` + ProjectionSleep time.Duration `json:"projectionSleep,omitempty"` +} + +func (e *RemovedEvent) Payload() interface{} { + return nil +} + +func (e *RemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate, projectionSleep time.Duration) *RemovedEvent { + return &RemovedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + RemovedEventType, + ), + ProjectionSleep: projectionSleep, + } +} + +func DebugRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) { + return &RemovedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + }, nil +} + +func AggregateFromWriteModel(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate { + return eventstore.AggregateFromWriteModelCtx(ctx, wm, AggregateType, AggregateVersion) +} diff --git a/internal/repository/debug_events/eventstore.go b/internal/repository/debug_events/eventstore.go new file mode 100644 index 0000000000..f774d2140d --- /dev/null +++ b/internal/repository/debug_events/eventstore.go @@ -0,0 +1,11 @@ +package debug_events + +import ( + "github.com/zitadel/zitadel/internal/eventstore" +) + +func init() { + eventstore.RegisterFilterEventMapper(AggregateType, AddedEventType, DebugAddedEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, ChangedEventType, DebugChangedEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, RemovedEventType, DebugRemovedEventMapper) +} diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index 16b7e3967e..68621597a8 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -12,18 +12,25 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, SecretGeneratorAddedEventType, SecretGeneratorAddedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, SecretGeneratorChangedEventType, SecretGeneratorChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, SecretGeneratorRemovedEventType, SecretGeneratorRemovedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigAddedEventType, SMTPConfigAddedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigChangedEventType, SMTPConfigChangedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigActivatedEventType, SMTPConfigActivatedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigDeactivatedEventType, SMTPConfigDeactivatedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigPasswordChangedEventType, SMTPConfigPasswordChangedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigRemovedEventType, SMTPConfigRemovedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioAddedEventType, SMSConfigTwilioAddedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioChangedEventType, SMSConfigTwilioChangedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioTokenChangedEventType, SMSConfigTwilioTokenChangedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigActivatedEventType, SMSConfigActivatedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigDeactivatedEventType, SMSConfigDeactivatedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigRemovedEventType, SMSConfigRemovedEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigAddedEventType, eventstore.GenericEventMapper[SMTPConfigAddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigChangedEventType, eventstore.GenericEventMapper[SMTPConfigChangedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigActivatedEventType, eventstore.GenericEventMapper[SMTPConfigActivatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigDeactivatedEventType, eventstore.GenericEventMapper[SMTPConfigDeactivatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigPasswordChangedEventType, eventstore.GenericEventMapper[SMTPConfigPasswordChangedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigHTTPAddedEventType, eventstore.GenericEventMapper[SMTPConfigHTTPAddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigHTTPChangedEventType, eventstore.GenericEventMapper[SMTPConfigHTTPChangedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMTPConfigRemovedEventType, eventstore.GenericEventMapper[SMTPConfigRemovedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioAddedEventType, eventstore.GenericEventMapper[SMSConfigTwilioAddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioChangedEventType, eventstore.GenericEventMapper[SMSConfigTwilioChangedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioTokenChangedEventType, eventstore.GenericEventMapper[SMSConfigTwilioTokenChangedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigHTTPAddedEventType, eventstore.GenericEventMapper[SMSConfigHTTPAddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigHTTPChangedEventType, eventstore.GenericEventMapper[SMSConfigHTTPChangedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioActivatedEventType, eventstore.GenericEventMapper[SMSConfigTwilioActivatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioDeactivatedEventType, eventstore.GenericEventMapper[SMSConfigTwilioDeactivatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigTwilioRemovedEventType, eventstore.GenericEventMapper[SMSConfigTwilioRemovedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigActivatedEventType, eventstore.GenericEventMapper[SMSConfigActivatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigDeactivatedEventType, eventstore.GenericEventMapper[SMSConfigDeactivatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SMSConfigRemovedEventType, eventstore.GenericEventMapper[SMSConfigRemovedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, DebugNotificationProviderFileAddedEventType, DebugNotificationProviderFileAddedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, DebugNotificationProviderFileChangedEventType, DebugNotificationProviderFileChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, DebugNotificationProviderFileRemovedEventType, DebugNotificationProviderFileRemovedEventMapper) diff --git a/internal/repository/instance/sms.go b/internal/repository/instance/sms.go index 1b79f1d77e..309ce9aa46 100644 --- a/internal/repository/instance/sms.go +++ b/internal/repository/instance/sms.go @@ -11,18 +11,25 @@ import ( const ( smsConfigPrefix = "sms.config" smsConfigTwilioPrefix = "twilio." + smsConfigHTTPPrefix = "http." SMSConfigTwilioAddedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "added" SMSConfigTwilioChangedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "changed" + SMSConfigHTTPAddedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigHTTPPrefix + "added" + SMSConfigHTTPChangedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigHTTPPrefix + "changed" SMSConfigTwilioTokenChangedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "token.changed" - SMSConfigActivatedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "activated" - SMSConfigDeactivatedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "deactivated" - SMSConfigRemovedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "removed" + SMSConfigTwilioActivatedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "activated" + SMSConfigTwilioDeactivatedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "deactivated" + SMSConfigTwilioRemovedEventType = instanceEventTypePrefix + smsConfigPrefix + smsConfigTwilioPrefix + "removed" + SMSConfigActivatedEventType = instanceEventTypePrefix + smsConfigPrefix + "activated" + SMSConfigDeactivatedEventType = instanceEventTypePrefix + smsConfigPrefix + "deactivated" + SMSConfigRemovedEventType = instanceEventTypePrefix + smsConfigPrefix + "removed" ) type SMSConfigTwilioAddedEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` SID string `json:"sid,omitempty"` Token *crypto.CryptoValue `json:"token,omitempty"` SenderNumber string `json:"senderNumber,omitempty"` @@ -32,23 +39,29 @@ func NewSMSConfigTwilioAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate, id, + description string, sid, senderNumber string, token *crypto.CryptoValue, ) *SMSConfigTwilioAddedEvent { return &SMSConfigTwilioAddedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMSConfigTwilioAddedEventType, ), ID: id, + Description: description, SID: sid, Token: token, SenderNumber: senderNumber, } } +func (e *SMSConfigTwilioAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMSConfigTwilioAddedEvent) Payload() interface{} { return e } @@ -57,22 +70,11 @@ func (e *SMSConfigTwilioAddedEvent) UniqueConstraints() []*eventstore.UniqueCons return nil } -func SMSConfigTwilioAddedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smsConfigAdded := &SMSConfigTwilioAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smsConfigAdded) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-smwiR", "unable to unmarshal sms config twilio added") - } - - return smsConfigAdded, nil -} - type SMSConfigTwilioChangedEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` ID string `json:"id,omitempty"` + Description *string `json:"description,omitempty"` SID *string `json:"sid,omitempty"` SenderNumber *string `json:"senderNumber,omitempty"` } @@ -87,7 +89,7 @@ func NewSMSConfigTwilioChangedEvent( return nil, zerrors.ThrowPreconditionFailed(nil, "IAM-smn8e", "Errors.NoChangesFound") } changeEvent := &SMSConfigTwilioChangedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMSConfigTwilioChangedEventType, @@ -108,12 +110,22 @@ func ChangeSMSConfigTwilioSID(sid string) func(event *SMSConfigTwilioChangedEven } } +func ChangeSMSConfigTwilioDescription(description string) func(event *SMSConfigTwilioChangedEvent) { + return func(e *SMSConfigTwilioChangedEvent) { + e.Description = &description + } +} + func ChangeSMSConfigTwilioSenderNumber(senderNumber string) func(event *SMSConfigTwilioChangedEvent) { return func(e *SMSConfigTwilioChangedEvent) { e.SenderNumber = &senderNumber } } +func (e *SMSConfigTwilioChangedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMSConfigTwilioChangedEvent) Payload() interface{} { return e } @@ -122,20 +134,8 @@ func (e *SMSConfigTwilioChangedEvent) UniqueConstraints() []*eventstore.UniqueCo return nil } -func SMSConfigTwilioChangedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smsConfigChanged := &SMSConfigTwilioChangedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smsConfigChanged) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-smwiR", "unable to unmarshal sms config twilio added") - } - - return smsConfigChanged, nil -} - type SMSConfigTwilioTokenChangedEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` ID string `json:"id,omitempty"` Token *crypto.CryptoValue `json:"token,omitempty"` @@ -148,7 +148,7 @@ func NewSMSConfigTokenChangedEvent( token *crypto.CryptoValue, ) *SMSConfigTwilioTokenChangedEvent { return &SMSConfigTwilioTokenChangedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMSConfigTwilioTokenChangedEventType, @@ -158,6 +158,10 @@ func NewSMSConfigTokenChangedEvent( } } +func (e *SMSConfigTwilioTokenChangedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMSConfigTwilioTokenChangedEvent) Payload() interface{} { return e } @@ -166,30 +170,130 @@ func (e *SMSConfigTwilioTokenChangedEvent) UniqueConstraints() []*eventstore.Uni return nil } -func SMSConfigTwilioTokenChangedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smtpConfigTokenChagned := &SMSConfigTwilioTokenChangedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smtpConfigTokenChagned) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-fi9Wf", "unable to unmarshal sms config token changed") - } +type SMSConfigHTTPAddedEvent struct { + *eventstore.BaseEvent `json:"-"` - return smtpConfigTokenChagned, nil + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + Endpoint string `json:"endpoint,omitempty"` +} + +func NewSMSConfigHTTPAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, + description, + endpoint string, +) *SMSConfigHTTPAddedEvent { + return &SMSConfigHTTPAddedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + SMSConfigHTTPAddedEventType, + ), + ID: id, + Description: description, + Endpoint: endpoint, + } +} + +func (e *SMSConfigHTTPAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *SMSConfigHTTPAddedEvent) Payload() interface{} { + return e +} + +func (e *SMSConfigHTTPAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +type SMSConfigHTTPChangedEvent struct { + *eventstore.BaseEvent `json:"-"` + + ID string `json:"id,omitempty"` + Description *string `json:"description,omitempty"` + Endpoint *string `json:"endpoint,omitempty"` +} + +func NewSMSConfigHTTPChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id string, + changes []SMSConfigHTTPChanges, +) (*SMSConfigHTTPChangedEvent, error) { + if len(changes) == 0 { + return nil, zerrors.ThrowPreconditionFailed(nil, "IAM-smn8e", "Errors.NoChangesFound") + } + changeEvent := &SMSConfigHTTPChangedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + SMSConfigHTTPChangedEventType, + ), + ID: id, + } + for _, change := range changes { + change(changeEvent) + } + return changeEvent, nil +} + +type SMSConfigHTTPChanges func(event *SMSConfigHTTPChangedEvent) + +func ChangeSMSConfigHTTPDescription(description string) func(event *SMSConfigHTTPChangedEvent) { + return func(e *SMSConfigHTTPChangedEvent) { + e.Description = &description + } +} +func ChangeSMSConfigHTTPEndpoint(endpoint string) func(event *SMSConfigHTTPChangedEvent) { + return func(e *SMSConfigHTTPChangedEvent) { + e.Endpoint = &endpoint + } +} + +func (e *SMSConfigHTTPChangedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *SMSConfigHTTPChangedEvent) Payload() interface{} { + return e +} + +func (e *SMSConfigHTTPChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +type SMSConfigTwilioActivatedEvent struct { + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` +} + +func (e *SMSConfigTwilioActivatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *SMSConfigTwilioActivatedEvent) Payload() interface{} { + return e +} + +func (e *SMSConfigTwilioActivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil } type SMSConfigActivatedEvent struct { - eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` } -func NewSMSConfigTwilioActivatedEvent( +func NewSMSConfigActivatedEvent( ctx context.Context, aggregate *eventstore.Aggregate, id string, ) *SMSConfigActivatedEvent { return &SMSConfigActivatedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMSConfigActivatedEventType, @@ -198,6 +302,10 @@ func NewSMSConfigTwilioActivatedEvent( } } +func (e *SMSConfigActivatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMSConfigActivatedEvent) Payload() interface{} { return e } @@ -206,21 +314,26 @@ func (e *SMSConfigActivatedEvent) UniqueConstraints() []*eventstore.UniqueConstr return nil } -func SMSConfigActivatedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smsConfigActivated := &SMSConfigActivatedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smsConfigActivated) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-dn92f", "unable to unmarshal sms config twilio activated changed") - } +type SMSConfigTwilioDeactivatedEvent struct { + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` +} - return smsConfigActivated, nil +func (e *SMSConfigTwilioDeactivatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *SMSConfigTwilioDeactivatedEvent) Payload() interface{} { + return e +} + +func (e *SMSConfigTwilioDeactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil } type SMSConfigDeactivatedEvent struct { - eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` } func NewSMSConfigDeactivatedEvent( @@ -229,7 +342,7 @@ func NewSMSConfigDeactivatedEvent( id string, ) *SMSConfigDeactivatedEvent { return &SMSConfigDeactivatedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMSConfigDeactivatedEventType, @@ -238,6 +351,10 @@ func NewSMSConfigDeactivatedEvent( } } +func (e *SMSConfigDeactivatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMSConfigDeactivatedEvent) Payload() interface{} { return e } @@ -246,21 +363,26 @@ func (e *SMSConfigDeactivatedEvent) UniqueConstraints() []*eventstore.UniqueCons return nil } -func SMSConfigDeactivatedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smsConfigDeactivated := &SMSConfigDeactivatedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smsConfigDeactivated) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-dn92f", "unable to unmarshal sms config twilio deactivated changed") - } +type SMSConfigTwilioRemovedEvent struct { + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` +} - return smsConfigDeactivated, nil +func (e *SMSConfigTwilioRemovedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *SMSConfigTwilioRemovedEvent) Payload() interface{} { + return e +} + +func (e *SMSConfigTwilioRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil } type SMSConfigRemovedEvent struct { - eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` } func NewSMSConfigRemovedEvent( @@ -269,7 +391,7 @@ func NewSMSConfigRemovedEvent( id string, ) *SMSConfigRemovedEvent { return &SMSConfigRemovedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMSConfigRemovedEventType, @@ -278,6 +400,10 @@ func NewSMSConfigRemovedEvent( } } +func (e *SMSConfigRemovedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMSConfigRemovedEvent) Payload() interface{} { return e } @@ -285,15 +411,3 @@ func (e *SMSConfigRemovedEvent) Payload() interface{} { func (e *SMSConfigRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { return nil } - -func SMSConfigRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smsConfigRemoved := &SMSConfigRemovedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smsConfigRemoved) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-99iNF", "unable to unmarshal sms config removed") - } - - return smsConfigRemoved, nil -} diff --git a/internal/repository/instance/smtp_config.go b/internal/repository/instance/smtp_config.go index 3f08fc8e8a..403cc569a3 100644 --- a/internal/repository/instance/smtp_config.go +++ b/internal/repository/instance/smtp_config.go @@ -10,16 +10,19 @@ import ( const ( smtpConfigPrefix = "smtp.config." + httpConfigPrefix = "http." SMTPConfigAddedEventType = instanceEventTypePrefix + smtpConfigPrefix + "added" SMTPConfigChangedEventType = instanceEventTypePrefix + smtpConfigPrefix + "changed" SMTPConfigPasswordChangedEventType = instanceEventTypePrefix + smtpConfigPrefix + "password.changed" + SMTPConfigHTTPAddedEventType = instanceEventTypePrefix + smtpConfigPrefix + httpConfigPrefix + "added" + SMTPConfigHTTPChangedEventType = instanceEventTypePrefix + smtpConfigPrefix + httpConfigPrefix + "changed" SMTPConfigRemovedEventType = instanceEventTypePrefix + smtpConfigPrefix + "removed" SMTPConfigActivatedEventType = instanceEventTypePrefix + smtpConfigPrefix + "activated" SMTPConfigDeactivatedEventType = instanceEventTypePrefix + smtpConfigPrefix + "deactivated" ) type SMTPConfigAddedEvent struct { - eventstore.BaseEvent `json:"-"` + *eventstore.BaseEvent `json:"-"` ID string `json:"id,omitempty"` Description string `json:"description,omitempty"` @@ -45,7 +48,7 @@ func NewSMTPConfigAddedEvent( password *crypto.CryptoValue, ) *SMTPConfigAddedEvent { return &SMTPConfigAddedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMTPConfigAddedEventType, @@ -61,6 +64,9 @@ func NewSMTPConfigAddedEvent( Password: password, } } +func (e *SMTPConfigAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} func (e *SMTPConfigAddedEvent) Payload() interface{} { return e @@ -70,29 +76,21 @@ func (e *SMTPConfigAddedEvent) UniqueConstraints() []*eventstore.UniqueConstrain return nil } -func SMTPConfigAddedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smtpConfigAdded := &SMTPConfigAddedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smtpConfigAdded) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-39fks", "unable to unmarshal smtp config added") - } - - return smtpConfigAdded, nil +type SMTPConfigChangedEvent struct { + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` + Description *string `json:"description,omitempty"` + FromAddress *string `json:"senderAddress,omitempty"` + FromName *string `json:"senderName,omitempty"` + ReplyToAddress *string `json:"replyToAddress,omitempty"` + TLS *bool `json:"tls,omitempty"` + Host *string `json:"host,omitempty"` + User *string `json:"user,omitempty"` + Password *crypto.CryptoValue `json:"password,omitempty"` } -type SMTPConfigChangedEvent struct { - eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` - Description *string `json:"description,omitempty"` - FromAddress *string `json:"senderAddress,omitempty"` - FromName *string `json:"senderName,omitempty"` - ReplyToAddress *string `json:"replyToAddress,omitempty"` - TLS *bool `json:"tls,omitempty"` - Host *string `json:"host,omitempty"` - User *string `json:"user,omitempty"` - Password *crypto.CryptoValue `json:"password,omitempty"` +func (e *SMTPConfigChangedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event } func (e *SMTPConfigChangedEvent) Payload() interface{} { @@ -113,7 +111,7 @@ func NewSMTPConfigChangeEvent( return nil, zerrors.ThrowPreconditionFailed(nil, "IAM-o0pWf", "Errors.NoChangesFound") } changeEvent := &SMTPConfigChangedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMTPConfigChangedEventType, @@ -182,23 +180,10 @@ func ChangeSMTPConfigSMTPPassword(password *crypto.CryptoValue) func(event *SMTP } } -func SMTPConfigChangedEventMapper(event eventstore.Event) (eventstore.Event, error) { - e := &SMTPConfigChangedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - - err := event.Unmarshal(e) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-m09oo", "unable to unmarshal smtp changed") - } - - return e, nil -} - type SMTPConfigPasswordChangedEvent struct { - eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` - Password *crypto.CryptoValue `json:"password,omitempty"` + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` + Password *crypto.CryptoValue `json:"password,omitempty"` } func NewSMTPConfigPasswordChangedEvent( @@ -208,7 +193,7 @@ func NewSMTPConfigPasswordChangedEvent( password *crypto.CryptoValue, ) *SMTPConfigPasswordChangedEvent { return &SMTPConfigPasswordChangedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMTPConfigPasswordChangedEventType, @@ -217,6 +202,10 @@ func NewSMTPConfigPasswordChangedEvent( } } +func (e *SMTPConfigPasswordChangedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMTPConfigPasswordChangedEvent) Payload() interface{} { return e } @@ -225,21 +214,109 @@ func (e *SMTPConfigPasswordChangedEvent) UniqueConstraints() []*eventstore.Uniqu return nil } -func SMTPConfigPasswordChangedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smtpConfigPasswordChanged := &SMTPConfigPasswordChangedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smtpConfigPasswordChanged) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-99iNF", "unable to unmarshal smtp config password changed") - } +type SMTPConfigHTTPAddedEvent struct { + *eventstore.BaseEvent `json:"-"` - return smtpConfigPasswordChanged, nil + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + Endpoint string `json:"endpoint,omitempty"` +} + +func NewSMTPConfigHTTPAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, description string, + endpoint string, +) *SMTPConfigHTTPAddedEvent { + return &SMTPConfigHTTPAddedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + SMTPConfigHTTPAddedEventType, + ), + ID: id, + Description: description, + Endpoint: endpoint, + } +} + +func (e *SMTPConfigHTTPAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *SMTPConfigHTTPAddedEvent) Payload() interface{} { + return e +} + +func (e *SMTPConfigHTTPAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +type SMTPConfigHTTPChangedEvent struct { + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` + Description *string `json:"description,omitempty"` + Endpoint *string `json:"endpoint,omitempty"` +} + +func (e *SMTPConfigHTTPChangedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + +func (e *SMTPConfigHTTPChangedEvent) Payload() interface{} { + return e +} + +func (e *SMTPConfigHTTPChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewSMTPConfigHTTPChangeEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id string, + changes []SMTPConfigHTTPChanges, +) (*SMTPConfigHTTPChangedEvent, error) { + if len(changes) == 0 { + return nil, zerrors.ThrowPreconditionFailed(nil, "IAM-o0pWf", "Errors.NoChangesFound") + } + changeEvent := &SMTPConfigHTTPChangedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + SMTPConfigHTTPChangedEventType, + ), + ID: id, + } + for _, change := range changes { + change(changeEvent) + } + return changeEvent, nil +} + +type SMTPConfigHTTPChanges func(event *SMTPConfigHTTPChangedEvent) + +func ChangeSMTPConfigHTTPID(id string) func(event *SMTPConfigHTTPChangedEvent) { + return func(e *SMTPConfigHTTPChangedEvent) { + e.ID = id + } +} + +func ChangeSMTPConfigHTTPDescription(description string) func(event *SMTPConfigHTTPChangedEvent) { + return func(e *SMTPConfigHTTPChangedEvent) { + e.Description = &description + } +} + +func ChangeSMTPConfigHTTPEndpoint(endpoint string) func(event *SMTPConfigHTTPChangedEvent) { + return func(e *SMTPConfigHTTPChangedEvent) { + e.Endpoint = &endpoint + } } type SMTPConfigActivatedEvent struct { - eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` } func NewSMTPConfigActivatedEvent( @@ -248,7 +325,7 @@ func NewSMTPConfigActivatedEvent( id string, ) *SMTPConfigActivatedEvent { return &SMTPConfigActivatedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMTPConfigActivatedEventType, @@ -257,6 +334,10 @@ func NewSMTPConfigActivatedEvent( } } +func (e *SMTPConfigActivatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMTPConfigActivatedEvent) Payload() interface{} { return e } @@ -265,21 +346,9 @@ func (e *SMTPConfigActivatedEvent) UniqueConstraints() []*eventstore.UniqueConst return nil } -func SMTPConfigActivatedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smtpConfigActivated := &SMTPConfigActivatedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smtpConfigActivated) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-KPr5t", "unable to unmarshal smtp config removed") - } - - return smtpConfigActivated, nil -} - type SMTPConfigDeactivatedEvent struct { - eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` } func NewSMTPConfigDeactivatedEvent( @@ -288,7 +357,7 @@ func NewSMTPConfigDeactivatedEvent( id string, ) *SMTPConfigDeactivatedEvent { return &SMTPConfigDeactivatedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMTPConfigDeactivatedEventType, @@ -297,6 +366,10 @@ func NewSMTPConfigDeactivatedEvent( } } +func (e *SMTPConfigDeactivatedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} + func (e *SMTPConfigDeactivatedEvent) Payload() interface{} { return e } @@ -305,21 +378,9 @@ func (e *SMTPConfigDeactivatedEvent) UniqueConstraints() []*eventstore.UniqueCon return nil } -func SMTPConfigDeactivatedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smtpConfigDeactivated := &SMTPConfigDeactivatedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smtpConfigDeactivated) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-KPr5t", "unable to unmarshal smtp config removed") - } - - return smtpConfigDeactivated, nil -} - type SMTPConfigRemovedEvent struct { - eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` + *eventstore.BaseEvent `json:"-"` + ID string `json:"id,omitempty"` } func NewSMTPConfigRemovedEvent( @@ -328,7 +389,7 @@ func NewSMTPConfigRemovedEvent( id string, ) *SMTPConfigRemovedEvent { return &SMTPConfigRemovedEvent{ - BaseEvent: *eventstore.NewBaseEventForPush( + BaseEvent: eventstore.NewBaseEventForPush( ctx, aggregate, SMTPConfigRemovedEventType, @@ -337,6 +398,9 @@ func NewSMTPConfigRemovedEvent( } } +func (e *SMTPConfigRemovedEvent) SetBaseEvent(event *eventstore.BaseEvent) { + e.BaseEvent = event +} func (e *SMTPConfigRemovedEvent) Payload() interface{} { return e } @@ -344,15 +408,3 @@ func (e *SMTPConfigRemovedEvent) Payload() interface{} { func (e *SMTPConfigRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { return nil } - -func SMTPConfigRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) { - smtpConfigRemoved := &SMTPConfigRemovedEvent{ - BaseEvent: *eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(smtpConfigRemoved) - if err != nil { - return nil, zerrors.ThrowInternal(err, "IAM-DVw1s", "unable to unmarshal smtp config removed") - } - - return smtpConfigRemoved, nil -} diff --git a/internal/repository/user/eventstore.go b/internal/repository/user/eventstore.go index e04754e54a..2b726d378a 100644 --- a/internal/repository/user/eventstore.go +++ b/internal/repository/user/eventstore.go @@ -137,4 +137,8 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, MachineSecretCheckSucceededType, MachineSecretCheckSucceededEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, MachineSecretCheckFailedType, MachineSecretCheckFailedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, MachineSecretHashUpdatedType, eventstore.GenericEventMapper[MachineSecretHashUpdatedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, HumanInviteCodeAddedType, eventstore.GenericEventMapper[HumanInviteCodeAddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, HumanInviteCodeSentType, eventstore.GenericEventMapper[HumanInviteCodeSentEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, HumanInviteCheckSucceededType, eventstore.GenericEventMapper[HumanInviteCheckSucceededEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, HumanInviteCheckFailedType, eventstore.GenericEventMapper[HumanInviteCheckFailedEvent]) } diff --git a/internal/repository/user/human.go b/internal/repository/user/human.go index e9fd49a359..ae1e9672ef 100644 --- a/internal/repository/user/human.go +++ b/internal/repository/user/human.go @@ -21,6 +21,10 @@ const ( HumanInitialCodeSentType = humanEventPrefix + "initialization.code.sent" HumanInitializedCheckSucceededType = humanEventPrefix + "initialization.check.succeeded" HumanInitializedCheckFailedType = humanEventPrefix + "initialization.check.failed" + HumanInviteCodeAddedType = humanEventPrefix + "invite.code.added" + HumanInviteCodeSentType = humanEventPrefix + "invite.code.sent" + HumanInviteCheckSucceededType = humanEventPrefix + "invite.check.succeeded" + HumanInviteCheckFailedType = humanEventPrefix + "invite.check.failed" HumanSignedOutType = humanEventPrefix + "signed.out" ) @@ -379,6 +383,137 @@ func HumanInitializedCheckFailedEventMapper(event eventstore.Event) (eventstore. }, nil } +type HumanInviteCodeAddedEvent struct { + *eventstore.BaseEvent `json:"-"` + Code *crypto.CryptoValue `json:"code,omitempty"` + Expiry time.Duration `json:"expiry,omitempty"` + TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` + URLTemplate string `json:"urlTemplate,omitempty"` + CodeReturned bool `json:"codeReturned,omitempty"` + ApplicationName string `json:"applicationName,omitempty"` + AuthRequestID string `json:"authRequestID,omitempty"` +} + +func (e *HumanInviteCodeAddedEvent) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = b +} + +func (e *HumanInviteCodeAddedEvent) Payload() interface{} { + return e +} + +func (e *HumanInviteCodeAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *HumanInviteCodeAddedEvent) TriggerOrigin() string { + return e.TriggeredAtOrigin +} + +func NewHumanInviteCodeAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + code *crypto.CryptoValue, + expiry time.Duration, + urlTemplate string, + codeReturned bool, + applicationName string, + authRequestID string, +) *HumanInviteCodeAddedEvent { + return &HumanInviteCodeAddedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + HumanInviteCodeAddedType, + ), + Code: code, + Expiry: expiry, + TriggeredAtOrigin: http.DomainContext(ctx).Origin(), + URLTemplate: urlTemplate, + CodeReturned: codeReturned, + ApplicationName: applicationName, + AuthRequestID: authRequestID, + } +} + +type HumanInviteCodeSentEvent struct { + *eventstore.BaseEvent `json:"-"` +} + +func (e *HumanInviteCodeSentEvent) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = b +} + +func (e *HumanInviteCodeSentEvent) Payload() interface{} { + return nil +} + +func (e *HumanInviteCodeSentEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewHumanInviteCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanInviteCodeSentEvent { + return &HumanInviteCodeSentEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + HumanInviteCodeSentType, + ), + } +} + +type HumanInviteCheckSucceededEvent struct { + *eventstore.BaseEvent `json:"-"` +} + +func (e *HumanInviteCheckSucceededEvent) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = b +} + +func (e *HumanInviteCheckSucceededEvent) Payload() interface{} { + return nil +} + +func (e *HumanInviteCheckSucceededEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewHumanInviteCheckSucceededEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanInviteCheckSucceededEvent { + return &HumanInviteCheckSucceededEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + HumanInviteCheckSucceededType, + ), + } +} + +type HumanInviteCheckFailedEvent struct { + *eventstore.BaseEvent `json:"-"` +} + +func (e *HumanInviteCheckFailedEvent) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = b +} + +func (e *HumanInviteCheckFailedEvent) Payload() interface{} { + return nil +} + +func (e *HumanInviteCheckFailedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewHumanInviteCheckFailedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanInviteCheckFailedEvent { + return &HumanInviteCheckFailedEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + HumanInviteCheckFailedType, + ), + } +} + type HumanSignedOutEvent struct { eventstore.BaseEvent `json:"-"` diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 5eac86ce0f..345acf42b0 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -705,6 +705,13 @@ EventTypes: check: succeeded: Проверката за инициализация е успешна failed: Проверката на инициализацията е неуспешна + invite: + code: + added: Генериран е код за покана + sent: Изпратен е код за покана + check: + succeeded: Проверката на поканата е успешна + failed: Проверката на поканата е неуспешна username: reserved: Потребителското име е запазено released: Потребителското име е освободено diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index 2c6c0dc266..75134b0112 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -686,6 +686,13 @@ EventTypes: check: succeeded: Kontrola inicializace byla úspěšná failed: Kontrola inicializace selhala + invite: + code: + added: Vygenerován pozvánkový kód + sent: Pozvánkový kód byl odeslán + check: + succeeded: Kontrola pozvánky byla úspěšná + failed: Kontrola pozvánky selhala username: reserved: Uživatelské jméno rezervováno released: Uživatelské jméno uvolněno diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 68ade0ed42..624adf84ce 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -688,6 +688,13 @@ EventTypes: check: succeeded: Benutzerinitialisierung erfolgreich failed: Benutzerinitialisierung fehlgeschlagen + invite: + code: + added: Einladungscode generiert + sent: Einladungscode gesendet + check: + succeeded: Einladungsprüfung erfolgreich + failed: Einladungsprüfung fehlgeschlagen username: reserved: Benutzername reserviert released: Benutzername freigegeben diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index cc920a3efb..8f271ec9c4 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -688,6 +688,13 @@ EventTypes: check: succeeded: Initialization check succeeded failed: Initialization check failed + invite: + code: + added: Invitation code generated + sent: Invitation code sent + check: + succeeded: Invitation check succeeded + failed: Invitation check failed username: reserved: Username reserved released: Username released diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 58ecfa261c..2d485ac5f5 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -688,6 +688,13 @@ EventTypes: check: succeeded: Comprobación exitosa de la inicialización failed: Fallo en la comprobación de la inicialización + invite: + code: + added: Código de invitación generado + sent: Código de invitación enviado + check: + succeeded: Comprobación de invitación correcta + failed: Comprobación de invitación fallida username: reserved: Nombre de usuario reservado released: Nombre de usuario liberado diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 8fa31c4667..683e0628e4 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -686,6 +686,13 @@ EventTypes: check: succeeded: Vérification de l'initialisation réussie failed: La vérification de l'initialisation a échoué + invite: + code: + added: Code d'invitation généré + sent: Code d'invitation envoyé + check: + succeeded: Vérification de l'invitation réussie + failed: Vérification de l'invitation échouée username: reserved: Nom d'utilisateur réservé released: Nom d'utilisateur libéré diff --git a/internal/static/i18n/id.yaml b/internal/static/i18n/id.yaml index 652759e6b9..60e4c395ad 100644 --- a/internal/static/i18n/id.yaml +++ b/internal/static/i18n/id.yaml @@ -680,6 +680,13 @@ EventTypes: check: succeeded: Pemeriksaan inisialisasi berhasil failed: Pemeriksaan inisialisasi gagal + invite: + code: + added: Kode undangan dihasilkan + sent: Kode undangan dikirim + check: + succeeded: Pemeriksaan undangan berhasil + failed: Pemeriksaan undangan gagal username: reserved: Nama pengguna dicadangkan released: Nama pengguna dirilis diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index d21c80e60d..33c73b0e08 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -687,6 +687,13 @@ EventTypes: check: succeeded: Controllo dell'inizializzazione riuscito failed: Controllo dell'inizializzazione fallito + invite: + code: + added: Codice invito generato + sent: Codice invito inviato + check: + succeeded: Controllo invito riuscito + failed: Controllo invito fallito username: reserved: Nome utente riservato released: Nome utente rilasciato diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index ed50c7db8d..33f74a72f5 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -677,6 +677,13 @@ EventTypes: check: succeeded: 初期化チェックの成功 failed: 初期化チェックの失敗 + invite: + code: + added: 招待コードが生成されました + sent: 招待コードが送信されました + check: + succeeded: 招待のチェックが成功しました + failed: 招待のチェックが失敗しました username: reserved: ユーザー名の予約 released: ユーザー名の解放 diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 7239b9ce9a..84145c4730 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -687,6 +687,13 @@ EventTypes: check: succeeded: Проверката на иницијализацијата е успешна failed: Проверката на иницијализацијата е неуспешна + invite: + code: + added: Генериран е код за покана + sent: Изпратен е код за покана + check: + succeeded: Проверката на поканата е успешна + failed: Проверката на поканата е неуспешна username: reserved: Корисничкото име е резервирано released: Корисничкото име е ослободено diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index 3991abb255..2cf0854ce5 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -688,6 +688,13 @@ EventTypes: check: succeeded: Initialisatiecontrole geslaagd failed: Initialisatiecontrole mislukt + invite: + code: + added: Uitnodigingscode gegenereerd + sent: Uitnodigingscode verzonden + check: + succeeded: Uitnodigingscontrole geslaagd + failed: Uitnodigingscontrole mislukt username: reserved: Gebruikersnaam gereserveerd released: Gebruikersnaam vrijgegeven diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index e9fcac3a3e..24cc746a49 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -688,6 +688,13 @@ EventTypes: check: succeeded: Sprawdzenie inicjalizacji zakończone powodzeniem failed: Sprawdzenie inicjalizacji nie powiodło się + invite: + code: + added: Wygenerowano kod zaproszenia + sent: Kod zaproszenia został wysłany + check: + succeeded: Kontrola zaproszenia zakończona sukcesem + failed: Kontrola zaproszenia zakończona niepowodzeniem username: reserved: Zarezerwowano nazwę użytkownika released: Zwolniono nazwę użytkownika diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index fa553641be..3dbf5901f0 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -683,6 +683,13 @@ EventTypes: check: succeeded: Verificação de inicialização bem-sucedida failed: Verificação de inicialização falhou + invite: + code: + added: Código de convite gerado + sent: Código de convite enviado + check: + succeeded: Verificação do convite bem-sucedida + failed: Verificação do convite falhou username: reserved: Nome de usuário reservado released: Nome de usuário liberado diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 00bfe57955..6d334a62a4 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -677,6 +677,13 @@ EventTypes: check: succeeded: Проверка инициализации прошла успешно failed: Проверка инициализации не удалась + invite: + code: + added: Сгенерирован код приглашения + sent: Код приглашения отправлен + check: + succeeded: Проверка приглашения успешно завершена + failed: Проверка приглашения завершилась неудачно username: reserved: Имя пользователя зарезервировано released: Имя пользователя опубликовано diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index da33e42269..91198335b8 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -687,6 +687,13 @@ EventTypes: check: succeeded: Initialiseringskontroll lyckades failed: Initialiseringskontroll misslyckades + invite: + code: + added: Inbjudningskod genererad + sent: Inbjudningskod skickad + check: + succeeded: Inbjudningskontroll lyckad + failed: Inbjudningskontroll misslyckad username: reserved: Användarnamn reserverat released: Användarnamn släppt diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 881909a20c..6e053c9687 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -687,6 +687,13 @@ EventTypes: check: succeeded: 初始化检查成功 failed: 初始化检查失败 + invite: + code: + added: 生成了邀请码 + sent: 发送了邀请码 + check: + succeeded: 邀请检查成功 + failed: 邀请检查失败 username: reserved: 保留用户名 released: 用户名已发布 diff --git a/internal/v2/database/number_filter.go b/internal/v2/database/number_filter.go index ce263ceeee..4853806457 100644 --- a/internal/v2/database/number_filter.go +++ b/internal/v2/database/number_filter.go @@ -3,6 +3,7 @@ package database import ( "time" + "github.com/shopspring/decimal" "github.com/zitadel/logging" "golang.org/x/exp/constraints" ) @@ -94,7 +95,7 @@ func (c numberCompare) String() string { } type number interface { - constraints.Integer | constraints.Float | time.Time + constraints.Integer | constraints.Float | time.Time | decimal.Decimal // TODO: condition must know if it's args are named parameters or not // constraints.Integer | constraints.Float | time.Time | placeholder } diff --git a/internal/v2/eventstore/event_store.go b/internal/v2/eventstore/event_store.go index cc447c5e15..e89786c657 100644 --- a/internal/v2/eventstore/event_store.go +++ b/internal/v2/eventstore/event_store.go @@ -2,6 +2,8 @@ package eventstore import ( "context" + + "github.com/shopspring/decimal" ) func NewEventstore(querier Querier, pusher Pusher) *EventStore { @@ -30,12 +32,12 @@ type healthier interface { } type GlobalPosition struct { - Position float64 + Position decimal.Decimal InPositionOrder uint32 } func (gp GlobalPosition) IsLess(other GlobalPosition) bool { - return gp.Position < other.Position || (gp.Position == other.Position && gp.InPositionOrder < other.InPositionOrder) + return gp.Position.LessThan(other.Position) || (gp.Position.Equal(other.Position) && gp.InPositionOrder < other.InPositionOrder) } type Reducer interface { diff --git a/internal/v2/eventstore/postgres/push_test.go b/internal/v2/eventstore/postgres/push_test.go index 91fdc1fcd7..6f8b224c41 100644 --- a/internal/v2/eventstore/postgres/push_test.go +++ b/internal/v2/eventstore/postgres/push_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database/mock" "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/zerrors" @@ -818,7 +820,7 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, }, ), @@ -899,11 +901,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), @@ -984,11 +986,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), @@ -1044,7 +1046,7 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, }, ), @@ -1099,7 +1101,7 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, }, ), @@ -1181,11 +1183,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), @@ -1272,11 +1274,11 @@ func Test_push(t *testing.T) { [][]driver.Value{ { time.Now(), - float64(123), + decimal.NewFromFloat(123).String(), }, { time.Now(), - float64(123.1), + decimal.NewFromFloat(123.1).String(), }, }, ), diff --git a/internal/v2/eventstore/postgres/query_test.go b/internal/v2/eventstore/postgres/query_test.go index 56f506ac50..34b73bd820 100644 --- a/internal/v2/eventstore/postgres/query_test.go +++ b/internal/v2/eventstore/postgres/query_test.go @@ -8,6 +8,8 @@ import ( "testing" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database" "github.com/zitadel/zitadel/internal/v2/database/mock" "github.com/zitadel/zitadel/internal/v2/eventstore" @@ -541,13 +543,13 @@ func Test_writeFilter(t *testing.T) { args: args{ filter: eventstore.NewFilter( eventstore.FilterPagination( - eventstore.PositionGreater(123.4, 0), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 0), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND position > $2 ORDER BY position, in_tx_order", - args: []any{"i1", 123.4}, + args: []any{"i1", decimal.NewFromFloat(123.4)}, }, }, { @@ -555,18 +557,18 @@ func Test_writeFilter(t *testing.T) { args: args{ filter: eventstore.NewFilter( eventstore.FilterPagination( - // eventstore.PositionGreater(123.4, 0), + // eventstore.PositionGreater(decimal.NewFromFloat(123.4), 0), // eventstore.PositionLess(125.4, 10), eventstore.PositionBetween( - &eventstore.GlobalPosition{Position: 123.4}, - &eventstore.GlobalPosition{Position: 125.4, InPositionOrder: 10}, + &eventstore.GlobalPosition{Position: decimal.NewFromFloat(123.4)}, + &eventstore.GlobalPosition{Position: decimal.NewFromFloat(125.4), InPositionOrder: 10}, ), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND ((position = $2 AND in_tx_order < $3) OR position < $4) AND position > $5 ORDER BY position, in_tx_order", - args: []any{"i1", 125.4, uint32(10), 125.4, 123.4}, + args: []any{"i1", decimal.NewFromFloat(125.4), uint32(10), decimal.NewFromFloat(125.4), decimal.NewFromFloat(123.4)}, // TODO: (adlerhurst) would require some refactoring to reuse existing args // query: " WHERE instance_id = $1 AND position > $2 AND ((position = $3 AND in_tx_order < $4) OR position < $3) ORDER BY position, in_tx_order", // args: []any{"i1", 123.4, 125.4, uint32(10)}, @@ -577,13 +579,13 @@ func Test_writeFilter(t *testing.T) { args: args{ filter: eventstore.NewFilter( eventstore.FilterPagination( - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND ((position = $2 AND in_tx_order > $3) OR position > $4) ORDER BY position, in_tx_order", - args: []any{"i1", 123.4, uint32(12), 123.4}, + args: []any{"i1", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4)}, }, }, { @@ -593,13 +595,13 @@ func Test_writeFilter(t *testing.T) { eventstore.FilterPagination( eventstore.Limit(10), eventstore.Offset(3), - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND ((position = $2 AND in_tx_order > $3) OR position > $4) ORDER BY position, in_tx_order LIMIT $5 OFFSET $6", - args: []any{"i1", 123.4, uint32(12), 123.4, uint32(10), uint32(3)}, + args: []any{"i1", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4), uint32(10), uint32(3)}, }, }, { @@ -609,14 +611,14 @@ func Test_writeFilter(t *testing.T) { eventstore.FilterPagination( eventstore.Limit(10), eventstore.Offset(3), - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), eventstore.AppendAggregateFilter("user"), ), }, want: wantQuery{ query: " WHERE instance_id = $1 AND aggregate_type = $2 AND ((position = $3 AND in_tx_order > $4) OR position > $5) ORDER BY position, in_tx_order LIMIT $6 OFFSET $7", - args: []any{"i1", "user", 123.4, uint32(12), 123.4, uint32(10), uint32(3)}, + args: []any{"i1", "user", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4), uint32(10), uint32(3)}, }, }, { @@ -626,7 +628,7 @@ func Test_writeFilter(t *testing.T) { eventstore.FilterPagination( eventstore.Limit(10), eventstore.Offset(3), - eventstore.PositionGreater(123.4, 12), + eventstore.PositionGreater(decimal.NewFromFloat(123.4), 12), ), eventstore.AppendAggregateFilter("user"), eventstore.AppendAggregateFilter( @@ -637,7 +639,7 @@ func Test_writeFilter(t *testing.T) { }, want: wantQuery{ query: " WHERE instance_id = $1 AND (aggregate_type = $2 OR (aggregate_type = $3 AND aggregate_id = $4)) AND ((position = $5 AND in_tx_order > $6) OR position > $7) ORDER BY position, in_tx_order LIMIT $8 OFFSET $9", - args: []any{"i1", "user", "org", "o1", 123.4, uint32(12), 123.4, uint32(10), uint32(3)}, + args: []any{"i1", "user", "org", "o1", decimal.NewFromFloat(123.4), uint32(12), decimal.NewFromFloat(123.4), uint32(10), uint32(3)}, }, }, } @@ -956,7 +958,7 @@ func Test_writeQueryUse_examples(t *testing.T) { ), eventstore.FilterPagination( // used because we need to check for first login and an app which is not console - eventstore.PositionGreater(12, 4), + eventstore.PositionGreater(decimal.NewFromInt(12), 4), ), ), eventstore.NewFilter( @@ -1065,9 +1067,9 @@ func Test_writeQueryUse_examples(t *testing.T) { "instance", "user", "user.token.added", - float64(12), + decimal.NewFromInt(12), uint32(4), - float64(12), + decimal.NewFromInt(12), "instance", "instance", []string{"instance.idp.config.added", "instance.idp.oauth.added", "instance.idp.oidc.added", "instance.idp.jwt.added", "instance.idp.azure.added", "instance.idp.github.added", "instance.idp.github.enterprise.added", "instance.idp.gitlab.added", "instance.idp.gitlab.selfhosted.added", "instance.idp.google.added", "instance.idp.ldap.added", "instance.idp.config.apple.added", "instance.idp.saml.added"}, @@ -1201,7 +1203,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), nil, "gigi", @@ -1235,7 +1237,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), []byte(`{"name": "gigi"}`), "gigi", @@ -1269,7 +1271,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), nil, "gigi", @@ -1283,7 +1285,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(24), - float64(124), + decimal.NewFromInt(124).String(), uint32(0), []byte(`{"name": "gigi"}`), "gigi", @@ -1317,7 +1319,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(23), - float64(123), + decimal.NewFromInt(123).String(), uint32(0), nil, "gigi", @@ -1331,7 +1333,7 @@ func Test_executeQuery(t *testing.T) { time.Now(), "event.type", uint32(24), - float64(124), + decimal.NewFromInt(124).String(), uint32(0), []byte(`{"name": "gigi"}`), "gigi", diff --git a/internal/v2/eventstore/query.go b/internal/v2/eventstore/query.go index c9b3cecd37..f7a30a2139 100644 --- a/internal/v2/eventstore/query.go +++ b/internal/v2/eventstore/query.go @@ -7,6 +7,8 @@ import ( "slices" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database" ) @@ -723,7 +725,7 @@ func (pc *PositionCondition) Min() *GlobalPosition { // PositionGreater prepares the condition as follows // if inPositionOrder is set: position = AND in_tx_order > OR or position > // if inPositionOrder is NOT set: position > -func PositionGreater(position float64, inPositionOrder uint32) paginationOpt { +func PositionGreater(position decimal.Decimal, inPositionOrder uint32) paginationOpt { return func(p *Pagination) { p.ensurePosition() p.position.min = &GlobalPosition{ @@ -743,7 +745,7 @@ func GlobalPositionGreater(position *GlobalPosition) paginationOpt { // PositionLess prepares the condition as follows // if inPositionOrder is set: position = AND in_tx_order > OR or position > // if inPositionOrder is NOT set: position > -func PositionLess(position float64, inPositionOrder uint32) paginationOpt { +func PositionLess(position decimal.Decimal, inPositionOrder uint32) paginationOpt { return func(p *Pagination) { p.ensurePosition() p.position.max = &GlobalPosition{ diff --git a/internal/v2/eventstore/query_test.go b/internal/v2/eventstore/query_test.go index 00c08914c1..0f313e9560 100644 --- a/internal/v2/eventstore/query_test.go +++ b/internal/v2/eventstore/query_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/database" ) @@ -74,13 +76,13 @@ func TestPaginationOpt(t *testing.T) { name: "global position greater", args: args{ opts: []paginationOpt{ - GlobalPositionGreater(&GlobalPosition{Position: 10}), + GlobalPositionGreater(&GlobalPosition{Position: decimal.NewFromInt(10)}), }, }, want: &Pagination{ position: &PositionCondition{ min: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 0, }, }, @@ -90,13 +92,13 @@ func TestPaginationOpt(t *testing.T) { name: "position greater", args: args{ opts: []paginationOpt{ - PositionGreater(10, 0), + PositionGreater(decimal.NewFromInt(10), 0), }, }, want: &Pagination{ position: &PositionCondition{ min: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 0, }, }, @@ -107,13 +109,13 @@ func TestPaginationOpt(t *testing.T) { name: "position less", args: args{ opts: []paginationOpt{ - PositionLess(10, 12), + PositionLess(decimal.NewFromInt(10), 12), }, }, want: &Pagination{ position: &PositionCondition{ max: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 12, }, }, @@ -123,13 +125,13 @@ func TestPaginationOpt(t *testing.T) { name: "global position less", args: args{ opts: []paginationOpt{ - GlobalPositionLess(&GlobalPosition{Position: 12, InPositionOrder: 24}), + GlobalPositionLess(&GlobalPosition{Position: decimal.NewFromInt(12), InPositionOrder: 24}), }, }, want: &Pagination{ position: &PositionCondition{ max: &GlobalPosition{ - Position: 12, + Position: decimal.NewFromInt(12), InPositionOrder: 24, }, }, @@ -140,19 +142,19 @@ func TestPaginationOpt(t *testing.T) { args: args{ opts: []paginationOpt{ PositionBetween( - &GlobalPosition{10, 12}, - &GlobalPosition{20, 0}, + &GlobalPosition{decimal.NewFromInt(10), 12}, + &GlobalPosition{decimal.NewFromInt(20), 0}, ), }, }, want: &Pagination{ position: &PositionCondition{ min: &GlobalPosition{ - Position: 10, + Position: decimal.NewFromInt(10), InPositionOrder: 12, }, max: &GlobalPosition{ - Position: 20, + Position: decimal.NewFromInt(20), InPositionOrder: 0, }, }, diff --git a/internal/v2/readmodel/last_successful_mirror.go b/internal/v2/readmodel/last_successful_mirror.go index 80b436b896..6635b73342 100644 --- a/internal/v2/readmodel/last_successful_mirror.go +++ b/internal/v2/readmodel/last_successful_mirror.go @@ -1,6 +1,8 @@ package readmodel import ( + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/v2/system" "github.com/zitadel/zitadel/internal/v2/system/mirror" @@ -8,7 +10,7 @@ import ( type LastSuccessfulMirror struct { ID string - Position float64 + Position decimal.Decimal source string } @@ -53,7 +55,7 @@ func (h *LastSuccessfulMirror) Reduce(events ...*eventstore.StorageEvent) (err e func (h *LastSuccessfulMirror) reduceSucceeded(event *eventstore.StorageEvent) error { // if position is set we skip all older events - if h.Position > 0 { + if h.Position.GreaterThan(decimal.NewFromInt(0)) { return nil } diff --git a/internal/v2/system/mirror/succeeded.go b/internal/v2/system/mirror/succeeded.go index 6d0fba2c25..34d74f184f 100644 --- a/internal/v2/system/mirror/succeeded.go +++ b/internal/v2/system/mirror/succeeded.go @@ -1,6 +1,8 @@ package mirror import ( + "github.com/shopspring/decimal" + "github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -9,7 +11,7 @@ type succeededPayload struct { // Source is the name of the database data are mirrored from Source string `json:"source"` // Position until data will be mirrored - Position float64 `json:"position"` + Position decimal.Decimal `json:"position"` } const SucceededType = eventTypePrefix + "succeeded" @@ -38,7 +40,7 @@ func SucceededEventFromStorage(event *eventstore.StorageEvent) (e *SucceededEven }, nil } -func NewSucceededCommand(source string, position float64) *eventstore.Command { +func NewSucceededCommand(source string, position decimal.Decimal) *eventstore.Command { return &eventstore.Command{ Action: eventstore.Action[any]{ Creator: Creator, diff --git a/internal/webauthn/webauthn.go b/internal/webauthn/webauthn.go index 5938f885bc..998c013a3c 100644 --- a/internal/webauthn/webauthn.go +++ b/internal/webauthn/webauthn.go @@ -100,7 +100,7 @@ func (w *Config) FinishRegistration(ctx context.Context, user *domain.Human, web } credentialData, err := protocol.ParseCredentialCreationResponseBody(bytes.NewReader(credData)) if err != nil { - logging.WithFields("error", tryExtractProtocolErrMsg(err)).Debug("webauthn credential could not be parsed") + logging.WithFields("error", tryExtractProtocolErrMsg(err), "err_id", "WEBAU-sEr8c").Debug("webauthn credential could not be parsed") return nil, zerrors.ThrowInternal(err, "WEBAU-sEr8c", "Errors.User.WebAuthN.ErrorOnParseCredential") } sessionData := WebAuthNToSessionData(webAuthN) @@ -115,7 +115,7 @@ func (w *Config) FinishRegistration(ctx context.Context, user *domain.Human, web sessionData, credentialData) if err != nil { - logging.WithFields("error", tryExtractProtocolErrMsg(err)).Debug("webauthn credential could not be created") + logging.WithFields("error", tryExtractProtocolErrMsg(err), "err_id", "WEBAU-3Vb9s").Debug("webauthn credential could not be created") return nil, zerrors.ThrowInternal(err, "WEBAU-3Vb9s", "Errors.User.WebAuthN.CreateCredentialFailed") } diff --git a/load-test/Makefile b/load-test/Makefile index 4d87760eca..bbd5ebf538 100644 --- a/load-test/Makefile +++ b/load-test/Makefile @@ -4,42 +4,49 @@ ZITADEL_HOST ?= ADMIN_LOGIN_NAME ?= ADMIN_PASSWORD ?= +K6 := ./../../xk6-modules/k6 + .PHONY: human_password_login human_password_login: bundle - k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/human_password_login.js --vus ${VUS} --duration ${DURATION} + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/human_password_login.js --vus ${VUS} --duration ${DURATION} .PHONY: machine_pat_login machine_pat_login: bundle - k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/machine_pat_login.js --vus ${VUS} --duration ${DURATION} + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/machine_pat_login.js --vus ${VUS} --duration ${DURATION} .PHONY: machine_client_credentials_login machine_client_credentials_login: bundle - k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/machine_client_credentials_login.js --vus ${VUS} --duration ${DURATION} + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/machine_client_credentials_login.js --vus ${VUS} --duration ${DURATION} .PHONY: user_info user_info: bundle - k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/user_info.js --vus ${VUS} --duration ${DURATION} + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/user_info.js --vus ${VUS} --duration ${DURATION} .PHONY: manipulate_user manipulate_user: bundle - k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/manipulate_user.js --vus ${VUS} --duration ${DURATION} + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/manipulate_user.js --vus ${VUS} --duration ${DURATION} .PHONY: introspect introspect: ensure_modules bundle go install go.k6.io/xk6/cmd/xk6@latest cd ../../xk6-modules && xk6 build --with xk6-zitadel=. - ./../../xk6-modules/k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/introspection.js --vus ${VUS} --duration ${DURATION} + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/introspection.js --vus ${VUS} --duration ${DURATION} .PHONY: add_session add_session: bundle - k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/session.js --vus ${VUS} --duration ${DURATION} + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/session.js --vus ${VUS} --duration ${DURATION} .PHONY: machine_jwt_profile_grant machine_jwt_profile_grant: ensure_modules ensure_key_pair bundle go install go.k6.io/xk6/cmd/xk6@latest cd ../../xk6-modules && xk6 build --with xk6-zitadel=. - ./../../xk6-modules/k6 run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/machine_jwt_profile_grant.js --iterations 1 - # --vus ${VUS} --duration ${DURATION} + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/machine_jwt_profile_grant.js --vus ${VUS} --duration ${DURATION} + +.PHONY: machine_jwt_profile_grant_single_user +machine_jwt_profile_grant_single_user: ensure_modules ensure_key_pair bundle + go install go.k6.io/xk6/cmd/xk6@latest + cd ../../xk6-modules && xk6 build --with xk6-zitadel=. + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/machine_jwt_profile_grant_single_user.js --vus ${VUS} --duration ${DURATION} .PHONY: lint lint: @@ -58,6 +65,8 @@ endif bundle: npm i npm run bundle + go install go.k6.io/xk6/cmd/xk6@latest + cd ../../xk6-modules && xk6 build --with xk6-zitadel=. .PHONY: ensure_key_pair ensure_key_pair: diff --git a/load-test/README.md b/load-test/README.md index e046372ee0..b4b2a6cae9 100644 --- a/load-test/README.md +++ b/load-test/README.md @@ -52,4 +52,7 @@ Before you run the tests you need an initialized user. The tests don't implement test: creates new sessions with user id check * `make machine_jwt_profile_grant` setup: generates private/public key, creates machine users, adds a key - test: creates a token and calls user info \ No newline at end of file + test: creates a token and calls user info +* `make machine_jwt_profile_grant_single_user` + setup: generates private/public key, creates machine user, adds a key + test: creates a token and calls user info in parallel for the same user \ No newline at end of file diff --git a/load-test/src/use_cases/machine_jwt_profile_grant_single_user.ts b/load-test/src/use_cases/machine_jwt_profile_grant_single_user.ts new file mode 100644 index 0000000000..c654fb9492 --- /dev/null +++ b/load-test/src/use_cases/machine_jwt_profile_grant_single_user.ts @@ -0,0 +1,35 @@ +import { loginByUsernamePassword } from '../login_ui'; +import { createOrg, removeOrg } from '../org'; +import {createMachine, User, addMachineKey} from '../user'; +import {JWTProfileRequest, token, userinfo} from '../oidc'; +import { Config } from '../config'; +import encoding from 'k6/encoding'; + +const publicKey = encoding.b64encode(open('../.keys/key.pem.pub')); + +export async function setup() { + const tokens = loginByUsernamePassword(Config.admin as User); + console.info('setup: admin signed in'); + + const org = await createOrg(tokens.accessToken!); + console.info(`setup: org (${org.organizationId}) created`); + + const machine = await createMachine(`zitachine`, org, tokens.accessToken!); + console.info(`setup: machine ${machine.userId} created`); + const key = await addMachineKey(machine.userId, org, tokens.accessToken!, publicKey); + console.info(`setup: key ${key.keyId} added`); + + return { tokens, machine: {userId: machine.userId, keyId: key.keyId}, org }; +} + +export default function (data: any) { + token(new JWTProfileRequest(data.machine.userId, data.machine.keyId)) + .then((token) => { + userinfo(token.accessToken!) + }) +} + +export function teardown(data: any) { + removeOrg(data.org, data.tokens.accessToken); + console.info('teardown: org removed'); +} diff --git a/load-test/src/use_cases/manipulate_user.ts b/load-test/src/use_cases/manipulate_user.ts index 2ea53bd324..058bde05f1 100644 --- a/load-test/src/use_cases/manipulate_user.ts +++ b/load-test/src/use_cases/manipulate_user.ts @@ -45,3 +45,4 @@ export function teardown(data: any) { removeOrg(data.org, data.tokens.accessToken); console.info('teardown: org removed'); } + diff --git a/pkg/grpc/settings/settings.go b/pkg/grpc/settings/settings.go index 32b6d83125..3daa4ed141 100644 --- a/pkg/grpc/settings/settings.go +++ b/pkg/grpc/settings/settings.go @@ -1,3 +1,5 @@ package settings type SMSConfig = isSMSProvider_Config + +type EmailConfig = isEmailProvider_Config diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index e6f098c2a2..6e1020cf5e 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -143,32 +143,32 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } security_definitions: { - security: { - key: "BasicAuth"; - value: { - type: TYPE_BASIC; - } - } - security: { - key: "OAuth2"; - value: { - type: TYPE_OAUTH2; - flow: FLOW_ACCESS_CODE; - authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; - token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; - scopes: { - scope: { - key: "openid"; - value: "openid"; - } - scope: { - key: "urn:zitadel:iam:org:project:id:zitadel:aud"; - value: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - } - } + security: { + key: "BasicAuth"; + value: { + type: TYPE_BASIC; + } + } + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } security: { security_requirement: { key: "OAuth2"; @@ -342,6 +342,7 @@ service AdminService { rpc AddInstanceTrustedDomain(AddInstanceTrustedDomainRequest) returns (AddInstanceTrustedDomainResponse) { option (google.api.http) = { post: "/trusted_domains"; + body: "*" }; option (zitadel.v1.auth_option) = { @@ -421,6 +422,11 @@ service AdminService { }; } + // Deprecated: Get active SMTP Configuration + // + // Returns the active SMTP configuration from the system. This is used to send E-Mails to the users. + // + // Deprecated: please move to the new endpoint GetEmailProvider. rpc GetSMTPConfig(GetSMTPConfigRequest) returns (GetSMTPConfigResponse) { option (google.api.http) = { get: "/smtp"; @@ -431,12 +437,16 @@ service AdminService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "SMTP"; - summary: "Get active SMTP Configuration"; - description: "Returns the active SMTP configuration from the system. This is used to send E-Mails to the users." + tags: "SMTP Configs"; + deprecated: true; }; } + // Deprecated: Get SMTP provider configuration by its id + // + // Get a specific SMTP provider configuration by its ID. + // + // Deprecated: please move to the new endpoint GetEmailProviderById. rpc GetSMTPConfigById(GetSMTPConfigByIdRequest) returns (GetSMTPConfigByIdResponse) { option (google.api.http) = { get: "/smtp/{id}"; @@ -447,12 +457,16 @@ service AdminService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "SMTP"; - summary: "Get SMTP provider configuration by its id"; - description: "Get a specific SMTP provider configuration by its ID."; + tags: "SMTP Configs"; + deprecated: true; }; } + // Deprecated: Add SMTP Configuration + // + // Add a new SMTP configuration if nothing is set yet. + // + // Deprecated: please move to the new endpoint AddEmailProviderSMTP. rpc AddSMTPConfig(AddSMTPConfigRequest) returns (AddSMTPConfigResponse) { option (google.api.http) = { post: "/smtp"; @@ -464,12 +478,16 @@ service AdminService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "SMTP"; - summary: "Add SMTP Configuration"; - description: "Add a new SMTP configuration if nothing is set yet." + tags: "SMTP Configs"; + deprecated: true; }; } + // Deprecated: Update SMTP Configuration + // + // Update the SMTP configuration, be aware that this will be activated as soon as it is saved. So the users will get notifications from the newly configured SMTP. + // + // Deprecated: please move to the new endpoint UpdateEmailProviderSMTP. rpc UpdateSMTPConfig(UpdateSMTPConfigRequest) returns (UpdateSMTPConfigResponse) { option (google.api.http) = { put: "/smtp/{id}"; @@ -481,12 +499,16 @@ service AdminService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "SMTP"; - summary: "Update SMTP Configuration"; - description: "Update the SMTP configuration, be aware that this will be activated as soon as it is saved. So the users will get notifications from the newly configured SMTP." + tags: "SMTP Configs"; + deprecated: true; }; } + // Deprecated: Update SMTP Password + // + // Update the SMTP password that is used for the host, be aware that this will be activated as soon as it is saved. So the users will get notifications from the newly configured SMTP. + // + // Deprecated: please move to the new endpoint UpdateEmailProviderSMTPPassword. rpc UpdateSMTPConfigPassword(UpdateSMTPConfigPasswordRequest) returns (UpdateSMTPConfigPasswordResponse) { option (google.api.http) = { put: "/smtp/{id}/password"; @@ -498,12 +520,16 @@ service AdminService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "SMTP"; - summary: "Update SMTP Password"; - description: "Update the SMTP password that is used for the host, be aware that this will be activated as soon as it is saved. So the users will get notifications from the newly configured SMTP." + tags: "SMTP Configs"; + deprecated: true; }; } + // Deprecated: Activate SMTP Provider + // + // Activate an SMTP provider. + // + // Deprecated: please move to the new endpoint ActivateEmailProvider. rpc ActivateSMTPConfig(ActivateSMTPConfigRequest) returns (ActivateSMTPConfigResponse) { option (google.api.http) = { post: "/smtp/{id}/_activate"; @@ -515,12 +541,18 @@ service AdminService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "SMTP Provider"; + tags: "SMTP Configs"; summary: "Activate SMTP Provider"; description: "Activate an SMTP provider." + deprecated: true; }; } + // Deprecated: Deactivate SMTP Provider + // + // Deactivate an SMTP provider. After deactivating the provider, the users will not be able to receive SMTP notifications from that provider anymore. + // + // Deprecated: please move to the new endpoint DeactivateEmailProvider. rpc DeactivateSMTPConfig(DeactivateSMTPConfigRequest) returns (DeactivateSMTPConfigResponse) { option (google.api.http) = { post: "/smtp/{id}/_deactivate"; @@ -532,12 +564,16 @@ service AdminService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "SMTP Provider"; - summary: "Deactivate SMTP Provider"; - description: "Deactivate an SMTP provider. After deactivating the provider, the users will not be able to receive SMTP notifications from that provider anymore." + tags: "SMTP Configs"; + deprecated: true; }; } + // Deprecated: Remove SMTP Configuration + // + // Remove the SMTP configuration, be aware that the users will not get an E-Mail if no SMTP is set. + // + // Deprecated: please move to the new endpoint RemoveEmailProvider. rpc RemoveSMTPConfig(RemoveSMTPConfigRequest) returns (RemoveSMTPConfigResponse) { option (google.api.http) = { delete: "/smtp/{id}"; @@ -548,12 +584,16 @@ service AdminService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "SMTP"; - summary: "Remove SMTP Configuration"; - description: "Remove the SMTP configuration, be aware that the users will not get an E-Mail if no SMTP is set." + tags: "SMTP Configs"; + deprecated: true; }; } + // Deprecated: Test SMTP Provider + // + // Test an SMTP provider identified by its ID. After testing the provider, the users will receive information about the test results. + // + // Deprecated: please move to the new endpoint TestEmailProviderSMTPById. rpc TestSMTPConfigById(TestSMTPConfigByIdRequest) returns (TestSMTPConfigByIdResponse) { option (google.api.http) = { post: "/smtp/{id}/_test"; @@ -565,12 +605,16 @@ service AdminService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "SMTP Provider"; - summary: "Test SMTP Provider "; - description: "Test an SMTP provider identified by its ID. After testing the provider, the users will receive information about the test results." + tags: "SMTP Configs"; + deprecated: true; }; } + // Deprecated: Test SMTP Provider + // + // Test an SMTP provider. After testing the provider, the users will receive information about the test results. + // + // Deprecated: please move to the new endpoint TestEmailProviderSMTP. rpc TestSMTPConfig(TestSMTPConfigRequest) returns (TestSMTPConfigResponse) { option (google.api.http) = { post: "/smtp/_test"; @@ -582,12 +626,16 @@ service AdminService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "SMTP Provider"; - summary: "Test SMTP Provider"; - description: "Test an SMTP provider. After testing the provider, the users will receive information about the test results." + tags: "SMTP Configs"; + deprecated: true; }; } + // Deprecated: List SMTP Configs + // + // Returns a list of SMTP configurations. + // + // Deprecated: please move to the new endpoint ListEmailProviders. rpc ListSMTPConfigs(ListSMTPConfigsRequest) returns (ListSMTPConfigsResponse) { option (google.api.http) = { post: "/smtp/_search" @@ -600,8 +648,225 @@ service AdminService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "SMTP Configs"; - summary: "List SMTP Configs"; - description: "Returns a list of SMTP configurations." + deprecated: true; + }; + } + + rpc ListEmailProviders(ListEmailProvidersRequest) returns (ListEmailProvidersResponse) { + option (google.api.http) = { + post: "/email/_search" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.read"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Email Provider"; + summary: "List Email providers"; + description: "Returns a list of Email providers." + }; + } + + rpc GetEmailProvider(GetEmailProviderRequest) returns (GetEmailProviderResponse) { + option (google.api.http) = { + get: "/email"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.read"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Email Provider"; + summary: "Get active Email provider"; + description: "Returns the active Email provider from the system. This is used to send E-Mails to the users." + }; + } + + rpc GetEmailProviderById(GetEmailProviderByIdRequest) returns (GetEmailProviderByIdResponse) { + option (google.api.http) = { + get: "/email/{id}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.read"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Email Provider"; + summary: "Get Email provider by its id"; + description: "Get a specific Email provider by its ID."; + }; + } + + rpc AddEmailProviderSMTP(AddEmailProviderSMTPRequest) returns (AddEmailProviderSMTPResponse) { + option (google.api.http) = { + post: "/email/smtp"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Email Provider"; + summary: "Add SMTP Email provider"; + description: "Add a new SMTP Email provider if nothing is set yet." + }; + } + + rpc UpdateEmailProviderSMTP(UpdateEmailProviderSMTPRequest) returns (UpdateEmailProviderSMTPResponse) { + option (google.api.http) = { + put: "/email/smtp/{id}"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Email Provider"; + summary: "Update SMTP Email provider"; + description: "Update the SMTP Email provider, be aware that this will be activated as soon as it is saved. So the users will get notifications from the newly configured SMTP." + }; + } + + rpc AddEmailProviderHTTP(AddEmailProviderHTTPRequest) returns (AddEmailProviderHTTPResponse) { + option (google.api.http) = { + post: "/email/http"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Email Provider"; + summary: "Add HTTP Email provider"; + description: "Add a new HTTP Email provider if nothing is set yet." + }; + } + + rpc UpdateEmailProviderHTTP(UpdateEmailProviderHTTPRequest) returns (UpdateEmailProviderHTTPResponse) { + option (google.api.http) = { + put: "/email/http/{id}"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Email Provider"; + summary: "Update HTTP Email provider"; + description: "Update the HTTP Email provider, be aware that this will be activated as soon as it is saved. So the users will get notifications from the newly configured HTTP." + }; + } + + rpc UpdateEmailProviderSMTPPassword(UpdateEmailProviderSMTPPasswordRequest) returns (UpdateEmailProviderSMTPPasswordResponse) { + option (google.api.http) = { + put: "/email/smtp/{id}/password"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Email Provider"; + summary: "Update SMTP Password"; + description: "Update the SMTP password that is used for the host, be aware that this will be activated as soon as it is saved. So the users will get notifications from the newly configured SMTP." + }; + } + + rpc ActivateEmailProvider(ActivateEmailProviderRequest) returns (ActivateEmailProviderResponse) { + option (google.api.http) = { + post: "/email/{id}/_activate"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Email Provider"; + summary: "Activate Email Provider"; + description: "Activate an Email provider." + }; + } + + rpc DeactivateEmailProvider(DeactivateEmailProviderRequest) returns (DeactivateEmailProviderResponse) { + option (google.api.http) = { + post: "/email/{id}/_deactivate"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Email Provider"; + summary: "Deactivate Email Provider"; + description: "Deactivate an Email provider. After deactivating the provider, the users will not be able to receive Email notifications from that provider anymore." + }; + } + + rpc RemoveEmailProvider(RemoveEmailProviderRequest) returns (RemoveEmailProviderResponse) { + option (google.api.http) = { + delete: "/email/{id}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Email Provider"; + summary: "Remove Email provider"; + description: "Remove the Email provider, be aware that the users will not get an E-Mail if no provider is set." + }; + } + + rpc TestEmailProviderSMTPById(TestEmailProviderSMTPByIdRequest) returns (TestEmailProviderSMTPByIdResponse) { + option (google.api.http) = { + post: "/email/smtp/{id}/_test"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Email Provider"; + summary: "Test SMTP Email Provider"; + description: "Test an SMTP Email provider identified by its ID. After testing the provider, the users will receive information about the test results." + }; + } + + rpc TestEmailProviderSMTP(TestEmailProviderSMTPRequest) returns (TestEmailProviderSMTPResponse) { + option (google.api.http) = { + post: "/email/smtp/_test"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Email Provider"; + summary: "Test SMTP Email Provider"; + description: "Test an SMTP Email provider. After testing the provider, the users will receive information about the test results." }; } @@ -689,6 +954,40 @@ service AdminService { }; } + rpc AddSMSProviderHTTP(AddSMSProviderHTTPRequest) returns (AddSMSProviderHTTPResponse) { + option (google.api.http) = { + post: "/sms/http"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "SMS Provider"; + summary: "Add HTTP SMS Provider"; + description: "Configure a new SMS provider of the type HTTP. A provider has to be activated to be able to send notifications." + }; + } + + rpc UpdateSMSProviderHTTP(UpdateSMSProviderHTTPRequest) returns (UpdateSMSProviderHTTPResponse) { + option (google.api.http) = { + put: "/sms/http/{id}"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "SMS Provider"; + summary: "Update HTTP SMS Provider"; + description: "Change the configuration of an SMS provider of the type HTTP. A provider has to be activated to be able to send notifications." + }; + } + rpc ActivateSMSProvider(ActivateSMSProviderRequest) returns (ActivateSMSProviderResponse) { option (google.api.http) = { post: "/sms/{id}/_activate"; @@ -3553,6 +3852,71 @@ service AdminService { }; } + rpc GetDefaultInviteUserMessageText(GetDefaultInviteUserMessageTextRequest) returns (GetDefaultInviteUserMessageTextResponse) { + option (google.api.http) = { + get: "/text/default/message/invite_user/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Message Texts"; + summary: "Get Default Invite User Message Text"; + description: "Get the default text of the invite user message/email that is stored as translation files in ZITADEL itself. The text will be sent to the users of all organizations, that do not have a custom text configured. The message is sent when an invite code email is requested." + }; + } + + rpc GetCustomInviteUserMessageText(GetCustomInviteUserMessageTextRequest) returns (GetCustomInviteUserMessageTextResponse) { + option (google.api.http) = { + get: "/text/message/invite_user/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.read"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Message Texts"; + summary: "Get Custom Invite User Message Text"; + description: "Get the custom text of the invite user message/email that is overwritten on the instance as settings/database. The text will be sent to the users of all organizations, that do not have a custom text configured. The message is sent when an invite code email is requested." + }; + } + + rpc SetDefaultInviteUserMessageText(SetDefaultInviteUserMessageTextRequest) returns (SetDefaultInviteUserMessageTextResponse) { + option (google.api.http) = { + put: "/text/message/invite_user/{language}"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Message Texts"; + summary: "Set Default Invite User Message Text"; + description: "Set the custom text of the invite user message/email that is overwritten on the instance as settings/database. The text will be sent to the users of all organizations, that do not have a custom text configured. The message is sent when an invite code email is requested. The Following Variables can be used: {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} {{.ApplicationName}}" + }; + } + + rpc ResetCustomInviteUserMessageTextToDefault(ResetCustomInviteUserMessageTextToDefaultRequest) returns (ResetCustomInviteUserMessageTextToDefaultResponse) { + option (google.api.http) = { + delete: "/text/message/invite_user/{language}" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.policy.delete" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Message Texts"; + summary: "Reset Custom Invite User Message Text to Default"; + description: "Removes the custom text of the invite user message that is overwritten on the instance and triggers the text from the translation files stored in ZITADEL itself. The text will be sent to the users of all organizations, that do not have a custom text configured." + }; + } + rpc GetDefaultLoginTexts(GetDefaultLoginTextsRequest) returns (GetDefaultLoginTextsResponse) { option (google.api.http) = { get: "/text/default/login/{language}"; @@ -4461,6 +4825,320 @@ message TestSMTPConfigRequest { // This is an empty response message TestSMTPConfigResponse {} +//This is an empty request +message GetEmailProviderRequest {} + +message GetEmailProviderResponse { + zitadel.settings.v1.EmailProvider config = 1; +} + +message GetEmailProviderByIdRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 100}]; +} + +message GetEmailProviderByIdResponse { + zitadel.settings.v1.EmailProvider config = 1; +} + +message ListEmailProvidersRequest { + zitadel.v1.ListQuery query = 1; +} + +message ListEmailProvidersResponse { + zitadel.v1.ListDetails details = 1; + repeated zitadel.settings.v1.EmailProvider result = 2; +} + +message AddEmailProviderSMTPRequest { + string sender_address = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"noreply@m.zitadel.cloud\""; + min_length: 1; + max_length: 200; + } + ]; + string sender_name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + min_length: 1; + max_length: 200; + } + ]; + bool tls = 3; + string host = 4 [ + (validate.rules).string = {min_len: 1, max_len: 500}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"smtp.postmarkapp.com:587\""; + description: "Make sure to include the port."; + min_length: 1; + max_length: 500; + } + ]; + string user = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"197f0117-529e-443d-bf6c-0292dd9a02b7\""; + } + ]; + string password = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"this-is-my-password\""; + } + ]; + string reply_to_address = 7 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"replyto@m.zitadel.cloud\""; + min_length: 0; + max_length: 200; + } + ]; + string description = 8 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"provider description\""; + min_length: 0; + max_length: 200; + } + ]; +} + +message AddEmailProviderSMTPResponse { + zitadel.v1.ObjectDetails details = 1; + string id = 2; +} + +message UpdateEmailProviderSMTPRequest { + string sender_address = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"noreply@m.zitadel.cloud\""; + min_length: 1; + max_length: 200; + } + ]; + string sender_name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + min_length: 1; + max_length: 200; + } + ]; + bool tls = 3; + string host = 4 [ + (validate.rules).string = {min_len: 1, max_len: 500}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"smtp.postmarkapp.com:587\""; + description: "Make sure to include the port."; + min_length: 1; + max_length: 500; + } + ]; + string user = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"197f0117-529e-443d-bf6c-0292dd9a02b7\""; + } + ]; + string reply_to_address = 6 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"replyto@m.zitadel.cloud\""; + min_length: 0; + max_length: 200; + } + ]; + string password = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"this-is-my-password\""; + } + ]; + string description = 8 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"provider description\""; + min_length: 0; + max_length: 200; + } + ]; + string id = 9 [(validate.rules).string = {min_len: 1, max_len: 100}]; +} + +message UpdateEmailProviderSMTPResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message UpdateEmailProviderSMTPPasswordRequest { + string password = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"this-is-my-updated-password\""; + } + ]; + string id = 2 [(validate.rules).string = {min_len: 1, max_len: 100}]; +} + +message UpdateEmailProviderSMTPPasswordResponse { + zitadel.v1.ObjectDetails details = 1; +} + + +message AddEmailProviderHTTPRequest { + string endpoint = 1 [ + (validate.rules).string = {min_len: 1, max_len: 2048}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"http://relay.example.com/provider\""; + min_length: 1; + max_length: 2048; + } + ]; + string description = 2 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"provider description\""; + min_length: 0; + max_length: 200; + } + ]; +} + +message AddEmailProviderHTTPResponse { + zitadel.v1.ObjectDetails details = 1; + string id = 2; +} + +message UpdateEmailProviderHTTPRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 100}]; + string endpoint = 2 [ + (validate.rules).string = {min_len: 1, max_len: 2048}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"http://relay.example.com/provider\""; + min_length: 1; + max_length: 2048; + } + ]; + string description = 3 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"provider description\""; + min_length: 0; + max_length: 200; + } + ]; +} + +message UpdateEmailProviderHTTPResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message ActivateEmailProviderRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message ActivateEmailProviderResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message DeactivateEmailProviderRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message DeactivateEmailProviderResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message RemoveEmailProviderRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 100}]; +} + +message RemoveEmailProviderResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message TestEmailProviderSMTPByIdRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 100}]; + string receiver_address = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200, email: true}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"noreply@m.zitadel.cloud\""; + min_length: 1; + max_length: 200; + } + ]; +} + +// This is an empty response +message TestEmailProviderSMTPByIdResponse {} + +message TestEmailProviderSMTPRequest { + string sender_address = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"noreply@m.zitadel.cloud\""; + min_length: 1; + max_length: 200; + } + ]; + string sender_name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + min_length: 1; + max_length: 200; + } + ]; + bool tls = 3; + string host = 4 [ + (validate.rules).string = {min_len: 1, max_len: 500}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"smtp.postmarkapp.com:587\""; + description: "Make sure to include the port."; + min_length: 1; + max_length: 500; + } + ]; + string user = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"197f0117-529e-443d-bf6c-0292dd9a02b7\""; + } + ]; + string password = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"this-is-my-password\""; + } + ]; + string receiver_address = 7 [ + (validate.rules).string = {min_len: 1, max_len: 200, email: true}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"noreply@m.zitadel.cloud\""; + min_length: 1; + max_length: 200; + } + ]; + string id = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Zitadel SMTP provider id in case you are not sending the password and want to reuse the stored password"; + example: "\"267191369515139464\""; + } + ]; +} + +// This is an empty response +message TestEmailProviderSMTPResponse {} + message ListSMSProvidersRequest { //list limitations and ordering zitadel.v1.ListQuery query = 1; @@ -4506,6 +5184,14 @@ message AddSMSProviderTwilioRequest { max_length: 200; } ]; + string description = 4 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"provider description\""; + min_length: 0; + max_length: 200; + } + ]; } message AddSMSProviderTwilioResponse { @@ -4533,6 +5219,14 @@ message UpdateSMSProviderTwilioRequest { max_length: 200; } ]; + string description = 4 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"provider description\""; + min_length: 0; + max_length: 200; + } + ]; } message UpdateSMSProviderTwilioResponse { @@ -4548,6 +5242,56 @@ message UpdateSMSProviderTwilioTokenResponse { zitadel.v1.ObjectDetails details = 1; } +message AddSMSProviderHTTPRequest { + string endpoint = 1 [ + (validate.rules).string = {min_len: 1, max_len: 2048}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"http://relay.example.com/provider\""; + min_length: 1; + max_length: 2048; + } + ]; + string description = 2 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"provider description\""; + min_length: 0; + max_length: 200; + } + ]; +} + +message AddSMSProviderHTTPResponse { + zitadel.v1.ObjectDetails details = 1; + string id = 2; +} + +message UpdateSMSProviderHTTPRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string endpoint = 2 [ + (validate.rules).string = {min_len: 1, max_len: 2048}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"http://relay.example.com/provider\""; + min_length: 1; + max_length: 2048; + } + ]; + string description = 3 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"provider description\""; + min_length: 0; + max_length: 200; + } + ]; +} + +message UpdateSMSProviderHTTPResponse { + zitadel.v1.ObjectDetails details = 1; +} + message ActivateSMSProviderRequest { string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } @@ -7688,6 +8432,89 @@ message ResetCustomPasswordChangeMessageTextToDefaultResponse { zitadel.v1.ObjectDetails details = 1; } +message GetDefaultInviteUserMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetDefaultInviteUserMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message GetCustomInviteUserMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetCustomInviteUserMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message SetDefaultInviteUserMessageTextRequest { + string language = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"de\""; + min_length: 1; + max_length: 200; + } + ]; + string title = 2 [ + (validate.rules).string = {max_bytes: 2000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Invitation to {{.ApplicationName}}\"" + max_length: 500; + } + ]; + string pre_header = 3 [ + (validate.rules).string = {max_bytes: 2000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Invitation to {{.ApplicationName}}\"" + max_length: 500; + } + ]; + string subject = 4 [ + (validate.rules).string = {max_bytes: 2000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Invitation to {{.ApplicationName}}\"" + max_length: 500; + } + ]; + string greeting = 5 [ + (validate.rules).string = {max_bytes: 4000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Hello {{.DisplayName}},\"" + max_length: 1000; + } + ]; + string text = 6 [ + (validate.rules).string = {max_bytes: 40000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Your user has been invited to {{.ApplicationName}}. Please click the button below to finish the invite process. If you didn't ask for this mail, please ignore it.\"" + max_length: 10000; + } + ]; + string button_text = 7 [ + (validate.rules).string = {max_bytes: 4000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Accept invite\"" + max_length: 1000; + } + ]; + string footer_text = 8 [(validate.rules).string = {max_len: 8000}]; +} + +message SetDefaultInviteUserMessageTextResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message ResetCustomInviteUserMessageTextToDefaultRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message ResetCustomInviteUserMessageTextToDefaultResponse { + zitadel.v1.ObjectDetails details = 1; +} + message GetDefaultPasswordlessRegistrationMessageTextRequest { string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; @@ -8138,6 +8965,7 @@ message DataOrg { repeated zitadel.management.v1.SetCustomVerifySMSOTPMessageTextRequest verify_sms_otp_messages = 37; repeated zitadel.management.v1.SetCustomVerifyEmailOTPMessageTextRequest verify_email_otp_messages = 38; + repeated zitadel.management.v1.SetCustomInviteUserMessageTextRequest invite_user_messages = 39; } message ImportDataResponse{ diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 7b57065a48..0eeaa29b0b 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -3976,7 +3976,7 @@ service ManagementService { tags: "Project Grants" tags: "Members"; tags: "ZITADEL Administrators"; - summary: "List Project Members"; + summary: "List Project Grant Members"; description: "Members are users with permission to administrate ZITADEL on different levels. This request returns all users with memberships on the project grant level, matching the search queries. The search queries will be AND linked." parameters: { headers: { @@ -6496,6 +6496,103 @@ service ManagementService { }; } + rpc GetCustomInviteUserMessageText(GetCustomInviteUserMessageTextRequest) returns (GetCustomInviteUserMessageTextResponse) { + option (google.api.http) = { + get: "/text/message/invite_user/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.read"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Message Texts"; + summary: "Get Custom Invite User Message Text"; + description: "Get the custom text of the password-changed message/email that is configured on the organization. The message is sent when an invite code email is requested." + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + }; + } + + rpc GetDefaultInviteUserMessageText(GetDefaultInviteUserMessageTextRequest) returns (GetDefaultInviteUserMessageTextResponse) { + option (google.api.http) = { + get: "/text/default/message/invite_user/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.read"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Message Texts"; + summary: "Get Default Invite User Message Text"; + description: "Get the default text of the invite user message/email that is configured on the instance or as translation files in ZITADEL itself. The message is sent when an invite code email is requested." + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + }; + } + + rpc SetCustomInviteUserMessageCustomText(SetCustomInviteUserMessageTextRequest) returns (SetCustomInviteUserMessageTextResponse) { + option (google.api.http) = { + put: "/text/message/invite_user/{language}"; + body: "*"; + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.write"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Message Texts"; + summary: "Set Custom Invite User Message Text"; + description: "Set the custom text of the invite user message/email for the organization. The message is sent when an invite code email is requested. The Following Variables can be used: {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}} {{.ApplicationName}}" + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + }; + } + + rpc ResetCustomInviteUserMessageTextToDefault(ResetCustomInviteUserMessageTextToDefaultRequest) returns (ResetCustomInviteUserMessageTextToDefaultResponse) { + option (google.api.http) = { + delete: "/text/message/invite_user/{language}" + }; + + option (zitadel.v1.auth_option) = { + permission: "policy.delete" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Message Texts"; + summary: "Reset Custom Invite User Message Text to Default"; + description: "Removes the custom text of the invite user message from the organization and therefore the default texts from the instance or translation files will be triggered for the users." + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + }; + } + rpc GetCustomLoginTexts(GetCustomLoginTextsRequest) returns (GetCustomLoginTextsResponse) { option (google.api.http) = { get: "/text/login/{language}"; @@ -11919,6 +12016,86 @@ message ResetCustomPasswordChangeMessageTextToDefaultResponse { zitadel.v1.ObjectDetails details = 1; } +message GetCustomInviteUserMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetCustomInviteUserMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message GetDefaultInviteUserMessageTextRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message GetDefaultInviteUserMessageTextResponse { + zitadel.text.v1.MessageCustomText custom_text = 1; +} + +message SetCustomInviteUserMessageTextRequest { + string language = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"de\"" + } + ]; + string title = 2 [ + (validate.rules).string = {max_bytes: 2000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Invitation to {{.ApplicationName}}\"" + max_length: 500; + } + ]; + string pre_header = 3 [ + (validate.rules).string = {max_bytes: 2000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Invitation to {{.ApplicationName}}\"" + max_length: 500; + } + ]; + string subject = 4 [ + (validate.rules).string = {max_bytes: 2000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Invitation to {{.ApplicationName}}\"" + max_length: 500; + } + ]; + string greeting = 5 [ + (validate.rules).string = {max_bytes: 4000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Hello {{.DisplayName}},\"" + max_length: 1000; + } + ]; + string text = 6 [ + (validate.rules).string = {max_bytes: 40000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Your user has been invited to {{.ApplicationName}}. Please click the button below to finish the invite process. If you didn't ask for this mail, please ignore it.\"" + max_length: 10000; + } + ]; + string button_text = 7 [ + (validate.rules).string = {max_bytes: 4000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Accept invite\"" + max_length: 500; + } + ]; + string footer_text = 8 [(validate.rules).string = {max_bytes: 8000}]; +} + +message SetCustomInviteUserMessageTextResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message ResetCustomInviteUserMessageTextToDefaultRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message ResetCustomInviteUserMessageTextToDefaultResponse { + zitadel.v1.ObjectDetails details = 1; +} + message GetOrgIDPByIDRequest { string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } diff --git a/proto/zitadel/resources/debug_events/v3alpha/debug_events_service.proto b/proto/zitadel/resources/debug_events/v3alpha/debug_events_service.proto new file mode 100644 index 0000000000..6a5990f783 --- /dev/null +++ b/proto/zitadel/resources/debug_events/v3alpha/debug_events_service.proto @@ -0,0 +1,223 @@ +syntax = "proto3"; + +package zitadel.resources.debug_events.v3alpha; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/object/v3alpha/object.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; +import "zitadel/resources/debug_events/v3alpha/event.proto"; +import "zitadel/resources/debug_events/v3alpha/state.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/debug_events/v3alpha;debug_events"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Debug Service"; + version: "3.0-preview"; + description: "This API is intended to push specific debug payload through ZITADEL's storage system."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + produces: "application/json"; + + consumes: "application/grpc"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/resources/v3alpha/debug"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +service ZITADELDebugEvents { + rpc CreateDebugEvents(CreateDebugEventsRequest) returns (CreateDebugEventsResponse) { + option (google.api.http) = { + post: "/" + body: "events" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.debug.write" + } + http_response: { + success_code: 201 + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Create a set of debug events."; + description: "Create a set of debug events which will be pushed to the eventstore and reduced to the projection." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + rpc GetDebugEventsStateById(GetDebugEventsStateByIdRequest) returns (GetDebugEventsStateByIdResponse) { + option (google.api.http) = { + get: "/v3alpha/debug_events/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.debug.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Debug events state successfully retrieved"; + } + }; + }; + } + + rpc ListDebugEventsStates(ListDebugEventsStatesRequest) returns (ListDebugEventsStatesResponse) { + option (google.api.http) = { + get: "/v3alpha/debug_events" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.debug.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Debug events states successfully retrieved"; + } + }; + }; + } +} + +message CreateDebugEventsRequest { + optional zitadel.object.v3alpha.Instance instance = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"domain from HOST or :authority header\"" + } + ]; + // unique identifier for the aggregate we want to push events to. + string aggregate_id = 2 [ + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + repeated Event events = 3; +} + +message CreateDebugEventsResponse { + zitadel.resources.object.v3alpha.Details details = 1; +} + +message GetDebugEventsStateByIdRequest { + // unique identifier of the aggregate. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + bool trigger_bulk = 2; +} + +message GetDebugEventsStateByIdResponse { + State state = 1; +} + +message ListDebugEventsStatesRequest { + bool trigger_bulk = 1; +} + +message ListDebugEventsStatesResponse { + repeated State states = 1; +} \ No newline at end of file diff --git a/proto/zitadel/resources/debug_events/v3alpha/event.proto b/proto/zitadel/resources/debug_events/v3alpha/event.proto new file mode 100644 index 0000000000..096c9918bb --- /dev/null +++ b/proto/zitadel/resources/debug_events/v3alpha/event.proto @@ -0,0 +1,47 @@ +syntax = "proto3"; + +package zitadel.resources.debug_events.v3alpha; + +import "google/protobuf/duration.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/debug_events/v3alpha;debug_events"; + +message Event { + oneof event { + AddedEvent add = 1; + ChangedEvent change = 2; + RemovedEvent remove = 3; + } +} + +message AddedEvent { + // issues a pg_sleep command in the projection reducer, simulating a slow query. + google.protobuf.Duration projection_sleep = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"5s\""; + } + ]; + // optional text that can be set as a state. + optional string blob = 2; +} + +message ChangedEvent { + // issues a pg_sleep command in the projection reducer, simulating a slow query. + google.protobuf.Duration projection_sleep = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"5s\""; + } + ]; + // optional text that can be set as a state. + optional string blob = 2; +} + +message RemovedEvent { + // issues a pg_sleep command in the projection reducer, simulating a slow query. + google.protobuf.Duration projection_sleep = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"5s\""; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/resources/debug_events/v3alpha/state.proto b/proto/zitadel/resources/debug_events/v3alpha/state.proto new file mode 100644 index 0000000000..5cdf60d740 --- /dev/null +++ b/proto/zitadel/resources/debug_events/v3alpha/state.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package zitadel.resources.debug_events.v3alpha; + +import "zitadel/resources/object/v3alpha/object.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/debug_events/v3alpha;debug_events"; + +message State { + // Details provide some base information (such as the last change date) of the schema. + zitadel.resources.object.v3alpha.Details details = 1; + + string blob = 2; +} \ No newline at end of file diff --git a/proto/zitadel/settings.proto b/proto/zitadel/settings.proto index 18b343df08..a2a6806c65 100644 --- a/proto/zitadel/settings.proto +++ b/proto/zitadel/settings.proto @@ -7,7 +7,7 @@ import "protoc-gen-openapiv2/options/annotations.proto"; package zitadel.settings.v1; -option go_package ="github.com/zitadel/zitadel/pkg/grpc/settings"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings"; message SecretGenerator { SecretGeneratorType generator_type = 1; @@ -95,13 +95,66 @@ message SMTPConfig { string id = 10; } +message EmailProvider { + zitadel.v1.ObjectDetails details = 1; + string id = 2; + EmailProviderState state = 3; + string description = 6; + + oneof config { + EmailProviderSMTP smtp = 4; + EmailProviderHTTP http = 5; + } +} + +enum EmailProviderState { + EMAIL_PROVIDER_STATE_UNSPECIFIED = 0; + EMAIL_PROVIDER_ACTIVE = 1; + EMAIL_PROVIDER_INACTIVE = 2; +} + +message EmailProviderSMTP { + string sender_address = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"noreply@m.zitadel.cloud\""; + } + ]; + string sender_name = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + bool tls = 3; + string host = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"smtp.postmarkapp.com:587\""; + } + ]; + string user = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"197f0117-529e-443d-bf6c-0292dd9a02b7\""; + } + ]; + string reply_to_address = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"replyto@m.zitadel.cloud\""; + } + ]; +} + +message EmailProviderHTTP { + string endpoint = 1; +} + message SMSProvider { zitadel.v1.ObjectDetails details = 1; string id = 2; SMSProviderConfigState state = 3; + string description = 6; oneof config { TwilioConfig twilio = 4; + HTTPConfig http = 5; } } @@ -110,6 +163,10 @@ message TwilioConfig { string sender_number = 2; } +message HTTPConfig { + string endpoint = 1; +} + enum SMSProviderConfigState { SMS_PROVIDER_CONFIG_STATE_UNSPECIFIED = 0; SMS_PROVIDER_CONFIG_ACTIVE = 1; @@ -117,8 +174,8 @@ enum SMSProviderConfigState { } message DebugNotificationProvider { - zitadel.v1.ObjectDetails details = 1; - bool compact = 2; + zitadel.v1.ObjectDetails details = 1; + bool compact = 2; } message OIDCSettings { diff --git a/proto/zitadel/settings/v2/login_settings.proto b/proto/zitadel/settings/v2/login_settings.proto index d7d41a8a90..ca004288fe 100644 --- a/proto/zitadel/settings/v2/login_settings.proto +++ b/proto/zitadel/settings/v2/login_settings.proto @@ -148,5 +148,6 @@ enum IdentityProviderType { IDENTITY_PROVIDER_TYPE_GITLAB = 8; IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED = 9; IDENTITY_PROVIDER_TYPE_GOOGLE = 10; - IDENTITY_PROVIDER_TYPE_SAML=11; + IDENTITY_PROVIDER_TYPE_SAML = 11; + IDENTITY_PROVIDER_TYPE_APPLE = 12; } diff --git a/proto/zitadel/user/v2/email.proto b/proto/zitadel/user/v2/email.proto index 2af5369fe6..cbc07b8762 100644 --- a/proto/zitadel/user/v2/email.proto +++ b/proto/zitadel/user/v2/email.proto @@ -23,7 +23,7 @@ message SetHumanEmail { oneof verification { SendEmailVerificationCode send_code = 2; ReturnEmailVerificationCode return_code = 3; - bool is_verified = 4 [(validate.rules).bool.const = true]; + bool is_verified = 4; } } diff --git a/proto/zitadel/user/v2/user.proto b/proto/zitadel/user/v2/user.proto index 9a0794197e..e1e98eb036 100644 --- a/proto/zitadel/user/v2/user.proto +++ b/proto/zitadel/user/v2/user.proto @@ -282,3 +282,29 @@ enum AuthFactorState { AUTH_FACTOR_STATE_READY = 2; AUTH_FACTOR_STATE_REMOVED = 3; } + +message SendInviteCode { + // Optionally set a url_template, which will be used in the invite mail sent by ZITADEL to guide the user to your invitation page. + // If no template is set, the default ZITADEL url will be used. + optional string url_template = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"https://example.com/user/invite?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}\""; + } + ]; + // Optionally set an application name, which will be used in the invite mail sent by ZITADEL. + // If no application name is set, ZITADEL will be used as default. + optional string application_name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"CustomerPortal\""; + } + ]; +} + +message ReturnInviteCode {} + diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 214c600bda..9b82bfe297 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -1083,6 +1083,79 @@ service UserService { }; }; } + + // Create an invite code for a user + // + // Create an invite code for a user to initialize their first authentication method (password, passkeys, IdP) depending on the organization's available methods. + rpc CreateInviteCode (CreateInviteCodeRequest) returns (CreateInviteCodeResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/invite_code" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Resend an invite code for a user + // + // Resend an invite code for a user to initialize their first authentication method (password, passkeys, IdP) depending on the organization's available methods. + // A resend is only possible if a code has been created previously and sent to the user. If there is no code or it was directly returned, an error will be returned. + rpc ResendInviteCode (ResendInviteCodeRequest) returns (ResendInviteCodeResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/invite_code/resend" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Verify an invite code for a user + // + // Verify the invite code of a user previously issued. This will set their email to a verified state and + // allow the user to set up their first authentication method (password, passkeys, IdP) depending on the organization's available methods. + rpc VerifyInviteCode (VerifyInviteCodeRequest) returns (VerifyInviteCodeResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/invite_code/verify" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } } message AddHumanUserRequest{ @@ -2076,3 +2149,67 @@ enum AuthenticationMethodType { AUTHENTICATION_METHOD_TYPE_OTP_SMS = 6; AUTHENTICATION_METHOD_TYPE_OTP_EMAIL = 7; } + +message CreateInviteCodeRequest { + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // if no verification is specified, an email is sent with the default url and application name (ZITADEL) + oneof verification { + SendInviteCode send_code = 2; + ReturnInviteCode return_code = 3; + } +} + +message CreateInviteCodeResponse { + zitadel.object.v2.Details details = 1; + // The invite code is returned if the verification was set to return_code. + optional string invite_code = 2; +} +message ResendInviteCodeRequest { + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; +} + +message ResendInviteCodeResponse { + zitadel.object.v2.Details details = 1; +} + +message VerifyInviteCodeRequest { + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + string verification_code = 2 [ + (validate.rules).string = {min_len: 1, max_len: 20}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 20; + example: "\"SKJd342k\""; + description: "\"the verification code generated during the invite code request\""; + } + ]; +} + +message VerifyInviteCodeResponse { + zitadel.object.v2.Details details = 1; +} \ No newline at end of file diff --git a/proto/zitadel/v1.proto b/proto/zitadel/v1.proto index 6b64f7db68..c186ea7d61 100644 --- a/proto/zitadel/v1.proto +++ b/proto/zitadel/v1.proto @@ -90,6 +90,8 @@ message DataOrg { repeated DataAppKey app_keys = 38; repeated DataMachineKey machine_keys = 39; + + repeated zitadel.management.v1.SetCustomInviteUserMessageTextRequest invite_user_messages = 40; } message DataOIDCIDP{