diff --git a/.gitignore b/.gitignore index b2f4277b2c..8b8a107f07 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ sandbox.go .idea .vscode .DS_STORE +.run # credential google-credentials diff --git a/Makefile b/Makefile index 2a36edfe9f..e3936b8379 100644 --- a/Makefile +++ b/Makefile @@ -103,7 +103,7 @@ core_unit_test: 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 --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml + ./zitadel setup --masterkeyFromEnv --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 .PHONY: core_integration_test diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 5daeec650e..56424b9e01 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -838,6 +838,11 @@ DefaultInstance: # DisallowPublicOrgRegistration defines if ZITADEL should expose the endpoint /ui/login/register/org # If it is true, the endpoint returns the HTTP status 404 on GET requests, and 409 on POST requests. DisallowPublicOrgRegistration: # ZITADEL_DEFAULTINSTANCE_RESTRICTIONS_DISALLOWPUBLICORGREGISTRATION + # AllowedLanguages restricts the languages that can be used. + # If the list is empty, all supported languages are allowed. + AllowedLanguages: # ZITADEL_DEFAULTINSTANCE_RESTRICTIONS_ALLOWEDLANGUAGES + # - en + # - de Quotas: # Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist. # The following unit types are supported diff --git a/cmd/initialise/sql/cockroach/01_user.sql b/cmd/initialise/sql/cockroach/01_user.sql index e9473e46c2..4e621216ce 100644 --- a/cmd/initialise/sql/cockroach/01_user.sql +++ b/cmd/initialise/sql/cockroach/01_user.sql @@ -1,2 +1,2 @@ -- replace %[1]s with the name of the user -CREATE USER IF NOT EXISTS %[1]s \ No newline at end of file +CREATE USER IF NOT EXISTS "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/cockroach/02_database.sql b/cmd/initialise/sql/cockroach/02_database.sql index 8d0e37e565..a0e3c3350f 100644 --- a/cmd/initialise/sql/cockroach/02_database.sql +++ b/cmd/initialise/sql/cockroach/02_database.sql @@ -1,2 +1,2 @@ -- replace %[1]s with the name of the database -CREATE DATABASE IF NOT EXISTS %[1]s \ No newline at end of file +CREATE DATABASE IF NOT EXISTS "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/cockroach/03_grant_user.sql b/cmd/initialise/sql/cockroach/03_grant_user.sql index 161b2f5ba4..de0d2743eb 100644 --- a/cmd/initialise/sql/cockroach/03_grant_user.sql +++ b/cmd/initialise/sql/cockroach/03_grant_user.sql @@ -1,4 +1,4 @@ -- replace the first %[1]s with the database -- replace the second \%[2]s with the user -GRANT ALL ON DATABASE %[1]s TO %[2]s; -GRANT SYSTEM VIEWACTIVITY TO %[2]s; \ No newline at end of file +GRANT ALL ON DATABASE "%[1]s" TO "%[2]s"; +GRANT SYSTEM VIEWACTIVITY TO "%[2]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/cockroach/04_eventstore.sql b/cmd/initialise/sql/cockroach/04_eventstore.sql index fca432b3a9..3cb4fc0d3e 100644 --- a/cmd/initialise/sql/cockroach/04_eventstore.sql +++ b/cmd/initialise/sql/cockroach/04_eventstore.sql @@ -1,3 +1,3 @@ CREATE SCHEMA IF NOT EXISTS eventstore; -GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO %[1]s; \ No newline at end of file +GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/cockroach/05_projections.sql b/cmd/initialise/sql/cockroach/05_projections.sql index eafbda805f..91ca6662ee 100644 --- a/cmd/initialise/sql/cockroach/05_projections.sql +++ b/cmd/initialise/sql/cockroach/05_projections.sql @@ -1,3 +1,3 @@ CREATE SCHEMA IF NOT EXISTS projections; -GRANT ALL ON ALL TABLES IN SCHEMA projections TO %[1]s; \ No newline at end of file +GRANT ALL ON ALL TABLES IN SCHEMA projections TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/cockroach/06_system.sql b/cmd/initialise/sql/cockroach/06_system.sql index f66c9dbe96..6c9138918b 100644 --- a/cmd/initialise/sql/cockroach/06_system.sql +++ b/cmd/initialise/sql/cockroach/06_system.sql @@ -1,3 +1,3 @@ CREATE SCHEMA IF NOT EXISTS system; -GRANT ALL ON ALL TABLES IN SCHEMA system TO %[1]s; \ No newline at end of file +GRANT ALL ON ALL TABLES IN SCHEMA system TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/01_user.sql b/cmd/initialise/sql/postgres/01_user.sql index 1afbce7ac7..cd60b9a2cf 100644 --- a/cmd/initialise/sql/postgres/01_user.sql +++ b/cmd/initialise/sql/postgres/01_user.sql @@ -1 +1 @@ -CREATE USER %[1]s \ No newline at end of file +CREATE USER "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/02_database.sql b/cmd/initialise/sql/postgres/02_database.sql index 809d3b9099..895a1f29d5 100644 --- a/cmd/initialise/sql/postgres/02_database.sql +++ b/cmd/initialise/sql/postgres/02_database.sql @@ -1 +1 @@ -CREATE DATABASE %[1]s \ No newline at end of file +CREATE DATABASE "%[1]s" \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/03_grant_user.sql b/cmd/initialise/sql/postgres/03_grant_user.sql index f96cb22b6a..13ce4ac4bb 100644 --- a/cmd/initialise/sql/postgres/03_grant_user.sql +++ b/cmd/initialise/sql/postgres/03_grant_user.sql @@ -1,3 +1,3 @@ -- replace the first %[1]s with the database -- replace the second \%[2]s with the user -GRANT ALL ON DATABASE %[1]s TO %[2]s; \ No newline at end of file +GRANT ALL ON DATABASE "%[1]s" TO "%[2]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/04_eventstore.sql b/cmd/initialise/sql/postgres/04_eventstore.sql index fca432b3a9..3cb4fc0d3e 100644 --- a/cmd/initialise/sql/postgres/04_eventstore.sql +++ b/cmd/initialise/sql/postgres/04_eventstore.sql @@ -1,3 +1,3 @@ CREATE SCHEMA IF NOT EXISTS eventstore; -GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO %[1]s; \ No newline at end of file +GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/05_projections.sql b/cmd/initialise/sql/postgres/05_projections.sql index eafbda805f..91ca6662ee 100644 --- a/cmd/initialise/sql/postgres/05_projections.sql +++ b/cmd/initialise/sql/postgres/05_projections.sql @@ -1,3 +1,3 @@ CREATE SCHEMA IF NOT EXISTS projections; -GRANT ALL ON ALL TABLES IN SCHEMA projections TO %[1]s; \ No newline at end of file +GRANT ALL ON ALL TABLES IN SCHEMA projections TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/sql/postgres/06_system.sql b/cmd/initialise/sql/postgres/06_system.sql index f66c9dbe96..6c9138918b 100644 --- a/cmd/initialise/sql/postgres/06_system.sql +++ b/cmd/initialise/sql/postgres/06_system.sql @@ -1,3 +1,3 @@ CREATE SCHEMA IF NOT EXISTS system; -GRANT ALL ON ALL TABLES IN SCHEMA system TO %[1]s; \ No newline at end of file +GRANT ALL ON ALL TABLES IN SCHEMA system TO "%[1]s"; \ No newline at end of file diff --git a/cmd/initialise/verify_database_test.go b/cmd/initialise/verify_database_test.go index 627a9192c8..ebdf0473b6 100644 --- a/cmd/initialise/verify_database_test.go +++ b/cmd/initialise/verify_database_test.go @@ -26,7 +26,7 @@ func Test_verifyDB(t *testing.T) { name: "doesn't exists, create fails", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS zitadel", sql.ErrTxDone), + expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", sql.ErrTxDone), ), database: "zitadel", }, @@ -36,7 +36,7 @@ func Test_verifyDB(t *testing.T) { name: "doesn't exists, create successful", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS zitadel", nil), + expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", nil), ), database: "zitadel", }, @@ -46,7 +46,7 @@ func Test_verifyDB(t *testing.T) { name: "already exists", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS zitadel", nil), + expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE IF NOT EXISTS \"zitadel\"", nil), ), database: "zitadel", }, diff --git a/cmd/initialise/verify_grant_test.go b/cmd/initialise/verify_grant_test.go index 93196037d7..a6bfa818ad 100644 --- a/cmd/initialise/verify_grant_test.go +++ b/cmd/initialise/verify_grant_test.go @@ -21,7 +21,7 @@ func Test_verifyGrant(t *testing.T) { name: "doesn't exists, create fails", args: args{ db: prepareDB(t, - expectExec("GRANT ALL ON DATABASE zitadel TO zitadel-user", sql.ErrTxDone), + expectExec("GRANT ALL ON DATABASE \"zitadel\" TO \"zitadel-user\"", sql.ErrTxDone), ), database: "zitadel", username: "zitadel-user", @@ -32,7 +32,7 @@ func Test_verifyGrant(t *testing.T) { name: "correct", args: args{ db: prepareDB(t, - expectExec("GRANT ALL ON DATABASE zitadel TO zitadel-user", nil), + expectExec("GRANT ALL ON DATABASE \"zitadel\" TO \"zitadel-user\"", nil), ), database: "zitadel", username: "zitadel-user", @@ -43,7 +43,7 @@ func Test_verifyGrant(t *testing.T) { name: "already exists", args: args{ db: prepareDB(t, - expectExec("GRANT ALL ON DATABASE zitadel TO zitadel-user", nil), + expectExec("GRANT ALL ON DATABASE \"zitadel\" TO \"zitadel-user\"", nil), ), database: "zitadel", username: "zitadel-user", diff --git a/cmd/initialise/verify_user_test.go b/cmd/initialise/verify_user_test.go index 0104319acc..da7afc1765 100644 --- a/cmd/initialise/verify_user_test.go +++ b/cmd/initialise/verify_user_test.go @@ -27,7 +27,7 @@ func Test_verifyUser(t *testing.T) { name: "doesn't exists, create fails", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS zitadel-user", sql.ErrTxDone), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\"", sql.ErrTxDone), ), username: "zitadel-user", password: "", @@ -38,7 +38,7 @@ func Test_verifyUser(t *testing.T) { name: "correct without password", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS zitadel-user", nil), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\"", nil), ), username: "zitadel-user", password: "", @@ -49,7 +49,7 @@ func Test_verifyUser(t *testing.T) { name: "correct with password", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS zitadel-user WITH PASSWORD 'password'", nil), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\" WITH PASSWORD 'password'", nil), ), username: "zitadel-user", password: "password", @@ -60,7 +60,7 @@ func Test_verifyUser(t *testing.T) { name: "already exists", args: args{ db: prepareDB(t, - expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS zitadel-user WITH PASSWORD 'password'", nil), + expectExec("-- replace zitadel-user with the name of the user\nCREATE USER IF NOT EXISTS \"zitadel-user\" WITH PASSWORD 'password'", nil), ), username: "zitadel-user", password: "", diff --git a/cmd/setup/07/logstore.sql b/cmd/setup/07/logstore.sql index b2df7f686f..934ffa2036 100644 --- a/cmd/setup/07/logstore.sql +++ b/cmd/setup/07/logstore.sql @@ -1,3 +1,3 @@ CREATE SCHEMA IF NOT EXISTS logstore; -GRANT ALL ON ALL TABLES IN SCHEMA logstore TO %[1]s; +GRANT ALL ON ALL TABLES IN SCHEMA logstore TO "%[1]s"; diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index a31c5e9ae2..c1d354d825 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -16,6 +16,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql" new_es "github.com/zitadel/zitadel/internal/eventstore/v3" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/migration" "github.com/zitadel/zitadel/internal/query/projection" ) @@ -64,6 +65,8 @@ func Setup(config *Config, steps *Steps, masterKey string) { ctx := context.Background() logging.Info("setup started") + i18n.MustLoadSupportedLanguagesFromDir() + zitadelDBClient, err := database.Connect(config.Database, false, false) logging.OnError(err).Fatal("unable to connect to database") esPusherDBClient, err := database.Connect(config.Database, false, true) diff --git a/cmd/start/start.go b/cmd/start/start.go index 519772ef7c..f4be4d33ad 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -62,6 +62,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql" 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/logstore" "github.com/zitadel/zitadel/internal/logstore/emitters/access" @@ -93,7 +94,6 @@ Requirements: if err != nil { return err } - return startZitadel(config, masterKey, server) }, } @@ -123,6 +123,8 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error ctx := context.Background() + i18n.MustLoadSupportedLanguagesFromDir() + zitadelDBClient, err := database.Connect(config.Database, false, false) if err != nil { return fmt.Errorf("cannot start client for projection: %w", err) @@ -215,6 +217,7 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error if err != nil { return fmt.Errorf("cannot start commands: %w", err) } + defer commands.Close(ctx) // wait for background jobs clock := clockpkg.New() actionsExecutionStdoutEmitter, err := logstore.NewEmitter[*record.ExecutionLog](ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Execution.Stdout.Enabled}, stdout.NewStdoutEmitter[*record.ExecutionLog]()) diff --git a/console/src/app/pages/actions/action-table/action-table.component.ts b/console/src/app/pages/actions/action-table/action-table.component.ts index 7e405de33b..3b617ce0a1 100644 --- a/console/src/app/pages/actions/action-table/action-table.component.ts +++ b/console/src/app/pages/actions/action-table/action-table.component.ts @@ -83,8 +83,8 @@ export class ActionTableComponent implements OnInit { this.mgmtService .deleteAction(action.id) .then(() => { + this.selection.clear(); this.toast.showInfo('FLOWS.DIALOG.DELETEACTION.DELETE_SUCCESS', true); - this.refreshPage(); }) .catch((error: any) => { diff --git a/console/src/app/pages/users/user-list/user-table/user-table.component.ts b/console/src/app/pages/users/user-list/user-table/user-table.component.ts index ace9a5bd4e..018c8043d7 100644 --- a/console/src/app/pages/users/user-list/user-table/user-table.component.ts +++ b/console/src/app/pages/users/user-list/user-table/user-table.component.ts @@ -309,6 +309,7 @@ export class UserTableComponent implements OnInit { setTimeout(() => { this.refreshPage(); }, 1000); + this.selection.clear(); this.toast.showInfo('USER.TOAST.DELETED', true); }) .catch((error) => { diff --git a/docs/docs/examples/secure-api/go.md b/docs/docs/examples/secure-api/go.md index 60609a0e38..7ec1cc2e4e 100644 --- a/docs/docs/examples/secure-api/go.md +++ b/docs/docs/examples/secure-api/go.md @@ -8,9 +8,26 @@ OAuth 2 Token Introspection. At the end of the guide you should have an API with a protected endpoint. +> This documentation references our HTTP example. There's also one for GRPC. Check them out on [GitHub](https://github.com/zitadel/zitadel-go/tree/authorization/example/api). + +## Set up application and obtain keys + +Before we begin developing our API, we need to perform a few configuration steps in the ZITADEL Console. +You'll need to provide some information about your app. We recommend creating a new app to start from scratch. Navigate to your Project, then add a new application at the top of the page. +Select the **API** application type and continue. + +![Create app in console](/img/go/api-create.png) + +We recommend that you use JWT Profile for authenticating at the Introspection Endpoint. + +![Create app in console](/img/go/api-create-auth.png) + +Then create a new key with your desired expiration date. Be sure to download it, as you won't be able to retrieve it again. + +![Create api key in console](/img/go/api-create-key.png) + ## Prerequisites -The client [SDK](https://github.com/zitadel/zitadel-go) will provides an interceptor for both GRPC and HTTP. This will handle the OAuth 2.0 introspection request including authentication using JWT with Private Key using our [OIDC client library](https://github.com/zitadel/oidc). All that is required, is to create your API and download the private key file later called `Key JSON` for the service user. @@ -18,134 +35,170 @@ All that is required, is to create your API and download the private key file la ### Add Go SDK to your project -You need to add the SDK into Go Modules by: +You need to add the [SDK](https://github.com/zitadel/zitadel-go) into Go Modules by: ```bash -go get github.com/zitadel/zitadel-go/v2 +go get -u github.com/zitadel/zitadel-go/v3 ``` ### Create example API -Create a new go file with the content below. This will create an API with two endpoints. On path `/public` it will always write -back `ok` and the current timestamp. On `/protected` it will respond the same but only if a valid access_token is sent. The token -must not be expired and the API has to be part of the audience (either client_id or project_id). +Create a new go file with the content below. This will create an API with three endpoints: +- `/api/healthz`: can be called by anyone and always returns `OK` +- `/api/tasks`: requires authorization and returns the available tasks +- `/api/add-task`: requires authorization with granted `admin` role and adds the task to the list -Make sure to fill the var `issuer` with your own domain. This is the domain of your instance you can find it on the instance detail in the ZITADEL Cloud Customer Portal or in the ZITADEL Console. -```go -package main +If authorization is required, the token must not be expired and the API has to be part of the audience (either client_id or project_id). -import ( - "flag" - "log" - "net/http" - "time" - - http_mw "github.com/zitadel/zitadel-go/v2/pkg/api/middleware/http" - "github.com/zitadel/zitadel-go/v2/pkg/client/middleware" -) - -var ( - issuer = flag.String("issuer", "", "issuer of your ZITADEL instance (in the form: https://.zitadel.cloud or https://)") -) - -func main() { - flag.Parse() - - introspection, err := http_mw.NewIntrospectionInterceptor(*issuer, middleware.OSKeyPath()) - if err != nil { - log.Fatal(err) - } - - router := http.NewServeMux() - router.HandleFunc("/public", writeOK) - router.HandleFunc("/protected", introspection.HandlerFunc(writeOK)) - - lis := "127.0.0.1:5001" - log.Fatal(http.ListenAndServe(lis, router)) -} - -func writeOK(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("OK " + time.Now().String())) -} +For tests we will use a Personal Access Token. +```go reference +https://github.com/zitadel/zitadel-go/blob/next/example/api/http/main.go ``` -#### Key JSON +You will need to provide some values for the program to run: +- `domain`: Your ZITADEL instance domain, e.g. https://my-domain.zitadel.cloud +- `key`: The path to the downloaded key.json +- `port`: The port on which the API will be accessible, default it 8089 -To provide the key JSON to the SDK, simply set an environment variable `ZITADEL_KEY_PATH` with the path to the JSON as value. - -```bash -export ZITADEL_KEY_PATH=/Users/test/apikey.json -``` - -For development purposes you should be able to set this in your IDE. - -If you're not able to set it via environment variable, you can also exchange the `middleware.OSKeyPath()` and pass it directly: - -```go -introspection, err := http_mw.NewIntrospectionInterceptor( - client.Issuer, - "/Users/test/apikey.json", -) -``` - -### Test API +## Test API After you have configured everything correctly, you can simply start the example by: ```bash -go run main.go +go run main.go --domain --key ``` -You can now call the API by browser or curl. Try the public endpoint first: +This could look like: ```bash -curl -i localhost:5001/public +go run main.go --domain https://my-domain.zitadel.cloud --key ./api.json +``` + +After you get a successful log: +``` +2023/12/04 10:27:42 INFO server listening, press ctrl+c to stop addr=http://localhost:8089 +``` + +### Public endpoint + +Now you can call the API by browser or curl. Try the healthz endpoint first: + +```bash +curl -i http://localhost:8089/api/healthz ``` it should return something like: ``` HTTP/1.1 200 OK -Date: Tue, 24 Aug 2021 11:11:17 GMT -Content-Length: 59 -Content-Type: text/plain; charset=utf-8 +Content-Type: application/json +Date: Mon, 04 Dec 2023 09:29:38 GMT +Content-Length: 4 -OK 2021-08-24 13:11:17.135719 +0200 CEST m=+30704.913892168 +"OK" ``` -and the protected: +### Task list + +and the task list endpoint: ```bash -curl -i localhost:5001/protected +curl -i http://localhost:8089/api/tasks ``` it will return: ``` HTTP/1.1 401 Unauthorized -Content-Type: application/json -Date: Tue, 24 Aug 2021 11:13:10 GMT -Content-Length: 21 +Content-Type: text/plain; charset=utf-8 +X-Content-Type-Options: nosniff +Date: Mon, 04 Dec 2023 09:41:54 GMT +Content-Length: 44 -"auth header missing" +unauthorized: authorization header is empty ``` -Get a valid access_token for the API. You can achieve this by login into an application of the same project or -by explicitly requesting the project_id for the audience by scope `urn:zitadel:iam:org:project:id:{projectid}:aud`. +Get a valid access_token for the API. You can either achieve this by getting an access token with the project_id in the audience +or use a PAT of a service account. If you provide a valid Bearer Token: ```bash -curl -i -H "Authorization: Bearer ${token}" localhost:5001/protected +curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/tasks ``` -it will return an OK response as well: +it will return an empty list: ``` HTTP/1.1 200 OK -Date: Tue, 24 Aug 2021 11:13:33 GMT -Content-Length: 59 -Content-Type: text/plain; charset=utf-8 +Content-Type: application/json +Date: Mon, 04 Dec 2023 09:49:06 GMT +Content-Length: 2 -OK 2021-08-24 13:13:33.131943 +0200 CEST m=+30840.911149251 +{} +``` + +### Try to add a new task + +Let's see what happens if you call the AddTask endpoint: + +```bash +curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/add-task +``` + +it will complain about the missing `admin` role: +``` +HTTP/1.1 403 Forbidden +Content-Type: text/plain; charset=utf-8 +X-Content-Type-Options: nosniff +Date: Mon, 04 Dec 2023 09:52:00 GMT +Content-Length: 50 + +permission denied: missing required role: `admin` +``` + +### Add admin role + +So let's create the role and grant it to the user. To do so, go to your project in ZITADEL Console +and create the role by selecting `Roles` in the navigation and then clicking on the `New Role` button. +Finally, create the role as shown below: + +![Create project role in console](/img/go/api-project-role.png) + +After you have created the role, let's grant it the user, who requested the tasks. +Click on `Authorization` in the navigation and create a new one by selecting the user and the `admin` role. +After successful creation, it should look like: + +![Created authorization in console](/img/go/api-project-auth.png) + +So you should now be able to add a new task: + +```bash +curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/add-task --data "task=My new task" +``` + +which will report back the successful addition: +``` +HTTP/1.1 200 OK +Content-Type: application/json +Date: Mon, 04 Dec 2023 10:06:29 GMT +Content-Length: 26 + +"task `My new task` added" +``` + +Let's now retrieve the task list again: + +```bash +curl -i -H "Authorization: Bearer ${token}" http://localhost:8089/api/tasks +``` + +As you can see your new task ist listed. And since you're an `admin` now, you will always get an additional `create a new task on /api/add-task`: +``` +HTTP/1.1 200 OK +Content-Type: application/json +Date: Mon, 04 Dec 2023 10:08:38 GMT +Content-Length: 62 + +{"tasks":["My new task","create a new task on /api/add-task"]} ``` diff --git a/docs/docs/guides/manage/customize/restrictions.md b/docs/docs/guides/manage/customize/restrictions.md index 443dd0a948..4ad29cef7a 100644 --- a/docs/docs/guides/manage/customize/restrictions.md +++ b/docs/docs/guides/manage/customize/restrictions.md @@ -8,7 +8,11 @@ Users with the role IAM_OWNER can change the restrictions of their instance usin Currently, the following restrictions are available: - *Disallow public organization registrations* - If restricted, only users with the role IAM_OWNERS can create new organizations. The endpoint */ui/login/register/org* returns HTTP status 404 on GET requests, and 409 on POST requests. -- *[Coming soon](https://github.com/zitadel/zitadel/issues/6250): AllowedLanguages* +- *AllowedLanguages* - The following rules apply if languages are restricted: + - Only allowed languages are listed in the OIDC discovery endpoint */.well-kown/openid-configuration*. + - Login UI texts are only rendered in allowed languages. + - Notification message texts are only rendered in allowed languages. + - Custom Texts can be created for disallowed languages as long as ZITADEL supports that language. Therefore, all texts can be customized before allowing a language. Feature restrictions for an instance are intended to be configured by a user that is managed within that instance. However, if you are self-hosting and need to control your virtual instances usage, [read about the APIs for limits and quotas](/self-hosting/manage/usage_control) that are intended to be used by system users. diff --git a/docs/docs/support/advisory/a10006.md b/docs/docs/support/advisory/a10006.md index 47042da4cb..72d5349068 100644 --- a/docs/docs/support/advisory/a10006.md +++ b/docs/docs/support/advisory/a10006.md @@ -12,6 +12,8 @@ Date: Calendar week 41/42 2023 Versions >= 2.39.0 require the cockroach database user of ZITADEL to be granted to the `VIEWACTIVITY` grant. This can either be reached by grant the role manually or execute the `zitadel init` command. +Cockroach versions 22.2 < 22.2.11 and 23.1 < 23.1.4 will fail the migration. Please make sure to upgrade to more recent versions first. ZITADEL recommends to use the latest stable version of Cockroachdb. + ## Statement To query correct order of events the cockroach database user of ZITADEL needs additional privileges to query the `crdb_internal.cluster_transactions`-table @@ -20,6 +22,8 @@ To query correct order of events the cockroach database user of ZITADEL needs ad Before migrating to versions >= 2.39.0 make sure the cockroach database user has sufficient grants. +Cockroachdb version is up to date. + ## Impact If the user doesn't have sufficient grants, events won't be updated. diff --git a/docs/static/img/go/api-create-auth.png b/docs/static/img/go/api-create-auth.png new file mode 100644 index 0000000000..f16980baa9 Binary files /dev/null and b/docs/static/img/go/api-create-auth.png differ diff --git a/docs/static/img/go/api-create-key.png b/docs/static/img/go/api-create-key.png new file mode 100644 index 0000000000..200b5f5d12 Binary files /dev/null and b/docs/static/img/go/api-create-key.png differ diff --git a/docs/static/img/go/api-create.png b/docs/static/img/go/api-create.png new file mode 100644 index 0000000000..1c21cf0706 Binary files /dev/null and b/docs/static/img/go/api-create.png differ diff --git a/docs/static/img/go/api-project-auth.png b/docs/static/img/go/api-project-auth.png new file mode 100644 index 0000000000..741c08a558 Binary files /dev/null and b/docs/static/img/go/api-project-auth.png differ diff --git a/docs/static/img/go/api-project-role.png b/docs/static/img/go/api-project-role.png new file mode 100644 index 0000000000..a819f6b8d3 Binary files /dev/null and b/docs/static/img/go/api-project-role.png differ diff --git a/go.mod b/go.mod index 815aad0685..6b4b6c530e 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/allegro/bigcache v1.2.1 github.com/benbjohnson/clock v1.3.5 github.com/boombuler/barcode v1.0.1 + github.com/brianvoe/gofakeit/v6 v6.25.0 github.com/cockroachdb/cockroach-go/v2 v2.3.5 github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be github.com/crewjam/saml v0.4.14 @@ -49,7 +50,6 @@ require ( github.com/muhlemmer/gu v0.3.1 github.com/muhlemmer/httpforwarded v0.1.0 github.com/nicksnyder/go-i18n/v2 v2.2.2 - github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 github.com/rakyll/statik v0.1.7 github.com/rs/cors v1.10.1 @@ -60,7 +60,7 @@ require ( github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 github.com/zitadel/logging v0.5.0 - github.com/zitadel/oidc/v3 v3.4.0 + github.com/zitadel/oidc/v3 v3.5.0 github.com/zitadel/passwap v0.4.0 github.com/zitadel/saml v0.1.2 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 @@ -74,10 +74,10 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.20.0 go.opentelemetry.io/otel/trace v1.21.0 go.uber.org/mock v0.3.0 - golang.org/x/crypto v0.15.0 + golang.org/x/crypto v0.16.0 golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 - golang.org/x/net v0.18.0 - golang.org/x/oauth2 v0.14.0 + golang.org/x/net v0.19.0 + golang.org/x/oauth2 v0.15.0 golang.org/x/sync v0.5.0 golang.org/x/text v0.14.0 google.golang.org/api v0.150.0 @@ -107,6 +107,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -201,7 +202,7 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - golang.org/x/sys v0.14.0 + golang.org/x/sys v0.15.0 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/appengine v1.6.8 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 98ea3f3ce3..7ea8f8f0bf 100644 --- a/go.sum +++ b/go.sum @@ -122,6 +122,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/brianvoe/gofakeit/v6 v6.25.0 h1:ZpFjktOpLZUeF8q223o0rUuXtA+m5qW5srjvVi+JkXk= +github.com/brianvoe/gofakeit/v6 v6.25.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= @@ -865,8 +867,8 @@ github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA= github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= -github.com/zitadel/oidc/v3 v3.4.0 h1:JkbNnrk/7IG+NOBoZp/P0kx6tPcBvnCekSqDTPCOok4= -github.com/zitadel/oidc/v3 v3.4.0/go.mod h1:jUnLnx5ihKlo88cSEduZkKlzeMrjzcWVZ8fTzKBxZKY= +github.com/zitadel/oidc/v3 v3.5.0 h1:z51AN6FPo5UuwYJ1r9nLvHlxpTGYd8QXg5MrtYm/dgM= +github.com/zitadel/oidc/v3 v3.5.0/go.mod h1:R8sF5DPR98QQnOoyySsaNqI4NcF/VFMkf/XoYiBUuXQ= github.com/zitadel/passwap v0.4.0 h1:cMaISx+Ve7ilgG7Q8xOli4Z6IWr8Gndss+jeBk5A3O0= github.com/zitadel/passwap v0.4.0/go.mod h1:yHaDM4A68yRkdic5BZ4iUNoc19hT+kYt8n1/Nz+I87g= github.com/zitadel/saml v0.1.2 h1:RICwNTuP2upX4A1sZ8iq1rv4/x3DhZHzFx1e5bTKoTo= @@ -951,8 +953,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1040,8 +1042,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1051,8 +1053,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= -golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= +golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1133,8 +1135,9 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 6b1e54fefa..f464a44702 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -12,17 +12,17 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/zitadel/logging" "google.golang.org/api/option" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/durationpb" - "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/authz" action_grpc "github.com/zitadel/zitadel/internal/api/grpc/action" "github.com/zitadel/zitadel/internal/api/grpc/authn" "github.com/zitadel/zitadel/internal/api/grpc/management" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -34,10 +34,10 @@ import ( type importResponse struct { ret *admin_pb.ImportDataResponse - count *count + count *counts err error } -type count struct { +type counts struct { humanUserCount int humanUserLen int machineUserCount int @@ -70,7 +70,7 @@ type count struct { machineKeysCount int } -func (c *count) getProgress() string { +func (c *counts) getProgress() string { return "progress:" + "human_users " + strconv.Itoa(c.humanUserCount) + "/" + strconv.Itoa(c.humanUserLen) + ", " + "machine_users " + strconv.Itoa(c.machineUserCount) + "/" + strconv.Itoa(c.machineUserLen) + ", " + @@ -91,7 +91,6 @@ func (c *count) getProgress() string { func (s *Server) ImportData(ctx context.Context, req *admin_pb.ImportDataRequest) (_ *admin_pb.ImportDataResponse, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if req.GetDataOrgs() != nil || req.GetDataOrgsv1() != nil { timeoutDuration, err := time.ParseDuration(req.Timeout) if err != nil { @@ -293,10 +292,736 @@ func getFileFromGCS(ctx context.Context, input *admin_pb.ImportDataRequest_GCSIn return ioutil.ReadAll(reader) } -func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*admin_pb.ImportDataResponse, *count, error) { +func importOrg1(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, ctxData authz.CtxData, org *admin_pb.DataOrg, success *admin_pb.ImportDataSuccess, count *counts, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode, appSecretGenerator crypto.Generator) error { + _, err := s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), []string{}) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "org", Id: org.GetOrgId(), Message: err.Error()}) + if _, err := s.query.OrgByID(ctx, true, org.OrgId); err != nil { + // TODO: Only nil if err != not found + return nil + } + } + successOrg := &admin_pb.ImportDataSuccessOrg{ + OrgId: org.GetOrgId(), + ProjectIds: []string{}, + OidcAppIds: []string{}, + ApiAppIds: []string{}, + HumanUserIds: []string{}, + MachineUserIds: []string{}, + ActionIds: []string{}, + ProjectGrants: []*admin_pb.ImportDataSuccessProjectGrant{}, + UserGrants: []*admin_pb.ImportDataSuccessUserGrant{}, + OrgMembers: []string{}, + ProjectMembers: []*admin_pb.ImportDataSuccessProjectMember{}, + ProjectGrantMembers: []*admin_pb.ImportDataSuccessProjectGrantMember{}, + } + logging.Debugf("successful org: %s", successOrg.OrgId) + success.Orgs = append(success.Orgs, successOrg) + + domainPolicy := org.GetDomainPolicy() + if org.DomainPolicy != nil { + _, err := s.command.AddOrgDomainPolicy(ctx, org.GetOrgId(), domainPolicy.UserLoginMustBeDomain, domainPolicy.ValidateOrgDomains, domainPolicy.SmtpSenderAddressMatchesInstanceDomain) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "domain_policy", Id: org.GetOrgId(), Message: err.Error()}) + } + } + return importResources(ctx, s, errors, successOrg, org, count, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode, appSecretGenerator) +} + +func importLabelPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) error { + if org.LabelPolicy == nil { + return nil + } + _, err := s.command.AddLabelPolicy(ctx, org.GetOrgId(), management.AddLabelPolicyToDomain(org.GetLabelPolicy())) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "label_policy", Id: org.GetOrgId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + } else { + _, err = s.command.ActivateLabelPolicy(ctx, org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "label_policy", Id: org.GetOrgId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + } + } + return nil +} + +func importLockoutPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.LockoutPolicy == nil { + return + } + _, err := s.command.AddLockoutPolicy(ctx, org.GetOrgId(), management.AddLockoutPolicyToDomain(org.GetLockoutPolicy())) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "lockout_policy", Id: org.GetOrgId(), Message: err.Error()}) + } +} + +func importOidcIdps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg) error { + if org.OidcIdps == nil { + return nil + } + for _, idp := range org.OidcIdps { + logging.Debugf("import oidcidp: %s", idp.IdpId) + _, err := s.command.ImportIDPConfig(ctx, management.AddOIDCIDPRequestToDomain(idp.Idp), idp.IdpId, org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "oidc_idp", Id: idp.IdpId, Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + logging.Debugf("successful oidcidp: %s", idp.GetIdpId()) + successOrg.OidcIpds = append(successOrg.OidcIpds, idp.GetIdpId()) + } + return nil +} + +func importJwtIdps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg) error { + if org.JwtIdps == nil { + return nil + } + for _, idp := range org.JwtIdps { + logging.Debugf("import jwtidp: %s", idp.IdpId) + _, err := s.command.ImportIDPConfig(ctx, management.AddJWTIDPRequestToDomain(idp.Idp), idp.IdpId, org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "jwt_idp", Id: idp.IdpId, Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + logging.Debugf("successful jwtidp: %s", idp.GetIdpId()) + successOrg.JwtIdps = append(successOrg.JwtIdps, idp.GetIdpId()) + } + return nil +} + +func importLoginPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.LoginPolicy == nil { + return + } + _, err := s.command.AddLoginPolicy(ctx, org.GetOrgId(), management.AddLoginPolicyToCommand(org.GetLoginPolicy())) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "login_policy", Id: org.GetOrgId(), Message: err.Error()}) + } +} + +func importPwComlexityPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.PasswordComplexityPolicy == nil { + return + } + _, err := s.command.AddPasswordComplexityPolicy(ctx, org.GetOrgId(), management.AddPasswordComplexityPolicyToDomain(org.GetPasswordComplexityPolicy())) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "password_complexity_policy", Id: org.GetOrgId(), Message: err.Error()}) + } +} + +func importPrivacyPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.PrivacyPolicy == nil { + return + } + _, err := s.command.AddPrivacyPolicy(ctx, org.GetOrgId(), management.AddPrivacyPolicyToDomain(org.GetPrivacyPolicy())) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "privacy_policy", Id: org.GetOrgId(), Message: err.Error()}) + } +} + +func importHumanUsers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode crypto.Generator) error { + if org.HumanUsers == nil { + return nil + } + for _, user := range org.GetHumanUsers() { + logging.Debugf("import user: %s", user.GetUserId()) + human, passwordless, links := management.ImportHumanUserRequestToDomain(user.User) + human.AggregateID = user.UserId + _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, links, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "human_user", Id: user.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + } else { + count.humanUserCount += 1 + logging.Debugf("successful user %d: %s", count.humanUserCount, user.GetUserId()) + successOrg.HumanUserIds = append(successOrg.HumanUserIds, user.GetUserId()) + } + + if user.User.OtpCode != "" { + logging.Debugf("import user otp: %s", user.GetUserId()) + if err := s.command.ImportHumanTOTP(ctx, user.UserId, "", org.GetOrgId(), user.User.OtpCode); err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "human_user_otp", Id: user.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + } else { + logging.Debugf("successful user otp: %s", user.GetUserId()) + } + } + } + return nil +} + +func importMachineUsers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.MachineUsers == nil { + return nil + } + for _, user := range org.GetMachineUsers() { + logging.Debugf("import user: %s", user.GetUserId()) + _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId())) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "machine_user", Id: user.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.machineUserCount += 1 + logging.Debugf("successful user %d: %s", count.machineUserCount, user.GetUserId()) + successOrg.MachineUserIds = append(successOrg.MachineUserIds, user.GetUserId()) + } + return nil +} + +func importUserMetadata(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.UserMetadata == nil { + return nil + } + for _, userMetadata := range org.GetUserMetadata() { + logging.Debugf("import usermetadata: %s", userMetadata.GetId()+"_"+userMetadata.GetKey()) + _, err := s.command.SetUserMetadata(ctx, &domain.Metadata{Key: userMetadata.GetKey(), Value: userMetadata.GetValue()}, userMetadata.GetId(), org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "user_metadata", Id: userMetadata.GetId() + "_" + userMetadata.GetKey(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.userMetadataCount += 1 + logging.Debugf("successful usermetadata %d: %s", count.userMetadataCount, userMetadata.GetId()+"_"+userMetadata.GetKey()) + successOrg.UserMetadata = append(successOrg.UserMetadata, &admin_pb.ImportDataSuccessUserMetadata{UserId: userMetadata.GetId(), Key: userMetadata.GetKey()}) + } + return nil +} + +func importMachineKeys(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.MachineKeys == nil { + return nil + } + for _, key := range org.GetMachineKeys() { + logging.Debugf("import machine_user_key: %s", key.KeyId) + _, err := s.command.AddUserMachineKey(ctx, &command.MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: key.UserId, + ResourceOwner: org.GetOrgId(), + }, + KeyID: key.KeyId, + Type: authn.KeyTypeToDomain(key.Type), + ExpirationDate: key.ExpirationDate.AsTime(), + PublicKey: key.PublicKey, + }) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "machine_user_key", Id: key.KeyId, Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.machineKeysCount += 1 + logging.Debugf("successful machine_user_key %d: %s", count.machineKeysCount, key.KeyId) + successOrg.MachineKeys = append(successOrg.MachineKeys, key.KeyId) + } + return nil +} + +func importUserLinks(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.UserLinks == nil { + return nil + } + for _, userLinks := range org.GetUserLinks() { + logging.Debugf("import userlink: %s", userLinks.GetUserId()+"_"+userLinks.GetIdpId()+"_"+userLinks.GetProvidedUserId()+"_"+userLinks.GetProvidedUserName()) + externalIDP := &command.AddLink{ + IDPID: userLinks.IdpId, + IDPExternalID: userLinks.ProvidedUserId, + DisplayName: userLinks.ProvidedUserName, + } + if _, err := s.command.AddUserIDPLink(ctx, userLinks.UserId, org.GetOrgId(), externalIDP); err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "user_link", Id: userLinks.UserId + "_" + userLinks.IdpId, Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.userLinksCount += 1 + logging.Debugf("successful userlink %d: %s", count.userLinksCount, userLinks.GetUserId()+"_"+userLinks.GetIdpId()+"_"+userLinks.GetProvidedUserId()+"_"+userLinks.GetProvidedUserName()) + successOrg.UserLinks = append(successOrg.UserLinks, &admin_pb.ImportDataSuccessUserLinks{UserId: userLinks.GetUserId(), IdpId: userLinks.GetIdpId(), ExternalUserId: userLinks.GetProvidedUserId(), DisplayName: userLinks.GetProvidedUserName()}) + } + return nil + +} + +func importProjects(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.Projects == nil { + return nil + } + for _, project := range org.GetProjects() { + logging.Debugf("import project: %s", project.GetProjectId()) + _, err := s.command.AddProjectWithID(ctx, management.ProjectCreateToDomain(project.GetProject()), org.GetOrgId(), project.GetProjectId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "project", Id: project.GetProjectId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.projectCount += 1 + logging.Debugf("successful project %d: %s", count.projectCount, project.GetProjectId()) + successOrg.ProjectIds = append(successOrg.ProjectIds, project.GetProjectId()) + } + return nil +} + +func importOIDCApps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts, appSecretGenerator crypto.Generator) error { + if org.OidcApps == nil { + return nil + } + for _, app := range org.GetOidcApps() { + logging.Debugf("import oidcapplication: %s", app.GetAppId()) + _, err := s.command.AddOIDCApplicationWithID(ctx, management.AddOIDCAppRequestToDomain(app.App), org.GetOrgId(), app.GetAppId(), appSecretGenerator) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "oidc_app", Id: app.GetAppId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.oidcAppCount += 1 + logging.Debugf("successful oidcapplication %d: %s", count.oidcAppCount, app.GetAppId()) + successOrg.OidcAppIds = append(successOrg.OidcAppIds, app.GetAppId()) + } + return nil +} + +func importAPIApps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts, appSecretGenerator crypto.Generator) error { + if org.ApiApps == nil { + return nil + } + for _, app := range org.GetApiApps() { + logging.Debugf("import apiapplication: %s", app.GetAppId()) + _, err := s.command.AddAPIApplicationWithID(ctx, management.AddAPIAppRequestToDomain(app.GetApp()), org.GetOrgId(), app.GetAppId(), appSecretGenerator) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "api_app", Id: app.GetAppId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.apiAppCount += 1 + logging.Debugf("successful apiapplication %d: %s", count.apiAppCount, app.GetAppId()) + successOrg.ApiAppIds = append(successOrg.ApiAppIds, app.GetAppId()) + } + return nil +} + +func importAppKeys(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.AppKeys == nil { + return nil + } + for _, key := range org.GetAppKeys() { + logging.Debugf("import app_key: %s", key.Id) + _, err := s.command.AddApplicationKeyWithID(ctx, &domain.ApplicationKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: key.ProjectId, + ResourceOwner: org.GetOrgId(), + }, + ApplicationID: key.AppId, + ClientID: key.ClientId, + KeyID: key.Id, + Type: authn.KeyTypeToDomain(key.Type), + ExpirationDate: key.ExpirationDate.AsTime(), + PublicKey: key.PublicKey, + }, org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "app_key", Id: key.Id, Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.appKeysCount += 1 + logging.Debugf("successful app_key %d: %s", count.appKeysCount, key.Id) + successOrg.AppKeys = append(successOrg.AppKeys, key.Id) + } + return nil +} + +func importActions(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.Actions == nil { + return nil + } + for _, action := range org.GetActions() { + logging.Debugf("import action: %s", action.GetActionId()) + _, _, err := s.command.AddActionWithID(ctx, management.CreateActionRequestToDomain(action.GetAction()), org.GetOrgId(), action.GetActionId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "action", Id: action.GetActionId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.actionCount += 1 + logging.Debugf("successful action %d: %s", count.actionCount, action.GetActionId()) + successOrg.ActionIds = append(successOrg.ActionIds, action.ActionId) + } + return nil +} +func importProjectRoles(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.ProjectRoles == nil { + return nil + } + for _, role := range org.GetProjectRoles() { + logging.Debugf("import projectroles: %s", role.ProjectId+"_"+role.RoleKey) + _, err := s.command.AddProjectRole(ctx, management.AddProjectRoleRequestToDomain(role), org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_role", Id: role.ProjectId + "_" + role.RoleKey, Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.projectRolesCount += 1 + logging.Debugf("successful projectroles %d: %s", count.projectRolesCount, role.ProjectId+"_"+role.RoleKey) + successOrg.ProjectRoles = append(successOrg.ProjectRoles, successOrg.ActionIds...) + successOrg.ProjectRoles = append(successOrg.ProjectRoles, role.ProjectId+"_"+role.RoleKey) + } + return nil +} + +func importResources(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode, appSecretGenerator crypto.Generator) error { + if err := importOrgDomains(ctx, s, errors, successOrg, org); err != nil { + return err + } + if err := importLabelPolicy(ctx, s, errors, org); err != nil { + return err + } + importLockoutPolicy(ctx, s, errors, org) + if err := importOidcIdps(ctx, s, errors, successOrg, org); err != nil { + return err + } + if err := importJwtIdps(ctx, s, errors, successOrg, org); err != nil { + return err + } + importLoginPolicy(ctx, s, errors, org) + importPwComlexityPolicy(ctx, s, errors, org) + importPrivacyPolicy(ctx, s, errors, org) + importLoginTexts(ctx, s, errors, org) + importInitMessageTexts(ctx, s, errors, org) + importPWResetMessageTexts(ctx, s, errors, org) + importVerifyEmailMessageTexts(ctx, s, errors, org) + importVerifyPhoneMessageTexts(ctx, s, errors, org) + importDomainClaimedMessageTexts(ctx, s, errors, org) + importPasswordlessRegistrationMessageTexts(ctx, s, errors, org) + if err := importHumanUsers(ctx, s, errors, successOrg, org, count, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode); err != nil { + return err + } + if err := importMachineUsers(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + if err := importUserMetadata(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + if err := importMachineKeys(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + if err := importUserLinks(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + if err := importProjects(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + if err := importOIDCApps(ctx, s, errors, successOrg, org, count, appSecretGenerator); err != nil { + return err + } + if err := importAPIApps(ctx, s, errors, successOrg, org, count, appSecretGenerator); err != nil { + return err + } + if err := importAppKeys(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + if err := importActions(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + if err := importProjectRoles(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + return nil +} + +func importOrgDomains(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg) error { + if org.Domains == nil { + return nil + } + for _, domainR := range org.Domains { + orgDomain := &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: org.GetOrgId(), + }, + Domain: domainR.DomainName, + Verified: domainR.IsVerified, + Primary: domainR.IsPrimary, + } + _, err := s.command.AddOrgDomain(ctx, org.GetOrgId(), domainR.DomainName, []string{}) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "domain", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + logging.Debugf("successful domain: %s", domainR.DomainName) + successOrg.Domains = append(successOrg.Domains, domainR.DomainName) + + if domainR.IsVerified { + if _, err := s.command.VerifyOrgDomain(ctx, org.GetOrgId(), domainR.DomainName); err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "domain_isverified", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) + } + } + if domainR.IsPrimary { + if _, err := s.command.SetPrimaryOrgDomain(ctx, orgDomain); err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "domain_isprimary", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) + } + } + } + return nil +} + +func importLoginTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.LoginTexts == nil { + return + } + for _, text := range org.GetLoginTexts() { + _, err := s.command.SetOrgLoginText(ctx, org.GetOrgId(), management.SetLoginCustomTextToDomain(text)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "login_texts", Id: org.GetOrgId() + "_" + text.Language, Message: err.Error()}) + } + } +} + +func importInitMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.InitMessages == nil { + return + } + for _, message := range org.GetInitMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetInitCustomTextToDomain(message)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "init_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } +} + +func importPWResetMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.PasswordResetMessages == nil { + return + } + for _, message := range org.GetPasswordResetMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetPasswordResetCustomTextToDomain(message)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "password_reset_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } +} + +func importVerifyEmailMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.VerifyEmailMessages == nil { + return + } + for _, message := range org.GetVerifyEmailMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetVerifyEmailCustomTextToDomain(message)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "verify_email_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } +} + +func importVerifyPhoneMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.VerifyPhoneMessages != nil { + return + } + for _, message := range org.GetVerifyPhoneMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetVerifyPhoneCustomTextToDomain(message)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "verify_phone_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } +} + +func importDomainClaimedMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.DomainClaimedMessages == nil { + return + } + for _, message := range org.GetDomainClaimedMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetDomainClaimedCustomTextToDomain(message)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "domain_claimed_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } +} + +func importPasswordlessRegistrationMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.PasswordlessRegistrationMessages == nil { + return + } + for _, message := range org.GetPasswordlessRegistrationMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetPasswordlessRegistrationCustomTextToDomain(message)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "passwordless_registration_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } +} + +func importOrg2(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, success *admin_pb.ImportDataSuccess, count *counts, org *admin_pb.DataOrg) error { + successOrg := findOldOrg(success, org.OrgId) + if successOrg == nil { + return nil + } + if org.TriggerActions != nil { + for _, triggerAction := range org.GetTriggerActions() { + _, err := s.command.SetTriggerActions(ctx, action_grpc.FlowTypeToDomain(triggerAction.FlowType), action_grpc.TriggerTypeToDomain(triggerAction.TriggerType), triggerAction.ActionIds, org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "trigger_action", Id: triggerAction.FlowType + "_" + triggerAction.TriggerType, Message: err.Error()}) + continue + } + successOrg.TriggerActions = append(successOrg.TriggerActions, &management_pb.SetTriggerActionsRequest{FlowType: triggerAction.FlowType, TriggerType: triggerAction.TriggerType, ActionIds: triggerAction.GetActionIds()}) + } + } + if org.ProjectGrants != nil { + for _, grant := range org.GetProjectGrants() { + logging.Debugf("import projectgrant: %s", grant.GetGrantId()+"_"+grant.GetProjectGrant().GetProjectId()+"_"+grant.GetProjectGrant().GetGrantedOrgId()) + _, err := s.command.AddProjectGrantWithID(ctx, management.AddProjectGrantRequestToDomain(grant.GetProjectGrant()), grant.GetGrantId(), org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_grant", Id: org.GetOrgId() + "_" + grant.GetProjectGrant().GetProjectId() + "_" + grant.GetProjectGrant().GetGrantedOrgId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.projectGrantCount += 1 + logging.Debugf("successful projectgrant %d: %s", count.projectGrantCount, grant.GetGrantId()+"_"+grant.GetProjectGrant().GetProjectId()+"_"+grant.GetProjectGrant().GetGrantedOrgId()) + successOrg.ProjectGrants = append(successOrg.ProjectGrants, &admin_pb.ImportDataSuccessProjectGrant{GrantId: grant.GetGrantId(), ProjectId: grant.GetProjectGrant().GetProjectId(), OrgId: grant.GetProjectGrant().GetGrantedOrgId()}) + } + } + if org.UserGrants != nil { + for _, grant := range org.GetUserGrants() { + logging.Debugf("import usergrant: %s", grant.GetProjectId()+"_"+grant.GetUserId()) + _, err := s.command.AddUserGrant(ctx, management.AddUserGrantRequestToDomain(grant), org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "user_grant", Id: org.GetOrgId() + "_" + grant.GetProjectId() + "_" + grant.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.userGrantCount += 1 + logging.Debugf("successful usergrant %d: %s", count.userGrantCount, grant.GetProjectId()+"_"+grant.GetUserId()) + successOrg.UserGrants = append(successOrg.UserGrants, &admin_pb.ImportDataSuccessUserGrant{ProjectId: grant.GetProjectId(), UserId: grant.GetUserId()}) + } + } + return nil +} + +func importOrg3(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, success *admin_pb.ImportDataSuccess, count *counts, org *admin_pb.DataOrg) error { + successOrg := findOldOrg(success, org.OrgId) + if successOrg == nil { + return nil + } + if err := importOrgMembers(ctx, s, errors, successOrg, count, org); err != nil { + return err + } + if err := importProjectGrantMembers(ctx, s, errors, successOrg, count, org); err != nil { + return err + } + return importProjectMembers(ctx, s, errors, successOrg, count, org) +} + +func importOrgMembers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, count *counts, org *admin_pb.DataOrg) error { + if org.OrgMembers == nil { + return nil + } + for _, member := range org.GetOrgMembers() { + logging.Debugf("import orgmember: %s", member.GetUserId()) + _, err := s.command.AddOrgMember(ctx, org.GetOrgId(), member.GetUserId(), member.GetRoles()...) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "org_member", Id: org.GetOrgId() + "_" + member.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.orgMemberCount += 1 + logging.Debugf("successful orgmember %d: %s", count.orgMemberCount, member.GetUserId()) + successOrg.OrgMembers = append(successOrg.OrgMembers, member.GetUserId()) + } + return nil +} + +func importProjectGrantMembers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, count *counts, org *admin_pb.DataOrg) error { + if org.ProjectGrantMembers == nil { + return nil + } + for _, member := range org.GetProjectGrantMembers() { + logging.Debugf("import projectgrantmember: %s", member.GetProjectId()+"_"+member.GetGrantId()+"_"+member.GetUserId()) + _, err := s.command.AddProjectGrantMember(ctx, management.AddProjectGrantMemberRequestToDomain(member)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_grant_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetGrantId() + "_" + member.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.projectGrantMemberCount += 1 + logging.Debugf("successful projectgrantmember %d: %s", count.projectGrantMemberCount, member.GetProjectId()+"_"+member.GetGrantId()+"_"+member.GetUserId()) + successOrg.ProjectGrantMembers = append(successOrg.ProjectGrantMembers, &admin_pb.ImportDataSuccessProjectGrantMember{ProjectId: member.GetProjectId(), GrantId: member.GetGrantId(), UserId: member.GetUserId()}) + } + return nil +} + +func importProjectMembers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, count *counts, org *admin_pb.DataOrg) error { + if org.ProjectMembers == nil { + return nil + } + for _, member := range org.GetProjectMembers() { + logging.Debugf("import orgmember: %s", member.GetProjectId()+"_"+member.GetUserId()) + _, err := s.command.AddProjectMember(ctx, management.AddProjectMemberRequestToDomain(member), org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.projectMembersCount += 1 + logging.Debugf("successful orgmember %d: %s", count.projectMembersCount, member.GetProjectId()+"_"+member.GetUserId()) + successOrg.ProjectMembers = append(successOrg.ProjectMembers, &admin_pb.ImportDataSuccessProjectMember{ProjectId: member.GetProjectId(), UserId: member.GetUserId()}) + } + return nil +} + +func findOldOrg(success *admin_pb.ImportDataSuccess, orgId string) *admin_pb.ImportDataSuccessOrg { + for _, oldOrd := range success.Orgs { + if orgId == oldOrd.OrgId { + return oldOrd + } + } + return nil +} + +func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*admin_pb.ImportDataResponse, *counts, error) { errors := make([]*admin_pb.ImportDataError, 0) success := &admin_pb.ImportDataSuccess{} - count := &count{} + count := &counts{} appSecretGenerator, err := s.query.InitHashGenerator(ctx, domain.SecretGeneratorTypeAppSecret, s.passwordHashAlg) if err != nil { @@ -338,533 +1063,21 @@ func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*adm count.machineKeysCount += len(org.GetMachineKeys()) count.appKeysCount += len(org.GetAppKeys()) } - for _, org := range orgs { - _, err := s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), []string{}) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "org", Id: org.GetOrgId(), Message: err.Error()}) - - if _, err := s.query.OrgByID(ctx, true, org.OrgId); err != nil { - continue - } - } - successOrg := &admin_pb.ImportDataSuccessOrg{ - OrgId: org.GetOrgId(), - ProjectIds: []string{}, - OidcAppIds: []string{}, - ApiAppIds: []string{}, - HumanUserIds: []string{}, - MachineUserIds: []string{}, - ActionIds: []string{}, - ProjectGrants: []*admin_pb.ImportDataSuccessProjectGrant{}, - UserGrants: []*admin_pb.ImportDataSuccessUserGrant{}, - OrgMembers: []string{}, - ProjectMembers: []*admin_pb.ImportDataSuccessProjectMember{}, - ProjectGrantMembers: []*admin_pb.ImportDataSuccessProjectGrantMember{}, - } - logging.Debugf("successful org: %s", successOrg.OrgId) - success.Orgs = append(success.Orgs, successOrg) - - domainPolicy := org.GetDomainPolicy() - if org.DomainPolicy != nil { - _, err := s.command.AddOrgDomainPolicy(ctx, org.GetOrgId(), domainPolicy.UserLoginMustBeDomain, domainPolicy.ValidateOrgDomains, domainPolicy.SmtpSenderAddressMatchesInstanceDomain) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "domain_policy", Id: org.GetOrgId(), Message: err.Error()}) - } - } - if org.Domains != nil { - for _, domainR := range org.Domains { - orgDomain := &domain.OrgDomain{ - ObjectRoot: models.ObjectRoot{ - AggregateID: org.GetOrgId(), - }, - Domain: domainR.DomainName, - Verified: domainR.IsVerified, - Primary: domainR.IsPrimary, - } - _, err := s.command.AddOrgDomain(ctx, org.GetOrgId(), domainR.DomainName, []string{}) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "domain", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - logging.Debugf("successful domain: %s", domainR.DomainName) - successOrg.Domains = append(successOrg.Domains, domainR.DomainName) - - if domainR.IsVerified { - if _, err := s.command.VerifyOrgDomain(ctx, org.GetOrgId(), domainR.DomainName); err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "domain_isverified", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) - } - } - if domainR.IsPrimary { - if _, err := s.command.SetPrimaryOrgDomain(ctx, orgDomain); err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "domain_isprimary", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) - } - } - } - } - if org.LabelPolicy != nil { - _, err = s.command.AddLabelPolicy(ctx, org.GetOrgId(), management.AddLabelPolicyToDomain(org.GetLabelPolicy())) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "label_policy", Id: org.GetOrgId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - } else { - _, err = s.command.ActivateLabelPolicy(ctx, org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "label_policy", Id: org.GetOrgId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - } - } - } - if org.LockoutPolicy != nil { - _, err = s.command.AddLockoutPolicy(ctx, org.GetOrgId(), management.AddLockoutPolicyToDomain(org.GetLockoutPolicy())) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "lockout_policy", Id: org.GetOrgId(), Message: err.Error()}) - } - } - if org.OidcIdps != nil { - for _, idp := range org.OidcIdps { - logging.Debugf("import oidcidp: %s", idp.IdpId) - _, err := s.command.ImportIDPConfig(ctx, management.AddOIDCIDPRequestToDomain(idp.Idp), idp.IdpId, org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "oidc_idp", Id: idp.IdpId, Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - logging.Debugf("successful oidcidp: %s", idp.GetIdpId()) - successOrg.OidcIpds = append(successOrg.OidcIpds, idp.GetIdpId()) - } - } - if org.JwtIdps != nil { - for _, idp := range org.JwtIdps { - logging.Debugf("import jwtidp: %s", idp.IdpId) - _, err := s.command.ImportIDPConfig(ctx, management.AddJWTIDPRequestToDomain(idp.Idp), idp.IdpId, org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "jwt_idp", Id: idp.IdpId, Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - logging.Debugf("successful jwtidp: %s", idp.GetIdpId()) - successOrg.JwtIdps = append(successOrg.JwtIdps, idp.GetIdpId()) - } - } - if org.LoginPolicy != nil { - _, err = s.command.AddLoginPolicy(ctx, org.GetOrgId(), management.AddLoginPolicyToCommand(org.GetLoginPolicy())) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "login_policy", Id: org.GetOrgId(), Message: err.Error()}) - } - } - if org.PasswordComplexityPolicy != nil { - _, err = s.command.AddPasswordComplexityPolicy(ctx, org.GetOrgId(), management.AddPasswordComplexityPolicyToDomain(org.GetPasswordComplexityPolicy())) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "password_complexity_policy", Id: org.GetOrgId(), Message: err.Error()}) - } - } - if org.PrivacyPolicy != nil { - _, err = s.command.AddPrivacyPolicy(ctx, org.GetOrgId(), management.AddPrivacyPolicyToDomain(org.GetPrivacyPolicy())) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "privacy_policy", Id: org.GetOrgId(), Message: err.Error()}) - } - } - if org.LoginTexts != nil { - for _, text := range org.GetLoginTexts() { - _, err := s.command.SetOrgLoginText(ctx, org.GetOrgId(), management.SetLoginCustomTextToDomain(text)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "login_texts", Id: org.GetOrgId() + "_" + text.Language, Message: err.Error()}) - } - } - } - if org.InitMessages != nil { - for _, message := range org.GetInitMessages() { - _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetInitCustomTextToDomain(message)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "init_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) - } - } - } - if org.PasswordResetMessages != nil { - for _, message := range org.GetPasswordResetMessages() { - _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetPasswordResetCustomTextToDomain(message)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "password_reset_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) - } - } - } - if org.VerifyEmailMessages != nil { - for _, message := range org.GetVerifyEmailMessages() { - _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetVerifyEmailCustomTextToDomain(message)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "verify_email_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) - } - } - } - if org.VerifyPhoneMessages != nil { - for _, message := range org.GetVerifyPhoneMessages() { - _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetVerifyPhoneCustomTextToDomain(message)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "verify_phone_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) - } - } - } - if org.DomainClaimedMessages != nil { - for _, message := range org.GetDomainClaimedMessages() { - _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetDomainClaimedCustomTextToDomain(message)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "domain_claimed_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) - } - } - } - if org.PasswordlessRegistrationMessages != nil { - for _, message := range org.GetPasswordlessRegistrationMessages() { - _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetPasswordlessRegistrationCustomTextToDomain(message)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "passwordless_registration_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) - } - } - } - - if org.HumanUsers != nil { - for _, user := range org.GetHumanUsers() { - logging.Debugf("import user: %s", user.GetUserId()) - human, passwordless, links := management.ImportHumanUserRequestToDomain(user.User) - human.AggregateID = user.UserId - _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, links, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "human_user", Id: user.GetUserId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - } else { - count.humanUserCount += 1 - logging.Debugf("successful user %d: %s", count.humanUserCount, user.GetUserId()) - successOrg.HumanUserIds = append(successOrg.HumanUserIds, user.GetUserId()) - } - - if user.User.OtpCode != "" { - logging.Debugf("import user otp: %s", user.GetUserId()) - if err := s.command.ImportHumanTOTP(ctx, user.UserId, "", org.GetOrgId(), user.User.OtpCode); err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "human_user_otp", Id: user.GetUserId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - } else { - logging.Debugf("successful user otp: %s", user.GetUserId()) - } - } - } - } - if org.MachineUsers != nil { - for _, user := range org.GetMachineUsers() { - logging.Debugf("import user: %s", user.GetUserId()) - _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId())) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "machine_user", Id: user.GetUserId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.machineUserCount += 1 - logging.Debugf("successful user %d: %s", count.machineUserCount, user.GetUserId()) - successOrg.MachineUserIds = append(successOrg.MachineUserIds, user.GetUserId()) - } - } - if org.UserMetadata != nil { - for _, userMetadata := range org.GetUserMetadata() { - logging.Debugf("import usermetadata: %s", userMetadata.GetId()+"_"+userMetadata.GetKey()) - _, err := s.command.SetUserMetadata(ctx, &domain.Metadata{Key: userMetadata.GetKey(), Value: userMetadata.GetValue()}, userMetadata.GetId(), org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "user_metadata", Id: userMetadata.GetId() + "_" + userMetadata.GetKey(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.userMetadataCount += 1 - logging.Debugf("successful usermetadata %d: %s", count.userMetadataCount, userMetadata.GetId()+"_"+userMetadata.GetKey()) - successOrg.UserMetadata = append(successOrg.UserMetadata, &admin_pb.ImportDataSuccessUserMetadata{UserId: userMetadata.GetId(), Key: userMetadata.GetKey()}) - } - } - if org.MachineKeys != nil { - for _, key := range org.GetMachineKeys() { - logging.Debugf("import machine_user_key: %s", key.KeyId) - _, err := s.command.AddUserMachineKey(ctx, &command.MachineKey{ - ObjectRoot: models.ObjectRoot{ - AggregateID: key.UserId, - ResourceOwner: org.GetOrgId(), - }, - KeyID: key.KeyId, - Type: authn.KeyTypeToDomain(key.Type), - ExpirationDate: key.ExpirationDate.AsTime(), - PublicKey: key.PublicKey, - }) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "machine_user_key", Id: key.KeyId, Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.machineKeysCount += 1 - logging.Debugf("successful machine_user_key %d: %s", count.machineKeysCount, key.KeyId) - successOrg.MachineKeys = append(successOrg.MachineKeys, key.KeyId) - } - } - if org.UserLinks != nil { - for _, userLinks := range org.GetUserLinks() { - logging.Debugf("import userlink: %s", userLinks.GetUserId()+"_"+userLinks.GetIdpId()+"_"+userLinks.GetProvidedUserId()+"_"+userLinks.GetProvidedUserName()) - externalIDP := &command.AddLink{ - IDPID: userLinks.IdpId, - IDPExternalID: userLinks.ProvidedUserId, - DisplayName: userLinks.ProvidedUserName, - } - if _, err := s.command.AddUserIDPLink(ctx, userLinks.UserId, org.GetOrgId(), externalIDP); err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "user_link", Id: userLinks.UserId + "_" + userLinks.IdpId, Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.userLinksCount += 1 - logging.Debugf("successful userlink %d: %s", count.userLinksCount, userLinks.GetUserId()+"_"+userLinks.GetIdpId()+"_"+userLinks.GetProvidedUserId()+"_"+userLinks.GetProvidedUserName()) - successOrg.UserLinks = append(successOrg.UserLinks, &admin_pb.ImportDataSuccessUserLinks{UserId: userLinks.GetUserId(), IdpId: userLinks.GetIdpId(), ExternalUserId: userLinks.GetProvidedUserId(), DisplayName: userLinks.GetProvidedUserName()}) - } - } - if org.Projects != nil { - for _, project := range org.GetProjects() { - logging.Debugf("import project: %s", project.GetProjectId()) - _, err := s.command.AddProjectWithID(ctx, management.ProjectCreateToDomain(project.GetProject()), org.GetOrgId(), project.GetProjectId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "project", Id: project.GetProjectId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.projectCount += 1 - logging.Debugf("successful project %d: %s", count.projectCount, project.GetProjectId()) - successOrg.ProjectIds = append(successOrg.ProjectIds, project.GetProjectId()) - } - } - if org.OidcApps != nil { - for _, app := range org.GetOidcApps() { - logging.Debugf("import oidcapplication: %s", app.GetAppId()) - _, err := s.command.AddOIDCApplicationWithID(ctx, management.AddOIDCAppRequestToDomain(app.App), org.GetOrgId(), app.GetAppId(), appSecretGenerator) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "oidc_app", Id: app.GetAppId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.oidcAppCount += 1 - logging.Debugf("successful oidcapplication %d: %s", count.oidcAppCount, app.GetAppId()) - successOrg.OidcAppIds = append(successOrg.OidcAppIds, app.GetAppId()) - } - } - if org.ApiApps != nil { - for _, app := range org.GetApiApps() { - logging.Debugf("import apiapplication: %s", app.GetAppId()) - _, err := s.command.AddAPIApplicationWithID(ctx, management.AddAPIAppRequestToDomain(app.GetApp()), org.GetOrgId(), app.GetAppId(), appSecretGenerator) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "api_app", Id: app.GetAppId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.apiAppCount += 1 - logging.Debugf("successful apiapplication %d: %s", count.apiAppCount, app.GetAppId()) - successOrg.ApiAppIds = append(successOrg.ApiAppIds, app.GetAppId()) - } - } - if org.AppKeys != nil { - for _, key := range org.GetAppKeys() { - logging.Debugf("import app_key: %s", key.Id) - _, err := s.command.AddApplicationKeyWithID(ctx, &domain.ApplicationKey{ - ObjectRoot: models.ObjectRoot{ - AggregateID: key.ProjectId, - ResourceOwner: org.GetOrgId(), - }, - ApplicationID: key.AppId, - ClientID: key.ClientId, - KeyID: key.Id, - Type: authn.KeyTypeToDomain(key.Type), - ExpirationDate: key.ExpirationDate.AsTime(), - PublicKey: key.PublicKey, - }, org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "app_key", Id: key.Id, Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.appKeysCount += 1 - logging.Debugf("successful app_key %d: %s", count.appKeysCount, key.Id) - successOrg.AppKeys = append(successOrg.AppKeys, key.Id) - } - } - if org.Actions != nil { - for _, action := range org.GetActions() { - logging.Debugf("import action: %s", action.GetActionId()) - _, _, err := s.command.AddActionWithID(ctx, management.CreateActionRequestToDomain(action.GetAction()), org.GetOrgId(), action.GetActionId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "action", Id: action.GetActionId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.actionCount += 1 - logging.Debugf("successful action %d: %s", count.actionCount, action.GetActionId()) - successOrg.ActionIds = append(successOrg.ActionIds, action.ActionId) - } - } - if org.ProjectRoles != nil { - for _, role := range org.GetProjectRoles() { - logging.Debugf("import projectroles: %s", role.ProjectId+"_"+role.RoleKey) - _, err := s.command.AddProjectRole(ctx, management.AddProjectRoleRequestToDomain(role), org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "project_role", Id: role.ProjectId + "_" + role.RoleKey, Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.projectRolesCount += 1 - logging.Debugf("successful projectroles %d: %s", count.projectRolesCount, role.ProjectId+"_"+role.RoleKey) - successOrg.ProjectRoles = append(successOrg.ActionIds, role.ProjectId+"_"+role.RoleKey) - } + if err = importOrg1(ctx, s, &errors, ctxData, org, success, count, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode, appSecretGenerator); err != nil { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err } } - for _, org := range orgs { - var successOrg *admin_pb.ImportDataSuccessOrg - for _, oldOrd := range success.Orgs { - if org.OrgId == oldOrd.OrgId { - successOrg = oldOrd - } - } - if successOrg == nil { - continue - } - - if org.TriggerActions != nil { - for _, triggerAction := range org.GetTriggerActions() { - _, err := s.command.SetTriggerActions(ctx, action_grpc.FlowTypeToDomain(triggerAction.FlowType), action_grpc.TriggerTypeToDomain(triggerAction.TriggerType), triggerAction.ActionIds, org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "trigger_action", Id: triggerAction.FlowType + "_" + triggerAction.TriggerType, Message: err.Error()}) - continue - } - successOrg.TriggerActions = append(successOrg.TriggerActions, &management_pb.SetTriggerActionsRequest{FlowType: triggerAction.FlowType, TriggerType: triggerAction.TriggerType, ActionIds: triggerAction.GetActionIds()}) - } - } - if org.ProjectGrants != nil { - for _, grant := range org.GetProjectGrants() { - logging.Debugf("import projectgrant: %s", grant.GetGrantId()+"_"+grant.GetProjectGrant().GetProjectId()+"_"+grant.GetProjectGrant().GetGrantedOrgId()) - _, err := s.command.AddProjectGrantWithID(ctx, management.AddProjectGrantRequestToDomain(grant.GetProjectGrant()), grant.GetGrantId(), org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "project_grant", Id: org.GetOrgId() + "_" + grant.GetProjectGrant().GetProjectId() + "_" + grant.GetProjectGrant().GetGrantedOrgId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.projectGrantCount += 1 - logging.Debugf("successful projectgrant %d: %s", count.projectGrantCount, grant.GetGrantId()+"_"+grant.GetProjectGrant().GetProjectId()+"_"+grant.GetProjectGrant().GetGrantedOrgId()) - successOrg.ProjectGrants = append(successOrg.ProjectGrants, &admin_pb.ImportDataSuccessProjectGrant{GrantId: grant.GetGrantId(), ProjectId: grant.GetProjectGrant().GetProjectId(), OrgId: grant.GetProjectGrant().GetGrantedOrgId()}) - } - } - if org.UserGrants != nil { - for _, grant := range org.GetUserGrants() { - logging.Debugf("import usergrant: %s", grant.GetProjectId()+"_"+grant.GetUserId()) - _, err := s.command.AddUserGrant(ctx, management.AddUserGrantRequestToDomain(grant), org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "user_grant", Id: org.GetOrgId() + "_" + grant.GetProjectId() + "_" + grant.GetUserId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.userGrantCount += 1 - logging.Debugf("successful usergrant %d: %s", count.userGrantCount, grant.GetProjectId()+"_"+grant.GetUserId()) - successOrg.UserGrants = append(successOrg.UserGrants, &admin_pb.ImportDataSuccessUserGrant{ProjectId: grant.GetProjectId(), UserId: grant.GetUserId()}) - } + if err = importOrg2(ctx, s, &errors, success, count, org); err != nil { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err } } - for _, org := range orgs { - var successOrg *admin_pb.ImportDataSuccessOrg - for _, oldOrd := range success.Orgs { - if org.OrgId == oldOrd.OrgId { - successOrg = oldOrd - } - } - if successOrg == nil { - continue - } - - if org.OrgMembers != nil { - for _, member := range org.GetOrgMembers() { - logging.Debugf("import orgmember: %s", member.GetUserId()) - _, err := s.command.AddOrgMember(ctx, org.GetOrgId(), member.GetUserId(), member.GetRoles()...) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "org_member", Id: org.GetOrgId() + "_" + member.GetUserId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.orgMemberCount += 1 - logging.Debugf("successful orgmember %d: %s", count.orgMemberCount, member.GetUserId()) - successOrg.OrgMembers = append(successOrg.OrgMembers, member.GetUserId()) - } - } - if org.ProjectGrantMembers != nil { - for _, member := range org.GetProjectGrantMembers() { - logging.Debugf("import projectgrantmember: %s", member.GetProjectId()+"_"+member.GetGrantId()+"_"+member.GetUserId()) - _, err := s.command.AddProjectGrantMember(ctx, management.AddProjectGrantMemberRequestToDomain(member)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "project_grant_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetGrantId() + "_" + member.GetUserId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.projectGrantMemberCount += 1 - logging.Debugf("successful projectgrantmember %d: %s", count.projectGrantMemberCount, member.GetProjectId()+"_"+member.GetGrantId()+"_"+member.GetUserId()) - successOrg.ProjectGrantMembers = append(successOrg.ProjectGrantMembers, &admin_pb.ImportDataSuccessProjectGrantMember{ProjectId: member.GetProjectId(), GrantId: member.GetGrantId(), UserId: member.GetUserId()}) - } - } - if org.ProjectMembers != nil { - for _, member := range org.GetProjectMembers() { - logging.Debugf("import orgmember: %s", member.GetProjectId()+"_"+member.GetUserId()) - _, err := s.command.AddProjectMember(ctx, management.AddProjectMemberRequestToDomain(member), org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "project_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetUserId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.projectMembersCount += 1 - logging.Debugf("successful orgmember %d: %s", count.projectMembersCount, member.GetProjectId()+"_"+member.GetUserId()) - successOrg.ProjectMembers = append(successOrg.ProjectMembers, &admin_pb.ImportDataSuccessProjectMember{ProjectId: member.GetProjectId(), UserId: member.GetUserId()}) - } + if err = importOrg3(ctx, s, &errors, success, count, org); err != nil { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err } } - return &admin_pb.ImportDataResponse{ Errors: errors, Success: success, diff --git a/internal/api/grpc/admin/language.go b/internal/api/grpc/admin/language.go index 73924a401e..dc3ca055b3 100644 --- a/internal/api/grpc/admin/language.go +++ b/internal/api/grpc/admin/language.go @@ -3,29 +3,23 @@ package admin import ( "context" - "golang.org/x/text/language" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object" - "github.com/zitadel/zitadel/internal/api/grpc/text" - caos_errors "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/i18n" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" ) func (s *Server) GetSupportedLanguages(ctx context.Context, req *admin_pb.GetSupportedLanguagesRequest) (*admin_pb.GetSupportedLanguagesResponse, error) { - langs, err := s.query.Languages(ctx) - if err != nil { - return nil, err - } - return &admin_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil + return &admin_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil } func (s *Server) SetDefaultLanguage(ctx context.Context, req *admin_pb.SetDefaultLanguageRequest) (*admin_pb.SetDefaultLanguageResponse, error) { - lang, err := language.Parse(req.Language) + lang, err := domain.ParseLanguage(req.Language) if err != nil { - return nil, caos_errors.ThrowInvalidArgument(err, "API-39nnf", "Errors.Language.Parse") + return nil, err } - details, err := s.command.SetDefaultLanguage(ctx, lang) + details, err := s.command.SetDefaultLanguage(ctx, lang[0]) if err != nil { return nil, err } diff --git a/internal/api/grpc/admin/language_converter.go b/internal/api/grpc/admin/language_converter.go new file mode 100644 index 0000000000..c36fc229ba --- /dev/null +++ b/internal/api/grpc/admin/language_converter.go @@ -0,0 +1,19 @@ +package admin + +import ( + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/pkg/grpc/admin" +) + +func selectLanguagesToCommand(languages *admin.SelectLanguages) (tags []language.Tag, err error) { + allowedLanguages := languages.GetList() + if allowedLanguages == nil && languages != nil { + allowedLanguages = make([]string, 0) + } + if allowedLanguages == nil { + return nil, nil + } + return domain.ParseLanguage(allowedLanguages...) +} diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index f3beb383e5..23fe94a78e 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -75,7 +75,6 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (* return nil, err } human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine - createdOrg, err := s.command.SetUpOrg(ctx, &command.OrgSetup{ Name: req.Org.Name, CustomDomain: req.Org.Domain, diff --git a/internal/api/grpc/admin/restrictions.go b/internal/api/grpc/admin/restrictions.go index 974f2e3555..ec4b6b7f18 100644 --- a/internal/api/grpc/admin/restrictions.go +++ b/internal/api/grpc/admin/restrictions.go @@ -5,11 +5,19 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/object" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/pkg/grpc/admin" ) func (s *Server) SetRestrictions(ctx context.Context, req *admin.SetRestrictionsRequest) (*admin.SetRestrictionsResponse, error) { - details, err := s.command.SetInstanceRestrictions(ctx, &command.SetRestrictions{DisallowPublicOrgRegistration: req.DisallowPublicOrgRegistration}) + lang, err := selectLanguagesToCommand(req.GetAllowedLanguages()) + if err != nil { + return nil, err + } + details, err := s.command.SetInstanceRestrictions(ctx, &command.SetRestrictions{ + DisallowPublicOrgRegistration: req.DisallowPublicOrgRegistration, + AllowedLanguages: lang, + }) if err != nil { return nil, err } @@ -26,5 +34,6 @@ func (s *Server) GetRestrictions(ctx context.Context, _ *admin.GetRestrictionsRe return &admin.GetRestrictionsResponse{ Details: object.ToViewDetailsPb(restrictions.Sequence, restrictions.CreationDate, restrictions.ChangeDate, restrictions.ResourceOwner), DisallowPublicOrgRegistration: restrictions.DisallowPublicOrgRegistration, + AllowedLanguages: domain.LanguagesToStrings(restrictions.AllowedLanguages), }, nil } diff --git a/internal/api/grpc/admin/restrictions_integration_test.go b/internal/api/grpc/admin/restrictions_integration_allow_public_org_registrations_test.go similarity index 54% rename from internal/api/grpc/admin/restrictions_integration_test.go rename to internal/api/grpc/admin/restrictions_integration_allow_public_org_registrations_test.go index 07a56409e9..b6e075ae39 100644 --- a/internal/api/grpc/admin/restrictions_integration_test.go +++ b/internal/api/grpc/admin/restrictions_integration_allow_public_org_registrations_test.go @@ -6,6 +6,7 @@ import ( "bytes" "context" "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" "io" "net/http" "net/http/cookiejar" @@ -29,19 +30,25 @@ func TestServer_Restrictions_DisallowPublicOrgRegistration(t *testing.T) { jar, err := cookiejar.New(nil) require.NoError(t, err) browserSession := &http.Client{Jar: jar} - // Default should be allowed - csrfToken := awaitAllowed(t, iamOwnerCtx, browserSession, regOrgUrl) - _, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(true)}) - require.NoError(t, err) - awaitDisallowed(t, iamOwnerCtx, browserSession, regOrgUrl, csrfToken) - _, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(false)}) - require.NoError(t, err) - awaitAllowed(t, iamOwnerCtx, browserSession, regOrgUrl) + var csrfToken string + t.Run("public org registration is allowed by default", func(*testing.T) { + csrfToken = awaitPubOrgRegAllowed(t, iamOwnerCtx, browserSession, regOrgUrl) + }) + t.Run("disallowing public org registration disables the endpoints", func(*testing.T) { + _, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(true)}) + require.NoError(t, err) + awaitPubOrgRegDisallowed(t, iamOwnerCtx, browserSession, regOrgUrl, csrfToken) + }) + t.Run("allowing public org registration again re-enables the endpoints", func(*testing.T) { + _, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(false)}) + require.NoError(t, err) + awaitPubOrgRegAllowed(t, iamOwnerCtx, browserSession, regOrgUrl) + }) } -// awaitAllowed doesn't accept a CSRF token, as we expected it to always produce a new one -func awaitAllowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL) string { - csrfToken := awaitGetResponse(t, ctx, client, parsedURL, http.StatusOK) +// awaitPubOrgRegAllowed doesn't accept a CSRF token, as we expected it to always produce a new one +func awaitPubOrgRegAllowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL) string { + csrfToken := awaitGetSSRGetResponse(t, ctx, client, parsedURL, http.StatusOK) awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusOK, csrfToken) restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{}) require.NoError(t, err) @@ -49,17 +56,17 @@ func awaitAllowed(t *testing.T, ctx context.Context, client *http.Client, parsed return csrfToken } -// awaitDisallowed accepts an old CSRF token, as we don't expect to get a CSRF token from the GET request anymore -func awaitDisallowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, reuseOldCSRFToken string) { - awaitGetResponse(t, ctx, client, parsedURL, http.StatusNotFound) +// awaitPubOrgRegDisallowed accepts an old CSRF token, as we don't expect to get a CSRF token from the GET request anymore +func awaitPubOrgRegDisallowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, reuseOldCSRFToken string) { + awaitGetSSRGetResponse(t, ctx, client, parsedURL, http.StatusNotFound) awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusConflict, reuseOldCSRFToken) restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{}) require.NoError(t, err) require.True(t, restrictions.DisallowPublicOrgRegistration) } -// awaitGetResponse cuts the CSRF token from the response body if it exists -func awaitGetResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int) string { +// awaitGetSSRGetResponse cuts the CSRF token from the response body if it exists +func awaitGetSSRGetResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int) string { var csrfToken []byte await(t, ctx, func() bool { resp, err := client.Get(parsedURL.String()) @@ -71,7 +78,7 @@ func awaitGetResponse(t *testing.T, ctx context.Context, client *http.Client, pa if hasCsrfToken { csrfToken, _, _ = bytes.Cut(after, []byte(`">`)) } - return resp.StatusCode == expectCode + return assert.Equal(NoopAssertionT, resp.StatusCode, expectCode) }) return string(csrfToken) } @@ -83,24 +90,6 @@ func awaitPostFormResponse(t *testing.T, ctx context.Context, client *http.Clien "gorilla.csrf.Token": {csrfToken}, }) require.NoError(t, err) - return resp.StatusCode == expectCode - + return assert.Equal(NoopAssertionT, resp.StatusCode, expectCode) }) } - -func await(t *testing.T, ctx context.Context, cb func() bool) { - deadline, ok := ctx.Deadline() - require.True(t, ok, "context must have deadline") - require.Eventuallyf( - t, - func() bool { - defer func() { - require.Nil(t, recover(), "panic in await callback") - }() - return cb() - }, - time.Until(deadline), - 100*time.Millisecond, - "awaiting successful callback failed", - ) -} diff --git a/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go b/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go new file mode 100644 index 0000000000..bfe9f0031c --- /dev/null +++ b/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go @@ -0,0 +1,258 @@ +//go:build integration + +package admin_test + +import ( + "context" + "encoding/json" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/admin" + "github.com/zitadel/zitadel/pkg/grpc/management" + "github.com/zitadel/zitadel/pkg/grpc/text" + "github.com/zitadel/zitadel/pkg/grpc/user" + "golang.org/x/text/language" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "io" + "net/http" + "testing" + "time" +) + +func TestServer_Restrictions_AllowedLanguages(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Hour) + defer cancel() + + var ( + defaultAndAllowedLanguage = language.German + supportedLanguagesStr = []string{language.German.String(), language.English.String(), language.Japanese.String()} + disallowedLanguage = language.Spanish + unsupportedLanguage1 = language.Afrikaans + unsupportedLanguage2 = language.Albanian + ) + + domain, _, iamOwnerCtx := Tester.UseIsolatedInstance(ctx, SystemCTX) + t.Run("assumed defaults are correct", func(tt *testing.T) { + tt.Run("languages are not restricted by default", func(ttt *testing.T) { + restrictions, err := Tester.Client.Admin.GetRestrictions(iamOwnerCtx, &admin.GetRestrictionsRequest{}) + require.NoError(ttt, err) + require.Len(ttt, restrictions.AllowedLanguages, 0) + }) + tt.Run("default language is English by default", func(ttt *testing.T) { + defaultLang, err := Tester.Client.Admin.GetDefaultLanguage(iamOwnerCtx, &admin.GetDefaultLanguageRequest{}) + require.NoError(ttt, err) + require.Equal(ttt, language.Make(defaultLang.Language), language.English) + }) + tt.Run("the discovery endpoint returns all supported languages", func(ttt *testing.T) { + checkDiscoveryEndpoint(ttt, domain, supportedLanguagesStr, nil) + }) + }) + t.Run("restricting the default language fails", func(tt *testing.T) { + _, err := Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{AllowedLanguages: &admin.SelectLanguages{List: []string{defaultAndAllowedLanguage.String()}}}) + expectStatus, ok := status.FromError(err) + require.True(tt, ok) + require.Equal(tt, codes.FailedPrecondition, expectStatus.Code()) + }) + t.Run("not defining any restrictions throws an error", func(tt *testing.T) { + _, err := Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{}) + expectStatus, ok := status.FromError(err) + require.True(tt, ok) + require.Equal(tt, codes.InvalidArgument, expectStatus.Code()) + }) + t.Run("setting the default language works", func(tt *testing.T) { + setAndAwaitDefaultLanguage(iamOwnerCtx, tt, defaultAndAllowedLanguage) + }) + t.Run("restricting allowed languages works", func(tt *testing.T) { + setAndAwaitAllowedLanguages(iamOwnerCtx, tt, []string{defaultAndAllowedLanguage.String()}) + }) + t.Run("setting the default language to a disallowed language fails", func(tt *testing.T) { + _, err := Tester.Client.Admin.SetDefaultLanguage(iamOwnerCtx, &admin.SetDefaultLanguageRequest{Language: disallowedLanguage.String()}) + expectStatus, ok := status.FromError(err) + require.True(tt, ok) + require.Equal(tt, codes.FailedPrecondition, expectStatus.Code()) + }) + t.Run("the list of supported languages includes the disallowed languages", func(tt *testing.T) { + supported, err := Tester.Client.Admin.GetSupportedLanguages(iamOwnerCtx, &admin.GetSupportedLanguagesRequest{}) + require.NoError(tt, err) + require.Condition(tt, contains(supported.GetLanguages(), supportedLanguagesStr)) + }) + t.Run("the disallowed language is not listed in the discovery endpoint", func(tt *testing.T) { + checkDiscoveryEndpoint(tt, domain, []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()}) + }) + t.Run("the login ui is rendered in the default language", func(tt *testing.T) { + checkLoginUILanguage(tt, domain, disallowedLanguage, defaultAndAllowedLanguage, "Allgemeine Geschäftsbedingungen und Datenschutz") + }) + t.Run("preferred languages are not restricted by the supported languages", func(tt *testing.T) { + var importedUser *management.ImportHumanUserResponse + tt.Run("import user", func(ttt *testing.T) { + var err error + importedUser, err = importUser(iamOwnerCtx, unsupportedLanguage1) + require.NoError(ttt, err) + }) + tt.Run("change user profile", func(ttt *testing.T) { + _, err := Tester.Client.Mgmt.UpdateHumanProfile(iamOwnerCtx, &management.UpdateHumanProfileRequest{ + UserId: importedUser.GetUserId(), + FirstName: "hodor", + LastName: "hodor", + NickName: integration.RandString(5), + DisplayName: "hodor", + PreferredLanguage: unsupportedLanguage2.String(), + Gender: user.Gender_GENDER_MALE, + }) + require.NoError(ttt, err) + }) + }) + t.Run("custom texts are only restricted by the supported languages", func(tt *testing.T) { + _, err := Tester.Client.Admin.SetCustomLoginText(iamOwnerCtx, &admin.SetCustomLoginTextsRequest{ + Language: disallowedLanguage.String(), + EmailVerificationText: &text.EmailVerificationScreenText{ + Description: "hodor", + }, + }) + assert.NoError(tt, err) + _, err = Tester.Client.Mgmt.SetCustomLoginText(iamOwnerCtx, &management.SetCustomLoginTextsRequest{ + Language: disallowedLanguage.String(), + EmailVerificationText: &text.EmailVerificationScreenText{ + Description: "hodor", + }, + }) + assert.NoError(tt, err) + _, err = Tester.Client.Mgmt.SetCustomInitMessageText(iamOwnerCtx, &management.SetCustomInitMessageTextRequest{ + Language: disallowedLanguage.String(), + Text: "hodor", + }) + assert.NoError(tt, err) + _, err = Tester.Client.Admin.SetDefaultInitMessageText(iamOwnerCtx, &admin.SetDefaultInitMessageTextRequest{ + Language: disallowedLanguage.String(), + Text: "hodor", + }) + assert.NoError(tt, err) + }) + t.Run("allowing all languages works", func(tt *testing.T) { + tt.Run("restricting allowed languages works", func(ttt *testing.T) { + setAndAwaitAllowedLanguages(iamOwnerCtx, ttt, make([]string, 0)) + }) + }) + + t.Run("allowing the language makes it usable again", func(tt *testing.T) { + tt.Run("the disallowed language is listed in the discovery endpoint again", func(ttt *testing.T) { + checkDiscoveryEndpoint(ttt, domain, []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()}) + }) + tt.Run("the login ui is rendered in the allowed language", func(ttt *testing.T) { + checkLoginUILanguage(ttt, domain, disallowedLanguage, disallowedLanguage, "Términos y condiciones") + }) + }) +} + +func setAndAwaitAllowedLanguages(ctx context.Context, t *testing.T, selectLanguages []string) { + _, err := Tester.Client.Admin.SetRestrictions(ctx, &admin.SetRestrictionsRequest{AllowedLanguages: &admin.SelectLanguages{List: selectLanguages}}) + require.NoError(t, err) + awaitCtx, awaitCancel := context.WithTimeout(ctx, 10*time.Second) + defer awaitCancel() + await(t, awaitCtx, func() bool { + restrictions, getErr := Tester.Client.Admin.GetRestrictions(awaitCtx, &admin.GetRestrictionsRequest{}) + expectLanguages := selectLanguages + if len(selectLanguages) == 0 { + expectLanguages = nil + } + return assert.NoError(NoopAssertionT, getErr) && + assert.Equal(NoopAssertionT, expectLanguages, restrictions.GetAllowedLanguages()) + }) +} + +func setAndAwaitDefaultLanguage(ctx context.Context, t *testing.T, lang language.Tag) { + _, err := Tester.Client.Admin.SetDefaultLanguage(ctx, &admin.SetDefaultLanguageRequest{Language: lang.String()}) + require.NoError(t, err) + awaitCtx, awaitCancel := context.WithTimeout(ctx, 10*time.Second) + defer awaitCancel() + await(t, awaitCtx, func() bool { + defaultLang, getErr := Tester.Client.Admin.GetDefaultLanguage(awaitCtx, &admin.GetDefaultLanguageRequest{}) + return assert.NoError(NoopAssertionT, getErr) && + assert.Equal(NoopAssertionT, lang.String(), defaultLang.GetLanguage()) + }) +} + +func importUser(ctx context.Context, preferredLanguage language.Tag) (*management.ImportHumanUserResponse, error) { + random := integration.RandString(5) + return Tester.Client.Mgmt.ImportHumanUser(ctx, &management.ImportHumanUserRequest{ + UserName: "integration-test-user_" + random, + Profile: &management.ImportHumanUserRequest_Profile{ + FirstName: "hodor", + LastName: "hodor", + NickName: "hodor", + PreferredLanguage: preferredLanguage.String(), + }, + Email: &management.ImportHumanUserRequest_Email{ + Email: random + "@hodor.hodor", + IsEmailVerified: true, + }, + PasswordChangeRequired: false, + Password: "Password1!", + }) +} + +func checkDiscoveryEndpoint(t *testing.T, domain string, containsUILocales, notContainsUILocales []string) { + resp, err := http.Get("http://" + domain + ":8080/.well-known/openid-configuration") + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + defer func() { + require.NoError(t, resp.Body.Close()) + }() + require.NoError(t, err) + doc := struct { + UILocalesSupported []string `json:"ui_locales_supported"` + }{} + require.NoError(t, json.Unmarshal(body, &doc)) + if containsUILocales != nil { + assert.Condition(NoopAssertionT, contains(doc.UILocalesSupported, containsUILocales)) + } + if notContainsUILocales != nil { + assert.Condition(NoopAssertionT, not(contains(doc.UILocalesSupported, notContainsUILocales))) + } +} + +func checkLoginUILanguage(t *testing.T, domain string, acceptLanguage language.Tag, expectLang language.Tag, containsText string) { + req, err := http.NewRequest(http.MethodGet, "http://"+domain+":8080/ui/login/register", nil) + req.Header.Set("Accept-Language", acceptLanguage.String()) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + defer func() { + require.NoError(t, resp.Body.Close()) + }() + require.NoError(t, err) + assert.Containsf(t, string(body), containsText, "login ui language is in "+expectLang.String()) +} + +// We would love to use assert.Contains here, but it doesn't work with slices of strings +func contains(container []string, subset []string) assert.Comparison { + return func() bool { + if subset == nil { + return true + } + for _, str := range subset { + var found bool + for _, containerStr := range container { + if str == containerStr { + found = true + break + } + } + if !found { + return false + } + } + return true + } +} + +func not(cmp assert.Comparison) assert.Comparison { + return func() bool { + return !cmp() + } +} diff --git a/internal/api/grpc/admin/server_integration_test.go b/internal/api/grpc/admin/server_integration_test.go index 64a761dd7f..de1b24b4b0 100644 --- a/internal/api/grpc/admin/server_integration_test.go +++ b/internal/api/grpc/admin/server_integration_test.go @@ -8,12 +8,17 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/internal/integration" ) var ( AdminCTX, SystemCTX context.Context Tester *integration.Tester + // NoopAssertionT is useful in combination with assert.Eventuallyf to use testify assertions in a callback + NoopAssertionT = new(noopAssertionT) ) func TestMain(m *testing.M) { @@ -30,3 +35,29 @@ func TestMain(m *testing.M) { return m.Run() }()) } + +func await(t *testing.T, ctx context.Context, cb func() bool) { + deadline, ok := ctx.Deadline() + require.True(t, ok, "context must have deadline") + assert.Eventuallyf( + t, + func() bool { + defer func() { + // Panics are not recovered and don't mark the test as failed, so we need to do that ourselves + require.Nil(t, recover(), "panic in await callback") + }() + return cb() + }, + time.Until(deadline), + 100*time.Millisecond, + "awaiting successful callback failed", + ) +} + +var _ assert.TestingT = (*noopAssertionT)(nil) + +type noopAssertionT struct{} + +func (*noopAssertionT) FailNow() {} + +func (*noopAssertionT) Errorf(string, ...interface{}) {} diff --git a/internal/api/grpc/auth/language.go b/internal/api/grpc/auth/language.go index 91f78cd150..9f1d65bbb7 100644 --- a/internal/api/grpc/auth/language.go +++ b/internal/api/grpc/auth/language.go @@ -2,15 +2,12 @@ package auth import ( "context" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/api/grpc/text" auth_pb "github.com/zitadel/zitadel/pkg/grpc/auth" ) -func (s *Server) GetSupportedLanguages(ctx context.Context, req *auth_pb.GetSupportedLanguagesRequest) (*auth_pb.GetSupportedLanguagesResponse, error) { - langs, err := s.query.Languages(ctx) - if err != nil { - return nil, err - } - return &auth_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil +func (s *Server) GetSupportedLanguages(context.Context, *auth_pb.GetSupportedLanguagesRequest) (*auth_pb.GetSupportedLanguagesResponse, error) { + return &auth_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil } diff --git a/internal/api/grpc/management/language.go b/internal/api/grpc/management/language.go index cab36f63e6..4b13ba5c4c 100644 --- a/internal/api/grpc/management/language.go +++ b/internal/api/grpc/management/language.go @@ -2,15 +2,12 @@ package management import ( "context" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/api/grpc/text" mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management" ) -func (s *Server) GetSupportedLanguages(ctx context.Context, req *mgmt_pb.GetSupportedLanguagesRequest) (*mgmt_pb.GetSupportedLanguagesResponse, error) { - langs, err := s.query.Languages(ctx) - if err != nil { - return nil, err - } - return &mgmt_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil +func (s *Server) GetSupportedLanguages(context.Context, *mgmt_pb.GetSupportedLanguagesRequest) (*mgmt_pb.GetSupportedLanguagesResponse, error) { + return &mgmt_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 754b02755a..cb2afdcf8e 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -220,8 +220,7 @@ func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRe func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) { human := AddHumanUserRequestToAddHuman(req) - err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true) - if err != nil { + if err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true); err != nil { return nil, err } return &mgmt_pb.AddHumanUserResponse{ diff --git a/internal/api/grpc/management/user_integration_test.go b/internal/api/grpc/management/user_integration_test.go index 6b5fe77d1a..5d612b158f 100644 --- a/internal/api/grpc/management/user_integration_test.go +++ b/internal/api/grpc/management/user_integration_test.go @@ -55,14 +55,14 @@ func TestImport_and_Get(t *testing.T) { // create unique names. lastName := strconv.FormatInt(time.Now().Unix(), 10) userName := strings.Join([]string{firstName, lastName}, "_") - email := strings.Join([]string{userName, "zitadel.com"}, "@") + email := strings.Join([]string{userName, "example.com"}, "@") res, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{ UserName: userName, Profile: &management.ImportHumanUserRequest_Profile{ FirstName: firstName, LastName: lastName, - PreferredLanguage: language.Afrikaans.String(), + PreferredLanguage: language.Japanese.String(), Gender: user.Gender_GENDER_DIVERSE, }, Email: &management.ImportHumanUserRequest_Email{ @@ -82,3 +82,21 @@ func TestImport_and_Get(t *testing.T) { }) } } + +func TestImport_UnparsablePreferredLanguage(t *testing.T) { + random := integration.RandString(5) + _, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{ + UserName: random, + Profile: &management.ImportHumanUserRequest_Profile{ + FirstName: random, + LastName: random, + PreferredLanguage: "not valid", + Gender: user.Gender_GENDER_DIVERSE, + }, + Email: &management.ImportHumanUserRequest_Email{ + Email: random + "@example.com", + IsEmailVerified: true, + }, + }) + require.NoError(t, err) +} diff --git a/internal/api/grpc/server/middleware/instance_interceptor.go b/internal/api/grpc/server/middleware/instance_interceptor.go index 77302fdf77..68389a0c4f 100644 --- a/internal/api/grpc/server/middleware/instance_interceptor.go +++ b/internal/api/grpc/server/middleware/instance_interceptor.go @@ -24,7 +24,7 @@ const ( ) func InstanceInterceptor(verifier authz.InstanceVerifier, headerName string, explicitInstanceIdServices ...string) grpc.UnaryServerInterceptor { - translator, err := newZitadelTranslator(language.English) + translator, err := i18n.NewZitadelTranslator(language.English) logging.OnError(err).Panic("unable to get translator") return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { return setInstance(ctx, req, info, handler, verifier, headerName, translator, explicitInstanceIdServices...) diff --git a/internal/api/grpc/server/middleware/translation_interceptor.go b/internal/api/grpc/server/middleware/translation_interceptor.go index 996e80acc6..08e1540531 100644 --- a/internal/api/grpc/server/middleware/translation_interceptor.go +++ b/internal/api/grpc/server/middleware/translation_interceptor.go @@ -7,6 +7,7 @@ import ( "google.golang.org/grpc" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/i18n" _ "github.com/zitadel/zitadel/internal/statik" "github.com/zitadel/zitadel/internal/telemetry/tracing" ) @@ -18,17 +19,15 @@ func TranslationHandler() func(ctx context.Context, req interface{}, info *grpc. defer func() { span.EndWithError(err) }() if loc, ok := resp.(localizers); ok && resp != nil { - translator, translatorError := newZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage()) + translator, translatorError := getTranslator(ctx) if translatorError != nil { - logging.New().WithError(translatorError).Error("could not load translator") return resp, err } translateFields(ctx, loc, translator) } if err != nil { - translator, translatorError := newZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage()) + translator, translatorError := getTranslator(ctx) if translatorError != nil { - logging.New().WithError(translatorError).Error("could not load translator") return resp, err } err = translateError(ctx, err, translator) @@ -36,3 +35,11 @@ func TranslationHandler() func(ctx context.Context, req interface{}, info *grpc. return resp, err } } + +func getTranslator(ctx context.Context) (*i18n.Translator, error) { + translator, err := i18n.NewZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage()) + if err != nil { + logging.New().WithError(err).Error("could not load translator") + } + return translator, err +} diff --git a/internal/api/grpc/server/middleware/translator.go b/internal/api/grpc/server/middleware/translator.go index f42741db0b..fa453c682f 100644 --- a/internal/api/grpc/server/middleware/translator.go +++ b/internal/api/grpc/server/middleware/translator.go @@ -4,10 +4,6 @@ import ( "context" "errors" - "github.com/rakyll/statik/fs" - "github.com/zitadel/logging" - "golang.org/x/text/language" - caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/i18n" ) @@ -39,14 +35,3 @@ func translateError(ctx context.Context, err error, translator *i18n.Translator) } return err } - -func newZitadelTranslator(defaultLanguage language.Tag) (*i18n.Translator, error) { - return translatorFromNamespace("zitadel", defaultLanguage) -} - -func translatorFromNamespace(namespace string, defaultLanguage language.Tag) (*i18n.Translator, error) { - dir, err := fs.NewWithNamespace(namespace) - logging.WithFields("namespace", namespace).OnError(err).Panic("unable to get namespace") - - return i18n.NewTranslator(dir, defaultLanguage, "") -} diff --git a/internal/api/grpc/settings/v2/settings.go b/internal/api/grpc/settings/v2/settings.go index ff458fa98d..5e09f8e89a 100644 --- a/internal/api/grpc/settings/v2/settings.go +++ b/internal/api/grpc/settings/v2/settings.go @@ -7,7 +7,8 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" - "github.com/zitadel/zitadel/internal/api/grpc/text" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/query" object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" @@ -116,13 +117,9 @@ func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *settings.G } func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralSettingsRequest) (*settings.GetGeneralSettingsResponse, error) { - langs, err := s.query.Languages(ctx) - if err != nil { - return nil, err - } instance := authz.GetInstance(ctx) return &settings.GetGeneralSettingsResponse{ - SupportedLanguages: text.LanguageTagsToStrings(langs), + SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()), DefaultOrgId: instance.DefaultOrganisationID(), DefaultLanguage: instance.DefaultLanguage().String(), }, nil diff --git a/internal/api/grpc/text/language.go b/internal/api/grpc/text/language.go deleted file mode 100644 index 9ae5b1ed1a..0000000000 --- a/internal/api/grpc/text/language.go +++ /dev/null @@ -1,13 +0,0 @@ -package text - -import ( - "golang.org/x/text/language" -) - -func LanguageTagsToStrings(langs []language.Tag) []string { - result := make([]string, len(langs)) - for i, lang := range langs { - result[i] = lang.String() - } - return result -} diff --git a/internal/api/grpc/user/converter.go b/internal/api/grpc/user/converter.go index 7b00d2f4cc..eca346bad8 100644 --- a/internal/api/grpc/user/converter.go +++ b/internal/api/grpc/user/converter.go @@ -72,7 +72,7 @@ func MachineToPb(view *query.Machine) *user_pb.Machine { return &user_pb.Machine{ Name: view.Name, Description: view.Description, - HasSecret: view.HasSecret, + HasSecret: view.Secret != nil, AccessTokenType: AccessTokenTypeToPb(view.AccessTokenType), } } diff --git a/internal/api/grpc/user/v2/passkey_integration_test.go b/internal/api/grpc/user/v2/passkey_integration_test.go index 139bef3e68..383eeb0c82 100644 --- a/internal/api/grpc/user/v2/passkey_integration_test.go +++ b/internal/api/grpc/user/v2/passkey_integration_test.go @@ -24,6 +24,10 @@ func TestServer_RegisterPasskey(t *testing.T) { }) require.NoError(t, err) + // We also need a user session + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + type args struct { ctx context.Context req *user.RegisterPasskeyRequest @@ -95,14 +99,12 @@ func TestServer_RegisterPasskey(t *testing.T) { }, wantErr: true, }, - /* TODO: after we are able to obtain a Bearer token for a human user - https://github.com/zitadel/zitadel/issues/6022 { - name: "human user", + name: "user setting its own passkey", args: args{ - ctx: CTX, + ctx: Tester.WithAuthorizationToken(CTX, sessionToken), req: &user.RegisterPasskeyRequest{ - UserId: humanUserID, + UserId: userID, }, }, want: &user.RegisterPasskeyResponse{ @@ -111,7 +113,6 @@ func TestServer_RegisterPasskey(t *testing.T) { }, }, }, - */ } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/grpc/user/v2/totp_integration_test.go b/internal/api/grpc/user/v2/totp_integration_test.go index 18a5e43bd1..4fef7bbf9b 100644 --- a/internal/api/grpc/user/v2/totp_integration_test.go +++ b/internal/api/grpc/user/v2/totp_integration_test.go @@ -5,16 +5,22 @@ package user_test import ( "context" "testing" + "time" + "github.com/pquerna/otp/totp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) func TestServer_RegisterTOTP(t *testing.T) { - // userID := Tester.CreateHumanUser(CTX).GetUserId() + userID := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + ctx := Tester.WithAuthorizationToken(CTX, sessionToken) type args struct { ctx context.Context @@ -29,7 +35,7 @@ func TestServer_RegisterTOTP(t *testing.T) { { name: "missing user id", args: args{ - ctx: CTX, + ctx: ctx, req: &user.RegisterTOTPRequest{}, }, wantErr: true, @@ -37,19 +43,17 @@ func TestServer_RegisterTOTP(t *testing.T) { { name: "user mismatch", args: args{ - ctx: CTX, + ctx: ctx, req: &user.RegisterTOTPRequest{ UserId: "wrong", }, }, wantErr: true, }, - /* TODO: after we are able to obtain a Bearer token for a human user - https://github.com/zitadel/zitadel/issues/6022 { - name: "human user", + name: "success", args: args{ - ctx: CTX, + ctx: ctx, req: &user.RegisterTOTPRequest{ UserId: userID, }, @@ -60,7 +64,6 @@ func TestServer_RegisterTOTP(t *testing.T) { }, }, }, - */ } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -80,15 +83,16 @@ func TestServer_RegisterTOTP(t *testing.T) { func TestServer_VerifyTOTPRegistration(t *testing.T) { userID := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + ctx := Tester.WithAuthorizationToken(CTX, sessionToken) - /* TODO: after we are able to obtain a Bearer token for a human user - reg, err := Client.RegisterTOTP(CTX, &user.RegisterTOTPRequest{ + reg, err := Client.RegisterTOTP(ctx, &user.RegisterTOTPRequest{ UserId: userID, }) require.NoError(t, err) code, err := totp.GenerateCode(reg.Secret, time.Now()) require.NoError(t, err) - */ type args struct { ctx context.Context @@ -103,7 +107,7 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) { { name: "user mismatch", args: args{ - ctx: CTX, + ctx: ctx, req: &user.VerifyTOTPRegistrationRequest{ UserId: "wrong", }, @@ -113,7 +117,7 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) { { name: "wrong code", args: args{ - ctx: CTX, + ctx: ctx, req: &user.VerifyTOTPRegistrationRequest{ UserId: userID, Code: "123", @@ -121,12 +125,10 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) { }, wantErr: true, }, - /* TODO: after we are able to obtain a Bearer token for a human user - https://github.com/zitadel/zitadel/issues/6022 { name: "success", args: args{ - ctx: CTX, + ctx: ctx, req: &user.VerifyTOTPRegistrationRequest{ UserId: userID, Code: code, @@ -138,7 +140,6 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) { }, }, }, - */ } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/grpc/user/v2/u2f_integration_test.go b/internal/api/grpc/user/v2/u2f_integration_test.go index febe0ddfb1..77653f8789 100644 --- a/internal/api/grpc/user/v2/u2f_integration_test.go +++ b/internal/api/grpc/user/v2/u2f_integration_test.go @@ -11,12 +11,17 @@ import ( "google.golang.org/protobuf/types/known/structpb" "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) func TestServer_RegisterU2F(t *testing.T) { userID := Tester.CreateHumanUser(CTX).GetUserId() + // We also need a user session + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + type args struct { ctx context.Context req *user.RegisterU2FRequest @@ -45,12 +50,10 @@ func TestServer_RegisterU2F(t *testing.T) { }, wantErr: true, }, - /* TODO: after we are able to obtain a Bearer token for a human user - https://github.com/zitadel/zitadel/issues/6022 { - name: "human user", + name: "user setting its own passkey", args: args{ - ctx: CTX, + ctx: Tester.WithAuthorizationToken(CTX, sessionToken), req: &user.RegisterU2FRequest{ UserId: userID, }, @@ -61,7 +64,6 @@ func TestServer_RegisterU2F(t *testing.T) { }, }, }, - */ } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -85,8 +87,11 @@ func TestServer_RegisterU2F(t *testing.T) { func TestServer_VerifyU2FRegistration(t *testing.T) { userID := Tester.CreateHumanUser(CTX).GetUserId() - /* TODO after we are able to obtain a Bearer token for a human user - pkr, err := Client.RegisterU2F(CTX, &user.RegisterU2FRequest{ + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + ctx := Tester.WithAuthorizationToken(CTX, sessionToken) + + pkr, err := Client.RegisterU2F(ctx, &user.RegisterU2FRequest{ UserId: userID, }) require.NoError(t, err) @@ -94,7 +99,6 @@ func TestServer_VerifyU2FRegistration(t *testing.T) { attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) require.NoError(t, err) - */ type args struct { ctx context.Context @@ -109,7 +113,7 @@ func TestServer_VerifyU2FRegistration(t *testing.T) { { name: "missing user id", args: args{ - ctx: CTX, + ctx: ctx, req: &user.VerifyU2FRegistrationRequest{ U2FId: "123", TokenName: "nice name", @@ -117,11 +121,10 @@ func TestServer_VerifyU2FRegistration(t *testing.T) { }, wantErr: true, }, - /* TODO after we are able to obtain a Bearer token for a human user { name: "success", args: args{ - ctx: CTX, + ctx: ctx, req: &user.VerifyU2FRegistrationRequest{ UserId: userID, U2FId: pkr.GetU2FId(), @@ -135,11 +138,10 @@ func TestServer_VerifyU2FRegistration(t *testing.T) { }, }, }, - */ { name: "wrong credential", args: args{ - ctx: CTX, + ctx: ctx, req: &user.VerifyU2FRegistrationRequest{ UserId: userID, U2FId: "123", diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index 4301bc097d..d7c26e9031 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -28,8 +28,7 @@ func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest return nil, err } orgID := authz.GetCtxData(ctx).OrgID - err = s.command.AddHuman(ctx, orgID, human, false) - if err != nil { + if err = s.command.AddHuman(ctx, orgID, human, false); err != nil { return nil, err } return &user.AddHumanUserResponse{ diff --git a/internal/api/grpc/user/v2/user_integration_test.go b/internal/api/grpc/user/v2/user_integration_test.go index 55f0e76a17..fc00e165f6 100644 --- a/internal/api/grpc/user/v2/user_integration_test.go +++ b/internal/api/grpc/user/v2/user_integration_test.go @@ -677,7 +677,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { parametersEqual: map[string]string{ "client_id": "clientID", "prompt": "select_account", - "redirect_uri": "http://localhost:8080/idps/callback", + "redirect_uri": "http://" + Tester.Config.ExternalDomain + ":8080/idps/callback", "response_type": "code", "scope": "openid profile email", }, @@ -704,7 +704,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { ChangeDate: timestamppb.Now(), ResourceOwner: Tester.Organisation.ID, }, - url: "http://localhost:8000/sso", + url: "http://" + Tester.Config.ExternalDomain + ":8000/sso", parametersExisting: []string{"RelayState", "SAMLRequest"}, }, wantErr: false, @@ -728,7 +728,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { ChangeDate: timestamppb.Now(), ResourceOwner: Tester.Organisation.ID, }, - url: "http://localhost:8000/sso", + url: "http://" + Tester.Config.ExternalDomain + ":8000/sso", parametersExisting: []string{"RelayState", "SAMLRequest"}, }, wantErr: false, diff --git a/internal/api/http/middleware/instance_interceptor.go b/internal/api/http/middleware/instance_interceptor.go index 276037301d..e04ca9f7ab 100644 --- a/internal/api/http/middleware/instance_interceptor.go +++ b/internal/api/http/middleware/instance_interceptor.go @@ -8,7 +8,6 @@ import ( "net/url" "strings" - "github.com/rakyll/statik/fs" "github.com/zitadel/logging" "golang.org/x/text/language" @@ -112,7 +111,7 @@ func hostFromOrigin(ctx context.Context) (host string, err error) { if err != nil { return "", err } - host = u.Hostname() + host = u.Host if host == "" { err = errors.New("empty host") } @@ -120,10 +119,7 @@ func hostFromOrigin(ctx context.Context) (host string, err error) { } func newZitadelTranslator() *i18n.Translator { - dir, err := fs.NewWithNamespace("zitadel") - logging.WithFields("namespace", "zitadel").OnError(err).Panic("unable to get namespace") - - translator, err := i18n.NewTranslator(dir, language.English, "") + translator, err := i18n.NewZitadelTranslator(language.English) logging.OnError(err).Panic("unable to get translator") return translator } diff --git a/internal/api/http/middleware/instance_interceptor_test.go b/internal/api/http/middleware/instance_interceptor_test.go index e61fade72d..5273d2fd58 100644 --- a/internal/api/http/middleware/instance_interceptor_test.go +++ b/internal/api/http/middleware/instance_interceptor_test.go @@ -221,7 +221,7 @@ func Test_setInstance(t *testing.T) { r.Header.Set("host", "fromrequest") return r.WithContext(zitadel_http.WithComposedOrigin(r.Context(), "https://fromorigin:9999")) }(), - verifier: &mockInstanceVerifier{"fromorigin"}, + verifier: &mockInstanceVerifier{"fromorigin:9999"}, headerName: "host", }, res{ diff --git a/internal/api/http/middleware/middleware_test.go b/internal/api/http/middleware/middleware_test.go new file mode 100644 index 0000000000..4d7cb6636d --- /dev/null +++ b/internal/api/http/middleware/middleware_test.go @@ -0,0 +1,18 @@ +package middleware + +import ( + "testing" + + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/i18n" +) + +var ( + SupportedLanguages = []language.Tag{language.English, language.German} +) + +func TestMain(m *testing.M) { + i18n.SupportLanguages(SupportedLanguages...) + m.Run() +} diff --git a/internal/api/oidc/access_token.go b/internal/api/oidc/access_token.go index d01badda98..35ab1edea6 100644 --- a/internal/api/oidc/access_token.go +++ b/internal/api/oidc/access_token.go @@ -10,7 +10,7 @@ import ( "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/command" - errz "github.com/zitadel/zitadel/internal/errors" + zerrors "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/user/model" ) @@ -55,7 +55,7 @@ func (s *Server) verifyAccessToken(ctx context.Context, tkn string) (*accessToke token, err := s.repo.TokenByIDs(ctx, subject, tokenID) if err != nil { - return nil, errz.ThrowPermissionDenied(err, "OIDC-Dsfb2", "token is not valid or has expired") + return nil, zerrors.ThrowPermissionDenied(err, "OIDC-Dsfb2", "token is not valid or has expired") } return accessTokenV1(tokenID, subject, token), nil } @@ -91,7 +91,7 @@ func (s *Server) assertClientScopesForPAT(ctx context.Context, token *accessToke token.audience = append(token.audience, clientID) projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(projectID) if err != nil { - return errz.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") + return zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") } roles, err := s.query.SearchProjectRoles(ctx, s.features.TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) if err != nil { diff --git a/internal/api/oidc/auth_request_integration_test.go b/internal/api/oidc/auth_request_integration_test.go index 83762e338a..9f8e77688d 100644 --- a/internal/api/oidc/auth_request_integration_test.go +++ b/internal/api/oidc/auth_request_integration_test.go @@ -17,6 +17,7 @@ import ( http_utils "github.com/zitadel/zitadel/internal/api/http" oidc_api "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/integration" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" ) @@ -500,8 +501,7 @@ func exchangeTokens(t testing.TB, clientID, code string) (*oidc.Tokens[*oidc.IDT provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) - codeVerifier := "codeVerifier" - return rp.CodeExchange[*oidc.IDTokenClaims](context.Background(), code, provider, rp.WithCodeVerifier(codeVerifier)) + return rp.CodeExchange[*oidc.IDTokenClaims](context.Background(), code, provider, rp.WithCodeVerifier(integration.CodeVerifier)) } func refreshTokens(t testing.TB, clientID, refreshToken string) (*oidc.Tokens[*oidc.IDTokenClaims], error) { diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 7035f3db56..6514583564 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -43,32 +43,14 @@ const ( func (o *OPStorage) GetClientByClientID(ctx context.Context, id string) (_ op.Client, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - client, err := o.query.AppByOIDCClientID(ctx, id) + client, err := o.query.GetOIDCClientByID(ctx, id, false) if err != nil { return nil, err } if client.State != domain.AppStateActive { return nil, errors.ThrowPreconditionFailed(nil, "OIDC-sdaGg", "client is not active") } - projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(client.ProjectID) - if err != nil { - return nil, errors.ThrowInternal(err, "OIDC-mPxqP", "Errors.Internal") - } - projectRoles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) - if err != nil { - return nil, err - } - allowedScopes := make([]string, len(projectRoles.ProjectRoles)) - for i, role := range projectRoles.ProjectRoles { - allowedScopes[i] = ScopeProjectRolePrefix + role.Key - } - - accessTokenLifetime, idTokenLifetime, _, _, err := o.getOIDCSettings(ctx) - if err != nil { - return nil, err - } - - return ClientFromBusiness(client, o.defaultLoginURL, o.defaultLoginURLV2, accessTokenLifetime, idTokenLifetime, allowedScopes) + return ClientFromBusiness(client, o.defaultLoginURL, o.defaultLoginURLV2), nil } func (o *OPStorage) GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (_ *jose.JSONWebKey, err error) { @@ -235,22 +217,10 @@ func (o *OPStorage) ClientCredentialsTokenRequest(ctx context.Context, clientID }, nil } -func (o *OPStorage) ClientCredentials(ctx context.Context, clientID, clientSecret string) (op.Client, error) { - loginname, err := query.NewUserLoginNamesSearchQuery(clientID) - if err != nil { - return nil, err - } - user, err := o.query.GetUser(ctx, false, loginname) - if err != nil { - return nil, err - } - if _, err := o.command.VerifyMachineSecret(ctx, user.ID, user.ResourceOwner, clientSecret); err != nil { - return nil, err - } - return &clientCredentialsClient{ - id: clientID, - tokenType: accessTokenTypeToOIDC(user.Machine.AccessTokenType), - }, nil +// ClientCredentials method is kept to keep the storage interface implemented. +// However, it should never be called as the VerifyClient method on the Server is overridden. +func (o *OPStorage) ClientCredentials(context.Context, string, string) (op.Client, error) { + return nil, errors.ThrowInternal(nil, "OIDC-Su8So", "Errors.Internal") } // isOriginAllowed checks whether a call by the client to the endpoint is allowed from the provided origin @@ -934,3 +904,67 @@ func userinfoClaims(userInfo *oidc.UserInfo) func(c *actions.FieldConfig) interf return c.Runtime.ToValue(claims) } } + +func (s *Server) VerifyClient(ctx context.Context, r *op.Request[op.ClientCredentials]) (_ op.Client, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if oidc.GrantType(r.Form.Get("grant_type")) == oidc.GrantTypeClientCredentials { + return s.clientCredentialsAuth(ctx, r.Data.ClientID, r.Data.ClientSecret) + } + + clientID, assertion, err := clientIDFromCredentials(r.Data) + if err != nil { + return nil, err + } + client, err := s.query.GetOIDCClientByID(ctx, clientID, assertion) + if errors.IsNotFound(err) { + return nil, oidc.ErrInvalidClient().WithParent(err).WithDescription("client not found") + } + if err != nil { + return nil, err // defaults to server error + } + if client.State != domain.AppStateActive { + return nil, oidc.ErrInvalidClient().WithDescription("client is not active") + } + + switch client.AuthMethodType { + case domain.OIDCAuthMethodTypeBasic, domain.OIDCAuthMethodTypePost: + err = s.verifyClientSecret(ctx, client, r.Data.ClientSecret) + case domain.OIDCAuthMethodTypePrivateKeyJWT: + err = s.verifyClientAssertion(ctx, client, r.Data.ClientAssertion) + case domain.OIDCAuthMethodTypeNone: + } + if err != nil { + return nil, err + } + + return ClientFromBusiness(client, s.defaultLoginURL, s.defaultLoginURLV2), nil +} + +func (s *Server) verifyClientAssertion(ctx context.Context, client *query.OIDCClient, assertion string) (err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if assertion == "" { + return oidc.ErrInvalidClient().WithDescription("empty client assertion") + } + verifier := op.NewJWTProfileVerifierKeySet(keySetMap(client.PublicKeys), op.IssuerFromContext(ctx), time.Hour, client.ClockSkew) + if _, err := op.VerifyJWTAssertion(ctx, assertion, verifier); err != nil { + return oidc.ErrInvalidClient().WithParent(err).WithDescription("invalid assertion") + } + return nil +} + +func (s *Server) verifyClientSecret(ctx context.Context, client *query.OIDCClient, secret string) (err error) { + _, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if secret == "" { + return oidc.ErrInvalidClient().WithDescription("empty client secret") + } + if err = crypto.CompareHash(client.ClientSecret, []byte(secret), s.hashAlg); err != nil { + return oidc.ErrInvalidClient().WithParent(err).WithDescription("invalid secret") + } + return nil +} diff --git a/internal/api/oidc/client_converter.go b/internal/api/oidc/client_converter.go index ec208db27c..1a6c5ed7c1 100644 --- a/internal/api/oidc/client_converter.go +++ b/internal/api/oidc/client_converter.go @@ -1,6 +1,7 @@ package oidc import ( + "slices" "strings" "time" @@ -9,43 +10,40 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query" ) type Client struct { - app *query.App - defaultLoginURL string - defaultLoginURLV2 string - defaultAccessTokenLifetime time.Duration - defaultIdTokenLifetime time.Duration - allowedScopes []string + client *query.OIDCClient + defaultLoginURL string + defaultLoginURLV2 string + allowedScopes []string } -func ClientFromBusiness(app *query.App, defaultLoginURL, defaultLoginURLV2 string, defaultAccessTokenLifetime, defaultIdTokenLifetime time.Duration, allowedScopes []string) (op.Client, error) { - if app.OIDCConfig == nil { - return nil, errors.ThrowInvalidArgument(nil, "OIDC-d5bhD", "client is not a proper oidc application") +func ClientFromBusiness(client *query.OIDCClient, defaultLoginURL, defaultLoginURLV2 string) op.Client { + allowedScopes := make([]string, len(client.ProjectRoleKeys)) + for i, roleKey := range client.ProjectRoleKeys { + allowedScopes[i] = ScopeProjectRolePrefix + roleKey } + return &Client{ - app: app, - defaultLoginURL: defaultLoginURL, - defaultLoginURLV2: defaultLoginURLV2, - defaultAccessTokenLifetime: defaultAccessTokenLifetime, - defaultIdTokenLifetime: defaultIdTokenLifetime, - allowedScopes: allowedScopes}, - nil + client: client, + defaultLoginURL: defaultLoginURL, + defaultLoginURLV2: defaultLoginURLV2, + allowedScopes: allowedScopes, + } } func (c *Client) ApplicationType() op.ApplicationType { - return op.ApplicationType(c.app.OIDCConfig.AppType) + return op.ApplicationType(c.client.ApplicationType) } func (c *Client) AuthMethod() oidc.AuthMethod { - return authMethodToOIDC(c.app.OIDCConfig.AuthMethodType) + return authMethodToOIDC(c.client.AuthMethodType) } func (c *Client) GetID() string { - return c.app.OIDCConfig.ClientID + return c.client.ClientID } func (c *Client) LoginURL(id string) string { @@ -56,28 +54,28 @@ func (c *Client) LoginURL(id string) string { } func (c *Client) RedirectURIs() []string { - return c.app.OIDCConfig.RedirectURIs + return c.client.RedirectURIs } func (c *Client) PostLogoutRedirectURIs() []string { - return c.app.OIDCConfig.PostLogoutRedirectURIs + return c.client.PostLogoutRedirectURIs } func (c *Client) ResponseTypes() []oidc.ResponseType { - return responseTypesToOIDC(c.app.OIDCConfig.ResponseTypes) + return responseTypesToOIDC(c.client.ResponseTypes) } func (c *Client) GrantTypes() []oidc.GrantType { - return grantTypesToOIDC(c.app.OIDCConfig.GrantTypes) + return grantTypesToOIDC(c.client.GrantTypes) } func (c *Client) DevMode() bool { - return c.app.OIDCConfig.IsDevMode + return c.client.IsDevMode } func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { return func(scopes []string) []string { - if c.app.OIDCConfig.AssertIDTokenRole { + if c.client.IDTokenRoleAssertion { return scopes } return removeScopeWithPrefix(scopes, ScopeProjectRolePrefix) @@ -86,7 +84,7 @@ func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []strin func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { return func(scopes []string) []string { - if c.app.OIDCConfig.AssertAccessTokenRole { + if c.client.AccessTokenRoleAssertion { return scopes } return removeScopeWithPrefix(scopes, ScopeProjectRolePrefix) @@ -94,15 +92,15 @@ func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []s } func (c *Client) AccessTokenLifetime() time.Duration { - return c.defaultAccessTokenLifetime //PLANNED: impl from real client + return c.client.AccessTokenLifetime } func (c *Client) IDTokenLifetime() time.Duration { - return c.defaultIdTokenLifetime //PLANNED: impl from real client + return c.client.IDTokenLifetime } func (c *Client) AccessTokenType() op.AccessTokenType { - return accessTokenTypeToOIDC(c.app.OIDCConfig.AccessTokenType) + return accessTokenTypeToOIDC(c.client.AccessTokenType) } func (c *Client) IsScopeAllowed(scope string) bool { @@ -127,20 +125,15 @@ func (c *Client) IsScopeAllowed(scope string) bool { if scope == ScopeProjectsRoles { return true } - for _, allowedScope := range c.allowedScopes { - if scope == allowedScope { - return true - } - } - return false + return slices.Contains(c.allowedScopes, scope) } func (c *Client) ClockSkew() time.Duration { - return c.app.OIDCConfig.ClockSkew + return c.client.ClockSkew } func (c *Client) IDTokenUserinfoClaimsAssertion() bool { - return c.app.OIDCConfig.AssertIDTokenUserinfo + return c.client.IDTokenUserinfoAssertion } func accessTokenTypeToOIDC(tokenType domain.OIDCTokenType) op.AccessTokenType { @@ -229,3 +222,14 @@ func removeScopeWithPrefix(scopes []string, scopePrefix ...string) []string { } return newScopeList } + +func clientIDFromCredentials(cc *op.ClientCredentials) (clientID string, assertion bool, err error) { + if cc.ClientAssertion != "" { + claims := new(oidc.JWTTokenRequest) + if _, err := oidc.ParseToken(cc.ClientAssertion, claims); err != nil { + return "", false, oidc.ErrInvalidClient().WithParent(err) + } + return claims.Issuer, true, nil + } + return cc.ClientID, false, nil +} diff --git a/internal/api/oidc/client_credentials.go b/internal/api/oidc/client_credentials.go index 3c2f272ead..b3de23a6f6 100644 --- a/internal/api/oidc/client_credentials.go +++ b/internal/api/oidc/client_credentials.go @@ -1,10 +1,15 @@ package oidc import ( + "context" "time" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/query" ) type clientCredentialsRequest struct { @@ -28,15 +33,42 @@ func (c *clientCredentialsRequest) GetScopes() []string { return c.scopes } +func (s *Server) clientCredentialsAuth(ctx context.Context, clientID, clientSecret string) (op.Client, error) { + searchQuery, err := query.NewUserLoginNamesSearchQuery(clientID) + if err != nil { + return nil, err + } + user, err := s.query.GetUser(ctx, false, searchQuery) + if errors.IsNotFound(err) { + return nil, oidc.ErrInvalidClient().WithParent(err).WithDescription("client not found") + } + if err != nil { + return nil, err // defaults to server error + } + if user.Machine == nil || user.Machine.Secret == nil { + return nil, errors.ThrowPreconditionFailed(nil, "OIDC-pieP8", "Errors.User.Machine.Secret.NotExisting") + } + if err = crypto.CompareHash(user.Machine.Secret, []byte(clientSecret), s.hashAlg); err != nil { + s.command.MachineSecretCheckFailed(ctx, user.ID, user.ResourceOwner) + return nil, errors.ThrowInvalidArgument(err, "OIDC-VoXo6", "Errors.User.Machine.Secret.Invalid") + } + + s.command.MachineSecretCheckSucceeded(ctx, user.ID, user.ResourceOwner) + return &clientCredentialsClient{ + id: clientID, + user: user, + }, nil +} + type clientCredentialsClient struct { - id string - tokenType op.AccessTokenType + id string + user *query.User } // AccessTokenType returns the AccessTokenType for the token to be created because of the client credentials request // machine users currently only have opaque tokens ([op.AccessTokenTypeBearer]) func (c *clientCredentialsClient) AccessTokenType() op.AccessTokenType { - return c.tokenType + return accessTokenTypeToOIDC(c.user.Machine.AccessTokenType) } // GetID returns the client_id (username of the machine user) for the token to be created because of the client credentials request diff --git a/internal/api/oidc/client_integration_test.go b/internal/api/oidc/client_integration_test.go index 2c3d8e3735..cd31607b3d 100644 --- a/internal/api/oidc/client_integration_test.go +++ b/internal/api/oidc/client_integration_test.go @@ -4,20 +4,26 @@ package oidc_test import ( "context" + "fmt" "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v3/pkg/client" "github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/client/rs" "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/text/language" oidc_api "github.com/zitadel/zitadel/internal/api/oidc" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/authn" "github.com/zitadel/zitadel/pkg/grpc/management" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user" ) func TestOPStorage_SetUserinfoFromToken(t *testing.T) { @@ -142,3 +148,245 @@ func assertIntrospection( assert.NotEmpty(t, introspection.Claims[oidc_api.ClaimResourceOwner+"name"]) assert.NotEmpty(t, introspection.Claims[oidc_api.ClaimResourceOwner+"primary_domain"]) } + +// TestServer_VerifyClient tests verification by running code flow tests +// with clients that have different authentication methods. +func TestServer_VerifyClient(t *testing.T) { + sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) + project, err := Tester.CreateProject(CTX) + require.NoError(t, err) + + inactiveClient, err := Tester.CreateOIDCInactivateClient(CTX, redirectURI, logoutRedirectURI, project.GetId()) + require.NoError(t, err) + nativeClient, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId()) + require.NoError(t, err) + basicWebClient, err := Tester.CreateOIDCWebClientBasic(CTX, redirectURI, logoutRedirectURI, project.GetId()) + require.NoError(t, err) + jwtWebClient, keyData, err := Tester.CreateOIDCWebClientJWT(CTX, redirectURI, logoutRedirectURI, project.GetId()) + require.NoError(t, err) + + type clientDetails struct { + authReqClientID string + clientID string + clientSecret string + keyData []byte + } + tests := []struct { + name string + client clientDetails + wantErr bool + }{ + { + name: "empty client ID error", + client: clientDetails{ + authReqClientID: nativeClient.GetClientId(), + }, + wantErr: true, + }, + { + name: "client not found error", + client: clientDetails{ + authReqClientID: nativeClient.GetClientId(), + clientID: "foo", + }, + wantErr: true, + }, + { + name: "client inactive error", + client: clientDetails{ + authReqClientID: nativeClient.GetClientId(), + clientID: inactiveClient.GetClientId(), + }, + wantErr: true, + }, + { + name: "native client success", + client: clientDetails{ + authReqClientID: nativeClient.GetClientId(), + clientID: nativeClient.GetClientId(), + }, + }, + { + name: "web client basic secret empty error", + client: clientDetails{ + authReqClientID: basicWebClient.GetClientId(), + clientID: basicWebClient.GetClientId(), + clientSecret: "", + }, + wantErr: true, + }, + { + name: "web client basic secret invalid error", + client: clientDetails{ + authReqClientID: basicWebClient.GetClientId(), + clientID: basicWebClient.GetClientId(), + clientSecret: "wrong", + }, + wantErr: true, + }, + { + name: "web client basic secret success", + client: clientDetails{ + authReqClientID: basicWebClient.GetClientId(), + clientID: basicWebClient.GetClientId(), + clientSecret: basicWebClient.GetClientSecret(), + }, + }, + { + name: "web client JWT profile empty assertion error", + client: clientDetails{ + authReqClientID: jwtWebClient.GetClientId(), + clientID: jwtWebClient.GetClientId(), + }, + wantErr: true, + }, + { + name: "web client JWT profile invalid assertion error", + client: clientDetails{ + authReqClientID: jwtWebClient.GetClientId(), + clientID: jwtWebClient.GetClientId(), + keyData: createInvalidKeyData(t, jwtWebClient), + }, + wantErr: true, + }, + { + name: "web client JWT profile success", + client: clientDetails{ + authReqClientID: jwtWebClient.GetClientId(), + clientID: jwtWebClient.GetClientId(), + keyData: keyData, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fmt.Printf("\n\n%s\n\n", tt.client.keyData) + + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, tt.client.authReqClientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, oidc.ScopeOpenID) + require.NoError(t, err) + linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ + AuthRequestId: authRequestID, + CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionID, + SessionToken: sessionToken, + }, + }, + }) + require.NoError(t, err) + + // use a new RP so we can inject different credentials + var options []rp.Option + if tt.client.keyData != nil { + options = append(options, rp.WithJWTProfile(rp.SignerFromKeyFile(tt.client.keyData))) + } + provider, err := rp.NewRelyingPartyOIDC(CTX, Tester.OIDCIssuer(), tt.client.clientID, tt.client.clientSecret, redirectURI, []string{oidc.ScopeOpenID}, options...) + require.NoError(t, err) + + // test code exchange + code := assertCodeResponse(t, linkResp.GetCallbackUrl()) + codeOpts := codeExchangeOptions(t, provider) + tokens, err := rp.CodeExchange[*oidc.IDTokenClaims](context.Background(), code, provider, codeOpts...) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assertTokens(t, tokens, false) + assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) + }) + } +} + +func codeExchangeOptions(t testing.TB, provider rp.RelyingParty) []rp.CodeExchangeOpt { + codeOpts := []rp.CodeExchangeOpt{rp.WithCodeVerifier(integration.CodeVerifier)} + if signer := provider.Signer(); signer != nil { + assertion, err := client.SignedJWTProfileAssertion(provider.OAuthConfig().ClientID, []string{provider.Issuer()}, time.Hour, provider.Signer()) + require.NoError(t, err) + codeOpts = append(codeOpts, rp.WithClientAssertionJWT(assertion)) + } + return codeOpts +} + +func createInvalidKeyData(t testing.TB, client *management.AddOIDCAppResponse) []byte { + key := domain.ApplicationKey{ + Type: domain.AuthNKeyTypeJSON, + KeyID: "1", + PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"), + ApplicationID: client.GetAppId(), + ClientID: client.GetClientId(), + } + data, err := key.Detail() + require.NoError(t, err) + return data +} + +func TestServer_CreateAccessToken_ClientCredentials(t *testing.T) { + clientID, clientSecret, err := Tester.CreateOIDCCredentialsClient(CTX) + require.NoError(t, err) + + type clientDetails struct { + clientID string + clientSecret string + keyData []byte + } + tests := []struct { + name string + clientID string + clientSecret string + wantErr bool + }{ + { + name: "missing client ID error", + clientID: "", + clientSecret: clientSecret, + wantErr: true, + }, + { + name: "client not found error", + clientID: "foo", + clientSecret: clientSecret, + wantErr: true, + }, + { + name: "machine user without secret error", + clientID: func() string { + name := gofakeit.Username() + _, err := Tester.Client.Mgmt.AddMachineUser(CTX, &management.AddMachineUserRequest{ + Name: name, + UserName: name, + AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, + }) + require.NoError(t, err) + return name + }(), + clientSecret: clientSecret, + wantErr: true, + }, + { + name: "wrong secret error", + clientID: clientID, + clientSecret: "bar", + wantErr: true, + }, + { + name: "success", + clientID: clientID, + clientSecret: clientSecret, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + provider, err := rp.NewRelyingPartyOIDC(CTX, Tester.OIDCIssuer(), tt.clientID, tt.clientSecret, redirectURI, []string{oidc.ScopeOpenID}) + require.NoError(t, err) + tokens, err := rp.ClientCredentials(CTX, provider, nil) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotNil(t, tokens) + assert.NotEmpty(t, tokens.AccessToken) + }) + } +} diff --git a/internal/api/oidc/introspect.go b/internal/api/oidc/introspect.go index d48bf001ec..cedac441eb 100644 --- a/internal/api/oidc/introspect.go +++ b/internal/api/oidc/introspect.go @@ -11,7 +11,7 @@ import ( "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/crypto" - errz "github.com/zitadel/zitadel/internal/errors" + zerrors "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/tracing" ) @@ -31,14 +31,14 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR ctx, cancel := context.WithCancel(ctx) defer cancel() - clientChan := make(chan *instrospectionClientResult) - go s.instrospectionClientAuth(ctx, r.Data.ClientCredentials, clientChan) + clientChan := make(chan *introspectionClientResult) + go s.introspectionClientAuth(ctx, r.Data.ClientCredentials, clientChan) tokenChan := make(chan *introspectionTokenResult) go s.introspectionToken(ctx, r.Data.Token, tokenChan) var ( - client *instrospectionClientResult + client *introspectionClientResult token *introspectionTokenResult ) @@ -116,13 +116,13 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR return op.NewResponse(introspectionResp), nil } -type instrospectionClientResult struct { +type introspectionClientResult struct { clientID string projectID string err error } -func (s *Server) instrospectionClientAuth(ctx context.Context, cc *op.ClientCredentials, rc chan<- *instrospectionClientResult) { +func (s *Server) introspectionClientAuth(ctx context.Context, cc *op.ClientCredentials, rc chan<- *introspectionClientResult) { ctx, span := tracing.NewSpan(ctx) clientID, projectID, err := func() (string, string, error) { @@ -147,7 +147,7 @@ func (s *Server) instrospectionClientAuth(ctx context.Context, cc *op.ClientCred span.EndWithError(err) - rc <- &instrospectionClientResult{ + rc <- &introspectionClientResult{ clientID: clientID, projectID: projectID, err: err, @@ -157,15 +157,11 @@ func (s *Server) instrospectionClientAuth(ctx context.Context, cc *op.ClientCred // clientFromCredentials parses the client ID early, // and makes a single query for the client for either auth methods. func (s *Server) clientFromCredentials(ctx context.Context, cc *op.ClientCredentials) (client *query.IntrospectionClient, err error) { - if cc.ClientAssertion != "" { - claims := new(oidc.JWTTokenRequest) - if _, err := oidc.ParseToken(cc.ClientAssertion, claims); err != nil { - return nil, oidc.ErrUnauthorizedClient().WithParent(err) - } - client, err = s.query.GetIntrospectionClientByID(ctx, claims.Issuer, true) - } else { - client, err = s.query.GetIntrospectionClientByID(ctx, cc.ClientID, false) + clientID, assertion, err := clientIDFromCredentials(cc) + if err != nil { + return nil, err } + client, err = s.query.GetIntrospectionClientByID(ctx, clientID, assertion) if errors.Is(err, sql.ErrNoRows) { return nil, oidc.ErrUnauthorizedClient().WithParent(err) } @@ -196,5 +192,5 @@ func validateIntrospectionAudience(audience []string, clientID, projectID string return nil } - return errz.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client") + return zerrors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client") } diff --git a/internal/api/oidc/oidc_integration_test.go b/internal/api/oidc/oidc_integration_test.go index e1531fff3b..0e4b6e9c9d 100644 --- a/internal/api/oidc/oidc_integration_test.go +++ b/internal/api/oidc/oidc_integration_test.go @@ -31,9 +31,9 @@ var ( ) const ( - redirectURI = "oidcintegrationtest://callback" + redirectURI = "https://callback" redirectURIImplicit = "http://localhost:9999/callback" - logoutRedirectURI = "oidcintegrationtest://logged-out" + logoutRedirectURI = "https://logged-out" zitadelAudienceScope = domain.ProjectIDScope + domain.ProjectIDScopeZITADEL + domain.AudSuffix ) diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index defa5fdcdc..1f34f62fb0 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -6,11 +6,9 @@ import ( "net/http" "time" - "github.com/rakyll/statik/fs" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" "golang.org/x/exp/slog" - "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/assets" http_utils "github.com/zitadel/zitadel/internal/api/http" @@ -23,7 +21,6 @@ import ( caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/crdb" - "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/metrics" ) @@ -131,6 +128,9 @@ func NewServer( query: query, command: command, keySet: newKeySet(context.TODO(), time.Hour, query.GetActivePublicKeyByID), + defaultLoginURL: fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID), + defaultLoginURLV2: config.DefaultLoginURLV2, + defaultLogoutURLV2: config.DefaultLogoutURLV2, fallbackLogger: fallbackLogger, hashAlg: crypto.NewBCrypt(10), // as we are only verifying in oidc, the cost is already part of the hash string and the config here is irrelevant. signingKeyAlgorithm: config.SigningKeyAlgorithm, @@ -167,10 +167,6 @@ func ignoredQuotaLimitEndpoint(endpoints *EndpointConfig) []string { } func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []byte) (*op.Config, error) { - supportedLanguages, err := getSupportedLanguages() - if err != nil { - return nil, err - } opConfig := &op.Config{ DefaultLogoutRedirectURI: defaultLogoutRedirectURI, CodeMethodS256: config.CodeMethodS256, @@ -178,7 +174,6 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey [] AuthMethodPrivateKeyJWT: config.AuthMethodPrivateKeyJWT, GrantTypeRefreshToken: config.GrantTypeRefreshToken, RequestObjectSupported: config.RequestObjectSupported, - SupportedUILocales: supportedLanguages, DeviceAuthorization: config.DeviceAuth.toOPConfig(), } if cryptoLength := len(cryptoKey); cryptoLength != 32 { @@ -211,11 +206,3 @@ func newStorage(config Config, command *command.Commands, query *query.Queries, func (o *OPStorage) Health(ctx context.Context) error { return o.repo.Health(ctx) } - -func getSupportedLanguages() ([]language.Tag, error) { - statikLoginFS, err := fs.NewWithNamespace("login") - if err != nil { - return nil, err - } - return i18n.SupportedLanguages(statikLoginFS) -} diff --git a/internal/api/oidc/server.go b/internal/api/oidc/server.go index fe16078f34..1782966e9c 100644 --- a/internal/api/oidc/server.go +++ b/internal/api/oidc/server.go @@ -12,6 +12,7 @@ import ( "github.com/zitadel/zitadel/internal/auth/repository" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/tracing" ) @@ -26,6 +27,10 @@ type Server struct { command *command.Commands keySet *keySetCache + defaultLoginURL string + defaultLoginURLV2 string + defaultLogoutURLV2 string + fallbackLogger *slog.Logger hashAlg crypto.HashAlgorithm signingKeyAlgorithm string @@ -103,8 +108,15 @@ func (s *Server) Ready(ctx context.Context, r *op.Request[struct{}]) (_ *op.Resp func (s *Server) Discovery(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - - return op.NewResponse(s.createDiscoveryConfig(ctx)), nil + restrictions, err := s.query.GetInstanceRestrictions(ctx) + if err != nil { + return nil, err + } + allowedLanguages := restrictions.AllowedLanguages + if len(allowedLanguages) == 0 { + allowedLanguages = i18n.SupportedLanguages() + } + return op.NewResponse(s.createDiscoveryConfig(ctx, allowedLanguages)), nil } func (s *Server) Keys(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) { @@ -135,13 +147,6 @@ func (s *Server) DeviceAuthorization(ctx context.Context, r *op.ClientRequest[oi return s.LegacyServer.DeviceAuthorization(ctx, r) } -func (s *Server) VerifyClient(ctx context.Context, r *op.Request[op.ClientCredentials]) (_ op.Client, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - return s.LegacyServer.VerifyClient(ctx, r) -} - func (s *Server) CodeExchange(ctx context.Context, r *op.ClientRequest[oidc.AccessTokenRequest]) (_ *op.Response, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -205,7 +210,7 @@ func (s *Server) EndSession(ctx context.Context, r *op.Request[oidc.EndSessionRe return s.LegacyServer.EndSession(ctx, r) } -func (s *Server) createDiscoveryConfig(ctx context.Context) *oidc.DiscoveryConfiguration { +func (s *Server) createDiscoveryConfig(ctx context.Context, supportedUILocales oidc.Locales) *oidc.DiscoveryConfiguration { issuer := op.IssuerFromContext(ctx) return &oidc.DiscoveryConfiguration{ Issuer: issuer, @@ -231,7 +236,7 @@ func (s *Server) createDiscoveryConfig(ctx context.Context) *oidc.DiscoveryConfi RevocationEndpointAuthMethodsSupported: op.AuthMethodsRevocationEndpoint(s.Provider()), ClaimsSupported: op.SupportedClaims(s.Provider()), CodeChallengeMethodsSupported: op.CodeChallengeMethods(s.Provider()), - UILocalesSupported: s.Provider().SupportedUILocales(), + UILocalesSupported: supportedUILocales, RequestParameterSupported: s.Provider().RequestObjectSupported(), } } diff --git a/internal/api/oidc/server_test.go b/internal/api/oidc/server_test.go index d7f258d0d2..c42c11d195 100644 --- a/internal/api/oidc/server_test.go +++ b/internal/api/oidc/server_test.go @@ -16,7 +16,8 @@ func TestServer_createDiscoveryConfig(t *testing.T) { signingKeyAlgorithm string } type args struct { - ctx context.Context + ctx context.Context + supportedUILocales []language.Tag } tests := []struct { name string @@ -36,7 +37,6 @@ func TestServer_createDiscoveryConfig(t *testing.T) { AuthMethodPrivateKeyJWT: true, GrantTypeRefreshToken: true, RequestObjectSupported: true, - SupportedUILocales: []language.Tag{language.English, language.German}, }, nil, ) @@ -56,7 +56,8 @@ func TestServer_createDiscoveryConfig(t *testing.T) { signingKeyAlgorithm: "RS256", }, args{ - ctx: op.ContextWithIssuer(context.Background(), "https://issuer.com"), + ctx: op.ContextWithIssuer(context.Background(), "https://issuer.com"), + supportedUILocales: []language.Tag{language.English, language.German}, }, &oidc.DiscoveryConfiguration{ Issuer: "https://issuer.com", @@ -113,7 +114,7 @@ func TestServer_createDiscoveryConfig(t *testing.T) { LegacyServer: tt.fields.LegacyServer, signingKeyAlgorithm: tt.fields.signingKeyAlgorithm, } - assert.Equalf(t, tt.want, s.createDiscoveryConfig(tt.args.ctx), "createDiscoveryConfig(%v)", tt.args.ctx) + assert.Equalf(t, tt.want, s.createDiscoveryConfig(tt.args.ctx, tt.args.supportedUILocales), "createDiscoveryConfig(%v)", tt.args.ctx) }) } } diff --git a/internal/api/ui/login/change_password_handler.go b/internal/api/ui/login/change_password_handler.go index 08eb8badeb..e85b99dcd5 100644 --- a/internal/api/ui/login/change_password_handler.go +++ b/internal/api/ui/login/change_password_handler.go @@ -36,13 +36,13 @@ func (l *Login) handleChangePassword(w http.ResponseWriter, r *http.Request) { } func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) { - var errID, errMessage string + var errType, errMessage string if err != nil { - errID, errMessage = l.getErrorMessage(r, err) + errType, errMessage = l.getErrorMessage(r, err) } translator := l.getTranslator(r.Context(), authReq) data := passwordData{ - baseData: l.getBaseData(r, authReq, "PasswordChange.Title", "PasswordChange.Description", errID, errMessage), + baseData: l.getBaseData(r, authReq, translator, "PasswordChange.Title", "PasswordChange.Description", errType, errMessage), profileData: l.getProfileData(authReq), } policy := l.getPasswordComplexityPolicy(r, authReq.UserOrgID) @@ -65,8 +65,7 @@ func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, aut } func (l *Login) renderChangePasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) { - var errType, errMessage string translator := l.getTranslator(r.Context(), authReq) - data := l.getUserData(r, authReq, "PasswordChange.Title", "PasswordChange.Description", errType, errMessage) + data := l.getUserData(r, authReq, translator, "PasswordChange.Title", "PasswordChange.Description", "", "") l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangePasswordDone], data, nil) } diff --git a/internal/api/ui/login/device_auth.go b/internal/api/ui/login/device_auth.go index e2322ee04f..ff5031ebcf 100644 --- a/internal/api/ui/login/device_auth.go +++ b/internal/api/ui/login/device_auth.go @@ -28,13 +28,13 @@ func (l *Login) renderDeviceAuthUserCode(w http.ResponseWriter, r *http.Request, logging.WithError(err).Error() errID, errMessage = l.getErrorMessage(r, err) } - - data := l.getBaseData(r, nil, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage) translator := l.getTranslator(r.Context(), nil) + data := l.getBaseData(r, nil, translator, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthUserCode], data, nil) } func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, scopes []string) { + translator := l.getTranslator(r.Context(), authReq) data := &struct { baseData AuthRequestID string @@ -42,14 +42,13 @@ func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, a ClientID string Scopes []string }{ - baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""), + baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""), AuthRequestID: authReq.ID, Username: authReq.UserName, ClientID: authReq.ApplicationID, Scopes: scopes, } - translator := l.getTranslator(r.Context(), authReq) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthAction], data, nil) } @@ -60,14 +59,13 @@ const ( // renderDeviceAuthDone renders success.html when the action was allowed and error.html when it was denied. func (l *Login) renderDeviceAuthDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, action string) { + translator := l.getTranslator(r.Context(), authReq) data := &struct { baseData Message string }{ - baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""), + baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""), } - - translator := l.getTranslator(r.Context(), authReq) switch action { case deviceAuthAllowed: data.Message = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Approved", nil) diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index 3bab7b90e6..a2f0afcbc2 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -549,7 +549,7 @@ func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Requ translator := l.getTranslator(r.Context(), authReq) data := externalNotFoundOptionData{ - baseData: l.getBaseData(r, authReq, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage), + baseData: l.getBaseData(r, authReq, translator, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage), externalNotFoundOptionFormData: externalNotFoundOptionFormData{ externalRegisterFormData: externalRegisterFormData{ Email: human.EmailAddress, diff --git a/internal/api/ui/login/init_password_handler.go b/internal/api/ui/login/init_password_handler.go index e6939e0c09..6c25130635 100644 --- a/internal/api/ui/login/init_password_handler.go +++ b/internal/api/ui/login/init_password_handler.go @@ -122,7 +122,7 @@ func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authR translator := l.getTranslator(r.Context(), authReq) data := initPasswordData{ - baseData: l.getBaseData(r, authReq, "InitPassword.Title", "InitPassword.Description", errID, errMessage), + baseData: l.getBaseData(r, authReq, translator, "InitPassword.Title", "InitPassword.Description", errID, errMessage), profileData: l.getProfileData(authReq), UserID: userID, Code: code, @@ -153,8 +153,8 @@ func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authR } func (l *Login) renderInitPasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) { - data := l.getUserData(r, authReq, "InitPasswordDone.Title", "InitPasswordDone.Description", "", "") translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "InitPasswordDone.Title", "InitPasswordDone.Description", "", "") if authReq == nil { l.customTexts(r.Context(), translator, orgID) } diff --git a/internal/api/ui/login/init_user_handler.go b/internal/api/ui/login/init_user_handler.go index df2f940d6a..850ddb32ae 100644 --- a/internal/api/ui/login/init_user_handler.go +++ b/internal/api/ui/login/init_user_handler.go @@ -118,7 +118,7 @@ func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq * translator := l.getTranslator(r.Context(), authReq) data := initUserData{ - baseData: l.getBaseData(r, authReq, "InitUser.Title", "InitUser.Description", errID, errMessage), + baseData: l.getBaseData(r, authReq, translator, "InitUser.Title", "InitUser.Description", errID, errMessage), profileData: l.getProfileData(authReq), UserID: userID, Code: code, @@ -155,8 +155,8 @@ func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq * } func (l *Login) renderInitUserDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) { - data := l.getUserData(r, authReq, "InitUserDone.Title", "InitUserDone.Description", "", "") translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "InitUserDone.Title", "InitUserDone.Description", "", "") if authReq == nil { l.customTexts(r.Context(), translator, orgID) } diff --git a/internal/api/ui/login/ldap_handler.go b/internal/api/ui/login/ldap_handler.go index 3ec49f4a7f..93590458f6 100644 --- a/internal/api/ui/login/ldap_handler.go +++ b/internal/api/ui/login/ldap_handler.go @@ -35,8 +35,9 @@ func (l *Login) renderLDAPLogin(w http.ResponseWriter, r *http.Request, authReq errID, errMessage = l.getErrorMessage(r, err) } temp := l.renderer.Templates[tmplLDAPLogin] - data := l.getUserData(r, authReq, "Login.Title", "Login.Description", errID, errMessage) - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), temp, data, nil) + translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "Login.Title", "Login.Description", errID, errMessage) + l.renderer.RenderTemplate(w, r, translator, temp, data, nil) } func (l *Login) handleLDAPCallback(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/link_users_handler.go b/internal/api/ui/login/link_users_handler.go index 09d42e91ec..1952ed1213 100644 --- a/internal/api/ui/login/link_users_handler.go +++ b/internal/api/ui/login/link_users_handler.go @@ -19,6 +19,7 @@ func (l *Login) linkUsers(w http.ResponseWriter, r *http.Request, authReq *domai func (l *Login) renderLinkUsersDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) { var errType, errMessage string - data := l.getUserData(r, authReq, "LinkingUsersDone.Title", "LinkingUsersDone.Description", errType, errMessage) - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLinkUsersDone], data, nil) + translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "LinkingUsersDone.Title", "LinkingUsersDone.Description", errType, errMessage) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLinkUsersDone], data, nil) } diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index dc8e834fcd..71bba44d5d 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -2,15 +2,12 @@ package login import ( "context" - "fmt" "net/http" "strings" "time" "github.com/gorilla/csrf" "github.com/gorilla/mux" - "github.com/rakyll/statik/fs" - "github.com/zitadel/zitadel/feature" "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" @@ -93,17 +90,12 @@ func CreateLogin(config Config, userCodeAlg: userCodeAlg, featureCheck: featureCheck, } - statikFS, err := fs.NewWithNamespace("login") - if err != nil { - return nil, fmt.Errorf("unable to create filesystem: %w", err) - } - csrfInterceptor := createCSRFInterceptor(config.CSRFCookieName, csrfCookieKey, externalSecure, login.csrfErrorHandler()) cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache) security := middleware.SecurityHeaders(csp(), login.cspErrorHandler) - login.router = CreateRouter(login, statikFS, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor, accessHandler) - login.renderer = CreateRenderer(HandlerPrefix, statikFS, staticStorage, config.LanguageCookieName) + login.router = CreateRouter(login, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor, accessHandler) + login.renderer = CreateRenderer(HandlerPrefix, staticStorage, config.LanguageCookieName) login.parser = form.NewParser() return login, nil } diff --git a/internal/api/ui/login/login_handler.go b/internal/api/ui/login/login_handler.go index c141600926..2369490638 100644 --- a/internal/api/ui/login/login_handler.go +++ b/internal/api/ui/login/login_handler.go @@ -99,7 +99,8 @@ func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *dom l.handleIDP(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID) return } - data := l.getUserData(r, authReq, "Login.Title", "Login.Description", errID, errMessage) + translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "Login.Title", "Login.Description", errID, errMessage) funcs := map[string]interface{}{ "hasUsernamePasswordLogin": func() bool { return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowUsernamePassword @@ -111,7 +112,7 @@ func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *dom return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowRegister }, } - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLogin], data, funcs) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLogin], data, funcs) } func singleIDPAllowed(authReq *domain.AuthRequest) bool { diff --git a/internal/api/ui/login/login_success_handler.go b/internal/api/ui/login/login_success_handler.go index f05ee48185..8186bfe61b 100644 --- a/internal/api/ui/login/login_success_handler.go +++ b/internal/api/ui/login/login_success_handler.go @@ -41,8 +41,9 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request, if err != nil { errID, errMessage = l.getErrorMessage(r, err) } + translator := l.getTranslator(r.Context(), authReq) data := loginSuccessData{ - userData: l.getUserData(r, authReq, "LoginSuccess.Title", "", errID, errMessage), + userData: l.getUserData(r, authReq, translator, "LoginSuccess.Title", "", errID, errMessage), } if authReq != nil { data.RedirectURI, err = l.authRequestCallback(r.Context(), authReq) @@ -51,7 +52,7 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request, return } } - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLoginSuccess], data, nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLoginSuccess], data, nil) } func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) { diff --git a/internal/api/ui/login/logout_handler.go b/internal/api/ui/login/logout_handler.go index 2146d47e49..e270cd5541 100644 --- a/internal/api/ui/login/logout_handler.go +++ b/internal/api/ui/login/logout_handler.go @@ -13,6 +13,7 @@ func (l *Login) handleLogoutDone(w http.ResponseWriter, r *http.Request) { } func (l *Login) renderLogoutDone(w http.ResponseWriter, r *http.Request) { - data := l.getUserData(r, nil, "LogoutDone.Title", "LogoutDone.Description", "", "") - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), nil), l.renderer.Templates[tmplLogoutDone], data, nil) + translator := l.getTranslator(r.Context(), nil) + data := l.getUserData(r, nil, translator, "LogoutDone.Title", "LogoutDone.Description", "", "") + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLogoutDone], data, nil) } diff --git a/internal/api/ui/login/mail_verify_handler.go b/internal/api/ui/login/mail_verify_handler.go index bfcb322fa2..50f03df811 100644 --- a/internal/api/ui/login/mail_verify_handler.go +++ b/internal/api/ui/login/mail_verify_handler.go @@ -95,7 +95,7 @@ func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, a translator := l.getTranslator(r.Context(), authReq) data := mailVerificationData{ - baseData: l.getBaseData(r, authReq, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage), + baseData: l.getBaseData(r, authReq, translator, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage), UserID: userID, profileData: l.getProfileData(authReq), } @@ -111,7 +111,7 @@ func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, a func (l *Login) renderMailVerified(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) { translator := l.getTranslator(r.Context(), authReq) data := mailVerificationData{ - baseData: l.getBaseData(r, authReq, "EmailVerificationDone.Title", "EmailVerificationDone.Description", "", ""), + baseData: l.getBaseData(r, authReq, translator, "EmailVerificationDone.Title", "EmailVerificationDone.Description", "", ""), profileData: l.getProfileData(authReq), } if authReq == nil { diff --git a/internal/api/ui/login/mfa_init_done_handler.go b/internal/api/ui/login/mfa_init_done_handler.go index f38927d5e7..437fde29f4 100644 --- a/internal/api/ui/login/mfa_init_done_handler.go +++ b/internal/api/ui/login/mfa_init_done_handler.go @@ -16,7 +16,7 @@ type mfaInitDoneData struct { func (l *Login) renderMFAInitDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaDoneData) { var errType, errMessage string translator := l.getTranslator(r.Context(), authReq) - data.baseData = l.getBaseData(r, authReq, "InitMFADone.Title", "InitMFADone.Description", errType, errMessage) + data.baseData = l.getBaseData(r, authReq, translator, "InitMFADone.Title", "InitMFADone.Description", errType, errMessage) data.profileData = l.getProfileData(authReq) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAInitDone], data, nil) } diff --git a/internal/api/ui/login/mfa_init_sms.go b/internal/api/ui/login/mfa_init_sms.go index 965806c90b..a947918634 100644 --- a/internal/api/ui/login/mfa_init_sms.go +++ b/internal/api/ui/login/mfa_init_sms.go @@ -57,10 +57,11 @@ func (l *Login) renderRegisterSMS(w http.ResponseWriter, r *http.Request, authRe if err != nil { errID, errMessage = l.getErrorMessage(r, err) } - data.baseData = l.getBaseData(r, authReq, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage) + translator := l.getTranslator(r.Context(), authReq) + data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage) data.profileData = l.getProfileData(authReq) data.MFAType = domain.MFATypeOTPSMS - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplMFASMSInit], data, nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFASMSInit], data, nil) } // handleRegisterSMSCheck handles form submissions of the SMS registration. diff --git a/internal/api/ui/login/mfa_init_u2f.go b/internal/api/ui/login/mfa_init_u2f.go index f00b398f0e..2cd1029ee5 100644 --- a/internal/api/ui/login/mfa_init_u2f.go +++ b/internal/api/ui/login/mfa_init_u2f.go @@ -29,14 +29,15 @@ func (l *Login) renderRegisterU2F(w http.ResponseWriter, r *http.Request, authRe if u2f != nil { credentialData = base64.RawURLEncoding.EncodeToString(u2f.CredentialCreationData) } + translator := l.getTranslator(r.Context(), authReq) data := &u2fInitData{ webAuthNData: webAuthNData{ - userData: l.getUserData(r, authReq, "InitMFAU2F.Title", "InitMFAU2F.Description", errID, errMessage), + userData: l.getUserData(r, authReq, translator, "InitMFAU2F.Title", "InitMFAU2F.Description", errID, errMessage), CredentialCreationData: credentialData, }, MFAType: domain.MFATypeU2F, } - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplMFAU2FInit], data, nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAU2FInit], data, nil) } func (l *Login) handleRegisterU2F(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/mfa_init_verify_handler.go b/internal/api/ui/login/mfa_init_verify_handler.go index e6f0749e92..5f0f0e3119 100644 --- a/internal/api/ui/login/mfa_init_verify_handler.go +++ b/internal/api/ui/login/mfa_init_verify_handler.go @@ -71,7 +71,7 @@ func (l *Login) renderMFAInitVerify(w http.ResponseWriter, r *http.Request, auth errID, errMessage = l.getErrorMessage(r, err) } translator := l.getTranslator(r.Context(), authReq) - data.baseData = l.getBaseData(r, authReq, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage) + data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage) data.profileData = l.getProfileData(authReq) if data.MFAType == domain.MFATypeTOTP { code, err := generateQrCode(data.totpData.Url) diff --git a/internal/api/ui/login/mfa_prompt_handler.go b/internal/api/ui/login/mfa_prompt_handler.go index 9f4e8be409..7aba581970 100644 --- a/internal/api/ui/login/mfa_prompt_handler.go +++ b/internal/api/ui/login/mfa_prompt_handler.go @@ -56,7 +56,7 @@ func (l *Login) renderMFAPrompt(w http.ResponseWriter, r *http.Request, authReq } translator := l.getTranslator(r.Context(), authReq) data := mfaData{ - baseData: l.getBaseData(r, authReq, "InitMFAPrompt.Title", "InitMFAPrompt.Description", errID, errMessage), + baseData: l.getBaseData(r, authReq, translator, "InitMFAPrompt.Title", "InitMFAPrompt.Description", errID, errMessage), profileData: l.getProfileData(authReq), } diff --git a/internal/api/ui/login/mfa_verify_handler.go b/internal/api/ui/login/mfa_verify_handler.go index addb7347fb..80f1c94e25 100644 --- a/internal/api/ui/login/mfa_verify_handler.go +++ b/internal/api/ui/login/mfa_verify_handler.go @@ -66,12 +66,12 @@ func (l *Login) renderMFAVerifySelected(w http.ResponseWriter, r *http.Request, if err != nil { errID, errMessage = l.getErrorMessage(r, err) } - data := l.getUserData(r, authReq, "", "", errID, errMessage) + translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "", "", errID, errMessage) if verificationStep == nil { l.renderError(w, r, authReq, err) return } - translator := l.getTranslator(r.Context(), authReq) switch selectedProvider { case domain.MFATypeU2F: diff --git a/internal/api/ui/login/mfa_verify_otp_handler.go b/internal/api/ui/login/mfa_verify_otp_handler.go index 88aa37c947..297485933a 100644 --- a/internal/api/ui/login/mfa_verify_otp_handler.go +++ b/internal/api/ui/login/mfa_verify_otp_handler.go @@ -61,12 +61,13 @@ func (l *Login) renderOTPVerification(w http.ResponseWriter, r *http.Request, au if err != nil { errID, errMessage = l.getErrorMessage(r, err) } + translator := l.getTranslator(r.Context(), authReq) data := &mfaOTPData{ - userData: l.getUserData(r, authReq, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage), + userData: l.getUserData(r, authReq, translator, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage), MFAProviders: removeSelectedProviderFromList(providers, selectedProvider), SelectedProvider: selectedProvider, } - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplOTPVerification], data, nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplOTPVerification], data, nil) } // handleOTPVerificationCheck handles form submissions of the OTP verification. diff --git a/internal/api/ui/login/mfa_verify_u2f_handler.go b/internal/api/ui/login/mfa_verify_u2f_handler.go index 2fc1361b44..c6cbe359ea 100644 --- a/internal/api/ui/login/mfa_verify_u2f_handler.go +++ b/internal/api/ui/login/mfa_verify_u2f_handler.go @@ -37,15 +37,16 @@ func (l *Login) renderU2FVerification(w http.ResponseWriter, r *http.Request, au if webAuthNLogin != nil { credentialData = base64.RawURLEncoding.EncodeToString(webAuthNLogin.CredentialAssertionData) } + translator := l.getTranslator(r.Context(), authReq) data := &mfaU2FData{ webAuthNData: webAuthNData{ - userData: l.getUserData(r, authReq, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage), + userData: l.getUserData(r, authReq, translator, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage), CredentialCreationData: credentialData, }, MFAProviders: providers, SelectedProvider: -1, } - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplU2FVerification], data, nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplU2FVerification], data, nil) } func (l *Login) handleU2FVerification(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/password_handler.go b/internal/api/ui/login/password_handler.go index 9e448842e9..28baf4a1e1 100644 --- a/internal/api/ui/login/password_handler.go +++ b/internal/api/ui/login/password_handler.go @@ -19,7 +19,8 @@ func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq * if err != nil { errID, errMessage = l.getErrorMessage(r, err) } - data := l.getUserData(r, authReq, "Password.Title", "Password.Description", errID, errMessage) + translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "Password.Title", "Password.Description", errID, errMessage) funcs := map[string]interface{}{ "showPasswordReset": func() bool { if authReq.LoginPolicy != nil { @@ -28,7 +29,7 @@ func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq * return true }, } - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPassword], data, funcs) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPassword], data, funcs) } func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/password_reset_handler.go b/internal/api/ui/login/password_reset_handler.go index ea8cd43321..2cd83ab2dc 100644 --- a/internal/api/ui/login/password_reset_handler.go +++ b/internal/api/ui/login/password_reset_handler.go @@ -48,6 +48,7 @@ func (l *Login) renderPasswordResetDone(w http.ResponseWriter, r *http.Request, if err != nil { errID, errMessage = l.getErrorMessage(r, err) } - data := l.getUserData(r, authReq, "PasswordResetDone.Title", "PasswordResetDone.Description", errID, errMessage) - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPasswordResetDone], data, nil) + translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "PasswordResetDone.Title", "PasswordResetDone.Description", errID, errMessage) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordResetDone], data, nil) } diff --git a/internal/api/ui/login/passwordless_login_handler.go b/internal/api/ui/login/passwordless_login_handler.go index 58cba32efb..8373a8fbdb 100644 --- a/internal/api/ui/login/passwordless_login_handler.go +++ b/internal/api/ui/login/passwordless_login_handler.go @@ -36,14 +36,15 @@ func (l *Login) renderPasswordlessVerification(w http.ResponseWriter, r *http.Re if passwordSet && authReq.LoginPolicy != nil { passwordSet = authReq.LoginPolicy.AllowUsernamePassword } + translator := l.getTranslator(r.Context(), authReq) data := &passwordlessData{ webAuthNData{ - userData: l.getUserData(r, authReq, "Passwordless.Title", "Passwordless.Description", errID, errMessage), + userData: l.getUserData(r, authReq, translator, "Passwordless.Title", "Passwordless.Description", errID, errMessage), CredentialCreationData: credentialData, }, passwordSet, } - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPasswordlessVerification], data, nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessVerification], data, nil) } func (l *Login) handlePasswordlessVerification(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/passwordless_prompt_handler.go b/internal/api/ui/login/passwordless_prompt_handler.go index 24a1eabf6c..ee70b76126 100644 --- a/internal/api/ui/login/passwordless_prompt_handler.go +++ b/internal/api/ui/login/passwordless_prompt_handler.go @@ -31,10 +31,9 @@ func (l *Login) renderPasswordlessPrompt(w http.ResponseWriter, r *http.Request, if err != nil { errID, errMessage = l.getErrorMessage(r, err) } - data := &passwordlessPromptData{ - userData: l.getUserData(r, authReq, "PasswordlessPrompt.Title", "PasswordlessPrompt.Description", errID, errMessage), - } - translator := l.getTranslator(r.Context(), authReq) + data := &passwordlessPromptData{ + userData: l.getUserData(r, authReq, translator, "PasswordlessPrompt.Title", "PasswordlessPrompt.Description", errID, errMessage), + } l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessPrompt], data, nil) } diff --git a/internal/api/ui/login/passwordless_registration_handler.go b/internal/api/ui/login/passwordless_registration_handler.go index b70aac0c60..4c2d379b48 100644 --- a/internal/api/ui/login/passwordless_registration_handler.go +++ b/internal/api/ui/login/passwordless_registration_handler.go @@ -99,11 +99,10 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re if webAuthNToken != nil { credentialData = base64.RawURLEncoding.EncodeToString(webAuthNToken.CredentialCreationData) } - translator := l.getTranslator(r.Context(), authReq) data := &passwordlessRegistrationData{ webAuthNData{ - userData: l.getUserData(r, authReq, "PasswordlessRegistration.Title", "PasswordlessRegistration.Description", errID, errMessage), + userData: l.getUserData(r, authReq, translator, "PasswordlessRegistration.Title", "PasswordlessRegistration.Description", errID, errMessage), CredentialCreationData: credentialData, }, code, @@ -117,8 +116,6 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re policy, err := l.query.ActiveLabelPolicyByOrg(r.Context(), orgID, false) logging.Log("HANDL-XjWKE").OnError(err).Error("unable to get active label policy") data.LabelPolicy = labelPolicyToDomain(policy) - - translator, err = l.renderer.NewTranslator(r.Context()) if err == nil { texts, err := l.authRepo.GetLoginText(r.Context(), orgID) logging.Log("LOGIN-HJK4t").OnError(err).Warn("could not get custom texts") @@ -193,9 +190,8 @@ func (l *Login) renderPasswordlessRegistrationDone(w http.ResponseWriter, r *htt errID, errMessage = l.getErrorMessage(r, err) } translator := l.getTranslator(r.Context(), authReq) - data := passwordlessRegistrationDoneDate{ - userData: l.getUserData(r, authReq, "PasswordlessRegistrationDone.Title", "PasswordlessRegistrationDone.Description", errID, errMessage), + userData: l.getUserData(r, authReq, translator, "PasswordlessRegistrationDone.Title", "PasswordlessRegistrationDone.Description", errID, errMessage), HideNextButton: authReq == nil, } if authReq == nil { diff --git a/internal/api/ui/login/register_handler.go b/internal/api/ui/login/register_handler.go index d2b4845db8..99ce94b3d9 100644 --- a/internal/api/ui/login/register_handler.go +++ b/internal/api/ui/login/register_handler.go @@ -96,7 +96,6 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) { l.renderRegister(w, r, authRequest, data, err) return } - user, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, nil, nil, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) if err != nil { l.renderRegister(w, r, authRequest, data, err) @@ -160,7 +159,7 @@ func (l *Login) renderRegister(w http.ResponseWriter, r *http.Request, authReque } data := registerData{ - baseData: l.getBaseData(r, authRequest, "RegistrationUser.Title", "RegistrationUser.Description", errID, errMessage), + baseData: l.getBaseData(r, authRequest, translator, "RegistrationUser.Title", "RegistrationUser.Description", errID, errMessage), registerFormData: *formData, } diff --git a/internal/api/ui/login/register_option_handler.go b/internal/api/ui/login/register_option_handler.go index b2d0c9e16f..7d88f76c6c 100644 --- a/internal/api/ui/login/register_option_handler.go +++ b/internal/api/ui/login/register_option_handler.go @@ -54,7 +54,7 @@ func (l *Login) renderRegisterOption(w http.ResponseWriter, r *http.Request, aut } translator := l.getTranslator(r.Context(), authReq) data := registerOptionData{ - baseData: l.getBaseData(r, authReq, "RegisterOption.Title", "RegisterOption.Description", errID, errMessage), + baseData: l.getBaseData(r, authReq, translator, "RegisterOption.Title", "RegisterOption.Description", errID, errMessage), } funcs := map[string]interface{}{ "hasRegistration": func() bool { diff --git a/internal/api/ui/login/register_org_handler.go b/internal/api/ui/login/register_org_handler.go index 662f683d02..fbea67784f 100644 --- a/internal/api/ui/login/register_org_handler.go +++ b/internal/api/ui/login/register_org_handler.go @@ -1,7 +1,6 @@ package login import ( - "context" "net/http" "github.com/zitadel/zitadel/internal/api/authz" @@ -39,8 +38,12 @@ type registerOrgData struct { } func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) { - disallowed, err := l.publicOrgRegistrationIsDisallowed(r.Context()) - if disallowed || err != nil { + restrictions, err := l.query.GetInstanceRestrictions(r.Context()) + if err != nil { + l.renderError(w, r, nil, err) + return + } + if restrictions.DisallowPublicOrgRegistration { w.WriteHeader(http.StatusNotFound) return } @@ -54,8 +57,12 @@ func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) { } func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) { - disallowed, err := l.publicOrgRegistrationIsDisallowed(r.Context()) - if disallowed || err != nil { + restrictions, err := l.query.GetInstanceRestrictions(r.Context()) + if err != nil { + l.renderError(w, r, nil, err) + return + } + if restrictions.DisallowPublicOrgRegistration { w.WriteHeader(http.StatusConflict) return } @@ -99,7 +106,7 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe } translator := l.getTranslator(r.Context(), authRequest) data := registerOrgData{ - baseData: l.getBaseData(r, authRequest, "RegistrationOrg.Title", "RegistrationOrg.Description", errID, errMessage), + baseData: l.getBaseData(r, authRequest, translator, "RegistrationOrg.Title", "RegistrationOrg.Description", errID, errMessage), registerOrgFormData: *formData, } pwPolicy := l.getPasswordComplexityPolicy(r, "0") @@ -130,11 +137,6 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplRegisterOrg], data, nil) } -func (l *Login) publicOrgRegistrationIsDisallowed(ctx context.Context) (bool, error) { - restrictions, err := l.query.GetInstanceRestrictions(ctx) - return restrictions.DisallowPublicOrgRegistration, err -} - func (d registerOrgFormData) toUserDomain() *domain.Human { if d.Username == "" { d.Username = string(d.Email) diff --git a/internal/api/ui/login/renderer.go b/internal/api/ui/login/renderer.go index d81ab4567e..a50ed2d4ea 100644 --- a/internal/api/ui/login/renderer.go +++ b/internal/api/ui/login/renderer.go @@ -39,7 +39,7 @@ type LanguageData struct { Lang string } -func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage static.Storage, cookieName string) *Renderer { +func CreateRenderer(pathPrefix string, staticStorage static.Storage, cookieName string) *Renderer { r := &Renderer{ pathPrefix: pathPrefix, staticStorage: staticStorage, @@ -238,7 +238,6 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage } var err error r.Renderer, err = renderer.NewRenderer( - staticDir, tmplMapping, funcs, cookieName, ) @@ -343,13 +342,14 @@ func (l *Login) renderInternalError(w http.ResponseWriter, r *http.Request, auth _, msg = l.getErrorMessage(r, err) } - data := l.getBaseData(r, authReq, "Errors.Internal", "", "Internal", msg) - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplError], data, nil) + translator := l.getTranslator(r.Context(), authReq) + data := l.getBaseData(r, authReq, translator, "Errors.Internal", "", "Internal", msg) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplError], data, nil) } -func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) userData { +func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) userData { userData := userData{ - baseData: l.getBaseData(r, authReq, titleI18nKey, descriptionI18nKey, errType, errMessage), + baseData: l.getBaseData(r, authReq, translator, titleI18nKey, descriptionI18nKey, errType, errMessage), profileData: l.getProfileData(authReq), } if authReq != nil && authReq.LinkingUsers != nil { @@ -358,9 +358,7 @@ func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, titleI return userData } -func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) baseData { - translator := l.getTranslator(r.Context(), authReq) - +func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) baseData { title := "" if titleI18nKey != "" { title = translator.LocalizeWithoutArgs(titleI18nKey) @@ -418,7 +416,11 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, titleI } func (l *Login) getTranslator(ctx context.Context, authReq *domain.AuthRequest) *i18n.Translator { - translator, err := l.renderer.NewTranslator(ctx) + restrictions, err := l.query.GetInstanceRestrictions(ctx) + if err != nil { + logging.OnError(err).Warn("cannot load instance restrictions to retrieve allowed languages for creating the translator") + } + translator, err := l.renderer.NewTranslator(ctx, restrictions.AllowedLanguages) logging.OnError(err).Warn("cannot load translator") if authReq != nil { l.addLoginTranslations(translator, authReq.DefaultTranslations) diff --git a/internal/api/ui/login/resources_handler.go b/internal/api/ui/login/resources_handler.go index 6abe666e98..7f263f6b8e 100644 --- a/internal/api/ui/login/resources_handler.go +++ b/internal/api/ui/login/resources_handler.go @@ -7,6 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/api/assets" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/i18n" ) type dynamicResourceData struct { @@ -15,8 +16,8 @@ type dynamicResourceData struct { FileName string `schema:"filename"` } -func (l *Login) handleResources(staticDir http.FileSystem) http.Handler { - return http.FileServer(staticDir) +func (l *Login) handleResources() http.Handler { + return http.FileServer(i18n.LoadFilesystem(i18n.LOGIN)) } func (l *Login) handleDynamicResources(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/router.go b/internal/api/ui/login/router.go index 34a0092607..414ffb1919 100644 --- a/internal/api/ui/login/router.go +++ b/internal/api/ui/login/router.go @@ -64,7 +64,7 @@ var ( } ) -func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.MiddlewareFunc) *mux.Router { +func CreateRouter(login *Login, interceptors ...mux.MiddlewareFunc) *mux.Router { router := mux.NewRouter() router.Use(interceptors...) router.HandleFunc(EndpointRoot, login.handleLogin).Methods(http.MethodGet) @@ -113,7 +113,7 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M router.HandleFunc(EndpointExternalRegisterCallback, login.handleExternalLoginCallback).Methods(http.MethodGet) router.HandleFunc(EndpointLogoutDone, login.handleLogoutDone).Methods(http.MethodGet) router.HandleFunc(EndpointDynamicResources, login.handleDynamicResources).Methods(http.MethodGet) - router.PathPrefix(EndpointResources).Handler(login.handleResources(staticDir)).Methods(http.MethodGet) + router.PathPrefix(EndpointResources).Handler(login.handleResources()).Methods(http.MethodGet) router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrg).Methods(http.MethodGet) router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrgCheck).Methods(http.MethodPost) router.HandleFunc(EndpointLoginSuccess, login.handleLoginSuccess).Methods(http.MethodGet) diff --git a/internal/api/ui/login/select_user_handler.go b/internal/api/ui/login/select_user_handler.go index d1078cbb83..2f9292d7ae 100644 --- a/internal/api/ui/login/select_user_handler.go +++ b/internal/api/ui/login/select_user_handler.go @@ -28,7 +28,7 @@ func (l *Login) renderUserSelection(w http.ResponseWriter, r *http.Request, auth descriptionI18nKey = "SelectAccount.DescriptionLinking" } data := userSelectionData{ - baseData: l.getBaseData(r, authReq, titleI18nKey, descriptionI18nKey, "", ""), + baseData: l.getBaseData(r, authReq, translator, titleI18nKey, descriptionI18nKey, "", ""), Users: selectionData.Users, Linking: linking, } diff --git a/internal/api/ui/login/static/templates/register.html b/internal/api/ui/login/static/templates/register.html index 00cda839cf..8430a56a11 100644 --- a/internal/api/ui/login/static/templates/register.html +++ b/internal/api/ui/login/static/templates/register.html @@ -106,8 +106,8 @@ {{template "error-message" .}}
- - {{t "RegistrationUser.BackButtonText"}} + + diff --git a/internal/api/ui/login/username_change_handler.go b/internal/api/ui/login/username_change_handler.go index 79affe9705..7a497c4eb5 100644 --- a/internal/api/ui/login/username_change_handler.go +++ b/internal/api/ui/login/username_change_handler.go @@ -21,7 +21,7 @@ func (l *Login) renderChangeUsername(w http.ResponseWriter, r *http.Request, aut errID, errMessage = l.getErrorMessage(r, err) } translator := l.getTranslator(r.Context(), authReq) - data := l.getUserData(r, authReq, "UsernameChange.Title", "UsernameChange.Description", errID, errMessage) + data := l.getUserData(r, authReq, translator, "UsernameChange.Title", "UsernameChange.Description", errID, errMessage) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangeUsername], data, nil) } @@ -43,6 +43,6 @@ func (l *Login) handleChangeUsername(w http.ResponseWriter, r *http.Request) { func (l *Login) renderChangeUsernameDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) { var errType, errMessage string translator := l.getTranslator(r.Context(), authReq) - data := l.getUserData(r, authReq, "UsernameChangeDone.Title", "UsernameChangeDone.Description", errType, errMessage) + data := l.getUserData(r, authReq, translator, "UsernameChangeDone.Title", "UsernameChangeDone.Description", errType, errMessage) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangeUsernameDone], data, nil) } diff --git a/internal/command/command.go b/internal/command/command.go index 3b45a9b874..e8e25203ce 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -9,8 +9,11 @@ import ( "math/big" "net/http" "strconv" + "sync" "time" + "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/api/authz" api_http "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command/preparation" @@ -37,12 +40,15 @@ import ( usr_repo "github.com/zitadel/zitadel/internal/repository/user" usr_grant_repo "github.com/zitadel/zitadel/internal/repository/usergrant" "github.com/zitadel/zitadel/internal/static" + "github.com/zitadel/zitadel/internal/telemetry/tracing" webauthn_helper "github.com/zitadel/zitadel/internal/webauthn" ) type Commands struct { httpClient *http.Client + jobs sync.WaitGroup + checkPermission domain.PermissionCheck newCode cryptoCodeFunc newCodeWithDefault cryptoCodeWithDefaultFunc @@ -257,3 +263,54 @@ func samlCertificateAndKeyGenerator(keySize int) func(id string) ([]byte, []byte return pem.EncodeToMemory(keyBlock), pem.EncodeToMemory(certBlock), nil } } + +// Close blocks until all async jobs are finished, +// the context expires or after eventstore.PushTimeout. +func (c *Commands) Close(ctx context.Context) error { + if c.eventstore.PushTimeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, c.eventstore.PushTimeout) + defer cancel() + } + + done := make(chan struct{}) + go func() { + c.jobs.Wait() + close(done) + }() + select { + case <-done: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// asyncPush attempts to push events to the eventstore in a separate Go routine. +// This can be used to speed up request times when the outcome of the push is +// not important for business logic but have a pure logging function. +// For example this can be used for Secret Check Success and Failed events. +// On push error, a log line describing the error will be emitted. +func (c *Commands) asyncPush(ctx context.Context, cmds ...eventstore.Command) { + // Create a new context, as the request scoped context might get + // canceled before we where able to push. + // The eventstore has its own PushTimeout setting, + // so we don't need to have a context with timeout here. + ctx = context.WithoutCancel(ctx) + + c.jobs.Add(1) + + go func() { + defer c.jobs.Done() + localCtx, span := tracing.NewSpan(ctx) + + _, err := c.eventstore.Push(localCtx, cmds...) + if err != nil { + for _, cmd := range cmds { + logging.WithError(err).Errorf("could not push event %q", cmd.Type()) + } + } + + span.EndWithError(err) + }() +} diff --git a/internal/command/command_test.go b/internal/command/command_test.go new file mode 100644 index 0000000000..2367930b89 --- /dev/null +++ b/internal/command/command_test.go @@ -0,0 +1,146 @@ +package command + +import ( + "context" + "io" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/i18n" + "github.com/zitadel/zitadel/internal/repository/user" +) + +var ( + SupportedLanguages = []language.Tag{language.English, language.German} + OnlyAllowedLanguages = []language.Tag{language.English} + AllowedLanguage = language.English + DisallowedLanguage = language.German + UnsupportedLanguage = language.Spanish +) + +func TestMain(m *testing.M) { + i18n.SupportLanguages(SupportedLanguages...) + os.Exit(m.Run()) +} + +func TestCommands_asyncPush(t *testing.T) { + // make sure the test terminates on deadlock + background := context.Background() + agg := user.NewAggregate("userID", "orgID") + cmd := user.NewMachineSecretCheckFailedEvent(background, &agg.Aggregate) + + tests := []struct { + name string + pushCtx func() (context.Context, context.CancelFunc) + eventstore func(*testing.T) *eventstore.Eventstore + closeCtx func() (context.Context, context.CancelFunc) + wantCloseErr bool + }{ + { + name: "push error", + pushCtx: func() (context.Context, context.CancelFunc) { + return context.WithCancel(background) + }, + eventstore: expectEventstore( + expectPushFailed(io.ErrClosedPipe, cmd), + ), + closeCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second) + }, + wantCloseErr: false, + }, + { + name: "success", + pushCtx: func() (context.Context, context.CancelFunc) { + return context.WithCancel(background) + }, + eventstore: expectEventstore( + expectPushSlow(time.Second/10, cmd), + ), + closeCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second) + }, + wantCloseErr: false, + }, + { + name: "success after push context cancels", + pushCtx: func() (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(background) + cancel() + return ctx, cancel + }, + eventstore: expectEventstore( + expectPushSlow(time.Second/10, cmd), + ), + closeCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second) + }, + wantCloseErr: false, + }, + { + name: "success after push context timeout", + pushCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second/100) + }, + eventstore: expectEventstore( + expectPushSlow(time.Second/10, cmd), + ), + closeCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second) + }, + wantCloseErr: false, + }, + { + name: "success after push context timeout", + pushCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second/100) + }, + eventstore: expectEventstore( + expectPushSlow(time.Second/10, cmd), + ), + closeCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second) + }, + wantCloseErr: false, + }, + { + name: "close timeout error", + pushCtx: func() (context.Context, context.CancelFunc) { + return context.WithCancel(background) + }, + eventstore: expectEventstore( + expectPushSlow(time.Second/10, cmd), + ), + closeCtx: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(background, time.Second/100) + }, + wantCloseErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.eventstore(t), + } + c.eventstore.PushTimeout = 10 * time.Second + pushCtx, cancel := tt.pushCtx() + c.asyncPush(pushCtx, cmd) + cancel() + + closeCtx, cancel := tt.closeCtx() + defer cancel() + err := c.Close(closeCtx) + if tt.wantCloseErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/internal/command/instance.go b/internal/command/instance.go index 75325b5b32..6913cdc5bd 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -13,6 +13,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/notification/channels/smtp" "github.com/zitadel/zitadel/internal/repository/feature" @@ -107,18 +108,22 @@ type InstanceSetup struct { EmailTemplate []byte MessageTexts []*domain.CustomMessageText SMTPConfiguration *smtp.Config - OIDCSettings *struct { - AccessTokenLifetime time.Duration - IdTokenLifetime time.Duration - RefreshTokenIdleExpiration time.Duration - RefreshTokenExpiration time.Duration - } - Quotas *struct { - Items []*SetQuota - } - Features map[domain.Feature]any - Limits *SetLimits - Restrictions *SetRestrictions + OIDCSettings *OIDCSettings + Quotas *SetQuotas + Features map[domain.Feature]any + Limits *SetLimits + Restrictions *SetRestrictions +} + +type OIDCSettings struct { + AccessTokenLifetime time.Duration + IdTokenLifetime time.Duration + RefreshTokenIdleExpiration time.Duration + RefreshTokenExpiration time.Duration +} + +type SetQuotas struct { + Items []*SetQuota } type SecretGenerators struct { @@ -289,183 +294,32 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str prepareAddDefaultEmailTemplate(instanceAgg, setup.EmailTemplate), } - - if setup.Quotas != nil { - for _, q := range setup.Quotas.Items { - quotaId, err := c.idGenerator.Next() - if err != nil { - return "", "", nil, nil, err - } - validations = append(validations, c.SetQuotaCommand(quota.NewAggregate(quotaId, instanceID), nil, true, q)) - } + if err := setupQuotas(c, &validations, setup.Quotas, instanceID); err != nil { + return "", "", nil, nil, err } - - for _, msg := range setup.MessageTexts { - validations = append(validations, prepareSetInstanceCustomMessageTexts(instanceAgg, msg)) - } - - console := &addOIDCApp{ - AddApp: AddApp{ - Aggregate: *projectAgg, - ID: setup.zitadel.consoleAppID, - Name: consoleAppName, - }, - Version: domain.OIDCVersionV1, - RedirectUris: []string{}, - ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, - GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeUserAgent, - AuthMethodType: domain.OIDCAuthMethodTypeNone, - PostLogoutRedirectUris: []string{}, - DevMode: !c.externalSecure, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: false, - IDTokenRoleAssertion: false, - IDTokenUserinfoAssertion: false, - ClockSkew: 0, - } - + setupMessageTexts(&validations, setup.MessageTexts, instanceAgg) validations = append(validations, AddOrgCommand(ctx, orgAgg, setup.Org.Name), c.prepareSetDefaultOrg(instanceAgg, orgAgg.ID), ) - - var pat *PersonalAccessToken - var machineKey *MachineKey - // only a human or a machine user should be created as owner - if setup.Org.Machine != nil && setup.Org.Machine.Machine != nil && !setup.Org.Machine.Machine.IsZero() { - validations = append(validations, - AddMachineCommand(userAgg, setup.Org.Machine.Machine), - ) - if setup.Org.Machine.Pat != nil { - pat = NewPersonalAccessToken(orgID, userID, setup.Org.Machine.Pat.ExpirationDate, setup.Org.Machine.Pat.Scopes, domain.UserTypeMachine) - pat.TokenID, err = c.idGenerator.Next() - if err != nil { - return "", "", nil, nil, err - } - validations = append(validations, prepareAddPersonalAccessToken(pat, c.keyAlgorithm)) - } - if setup.Org.Machine.MachineKey != nil { - machineKey = NewMachineKey(orgID, userID, setup.Org.Machine.MachineKey.ExpirationDate, setup.Org.Machine.MachineKey.Type) - machineKey.KeyID, err = c.idGenerator.Next() - if err != nil { - return "", "", nil, nil, err - } - validations = append(validations, prepareAddUserMachineKey(machineKey, c.machineKeySize)) - } - } else if setup.Org.Human != nil { - setup.Org.Human.ID = userID - validations = append(validations, - c.AddHumanCommand(setup.Org.Human, orgID, c.userPasswordHasher, c.userEncryption, true), - ) - } - - validations = append(validations, - c.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner), - c.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMOwner), - AddProjectCommand(projectAgg, zitadelProjectName, userID, false, false, false, domain.PrivateLabelingSettingUnspecified), - SetIAMProject(instanceAgg, projectAgg.ID), - - c.AddAPIAppCommand( - &addAPIApp{ - AddApp: AddApp{ - Aggregate: *projectAgg, - ID: setup.zitadel.mgmtAppID, - Name: mgmtAppName, - }, - AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, - }, - nil, - ), - - c.AddAPIAppCommand( - &addAPIApp{ - AddApp: AddApp{ - Aggregate: *projectAgg, - ID: setup.zitadel.adminAppID, - Name: adminAppName, - }, - AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, - }, - nil, - ), - - c.AddAPIAppCommand( - &addAPIApp{ - AddApp: AddApp{ - Aggregate: *projectAgg, - ID: setup.zitadel.authAppID, - Name: authAppName, - }, - AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, - }, - nil, - ), - - c.AddOIDCAppCommand(console, nil), - SetIAMConsoleID(instanceAgg, &console.ClientID, &setup.zitadel.consoleAppID), - ) - - addGeneratedDomain, err := c.addGeneratedInstanceDomain(ctx, instanceAgg, setup.InstanceName) + pat, machineKey, err := setupAdmin(c, &validations, setup.Org.Machine, setup.Org.Human, orgID, userID, userAgg) if err != nil { return "", "", nil, nil, err } - validations = append(validations, addGeneratedDomain...) - if setup.CustomDomain != "" { - validations = append(validations, - c.addInstanceDomain(instanceAgg, setup.CustomDomain, false), - setPrimaryInstanceDomain(instanceAgg, setup.CustomDomain), - ) + setupMinimalInterfaces(c, &validations, instanceAgg, projectAgg, orgAgg, userID, setup.zitadel) + if err := setupGeneratedDomain(ctx, c, &validations, instanceAgg, setup.InstanceName); err != nil { + return "", "", nil, nil, err } - - if setup.SMTPConfiguration != nil { - validations = append(validations, - c.prepareAddSMTPConfig( - instanceAgg, - setup.SMTPConfiguration.From, - setup.SMTPConfiguration.FromName, - setup.SMTPConfiguration.ReplyToAddress, - setup.SMTPConfiguration.SMTP.Host, - setup.SMTPConfiguration.SMTP.User, - []byte(setup.SMTPConfiguration.SMTP.Password), - setup.SMTPConfiguration.Tls, - ), - ) - } - - if setup.OIDCSettings != nil { - validations = append(validations, - c.prepareAddOIDCSettings( - instanceAgg, - setup.OIDCSettings.AccessTokenLifetime, - setup.OIDCSettings.IdTokenLifetime, - setup.OIDCSettings.RefreshTokenIdleExpiration, - setup.OIDCSettings.RefreshTokenExpiration, - ), - ) - } - - for f, value := range setup.Features { - switch v := value.(type) { - case bool: - wm, err := NewInstanceFeatureWriteModel[feature.Boolean](instanceID, f) - if err != nil { - return "", "", nil, nil, err - } - validations = append(validations, prepareSetFeature(wm, feature.Boolean{Boolean: v}, c.idGenerator)) - default: - return "", "", nil, nil, errors.ThrowInvalidArgument(nil, "INST-GE4tg", "Errors.Feature.TypeNotSupported") - } - } - - if setup.Limits != nil { - validations = append(validations, c.SetLimitsCommand(limitsAgg, &limitsWriteModel{}, setup.Limits)) - } - - if setup.Restrictions != nil { - validations = append(validations, c.SetRestrictionsCommand(restrictionsAgg, &restrictionsWriteModel{}, setup.Restrictions)) + setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain) + setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg) + setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg) + if err := setupFeatures(c, &validations, setup.Features, instanceID); err != nil { + return "", "", nil, nil, err } + setupLimits(c, &validations, limitsAgg, setup.Limits) + setupRestrictions(c, &validations, restrictionsAgg, setup.Restrictions) + //nolint:staticcheck cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) if err != nil { return "", "", nil, nil, err @@ -488,6 +342,205 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str }, nil } +func setupLimits(commands *Commands, validations *[]preparation.Validation, limitsAgg *limits.Aggregate, setLimits *SetLimits) { + if setLimits != nil { + *validations = append(*validations, commands.SetLimitsCommand(limitsAgg, &limitsWriteModel{}, setLimits)) + } +} + +func setupRestrictions(commands *Commands, validations *[]preparation.Validation, restrictionsAgg *restrictions.Aggregate, setRestrictions *SetRestrictions) { + if setRestrictions != nil { + *validations = append(*validations, commands.SetRestrictionsCommand(restrictionsAgg, &restrictionsWriteModel{}, setRestrictions)) + } +} + +func setupQuotas(commands *Commands, validations *[]preparation.Validation, setQuotas *SetQuotas, instanceID string) error { + if setQuotas == nil { + return nil + } + for _, q := range setQuotas.Items { + quotaId, err := commands.idGenerator.Next() + if err != nil { + return err + } + *validations = append(*validations, commands.SetQuotaCommand(quota.NewAggregate(quotaId, instanceID), nil, true, q)) + } + return nil +} + +func setupFeatures(commands *Commands, validations *[]preparation.Validation, enableFeatures map[domain.Feature]any, instanceID string) error { + for f, value := range enableFeatures { + switch v := value.(type) { + case bool: + wm, err := NewInstanceFeatureWriteModel[feature.Boolean](instanceID, f) + if err != nil { + return err + } + *validations = append(*validations, prepareSetFeature(wm, feature.Boolean{Boolean: v}, commands.idGenerator)) + default: + return errors.ThrowInvalidArgument(nil, "INST-GE4tg", "Errors.Feature.TypeNotSupported") + } + } + return nil +} + +func setupOIDCSettings(commands *Commands, validations *[]preparation.Validation, oidcSettings *OIDCSettings, instanceAgg *instance.Aggregate) { + if oidcSettings == nil { + return + } + *validations = append(*validations, + commands.prepareAddOIDCSettings( + instanceAgg, + oidcSettings.AccessTokenLifetime, + oidcSettings.IdTokenLifetime, + oidcSettings.RefreshTokenIdleExpiration, + oidcSettings.RefreshTokenExpiration, + ), + ) +} + +func setupSMTPSettings(commands *Commands, validations *[]preparation.Validation, smtpConfig *smtp.Config, instanceAgg *instance.Aggregate) { + if smtpConfig == nil { + return + } + *validations = append(*validations, + commands.prepareAddSMTPConfig( + instanceAgg, + smtpConfig.From, + smtpConfig.FromName, + smtpConfig.ReplyToAddress, + smtpConfig.SMTP.Host, + smtpConfig.SMTP.User, + []byte(smtpConfig.SMTP.Password), + smtpConfig.Tls, + ), + ) +} + +func setupCustomDomain(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, customDomain string) { + if customDomain == "" { + return + } + *validations = append(*validations, + commands.addInstanceDomain(instanceAgg, customDomain, false), + setPrimaryInstanceDomain(instanceAgg, customDomain), + ) +} + +func setupGeneratedDomain(ctx context.Context, commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, instanceName string) error { + addGeneratedDomain, err := commands.addGeneratedInstanceDomain(ctx, instanceAgg, instanceName) + if err != nil { + return err + } + *validations = append(*validations, addGeneratedDomain...) + return nil +} + +func setupMinimalInterfaces(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, projectAgg *project.Aggregate, orgAgg *org.Aggregate, userID string, ids ZitadelConfig) { + cnsl := &addOIDCApp{ + AddApp: AddApp{ + Aggregate: *projectAgg, + ID: ids.consoleAppID, + Name: consoleAppName, + }, + Version: domain.OIDCVersionV1, + RedirectUris: []string{}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectUris: []string{}, + DevMode: !commands.externalSecure, + AccessTokenType: domain.OIDCTokenTypeBearer, + AccessTokenRoleAssertion: false, + IDTokenRoleAssertion: false, + IDTokenUserinfoAssertion: false, + ClockSkew: 0, + } + *validations = append(*validations, + commands.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner), + commands.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMOwner), + AddProjectCommand(projectAgg, zitadelProjectName, userID, false, false, false, domain.PrivateLabelingSettingUnspecified), + SetIAMProject(instanceAgg, projectAgg.ID), + + commands.AddAPIAppCommand( + &addAPIApp{ + AddApp: AddApp{ + Aggregate: *projectAgg, + ID: ids.mgmtAppID, + Name: mgmtAppName, + }, + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + nil, + ), + + commands.AddAPIAppCommand( + &addAPIApp{ + AddApp: AddApp{ + Aggregate: *projectAgg, + ID: ids.adminAppID, + Name: adminAppName, + }, + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + nil, + ), + + commands.AddAPIAppCommand( + &addAPIApp{ + AddApp: AddApp{ + Aggregate: *projectAgg, + ID: ids.authAppID, + Name: authAppName, + }, + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + nil, + ), + + commands.AddOIDCAppCommand(cnsl, nil), + SetIAMConsoleID(instanceAgg, &cnsl.ClientID, &ids.consoleAppID), + ) +} + +func setupAdmin(commands *Commands, validations *[]preparation.Validation, machine *AddMachine, human *AddHuman, orgID, userID string, userAgg *user.Aggregate) (pat *PersonalAccessToken, machineKey *MachineKey, err error) { + // only a human or a machine user should be created as owner + if machine != nil && machine.Machine != nil && !machine.Machine.IsZero() { + *validations = append(*validations, + AddMachineCommand(userAgg, machine.Machine), + ) + if machine.Pat != nil { + pat = NewPersonalAccessToken(orgID, userID, machine.Pat.ExpirationDate, machine.Pat.Scopes, domain.UserTypeMachine) + pat.TokenID, err = commands.idGenerator.Next() + if err != nil { + return nil, nil, err + } + *validations = append(*validations, prepareAddPersonalAccessToken(pat, commands.keyAlgorithm)) + } + if machine.MachineKey != nil { + machineKey = NewMachineKey(orgID, userID, machine.MachineKey.ExpirationDate, machine.MachineKey.Type) + machineKey.KeyID, err = commands.idGenerator.Next() + if err != nil { + return nil, nil, err + } + *validations = append(*validations, prepareAddUserMachineKey(machineKey, commands.machineKeySize)) + } + } else if human != nil { + human.ID = userID + *validations = append(*validations, + commands.AddHumanCommand(human, orgID, commands.userPasswordHasher, commands.userEncryption, true), + ) + } + return pat, machineKey, nil +} + +func setupMessageTexts(validations *[]preparation.Validation, setupMessageTexts []*domain.CustomMessageText, instanceAgg *instance.Aggregate) { + for _, msg := range setupMessageTexts { + *validations = append(*validations, prepareSetInstanceCustomMessageTexts(instanceAgg, msg)) + } +} + func (c *Commands) UpdateInstance(ctx context.Context, name string) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) validation := c.prepareUpdateInstance(instanceAgg, name) @@ -656,16 +709,27 @@ func (c *Commands) prepareUpdateInstance(a *instance.Aggregate, name string) pre func (c *Commands) prepareSetDefaultLanguage(a *instance.Aggregate, defaultLanguage language.Tag) preparation.Validation { return func() (preparation.CreateCommands, error) { - if defaultLanguage == language.Und { - return nil, errors.ThrowInvalidArgument(nil, "INST-28nlD", "Errors.Invalid.Argument") + if err := domain.LanguageIsDefined(defaultLanguage); err != nil { + return nil, err + } + if err := domain.LanguagesAreSupported(i18n.SupportedLanguages(), defaultLanguage); err != nil { + return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { writeModel, err := getInstanceWriteModel(ctx, filter) + if writeModel.DefaultLanguage == defaultLanguage { + return nil, errors.ThrowPreconditionFailed(nil, "INST-DS3rq", "Errors.Instance.NotChanged") + } + instanceID := authz.GetInstance(ctx).InstanceID() + restrictionsWM, err := c.getRestrictionsWriteModel(ctx, instanceID, instanceID) if err != nil { return nil, err } - if writeModel.DefaultLanguage == defaultLanguage { - return nil, errors.ThrowPreconditionFailed(nil, "INST-DS3rq", "Errors.Instance.NotChanged") + if err := domain.LanguageIsAllowed(false, restrictionsWM.allowedLanguages, defaultLanguage); err != nil { + return nil, err + } + if err != nil { + return nil, err } return []eventstore.Command{instance.NewDefaultLanguageSetEvent(ctx, &a.Aggregate, defaultLanguage)}, nil }, nil diff --git a/internal/command/instance_custom_login_text.go b/internal/command/instance_custom_login_text.go index 7b332b0a15..321d159768 100644 --- a/internal/command/instance_custom_login_text.go +++ b/internal/command/instance_custom_login_text.go @@ -2,16 +2,18 @@ package command import ( "context" - - "github.com/zitadel/zitadel/internal/api/authz" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/repository/instance" ) +// SetCustomInstanceLoginText only validates if the language is supported, not if it is allowed. +// This enables setting texts before allowing a language func (c *Commands) SetCustomInstanceLoginText(ctx context.Context, loginText *domain.CustomLoginText) (*domain.ObjectDetails, error) { iamAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) events, existingMailText, err := c.setCustomInstanceLoginText(ctx, &iamAgg.Aggregate, loginText) @@ -53,8 +55,8 @@ func (c *Commands) RemoveCustomInstanceLoginTexts(ctx context.Context, lang lang } func (c *Commands) setCustomInstanceLoginText(ctx context.Context, instanceAgg *eventstore.Aggregate, text *domain.CustomLoginText) ([]eventstore.Command, *InstanceCustomLoginTextReadModel, error) { - if !text.IsValid() { - return nil, nil, caos_errs.ThrowInvalidArgument(nil, "Instance-kd9fs", "Errors.CustomText.Invalid") + if err := text.IsValid(i18n.SupportedLanguages()); err != nil { + return nil, nil, err } existingLoginText, err := c.defaultLoginTextWriteModelByID(ctx, text.Language) if err != nil { diff --git a/internal/command/instance_custom_login_text_test.go b/internal/command/instance_custom_login_text_test.go index fdb47dff17..8b5603be06 100644 --- a/internal/command/instance_custom_login_text_test.go +++ b/internal/command/instance_custom_login_text_test.go @@ -33,20 +33,54 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { res res }{ { - name: "invalid custom login text, error", + name: "empty custom login text, success", fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), + expectPush(), ), }, args: args{ - ctx: context.Background(), + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + config: &domain.CustomLoginText{ + Language: AllowedLanguage, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "undefined language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), config: &domain.CustomLoginText{}, }, res: res{ err: caos_errs.IsErrorInvalidArgument, }, }, + { + name: "unsupported language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + config: &domain.CustomLoginText{ + Language: UnsupportedLanguage, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, { name: "custom login text set all fields, ok", fields: fields{ diff --git a/internal/command/instance_custom_message_text.go b/internal/command/instance_custom_message_text.go index a70b6c5de8..2f67c6eb2a 100644 --- a/internal/command/instance_custom_message_text.go +++ b/internal/command/instance_custom_message_text.go @@ -9,9 +9,12 @@ import ( "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/repository/instance" ) +// SetDefaultMessageText only validates if the language is supported, not if it is allowed. +// This enables setting texts before allowing a language func (c *Commands) SetDefaultMessageText(ctx context.Context, instanceID string, messageText *domain.CustomMessageText) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(instanceID) events, existingMessageText, err := c.setDefaultMessageText(ctx, &instanceAgg.Aggregate, messageText) @@ -30,8 +33,8 @@ func (c *Commands) SetDefaultMessageText(ctx context.Context, instanceID string, } func (c *Commands) setDefaultMessageText(ctx context.Context, instanceAgg *eventstore.Aggregate, msg *domain.CustomMessageText) ([]eventstore.Command, *InstanceCustomMessageTextWriteModel, error) { - if !msg.IsValid() { - return nil, nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-kd9fs", "Errors.CustomMessageText.Invalid") + if err := msg.IsValid(i18n.SupportedLanguages()); err != nil { + return nil, nil, err } existingMessageText, err := c.defaultCustomMessageTextWriteModelByID(ctx, msg.MessageTextType, msg.Language) @@ -129,8 +132,8 @@ func prepareSetInstanceCustomMessageTexts( msg *domain.CustomMessageText, ) preparation.Validation { return func() (preparation.CreateCommands, error) { - if !msg.IsValid() { - return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-kd9fs", "Errors.CustomMessageText.Invalid") + if err := msg.IsValid(i18n.SupportedLanguages()); err != nil { + return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { existing, err := existingInstanceCustomMessageText(ctx, filter, msg.MessageTextType, msg.Language) diff --git a/internal/command/instance_custom_message_text_test.go b/internal/command/instance_custom_message_text_test.go index b1c8539cab..c7eef8b775 100644 --- a/internal/command/instance_custom_message_text_test.go +++ b/internal/command/instance_custom_message_text_test.go @@ -9,7 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" - caos_errs "github.com/zitadel/zitadel/internal/errors" + zitadel_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/instance" ) @@ -34,19 +34,68 @@ func TestCommandSide_SetDefaultMessageText(t *testing.T) { res res }{ { - name: "invalid custom text, error", + name: "empty message type, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + config: &domain.CustomMessageText{ + Language: AllowedLanguage, + }, + }, + res: res{ + err: zitadel_errs.IsErrorInvalidArgument, + }, + }, + { + name: "empty custom message text, success", fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), + expectPush(), ), }, args: args{ - ctx: context.Background(), - instanceID: "INSTANCE", - config: &domain.CustomMessageText{}, + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + config: &domain.CustomMessageText{ + MessageTextType: "Some type", // TODO: check the type! + Language: AllowedLanguage, + }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "undefined language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + config: &domain.CustomMessageText{}, + }, + res: res{ + err: zitadel_errs.IsErrorInvalidArgument, + }, + }, + { + name: "unsupported language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + config: &domain.CustomMessageText{ + Language: UnsupportedLanguage, + }, + }, + res: res{ + err: zitadel_errs.IsErrorInvalidArgument, }, }, { diff --git a/internal/command/main_test.go b/internal/command/main_test.go index 6c34dfa1e9..a7c447b2fc 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -95,7 +95,13 @@ func eventPusherToEvents(eventsPushes ...eventstore.Command) []*repository.Event func expectPush(commands ...eventstore.Command) expect { return func(m *mock.MockRepository) { - m.ExpectPush(commands) + m.ExpectPush(commands, 0) + } +} + +func expectPushSlow(sleep time.Duration, commands ...eventstore.Command) expect { + return func(m *mock.MockRepository) { + m.ExpectPush(commands, sleep) } } @@ -212,7 +218,7 @@ func (m *mockInstance) ConsoleApplicationID() string { } func (m *mockInstance) DefaultLanguage() language.Tag { - return language.English + return AllowedLanguage } func (m *mockInstance) DefaultOrganisationID() string { diff --git a/internal/command/org_custom_login_text.go b/internal/command/org_custom_login_text.go index 83a846d247..9c2a709560 100644 --- a/internal/command/org_custom_login_text.go +++ b/internal/command/org_custom_login_text.go @@ -8,9 +8,12 @@ import ( "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/repository/org" ) +// SetOrgLoginText only validates if the language is supported, not if it is allowed. +// This enables setting texts before allowing a language func (c *Commands) SetOrgLoginText(ctx context.Context, resourceOwner string, loginText *domain.CustomLoginText) (*domain.ObjectDetails, error) { if resourceOwner == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-m29rF", "Errors.ResourceOwnerMissing") @@ -32,10 +35,9 @@ func (c *Commands) SetOrgLoginText(ctx context.Context, resourceOwner string, lo } func (c *Commands) setOrgLoginText(ctx context.Context, orgAgg *eventstore.Aggregate, loginText *domain.CustomLoginText) ([]eventstore.Command, *OrgCustomLoginTextReadModel, error) { - if !loginText.IsValid() { - return nil, nil, caos_errs.ThrowInvalidArgument(nil, "ORG-PPo2w", "Errors.CustomText.Invalid") + if err := loginText.IsValid(i18n.SupportedLanguages()); err != nil { + return nil, nil, err } - existingLoginText, err := c.orgCustomLoginTextWriteModelByID(ctx, orgAgg.ID, loginText.Language) if err != nil { return nil, nil, err diff --git a/internal/command/org_custom_login_text_test.go b/internal/command/org_custom_login_text_test.go index 19ddb189cf..419ee724d6 100644 --- a/internal/command/org_custom_login_text_test.go +++ b/internal/command/org_custom_login_text_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" @@ -40,22 +41,44 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { ), }, args: args{ - ctx: context.Background(), - config: &domain.CustomLoginText{}, + ctx: authz.WithInstanceID(context.Background(), "org1"), + config: &domain.CustomLoginText{ + Language: AllowedLanguage, + }, }, res: res{ err: caos_errs.IsErrorInvalidArgument, }, }, { - name: "invalid custom login text, error", + name: "empty custom login text, success", fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), + expectPush(), ), }, args: args{ - ctx: context.Background(), + ctx: authz.WithInstanceID(context.Background(), "org1"), + resourceOwner: "org1", + config: &domain.CustomLoginText{ + Language: AllowedLanguage, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "undefined language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "org1"), resourceOwner: "org1", config: &domain.CustomLoginText{}, }, @@ -63,6 +86,22 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { err: caos_errs.IsErrorInvalidArgument, }, }, + { + name: "unsupported language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "org1"), + resourceOwner: "org1", + config: &domain.CustomLoginText{ + Language: UnsupportedLanguage, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, { name: "custom login text set all fields, ok", fields: fields{ diff --git a/internal/command/org_custom_message_text.go b/internal/command/org_custom_message_text.go index 7aacd6a35a..8676fefad0 100644 --- a/internal/command/org_custom_message_text.go +++ b/internal/command/org_custom_message_text.go @@ -8,9 +8,12 @@ import ( "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/repository/org" ) +// SetOrgMessageText only validates if the language is supported, not if it is allowed. +// This enables setting texts before allowing a language func (c *Commands) SetOrgMessageText(ctx context.Context, resourceOwner string, messageText *domain.CustomMessageText) (*domain.ObjectDetails, error) { if resourceOwner == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-2biiR", "Errors.ResourceOwnerMissing") @@ -32,10 +35,9 @@ func (c *Commands) SetOrgMessageText(ctx context.Context, resourceOwner string, } func (c *Commands) setOrgMessageText(ctx context.Context, orgAgg *eventstore.Aggregate, message *domain.CustomMessageText) ([]eventstore.Command, *OrgCustomMessageTextReadModel, error) { - if !message.IsValid() { - return nil, nil, caos_errs.ThrowInvalidArgument(nil, "ORG-2jfsf", "Errors.CustomText.Invalid") + if err := message.IsValid(i18n.SupportedLanguages()); err != nil { + return nil, nil, err } - existingMessageText, err := c.orgCustomMessageTextWriteModelByID(ctx, orgAgg.ID, message.MessageTextType, message.Language) if err != nil { return nil, nil, err diff --git a/internal/command/org_custom_message_text_test.go b/internal/command/org_custom_message_text_test.go index 61a562e456..8b24ff5c53 100644 --- a/internal/command/org_custom_message_text_test.go +++ b/internal/command/org_custom_message_text_test.go @@ -8,7 +8,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" - caos_errs "github.com/zitadel/zitadel/internal/errors" + zitadel_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/org" ) @@ -35,32 +35,83 @@ func TestCommandSide_SetCustomMessageText(t *testing.T) { { name: "no resource owner, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: eventstoreExpect(t), }, args: args{ ctx: context.Background(), config: &domain.CustomMessageText{}, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { - name: "invalid custom text, error", + name: "empty message type, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + config: &domain.CustomMessageText{ + Language: AllowedLanguage, + }, + }, + res: res{ + err: zitadel_errs.IsErrorInvalidArgument, + }, + }, + { + name: "empty custom message text, success", fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), + expectPush(), ), }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + config: &domain.CustomMessageText{ + MessageTextType: "Some type", // TODO: check the type! + Language: AllowedLanguage, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "undefined language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, args: args{ ctx: context.Background(), resourceOwner: "org1", config: &domain.CustomMessageText{}, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, + }, + }, + { + name: "unsupported language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + config: &domain.CustomMessageText{ + Language: UnsupportedLanguage, + }, + }, + res: res{ + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -345,7 +396,7 @@ func TestCommandSide_RemoveCustomMessageText(t *testing.T) { lang: language.English, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -361,7 +412,7 @@ func TestCommandSide_RemoveCustomMessageText(t *testing.T) { lang: language.English, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -377,7 +428,7 @@ func TestCommandSide_RemoveCustomMessageText(t *testing.T) { mailTextType: "Template", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -471,6 +522,43 @@ func TestCommandSide_RemoveCustomMessageText(t *testing.T) { }, }, }, + { + name: "remove unsupported language ok, especially because we never validated whether a language is supported in previous ZITADEL versions", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "Template", + domain.MessageGreeting, + "Greeting", + UnsupportedLanguage, + ), + ), + ), + expectPush( + org.NewCustomTextTemplateRemovedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "Template", + UnsupportedLanguage, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + mailTextType: "Template", + lang: UnsupportedLanguage, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/command/org_test.go b/internal/command/org_test.go index abaeb8d351..9bfe32a1c7 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -141,7 +141,7 @@ func TestCommandSide_AddOrg(t *testing.T) { "lastname1", "nickname1", "displayname1", - language.German, + language.English, domain.GenderMale, "email1", true, @@ -185,7 +185,7 @@ func TestCommandSide_AddOrg(t *testing.T) { "lastname1", "nickname1", "displayname1", - language.German, + language.English, domain.GenderMale, "email1", true, @@ -253,7 +253,7 @@ func TestCommandSide_AddOrg(t *testing.T) { "lastname1", "nickname1", "displayname1", - language.German, + language.English, domain.GenderMale, "email1", true, @@ -321,7 +321,7 @@ func TestCommandSide_AddOrg(t *testing.T) { "lastname1", "nickname1", "displayname1", - language.German, + language.English, domain.GenderMale, "email1", true, @@ -392,7 +392,7 @@ func TestCommandSide_AddOrg(t *testing.T) { "lastname1", "nickname1", "displayname1", - language.German, + language.English, domain.GenderMale, "email1", true, @@ -1181,7 +1181,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { "lastname1", "nickname1", "displayname1", - language.German, + language.English, domain.GenderMale, "email1", false, diff --git a/internal/command/restrictions.go b/internal/command/restrictions.go index 1658c99592..b386ce8287 100644 --- a/internal/command/restrictions.go +++ b/internal/command/restrictions.go @@ -3,16 +3,38 @@ package command import ( "context" + "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/errors" + zitadel_errors "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/repository/restrictions" ) type SetRestrictions struct { DisallowPublicOrgRegistration *bool + AllowedLanguages []language.Tag +} + +func (s *SetRestrictions) Validate(defaultLanguage language.Tag) error { + if s == nil || (s.DisallowPublicOrgRegistration == nil && s.AllowedLanguages == nil) { + return zitadel_errors.ThrowInvalidArgument(nil, "COMMAND-oASwj", "Errors.Restrictions.NoneSpecified") + } + if s.AllowedLanguages != nil { + if err := domain.LanguagesHaveDuplicates(s.AllowedLanguages); err != nil { + return err + } + if err := domain.LanguagesAreSupported(i18n.SupportedLanguages(), s.AllowedLanguages...); err != nil { + return err + } + if err := domain.LanguageIsAllowed(false, s.AllowedLanguages, defaultLanguage); err != nil { + return zitadel_errors.ThrowPreconditionFailedf(err, "COMMAND-L0m2u", "Errors.Restrictions.DefaultLanguageMustBeAllowed") + } + } + return nil } // SetRestrictions creates new restrictions or updates existing restrictions. @@ -60,10 +82,10 @@ func (c *Commands) getRestrictionsWriteModel(ctx context.Context, instanceId, re func (c *Commands) SetRestrictionsCommand(a *restrictions.Aggregate, wm *restrictionsWriteModel, setRestrictions *SetRestrictions) preparation.Validation { return func() (preparation.CreateCommands, error) { - if setRestrictions == nil || setRestrictions.DisallowPublicOrgRegistration == nil { - return nil, errors.ThrowInvalidArgument(nil, "COMMAND-oASwj", "Errors.Restrictions.NoneSpecified") - } return func(ctx context.Context, _ preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + if err := setRestrictions.Validate(authz.GetInstance(ctx).DefaultLanguage()); err != nil { + return nil, err + } changes := wm.NewChanges(setRestrictions) if len(changes) == 0 { return nil, nil diff --git a/internal/command/restrictions_model.go b/internal/command/restrictions_model.go index cabf1981ac..81ada1f4f1 100644 --- a/internal/command/restrictions_model.go +++ b/internal/command/restrictions_model.go @@ -1,13 +1,17 @@ package command import ( + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/restrictions" ) type restrictionsWriteModel struct { eventstore.WriteModel - disallowPublicOrgRegistrations bool + disallowPublicOrgRegistration bool + allowedLanguages []language.Tag } // newRestrictionsWriteModel aggregateId is filled by reducing unit matching events @@ -34,8 +38,15 @@ func (wm *restrictionsWriteModel) Query() *eventstore.SearchQueryBuilder { func (wm *restrictionsWriteModel) Reduce() error { for _, event := range wm.Events { wm.ChangeDate = event.CreatedAt() - if e, ok := event.(*restrictions.SetEvent); ok && e.DisallowPublicOrgRegistrations != nil { - wm.disallowPublicOrgRegistrations = *e.DisallowPublicOrgRegistrations + e, ok := event.(*restrictions.SetEvent) + if !ok { + continue + } + if e.DisallowPublicOrgRegistration != nil { + wm.disallowPublicOrgRegistration = *e.DisallowPublicOrgRegistration + } + if e.AllowedLanguages != nil { + wm.allowedLanguages = *e.AllowedLanguages } } return wm.WriteModel.Reduce() @@ -48,8 +59,11 @@ func (wm *restrictionsWriteModel) NewChanges(setRestrictions *SetRestrictions) ( return nil } changes = make([]restrictions.RestrictionsChange, 0, 1) - if setRestrictions.DisallowPublicOrgRegistration != nil && (wm.disallowPublicOrgRegistrations != *setRestrictions.DisallowPublicOrgRegistration) { - changes = append(changes, restrictions.ChangePublicOrgRegistrations(*setRestrictions.DisallowPublicOrgRegistration)) + if setRestrictions.DisallowPublicOrgRegistration != nil && (wm.disallowPublicOrgRegistration != *setRestrictions.DisallowPublicOrgRegistration) { + changes = append(changes, restrictions.ChangeDisallowPublicOrgRegistration(*setRestrictions.DisallowPublicOrgRegistration)) + } + if setRestrictions.AllowedLanguages != nil && domain.LanguagesDiffer(wm.allowedLanguages, setRestrictions.AllowedLanguages) { + changes = append(changes, restrictions.ChangeAllowedLanguages(setRestrictions.AllowedLanguages)) } return changes } diff --git a/internal/command/restrictions_test.go b/internal/command/restrictions_test.go index 1fcefb8065..6f4bf4ee9d 100644 --- a/internal/command/restrictions_test.go +++ b/internal/command/restrictions_test.go @@ -6,6 +6,7 @@ import ( "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" + "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" @@ -19,7 +20,6 @@ import ( func TestSetRestrictions(t *testing.T) { type fields func(*testing.T) (*eventstore.Eventstore, id.Generator) type args struct { - ctx context.Context setRestrictions *SetRestrictions } type res struct { @@ -40,14 +40,14 @@ func TestSetRestrictions(t *testing.T) { expectFilter(), expectPush( eventFromEventPusherWithInstanceID( - "instance1", + "INSTANCE", restrictions.NewSetEvent( eventstore.NewBaseEventForPush( context.Background(), - &restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate, + &restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate, restrictions.SetEventType, ), - restrictions.ChangePublicOrgRegistrations(true), + restrictions.ChangeDisallowPublicOrgRegistration(true), ), ), ), @@ -55,14 +55,13 @@ func TestSetRestrictions(t *testing.T) { id_mock.NewIDGeneratorExpectIDs(t, "restrictions1") }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "instance1"), setRestrictions: &SetRestrictions{ DisallowPublicOrgRegistration: gu.Ptr(true), }, }, res: res{ want: &domain.ObjectDetails{ - ResourceOwner: "instance1", + ResourceOwner: "INSTANCE", }, }, }, @@ -76,23 +75,23 @@ func TestSetRestrictions(t *testing.T) { restrictions.NewSetEvent( eventstore.NewBaseEventForPush( context.Background(), - &restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate, + &restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate, restrictions.SetEventType, ), - restrictions.ChangePublicOrgRegistrations(true), + restrictions.ChangeDisallowPublicOrgRegistration(true), ), ), ), expectPush( eventFromEventPusherWithInstanceID( - "instance1", + "INSTANCE", restrictions.NewSetEvent( eventstore.NewBaseEventForPush( context.Background(), - &restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate, + &restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate, restrictions.SetEventType, ), - restrictions.ChangePublicOrgRegistrations(false), + restrictions.ChangeDisallowPublicOrgRegistration(false), ), ), ), @@ -100,14 +99,13 @@ func TestSetRestrictions(t *testing.T) { nil }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "instance1"), setRestrictions: &SetRestrictions{ DisallowPublicOrgRegistration: gu.Ptr(false), }, }, res: res{ want: &domain.ObjectDetails{ - ResourceOwner: "instance1", + ResourceOwner: "INSTANCE", }, }, }, @@ -121,10 +119,10 @@ func TestSetRestrictions(t *testing.T) { restrictions.NewSetEvent( eventstore.NewBaseEventForPush( context.Background(), - &restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate, + &restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate, restrictions.SetEventType, ), - restrictions.ChangePublicOrgRegistrations(true), + restrictions.ChangeDisallowPublicOrgRegistration(true), ), ), ), @@ -132,14 +130,13 @@ func TestSetRestrictions(t *testing.T) { nil }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "instance1"), setRestrictions: &SetRestrictions{ DisallowPublicOrgRegistration: gu.Ptr(true), }, }, res: res{ want: &domain.ObjectDetails{ - ResourceOwner: "instance1", + ResourceOwner: "INSTANCE", }, }, }, @@ -152,29 +149,82 @@ func TestSetRestrictions(t *testing.T) { restrictions.NewSetEvent( eventstore.NewBaseEventForPush( context.Background(), - &restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate, + &restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate, restrictions.SetEventType, ), - restrictions.ChangePublicOrgRegistrations(true), + restrictions.ChangeDisallowPublicOrgRegistration(true), ), ), ), ), nil }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "instance1"), setRestrictions: &SetRestrictions{}, }, res: res{ err: zitadel_errs.IsErrorInvalidArgument, }, }, + { + name: "unsupported language restricted", + fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) { + return eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + restrictions.NewSetEvent( + eventstore.NewBaseEventForPush( + context.Background(), + &restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate, + restrictions.SetEventType, + ), + restrictions.ChangeAllowedLanguages(SupportedLanguages), + ), + ), + ), + ), nil + }, + args: args{ + setRestrictions: &SetRestrictions{ + AllowedLanguages: []language.Tag{AllowedLanguage, UnsupportedLanguage}, + }, + }, + res: res{ + err: zitadel_errs.IsErrorInvalidArgument, + }, + }, + { + name: "default language not allowed", + fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) { + return eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + restrictions.NewSetEvent( + eventstore.NewBaseEventForPush( + context.Background(), + &restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate, + restrictions.SetEventType, + ), + restrictions.ChangeAllowedLanguages(OnlyAllowedLanguages), + ), + ), + ), + ), nil + }, + args: args{ + setRestrictions: &SetRestrictions{ + AllowedLanguages: []language.Tag{DisallowedLanguage}, + }, + }, + res: res{ + err: zitadel_errs.IsPreconditionFailed, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := new(Commands) r.eventstore, r.idGenerator = tt.fields(t) - got, err := r.SetInstanceRestrictions(tt.args.ctx, tt.args.setRestrictions) + got, err := r.SetInstanceRestrictions(authz.WithInstance(context.Background(), &mockInstance{}), tt.args.setRestrictions) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_human_profile_test.go b/internal/command/user_human_profile_test.go index aea3ef562e..e8849adf11 100644 --- a/internal/command/user_human_profile_test.go +++ b/internal/command/user_human_profile_test.go @@ -8,7 +8,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" - caos_errs "github.com/zitadel/zitadel/internal/errors" + zitadel_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/user" @@ -36,8 +36,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: eventstoreExpect(t, expectFilter(), ), }, @@ -51,13 +50,13 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { LastName: "lastname", NickName: "nickname", DisplayName: "displayname", - PreferredLanguage: language.German, + PreferredLanguage: AllowedLanguage, Gender: domain.GenderFemale, }, resourceOwner: "org1", }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { @@ -74,7 +73,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { "lastname", "nickname", "displayname", - language.German, + AllowedLanguage, domain.GenderFemale, "email", true, @@ -93,13 +92,13 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { LastName: "lastname", NickName: "nickname", DisplayName: "displayname", - PreferredLanguage: language.German, + PreferredLanguage: AllowedLanguage, Gender: domain.GenderFemale, }, resourceOwner: "org1", }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { @@ -116,7 +115,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { "lastname", "nickname", "displayname", - language.German, + DisallowedLanguage, domain.GenderUnspecified, "email", true, @@ -130,7 +129,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { "lastname2", "nickname2", "displayname2", - language.English, + AllowedLanguage, domain.GenderMale, ), ), @@ -146,7 +145,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { LastName: "lastname2", NickName: "nickname2", DisplayName: "displayname2", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, Gender: domain.GenderMale, }, resourceOwner: "org1", @@ -161,7 +160,133 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { LastName: "lastname2", NickName: "nickname2", DisplayName: "displayname2", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, + Gender: domain.GenderMale, + }, + }, + }, + { + name: "undefined preferred language, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + DisallowedLanguage, + domain.GenderUnspecified, + "email", + true, + ), + ), + ), + expectPush( + newProfileChangedEvent(context.Background(), + "user1", "org1", + "firstname2", + "lastname2", + "nickname2", + "displayname2", + language.Und, + domain.GenderMale, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + address: &domain.Profile{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + FirstName: "firstname2", + LastName: "lastname2", + NickName: "nickname2", + DisplayName: "displayname2", + Gender: domain.GenderMale, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.Profile{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + FirstName: "firstname2", + LastName: "lastname2", + NickName: "nickname2", + DisplayName: "displayname2", + PreferredLanguage: language.Und, + Gender: domain.GenderMale, + }, + }, + }, { + name: "unsupported preferred language, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + DisallowedLanguage, + domain.GenderUnspecified, + "email", + true, + ), + ), + ), + expectPush( + newProfileChangedEvent(context.Background(), + "user1", "org1", + "firstname2", + "lastname2", + "nickname2", + "displayname2", + UnsupportedLanguage, + domain.GenderMale, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + address: &domain.Profile{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + FirstName: "firstname2", + LastName: "lastname2", + NickName: "nickname2", + DisplayName: "displayname2", + PreferredLanguage: UnsupportedLanguage, + Gender: domain.GenderMale, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.Profile{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + FirstName: "firstname2", + LastName: "lastname2", + NickName: "nickname2", + DisplayName: "displayname2", + PreferredLanguage: UnsupportedLanguage, Gender: domain.GenderMale, }, }, diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index 6e630f8885..d8ee4f1333 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -14,7 +14,7 @@ import ( "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" - caos_errs "github.com/zitadel/zitadel/internal/errors" + zitadel_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/id" @@ -74,7 +74,7 @@ func TestCommandSide_AddHuman(t *testing.T) { }, res: res{ err: func(err error) bool { - return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")) + return errors.Is(err, zitadel_errs.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")) }, }, }, @@ -94,7 +94,7 @@ func TestCommandSide_AddHuman(t *testing.T) { }, res: res{ err: func(err error) bool { - return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty")) + return errors.Is(err, zitadel_errs.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty")) }, }, }, @@ -104,7 +104,7 @@ func TestCommandSide_AddHuman(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - newAddHumanEvent("$plain$x$password", true, true, ""), + newAddHumanEvent("$plain$x$password", true, true, "", AllowedLanguage), ), ), ), @@ -120,18 +120,19 @@ func TestCommandSide_AddHuman(t *testing.T) { Email: Email{ Address: "email@test.ch", }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, allowInitMail: true, }, res: res{ err: func(err error) bool { - return errors.Is(err, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting")) + return errors.Is(err, zitadel_errs.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting")) }, }, }, { name: "domain policy not found, precondition error", + fields: fields{ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), eventstore: expectEventstore( @@ -150,13 +151,13 @@ func TestCommandSide_AddHuman(t *testing.T) { Email: Email{ Address: "email@test.ch", }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, allowInitMail: true, }, res: res{ err: func(err error) bool { - return errors.Is(err, caos_errs.ThrowInternal(nil, "USER-Ggk9n", "Errors.Internal")) + return errors.Is(err, zitadel_errs.ThrowInternal(nil, "USER-Ggk9n", "Errors.Internal")) }, }, }, @@ -192,18 +193,18 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", Verified: true, }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, allowInitMail: true, }, res: res{ err: func(err error) bool { - return errors.Is(err, caos_errs.ThrowInternal(nil, "USER-uQ96e", "Errors.Internal")) + return errors.Is(err, zitadel_errs.ThrowInternal(nil, "USER-uQ96e", "Errors.Internal")) }, }, }, { - name: "add human (with initial code), ok", + name: "add human with undefined preferred language, ok", fields: fields{ eventstore: expectEventstore( expectFilter(), @@ -225,7 +226,7 @@ func TestCommandSide_AddHuman(t *testing.T) { "lastname", "", "firstname lastname", - language.English, + language.Und, domain.GenderUnspecified, "email@test.ch", true, @@ -256,7 +257,142 @@ func TestCommandSide_AddHuman(t *testing.T) { Email: Email{ Address: "email@test.ch", }, - PreferredLanguage: language.English, + }, + secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + wantID: "user1", + }, + }, + { + name: "add human with unsupported preferred language, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &userAgg.Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewHumanAddedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + UnsupportedLanguage, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &userAgg.Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("userinit"), + }, + time.Hour*1, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + newCode: mockCode("userinit", time.Hour), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", + }, + PreferredLanguage: UnsupportedLanguage, + }, + secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + wantID: "user1", + }, + }, + { + name: "add human (with initial code), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &userAgg.Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewHumanAddedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + AllowedLanguage, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &userAgg.Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("userinit"), + }, + time.Hour*1, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + newCode: mockCode("userinit", time.Hour), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", + }, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -298,7 +434,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", false, true, ""), + newAddHumanEvent("$plain$x$password", false, true, "", AllowedLanguage), user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -327,7 +463,7 @@ func TestCommandSide_AddHuman(t *testing.T) { Email: Email{ Address: "email@test.ch", }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -367,7 +503,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", false, true, ""), + newAddHumanEvent("$plain$x$password", false, true, "", AllowedLanguage), user.NewHumanEmailCodeAddedEventV2(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -399,7 +535,7 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", URLTemplate: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}", }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: false, @@ -439,7 +575,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", false, true, ""), + newAddHumanEvent("$plain$x$password", false, true, "", AllowedLanguage), user.NewHumanEmailCodeAddedEventV2(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -471,7 +607,7 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", ReturnCode: true, }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: false, @@ -512,7 +648,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", true, true, ""), + newAddHumanEvent("$plain$x$password", true, true, "", AllowedLanguage), user.NewHumanEmailVerifiedEvent(context.Background(), &userAgg.Aggregate, ), @@ -534,8 +670,8 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", Verified: true, }, - PreferredLanguage: language.English, PasswordChangeRequired: true, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -575,7 +711,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", true, true, ""), + newAddHumanEvent("$plain$x$password", true, true, "", AllowedLanguage), user.NewHumanEmailVerifiedEvent(context.Background(), &userAgg.Aggregate, ), @@ -597,8 +733,8 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", Verified: true, }, - PreferredLanguage: language.English, PasswordChangeRequired: true, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -638,7 +774,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", true, false, ""), + newAddHumanEvent("$plain$x$password", true, false, "", AllowedLanguage), user.NewHumanEmailVerifiedEvent(context.Background(), &userAgg.Aggregate, ), @@ -660,8 +796,8 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", Verified: true, }, - PreferredLanguage: language.English, PasswordChangeRequired: true, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -713,15 +849,15 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", Verified: true, }, - PreferredLanguage: language.English, PasswordChangeRequired: true, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, }, res: res{ err: func(err error) bool { - return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername")) + return errors.Is(err, zitadel_errs.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername")) }, }, }, @@ -769,7 +905,7 @@ func TestCommandSide_AddHuman(t *testing.T) { "lastname", "", "firstname lastname", - language.English, + AllowedLanguage, domain.GenderUnspecified, "email@test.ch", false, @@ -798,8 +934,8 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", Verified: true, }, - PreferredLanguage: language.English, PasswordChangeRequired: true, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -840,7 +976,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", false, true, "+41711234567"), + newAddHumanEvent("$plain$x$password", false, true, "+41711234567", AllowedLanguage), user.NewHumanEmailVerifiedEvent( context.Background(), &userAgg.Aggregate, @@ -877,7 +1013,7 @@ func TestCommandSide_AddHuman(t *testing.T) { Phone: Phone{ Number: "+41711234567", }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -905,7 +1041,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("", false, true, "+41711234567"), + newAddHumanEvent("", false, true, "+41711234567", AllowedLanguage), user.NewHumanInitialCodeAddedEvent( context.Background(), &userAgg.Aggregate, @@ -941,7 +1077,7 @@ func TestCommandSide_AddHuman(t *testing.T) { Number: "+41711234567", Verified: true, }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -980,7 +1116,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", false, true, "+41711234567"), + newAddHumanEvent("$plain$x$password", false, true, "+41711234567", AllowedLanguage), user.NewHumanEmailVerifiedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate), user.NewHumanPhoneCodeAddedEventV2( @@ -1018,7 +1154,7 @@ func TestCommandSide_AddHuman(t *testing.T) { Number: "+41711234567", ReturnCode: true, }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -1046,7 +1182,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("", false, true, ""), + newAddHumanEvent("", false, true, "", AllowedLanguage), user.NewHumanInitialCodeAddedEvent( context.Background(), &userAgg.Aggregate, @@ -1080,13 +1216,13 @@ func TestCommandSide_AddHuman(t *testing.T) { Email: Email{ Address: "email@test.ch", }, - PreferredLanguage: language.English, Metadata: []*AddMetadataEntry{ { Key: "testKey", Value: []byte("testValue"), }, }, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -1147,206 +1283,218 @@ func TestCommandSide_ImportHuman(t *testing.T) { err func(error) bool } tests := []struct { - name string - fields fields - args args - res res + name string + given func(t *testing.T) (fields, args) + res res }{ { name: "orgid missing, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - orgID: "", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + ), }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - }, + args{ + ctx: context.Background(), + orgID: "", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + } }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { name: "org policy not found, precondition error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - expectFilter(), - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter(), + ), }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + } }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { name: "password policy not found, precondition error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), ), + expectFilter(), + expectFilter(), ), - ), - expectFilter(), - expectFilter(), - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + } }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { name: "user invalid, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + 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, + ), + ), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", }, - }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + PreferredLanguage: AllowedLanguage, + }, + }, + } }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, - }, - { + }, { name: "add human (with password and initial code), ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + 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", true, true, "", AllowedLanguage), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectPush( - newAddHumanEvent("$plain$x$password", true, true, ""), - user.NewHumanInitialCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("a"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: true, }, - time.Hour*1, - ), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Password: &domain.Password{ - SecretString: "password", - ChangeRequired: true, - }, - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - }, - secretGenerator: GetMockSecretGenerator(t), + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } }, res: res{ wantHuman: &domain.Human{ @@ -1359,7 +1507,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -1370,61 +1518,63 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, { name: "add human email verified password change not required, ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + 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), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectPush( - newAddHumanEvent("$plain$x$password", false, true, ""), - user.NewHumanEmailVerifiedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - ), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Password: &domain.Password{ - SecretString: "password", - ChangeRequired: false, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), }, - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - IsEmailVerified: true, - }, - }, - secretGenerator: GetMockSecretGenerator(t), + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: false, + }, + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } }, res: res{ wantHuman: &domain.Human{ @@ -1437,7 +1587,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -1449,70 +1599,72 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, { name: "add human email verified passwordless only, ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + 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, + ), + ), + ), + expectFilter(), + expectPush( + newAddHumanEvent("", false, true, "", AllowedLanguage), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + user.NewHumanPasswordlessInitCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "code1", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour, + ), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectFilter(), - expectPush( - newAddHumanEvent("", false, true, ""), - user.NewHumanEmailVerifiedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), - user.NewHumanPasswordlessInitCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - "code1", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("a"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, - time.Hour, - ), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - IsEmailVerified: true, - }, - }, - passwordless: true, - secretGenerator: GetMockSecretGenerator(t), - passwordlessInitCode: GetMockSecretGenerator(t), + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + passwordless: true, + secretGenerator: GetMockSecretGenerator(t), + passwordlessInitCode: GetMockSecretGenerator(t), + } }, res: res{ wantHuman: &domain.Human{ @@ -1525,7 +1677,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -1547,74 +1699,76 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, { name: "add human email verified passwordless and password change not required, ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + 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, + ), + ), + ), + expectFilter(), + expectPush( + newAddHumanEvent("$plain$x$password", false, true, "", AllowedLanguage), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + user.NewHumanPasswordlessInitCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "code1", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour, + ), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectFilter(), - expectPush( - newAddHumanEvent("$plain$x$password", false, true, ""), - user.NewHumanEmailVerifiedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), - user.NewHumanPasswordlessInitCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - "code1", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("a"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: false, }, - time.Hour, - ), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Password: &domain.Password{ - SecretString: "password", - ChangeRequired: false, - }, - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - IsEmailVerified: true, - }, - }, - passwordless: true, - secretGenerator: GetMockSecretGenerator(t), - passwordlessInitCode: GetMockSecretGenerator(t), + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + passwordless: true, + secretGenerator: GetMockSecretGenerator(t), + passwordlessInitCode: GetMockSecretGenerator(t), + } }, res: res{ wantHuman: &domain.Human{ @@ -1627,7 +1781,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -1649,79 +1803,81 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, { name: "add human (with phone), ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + 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, "+41711234567", AllowedLanguage), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + user.NewHumanPhoneCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectPush( - newAddHumanEvent("$plain$x$password", false, true, "+41711234567"), - user.NewHumanInitialCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("a"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, - time.Hour*1, - ), - user.NewHumanPhoneCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("a"), + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: false, }, - time.Hour*1), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, - }, - Password: &domain.Password{ - SecretString: "password", - ChangeRequired: false, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - Phone: &domain.Phone{ - PhoneNumber: "+41711234567", - }, - }, - secretGenerator: GetMockSecretGenerator(t), + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Phone: &domain.Phone{ + PhoneNumber: "+41711234567", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } }, res: res{ wantHuman: &domain.Human{ @@ -1734,7 +1890,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -1748,73 +1904,75 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, { name: "add human (with verified phone), ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + 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, "+41711234567", AllowedLanguage), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + user.NewHumanPhoneVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectPush( - newAddHumanEvent("$plain$x$password", false, true, "+41711234567"), - user.NewHumanInitialCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("a"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, - time.Hour*1, - ), - user.NewHumanPhoneVerifiedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, - }, - Password: &domain.Password{ - SecretString: "password", - ChangeRequired: false, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - Phone: &domain.Phone{ - PhoneNumber: "+41711234567", - IsPhoneVerified: true, - }, - }, - secretGenerator: GetMockSecretGenerator(t), + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: false, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Phone: &domain.Phone{ + PhoneNumber: "+41711234567", + IsPhoneVerified: true, + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } }, res: res{ wantHuman: &domain.Human{ @@ -1827,7 +1985,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -1840,127 +1998,69 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, { - name: "add human (with idp), ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + name: "add human (with undefined preferred language), ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + 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, "", language.Und), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectFilter( - eventFromEventPusher( - org.NewIDPConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "idpID", - "name", - domain.IDPConfigTypeOIDC, - domain.IDPConfigStylingTypeUnspecified, - false, - ), - ), - eventFromEventPusher( - org.NewIDPOIDCConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "clientID", - "idpID", - "issuer", - "authEndpoint", - "tokenEndpoint", - nil, - domain.OIDCMappingFieldUnspecified, - domain.OIDCMappingFieldUnspecified, - ), - ), - ), - expectFilter( - eventFromEventPusher( - org.NewIDPConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "idpID", - "name", - domain.IDPConfigTypeOIDC, - domain.IDPConfigStylingTypeUnspecified, - false, - ), - ), - eventFromEventPusher( - org.NewIDPOIDCConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "clientID", - "idpID", - "issuer", - "authEndpoint", - "tokenEndpoint", - nil, - domain.OIDCMappingFieldUnspecified, - domain.OIDCMappingFieldUnspecified, - ), - ), - eventFromEventPusher( - org.NewIdentityProviderAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "idpID", - domain.IdentityProviderTypeOrg, - ), - ), - ), - expectPush( - newAddHumanEvent("", false, true, ""), - user.NewUserIDPLinkAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - "idpID", - "name", - "externalID", - ), - user.NewHumanEmailVerifiedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - IsEmailVerified: true, - }, - }, - links: []*domain.UserIDPLink{ - { - IDPConfigID: "idpID", - ExternalUserID: "externalID", - DisplayName: "name", - }, - }, - secretGenerator: GetMockSecretGenerator(t), + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: false, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } }, res: res{ wantHuman: &domain.Human{ @@ -1973,7 +2073,238 @@ func TestCommandSide_ImportHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.English, + PreferredLanguage: language.Und, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + State: domain.UserStateInitial, + }, + }, + }, + { + name: "add human (with unsupported preferred language), ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + 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, "", UnsupportedLanguage), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: UnsupportedLanguage, + }, + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: false, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } + }, + res: res{ + wantHuman: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: UnsupportedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + State: domain.UserStateInitial, + }, + }, + }, + { + name: "add human (with idp), ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + 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, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + domain.IdentityProviderTypeOrg, + ), + ), + ), + expectPush( + newAddHumanEvent("", false, true, "", AllowedLanguage), + user.NewUserIDPLinkAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "idpID", + "name", + "externalID", + ), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + links: []*domain.UserIDPLink{ + { + IDPConfigID: "idpID", + ExternalUserID: "externalID", + DisplayName: "name", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } + }, + res: res{ + wantHuman: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -1985,153 +2316,155 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, { name: "add human (with idp, creation not allowed), precondition error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + 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, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), + }, + ) + return e + }(), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), + }, + ) + return e + }(), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + domain.IdentityProviderTypeOrg, + ), + ), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectFilter( - eventFromEventPusher( - org.NewIDPConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "idpID", - "name", - domain.IDPConfigTypeOIDC, - domain.IDPConfigStylingTypeUnspecified, - false, - ), - ), - eventFromEventPusher( - org.NewIDPOIDCConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "clientID", - "idpID", - "issuer", - "authEndpoint", - "tokenEndpoint", - nil, - domain.OIDCMappingFieldUnspecified, - domain.OIDCMappingFieldUnspecified, - ), - ), - eventFromEventPusher( - func() eventstore.Command { - e, _ := org.NewOIDCIDPChangedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "config1", - []idp.OIDCIDPChanges{ - idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), - }, - ) - return e - }(), - ), - ), - expectFilter( - eventFromEventPusher( - org.NewIDPConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "idpID", - "name", - domain.IDPConfigTypeOIDC, - domain.IDPConfigStylingTypeUnspecified, - false, - ), - ), - eventFromEventPusher( - org.NewIDPOIDCConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "clientID", - "idpID", - "issuer", - "authEndpoint", - "tokenEndpoint", - nil, - domain.OIDCMappingFieldUnspecified, - domain.OIDCMappingFieldUnspecified, - ), - ), - eventFromEventPusher( - func() eventstore.Command { - e, _ := org.NewOIDCIDPChangedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "config1", - []idp.OIDCIDPChanges{ - idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), - }, - ) - return e - }(), - ), - eventFromEventPusher( - org.NewIdentityProviderAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "idpID", - domain.IdentityProviderTypeOrg, - ), - ), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - IsEmailVerified: true, - }, - }, - links: []*domain.UserIDPLink{ - { - IDPConfigID: "idpID", - ExternalUserID: "externalID", - DisplayName: "name", - }, - }, - secretGenerator: GetMockSecretGenerator(t), + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + links: []*domain.UserIDPLink{ + { + IDPConfigID: "idpID", + ExternalUserID: "externalID", + DisplayName: "name", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + f, a := tt.given(t) r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, - userPasswordHasher: tt.fields.userPasswordHasher, + eventstore: f.eventstore, + idGenerator: f.idGenerator, + userPasswordHasher: f.userPasswordHasher, } - gotHuman, gotCode, err := r.ImportHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.passwordless, tt.args.links, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator) + gotHuman, gotCode, err := r.ImportHuman(a.ctx, a.orgID, a.human, a.passwordless, a.links, a.secretGenerator, a.secretGenerator, a.secretGenerator, a.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } @@ -2139,7 +2472,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.wantHuman, gotHuman) + assert.Equal(t, tt.res.wantHuman.PreferredLanguage, gotHuman.PreferredLanguage) assert.Equal(t, tt.res.wantCode, gotCode) } }) @@ -2192,7 +2525,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -2222,7 +2555,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { @@ -2262,7 +2595,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { @@ -2310,7 +2643,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { @@ -2380,7 +2713,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { @@ -2450,7 +2783,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -2537,7 +2870,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -2625,7 +2958,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), expectPush( - newRegisterHumanEvent("email@test.ch", "$plain$x$password", false, false, ""), + newRegisterHumanEvent("email@test.ch", "$plain$x$password", false, false, "", AllowedLanguage), user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -2649,8 +2982,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) { SecretString: "password", }, Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2669,7 +3003,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.Und, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2730,7 +3064,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), expectPush( - newRegisterHumanEvent("username", "$plain$x$password", false, false, ""), + newRegisterHumanEvent("username", "$plain$x$password", false, false, "", AllowedLanguage), user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -2754,8 +3088,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) { SecretString: "password", }, Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2775,7 +3110,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.Und, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2836,7 +3171,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), expectPush( - newRegisterHumanEvent("username", "$plain$x$password", false, true, ""), + newRegisterHumanEvent("username", "$plain$x$password", false, true, "", AllowedLanguage), user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -2861,8 +3196,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) { SecretString: "password", }, Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2881,7 +3217,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.Und, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2942,7 +3278,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), expectPush( - newRegisterHumanEvent("username", "$plain$x$password", false, true, ""), + newRegisterHumanEvent("username", "$plain$x$password", false, true, "", AllowedLanguage), user.NewHumanEmailVerifiedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate), ), @@ -2959,8 +3295,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) { SecretString: "password", }, Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2980,7 +3317,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.Und, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -3042,7 +3379,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), expectPush( - newRegisterHumanEvent("username", "$plain$x$password", false, true, "+41711234567"), + newRegisterHumanEvent("username", "$plain$x$password", false, true, "+41711234567", AllowedLanguage), user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -3074,8 +3411,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) { human: &domain.Human{ Username: "username", Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -3100,7 +3438,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.Und, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -3164,7 +3502,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), expectPush( - newRegisterHumanEvent("username", "$plain$x$password", false, true, "+41711234567"), + newRegisterHumanEvent("username", "$plain$x$password", false, true, "+41711234567", AllowedLanguage), user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -3189,8 +3527,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) { human: &domain.Human{ Username: "username", Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -3216,7 +3555,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.Und, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -3228,6 +3567,218 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, }, + { + name: "add human (with unsupported preferred language), ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + domain.PasswordlessTypeNotAllowed, + "", + time.Hour*1, + time.Hour*2, + time.Hour*3, + time.Hour*4, + time.Hour*5, + ), + ), + ), + expectPush( + newRegisterHumanEvent("username", "$plain$x$password", false, true, "", UnsupportedLanguage), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: UnsupportedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Password: &domain.Password{ + SecretString: "password", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + }, + res: res{ + want: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: UnsupportedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + State: domain.UserStateInitial, + }, + }, + }, + { + name: "add human (with undefined preferred language), ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + domain.PasswordlessTypeNotAllowed, + "", + time.Hour*1, + time.Hour*2, + time.Hour*3, + time.Hour*4, + time.Hour*5, + ), + ), + ), + expectPush( + newRegisterHumanEvent("username", "$plain$x$password", false, true, "", language.Und), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Password: &domain.Password{ + SecretString: "password", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + }, + res: res{ + want: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + State: domain.UserStateInitial, + }, + }, + }, { name: "add with idp link, email verified, ok", fields: fields{ @@ -3337,7 +3888,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), expectPush( - newRegisterHumanEvent("username", "$plain$x$password", false, true, ""), + newRegisterHumanEvent("username", "$plain$x$password", false, true, "", AllowedLanguage), user.NewUserIDPLinkAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "idpID", @@ -3361,8 +3912,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) { SecretString: "password", }, Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -3387,7 +3939,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.Und, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -3453,7 +4005,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) { userID: "", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -3470,7 +4022,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) { userID: "user1", }, res: res{ - err: caos_errs.IsNotFound, + err: zitadel_errs.IsNotFound, }, }, { @@ -3487,7 +4039,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) { "lastname", "nickname", "displayname", - language.German, + AllowedLanguage, domain.GenderUnspecified, "email@test.ch", true, @@ -3563,7 +4115,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { userIDs: []string{"user1"}, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -3579,7 +4131,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { userIDs: []string{}, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -3611,7 +4163,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { "lastname", "nickname", "displayname", - language.German, + AllowedLanguage, domain.GenderUnspecified, "email@test.ch", true, @@ -3651,7 +4203,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { "lastname", "nickname", "displayname", - language.German, + AllowedLanguage, domain.GenderUnspecified, "email@test.ch", true, @@ -3667,7 +4219,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { "lastname", "nickname", "displayname", - language.German, + AllowedLanguage, domain.GenderUnspecified, "email@test.ch", true, @@ -3714,7 +4266,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { } } -func newAddHumanEvent(password string, changeRequired, userLoginMustBeDomain bool, phone string) *user.HumanAddedEvent { +func newAddHumanEvent(password string, changeRequired, userLoginMustBeDomain bool, phone string, preferredLanguage language.Tag) *user.HumanAddedEvent { event := user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "username", @@ -3722,7 +4274,7 @@ func newAddHumanEvent(password string, changeRequired, userLoginMustBeDomain boo "lastname", "", "firstname lastname", - language.English, + preferredLanguage, domain.GenderUnspecified, "email@test.ch", userLoginMustBeDomain, @@ -3736,7 +4288,7 @@ func newAddHumanEvent(password string, changeRequired, userLoginMustBeDomain boo return event } -func newRegisterHumanEvent(username, password string, changeRequired, userLoginMustBeUnique bool, phone string) *user.HumanRegisteredEvent { +func newRegisterHumanEvent(username, password string, changeRequired, userLoginMustBeUnique bool, phone string, preferredLanguage language.Tag) *user.HumanRegisteredEvent { event := user.NewHumanRegisteredEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, username, @@ -3744,7 +4296,7 @@ func newRegisterHumanEvent(username, password string, changeRequired, userLoginM "lastname", "", "firstname lastname", - language.Und, + preferredLanguage, domain.GenderUnspecified, "email@test.ch", userLoginMustBeUnique, @@ -3784,27 +4336,28 @@ func TestAddHumanCommand(t *testing.T) { Email: Email{ Address: "invalid", }, + PreferredLanguage: AllowedLanguage, }, orgID: "ro", }, want: Want{ - ValidationErr: caos_errs.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"), + ValidationErr: zitadel_errs.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"), }, }, { name: "invalid first name", args: args{ human: &AddHuman{ - Username: "username", - PreferredLanguage: language.English, + Username: "username", Email: Email{ Address: "support@zitadel.com", }, + PreferredLanguage: AllowedLanguage, }, orgID: "ro", }, want: Want{ - ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"), + ValidationErr: zitadel_errs.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"), }, }, { @@ -3812,14 +4365,14 @@ func TestAddHumanCommand(t *testing.T) { args: args{ human: &AddHuman{ Username: "username", - PreferredLanguage: language.English, FirstName: "hurst", Email: Email{Address: "support@zitadel.com"}, + PreferredLanguage: AllowedLanguage, }, orgID: "ro", }, want: Want{ - ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"), + ValidationErr: zitadel_errs.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"), }, }, { @@ -3827,17 +4380,17 @@ func TestAddHumanCommand(t *testing.T) { args: args{ human: &AddHuman{ Email: Email{Address: "support@zitadel.com", Verified: true}, - PreferredLanguage: language.English, FirstName: "gigi", LastName: "giraffe", EncodedPasswordHash: "$foo$x$password", Username: "username", + PreferredLanguage: AllowedLanguage, }, orgID: "ro", hasher: mockPasswordHasher("x"), }, want: Want{ - ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.User.Password.NotSupported"), + ValidationErr: zitadel_errs.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.User.Password.NotSupported"), }, }, { @@ -3848,11 +4401,11 @@ func TestAddHumanCommand(t *testing.T) { args: args{ human: &AddHuman{ Email: Email{Address: "support@zitadel.com"}, - PreferredLanguage: language.English, FirstName: "gigi", LastName: "giraffe", Password: "short", Username: "username", + PreferredLanguage: AllowedLanguage, }, orgID: "ro", filter: NewMultiFilter().Append( @@ -3888,7 +4441,7 @@ func TestAddHumanCommand(t *testing.T) { Filter(), }, want: Want{ - CreateErr: caos_errs.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"), + CreateErr: zitadel_errs.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"), }, }, { @@ -3899,11 +4452,11 @@ func TestAddHumanCommand(t *testing.T) { args: args{ human: &AddHuman{ Email: Email{Address: "support@zitadel.com", Verified: true}, - PreferredLanguage: language.English, FirstName: "gigi", LastName: "giraffe", Password: "password", Username: "username", + PreferredLanguage: AllowedLanguage, }, orgID: "ro", hasher: mockPasswordHasher("x"), @@ -3951,7 +4504,150 @@ func TestAddHumanCommand(t *testing.T) { "giraffe", "", "gigi giraffe", - language.English, + AllowedLanguage, + 0, + "support@zitadel.com", + true, + ) + event.AddPasswordData("$plain$x$password", false) + return event + }(), + user.NewHumanEmailVerifiedEvent(context.Background(), &agg.Aggregate), + }, + }, + }, + { + name: "undefined preferred language, ok", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"), + }, + args: args{ + human: &AddHuman{ + Email: Email{Address: "support@zitadel.com", Verified: true}, + FirstName: "gigi", + LastName: "giraffe", + Password: "password", + Username: "username", + }, + orgID: "ro", + hasher: mockPasswordHasher("x"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + filter: NewMultiFilter().Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{}, nil + }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewDomainPolicyAddedEvent( + ctx, + &org.NewAggregate("id").Aggregate, + true, + true, + true, + ), + }, nil + }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewPasswordComplexityPolicyAddedEvent( + ctx, + &org.NewAggregate("id").Aggregate, + 2, + false, + false, + false, + false, + ), + }, nil + }). + Filter(), + }, + want: Want{ + Commands: []eventstore.Command{ + func() *user.HumanAddedEvent { + event := user.NewHumanAddedEvent( + context.Background(), + &agg.Aggregate, + "username", + "gigi", + "giraffe", + "", + "gigi giraffe", + language.Und, + 0, + "support@zitadel.com", + true, + ) + event.AddPasswordData("$plain$x$password", false) + return event + }(), + user.NewHumanEmailVerifiedEvent(context.Background(), &agg.Aggregate), + }, + }, + }, + { + name: "unsupported preferred language, ok", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"), + }, + args: args{ + human: &AddHuman{ + Email: Email{Address: "support@zitadel.com", Verified: true}, + FirstName: "gigi", + LastName: "giraffe", + Password: "password", + Username: "username", + PreferredLanguage: UnsupportedLanguage, + }, + orgID: "ro", + hasher: mockPasswordHasher("x"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + filter: NewMultiFilter().Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{}, nil + }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewDomainPolicyAddedEvent( + ctx, + &org.NewAggregate("id").Aggregate, + true, + true, + true, + ), + }, nil + }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewPasswordComplexityPolicyAddedEvent( + ctx, + &org.NewAggregate("id").Aggregate, + 2, + false, + false, + false, + false, + ), + }, nil + }). + Filter(), + }, + want: Want{ + Commands: []eventstore.Command{ + func() *user.HumanAddedEvent { + event := user.NewHumanAddedEvent( + context.Background(), + &agg.Aggregate, + "username", + "gigi", + "giraffe", + "", + "gigi giraffe", + UnsupportedLanguage, 0, "support@zitadel.com", true, @@ -3971,11 +4667,11 @@ func TestAddHumanCommand(t *testing.T) { args: args{ human: &AddHuman{ Email: Email{Address: "support@zitadel.com", Verified: true}, - PreferredLanguage: language.English, FirstName: "gigi", LastName: "giraffe", EncodedPasswordHash: "$plain$x$password", Username: "username", + PreferredLanguage: AllowedLanguage, }, orgID: "ro", hasher: mockPasswordHasher("x"), @@ -4023,7 +4719,7 @@ func TestAddHumanCommand(t *testing.T) { "giraffe", "", "gigi giraffe", - language.English, + AllowedLanguage, 0, "support@zitadel.com", true, diff --git a/internal/command/user_machine_secret.go b/internal/command/user_machine_secret.go index ebec62d85f..1be051c5e8 100644 --- a/internal/command/user_machine_secret.go +++ b/internal/command/user_machine_secret.go @@ -3,8 +3,6 @@ package command import ( "context" - "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -111,59 +109,12 @@ func prepareRemoveMachineSecret(a *user.Aggregate) preparation.Validation { } } -func (c *Commands) VerifyMachineSecret(ctx context.Context, userID string, resourceOwner string, secret string) (*domain.ObjectDetails, error) { +func (c *Commands) MachineSecretCheckSucceeded(ctx context.Context, userID, resourceOwner string) { agg := user.NewAggregate(userID, resourceOwner) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareVerifyMachineSecret(agg, secret, c.codeAlg)) - if err != nil { - return nil, err - } - - events, err := c.eventstore.Push(ctx, cmds...) - for _, cmd := range cmds { - if cmd.Type() == user.MachineSecretCheckFailedType { - logging.OnError(err).Error("could not push event MachineSecretCheckFailed") - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3kjh", "Errors.User.Machine.Secret.Invalid") - } - } - if err != nil { - return nil, err - } - - return &domain.ObjectDetails{ - Sequence: events[len(events)-1].Sequence(), - EventDate: events[len(events)-1].CreatedAt(), - ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, - }, nil + c.asyncPush(ctx, user.NewMachineSecretCheckSucceededEvent(ctx, &agg.Aggregate)) } -func prepareVerifyMachineSecret(a *user.Aggregate, secret string, algorithm crypto.HashAlgorithm) preparation.Validation { - return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-0qp2hus", "Errors.ResourceOwnerMissing") - } - if a.ID == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-bzosjs", "Errors.User.UserIDMissing") - } - return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) - if err != nil { - return nil, err - } - if !isUserStateExists(writeModel.UserState) { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-569sh2o", "Errors.User.NotExisting") - } - if writeModel.ClientSecret == nil { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-x8910n", "Errors.User.Machine.Secret.NotExisting") - } - err = crypto.CompareHash(writeModel.ClientSecret, []byte(secret), algorithm) - if err == nil { - return []eventstore.Command{ - user.NewMachineSecretCheckSucceededEvent(ctx, &a.Aggregate), - }, nil - } - return []eventstore.Command{ - user.NewMachineSecretCheckFailedEvent(ctx, &a.Aggregate), - }, nil - }, nil - } +func (c *Commands) MachineSecretCheckFailed(ctx context.Context, userID, resourceOwner string) { + agg := user.NewAggregate(userID, resourceOwner) + c.asyncPush(ctx, user.NewMachineSecretCheckFailedEvent(ctx, &agg.Aggregate)) } diff --git a/internal/command/user_machine_secret_test.go b/internal/command/user_machine_secret_test.go index 77343c8fb0..9f7ead9595 100644 --- a/internal/command/user_machine_secret_test.go +++ b/internal/command/user_machine_secret_test.go @@ -3,8 +3,10 @@ package command import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -319,212 +321,34 @@ func TestCommandSide_RemoveMachineSecret(t *testing.T) { } } -func TestCommandSide_VerifyMachineSecret(t *testing.T) { - type fields struct { - eventstore *eventstore.Eventstore - } - type args struct { - ctx context.Context - userID string - resourceOwner string - secret string - } - type res struct { - want *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - name: "user invalid, invalid argument error userID", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - userID: "", - resourceOwner: "org1", - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "user invalid, invalid argument error resourceowner", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "", - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "user not existing, precondition error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - ), - }, - args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - }, - res: res{ - err: caos_errs.IsPreconditionFailed, - }, - }, - { - name: "user existing without secret, precondition error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - "user1", - "username", - "user", - false, - domain.OIDCTokenTypeBearer, - ), - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - }, - res: res{ - err: caos_errs.IsPreconditionFailed, - }, - }, - { - name: "verify machine secret, ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - "user1", - "username", - "user", - false, - domain.OIDCTokenTypeBearer, - ), - ), - eventFromEventPusher( - user.NewMachineSecretSetEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "bcrypt", - KeyID: "id", - Crypted: []byte("$2a$14$HxC7TAXMeowdqHdSBUfsjOUc0IGajYeApxdYl9lAYC0duZmSkgFia"), - }, - ), - ), - ), - expectPush( - user.NewMachineSecretCheckSucceededEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - secret: "test", - }, - res: res{ - want: &domain.ObjectDetails{ - ResourceOwner: "org1", - }, - }, - }, - { - name: "verify machine secret, failed", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - "user1", - "username", - "user", - false, - domain.OIDCTokenTypeBearer, - ), - ), - eventFromEventPusher( - user.NewMachineSecretSetEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "bcrypt", - KeyID: "id", - Crypted: []byte("$2a$14$HxC7TAXMeowdqHdSBUfsjOUc0IGajYeApxdYl9lAYC0duZmSkgFia"), - }, - ), - ), - ), - expectPush( - user.NewMachineSecretCheckFailedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - secret: "wrong", - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &Commands{ - eventstore: tt.fields.eventstore, - codeAlg: crypto.NewBCrypt(14), - } - got, err := r.VerifyMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.secret) - 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 { - assert.Equal(t, tt.res.want, got) - } - }) +func TestCommands_MachineSecretCheckSucceeded(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + agg := user.NewAggregate("userID", "orgID") + cmd := user.NewMachineSecretCheckSucceededEvent(ctx, &agg.Aggregate) + + c := &Commands{ + eventstore: eventstoreExpect(t, + expectPushSlow(time.Second/100, cmd), + ), } + c.MachineSecretCheckSucceeded(ctx, "userID", "orgID") + require.NoError(t, c.Close(ctx)) +} + +func TestCommands_MachineSecretCheckFailed(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + agg := user.NewAggregate("userID", "orgID") + cmd := user.NewMachineSecretCheckFailedEvent(ctx, &agg.Aggregate) + + c := &Commands{ + eventstore: eventstoreExpect(t, + expectPushSlow(time.Second/100, cmd), + ), + } + c.MachineSecretCheckFailed(ctx, "userID", "orgID") + require.NoError(t, c.Close(ctx)) } diff --git a/internal/command/user_v2_password_test.go b/internal/command/user_v2_password_test.go index 2465c240f3..a88fd6e6aa 100644 --- a/internal/command/user_v2_password_test.go +++ b/internal/command/user_v2_password_test.go @@ -5,11 +5,11 @@ import ( "testing" "time" + "golang.org/x/text/language" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/text/language" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" diff --git a/internal/config/hook/tag_to_language.go b/internal/config/hook/tag_to_language.go index b8ac8c4f39..e7e5f3acac 100644 --- a/internal/config/hook/tag_to_language.go +++ b/internal/config/hook/tag_to_language.go @@ -5,6 +5,8 @@ import ( "github.com/mitchellh/mapstructure" "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/domain" ) func TagToLanguageHookFunc() mapstructure.DecodeHookFuncType { @@ -21,6 +23,7 @@ func TagToLanguageHookFunc() mapstructure.DecodeHookFuncType { return data, nil } - return language.Parse(data.(string)) + lang, err := domain.ParseLanguage(data.(string)) + return lang[0], err } } diff --git a/internal/database/database.go b/internal/database/database.go index c889a88420..d88a5f7e06 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -3,6 +3,8 @@ package database import ( "context" "database/sql" + "encoding/json" + "errors" "reflect" "github.com/mitchellh/mapstructure" @@ -11,7 +13,7 @@ import ( _ "github.com/zitadel/zitadel/internal/database/cockroach" "github.com/zitadel/zitadel/internal/database/dialect" _ "github.com/zitadel/zitadel/internal/database/postgres" - "github.com/zitadel/zitadel/internal/errors" + zerrors "github.com/zitadel/zitadel/internal/errors" ) type Config struct { @@ -89,6 +91,24 @@ func (db *DB) QueryRowContext(ctx context.Context, scan func(row *sql.Row) error return row.Err() } +func QueryJSONObject[T any](ctx context.Context, db *DB, query string, args ...any) (*T, error) { + var data []byte + err := db.QueryRowContext(ctx, func(row *sql.Row) error { + return row.Scan(&data) + }, query, args...) + if errors.Is(err, sql.ErrNoRows) { + return nil, err + } + if err != nil { + return nil, zerrors.ThrowInternal(err, "DATAB-Oath6", "Errors.Internal") + } + obj := new(T) + if err = json.Unmarshal(data, obj); err != nil { + return nil, zerrors.ThrowInternal(err, "DATAB-Vohs6", "Errors.Internal") + } + return obj, nil +} + const ( zitadelAppName = "zitadel" EventstorePusherAppName = "zitadel_es_pusher" @@ -106,7 +126,7 @@ func Connect(config Config, useAdmin, isEventPusher bool) (*DB, error) { } if err := client.Ping(); err != nil { - return nil, errors.ThrowPreconditionFailed(err, "DATAB-0pIWD", "Errors.Database.Connection.Failed") + return nil, zerrors.ThrowPreconditionFailed(err, "DATAB-0pIWD", "Errors.Database.Connection.Failed") } return &DB{ diff --git a/internal/database/database_test.go b/internal/database/database_test.go new file mode 100644 index 0000000000..f7d63d6d64 --- /dev/null +++ b/internal/database/database_test.go @@ -0,0 +1,92 @@ +package database + +import ( + "context" + "database/sql" + "database/sql/driver" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/database/mock" + zerrors "github.com/zitadel/zitadel/internal/errors" +) + +func TestQueryJSONObject(t *testing.T) { + type dst struct { + A int `json:"a,omitempty"` + } + const ( + query = `select $1;` + arg = 1 + ) + + tests := []struct { + name string + mock func(*testing.T) *mock.SQLMock + want *dst + wantErr error + }{ + { + name: "tx error", + mock: func(t *testing.T) *mock.SQLMock { + return mock.NewSQLMock(t, mock.ExpectBegin(sql.ErrConnDone)) + }, + wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "DATAB-Oath6", "Errors.Internal"), + }, + { + name: "no rows", + mock: func(t *testing.T) *mock.SQLMock { + return mock.NewSQLMock(t, + mock.ExpectBegin(nil), + mock.ExpectQuery(query, + mock.WithQueryArgs(arg), + mock.WithQueryResult([]string{"json"}, [][]driver.Value{}), + ), + ) + }, + wantErr: sql.ErrNoRows, + }, + { + name: "unmarshal error", + mock: func(t *testing.T) *mock.SQLMock { + return mock.NewSQLMock(t, + mock.ExpectBegin(nil), + mock.ExpectQuery(query, + mock.WithQueryArgs(arg), + mock.WithQueryResult([]string{"json"}, [][]driver.Value{{`~~~`}}), + ), + mock.ExpectCommit(nil), + ) + }, + wantErr: zerrors.ThrowInternal(nil, "DATAB-Vohs6", "Errors.Internal"), + }, + { + name: "success", + mock: func(t *testing.T) *mock.SQLMock { + return mock.NewSQLMock(t, + mock.ExpectBegin(nil), + mock.ExpectQuery(query, + mock.WithQueryArgs(arg), + mock.WithQueryResult([]string{"json"}, [][]driver.Value{{`{"a":1}`}}), + ), + mock.ExpectCommit(nil), + ) + }, + want: &dst{A: 1}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := tt.mock(t) + defer mock.Assert(t) + db := &DB{ + DB: mock.DB, + } + got, err := QueryJSONObject[dst](context.Background(), db, query, arg) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/database/mock/sql_mock.go b/internal/database/mock/sql_mock.go index 07d74151e0..e05b188afc 100644 --- a/internal/database/mock/sql_mock.go +++ b/internal/database/mock/sql_mock.go @@ -53,6 +53,15 @@ func ExpectBegin(err error) expectation { } } +func ExpectCommit(err error) expectation { + return func(m sqlmock.Sqlmock) { + e := m.ExpectCommit() + if err != nil { + e.WillReturnError(err) + } + } +} + type ExecOpt func(e *sqlmock.ExpectedExec) *sqlmock.ExpectedExec func WithExecArgs(args ...driver.Value) ExecOpt { diff --git a/internal/domain/custom_login_text.go b/internal/domain/custom_login_text.go index 18d999ce93..63a5599eb8 100644 --- a/internal/domain/custom_login_text.go +++ b/internal/domain/custom_login_text.go @@ -343,8 +343,11 @@ type CustomLoginText struct { Footer FooterText } -func (m *CustomLoginText) IsValid() bool { - return m.Language != language.Und +func (m *CustomLoginText) IsValid(supportedLanguages []language.Tag) error { + if err := LanguageIsDefined(m.Language); err != nil { + return err + } + return LanguagesAreSupported(supportedLanguages, m.Language) } type SelectAccountScreenText struct { diff --git a/internal/domain/custom_message_text.go b/internal/domain/custom_message_text.go index 263cd57794..dbfbb56254 100644 --- a/internal/domain/custom_message_text.go +++ b/internal/domain/custom_message_text.go @@ -3,6 +3,7 @@ package domain import ( "golang.org/x/text/language" + zitadel_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore/v1/models" ) @@ -51,8 +52,14 @@ type CustomMessageText struct { FooterText string } -func (m *CustomMessageText) IsValid() bool { - return m.MessageTextType != "" && m.Language != language.Und +func (m *CustomMessageText) IsValid(supportedLanguages []language.Tag) error { + if m.MessageTextType == "" { + return zitadel_errs.ThrowInvalidArgument(nil, "INSTANCE-kd9fs", "Errors.CustomMessageText.Invalid") + } + if err := LanguageIsDefined(m.Language); err != nil { + return err + } + return LanguagesAreSupported(supportedLanguages, m.Language) } func IsMessageTextType(textType string) bool { diff --git a/internal/domain/language.go b/internal/domain/language.go new file mode 100644 index 0000000000..ffb00b20e0 --- /dev/null +++ b/internal/domain/language.go @@ -0,0 +1,130 @@ +package domain + +import ( + "errors" + + "golang.org/x/text/language" + + z_errors "github.com/zitadel/zitadel/internal/errors" +) + +func StringsToLanguages(langs []string) []language.Tag { + return GenericMapSlice(langs, language.Make) +} + +func LanguagesToStrings(langs []language.Tag) []string { + return GenericMapSlice(langs, func(lang language.Tag) string { return lang.String() }) +} + +func GenericMapSlice[T any, U any](from []T, mapTo func(T) U) []U { + if from == nil { + return nil + } + result := make([]U, len(from)) + for i, lang := range from { + result[i] = mapTo(lang) + } + return result +} + +// LanguagesDiffer returns true if the languages differ. +func LanguagesDiffer(left, right []language.Tag) bool { + if left == nil && right == nil { + return false + } + if left == nil || right == nil || len(left) != len(right) { + return true + } + return !languagesAreContained(left, right) +} + +func LanguageIsAllowed(allowUndefined bool, allowedLanguages []language.Tag, lang language.Tag) error { + err := LanguageIsDefined(lang) + if err != nil && allowUndefined { + return nil + } + if err != nil { + return err + } + if len(allowedLanguages) > 0 && !languageIsContained(allowedLanguages, lang) { + return z_errors.ThrowPreconditionFailed(nil, "LANG-2M9fs", "Errors.Language.NotAllowed") + } + return nil +} + +func LanguagesAreSupported(supportedLanguages []language.Tag, lang ...language.Tag) error { + unsupported := make([]language.Tag, 0) + for _, l := range lang { + if l.IsRoot() { + continue + } + if !languageIsContained(supportedLanguages, l) { + unsupported = append(unsupported, l) + } + } + if len(unsupported) == 0 { + return nil + } + if len(unsupported) == 1 { + return z_errors.ThrowInvalidArgument(nil, "LANG-lg4DP", "Errors.Language.NotSupported") + } + return z_errors.ThrowInvalidArgumentf(nil, "LANG-XHiK5", "Errors.Languages.NotSupported: %s", LanguagesToStrings(unsupported)) +} + +func LanguageIsDefined(lang language.Tag) error { + if lang.IsRoot() { + return z_errors.ThrowInvalidArgument(nil, "LANG-3M9f2", "Errors.Language.Undefined") + } + return nil +} + +// LanguagesHaveDuplicates returns an error if the passed slices contains duplicates. +// The error lists the duplicates. +func LanguagesHaveDuplicates(langs []language.Tag) error { + unique := make(map[language.Tag]struct{}) + duplicates := make([]language.Tag, 0) + for _, lang := range langs { + if _, ok := unique[lang]; ok { + duplicates = append(duplicates, lang) + } + unique[lang] = struct{}{} + } + if len(duplicates) == 0 { + return nil + } + if len(duplicates) > 1 { + return z_errors.ThrowInvalidArgument(nil, "LANG-3M9f2", "Errors.Language.Duplicate") + } + return z_errors.ThrowInvalidArgumentf(nil, "LANG-XHiK5", "Errors.Languages.Duplicate: %s", LanguagesToStrings(duplicates)) +} + +func ParseLanguage(lang ...string) (tags []language.Tag, err error) { + tags = make([]language.Tag, len(lang)) + for i := range lang { + var parseErr error + tags[i], parseErr = language.Parse(lang[i]) + err = errors.Join(err, parseErr) + } + if err != nil { + err = z_errors.ThrowInvalidArgument(err, "LANG-jc8Sq", "Errors.Language.NotParsed") + } + return tags, err +} + +func languagesAreContained(languages, search []language.Tag) bool { + for _, s := range search { + if !languageIsContained(languages, s) { + return false + } + } + return true +} + +func languageIsContained(languages []language.Tag, search language.Tag) bool { + for _, lang := range languages { + if lang == search { + return true + } + } + return false +} diff --git a/internal/eventstore/aggregate.go b/internal/eventstore/aggregate.go index 30053079da..9939d8335c 100644 --- a/internal/eventstore/aggregate.go +++ b/internal/eventstore/aggregate.go @@ -55,6 +55,7 @@ func AggregateFromWriteModel( version Version, ) *Aggregate { return NewAggregate( + // TODO: the linter complains if this function is called without passing a context context.Background(), wm.AggregateID, typ, diff --git a/internal/eventstore/repository/mock/repository.mock.impl.go b/internal/eventstore/repository/mock/repository.mock.impl.go index bf49929c7b..6ae64ddf0f 100644 --- a/internal/eventstore/repository/mock/repository.mock.impl.go +++ b/internal/eventstore/repository/mock/repository.mock.impl.go @@ -75,11 +75,15 @@ func (m *MockRepository) ExpectInstanceIDsError(err error) *MockRepository { return m } -func (m *MockRepository) ExpectPush(expectedCommands []eventstore.Command) *MockRepository { +// ExpectPush checks if the expectedCommands are send to the Push method. +// The call will sleep at least the amount of passed duration. +func (m *MockRepository) ExpectPush(expectedCommands []eventstore.Command, sleep time.Duration) *MockRepository { m.MockPusher.EXPECT().Push(gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, commands ...eventstore.Command) ([]eventstore.Event, error) { m.MockPusher.ctrl.T.Helper() + time.Sleep(sleep) + if len(expectedCommands) != len(commands) { return nil, fmt.Errorf("unexpected amount of commands: want %d, got %d", len(expectedCommands), len(commands)) } diff --git a/internal/i18n/bundle.go b/internal/i18n/bundle.go new file mode 100644 index 0000000000..78b4550d83 --- /dev/null +++ b/internal/i18n/bundle.go @@ -0,0 +1,60 @@ +package i18n + +import ( + "encoding/json" + "io" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/BurntSushi/toml" + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" + "sigs.k8s.io/yaml" + + "github.com/zitadel/zitadel/internal/domain" + zitadel_errors "github.com/zitadel/zitadel/internal/errors" +) + +const i18nPath = "/i18n" + +func newBundle(dir http.FileSystem, defaultLanguage language.Tag, allowedLanguages []language.Tag) (*i18n.Bundle, error) { + bundle := i18n.NewBundle(defaultLanguage) + bundle.RegisterUnmarshalFunc("yaml", func(data []byte, v interface{}) error { return yaml.Unmarshal(data, v) }) + bundle.RegisterUnmarshalFunc("json", json.Unmarshal) + bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) + i18nDir, err := dir.Open(i18nPath) + if err != nil { + return nil, zitadel_errors.ThrowNotFound(err, "I18N-MnXRie", "path not found") + } + defer i18nDir.Close() + files, err := i18nDir.Readdir(0) + if err != nil { + return nil, zitadel_errors.ThrowNotFound(err, "I18N-Gew23", "cannot read dir") + } + for _, file := range files { + fileLang, _ := strings.CutSuffix(file.Name(), filepath.Ext(file.Name())) + if err = domain.LanguageIsAllowed(false, allowedLanguages, language.Make(fileLang)); err != nil { + continue + } + if err := addFileFromFileSystemToBundle(dir, bundle, file); err != nil { + return nil, zitadel_errors.ThrowNotFoundf(err, "I18N-ZS2AW", "cannot append file %s to Bundle", file.Name()) + } + } + return bundle, nil +} + +func addFileFromFileSystemToBundle(dir http.FileSystem, bundle *i18n.Bundle, file os.FileInfo) error { + f, err := dir.Open("/i18n/" + file.Name()) + if err != nil { + return err + } + defer f.Close() + content, err := io.ReadAll(f) + if err != nil { + return err + } + _, err = bundle.ParseMessageFileBytes(content, file.Name()) + return err +} diff --git a/internal/i18n/fs.go b/internal/i18n/fs.go new file mode 100644 index 0000000000..eac34ba8e6 --- /dev/null +++ b/internal/i18n/fs.go @@ -0,0 +1,48 @@ +package i18n + +import ( + "net/http" + + "github.com/rakyll/statik/fs" + "github.com/zitadel/logging" +) + +var zitadelFS, loginFS, notificationFS http.FileSystem + +type Namespace string + +const ( + ZITADEL Namespace = "zitadel" + LOGIN Namespace = "login" + NOTIFICATION Namespace = "notification" +) + +func LoadFilesystem(ns Namespace) http.FileSystem { + var err error + defer func() { + if err != nil { + logging.WithFields("namespace", ns).OnError(err).Panic("unable to get namespace") + } + }() + switch ns { + case ZITADEL: + if zitadelFS != nil { + return zitadelFS + } + zitadelFS, err = fs.NewWithNamespace(string(ns)) + return zitadelFS + case LOGIN: + if loginFS != nil { + return loginFS + } + loginFS, err = fs.NewWithNamespace(string(ns)) + return loginFS + case NOTIFICATION: + if notificationFS != nil { + return notificationFS + } + notificationFS, err = fs.NewWithNamespace(string(ns)) + return notificationFS + } + return nil +} diff --git a/internal/i18n/languages.go b/internal/i18n/languages.go new file mode 100644 index 0000000000..f3acdb4eba --- /dev/null +++ b/internal/i18n/languages.go @@ -0,0 +1,51 @@ +package i18n + +import ( + "errors" + "strings" + + "golang.org/x/text/language" +) + +var supportedLanguages []language.Tag + +func SupportedLanguages() []language.Tag { + if supportedLanguages == nil { + panic("supported languages not loaded") + } + return supportedLanguages +} + +func SupportLanguages(languages ...language.Tag) { + supportedLanguages = languages +} + +func MustLoadSupportedLanguagesFromDir() { + var err error + defer func() { + if err != nil { + panic("failed to load supported languages: " + err.Error()) + } + }() + if supportedLanguages != nil { + return + } + i18nDir, err := LoadFilesystem(LOGIN).Open(i18nPath) + if err != nil { + return + } + defer func() { + err = errors.Join(err, i18nDir.Close()) + }() + files, err := i18nDir.Readdir(0) + if err != nil { + return + } + supportedLanguages = make([]language.Tag, 0, len(files)) + for _, file := range files { + lang := language.Make(strings.TrimSuffix(file.Name(), ".yaml")) + if lang != language.Und { + supportedLanguages = append(supportedLanguages, lang) + } + } +} diff --git a/internal/i18n/i18n.go b/internal/i18n/translator.go similarity index 63% rename from internal/i18n/i18n.go rename to internal/i18n/translator.go index a399ae1fc7..a60932bd6c 100644 --- a/internal/i18n/i18n.go +++ b/internal/i18n/translator.go @@ -2,26 +2,15 @@ package i18n import ( "context" - "encoding/json" - "io/ioutil" "net/http" - "os" - "strings" - "github.com/BurntSushi/toml" "github.com/grpc-ecosystem/go-grpc-middleware/util/metautils" "github.com/nicksnyder/go-i18n/v2/i18n" "github.com/zitadel/logging" "golang.org/x/text/language" - "sigs.k8s.io/yaml" "github.com/zitadel/zitadel/internal/api/authz" http_util "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/errors" -) - -const ( - i18nPath = "/i18n" ) type Translator struct { @@ -29,6 +18,7 @@ type Translator struct { cookieName string cookieHandler *http_util.CookieHandler preferredLanguages []string + allowedLanguages []language.Tag } type TranslatorConfig struct { @@ -41,10 +31,27 @@ type Message struct { Text string } -func NewTranslator(dir http.FileSystem, defaultLanguage language.Tag, cookieName string) (*Translator, error) { +// NewZitadelTranslator translates to all supported languages, as the ZITADEL texts are not customizable. +func NewZitadelTranslator(defaultLanguage language.Tag) (*Translator, error) { + return newTranslator(ZITADEL, defaultLanguage, SupportedLanguages(), "") +} + +func NewNotificationTranslator(defaultLanguage language.Tag, allowedLanguages []language.Tag) (*Translator, error) { + return newTranslator(NOTIFICATION, defaultLanguage, allowedLanguages, "") +} + +func NewLoginTranslator(defaultLanguage language.Tag, allowedLanguages []language.Tag, cookieName string) (*Translator, error) { + return newTranslator(LOGIN, defaultLanguage, allowedLanguages, cookieName) +} + +func newTranslator(ns Namespace, defaultLanguage language.Tag, allowedLanguages []language.Tag, cookieName string) (*Translator, error) { t := new(Translator) var err error - t.bundle, err = newBundle(dir, defaultLanguage) + t.allowedLanguages = allowedLanguages + if len(t.allowedLanguages) == 0 { + t.allowedLanguages = SupportedLanguages() + } + t.bundle, err = newBundle(LoadFilesystem(ns), defaultLanguage, t.allowedLanguages) if err != nil { return nil, err } @@ -53,64 +60,8 @@ func NewTranslator(dir http.FileSystem, defaultLanguage language.Tag, cookieName return t, nil } -func newBundle(dir http.FileSystem, defaultLanguage language.Tag) (*i18n.Bundle, error) { - bundle := i18n.NewBundle(defaultLanguage) - bundle.RegisterUnmarshalFunc("yaml", func(data []byte, v interface{}) error { return yaml.Unmarshal(data, v) }) - bundle.RegisterUnmarshalFunc("json", json.Unmarshal) - bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) - i18nDir, err := dir.Open(i18nPath) - if err != nil { - return nil, errors.ThrowNotFound(err, "I18N-MnXRie", "path not found") - } - defer i18nDir.Close() - files, err := i18nDir.Readdir(0) - if err != nil { - return nil, errors.ThrowNotFound(err, "I18N-Gew23", "cannot read dir") - } - for _, file := range files { - if err := addFileFromFileSystemToBundle(dir, bundle, file); err != nil { - return nil, errors.ThrowNotFoundf(err, "I18N-ZS2AW", "cannot append file %s to Bundle", file.Name()) - } - } - return bundle, nil -} - -func addFileFromFileSystemToBundle(dir http.FileSystem, bundle *i18n.Bundle, file os.FileInfo) error { - f, err := dir.Open("/i18n/" + file.Name()) - if err != nil { - return err - } - defer f.Close() - content, err := ioutil.ReadAll(f) - if err != nil { - return err - } - _, err = bundle.ParseMessageFileBytes(content, file.Name()) - return err -} - -func SupportedLanguages(dir http.FileSystem) ([]language.Tag, error) { - i18nDir, err := dir.Open("/i18n") - if err != nil { - return nil, errors.ThrowNotFound(err, "I18N-Dbt42", "cannot open dir") - } - defer i18nDir.Close() - files, err := i18nDir.Readdir(0) - if err != nil { - return nil, errors.ThrowNotFound(err, "I18N-Gh4zk", "cannot read dir") - } - languages := make([]language.Tag, 0, len(files)) - for _, file := range files { - lang := language.Make(strings.TrimSuffix(file.Name(), ".yaml")) - if lang != language.Und { - languages = append(languages, lang) - } - } - return languages, nil -} - func (t *Translator) SupportedLanguages() []language.Tag { - return t.bundle.LanguageTags() + return t.allowedLanguages } func (t *Translator) AddMessages(tag language.Tag, messages ...Message) error { @@ -144,7 +95,7 @@ func (t *Translator) LocalizeWithoutArgs(id string, langs ...string) string { } func (t *Translator) Lang(r *http.Request) language.Tag { - matcher := language.NewMatcher(t.bundle.LanguageTags()) + matcher := language.NewMatcher(t.allowedLanguages) tag, _ := language.MatchStrings(matcher, t.langsFromRequest(r)...) return tag } diff --git a/internal/integration/client.go b/internal/integration/client.go index cc4ccc75bd..4e5f2677dc 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -60,7 +60,7 @@ func newClient(cc *grpc.ClientConn) Client { } func (t *Tester) UseIsolatedInstance(iamOwnerCtx, systemCtx context.Context) (primaryDomain, instanceId string, authenticatedIamOwnerCtx context.Context) { - primaryDomain = randString(5) + ".integration.localhost" + primaryDomain = RandString(5) + ".integration.localhost" instance, err := t.Client.System.CreateInstance(systemCtx, &system.CreateInstanceRequest{ InstanceName: "testinstance", CustomDomain: primaryDomain, @@ -85,8 +85,8 @@ func (t *Tester) UseIsolatedInstance(iamOwnerCtx, systemCtx context.Context) (pr func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse { resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{ - Organisation: &object.Organisation{ - Org: &object.Organisation_OrgId{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ OrgId: s.Organisation.ID, }, }, diff --git a/internal/integration/config/zitadel.yaml b/internal/integration/config/zitadel.yaml index 5bf6937af9..8768e8e513 100644 --- a/internal/integration/config/zitadel.yaml +++ b/internal/integration/config/zitadel.yaml @@ -51,3 +51,9 @@ DefaultInstance: SystemAPIUsers: - tester: KeyData: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aStGRlNKTDdmNXl3NEtUd3pnTQpQMzRlUEd5Y20vTStrVDBNN1Y0Q2d4NVYzRWFESXZUUUtUTGZCYUVCNDV6YjlMdGpJWHpEdzByWFJvUzJoTzZ0CmgrQ1lRQ3ozS0N2aDA5QzBJenhaaUIySVMzSC9hVCs1Qng5RUZZK3ZuQWtaamNjYnlHNVlOUnZtdE9sbnZJZUkKSDdxWjB0RXdrUGZGNUdFWk5QSlB0bXkzVUdWN2lvZmRWUVMxeFJqNzMrYU13NXJ2SDREOElkeWlBQzNWZWtJYgpwdDBWajBTVVgzRHdLdG9nMzM3QnpUaVBrM2FYUkYwc2JGaFFvcWRKUkk4TnFnWmpDd2pxOXlmSTV0eXhZc3duCitKR3pIR2RIdlczaWRPRGxtd0V0NUsycGFzaVJJV0syT0dmcSt3MEVjbHRRSGFidXFFUGdabG1oQ2tSZE5maXgKQndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" + Memberships: + - MemberType: System + Roles: + - "SYSTEM_OWNER" + - "IAM_OWNER" + - "ORG_OWNER" diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index b6edcd3aea..aaf3ff1e31 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -8,26 +8,30 @@ import ( "strings" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/zitadel/oidc/v3/pkg/client" "github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/client/rs" "github.com/zitadel/oidc/v3/pkg/oidc" + "google.golang.org/protobuf/types/known/timestamppb" http_util "github.com/zitadel/zitadel/internal/api/http" oidc_internal "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/pkg/grpc/app" + "github.com/zitadel/zitadel/pkg/grpc/authn" "github.com/zitadel/zitadel/pkg/grpc/management" + "github.com/zitadel/zitadel/pkg/grpc/user" ) -func (s *Tester) CreateOIDCNativeClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) { +func (s *Tester) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, appType app.OIDCAppType, authMethod app.OIDCAuthMethodType) (*management.AddOIDCAppResponse, error) { return s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{ ProjectId: projectID, Name: fmt.Sprintf("app-%d", time.Now().UnixNano()), RedirectUris: []string{redirectURI}, ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN}, - AppType: app.OIDCAppType_OIDC_APP_TYPE_NATIVE, - AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, + AppType: appType, + AuthMethodType: authMethod, PostLogoutRedirectUris: []string{logoutRedirectURI}, Version: app.OIDCVersion_OIDC_VERSION_1_0, DevMode: false, @@ -41,6 +45,46 @@ func (s *Tester) CreateOIDCNativeClient(ctx context.Context, redirectURI, logout }) } +func (s *Tester) CreateOIDCNativeClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (*management.AddOIDCAppResponse, error) { + return s.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE) +} + +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) +} + +func (s *Tester) CreateOIDCWebClientJWT(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (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) + if err != nil { + return nil, nil, err + } + key, err := s.Client.Mgmt.AddAppKey(ctx, &management.AddAppKeyRequest{ + ProjectId: projectID, + AppId: client.GetAppId(), + Type: authn.KeyType_KEY_TYPE_JSON, + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour)), + }) + if err != nil { + return nil, nil, 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) + if err != nil { + return nil, err + } + _, err = s.Client.Mgmt.DeactivateApp(ctx, &management.DeactivateAppRequest{ + ProjectId: projectID, + AppId: client.GetAppId(), + }) + if err != nil { + return nil, err + } + return client, err +} + func (s *Tester) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI string) (*management.AddOIDCAppResponse, error) { project, err := s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), @@ -83,14 +127,14 @@ func (s *Tester) CreateAPIClient(ctx context.Context, projectID string) (*manage }) } +const CodeVerifier = "codeVerifier" + func (s *Tester) CreateOIDCAuthRequest(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { provider, err := s.CreateRelyingParty(ctx, clientID, redirectURI, scope...) if err != nil { return "", err } - - codeVerifier := "codeVerifier" - codeChallenge := oidc.NewSHACodeChallenge(codeVerifier) + codeChallenge := oidc.NewSHACodeChallenge(CodeVerifier) authURL := rp.AuthURL("state", provider, rp.WithCodeChallenge(codeChallenge)) req, err := GetRequest(authURL, map[string]string{oidc_internal.LoginClientHeader: loginClient}) @@ -196,3 +240,22 @@ func CheckRedirect(req *http.Request) (*url.URL, error) { return resp.Location() } + +func (s *Tester) CreateOIDCCredentialsClient(ctx context.Context) (string, string, error) { + name := gofakeit.Username() + user, err := s.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{ + Name: name, + UserName: name, + AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, + }) + if err != nil { + return "", "", err + } + secret, err := s.Client.Mgmt.GenerateMachineSecret(ctx, &management.GenerateMachineSecretRequest{ + UserId: user.GetUserId(), + }) + if err != nil { + return "", "", err + } + return secret.GetClientId(), secret.GetClientSecret(), nil +} diff --git a/internal/integration/rand.go b/internal/integration/rand.go index 4425c97c8c..d4f01b51c8 100644 --- a/internal/integration/rand.go +++ b/internal/integration/rand.go @@ -11,7 +11,7 @@ func init() { var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz") -func randString(n int) string { +func RandString(n int) string { b := make([]rune, n) for i := range b { b[i] = letterRunes[rand.Intn(len(letterRunes))] diff --git a/internal/notification/channels/smtp/channel.go b/internal/notification/channels/smtp/channel.go index ceea5c9dfb..35da2b3e1c 100644 --- a/internal/notification/channels/smtp/channel.go +++ b/internal/notification/channels/smtp/channel.go @@ -2,10 +2,10 @@ package smtp import ( "crypto/tls" + "errors" "net" "net/smtp" - "github.com/pkg/errors" "github.com/zitadel/logging" caos_errs "github.com/zitadel/zitadel/internal/errors" diff --git a/internal/notification/handlers/mock/queries.mock.go b/internal/notification/handlers/mock/queries.mock.go index 4d4c5516ac..e31aacad0f 100644 --- a/internal/notification/handlers/mock/queries.mock.go +++ b/internal/notification/handlers/mock/queries.mock.go @@ -85,6 +85,21 @@ func (mr *MockQueriesMockRecorder) GetDefaultLanguage(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultLanguage", reflect.TypeOf((*MockQueries)(nil).GetDefaultLanguage), arg0) } +// GetInstanceRestrictions mocks base method. +func (m *MockQueries) GetInstanceRestrictions(arg0 context.Context) (query.Restrictions, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetInstanceRestrictions", arg0) + ret0, _ := ret[0].(query.Restrictions) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetInstanceRestrictions indicates an expected call of GetInstanceRestrictions. +func (mr *MockQueriesMockRecorder) GetInstanceRestrictions(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceRestrictions", reflect.TypeOf((*MockQueries)(nil).GetInstanceRestrictions), arg0) +} + // GetNotifyUserByID mocks base method. func (m *MockQueries) GetNotifyUserByID(arg0 context.Context, arg1 bool, arg2 string, arg3 ...query.SearchQuery) (*query.NotifyUser, error) { m.ctrl.T.Helper() diff --git a/internal/notification/handlers/queries.go b/internal/notification/handlers/queries.go index cd50a26b41..eadfbd7573 100644 --- a/internal/notification/handlers/queries.go +++ b/internal/notification/handlers/queries.go @@ -2,8 +2,6 @@ package handlers import ( "context" - "net/http" - "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/crypto" @@ -25,6 +23,7 @@ type Queries interface { SMSProviderConfig(ctx context.Context, queries ...query.SearchQuery) (*query.SMSConfig, error) SMTPConfigByAggregateID(ctx context.Context, aggregateID string) (*query.SMTPConfig, error) GetDefaultLanguage(ctx context.Context) language.Tag + GetInstanceRestrictions(ctx context.Context) (restrictions query.Restrictions, err error) } type NotificationQueries struct { @@ -37,7 +36,6 @@ type NotificationQueries struct { UserDataCrypto crypto.EncryptionAlgorithm SMTPPasswordCrypto crypto.EncryptionAlgorithm SMSTokenCrypto crypto.EncryptionAlgorithm - statikDir http.FileSystem } func NewNotificationQueries( @@ -50,7 +48,6 @@ func NewNotificationQueries( userDataCrypto crypto.EncryptionAlgorithm, smtpPasswordCrypto crypto.EncryptionAlgorithm, smsTokenCrypto crypto.EncryptionAlgorithm, - statikDir http.FileSystem, ) *NotificationQueries { return &NotificationQueries{ Queries: baseQueries, @@ -62,6 +59,5 @@ func NewNotificationQueries( UserDataCrypto: userDataCrypto, SMTPPasswordCrypto: smtpPasswordCrypto, SMSTokenCrypto: smsTokenCrypto, - statikDir: statikDir, } } diff --git a/internal/notification/handlers/translator.go b/internal/notification/handlers/translator.go index 627bb42a27..d805985795 100644 --- a/internal/notification/handlers/translator.go +++ b/internal/notification/handlers/translator.go @@ -10,7 +10,11 @@ import ( ) func (n *NotificationQueries) GetTranslatorWithOrgTexts(ctx context.Context, orgID, textType string) (*i18n.Translator, error) { - translator, err := i18n.NewTranslator(n.statikDir, n.GetDefaultLanguage(ctx), "") + restrictions, err := n.Queries.GetInstanceRestrictions(ctx) + if err != nil { + return nil, err + } + translator, err := i18n.NewNotificationTranslator(n.GetDefaultLanguage(ctx), restrictions.AllowedLanguages) if err != nil { return nil, err } diff --git a/internal/notification/handlers/user_notifier_test.go b/internal/notification/handlers/user_notifier_test.go index 1ca10a3d15..86b843fc52 100644 --- a/internal/notification/handlers/user_notifier_test.go +++ b/internal/notification/handlers/user_notifier_test.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "fmt" - "net/http" "testing" "time" @@ -12,7 +11,6 @@ import ( "github.com/zitadel/zitadel/internal/notification/messages" - statik_fs "github.com/rakyll/statik/fs" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "golang.org/x/text/language" @@ -202,15 +200,13 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { }, }} // TODO: Why don't we have an url template on user.HumanInitialCodeAddedEvent? - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reduceInitCodeAdded(a.event) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceInitCodeAdded(a.event) if w.err != nil { w.err(t, err) } else { @@ -423,15 +419,13 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { }, w }, }} - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reduceEmailCodeAdded(a.event) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceEmailCodeAdded(a.event) if w.err != nil { w.err(t, err) } else { @@ -644,15 +638,13 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { }, w }, }} - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reducePasswordCodeAdded(a.event) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordCodeAdded(a.event) if w.err != nil { w.err(t, err) } else { @@ -737,15 +729,13 @@ func Test_userNotifier_reduceDomainClaimed(t *testing.T) { }, w }, }} - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reduceDomainClaimed(a.event) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceDomainClaimed(a.event) if w.err != nil { w.err(t, err) } else { @@ -963,15 +953,13 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { }, w }, }} - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reducePasswordlessCodeRequested(a.event) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordlessCodeRequested(a.event) if w.err != nil { w.err(t, err) } else { @@ -1062,15 +1050,13 @@ func Test_userNotifier_reducePasswordChanged(t *testing.T) { }, w }, }} - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reducePasswordChanged(a.event) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordChanged(a.event) if w.err != nil { w.err(t, err) } else { @@ -1287,15 +1273,13 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { }, w }, }} - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - _, err = newUserNotifier(t, ctrl, queries, fs, f, a, w).reduceSessionOTPEmailChallenged(a.event) + _, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceSessionOTPEmailChallenged(a.event) if w.err != nil { w.err(t, err) } else { @@ -1320,7 +1304,7 @@ type want struct { err assert.ErrorAssertionFunc } -func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, fs http.FileSystem, f fields, a args, w want) *userNotifier { +func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, f fields, a args, w want) *userNotifier { queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") channel := channel_mock.NewMockNotificationChannel(ctrl) @@ -1340,7 +1324,6 @@ func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQu f.userDataCrypto, smtpAlg, f.SMSTokenCrypto, - fs, ), otpEmailTmpl: defaultOTPEmailTemplate, channels: &channels{Chain: *senders.ChainChannels(channel)}, @@ -1366,6 +1349,9 @@ func (c *channels) Webhook(context.Context, webhook.Config) (*senders.Chain, err } func expectTemplateQueries(queries *mock.MockQueries, template string) { + queries.EXPECT().GetInstanceRestrictions(gomock.Any()).Return(query.Restrictions{ + AllowedLanguages: []language.Tag{language.English}, + }, nil) queries.EXPECT().ActiveLabelPolicyByOrg(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.LabelPolicy{ ID: policyID, Light: query.Theme{ diff --git a/internal/notification/projections.go b/internal/notification/projections.go index b2630d2330..341e351461 100644 --- a/internal/notification/projections.go +++ b/internal/notification/projections.go @@ -3,9 +3,6 @@ package notification import ( "context" - statik_fs "github.com/rakyll/statik/fs" - "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" @@ -29,9 +26,7 @@ func Start( fileSystemPath string, userEncryption, smtpEncryption, smsEncryption crypto.EncryptionAlgorithm, ) { - statikFS, err := statik_fs.NewWithNamespace("notification") - logging.OnError(err).Panic("unable to start listener") - q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption, statikFS) + q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption) c := newChannels(q) handlers.NewUserNotifier(ctx, projection.ApplyCustomConfig(userHandlerCustomConfig), commands, q, c, otpEmailTmpl).Start(ctx) handlers.NewQuotaNotifier(ctx, projection.ApplyCustomConfig(quotaHandlerCustomConfig), commands, q, c).Start(ctx) diff --git a/internal/query/custom_text.go b/internal/query/custom_text.go index 794271f3ec..6641b1b205 100644 --- a/internal/query/custom_text.go +++ b/internal/query/custom_text.go @@ -17,6 +17,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" ) @@ -217,9 +218,9 @@ func (q *Queries) readLoginTranslationFile(ctx context.Context, lang string) ([] contents, ok := q.LoginTranslationFileContents[lang] var err error if !ok { - contents, err = q.readTranslationFile(q.LoginDir, fmt.Sprintf("/i18n/%s.yaml", lang)) + contents, err = q.readTranslationFile(i18n.LOGIN, fmt.Sprintf("/i18n/%s.yaml", lang)) if errors.IsNotFound(err) { - contents, err = q.readTranslationFile(q.LoginDir, fmt.Sprintf("/i18n/%s.yaml", authz.GetInstance(ctx).DefaultLanguage().String())) + contents, err = q.readTranslationFile(i18n.LOGIN, fmt.Sprintf("/i18n/%s.yaml", authz.GetInstance(ctx).DefaultLanguage().String())) } if err != nil { return nil, err diff --git a/internal/query/embed/oidc_client_by_id.sql b/internal/query/embed/oidc_client_by_id.sql new file mode 100644 index 0000000000..64986a42da --- /dev/null +++ b/internal/query/embed/oidc_client_by_id.sql @@ -0,0 +1,46 @@ +--deallocate q; +--prepare q(text, text, boolean) as + +with client as ( + select c.instance_id, + c.app_id, c.client_id, c.client_secret, c.redirect_uris, c.response_types, c.grant_types, + c.application_type, c.auth_method_type, c.post_logout_redirect_uris, c.is_dev_mode, + c.access_token_type, c.access_token_role_assertion, c.id_token_role_assertion, + c.id_token_userinfo_assertion, c.clock_skew, c.additional_origins, a.project_id, a.state + from projections.apps6_oidc_configs c + join projections.apps6 a on a.id = c.app_id and a.instance_id = c.instance_id + where c.instance_id = $1 + and c.client_id = $2 +), +roles as ( + select p.project_id, json_agg(p.role_key) as project_role_keys + from projections.project_roles4 p + join client c on c.project_id = p.project_id + and p.instance_id = c.instance_id + group by p.project_id +), +keys as ( + select identifier as client_id, json_object_agg(id, encode(public_key, 'base64')) as public_keys + from projections.authn_keys2 + where $3 = true -- when argument is false, don't waste time on trying to query for keys. + and instance_id = $1 + and identifier = $2 + and expiration > current_timestamp + group by identifier +), +settings as ( + select instance_id, access_token_lifetime, id_token_lifetime + from projections.oidc_settings2 + where aggregate_id = $1 + and instance_id = $1 +) + +select row_to_json(r) as client from ( + select c.*, r.project_role_keys, k.public_keys, s.access_token_lifetime, s.id_token_lifetime + from client c + left join roles r on r.project_id = c.project_id + left join keys k on k.client_id = c.client_id + join settings s on s.instance_id = s.instance_id +) r; + +--execute q('230690539048009730', '236647088211951618@tests', true); \ No newline at end of file diff --git a/internal/query/embed/userinfo_by_id.sql b/internal/query/embed/userinfo_by_id.sql index 1f289f60c9..ad959e43f0 100644 --- a/internal/query/embed/userinfo_by_id.sql +++ b/internal/query/embed/userinfo_by_id.sql @@ -1,6 +1,6 @@ with usr as ( select u.id, u.creation_date, u.change_date, u.sequence, u.state, u.resource_owner, u.username, n.login_name as preferred_login_name - from projections.users9 u + from projections.users10 u left join projections.login_names3 n on u.id = n.user_id and u.instance_id = n.instance_id where u.id = $1 and u.instance_id = $2 @@ -9,7 +9,7 @@ with usr as ( human as ( select $1 as user_id, row_to_json(r) as human from ( select first_name, last_name, nick_name, display_name, avatar_key, preferred_language, gender, email, is_email_verified, phone, is_phone_verified - from projections.users9_humans + from projections.users10_humans where user_id = $1 and instance_id = $2 ) r @@ -17,7 +17,7 @@ human as ( machine as ( select $1 as user_id, row_to_json(r) as machine from ( select name, description - from projections.users9_machines + from projections.users10_machines where user_id = $1 and instance_id = $2 ) r diff --git a/internal/query/iam_member_test.go b/internal/query/iam_member_test.go index 476e4e2358..8f1857eae9 100644 --- a/internal/query/iam_member_test.go +++ b/internal/query/iam_member_test.go @@ -21,21 +21,21 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names3.login_name" + - ", projections.users9_humans.email" + - ", projections.users9_humans.first_name" + - ", projections.users9_humans.last_name" + - ", projections.users9_humans.display_name" + - ", projections.users9_machines.name" + - ", projections.users9_humans.avatar_key" + - ", projections.users9.type" + + ", projections.users10_humans.email" + + ", projections.users10_humans.first_name" + + ", projections.users10_humans.last_name" + + ", projections.users10_humans.display_name" + + ", projections.users10_machines.name" + + ", projections.users10_humans.avatar_key" + + ", projections.users10.type" + ", COUNT(*) OVER () " + "FROM projections.instance_members4 AS members " + - "LEFT JOIN projections.users9_humans " + - "ON members.user_id = projections.users9_humans.user_id AND members.instance_id = projections.users9_humans.instance_id " + - "LEFT JOIN projections.users9_machines " + - "ON members.user_id = projections.users9_machines.user_id AND members.instance_id = projections.users9_machines.instance_id " + - "LEFT JOIN projections.users9 " + - "ON members.user_id = projections.users9.id AND members.instance_id = projections.users9.instance_id " + + "LEFT JOIN projections.users10_humans " + + "ON members.user_id = projections.users10_humans.user_id AND members.instance_id = projections.users10_humans.instance_id " + + "LEFT JOIN projections.users10_machines " + + "ON members.user_id = projections.users10_machines.user_id AND members.instance_id = projections.users10_machines.instance_id " + + "LEFT JOIN projections.users10 " + + "ON members.user_id = projections.users10.id AND members.instance_id = projections.users10.instance_id " + "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id AND members.instance_id = projections.login_names3.instance_id " + "AS OF SYSTEM TIME '-1 ms' " + diff --git a/internal/query/languages.go b/internal/query/languages.go deleted file mode 100644 index f12c29677e..0000000000 --- a/internal/query/languages.go +++ /dev/null @@ -1,22 +0,0 @@ -package query - -import ( - "context" - - "github.com/zitadel/logging" - "golang.org/x/text/language" - - "github.com/zitadel/zitadel/internal/i18n" -) - -func (q *Queries) Languages(ctx context.Context) ([]language.Tag, error) { - if len(q.supportedLangs) == 0 { - langs, err := i18n.SupportedLanguages(q.LoginDir) - if err != nil { - logging.Log("ADMIN-tiMWs").WithError(err).Debug("unable to parse language") - return nil, err - } - q.supportedLangs = langs - } - return q.supportedLangs, nil -} diff --git a/internal/query/message_text.go b/internal/query/message_text.go index 4bc656c0cc..ab7674c7ae 100644 --- a/internal/query/message_text.go +++ b/internal/query/message_text.go @@ -7,7 +7,6 @@ import ( errs "errors" "fmt" "io/ioutil" - "net/http" "os" "time" @@ -19,6 +18,7 @@ import ( "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" ) @@ -236,9 +236,9 @@ func (q *Queries) readNotificationTextMessages(ctx context.Context, language str var err error contents, ok := q.NotificationTranslationFileContents[language] if !ok { - contents, err = q.readTranslationFile(q.NotificationDir, fmt.Sprintf("/i18n/%s.yaml", language)) + contents, err = q.readTranslationFile(i18n.NOTIFICATION, fmt.Sprintf("/i18n/%s.yaml", language)) if errors.IsNotFound(err) { - contents, err = q.readTranslationFile(q.NotificationDir, fmt.Sprintf("/i18n/%s.yaml", authz.GetInstance(ctx).DefaultLanguage().String())) + contents, err = q.readTranslationFile(i18n.NOTIFICATION, fmt.Sprintf("/i18n/%s.yaml", authz.GetInstance(ctx).DefaultLanguage().String())) } if err != nil { return nil, err @@ -311,8 +311,8 @@ func prepareMessageTextQuery(ctx context.Context, db prepareDatabase) (sq.Select } } -func (q *Queries) readTranslationFile(dir http.FileSystem, filename string) ([]byte, error) { - r, err := dir.Open(filename) +func (q *Queries) readTranslationFile(namespace i18n.Namespace, filename string) ([]byte, error) { + r, err := i18n.LoadFilesystem(namespace).Open(filename) if os.IsNotExist(err) { return nil, errors.ThrowNotFound(err, "QUERY-sN9wg", "Errors.TranslationFile.NotFound") } diff --git a/internal/query/oidc_client.go b/internal/query/oidc_client.go new file mode 100644 index 0000000000..60ecf25174 --- /dev/null +++ b/internal/query/oidc_client.go @@ -0,0 +1,61 @@ +package query + +import ( + "context" + "database/sql" + _ "embed" + "errors" + "time" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + zerrors "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +type OIDCClient struct { + InstanceID string `json:"instance_id,omitempty"` + AppID string `json:"app_id,omitempty"` + State domain.AppState `json:"state,omitempty"` + ClientID string `json:"client_id,omitempty"` + ClientSecret *crypto.CryptoValue `json:"client_secret,omitempty"` + RedirectURIs []string `json:"redirect_uris,omitempty"` + ResponseTypes []domain.OIDCResponseType `json:"response_types,omitempty"` + GrantTypes []domain.OIDCGrantType `json:"grant_types,omitempty"` + ApplicationType domain.OIDCApplicationType `json:"application_type,omitempty"` + AuthMethodType domain.OIDCAuthMethodType `json:"auth_method_type,omitempty"` + PostLogoutRedirectURIs []string `json:"post_logout_redirect_uris,omitempty"` + IsDevMode bool `json:"is_dev_mode,omitempty"` + AccessTokenType domain.OIDCTokenType `json:"access_token_type,omitempty"` + AccessTokenRoleAssertion bool `json:"access_token_role_assertion,omitempty"` + IDTokenRoleAssertion bool `json:"id_token_role_assertion,omitempty"` + IDTokenUserinfoAssertion bool `json:"id_token_userinfo_assertion,omitempty"` + ClockSkew time.Duration `json:"clock_skew,omitempty"` + AdditionalOrigins []string `json:"additional_origins,omitempty"` + PublicKeys map[string][]byte `json:"public_keys,omitempty"` + ProjectID string `json:"project_id,omitempty"` + ProjectRoleKeys []string `json:"project_role_keys,omitempty"` + AccessTokenLifetime time.Duration `json:"access_token_lifetime,omitempty"` + IDTokenLifetime time.Duration `json:"id_token_lifetime,omitempty"` +} + +//go:embed embed/oidc_client_by_id.sql +var oidcClientQuery string + +func (q *Queries) GetOIDCClientByID(ctx context.Context, clientID string, getKeys bool) (client *OIDCClient, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + client, err = database.QueryJSONObject[OIDCClient](ctx, q.client, oidcClientQuery, + authz.GetInstance(ctx).InstanceID(), clientID, getKeys, + ) + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-wu6Ee", "Errors.App.NotFound") + } + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-ieR7R", "Errors.Internal") + } + return client, err +} diff --git a/internal/query/oidc_client_test.go b/internal/query/oidc_client_test.go new file mode 100644 index 0000000000..0593e1ce03 --- /dev/null +++ b/internal/query/oidc_client_test.go @@ -0,0 +1,167 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + _ "embed" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + zerrors "github.com/zitadel/zitadel/internal/errors" +) + +var ( + //go:embed testdata/oidc_client_jwt.json + testdataOidcClientJWT string + //go:embed testdata/oidc_client_public.json + testdataOidcClientPublic string + //go:embed testdata/oidc_client_secret.json + testdataOidcClientSecret string +) + +func TestQueries_GetOIDCClientByID(t *testing.T) { + expQuery := regexp.QuoteMeta(oidcClientQuery) + cols := []string{"client"} + pubkey := `-----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 *OIDCClient + wantErr error + }{ + { + name: "no rows", + mock: mockQueryErr(expQuery, sql.ErrNoRows, "instanceID", "clientID", true), + wantErr: zerrors.ThrowNotFound(sql.ErrNoRows, "QUERY-wu6Ee", "Errors.App.NotFound"), + }, + { + name: "internal error", + mock: mockQueryErr(expQuery, sql.ErrConnDone, "instanceID", "clientID", true), + wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-ieR7R", "Errors.Internal"), + }, + { + name: "jwt client", + mock: mockQuery(expQuery, cols, []driver.Value{testdataOidcClientJWT}, "instanceID", "clientID", true), + want: &OIDCClient{ + InstanceID: "230690539048009730", + AppID: "236647088211886082", + State: domain.AppStateActive, + ClientID: "236647088211951618@tests", + ClientSecret: nil, + RedirectURIs: []string{"http://localhost:9999/auth/callback"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode, domain.OIDCGrantTypeRefreshToken}, + ApplicationType: domain.OIDCApplicationTypeWeb, + AuthMethodType: domain.OIDCAuthMethodTypePrivateKeyJWT, + PostLogoutRedirectURIs: []string{"https://example.com/logout"}, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeJWT, + AccessTokenRoleAssertion: true, + IDTokenRoleAssertion: true, + IDTokenUserinfoAssertion: true, + ClockSkew: 1000000000, + AdditionalOrigins: []string{"https://example.com"}, + ProjectID: "236645808328409090", + PublicKeys: map[string][]byte{"236647201860747266": []byte(pubkey)}, + ProjectRoleKeys: []string{"role1", "role2"}, + AccessTokenLifetime: 43200000000000, + IDTokenLifetime: 43200000000000, + }, + }, + { + name: "public client", + mock: mockQuery(expQuery, cols, []driver.Value{testdataOidcClientPublic}, "instanceID", "clientID", true), + want: &OIDCClient{ + InstanceID: "230690539048009730", + AppID: "236646457053020162", + State: domain.AppStateActive, + ClientID: "236646457053085698@tests", + ClientSecret: nil, + RedirectURIs: []string{"http://localhost:9999/auth/callback"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: domain.OIDCApplicationTypeWeb, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectURIs: nil, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeBearer, + AccessTokenRoleAssertion: false, + IDTokenRoleAssertion: false, + IDTokenUserinfoAssertion: false, + ClockSkew: 0, + AdditionalOrigins: nil, + PublicKeys: nil, + ProjectID: "236645808328409090", + ProjectRoleKeys: []string{"role1", "role2"}, + AccessTokenLifetime: 43200000000000, + IDTokenLifetime: 43200000000000, + }, + }, + { + name: "secret client", + mock: mockQuery(expQuery, cols, []driver.Value{testdataOidcClientSecret}, "instanceID", "clientID", true), + want: &OIDCClient{ + InstanceID: "230690539048009730", + AppID: "236646858984783874", + State: domain.AppStateActive, + ClientID: "236646858984849410@tests", + ClientSecret: &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "bcrypt", + Crypted: []byte(`$2a$14$OzZ0XEZZEtD13py/EPba2evsS6WcKZ5orVMj9pWHEGEHmLu2h3PFq`), + }, + RedirectURIs: []string{"http://localhost:9999/auth/callback"}, + ResponseTypes: []domain.OIDCResponseType{0}, + GrantTypes: []domain.OIDCGrantType{0}, + ApplicationType: domain.OIDCApplicationTypeWeb, + AuthMethodType: domain.OIDCAuthMethodTypeBasic, + PostLogoutRedirectURIs: nil, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeBearer, + AccessTokenRoleAssertion: false, + IDTokenRoleAssertion: false, + IDTokenUserinfoAssertion: false, + ClockSkew: 0, + AdditionalOrigins: nil, + PublicKeys: nil, + ProjectID: "236645808328409090", + ProjectRoleKeys: []string{"role1", "role2"}, + AccessTokenLifetime: 43200000000000, + IDTokenLifetime: 43200000000000, + }, + }, + } + 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", "loginClient") + got, err := q.GetOIDCClientByID(ctx, "clientID", true) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + }) + } +} diff --git a/internal/query/org_member_test.go b/internal/query/org_member_test.go index 7dc80dffae..37443d4dc9 100644 --- a/internal/query/org_member_test.go +++ b/internal/query/org_member_test.go @@ -21,24 +21,24 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names3.login_name" + - ", projections.users9_humans.email" + - ", projections.users9_humans.first_name" + - ", projections.users9_humans.last_name" + - ", projections.users9_humans.display_name" + - ", projections.users9_machines.name" + - ", projections.users9_humans.avatar_key" + - ", projections.users9.type" + + ", projections.users10_humans.email" + + ", projections.users10_humans.first_name" + + ", projections.users10_humans.last_name" + + ", projections.users10_humans.display_name" + + ", projections.users10_machines.name" + + ", projections.users10_humans.avatar_key" + + ", projections.users10.type" + ", COUNT(*) OVER () " + "FROM projections.org_members4 AS members " + - "LEFT JOIN projections.users9_humans " + - "ON members.user_id = projections.users9_humans.user_id " + - "AND members.instance_id = projections.users9_humans.instance_id " + - "LEFT JOIN projections.users9_machines " + - "ON members.user_id = projections.users9_machines.user_id " + - "AND members.instance_id = projections.users9_machines.instance_id " + - "LEFT JOIN projections.users9 " + - "ON members.user_id = projections.users9.id " + - "AND members.instance_id = projections.users9.instance_id " + + "LEFT JOIN projections.users10_humans " + + "ON members.user_id = projections.users10_humans.user_id " + + "AND members.instance_id = projections.users10_humans.instance_id " + + "LEFT JOIN projections.users10_machines " + + "ON members.user_id = projections.users10_machines.user_id " + + "AND members.instance_id = projections.users10_machines.instance_id " + + "LEFT JOIN projections.users10 " + + "ON members.user_id = projections.users10.id " + + "AND members.instance_id = projections.users10.instance_id " + "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id " + "AND members.instance_id = projections.login_names3.instance_id " + diff --git a/internal/query/project_grant_member_test.go b/internal/query/project_grant_member_test.go index 2cf413913c..91cd210679 100644 --- a/internal/query/project_grant_member_test.go +++ b/internal/query/project_grant_member_test.go @@ -21,24 +21,24 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names3.login_name" + - ", projections.users9_humans.email" + - ", projections.users9_humans.first_name" + - ", projections.users9_humans.last_name" + - ", projections.users9_humans.display_name" + - ", projections.users9_machines.name" + - ", projections.users9_humans.avatar_key" + - ", projections.users9.type" + + ", projections.users10_humans.email" + + ", projections.users10_humans.first_name" + + ", projections.users10_humans.last_name" + + ", projections.users10_humans.display_name" + + ", projections.users10_machines.name" + + ", projections.users10_humans.avatar_key" + + ", projections.users10.type" + ", COUNT(*) OVER () " + "FROM projections.project_grant_members4 AS members " + - "LEFT JOIN projections.users9_humans " + - "ON members.user_id = projections.users9_humans.user_id " + - "AND members.instance_id = projections.users9_humans.instance_id " + - "LEFT JOIN projections.users9_machines " + - "ON members.user_id = projections.users9_machines.user_id " + - "AND members.instance_id = projections.users9_machines.instance_id " + - "LEFT JOIN projections.users9 " + - "ON members.user_id = projections.users9.id " + - "AND members.instance_id = projections.users9.instance_id " + + "LEFT JOIN projections.users10_humans " + + "ON members.user_id = projections.users10_humans.user_id " + + "AND members.instance_id = projections.users10_humans.instance_id " + + "LEFT JOIN projections.users10_machines " + + "ON members.user_id = projections.users10_machines.user_id " + + "AND members.instance_id = projections.users10_machines.instance_id " + + "LEFT JOIN projections.users10 " + + "ON members.user_id = projections.users10.id " + + "AND members.instance_id = projections.users10.instance_id " + "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id " + "AND members.instance_id = projections.login_names3.instance_id " + diff --git a/internal/query/project_member_test.go b/internal/query/project_member_test.go index b280750247..defec46d49 100644 --- a/internal/query/project_member_test.go +++ b/internal/query/project_member_test.go @@ -21,24 +21,24 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names3.login_name" + - ", projections.users9_humans.email" + - ", projections.users9_humans.first_name" + - ", projections.users9_humans.last_name" + - ", projections.users9_humans.display_name" + - ", projections.users9_machines.name" + - ", projections.users9_humans.avatar_key" + - ", projections.users9.type" + + ", projections.users10_humans.email" + + ", projections.users10_humans.first_name" + + ", projections.users10_humans.last_name" + + ", projections.users10_humans.display_name" + + ", projections.users10_machines.name" + + ", projections.users10_humans.avatar_key" + + ", projections.users10.type" + ", COUNT(*) OVER () " + "FROM projections.project_members4 AS members " + - "LEFT JOIN projections.users9_humans " + - "ON members.user_id = projections.users9_humans.user_id " + - "AND members.instance_id = projections.users9_humans.instance_id " + - "LEFT JOIN projections.users9_machines " + - "ON members.user_id = projections.users9_machines.user_id " + - "AND members.instance_id = projections.users9_machines.instance_id " + - "LEFT JOIN projections.users9 " + - "ON members.user_id = projections.users9.id " + - "AND members.instance_id = projections.users9.instance_id " + + "LEFT JOIN projections.users10_humans " + + "ON members.user_id = projections.users10_humans.user_id " + + "AND members.instance_id = projections.users10_humans.instance_id " + + "LEFT JOIN projections.users10_machines " + + "ON members.user_id = projections.users10_machines.user_id " + + "AND members.instance_id = projections.users10_machines.instance_id " + + "LEFT JOIN projections.users10 " + + "ON members.user_id = projections.users10.id " + + "AND members.instance_id = projections.users10.instance_id " + "LEFT JOIN projections.login_names3 " + "ON members.user_id = projections.login_names3.user_id " + "AND members.instance_id = projections.login_names3.instance_id " + diff --git a/internal/query/projection/restrictions.go b/internal/query/projection/restrictions.go index 597a6fac64..44bf97c5d0 100644 --- a/internal/query/projection/restrictions.go +++ b/internal/query/projection/restrictions.go @@ -3,6 +3,7 @@ package projection import ( "context" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -11,7 +12,7 @@ import ( ) const ( - RestrictionsProjectionTable = "projections.restrictions" + RestrictionsProjectionTable = "projections.restrictions2" RestrictionsColumnAggregateID = "aggregate_id" RestrictionsColumnCreationDate = "creation_date" @@ -21,6 +22,7 @@ const ( RestrictionsColumnSequence = "sequence" RestrictionsColumnDisallowPublicOrgRegistration = "disallow_public_org_registration" + RestrictionsColumnAllowedLanguages = "allowed_languages" ) type restrictionsProjection struct{} @@ -42,7 +44,8 @@ func (*restrictionsProjection) Init() *old_handler.Check { handler.NewColumn(RestrictionsColumnResourceOwner, handler.ColumnTypeText), handler.NewColumn(RestrictionsColumnInstanceID, handler.ColumnTypeText), handler.NewColumn(RestrictionsColumnSequence, handler.ColumnTypeInt64), - handler.NewColumn(RestrictionsColumnDisallowPublicOrgRegistration, handler.ColumnTypeBool), + handler.NewColumn(RestrictionsColumnDisallowPublicOrgRegistration, handler.ColumnTypeBool, handler.Nullable()), + handler.NewColumn(RestrictionsColumnAllowedLanguages, handler.ColumnTypeTextArray, handler.Nullable()), }, handler.NewPrimaryKey(RestrictionsColumnInstanceID, RestrictionsColumnResourceOwner), ), @@ -89,8 +92,11 @@ func (p *restrictionsProjection) reduceRestrictionsSet(event eventstore.Event) ( handler.NewCol(RestrictionsColumnSequence, e.Sequence()), handler.NewCol(RestrictionsColumnAggregateID, e.Aggregate().ID), } - if e.DisallowPublicOrgRegistrations != nil { - updateCols = append(updateCols, handler.NewCol(RestrictionsColumnDisallowPublicOrgRegistration, *e.DisallowPublicOrgRegistrations)) + if e.DisallowPublicOrgRegistration != nil { + updateCols = append(updateCols, handler.NewCol(RestrictionsColumnDisallowPublicOrgRegistration, *e.DisallowPublicOrgRegistration)) + } + if e.AllowedLanguages != nil { + updateCols = append(updateCols, handler.NewCol(RestrictionsColumnAllowedLanguages, domain.LanguagesToStrings(*e.AllowedLanguages))) } return handler.NewUpsertStatement(e, conflictCols, updateCols), nil } diff --git a/internal/query/projection/restrictions_test.go b/internal/query/projection/restrictions_test.go index f018ab00c0..a9058b17b0 100644 --- a/internal/query/projection/restrictions_test.go +++ b/internal/query/projection/restrictions_test.go @@ -25,7 +25,7 @@ func TestRestrictionsProjection_reduces(t *testing.T) { event: getEvent(testEvent( restrictions.SetEventType, restrictions.AggregateType, - []byte(`{ "disallowPublicOrgRegistrations": true }`), + []byte(`{ "disallowPublicOrgRegistration": true }`), ), restrictions.SetEventMapper), }, reduce: (&restrictionsProjection{}).reduceRestrictionsSet, @@ -35,7 +35,7 @@ func TestRestrictionsProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.restrictions (instance_id, resource_owner, creation_date, change_date, sequence, aggregate_id, disallow_public_org_registration) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (instance_id, resource_owner) DO UPDATE SET (creation_date, change_date, sequence, aggregate_id, disallow_public_org_registration) = (projections.restrictions.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.aggregate_id, EXCLUDED.disallow_public_org_registration)", + expectedStmt: "INSERT INTO projections.restrictions2 (instance_id, resource_owner, creation_date, change_date, sequence, aggregate_id, disallow_public_org_registration) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (instance_id, resource_owner) DO UPDATE SET (creation_date, change_date, sequence, aggregate_id, disallow_public_org_registration) = (projections.restrictions2.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.aggregate_id, EXCLUDED.disallow_public_org_registration)", expectedArgs: []interface{}{ "instance-id", "ro-id", @@ -66,7 +66,7 @@ func TestRestrictionsProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.restrictions (instance_id, resource_owner, creation_date, change_date, sequence, aggregate_id) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (instance_id, resource_owner) DO UPDATE SET (creation_date, change_date, sequence, aggregate_id) = (projections.restrictions.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.aggregate_id)", + expectedStmt: "INSERT INTO projections.restrictions2 (instance_id, resource_owner, creation_date, change_date, sequence, aggregate_id) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (instance_id, resource_owner) DO UPDATE SET (creation_date, change_date, sequence, aggregate_id) = (projections.restrictions2.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.aggregate_id)", expectedArgs: []interface{}{ "instance-id", "ro-id", diff --git a/internal/query/projection/user.go b/internal/query/projection/user.go index 3451e6fc37..d9813f1801 100644 --- a/internal/query/projection/user.go +++ b/internal/query/projection/user.go @@ -15,7 +15,7 @@ import ( ) const ( - UserTable = "projections.users9" + UserTable = "projections.users10" UserHumanTable = UserTable + "_" + UserHumanSuffix UserMachineTable = UserTable + "_" + UserMachineSuffix UserNotifyTable = UserTable + "_" + UserNotifySuffix @@ -57,7 +57,7 @@ const ( MachineUserInstanceIDCol = "instance_id" MachineNameCol = "name" MachineDescriptionCol = "description" - MachineHasSecretCol = "has_secret" + MachineSecretCol = "secret" MachineAccessTokenTypeCol = "access_token_type" // notify @@ -122,7 +122,7 @@ func (*userProjection) Init() *old_handler.Check { handler.NewColumn(MachineUserInstanceIDCol, handler.ColumnTypeText), handler.NewColumn(MachineNameCol, handler.ColumnTypeText), handler.NewColumn(MachineDescriptionCol, handler.ColumnTypeText, handler.Nullable()), - handler.NewColumn(MachineHasSecretCol, handler.ColumnTypeBool, handler.Default(false)), + handler.NewColumn(MachineSecretCol, handler.ColumnTypeJSONB, handler.Nullable()), handler.NewColumn(MachineAccessTokenTypeCol, handler.ColumnTypeEnum, handler.Default(0)), }, handler.NewPrimaryKey(MachineUserInstanceIDCol, MachineUserIDCol), @@ -936,7 +936,7 @@ func (p *userProjection) reduceMachineSecretSet(event eventstore.Event) (*handle ), handler.AddUpdateStatement( []handler.Column{ - handler.NewCol(MachineHasSecretCol, true), + handler.NewCol(MachineSecretCol, e.ClientSecret), }, []handler.Condition{ handler.NewCond(MachineUserIDCol, e.Aggregate().ID), @@ -967,7 +967,7 @@ func (p *userProjection) reduceMachineSecretRemoved(event eventstore.Event) (*ha ), handler.AddUpdateStatement( []handler.Column{ - handler.NewCol(MachineHasSecretCol, false), + handler.NewCol(MachineSecretCol, nil), }, []handler.Condition{ handler.NewCond(MachineUserIDCol, e.Aggregate().ID), diff --git a/internal/query/projection/user_test.go b/internal/query/projection/user_test.go index da4f2a860c..fe0a8dad4f 100644 --- a/internal/query/projection/user_test.go +++ b/internal/query/projection/user_test.go @@ -4,6 +4,7 @@ import ( "database/sql" "testing" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" @@ -50,7 +51,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -64,7 +65,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users10_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -79,7 +80,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -119,7 +120,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -133,7 +134,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users10_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -148,7 +149,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -183,7 +184,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -197,7 +198,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users10_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -212,7 +213,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -252,7 +253,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -266,7 +267,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users10_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -281,7 +282,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -321,7 +322,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -335,7 +336,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users10_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -350,7 +351,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -385,7 +386,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -399,7 +400,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users10_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -414,7 +415,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -444,7 +445,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateInitial, "agg-id", @@ -472,7 +473,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateInitial, "agg-id", @@ -500,7 +501,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateActive, "agg-id", @@ -528,7 +529,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateActive, "agg-id", @@ -556,7 +557,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users10 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateLocked, @@ -586,7 +587,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users10 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateActive, @@ -616,7 +617,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users10 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateInactive, @@ -646,7 +647,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users10 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateActive, @@ -676,7 +677,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.users9 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.users10 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -705,7 +706,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users10 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, "username", @@ -737,7 +738,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users10 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, "id@temporary.domain", @@ -774,7 +775,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -783,7 +784,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", + expectedStmt: "UPDATE projections.users10_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ "first-name", "last-name", @@ -823,7 +824,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -832,7 +833,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", + expectedStmt: "UPDATE projections.users10_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ "first-name", "last-name", @@ -867,7 +868,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -876,7 +877,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ domain.PhoneNumber("+41 00 000 00 00"), false, @@ -885,7 +886,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "+41 00 000 00 00", Valid: true}, "agg-id", @@ -915,7 +916,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -924,7 +925,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ domain.PhoneNumber("+41 00 000 00 00"), false, @@ -933,7 +934,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "+41 00 000 00 00", Valid: true}, "agg-id", @@ -961,7 +962,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -970,7 +971,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -979,7 +980,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -1008,7 +1009,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1017,7 +1018,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -1026,7 +1027,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -1055,7 +1056,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1064,7 +1065,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1072,7 +1073,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET verified_phone = last_phone WHERE (user_id = $1) AND (instance_id = $2)", + expectedStmt: "UPDATE projections.users10_notifications SET verified_phone = last_phone WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1099,7 +1100,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1108,7 +1109,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1116,7 +1117,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET verified_phone = last_phone WHERE (user_id = $1) AND (instance_id = $2)", + expectedStmt: "UPDATE projections.users10_notifications SET verified_phone = last_phone WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1145,7 +1146,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1154,7 +1155,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ domain.EmailAddress("email@zitadel.com"), false, @@ -1163,7 +1164,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "email@zitadel.com", Valid: true}, "agg-id", @@ -1193,7 +1194,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1202,7 +1203,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ domain.EmailAddress("email@zitadel.com"), false, @@ -1211,7 +1212,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "email@zitadel.com", Valid: true}, "agg-id", @@ -1239,7 +1240,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1248,7 +1249,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1256,7 +1257,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET verified_email = last_email WHERE (user_id = $1) AND (instance_id = $2)", + expectedStmt: "UPDATE projections.users10_notifications SET verified_email = last_email WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1283,7 +1284,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1292,7 +1293,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1300,7 +1301,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_notifications SET verified_email = last_email WHERE (user_id = $1) AND (instance_id = $2)", + expectedStmt: "UPDATE projections.users10_notifications SET verified_email = last_email WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1329,7 +1330,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1338,7 +1339,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "users/agg-id/avatar", "agg-id", @@ -1366,7 +1367,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1375,7 +1376,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ nil, "agg-id", @@ -1406,7 +1407,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -1420,7 +1421,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_machines (user_id, instance_id, name, description, access_token_type) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_machines (user_id, instance_id, name, description, access_token_type) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1454,7 +1455,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users9 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users10 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -1468,7 +1469,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users9_machines (user_id, instance_id, name, description, access_token_type) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users10_machines (user_id, instance_id, name, description, access_token_type) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1501,7 +1502,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1510,7 +1511,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_machines SET (name, description) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10_machines SET (name, description) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "machine-name", "description", @@ -1541,7 +1542,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1550,7 +1551,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_machines SET name = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_machines SET name = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "machine-name", "agg-id", @@ -1580,7 +1581,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1589,7 +1590,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_machines SET description = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_machines SET description = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "description", "agg-id", @@ -1627,7 +1628,7 @@ func TestUserProjection_reduces(t *testing.T) { user.MachineSecretSetType, user.AggregateType, []byte(`{ - "client_secret": {} + "clientSecret": {"CryptoType":1,"Algorithm":"bcrypt","Crypted":"deadbeef"} }`), ), user.MachineSecretSetEventMapper), }, @@ -1638,7 +1639,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1647,9 +1648,13 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_machines SET has_secret = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_machines SET secret = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ - true, + &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "bcrypt", + Crypted: []byte{117, 230, 157, 109, 231, 159}, + }, "agg-id", "instance-id", }, @@ -1659,7 +1664,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - name: "reduceMachineSecretSet", + name: "reduceMachineSecretRemoved", args: args{ event: getEvent( testEvent( @@ -1675,7 +1680,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users9 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users10 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1684,9 +1689,9 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users9_machines SET has_secret = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users10_machines SET secret = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ - false, + nil, "agg-id", "instance-id", }, @@ -1712,7 +1717,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.users9 WHERE (instance_id = $1) AND (resource_owner = $2)", + expectedStmt: "DELETE FROM projections.users10 WHERE (instance_id = $1) AND (resource_owner = $2)", expectedArgs: []interface{}{ "instance-id", "agg-id", @@ -1739,7 +1744,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.users9 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.users10 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, diff --git a/internal/query/query.go b/internal/query/query.go index 69cb422849..c2538414bb 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -3,12 +3,10 @@ package query import ( "context" "fmt" - "net/http" "regexp" "sync" "time" - "github.com/rakyll/statik/fs" "github.com/zitadel/logging" "golang.org/x/text/language" @@ -47,8 +45,6 @@ type Queries struct { checkPermission domain.PermissionCheck DefaultLanguage language.Tag - LoginDir http.FileSystem - NotificationDir http.FileSystem mutex sync.Mutex LoginTranslationFileContents map[string][]byte NotificationTranslationFileContents map[string][]byte @@ -71,22 +67,10 @@ func StartQueries( defaultAuditLogRetention time.Duration, systemAPIUsers map[string]*authz.SystemAPIUser, ) (repo *Queries, err error) { - statikLoginFS, err := fs.NewWithNamespace("login") - if err != nil { - return nil, fmt.Errorf("unable to start login statik dir") - } - - statikNotificationFS, err := fs.NewWithNamespace("notification") - if err != nil { - return nil, fmt.Errorf("unable to start notification statik dir") - } - repo = &Queries{ eventstore: es, client: sqlClient, DefaultLanguage: language.Und, - LoginDir: statikLoginFS, - NotificationDir: statikNotificationFS, LoginTranslationFileContents: make(map[string][]byte), NotificationTranslationFileContents: make(map[string][]byte), zitadelRoles: zitadelRoles, diff --git a/internal/query/quota_notifications.go b/internal/query/quota_notifications.go index 7fc0748f63..fb85f9373e 100644 --- a/internal/query/quota_notifications.go +++ b/internal/query/quota_notifications.go @@ -3,12 +3,11 @@ package query import ( "context" "database/sql" - errs "errors" + "errors" "math" "time" sq "github.com/Masterminds/squirrel" - "github.com/pkg/errors" "github.com/zitadel/zitadel/internal/api/call" zitadel_errors "github.com/zitadel/zitadel/internal/errors" @@ -166,7 +165,7 @@ func prepareQuotaNotificationsQuery(ctx context.Context, db prepareDatabase) (sq var nextDueThreshold sql.NullInt16 err := rows.Scan(&cfg.ID, &cfg.CallURL, &cfg.Percent, &cfg.Repeat, &nextDueThreshold) if err != nil { - if errs.Is(err, sql.ErrNoRows) { + if errors.Is(err, sql.ErrNoRows) { return nil, zitadel_errors.ThrowNotFound(err, "QUERY-bbqWb", "Errors.QuotaNotification.NotExisting") } return nil, zitadel_errors.ThrowInternal(err, "QUERY-8copS", "Errors.Internal") diff --git a/internal/query/restrictions.go b/internal/query/restrictions.go index 80217164ab..e24a5e4092 100644 --- a/internal/query/restrictions.go +++ b/internal/query/restrictions.go @@ -7,9 +7,12 @@ import ( "time" sq "github.com/Masterminds/squirrel" + "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/call" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" zitade_errors "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -44,10 +47,14 @@ var ( name: projection.RestrictionsColumnSequence, table: restrictionsTable, } - RestrictionsColumnDisallowPublicOrgRegistrations = Column{ + RestrictionsColumnDisallowPublicOrgRegistration = Column{ name: projection.RestrictionsColumnDisallowPublicOrgRegistration, table: restrictionsTable, } + RestrictionsColumnAllowedLanguages = Column{ + name: projection.RestrictionsColumnAllowedLanguages, + table: restrictionsTable, + } ) type Restrictions struct { @@ -58,6 +65,7 @@ type Restrictions struct { Sequence uint64 DisallowPublicOrgRegistration bool + AllowedLanguages []language.Tag } func (q *Queries) GetInstanceRestrictions(ctx context.Context) (restrictions Restrictions, err error) { @@ -91,18 +99,25 @@ func prepareRestrictionsQuery(ctx context.Context, db prepareDatabase) (sq.Selec RestrictionsColumnChangeDate.identifier(), RestrictionsColumnResourceOwner.identifier(), RestrictionsColumnSequence.identifier(), - RestrictionsColumnDisallowPublicOrgRegistrations.identifier(), + RestrictionsColumnDisallowPublicOrgRegistration.identifier(), + RestrictionsColumnAllowedLanguages.identifier(), ). From(restrictionsTable.identifier() + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (restrictions Restrictions, err error) { - return restrictions, row.Scan( + allowedLanguages := database.TextArray[string](make([]string, 0)) + disallowPublicOrgRegistration := sql.NullBool{} + err = row.Scan( &restrictions.AggregateID, &restrictions.CreationDate, &restrictions.ChangeDate, &restrictions.ResourceOwner, &restrictions.Sequence, - &restrictions.DisallowPublicOrgRegistration, + &disallowPublicOrgRegistration, + &allowedLanguages, ) + restrictions.DisallowPublicOrgRegistration = disallowPublicOrgRegistration.Bool + restrictions.AllowedLanguages = domain.StringsToLanguages(allowedLanguages) + return restrictions, err } } diff --git a/internal/query/restrictions_test.go b/internal/query/restrictions_test.go index 83e6d9a8fe..cc7ee8442a 100644 --- a/internal/query/restrictions_test.go +++ b/internal/query/restrictions_test.go @@ -7,16 +7,21 @@ import ( "fmt" "regexp" "testing" + + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/database" ) var ( - expectedRestrictionsQuery = regexp.QuoteMeta("SELECT projections.restrictions.aggregate_id," + - " projections.restrictions.creation_date," + - " projections.restrictions.change_date," + - " projections.restrictions.resource_owner," + - " projections.restrictions.sequence," + - " projections.restrictions.disallow_public_org_registration" + - " FROM projections.restrictions" + + expectedRestrictionsQuery = regexp.QuoteMeta("SELECT projections.restrictions2.aggregate_id," + + " projections.restrictions2.creation_date," + + " projections.restrictions2.change_date," + + " projections.restrictions2.resource_owner," + + " projections.restrictions2.sequence," + + " projections.restrictions2.disallow_public_org_registration," + + " projections.restrictions2.allowed_languages" + + " FROM projections.restrictions2" + " AS OF SYSTEM TIME '-1 ms'", ) @@ -27,6 +32,7 @@ var ( "resource_owner", "sequence", "disallow_public_org_registration", + "allowed_languages", } ) @@ -56,7 +62,9 @@ func Test_RestrictionsPrepare(t *testing.T) { } return nil, true }, - object: Restrictions{}, + object: Restrictions{ + AllowedLanguages: make([]language.Tag, 0), + }, }, }, { @@ -73,6 +81,7 @@ func Test_RestrictionsPrepare(t *testing.T) { "instance1", 0, true, + database.TextArray[string]([]string{"en", "de", "ru"}), }, ), object: Restrictions{ @@ -82,6 +91,7 @@ func Test_RestrictionsPrepare(t *testing.T) { ResourceOwner: "instance1", Sequence: 0, DisallowPublicOrgRegistration: true, + AllowedLanguages: []language.Tag{language.Make("en"), language.Make("de"), language.Make("ru")}, }, }, }, diff --git a/internal/query/sessions_test.go b/internal/query/sessions_test.go index d4f621073a..2bb90bbca1 100644 --- a/internal/query/sessions_test.go +++ b/internal/query/sessions_test.go @@ -31,7 +31,7 @@ var ( ` projections.sessions8.user_resource_owner,` + ` projections.sessions8.user_checked_at,` + ` projections.login_names3.login_name,` + - ` projections.users9_humans.display_name,` + + ` projections.users10_humans.display_name,` + ` projections.sessions8.password_checked_at,` + ` projections.sessions8.intent_checked_at,` + ` projections.sessions8.webauthn_checked_at,` + @@ -48,8 +48,8 @@ var ( ` projections.sessions8.expiration` + ` FROM projections.sessions8` + ` LEFT JOIN projections.login_names3 ON projections.sessions8.user_id = projections.login_names3.user_id AND projections.sessions8.instance_id = projections.login_names3.instance_id` + - ` LEFT JOIN projections.users9_humans ON projections.sessions8.user_id = projections.users9_humans.user_id AND projections.sessions8.instance_id = projections.users9_humans.instance_id` + - ` LEFT JOIN projections.users9 ON projections.sessions8.user_id = projections.users9.id AND projections.sessions8.instance_id = projections.users9.instance_id` + + ` LEFT JOIN projections.users10_humans ON projections.sessions8.user_id = projections.users10_humans.user_id AND projections.sessions8.instance_id = projections.users10_humans.instance_id` + + ` LEFT JOIN projections.users10 ON projections.sessions8.user_id = projections.users10.id AND projections.sessions8.instance_id = projections.users10.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions8.id,` + ` projections.sessions8.creation_date,` + @@ -62,7 +62,7 @@ var ( ` projections.sessions8.user_resource_owner,` + ` projections.sessions8.user_checked_at,` + ` projections.login_names3.login_name,` + - ` projections.users9_humans.display_name,` + + ` projections.users10_humans.display_name,` + ` projections.sessions8.password_checked_at,` + ` projections.sessions8.intent_checked_at,` + ` projections.sessions8.webauthn_checked_at,` + @@ -75,8 +75,8 @@ var ( ` COUNT(*) OVER ()` + ` FROM projections.sessions8` + ` LEFT JOIN projections.login_names3 ON projections.sessions8.user_id = projections.login_names3.user_id AND projections.sessions8.instance_id = projections.login_names3.instance_id` + - ` LEFT JOIN projections.users9_humans ON projections.sessions8.user_id = projections.users9_humans.user_id AND projections.sessions8.instance_id = projections.users9_humans.instance_id` + - ` LEFT JOIN projections.users9 ON projections.sessions8.user_id = projections.users9.id AND projections.sessions8.instance_id = projections.users9.instance_id` + + ` LEFT JOIN projections.users10_humans ON projections.sessions8.user_id = projections.users10_humans.user_id AND projections.sessions8.instance_id = projections.users10_humans.instance_id` + + ` LEFT JOIN projections.users10 ON projections.sessions8.user_id = projections.users10.id AND projections.sessions8.instance_id = projections.users10.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) sessionCols = []string{ diff --git a/internal/query/testdata/oidc_client_jwt.json b/internal/query/testdata/oidc_client_jwt.json new file mode 100644 index 0000000000..d32e5a5110 --- /dev/null +++ b/internal/query/testdata/oidc_client_jwt.json @@ -0,0 +1,27 @@ +{ + "instance_id": "230690539048009730", + "app_id": "236647088211886082", + "client_id": "236647088211951618@tests", + "client_secret": null, + "redirect_uris": ["http://localhost:9999/auth/callback"], + "response_types": [0], + "grant_types": [0, 2], + "application_type": 0, + "auth_method_type": 3, + "post_logout_redirect_uris": ["https://example.com/logout"], + "is_dev_mode": true, + "access_token_type": 1, + "access_token_role_assertion": true, + "id_token_role_assertion": true, + "id_token_userinfo_assertion": true, + "clock_skew": 1000000000, + "additional_origins": ["https://example.com"], + "project_id": "236645808328409090", + "state": 1, + "project_role_keys": ["role1", "role2"], + "public_keys": { + "236647201860747266": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFB\nT0NBUThBTUlJQkNnS0NBUUVBMnVmQUwxYjcyYkl5MWFyK1dzNmIKR29oSkpRRkI3ZGZSYXBEcWVx\nTThVa3A2Q1ZkUHpxL3BPejF2aUFxNTB5eldaSnJ5Risyd3NoRkFLR0Y5QTIvQgoyWWY5YkpYUFov\nS2JrRnJZVDNOVHZZRGt2bGFTVGw5bU1uenJVMjlzNDhGMVBUV0tmQitDM2FNc09FRzFCdWZWCnM2\nM3FGNG5yRVBqU2JobGpJY285RlpxNFhwcEl6aE1RMGZEZEEvK1h5Z0NKcXZ1YUwwTGliTTFLcmxV\nZG51NzEKWWVraFNKakVQbnZPaXNYSWs0SVh5d29HSU93dGp4a0R2Tkl0UXZhTVZsZHI0L2tiNnV2\nYmdkV3dxNUV3QlpYcQpsb3cya3lKb3YzOFY0VWsySThrdVhwTGNucnB3NVRpbzJvb2lVRTI3YjB2\nSFpxQktPZWk5VW84OHFDcm4zRUt4CjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0t\nLS0K" + }, + "access_token_lifetime": 43200000000000, + "id_token_lifetime": 43200000000000 +} diff --git a/internal/query/testdata/oidc_client_public.json b/internal/query/testdata/oidc_client_public.json new file mode 100644 index 0000000000..ba23c95351 --- /dev/null +++ b/internal/query/testdata/oidc_client_public.json @@ -0,0 +1,25 @@ +{ + "instance_id": "230690539048009730", + "app_id": "236646457053020162", + "client_id": "236646457053085698@tests", + "client_secret": null, + "redirect_uris": ["http://localhost:9999/auth/callback"], + "response_types": [0], + "grant_types": [0], + "application_type": 0, + "auth_method_type": 2, + "post_logout_redirect_uris": null, + "is_dev_mode": true, + "access_token_type": 0, + "access_token_role_assertion": false, + "id_token_role_assertion": false, + "id_token_userinfo_assertion": false, + "clock_skew": 0, + "additional_origins": null, + "project_id": "236645808328409090", + "state": 1, + "project_role_keys": ["role1", "role2"], + "public_keys": null, + "access_token_lifetime": 43200000000000, + "id_token_lifetime": 43200000000000 +} diff --git a/internal/query/testdata/oidc_client_secret.json b/internal/query/testdata/oidc_client_secret.json new file mode 100644 index 0000000000..43b3256bb6 --- /dev/null +++ b/internal/query/testdata/oidc_client_secret.json @@ -0,0 +1,30 @@ +{ + "instance_id": "230690539048009730", + "app_id": "236646858984783874", + "client_id": "236646858984849410@tests", + "client_secret": { + "KeyID": "", + "Crypted": "JDJhJDE0JE96WjBYRVpaRXREMTNweS9FUGJhMmV2c1M2V2NLWjVvclZNajlwV0hFR0VIbUx1MmgzUEZx", + "Algorithm": "bcrypt", + "CryptoType": 1 + }, + "redirect_uris": ["http://localhost:9999/auth/callback"], + "response_types": [0], + "grant_types": [0], + "application_type": 0, + "auth_method_type": 0, + "post_logout_redirect_uris": null, + "is_dev_mode": true, + "access_token_type": 0, + "access_token_role_assertion": false, + "id_token_role_assertion": false, + "id_token_userinfo_assertion": false, + "clock_skew": 0, + "additional_origins": null, + "project_id": "236645808328409090", + "state": 1, + "project_role_keys": ["role1", "role2"], + "public_keys": null, + "access_token_lifetime": 43200000000000, + "id_token_lifetime": 43200000000000 +} diff --git a/internal/query/user.go b/internal/query/user.go index 144efec1dc..53a1647228 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -12,6 +12,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/call" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" @@ -91,7 +92,7 @@ type Phone struct { type Machine struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` - HasSecret bool `json:"has_secret,omitempty"` + Secret *crypto.CryptoValue `json:"secret,omitempty"` AccessTokenType domain.OIDCTokenType `json:"access_token_type,omitempty"` } @@ -270,8 +271,8 @@ var ( name: projection.MachineDescriptionCol, table: machineTable, } - MachineHasSecretCol = Column{ - name: projection.MachineHasSecretCol, + MachineSecretCol = Column{ + name: projection.MachineSecretCol, table: machineTable, } MachineAccessTokenTypeCol = Column{ @@ -740,7 +741,7 @@ func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder MachineUserIDCol.identifier(), MachineNameCol.identifier(), MachineDescriptionCol.identifier(), - MachineHasSecretCol.identifier(), + MachineSecretCol.identifier(), MachineAccessTokenTypeCol.identifier(), countColumn.identifier(), ). @@ -777,7 +778,7 @@ func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder machineID := sql.NullString{} name := sql.NullString{} description := sql.NullString{} - hasSecret := sql.NullBool{} + var secret *crypto.CryptoValue accessTokenType := sql.NullInt32{} err := row.Scan( @@ -806,7 +807,7 @@ func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder &machineID, &name, &description, - &hasSecret, + &secret, &accessTokenType, &count, ) @@ -838,7 +839,7 @@ func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder u.Machine = &Machine{ Name: name.String, Description: description.String, - HasSecret: hasSecret.Bool, + Secret: secret, AccessTokenType: domain.OIDCTokenType(accessTokenType.Int32), } } @@ -1210,7 +1211,7 @@ func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde MachineUserIDCol.identifier(), MachineNameCol.identifier(), MachineDescriptionCol.identifier(), - MachineHasSecretCol.identifier(), + MachineSecretCol.identifier(), MachineAccessTokenTypeCol.identifier(), countColumn.identifier()). From(userTable.identifier()). @@ -1249,7 +1250,7 @@ func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde machineID := sql.NullString{} name := sql.NullString{} description := sql.NullString{} - hasSecret := sql.NullBool{} + secret := new(crypto.CryptoValue) accessTokenType := sql.NullInt32{} err := rows.Scan( @@ -1278,7 +1279,7 @@ func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde &machineID, &name, &description, - &hasSecret, + secret, &accessTokenType, &count, ) @@ -1309,7 +1310,7 @@ func prepareUsersQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilde u.Machine = &Machine{ Name: name.String, Description: description.String, - HasSecret: hasSecret.Bool, + Secret: secret, AccessTokenType: domain.OIDCTokenType(accessTokenType.Int32), } } diff --git a/internal/query/user_auth_method_test.go b/internal/query/user_auth_method_test.go index a9aae1d1c5..c78bf85699 100644 --- a/internal/query/user_auth_method_test.go +++ b/internal/query/user_auth_method_test.go @@ -39,38 +39,38 @@ var ( "method_type", "count", } - prepareActiveAuthMethodTypesStmt = `SELECT projections.users9_notifications.password_set,` + + prepareActiveAuthMethodTypesStmt = `SELECT projections.users10_notifications.password_set,` + ` auth_method_types.method_type,` + ` user_idps_count.count` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_notifications ON projections.users9.id = projections.users9_notifications.user_id AND projections.users9.instance_id = projections.users9_notifications.instance_id` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_notifications ON projections.users10.id = projections.users10_notifications.user_id AND projections.users10.instance_id = projections.users10_notifications.instance_id` + ` LEFT JOIN (SELECT DISTINCT(auth_method_types.method_type), auth_method_types.user_id, auth_method_types.instance_id FROM projections.user_auth_methods4 AS auth_method_types` + ` WHERE auth_method_types.state = $1) AS auth_method_types` + - ` ON auth_method_types.user_id = projections.users9.id AND auth_method_types.instance_id = projections.users9.instance_id` + + ` ON auth_method_types.user_id = projections.users10.id AND auth_method_types.instance_id = projections.users10.instance_id` + ` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` + ` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` + - ` ON user_idps_count.user_id = projections.users9.id AND user_idps_count.instance_id = projections.users9.instance_id` + + ` ON user_idps_count.user_id = projections.users10.id AND user_idps_count.instance_id = projections.users10.instance_id` + ` AS OF SYSTEM TIME '-1 ms` prepareActiveAuthMethodTypesCols = []string{ "password_set", "method_type", "idps_count", } - prepareAuthMethodTypesRequiredStmt = `SELECT projections.users9_notifications.password_set,` + + prepareAuthMethodTypesRequiredStmt = `SELECT projections.users10_notifications.password_set,` + ` auth_method_types.method_type,` + ` user_idps_count.count,` + ` auth_methods_force_mfa.force_mfa,` + ` auth_methods_force_mfa.force_mfa_local_only` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_notifications ON projections.users9.id = projections.users9_notifications.user_id AND projections.users9.instance_id = projections.users9_notifications.instance_id` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_notifications ON projections.users10.id = projections.users10_notifications.user_id AND projections.users10.instance_id = projections.users10_notifications.instance_id` + ` LEFT JOIN (SELECT DISTINCT(auth_method_types.method_type), auth_method_types.user_id, auth_method_types.instance_id FROM projections.user_auth_methods4 AS auth_method_types` + ` WHERE auth_method_types.state = $1) AS auth_method_types` + - ` ON auth_method_types.user_id = projections.users9.id AND auth_method_types.instance_id = projections.users9.instance_id` + + ` ON auth_method_types.user_id = projections.users10.id AND auth_method_types.instance_id = projections.users10.instance_id` + ` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` + ` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` + - ` ON user_idps_count.user_id = projections.users9.id AND user_idps_count.instance_id = projections.users9.instance_id` + + ` ON user_idps_count.user_id = projections.users10.id AND user_idps_count.instance_id = projections.users10.instance_id` + ` LEFT JOIN (SELECT auth_methods_force_mfa.force_mfa, auth_methods_force_mfa.force_mfa_local_only, auth_methods_force_mfa.instance_id, auth_methods_force_mfa.aggregate_id FROM projections.login_policies5 AS auth_methods_force_mfa ORDER BY auth_methods_force_mfa.is_default) AS auth_methods_force_mfa` + - ` ON (auth_methods_force_mfa.aggregate_id = projections.users9.instance_id OR auth_methods_force_mfa.aggregate_id = projections.users9.resource_owner) AND auth_methods_force_mfa.instance_id = projections.users9.instance_id` + + ` ON (auth_methods_force_mfa.aggregate_id = projections.users10.instance_id OR auth_methods_force_mfa.aggregate_id = projections.users10.resource_owner) AND auth_methods_force_mfa.instance_id = projections.users10.instance_id` + ` AS OF SYSTEM TIME '-1 ms ` prepareAuthMethodTypesRequiredCols = []string{ diff --git a/internal/query/user_grant_test.go b/internal/query/user_grant_test.go index 962c6a1ed6..3fe6aca131 100644 --- a/internal/query/user_grant_test.go +++ b/internal/query/user_grant_test.go @@ -23,14 +23,14 @@ var ( ", projections.user_grants3.roles" + ", projections.user_grants3.state" + ", projections.user_grants3.user_id" + - ", projections.users9.username" + - ", projections.users9.type" + - ", projections.users9.resource_owner" + - ", projections.users9_humans.first_name" + - ", projections.users9_humans.last_name" + - ", projections.users9_humans.email" + - ", projections.users9_humans.display_name" + - ", projections.users9_humans.avatar_key" + + ", projections.users10.username" + + ", projections.users10.type" + + ", projections.users10.resource_owner" + + ", projections.users10_humans.first_name" + + ", projections.users10_humans.last_name" + + ", projections.users10_humans.email" + + ", projections.users10_humans.display_name" + + ", projections.users10_humans.avatar_key" + ", projections.login_names3.login_name" + ", projections.user_grants3.resource_owner" + ", projections.orgs1.name" + @@ -38,8 +38,8 @@ var ( ", projections.user_grants3.project_id" + ", projections.projects4.name" + " FROM projections.user_grants3" + - " LEFT JOIN projections.users9 ON projections.user_grants3.user_id = projections.users9.id AND projections.user_grants3.instance_id = projections.users9.instance_id" + - " LEFT JOIN projections.users9_humans ON projections.user_grants3.user_id = projections.users9_humans.user_id AND projections.user_grants3.instance_id = projections.users9_humans.instance_id" + + " LEFT JOIN projections.users10 ON projections.user_grants3.user_id = projections.users10.id AND projections.user_grants3.instance_id = projections.users10.instance_id" + + " LEFT JOIN projections.users10_humans ON projections.user_grants3.user_id = projections.users10_humans.user_id AND projections.user_grants3.instance_id = projections.users10_humans.instance_id" + " LEFT JOIN projections.orgs1 ON projections.user_grants3.resource_owner = projections.orgs1.id AND projections.user_grants3.instance_id = projections.orgs1.instance_id" + " LEFT JOIN projections.projects4 ON projections.user_grants3.project_id = projections.projects4.id AND projections.user_grants3.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.login_names3 ON projections.user_grants3.user_id = projections.login_names3.user_id AND projections.user_grants3.instance_id = projections.login_names3.instance_id" + @@ -78,14 +78,14 @@ var ( ", projections.user_grants3.roles" + ", projections.user_grants3.state" + ", projections.user_grants3.user_id" + - ", projections.users9.username" + - ", projections.users9.type" + - ", projections.users9.resource_owner" + - ", projections.users9_humans.first_name" + - ", projections.users9_humans.last_name" + - ", projections.users9_humans.email" + - ", projections.users9_humans.display_name" + - ", projections.users9_humans.avatar_key" + + ", projections.users10.username" + + ", projections.users10.type" + + ", projections.users10.resource_owner" + + ", projections.users10_humans.first_name" + + ", projections.users10_humans.last_name" + + ", projections.users10_humans.email" + + ", projections.users10_humans.display_name" + + ", projections.users10_humans.avatar_key" + ", projections.login_names3.login_name" + ", projections.user_grants3.resource_owner" + ", projections.orgs1.name" + @@ -94,8 +94,8 @@ var ( ", projections.projects4.name" + ", COUNT(*) OVER ()" + " FROM projections.user_grants3" + - " LEFT JOIN projections.users9 ON projections.user_grants3.user_id = projections.users9.id AND projections.user_grants3.instance_id = projections.users9.instance_id" + - " LEFT JOIN projections.users9_humans ON projections.user_grants3.user_id = projections.users9_humans.user_id AND projections.user_grants3.instance_id = projections.users9_humans.instance_id" + + " LEFT JOIN projections.users10 ON projections.user_grants3.user_id = projections.users10.id AND projections.user_grants3.instance_id = projections.users10.instance_id" + + " LEFT JOIN projections.users10_humans ON projections.user_grants3.user_id = projections.users10_humans.user_id AND projections.user_grants3.instance_id = projections.users10_humans.instance_id" + " LEFT JOIN projections.orgs1 ON projections.user_grants3.resource_owner = projections.orgs1.id AND projections.user_grants3.instance_id = projections.orgs1.instance_id" + " LEFT JOIN projections.projects4 ON projections.user_grants3.project_id = projections.projects4.id AND projections.user_grants3.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.login_names3 ON projections.user_grants3.user_id = projections.login_names3.user_id AND projections.user_grants3.instance_id = projections.login_names3.instance_id" + diff --git a/internal/query/user_test.go b/internal/query/user_test.go index 1076dd8729..3459c45dba 100644 --- a/internal/query/user_test.go +++ b/internal/query/user_test.go @@ -10,6 +10,7 @@ import ( "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" errs "github.com/zitadel/zitadel/internal/errors" @@ -22,43 +23,43 @@ var ( preferredLoginNameQuery = `SELECT preferred_login_name.user_id, preferred_login_name.login_name, preferred_login_name.instance_id` + ` FROM projections.login_names3 AS preferred_login_name` + ` WHERE preferred_login_name.is_primary = $1` - userQuery = `SELECT projections.users9.id,` + - ` projections.users9.creation_date,` + - ` projections.users9.change_date,` + - ` projections.users9.resource_owner,` + - ` projections.users9.sequence,` + - ` projections.users9.state,` + - ` projections.users9.type,` + - ` projections.users9.username,` + + userQuery = `SELECT projections.users10.id,` + + ` projections.users10.creation_date,` + + ` projections.users10.change_date,` + + ` projections.users10.resource_owner,` + + ` projections.users10.sequence,` + + ` projections.users10.state,` + + ` projections.users10.type,` + + ` projections.users10.username,` + ` login_names.loginnames,` + ` preferred_login_name.login_name,` + - ` projections.users9_humans.user_id,` + - ` projections.users9_humans.first_name,` + - ` projections.users9_humans.last_name,` + - ` projections.users9_humans.nick_name,` + - ` projections.users9_humans.display_name,` + - ` projections.users9_humans.preferred_language,` + - ` projections.users9_humans.gender,` + - ` projections.users9_humans.avatar_key,` + - ` projections.users9_humans.email,` + - ` projections.users9_humans.is_email_verified,` + - ` projections.users9_humans.phone,` + - ` projections.users9_humans.is_phone_verified,` + - ` projections.users9_machines.user_id,` + - ` projections.users9_machines.name,` + - ` projections.users9_machines.description,` + - ` projections.users9_machines.has_secret,` + - ` projections.users9_machines.access_token_type,` + + ` projections.users10_humans.user_id,` + + ` projections.users10_humans.first_name,` + + ` projections.users10_humans.last_name,` + + ` projections.users10_humans.nick_name,` + + ` projections.users10_humans.display_name,` + + ` projections.users10_humans.preferred_language,` + + ` projections.users10_humans.gender,` + + ` projections.users10_humans.avatar_key,` + + ` projections.users10_humans.email,` + + ` projections.users10_humans.is_email_verified,` + + ` projections.users10_humans.phone,` + + ` projections.users10_humans.is_phone_verified,` + + ` projections.users10_machines.user_id,` + + ` projections.users10_machines.name,` + + ` projections.users10_machines.description,` + + ` projections.users10_machines.secret,` + + ` projections.users10_machines.access_token_type,` + ` COUNT(*) OVER ()` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_humans ON projections.users9.id = projections.users9_humans.user_id AND projections.users9.instance_id = projections.users9_humans.instance_id` + - ` LEFT JOIN projections.users9_machines ON projections.users9.id = projections.users9_machines.user_id AND projections.users9.instance_id = projections.users9_machines.instance_id` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_humans ON projections.users10.id = projections.users10_humans.user_id AND projections.users10.instance_id = projections.users10_humans.instance_id` + + ` LEFT JOIN projections.users10_machines ON projections.users10.id = projections.users10_machines.user_id AND projections.users10.instance_id = projections.users10_machines.instance_id` + ` LEFT JOIN` + ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users9.id AND login_names.instance_id = projections.users9.instance_id` + + ` ON login_names.user_id = projections.users10.id AND login_names.instance_id = projections.users10.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users9.id AND preferred_login_name.instance_id = projections.users9.instance_id` + + ` ON preferred_login_name.user_id = projections.users10.id AND preferred_login_name.instance_id = projections.users10.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` userCols = []string{ "id", @@ -71,7 +72,7 @@ var ( "username", "loginnames", "login_name", - //human + // human "user_id", "first_name", "last_name", @@ -84,29 +85,29 @@ var ( "is_email_verified", "phone", "is_phone_verified", - //machine + // machine "user_id", "name", "description", - "has_secret", + "secret", "access_token_type", "count", } - profileQuery = `SELECT projections.users9.id,` + - ` projections.users9.creation_date,` + - ` projections.users9.change_date,` + - ` projections.users9.resource_owner,` + - ` projections.users9.sequence,` + - ` projections.users9_humans.user_id,` + - ` projections.users9_humans.first_name,` + - ` projections.users9_humans.last_name,` + - ` projections.users9_humans.nick_name,` + - ` projections.users9_humans.display_name,` + - ` projections.users9_humans.preferred_language,` + - ` projections.users9_humans.gender,` + - ` projections.users9_humans.avatar_key` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_humans ON projections.users9.id = projections.users9_humans.user_id AND projections.users9.instance_id = projections.users9_humans.instance_id` + + profileQuery = `SELECT projections.users10.id,` + + ` projections.users10.creation_date,` + + ` projections.users10.change_date,` + + ` projections.users10.resource_owner,` + + ` projections.users10.sequence,` + + ` projections.users10_humans.user_id,` + + ` projections.users10_humans.first_name,` + + ` projections.users10_humans.last_name,` + + ` projections.users10_humans.nick_name,` + + ` projections.users10_humans.display_name,` + + ` projections.users10_humans.preferred_language,` + + ` projections.users10_humans.gender,` + + ` projections.users10_humans.avatar_key` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_humans ON projections.users10.id = projections.users10_humans.user_id AND projections.users10.instance_id = projections.users10_humans.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` profileCols = []string{ "id", @@ -123,16 +124,16 @@ var ( "gender", "avatar_key", } - emailQuery = `SELECT projections.users9.id,` + - ` projections.users9.creation_date,` + - ` projections.users9.change_date,` + - ` projections.users9.resource_owner,` + - ` projections.users9.sequence,` + - ` projections.users9_humans.user_id,` + - ` projections.users9_humans.email,` + - ` projections.users9_humans.is_email_verified` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_humans ON projections.users9.id = projections.users9_humans.user_id AND projections.users9.instance_id = projections.users9_humans.instance_id` + + emailQuery = `SELECT projections.users10.id,` + + ` projections.users10.creation_date,` + + ` projections.users10.change_date,` + + ` projections.users10.resource_owner,` + + ` projections.users10.sequence,` + + ` projections.users10_humans.user_id,` + + ` projections.users10_humans.email,` + + ` projections.users10_humans.is_email_verified` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_humans ON projections.users10.id = projections.users10_humans.user_id AND projections.users10.instance_id = projections.users10_humans.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` emailCols = []string{ "id", @@ -144,16 +145,16 @@ var ( "email", "is_email_verified", } - phoneQuery = `SELECT projections.users9.id,` + - ` projections.users9.creation_date,` + - ` projections.users9.change_date,` + - ` projections.users9.resource_owner,` + - ` projections.users9.sequence,` + - ` projections.users9_humans.user_id,` + - ` projections.users9_humans.phone,` + - ` projections.users9_humans.is_phone_verified` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_humans ON projections.users9.id = projections.users9_humans.user_id AND projections.users9.instance_id = projections.users9_humans.instance_id` + + phoneQuery = `SELECT projections.users10.id,` + + ` projections.users10.creation_date,` + + ` projections.users10.change_date,` + + ` projections.users10.resource_owner,` + + ` projections.users10.sequence,` + + ` projections.users10_humans.user_id,` + + ` projections.users10_humans.phone,` + + ` projections.users10_humans.is_phone_verified` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_humans ON projections.users10.id = projections.users10_humans.user_id AND projections.users10.instance_id = projections.users10_humans.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` phoneCols = []string{ "id", @@ -165,14 +166,14 @@ var ( "phone", "is_phone_verified", } - userUniqueQuery = `SELECT projections.users9.id,` + - ` projections.users9.state,` + - ` projections.users9.username,` + - ` projections.users9_humans.user_id,` + - ` projections.users9_humans.email,` + - ` projections.users9_humans.is_email_verified` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_humans ON projections.users9.id = projections.users9_humans.user_id AND projections.users9.instance_id = projections.users9_humans.instance_id` + + userUniqueQuery = `SELECT projections.users10.id,` + + ` projections.users10.state,` + + ` projections.users10.username,` + + ` projections.users10_humans.user_id,` + + ` projections.users10_humans.email,` + + ` projections.users10_humans.is_email_verified` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_humans ON projections.users10.id = projections.users10_humans.user_id AND projections.users10.instance_id = projections.users10_humans.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` userUniqueCols = []string{ "id", @@ -182,40 +183,40 @@ var ( "email", "is_email_verified", } - notifyUserQuery = `SELECT projections.users9.id,` + - ` projections.users9.creation_date,` + - ` projections.users9.change_date,` + - ` projections.users9.resource_owner,` + - ` projections.users9.sequence,` + - ` projections.users9.state,` + - ` projections.users9.type,` + - ` projections.users9.username,` + + notifyUserQuery = `SELECT projections.users10.id,` + + ` projections.users10.creation_date,` + + ` projections.users10.change_date,` + + ` projections.users10.resource_owner,` + + ` projections.users10.sequence,` + + ` projections.users10.state,` + + ` projections.users10.type,` + + ` projections.users10.username,` + ` login_names.loginnames,` + ` preferred_login_name.login_name,` + - ` projections.users9_humans.user_id,` + - ` projections.users9_humans.first_name,` + - ` projections.users9_humans.last_name,` + - ` projections.users9_humans.nick_name,` + - ` projections.users9_humans.display_name,` + - ` projections.users9_humans.preferred_language,` + - ` projections.users9_humans.gender,` + - ` projections.users9_humans.avatar_key,` + - ` projections.users9_notifications.user_id,` + - ` projections.users9_notifications.last_email,` + - ` projections.users9_notifications.verified_email,` + - ` projections.users9_notifications.last_phone,` + - ` projections.users9_notifications.verified_phone,` + - ` projections.users9_notifications.password_set,` + + ` projections.users10_humans.user_id,` + + ` projections.users10_humans.first_name,` + + ` projections.users10_humans.last_name,` + + ` projections.users10_humans.nick_name,` + + ` projections.users10_humans.display_name,` + + ` projections.users10_humans.preferred_language,` + + ` projections.users10_humans.gender,` + + ` projections.users10_humans.avatar_key,` + + ` projections.users10_notifications.user_id,` + + ` projections.users10_notifications.last_email,` + + ` projections.users10_notifications.verified_email,` + + ` projections.users10_notifications.last_phone,` + + ` projections.users10_notifications.verified_phone,` + + ` projections.users10_notifications.password_set,` + ` COUNT(*) OVER ()` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_humans ON projections.users9.id = projections.users9_humans.user_id AND projections.users9.instance_id = projections.users9_humans.instance_id` + - ` LEFT JOIN projections.users9_notifications ON projections.users9.id = projections.users9_notifications.user_id AND projections.users9.instance_id = projections.users9_notifications.instance_id` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_humans ON projections.users10.id = projections.users10_humans.user_id AND projections.users10.instance_id = projections.users10_humans.instance_id` + + ` LEFT JOIN projections.users10_notifications ON projections.users10.id = projections.users10_notifications.user_id AND projections.users10.instance_id = projections.users10_notifications.instance_id` + ` LEFT JOIN` + ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users9.id AND login_names.instance_id = projections.users9.instance_id` + + ` ON login_names.user_id = projections.users10.id AND login_names.instance_id = projections.users10.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users9.id AND preferred_login_name.instance_id = projections.users9.instance_id` + + ` ON preferred_login_name.user_id = projections.users10.id AND preferred_login_name.instance_id = projections.users10.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` notifyUserCols = []string{ "id", @@ -228,7 +229,7 @@ var ( "username", "loginnames", "login_name", - //human + // human "user_id", "first_name", "last_name", @@ -237,7 +238,7 @@ var ( "preferred_language", "gender", "avatar_key", - //machine + // machine "user_id", "last_email", "verified_email", @@ -246,43 +247,43 @@ var ( "password_set", "count", } - usersQuery = `SELECT projections.users9.id,` + - ` projections.users9.creation_date,` + - ` projections.users9.change_date,` + - ` projections.users9.resource_owner,` + - ` projections.users9.sequence,` + - ` projections.users9.state,` + - ` projections.users9.type,` + - ` projections.users9.username,` + + usersQuery = `SELECT projections.users10.id,` + + ` projections.users10.creation_date,` + + ` projections.users10.change_date,` + + ` projections.users10.resource_owner,` + + ` projections.users10.sequence,` + + ` projections.users10.state,` + + ` projections.users10.type,` + + ` projections.users10.username,` + ` login_names.loginnames,` + ` preferred_login_name.login_name,` + - ` projections.users9_humans.user_id,` + - ` projections.users9_humans.first_name,` + - ` projections.users9_humans.last_name,` + - ` projections.users9_humans.nick_name,` + - ` projections.users9_humans.display_name,` + - ` projections.users9_humans.preferred_language,` + - ` projections.users9_humans.gender,` + - ` projections.users9_humans.avatar_key,` + - ` projections.users9_humans.email,` + - ` projections.users9_humans.is_email_verified,` + - ` projections.users9_humans.phone,` + - ` projections.users9_humans.is_phone_verified,` + - ` projections.users9_machines.user_id,` + - ` projections.users9_machines.name,` + - ` projections.users9_machines.description,` + - ` projections.users9_machines.has_secret,` + - ` projections.users9_machines.access_token_type,` + + ` projections.users10_humans.user_id,` + + ` projections.users10_humans.first_name,` + + ` projections.users10_humans.last_name,` + + ` projections.users10_humans.nick_name,` + + ` projections.users10_humans.display_name,` + + ` projections.users10_humans.preferred_language,` + + ` projections.users10_humans.gender,` + + ` projections.users10_humans.avatar_key,` + + ` projections.users10_humans.email,` + + ` projections.users10_humans.is_email_verified,` + + ` projections.users10_humans.phone,` + + ` projections.users10_humans.is_phone_verified,` + + ` projections.users10_machines.user_id,` + + ` projections.users10_machines.name,` + + ` projections.users10_machines.description,` + + ` projections.users10_machines.secret,` + + ` projections.users10_machines.access_token_type,` + ` COUNT(*) OVER ()` + - ` FROM projections.users9` + - ` LEFT JOIN projections.users9_humans ON projections.users9.id = projections.users9_humans.user_id AND projections.users9.instance_id = projections.users9_humans.instance_id` + - ` LEFT JOIN projections.users9_machines ON projections.users9.id = projections.users9_machines.user_id AND projections.users9.instance_id = projections.users9_machines.instance_id` + + ` FROM projections.users10` + + ` LEFT JOIN projections.users10_humans ON projections.users10.id = projections.users10_humans.user_id AND projections.users10.instance_id = projections.users10_humans.instance_id` + + ` LEFT JOIN projections.users10_machines ON projections.users10.id = projections.users10_machines.user_id AND projections.users10.instance_id = projections.users10_machines.instance_id` + ` LEFT JOIN` + ` (` + loginNamesQuery + `) AS login_names` + - ` ON login_names.user_id = projections.users9.id AND login_names.instance_id = projections.users9.instance_id` + + ` ON login_names.user_id = projections.users10.id AND login_names.instance_id = projections.users10.instance_id` + ` LEFT JOIN` + ` (` + preferredLoginNameQuery + `) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users9.id AND preferred_login_name.instance_id = projections.users9.instance_id` + + ` ON preferred_login_name.user_id = projections.users10.id AND preferred_login_name.instance_id = projections.users10.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` usersCols = []string{ "id", @@ -295,7 +296,7 @@ var ( "username", "loginnames", "login_name", - //human + // human "user_id", "first_name", "last_name", @@ -308,11 +309,11 @@ var ( "is_email_verified", "phone", "is_phone_verified", - //machine + // machine "user_id", "name", "description", - "has_secret", + "secret", "access_token_type", "count", } @@ -365,7 +366,7 @@ func Test_UserPrepares(t *testing.T) { "username", database.TextArray[string]{"login_name1", "login_name2"}, "login_name1", - //human + // human "id", "first_name", "last_name", @@ -378,7 +379,7 @@ func Test_UserPrepares(t *testing.T) { true, "phone", true, - //machine + // machine nil, nil, nil, @@ -432,7 +433,7 @@ func Test_UserPrepares(t *testing.T) { "username", database.TextArray[string]{"login_name1", "login_name2"}, "login_name1", - //human + // human nil, nil, nil, @@ -445,11 +446,11 @@ func Test_UserPrepares(t *testing.T) { nil, nil, nil, - //machine + // machine "id", "name", "description", - true, + nil, domain.OIDCTokenTypeBearer, 1, }, @@ -469,7 +470,71 @@ func Test_UserPrepares(t *testing.T) { Machine: &Machine{ Name: "name", Description: "description", - HasSecret: true, + Secret: nil, + AccessTokenType: domain.OIDCTokenTypeBearer, + }, + }, + }, + { + name: "prepareUserQuery machine with secret found", + prepare: prepareUserQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(userQuery), + userCols, + []driver.Value{ + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + domain.UserStateActive, + domain.UserTypeMachine, + "username", + database.TextArray[string]{"login_name1", "login_name2"}, + "login_name1", + // human + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + // machine + "id", + "name", + "description", + `{"CryptoType":1,"Algorithm":"bcrypt","Crypted":"deadbeef"}`, + domain.OIDCTokenTypeBearer, + 1, + }, + ), + }, + object: &User{ + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + State: domain.UserStateActive, + Type: domain.UserTypeMachine, + Username: "username", + LoginNames: database.TextArray[string]{"login_name1", "login_name2"}, + PreferredLoginName: "login_name1", + Machine: &Machine{ + Name: "name", + Description: "description", + Secret: &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "bcrypt", + Crypted: []byte{117, 230, 157, 109, 231, 159}, + }, AccessTokenType: domain.OIDCTokenTypeBearer, }, }, @@ -875,7 +940,7 @@ func Test_UserPrepares(t *testing.T) { "username", database.TextArray[string]{"login_name1", "login_name2"}, "login_name1", - //human + // human "id", "first_name", "last_name", @@ -938,7 +1003,7 @@ func Test_UserPrepares(t *testing.T) { "username", database.TextArray[string]{"login_name1", "login_name2"}, "login_name1", - //human + // human "id", "first_name", "last_name", @@ -1019,7 +1084,7 @@ func Test_UserPrepares(t *testing.T) { "username", database.TextArray[string]{"login_name1", "login_name2"}, "login_name1", - //human + // human "id", "first_name", "last_name", @@ -1032,7 +1097,7 @@ func Test_UserPrepares(t *testing.T) { true, "phone", true, - //machine + // machine nil, nil, nil, @@ -1094,7 +1159,7 @@ func Test_UserPrepares(t *testing.T) { "username", database.TextArray[string]{"login_name1", "login_name2"}, "login_name1", - //human + // human "id", "first_name", "last_name", @@ -1107,7 +1172,7 @@ func Test_UserPrepares(t *testing.T) { true, "phone", true, - //machine + // machine nil, nil, nil, @@ -1125,7 +1190,7 @@ func Test_UserPrepares(t *testing.T) { "username", database.TextArray[string]{"login_name1", "login_name2"}, "login_name1", - //human + // human nil, nil, nil, @@ -1138,11 +1203,11 @@ func Test_UserPrepares(t *testing.T) { nil, nil, nil, - //machine + // machine "id", "name", "description", - true, + `{"CryptoType":1,"Algorithm":"bcrypt","Crypted":"deadbeef"}`, domain.OIDCTokenTypeBearer, }, }, @@ -1190,9 +1255,13 @@ func Test_UserPrepares(t *testing.T) { LoginNames: database.TextArray[string]{"login_name1", "login_name2"}, PreferredLoginName: "login_name1", Machine: &Machine{ - Name: "name", - Description: "description", - HasSecret: true, + Name: "name", + Description: "description", + Secret: &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "bcrypt", + Crypted: []byte{117, 230, 157, 109, 231, 159}, + }, AccessTokenType: domain.OIDCTokenTypeBearer, }, }, diff --git a/internal/query/userinfo.go b/internal/query/userinfo.go index c42a893319..9ee2721f54 100644 --- a/internal/query/userinfo.go +++ b/internal/query/userinfo.go @@ -4,12 +4,12 @@ import ( "context" "database/sql" _ "embed" - "encoding/json" + "errors" "sync" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/errors" + zerrors "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -40,23 +40,17 @@ func (q *Queries) GetOIDCUserInfo(ctx context.Context, userID string, roleAudien ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - var data []byte - err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { - return row.Scan(&data) - }, - oidcUserInfoQuery, + userInfo, err := database.QueryJSONObject[OIDCUserInfo](ctx, q.client, oidcUserInfoQuery, userID, authz.GetInstance(ctx).InstanceID(), database.TextArray[string](roleAudience), ) - if err != nil { - return nil, errors.ThrowInternal(err, "QUERY-Oath6", "Errors.Internal") + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-Eey2a", "Errors.User.NotFound") } - - userInfo := new(OIDCUserInfo) - if err = json.Unmarshal(data, userInfo); err != nil { - return nil, errors.ThrowInternal(err, "QUERY-Vohs6", "Errors.Internal") + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-Oath6", "Errors.Internal") } if userInfo.User == nil { - return nil, errors.ThrowNotFound(nil, "QUERY-ahs4S", "Errors.User.NotFound") + return nil, zerrors.ThrowNotFound(nil, "QUERY-ahs4S", "Errors.User.NotFound") } return userInfo, nil diff --git a/internal/query/userinfo_test.go b/internal/query/userinfo_test.go index 34c713d506..7629247605 100644 --- a/internal/query/userinfo_test.go +++ b/internal/query/userinfo_test.go @@ -68,14 +68,6 @@ func TestQueries_GetOIDCUserInfo(t *testing.T) { mock: mockQueryErr(expQuery, sql.ErrConnDone, "231965491734773762", "instanceID", nil), wantErr: sql.ErrConnDone, }, - { - name: "unmarshal error", - args: args{ - userID: "231965491734773762", - }, - mock: mockQuery(expQuery, []string{"json_build_object"}, []driver.Value{`~~~`}, "231965491734773762", "instanceID", nil), - wantErr: errors.ThrowInternal(nil, "QUERY-Vohs6", "Errors.Internal"), - }, { name: "user not found", args: args{ diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go index 72da287a36..ea889c1045 100644 --- a/internal/renderer/renderer.go +++ b/internal/renderer/renderer.go @@ -23,17 +23,13 @@ const ( type Renderer struct { Templates map[string]*template.Template - dir http.FileSystem cookieName string } -func NewRenderer(dir http.FileSystem, tmplMapping map[string]string, funcs map[string]interface{}, cookieName string) (*Renderer, error) { +func NewRenderer(tmplMapping map[string]string, funcs map[string]interface{}, cookieName string) (*Renderer, error) { var err error - r := &Renderer{ - dir: dir, - cookieName: cookieName, - } - err = r.loadTemplates(dir, nil, tmplMapping, funcs) + r := &Renderer{cookieName: cookieName} + err = r.loadTemplates(i18n.LoadFilesystem(i18n.LOGIN), nil, tmplMapping, funcs) if err != nil { return nil, err } @@ -47,8 +43,8 @@ func (r *Renderer) RenderTemplate(w http.ResponseWriter, req *http.Request, tran } } -func (r *Renderer) NewTranslator(ctx context.Context) (*i18n.Translator, error) { - return i18n.NewTranslator(r.dir, authz.GetInstance(ctx).DefaultLanguage(), r.cookieName) +func (r *Renderer) NewTranslator(ctx context.Context, allowedLanguages []language.Tag) (*i18n.Translator, error) { + return i18n.NewLoginTranslator(authz.GetInstance(ctx).DefaultLanguage(), allowedLanguages, r.cookieName) } func (r *Renderer) Localize(translator *i18n.Translator, id string, args map[string]interface{}) string { diff --git a/internal/repository/restrictions/events.go b/internal/repository/restrictions/events.go index e15fd7c767..7b28af3c30 100644 --- a/internal/repository/restrictions/events.go +++ b/internal/repository/restrictions/events.go @@ -2,6 +2,7 @@ package restrictions import ( "github.com/muhlemmer/gu" + "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/eventstore" ) @@ -13,8 +14,9 @@ const ( // SetEvent describes that restrictions are added or modified and contains only changed properties type SetEvent struct { - *eventstore.BaseEvent `json:"-"` - DisallowPublicOrgRegistrations *bool `json:"disallowPublicOrgRegistrations,omitempty"` + *eventstore.BaseEvent `json:"-"` + DisallowPublicOrgRegistration *bool `json:"disallowPublicOrgRegistration,omitempty"` + AllowedLanguages *[]language.Tag `json:"allowedLanguages,omitempty"` } func (e *SetEvent) Payload() any { @@ -44,9 +46,15 @@ func NewSetEvent( type RestrictionsChange func(*SetEvent) -func ChangePublicOrgRegistrations(disallow bool) RestrictionsChange { +func ChangeDisallowPublicOrgRegistration(disallow bool) RestrictionsChange { return func(e *SetEvent) { - e.DisallowPublicOrgRegistrations = gu.Ptr(disallow) + e.DisallowPublicOrgRegistration = gu.Ptr(disallow) + } +} + +func ChangeAllowedLanguages(allowedLanguages []language.Tag) RestrictionsChange { + return func(e *SetEvent) { + e.AllowedLanguages = &allowedLanguages } } diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index ff7f86601a..773eb93f64 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Не са посочени лимити Restrictions: NoneSpecified: Не са посочени ограничения + DefaultLanguageMustBeAllowed: Езикът по подразбиране трябва да бъде разрешен Language: NotParsed: Езикът не можа да бъде анализиран синтактично + NotSupported: Езикът не се поддържа + NotAllowed: Езикът не е разрешен + Undefined: Езикът е неопределен + Duplicate: Езиците имат дубликати OIDCSettings: NotFound: Конфигурацията на OIDC не е намерена AlreadyExists: OIDC конфигурацията вече съществува diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index 4230dc7749..55bc9b7616 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Nebyly určeny žádné limity Restrictions: NoneSpecified: Nebyla určena žádná omezení + DefaultLanguageMustBeAllowed: Výchozí jazyk musí být povolen Language: NotParsed: Jazyk nelze určit + NotSupported: Jazyk není podporován + NotAllowed: Jazyk není povolen + Undefined: Jazyk není definován + Duplicate: Jazyky mají duplikáty OIDCSettings: NotFound: Konfigurace OIDC nebyla nalezena AlreadyExists: Konfigurace OIDC již existuje diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 65cd5b1168..e959737285 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Keine Limits angegeben Restrictions: NoneSpecified: Keine Restriktionen angegeben + DefaultLanguageMustBeAllowed: Default Sprache muss erlaubt sein Language: NotParsed: Sprache konnte nicht gemapped werden + NotSupported: Sprache wird nicht unterstützt + NotAllowed: Sprache ist nicht erlaubt + Undefined: Sprache ist nicht definiert + Duplicate: Sprachen haben Duplikate OIDCSettings: NotFound: OIDC Konfiguration konnte nicht gefunden werden AlreadyExists: OIDC Konfiguration existiert bereits diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index e3cdbcf2e8..536d81e7d9 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: No limits specified Restrictions: NoneSpecified: No restrictions specified + DefaultLanguageMustBeAllowed: The default language must be allowed Language: NotParsed: Could not parse language + NotSupported: Language is not supported + NotAllowed: Language is not allowed + Undefined: Language is undefined + Duplicate: Languages have duplicates OIDCSettings: NotFound: OIDC Configuration not found AlreadyExists: OIDC configuration already exists diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 7a5d0ab297..c85456b3b6 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: No se especificaron límites Restrictions: NoneSpecified: No se especificaron restricciones + DefaultLanguageMustBeAllowed: El idioma por defecto debe estar permitido Language: NotParsed: No pude analizar el idioma + NotSupported: El idioma no está soportado + NotAllowed: El idioma no está permitido + Undefined: El idioma no está definido + Duplicate: Idiomas duplicados OIDCSettings: NotFound: Configuración OIDC no encontrada AlreadyExists: La configuración OIDC ya existe diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 43f8b8453b..3a75c2fa03 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Aucune limite spécifiée Restrictions: NoneSpecified: Aucune restriction spécifiée + DefaultLanguageMustBeAllowed: La langue par défaut doit être autorisée Language: NotParsed: Impossible d'analyser la langue + NotSupported: Langue non prise en charge + NotAllowed: Langue non autorisée + Undefined: Langue non définie + Duplicate: Langues en double OIDCSettings: NotFound: Configuration OIDC non trouvée AlreadyExists: La configuration OIDC existe déjà diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 045a7b06bc..352c691a5b 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Nessun limite specificato Restrictions: NoneSpecified: Nessuna restrizione specificata + DefaultLanguageMustBeAllowed: La lingua predefinita deve essere consentita Language: NotParsed: Impossibile analizzare la lingua + NotSupported: Lingua non supportata + NotAllowed: Lingua non consentita + Undefined: Lingua non definita + Duplicate: Lingue duplicate OIDCSettings: NotFound: Impossibile trovare la configurazione OIDC AlreadyExists: La configurazione OIDC esiste già diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index aa6e591987..c2a353f1c8 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: 制限が指定されていません Restrictions: NoneSpecified: 制限が指定されていません + DefaultLanguageMustBeAllowed: デフォルト言語は許可されている必要があります Language: NotParsed: 言語のパースに失敗しました + NotSupported: 言語はサポートされていません + NotAllowed: 言語は許可されていません + Undefined: 言語は未定義です + Duplicate: 言語に重複があります OIDCSettings: NotFound: OIDC構成が見つかりません AlreadyExists: すでに存在するOIDC構成です diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index a28190f645..ba1dbd47b6 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Не се наведени лимити Restrictions: NoneSpecified: Не се наведени ограничувања + DefaultLanguageMustBeAllowed: Стандардниот јазик мора да биде дозволен Language: NotParsed: Јазикот не може да се парсира + NotSupported: Јазикот не е поддржан + NotAllowed: Јазикот не е дозволен + Undefined: Јазикот е недефиниран + Duplicate: Јазиците имаат дупликати OIDCSettings: NotFound: OIDC конфигурацијата не е пронајдена AlreadyExists: OIDC конфигурацијата веќе постои diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index d9489dbb9e..322f25b2f8 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Geen limieten gespecificeerd Restrictions: NoneSpecified: Geen beperkingen gespecificeerd + DefaultLanguageMustBeAllowed: De standaardtaal moet worden toegestaan Language: NotParsed: Kon taal niet parsen + NotSupported: Taal wordt niet ondersteund + NotAllowed: Taal is niet toegestaan + Undefined: Taal is niet gedefinieerd + Duplicate: Talen hebben duplicaten OIDCSettings: NotFound: OIDC-configuratie niet gevonden AlreadyExists: OIDC-configuratie bestaat al diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 2359c4a6d3..6a06939d55 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Nie określono limitów Restrictions: NoneSpecified: Nie określono ograniczeń + DefaultLanguageMustBeAllowed: Domyślny język musi być dozwolony Language: NotParsed: Nie można przeanalizować języka + NotSupported: Język nie jest obsługiwany + NotAllowed: Język nie jest dozwolony + Undefined: Język jest niezdefiniowany + Duplicate: Języki mają duplikaty OIDCSettings: NotFound: Konfiguracja OIDC nie znaleziona AlreadyExists: Konfiguracja OIDC już istnieje diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 1633181539..4a0e01e40c 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Nenhum limite especificado Restrictions: NoneSpecified: Nenhuma restrição especificada + DefaultLanguageMustBeAllowed: O idioma padrão deve ser permitido Language: NotParsed: Não foi possível analisar o idioma + NotSupported: Idioma não suportado + NotAllowed: Idioma não permitido + Undefined: Idioma indefinido + Duplicate: Idiomas têm duplicatas OIDCSettings: NotFound: Configuração OIDC não encontrada AlreadyExists: Configuração OIDC já existe diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 21d8dd9a60..8d46081cf0 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Не указаны лимиты Restrictions: NoneSpecified: Не указаны ограничения + DefaultLanguageMustBeAllowed: Язык по умолчанию должен быть разрешен Language: NotParsed: Не удалось разобрать язык + NotSupported: Язык не поддерживается + NotAllowed: Язык не разрешен + Undefined: Язык не определен + Duplicate: Языки имеют дубликаты OIDCSettings: NotFound: Конфигурация OIDC не найдена AlreadyExists: Конфигурация OIDC уже существует diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 5b3ee8cab2..7e22dc5d57 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: 未指定限制 Restrictions: NoneSpecified: 未指定限制 + DefaultLanguageMustBeAllowed: 默认语言必须被允许 Language: NotParsed: 无法解析语言 + NotSupported: 语言不支持 + NotAllowed: 语言不被允许 + Undefined: 语言未定义 + Duplicate: 语言有重复 OIDCSettings: NotFound: OIDC 配置未找到 AlreadyExists: OIDC 配置已存在 diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 448fcca303..3e1d56fdda 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -3843,7 +3843,7 @@ service AdminService { responses: { key: "200"; value: { - description: "The status 200 is also returned if no restrictions were ever set. In this case, all feature restrictions have zero values."; + description: "The status 200 is also returned if no restrictions were ever set. In this case, all feature restrictions are undefined."; }; }; }; @@ -7994,6 +7994,20 @@ message SetRestrictionsRequest { description: "defines if ZITADEL should expose the endpoint /ui/login/register/org. If it is true, the org registration endpoint returns the HTTP status 404 on GET requests, and 409 on POST requests."; } ]; + optional SelectLanguages allowed_languages = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "restricts the allowed languages. If allowed_languages is undefined, the allowed languages are not changed."; + } + ]; +} + +// We have to wrap the languages list into a message so we can serialize empty lists. +message SelectLanguages { + repeated string list = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which languages to select. An empty list means all languages are selected."; + } + ]; } message SetRestrictionsResponse { @@ -8009,5 +8023,10 @@ message GetRestrictionsResponse { description: "defines if ZITADEL should expose the endpoint /ui/login/register/org. If it is true, the org registration endpoint returns the HTTP status 404 on GET requests, and 409 on POST requests."; } ]; + repeated string allowed_languages = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines the allowed languages. If allowed_languages has one or more entries, only these languages are allowed. If it has no entries, all supported languages are allowed"; + } + ]; } diff --git a/proto/zitadel/session/v2beta/session_service.proto b/proto/zitadel/session/v2beta/session_service.proto index ef1108e2c7..3461772c9a 100644 --- a/proto/zitadel/session/v2beta/session_service.proto +++ b/proto/zitadel/session/v2beta/session_service.proto @@ -280,7 +280,8 @@ message CreateSessionRequest{ optional google.protobuf.Duration lifetime = 5 [ (validate.rules).duration = {gt: {seconds: 0}}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "\"duration after which the session will be automatically invalidated\""; + description: "\"duration (in seconds) after which the session will be automatically invalidated\""; + example:"\"18000s\"" } ]; } @@ -333,7 +334,8 @@ message SetSessionRequest{ optional google.protobuf.Duration lifetime = 6 [ (validate.rules).duration = {gt: {seconds: 0}}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "\"duration after which the session will be automatically invalidated\""; + description: "\"duration (in seconds) after which the session will be automatically invalidated\""; + example:"\"18000s\"" } ]; }