diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index ee49ed33c9..6796658f6f 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -5,6 +5,7 @@ on:
jobs:
test:
+ timeout-minutes: 10
strategy:
fail-fast: false
matrix:
diff --git a/.github/workflows/ready_for_review.yml b/.github/workflows/ready_for_review.yml
index f46ee4a407..2ead263dc9 100644
--- a/.github/workflows/ready_for_review.yml
+++ b/.github/workflows/ready_for_review.yml
@@ -9,7 +9,7 @@ jobs:
- uses: actions/github-script@v7
with:
script: |
- const content = `### Thanks for your contribution! 🎉
+ const content = `### Thanks for your contribution @${{ github.event.pull_request.user.login }}! 🎉
Please make sure you tick the following checkboxes before marking this Pull Request (PR) as ready for review:
diff --git a/MEETING_SCHEDULE.md b/MEETING_SCHEDULE.md
new file mode 100644
index 0000000000..04dd4ff849
--- /dev/null
+++ b/MEETING_SCHEDULE.md
@@ -0,0 +1,29 @@
+# ZITADEL Office Hours
+
+Dear community!
+We're excited to announce bi-weekly office hours.
+
+## #1 Dive Deep into Actions v2
+
+The first office hour is dedicated to exploring the [new Actions v2 feature](https://zitadel.com/docs/concepts/features/actions_v2).
+
+What to expect:
+
+* **Deep Dive**: @adlerhurst will walk you through the functionalities and benefits of Actions v2.
+* **Live Q&A**: Get your questions answered directly by the ZITADEL team during the dedicated Q&A session.
+
+Details:
+
+* **Target Audience**: Developers and IT Ops personnel using ZITADEL
+* **Topic**: Actions v2 deep dive and Q&A
+* **When**: Wednesday 29th of May 12 pm PST / 3 pm EST / 9 pm CEST
+* **Duration**: about 1 hour
+* **Platform**: Zitadel Discord Server (Join us here: https://zitadel.com/office-hours?event=1243282884677341275 )
+
+In the meantime:
+
+Feel free to share any questions you already have about Actions v2 in the chat of the [office hours channel](https://zitadel.com/office-hours) on our Discord server.
+
+We look forward to seeing you there!
+
+P.S. Spread the word! Share this announcement with your fellow ZITADEL users who might be interested.
\ No newline at end of file
diff --git a/README.md b/README.md
index 449ecb3589..1840218eb7 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,9 @@
+|Community Meeting|
+|------------------|
+|ZITADEL holds bi-weekly community calls. To join the community calls or to watch previous meeting notes and recordings, please visit the [meeting schedule](https://github.com/zitadel/zitadel/blob/main/MEETING_SCHEDULE.md).|
Are you searching for a user management tool that is quickly set up like Auth0 and open source like Keycloak?
diff --git a/cmd/initialise/init.go b/cmd/initialise/init.go
index 7451f73ad3..917e6a2d93 100644
--- a/cmd/initialise/init.go
+++ b/cmd/initialise/init.go
@@ -19,6 +19,7 @@ var (
createUserStmt string
grantStmt string
+ settingsStmt string
databaseStmt string
createEventstoreStmt string
createProjectionsStmt string
@@ -39,7 +40,7 @@ func New() *cobra.Command {
Long: `Sets up the minimum requirements to start ZITADEL.
Prerequisites:
-- cockroachDB
+- database (PostgreSql or cockroachdb)
The user provided by flags needs privileges to
- create the database if it does not exist
@@ -53,7 +54,7 @@ The user provided by flags needs privileges to
},
}
- cmd.AddCommand(newZitadel(), newDatabase(), newUser(), newGrant())
+ cmd.AddCommand(newZitadel(), newDatabase(), newUser(), newGrant(), newSettings())
return cmd
}
@@ -62,6 +63,7 @@ func InitAll(ctx context.Context, config *Config) {
VerifyUser(config.Database.Username(), config.Database.Password()),
VerifyDatabase(config.Database.DatabaseName()),
VerifyGrant(config.Database.DatabaseName(), config.Database.Username()),
+ VerifySettings(config.Database.DatabaseName(), config.Database.Username()),
)
logging.OnError(err).Fatal("unable to initialize the database")
@@ -147,6 +149,11 @@ func ReadStmts(typ string) (err error) {
return err
}
+ settingsStmt, err = readStmt(typ, "11_settings")
+ if err != nil {
+ return err
+ }
+
return nil
}
diff --git a/cmd/initialise/sql/cockroach/11_settings.sql b/cmd/initialise/sql/cockroach/11_settings.sql
new file mode 100644
index 0000000000..5fa9dd72f6
--- /dev/null
+++ b/cmd/initialise/sql/cockroach/11_settings.sql
@@ -0,0 +1,4 @@
+-- replace the first %[1]q with the database in double quotes
+-- replace the second \%[2]q with the user in double quotes$
+-- For more information see technical advisory 10009 (https://zitadel.com/docs/support/advisory/a10009)
+ALTER ROLE %[2]q IN DATABASE %[1]q SET enable_durable_locking_for_serializable = on;
\ No newline at end of file
diff --git a/cmd/initialise/sql/postgres/11_settings.sql b/cmd/initialise/sql/postgres/11_settings.sql
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/cmd/initialise/verify_settings.go b/cmd/initialise/verify_settings.go
new file mode 100644
index 0000000000..75e811663f
--- /dev/null
+++ b/cmd/initialise/verify_settings.go
@@ -0,0 +1,44 @@
+package initialise
+
+import (
+ _ "embed"
+ "fmt"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+ "github.com/zitadel/logging"
+
+ "github.com/zitadel/zitadel/internal/database"
+)
+
+func newSettings() *cobra.Command {
+ return &cobra.Command{
+ Use: "settings",
+ Short: "Ensures proper settings on the database",
+ Long: `Ensures proper settings on the database.
+
+Prerequisites:
+- cockroachDB or postgreSQL
+
+Cockroach
+- Sets enable_durable_locking_for_serializable to on for the zitadel user and database
+`,
+ Run: func(cmd *cobra.Command, args []string) {
+ config := MustNewConfig(viper.GetViper())
+
+ err := initialise(config.Database, VerifySettings(config.Database.DatabaseName(), config.Database.Username()))
+ logging.OnError(err).Fatal("unable to set settings")
+ },
+ }
+}
+
+func VerifySettings(databaseName, username string) func(*database.DB) error {
+ return func(db *database.DB) error {
+ if db.Type() == "postgres" {
+ return nil
+ }
+ logging.WithFields("user", username, "database", databaseName).Info("verify settings")
+
+ return exec(db, fmt.Sprintf(settingsStmt, databaseName, username), nil)
+ }
+}
diff --git a/cmd/mirror/auth.go b/cmd/mirror/auth.go
new file mode 100644
index 0000000000..df94708e71
--- /dev/null
+++ b/cmd/mirror/auth.go
@@ -0,0 +1,91 @@
+package mirror
+
+import (
+ "context"
+ _ "embed"
+ "io"
+ "time"
+
+ "github.com/jackc/pgx/v5/stdlib"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+ "github.com/zitadel/logging"
+
+ "github.com/zitadel/zitadel/internal/database"
+ "github.com/zitadel/zitadel/internal/database/dialect"
+)
+
+func authCmd() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "auth",
+ Short: "mirrors the auth requests table from one database to another",
+ Long: `mirrors the auth requests table from one database to another
+ZITADEL needs to be initialized and set up with the --for-mirror flag
+Only auth requests are mirrored`,
+ Run: func(cmd *cobra.Command, args []string) {
+ config := mustNewMigrationConfig(viper.GetViper())
+ copyAuth(cmd.Context(), config)
+ },
+ }
+
+ cmd.Flags().BoolVar(&shouldReplace, "replace", false, "allow delete auth requests of defined instances before copy")
+
+ return cmd
+}
+
+func copyAuth(ctx context.Context, config *Migration) {
+ sourceClient, err := database.Connect(config.Source, false, dialect.DBPurposeQuery)
+ logging.OnError(err).Fatal("unable to connect to source database")
+ defer sourceClient.Close()
+
+ destClient, err := database.Connect(config.Destination, false, dialect.DBPurposeEventPusher)
+ logging.OnError(err).Fatal("unable to connect to destination database")
+ defer destClient.Close()
+
+ copyAuthRequests(ctx, sourceClient, destClient)
+}
+
+func copyAuthRequests(ctx context.Context, source, dest *database.DB) {
+ start := time.Now()
+
+ sourceConn, err := source.Conn(ctx)
+ logging.OnError(err).Fatal("unable to acquire connection")
+ defer sourceConn.Close()
+
+ r, w := io.Pipe()
+ errs := make(chan error, 1)
+
+ go func() {
+ err = sourceConn.Raw(func(driverConn interface{}) error {
+ conn := driverConn.(*stdlib.Conn).Conn()
+ _, err := conn.PgConn().CopyTo(ctx, w, "COPY (SELECT id, regexp_replace(request::TEXT, '\\\\u0000', '', 'g')::JSON request, code, request_type, creation_date, change_date, instance_id FROM auth.auth_requests "+instanceClause()+") TO STDOUT")
+ w.Close()
+ return err
+ })
+ errs <- err
+ }()
+
+ destConn, err := dest.Conn(ctx)
+ logging.OnError(err).Fatal("unable to acquire connection")
+ defer destConn.Close()
+
+ var affected int64
+ err = destConn.Raw(func(driverConn interface{}) error {
+ conn := driverConn.(*stdlib.Conn).Conn()
+
+ if shouldReplace {
+ _, err := conn.Exec(ctx, "DELETE FROM auth.auth_requests "+instanceClause())
+ if err != nil {
+ return err
+ }
+ }
+
+ tag, err := conn.PgConn().CopyFrom(ctx, r, "COPY auth.auth_requests FROM STDIN")
+ affected = tag.RowsAffected()
+
+ return err
+ })
+ logging.OnError(err).Fatal("unable to copy auth requests to destination")
+ logging.OnError(<-errs).Fatal("unable to copy auth requests from source")
+ logging.WithFields("took", time.Since(start), "count", affected).Info("auth requests migrated")
+}
diff --git a/cmd/mirror/config.go b/cmd/mirror/config.go
new file mode 100644
index 0000000000..5d2ec8fac7
--- /dev/null
+++ b/cmd/mirror/config.go
@@ -0,0 +1,80 @@
+package mirror
+
+import (
+ _ "embed"
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/spf13/viper"
+ "github.com/zitadel/logging"
+
+ "github.com/zitadel/zitadel/cmd/hooks"
+ "github.com/zitadel/zitadel/internal/actions"
+ internal_authz "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/command"
+ "github.com/zitadel/zitadel/internal/config/hook"
+ "github.com/zitadel/zitadel/internal/database"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/id"
+)
+
+type Migration struct {
+ Source database.Config
+ Destination database.Config
+
+ EventBulkSize uint32
+
+ Log *logging.Config
+ Machine *id.Config
+}
+
+var (
+ //go:embed defaults.yaml
+ defaultConfig []byte
+)
+
+func mustNewMigrationConfig(v *viper.Viper) *Migration {
+ config := new(Migration)
+ mustNewConfig(v, config)
+
+ err := config.Log.SetLogger()
+ logging.OnError(err).Fatal("unable to set logger")
+
+ id.Configure(config.Machine)
+
+ return config
+}
+
+func mustNewProjectionsConfig(v *viper.Viper) *ProjectionsConfig {
+ config := new(ProjectionsConfig)
+ mustNewConfig(v, config)
+
+ err := config.Log.SetLogger()
+ logging.OnError(err).Fatal("unable to set logger")
+
+ id.Configure(config.Machine)
+
+ return config
+}
+
+func mustNewConfig(v *viper.Viper, config any) {
+ err := v.Unmarshal(config,
+ viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
+ hooks.SliceTypeStringDecode[*domain.CustomMessageText],
+ hooks.SliceTypeStringDecode[*command.SetQuota],
+ hooks.SliceTypeStringDecode[internal_authz.RoleMapping],
+ hooks.MapTypeStringDecode[string, *internal_authz.SystemAPIUser],
+ hooks.MapTypeStringDecode[domain.Feature, any],
+ hooks.MapHTTPHeaderStringDecode,
+ hook.Base64ToBytesHookFunc(),
+ hook.TagToLanguageHookFunc(),
+ mapstructure.StringToTimeDurationHookFunc(),
+ mapstructure.StringToTimeHookFunc(time.RFC3339),
+ mapstructure.StringToSliceHookFunc(","),
+ database.DecodeHook,
+ actions.HTTPConfigDecodeHook,
+ hook.EnumHookFunc(internal_authz.MemberTypeString),
+ )),
+ )
+ logging.OnError(err).Fatal("unable to read default config")
+}
diff --git a/cmd/mirror/defaults.yaml b/cmd/mirror/defaults.yaml
new file mode 100644
index 0000000000..7db91ecc0b
--- /dev/null
+++ b/cmd/mirror/defaults.yaml
@@ -0,0 +1,114 @@
+Source:
+ cockroach:
+ Host: localhost # ZITADEL_DATABASE_COCKROACH_HOST
+ Port: 26257 # ZITADEL_DATABASE_COCKROACH_PORT
+ Database: zitadel # ZITADEL_DATABASE_COCKROACH_DATABASE
+ MaxOpenConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXOPENCONNS
+ MaxIdleConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXIDLECONNS
+ EventPushConnRatio: 0.33 # ZITADEL_DATABASE_COCKROACH_EVENTPUSHCONNRATIO
+ ProjectionSpoolerConnRatio: 0.33 # ZITADEL_DATABASE_COCKROACH_PROJECTIONSPOOLERCONNRATIO
+ MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME
+ MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME
+ Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS
+ User:
+ Username: zitadel # ZITADEL_DATABASE_COCKROACH_USER_USERNAME
+ Password: "" # ZITADEL_DATABASE_COCKROACH_USER_PASSWORD
+ SSL:
+ Mode: disable # ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE
+ RootCert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT
+ Cert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT
+ Key: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY
+ # Postgres is used as soon as a value is set
+ # The values describe the possible fields to set values
+ postgres:
+ Host: # ZITADEL_DATABASE_POSTGRES_HOST
+ Port: # ZITADEL_DATABASE_POSTGRES_PORT
+ Database: # ZITADEL_DATABASE_POSTGRES_DATABASE
+ MaxOpenConns: # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS
+ MaxIdleConns: # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS
+ MaxConnLifetime: # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME
+ MaxConnIdleTime: # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME
+ Options: # ZITADEL_DATABASE_POSTGRES_OPTIONS
+ User:
+ Username: # ZITADEL_DATABASE_POSTGRES_USER_USERNAME
+ Password: # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD
+ SSL:
+ Mode: # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE
+ RootCert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT
+ Cert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT
+ Key: # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY
+
+Destination:
+ cockroach:
+ Host: localhost # ZITADEL_DATABASE_COCKROACH_HOST
+ Port: 26257 # ZITADEL_DATABASE_COCKROACH_PORT
+ Database: zitadel # ZITADEL_DATABASE_COCKROACH_DATABASE
+ MaxOpenConns: 0 # ZITADEL_DATABASE_COCKROACH_MAXOPENCONNS
+ MaxIdleConns: 0 # ZITADEL_DATABASE_COCKROACH_MAXIDLECONNS
+ MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME
+ MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME
+ EventPushConnRatio: 0.01 # ZITADEL_DATABASE_COCKROACH_EVENTPUSHCONNRATIO
+ ProjectionSpoolerConnRatio: 0.5 # ZITADEL_DATABASE_COCKROACH_PROJECTIONSPOOLERCONNRATIO
+ Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS
+ User:
+ Username: zitadel # ZITADEL_DATABASE_COCKROACH_USER_USERNAME
+ Password: "" # ZITADEL_DATABASE_COCKROACH_USER_PASSWORD
+ SSL:
+ Mode: disable # ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE
+ RootCert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT
+ Cert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT
+ Key: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY
+ # Postgres is used as soon as a value is set
+ # The values describe the possible fields to set values
+ postgres:
+ Host: # ZITADEL_DATABASE_POSTGRES_HOST
+ Port: # ZITADEL_DATABASE_POSTGRES_PORT
+ Database: # ZITADEL_DATABASE_POSTGRES_DATABASE
+ MaxOpenConns: # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS
+ MaxIdleConns: # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS
+ MaxConnLifetime: # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME
+ MaxConnIdleTime: # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME
+ Options: # ZITADEL_DATABASE_POSTGRES_OPTIONS
+ User:
+ Username: # ZITADEL_DATABASE_POSTGRES_USER_USERNAME
+ Password: # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD
+ SSL:
+ Mode: # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE
+ RootCert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT
+ Cert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT
+ Key: # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY
+
+EventBulkSize: 10000
+
+Projections:
+ # The maximum duration a transaction remains open
+ # before it spots left folding additional events
+ # and updates the table.
+ TransactionDuration: 0s # ZITADEL_PROJECTIONS_TRANSACTIONDURATION
+ # turn off scheduler during operation
+ RequeueEvery: 0s
+ ConcurrentInstances: 7
+ EventBulkLimit: 1000
+ Customizations:
+ notifications:
+ MaxFailureCount: 1
+
+Eventstore:
+ MaxRetries: 3
+
+Auth:
+ Spooler:
+ TransactionDuration: 0s #ZITADEL_AUTH_SPOOLER_TRANSACTIONDURATION
+ BulkLimit: 1000 #ZITADEL_AUTH_SPOOLER_BULKLIMIT
+
+Admin:
+ Spooler:
+ TransactionDuration: 0s #ZITADEL_AUTH_SPOOLER_TRANSACTIONDURATION
+ BulkLimit: 10 #ZITADEL_AUTH_SPOOLER_BULKLIMIT
+
+FirstInstance:
+ # We only need to create an empty zitadel database so this step must be skipped
+ Skip: true
+
+Log:
+ Level: info
diff --git a/cmd/mirror/event.go b/cmd/mirror/event.go
new file mode 100644
index 0000000000..2bb0d52f45
--- /dev/null
+++ b/cmd/mirror/event.go
@@ -0,0 +1,96 @@
+package mirror
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/v2/eventstore"
+ "github.com/zitadel/zitadel/internal/v2/projection"
+ "github.com/zitadel/zitadel/internal/v2/readmodel"
+ "github.com/zitadel/zitadel/internal/v2/system"
+ mirror_event "github.com/zitadel/zitadel/internal/v2/system/mirror"
+)
+
+func queryLastSuccessfulMigration(ctx context.Context, destinationES *eventstore.EventStore, source string) (*readmodel.LastSuccessfulMirror, error) {
+ lastSuccess := readmodel.NewLastSuccessfulMirror(source)
+ if shouldIgnorePrevious {
+ return lastSuccess, nil
+ }
+ _, err := destinationES.Query(
+ ctx,
+ eventstore.NewQuery(
+ system.AggregateInstance,
+ lastSuccess,
+ eventstore.SetFilters(lastSuccess.Filter()),
+ ),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return lastSuccess, nil
+}
+
+func writeMigrationStart(ctx context.Context, sourceES *eventstore.EventStore, id string, destination string) (_ float64, err error) {
+ var cmd *eventstore.Command
+ if len(instanceIDs) > 0 {
+ cmd, err = mirror_event.NewStartedInstancesCommand(destination, instanceIDs)
+ if err != nil {
+ return 0, err
+ }
+ } else {
+ cmd = mirror_event.NewStartedSystemCommand(destination)
+ }
+
+ var position projection.HighestPosition
+
+ err = sourceES.Push(
+ ctx,
+ eventstore.NewPushIntent(
+ system.AggregateInstance,
+ eventstore.AppendAggregate(
+ system.AggregateOwner,
+ system.AggregateType,
+ id,
+ eventstore.CurrentSequenceMatches(0),
+ eventstore.AppendCommands(cmd),
+ ),
+ eventstore.PushReducer(&position),
+ ),
+ )
+ if err != nil {
+ return 0, err
+ }
+ return position.Position, nil
+}
+
+func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position float64) error {
+ return destinationES.Push(
+ ctx,
+ eventstore.NewPushIntent(
+ system.AggregateInstance,
+ eventstore.AppendAggregate(
+ system.AggregateOwner,
+ system.AggregateType,
+ id,
+ eventstore.CurrentSequenceMatches(0),
+ eventstore.AppendCommands(mirror_event.NewSucceededCommand(source, position)),
+ ),
+ ),
+ )
+}
+
+func writeMigrationFailed(ctx context.Context, destinationES *eventstore.EventStore, id, source string, err error) error {
+ return destinationES.Push(
+ ctx,
+ eventstore.NewPushIntent(
+ system.AggregateInstance,
+ eventstore.AppendAggregate(
+ system.AggregateOwner,
+ system.AggregateType,
+ id,
+ eventstore.CurrentSequenceMatches(0),
+ eventstore.AppendCommands(mirror_event.NewFailedCommand(source, err)),
+ ),
+ ),
+ )
+}
diff --git a/cmd/mirror/event_store.go b/cmd/mirror/event_store.go
new file mode 100644
index 0000000000..358f878d77
--- /dev/null
+++ b/cmd/mirror/event_store.go
@@ -0,0 +1,250 @@
+package mirror
+
+import (
+ "context"
+ "database/sql"
+ _ "embed"
+ "errors"
+ "io"
+ "time"
+
+ "github.com/jackc/pgx/v5/stdlib"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+ "github.com/zitadel/logging"
+
+ db "github.com/zitadel/zitadel/internal/database"
+ "github.com/zitadel/zitadel/internal/database/dialect"
+ "github.com/zitadel/zitadel/internal/id"
+ "github.com/zitadel/zitadel/internal/v2/database"
+ "github.com/zitadel/zitadel/internal/v2/eventstore"
+ "github.com/zitadel/zitadel/internal/v2/eventstore/postgres"
+ "github.com/zitadel/zitadel/internal/zerrors"
+)
+
+var shouldIgnorePrevious bool
+
+func eventstoreCmd() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "eventstore",
+ Short: "mirrors the eventstore of an instance from one database to another",
+ Long: `mirrors the eventstore of an instance from one database to another
+ZITADEL needs to be initialized and set up with the --for-mirror flag
+Migrate only copies events2 and unique constraints`,
+ Run: func(cmd *cobra.Command, args []string) {
+ config := mustNewMigrationConfig(viper.GetViper())
+ copyEventstore(cmd.Context(), config)
+ },
+ }
+
+ cmd.Flags().BoolVar(&shouldReplace, "replace", false, "allow delete unique constraints of defined instances before copy")
+ cmd.Flags().BoolVar(&shouldIgnorePrevious, "ignore-previous", false, "ignores previous migrations of the events table")
+
+ return cmd
+}
+
+func copyEventstore(ctx context.Context, config *Migration) {
+ sourceClient, err := db.Connect(config.Source, false, dialect.DBPurposeQuery)
+ logging.OnError(err).Fatal("unable to connect to source database")
+ defer sourceClient.Close()
+
+ destClient, err := db.Connect(config.Destination, false, dialect.DBPurposeEventPusher)
+ logging.OnError(err).Fatal("unable to connect to destination database")
+ defer destClient.Close()
+
+ copyEvents(ctx, sourceClient, destClient, config.EventBulkSize)
+ copyUniqueConstraints(ctx, sourceClient, destClient)
+}
+
+func positionQuery(db *db.DB) string {
+ switch db.Type() {
+ case "postgres":
+ return "SELECT EXTRACT(EPOCH FROM clock_timestamp())"
+ case "cockroach":
+ return "SELECT cluster_logical_timestamp()"
+ default:
+ logging.WithFields("db_type", db.Type()).Fatal("database type not recognized")
+ return ""
+ }
+}
+
+func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
+ start := time.Now()
+ reader, writer := io.Pipe()
+
+ migrationID, err := id.SonyFlakeGenerator().Next()
+ logging.OnError(err).Fatal("unable to generate migration id")
+
+ sourceConn, err := source.Conn(ctx)
+ logging.OnError(err).Fatal("unable to acquire source connection")
+
+ destConn, err := dest.Conn(ctx)
+ logging.OnError(err).Fatal("unable to acquire dest connection")
+
+ sourceES := eventstore.NewEventstoreFromOne(postgres.New(source, &postgres.Config{
+ MaxRetries: 3,
+ }))
+ destinationES := eventstore.NewEventstoreFromOne(postgres.New(dest, &postgres.Config{
+ MaxRetries: 3,
+ }))
+
+ previousMigration, err := queryLastSuccessfulMigration(ctx, destinationES, source.DatabaseName())
+ logging.OnError(err).Fatal("unable to query latest successful migration")
+
+ maxPosition, err := writeMigrationStart(ctx, sourceES, migrationID, dest.DatabaseName())
+ logging.OnError(err).Fatal("unable to write migration started event")
+
+ logging.WithFields("from", previousMigration.Position, "to", maxPosition).Info("start event migration")
+
+ nextPos := make(chan bool, 1)
+ pos := make(chan float64, 1)
+ errs := make(chan error, 3)
+
+ go func() {
+ err := sourceConn.Raw(func(driverConn interface{}) error {
+ conn := driverConn.(*stdlib.Conn).Conn()
+ nextPos <- true
+ var i uint32
+ for position := range pos {
+ var stmt database.Statement
+ stmt.WriteString("COPY (SELECT instance_id, aggregate_type, aggregate_id, event_type, sequence, revision, created_at, regexp_replace(payload::TEXT, '\\\\u0000', '', 'g')::JSON payload, creator, owner, ")
+ stmt.WriteArg(position)
+ stmt.WriteString(" position, row_number() OVER (PARTITION BY instance_id ORDER BY position, in_tx_order) AS in_tx_order FROM eventstore.events2 ")
+ stmt.WriteString(instanceClause())
+ stmt.WriteString(" AND ")
+ database.NewNumberAtMost(maxPosition).Write(&stmt, "position")
+ stmt.WriteString(" AND ")
+ database.NewNumberGreater(previousMigration.Position).Write(&stmt, "position")
+ stmt.WriteString(" ORDER BY instance_id, position, in_tx_order")
+ stmt.WriteString(" LIMIT ")
+ stmt.WriteArg(bulkSize)
+ stmt.WriteString(" OFFSET ")
+ stmt.WriteArg(bulkSize * i)
+ stmt.WriteString(") TO STDOUT")
+
+ // Copy does not allow args so we use we replace the args in the statement
+ tag, err := conn.PgConn().CopyTo(ctx, writer, stmt.Debug())
+ if err != nil {
+ return zerrors.ThrowUnknownf(err, "MIGRA-KTuSq", "unable to copy events from source during iteration %d", i)
+ }
+ if tag.RowsAffected() < int64(bulkSize) {
+ return nil
+ }
+
+ nextPos <- true
+ i++
+ }
+ return nil
+ })
+ writer.Close()
+ close(nextPos)
+ errs <- err
+ }()
+
+ // generate next position for
+ go func() {
+ defer close(pos)
+ for range nextPos {
+ var position float64
+ err := dest.QueryRowContext(
+ ctx,
+ func(row *sql.Row) error {
+ return row.Scan(&position)
+ },
+ positionQuery(dest),
+ )
+ if err != nil {
+ errs <- zerrors.ThrowUnknown(err, "MIGRA-kMyPH", "unable to query next position")
+ return
+ }
+ pos <- position
+ }
+ }()
+
+ var eventCount int64
+ errs <- destConn.Raw(func(driverConn interface{}) error {
+ conn := driverConn.(*stdlib.Conn).Conn()
+
+ tag, err := conn.PgConn().CopyFrom(ctx, reader, "COPY eventstore.events2 FROM STDIN")
+ eventCount = tag.RowsAffected()
+ if err != nil {
+ return zerrors.ThrowUnknown(err, "MIGRA-DTHi7", "unable to copy events into destination")
+ }
+
+ return nil
+ })
+
+ close(errs)
+ writeCopyEventsDone(ctx, destinationES, migrationID, source.DatabaseName(), maxPosition, errs)
+
+ logging.WithFields("took", time.Since(start), "count", eventCount).Info("events migrated")
+}
+
+func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, source string, position float64, errs <-chan error) {
+ joinedErrs := make([]error, 0, len(errs))
+ for err := range errs {
+ joinedErrs = append(joinedErrs, err)
+ }
+ err := errors.Join(joinedErrs...)
+
+ if err != nil {
+ logging.WithError(err).Error("unable to mirror events")
+ err := writeMigrationFailed(ctx, es, id, source, err)
+ logging.OnError(err).Fatal("unable to write failed event")
+ return
+ }
+
+ err = writeMigrationSucceeded(ctx, es, id, source, position)
+ logging.OnError(err).Fatal("unable to write failed event")
+}
+
+func copyUniqueConstraints(ctx context.Context, source, dest *db.DB) {
+ start := time.Now()
+ reader, writer := io.Pipe()
+ errs := make(chan error, 1)
+
+ sourceConn, err := source.Conn(ctx)
+ logging.OnError(err).Fatal("unable to acquire source connection")
+
+ go func() {
+ err := sourceConn.Raw(func(driverConn interface{}) error {
+ conn := driverConn.(*stdlib.Conn).Conn()
+ var stmt database.Statement
+ stmt.WriteString("COPY (SELECT instance_id, unique_type, unique_field FROM eventstore.unique_constraints ")
+ stmt.WriteString(instanceClause())
+ stmt.WriteString(") TO stdout")
+
+ _, err := conn.PgConn().CopyTo(ctx, writer, stmt.String())
+ writer.Close()
+ return err
+ })
+ errs <- err
+ }()
+
+ destConn, err := dest.Conn(ctx)
+ logging.OnError(err).Fatal("unable to acquire dest connection")
+
+ var eventCount int64
+ err = destConn.Raw(func(driverConn interface{}) error {
+ conn := driverConn.(*stdlib.Conn).Conn()
+
+ if shouldReplace {
+ var stmt database.Statement
+ stmt.WriteString("DELETE FROM eventstore.unique_constraints ")
+ stmt.WriteString(instanceClause())
+
+ _, err := conn.Exec(ctx, stmt.String())
+ if err != nil {
+ return err
+ }
+ }
+
+ tag, err := conn.PgConn().CopyFrom(ctx, reader, "COPY eventstore.unique_constraints FROM stdin")
+ eventCount = tag.RowsAffected()
+
+ return err
+ })
+ logging.OnError(err).Fatal("unable to copy unique constraints to destination")
+ logging.OnError(<-errs).Fatal("unable to copy unique constraints from source")
+ logging.WithFields("took", time.Since(start), "count", eventCount).Info("unique constraints migrated")
+}
diff --git a/cmd/mirror/mirror.go b/cmd/mirror/mirror.go
new file mode 100644
index 0000000000..69e830658a
--- /dev/null
+++ b/cmd/mirror/mirror.go
@@ -0,0 +1,94 @@
+package mirror
+
+import (
+ "bytes"
+ _ "embed"
+ "strings"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+ "github.com/zitadel/logging"
+
+ "github.com/zitadel/zitadel/cmd/key"
+)
+
+var (
+ instanceIDs []string
+ isSystem bool
+ shouldReplace bool
+)
+
+func New() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "mirror",
+ Short: "mirrors all data of ZITADEL from one database to another",
+ Long: `mirrors all data of ZITADEL from one database to another
+ZITADEL needs to be initialized and set up with --for-mirror
+
+The command does mirror all data needed and recomputes the projections.
+For more details call the help functions of the sub commands.
+
+Order of execution:
+1. mirror system tables
+2. mirror auth tables
+3. mirror event store tables
+4. recompute projections
+5. verify`,
+ PersistentPreRun: func(cmd *cobra.Command, args []string) {
+ err := viper.MergeConfig(bytes.NewBuffer(defaultConfig))
+ logging.OnError(err).Fatal("unable to read default config")
+ },
+ Run: func(cmd *cobra.Command, args []string) {
+ config := mustNewMigrationConfig(viper.GetViper())
+ projectionConfig := mustNewProjectionsConfig(viper.GetViper())
+
+ masterKey, err := key.MasterKey(cmd)
+ logging.OnError(err).Fatal("unable to read master key")
+
+ copySystem(cmd.Context(), config)
+ copyAuth(cmd.Context(), config)
+ copyEventstore(cmd.Context(), config)
+
+ projections(cmd.Context(), projectionConfig, masterKey)
+ verifyMigration(cmd.Context(), config)
+ },
+ }
+
+ mirrorFlags(cmd)
+ cmd.Flags().BoolVar(&shouldIgnorePrevious, "ignore-previous", false, "ignores previous migrations of the events table")
+ cmd.Flags().BoolVar(&shouldReplace, "replace", false, `replaces all data of the following tables for the provided instances or all if the "--system"-flag is set:
+* system.assets
+* auth.auth_requests
+* eventstore.unique_constraints
+The flag should be provided if you want to execute the mirror command multiple times so that the static data are also mirrored to prevent inconsistent states.`)
+ migrateProjectionsFlags(cmd)
+
+ cmd.AddCommand(
+ eventstoreCmd(),
+ systemCmd(),
+ projectionsCmd(),
+ authCmd(),
+ verifyCmd(),
+ )
+
+ return cmd
+}
+
+func mirrorFlags(cmd *cobra.Command) {
+ cmd.PersistentFlags().StringSliceVar(&instanceIDs, "instance", nil, "id or comma separated ids of the instance(s) to migrate. Either this or the `--system`-flag must be set. Make sure to always use the same flag if you execute the command multiple times.")
+ cmd.PersistentFlags().BoolVar(&isSystem, "system", false, "migrates the whole system. Either this or the `--instance`-flag must be set. Make sure to always use the same flag if you execute the command multiple times.")
+ cmd.MarkFlagsOneRequired("system", "instance")
+ cmd.MarkFlagsMutuallyExclusive("system", "instance")
+}
+
+func instanceClause() string {
+ if isSystem {
+ return "WHERE instance_id <> ''"
+ }
+ for i := range instanceIDs {
+ instanceIDs[i] = "'" + instanceIDs[i] + "'"
+ }
+
+ // COPY does not allow parameters so we need to set them directly
+ return "WHERE instance_id IN (" + strings.Join(instanceIDs, ", ") + ")"
+}
diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go
new file mode 100644
index 0000000000..af7ba98c5c
--- /dev/null
+++ b/cmd/mirror/projections.go
@@ -0,0 +1,316 @@
+package mirror
+
+import (
+ "context"
+ "database/sql"
+ "net/http"
+ "sync"
+ "time"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+ "github.com/zitadel/logging"
+
+ "github.com/zitadel/zitadel/cmd/encryption"
+ "github.com/zitadel/zitadel/cmd/key"
+ "github.com/zitadel/zitadel/cmd/tls"
+ admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
+ admin_handler "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/handler"
+ admin_view "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/view"
+ internal_authz "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/api/oidc"
+ "github.com/zitadel/zitadel/internal/api/ui/login"
+ auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing"
+ auth_handler "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/handler"
+ auth_view "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view"
+ "github.com/zitadel/zitadel/internal/authz"
+ authz_es "github.com/zitadel/zitadel/internal/authz/repository/eventsourcing/eventstore"
+ "github.com/zitadel/zitadel/internal/command"
+ "github.com/zitadel/zitadel/internal/config/systemdefaults"
+ crypto_db "github.com/zitadel/zitadel/internal/crypto/database"
+ "github.com/zitadel/zitadel/internal/database"
+ "github.com/zitadel/zitadel/internal/database/dialect"
+ "github.com/zitadel/zitadel/internal/domain"
+ "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/notification"
+ "github.com/zitadel/zitadel/internal/notification/handlers"
+ "github.com/zitadel/zitadel/internal/query"
+ "github.com/zitadel/zitadel/internal/query/projection"
+ static_config "github.com/zitadel/zitadel/internal/static/config"
+ es_v4 "github.com/zitadel/zitadel/internal/v2/eventstore"
+ es_v4_pg "github.com/zitadel/zitadel/internal/v2/eventstore/postgres"
+ "github.com/zitadel/zitadel/internal/webauthn"
+)
+
+func projectionsCmd() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "projections",
+ Short: "calls the projections synchronously",
+ Run: func(cmd *cobra.Command, args []string) {
+ config := mustNewProjectionsConfig(viper.GetViper())
+
+ masterKey, err := key.MasterKey(cmd)
+ logging.OnError(err).Fatal("unable to read master key")
+
+ projections(cmd.Context(), config, masterKey)
+ },
+ }
+
+ migrateProjectionsFlags(cmd)
+
+ return cmd
+}
+
+type ProjectionsConfig struct {
+ Destination database.Config
+ Projections projection.Config
+ EncryptionKeys *encryption.EncryptionKeyConfig
+ SystemAPIUsers map[string]*internal_authz.SystemAPIUser
+ Eventstore *eventstore.Config
+
+ Admin admin_es.Config
+ Auth auth_es.Config
+
+ Log *logging.Config
+ Machine *id.Config
+
+ ExternalPort uint16
+ ExternalDomain string
+ ExternalSecure bool
+ InternalAuthZ internal_authz.Config
+ SystemDefaults systemdefaults.SystemDefaults
+ Telemetry *handlers.TelemetryPusherConfig
+ Login login.Config
+ OIDC oidc.Config
+ WebAuthNName string
+ DefaultInstance command.InstanceSetup
+ AssetStorage static_config.AssetStorageConfig
+}
+
+func migrateProjectionsFlags(cmd *cobra.Command) {
+ key.AddMasterKeyFlag(cmd)
+ tls.AddTLSModeFlag(cmd)
+}
+
+func projections(
+ ctx context.Context,
+ config *ProjectionsConfig,
+ masterKey string,
+) {
+ start := time.Now()
+
+ client, err := database.Connect(config.Destination, false, dialect.DBPurposeQuery)
+ logging.OnError(err).Fatal("unable to connect to database")
+
+ keyStorage, err := crypto_db.NewKeyStorage(client, masterKey)
+ logging.OnError(err).Fatal("cannot start key storage")
+
+ keys, err := encryption.EnsureEncryptionKeys(ctx, config.EncryptionKeys, keyStorage)
+ logging.OnError(err).Fatal("unable to read encryption keys")
+
+ staticStorage, err := config.AssetStorage.NewStorage(client.DB)
+ logging.OnError(err).Fatal("unable create static storage")
+
+ config.Eventstore.Querier = old_es.NewCRDB(client)
+ esPusherDBClient, err := database.Connect(config.Destination, false, dialect.DBPurposeEventPusher)
+ logging.OnError(err).Fatal("unable to connect eventstore push client")
+ config.Eventstore.Pusher = new_es.NewEventstore(esPusherDBClient)
+ es := eventstore.NewEventstore(config.Eventstore)
+ esV4 := es_v4.NewEventstoreFromOne(es_v4_pg.New(client, &es_v4_pg.Config{
+ MaxRetries: config.Eventstore.MaxRetries,
+ }))
+
+ sessionTokenVerifier := internal_authz.SessionTokenVerifier(keys.OIDC)
+
+ queries, err := query.StartQueries(
+ ctx,
+ es,
+ esV4.Querier,
+ client,
+ client,
+ config.Projections,
+ config.SystemDefaults,
+ keys.IDPConfig,
+ keys.OTP,
+ keys.OIDC,
+ keys.SAML,
+ config.InternalAuthZ.RolePermissionMappings,
+ sessionTokenVerifier,
+ func(q *query.Queries) domain.PermissionCheck {
+ return func(ctx context.Context, permission, orgID, resourceID string) (err error) {
+ return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID)
+ }
+ },
+ 0,
+ config.SystemAPIUsers,
+ false,
+ )
+ logging.OnError(err).Fatal("unable to start queries")
+
+ authZRepo, err := authz.Start(queries, es, client, keys.OIDC, config.ExternalSecure)
+ logging.OnError(err).Fatal("unable to start authz repo")
+
+ webAuthNConfig := &webauthn.Config{
+ DisplayName: config.WebAuthNName,
+ ExternalSecure: config.ExternalSecure,
+ }
+ commands, err := command.StartCommands(
+ es,
+ config.SystemDefaults,
+ config.InternalAuthZ.RolePermissionMappings,
+ staticStorage,
+ webAuthNConfig,
+ config.ExternalDomain,
+ config.ExternalSecure,
+ config.ExternalPort,
+ keys.IDPConfig,
+ keys.OTP,
+ keys.SMTP,
+ keys.SMS,
+ keys.User,
+ keys.DomainVerification,
+ keys.OIDC,
+ keys.SAML,
+ &http.Client{},
+ func(ctx context.Context, permission, orgID, resourceID string) (err error) {
+ return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID)
+ },
+ sessionTokenVerifier,
+ config.OIDC.DefaultAccessTokenLifetime,
+ config.OIDC.DefaultRefreshTokenExpiration,
+ config.OIDC.DefaultRefreshTokenIdleExpiration,
+ config.DefaultInstance.SecretGenerators,
+ )
+ logging.OnError(err).Fatal("unable to start commands")
+
+ err = projection.Create(ctx, client, es, config.Projections, keys.OIDC, keys.SAML, config.SystemAPIUsers)
+ logging.OnError(err).Fatal("unable to start projections")
+
+ i18n.MustLoadSupportedLanguagesFromDir()
+
+ notification.Register(
+ ctx,
+ config.Projections.Customizations["notifications"],
+ config.Projections.Customizations["notificationsquotas"],
+ config.Projections.Customizations["telemetry"],
+ *config.Telemetry,
+ config.ExternalDomain,
+ config.ExternalPort,
+ config.ExternalSecure,
+ commands,
+ queries,
+ es,
+ config.Login.DefaultOTPEmailURLV2,
+ config.SystemDefaults.Notifications.FileSystemPath,
+ keys.User,
+ keys.SMTP,
+ keys.SMS,
+ )
+
+ config.Auth.Spooler.Client = client
+ config.Auth.Spooler.Eventstore = es
+ authView, err := auth_view.StartView(config.Auth.Spooler.Client, keys.OIDC, queries, config.Auth.Spooler.Eventstore)
+ logging.OnError(err).Fatal("unable to start auth view")
+ auth_handler.Register(ctx, config.Auth.Spooler, authView, queries)
+
+ config.Admin.Spooler.Client = client
+ config.Admin.Spooler.Eventstore = es
+ adminView, err := admin_view.StartView(config.Admin.Spooler.Client)
+ logging.OnError(err).Fatal("unable to start admin view")
+
+ admin_handler.Register(ctx, config.Admin.Spooler, adminView, staticStorage)
+
+ instances := make(chan string, config.Projections.ConcurrentInstances)
+ failedInstances := make(chan string)
+ wg := sync.WaitGroup{}
+ wg.Add(int(config.Projections.ConcurrentInstances))
+
+ go func() {
+ for instance := range failedInstances {
+ logging.WithFields("instance", instance).Error("projection failed")
+ }
+ }()
+
+ for i := 0; i < int(config.Projections.ConcurrentInstances); i++ {
+ go execProjections(ctx, instances, failedInstances, &wg)
+ }
+
+ for _, instance := range queryInstanceIDs(ctx, client) {
+ instances <- instance
+ }
+ close(instances)
+ wg.Wait()
+
+ close(failedInstances)
+
+ logging.WithFields("took", time.Since(start)).Info("projections executed")
+}
+
+func execProjections(ctx context.Context, instances <-chan string, failedInstances chan<- string, wg *sync.WaitGroup) {
+ for instance := range instances {
+ logging.WithFields("instance", instance).Info("start projections")
+ ctx = internal_authz.WithInstanceID(ctx, instance)
+
+ err := projection.ProjectInstance(ctx)
+ if err != nil {
+ logging.WithFields("instance", instance).OnError(err).Info("trigger failed")
+ failedInstances <- instance
+ continue
+ }
+
+ err = admin_handler.ProjectInstance(ctx)
+ if err != nil {
+ logging.WithFields("instance", instance).OnError(err).Info("trigger admin handler failed")
+ failedInstances <- instance
+ continue
+ }
+
+ err = auth_handler.ProjectInstance(ctx)
+ if err != nil {
+ logging.WithFields("instance", instance).OnError(err).Info("trigger auth handler failed")
+ failedInstances <- instance
+ continue
+ }
+
+ err = notification.ProjectInstance(ctx)
+ if err != nil {
+ logging.WithFields("instance", instance).OnError(err).Info("trigger notification failed")
+ failedInstances <- instance
+ continue
+ }
+ logging.WithFields("instance", instance).Info("projections done")
+ }
+ wg.Done()
+}
+
+// returns the instance configured by flag
+// or all instances which are not removed
+func queryInstanceIDs(ctx context.Context, source *database.DB) []string {
+ if len(instanceIDs) > 0 {
+ return instanceIDs
+ }
+
+ instances := []string{}
+ err := source.QueryContext(
+ ctx,
+ func(r *sql.Rows) error {
+ for r.Next() {
+ var instance string
+
+ if err := r.Scan(&instance); err != nil {
+ return err
+ }
+ instances = append(instances, instance)
+ }
+ return r.Err()
+ },
+ "SELECT DISTINCT instance_id FROM eventstore.events2 WHERE instance_id <> '' AND aggregate_type = 'instance' AND event_type = 'instance.added' AND instance_id NOT IN (SELECT instance_id FROM eventstore.events2 WHERE instance_id <> '' AND aggregate_type = 'instance' AND event_type = 'instance.removed')",
+ )
+ logging.OnError(err).Fatal("unable to query instances")
+
+ return instances
+}
diff --git a/cmd/mirror/system.go b/cmd/mirror/system.go
new file mode 100644
index 0000000000..e16836aa8c
--- /dev/null
+++ b/cmd/mirror/system.go
@@ -0,0 +1,139 @@
+package mirror
+
+import (
+ "context"
+ _ "embed"
+ "io"
+ "time"
+
+ "github.com/jackc/pgx/v5/stdlib"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+ "github.com/zitadel/logging"
+
+ "github.com/zitadel/zitadel/internal/database"
+ "github.com/zitadel/zitadel/internal/database/dialect"
+)
+
+func systemCmd() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "system",
+ Short: "mirrors the system tables of ZITADEL from one database to another",
+ Long: `mirrors the system tables of ZITADEL from one database to another
+ZITADEL needs to be initialized
+Only keys and assets are mirrored`,
+ Run: func(cmd *cobra.Command, args []string) {
+ config := mustNewMigrationConfig(viper.GetViper())
+ copySystem(cmd.Context(), config)
+ },
+ }
+
+ cmd.Flags().BoolVar(&shouldReplace, "replace", false, "allow delete ALL keys and assets of defined instances before copy")
+
+ return cmd
+}
+
+func copySystem(ctx context.Context, config *Migration) {
+ sourceClient, err := database.Connect(config.Source, false, dialect.DBPurposeQuery)
+ logging.OnError(err).Fatal("unable to connect to source database")
+ defer sourceClient.Close()
+
+ destClient, err := database.Connect(config.Destination, false, dialect.DBPurposeEventPusher)
+ logging.OnError(err).Fatal("unable to connect to destination database")
+ defer destClient.Close()
+
+ copyAssets(ctx, sourceClient, destClient)
+ copyEncryptionKeys(ctx, sourceClient, destClient)
+}
+
+func copyAssets(ctx context.Context, source, dest *database.DB) {
+ start := time.Now()
+
+ sourceConn, err := source.Conn(ctx)
+ logging.OnError(err).Fatal("unable to acquire source connection")
+ defer sourceConn.Close()
+
+ r, w := io.Pipe()
+ errs := make(chan error, 1)
+
+ go func() {
+ err = sourceConn.Raw(func(driverConn interface{}) error {
+ conn := driverConn.(*stdlib.Conn).Conn()
+ // ignore hash column because it's computed
+ _, err := conn.PgConn().CopyTo(ctx, w, "COPY (SELECT instance_id, asset_type, resource_owner, name, content_type, data, updated_at FROM system.assets "+instanceClause()+") TO stdout")
+ w.Close()
+ return err
+ })
+ errs <- err
+ }()
+
+ destConn, err := dest.Conn(ctx)
+ logging.OnError(err).Fatal("unable to acquire dest connection")
+ defer destConn.Close()
+
+ var eventCount int64
+ err = destConn.Raw(func(driverConn interface{}) error {
+ conn := driverConn.(*stdlib.Conn).Conn()
+
+ if shouldReplace {
+ _, err := conn.Exec(ctx, "DELETE FROM system.assets "+instanceClause())
+ if err != nil {
+ return err
+ }
+ }
+
+ tag, err := conn.PgConn().CopyFrom(ctx, r, "COPY system.assets (instance_id, asset_type, resource_owner, name, content_type, data, updated_at) FROM stdin")
+ eventCount = tag.RowsAffected()
+
+ return err
+ })
+ logging.OnError(err).Fatal("unable to copy assets to destination")
+ logging.OnError(<-errs).Fatal("unable to copy assets from source")
+ logging.WithFields("took", time.Since(start), "count", eventCount).Info("assets migrated")
+}
+
+func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) {
+ start := time.Now()
+
+ sourceConn, err := source.Conn(ctx)
+ logging.OnError(err).Fatal("unable to acquire source connection")
+ defer sourceConn.Close()
+
+ r, w := io.Pipe()
+ errs := make(chan error, 1)
+
+ go func() {
+ err = sourceConn.Raw(func(driverConn interface{}) error {
+ conn := driverConn.(*stdlib.Conn).Conn()
+ // ignore hash column because it's computed
+ _, err := conn.PgConn().CopyTo(ctx, w, "COPY system.encryption_keys TO stdout")
+ w.Close()
+ return err
+ })
+ errs <- err
+ }()
+
+ destConn, err := dest.Conn(ctx)
+ logging.OnError(err).Fatal("unable to acquire dest connection")
+ defer destConn.Close()
+
+ var eventCount int64
+ err = destConn.Raw(func(driverConn interface{}) error {
+ conn := driverConn.(*stdlib.Conn).Conn()
+
+ if shouldReplace {
+ _, err := conn.Exec(ctx, "TRUNCATE system.encryption_keys")
+ if err != nil {
+ return err
+ }
+ }
+
+ tag, err := conn.PgConn().CopyFrom(ctx, r, "COPY system.encryption_keys FROM stdin")
+ eventCount = tag.RowsAffected()
+
+ return err
+ })
+ logging.OnError(err).Fatal("unable to copy encryption keys to destination")
+ logging.OnError(<-errs).Fatal("unable to copy encryption keys from source")
+ logging.WithFields("took", time.Since(start), "count", eventCount).Info("encryption keys migrated")
+}
diff --git a/cmd/mirror/verify.go b/cmd/mirror/verify.go
new file mode 100644
index 0000000000..7b90ad89aa
--- /dev/null
+++ b/cmd/mirror/verify.go
@@ -0,0 +1,111 @@
+package mirror
+
+import (
+ "context"
+ "database/sql"
+ _ "embed"
+ "fmt"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+ "github.com/zitadel/logging"
+
+ "github.com/zitadel/zitadel/internal/database"
+ "github.com/zitadel/zitadel/internal/database/dialect"
+)
+
+func verifyCmd() *cobra.Command {
+ return &cobra.Command{
+ Use: "verify",
+ Short: "counts if source and dest have the same amount of entries",
+ Run: func(cmd *cobra.Command, args []string) {
+ config := mustNewMigrationConfig(viper.GetViper())
+ verifyMigration(cmd.Context(), config)
+ },
+ }
+}
+
+var schemas = []string{
+ "adminapi",
+ "auth",
+ "eventstore",
+ "projections",
+ "system",
+}
+
+func verifyMigration(ctx context.Context, config *Migration) {
+ sourceClient, err := database.Connect(config.Source, false, dialect.DBPurposeQuery)
+ logging.OnError(err).Fatal("unable to connect to source database")
+ defer sourceClient.Close()
+
+ destClient, err := database.Connect(config.Destination, false, dialect.DBPurposeEventPusher)
+ logging.OnError(err).Fatal("unable to connect to destination database")
+ defer destClient.Close()
+
+ for _, schema := range schemas {
+ for _, table := range append(getTables(ctx, destClient, schema), getViews(ctx, destClient, schema)...) {
+ sourceCount := countEntries(ctx, sourceClient, table)
+ destCount := countEntries(ctx, destClient, table)
+
+ entry := logging.WithFields("table", table, "dest", destCount, "source", sourceCount)
+ if sourceCount == destCount {
+ entry.Debug("equal count")
+ continue
+ }
+ entry.WithField("diff", destCount-sourceCount).Info("unequal count")
+ }
+ }
+}
+
+func getTables(ctx context.Context, dest *database.DB, schema string) (tables []string) {
+ err := dest.QueryContext(
+ ctx,
+ func(r *sql.Rows) error {
+ for r.Next() {
+ var table string
+ if err := r.Scan(&table); err != nil {
+ return err
+ }
+ tables = append(tables, table)
+ }
+ return r.Err()
+ },
+ "SELECT CONCAT(schemaname, '.', tablename) FROM pg_tables WHERE schemaname = $1",
+ schema,
+ )
+ logging.WithFields("schema", schema).OnError(err).Fatal("unable to query tables")
+ return tables
+}
+
+func getViews(ctx context.Context, dest *database.DB, schema string) (tables []string) {
+ err := dest.QueryContext(
+ ctx,
+ func(r *sql.Rows) error {
+ for r.Next() {
+ var table string
+ if err := r.Scan(&table); err != nil {
+ return err
+ }
+ tables = append(tables, table)
+ }
+ return r.Err()
+ },
+ "SELECT CONCAT(schemaname, '.', viewname) FROM pg_views WHERE schemaname = $1",
+ schema,
+ )
+ logging.WithFields("schema", schema).OnError(err).Fatal("unable to query views")
+ return tables
+}
+
+func countEntries(ctx context.Context, client *database.DB, table string) (count int) {
+ err := client.QueryRowContext(
+ ctx,
+ func(r *sql.Row) error {
+ return r.Scan(&count)
+ },
+ fmt.Sprintf("SELECT COUNT(*) FROM %s %s", table, instanceClause()),
+ )
+ logging.WithFields("table", table, "db", client.DatabaseName()).OnError(err).Error("unable to count")
+
+ return count
+}
diff --git a/cmd/setup/03.go b/cmd/setup/03.go
index 0d8c988688..4860ae3eec 100644
--- a/cmd/setup/03.go
+++ b/cmd/setup/03.go
@@ -26,6 +26,8 @@ type FirstInstance struct {
PatPath string
Features *command.InstanceFeatures
+ Skip bool
+
instanceSetup command.InstanceSetup
userEncryptionKey *crypto.KeyConfig
smtpEncryptionKey *crypto.KeyConfig
@@ -42,6 +44,9 @@ type FirstInstance struct {
}
func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error {
+ if mig.Skip {
+ return nil
+ }
keyStorage, err := mig.verifyEncryptionKeys(ctx)
if err != nil {
return err
diff --git a/cmd/setup/config.go b/cmd/setup/config.go
index 1ba85804ab..81ec6f2332 100644
--- a/cmd/setup/config.go
+++ b/cmd/setup/config.go
@@ -28,6 +28,7 @@ import (
)
type Config struct {
+ ForMirror bool
Database database.Config
SystemDefaults systemdefaults.SystemDefaults
InternalAuthZ internal_authz.Config
diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go
index bb0111d5be..6ed0cc4dc7 100644
--- a/cmd/setup/setup.go
+++ b/cmd/setup/setup.go
@@ -34,6 +34,8 @@ import (
notify_handler "github.com/zitadel/zitadel/internal/notification"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/query/projection"
+ es_v4 "github.com/zitadel/zitadel/internal/v2/eventstore"
+ es_v4_pg "github.com/zitadel/zitadel/internal/v2/eventstore/postgres"
"github.com/zitadel/zitadel/internal/webauthn"
)
@@ -57,13 +59,16 @@ Requirements:
err = BindInitProjections(cmd)
logging.OnError(err).Fatal("unable to bind \"init-projections\" flag")
+ err = bindForMirror(cmd)
+ logging.OnError(err).Fatal("unable to bind \"for-mirror\" flag")
+
config := MustNewConfig(viper.GetViper())
steps := MustNewSteps(viper.New())
masterKey, err := key.MasterKey(cmd)
logging.OnError(err).Panic("No master key provided")
- Setup(config, steps, masterKey)
+ Setup(cmd.Context(), config, steps, masterKey)
},
}
@@ -77,6 +82,7 @@ Requirements:
func Flags(cmd *cobra.Command) {
cmd.PersistentFlags().StringArrayVar(&stepFiles, "steps", nil, "paths to step files to overwrite default steps")
cmd.Flags().Bool("init-projections", viper.GetBool("InitProjections"), "beta feature: initializes projections after they are created, allows smooth start as projections are up to date")
+ cmd.Flags().Bool("for-mirror", viper.GetBool("ForMirror"), "use this flag if you want to mirror your existing data")
key.AddMasterKeyFlag(cmd)
tls.AddTLSModeFlag(cmd)
}
@@ -85,8 +91,11 @@ func BindInitProjections(cmd *cobra.Command) error {
return viper.BindPFlag("InitProjections.Enabled", cmd.Flags().Lookup("init-projections"))
}
-func Setup(config *Config, steps *Steps, masterKey string) {
- ctx := context.Background()
+func bindForMirror(cmd *cobra.Command) error {
+ return viper.BindPFlag("ForMirror", cmd.Flags().Lookup("for-mirror"))
+}
+
+func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) {
logging.Info("setup started")
i18n.MustLoadSupportedLanguagesFromDir()
@@ -102,10 +111,14 @@ func Setup(config *Config, steps *Steps, masterKey string) {
config.Eventstore.Pusher = new_es.NewEventstore(esPusherDBClient)
eventstoreClient := eventstore.NewEventstore(config.Eventstore)
logging.OnError(err).Fatal("unable to start eventstore")
+ eventstoreV4 := es_v4.NewEventstoreFromOne(es_v4_pg.New(queryDBClient, &es_v4_pg.Config{
+ MaxRetries: config.Eventstore.MaxRetries,
+ }))
steps.s1ProjectionTable = &ProjectionTable{dbClient: queryDBClient.DB}
steps.s2AssetsTable = &AssetTable{dbClient: queryDBClient.DB}
+ steps.FirstInstance.Skip = config.ForMirror || steps.FirstInstance.Skip
steps.FirstInstance.instanceSetup = config.DefaultInstance
steps.FirstInstance.userEncryptionKey = config.EncryptionKeys.User
steps.FirstInstance.smtpEncryptionKey = config.EncryptionKeys.SMTP
@@ -197,10 +210,11 @@ func Setup(config *Config, steps *Steps, masterKey string) {
}
// projection initialization must be done last, since the steps above might add required columns to the projections
- if config.InitProjections.Enabled {
+ if !config.ForMirror && config.InitProjections.Enabled {
initProjections(
ctx,
eventstoreClient,
+ eventstoreV4,
queryDBClient,
projectionDBClient,
masterKey,
@@ -222,6 +236,7 @@ func readStmt(fs embed.FS, folder, typ, filename string) (string, error) {
func initProjections(
ctx context.Context,
eventstoreClient *eventstore.Eventstore,
+ eventstoreV4 *es_v4.EventStore,
queryDBClient,
projectionDBClient *database.DB,
masterKey string,
@@ -278,6 +293,7 @@ func initProjections(
queries, err := query.StartQueries(
ctx,
eventstoreClient,
+ eventstoreV4.Querier,
queryDBClient,
projectionDBClient,
config.Projections,
diff --git a/cmd/setup/steps.yaml b/cmd/setup/steps.yaml
index 5e7805f24d..03476c6648 100644
--- a/cmd/setup/steps.yaml
+++ b/cmd/setup/steps.yaml
@@ -1,5 +1,7 @@
# By using the FirstInstance section, you can overwrite the DefaultInstance configuration for the first instance created by zitadel setup.
FirstInstance:
+ # If set to true zitadel is setup without initial data
+ Skip: false
# The machine key from the section FirstInstance.Org.Machine.MachineKey is written to the MachineKeyPath.
MachineKeyPath: # ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH
# The personal access token from the section FirstInstance.Org.Machine.Pat is written to the PatPath.
diff --git a/cmd/start/start.go b/cmd/start/start.go
index 49de4d2073..a7688a83cb 100644
--- a/cmd/start/start.go
+++ b/cmd/start/start.go
@@ -78,6 +78,8 @@ import (
"github.com/zitadel/zitadel/internal/notification"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/static"
+ es_v4 "github.com/zitadel/zitadel/internal/v2/eventstore"
+ es_v4_pg "github.com/zitadel/zitadel/internal/v2/eventstore/postgres"
"github.com/zitadel/zitadel/internal/webauthn"
"github.com/zitadel/zitadel/openapi"
)
@@ -153,12 +155,16 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
config.Eventstore.Pusher = new_es.NewEventstore(esPusherDBClient)
config.Eventstore.Querier = old_es.NewCRDB(queryDBClient)
eventstoreClient := eventstore.NewEventstore(config.Eventstore)
+ eventstoreV4 := es_v4.NewEventstoreFromOne(es_v4_pg.New(queryDBClient, &es_v4_pg.Config{
+ MaxRetries: config.Eventstore.MaxRetries,
+ }))
sessionTokenVerifier := internal_authz.SessionTokenVerifier(keys.OIDC)
queries, err := query.StartQueries(
ctx,
eventstoreClient,
+ eventstoreV4.Querier,
queryDBClient,
projectionDBClient,
config.Projections,
diff --git a/cmd/start/start_from_init.go b/cmd/start/start_from_init.go
index 78c0be719d..38a6a6c4d1 100644
--- a/cmd/start/start_from_init.go
+++ b/cmd/start/start_from_init.go
@@ -36,7 +36,7 @@ Requirements:
setupConfig := setup.MustNewConfig(viper.GetViper())
setupSteps := setup.MustNewSteps(viper.New())
- setup.Setup(setupConfig, setupSteps, masterKey)
+ setup.Setup(cmd.Context(), setupConfig, setupSteps, masterKey)
startConfig := MustNewConfig(viper.GetViper())
diff --git a/cmd/start/start_from_setup.go b/cmd/start/start_from_setup.go
index ec26f47414..a8b7295f2a 100644
--- a/cmd/start/start_from_setup.go
+++ b/cmd/start/start_from_setup.go
@@ -34,7 +34,7 @@ Requirements:
setupConfig := setup.MustNewConfig(viper.GetViper())
setupSteps := setup.MustNewSteps(viper.New())
- setup.Setup(setupConfig, setupSteps, masterKey)
+ setup.Setup(cmd.Context(), setupConfig, setupSteps, masterKey)
startConfig := MustNewConfig(viper.GetViper())
diff --git a/cmd/zitadel.go b/cmd/zitadel.go
index fdddfed007..c855dd4495 100644
--- a/cmd/zitadel.go
+++ b/cmd/zitadel.go
@@ -15,6 +15,7 @@ import (
"github.com/zitadel/zitadel/cmd/build"
"github.com/zitadel/zitadel/cmd/initialise"
"github.com/zitadel/zitadel/cmd/key"
+ "github.com/zitadel/zitadel/cmd/mirror"
"github.com/zitadel/zitadel/cmd/ready"
"github.com/zitadel/zitadel/cmd/setup"
"github.com/zitadel/zitadel/cmd/start"
@@ -55,6 +56,7 @@ func New(out io.Writer, in io.Reader, args []string, server chan<- *start.Server
start.New(server),
start.NewStartFromInit(server),
start.NewStartFromSetup(server),
+ mirror.New(),
key.New(),
ready.New(),
)
diff --git a/console/src/app/modules/password-complexity-view/password-complexity-view.component.html b/console/src/app/modules/password-complexity-view/password-complexity-view.component.html
index 7183bff70e..0a5649dabb 100644
--- a/console/src/app/modules/password-complexity-view/password-complexity-view.component.html
+++ b/console/src/app/modules/password-complexity-view/password-complexity-view.component.html
@@ -50,4 +50,12 @@
{{ 'ERRORS.LOWERCASEMISSING' | translate }}
+
+
+ 70" class="las la-times red">
+
+ {{ 'USER.PASSWORD.MAXLENGTHERROR' | translate: { value: 70 } }} ({{ password?.value?.length }}/{{ 70 }})
+
+
diff --git a/console/src/app/pages/projects/projects.component.html b/console/src/app/pages/projects/projects.component.html
index 8f6dea63f9..dd71661c63 100644
--- a/console/src/app/pages/projects/projects.component.html
+++ b/console/src/app/pages/projects/projects.component.html
@@ -6,9 +6,11 @@
info_outline
-
- {{ 'DESCRIPTIONS.PROJECTS.DESCRIPTION' | translate }}
-
+
+
, force: boolean = false): Promise {
if (partialConfig) {
Object.assign(this.authConfig, partialConfig);
diff --git a/console/src/app/services/grpc.service.ts b/console/src/app/services/grpc.service.ts
index 8a87f95ef0..5c4c0fd510 100644
--- a/console/src/app/services/grpc.service.ts
+++ b/console/src/app/services/grpc.service.ts
@@ -18,6 +18,7 @@ import { I18nInterceptor } from './interceptors/i18n.interceptor';
import { OrgInterceptor } from './interceptors/org.interceptor';
import { StorageService } from './storage.service';
import { FeatureServiceClient } from '../proto/generated/zitadel/feature/v2beta/Feature_serviceServiceClientPb';
+import { GrpcAuthService } from './grpc-auth.service';
@Injectable({
providedIn: 'root',
diff --git a/console/src/app/services/interceptors/auth.interceptor.ts b/console/src/app/services/interceptors/auth.interceptor.ts
index d21bb5cdaa..4ccdc768c7 100644
--- a/console/src/app/services/interceptors/auth.interceptor.ts
+++ b/console/src/app/services/interceptors/auth.interceptor.ts
@@ -2,11 +2,13 @@ import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Request, UnaryInterceptor, UnaryResponse } from 'grpc-web';
import { Subject } from 'rxjs';
-import { debounceTime, filter, first, take } from 'rxjs/operators';
+import { debounceTime, filter, first, map, take, tap } from 'rxjs/operators';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { AuthenticationService } from '../authentication.service';
import { StorageService } from '../storage.service';
+import { AuthConfig } from 'angular-oauth2-oidc';
+import { GrpcAuthService } from '../grpc-auth.service';
const authorizationKey = 'Authorization';
const bearerPrefix = 'Bearer';
@@ -44,7 +46,7 @@ export class AuthInterceptor implements UnaryIn
return response;
})
.catch(async (error: any) => {
- if (error.code === 16) {
+ if (error.code === 16 || (error.code === 7 && error.message === 'mfa required (AUTHZ-Kl3p0)')) {
this.triggerDialog.next(true);
}
return Promise.reject(error);
@@ -67,7 +69,13 @@ export class AuthInterceptor implements UnaryIn
.pipe(take(1))
.subscribe((resp) => {
if (resp) {
- this.authenticationService.authenticate(undefined, true);
+ const idToken = this.authenticationService.getIdToken();
+ const configWithPrompt: Partial = {
+ customQueryParams: {
+ id_token_hint: idToken,
+ },
+ };
+ this.authenticationService.authenticate(configWithPrompt, true);
}
});
}
diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json
index 7da4e58dca..4a1131920a 100644
--- a/console/src/assets/i18n/bg.json
+++ b/console/src/assets/i18n/bg.json
@@ -588,6 +588,7 @@
"INVALID_FORMAT": "Форматирането е невалидно.",
"NOTANEMAIL": "Дадената стойност не е имейл адрес.",
"MINLENGTH": "Трябва да е поне {{requiredLength}} знака дълги.",
+ "MAXLENGTH": "Трябва да е по-малко от {{requiredLength}} символа",
"UPPERCASEMISSING": "Трябва да включва главна буква.",
"LOWERCASEMISSING": "Трябва да включва малка буква.",
"SYMBOLERROR": "Трябва да включва символ или препинателен знак.",
@@ -873,7 +874,8 @@
"SET": "Задайте нова парола",
"RESENDNOTIFICATION": "Изпратете връзка за повторно задаване на парола",
"REQUIRED": "Някои задължителни полета липсват.",
- "MINLENGTHERROR": "Трябва да бъде поне {{value}} знака дълги."
+ "MINLENGTHERROR": "Трябва да бъде поне {{value}} знака дълги.",
+ "MAXLENGTHERROR": "Трябва да съдържа по-малко от {{value}} знака."
},
"ID": "документ за самоличност",
"EMAIL": "Електронна поща",
diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json
index 3e3f4e3ff5..f7b098c098 100644
--- a/console/src/assets/i18n/cs.json
+++ b/console/src/assets/i18n/cs.json
@@ -595,6 +595,7 @@
"INVALID_FORMAT": "Formát je neplatný.",
"NOTANEMAIL": "Zadaná hodnota není e-mailová adresa.",
"MINLENGTH": "Musí být dlouhé alespoň {{requiredLength}} znaků.",
+ "MAXLENGTH": "Musí být kratší než {{requiredLength}} znaků",
"UPPERCASEMISSING": "Musí obsahovat velké písmeno.",
"LOWERCASEMISSING": "Musí obsahovat malé písmeno.",
"SYMBOLERROR": "Musí obsahovat symbol nebo interpunkční znaménko.",
@@ -880,7 +881,8 @@
"SET": "Nastavit nové heslo",
"RESENDNOTIFICATION": "Odeslat odkaz pro reset hesla",
"REQUIRED": "Některá povinná pole nejsou vyplněna.",
- "MINLENGTHERROR": "Musí být alespoň {{value}} znaků dlouhé."
+ "MINLENGTHERROR": "Musí být alespoň {{value}} znaků dlouhé.",
+ "MAXLENGTHERROR": "Musí být kratší než {{value}} znaků."
},
"ID": "ID",
"EMAIL": "E-mail",
diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json
index a49819c241..4c595773c4 100644
--- a/console/src/assets/i18n/de.json
+++ b/console/src/assets/i18n/de.json
@@ -594,6 +594,7 @@
"INVALID_FORMAT": "Das Format is ungültig.",
"NOTANEMAIL": "Der eingegebene Wert ist keine E-Mail Adresse.",
"MINLENGTH": "Muss mindestens {{requiredLength}} Zeichen lang sein.",
+ "MAXLENGTH": "Muss weniger als {{requiredLength}} Zeichen enthalten",
"UPPERCASEMISSING": "Muss einen Grossbuchstaben beinhalten.",
"LOWERCASEMISSING": "Muss einen Kleinbuchstaben beinhalten.",
"SYMBOLERROR": "Muss ein Symbol/Satzzeichen beinhalten.",
@@ -879,7 +880,8 @@
"SET": "Passwort neu setzen",
"RESENDNOTIFICATION": "Email zum Zurücksetzen senden",
"REQUIRED": "Bitte prüfe, dass alle notwendigen Felder ausgefüllt sind.",
- "MINLENGTHERROR": "Muss mindestens {{value}} Zeichen lang sein."
+ "MINLENGTHERROR": "Muss mindestens {{value}} Zeichen lang sein.",
+ "MAXLENGTHERROR": "Muss weniger als {{value}} Zeichen umfassen."
},
"ID": "ID",
"EMAIL": "E-Mail",
diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json
index 9f686bbfb1..8a50cae2cf 100644
--- a/console/src/assets/i18n/en.json
+++ b/console/src/assets/i18n/en.json
@@ -54,7 +54,7 @@
},
"MACHINES": {
"TITLE": "Service Users",
- "DESCRIPTION": "Les utilisateurs du service s'authentifient de manière non interactive à l'aide d'un jeton de porteur JWT signé avec une clé privée. Ils peuvent également utiliser un jeton d'accès personnel.",
+ "DESCRIPTION": "Service Users authenticate non-interactively using a JWT bearer token signed with a private key. They can also use a personal access token.",
"METADATA": "Add custom attributes to the user like the authenticating system. You can use this information in your actions."
},
"SELF": {
@@ -595,8 +595,9 @@
"INVALID_FORMAT": "The formatting is invalid.",
"NOTANEMAIL": "The given value is not an e-mail address.",
"MINLENGTH": "Must be at least {{requiredLength}} characters long.",
- "UPPERCASEMISSING": "Must include an uppercase character.",
- "LOWERCASEMISSING": "Must include a lowercase character.",
+ "MAXLENGTH": "Must be less than {{requiredLength}} characters.",
+ "UPPERCASEMISSING": "Must include an uppercase letter.",
+ "LOWERCASEMISSING": "Must include a lowercase letter.",
"SYMBOLERROR": "Must include a symbol or punctuation mark.",
"NUMBERERROR": "Must include a digit.",
"PWNOTEQUAL": "The passwords provided do not match.",
@@ -880,7 +881,8 @@
"SET": "Set New Password",
"RESENDNOTIFICATION": "Send Password Reset Link",
"REQUIRED": "Some required fields are missing.",
- "MINLENGTHERROR": "Has to be at least {{value}} characters long."
+ "MINLENGTHERROR": "Must be at least {{value}} characters long.",
+ "MAXLENGTHERROR": "Must be less than {{value}} characters."
},
"ID": "ID",
"EMAIL": "E-mail",
diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json
index e07c0d978c..d535f44252 100644
--- a/console/src/assets/i18n/es.json
+++ b/console/src/assets/i18n/es.json
@@ -595,6 +595,7 @@
"INVALID_FORMAT": "El formato no es valido.",
"NOTANEMAIL": "El valor proporcionado no es una dirección de email.",
"MINLENGTH": "Debe tener al menos {{requiredLength}} caracteres de longitud.",
+ "MAXLENGTH": "Debe tener menos de {{requiredLength}} caracteres.",
"UPPERCASEMISSING": "Debe incluir un carácter en mayúscula.",
"LOWERCASEMISSING": "Debe incluir un carácter en minúscula.",
"SYMBOLERROR": "Debe incluir un símbolo o un signo de puntuación.",
@@ -880,7 +881,8 @@
"SET": "Establecer nueva contraseña",
"RESENDNOTIFICATION": "Enviar enlace para restablecer la contraseña",
"REQUIRED": "Faltan algunos campos requeridos.",
- "MINLENGTHERROR": "Debe tener al menos {{value}} caracteres de longitud."
+ "MINLENGTHERROR": "Debe tener al menos {{value}} caracteres de longitud.",
+ "MAXLENGTHERROR": "Debe tener menos de {{value}} caracteres"
},
"ID": "ID",
"EMAIL": "Email",
diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json
index d5fbfd9c08..110f0589d3 100644
--- a/console/src/assets/i18n/fr.json
+++ b/console/src/assets/i18n/fr.json
@@ -594,6 +594,7 @@
"INVALID_FORMAT": "Le format n'est pas valide.",
"NOTANEMAIL": "La valeur donnée n'est pas une adresse e-mail.",
"MINLENGTH": "Doit comporter au moins {{length}} caractères.",
+ "MAXLENGTH": "Doit contenir moins de {{requiredLength}} caractères.",
"UPPERCASEMISSING": "Doit inclure un caractère majuscule.",
"LOWERCASEMISSING": "Doit inclure un caractère minuscule.",
"SYMBOLERROR": "Doit inclure un symbole ou un signe de ponctuation.",
@@ -879,7 +880,8 @@
"SET": "Définir un nouveau mot de passe",
"RESENDNOTIFICATION": "Envoyer le lien de réinitialisation du mot de passe",
"REQUIRED": "Certains champs obligatoires sont manquants.",
- "MINLENGTHERROR": "Doit comporter au moins {{value}} caractères."
+ "MINLENGTHERROR": "Doit comporter au moins {{value}} caractères.",
+ "MAXLENGTHERROR": "Doit contenir moins de {{value}} caractères"
},
"ID": "ID",
"EMAIL": "E-mail",
diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json
index 3d986999eb..258635148e 100644
--- a/console/src/assets/i18n/it.json
+++ b/console/src/assets/i18n/it.json
@@ -593,6 +593,7 @@
"INVALID_FORMAT": "Il formato non è valido.",
"NOTANEMAIL": "Il valore dato non è un indirizzo e-mail.",
"MINLENGTH": "Deve essere lunga almeno {{requiredLength}} caratteri.",
+ "MAXLENGTH": "Deve contenere meno di {{requiredLength}} caratteri.",
"UPPERCASEMISSING": "Deve includere un carattere maiuscolo.",
"LOWERCASEMISSING": "Deve includere un carattere minuscolo.",
"SYMBOLERROR": "Deve includere un simbolo o un segno di punteggiatura.",
@@ -878,7 +879,8 @@
"SET": "Imposta nuova password",
"RESENDNOTIFICATION": "Invia email per la reimpostazione",
"REQUIRED": "Mancano alcuni campi obbligatori.",
- "MINLENGTHERROR": "Deve essere lunga almeno {{valore}} caratteri."
+ "MINLENGTHERROR": "Deve essere lunga almeno {{valore}} caratteri.",
+ "MAXLENGTHERROR": "Deve contenere meno di {{value}} caratteri"
},
"ID": "ID",
"EMAIL": "E-mail",
diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json
index 82f36fa7bf..30f1f8485c 100644
--- a/console/src/assets/i18n/ja.json
+++ b/console/src/assets/i18n/ja.json
@@ -595,6 +595,7 @@
"INVALID_FORMAT": "不正なフォーマットです",
"NOTANEMAIL": "入力された値がメールアドレスではありません。",
"MINLENGTH": "{{requiredLength}} 文字以上の文字列が必要です。",
+ "MAXLENGTH": "{{requiredLength}}文字以下でなければなりません.",
"UPPERCASEMISSING": "大文字を含める必要があります。",
"LOWERCASEMISSING": "小文字を含める必要があります。",
"SYMBOLERROR": "記号を含める必要があります。",
@@ -880,7 +881,8 @@
"SET": "新しいパスワードを設定する",
"RESENDNOTIFICATION": "パスワードリセットのリンクを送信する",
"REQUIRED": "一部の必須項目が不足しています。",
- "MINLENGTHERROR": "最小で{{value}}文字の長さが必要です。"
+ "MINLENGTHERROR": "最小で{{value}}文字の長さが必要です。",
+ "MAXLENGTHERROR": "{{value}}文字以下でなければなりません"
},
"ID": "id",
"EMAIL": "Eメール",
diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json
index 7b0095bf5e..55f52bc24b 100644
--- a/console/src/assets/i18n/mk.json
+++ b/console/src/assets/i18n/mk.json
@@ -595,6 +595,7 @@
"INVALID_FORMAT": "Невалиден формат.",
"NOTANEMAIL": "Внесената вредност не е е-пошта.",
"MINLENGTH": "Мора да биде најмалку {{requiredLength}} карактери долга.",
+ "MAXLENGTH": "Мора да биде помалку од {{requiredLength}} карактери.",
"UPPERCASEMISSING": "Мора да содржи голема буква.",
"LOWERCASEMISSING": "Мора да содржи мала буква.",
"SYMBOLERROR": "Мора да содржи симбол или знак за интерпункција.",
@@ -880,7 +881,8 @@
"SET": "Постави нова лозинка",
"RESENDNOTIFICATION": "Испрати линк за ресетирање на лозинката",
"REQUIRED": "Некои задолжителни полиња не се пополнети.",
- "MINLENGTHERROR": "Мора да биде најмалку {{value}} карактери долга."
+ "MINLENGTHERROR": "Мора да биде најмалку {{value}} карактери долга.",
+ "MAXLENGTHERROR": "Мора да биде помалку од {{value}} карактери"
},
"ID": "ID",
"EMAIL": "E-пошта",
diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json
index 1e4fb7abfd..c2526c848d 100644
--- a/console/src/assets/i18n/nl.json
+++ b/console/src/assets/i18n/nl.json
@@ -595,6 +595,7 @@
"INVALID_FORMAT": "De opmaak is ongeldig.",
"NOTANEMAIL": "De opgegeven waarde is geen e-mailadres.",
"MINLENGTH": "Moet minimaal {{requiredLength}} tekens lang zijn.",
+ "MAXLENGTH": "Moet minder dan {{requiredLength}} tekens bevatten.",
"UPPERCASEMISSING": "Moet een hoofdletter bevatten.",
"LOWERCASEMISSING": "Moet een kleine letter bevatten.",
"SYMBOLERROR": "Moet een symbool of leesteken bevatten.",
@@ -880,7 +881,8 @@
"SET": "Stel nieuw wachtwoord in",
"RESENDNOTIFICATION": "Stuur wachtwoord reset link",
"REQUIRED": "Sommige verplichte velden ontbreken.",
- "MINLENGTHERROR": "Moet minstens {{value}} tekens lang zijn."
+ "MINLENGTHERROR": "Moet minstens {{value}} tekens lang zijn.",
+ "MAXLENGTHERROR": "Moet minder dan {{value}} tekens bevatten"
},
"ID": "ID",
"EMAIL": "E-mail",
diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json
index cab3498076..65e13ce71f 100644
--- a/console/src/assets/i18n/pl.json
+++ b/console/src/assets/i18n/pl.json
@@ -594,6 +594,7 @@
"INVALID_FORMAT": "Format jest nieprawidłowy.",
"NOTANEMAIL": "Podana wartość nie jest adresem e-mail.",
"MINLENGTH": "Musi mieć co najmniej {{requiredLength}} znaków.",
+ "MAXLENGTH": "Musi zawierać mniej niż {{requiredLength}} znaków.",
"UPPERCASEMISSING": "Musi zawierać wielką literę.",
"LOWERCASEMISSING": "Musi zawierać małą literę.",
"SYMBOLERROR": "Musi zawierać symbol lub znak interpunkcyjny.",
@@ -879,7 +880,8 @@
"SET": "Ustaw nowe hasło",
"RESENDNOTIFICATION": "Wyślij link resetowania hasła",
"REQUIRED": "Brakuje niektórych wymaganych pól.",
- "MINLENGTHERROR": "Musi mieć co najmniej {{value}} znaków."
+ "MINLENGTHERROR": "Musi mieć co najmniej {{value}} znaków.",
+ "MAXLENGTHERROR": "Musi zawierać mniej niż {{value}} znaków"
},
"ID": "ID",
"EMAIL": "E-mail",
diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json
index 1c9d5a3c36..aee0da5a02 100644
--- a/console/src/assets/i18n/pt.json
+++ b/console/src/assets/i18n/pt.json
@@ -595,6 +595,7 @@
"INVALID_FORMAT": "O formato é inválido.",
"NOTANEMAIL": "O valor fornecido não é um endereço de e-mail.",
"MINLENGTH": "Deve ter pelo menos {{requiredLength}} caracteres.",
+ "MAXLENGTH": "Deve ter menos de {{requiredLength}} caracteres.",
"UPPERCASEMISSING": "Deve incluir uma letra maiúscula.",
"LOWERCASEMISSING": "Deve incluir uma letra minúscula.",
"SYMBOLERROR": "Deve incluir um símbolo ou caractere de pontuação.",
@@ -880,7 +881,8 @@
"SET": "Definir Nova Senha",
"RESENDNOTIFICATION": "Enviar Link de Redefinição de Senha",
"REQUIRED": "Algumas informações obrigatórias estão faltando.",
- "MINLENGTHERROR": "Deve ter pelo menos {{value}} caracteres."
+ "MINLENGTHERROR": "Deve ter pelo menos {{value}} caracteres.",
+ "MAXLENGTHERROR": "Deve ter menos de {{value}} caracteres"
},
"ID": "ID",
"EMAIL": "E-mail",
diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json
index 4363ee1ae7..294039adc9 100644
--- a/console/src/assets/i18n/ru.json
+++ b/console/src/assets/i18n/ru.json
@@ -594,6 +594,7 @@
"INVALID_FORMAT": "Форматирование неверно.",
"NOTANEMAIL": "Данное значение не является адресом электронной почты.",
"MINLENGTH": "Должно быть не менее {{requiredLength}} символов.",
+ "MAXLENGTH": "Должно быть меньше {{requiredLength}} символов.",
"UPPERCASEMISSING": "Должен содержать символ верхнего регистра.",
"LOWERCASEMISSING": "Должен включать строчные буквы.",
"SYMBOLERROR": "Должен содержать символ или знак препинания.",
@@ -887,7 +888,8 @@
"RESENDNOTIFICATION": "Отправить ссылку для сброса пароля",
"REQUIRED": "Отсутствуют некоторые обязательные поля.",
"MINLENGTHERROR": "Должно быть не менее {{value}} символов.",
- "NOTEQUAL": "Указанные пароли не совпадают."
+ "NOTEQUAL": "Указанные пароли не совпадают.",
+ "MAXLENGTHERROR": "Должно быть меньше {{value}} символов"
},
"ID": "Идентификатор",
"EMAIL": "Электронная почта",
diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json
index f983fcf138..c8b2baa0a1 100644
--- a/console/src/assets/i18n/zh.json
+++ b/console/src/assets/i18n/zh.json
@@ -594,6 +594,7 @@
"INVALID_FORMAT": "格式是无效的。",
"NOTANEMAIL": "给定的值不是合法电子邮件地址。",
"MINLENGTH": "长度必须至少是{{requiredLength}}字符。",
+ "MAXLENGTH": "必须少于{{requiredLength}}个字符.",
"UPPERCASEMISSING": "密码必须包含大写字符。",
"LOWERCASEMISSING": "密码必须包含小写字符。",
"SYMBOLERROR": "密码必须包含符号或标点符号。",
@@ -879,7 +880,8 @@
"SET": "设置新密码",
"RESENDNOTIFICATION": "发送重置密码链接",
"REQUIRED": "缺少必填字段。",
- "MINLENGTHERROR": "密码长度必须至少为 {{value}} 个字符。"
+ "MINLENGTHERROR": "密码长度必须至少为 {{value}} 个字符。",
+ "MAXLENGTHERROR": "必须少于{{value}}个字符"
},
"ID": "ID",
"EMAIL": "电子邮件",
diff --git a/docs/docs/apis/introduction.mdx b/docs/docs/apis/introduction.mdx
index a4220abba5..081b31cbce 100644
--- a/docs/docs/apis/introduction.mdx
+++ b/docs/docs/apis/introduction.mdx
@@ -281,7 +281,7 @@ ZITADEL hosts everything under a single domain: `{instance}.zitadel.cloud` or yo
The domain is used as the OIDC issuer and as the base url for the gRPC and REST APIs, the Login and Console UI, which you'll find under `{your_domain}/ui/console/`.
-Are you self-hosting and having troubles with *Instance not found* errors? [Check out this page](https://zitadel.com/docs/self-hosting/manage/custom-domain).
+Are you self-hosting and having troubles with *Instance not found* errors? [Check out this page](/docs/self-hosting/manage/custom-domain).
## API path prefixes
diff --git a/docs/docs/concepts/features/audit-trail.md b/docs/docs/concepts/features/audit-trail.md
index a4c79e003e..355029a6a4 100644
--- a/docs/docs/concepts/features/audit-trail.md
+++ b/docs/docs/concepts/features/audit-trail.md
@@ -14,6 +14,15 @@ This form of audit log has several benefits over storing classic audit logs.
You can view past data in-context of the whole system at a single point in time.
Reviewing a past state of the application can be important when tracing an incident that happened months back. Moreover the eventstore provides a truly complete and clean audit log.
+:::info Future Plans
+There will be three major areas for future development on the audit data
+
+- [Metrics](https://github.com/zitadel/zitadel/issues/4458) and [standard reports](https://github.com/zitadel/zitadel/discussions/2162#discussioncomment-1153259)
+- [Feedback loop](https://github.com/zitadel/zitadel/issues/5102) and threat detection
+- Forensics and replay of events
+
+:::
+
## Accessing the Audit Log
### Last changes of an object
@@ -42,24 +51,6 @@ Access to the API is possible with a [Service User](/docs/guides/integrate/servi
## Using logs in external systems
-You can use the [Event API](#event-api) to pull data and ingest it in an external system.
+You can use the events from the audit log in external systems such as a SOC/SIEM solution.
-[Actions](actions.md) can be used to write events to the stdout and [process the events as logs](../../self-hosting/manage/production#logging).
-Please refer to the zitadel/actions repository for a [code sample](https://github.com/zitadel/actions/blob/main/examples/post_auth_log.js).
-You can use your log processing pipeline to parse and ingest the events in your favorite analytics tool.
-
-It is possible to send events directly with an http request to an external tool.
-We don't recommend this approach since this would create back-pressure and increase the overall processing time for requests.
-
-:::info Scope of Actions
-At this moment Actions can be invoked on certain events, but not generally on every event.
-This is not a technical limitation, but a [feature on our backlog](https://github.com/zitadel/zitadel/issues/5101).
-:::
-
-## Future plans
-
-There will be three major areas for future development on the audit data
-
-- [Metrics](https://github.com/zitadel/zitadel/issues/4458) and [standard reports](https://github.com/zitadel/zitadel/discussions/2162#discussioncomment-1153259)
-- [Feedback loop](https://github.com/zitadel/zitadel/issues/5102) and threat detection
-- Forensics and replay of events
+Follow our guide on how to [integrate ZITADEL with external systems for streaming events and audit logs](/docs/guides/integrate/external-audit-log).
diff --git a/docs/docs/examples/login/symfony.md b/docs/docs/examples/login/symfony.md
index 3a5bfa3ac9..86a793b02f 100644
--- a/docs/docs/examples/login/symfony.md
+++ b/docs/docs/examples/login/symfony.md
@@ -102,7 +102,7 @@ composer require drenso/symfony-oidc-bundle
First, we need to create a User class for the database, so we can persist user info between requests. In this case you don't need password authentication.
Email addresses are not unique for ZITADEL users. There can be multiple user accounts with the same email address.
-See [User Constraints](https://zitadel.com/docs/concepts/structure/users#constraints) for more details.
+See [User Constraints](/docs/concepts/structure/users#constraints) for more details.
We will use the User Info `sub` claim as unique "display" name for the user. `sub` equals the unique User ID from ZITADEL.
This creates a User Repository and Entity that implements the `UserInterface`:
diff --git a/docs/docs/examples/secure-api/nodejs-nestjs.md b/docs/docs/examples/secure-api/nodejs-nestjs.md
index 13c2191140..54da814225 100644
--- a/docs/docs/examples/secure-api/nodejs-nestjs.md
+++ b/docs/docs/examples/secure-api/nodejs-nestjs.md
@@ -9,9 +9,9 @@ This documentation section guides you through the process of integrating ZITADEL
## Overview
-The NestJS API includes a single secured route that prints "Hello World!" when authenticated. The API expects an authorization header with a valid JWT, serving as a bearer token to authenticate the user when calling the API. The API will validate the access token on the [introspect endpoint](https://zitadel.com/docs/apis/openidoauth/endpoints#introspection_endpoint) and receive the user from ZITADEL.
+The NestJS API includes a single secured route that prints "Hello World!" when authenticated. The API expects an authorization header with a valid JWT, serving as a bearer token to authenticate the user when calling the API. The API will validate the access token on the [introspect endpoint](/docs/apis/openidoauth/endpoints#introspection_endpoint) and receive the user from ZITADEL.
-The API application utilizes [JWT with Private Key](https://zitadel.com/docs/apis/openidoauth/authn-methods#jwt-with-private-key) for authentication against ZITADEL and accessing the introspection endpoint. Make sure to create an API Application within Zitadel and download the JSON. In this instance, we use this service account, so make sure to provide the secrets in the example application via environmental variables.
+The API application utilizes [JWT with Private Key](/docs/apis/openidoauth/authn-methods#jwt-with-private-key) for authentication against ZITADEL and accessing the introspection endpoint. Make sure to create an API Application within Zitadel and download the JSON. In this instance, we use this service account, so make sure to provide the secrets in the example application via environmental variables.
## Overview
@@ -25,7 +25,7 @@ Make sure you have Node.js and npm installed on your machine.
### ZITADEL Configuration for the API
-1. Create a ZITADEL instance and a project by following the steps [here](https://zitadel.com/docs/guides/start/quickstart#2-create-your-first-instance).
+1. Create a ZITADEL instance and a project by following the steps [here](/docs/guides/start/quickstart#2-create-your-first-instance).
2. Set up an API application within your project:
- Create a new application of type "API" with authentication method "Private Key".
diff --git a/docs/docs/examples/secure-api/python-django.mdx b/docs/docs/examples/secure-api/python-django.mdx
index 215f3a8e7a..f571c30f57 100644
--- a/docs/docs/examples/secure-api/python-django.mdx
+++ b/docs/docs/examples/secure-api/python-django.mdx
@@ -145,7 +145,7 @@ python manage.py runserver
### Call the API
To call the API you need an access token, which is then verified by ZITADEL.
-Please follow [this guide here](https://zitadel.com/docs/guides/integrate/private-key-jwt#get-an-access-token), ignoring the first step as we already have the `.json`-key-file from the serviceaccount.
+Please follow [this guide here](/docs/guides/integrate/token-introspection/private-key-jwt#get-an-access-token), ignoring the first step as we already have the `.json`-key-file from the serviceaccount.
Optionally set the token as an environment variable:
```
diff --git a/docs/docs/examples/secure-api/python-flask.mdx b/docs/docs/examples/secure-api/python-flask.mdx
index 1263c945e9..798dfe5f1e 100644
--- a/docs/docs/examples/secure-api/python-flask.mdx
+++ b/docs/docs/examples/secure-api/python-flask.mdx
@@ -12,11 +12,11 @@ This example shows you how to secure a Python3 Flask API with both authenticatio
The Python API will have public, private, and private-scoped routes and check if a user is authenticated and authorized to access the routes.
The private routes expect an authorization header with a valid access token in the request. The access token is used as a bearer token to authenticate the user when calling the API.
-The API will validate the access token on the [introspect endpoint](https://zitadel.com/docs/apis/openidoauth/endpoints#introspection_endpoint) and will receive the user's roles from ZITADEL.
+The API will validate the access token on the [introspect endpoint](/docs/apis/openidoauth/endpoints#introspection_endpoint) and will receive the user's roles from ZITADEL.
-The API application uses [Client Secret Basic](https://zitadel.com/docs/apis/openidoauth/authn-methods#client-secret-basic) to authenticate against ZITADEL and access the introspection endpoint.
+The API application uses [Client Secret Basic](/docs/apis/openidoauth/authn-methods#client-secret-basic) to authenticate against ZITADEL and access the introspection endpoint.
You can use any valid access_token from a user or service account to send requests to the example API.
-In this example we will use a service account with a [personal access token](https://zitadel.com/docs/guides/integrate/service-users/personal-access-token) which can be used directly to access the example API.
+In this example we will use a service account with a [personal access token](/docs/guides/integrate/service-users/personal-access-token) which can be used directly to access the example API.
## Running the example
@@ -31,9 +31,9 @@ In order to run the example you need to have `python3` and `pip3` installed.
You need to setup a couple of things in ZITADEL.
-1. If you don't have an instance yet, please go ahead and create an instance as explained [here](https://zitadel.com/docs/guides/start/quickstart#2-create-your-first-instance). Also, create a new project by following the steps [here](https://zitadel.com/docs/guides/start/quickstart#2-create-your-first-instance).
+1. If you don't have an instance yet, please go ahead and create an instance as explained [here](/docs/guides/start/quickstart#2-create-your-first-instance). Also, create a new project by following the steps [here](/docs/guides/start/quickstart#2-create-your-first-instance).
-2. You must create an API application in your project. Follow [this guide](https://zitadel.com/docs/guides/manage/console/applications) to create a new application of type "API" with authentication method "Basic". Save both the ClientID and ClientSecret after you create the application.
+2. You must create an API application in your project. Follow [this guide](/docs/guides/manage/console/applications) to create a new application of type "API" with authentication method "Basic". Save both the ClientID and ClientSecret after you create the application.
### Create the API
@@ -179,7 +179,7 @@ class ZitadelIntrospectTokenValidator(IntrospectTokenValidator):
res = self.introspect_token(*args, **kwargs)
return res
```
-3. Create a new file named ".env" in the directory. Copy the configuration in the [".env.example"](https://github.com/zitadel/example-api-python3-flask/blob/main/.env.example) file to the newly created .env file. Set the values with your Instance Domain/Issuer URL, Client ID, and Client Secret from the previous steps. Obtain your Issuer URL by following [these steps](https://zitadel.com/docs/guides/start/quickstart#referred1).
+3. Create a new file named ".env" in the directory. Copy the configuration in the [".env.example"](https://github.com/zitadel/example-api-python3-flask/blob/main/.env.example) file to the newly created .env file. Set the values with your Instance Domain/Issuer URL, Client ID, and Client Secret from the previous steps. Obtain your Issuer URL by following [these steps](/docs/guides/start/quickstart#referred1).
```python
ZITADEL_DOMAIN = "https://your-domain-abcdef.zitadel.cloud"
@@ -191,9 +191,9 @@ CLIENT_SECRET = "NVAp70IqiGmJldbS...."
![Create a service user](/img/python-flask/3.png)
-1. Create a service user and a Personal Access Token (PAT) for that user by following [this guide](https://zitadel.com/docs/guides/integrate/service-users/personal-access-token#create-a-service-user-with-a-pat).
-2. To enable authorization, follow [this guide](https://zitadel.com/docs/guides/manage/console/roles) to create a role `read:messages` on your project.
-3. Next, create an authorization for the service user you created by adding the role `read:messages` to the user. Follow this [guide](https://zitadel.com/docs/guides/manage/console/roles#authorizations) for more information on creating an authorization.
+1. Create a service user and a Personal Access Token (PAT) for that user by following [this guide](/docs/guides/integrate/service-users/personal-access-token#create-a-service-user-with-a-pat).
+2. To enable authorization, follow [this guide](/docs/guides/manage/console/roles) to create a role `read:messages` on your project.
+3. Next, create an authorization for the service user you created by adding the role `read:messages` to the user. Follow this [guide](/docs/guides/manage/console/roles#authorizations) for more information on creating an authorization.
### Run the API
diff --git a/docs/docs/guides/integrate/identity-providers/introduction.md b/docs/docs/guides/integrate/identity-providers/introduction.md
index 4ecea3424a..b97b50b6db 100644
--- a/docs/docs/guides/integrate/identity-providers/introduction.md
+++ b/docs/docs/guides/integrate/identity-providers/introduction.md
@@ -131,9 +131,9 @@ In the guides below, some of which utilize the Generic OIDC or SAML templates fo
If ZITADEL doesn't offer a specific template for your Identity Provider (IdP) and your IdP is fully compliant with OpenID Connect (OIDC), you have the option to use the generic OIDC provider configuration.
-For those utilizing a SAML Service Provider, the SAML Service Provider option is available. You can learn how to set up a SAML Service Provider with our [MockSAML example](https://zitadel.com/docs/guides/integrate/identity-providers/mocksaml).
+For those utilizing a SAML Service Provider, the SAML Service Provider option is available. You can learn how to set up a SAML Service Provider with our [MockSAML example](/docs/guides/integrate/identity-providers/mocksaml).
-Should you wish to transition from a generic OIDC provider to Entra ID (formerly Azure Active Directory) or Google, consider following this [guide](https://zitadel.com/docs/guides/integrate/identity-providers/migrate).
+Should you wish to transition from a generic OIDC provider to Entra ID (formerly Azure Active Directory) or Google, consider following this [guide](/docs/guides/integrate/identity-providers/migrate).
@@ -176,6 +176,6 @@ Deciding whether to configure an external Identity Provider (IdP) at the organiz
## References
-- [Identity brokering in ZITADEL](https://zitadel.com/docs/concepts/features/identity-brokering)
-- [The ZITADEL API reference for managing external IdPs](https://zitadel.com/docs/category/apis/resources/admin/identity-providers)
-- [Handle external logins in a custom login UI](https://zitadel.com/docs/guides/integrate/login-ui/external-login)
\ No newline at end of file
+- [Identity brokering in ZITADEL](/docs/concepts/features/identity-brokering)
+- [The ZITADEL API reference for managing external IdPs](/docs/category/apis/resources/admin/identity-providers)
+- [Handle external logins in a custom login UI](/docs/guides/integrate/login-ui/external-login)
\ No newline at end of file
diff --git a/docs/docs/guides/integrate/login-ui/_logout.mdx b/docs/docs/guides/integrate/login-ui/_logout.mdx
index 6cb2a757a9..33bc3457f6 100644
--- a/docs/docs/guides/integrate/login-ui/_logout.mdx
+++ b/docs/docs/guides/integrate/login-ui/_logout.mdx
@@ -1,5 +1,5 @@
When your user is done using your application and clicks on the logout button, you have to send a request to the terminate session endpoint.
-[Terminate Session Documentation](https://zitadel.com/docs/apis/resources/session_service/session-service-delete-session)
+[Terminate Session Documentation](/docs/apis/resources/session_service/session-service-delete-session)
Sessions can be terminated by either:
- the authenticated user
diff --git a/docs/docs/guides/integrate/login-ui/_select-account.mdx b/docs/docs/guides/integrate/login-ui/_select-account.mdx
index b16cce281c..97528186e6 100644
--- a/docs/docs/guides/integrate/login-ui/_select-account.mdx
+++ b/docs/docs/guides/integrate/login-ui/_select-account.mdx
@@ -3,7 +3,7 @@ If you want to build your own select account/account picker, you have to cache t
We recommend storing a list of the session Ids with the corresponding session token in a cookie.
The list of session IDs can be sent in the “search sessions” request to get a detailed list of sessions for the account selection.
-[Search Sessions Documentation](https://zitadel.com/docs/apis/resources/session_service/session-service-list-sessions)
+[Search Sessions Documentation](/docs/apis/resources/session_service/session-service-list-sessions)
### Request
diff --git a/docs/docs/guides/integrate/onboarding/end-users.mdx b/docs/docs/guides/integrate/onboarding/end-users.mdx
index 3c8038c5ca..274c2f7eaa 100644
--- a/docs/docs/guides/integrate/onboarding/end-users.mdx
+++ b/docs/docs/guides/integrate/onboarding/end-users.mdx
@@ -115,7 +115,7 @@ We do have a guide series on how to build your own login ui, which also includes
- Passkeys
- External Login Providers
-You can find all the guides here: [Build your own login UI](https://zitadel.com/docs/guides/integrate/login-ui)
+You can find all the guides here: [Build your own login UI](/docs/guides/integrate/login-ui)
The create user request also allows you to add metadata (key, value) to the user.
This gives you the possibility to collect additional data from your users during the registration process and store it directly to the user in ZITADEL.
diff --git a/docs/docs/guides/integrate/service-users/client-credentials.md b/docs/docs/guides/integrate/service-users/client-credentials.md
index 8d2caf6e83..7924f2b6ae 100644
--- a/docs/docs/guides/integrate/service-users/client-credentials.md
+++ b/docs/docs/guides/integrate/service-users/client-credentials.md
@@ -26,7 +26,7 @@ If you lose it, you will have to generate a new one.
![Create new service user](/img/console_serviceusers_secret.gif)
-## 2. Authenticating a service user and request a token
+### 2. Authenticating a service user and request a token
In this step, we will authenticate a service user and receive an access_token to use against the ZITADEL API.
@@ -36,13 +36,15 @@ You will need to craft a POST request to ZITADEL's token endpoint:
curl --request POST \
--url https://$CUSTOM-DOMAIN/oauth/v2/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
- --header 'Authorization: Basic ${BASIC_AUTH}' \
--data grant_type=client_credentials \
- --data scope='openid profile'
+ --data scope='openid profile' \
+ --user "$CLIENT_ID:$CLIENT_SECRET"
```
+* `CUSTOM_DOMAIN` should be set to your [custom domain](/docs/concepts/features/custom-domain)
* `grant_type` should be set to `client_credentials`
* `scope` should contain any [Scopes](/apis/openidoauth/scopes) you want to include, but must include `openid`. For this example, please include `profile`
+* `CLIENT_ID` and `CLIENT_SECRET` should be set with the values shown in Console when generating a new secret to enable [basic authentication](/docs/apis/openidoauth/authn-methods)
If you want to access ZITADEL APIs, make sure to include the required scopes `urn:zitadel:iam:org:project:id:zitadel:aud`.
Read our guide [how to access ZITADEL APIs](../zitadel-apis/access-zitadel-apis) to learn more.
diff --git a/docs/docs/guides/integrate/token-introspection/basic-auth.mdx b/docs/docs/guides/integrate/token-introspection/basic-auth.mdx
index cad0f55c75..6beb244ed0 100644
--- a/docs/docs/guides/integrate/token-introspection/basic-auth.mdx
+++ b/docs/docs/guides/integrate/token-introspection/basic-auth.mdx
@@ -5,7 +5,7 @@ sidebar_label: Basic Authentication
import IntrospectionResponse from './_introspection-response.mdx';
-This is a guide on how to secure your API using [Basic Authentication](https://zitadel.com/docs/apis/openidoauth/authn-methods#client-secret-basic).
+This is a guide on how to secure your API using [Basic Authentication](/docs/apis/openidoauth/authn-methods#client-secret-basic).
## Register the API in ZITADEL
diff --git a/docs/docs/guides/integrate/zitadel-apis/event-api.md b/docs/docs/guides/integrate/zitadel-apis/event-api.md
index e4899421f1..fcc5649ccc 100644
--- a/docs/docs/guides/integrate/zitadel-apis/event-api.md
+++ b/docs/docs/guides/integrate/zitadel-apis/event-api.md
@@ -7,7 +7,7 @@ ZITADEL leverages the power of eventsourcing, meaning every action and change wi
To provide you with greater flexibility and access to these events, ZITADEL has introduced an Event API.
This API allows you to easily retrieve and utilize the events generated within the system, enabling you to integrate them into your own system and respond to specific events as they occur.
-You need to give a user the [manager role](https://zitadel.com/docs/guides/manage/console/managers) IAM_OWNER_VIEWER or IAM_OWNER to access the Event API.
+You need to give a user the [manager role](/docs/guides/manage/console/managers) IAM_OWNER_VIEWER or IAM_OWNER to access the Event API.
If you like to know more about eventsourcing/eventstore and how this works in ZITADEL, head over to our [concepts](/docs/concepts/eventstore/overview).
## Request Events
diff --git a/docs/docs/guides/manage/cloud/settings.md b/docs/docs/guides/manage/cloud/settings.md
index f4c4543ec7..5b646db3e1 100644
--- a/docs/docs/guides/manage/cloud/settings.md
+++ b/docs/docs/guides/manage/cloud/settings.md
@@ -19,7 +19,7 @@ You can subscribe and unsubscribe to notifications and newsletters:
- Security: Receive notifications related to security issues
:::info Technical Advisories
-If you want to stay up to date on our technical advisories, we recommend [subscribing here to the mailing list](https://zitadel.com/docs/support/technical_advisory#subscribe-to-our-mailing-list).
+If you want to stay up to date on our technical advisories, we recommend [subscribing here to the mailing list](/docs/support/technical_advisory#subscribe-to-our-mailing-list).
Technical advisories are notices that report major issues with ZITADEL Self-Hosted or the ZITADEL Cloud platform that could potentially impact security or stability in production environments.
:::
diff --git a/docs/docs/guides/migrate/sources/keycloak.md b/docs/docs/guides/migrate/sources/keycloak.md
index b432b62163..ccb8c6e704 100644
--- a/docs/docs/guides/migrate/sources/keycloak.md
+++ b/docs/docs/guides/migrate/sources/keycloak.md
@@ -5,7 +5,7 @@ sidebar_label: From Keycloak
## Migrating from Keycloak to ZITADEL
-This guide will use [Docker installation](https://www.docker.com/) to run Keycloak and ZITADEL. However, both Keycloak and ZITADEL offer different installation methods. As a result, this guide won't include any required production tuning or security hardening for either system. However, it's advised you follow [recommended guidelines](https://zitadel.com/docs/guides/manage/self-hosted/production) before putting those systems into production. You can skip setting up Keycloak and ZITADEL if you already have running instances.
+This guide will use [Docker installation](https://www.docker.com/) to run Keycloak and ZITADEL. However, both Keycloak and ZITADEL offer different installation methods. As a result, this guide won't include any required production tuning or security hardening for either system. However, it's advised you follow [recommended guidelines](/docs/self-hosting/manage/production) before putting those systems into production. You can skip setting up Keycloak and ZITADEL if you already have running instances.
## Set up Keycloak
### Run Keycloak
@@ -77,7 +77,7 @@ docker cp :/tmp/my-realm-users-0.json .
## Set up ZITADEL
-After creating a sample application that connects to Keycloak, you need to set up ZITADEL in order to migrate the application and users from Keycloak to ZITADEL. For this, ZITADEL offers a [Docker Compose](https://zitadel.com/docs/self-hosting/deploy/compose) installation guide. Follow the instructions under the [Docker compose](https://zitadel.com/docs/self-hosting/deploy/compose#docker-compose) section to run a ZITADEL instance locally.
+After creating a sample application that connects to Keycloak, you need to set up ZITADEL in order to migrate the application and users from Keycloak to ZITADEL. For this, ZITADEL offers a [Docker Compose](/docs/self-hosting/deploy/compose) installation guide. Follow the instructions under the [Docker compose](/docs/self-hosting/deploy/compose#docker-compose) section to run a ZITADEL instance locally.
Next, the application will be available at [http://localhost:8080/ui/console/](http://localhost:8080/ui/console/).
@@ -91,13 +91,13 @@ Now you can access the console with the following default credentials:
## Import Keycloak users into ZITADEL
-As explained in this [ZITADEL user migration guide](https://zitadel.com/docs/guides/migrate/users), you can import users individually or in bulk. Since we are looking at importing a single user from Keycloak, migrating that individual user to ZITADEL can be done with the [ImportHumanUser](https://zitadel.com/docs/apis/resources/mgmt/management-service-import-human-user) endpoint.
+As explained in this [ZITADEL user migration guide](/docs/guides/migrate/users), you can import users individually or in bulk. Since we are looking at importing a single user from Keycloak, migrating that individual user to ZITADEL can be done with the [ImportHumanUser](/docs/apis/resources/mgmt/management-service-import-human-user) endpoint.
> With this endpoint, an email will only be sent to the user if the email is marked as not verified or if there's no password set.
### Create a service user to consume ZITADEL API
-But first of all, in order to use this ZITADEL API, you need to create a [service user](https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users#exercise-create-a-service-user).
+But first of all, in order to use this ZITADEL API, you need to create a [service user](/docs/guides/integrate/service-users/authenticate-service-users#exercise-create-a-service-user).
Go to the **Users** menu and select the **Service Users** tab. And click the **+ New** button.
@@ -167,7 +167,7 @@ if your Keycloak Realm has a single user, your `my-realm-users-0.json` file, int
}
```
-Now, you need to transform the JSON to the ZITADEL data format by adhering to the ZITADEL API [specification](https://zitadel.com/docs/apis/resources/mgmt/management-service-import-human-user) to import a user. The minimal format would be as shown below:
+Now, you need to transform the JSON to the ZITADEL data format by adhering to the ZITADEL API [specification](/docs/apis/resources/mgmt/management-service-import-human-user) to import a user. The minimal format would be as shown below:
```js
{
diff --git a/docs/docs/guides/migrate/users.md b/docs/docs/guides/migrate/users.md
index bf8a2cb992..5eb8ceb694 100644
--- a/docs/docs/guides/migrate/users.md
+++ b/docs/docs/guides/migrate/users.md
@@ -42,7 +42,7 @@ Please also consult our [guide](/docs/guides/manage/user/reg-create-user) on how
## Bulk import
-For bulk import use the [import endpoint](https://zitadel.com/docs/apis/resources/admin/admin-service-import-data) on the admin API:
+For bulk import use the [import endpoint](/docs/apis/resources/admin/admin-service-import-data) on the admin API:
```json
{
@@ -191,7 +191,7 @@ Currently it is not possible to migrate passkeys directly from another system.
## Users linked to an external IDP
-A users `sub` is bound to the external [IDP's Client ID](https://zitadel.com/docs/guides/manage/console/default-settings#identity-providers).
+A users `sub` is bound to the external [IDP's Client ID](/docs/guides/manage/console/default-settings#identity-providers).
This means that the IDP Client ID configured in ZITADEL must be the same ID as in the legacy system.
Users should be imported with their `externalUserId`.
@@ -211,7 +211,7 @@ _snippet from [bulk-import](#bulk-import) example:_
}
```
-You can use an Action with [post-creation flow](https://zitadel.com/docs/apis/actions/external-authentication#post-creation) to pull information such as roles from the old system and apply them to the user in ZITADEL.
+You can use an Action with [post-creation flow](/docs/apis/actions/external-authentication#post-creation) to pull information such as roles from the old system and apply them to the user in ZITADEL.
## Metadata
@@ -220,7 +220,7 @@ Use metadata to store additional attributes of the users, such as organizational
:::info
Metadata must be added to users after the users were created. Currently metadata can't be added during user creation.
-[API reference: User Metadata](https://zitadel.com/docs/category/apis/resources/mgmt/user-metadata)
+[API reference: User Metadata](/docs/category/apis/resources/mgmt/user-metadata)
:::
Request metadata from the userinfo endpoint by passing the required [reserved scope](/docs/apis/openidoauth/scopes#reserved-scopes) in your auth request.
@@ -232,5 +232,5 @@ You can assign roles from owned or granted projects to a user.
:::info
Authorizations must be added to users after the users were created. Currently metadata can't be added during user creation.
-[API reference: User Authorization / Grants](https://zitadel.com/docs/category/apis/resources/auth/user-authorizations-grants)
+[API reference: User Authorization / Grants](/docs/category/apis/resources/auth/user-authorizations-grants)
:::
\ No newline at end of file
diff --git a/docs/docs/guides/overview.mdx b/docs/docs/guides/overview.mdx
index 3a42374e82..d01a4a6903 100644
--- a/docs/docs/guides/overview.mdx
+++ b/docs/docs/guides/overview.mdx
@@ -39,6 +39,10 @@ Join our [Discord chat](https://zitadel.com/chat) or open a [discussion](https:/
Cloud and enterprise customers can additionally reach us privately via our [support communication channels](/legal/service-description/support-services).
+## Office Hours
+
+ZITADEL holds bi-weekly community calls. To join the community calls use [this link](https://zitadel.com/office-hours), or find further information [here](https://github.com/zitadel/zitadel/blob/main/MEETING_SCHEDULE.md).
+
## Contribute
ZITADEL is open source — and so is the documentation.
diff --git a/docs/docs/guides/start/quickstart.mdx b/docs/docs/guides/start/quickstart.mdx
index e76c98bd05..99f193ddd5 100644
--- a/docs/docs/guides/start/quickstart.mdx
+++ b/docs/docs/guides/start/quickstart.mdx
@@ -353,7 +353,7 @@ The provided config extends the `UserManagerSettings` of the `oidc-client-ts` li
- redirect_uri (the URL to redirect to after the authorization flow is complete)
- post_logout_redirect_uri (the URL to redirect to after the user logs out)
- scope (the permissions requested from the user)
-- project_resource_id (To add a ZITADEL project scope. `urn:zitadel:iam:org:project:id:[projectId]:aud` and `urn:zitadel:iam:org:projects:roles` [scopes](https://zitadel.com/docs/apis/openidoauth/scopes#reserved-scopes).)
+- project_resource_id (To add a ZITADEL project scope. `urn:zitadel:iam:org:project:id:[projectId]:aud` and `urn:zitadel:iam:org:projects:roles` [scopes](/docs/apis/openidoauth/scopes#reserved-scopes).)
- prompt ([the OIDC prompt parameter](/apis/openidoauth/endpoints#additional-parameters))
2. Create a folder named components in the src directory. Create two files named Login.js and Callback.js.
@@ -412,4 +412,4 @@ And this brings us to the end of this quick start guide!
This tutorial covered how to configure ZITADEL and how to use React to build an app that communicates with ZITADEL to access secured resources.
-We hope you enjoyed the tutorial and encourage you to check out the ZITADEL [documentation](https://zitadel.com/docs) for more information on how to use the ZITADEL platform to its full potential. Thanks for joining us!
+We hope you enjoyed the tutorial and encourage you to check out the ZITADEL [documentation](/docs) for more information on how to use the ZITADEL platform to its full potential. Thanks for joining us!
diff --git a/docs/docs/legal/policies/brand-trademark-policy.md b/docs/docs/legal/policies/brand-trademark-policy.md
index b6b1c6aaff..8016760c58 100644
--- a/docs/docs/legal/policies/brand-trademark-policy.md
+++ b/docs/docs/legal/policies/brand-trademark-policy.md
@@ -34,7 +34,7 @@ To ensure the logo is used as intended, we provide specific examples below and r
- Use in architecture diagrams without implying affiliation or partnership
- Editorial and informational purposes such as blog posts or news articles
-- Linking back to our [website](https://zitadel.com), official [repositories](https://github.com/zitadel), or [documentation](https://zitadel.com/docs)
+- Linking back to our [website](https://zitadel.com), official [repositories](https://github.com/zitadel), or [documentation](/docs)
- Indicating that the software is available for use or installation without implying any affiliation or endorsement
### Not acceptable
diff --git a/docs/docs/legal/policies/rate-limit-policy.md b/docs/docs/legal/policies/rate-limit-policy.md
index 378bab28e4..6075b7e6ab 100644
--- a/docs/docs/legal/policies/rate-limit-policy.md
+++ b/docs/docs/legal/policies/rate-limit-policy.md
@@ -3,7 +3,7 @@ title: Rate Limit Policy
custom_edit_url: null
---
-Last updated on April 20, 2023
+Last updated on April 24, 2024
This policy is an annex to the [Terms of Service](../terms-of-service) and clarifies your obligations while using our Services, specifically how we will use rate limiting to enforce certain aspects of our [Acceptable Use Policy](acceptable-use-policy).
@@ -35,10 +35,12 @@ For ZITADEL Cloud, we have a rate limiting rule for login paths (login, register
Rate limits are implemented with the following rules:
-| Path | Description | Rate Limiting | One Minute Banning |
-|--------------------------|----------------------------------------|--------------------------------------|----------------------------------------|
-| /ui/login* | Global Login, Register and Reset Limit | 10 requests per second over a minute | 15 requests per second over 3 minutes |
-| All other paths | All gRPC- and REST APIs as well as the ZITADEL Customer Portal | 10 requests per second over a minute | 10 requests per second over 3 minutes |
+| Path | Description | Rate Limiting | One Minute Banning |
+| -------------------- | -------------------------------------------------------------- | ------------------------------------ | ------------------------------------- |
+| /ui/login\* | Global Login, Register and Reset Limit | 10 requests per second over a minute | 15 requests per second over 3 minutes |
+| /oauth/v2/keys | OAuth/OpenID Public Keys Endpoint | 20 requests per second over a minute | 15 requests per second over 3 minutes |
+| /oauth/v2/introspect | OAuth Introspection Endpoint | 20 requests per second over a minute | 15 requests per second over 3 minutes |
+| All other paths | All gRPC- and REST APIs as well as the ZITADEL Customer Portal | 10 requests per second over a minute | 10 requests per second over 3 minutes |
## Load Testing
diff --git a/docs/docs/legal/policies/vulnerability-disclosure-policy.mdx b/docs/docs/legal/policies/vulnerability-disclosure-policy.mdx
index 1566655243..df8245d72e 100644
--- a/docs/docs/legal/policies/vulnerability-disclosure-policy.mdx
+++ b/docs/docs/legal/policies/vulnerability-disclosure-policy.mdx
@@ -57,7 +57,7 @@ We will not publish this information by default to protect your privacy.
### What not to report
- Disclosure of known public files or directories, e.g. robots.txt, files under .well-known, or files that are included in our public repositories (eg, go.mod)
-- DoS of users when [Lockout Policy is enabled](https://zitadel.com/docs/guides/manage/console/default-settings#lockout)
+- DoS of users when [Lockout Policy is enabled](/docs/guides/manage/console/default-settings#lockout)
- Suggestions on Certificate Authority Authorization (CAA) rules
- Suggestions on DMARC/DKIM/SPF settings
- Suggestions on DNSSEC settings
diff --git a/docs/docs/self-hosting/deploy/troubleshooting/_instance_not_found.mdx b/docs/docs/self-hosting/deploy/troubleshooting/_instance_not_found.mdx
index 98785275b9..76b8e7b2f6 100644
--- a/docs/docs/self-hosting/deploy/troubleshooting/_instance_not_found.mdx
+++ b/docs/docs/self-hosting/deploy/troubleshooting/_instance_not_found.mdx
@@ -1,5 +1,5 @@
`ID=QUERY-n0wng Message=Instance not found`
If you're self hosting with a custom domain, you need to instruct ZITADEL to use the `ExternalDomain`.
-You can find further instructions in our guide about [custom domains](https://zitadel.com/docs/self-hosting/manage/custom-domain).
-We also provide a guide on how to [configure](https://zitadel.com/docs/self-hosting/manage/configure) ZITADEL with variables from files or environment variables.
+You can find further instructions in our guide about [custom domains](/docs/self-hosting/manage/custom-domain).
+We also provide a guide on how to [configure](/docs/self-hosting/manage/configure) ZITADEL with variables from files or environment variables.
diff --git a/docs/docs/self-hosting/manage/cli/mirror.mdx b/docs/docs/self-hosting/manage/cli/mirror.mdx
new file mode 100644
index 0000000000..8f96ed93df
--- /dev/null
+++ b/docs/docs/self-hosting/manage/cli/mirror.mdx
@@ -0,0 +1,232 @@
+---
+title: Mirror data to another database
+sidebar_label: Mirror command
+---
+
+The `mirror` command allows you to do database to database migrations. This functionality is useful to copy data from one database to another.
+
+The data can be mirrored to multiple database without influencing each other.
+
+## Use cases
+
+Migrate from cockroachdb to postgres or vice versa.
+
+Replicate data to a secondary environment for testing.
+
+## Prerequisites
+
+You need an existing source database, most probably the database ZITADEL currently serves traffic from.
+
+To mirror the data the destination database needs to be initialized and setup without an instance.
+
+### Start the destination database
+
+Follow one of the following guides to start the database:
+
+* [Linux](/docs/self-hosting/deploy/linux#run-postgresql)
+* [MacOS](/docs/self-hosting/deploy/macos#install-postgresql)
+
+Or use the following commands for [Docker Compose](/docs/self-hosting/deploy/compose)
+
+```bash
+# Download the docker compose example configuration.
+wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/docker-compose.yaml
+
+# Run the database and application containers.
+docker compose up db --detach
+```
+
+## Example
+
+The following commands setup the database as described above. See [configuration](#configuration) for more details about the configuration options.
+
+```bash
+zitadel init --config /path/to/your/new/config.yaml
+zitadel setup --for-init --config /path/to/your/new/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment
+zitadel mirror --system --config /path/to/your/mirror/config.yaml # make sure to set --tlsMode and masterkey analog to your current deployment
+```
+
+## Usage
+
+The general syntax for the mirror command is:
+
+```bash
+zitadel mirror [flags]
+
+Flags:
+ -h, --help help for mirror
+
+ --config stringArray path to config file to overwrite system defaults
+
+ --ignore-previous ignores previous migrations of the events table. This flag should be used if you manually dropped previously mirrored events.
+ --replace replaces all data of the following tables for the provided instances or all if the `--system`-flag is set:
+ * system.assets
+ * auth.auth_requests
+ * eventstore.unique_constraints
+ The should be provided if you want to execute the mirror command multiple times so that the static data are also mirrored to prevent inconsistent states.
+
+ --instance strings id or comma separated ids of the instance(s) to migrate. Either this or the `--system`-flag must be set. Make sure to always use the same flag if you execute the command multiple times.
+ --system migrates the whole system. Either this or the `--instance`-flag must be set. Make sure to always use the same flag if you execute the command multiple times.
+
+# For the flags below use the same configuration you also use in the current deployment
+
+ --masterkey string masterkey as argument for en/decryption keys
+ -m, --masterkeyFile string path to the masterkey for en/decryption keys
+ --masterkeyFromEnv read masterkey for en/decryption keys from environment variable (ZITADEL_MASTERKEY)
+ --tlsMode externalSecure start ZITADEL with (enabled), without (disabled) TLS or external component e.g. reverse proxy (external) terminating TLS, this flag will overwrite externalSecure and `tls.enabled` in configs files
+```
+
+## Configuration
+
+```yaml
+# The source database the data are copied from. Use either cockroach or postgres, by default cockroach is used
+Source:
+ cockroach:
+ Host: localhost # ZITADEL_SOURCE_COCKROACH_HOST
+ Port: 26257 # ZITADEL_SOURCE_COCKROACH_PORT
+ Database: zitadel # ZITADEL_SOURCE_COCKROACH_DATABASE
+ MaxOpenConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXOPENCONNS
+ MaxIdleConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXIDLECONNS
+ EventPushConnRatio: 0.33 # ZITADEL_SOURCE_COCKROACH_EVENTPUSHCONNRATIO
+ ProjectionSpoolerConnRatio: 0.33 # ZITADEL_SOURCE_COCKROACH_PROJECTIONSPOOLERCONNRATIO
+ MaxConnLifetime: 30m # ZITADEL_SOURCE_COCKROACH_MAXCONNLIFETIME
+ MaxConnIdleTime: 5m # ZITADEL_SOURCE_COCKROACH_MAXCONNIDLETIME
+ Options: "" # ZITADEL_SOURCE_COCKROACH_OPTIONS
+ User:
+ Username: zitadel # ZITADEL_SOURCE_COCKROACH_USER_USERNAME
+ Password: "" # ZITADEL_SOURCE_COCKROACH_USER_PASSWORD
+ SSL:
+ Mode: disable # ZITADEL_SOURCE_COCKROACH_USER_SSL_MODE
+ RootCert: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_ROOTCERT
+ Cert: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_CERT
+ Key: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_KEY
+ # Postgres is used as soon as a value is set
+ # The values describe the possible fields to set values
+ postgres:
+ Host: # ZITADEL_SOURCE_POSTGRES_HOST
+ Port: # ZITADEL_SOURCE_POSTGRES_PORT
+ Database: # ZITADEL_SOURCE_POSTGRES_DATABASE
+ MaxOpenConns: # ZITADEL_SOURCE_POSTGRES_MAXOPENCONNS
+ MaxIdleConns: # ZITADEL_SOURCE_POSTGRES_MAXIDLECONNS
+ MaxConnLifetime: # ZITADEL_SOURCE_POSTGRES_MAXCONNLIFETIME
+ MaxConnIdleTime: # ZITADEL_SOURCE_POSTGRES_MAXCONNIDLETIME
+ Options: # ZITADEL_SOURCE_POSTGRES_OPTIONS
+ User:
+ Username: # ZITADEL_SOURCE_POSTGRES_USER_USERNAME
+ Password: # ZITADEL_SOURCE_POSTGRES_USER_PASSWORD
+ SSL:
+ Mode: # ZITADEL_SOURCE_POSTGRES_USER_SSL_MODE
+ RootCert: # ZITADEL_SOURCE_POSTGRES_USER_SSL_ROOTCERT
+ Cert: # ZITADEL_SOURCE_POSTGRES_USER_SSL_CERT
+ Key: # ZITADEL_SOURCE_POSTGRES_USER_SSL_KEY
+
+# The destination database the data are copied to. Use either cockroach or postgres, by default cockroach is used
+Destination:
+ cockroach:
+ Host: localhost # ZITADEL_DESTINATION_COCKROACH_HOST
+ Port: 26257 # ZITADEL_DESTINATION_COCKROACH_PORT
+ Database: zitadel # ZITADEL_DESTINATION_COCKROACH_DATABASE
+ MaxOpenConns: 0 # ZITADEL_DESTINATION_COCKROACH_MAXOPENCONNS
+ MaxIdleConns: 0 # ZITADEL_DESTINATION_COCKROACH_MAXIDLECONNS
+ MaxConnLifetime: 30m # ZITADEL_DESTINATION_COCKROACH_MAXCONNLIFETIME
+ MaxConnIdleTime: 5m # ZITADEL_DESTINATION_COCKROACH_MAXCONNIDLETIME
+ EventPushConnRatio: 0.01 # ZITADEL_DESTINATION_COCKROACH_EVENTPUSHCONNRATIO
+ ProjectionSpoolerConnRatio: 0.5 # ZITADEL_DESTINATION_COCKROACH_PROJECTIONSPOOLERCONNRATIO
+ Options: "" # ZITADEL_DESTINATION_COCKROACH_OPTIONS
+ User:
+ Username: zitadel # ZITADEL_DESTINATION_COCKROACH_USER_USERNAME
+ Password: "" # ZITADEL_DESTINATION_COCKROACH_USER_PASSWORD
+ SSL:
+ Mode: disable # ZITADEL_DESTINATION_COCKROACH_USER_SSL_MODE
+ RootCert: "" # ZITADEL_DESTINATION_COCKROACH_USER_SSL_ROOTCERT
+ Cert: "" # ZITADEL_DESTINATION_COCKROACH_USER_SSL_CERT
+ Key: "" # ZITADEL_DESTINATION_COCKROACH_USER_SSL_KEY
+ # Postgres is used as soon as a value is set
+ # The values describe the possible fields to set values
+ postgres:
+ Host: # ZITADEL_DESTINATION_POSTGRES_HOST
+ Port: # ZITADEL_DESTINATION_POSTGRES_PORT
+ Database: # ZITADEL_DESTINATION_POSTGRES_DATABASE
+ MaxOpenConns: # ZITADEL_DESTINATION_POSTGRES_MAXOPENCONNS
+ MaxIdleConns: # ZITADEL_DESTINATION_POSTGRES_MAXIDLECONNS
+ MaxConnLifetime: # ZITADEL_DESTINATION_POSTGRES_MAXCONNLIFETIME
+ MaxConnIdleTime: # ZITADEL_DESTINATION_POSTGRES_MAXCONNIDLETIME
+ Options: # ZITADEL_DESTINATION_POSTGRES_OPTIONS
+ User:
+ Username: # ZITADEL_DESTINATION_POSTGRES_USER_USERNAME
+ Password: # ZITADEL_DESTINATION_POSTGRES_USER_PASSWORD
+ SSL:
+ Mode: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_MODE
+ RootCert: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_ROOTCERT
+ Cert: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_CERT
+ Key: # ZITADEL_DESTINATION_POSTGRES_USER_SSL_KEY
+
+# As cockroachdb first copies the data into memory this parameter is used to iterate through the events table and fetch only the given amount of events per iteration
+EventBulkSize: 10000 # ZITADEL_EVENTBULKSIZE
+
+Projections:
+ # Defines how many projections are allowed to run in parallel
+ ConcurrentInstances: 7 # ZITADEL_PROJECTIONS_CONCURRENTINSTANCES
+ # Limits the amount of events projected by each iteration
+ EventBulkLimit: 1000 # ZITADEL_PROJECTIONS_EVENTBULKLIMIT
+
+Auth:
+ Spooler:
+ # Limits the amount of events projected by each iteration
+ BulkLimit: 1000 #ZITADEL_AUTH_SPOOLER_BULKLIMIT
+
+Admin:
+ Spooler:
+ # Limits the amount of events projected by each iteration
+ BulkLimit: 10 #ZITADEL_ADMIN_SPOOLER_BULKLIMIT
+
+Log:
+ Level: info
+```
+
+## Sub commands
+
+The provided sub commands allow more fine grained execution of copying the data.
+
+The following commands are safe to execute multiple times by adding the `--replace`-flag which replaces the data not provided by the events in the destination database.
+
+### `zitadel mirror auth`
+
+Copies the auth requests to the destination database.
+
+### `zitadel mirror eventstore`
+
+Copies the events since the last migration and unique constraints to the destination database.
+
+### `zitadel mirror projections`
+
+Executes all projections in the destination database.
+
+It is NOOP if the projections are already up-to-date.
+
+### `zitadel mirror system`
+
+Copies encryption keys and assets to the destination database.
+
+### `zitadel mirror verify`
+
+Prints the amount of rows of the source and destination database and the diff. Positive numbers indicate more rows in the destination table that in the source, negative numbers the opposite.
+
+The following tables will likely have an unequal count:
+
+* **projections.current_states**: If your deployment was upgraded several times, the number of entries in the destination will be lower
+* **projections.locks**: If your deployment was upgraded several times, the number of entries in the destination will be lower
+* **projections.keys4\***: Only not expired keys are inserted, the number of entries in the destination will be lower
+* **projections.failed_events**: Should be lower or equal.
+* **auth.users2**: Was replaced with auth.users3, the number of entries in the destination will be 0
+* **auth.users3**: Is the replacement of auth.users2, the number of entries in the destination will be equal or higher
+
+## Limitations
+
+It is not possible to use files as source or destination. See github issue [here](https://github.com/zitadel/zitadel/issues/7966)
+
+Currently the encryption keys of the source database must be copied to the destination database. See github issue [here](https://github.com/zitadel/zitadel/issues/7964)
+
+It is not possible to change the domain of the ZITADEL deployment.
+
+Once you mirrored an instance using the `--instance` flag, you have to make sure you don't mirror other preexisting instances. This means for example, you cannot mirror a few instances and then pass the `--system` flag. You have to pass all remaining instances explicitly, once you used the `--instance` flag
diff --git a/docs/docs/self-hosting/manage/cli/overview.mdx b/docs/docs/self-hosting/manage/cli/overview.mdx
new file mode 100644
index 0000000000..31fd71f4d0
--- /dev/null
+++ b/docs/docs/self-hosting/manage/cli/overview.mdx
@@ -0,0 +1,34 @@
+---
+title: ZITADEL Command Line Interface
+sidebar_label: Overview
+---
+
+This documentation serves as your guide to interacting with Zitadel through the command line interface (CLI). The Zitadel CLI empowers you to manage various aspects of your Zitadel system efficiently from your terminal.
+
+This introductory section provides a brief overview of what the Zitadel CLI offers and who can benefit from using it.
+
+Let's dive in!
+
+## Download the CLI
+
+Download the CLI for [Linux](/docs/self-hosting/deploy/linux#install-zitadel) or [MacOS](/docs/self-hosting/deploy/macos#install-zitadel).
+
+## Quick start
+
+The easiest way to start ZITADEL is by following the [docker compose example](/docs/self-hosting/deploy/compose) which executes the commands for you.
+
+## Initialize the database
+
+The `zitadel init`-command sets up the zitadel database. The statements executed need a user with `ADMIN`-privilege. See [init phase](/docs/self-hosting/manage/updating_scaling#the-init-phase) for more information.
+
+## Setup ZITADEL
+
+The `zitadel setup`-command further sets up the database created using `zitadel init`. This command only requires the user created in the previous step. See [setup phase](/docs/self-hosting/manage/updating_scaling#the-setup-phase) for more information.
+
+## Start ZITADEL
+
+The `zitadel start`-command runs the ZITADEL server. See [runtime phase](/docs/self-hosting/manage/updating_scaling#the-runtime-phase) for more information.
+
+The `zitadel start-from-setup`-command first executes [the setup phase](#setup-zitadel) and afterwards runs the ZITADEL server.
+
+The `zitadel start-from-init`-command first executes [the init phase](#Initialize-the-database), afterwards [the setup phase](#setup-zitadel) and lastly runs the ZITADEL server.
diff --git a/docs/docs/self-hosting/manage/database/_cockroachdb.mdx b/docs/docs/self-hosting/manage/database/_cockroachdb.mdx
index 2836640669..edc3f139fd 100644
--- a/docs/docs/self-hosting/manage/database/_cockroachdb.mdx
+++ b/docs/docs/self-hosting/manage/database/_cockroachdb.mdx
@@ -2,6 +2,8 @@
The default database of ZITADEL is [CockroachDB](https://www.cockroachlabs.com). The SQL database provides a bunch of features like horizontal scalability, data regionality and many more.
+Currently versions >= 23.2 are supported.
+
The default configuration of the database looks like this:
```yaml
diff --git a/docs/docs/self-hosting/manage/productionchecklist.md b/docs/docs/self-hosting/manage/productionchecklist.md
index aaf52d0de8..fb85557a23 100644
--- a/docs/docs/self-hosting/manage/productionchecklist.md
+++ b/docs/docs/self-hosting/manage/productionchecklist.md
@@ -25,7 +25,7 @@ To apply best practices to your production setup we created a step by step check
- [ ] Secure database connections from outside your network and/or use an internal subnet for database connectivity
- [ ] High Availability for critical infrastructure components (depending on your setup)
- [ ] Loadbalancer
- - [ ] [Reverse Proxies](https://zitadel.com/docs/self-hosting/manage/reverseproxy/reverse_proxy)
+ - [ ] [Reverse Proxies](/docs/self-hosting/manage/reverseproxy/reverse_proxy)
- [ ] Web Application Firewall
#### Networking
@@ -41,7 +41,7 @@ To apply best practices to your production setup we created a step by step check
- [ ] Add [Custom Branding](/docs/guides/manage/customize/branding) if required
- [ ] Configure a valid [SMS Service](/docs/guides/manage/console/default-settings#sms) such as Twilio if needed
- [ ] Configure your privacy policy, terms of service and a help Link if needed
-- [ ] Keep your [masterkey](https://zitadel.com/docs/self-hosting/manage/configure) in a secure storage
+- [ ] Keep your [masterkey](/docs/self-hosting/manage/configure) in a secure storage
- [ ] Declare and apply zitadel configuration using the zitadel terraform [provider](https://github.com/zitadel/terraform-provider-zitadel)
### Security
diff --git a/docs/docs/support/advisory/a10003.md b/docs/docs/support/advisory/a10003.md
index 9be32fbd78..b132e5738d 100644
--- a/docs/docs/support/advisory/a10003.md
+++ b/docs/docs/support/advisory/a10003.md
@@ -21,7 +21,7 @@ If users are redirected to the Login-UI without any organizational context, they
:::note
If the registration (and also authentication) needs to occur on a specified organization, apps can already
-specify this by providing [an organization scope](https://zitadel.com/docs/apis/openidoauth/scopes#reserved-scopes).
+specify this by providing [an organization scope](/docs/apis/openidoauth/scopes#reserved-scopes).
:::
## Statement
@@ -37,7 +37,7 @@ There's no action needed on your side currently as existing instances are not af
Once this update has been released and deployed, newly created instances will always use the default organization and its settings as default context for the login.
-Already existing instances will still use the instance settings by default and can switch to the new default by ["Activating the 'LoginDefaultOrg' feature"](https://zitadel.com/docs/apis/resources/admin/admin-service-activate-feature-login-default-org) through the Admin API.
+Already existing instances will still use the instance settings by default and can switch to the new default by ["Activating the 'LoginDefaultOrg' feature"](/docs/apis/resources/admin/admin-service-activate-feature-login-default-org) through the Admin API.
**This change is irreversible!**
:::note
diff --git a/docs/docs/support/advisory/a10009.md b/docs/docs/support/advisory/a10009.md
new file mode 100644
index 0000000000..014700760a
--- /dev/null
+++ b/docs/docs/support/advisory/a10009.md
@@ -0,0 +1,27 @@
+---
+title: Technical Advisory 10009
+---
+
+## Date and Version
+
+Version: 2.53.0
+
+Date: 2024-05-28
+
+## Description
+
+There were rare cases where Cockroachdb got blocked during runtime of ZITADEL and returned `WRITE_TOO_OLD`-errors to ZITADEL. The root cause of the problem is described in [this github issue of the database](https://github.com/cockroachdb/cockroach/issues/77119). The workaround provided by the database is enabling the `enable_durable_locking_for_serializable`-flag as described [here](https://github.com/cockroachdb/cockroach/issues/75456#issuecomment-1936277716).
+
+Because enabling flags requires admin privileges the statement must be executed manually or by executing `zitadel init` command.
+
+## Statement
+
+Ensure lock distribution for `FOR UPDATE`-statements on Cockroachdb.
+
+## Mitigation
+
+Cockroachdb version >= 23.2.
+
+## Impact
+
+Adding additional raft queries to `FOR UPDATE`-statements can impact performance slightly but ensures availability of the system.
diff --git a/docs/docs/support/advisory/a10010.md b/docs/docs/support/advisory/a10010.md
new file mode 100644
index 0000000000..c2fd95902e
--- /dev/null
+++ b/docs/docs/support/advisory/a10010.md
@@ -0,0 +1,30 @@
+---
+title: Technical Advisory 10010
+---
+
+## Date and Version
+
+Version: 2.53.0
+
+Date: 2024-05-28
+
+## Description
+
+Version 2.53.0 optimizes the way tokens are created and migrates them to the v2 implementation already used by OAuth / OIDC tokens created through the session API.
+
+Because of this tokens events are no longer created on the user itself. To be as backwards compatible as possible a separate event is created on the user for the audit log.
+
+## Statement
+
+This change was tracked in the following PR:
+[perf(oidc): optimize token creation](https://github.com/zitadel/zitadel/pull/7822), which was released in Version [2.53.0](https://github.com/zitadel/zitadel/releases/tag/v2.53.0)
+
+## Mitigation
+
+If you use the ListEvents API to check the audit trail of a user or being able to compute Daily or Monthly Active Users, be sure to also include the `user.token.v2.added` event type in your search
+if you already query for the `user.token.added` event type.
+
+## Impact
+
+Once this update has been released and deployed, the `user.token.added` event will no longer be created when a user access token is created, but instead a `user.token.v2.added`.
+Existing `user.token.added` events will be untouched.
diff --git a/docs/docs/support/software-release-cycles-support.md b/docs/docs/support/software-release-cycles-support.md
index 6524c489fd..162b428269 100644
--- a/docs/docs/support/software-release-cycles-support.md
+++ b/docs/docs/support/software-release-cycles-support.md
@@ -74,7 +74,7 @@ During this phase, support is limited as we focus on testing and bug fixing.
### General available
Generally available features are available to everyone and have the appropriate test coverage to be used for critical tasks.
-The software will be backwards-compatible with previous versions, for exceptions we will publish a [technical advisory](https://zitadel.com/docs/support/technical_advisory).
+The software will be backwards-compatible with previous versions, for exceptions we will publish a [technical advisory](/docs/support/technical_advisory).
Features in General Availability are not marked explicitly.
## Release types
diff --git a/docs/docs/support/technical_advisory.mdx b/docs/docs/support/technical_advisory.mdx
index d1a865f88d..e205b87683 100644
--- a/docs/docs/support/technical_advisory.mdx
+++ b/docs/docs/support/technical_advisory.mdx
@@ -149,11 +149,35 @@ We understand that these advisories may include breaking changes, and we aim to
New flag to prefill projections during setup instead of after start |
Feature description |
- new flag `--init-projections` introduced to `zitadel setup` commands (`setup`, `start-from-setup`, `start-from-init`)
+ New flag `--init-projections` introduced to `zitadel setup` commands (`setup`, `start-from-setup`, `start-from-init`)
|
2.44.0, 2.43.6, 2.42.12 |
2024-01-25 |
+
+
+ A-10009
+ |
+ Ensure lock distribution for `FOR UPDATE`-statements on Cockroachdb |
+ Feature description |
+
+ Fixes rare cases where updating projections was blocked by a `WRITE_TOO_OLD`-error when using cockroachdb.
+ |
+ 2.53.0 |
+ 2024-05-28 |
+
+
+
+ A-10010
+ |
+ Event type of token added event changed |
+ Breaking Behavior Change |
+
+ Version 2.53.0 improves the token issuance. Due to this there are changes to the event types created on token creation.
+ |
+ 2.53.0 |
+ 2024-05-28 |
+
## Subscribe to our Mailing List
diff --git a/docs/sidebars.js b/docs/sidebars.js
index d0a0ab9cf0..d142431638 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -881,6 +881,18 @@ module.exports = {
"self-hosting/manage/database/database",
"self-hosting/manage/updating_scaling",
"self-hosting/manage/usage_control",
+ {
+ type: "category",
+ label: "Command Line Interface",
+ collapsed: false,
+ link: {
+ type: "doc",
+ id: "self-hosting/manage/cli/overview"
+ },
+ items: [
+ "self-hosting/manage/cli/mirror"
+ ],
+ },
],
},
],
diff --git a/go.mod b/go.mod
index 9d0da91cba..1f5bcc264e 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
github.com/envoyproxy/protoc-gen-validate v1.0.4
github.com/fatih/color v1.16.0
github.com/gabriel-vasile/mimetype v1.4.3
- github.com/go-jose/go-jose/v4 v4.0.1
+ github.com/go-jose/go-jose/v4 v4.0.2
github.com/go-ldap/ldap/v3 v3.4.7
github.com/go-webauthn/webauthn v0.10.2
github.com/gorilla/csrf v1.7.2
@@ -31,7 +31,7 @@ require (
github.com/gorilla/securecookie v1.1.2
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/grpc-ecosystem/grpc-gateway v1.16.0
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0
github.com/h2non/gock v1.2.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/improbable-eng/grpc-web v0.15.0
@@ -58,31 +58,31 @@ require (
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203
github.com/ttacon/libphonenumber v1.2.1
github.com/zitadel/logging v0.6.0
- github.com/zitadel/oidc/v3 v3.23.2
+ github.com/zitadel/oidc/v3 v3.25.0
github.com/zitadel/passwap v0.5.0
github.com/zitadel/saml v0.1.3
github.com/zitadel/schema v1.3.0
- go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0
- go.opentelemetry.io/otel v1.26.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0
- go.opentelemetry.io/otel/exporters/prometheus v0.48.0
- go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.26.0
- go.opentelemetry.io/otel/metric v1.26.0
- go.opentelemetry.io/otel/sdk v1.26.0
- go.opentelemetry.io/otel/sdk/metric v1.26.0
- go.opentelemetry.io/otel/trace v1.26.0
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0
+ go.opentelemetry.io/otel v1.27.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0
+ go.opentelemetry.io/otel/exporters/prometheus v0.49.0
+ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0
+ go.opentelemetry.io/otel/metric v1.27.0
+ go.opentelemetry.io/otel/sdk v1.27.0
+ go.opentelemetry.io/otel/sdk/metric v1.27.0
+ go.opentelemetry.io/otel/trace v1.27.0
go.uber.org/mock v0.4.0
- golang.org/x/crypto v0.22.0
+ golang.org/x/crypto v0.24.0
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8
- golang.org/x/net v0.24.0
- golang.org/x/oauth2 v0.19.0
+ golang.org/x/net v0.26.0
+ golang.org/x/oauth2 v0.21.0
golang.org/x/sync v0.7.0
- golang.org/x/text v0.14.0
+ golang.org/x/text v0.16.0
google.golang.org/api v0.172.0
- google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6
- google.golang.org/grpc v1.63.2
- google.golang.org/protobuf v1.34.0
+ google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3
+ google.golang.org/grpc v1.64.0
+ google.golang.org/protobuf v1.34.2
sigs.k8s.io/yaml v1.4.0
)
@@ -91,7 +91,7 @@ require (
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
github.com/crewjam/httperr v0.2.0 // indirect
github.com/go-chi/chi/v5 v5.0.12 // indirect
- github.com/go-logr/logr v1.4.1 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/go-webauthn/x v0.1.9 // indirect
@@ -116,13 +116,12 @@ require (
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto v0.0.0-20240412170617-26222e5d3d56 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3 // indirect
)
require (
cloud.google.com/go v0.112.2 // indirect
- cloud.google.com/go/compute v1.25.1 // indirect
- cloud.google.com/go/compute/metadata v0.2.3 // indirect
+ cloud.google.com/go/compute/metadata v0.3.0 // indirect
cloud.google.com/go/iam v1.1.7 // indirect
cloud.google.com/go/trace v1.10.6 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
@@ -181,10 +180,10 @@ require (
github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 // indirect
github.com/muesli/kmeans v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
- github.com/prometheus/client_golang v1.19.0
+ github.com/prometheus/client_golang v1.19.1
github.com/prometheus/client_model v0.6.1 // indirect
- github.com/prometheus/common v0.53.0 // indirect
- github.com/prometheus/procfs v0.14.0 // indirect
+ github.com/prometheus/common v0.54.0 // indirect
+ github.com/prometheus/procfs v0.15.1 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/russellhaering/goxmldsig v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3
@@ -196,9 +195,9 @@ require (
github.com/x448/float16 v0.8.4 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
go.opencensus.io v0.24.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 // indirect
- go.opentelemetry.io/proto/otlp v1.2.0 // indirect
- golang.org/x/sys v0.19.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.3.1 // indirect
+ golang.org/x/sys v0.21.0
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index dd1521bb3f..1ec896708a 100644
--- a/go.sum
+++ b/go.sum
@@ -2,10 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw=
cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms=
-cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
-cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
-cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
-cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
cloud.google.com/go/logging v1.9.0 h1:iEIOXFO9EmSiTjDmfpbRjOxECO7R8C7b8IXUGOj7xZw=
@@ -214,8 +212,8 @@ github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWE
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
-github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
+github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
+github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
@@ -226,8 +224,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
-github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -357,8 +355,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
@@ -603,8 +601,8 @@ github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
-github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
-github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
+github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
+github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -618,16 +616,16 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
-github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
-github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
+github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
+github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s=
-github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ=
github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
@@ -636,8 +634,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
-github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
-github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
@@ -731,8 +729,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.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank=
github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow=
-github.com/zitadel/oidc/v3 v3.23.2 h1:vRUM6SKudr6WR/lqxue4cvCbgR+IdEJGVBklucKKXgk=
-github.com/zitadel/oidc/v3 v3.23.2/go.mod h1:9snlhm3W/GNURqxtchjL1AAuClWRZ2NTkn9sLs1WYfM=
+github.com/zitadel/oidc/v3 v3.25.0 h1:DosOUc31IPM9ZtKaT58+0iNicwDFTFk5Ctt7mgYtsA8=
+github.com/zitadel/oidc/v3 v3.25.0/go.mod h1:UDwD+PRFbUBzabyPd9JORrakty3/wec7VpKZYi9Ahh0=
github.com/zitadel/passwap v0.5.0 h1:kFMoRyo0GnxtOz7j9+r/CsRwSCjHGRaAKoUe69NwPvs=
github.com/zitadel/passwap v0.5.0/go.mod h1:uqY7D3jqdTFcKsW0Q3Pcv5qDMmSHpVTzUZewUKC1KZA=
github.com/zitadel/saml v0.1.3 h1:LI4DOCVyyU1qKPkzs3vrGcA5J3H4pH3+CL9zr9ShkpM=
@@ -746,30 +744,30 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
-go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
-go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 h1:Waw9Wfpo/IXzOI8bCB7DIk+0JZcqqsyn1JFnAc+iam8=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0/go.mod h1:wnJIG4fOqyynOnnQF/eQb4/16VlX2EJAHhHgqIqWfAo=
-go.opentelemetry.io/otel/exporters/prometheus v0.48.0 h1:sBQe3VNGUjY9IKWQC6z2lNqa5iGbDSxhs60ABwK4y0s=
-go.opentelemetry.io/otel/exporters/prometheus v0.48.0/go.mod h1:DtrbMzoZWwQHyrQmCfLam5DZbnmorsGbOtTbYHycU5o=
-go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.26.0 h1:0W5o9SzoR15ocYHEQfvfipzcNog1lBxOLfnex91Hk6s=
-go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.26.0/go.mod h1:zVZ8nz+VSggWmnh6tTsJqXQ7rU4xLwRtna1M4x5jq58=
-go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
-go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
-go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8=
-go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs=
-go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y=
-go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE=
-go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
-go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
-go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
-go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 h1:vS1Ao/R55RNV4O7TA2Qopok8yN+X0LIP6RVWLFkprck=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0/go.mod h1:BMsdeOxN04K0L5FNUBfjFdvwWGNe/rkmSwH4Aelu/X0=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0=
+go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
+go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
+go.opentelemetry.io/otel/exporters/prometheus v0.49.0 h1:Er5I1g/YhfYv9Affk9nJLfH/+qCCVVg1f2R9AbJfqDQ=
+go.opentelemetry.io/otel/exporters/prometheus v0.49.0/go.mod h1:KfQ1wpjf3zsHjzP149P4LyAwWRupc6c7t1ZJ9eXpKQM=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 h1:/0YaXu3755A/cFbtXp+21lkXgI0QE5avTWA2HjU9/WE=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0/go.mod h1:m7SFxp0/7IxmJPLIY3JhOcU9CoFzDaCPL6xxQIxhA+o=
+go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=
+go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
+go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI=
+go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A=
+go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI=
+go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw=
+go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=
+go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
+go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
+go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
@@ -801,8 +799,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
-golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
-golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
+golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
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-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
@@ -863,13 +861,13 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
-golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
-golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
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-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
-golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
+golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
+golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
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=
@@ -924,8 +922,8 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
-golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
+golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -941,8 +939,9 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
@@ -991,10 +990,10 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY
google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20240412170617-26222e5d3d56 h1:LlcUFJ4BLmJVS4Kly+WCK7LQqcevmycHj88EPgyhNx8=
google.golang.org/genproto v0.0.0-20240412170617-26222e5d3d56/go.mod h1:n1CaIKYMIlxFt1zJE/1kU40YpSL0drGMbl0Idum1VSs=
-google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 h1:DTJM0R8LECCgFeUwApvcEJHz85HLagW8uRENYxHh1ww=
-google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6/go.mod h1:10yRODfgim2/T8csjQsMPgZOMvtytXKTDRzH6HRGzRw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
+google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3 h1:QW9+G6Fir4VcRXVH8x3LilNAb6cxBGLa6+GM4hRwexE=
+google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3/go.mod h1:kdrSS/OiLkPrNUpzD4aHgCq2rVuC/YRxok32HXZ4vRE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3 h1:9Xyg6I9IWQZhRVfCWjKK+l6kI0jHcPesVlMnT//aHNo=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
@@ -1010,8 +1009,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
-google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
+google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
+google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1022,8 +1021,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
-google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/admin/repository/eventsourcing/handler/handler.go b/internal/admin/repository/eventsourcing/handler/handler.go
index e4791b70d9..06720144e1 100644
--- a/internal/admin/repository/eventsourcing/handler/handler.go
+++ b/internal/admin/repository/eventsourcing/handler/handler.go
@@ -41,14 +41,24 @@ func Register(ctx context.Context, config Config, view *view.View, static static
))
}
+func Projections() []*handler2.Handler {
+ return projections
+}
+
func Start(ctx context.Context) {
for _, projection := range projections {
projection.Start(ctx)
}
}
-func Projections() []*handler2.Handler {
- return projections
+func ProjectInstance(ctx context.Context) error {
+ for _, projection := range projections {
+ _, err := projection.Trigger(ctx)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
}
func (config Config) overwrite(viewModel string) handler2.Config {
diff --git a/internal/api/grpc/admin/restrictions_integration_allow_public_org_registrations_test.go b/internal/api/grpc/admin/restrictions_integration_allow_public_org_registrations_test.go
index 30250e772a..5b29de19bb 100644
--- a/internal/api/grpc/admin/restrictions_integration_allow_public_org_registrations_test.go
+++ b/internal/api/grpc/admin/restrictions_integration_allow_public_org_registrations_test.go
@@ -22,7 +22,7 @@ import (
func TestServer_Restrictions_DisallowPublicOrgRegistration(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
- domain, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, ctx, SystemCTX)
+ domain, _, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, ctx, SystemCTX)
regOrgUrl, err := url.Parse("http://" + domain + ":8080/ui/login/register/org")
require.NoError(t, err)
// The CSRF cookie must be sent with every request.
diff --git a/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go b/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go
index 277375f525..3e00978676 100644
--- a/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go
+++ b/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go
@@ -33,7 +33,7 @@ func TestServer_Restrictions_AllowedLanguages(t *testing.T) {
unsupportedLanguage = language.Afrikaans
)
- domain, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, ctx, SystemCTX)
+ domain, _, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, 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{})
diff --git a/internal/api/grpc/feature/v2/converter.go b/internal/api/grpc/feature/v2/converter.go
index aa8b1287f4..259a6f8a42 100644
--- a/internal/api/grpc/feature/v2/converter.go
+++ b/internal/api/grpc/feature/v2/converter.go
@@ -16,6 +16,7 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.
UserSchema: req.UserSchema,
Actions: req.Actions,
TokenExchange: req.OidcTokenExchange,
+ ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
}
}
@@ -28,6 +29,7 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe
UserSchema: featureSourceToFlagPb(&f.UserSchema),
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
Actions: featureSourceToFlagPb(&f.Actions),
+ ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
}
}
@@ -39,6 +41,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm
UserSchema: req.UserSchema,
TokenExchange: req.OidcTokenExchange,
Actions: req.Actions,
+ ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
}
}
@@ -51,6 +54,14 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat
UserSchema: featureSourceToFlagPb(&f.UserSchema),
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
Actions: featureSourceToFlagPb(&f.Actions),
+ ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
+ }
+}
+
+func featureSourceToImprovedPerformanceFlagPb(fs *query.FeatureSource[[]feature.ImprovedPerformanceType]) *feature_pb.ImprovedPerformanceFeatureFlag {
+ return &feature_pb.ImprovedPerformanceFeatureFlag{
+ ExecutionPaths: improvedPerformanceTypesToPb(fs.Value),
+ Source: featureLevelToSourcePb(fs.Level),
}
}
@@ -81,3 +92,48 @@ func featureLevelToSourcePb(level feature.Level) feature_pb.Source {
return feature_pb.Source(level)
}
}
+
+func improvedPerformanceTypesToPb(types []feature.ImprovedPerformanceType) []feature_pb.ImprovedPerformance {
+ res := make([]feature_pb.ImprovedPerformance, len(types))
+
+ for i, typ := range types {
+ res[i] = improvedPerformanceTypeToPb(typ)
+ }
+
+ return res
+}
+
+func improvedPerformanceTypeToPb(typ feature.ImprovedPerformanceType) feature_pb.ImprovedPerformance {
+ switch typ {
+ case feature.ImprovedPerformanceTypeUnknown:
+ return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED
+ case feature.ImprovedPerformanceTypeOrgByID:
+ return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID
+ default:
+ return feature_pb.ImprovedPerformance(typ)
+ }
+}
+
+func improvedPerformanceListToDomain(list []feature_pb.ImprovedPerformance) []feature.ImprovedPerformanceType {
+ if list == nil {
+ return nil
+ }
+ res := make([]feature.ImprovedPerformanceType, len(list))
+
+ for i, typ := range list {
+ res[i] = improvedPerformanceToDomain(typ)
+ }
+
+ return res
+}
+
+func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.ImprovedPerformanceType {
+ switch typ {
+ case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED:
+ return feature.ImprovedPerformanceTypeUnknown
+ case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID:
+ return feature.ImprovedPerformanceTypeOrgByID
+ default:
+ return feature.ImprovedPerformanceTypeUnknown
+ }
+}
diff --git a/internal/api/grpc/feature/v2/converter_test.go b/internal/api/grpc/feature/v2/converter_test.go
index 78aea5eb15..35dbf98014 100644
--- a/internal/api/grpc/feature/v2/converter_test.go
+++ b/internal/api/grpc/feature/v2/converter_test.go
@@ -24,6 +24,7 @@ func Test_systemFeaturesToCommand(t *testing.T) {
UserSchema: gu.Ptr(true),
Actions: gu.Ptr(true),
OidcTokenExchange: gu.Ptr(true),
+ ImprovedPerformance: nil,
}
want := &command.SystemFeatures{
LoginDefaultOrg: gu.Ptr(true),
@@ -32,6 +33,7 @@ func Test_systemFeaturesToCommand(t *testing.T) {
UserSchema: gu.Ptr(true),
Actions: gu.Ptr(true),
TokenExchange: gu.Ptr(true),
+ ImprovedPerformance: nil,
}
got := systemFeaturesToCommand(arg)
assert.Equal(t, want, got)
@@ -68,6 +70,10 @@ func Test_systemFeaturesToPb(t *testing.T) {
Level: feature.LevelSystem,
Value: false,
},
+ ImprovedPerformance: query.FeatureSource[[]feature.ImprovedPerformanceType]{
+ Level: feature.LevelSystem,
+ Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID},
+ },
}
want := &feature_pb.GetSystemFeaturesResponse{
Details: &object.Details{
@@ -99,6 +105,10 @@ func Test_systemFeaturesToPb(t *testing.T) {
Enabled: true,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
+ ImprovedPerformance: &feature_pb.ImprovedPerformanceFeatureFlag{
+ ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID},
+ Source: feature_pb.Source_SOURCE_SYSTEM,
+ },
}
got := systemFeaturesToPb(arg)
assert.Equal(t, want, got)
@@ -112,6 +122,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
UserSchema: gu.Ptr(true),
OidcTokenExchange: gu.Ptr(true),
Actions: gu.Ptr(true),
+ ImprovedPerformance: nil,
}
want := &command.InstanceFeatures{
LoginDefaultOrg: gu.Ptr(true),
@@ -120,6 +131,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
UserSchema: gu.Ptr(true),
TokenExchange: gu.Ptr(true),
Actions: gu.Ptr(true),
+ ImprovedPerformance: nil,
}
got := instanceFeaturesToCommand(arg)
assert.Equal(t, want, got)
@@ -156,6 +168,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Level: feature.LevelSystem,
Value: false,
},
+ ImprovedPerformance: query.FeatureSource[[]feature.ImprovedPerformanceType]{
+ Level: feature.LevelSystem,
+ Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID},
+ },
}
want := &feature_pb.GetInstanceFeaturesResponse{
Details: &object.Details{
@@ -187,6 +203,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Enabled: false,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
+ ImprovedPerformance: &feature_pb.ImprovedPerformanceFeatureFlag{
+ ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID},
+ Source: feature_pb.Source_SOURCE_SYSTEM,
+ },
}
got := instanceFeaturesToPb(arg)
assert.Equal(t, want, got)
diff --git a/internal/api/grpc/server/gateway.go b/internal/api/grpc/server/gateway.go
index 6a3ac94bad..327865bd6c 100644
--- a/internal/api/grpc/server/gateway.go
+++ b/internal/api/grpc/server/gateway.go
@@ -10,9 +10,11 @@ import (
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/zitadel/logging"
"google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
+ "google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
@@ -36,6 +38,23 @@ var (
},
}
+ httpErrorHandler = runtime.RoutingErrorHandlerFunc(
+ func(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, r *http.Request, httpStatus int) {
+ if httpStatus != http.StatusMethodNotAllowed {
+ runtime.DefaultRoutingErrorHandler(ctx, mux, marshaler, w, r, httpStatus)
+ return
+ }
+
+ // Use HTTPStatusError to customize the DefaultHTTPErrorHandler status code
+ err := &runtime.HTTPStatusError{
+ HTTPStatus: httpStatus,
+ Err: status.Error(codes.Unimplemented, http.StatusText(httpStatus)),
+ }
+
+ runtime.DefaultHTTPErrorHandler(ctx, mux, marshaler, w, r, err)
+ },
+ )
+
serveMuxOptions = []runtime.ServeMuxOption{
runtime.WithMarshalerOption(jsonMarshaler.ContentType(nil), jsonMarshaler),
runtime.WithMarshalerOption(mimeWildcard, jsonMarshaler),
@@ -43,6 +62,7 @@ var (
runtime.WithIncomingHeaderMatcher(headerMatcher),
runtime.WithOutgoingHeaderMatcher(runtime.DefaultHeaderMatcher),
runtime.WithForwardResponseOption(responseForwarder),
+ runtime.WithRoutingErrorHandler(httpErrorHandler),
}
headerMatcher = runtime.HeaderMatcherFunc(
diff --git a/internal/api/grpc/session/v2/session_integration_test.go b/internal/api/grpc/session/v2/session_integration_test.go
index 9423eea9ae..92d3b0baf7 100644
--- a/internal/api/grpc/session/v2/session_integration_test.go
+++ b/internal/api/grpc/session/v2/session_integration_test.go
@@ -14,12 +14,15 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/logging"
+ "google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
+ "google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
+ mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
@@ -27,6 +30,7 @@ import (
var (
CTX context.Context
+ IAMOwnerCTX context.Context
Tester *integration.Tester
Client session.SessionServiceClient
User *user.AddHumanUserResponse
@@ -44,6 +48,7 @@ func TestMain(m *testing.M) {
Client = Tester.Client.SessionV2
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
+ IAMOwnerCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
User = createFullUser(CTX)
DeactivatedUser = createDeactivatedUser(CTX)
LockedUser = createLockedUser(CTX)
@@ -341,6 +346,48 @@ func TestServer_CreateSession(t *testing.T) {
}
}
+func TestServer_CreateSession_lock_user(t *testing.T) {
+ // create a separate org so we don't interfere with any other test
+ org := Tester.CreateOrganization(IAMOwnerCTX,
+ fmt.Sprintf("TestServer_CreateSession_lock_user_%d", time.Now().UnixNano()),
+ fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
+ )
+ userID := org.CreatedAdmins[0].GetUserId()
+ Tester.SetUserPassword(IAMOwnerCTX, userID, integration.UserPassword, false)
+
+ // enable password lockout
+ maxAttempts := 2
+ ctxOrg := metadata.AppendToOutgoingContext(IAMOwnerCTX, "x-zitadel-orgid", org.GetOrganizationId())
+ _, err := Tester.Client.Mgmt.AddCustomLockoutPolicy(ctxOrg, &mgmt.AddCustomLockoutPolicyRequest{
+ MaxPasswordAttempts: uint32(maxAttempts),
+ })
+ require.NoError(t, err)
+
+ for i := 0; i <= maxAttempts; i++ {
+ _, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
+ Checks: &session.Checks{
+ User: &session.CheckUser{
+ Search: &session.CheckUser_UserId{
+ UserId: userID,
+ },
+ },
+ Password: &session.CheckPassword{
+ Password: "invalid",
+ },
+ },
+ })
+ assert.Error(t, err)
+ statusCode := status.Code(err)
+ expectedCode := codes.InvalidArgument
+ // as soon as we hit the limit the user is locked and following request will
+ // already deny any check with a precondition failed since the user is locked
+ if i >= maxAttempts {
+ expectedCode = codes.FailedPrecondition
+ }
+ assert.Equal(t, expectedCode, statusCode)
+ }
+}
+
func TestServer_CreateSession_webauthn(t *testing.T) {
// create new session with user and request the webauthn challenge
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
@@ -429,6 +476,14 @@ func TestServer_CreateSession_successfulIntent_instant(t *testing.T) {
func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
idpID := Tester.AddGenericOAuthProvider(t, CTX)
+ // successful intent without known / linked user
+ idpUserID := "id"
+ intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", idpUserID)
+
+ // link the user (with info from intent)
+ Tester.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId())
+
+ // session with intent check must now succeed
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
@@ -436,28 +491,6 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
UserId: User.GetUserId(),
},
},
- },
- })
- require.NoError(t, err)
- verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
-
- idpUserID := "id"
- intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", idpUserID)
- updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{
- SessionId: createResp.GetSessionId(),
- Checks: &session.Checks{
- IdpIntent: &session.CheckIDPIntent{
- IdpIntentId: intentID,
- IdpIntentToken: token,
- },
- },
- })
- require.Error(t, err)
- Tester.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId())
- intentID, token, _, _ = Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), idpUserID)
- updateResp, err = Client.SetSession(CTX, &session.SetSessionRequest{
- SessionId: createResp.GetSessionId(),
- Checks: &session.Checks{
IdpIntent: &session.CheckIDPIntent{
IdpIntentId: intentID,
IdpIntentToken: token,
@@ -465,7 +498,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
},
})
require.NoError(t, err)
- verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor)
+ verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor)
}
func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
@@ -916,15 +949,6 @@ func Test_ZITADEL_API_missing_authentication(t *testing.T) {
require.Nil(t, sessionResp)
}
-func Test_ZITADEL_API_missing_mfa(t *testing.T) {
- id, token, _, _ := Tester.CreatePasswordSession(t, CTX, User.GetUserId(), integration.UserPassword)
-
- ctx := Tester.WithAuthorizationToken(context.Background(), token)
- sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
- require.Error(t, err)
- require.Nil(t, sessionResp)
-}
-
func Test_ZITADEL_API_success(t *testing.T) {
id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId())
diff --git a/internal/api/grpc/system/instance_integration_test.go b/internal/api/grpc/system/instance_integration_test.go
index b4a6cea227..50d1dda50b 100644
--- a/internal/api/grpc/system/instance_integration_test.go
+++ b/internal/api/grpc/system/instance_integration_test.go
@@ -14,7 +14,7 @@ import (
)
func TestServer_ListInstances(t *testing.T) {
- domain, instanceID, _ := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
+ domain, instanceID, _, _ := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
tests := []struct {
name string
diff --git a/internal/api/grpc/system/limits_integration_auditlogretention_test.go b/internal/api/grpc/system/limits_integration_auditlogretention_test.go
index 55ac76e998..b31ee818bf 100644
--- a/internal/api/grpc/system/limits_integration_auditlogretention_test.go
+++ b/internal/api/grpc/system/limits_integration_auditlogretention_test.go
@@ -21,7 +21,7 @@ import (
)
func TestServer_Limits_AuditLogRetention(t *testing.T) {
- _, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
+ _, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
userID, projectID, appID, projectGrantID := seedObjects(iamOwnerCtx, t)
beforeTime := time.Now()
farPast := timestamppb.New(beforeTime.Add(-10 * time.Hour).UTC())
diff --git a/internal/api/grpc/system/limits_integration_block_test.go b/internal/api/grpc/system/limits_integration_block_test.go
index f007dd46c4..68426dd05e 100644
--- a/internal/api/grpc/system/limits_integration_block_test.go
+++ b/internal/api/grpc/system/limits_integration_block_test.go
@@ -27,7 +27,7 @@ import (
)
func TestServer_Limits_Block(t *testing.T) {
- domain, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
+ domain, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
tests := []*test{
publicAPIBlockingTest(domain),
{
diff --git a/internal/api/grpc/system/quotas_enabled/quota_integration_test.go b/internal/api/grpc/system/quotas_enabled/quota_integration_test.go
index a6bd30707f..225b4a2daa 100644
--- a/internal/api/grpc/system/quotas_enabled/quota_integration_test.go
+++ b/internal/api/grpc/system/quotas_enabled/quota_integration_test.go
@@ -66,7 +66,7 @@ func TestServer_QuotaNotification_Limit(t *testing.T) {
}
func TestServer_QuotaNotification_NoLimit(t *testing.T) {
- _, instanceID, IAMOwnerCTX := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
+ _, instanceID, _, IAMOwnerCTX := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
amount := 10
percent := 50
percentAmount := amount * percent / 100
@@ -148,7 +148,7 @@ func awaitNotification(t *testing.T, bodies chan []byte, unit quota.Unit, percen
}
func TestServer_AddAndRemoveQuota(t *testing.T) {
- _, instanceID, _ := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
+ _, instanceID, _, _ := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
got, err := Tester.Client.System.SetQuota(SystemCTX, &system.SetQuotaRequest{
InstanceId: instanceID,
diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go
index 1abe59dc88..5d23a8bd98 100644
--- a/internal/api/oidc/auth_request.go
+++ b/internal/api/oidc/auth_request.go
@@ -471,7 +471,7 @@ func (s *Server) CreateTokenCallbackURL(ctx context.Context, req op.AuthRequest)
if err != nil {
return "", err
}
- resp, err := s.accessTokenResponseFromSession(ctx, client, session, state, client.client.ProjectID, client.client.ProjectRoleAssertion)
+ resp, err := s.accessTokenResponseFromSession(ctx, client, session, state, client.client.ProjectID, client.client.ProjectRoleAssertion, client.client.AccessTokenRoleAssertion, client.client.IDTokenRoleAssertion, client.client.IDTokenUserinfoAssertion)
if err != nil {
return "", err
}
@@ -563,7 +563,7 @@ func (s *Server) authResponseToken(authReq *AuthRequest, authorizer op.Authorize
op.AuthRequestError(w, r, authReq, err, authorizer)
return err
}
- resp, err := s.accessTokenResponseFromSession(ctx, client, session, authReq.GetState(), client.client.ProjectID, client.client.ProjectRoleAssertion)
+ resp, err := s.accessTokenResponseFromSession(ctx, client, session, authReq.GetState(), client.client.ProjectID, client.client.ProjectRoleAssertion, client.client.AccessTokenRoleAssertion, client.client.IDTokenRoleAssertion, client.client.IDTokenUserinfoAssertion)
if err != nil {
op.AuthRequestError(w, r, authReq, err, authorizer)
return err
diff --git a/internal/api/oidc/auth_request_integration_test.go b/internal/api/oidc/auth_request_integration_test.go
index c36e06c6aa..630b20bc09 100644
--- a/internal/api/oidc/auth_request_integration_test.go
+++ b/internal/api/oidc/auth_request_integration_test.go
@@ -54,7 +54,7 @@ func TestOPStorage_CreateAccessToken_code(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, false)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
// callback on a succeeded request must fail
linkResp, err = Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
@@ -108,7 +108,7 @@ func TestOPStorage_CreateAccessToken_implicit(t *testing.T) {
require.NoError(t, err)
claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), accessToken, idToken, provider.IDTokenVerifier())
require.NoError(t, err)
- assertIDTokenClaims(t, claims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, claims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
// callback on a succeeded request must fail
linkResp, err = Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
@@ -143,7 +143,7 @@ func TestOPStorage_CreateAccessAndRefreshTokens_code(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, true)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
}
func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) {
@@ -168,14 +168,14 @@ func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, true)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
// test actual refresh grant
newTokens, err := refreshTokens(t, clientID, tokens.RefreshToken)
require.NoError(t, err)
assertTokens(t, newTokens, true)
// auth time must still be the initial
- assertIDTokenClaims(t, newTokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, newTokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
// refresh with an old refresh_token must fail
_, err = rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, tokens.RefreshToken, "", "")
@@ -204,7 +204,7 @@ func TestOPStorage_RevokeToken_access_token(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, true)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
// revoke access token
err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "access_token")
@@ -247,7 +247,7 @@ func TestOPStorage_RevokeToken_access_token_invalid_token_hint_type(t *testing.T
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, true)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
// revoke access token
err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "refresh_token")
@@ -284,7 +284,7 @@ func TestOPStorage_RevokeToken_refresh_token(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, true)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
// revoke refresh token -> invalidates also access token
err = rp.RevokeToken(CTX, provider, tokens.RefreshToken, "refresh_token")
@@ -327,7 +327,7 @@ func TestOPStorage_RevokeToken_refresh_token_invalid_token_type_hint(t *testing.
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, true)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
// revoke refresh token even with a wrong hint
err = rp.RevokeToken(CTX, provider, tokens.RefreshToken, "access_token")
@@ -362,7 +362,7 @@ func TestOPStorage_RevokeToken_invalid_client(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, true)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
// simulate second client (not part of the audience) trying to revoke the token
otherClientID, _ := createClient(t)
@@ -394,7 +394,7 @@ func TestOPStorage_TerminateSession(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, false)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
// userinfo must not fail
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
@@ -431,7 +431,7 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, true)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
// userinfo must not fail
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
@@ -475,7 +475,7 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, false)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
postLogoutRedirect, err := rp.EndSession(CTX, provider, "", logoutRedirectURI, "state")
require.NoError(t, err)
@@ -528,10 +528,18 @@ func assertTokens(t *testing.T, tokens *oidc.Tokens[*oidc.IDTokenClaims], requir
} else {
assert.Empty(t, tokens.RefreshToken)
}
+ // since we test implicit flow directly, we can check that any token response must not
+ // return a `state` in the response
+ assert.Empty(t, tokens.Extra("state"))
}
-func assertIDTokenClaims(t *testing.T, claims *oidc.IDTokenClaims, userID string, arm []string, sessionStart, sessionChange time.Time) {
+func assertIDTokenClaims(t *testing.T, claims *oidc.IDTokenClaims, userID string, arm []string, sessionStart, sessionChange time.Time, sessionID string) {
assert.Equal(t, userID, claims.Subject)
assert.Equal(t, arm, claims.AuthenticationMethodsReferences)
assertOIDCTimeRange(t, claims.AuthTime, sessionStart, sessionChange)
+ assert.Equal(t, sessionID, claims.SessionID)
+ assert.Empty(t, claims.Name)
+ assert.Empty(t, claims.GivenName)
+ assert.Empty(t, claims.FamilyName)
+ assert.Empty(t, claims.PreferredUsername)
}
diff --git a/internal/api/oidc/client_integration_test.go b/internal/api/oidc/client_integration_test.go
index 58fdebef07..21d54a59dc 100644
--- a/internal/api/oidc/client_integration_test.go
+++ b/internal/api/oidc/client_integration_test.go
@@ -122,7 +122,7 @@ func TestServer_Introspect(t *testing.T) {
tokens, err := exchangeTokens(t, app.GetClientId(), code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, true)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
// test actual introspection
introspection, err := rs.Introspect[*oidc.IntrospectionResponse](context.Background(), resourceServer, tokens.AccessToken)
@@ -317,7 +317,7 @@ func TestServer_VerifyClient(t *testing.T) {
}
require.NoError(t, err)
assertTokens(t, tokens, false)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
})
}
}
diff --git a/internal/api/oidc/introspect.go b/internal/api/oidc/introspect.go
index a2e59c9a45..b0881b6d65 100644
--- a/internal/api/oidc/introspect.go
+++ b/internal/api/oidc/introspect.go
@@ -11,6 +11,7 @@ import (
"github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -80,7 +81,11 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR
// with active: false
defer func() {
if err != nil {
- s.getLogger(ctx).ErrorContext(ctx, "oidc introspection", "err", err)
+ if zerrors.IsInternal(err) {
+ s.getLogger(ctx).ErrorContext(ctx, "oidc introspection", "err", err)
+ } else {
+ s.getLogger(ctx).InfoContext(ctx, "oidc introspection", "err", err)
+ }
resp, err = op.NewResponse(new(oidc.IntrospectionResponse)), nil
}
}()
@@ -99,7 +104,14 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR
if err = validateIntrospectionAudience(token.audience, client.clientID, client.projectID); err != nil {
return nil, err
}
- userInfo, err := s.userInfo(ctx, token.userID, token.scope, client.projectID, client.projectRoleAssertion, true)
+ userInfo, err := s.userInfo(
+ token.userID,
+ token.scope,
+ client.projectID,
+ client.projectRoleAssertion,
+ true,
+ true,
+ )(ctx, true, domain.TriggerTypePreUserinfoCreation)
if err != nil {
return nil, err
}
diff --git a/internal/api/oidc/oidc_integration_test.go b/internal/api/oidc/oidc_integration_test.go
index 09e76391bd..0baeb53363 100644
--- a/internal/api/oidc/oidc_integration_test.go
+++ b/internal/api/oidc/oidc_integration_test.go
@@ -18,6 +18,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/auth"
+ mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
@@ -26,6 +27,7 @@ import (
var (
CTX context.Context
CTXLOGIN context.Context
+ CTXIAM context.Context
Tester *integration.Tester
User *user.AddHumanUserResponse
)
@@ -50,6 +52,7 @@ func TestMain(m *testing.M) {
Tester.SetUserPassword(CTX, User.GetUserId(), integration.UserPassword, false)
Tester.RegisterUserPasskey(CTX, User.GetUserId())
CTXLOGIN = Tester.WithAuthorization(ctx, integration.Login)
+ CTXIAM = Tester.WithAuthorization(ctx, integration.IAMOwner)
return m.Run()
}())
}
@@ -74,7 +77,7 @@ func Test_ZITADEL_API_missing_audience_scope(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, false)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
@@ -117,10 +120,13 @@ func Test_ZITADEL_API_missing_authentication(t *testing.T) {
require.Nil(t, myUserResp)
}
-func Test_ZITADEL_API_missing_mfa(t *testing.T) {
+func Test_ZITADEL_API_missing_mfa_policy(t *testing.T) {
clientID, _ := createClient(t)
+ org := Tester.CreateOrganization(CTXIAM, fmt.Sprintf("ZITADEL_API_MISSING_MFA_%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()))
+ userID := org.CreatedAdmins[0].GetUserId()
+ Tester.SetUserPassword(CTXIAM, userID, integration.UserPassword, false)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope)
- sessionID, sessionToken, startTime, changeTime := Tester.CreatePasswordSession(t, CTX, User.GetUserId(), integration.UserPassword)
+ sessionID, sessionToken, startTime, changeTime := Tester.CreatePasswordSession(t, CTXLOGIN, userID, integration.UserPassword)
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@@ -136,11 +142,36 @@ func Test_ZITADEL_API_missing_mfa(t *testing.T) {
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPassword, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, userID, armPassword, startTime, changeTime, sessionID)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
+ // pre check if request would succeed
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
+ require.NoError(t, err)
+ require.Equal(t, userID, myUserResp.GetUser().GetId())
+
+ // require MFA
+ ctxOrg := metadata.AppendToOutgoingContext(CTXIAM, "x-zitadel-orgid", org.GetOrganizationId())
+ _, err = Tester.Client.Mgmt.AddCustomLoginPolicy(ctxOrg, &mgmt.AddCustomLoginPolicyRequest{
+ ForceMfa: true,
+ })
+ require.NoError(t, err)
+
+ // make sure policy is projected
+ retryDuration := 5 * time.Second
+ if ctxDeadline, ok := CTX.Deadline(); ok {
+ retryDuration = time.Until(ctxDeadline)
+ }
+ require.EventuallyWithT(t, func(ttt *assert.CollectT) {
+ got, getErr := Tester.Client.Mgmt.GetLoginPolicy(ctxOrg, &mgmt.GetLoginPolicyRequest{})
+ assert.NoError(ttt, getErr)
+ assert.False(ttt, got.GetPolicy().IsDefault)
+
+ }, retryDuration, time.Millisecond*100, "timeout waiting for login policy")
+
+ // now it must fail
+ myUserResp, err = Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
require.Error(t, err)
require.Nil(t, myUserResp)
}
@@ -165,7 +196,7 @@ func Test_ZITADEL_API_success(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, false)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
@@ -199,7 +230,7 @@ func Test_ZITADEL_API_glob_redirects(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, false)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
@@ -228,7 +259,7 @@ func Test_ZITADEL_API_inactive_access_token(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, true)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
// make sure token works
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
@@ -270,7 +301,7 @@ func Test_ZITADEL_API_terminated_session(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, true)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
// make sure token works
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
@@ -340,7 +371,7 @@ func Test_ZITADEL_API_terminated_session_user_disabled(t *testing.T) {
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, true)
- assertIDTokenClaims(t, tokens.IDTokenClaims, disabledUser.GetUserId(), armPassword, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, disabledUser.GetUserId(), armPassword, startTime, changeTime, sessionID)
// make sure token works
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
diff --git a/internal/api/oidc/token.go b/internal/api/oidc/token.go
index c45eb98acb..be3a30ed73 100644
--- a/internal/api/oidc/token.go
+++ b/internal/api/oidc/token.go
@@ -29,8 +29,8 @@ In some cases step 1 till 3 are completely implemented in the command package,
for example the v2 code exchange and refresh token.
*/
-func (s *Server) accessTokenResponseFromSession(ctx context.Context, client op.Client, session *command.OIDCSession, state, projectID string, projectRoleAssertion bool) (_ *oidc.AccessTokenResponse, err error) {
- getUserInfo := s.getUserInfoOnce(session.UserID, projectID, projectRoleAssertion, session.Scope)
+func (s *Server) accessTokenResponseFromSession(ctx context.Context, client op.Client, session *command.OIDCSession, state, projectID string, projectRoleAssertion, accessTokenRoleAssertion, idTokenRoleAssertion, userInfoAssertion bool) (_ *oidc.AccessTokenResponse, err error) {
+ getUserInfo := s.getUserInfo(session.UserID, projectID, projectRoleAssertion, userInfoAssertion, session.Scope)
getSigner := s.getSignerOnce()
resp := &oidc.AccessTokenResponse{
@@ -43,7 +43,7 @@ func (s *Server) accessTokenResponseFromSession(ctx context.Context, client op.C
// If the session does not have a token ID, it is an implicit ID-Token only response.
if session.TokenID != "" {
if client.AccessTokenType() == op.AccessTokenTypeJWT {
- resp.AccessToken, err = s.createJWT(ctx, client, session, getUserInfo, getSigner)
+ resp.AccessToken, err = s.createJWT(ctx, client, session, getUserInfo, accessTokenRoleAssertion, getSigner)
} else {
resp.AccessToken, err = op.CreateBearerToken(session.TokenID, session.UserID, s.opCrypto)
}
@@ -53,7 +53,7 @@ func (s *Server) accessTokenResponseFromSession(ctx context.Context, client op.C
}
if slices.Contains(session.Scope, oidc.ScopeOpenID) {
- resp.IDToken, _, err = s.createIDToken(ctx, client, getUserInfo, getSigner, session.SessionID, resp.AccessToken, session.Audience, session.AuthMethods, session.AuthTime, session.Nonce, session.Actor)
+ resp.IDToken, _, err = s.createIDToken(ctx, client, getUserInfo, idTokenRoleAssertion, getSigner, session.SessionID, resp.AccessToken, session.Audience, session.AuthMethods, session.AuthTime, session.Nonce, session.Actor)
}
return resp, err
}
@@ -92,31 +92,22 @@ func (s *Server) getSignerOnce() signerFunc {
}
// userInfoFunc is a getter function that allows add-hoc retrieval of a user.
-type userInfoFunc func(ctx context.Context) (*oidc.UserInfo, error)
+type userInfoFunc func(ctx context.Context, roleAssertion bool, triggerType domain.TriggerType) (*oidc.UserInfo, error)
-// getUserInfoOnce returns a function which retrieves userinfo from the database once.
-// Repeated calls of the returned function return the same results.
-func (s *Server) getUserInfoOnce(userID, projectID string, projectRoleAssertion bool, scope []string) userInfoFunc {
- var (
- once sync.Once
- userInfo *oidc.UserInfo
- err error
- )
- return func(ctx context.Context) (*oidc.UserInfo, error) {
- once.Do(func() {
- ctx, span := tracing.NewSpan(ctx)
- defer func() { span.EndWithError(err) }()
- userInfo, err = s.userInfo(ctx, userID, scope, projectID, projectRoleAssertion, false)
- })
- return userInfo, err
+// getUserInfo returns a function which retrieves userinfo from the database once.
+// However, each time, role claims are asserted and also action flows will trigger.
+func (s *Server) getUserInfo(userID, projectID string, projectRoleAssertion, userInfoAssertion bool, scope []string) userInfoFunc {
+ userInfo := s.userInfo(userID, scope, projectID, projectRoleAssertion, userInfoAssertion, false)
+ return func(ctx context.Context, roleAssertion bool, triggerType domain.TriggerType) (*oidc.UserInfo, error) {
+ return userInfo(ctx, roleAssertion, triggerType)
}
}
-func (*Server) createIDToken(ctx context.Context, client op.Client, getUserInfo userInfoFunc, getSigningKey signerFunc, sessionID, accessToken string, audience []string, authMethods []domain.UserAuthMethodType, authTime time.Time, nonce string, actor *domain.TokenActor) (idToken string, exp uint64, err error) {
+func (*Server) createIDToken(ctx context.Context, client op.Client, getUserInfo userInfoFunc, roleAssertion bool, getSigningKey signerFunc, sessionID, accessToken string, audience []string, authMethods []domain.UserAuthMethodType, authTime time.Time, nonce string, actor *domain.TokenActor) (idToken string, exp uint64, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
- userInfo, err := getUserInfo(ctx)
+ userInfo, err := getUserInfo(ctx, roleAssertion, domain.TriggerTypePreUserinfoCreation)
if err != nil {
return "", 0, err
}
@@ -156,11 +147,11 @@ func timeToOIDCExpiresIn(exp time.Time) uint64 {
return uint64(time.Until(exp) / time.Second)
}
-func (*Server) createJWT(ctx context.Context, client op.Client, session *command.OIDCSession, getUserInfo userInfoFunc, getSigner signerFunc) (_ string, err error) {
+func (s *Server) createJWT(ctx context.Context, client op.Client, session *command.OIDCSession, getUserInfo userInfoFunc, assertRoles bool, getSigner signerFunc) (_ string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
- userInfo, err := getUserInfo(ctx)
+ userInfo, err := getUserInfo(ctx, assertRoles, domain.TriggerTypePreAccessTokenCreation)
if err != nil {
return "", err
}
diff --git a/internal/api/oidc/token_client_credentials.go b/internal/api/oidc/token_client_credentials.go
index e0cb29770b..5d19e398a6 100644
--- a/internal/api/oidc/token_client_credentials.go
+++ b/internal/api/oidc/token_client_credentials.go
@@ -46,6 +46,9 @@ func (s *Server) ClientCredentialsExchange(ctx context.Context, r *op.ClientRequ
nil,
false,
)
+ if err != nil {
+ return nil, err
+ }
- return response(s.accessTokenResponseFromSession(ctx, client, session, "", "", false))
+ return response(s.accessTokenResponseFromSession(ctx, client, session, "", "", false, true, false, false))
}
diff --git a/internal/api/oidc/token_client_credentials_integration_test.go b/internal/api/oidc/token_client_credentials_integration_test.go
index 3517438596..21a1c4de75 100644
--- a/internal/api/oidc/token_client_credentials_integration_test.go
+++ b/internal/api/oidc/token_client_credentials_integration_test.go
@@ -4,6 +4,7 @@ package oidc_test
import (
"testing"
+ "time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
@@ -18,10 +19,13 @@ import (
)
func TestServer_ClientCredentialsExchange(t *testing.T) {
- userID, clientID, clientSecret, err := Tester.CreateOIDCCredentialsClient(CTX)
+ machine, name, clientID, clientSecret, err := Tester.CreateOIDCCredentialsClient(CTX)
require.NoError(t, err)
type claims struct {
+ name string
+ username string
+ updated time.Time
resourceOwnerID any
resourceOwnerName any
resourceOwnerPrimaryDomain any
@@ -78,6 +82,17 @@ func TestServer_ClientCredentialsExchange(t *testing.T) {
clientSecret: clientSecret,
scope: []string{oidc.ScopeOpenID},
},
+ {
+ name: "openid, profile, email",
+ clientID: clientID,
+ clientSecret: clientSecret,
+ scope: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail},
+ wantClaims: claims{
+ name: name,
+ username: name,
+ updated: machine.GetDetails().GetChangeDate().AsTime(),
+ },
+ },
{
name: "org id and domain scope",
clientID: clientID,
@@ -132,12 +147,20 @@ func TestServer_ClientCredentialsExchange(t *testing.T) {
}
require.NoError(t, err)
require.NotNil(t, tokens)
- userinfo, err := rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, oidc.BearerToken, userID, provider)
+ userinfo, err := rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, oidc.BearerToken, machine.GetUserId(), provider)
require.NoError(t, err)
assert.Equal(t, tt.wantClaims.resourceOwnerID, userinfo.Claims[oidc_api.ClaimResourceOwnerID])
assert.Equal(t, tt.wantClaims.resourceOwnerName, userinfo.Claims[oidc_api.ClaimResourceOwnerName])
assert.Equal(t, tt.wantClaims.resourceOwnerPrimaryDomain, userinfo.Claims[oidc_api.ClaimResourceOwnerPrimaryDomain])
assert.Equal(t, tt.wantClaims.orgDomain, userinfo.Claims[domain.OrgDomainPrimaryClaim])
+ assert.Equal(t, tt.wantClaims.name, userinfo.Name)
+ assert.Equal(t, tt.wantClaims.username, userinfo.PreferredUsername)
+ assertOIDCTime(t, userinfo.UpdatedAt, tt.wantClaims.updated)
+ assert.Empty(t, userinfo.UserInfoProfile.FamilyName)
+ assert.Empty(t, userinfo.UserInfoProfile.GivenName)
+ assert.Empty(t, userinfo.UserInfoEmail)
+ assert.Empty(t, userinfo.UserInfoPhone)
+ assert.Empty(t, userinfo.Address)
})
}
}
diff --git a/internal/api/oidc/token_code.go b/internal/api/oidc/token_code.go
index 85aa847579..2e47c55641 100644
--- a/internal/api/oidc/token_code.go
+++ b/internal/api/oidc/token_code.go
@@ -34,41 +34,40 @@ func (s *Server) CodeExchange(ctx context.Context, r *op.ClientRequest[oidc.Acce
var (
session *command.OIDCSession
- state string
)
if strings.HasPrefix(plainCode, command.IDPrefixV2) {
- session, state, err = s.command.CreateOIDCSessionFromAuthRequest(
+ session, _, err = s.command.CreateOIDCSessionFromAuthRequest(
setContextUserSystem(ctx),
plainCode,
codeExchangeComplianceChecker(client, r.Data),
slices.Contains(client.GrantTypes(), oidc.GrantTypeRefreshToken),
)
} else {
- session, state, err = s.codeExchangeV1(ctx, client, r.Data, r.Data.Code)
+ session, err = s.codeExchangeV1(ctx, client, r.Data, r.Data.Code)
}
if err != nil {
return nil, err
}
- return response(s.accessTokenResponseFromSession(ctx, client, session, state, client.client.ProjectID, client.client.ProjectRoleAssertion))
+ return response(s.accessTokenResponseFromSession(ctx, client, session, "", client.client.ProjectID, client.client.ProjectRoleAssertion, client.client.AccessTokenRoleAssertion, client.client.IDTokenRoleAssertion, client.client.IDTokenUserinfoAssertion))
}
// codeExchangeV1 creates a v2 token from a v1 auth request.
-func (s *Server) codeExchangeV1(ctx context.Context, client *Client, req *oidc.AccessTokenRequest, code string) (session *command.OIDCSession, state string, err error) {
+func (s *Server) codeExchangeV1(ctx context.Context, client *Client, req *oidc.AccessTokenRequest, code string) (session *command.OIDCSession, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
authReq, err := s.getAuthRequestV1ByCode(ctx, code)
if err != nil {
- return nil, "", err
+ return nil, err
}
if challenge := authReq.GetCodeChallenge(); challenge != nil || client.AuthMethod() == oidc.AuthMethodNone {
if err = op.AuthorizeCodeChallenge(req.CodeVerifier, challenge); err != nil {
- return nil, "", err
+ return nil, err
}
}
if req.RedirectURI != authReq.GetRedirectURI() {
- return nil, "", oidc.ErrInvalidGrant().WithDescription("redirect_uri does not correspond")
+ return nil, oidc.ErrInvalidGrant().WithDescription("redirect_uri does not correspond")
}
scope := authReq.GetScopes()
@@ -88,9 +87,9 @@ func (s *Server) codeExchangeV1(ctx context.Context, client *Client, req *oidc.A
slices.Contains(scope, oidc.ScopeOfflineAccess),
)
if err != nil {
- return nil, "", err
+ return nil, err
}
- return session, authReq.TransferState, s.repo.DeleteAuthRequest(ctx, authReq.ID)
+ return session, s.repo.DeleteAuthRequest(ctx, authReq.ID)
}
// getAuthRequestV1ByCode finds the v1 auth request by code.
diff --git a/internal/api/oidc/token_device.go b/internal/api/oidc/token_device.go
index c70fa9c1e9..b574af1260 100644
--- a/internal/api/oidc/token_device.go
+++ b/internal/api/oidc/token_device.go
@@ -26,7 +26,7 @@ func (s *Server) DeviceToken(ctx context.Context, r *op.ClientRequest[oidc.Devic
}
session, err := s.command.CreateOIDCSessionFromDeviceAuth(ctx, r.Data.DeviceCode)
if err == nil {
- return response(s.accessTokenResponseFromSession(ctx, client, session, "", client.client.ProjectID, client.client.ProjectRoleAssertion))
+ return response(s.accessTokenResponseFromSession(ctx, client, session, "", client.client.ProjectID, client.client.ProjectRoleAssertion, client.client.AccessTokenRoleAssertion, client.client.IDTokenRoleAssertion, client.client.IDTokenUserinfoAssertion))
}
if errors.Is(err, context.DeadlineExceeded) {
return nil, oidc.ErrSlowDown().WithParent(err)
diff --git a/internal/api/oidc/token_exchange.go b/internal/api/oidc/token_exchange.go
index 31b1a37db3..c29b7eb80b 100644
--- a/internal/api/oidc/token_exchange.go
+++ b/internal/api/oidc/token_exchange.go
@@ -176,7 +176,7 @@ func (s *Server) jwtProfileUserCheck(ctx context.Context, resourceOwner *string,
}
func validateTokenExchangeScopes(client *Client, requestedScopes, subjectScopes, actorScopes []string) ([]string, error) {
- // Scope always has 1 empty string is the space delimited array was an empty string.
+ // Scope always has 1 empty string if the space delimited array was an empty string.
scopes := slices.DeleteFunc(requestedScopes, func(s string) bool {
return s == ""
})
@@ -218,7 +218,7 @@ func validateTokenExchangeAudience(requestedAudience, subjectAudience, actorAudi
// Both tokens may point to the same object (subjectToken) in case of a regular Token Exchange.
// When the subject and actor Tokens point to different objects, the new tokens will be for impersonation / delegation.
func (s *Server) createExchangeTokens(ctx context.Context, tokenType oidc.TokenType, client *Client, subjectToken, actorToken *exchangeToken, audience, scopes []string) (_ *oidc.TokenExchangeResponse, err error) {
- getUserInfo := s.getUserInfoOnce(subjectToken.userID, client.client.ProjectID, client.client.ProjectRoleAssertion, scopes)
+ getUserInfo := s.getUserInfo(subjectToken.userID, client.client.ProjectID, client.client.ProjectRoleAssertion, client.IDTokenUserinfoClaimsAssertion(), scopes)
getSigner := s.getSignerOnce()
resp := &oidc.TokenExchangeResponse{
@@ -240,12 +240,12 @@ func (s *Server) createExchangeTokens(ctx context.Context, tokenType oidc.TokenT
resp.IssuedTokenType = oidc.AccessTokenType
case oidc.JWTTokenType:
- resp.AccessToken, resp.RefreshToken, resp.ExpiresIn, err = s.createExchangeJWT(ctx, client, getUserInfo, getSigner, subjectToken.userID, subjectToken.resourceOwner, audience, scopes, actorToken.authMethods, actorToken.authTime, subjectToken.preferredLanguage, reason, actor)
+ resp.AccessToken, resp.RefreshToken, resp.ExpiresIn, err = s.createExchangeJWT(ctx, client, getUserInfo, client.client.AccessTokenRoleAssertion, getSigner, subjectToken.userID, subjectToken.resourceOwner, audience, scopes, actorToken.authMethods, actorToken.authTime, subjectToken.preferredLanguage, reason, actor)
resp.TokenType = oidc.BearerToken
resp.IssuedTokenType = oidc.JWTTokenType
case oidc.IDTokenType:
- resp.AccessToken, resp.ExpiresIn, err = s.createIDToken(ctx, client, getUserInfo, getSigner, "", resp.AccessToken, audience, actorToken.authMethods, actorToken.authTime, "", actor)
+ resp.AccessToken, resp.ExpiresIn, err = s.createIDToken(ctx, client, getUserInfo, client.client.IDTokenRoleAssertion, getSigner, "", resp.AccessToken, audience, actorToken.authMethods, actorToken.authTime, "", actor)
resp.TokenType = TokenTypeNA
resp.IssuedTokenType = oidc.IDTokenType
@@ -259,7 +259,7 @@ func (s *Server) createExchangeTokens(ctx context.Context, tokenType oidc.TokenT
}
if slices.Contains(scopes, oidc.ScopeOpenID) && tokenType != oidc.IDTokenType {
- resp.IDToken, _, err = s.createIDToken(ctx, client, getUserInfo, getSigner, sessionID, resp.AccessToken, audience, actorToken.authMethods, actorToken.authTime, "", actor)
+ resp.IDToken, _, err = s.createIDToken(ctx, client, getUserInfo, client.client.IDTokenRoleAssertion, getSigner, sessionID, resp.AccessToken, audience, actorToken.authMethods, actorToken.authTime, "", actor)
if err != nil {
return nil, err
}
@@ -313,6 +313,7 @@ func (s *Server) createExchangeJWT(
ctx context.Context,
client *Client,
getUserInfo userInfoFunc,
+ roleAssertion bool,
getSigner signerFunc,
userID,
resourceOwner string,
@@ -342,7 +343,7 @@ func (s *Server) createExchangeJWT(
actor,
slices.Contains(scope, oidc.ScopeOfflineAccess),
)
- accessToken, err = s.createJWT(ctx, client, session, getUserInfo, getSigner)
+ accessToken, err = s.createJWT(ctx, client, session, getUserInfo, roleAssertion, getSigner)
if err != nil {
return "", "", 0, err
}
diff --git a/internal/api/oidc/token_jwt_profile.go b/internal/api/oidc/token_jwt_profile.go
index b23a24f77f..399fa5302e 100644
--- a/internal/api/oidc/token_jwt_profile.go
+++ b/internal/api/oidc/token_jwt_profile.go
@@ -54,7 +54,10 @@ func (s *Server) JWTProfile(ctx context.Context, r *op.Request[oidc.JWTProfileGr
nil,
false,
)
- return response(s.accessTokenResponseFromSession(ctx, client, session, "", "", false))
+ if err != nil {
+ return nil, err
+ }
+ return response(s.accessTokenResponseFromSession(ctx, client, session, "", "", false, true, false, false))
}
func (s *Server) verifyJWTProfile(ctx context.Context, req *oidc.JWTProfileGrantRequest) (user *query.User, tokenRequest *oidc.JWTTokenRequest, err error) {
diff --git a/internal/api/oidc/token_jwt_profile_integration_test.go b/internal/api/oidc/token_jwt_profile_integration_test.go
index b80ea09bcd..0ad8d76da2 100644
--- a/internal/api/oidc/token_jwt_profile_integration_test.go
+++ b/internal/api/oidc/token_jwt_profile_integration_test.go
@@ -4,6 +4,7 @@ package oidc_test
import (
"testing"
+ "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -16,10 +17,13 @@ import (
)
func TestServer_JWTProfile(t *testing.T) {
- userID, keyData, err := Tester.CreateOIDCJWTProfileClient(CTX)
+ user, name, keyData, err := Tester.CreateOIDCJWTProfileClient(CTX)
require.NoError(t, err)
type claims struct {
+ name string
+ username string
+ updated time.Time
resourceOwnerID any
resourceOwnerName any
resourceOwnerPrimaryDomain any
@@ -37,6 +41,16 @@ func TestServer_JWTProfile(t *testing.T) {
keyData: keyData,
scope: []string{oidc.ScopeOpenID},
},
+ {
+ name: "openid, profile, email",
+ keyData: keyData,
+ scope: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail},
+ wantClaims: claims{
+ name: name,
+ username: name,
+ updated: user.GetDetails().GetChangeDate().AsTime(),
+ },
+ },
{
name: "org id and domain scope",
keyData: keyData,
@@ -92,12 +106,20 @@ func TestServer_JWTProfile(t *testing.T) {
provider, err := rp.NewRelyingPartyOIDC(CTX, Tester.OIDCIssuer(), "", "", redirectURI, tt.scope)
require.NoError(t, err)
- userinfo, err := rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, oidc.BearerToken, userID, provider)
+ userinfo, err := rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, oidc.BearerToken, user.GetUserId(), provider)
require.NoError(t, err)
assert.Equal(t, tt.wantClaims.resourceOwnerID, userinfo.Claims[oidc_api.ClaimResourceOwnerID])
assert.Equal(t, tt.wantClaims.resourceOwnerName, userinfo.Claims[oidc_api.ClaimResourceOwnerName])
assert.Equal(t, tt.wantClaims.resourceOwnerPrimaryDomain, userinfo.Claims[oidc_api.ClaimResourceOwnerPrimaryDomain])
assert.Equal(t, tt.wantClaims.orgDomain, userinfo.Claims[domain.OrgDomainPrimaryClaim])
+ assert.Equal(t, tt.wantClaims.name, userinfo.Name)
+ assert.Equal(t, tt.wantClaims.username, userinfo.PreferredUsername)
+ assertOIDCTime(t, userinfo.UpdatedAt, tt.wantClaims.updated)
+ assert.Empty(t, userinfo.UserInfoProfile.FamilyName)
+ assert.Empty(t, userinfo.UserInfoProfile.GivenName)
+ assert.Empty(t, userinfo.UserInfoEmail)
+ assert.Empty(t, userinfo.UserInfoPhone)
+ assert.Empty(t, userinfo.Address)
})
}
}
diff --git a/internal/api/oidc/token_refresh.go b/internal/api/oidc/token_refresh.go
index 66a8b8a263..1dcce2879a 100644
--- a/internal/api/oidc/token_refresh.go
+++ b/internal/api/oidc/token_refresh.go
@@ -28,7 +28,7 @@ func (s *Server) RefreshToken(ctx context.Context, r *op.ClientRequest[oidc.Refr
session, err := s.command.ExchangeOIDCSessionRefreshAndAccessToken(ctx, r.Data.RefreshToken, r.Data.Scopes, refreshTokenComplianceChecker())
if err == nil {
- return response(s.accessTokenResponseFromSession(ctx, client, session, "", client.client.ProjectID, client.client.ProjectRoleAssertion))
+ return response(s.accessTokenResponseFromSession(ctx, client, session, "", client.client.ProjectID, client.client.ProjectRoleAssertion, client.client.AccessTokenRoleAssertion, client.client.IDTokenRoleAssertion, client.client.IDTokenUserinfoAssertion))
} else if errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "OIDCS-JOI23", "Errors.OIDCSession.RefreshTokenInvalid")) {
// We try again for v1 tokens when we encountered specific parsing error
return s.refreshTokenV1(ctx, client, r)
@@ -78,7 +78,7 @@ func (s *Server) refreshTokenV1(ctx context.Context, client *Client, r *op.Clien
return nil, err
}
- return response(s.accessTokenResponseFromSession(ctx, client, session, "", client.client.ProjectID, client.client.ProjectRoleAssertion))
+ return response(s.accessTokenResponseFromSession(ctx, client, session, "", client.client.ProjectID, client.client.ProjectRoleAssertion, client.client.AccessTokenRoleAssertion, client.client.IDTokenRoleAssertion, client.client.IDTokenUserinfoAssertion))
}
// refreshTokenComplianceChecker validates that the requested scope is a subset of the original auth request scope.
diff --git a/internal/api/oidc/userinfo.go b/internal/api/oidc/userinfo.go
index c21b746c49..415c337c76 100644
--- a/internal/api/oidc/userinfo.go
+++ b/internal/api/oidc/userinfo.go
@@ -5,9 +5,11 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
+ "maps"
"net/http"
"slices"
"strings"
+ "sync"
"github.com/dop251/goja"
"github.com/zitadel/logging"
@@ -55,7 +57,14 @@ func (s *Server) UserInfo(ctx context.Context, r *op.Request[oidc.UserInfoReques
}
}
- userInfo, err := s.userInfo(ctx, token.userID, token.scope, projectID, assertion, false)
+ userInfo, err := s.userInfo(
+ token.userID,
+ token.scope,
+ projectID,
+ assertion,
+ true,
+ false,
+ )(ctx, true, domain.TriggerTypePreUserinfoCreation)
if err != nil {
return nil, err
}
@@ -66,24 +75,53 @@ func (s *Server) UserInfo(ctx context.Context, r *op.Request[oidc.UserInfoReques
// The returned UserInfo contains standard and reserved claims, documented
// here: https://zitadel.com/docs/apis/openidoauth/claims.
//
+// User information is only retrieved once from the database.
+// However, each time, role claims are asserted and also action flows will trigger.
+//
// projectID is an optional parameter which defines the default audience when there are any (or all) role claims requested.
// projectRoleAssertion sets the default of returning all project roles, only if no specific roles were requested in the scope.
+// roleAssertion decides whether the roles will be returned (in the token or response)
+// userInfoAssertion decides whether the user information (profile data like name, email, ...) are returned
//
// currentProjectOnly can be set to use the current project ID only and ignore the audience from the scope.
// It should be set in cases where the client doesn't need to know roles outside its own project,
// for example an introspection client.
-func (s *Server) userInfo(ctx context.Context, userID string, scope []string, projectID string, projectRoleAssertion, currentProjectOnly bool) (_ *oidc.UserInfo, err error) {
- ctx, span := tracing.NewSpan(ctx)
- defer func() { span.EndWithError(err) }()
+func (s *Server) userInfo(
+ userID string,
+ scope []string,
+ projectID string,
+ projectRoleAssertion, userInfoAssertion, currentProjectOnly bool,
+) func(ctx context.Context, roleAssertion bool, triggerType domain.TriggerType) (_ *oidc.UserInfo, err error) {
+ var (
+ once sync.Once
+ rawUserInfo *oidc.UserInfo
+ qu *query.OIDCUserInfo
+ roleAudience, requestedRoles []string
+ )
+ return func(ctx context.Context, roleAssertion bool, triggerType domain.TriggerType) (_ *oidc.UserInfo, err error) {
+ once.Do(func() {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
- roleAudience, requestedRoles := prepareRoles(ctx, scope, projectID, projectRoleAssertion, currentProjectOnly)
- qu, err := s.query.GetOIDCUserInfo(ctx, userID, roleAudience)
- if err != nil {
- return nil, err
+ roleAudience, requestedRoles = prepareRoles(ctx, scope, projectID, projectRoleAssertion, currentProjectOnly)
+ qu, err = s.query.GetOIDCUserInfo(ctx, userID, roleAudience)
+ if err != nil {
+ return
+ }
+ rawUserInfo = userInfoToOIDC(qu, userInfoAssertion, scope, s.assetAPIPrefix(ctx))
+ })
+ // copy the userinfo to make sure the assert roles and actions use their own copy (e.g. map)
+ userInfo := &oidc.UserInfo{
+ Subject: rawUserInfo.Subject,
+ UserInfoProfile: rawUserInfo.UserInfoProfile,
+ UserInfoEmail: rawUserInfo.UserInfoEmail,
+ UserInfoPhone: rawUserInfo.UserInfoPhone,
+ Address: rawUserInfo.Address,
+ Claims: maps.Clone(rawUserInfo.Claims),
+ }
+ assertRoles(projectID, qu, roleAudience, requestedRoles, roleAssertion, userInfo)
+ return userInfo, s.userinfoFlows(ctx, qu, userInfo, triggerType)
}
-
- userInfo := userInfoToOIDC(projectID, qu, scope, roleAudience, requestedRoles, s.assetAPIPrefix(ctx))
- return userInfo, s.userinfoFlows(ctx, qu, userInfo)
}
// prepareRoles scans the requested scopes and builds the requested roles
@@ -120,20 +158,32 @@ func prepareRoles(ctx context.Context, scope []string, projectID string, project
return roleAudience, requestedRoles
}
-func userInfoToOIDC(projectID string, user *query.OIDCUserInfo, scope, roleAudience, requestedRoles []string, assetPrefix string) *oidc.UserInfo {
+func userInfoToOIDC(user *query.OIDCUserInfo, userInfoAssertion bool, scope []string, assetPrefix string) *oidc.UserInfo {
out := new(oidc.UserInfo)
for _, s := range scope {
switch s {
case oidc.ScopeOpenID:
out.Subject = user.User.ID
case oidc.ScopeEmail:
+ if !userInfoAssertion {
+ continue
+ }
out.UserInfoEmail = userInfoEmailToOIDC(user.User)
case oidc.ScopeProfile:
+ if !userInfoAssertion {
+ continue
+ }
out.UserInfoProfile = userInfoProfileToOidc(user.User, assetPrefix)
case oidc.ScopePhone:
+ if !userInfoAssertion {
+ continue
+ }
out.UserInfoPhone = userInfoPhoneToOIDC(user.User)
case oidc.ScopeAddress:
- //TODO: handle address for human users as soon as implemented
+ if !userInfoAssertion {
+ continue
+ }
+ // TODO: handle address for human users as soon as implemented
case ScopeUserMetaData:
setUserInfoMetadata(user.Metadata, out)
case ScopeResourceOwner:
@@ -148,12 +198,17 @@ func userInfoToOIDC(projectID string, user *query.OIDCUserInfo, scope, roleAudie
}
}
}
+ return out
+}
+func assertRoles(projectID string, user *query.OIDCUserInfo, roleAudience, requestedRoles []string, assertion bool, info *oidc.UserInfo) {
+ if !assertion {
+ return
+ }
// prevent returning obtained grants if none where requested
if (projectID != "" && len(requestedRoles) > 0) || len(roleAudience) > 0 {
- setUserInfoRoleClaims(out, newProjectRoles(projectID, user.UserGrants, requestedRoles))
+ setUserInfoRoleClaims(info, newProjectRoles(projectID, user.UserGrants, requestedRoles))
}
- return out
}
func userInfoEmailToOIDC(user *query.User) oidc.UserInfoEmail {
@@ -230,11 +285,11 @@ func setUserInfoRoleClaims(userInfo *oidc.UserInfo, roles *projectsRoles) {
}
}
-func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, userInfo *oidc.UserInfo) (err error) {
+func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, userInfo *oidc.UserInfo, triggerType domain.TriggerType) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
- queriedActions, err := s.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeCustomiseToken, domain.TriggerTypePreUserinfoCreation, qu.User.ResourceOwner)
+ queriedActions, err := s.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeCustomiseToken, triggerType, qu.User.ResourceOwner)
if err != nil {
return err
}
diff --git a/internal/api/oidc/userinfo_integration_test.go b/internal/api/oidc/userinfo_integration_test.go
index 22e688ff4b..7f39ed38ba 100644
--- a/internal/api/oidc/userinfo_integration_test.go
+++ b/internal/api/oidc/userinfo_integration_test.go
@@ -231,9 +231,9 @@ func TestServer_UserInfo_Issue6662(t *testing.T) {
project, err := Tester.CreateProject(CTX)
projectID := project.GetId()
require.NoError(t, err)
- userID, clientID, clientSecret, err := Tester.CreateOIDCCredentialsClient(CTX)
+ user, _, clientID, clientSecret, err := Tester.CreateOIDCCredentialsClient(CTX)
require.NoError(t, err)
- addProjectRolesGrants(t, userID, projectID, roleFoo, roleBar)
+ addProjectRolesGrants(t, user.GetUserId(), projectID, roleFoo, roleBar)
scope := []string{oidc.ScopeProfile, oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeOfflineAccess,
oidc_api.ScopeProjectRolePrefix + roleFoo,
@@ -245,7 +245,7 @@ func TestServer_UserInfo_Issue6662(t *testing.T) {
tokens, err := rp.ClientCredentials(CTX, provider, nil)
require.NoError(t, err)
- userinfo, err := rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, userID, provider)
+ userinfo, err := rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, user.GetUserId(), provider)
require.NoError(t, err)
assertProjectRoleClaims(t, projectID, userinfo.Claims, false, roleFoo)
}
@@ -291,7 +291,7 @@ func getTokens(t *testing.T, clientID string, scope []string) *oidc.Tokens[*oidc
tokens, err := exchangeTokens(t, clientID, code, redirectURI)
require.NoError(t, err)
assertTokens(t, tokens, true)
- assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime)
+ assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID)
return tokens
}
diff --git a/internal/api/oidc/userinfo_test.go b/internal/api/oidc/userinfo_test.go
index 65354a4040..741f7eed36 100644
--- a/internal/api/oidc/userinfo_test.go
+++ b/internal/api/oidc/userinfo_test.go
@@ -3,7 +3,6 @@ package oidc
import (
"context"
"encoding/base64"
- "fmt"
"testing"
"time"
@@ -267,11 +266,9 @@ func Test_userInfoToOIDC(t *testing.T) {
}
type args struct {
- projectID string
- user *query.OIDCUserInfo
- scope []string
- roleAudience []string
- requestedRoles []string
+ user *query.OIDCUserInfo
+ userInfoAssertion bool
+ scope []string
}
tests := []struct {
name string
@@ -281,25 +278,22 @@ func Test_userInfoToOIDC(t *testing.T) {
{
name: "human, empty",
args: args{
- projectID: "project1",
- user: humanUserInfo,
+ user: humanUserInfo,
},
want: &oidc.UserInfo{},
},
{
name: "machine, empty",
args: args{
- projectID: "project1",
- user: machineUserInfo,
+ user: machineUserInfo,
},
want: &oidc.UserInfo{},
},
{
name: "human, scope openid",
args: args{
- projectID: "project1",
- user: humanUserInfo,
- scope: []string{oidc.ScopeOpenID},
+ user: humanUserInfo,
+ scope: []string{oidc.ScopeOpenID},
},
want: &oidc.UserInfo{
Subject: "human1",
@@ -308,20 +302,19 @@ func Test_userInfoToOIDC(t *testing.T) {
{
name: "machine, scope openid",
args: args{
- projectID: "project1",
- user: machineUserInfo,
- scope: []string{oidc.ScopeOpenID},
+ user: machineUserInfo,
+ scope: []string{oidc.ScopeOpenID},
},
want: &oidc.UserInfo{
Subject: "machine1",
},
},
{
- name: "human, scope email",
+ name: "human, scope email, profileInfoAssertion",
args: args{
- projectID: "project1",
- user: humanUserInfo,
- scope: []string{oidc.ScopeEmail},
+ user: humanUserInfo,
+ userInfoAssertion: true,
+ scope: []string{oidc.ScopeEmail},
},
want: &oidc.UserInfo{
UserInfoEmail: oidc.UserInfoEmail{
@@ -331,22 +324,29 @@ func Test_userInfoToOIDC(t *testing.T) {
},
},
{
- name: "machine, scope email",
+ name: "human, scope email",
args: args{
- projectID: "project1",
- user: machineUserInfo,
- scope: []string{oidc.ScopeEmail},
+ user: humanUserInfo,
+ scope: []string{oidc.ScopeEmail},
+ },
+ want: &oidc.UserInfo{},
+ },
+ {
+ name: "machine, scope email, profileInfoAssertion",
+ args: args{
+ user: machineUserInfo,
+ scope: []string{oidc.ScopeEmail},
},
want: &oidc.UserInfo{
UserInfoEmail: oidc.UserInfoEmail{},
},
},
{
- name: "human, scope profile",
+ name: "human, scope profile, profileInfoAssertion",
args: args{
- projectID: "project1",
- user: humanUserInfo,
- scope: []string{oidc.ScopeProfile},
+ user: humanUserInfo,
+ userInfoAssertion: true,
+ scope: []string{oidc.ScopeProfile},
},
want: &oidc.UserInfo{
UserInfoProfile: oidc.UserInfoProfile{
@@ -363,11 +363,11 @@ func Test_userInfoToOIDC(t *testing.T) {
},
},
{
- name: "machine, scope profile",
+ name: "machine, scope profile, profileInfoAssertion",
args: args{
- projectID: "project1",
- user: machineUserInfo,
- scope: []string{oidc.ScopeProfile},
+ user: machineUserInfo,
+ userInfoAssertion: true,
+ scope: []string{oidc.ScopeProfile},
},
want: &oidc.UserInfo{
UserInfoProfile: oidc.UserInfoProfile{
@@ -378,11 +378,19 @@ func Test_userInfoToOIDC(t *testing.T) {
},
},
{
- name: "human, scope phone",
+ name: "machine, scope profile",
args: args{
- projectID: "project1",
- user: humanUserInfo,
- scope: []string{oidc.ScopePhone},
+ user: machineUserInfo,
+ scope: []string{oidc.ScopeProfile},
+ },
+ want: &oidc.UserInfo{},
+ },
+ {
+ name: "human, scope phone, profileInfoAssertion",
+ args: args{
+ user: humanUserInfo,
+ userInfoAssertion: true,
+ scope: []string{oidc.ScopePhone},
},
want: &oidc.UserInfo{
UserInfoPhone: oidc.UserInfoPhone{
@@ -391,12 +399,19 @@ func Test_userInfoToOIDC(t *testing.T) {
},
},
},
+ {
+ name: "human, scope phone",
+ args: args{
+ user: humanUserInfo,
+ scope: []string{oidc.ScopePhone},
+ },
+ want: &oidc.UserInfo{},
+ },
{
name: "machine, scope phone",
args: args{
- projectID: "project1",
- user: machineUserInfo,
- scope: []string{oidc.ScopePhone},
+ user: machineUserInfo,
+ scope: []string{oidc.ScopePhone},
},
want: &oidc.UserInfo{
UserInfoPhone: oidc.UserInfoPhone{},
@@ -405,9 +420,8 @@ func Test_userInfoToOIDC(t *testing.T) {
{
name: "human, scope metadata",
args: args{
- projectID: "project1",
- user: humanUserInfo,
- scope: []string{ScopeUserMetaData},
+ user: humanUserInfo,
+ scope: []string{ScopeUserMetaData},
},
want: &oidc.UserInfo{
Claims: map[string]any{
@@ -421,18 +435,16 @@ func Test_userInfoToOIDC(t *testing.T) {
{
name: "machine, scope metadata, none found",
args: args{
- projectID: "project1",
- user: machineUserInfo,
- scope: []string{ScopeUserMetaData},
+ user: machineUserInfo,
+ scope: []string{ScopeUserMetaData},
},
want: &oidc.UserInfo{},
},
{
name: "machine, scope resource owner",
args: args{
- projectID: "project1",
- user: machineUserInfo,
- scope: []string{ScopeResourceOwner},
+ user: machineUserInfo,
+ scope: []string{ScopeResourceOwner},
},
want: &oidc.UserInfo{
Claims: map[string]any{
@@ -445,9 +457,8 @@ func Test_userInfoToOIDC(t *testing.T) {
{
name: "human, scope org primary domain prefix",
args: args{
- projectID: "project1",
- user: humanUserInfo,
- scope: []string{domain.OrgDomainPrimaryScope + "foo.com"},
+ user: humanUserInfo,
+ scope: []string{domain.OrgDomainPrimaryScope + "foo.com"},
},
want: &oidc.UserInfo{
Claims: map[string]any{
@@ -458,9 +469,8 @@ func Test_userInfoToOIDC(t *testing.T) {
{
name: "machine, scope org id",
args: args{
- projectID: "project1",
- user: machineUserInfo,
- scope: []string{domain.OrgIDScope + "orgID"},
+ user: machineUserInfo,
+ scope: []string{domain.OrgIDScope + "orgID"},
},
want: &oidc.UserInfo{
Claims: map[string]any{
@@ -471,50 +481,11 @@ func Test_userInfoToOIDC(t *testing.T) {
},
},
},
- {
- name: "human, roleAudience",
- args: args{
- projectID: "project1",
- user: humanUserInfo,
- roleAudience: []string{"project1"},
- },
- want: &oidc.UserInfo{
- Claims: map[string]any{
- ClaimProjectRoles: projectRoles{
- "role1": {"orgID": "orgDomain"},
- "role2": {"orgID": "orgDomain"},
- },
- fmt.Sprintf(ClaimProjectRolesFormat, "project1"): projectRoles{
- "role1": {"orgID": "orgDomain"},
- "role2": {"orgID": "orgDomain"},
- },
- },
- },
- },
- {
- name: "human, requested roles",
- args: args{
- projectID: "project1",
- user: humanUserInfo,
- roleAudience: []string{"project1"},
- requestedRoles: []string{"role2"},
- },
- want: &oidc.UserInfo{
- Claims: map[string]any{
- ClaimProjectRoles: projectRoles{
- "role2": {"orgID": "orgDomain"},
- },
- fmt.Sprintf(ClaimProjectRolesFormat, "project1"): projectRoles{
- "role2": {"orgID": "orgDomain"},
- },
- },
- },
- },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assetPrefix := "https://foo.com/assets"
- got := userInfoToOIDC(tt.args.projectID, tt.args.user, tt.args.scope, tt.args.roleAudience, tt.args.requestedRoles, assetPrefix)
+ got := userInfoToOIDC(tt.args.user, tt.args.userInfoAssertion, tt.args.scope, assetPrefix)
assert.Equal(t, tt.want, got)
})
}
diff --git a/internal/api/ui/login/auth_request.go b/internal/api/ui/login/auth_request.go
index f2dcce57da..4da045efac 100644
--- a/internal/api/ui/login/auth_request.go
+++ b/internal/api/ui/login/auth_request.go
@@ -7,6 +7,7 @@ import (
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/zerrors"
)
const (
@@ -23,6 +24,14 @@ func (l *Login) getAuthRequest(r *http.Request) (*domain.AuthRequest, error) {
return l.authRepo.AuthRequestByID(r.Context(), authRequestID, userAgentID)
}
+func (l *Login) ensureAuthRequest(r *http.Request) (*domain.AuthRequest, error) {
+ authRequest, err := l.getAuthRequest(r)
+ if authRequest != nil || err != nil {
+ return authRequest, err
+ }
+ return nil, zerrors.ThrowInvalidArgument(nil, "LOGIN-OLah9", "invalid or missing auth request")
+}
+
func (l *Login) getAuthRequestAndParseData(r *http.Request, data interface{}) (*domain.AuthRequest, error) {
authReq, err := l.getAuthRequest(r)
if err != nil {
@@ -32,6 +41,15 @@ func (l *Login) getAuthRequestAndParseData(r *http.Request, data interface{}) (*
return authReq, err
}
+func (l *Login) ensureAuthRequestAndParseData(r *http.Request, data interface{}) (*domain.AuthRequest, error) {
+ authReq, err := l.ensureAuthRequest(r)
+ if err != nil {
+ return authReq, err
+ }
+ err = l.parser.Parse(r, data)
+ return authReq, err
+}
+
func (l *Login) getParseData(r *http.Request, data interface{}) error {
return l.parser.Parse(r, data)
}
diff --git a/internal/api/ui/login/change_password_handler.go b/internal/api/ui/login/change_password_handler.go
index 1eacdcebd5..5c36d0f74f 100644
--- a/internal/api/ui/login/change_password_handler.go
+++ b/internal/api/ui/login/change_password_handler.go
@@ -20,7 +20,7 @@ type changePasswordData struct {
func (l *Login) handleChangePassword(w http.ResponseWriter, r *http.Request) {
data := new(changePasswordData)
- authReq, err := l.getAuthRequestAndParseData(r, data)
+ authReq, err := l.ensureAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/device_auth.go b/internal/api/ui/login/device_auth.go
index 659f2ac144..0d5349903e 100644
--- a/internal/api/ui/login/device_auth.go
+++ b/internal/api/ui/login/device_auth.go
@@ -14,7 +14,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain"
- "github.com/zitadel/zitadel/internal/zerrors"
)
const (
@@ -145,9 +144,8 @@ func (l *Login) redirectDeviceAuthStart(w http.ResponseWriter, r *http.Request,
// When the action of "allowed" or "denied", the device authorization is updated accordingly.
// Else the user is presented with a page where they can choose / submit either action.
func (l *Login) handleDeviceAuthAction(w http.ResponseWriter, r *http.Request) {
- authReq, err := l.getAuthRequest(r)
- if authReq == nil {
- err = zerrors.ThrowInvalidArgument(err, "LOGIN-OLah8", "invalid or missing auth request")
+ authReq, err := l.ensureAuthRequest(r)
+ if err != nil {
l.redirectDeviceAuthStart(w, r, err.Error())
return
}
diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go
index e03b32086f..ab0d98b997 100644
--- a/internal/api/ui/login/external_provider_handler.go
+++ b/internal/api/ui/login/external_provider_handler.go
@@ -127,7 +127,7 @@ func (l *Login) handleExternalLogin(w http.ResponseWriter, r *http.Request) {
// handleExternalRegister is called when a user selects the idp on the register options page
func (l *Login) handleExternalRegister(w http.ResponseWriter, r *http.Request) {
data := new(externalIDPData)
- authReq, err := l.getAuthRequestAndParseData(r, data)
+ authReq, err := l.ensureAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
@@ -657,7 +657,7 @@ func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Requ
// and either links or creates an externalUser
func (l *Login) handleExternalNotFoundOptionCheck(w http.ResponseWriter, r *http.Request) {
data := new(externalNotFoundOptionFormData)
- authReq, err := l.getAuthRequestAndParseData(r, data)
+ authReq, err := l.ensureAuthRequestAndParseData(r, data)
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err)
return
diff --git a/internal/api/ui/login/init_password_handler.go b/internal/api/ui/login/init_password_handler.go
index d57d1f83ff..91a197ef64 100644
--- a/internal/api/ui/login/init_password_handler.go
+++ b/internal/api/ui/login/init_password_handler.go
@@ -91,16 +91,18 @@ func (l *Login) checkPWCode(w http.ResponseWriter, r *http.Request, authReq *dom
func (l *Login) resendPasswordSet(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *initPasswordFormData) {
userOrg := data.OrgID
userID := data.UserID
+ var authReqID string
if authReq != nil {
userOrg = authReq.UserOrgID
userID = authReq.UserID
+ authReqID = authReq.ID
}
passwordCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypePasswordResetCode, l.userCodeAlg)
if err != nil {
l.renderInitPassword(w, r, authReq, userID, "", err)
return
}
- _, err = l.command.RequestSetPassword(setContext(r.Context(), userOrg), userID, userOrg, domain.NotificationTypeEmail, passwordCodeGenerator, authReq.ID)
+ _, err = l.command.RequestSetPassword(setContext(r.Context(), userOrg), userID, userOrg, domain.NotificationTypeEmail, passwordCodeGenerator, authReqID)
l.renderInitPassword(w, r, authReq, userID, "", err)
}
diff --git a/internal/api/ui/login/ldap_handler.go b/internal/api/ui/login/ldap_handler.go
index 93590458f6..0fd47c5a6a 100644
--- a/internal/api/ui/login/ldap_handler.go
+++ b/internal/api/ui/login/ldap_handler.go
@@ -42,7 +42,7 @@ func (l *Login) renderLDAPLogin(w http.ResponseWriter, r *http.Request, authReq
func (l *Login) handleLDAPCallback(w http.ResponseWriter, r *http.Request) {
data := new(ldapFormData)
- authReq, err := l.getAuthRequestAndParseData(r, data)
+ authReq, err := l.ensureAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/link_prompt_handler.go b/internal/api/ui/login/link_prompt_handler.go
index 04293bd864..30ac186298 100644
--- a/internal/api/ui/login/link_prompt_handler.go
+++ b/internal/api/ui/login/link_prompt_handler.go
@@ -44,7 +44,7 @@ func (l *Login) renderLinkingUserPrompt(w http.ResponseWriter, r *http.Request,
func (l *Login) handleLinkingUserPrompt(w http.ResponseWriter, r *http.Request) {
data := new(linkingUserPromptFormData)
- authReq, err := l.getAuthRequestAndParseData(r, data)
+ authReq, err := l.ensureAuthRequestAndParseData(r, data)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/login_handler.go b/internal/api/ui/login/login_handler.go
index ae21d84d87..059048eecb 100644
--- a/internal/api/ui/login/login_handler.go
+++ b/internal/api/ui/login/login_handler.go
@@ -69,7 +69,7 @@ func (l *Login) handleLoginNameCheck(w http.ResponseWriter, r *http.Request) {
return
}
if data.Register {
- if authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowExternalIDP && authReq.AllowedExternalIDPs != nil && len(authReq.AllowedExternalIDPs) > 0 {
+ if authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowExternalIDP && authReq.AllowedExternalIDPs != nil && len(authReq.AllowedExternalIDPs) > 0 {
l.handleRegisterOption(w, r)
return
}
diff --git a/internal/api/ui/login/mail_verify_handler.go b/internal/api/ui/login/mail_verify_handler.go
index 327b8a1182..aa14969008 100644
--- a/internal/api/ui/login/mail_verify_handler.go
+++ b/internal/api/ui/login/mail_verify_handler.go
@@ -58,16 +58,17 @@ func (l *Login) handleMailVerificationCheck(w http.ResponseWriter, r *http.Reque
l.checkMailCode(w, r, authReq, data.UserID, data.Code)
return
}
- userOrg := ""
+ var userOrg, authReqID string
if authReq != nil {
userOrg = authReq.UserOrgID
+ authReqID = authReq.ID
}
emailCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyEmailCode, l.userCodeAlg)
if err != nil {
l.checkMailCode(w, r, authReq, data.UserID, data.Code)
return
}
- _, err = l.command.CreateHumanEmailVerificationCode(setContext(r.Context(), userOrg), data.UserID, userOrg, emailCodeGenerator, authReq.ID)
+ _, err = l.command.CreateHumanEmailVerificationCode(setContext(r.Context(), userOrg), data.UserID, userOrg, emailCodeGenerator, authReqID)
l.renderMailVerification(w, r, authReq, data.UserID, err)
}
diff --git a/internal/api/ui/login/mfa_init_sms.go b/internal/api/ui/login/mfa_init_sms.go
index 9aaa744a39..03f2c32014 100644
--- a/internal/api/ui/login/mfa_init_sms.go
+++ b/internal/api/ui/login/mfa_init_sms.go
@@ -71,7 +71,7 @@ func (l *Login) renderRegisterSMS(w http.ResponseWriter, r *http.Request, authRe
// and a successful OTP SMS check will be added to the auth request.
func (l *Login) handleRegisterSMSCheck(w http.ResponseWriter, r *http.Request) {
formData := new(smsInitFormData)
- authReq, err := l.getAuthRequestAndParseData(r, formData)
+ authReq, err := l.ensureAuthRequestAndParseData(r, formData)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/mfa_init_u2f.go b/internal/api/ui/login/mfa_init_u2f.go
index 0b7718bb80..c84948796c 100644
--- a/internal/api/ui/login/mfa_init_u2f.go
+++ b/internal/api/ui/login/mfa_init_u2f.go
@@ -42,7 +42,7 @@ func (l *Login) renderRegisterU2F(w http.ResponseWriter, r *http.Request, authRe
func (l *Login) handleRegisterU2F(w http.ResponseWriter, r *http.Request) {
data := new(webAuthNFormData)
- authReq, err := l.getAuthRequestAndParseData(r, data)
+ authReq, err := l.ensureAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/mfa_init_verify_handler.go b/internal/api/ui/login/mfa_init_verify_handler.go
index 0b66451434..cd6a9091e2 100644
--- a/internal/api/ui/login/mfa_init_verify_handler.go
+++ b/internal/api/ui/login/mfa_init_verify_handler.go
@@ -26,7 +26,7 @@ type mfaInitVerifyData struct {
func (l *Login) handleMFAInitVerify(w http.ResponseWriter, r *http.Request) {
data := new(mfaInitVerifyData)
- authReq, err := l.getAuthRequestAndParseData(r, data)
+ authReq, err := l.ensureAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/mfa_prompt_handler.go b/internal/api/ui/login/mfa_prompt_handler.go
index ade1b53229..ce1b7240ec 100644
--- a/internal/api/ui/login/mfa_prompt_handler.go
+++ b/internal/api/ui/login/mfa_prompt_handler.go
@@ -18,7 +18,7 @@ type mfaPromptData struct {
func (l *Login) handleMFAPrompt(w http.ResponseWriter, r *http.Request) {
data := new(mfaPromptData)
- authReq, err := l.getAuthRequestAndParseData(r, data)
+ authReq, err := l.ensureAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/mfa_verify_handler.go b/internal/api/ui/login/mfa_verify_handler.go
index 80f1c94e25..cfffc6fced 100644
--- a/internal/api/ui/login/mfa_verify_handler.go
+++ b/internal/api/ui/login/mfa_verify_handler.go
@@ -19,7 +19,7 @@ type mfaVerifyFormData struct {
func (l *Login) handleMFAVerify(w http.ResponseWriter, r *http.Request) {
data := new(mfaVerifyFormData)
- authReq, err := l.getAuthRequestAndParseData(r, data)
+ authReq, err := l.ensureAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/mfa_verify_otp_handler.go b/internal/api/ui/login/mfa_verify_otp_handler.go
index 2c0d5d8568..b39605e667 100644
--- a/internal/api/ui/login/mfa_verify_otp_handler.go
+++ b/internal/api/ui/login/mfa_verify_otp_handler.go
@@ -75,8 +75,8 @@ func (l *Login) renderOTPVerification(w http.ResponseWriter, r *http.Request, au
// A user is also able to request a code resend or choose another provider.
func (l *Login) handleOTPVerificationCheck(w http.ResponseWriter, r *http.Request) {
formData := new(mfaOTPFormData)
- authReq, err := l.getAuthRequestAndParseData(r, formData)
- if authReq == nil || err != nil {
+ authReq, err := l.ensureAuthRequestAndParseData(r, formData)
+ if err != nil {
l.renderError(w, r, authReq, err)
return
}
diff --git a/internal/api/ui/login/mfa_verify_u2f_handler.go b/internal/api/ui/login/mfa_verify_u2f_handler.go
index 3afa416823..7873468616 100644
--- a/internal/api/ui/login/mfa_verify_u2f_handler.go
+++ b/internal/api/ui/login/mfa_verify_u2f_handler.go
@@ -50,7 +50,7 @@ func (l *Login) renderU2FVerification(w http.ResponseWriter, r *http.Request, au
func (l *Login) handleU2FVerification(w http.ResponseWriter, r *http.Request) {
formData := new(mfaU2FFormData)
- authReq, err := l.getAuthRequestAndParseData(r, formData)
+ authReq, err := l.ensureAuthRequestAndParseData(r, formData)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/password_handler.go b/internal/api/ui/login/password_handler.go
index 28baf4a1e1..026963bbde 100644
--- a/internal/api/ui/login/password_handler.go
+++ b/internal/api/ui/login/password_handler.go
@@ -34,7 +34,7 @@ func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq *
func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) {
data := new(passwordFormData)
- authReq, err := l.getAuthRequestAndParseData(r, data)
+ authReq, err := l.ensureAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/password_reset_handler.go b/internal/api/ui/login/password_reset_handler.go
index ff57321e92..bc9a5069d1 100644
--- a/internal/api/ui/login/password_reset_handler.go
+++ b/internal/api/ui/login/password_reset_handler.go
@@ -12,7 +12,7 @@ const (
)
func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) {
- authReq, err := l.getAuthRequest(r)
+ authReq, err := l.ensureAuthRequest(r)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/passwordless_login_handler.go b/internal/api/ui/login/passwordless_login_handler.go
index 8373a8fbdb..52b9d06fed 100644
--- a/internal/api/ui/login/passwordless_login_handler.go
+++ b/internal/api/ui/login/passwordless_login_handler.go
@@ -49,7 +49,7 @@ func (l *Login) renderPasswordlessVerification(w http.ResponseWriter, r *http.Re
func (l *Login) handlePasswordlessVerification(w http.ResponseWriter, r *http.Request) {
formData := new(passwordlessFormData)
- authReq, err := l.getAuthRequestAndParseData(r, formData)
+ authReq, err := l.ensureAuthRequestAndParseData(r, formData)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/passwordless_registration_handler.go b/internal/api/ui/login/passwordless_registration_handler.go
index 0346374cee..976a9277b2 100644
--- a/internal/api/ui/login/passwordless_registration_handler.go
+++ b/internal/api/ui/login/passwordless_registration_handler.go
@@ -114,11 +114,11 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re
}
if authReq == nil {
policy, err := l.query.ActiveLabelPolicyByOrg(r.Context(), orgID, false)
- logging.Log("HANDL-XjWKE").OnError(err).Error("unable to get active label policy")
+ logging.OnError(err).Error("unable to get active label policy")
data.LabelPolicy = labelPolicyToDomain(policy)
if err == nil {
texts, err := l.authRepo.GetLoginText(r.Context(), orgID)
- logging.Log("LOGIN-HJK4t").OnError(err).Warn("could not get custom texts")
+ logging.OnError(err).Warn("could not get custom texts")
l.addLoginTranslations(translator, texts)
}
}
diff --git a/internal/api/ui/login/renderer.go b/internal/api/ui/login/renderer.go
index 2154248bae..e6234d9894 100644
--- a/internal/api/ui/login/renderer.go
+++ b/internal/api/ui/login/renderer.go
@@ -342,7 +342,11 @@ func (l *Login) renderInternalError(w http.ResponseWriter, r *http.Request, auth
if authReq != nil {
log = log.WithField("auth_req_id", authReq.ID)
}
- log.Error()
+ if zerrors.IsInternal(err) {
+ log.Error()
+ } else {
+ log.Info()
+ }
_, msg = l.getErrorMessage(r, err)
}
diff --git a/internal/api/ui/login/select_user_handler.go b/internal/api/ui/login/select_user_handler.go
index 3febbaed7a..98c3993376 100644
--- a/internal/api/ui/login/select_user_handler.go
+++ b/internal/api/ui/login/select_user_handler.go
@@ -36,7 +36,7 @@ func (l *Login) renderUserSelection(w http.ResponseWriter, r *http.Request, auth
func (l *Login) handleSelectUser(w http.ResponseWriter, r *http.Request) {
data := new(userSelectionFormData)
- authSession, err := l.getAuthRequestAndParseData(r, data)
+ authSession, err := l.ensureAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authSession, err)
return
diff --git a/internal/api/ui/login/static/i18n/bg.yaml b/internal/api/ui/login/static/i18n/bg.yaml
index 12f5e57bd6..61ef709f69 100644
--- a/internal/api/ui/login/static/i18n/bg.yaml
+++ b/internal/api/ui/login/static/i18n/bg.yaml
@@ -27,19 +27,22 @@ SelectAccount:
SessionState0: активен
SessionState1: Не сте в профила си
MustBeMemberOfOrg: 'Потребителят трябва да е член на {{.OrgName}} организация.'
+
Password:
Title: Парола
- Description: Въведете вашите данни за вход.
+ Description: Въведете данните си за вход.
PasswordLabel: Парола
- MinLength: Минимална дължина
- HasUppercase: Главна буква
- HasLowercase: Малка буква
- HasNumber: Номер
- HasSymbol: Символ
- Confirmation: Съвпадение за потвърждение
- ResetLinkText: нулиране на парола
- BackButtonText: обратно
- NextButtonText: следващия
+ MinLength: Трябва да е поне
+ MinLengthp2: символа дълга.
+ MaxLength: Трябва да е по-малко от 70 символа.
+ HasUppercase: Трябва да включва главна буква.
+ HasLowercase: Трябва да включва малка буква.
+ HasNumber: Трябва да включва число.
+ HasSymbol: Трябва да включва символ.
+ Confirmation: Потвърждението на паролата съвпада.
+ ResetLinkText: Нулиране на паролата
+ BackButtonText: Назад
+ NextButtonText: Напред
UsernameChange:
Title: Промяна на потребителското име
Description: Задайте новото си потребителско име
diff --git a/internal/api/ui/login/static/i18n/cs.yaml b/internal/api/ui/login/static/i18n/cs.yaml
index 3b97b14bf2..52e88e526b 100644
--- a/internal/api/ui/login/static/i18n/cs.yaml
+++ b/internal/api/ui/login/static/i18n/cs.yaml
@@ -32,12 +32,14 @@ Password:
Title: Heslo
Description: Zadejte své přihlašovací údaje.
PasswordLabel: Heslo
- MinLength: Minimální délka
- HasUppercase: Velké písmeno
- HasLowercase: Malé písmeno
- HasNumber: Číslo
- HasSymbol: Symbol
- Confirmation: Potvrzení shody
+ MinLength: Musí být alespoň
+ MinLengthp2: znaků dlouhé.
+ MaxLength: Musí být kratší než 70 znaků.
+ HasUppercase: Musí obsahovat velké písmeno.
+ HasLowercase: Musí obsahovat malé písmeno.
+ HasNumber: Musí obsahovat číslo.
+ HasSymbol: Musí obsahovat symbol.
+ Confirmation: Potvrzení hesla odpovídá.
ResetLinkText: Obnovit heslo
BackButtonText: Zpět
NextButtonText: Další
diff --git a/internal/api/ui/login/static/i18n/de.yaml b/internal/api/ui/login/static/i18n/de.yaml
index d258f09aa6..4022abfa74 100644
--- a/internal/api/ui/login/static/i18n/de.yaml
+++ b/internal/api/ui/login/static/i18n/de.yaml
@@ -29,15 +29,17 @@ SelectAccount:
MustBeMemberOfOrg: Der Benutzer muss der Organisation {{.OrgName}} angehören.
Password:
- Title: Willkommen zurück!
- Description: Gib deine Benutzerdaten ein.
+ Title: Passwort
+ Description: Geben Sie Ihre Anmeldedaten ein.
PasswordLabel: Passwort
- MinLength: Mindestlänge
- HasUppercase: Großbuchstaben
- HasLowercase: Kleinbuchstaben
- HasNumber: Nummer
- HasSymbol: Symbol
- Confirmation: Wiederholung stimmt überein
+ MinLength: Muss mindestens
+ MinLengthp2: Zeichen lang sein.
+ MaxLength: Muss weniger als 70 Zeichen lang sein.
+ HasUppercase: Muss einen Großbuchstaben enthalten.
+ HasLowercase: Muss einen Kleinbuchstaben enthalten.
+ HasNumber: Muss eine Zahl enthalten.
+ HasSymbol: Muss ein Symbol enthalten.
+ Confirmation: Passwortbestätigung stimmt überein.
ResetLinkText: Passwort zurücksetzen
BackButtonText: Zurück
NextButtonText: Weiter
diff --git a/internal/api/ui/login/static/i18n/en.yaml b/internal/api/ui/login/static/i18n/en.yaml
index 338107060d..b3f6cd4793 100644
--- a/internal/api/ui/login/static/i18n/en.yaml
+++ b/internal/api/ui/login/static/i18n/en.yaml
@@ -32,12 +32,14 @@ Password:
Title: Password
Description: Enter your login data.
PasswordLabel: Password
- MinLength: Minimum length
- HasUppercase: Uppercase letter
- HasLowercase: Lowercase letter
- HasNumber: Number
- HasSymbol: Symbol
- Confirmation: Confirmation match
+ MinLength: Must be at least
+ MinLengthp2: characters long.
+ MaxLength: Must be less than 70 characters long.
+ HasUppercase: Must include an uppercase letter.
+ HasLowercase: Must include a lowercase letter.
+ HasNumber: Must include a number.
+ HasSymbol: Must include a symbol.
+ Confirmation: Password confirmation matched.
ResetLinkText: Reset Password
BackButtonText: Back
NextButtonText: Next
diff --git a/internal/api/ui/login/static/i18n/es.yaml b/internal/api/ui/login/static/i18n/es.yaml
index ef091f2910..29336ab5a2 100644
--- a/internal/api/ui/login/static/i18n/es.yaml
+++ b/internal/api/ui/login/static/i18n/es.yaml
@@ -30,17 +30,19 @@ SelectAccount:
Password:
Title: Contraseña
- Description: Introduce tus datos de inicio de sesión.
+ Description: Introduce tus datos de acceso.
PasswordLabel: Contraseña
- MinLength: Longitud mínima
- HasUppercase: Una letra mayúscula
- HasLowercase: Una letra minúscula
- HasNumber: Número
- HasSymbol: Símbolo
- Confirmation: Las contraseñas coinciden
- ResetLinkText: restablecer contraseña
- BackButtonText: atrás
- NextButtonText: siguiente
+ MinLength: Debe tener al menos
+ MinLengthp2: caracteres de longitud.
+ MaxLength: Debe tener menos de 70 caracteres de longitud.
+ HasUppercase: Debe incluir una letra mayúscula.
+ HasLowercase: Debe incluir una letra minúscula.
+ HasNumber: Debe incluir un número.
+ HasSymbol: Debe incluir un símbolo.
+ Confirmation: La confirmación de la contraseña coincide.
+ ResetLinkText: Restablecer contraseña
+ BackButtonText: Atrás
+ NextButtonText: Siguiente
UsernameChange:
Title: Cambiar nombre de usuario
diff --git a/internal/api/ui/login/static/i18n/fr.yaml b/internal/api/ui/login/static/i18n/fr.yaml
index 44532ca8de..69cd70518a 100644
--- a/internal/api/ui/login/static/i18n/fr.yaml
+++ b/internal/api/ui/login/static/i18n/fr.yaml
@@ -32,12 +32,14 @@ Password:
Title: Mot de passe
Description: Entrez vos identifiants.
PasswordLabel: Mot de passe
- MinLength: Longueur minimale
- HasUppercase: Lettre majuscule
- HasLowercase: Lettre minuscule
- HasNumber: Numéro
- HasSymbol: Symbole
- Confirmation: Les mots de passe sont identiques
+ MinLength: Doit contenir au moins
+ MinLengthp2: caractères.
+ MaxLength: Doit contenir moins de 70 caractères.
+ HasUppercase: Doit inclure une lettre majuscule.
+ HasLowercase: Doit inclure une lettre minuscule.
+ HasNumber: Doit inclure un chiffre.
+ HasSymbol: Doit inclure un symbole.
+ Confirmation: La confirmation du mot de passe correspond.
ResetLinkText: Réinitialiser le mot de passe
BackButtonText: Retour
NextButtonText: Suivant
diff --git a/internal/api/ui/login/static/i18n/it.yaml b/internal/api/ui/login/static/i18n/it.yaml
index 24e9733943..8b969e6c6f 100644
--- a/internal/api/ui/login/static/i18n/it.yaml
+++ b/internal/api/ui/login/static/i18n/it.yaml
@@ -32,14 +32,16 @@ Password:
Title: Password
Description: Inserisci i tuoi dati di accesso.
PasswordLabel: Password
- MinLength: Lunghezza minima
- HasUppercase: Lettera maiuscola
- HasLowercase: Lettera minuscola
- HasNumber: Numero
- HasSymbol: Simbolo
- Confirmation: Conferma password
- ResetLinkText: Password dimenticata?
- BackButtonText: indietro
+ MinLength: Deve contenere almeno
+ MinLengthp2: caratteri.
+ MaxLength: Deve contenere meno di 70 caratteri.
+ HasUppercase: Deve includere una lettera maiuscola.
+ HasLowercase: Deve includere una lettera minuscola.
+ HasNumber: Deve includere un numero.
+ HasSymbol: Deve includere un simbolo.
+ Confirmation: La conferma della password corrisponde.
+ ResetLinkText: Reimposta password
+ BackButtonText: Indietro
NextButtonText: Avanti
UsernameChange:
diff --git a/internal/api/ui/login/static/i18n/ja.yaml b/internal/api/ui/login/static/i18n/ja.yaml
index e4ca75bad7..da55ccb523 100644
--- a/internal/api/ui/login/static/i18n/ja.yaml
+++ b/internal/api/ui/login/static/i18n/ja.yaml
@@ -22,16 +22,18 @@ SelectAccount:
MustBeMemberOfOrg: ユーザーは組織 {{.OrgName}} のメンバーである必要があります。
Password:
- Title: パスワードの入力
- Description: ログインデータを入力します。
+ Title: パスワード
+ Description: ログインデータを入力してください。
PasswordLabel: パスワード
- MinLength: 文字列の長さ
- HasUppercase: 大文字
- HasLowercase: 小文字
- HasNumber: 数字
- HasSymbol: シンボル
- Confirmation: パスワードの確認
- ResetLinkText: パスワードを再設定する
+ MinLength: 少なくとも
+ MinLengthp2: 文字でなければなりません。
+ MaxLength: 70文字以下でなければなりません。
+ HasUppercase: 大文字を含む必要があります。
+ HasLowercase: 小文字を含む必要があります。
+ HasNumber: 数字を含む必要があります。
+ HasSymbol: 記号を含む必要があります。
+ Confirmation: パスワードの確認が一致しました。
+ ResetLinkText: パスワードをリセット
BackButtonText: 戻る
NextButtonText: 次へ
diff --git a/internal/api/ui/login/static/i18n/mk.yaml b/internal/api/ui/login/static/i18n/mk.yaml
index dafc520d56..c05af2b691 100644
--- a/internal/api/ui/login/static/i18n/mk.yaml
+++ b/internal/api/ui/login/static/i18n/mk.yaml
@@ -32,15 +32,17 @@ Password:
Title: Лозинка
Description: Внесете ги вашите податоци за најава.
PasswordLabel: Лозинка
- MinLength: Минимална должина
- HasUppercase: Голема буква
- HasLowercase: Мала буква
- HasNumber: Број
- HasSymbol: Симбол
- Confirmation: Потврда на лозинка
- ResetLinkText: ресетирај лозинка
- BackButtonText: назад
- NextButtonText: следно
+ MinLength: Мора да биде најмалку
+ MinLengthp2: карактери долго.
+ MaxLength: Мора да биде помалку од 70 карактери.
+ HasUppercase: Мора да вклучи голема буква.
+ HasLowercase: Мора да вклучи мала буква.
+ HasNumber: Мора да вклучи број.
+ HasSymbol: Мора да вклучи симбол.
+ Confirmation: Потврдата за лозинката се совпаѓа.
+ ResetLinkText: Ресетирај лозинка
+ BackButtonText: Назад
+ NextButtonText: Напред
UsernameChange:
Title: Промена на корисничко име
diff --git a/internal/api/ui/login/static/i18n/nl.yaml b/internal/api/ui/login/static/i18n/nl.yaml
index 68d8ffa4a1..5b76ee4179 100644
--- a/internal/api/ui/login/static/i18n/nl.yaml
+++ b/internal/api/ui/login/static/i18n/nl.yaml
@@ -32,13 +32,15 @@ Password:
Title: Wachtwoord
Description: Voer uw inloggegevens in.
PasswordLabel: Wachtwoord
- MinLength: Minimum lengte
- HasUppercase: Hoofdletter
- HasLowercase: Kleine letter
- HasNumber: Nummer
- HasSymbol: Symbool
- Confirmation: Bevestiging komt overeen
- ResetLinkText: Reset Wachtwoord
+ MinLength: Moet minstens
+ MinLengthp2: tekens lang zijn.
+ MaxLength: Moet minder dan 70 tekens lang zijn.
+ HasUppercase: Moet een hoofdletter bevatten.
+ HasLowercase: Moet een kleine letter bevatten.
+ HasNumber: Moet een nummer bevatten.
+ HasSymbol: Moet een symbool bevatten.
+ Confirmation: Wachtwoordbevestiging komt overeen.
+ ResetLinkText: Wachtwoord resetten
BackButtonText: Terug
NextButtonText: Volgende
diff --git a/internal/api/ui/login/static/i18n/pl.yaml b/internal/api/ui/login/static/i18n/pl.yaml
index e23ce3fa71..3f9def93a2 100644
--- a/internal/api/ui/login/static/i18n/pl.yaml
+++ b/internal/api/ui/login/static/i18n/pl.yaml
@@ -32,15 +32,17 @@ Password:
Title: Hasło
Description: Wprowadź swoje dane logowania.
PasswordLabel: Hasło
- MinLength: Minimalna długość
- HasUppercase: Duża litera
- HasLowercase: Mała litera
- HasNumber: Liczba
- HasSymbol: Symbol
- Confirmation: Potwierdzenie zgodności
- ResetLinkText: zresetuj hasło
- BackButtonText: wróć
- NextButtonText: dalej
+ MinLength: Musi zawierać co najmniej
+ MinLengthp2: znaków.
+ MaxLength: Musi zawierać mniej niż 70 znaków.
+ HasUppercase: Musi zawierać dużą literę.
+ HasLowercase: Musi zawierać małą literę.
+ HasNumber: Musi zawierać numer.
+ HasSymbol: Musi zawierać symbol.
+ Confirmation: Potwierdzenie hasła pasuje.
+ ResetLinkText: Zresetuj hasło
+ BackButtonText: Wstecz
+ NextButtonText: Dalej
UsernameChange:
Title: Zmiana nazwy użytkownika
diff --git a/internal/api/ui/login/static/i18n/pt.yaml b/internal/api/ui/login/static/i18n/pt.yaml
index 19d00c72f4..99aff3a8c3 100644
--- a/internal/api/ui/login/static/i18n/pt.yaml
+++ b/internal/api/ui/login/static/i18n/pt.yaml
@@ -32,15 +32,17 @@ Password:
Title: Senha
Description: Insira seus dados de login.
PasswordLabel: Senha
- MinLength: Comprimento mínimo
- HasUppercase: Letra maiúscula
- HasLowercase: Letra minúscula
- HasNumber: Número
- HasSymbol: Símbolo
- Confirmation: Confirmação corresponde
- ResetLinkText: redefinir senha
- BackButtonText: voltar
- NextButtonText: próximo
+ MinLength: Deve ter pelo menos
+ MinLengthp2: caracteres.
+ MaxLength: Deve ter menos de 70 caracteres.
+ HasUppercase: Deve incluir uma letra maiúscula.
+ HasLowercase: Deve incluir uma letra minúscula.
+ HasNumber: Deve incluir um número.
+ HasSymbol: Deve incluir um símbolo.
+ Confirmation: A confirmação da senha corresponde.
+ ResetLinkText: Redefinir senha
+ BackButtonText: Voltar
+ NextButtonText: Próximo
UsernameChange:
Title: Alterar nome de usuário
diff --git a/internal/api/ui/login/static/i18n/ru.yaml b/internal/api/ui/login/static/i18n/ru.yaml
index fb88312486..cd756cbdd8 100644
--- a/internal/api/ui/login/static/i18n/ru.yaml
+++ b/internal/api/ui/login/static/i18n/ru.yaml
@@ -29,17 +29,19 @@ SelectAccount:
Password:
Title: Пароль
- Description: Введите ваши данные.
+ Description: Введите свои данные для входа.
PasswordLabel: Пароль
- MinLength: Минимальная длина
- HasUppercase: Заглавная буква
- HasLowercase: Строчная буква
- HasNumber: Цифра
- HasSymbol: Символ
- Confirmation: Подтверждение пароля
- ResetLinkText: сброс пароля
- BackButtonText: назад
- NextButtonText: далее
+ MinLength: Должно быть не менее
+ MinLengthp2: символов.
+ MaxLength: Должно быть меньше 70 символов.
+ HasUppercase: Должно содержать заглавную букву.
+ HasLowercase: Должно содержать строчную букву.
+ HasNumber: Должно содержать число.
+ HasSymbol: Должно содержать символ.
+ Confirmation: Подтверждение пароля совпадает.
+ ResetLinkText: Сбросить пароль
+ BackButtonText: Назад
+ NextButtonText: Вперед
UsernameChange:
Title: Изменение логина
diff --git a/internal/api/ui/login/static/i18n/zh.yaml b/internal/api/ui/login/static/i18n/zh.yaml
index bf31d270bb..85230efe0f 100644
--- a/internal/api/ui/login/static/i18n/zh.yaml
+++ b/internal/api/ui/login/static/i18n/zh.yaml
@@ -32,15 +32,17 @@ Password:
Title: 密码
Description: 输入您的登录数据。
PasswordLabel: 密码
- MinLength: 密码
- HasUppercase: 大写字母
- HasLowercase: 小写字母
- HasNumber: 数字
- HasSymbol: 符号
- Confirmation: 确认匹配
- ResetLinkText: 重设密码
- BackButtonText: 后退
- NextButtonText: 继续
+ MinLength: 必须至少有
+ MinLengthp2: 个字符。
+ MaxLength: 必须少于70个字符。
+ HasUppercase: 必须包含一个大写字母。
+ HasLowercase: 必须包含一个小写字母。
+ HasNumber: 必须包含一个数字。
+ HasSymbol: 必须包含一个符号。
+ Confirmation: 密码确认匹配。
+ ResetLinkText: 重置密码
+ BackButtonText: 返回
+ NextButtonText: 下一步
UsernameChange:
Title: 更改用户名
diff --git a/internal/api/ui/login/static/resources/scripts/password_policy_check.js b/internal/api/ui/login/static/resources/scripts/password_policy_check.js
index ce5936b32b..4a9712f815 100644
--- a/internal/api/ui/login/static/resources/scripts/password_policy_check.js
+++ b/internal/api/ui/login/static/resources/scripts/password_policy_check.js
@@ -15,6 +15,14 @@ function ComplexityPolicyCheck(passwordElement, passwordConfirmationElement) {
invalid++;
}
+ const maxLengthElem = document.getElementById("maxlength");
+ if (passwordElement.value.length <= 70) {
+ ValidPolicyFlipped(maxLengthElem);
+ } else {
+ InvalidPolicyFlipped(maxLengthElem);
+ invalid++;
+ }
+
const upper = document.getElementById("uppercase");
if (upperRegex !== "") {
if (RegExp(upperRegex).test(passwordElement.value)) {
@@ -84,6 +92,14 @@ function ValidPolicy(element) {
element.getElementsByTagName("i")[0].classList.add("lgn-valid");
}
+function ValidPolicyFlipped(element) {
+ element.classList.add("valid");
+ element.getElementsByTagName("i")[0].classList.remove("lgn-warn");
+ element.getElementsByTagName("i")[0].classList.remove("lgn-icon-times-solid");
+ element.getElementsByTagName("i")[0].classList.add("lgn-valid");
+ element.getElementsByTagName("i")[0].classList.add("lgn-icon-check-solid");
+}
+
function InvalidPolicy(element) {
element.classList.add("invalid");
element.getElementsByTagName("i")[0].classList.remove("lgn-valid");
@@ -91,3 +107,11 @@ function InvalidPolicy(element) {
element.getElementsByTagName("i")[0].classList.add("lgn-warn");
element.getElementsByTagName("i")[0].classList.add("lgn-icon-times-solid");
}
+
+function InvalidPolicyFlipped(element) {
+ element.classList.remove("valid");
+ element.getElementsByTagName("i")[0].classList.remove("lgn-icon-check-solid");
+ element.getElementsByTagName("i")[0].classList.remove("lgn-valid");
+ element.getElementsByTagName("i")[0].classList.add("lgn-icon-times-solid");
+ element.getElementsByTagName("i")[0].classList.add("lgn-warn");
+}
diff --git a/internal/api/ui/login/static/templates/ldap_login.html b/internal/api/ui/login/static/templates/ldap_login.html
index 850bf27e57..1f91f14051 100644
--- a/internal/api/ui/login/static/templates/ldap_login.html
+++ b/internal/api/ui/login/static/templates/ldap_login.html
@@ -34,7 +34,7 @@
-
-
+
+
{{template "main-bottom" .}}
\ No newline at end of file
diff --git a/internal/api/ui/login/static/templates/password_complexity_policy.html b/internal/api/ui/login/static/templates/password_complexity_policy.html
index f8f5d34765..19a4c9ce96 100644
--- a/internal/api/ui/login/static/templates/password_complexity_policy.html
+++ b/internal/api/ui/login/static/templates/password_complexity_policy.html
@@ -1,6 +1,7 @@
{{define "password-complexity-policy-description"}}
- - {{t "Password.MinLength"}} {{.MinLength}}
+ - {{t "Password.MinLength"}} {{.MinLength}} {{t "Password.MinLengthp2"}}
+ - {{t "Password.MaxLength"}}
{{if .HasUppercase }}
- {{t "Password.HasUppercase"}}
{{end}}
diff --git a/internal/api/ui/login/username_change_handler.go b/internal/api/ui/login/username_change_handler.go
index 7a497c4eb5..f11fe43a72 100644
--- a/internal/api/ui/login/username_change_handler.go
+++ b/internal/api/ui/login/username_change_handler.go
@@ -27,7 +27,7 @@ func (l *Login) renderChangeUsername(w http.ResponseWriter, r *http.Request, aut
func (l *Login) handleChangeUsername(w http.ResponseWriter, r *http.Request) {
data := new(changeUsernameData)
- authReq, err := l.getAuthRequestAndParseData(r, data)
+ authReq, err := l.ensureAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go
index 9de07b742f..e695d83511 100644
--- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go
+++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go
@@ -2,6 +2,7 @@ package eventstore
import (
"context"
+ "slices"
"strings"
"time"
@@ -339,11 +340,7 @@ func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, authReqID, user
}
return err
}
- policy, err := repo.getLockoutPolicy(ctx, resourceOwner)
- if err != nil {
- return err
- }
- err = repo.Command.HumanCheckPassword(ctx, resourceOwner, userID, password, request.WithCurrentInfo(info), lockoutPolicyToDomain(policy))
+ err = repo.Command.HumanCheckPassword(ctx, resourceOwner, userID, password, request.WithCurrentInfo(info))
if isIgnoreUserInvalidPasswordError(err, request) {
return zerrors.ThrowInvalidArgument(nil, "EVENT-Jsf32", "Errors.User.UsernameOrPassword.Invalid")
}
@@ -1030,15 +1027,11 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
if err != nil {
return nil, err
}
- if (!isInternalLogin || len(idps.Links) > 0) && len(request.LinkingUsers) == 0 && !checkVerificationTimeMaxAge(userSession.ExternalLoginVerification, request.LoginPolicy.ExternalLoginCheckLifetime, request) {
- selectedIDPConfigID := request.SelectedIDPConfigID
- if selectedIDPConfigID == "" {
- selectedIDPConfigID = userSession.SelectedIDPConfigID
+ if (!isInternalLogin || len(idps.Links) > 0) && len(request.LinkingUsers) == 0 {
+ step := repo.idpChecked(request, idps.Links, userSession)
+ if step != nil {
+ return append(steps, step), nil
}
- if selectedIDPConfigID == "" {
- selectedIDPConfigID = idps.Links[0].IDPID
- }
- return append(steps, &domain.ExternalLoginStep{SelectedIDPConfigID: selectedIDPConfigID}), nil
}
if isInternalLogin || (!isInternalLogin && len(request.LinkingUsers) > 0) {
step := repo.firstFactorChecked(request, user, userSession)
@@ -1198,6 +1191,7 @@ func (repo *AuthRequestRepo) firstFactorChecked(request *domain.AuthRequest, use
var step domain.NextStep
if request.LoginPolicy.PasswordlessType != domain.PasswordlessTypeNotAllowed && user.IsPasswordlessReady() {
if checkVerificationTimeMaxAge(userSession.PasswordlessVerification, request.LoginPolicy.MultiFactorCheckLifetime, request) {
+ request.MFAsVerified = append(request.MFAsVerified, domain.MFATypeU2FUserVerification)
request.AuthTime = userSession.PasswordlessVerification
return nil
}
@@ -1225,8 +1219,27 @@ func (repo *AuthRequestRepo) firstFactorChecked(request *domain.AuthRequest, use
return &domain.PasswordStep{}
}
+func (repo *AuthRequestRepo) idpChecked(request *domain.AuthRequest, idps []*query.IDPUserLink, userSession *user_model.UserSessionView) domain.NextStep {
+ if checkVerificationTimeMaxAge(userSession.ExternalLoginVerification, request.LoginPolicy.ExternalLoginCheckLifetime, request) {
+ request.IDPLoginChecked = true
+ request.AuthTime = userSession.ExternalLoginVerification
+ return nil
+ }
+ selectedIDPConfigID := request.SelectedIDPConfigID
+ if selectedIDPConfigID == "" {
+ selectedIDPConfigID = userSession.SelectedIDPConfigID
+ }
+ if selectedIDPConfigID == "" && len(idps) > 0 {
+ selectedIDPConfigID = idps[0].IDPID
+ }
+ return &domain.ExternalLoginStep{SelectedIDPConfigID: selectedIDPConfigID}
+}
+
func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView, request *domain.AuthRequest, user *user_model.UserView, isInternalAuthentication bool) (domain.NextStep, bool, error) {
mfaLevel := request.MFALevel()
+ if slices.Contains(request.MFAsVerified, domain.MFATypeU2FUserVerification) {
+ return nil, true, nil
+ }
allowedProviders, required := user.MFATypesAllowed(mfaLevel, request.LoginPolicy, isInternalAuthentication)
promptRequired := (user.MFAMaxSetUp < mfaLevel) || (len(allowedProviders) == 0 && required)
if promptRequired || !repo.mfaSkippedOrSetUp(user, request) {
diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go
index 324039a765..35ce216877 100644
--- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go
+++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go
@@ -89,7 +89,7 @@ func (m *mockViewUserSession) UserSessionsByAgentID(string, string) ([]*user_vie
for i, user := range m.Users {
sessions[i] = &user_view_model.UserSessionView{
ResourceOwner: user.ResourceOwner,
- State: int32(user.SessionState),
+ State: sql.Null[domain.UserSessionState]{V: user.SessionState},
UserID: user.UserID,
LoginName: sql.NullString{String: user.LoginName},
}
@@ -1682,11 +1682,12 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
isInternal bool
}
tests := []struct {
- name string
- args args
- want domain.NextStep
- wantChecked bool
- errFunc func(err error) bool
+ name string
+ args args
+ want domain.NextStep
+ wantChecked bool
+ errFunc func(err error) bool
+ wantMFAVerified []domain.MFAType
}{
//{
// "required, prompt and false", //TODO: enable when LevelsOfAssurance is checked
@@ -1718,6 +1719,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
nil,
false,
zerrors.IsPreconditionFailed,
+ nil,
},
{
"not set up, no mfas configured, no prompt and true",
@@ -1737,6 +1739,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
nil,
true,
nil,
+ nil,
},
{
"not set up, prompt and false",
@@ -1761,6 +1764,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
},
false,
nil,
+ nil,
},
{
"not set up, forced by org, true",
@@ -1787,6 +1791,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
},
false,
nil,
+ nil,
},
{
"not set up and skipped, true",
@@ -1807,6 +1812,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
nil,
true,
nil,
+ nil,
},
{
"checked second factor, true",
@@ -1829,6 +1835,38 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
nil,
true,
nil,
+ []domain.MFAType{domain.MFATypeTOTP},
+ },
+ {
+ "checked passwordless, true",
+ args{
+ request: &domain.AuthRequest{
+ LoginPolicy: &domain.LoginPolicy{
+ SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP},
+ SecondFactorCheckLifetime: 18 * time.Hour,
+ MultiFactors: []domain.MultiFactorType{domain.MultiFactorTypeU2FWithPIN},
+ MultiFactorCheckLifetime: 18 * time.Hour,
+ },
+ MFAsVerified: []domain.MFAType{domain.MFATypeU2FUserVerification},
+ },
+ user: &user_model.UserView{
+ HumanView: &user_model.HumanView{
+ MFAMaxSetUp: domain.MFALevelMultiFactor,
+ PasswordlessTokens: []*user_model.WebAuthNView{
+ {
+ TokenID: "tokenID",
+ State: user_model.MFAStateReady,
+ },
+ },
+ },
+ },
+ userSession: &user_model.UserSessionView{PasswordlessVerification: testNow.Add(-5 * time.Hour)},
+ isInternal: true,
+ },
+ nil,
+ true,
+ nil,
+ []domain.MFAType{domain.MFATypeU2FUserVerification},
},
{
"not checked, check and false",
@@ -1854,6 +1892,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
},
false,
nil,
+ nil,
},
{
"external not checked or forced but set up, want step",
@@ -1878,6 +1917,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
},
false,
nil,
+ nil,
},
{
"external not forced but checked",
@@ -1900,6 +1940,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
nil,
true,
nil,
+ []domain.MFAType{domain.MFATypeTOTP},
},
{
"external not checked but required, want step",
@@ -1927,6 +1968,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
},
false,
nil,
+ nil,
},
{
"external not checked but local required",
@@ -1950,6 +1992,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
nil,
true,
nil,
+ nil,
},
}
for _, tt := range tests {
@@ -1964,6 +2007,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
t.Errorf("mfaChecked() checked = %v, want %v", ok, tt.wantChecked)
}
assert.Equal(t, tt.want, got)
+ assert.ElementsMatch(t, tt.args.request.MFAsVerified, tt.wantMFAVerified)
})
}
}
diff --git a/internal/auth/repository/eventsourcing/eventstore/user.go b/internal/auth/repository/eventsourcing/eventstore/user.go
index 83f09be6ae..cfa573e7e3 100644
--- a/internal/auth/repository/eventsourcing/eventstore/user.go
+++ b/internal/auth/repository/eventsourcing/eventstore/user.go
@@ -32,7 +32,7 @@ func (repo *UserRepo) UserSessionUserIDsByAgentID(ctx context.Context, agentID s
}
userIDs := make([]string, 0, len(userSessions))
for _, session := range userSessions {
- if session.State == int32(domain.UserSessionStateActive) {
+ if session.State.V == domain.UserSessionStateActive {
userIDs = append(userIDs, session.UserID)
}
}
diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go
index 03e367829f..dedb1bce4e 100644
--- a/internal/auth/repository/eventsourcing/handler/handler.go
+++ b/internal/auth/repository/eventsourcing/handler/handler.go
@@ -63,6 +63,16 @@ func Projections() []*handler2.Handler {
return projections
}
+func ProjectInstance(ctx context.Context) error {
+ for _, projection := range projections {
+ _, err := projection.Trigger(ctx)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
func (config Config) overwrite(viewModel string) handler2.Config {
c := handler2.Config{
Client: config.Client,
diff --git a/internal/auth/repository/eventsourcing/handler/user_session.go b/internal/auth/repository/eventsourcing/handler/user_session.go
index de97a2062e..3147b336f3 100644
--- a/internal/auth/repository/eventsourcing/handler/user_session.go
+++ b/internal/auth/repository/eventsourcing/handler/user_session.go
@@ -220,6 +220,7 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
user.HumanPasswordCheckFailedType:
columns, err := sessionColumns(event,
handler.NewCol(view_model.UserSessionKeyPasswordVerification, time.Time{}),
+ handler.NewCol(view_model.UserSessionKeyState, domain.UserSessionStateActive),
)
if err != nil {
return nil, err
@@ -241,6 +242,7 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
user.HumanU2FTokenCheckFailedType:
columns, err := sessionColumns(event,
handler.NewCol(view_model.UserSessionKeySecondFactorVerification, time.Time{}),
+ handler.NewCol(view_model.UserSessionKeyState, domain.UserSessionStateActive),
)
if err != nil {
return nil, err
@@ -317,6 +319,7 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
columns, err := sessionColumns(event,
handler.NewCol(view_model.UserSessionKeyPasswordlessVerification, time.Time{}),
handler.NewCol(view_model.UserSessionKeyMultiFactorVerification, time.Time{}),
+ handler.NewCol(view_model.UserSessionKeyState, domain.UserSessionStateActive),
)
if err != nil {
return nil, err
diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go
index c02e9cb329..4d8823913d 100644
--- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go
+++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go
@@ -172,7 +172,7 @@ func (repo *TokenVerifierRepo) verifySessionToken(ctx context.Context, sessionID
}
// checkAuthentication ensures the session or token was authenticated (at least a single [domain.UserAuthMethodType]).
-// It will also check if there was a multi factor authentication, if either MFA is forced by the login policy or if the user has set up any
+// It will also check if there was a multi factor authentication, if either MFA is forced by the login policy or if the user has set up any second factor
func (repo *TokenVerifierRepo) checkAuthentication(ctx context.Context, authMethods []domain.UserAuthMethodType, userID string) error {
if len(authMethods) == 0 {
return zerrors.ThrowPermissionDenied(nil, "AUTHZ-Kl3p0", "authentication required")
@@ -190,8 +190,8 @@ func (repo *TokenVerifierRepo) checkAuthentication(ctx context.Context, authMeth
if domain.RequiresMFA(
requirements.ForceMFA,
requirements.ForceMFALocalOnly,
- !hasIDPAuthentication(authMethods)) ||
- domain.HasMFA(requirements.AuthMethods) {
+ !hasIDPAuthentication(authMethods),
+ ) {
return zerrors.ThrowPermissionDenied(nil, "AUTHZ-Kl3p0", "mfa required")
}
return nil
diff --git a/internal/command/command.go b/internal/command/command.go
index 8d77be765e..d8e6cfb7cf 100644
--- a/internal/command/command.go
+++ b/internal/command/command.go
@@ -82,6 +82,8 @@ type Commands struct {
ActionFunctionExisting func(function string) bool
EventExisting func(event string) bool
EventGroupExisting func(group string) bool
+
+ GenerateDomain func(instanceName, domain string) (string, error)
}
func StartCommands(
@@ -168,6 +170,7 @@ func StartCommands(
Issuer: defaults.Multifactors.OTP.Issuer,
},
},
+ GenerateDomain: domain.NewGeneratedInstanceDomain,
}
if defaultSecretGenerators != nil && defaultSecretGenerators.ClientSecret != nil {
diff --git a/internal/command/instance.go b/internal/command/instance.go
index 852e92ee6f..b5fb700459 100644
--- a/internal/command/instance.go
+++ b/internal/command/instance.go
@@ -142,6 +142,8 @@ type SecretGenerators struct {
}
type ZitadelConfig struct {
+ instanceID string
+ orgID string
projectID string
mgmtAppID string
adminAppID string
@@ -152,6 +154,16 @@ type ZitadelConfig struct {
}
func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) {
+ s.zitadel.instanceID, err = idGenerator.Next()
+ if err != nil {
+ return err
+ }
+
+ s.zitadel.orgID, err = idGenerator.Next()
+ if err != nil {
+ return err
+ }
+
s.zitadel.projectID, err = idGenerator.Next()
if err != nil {
return err
@@ -185,36 +197,88 @@ func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) {
}
func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (string, string, *MachineKey, *domain.ObjectDetails, error) {
- instanceID, err := c.idGenerator.Next()
+ if err := setup.generateIDs(c.idGenerator); err != nil {
+ return "", "", nil, nil, err
+ }
+ ctx = contextWithInstanceSetupInfo(ctx, setup.zitadel.instanceID, setup.zitadel.projectID, setup.zitadel.consoleAppID, c.externalDomain)
+
+ validations, pat, machineKey, err := setUpInstance(ctx, c, setup)
if err != nil {
return "", "", nil, nil, err
}
- ctx = authz.SetCtxData(authz.WithRequestedDomain(authz.WithInstanceID(ctx, instanceID), c.externalDomain), authz.CtxData{OrgID: instanceID, ResourceOwner: instanceID})
-
- orgID, err := c.idGenerator.Next()
+ //nolint:staticcheck
+ cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
if err != nil {
return "", "", nil, nil, err
}
- userID, err := c.idGenerator.Next()
+ events, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return "", "", nil, nil, err
}
- if err = setup.generateIDs(c.idGenerator); err != nil {
- return "", "", nil, nil, err
+ var token string
+ if pat != nil {
+ token = pat.Token
}
- ctx = authz.WithConsole(ctx, setup.zitadel.projectID, setup.zitadel.consoleAppID)
- instanceAgg := instance.NewAggregate(instanceID)
- orgAgg := org.NewAggregate(orgID)
- userAgg := user.NewAggregate(userID, orgID)
- projectAgg := project.NewAggregate(setup.zitadel.projectID, orgID)
- limitsAgg := limits.NewAggregate(setup.zitadel.limitsID, instanceID)
- restrictionsAgg := restrictions.NewAggregate(setup.zitadel.restrictionsID, instanceID, instanceID)
+ return setup.zitadel.instanceID, token, machineKey, &domain.ObjectDetails{
+ Sequence: events[len(events)-1].Sequence(),
+ EventDate: events[len(events)-1].CreatedAt(),
+ ResourceOwner: setup.zitadel.orgID,
+ }, nil
+}
- validations := []preparation.Validation{
+func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string) context.Context {
+ return authz.WithConsole(
+ authz.SetCtxData(
+ authz.WithRequestedDomain(
+ authz.WithInstanceID(
+ ctx,
+ instanceID),
+ externalDomain,
+ ),
+ authz.CtxData{ResourceOwner: instanceID},
+ ),
+ projectID,
+ consoleAppID,
+ )
+}
+
+func setUpInstance(ctx context.Context, c *Commands, setup *InstanceSetup) (validations []preparation.Validation, pat *PersonalAccessToken, machineKey *MachineKey, err error) {
+ instanceAgg := instance.NewAggregate(setup.zitadel.instanceID)
+
+ validations = setupInstanceElements(instanceAgg, setup)
+
+ // default organization on setup'd instance
+ pat, machineKey, err = setupDefaultOrg(ctx, c, &validations, instanceAgg, setup.Org.Name, setup.Org.Machine, setup.Org.Human, setup.zitadel)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ // domains
+ if err := setupGeneratedDomain(ctx, c, &validations, instanceAgg, setup.InstanceName); err != nil {
+ return nil, nil, nil, err
+ }
+ setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain)
+
+ // optional setting if set
+ setupMessageTexts(&validations, setup.MessageTexts, instanceAgg)
+ if err := setupQuotas(c, &validations, setup.Quotas, setup.zitadel.instanceID); err != nil {
+ return nil, nil, nil, err
+ }
+ setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg)
+ setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg)
+ setupFeatures(&validations, setup.Features, setup.zitadel.instanceID)
+ setupLimits(c, &validations, limits.NewAggregate(setup.zitadel.limitsID, setup.zitadel.instanceID), setup.Limits)
+ setupRestrictions(c, &validations, restrictions.NewAggregate(setup.zitadel.restrictionsID, setup.zitadel.instanceID, setup.zitadel.instanceID), setup.Restrictions)
+
+ return validations, pat, machineKey, nil
+}
+
+func setupInstanceElements(instanceAgg *instance.Aggregate, setup *InstanceSetup) []preparation.Validation {
+ return []preparation.Validation{
prepareAddInstance(instanceAgg, setup.InstanceName, setup.DefaultLanguage),
prepareAddSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeAppSecret, setup.SecretGenerators.ClientSecret),
prepareAddSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeInitCode, setup.SecretGenerators.InitializeUserCode),
@@ -292,54 +356,8 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
setup.LabelPolicy.DisableWatermark,
setup.LabelPolicy.ThemeMode,
),
- prepareActivateDefaultLabelPolicy(instanceAgg),
-
prepareAddDefaultEmailTemplate(instanceAgg, setup.EmailTemplate),
}
- if err := setupQuotas(c, &validations, setup.Quotas, instanceID); err != nil {
- return "", "", nil, nil, err
- }
- setupMessageTexts(&validations, setup.MessageTexts, instanceAgg)
- validations = append(validations,
- AddOrgCommand(ctx, orgAgg, setup.Org.Name),
- c.prepareSetDefaultOrg(instanceAgg, orgAgg.ID),
- )
- pat, machineKey, err := setupAdmin(c, &validations, setup.Org.Machine, setup.Org.Human, orgID, userID, userAgg)
- if err != nil {
- return "", "", nil, nil, err
- }
- setupMinimalInterfaces(c, &validations, instanceAgg, projectAgg, orgAgg, userID, setup.zitadel)
- if err := setupGeneratedDomain(ctx, c, &validations, instanceAgg, setup.InstanceName); err != nil {
- return "", "", nil, nil, err
- }
- setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain)
- setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg)
- setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg)
- setupFeatures(&validations, setup.Features, instanceID)
- 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
- }
-
- events, err := c.eventstore.Push(ctx, cmds...)
- if err != nil {
- return "", "", nil, nil, err
- }
-
- var token string
- if pat != nil {
- token = pat.Token
- }
-
- return instanceID, token, machineKey, &domain.ObjectDetails{
- Sequence: events[len(events)-1].Sequence(),
- EventDate: events[len(events)-1].CreatedAt(),
- ResourceOwner: orgID,
- }, nil
}
func setupLimits(commands *Commands, validations *[]preparation.Validation, limitsAgg *limits.Aggregate, setLimits *SetLimits) {
@@ -369,7 +387,9 @@ func setupQuotas(commands *Commands, validations *[]preparation.Validation, setQ
}
func setupFeatures(validations *[]preparation.Validation, features *InstanceFeatures, instanceID string) {
- *validations = append(*validations, prepareSetFeatures(instanceID, features))
+ if features != nil {
+ *validations = append(*validations, prepareSetFeatures(instanceID, features))
+ }
}
func setupOIDCSettings(commands *Commands, validations *[]preparation.Validation, oidcSettings *OIDCSettings, instanceAgg *instance.Aggregate) {
@@ -425,7 +445,9 @@ func setupGeneratedDomain(ctx context.Context, commands *Commands, validations *
return nil
}
-func setupMinimalInterfaces(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, projectAgg *project.Aggregate, orgAgg *org.Aggregate, userID string, ids ZitadelConfig) {
+func setupMinimalInterfaces(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, orgAgg *org.Aggregate, projectOwner string, ids ZitadelConfig) {
+ projectAgg := project.NewAggregate(ids.projectID, orgAgg.ID)
+
cnsl := &addOIDCApp{
AddApp: AddApp{
Aggregate: *projectAgg,
@@ -446,10 +468,9 @@ func setupMinimalInterfaces(commands *Commands, validations *[]preparation.Valid
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),
+ AddProjectCommand(projectAgg, zitadelProjectName, projectOwner, false, false, false, domain.PrivateLabelingSettingUnspecified),
SetIAMProject(instanceAgg, projectAgg.ID),
commands.AddAPIAppCommand(
@@ -490,37 +511,103 @@ func setupMinimalInterfaces(commands *Commands, validations *[]preparation.Valid
)
}
-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
+func setupDefaultOrg(ctx context.Context,
+ commands *Commands,
+ validations *[]preparation.Validation,
+ instanceAgg *instance.Aggregate,
+ name string,
+ machine *AddMachine,
+ human *AddHuman,
+ ids ZitadelConfig,
+) (pat *PersonalAccessToken, machineKey *MachineKey, err error) {
+ orgAgg := org.NewAggregate(ids.orgID)
+
+ *validations = append(
+ *validations,
+ AddOrgCommand(ctx, orgAgg, name),
+ commands.prepareSetDefaultOrg(instanceAgg, ids.orgID),
+ )
+
+ projectOwner, pat, machineKey, err := setupAdmins(commands, validations, instanceAgg, orgAgg, machine, human)
+ if err != nil {
+ return nil, nil, err
+ }
+ setupMinimalInterfaces(commands, validations, instanceAgg, orgAgg, projectOwner, ids)
+ return pat, machineKey, nil
+}
+
+func setupAdmins(commands *Commands,
+ validations *[]preparation.Validation,
+ instanceAgg *instance.Aggregate,
+ orgAgg *org.Aggregate,
+ machine *AddMachine,
+ human *AddHuman,
+) (owner string, pat *PersonalAccessToken, machineKey *MachineKey, err error) {
+ if human == nil && machine == nil {
+ return "", nil, nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-z1yi2q2ot7", "Error.Instance.NoAdmin")
+ }
+
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))
+ machineUserID, err := commands.idGenerator.Next()
+ if err != nil {
+ return "", nil, nil, err
}
- 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))
+ owner = machineUserID
+
+ pat, machineKey, err = setupMachineAdmin(commands, validations, machine, orgAgg.ID, machineUserID)
+ if err != nil {
+ return "", nil, nil, err
}
- } else if human != nil {
- human.ID = userID
+
+ setupAdminMembers(commands, validations, instanceAgg, orgAgg, machineUserID)
+ }
+ if human != nil {
+ humanUserID, err := commands.idGenerator.Next()
+ if err != nil {
+ return "", nil, nil, err
+ }
+ owner = humanUserID
+ human.ID = humanUserID
+
*validations = append(*validations,
- commands.AddHumanCommand(human, orgID, commands.userPasswordHasher, commands.userEncryption, true),
+ commands.AddHumanCommand(human, orgAgg.ID, commands.userPasswordHasher, commands.userEncryption, true),
)
+
+ setupAdminMembers(commands, validations, instanceAgg, orgAgg, humanUserID)
+ }
+ return owner, pat, machineKey, nil
+}
+
+func setupMachineAdmin(commands *Commands, validations *[]preparation.Validation, machine *AddMachine, orgID, userID string) (pat *PersonalAccessToken, machineKey *MachineKey, err error) {
+ *validations = append(*validations,
+ AddMachineCommand(user.NewAggregate(userID, orgID), 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))
}
return pat, machineKey, nil
}
+func setupAdminMembers(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, orgAgg *org.Aggregate, userID string) {
+ *validations = append(*validations,
+ commands.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner),
+ commands.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMOwner),
+ )
+}
+
func setupMessageTexts(validations *[]preparation.Validation, setupMessageTexts []*domain.CustomMessageText, instanceAgg *instance.Aggregate) {
for _, msg := range setupMessageTexts {
*validations = append(*validations, prepareSetInstanceCustomMessageTexts(instanceAgg, msg))
diff --git a/internal/command/instance_domain.go b/internal/command/instance_domain.go
index d5f6808cfb..43e1aebbf8 100644
--- a/internal/command/instance_domain.go
+++ b/internal/command/instance_domain.go
@@ -74,7 +74,7 @@ func (c *Commands) RemoveInstanceDomain(ctx context.Context, instanceDomain stri
}
func (c *Commands) addGeneratedInstanceDomain(ctx context.Context, a *instance.Aggregate, instanceName string) ([]preparation.Validation, error) {
- domain, err := domain.NewGeneratedInstanceDomain(instanceName, authz.GetInstance(ctx).RequestedDomain())
+ domain, err := c.GenerateDomain(instanceName, authz.GetInstance(ctx).RequestedDomain())
if err != nil {
return nil, err
}
diff --git a/internal/command/instance_features.go b/internal/command/instance_features.go
index 35ef57da37..3acc789d1b 100644
--- a/internal/command/instance_features.go
+++ b/internal/command/instance_features.go
@@ -7,6 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/feature"
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -18,6 +19,7 @@ type InstanceFeatures struct {
UserSchema *bool
TokenExchange *bool
Actions *bool
+ ImprovedPerformance []feature.ImprovedPerformanceType
}
func (m *InstanceFeatures) isEmpty() bool {
@@ -26,7 +28,9 @@ func (m *InstanceFeatures) isEmpty() bool {
m.LegacyIntrospection == nil &&
m.UserSchema == nil &&
m.TokenExchange == nil &&
- m.Actions == nil
+ m.Actions == nil &&
+ // nil check to allow unset improvements
+ m.ImprovedPerformance == nil
}
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {
@@ -37,11 +41,11 @@ func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures)
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
return nil, err
}
- cmds := wm.setCommands(ctx, f)
- if len(cmds) == 0 {
+ commands := wm.setCommands(ctx, f)
+ if len(commands) == 0 {
return writeModelToObjectDetails(wm.WriteModel), nil
}
- events, err := c.eventstore.Push(ctx, cmds...)
+ events, err := c.eventstore.Push(ctx, commands...)
if err != nil {
return nil, err
}
diff --git a/internal/command/instance_features_model.go b/internal/command/instance_features_model.go
index 55ce5ea3da..bfd606e672 100644
--- a/internal/command/instance_features_model.go
+++ b/internal/command/instance_features_model.go
@@ -30,14 +30,23 @@ func (m *InstanceFeaturesWriteModel) Reduce() (err error) {
case *feature_v2.ResetEvent:
m.reduceReset()
case *feature_v1.SetEvent[feature_v1.Boolean]:
- err = m.reduceBoolFeature(
- feature_v1.DefaultLoginInstanceEventToV2(e),
+ reduceInstanceFeature(
+ &m.InstanceFeatures,
+ feature.KeyLoginDefaultOrg,
+ feature_v1.DefaultLoginInstanceEventToV2(e).Value,
)
case *feature_v2.SetEvent[bool]:
- err = m.reduceBoolFeature(e)
- }
- if err != nil {
- return err
+ _, key, err := e.FeatureInfo()
+ if err != nil {
+ return err
+ }
+ reduceInstanceFeature(&m.InstanceFeatures, key, e.Value)
+ case *feature_v2.SetEvent[[]feature.ImprovedPerformanceType]:
+ _, key, err := e.FeatureInfo()
+ if err != nil {
+ return err
+ }
+ reduceInstanceFeature(&m.InstanceFeatures, key, e.Value)
}
}
return m.WriteModel.Reduce()
@@ -57,41 +66,41 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceUserSchemaEventType,
feature_v2.InstanceTokenExchangeEventType,
feature_v2.InstanceActionsEventType,
+ feature_v2.InstanceImprovedPerformanceEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
func (m *InstanceFeaturesWriteModel) reduceReset() {
- m.LoginDefaultOrg = nil
- m.TriggerIntrospectionProjections = nil
- m.LegacyIntrospection = nil
- m.UserSchema = nil
- m.TokenExchange = nil
- m.Actions = nil
+ m.InstanceFeatures = InstanceFeatures{}
}
-func (m *InstanceFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
- _, key, err := event.FeatureInfo()
- if err != nil {
- return err
- }
+func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value any) {
switch key {
case feature.KeyUnspecified:
- return nil
+ return
case feature.KeyLoginDefaultOrg:
- m.LoginDefaultOrg = &event.Value
+ v := value.(bool)
+ features.LoginDefaultOrg = &v
case feature.KeyTriggerIntrospectionProjections:
- m.TriggerIntrospectionProjections = &event.Value
+ v := value.(bool)
+ features.TriggerIntrospectionProjections = &v
case feature.KeyLegacyIntrospection:
- m.LegacyIntrospection = &event.Value
+ v := value.(bool)
+ features.LegacyIntrospection = &v
case feature.KeyTokenExchange:
- m.TokenExchange = &event.Value
+ v := value.(bool)
+ features.TokenExchange = &v
case feature.KeyUserSchema:
- m.UserSchema = &event.Value
+ v := value.(bool)
+ features.UserSchema = &v
case feature.KeyActions:
- m.Actions = &event.Value
+ v := value.(bool)
+ features.Actions = &v
+ case feature.KeyImprovedPerformance:
+ v := value.([]feature.ImprovedPerformanceType)
+ features.ImprovedPerformance = v
}
- return nil
}
func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *InstanceFeatures) []eventstore.Command {
@@ -103,5 +112,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.InstanceTokenExchangeEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.InstanceUserSchemaEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.Actions, f.Actions, feature_v2.InstanceActionsEventType)
+ cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.InstanceImprovedPerformanceEventType)
return cmds
}
diff --git a/internal/command/instance_policy_label.go b/internal/command/instance_policy_label.go
index 8392a7d564..3d047f65bc 100644
--- a/internal/command/instance_policy_label.go
+++ b/internal/command/instance_policy_label.go
@@ -12,38 +12,6 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
-func (c *Commands) AddDefaultLabelPolicy(
- ctx context.Context,
- primaryColor, backgroundColor, warnColor, fontColor, primaryColorDark, backgroundColorDark, warnColorDark, fontColorDark string,
- hideLoginNameSuffix, errorMsgPopup, disableWatermark bool, themeMode domain.LabelPolicyThemeMode,
-) (*domain.ObjectDetails, error) {
- instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
- cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter,
- prepareAddDefaultLabelPolicy(
- instanceAgg,
- primaryColor,
- backgroundColor,
- warnColor,
- fontColor,
- primaryColorDark,
- backgroundColorDark,
- warnColorDark,
- fontColorDark,
- hideLoginNameSuffix,
- errorMsgPopup,
- disableWatermark,
- themeMode,
- ))
- if err != nil {
- return nil, err
- }
- pushedEvents, err := c.eventstore.Push(ctx, cmds...)
- if err != nil {
- return nil, err
- }
- return pushedEventsToObjectDetails(pushedEvents), nil
-}
-
func (c *Commands) ChangeDefaultLabelPolicy(ctx context.Context, policy *domain.LabelPolicy) (*domain.LabelPolicy, error) {
if err := policy.IsValid(); err != nil {
return nil, err
@@ -373,6 +341,8 @@ func (c *Commands) getDefaultLabelPolicy(ctx context.Context) (*domain.LabelPoli
return policy, nil
}
+// prepareAddDefaultLabelPolicy adds a default label policy, if none exists prior, and activates it directly
+// this functions is only used on instance setup so the policy can be activated directly
func prepareAddDefaultLabelPolicy(
a *instance.Aggregate,
primaryColor,
@@ -417,6 +387,7 @@ func prepareAddDefaultLabelPolicy(
disableWatermark,
themeMode,
),
+ instance.NewLabelPolicyActivatedEvent(ctx, &a.Aggregate),
}, nil
}, nil
}
diff --git a/internal/command/instance_policy_label_test.go b/internal/command/instance_policy_label_test.go
index ca508d414b..3720fba094 100644
--- a/internal/command/instance_policy_label_test.go
+++ b/internal/command/instance_policy_label_test.go
@@ -19,160 +19,6 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
-func TestCommandSide_AddDefaultLabelPolicy(t *testing.T) {
- type fields struct {
- eventstore *eventstore.Eventstore
- }
- type args struct {
- ctx context.Context
- primaryColor string
- backgroundColor string
- warnColor string
- fontColor string
- primaryColorDark string
- backgroundColorDark string
- warnColorDark string
- fontColorDark string
- hideLoginNameSuffix bool
- errorMsgPopup bool
- disableWatermark bool
- themeMode domain.LabelPolicyThemeMode
- }
- type res struct {
- want *domain.ObjectDetails
- err func(error) bool
- }
- tests := []struct {
- name string
- fields fields
- args args
- res res
- }{
- {
- name: "labelpolicy already existing, already exists error",
- fields: fields{
- eventstore: eventstoreExpect(
- t,
- expectFilter(
- eventFromEventPusher(
- instance.NewLabelPolicyAddedEvent(context.Background(),
- &instance.NewAggregate("INSTANCE").Aggregate,
- "#ffffff",
- "#ffffff",
- "#ffffff",
- "#ffffff",
- "#ffffff",
- "#ffffff",
- "#ffffff",
- "#ffffff",
- true,
- true,
- true,
- domain.LabelPolicyThemeAuto,
- ),
- ),
- ),
- ),
- },
- args: args{
- ctx: context.Background(),
- primaryColor: "#ffffff",
- backgroundColor: "#ffffff",
- warnColor: "#ffffff",
- fontColor: "#ffffff",
- primaryColorDark: "#ffffff",
- backgroundColorDark: "#ffffff",
- warnColorDark: "#ffffff",
- fontColorDark: "#ffffff",
- hideLoginNameSuffix: true,
- errorMsgPopup: true,
- disableWatermark: true,
- themeMode: domain.LabelPolicyThemeAuto,
- },
- res: res{
- err: zerrors.IsErrorAlreadyExists,
- },
- },
- {
- name: "add policy,ok",
- fields: fields{
- eventstore: eventstoreExpect(
- t,
- expectFilter(),
- expectPush(
- instance.NewLabelPolicyAddedEvent(context.Background(),
- &instance.NewAggregate("INSTANCE").Aggregate,
- "#ffffff",
- "#ffffff",
- "#ffffff",
- "#ffffff",
- "#ffffff",
- "#ffffff",
- "#ffffff",
- "#ffffff",
- true,
- true,
- true,
- domain.LabelPolicyThemeDark,
- ),
- ),
- ),
- },
- args: args{
- ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
- primaryColor: "#ffffff",
- backgroundColor: "#ffffff",
- warnColor: "#ffffff",
- fontColor: "#ffffff",
- primaryColorDark: "#ffffff",
- backgroundColorDark: "#ffffff",
- warnColorDark: "#ffffff",
- fontColorDark: "#ffffff",
- hideLoginNameSuffix: true,
- errorMsgPopup: true,
- disableWatermark: true,
- themeMode: domain.LabelPolicyThemeDark,
- },
- res: res{
- want: &domain.ObjectDetails{
- ResourceOwner: "INSTANCE",
- },
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- r := &Commands{
- eventstore: tt.fields.eventstore,
- }
- got, err := r.AddDefaultLabelPolicy(
- tt.args.ctx,
- tt.args.primaryColor,
- tt.args.backgroundColor,
- tt.args.warnColor,
- tt.args.fontColor,
- tt.args.primaryColorDark,
- tt.args.backgroundColorDark,
- tt.args.warnColorDark,
- tt.args.fontColorDark,
- tt.args.hideLoginNameSuffix,
- tt.args.errorMsgPopup,
- tt.args.disableWatermark,
- tt.args.themeMode,
- )
- 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 TestCommandSide_ChangeDefaultLabelPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
diff --git a/internal/command/instance_policy_password_lockout.go b/internal/command/instance_policy_password_lockout.go
index 59766ae38f..9e677c573c 100644
--- a/internal/command/instance_policy_password_lockout.go
+++ b/internal/command/instance_policy_password_lockout.go
@@ -32,7 +32,7 @@ func (c *Commands) AddDefaultLockoutPolicy(ctx context.Context, maxPasswordAttem
}
func (c *Commands) ChangeDefaultLockoutPolicy(ctx context.Context, policy *domain.LockoutPolicy) (*domain.LockoutPolicy, error) {
- existingPolicy, err := c.defaultLockoutPolicyWriteModelByID(ctx)
+ existingPolicy, err := defaultLockoutPolicyWriteModelByID(ctx, c.eventstore.FilterToQueryReducer)
if err != nil {
return nil, err
}
@@ -63,12 +63,12 @@ func (c *Commands) ChangeDefaultLockoutPolicy(ctx context.Context, policy *domai
return writeModelToLockoutPolicy(&existingPolicy.LockoutPolicyWriteModel), nil
}
-func (c *Commands) defaultLockoutPolicyWriteModelByID(ctx context.Context) (policy *InstanceLockoutPolicyWriteModel, err error) {
+func defaultLockoutPolicyWriteModelByID(ctx context.Context, reducer func(ctx context.Context, r eventstore.QueryReducer) error) (policy *InstanceLockoutPolicyWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel := NewInstanceLockoutPolicyWriteModel(ctx)
- err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
+ err = reducer(ctx, writeModel)
if err != nil {
return nil, err
}
diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go
index 8f4f5b68e3..9cfcbb6926 100644
--- a/internal/command/instance_test.go
+++ b/internal/command/instance_test.go
@@ -2,17 +2,1224 @@ package command
import (
"context"
+ "encoding/json"
+ "slices"
+ "strings"
"testing"
+ "time"
"github.com/stretchr/testify/assert"
+ "go.uber.org/mock/gomock"
+ "golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/command/preparation"
+ "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/id"
+ id_mock "github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/instance"
+ "github.com/zitadel/zitadel/internal/repository/org"
+ "github.com/zitadel/zitadel/internal/repository/project"
+ "github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
+func instanceSetupZitadelIDs() ZitadelConfig {
+ return ZitadelConfig{
+ instanceID: "INSTANCE",
+ orgID: "ORG",
+ projectID: "PROJECT",
+ consoleAppID: "console-id",
+ authAppID: "auth-id",
+ mgmtAppID: "mgmt-id",
+ adminAppID: "admin-id",
+ }
+}
+
+func projectAddedEvents(ctx context.Context, instanceID, orgID, id, owner string, externalSecure bool) []eventstore.Command {
+ events := []eventstore.Command{
+ project.NewProjectAddedEvent(ctx,
+ &project.NewAggregate(id, orgID).Aggregate,
+ "ZITADEL",
+ false,
+ false,
+ false,
+ domain.PrivateLabelingSettingUnspecified,
+ ),
+ project.NewProjectMemberAddedEvent(ctx,
+ &project.NewAggregate(id, orgID).Aggregate,
+ owner,
+ domain.RoleProjectOwner,
+ ),
+ instance.NewIAMProjectSetEvent(ctx,
+ &instance.NewAggregate(instanceID).Aggregate,
+ id,
+ ),
+ }
+ events = append(events, apiAppEvents(ctx, orgID, id, "mgmt-id", "Management-API")...)
+ events = append(events, apiAppEvents(ctx, orgID, id, "admin-id", "Admin-API")...)
+ events = append(events, apiAppEvents(ctx, orgID, id, "auth-id", "Auth-API")...)
+
+ consoleAppID := "console-id"
+ consoleClientID := "clientID@zitadel"
+ events = append(events, oidcAppEvents(ctx, orgID, id, consoleAppID, "Console", consoleClientID, externalSecure)...)
+ events = append(events,
+ instance.NewIAMConsoleSetEvent(ctx,
+ &instance.NewAggregate(instanceID).Aggregate,
+ &consoleClientID,
+ &consoleAppID,
+ ),
+ )
+ return events
+}
+
+func projectClientIDs() []string {
+ return []string{"clientID", "clientID", "clientID", "clientID"}
+}
+
+func apiAppEvents(ctx context.Context, orgID, projectID, id, name string) []eventstore.Command {
+ return []eventstore.Command{
+ project.NewApplicationAddedEvent(
+ ctx,
+ &project.NewAggregate(projectID, orgID).Aggregate,
+ id,
+ name,
+ ),
+ project.NewAPIConfigAddedEvent(ctx,
+ &project.NewAggregate(projectID, orgID).Aggregate,
+ id,
+ "clientID@zitadel",
+ "",
+ domain.APIAuthMethodTypePrivateKeyJWT,
+ ),
+ }
+}
+
+func oidcAppEvents(ctx context.Context, orgID, projectID, id, name, clientID string, externalSecure bool) []eventstore.Command {
+ return []eventstore.Command{
+ project.NewApplicationAddedEvent(
+ ctx,
+ &project.NewAggregate(projectID, orgID).Aggregate,
+ id,
+ name,
+ ),
+ project.NewOIDCConfigAddedEvent(ctx,
+ &project.NewAggregate(projectID, orgID).Aggregate,
+ domain.OIDCVersionV1,
+ id,
+ clientID,
+ "",
+ []string{},
+ []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
+ []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
+ domain.OIDCApplicationTypeUserAgent,
+ domain.OIDCAuthMethodTypeNone,
+ []string{},
+ !externalSecure,
+ domain.OIDCTokenTypeBearer,
+ false,
+ false,
+ false,
+ 0,
+ nil,
+ false,
+ ),
+ }
+}
+
+func orgFilters(orgID string, machine, human bool) []expect {
+ filters := []expect{
+ expectFilter(),
+ expectFilter(
+ org.NewOrgAddedEvent(context.Background(), &org.NewAggregate(orgID).Aggregate, ""),
+ ),
+ }
+ if machine {
+ filters = append(filters, machineFilters(orgID, true)...)
+ filters = append(filters, adminMemberFilters(orgID, "USER-MACHINE")...)
+ }
+ if human {
+ filters = append(filters, humanFilters(orgID)...)
+ filters = append(filters, adminMemberFilters(orgID, "USER")...)
+ }
+
+ return append(filters,
+ projectFilters()...,
+ )
+}
+
+func orgEvents(ctx context.Context, instanceID, orgID, name, projectID, defaultDomain string, externalSecure bool, machine, human bool) []eventstore.Command {
+ instanceAgg := instance.NewAggregate(instanceID)
+ orgAgg := org.NewAggregate(orgID)
+ domain := strings.ToLower(name + "." + defaultDomain)
+ events := []eventstore.Command{
+ org.NewOrgAddedEvent(ctx, &orgAgg.Aggregate, name),
+ org.NewDomainAddedEvent(ctx, &orgAgg.Aggregate, domain),
+ org.NewDomainVerifiedEvent(ctx, &orgAgg.Aggregate, domain),
+ org.NewDomainPrimarySetEvent(ctx, &orgAgg.Aggregate, domain),
+ instance.NewDefaultOrgSetEventEvent(ctx, &instanceAgg.Aggregate, orgID),
+ }
+
+ owner := ""
+ if machine {
+ machineID := "USER-MACHINE"
+ events = append(events, machineEvents(ctx, instanceID, orgID, machineID, "PAT")...)
+ owner = machineID
+ }
+ if human {
+ userID := "USER"
+ events = append(events, humanEvents(ctx, instanceID, orgID, userID)...)
+ owner = userID
+ }
+
+ events = append(events, projectAddedEvents(ctx, instanceID, orgID, projectID, owner, externalSecure)...)
+ return events
+}
+
+func orgIDs() []string {
+ return slices.Concat([]string{"USER-MACHINE", "PAT", "USER"}, projectClientIDs())
+}
+
+func instancePoliciesFilters(instanceID string) []expect {
+ return []expect{
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ }
+}
+
+func instancePoliciesEvents(ctx context.Context, instanceID string) []eventstore.Command {
+ instanceAgg := instance.NewAggregate(instanceID)
+ return []eventstore.Command{
+ instance.NewPasswordComplexityPolicyAddedEvent(ctx, &instanceAgg.Aggregate, 8, true, true, true, true),
+ instance.NewPasswordAgePolicyAddedEvent(ctx, &instanceAgg.Aggregate, 0, 0),
+ instance.NewDomainPolicyAddedEvent(ctx, &instanceAgg.Aggregate, false, false, false),
+ instance.NewLoginPolicyAddedEvent(ctx, &instanceAgg.Aggregate, true, true, true, false, false, false, false, true, false, false, domain.PasswordlessTypeAllowed, "", 240*time.Hour, 240*time.Hour, 720*time.Hour, 18*time.Hour, 12*time.Hour),
+ instance.NewLoginPolicySecondFactorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecondFactorTypeTOTP),
+ instance.NewLoginPolicySecondFactorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecondFactorTypeU2F),
+ instance.NewLoginPolicyMultiFactorAddedEvent(ctx, &instanceAgg.Aggregate, domain.MultiFactorTypeU2FWithPIN),
+ instance.NewPrivacyPolicyAddedEvent(ctx, &instanceAgg.Aggregate, "", "", "", "", "", "", ""),
+ instance.NewNotificationPolicyAddedEvent(ctx, &instanceAgg.Aggregate, true),
+ instance.NewLockoutPolicyAddedEvent(ctx, &instanceAgg.Aggregate, 0, 0, true),
+ instance.NewLabelPolicyAddedEvent(ctx, &instanceAgg.Aggregate, "#5469d4", "#fafafa", "#cd3d56", "#000000", "#2073c4", "#111827", "#ff3b5b", "#ffffff", false, false, false, domain.LabelPolicyThemeAuto),
+ instance.NewLabelPolicyActivatedEvent(ctx, &instanceAgg.Aggregate),
+ }
+}
+
+func instanceSetupPoliciesConfig() *InstanceSetup {
+ return &InstanceSetup{
+ PasswordComplexityPolicy: struct {
+ MinLength uint64
+ HasLowercase bool
+ HasUppercase bool
+ HasNumber bool
+ HasSymbol bool
+ }{8, true, true, true, true},
+ PasswordAgePolicy: struct {
+ ExpireWarnDays uint64
+ MaxAgeDays uint64
+ }{0, 0},
+ DomainPolicy: struct {
+ UserLoginMustBeDomain bool
+ ValidateOrgDomains bool
+ SMTPSenderAddressMatchesInstanceDomain bool
+ }{false, false, false},
+ LoginPolicy: struct {
+ AllowUsernamePassword bool
+ AllowRegister bool
+ AllowExternalIDP bool
+ ForceMFA bool
+ ForceMFALocalOnly bool
+ HidePasswordReset bool
+ IgnoreUnknownUsername bool
+ AllowDomainDiscovery bool
+ DisableLoginWithEmail bool
+ DisableLoginWithPhone bool
+ PasswordlessType domain.PasswordlessType
+ DefaultRedirectURI string
+ PasswordCheckLifetime time.Duration
+ ExternalLoginCheckLifetime time.Duration
+ MfaInitSkipLifetime time.Duration
+ SecondFactorCheckLifetime time.Duration
+ MultiFactorCheckLifetime time.Duration
+ }{true, true, true, false, false, false, false, true, false, false, domain.PasswordlessTypeAllowed, "", 240 * time.Hour, 240 * time.Hour, 720 * time.Hour, 18 * time.Hour, 12 * time.Hour},
+ NotificationPolicy: struct {
+ PasswordChange bool
+ }{true},
+ PrivacyPolicy: struct {
+ TOSLink string
+ PrivacyLink string
+ HelpLink string
+ SupportEmail domain.EmailAddress
+ DocsLink string
+ CustomLink string
+ CustomLinkText string
+ }{"", "", "", "", "", "", ""},
+ LabelPolicy: struct {
+ PrimaryColor string
+ BackgroundColor string
+ WarnColor string
+ FontColor string
+ PrimaryColorDark string
+ BackgroundColorDark string
+ WarnColorDark string
+ FontColorDark string
+ HideLoginNameSuffix bool
+ ErrorMsgPopup bool
+ DisableWatermark bool
+ ThemeMode domain.LabelPolicyThemeMode
+ }{"#5469d4", "#fafafa", "#cd3d56", "#000000", "#2073c4", "#111827", "#ff3b5b", "#ffffff", false, false, false, domain.LabelPolicyThemeAuto},
+ LockoutPolicy: struct {
+ MaxPasswordAttempts uint64
+ MaxOTPAttempts uint64
+ ShouldShowLockoutFailure bool
+ }{0, 0, true},
+ }
+}
+
+func setupInstanceElementsFilters(instanceID string) []expect {
+ return slices.Concat(
+ instanceElementsFilters(),
+ instancePoliciesFilters(instanceID),
+ // email template
+ []expect{expectFilter()},
+ )
+}
+
+func setupInstanceElementsConfig() *InstanceSetup {
+ conf := instanceSetupPoliciesConfig()
+ conf.InstanceName = "ZITADEL"
+ conf.DefaultLanguage = language.English
+ conf.zitadel = instanceSetupZitadelIDs()
+ conf.SecretGenerators = instanceElementsConfig()
+ conf.EmailTemplate = []byte("something")
+ return conf
+}
+
+func setupInstanceElementsEvents(ctx context.Context, instanceID, instanceName string, defaultLanguage language.Tag) []eventstore.Command {
+ instanceAgg := instance.NewAggregate(instanceID)
+ return slices.Concat(
+ instanceElementsEvents(ctx, instanceID, instanceName, defaultLanguage),
+ instancePoliciesEvents(ctx, instanceID),
+ []eventstore.Command{instance.NewMailTemplateAddedEvent(ctx, &instanceAgg.Aggregate, []byte("something"))},
+ )
+}
+
+func instanceElementsFilters() []expect {
+ return []expect{
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ }
+}
+
+func instanceElementsEvents(ctx context.Context, instanceID, instanceName string, defaultLanguage language.Tag) []eventstore.Command {
+ instanceAgg := instance.NewAggregate(instanceID)
+ return []eventstore.Command{
+ instance.NewInstanceAddedEvent(ctx, &instanceAgg.Aggregate, instanceName),
+ instance.NewDefaultLanguageSetEvent(ctx, &instanceAgg.Aggregate, defaultLanguage),
+ instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeAppSecret, 64, 0, true, true, true, false),
+ instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeInitCode, 6, 72*time.Hour, false, true, true, false),
+ instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeVerifyEmailCode, 6, time.Hour, false, true, true, false),
+ instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeVerifyPhoneCode, 6, time.Hour, false, true, true, false),
+ instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypePasswordResetCode, 6, time.Hour, false, true, true, false),
+ instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypePasswordlessInitCode, 12, time.Hour, true, true, true, false),
+ instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeVerifyDomain, 32, 0, true, true, true, false),
+ instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeOTPSMS, 8, 5*time.Minute, false, false, true, false),
+ instance.NewSecretGeneratorAddedEvent(ctx, &instanceAgg.Aggregate, domain.SecretGeneratorTypeOTPEmail, 8, 5*time.Minute, false, false, true, false),
+ }
+}
+func instanceElementsConfig() *SecretGenerators {
+ return &SecretGenerators{
+ ClientSecret: &crypto.GeneratorConfig{Length: 64, IncludeLowerLetters: true, IncludeUpperLetters: true, IncludeDigits: true},
+ InitializeUserCode: &crypto.GeneratorConfig{Length: 6, Expiry: 72 * time.Hour, IncludeUpperLetters: true, IncludeDigits: true},
+ EmailVerificationCode: &crypto.GeneratorConfig{Length: 6, Expiry: time.Hour, IncludeUpperLetters: true, IncludeDigits: true},
+ PhoneVerificationCode: &crypto.GeneratorConfig{Length: 6, Expiry: time.Hour, IncludeUpperLetters: true, IncludeDigits: true},
+ PasswordVerificationCode: &crypto.GeneratorConfig{Length: 6, Expiry: time.Hour, IncludeUpperLetters: true, IncludeDigits: true},
+ PasswordlessInitCode: &crypto.GeneratorConfig{Length: 12, Expiry: time.Hour, IncludeLowerLetters: true, IncludeUpperLetters: true, IncludeDigits: true},
+ DomainVerification: &crypto.GeneratorConfig{Length: 32, IncludeLowerLetters: true, IncludeUpperLetters: true, IncludeDigits: true},
+ OTPSMS: &crypto.GeneratorConfig{Length: 8, Expiry: 5 * time.Minute, IncludeDigits: true},
+ OTPEmail: &crypto.GeneratorConfig{Length: 8, Expiry: 5 * time.Minute, IncludeDigits: true},
+ }
+}
+
+func setupInstanceFilters(instanceID, orgID, projectID, appID, domain string) []expect {
+ return slices.Concat(
+ setupInstanceElementsFilters(instanceID),
+ orgFilters(orgID, true, true),
+ generatedDomainFilters(instanceID, orgID, projectID, appID, domain),
+ )
+}
+
+func setupInstanceEvents(ctx context.Context, instanceID, orgID, projectID, appID, instanceName, orgName string, defaultLanguage language.Tag, domain string, externalSecure bool) []eventstore.Command {
+ return slices.Concat(
+ setupInstanceElementsEvents(ctx, instanceID, instanceName, defaultLanguage),
+ orgEvents(ctx, instanceID, orgID, orgName, projectID, domain, externalSecure, true, true),
+ generatedDomainEvents(ctx, instanceID, orgID, projectID, appID, domain),
+ )
+}
+
+func setupInstanceConfig() *InstanceSetup {
+ conf := setupInstanceElementsConfig()
+ conf.Org = InstanceOrgSetup{
+ Name: "ZITADEL",
+ Machine: instanceSetupMachineConfig(),
+ Human: instanceSetupHumanConfig(),
+ }
+ conf.CustomDomain = ""
+ return conf
+}
+
+func generatedDomainEvents(ctx context.Context, instanceID, orgID, projectID, appID, defaultDomain string) []eventstore.Command {
+ instanceAgg := instance.NewAggregate(instanceID)
+ changed, _ := project.NewOIDCConfigChangedEvent(ctx, &project.NewAggregate(projectID, orgID).Aggregate, appID,
+ []project.OIDCConfigChanges{
+ project.ChangeRedirectURIs([]string{"http://" + defaultDomain + "/ui/console/auth/callback"}),
+ project.ChangePostLogoutRedirectURIs([]string{"http://" + defaultDomain + "/ui/console/signedout"}),
+ },
+ )
+ return []eventstore.Command{
+ instance.NewDomainAddedEvent(ctx, &instanceAgg.Aggregate, defaultDomain, true),
+ changed,
+ instance.NewDomainPrimarySetEvent(ctx, &instanceAgg.Aggregate, defaultDomain),
+ }
+}
+
+func generatedDomainFilters(instanceID, orgID, projectID, appID, generatedDomain string) []expect {
+ return []expect{
+ expectFilter(),
+ expectFilter(
+ project.NewApplicationAddedEvent(context.Background(),
+ &project.NewAggregate(projectID, orgID).Aggregate,
+ appID,
+ "console",
+ ),
+ project.NewOIDCConfigAddedEvent(context.Background(),
+ &project.NewAggregate(projectID, orgID).Aggregate,
+ domain.OIDCVersionV1,
+ appID,
+ "clientID",
+ "",
+ []string{},
+ []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
+ []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
+ domain.OIDCApplicationTypeUserAgent,
+ domain.OIDCAuthMethodTypeNone,
+ []string{},
+ true,
+ domain.OIDCTokenTypeBearer,
+ false,
+ false,
+ false,
+ 0,
+ nil,
+ false,
+ ),
+ ),
+ expectFilter(
+ func() eventstore.Event {
+ event := instance.NewDomainAddedEvent(context.Background(),
+ &instance.NewAggregate(instanceID).Aggregate,
+ generatedDomain,
+ true,
+ )
+ event.Data, _ = json.Marshal(event)
+ return event
+ }(),
+ ),
+ }
+}
+
+func humanFilters(orgID string) []expect {
+ return []expect{
+ expectFilter(),
+ expectFilter(
+ org.NewDomainPolicyAddedEvent(
+ context.Background(),
+ &org.NewAggregate(orgID).Aggregate,
+ true,
+ true,
+ true,
+ ),
+ ),
+ expectFilter(
+ org.NewPasswordComplexityPolicyAddedEvent(
+ context.Background(),
+ &org.NewAggregate(orgID).Aggregate,
+ 2,
+ false,
+ false,
+ false,
+ false,
+ ),
+ ),
+ }
+}
+
+func instanceSetupHumanConfig() *AddHuman {
+ return &AddHuman{
+ Username: "zitadel-admin",
+ FirstName: "ZITADEL",
+ LastName: "Admin",
+ Email: Email{
+ Address: domain.EmailAddress("admin@zitadel.test"),
+ Verified: true,
+ },
+ PreferredLanguage: language.English,
+ Password: "password",
+ PasswordChangeRequired: false,
+ }
+}
+
+func machineFilters(orgID string, pat bool) []expect {
+ filters := []expect{
+ expectFilter(),
+ expectFilter(
+ org.NewDomainPolicyAddedEvent(
+ context.Background(),
+ &org.NewAggregate(orgID).Aggregate,
+ true,
+ true,
+ true,
+ ),
+ ),
+ }
+ if pat {
+ filters = append(filters,
+ expectFilter(),
+ expectFilter(),
+ )
+ }
+ return filters
+}
+
+func instanceSetupMachineConfig() *AddMachine {
+ return &AddMachine{
+ Machine: &Machine{
+ Username: "zitadel-admin-machine",
+ Name: "ZITADEL-machine",
+ Description: "Admin",
+ AccessTokenType: domain.OIDCTokenTypeBearer,
+ },
+ Pat: &AddPat{
+ ExpirationDate: time.Time{},
+ Scopes: nil,
+ },
+ /* not predictable with the key value in the events
+ MachineKey: &AddMachineKey{
+ Type: domain.AuthNKeyTypeJSON,
+ ExpirationDate: time.Time{},
+ },
+ */
+ }
+}
+
+func projectFilters() []expect {
+ return []expect{
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ expectFilter(),
+ }
+}
+
+func adminMemberFilters(orgID, userID string) []expect {
+ return []expect{
+ expectFilter(
+ addHumanEvent(context.Background(), orgID, userID),
+ ),
+ expectFilter(),
+ expectFilter(
+ addHumanEvent(context.Background(), orgID, userID),
+ ),
+ expectFilter(),
+ }
+}
+
+func humanEvents(ctx context.Context, instanceID, orgID, userID string) []eventstore.Command {
+ agg := user.NewAggregate(userID, orgID)
+ instanceAgg := instance.NewAggregate(instanceID)
+ orgAgg := org.NewAggregate(orgID)
+ return []eventstore.Command{
+ addHumanEvent(ctx, orgID, userID),
+ user.NewHumanEmailVerifiedEvent(ctx, &agg.Aggregate),
+ org.NewMemberAddedEvent(ctx, &orgAgg.Aggregate, userID, domain.RoleOrgOwner),
+ instance.NewMemberAddedEvent(ctx, &instanceAgg.Aggregate, userID, domain.RoleIAMOwner),
+ }
+}
+
+func addHumanEvent(ctx context.Context, orgID, userID string) *user.HumanAddedEvent {
+ agg := user.NewAggregate(userID, orgID)
+ return func() *user.HumanAddedEvent {
+ event := user.NewHumanAddedEvent(
+ ctx,
+ &agg.Aggregate,
+ "zitadel-admin",
+ "ZITADEL",
+ "Admin",
+ "",
+ "ZITADEL Admin",
+ language.English,
+ 0,
+ "admin@zitadel.test",
+ false,
+ )
+ event.AddPasswordData("$plain$x$password", false)
+ return event
+ }()
+}
+
+// machineEvents all events from setup to create the machine user, machinekey can't be tested here, as the public key is not provided and as such the value in the event can't be expected
+func machineEvents(ctx context.Context, instanceID, orgID, userID, patID string) []eventstore.Command {
+ agg := user.NewAggregate(userID, orgID)
+ instanceAgg := instance.NewAggregate(instanceID)
+ orgAgg := org.NewAggregate(orgID)
+ events := []eventstore.Command{addMachineEvent(ctx, orgID, userID)}
+ if patID != "" {
+ events = append(events,
+ user.NewPersonalAccessTokenAddedEvent(
+ ctx,
+ &agg.Aggregate,
+ patID,
+ time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC),
+ nil,
+ ),
+ )
+ }
+ return append(events,
+ org.NewMemberAddedEvent(ctx, &orgAgg.Aggregate, userID, domain.RoleOrgOwner),
+ instance.NewMemberAddedEvent(ctx, &instanceAgg.Aggregate, userID, domain.RoleIAMOwner),
+ )
+}
+
+func addMachineEvent(ctx context.Context, orgID, userID string) *user.MachineAddedEvent {
+ agg := user.NewAggregate(userID, orgID)
+ return user.NewMachineAddedEvent(ctx,
+ &agg.Aggregate,
+ "zitadel-admin-machine",
+ "ZITADEL-machine",
+ "Admin",
+ false,
+ domain.OIDCTokenTypeBearer,
+ )
+}
+
+func testSetup(ctx context.Context, c *Commands, validations []preparation.Validation) error {
+ //nolint:staticcheck
+ cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
+ if err != nil {
+ return err
+ }
+
+ _, err = c.eventstore.Push(ctx, cmds...)
+ return err
+}
+
+func TestCommandSide_setupMinimalInterfaces(t *testing.T) {
+ type fields struct {
+ eventstore func(t *testing.T) *eventstore.Eventstore
+ idGenerator id.Generator
+ }
+ type args struct {
+ ctx context.Context
+ instanceAgg *instance.Aggregate
+ orgAgg *org.Aggregate
+ owner string
+ ids ZitadelConfig
+ }
+ type res struct {
+ err func(error) bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "create, ok",
+ fields: fields{
+ eventstore: expectEventstore(
+ slices.Concat(
+ projectFilters(),
+ []expect{expectPush(
+ projectAddedEvents(context.Background(),
+ "INSTANCE",
+ "ORG",
+ "PROJECT",
+ "owner",
+ false,
+ )...,
+ ),
+ },
+ )...,
+ ),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, projectClientIDs()...),
+ },
+ args: args{
+ ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"),
+ instanceAgg: instance.NewAggregate("INSTANCE"),
+ orgAgg: org.NewAggregate("ORG"),
+ owner: "owner",
+ ids: instanceSetupZitadelIDs(),
+ },
+ res: res{
+ err: nil,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore(t),
+ idGenerator: tt.fields.idGenerator,
+ }
+ validations := make([]preparation.Validation, 0)
+ setupMinimalInterfaces(r, &validations, tt.args.instanceAgg, tt.args.orgAgg, tt.args.owner, tt.args.ids)
+
+ err := testSetup(tt.args.ctx, r, validations)
+ 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)
+ }
+ })
+ }
+}
+
+func TestCommandSide_setupAdmins(t *testing.T) {
+ type fields struct {
+ eventstore func(t *testing.T) *eventstore.Eventstore
+ idGenerator id.Generator
+ userPasswordHasher *crypto.Hasher
+ roles []authz.RoleMapping
+ keyAlgorithm crypto.EncryptionAlgorithm
+ }
+ type args struct {
+ ctx context.Context
+ instanceAgg *instance.Aggregate
+ orgAgg *org.Aggregate
+ machine *AddMachine
+ human *AddHuman
+ }
+ type res struct {
+ owner string
+ pat bool
+ machineKey bool
+ err func(error) bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "human, ok",
+ fields: fields{
+ eventstore: expectEventstore(
+ slices.Concat(
+ humanFilters("ORG"),
+ adminMemberFilters("ORG", "USER"),
+ []expect{
+ expectPush(
+ humanEvents(context.Background(),
+ "INSTANCE",
+ "ORG",
+ "USER",
+ )...,
+ ),
+ },
+ )...,
+ ),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER"),
+ userPasswordHasher: mockPasswordHasher("x"),
+ roles: []authz.RoleMapping{
+ {Role: domain.RoleOrgOwner, Permissions: []string{""}},
+ {Role: domain.RoleIAMOwner, Permissions: []string{""}},
+ },
+ },
+ args: args{
+ ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"),
+ instanceAgg: instance.NewAggregate("INSTANCE"),
+ orgAgg: org.NewAggregate("ORG"),
+ human: instanceSetupHumanConfig(),
+ },
+ res: res{
+ owner: "USER",
+ pat: false,
+ machineKey: false,
+ err: nil,
+ },
+ },
+ {
+ name: "machine, ok",
+ fields: fields{
+ eventstore: expectEventstore(
+ slices.Concat(
+ machineFilters("ORG", true),
+ adminMemberFilters("ORG", "USER-MACHINE"),
+ []expect{
+ expectPush(
+ machineEvents(context.Background(),
+ "INSTANCE",
+ "ORG",
+ "USER-MACHINE",
+ "PAT",
+ )...,
+ ),
+ },
+ )...,
+ ),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT"),
+ roles: []authz.RoleMapping{
+ {Role: domain.RoleOrgOwner, Permissions: []string{""}},
+ {Role: domain.RoleIAMOwner, Permissions: []string{""}},
+ },
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"),
+ instanceAgg: instance.NewAggregate("INSTANCE"),
+ orgAgg: org.NewAggregate("ORG"),
+ machine: instanceSetupMachineConfig(),
+ },
+ res: res{
+ owner: "USER-MACHINE",
+ pat: true,
+ machineKey: false,
+ err: nil,
+ },
+ },
+ {
+ name: "human and machine, ok",
+ fields: fields{
+ eventstore: expectEventstore(
+ slices.Concat(
+ machineFilters("ORG", true),
+ adminMemberFilters("ORG", "USER-MACHINE"),
+ humanFilters("ORG"),
+ adminMemberFilters("ORG", "USER"),
+ []expect{
+ expectPush(
+ slices.Concat(
+ machineEvents(context.Background(),
+ "INSTANCE",
+ "ORG",
+ "USER-MACHINE",
+ "PAT",
+ ),
+ humanEvents(context.Background(),
+ "INSTANCE",
+ "ORG",
+ "USER",
+ ),
+ )...,
+ ),
+ },
+ )...,
+ ),
+ userPasswordHasher: mockPasswordHasher("x"),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT", "USER"),
+ roles: []authz.RoleMapping{
+ {Role: domain.RoleOrgOwner, Permissions: []string{""}},
+ {Role: domain.RoleIAMOwner, Permissions: []string{""}},
+ },
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"),
+ instanceAgg: instance.NewAggregate("INSTANCE"),
+ orgAgg: org.NewAggregate("ORG"),
+ machine: instanceSetupMachineConfig(),
+ human: instanceSetupHumanConfig(),
+ },
+ res: res{
+ owner: "USER",
+ pat: true,
+ machineKey: false,
+ err: nil,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore(t),
+ idGenerator: tt.fields.idGenerator,
+ zitadelRoles: tt.fields.roles,
+ userPasswordHasher: tt.fields.userPasswordHasher,
+ keyAlgorithm: tt.fields.keyAlgorithm,
+ }
+ validations := make([]preparation.Validation, 0)
+ owner, pat, mk, err := setupAdmins(r, &validations, tt.args.instanceAgg, tt.args.orgAgg, tt.args.machine, tt.args.human)
+ 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)
+ }
+
+ err = testSetup(tt.args.ctx, r, validations)
+ 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, owner, tt.res.owner)
+ if tt.res.pat {
+ assert.NotNil(t, pat)
+ }
+ if tt.res.machineKey {
+ assert.NotNil(t, mk)
+ }
+ }
+ })
+ }
+}
+
+func TestCommandSide_setupDefaultOrg(t *testing.T) {
+ type fields struct {
+ eventstore func(t *testing.T) *eventstore.Eventstore
+ idGenerator id.Generator
+ userPasswordHasher *crypto.Hasher
+ roles []authz.RoleMapping
+ keyAlgorithm crypto.EncryptionAlgorithm
+ }
+ type args struct {
+ ctx context.Context
+ instanceAgg *instance.Aggregate
+ orgName string
+ machine *AddMachine
+ human *AddHuman
+ ids ZitadelConfig
+ }
+ type res struct {
+ pat bool
+ machineKey bool
+ err func(error) bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "human and machine, ok",
+ fields: fields{
+ eventstore: expectEventstore(
+ slices.Concat(
+ orgFilters(
+ "ORG",
+ true,
+ true,
+ ),
+ []expect{
+ expectPush(
+ slices.Concat(
+ orgEvents(context.Background(),
+ "INSTANCE",
+ "ORG",
+ "ZITADEL",
+ "PROJECT",
+ "DOMAIN",
+ false,
+ true,
+ true,
+ ),
+ )...,
+ ),
+ },
+ )...,
+ ),
+ userPasswordHasher: mockPasswordHasher("x"),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...),
+ roles: []authz.RoleMapping{
+ {Role: domain.RoleOrgOwner, Permissions: []string{""}},
+ {Role: domain.RoleIAMOwner, Permissions: []string{""}},
+ },
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"),
+ instanceAgg: instance.NewAggregate("INSTANCE"),
+ orgName: "ZITADEL",
+ machine: &AddMachine{
+ Machine: &Machine{
+ Username: "zitadel-admin-machine",
+ Name: "ZITADEL-machine",
+ Description: "Admin",
+ AccessTokenType: domain.OIDCTokenTypeBearer,
+ },
+ Pat: &AddPat{
+ ExpirationDate: time.Time{},
+ Scopes: nil,
+ },
+ /* not predictable with the key value in the events
+ MachineKey: &AddMachineKey{
+ Type: domain.AuthNKeyTypeJSON,
+ ExpirationDate: time.Time{},
+ },
+ */
+ },
+ human: &AddHuman{
+ Username: "zitadel-admin",
+ FirstName: "ZITADEL",
+ LastName: "Admin",
+ Email: Email{
+ Address: domain.EmailAddress("admin@zitadel.test"),
+ Verified: true,
+ },
+ PreferredLanguage: language.English,
+ Password: "password",
+ PasswordChangeRequired: false,
+ },
+ ids: ZitadelConfig{
+ instanceID: "INSTANCE",
+ orgID: "ORG",
+ projectID: "PROJECT",
+ consoleAppID: "console-id",
+ authAppID: "auth-id",
+ mgmtAppID: "mgmt-id",
+ adminAppID: "admin-id",
+ },
+ },
+ res: res{
+ pat: true,
+ machineKey: false,
+ err: nil,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore(t),
+ idGenerator: tt.fields.idGenerator,
+ zitadelRoles: tt.fields.roles,
+ userPasswordHasher: tt.fields.userPasswordHasher,
+ keyAlgorithm: tt.fields.keyAlgorithm,
+ }
+ validations := make([]preparation.Validation, 0)
+ pat, mk, err := setupDefaultOrg(tt.args.ctx, r, &validations, tt.args.instanceAgg, tt.args.orgName, tt.args.machine, tt.args.human, tt.args.ids)
+ 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)
+ }
+
+ err = testSetup(context.Background(), r, validations)
+ 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 {
+ if tt.res.pat {
+ assert.NotNil(t, pat)
+ }
+ if tt.res.machineKey {
+ assert.NotNil(t, mk)
+ }
+ }
+ })
+ }
+}
+
+func TestCommandSide_setupInstanceElements(t *testing.T) {
+ type fields struct {
+ eventstore func(t *testing.T) *eventstore.Eventstore
+ }
+ type args struct {
+ ctx context.Context
+ instanceAgg *instance.Aggregate
+ setup *InstanceSetup
+ }
+ type res struct {
+ err func(error) bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "ok",
+ fields: fields{
+ eventstore: expectEventstore(
+ slices.Concat(
+ setupInstanceElementsFilters("INSTANCE"),
+ []expect{
+ expectPush(
+ setupInstanceElementsEvents(context.Background(),
+ "INSTANCE",
+ "ZITADEL",
+ language.English,
+ )...,
+ ),
+ },
+ )...,
+ ),
+ },
+ args: args{
+ ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"),
+ instanceAgg: instance.NewAggregate("INSTANCE"),
+ setup: setupInstanceElementsConfig(),
+ },
+ res: res{
+ err: nil,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore(t),
+ }
+ validations := setupInstanceElements(tt.args.instanceAgg, tt.args.setup)
+
+ err := testSetup(context.Background(), r, validations)
+ 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)
+ }
+ })
+ }
+}
+
+func TestCommandSide_setUpInstance(t *testing.T) {
+ type fields struct {
+ eventstore func(t *testing.T) *eventstore.Eventstore
+ idGenerator id.Generator
+ userPasswordHasher *crypto.Hasher
+ roles []authz.RoleMapping
+ keyAlgorithm crypto.EncryptionAlgorithm
+ generateDomain func(string, string) (string, error)
+ }
+ type args struct {
+ ctx context.Context
+ setup *InstanceSetup
+ }
+ type res struct {
+ pat bool
+ machineKey bool
+ err func(error) bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "ok",
+ fields: fields{
+ eventstore: expectEventstore(
+ slices.Concat(
+ setupInstanceFilters("INSTANCE", "ORG", "PROJECT", "console-id", "DOMAIN"),
+ []expect{
+ expectPush(
+ setupInstanceEvents(context.Background(),
+ "INSTANCE",
+ "ORG",
+ "PROJECT",
+ "console-id",
+ "ZITADEL",
+ "ZITADEL",
+ language.English,
+ "DOMAIN",
+ false,
+ )...,
+ ),
+ },
+ )...,
+ ),
+ userPasswordHasher: mockPasswordHasher("x"),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...),
+ roles: []authz.RoleMapping{
+ {Role: domain.RoleOrgOwner, Permissions: []string{""}},
+ {Role: domain.RoleIAMOwner, Permissions: []string{""}},
+ },
+ keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ generateDomain: func(string, string) (string, error) {
+ return "DOMAIN", nil
+ },
+ },
+ args: args{
+ ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN"),
+ setup: setupInstanceConfig(),
+ },
+ res: res{
+ err: nil,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore(t),
+ idGenerator: tt.fields.idGenerator,
+ zitadelRoles: tt.fields.roles,
+ userPasswordHasher: tt.fields.userPasswordHasher,
+ keyAlgorithm: tt.fields.keyAlgorithm,
+ GenerateDomain: tt.fields.generateDomain,
+ }
+
+ validations, pat, mk, err := setUpInstance(tt.args.ctx, r, tt.args.setup)
+ 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)
+ }
+
+ err = testSetup(tt.args.ctx, r, validations)
+ 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 {
+ if tt.res.pat {
+ assert.NotNil(t, pat)
+ }
+ if tt.res.machineKey {
+ assert.NotNil(t, mk)
+ }
+ }
+ })
+ }
+}
+
func TestCommandSide_UpdateInstance(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
diff --git a/internal/command/org.go b/internal/command/org.go
index 5f997183af..db963762b1 100644
--- a/internal/command/org.go
+++ b/internal/command/org.go
@@ -63,7 +63,7 @@ type CreatedOrgAdmin struct {
}
func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID string, allowInitialMail bool, userIDs ...string) (_ *CreatedOrg, err error) {
- cmds := c.newOrgSetupCommands(ctx, orgID, o, userIDs)
+ cmds := c.newOrgSetupCommands(ctx, orgID, o)
for _, admin := range o.Admins {
if err = cmds.setupOrgAdmin(admin, allowInitialMail); err != nil {
return nil, err
@@ -76,10 +76,10 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID strin
return cmds.push(ctx)
}
-func (c *Commands) newOrgSetupCommands(ctx context.Context, orgID string, orgSetup *OrgSetup, userIDs []string) *orgSetupCommands {
+func (c *Commands) newOrgSetupCommands(ctx context.Context, orgID string, orgSetup *OrgSetup) *orgSetupCommands {
orgAgg := org.NewAggregate(orgID)
validations := []preparation.Validation{
- AddOrgCommand(ctx, orgAgg, orgSetup.Name, userIDs...),
+ AddOrgCommand(ctx, orgAgg, orgSetup.Name),
}
return &orgSetupCommands{
validations: validations,
@@ -233,7 +233,7 @@ func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, allowInitialMail b
// AddOrgCommand defines the commands to create a new org,
// this includes the verified default domain
-func AddOrgCommand(ctx context.Context, a *org.Aggregate, name string, userIDs ...string) preparation.Validation {
+func AddOrgCommand(ctx context.Context, a *org.Aggregate, name string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if name = strings.TrimSpace(name); name == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument")
diff --git a/internal/command/org_policy_lockout.go b/internal/command/org_policy_lockout.go
index 052fd9e239..d7ace6f69e 100644
--- a/internal/command/org_policy_lockout.go
+++ b/internal/command/org_policy_lockout.go
@@ -4,6 +4,7 @@ import (
"context"
"github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -12,7 +13,7 @@ func (c *Commands) AddLockoutPolicy(ctx context.Context, resourceOwner string, p
if resourceOwner == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "Org-8fJif", "Errors.ResourceOwnerMissing")
}
- addedPolicy, err := c.orgLockoutPolicyWriteModelByID(ctx, resourceOwner)
+ addedPolicy, err := orgLockoutPolicyWriteModelByID(ctx, resourceOwner, c.eventstore.FilterToQueryReducer)
if err != nil {
return nil, err
}
@@ -42,7 +43,7 @@ func (c *Commands) ChangeLockoutPolicy(ctx context.Context, resourceOwner string
if resourceOwner == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "Org-3J9fs", "Errors.ResourceOwnerMissing")
}
- existingPolicy, err := c.orgLockoutPolicyWriteModelByID(ctx, resourceOwner)
+ existingPolicy, err := orgLockoutPolicyWriteModelByID(ctx, resourceOwner, c.eventstore.FilterToQueryReducer)
if err != nil {
return nil, err
}
@@ -71,7 +72,7 @@ func (c *Commands) RemoveLockoutPolicy(ctx context.Context, orgID string) (*doma
if orgID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "Org-4J9fs", "Errors.ResourceOwnerMissing")
}
- existingPolicy, err := c.orgLockoutPolicyWriteModelByID(ctx, orgID)
+ existingPolicy, err := orgLockoutPolicyWriteModelByID(ctx, orgID, c.eventstore.FilterToQueryReducer)
if err != nil {
return nil, err
}
@@ -93,7 +94,7 @@ func (c *Commands) RemoveLockoutPolicy(ctx context.Context, orgID string) (*doma
}
func (c *Commands) removeLockoutPolicyIfExists(ctx context.Context, orgID string) (*org.LockoutPolicyRemovedEvent, error) {
- existingPolicy, err := c.orgLockoutPolicyWriteModelByID(ctx, orgID)
+ existingPolicy, err := orgLockoutPolicyWriteModelByID(ctx, orgID, c.eventstore.FilterToQueryReducer)
if err != nil {
return nil, err
}
@@ -104,24 +105,24 @@ func (c *Commands) removeLockoutPolicyIfExists(ctx context.Context, orgID string
return org.NewLockoutPolicyRemovedEvent(ctx, orgAgg), nil
}
-func (c *Commands) orgLockoutPolicyWriteModelByID(ctx context.Context, orgID string) (*OrgLockoutPolicyWriteModel, error) {
+func orgLockoutPolicyWriteModelByID(ctx context.Context, orgID string, queryReducer func(ctx context.Context, r eventstore.QueryReducer) error) (*OrgLockoutPolicyWriteModel, error) {
policy := NewOrgLockoutPolicyWriteModel(orgID)
- err := c.eventstore.FilterToQueryReducer(ctx, policy)
+ err := queryReducer(ctx, policy)
if err != nil {
return nil, err
}
return policy, nil
}
-func (c *Commands) getLockoutPolicy(ctx context.Context, orgID string) (*domain.LockoutPolicy, error) {
- orgWm, err := c.orgLockoutPolicyWriteModelByID(ctx, orgID)
+func getLockoutPolicy(ctx context.Context, orgID string, queryReducer func(ctx context.Context, r eventstore.QueryReducer) error) (*domain.LockoutPolicy, error) {
+ orgWm, err := orgLockoutPolicyWriteModelByID(ctx, orgID, queryReducer)
if err != nil {
return nil, err
}
if orgWm.State == domain.PolicyStateActive {
return writeModelToLockoutPolicy(&orgWm.LockoutPolicyWriteModel), nil
}
- instanceWm, err := c.defaultLockoutPolicyWriteModelByID(ctx)
+ instanceWm, err := defaultLockoutPolicyWriteModelByID(ctx, queryReducer)
if err != nil {
return nil, err
}
diff --git a/internal/command/session.go b/internal/command/session.go
index c9fd29ac46..ccc224cab7 100644
--- a/internal/command/session.go
+++ b/internal/command/session.go
@@ -7,6 +7,7 @@ import (
"fmt"
"time"
+ "github.com/zitadel/logging"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/activity"
@@ -17,21 +18,18 @@ import (
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/repository/user"
- "github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
-type SessionCommand func(ctx context.Context, cmd *SessionCommands) error
+type SessionCommand func(ctx context.Context, cmd *SessionCommands) ([]eventstore.Command, error)
type SessionCommands struct {
sessionCommands []SessionCommand
- sessionWriteModel *SessionWriteModel
- passwordWriteModel *HumanPasswordWriteModel
- intentWriteModel *IDPIntentWriteModel
- totpWriteModel *HumanTOTPWriteModel
- eventstore *eventstore.Eventstore
- eventCommands []eventstore.Command
+ sessionWriteModel *SessionWriteModel
+ intentWriteModel *IDPIntentWriteModel
+ eventstore *eventstore.Eventstore
+ eventCommands []eventstore.Command
hasher *crypto.Hasher
intentAlg crypto.EncryptionAlgorithm
@@ -59,114 +57,92 @@ func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWri
// CheckUser defines a user check to be executed for a session update
func CheckUser(id string, resourceOwner string, preferredLanguage *language.Tag) SessionCommand {
- return func(ctx context.Context, cmd *SessionCommands) error {
+ return func(ctx context.Context, cmd *SessionCommands) ([]eventstore.Command, error) {
if cmd.sessionWriteModel.UserID != "" && id != "" && cmd.sessionWriteModel.UserID != id {
- return zerrors.ThrowInvalidArgument(nil, "", "user change not possible")
+ return nil, zerrors.ThrowInvalidArgument(nil, "", "user change not possible")
}
- return cmd.UserChecked(ctx, id, resourceOwner, cmd.now(), preferredLanguage)
+ return nil, cmd.UserChecked(ctx, id, resourceOwner, cmd.now(), preferredLanguage)
}
}
// CheckPassword defines a password check to be executed for a session update
func CheckPassword(password string) SessionCommand {
- return func(ctx context.Context, cmd *SessionCommands) error {
- if cmd.sessionWriteModel.UserID == "" {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Sfw3f", "Errors.User.UserIDMissing")
- }
- cmd.passwordWriteModel = NewHumanPasswordWriteModel(cmd.sessionWriteModel.UserID, "")
- err := cmd.eventstore.FilterToQueryReducer(ctx, cmd.passwordWriteModel)
+ return func(ctx context.Context, cmd *SessionCommands) ([]eventstore.Command, error) {
+ commands, err := checkPassword(ctx, cmd.sessionWriteModel.UserID, password, cmd.eventstore, cmd.hasher, nil)
if err != nil {
- return err
+ return commands, err
}
- if cmd.passwordWriteModel.UserState == domain.UserStateUnspecified || cmd.passwordWriteModel.UserState == domain.UserStateDeleted {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Df4b3", "Errors.User.NotFound")
- }
-
- if cmd.passwordWriteModel.EncodedHash == "" {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-WEf3t", "Errors.User.Password.NotSet")
- }
- ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "passwap.Verify")
- updated, err := cmd.hasher.Verify(cmd.passwordWriteModel.EncodedHash, password)
- spanPasswordComparison.EndWithError(err)
- if err != nil {
- //TODO: maybe we want to reset the session in the future https://github.com/zitadel/zitadel/issues/5807
- return zerrors.ThrowInvalidArgument(err, "COMMAND-SAF3g", "Errors.User.Password.Invalid")
- }
- if updated != "" {
- cmd.eventCommands = append(cmd.eventCommands, user.NewHumanPasswordHashUpdatedEvent(ctx, UserAggregateFromWriteModel(&cmd.passwordWriteModel.WriteModel), updated))
- }
-
+ cmd.eventCommands = append(cmd.eventCommands, commands...)
cmd.PasswordChecked(ctx, cmd.now())
- return nil
+ return nil, nil
}
}
// CheckIntent defines a check for a succeeded intent to be executed for a session update
func CheckIntent(intentID, token string) SessionCommand {
- return func(ctx context.Context, cmd *SessionCommands) error {
+ return func(ctx context.Context, cmd *SessionCommands) ([]eventstore.Command, error) {
if cmd.sessionWriteModel.UserID == "" {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Sfw3r", "Errors.User.UserIDMissing")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Sfw3r", "Errors.User.UserIDMissing")
}
if err := crypto.CheckToken(cmd.intentAlg, token, intentID); err != nil {
- return err
+ return nil, err
}
cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "")
err := cmd.eventstore.FilterToQueryReducer(ctx, cmd.intentWriteModel)
if err != nil {
- return err
+ return nil, err
}
if cmd.intentWriteModel.State != domain.IDPIntentStateSucceeded {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded")
}
if cmd.intentWriteModel.UserID != "" {
if cmd.intentWriteModel.UserID != cmd.sessionWriteModel.UserID {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser")
}
} else {
- linkWriteModel := NewUserIDPLinkWriteModel(cmd.sessionWriteModel.UserID, cmd.intentWriteModel.IDPID, cmd.intentWriteModel.IDPUserID, cmd.intentWriteModel.ResourceOwner)
+ linkWriteModel := NewUserIDPLinkWriteModel(cmd.sessionWriteModel.UserID, cmd.intentWriteModel.IDPID, cmd.intentWriteModel.IDPUserID, cmd.sessionWriteModel.UserResourceOwner)
err := cmd.eventstore.FilterToQueryReducer(ctx, linkWriteModel)
if err != nil {
- return err
+ return nil, err
}
if linkWriteModel.State != domain.UserIDPLinkStateActive {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser")
}
}
cmd.IntentChecked(ctx, cmd.now())
- return nil
+ return nil, nil
}
}
func CheckTOTP(code string) SessionCommand {
- return func(ctx context.Context, cmd *SessionCommands) (err error) {
- if cmd.sessionWriteModel.UserID == "" {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Neil7", "Errors.User.UserIDMissing")
- }
- cmd.totpWriteModel = NewHumanTOTPWriteModel(cmd.sessionWriteModel.UserID, "")
- err = cmd.eventstore.FilterToQueryReducer(ctx, cmd.totpWriteModel)
+ return func(ctx context.Context, cmd *SessionCommands) (_ []eventstore.Command, err error) {
+ commands, err := checkTOTP(
+ ctx,
+ cmd.sessionWriteModel.UserID,
+ "",
+ code,
+ cmd.eventstore.FilterToQueryReducer,
+ cmd.totpAlg,
+ nil,
+ )
if err != nil {
- return err
- }
- if cmd.totpWriteModel.State != domain.MFAStateReady {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-eej1U", "Errors.User.MFA.OTP.NotReady")
- }
- err = domain.VerifyTOTP(code, cmd.totpWriteModel.Secret, cmd.totpAlg)
- if err != nil {
- return err
+ return commands, err
}
+ cmd.eventCommands = append(cmd.eventCommands, commands...)
cmd.TOTPChecked(ctx, cmd.now())
- return nil
+ return nil, nil
}
}
-// Exec will execute the commands specified and returns an error on the first occurrence
-func (s *SessionCommands) Exec(ctx context.Context) error {
+// Exec will execute the commands specified and returns an error on the first occurrence.
+// In case of an error there might be specific commands returned, e.g. a failed pw check will have to be stored.
+func (s *SessionCommands) Exec(ctx context.Context) ([]eventstore.Command, error) {
for _, cmd := range s.sessionCommands {
- if err := cmd(ctx, s); err != nil {
- return err
+ if cmds, err := cmd(ctx, s); err != nil {
+ return cmds, err
}
}
- return nil
+ return nil, nil
}
func (s *SessionCommands) Start(ctx context.Context, userAgent *domain.UserAgent) {
@@ -360,8 +336,11 @@ func (c *Commands) updateSession(ctx context.Context, checks *SessionCommands, m
if err = checks.sessionWriteModel.CheckNotInvalidated(); err != nil {
return nil, err
}
- if err := checks.Exec(ctx); err != nil {
- // TODO: how to handle failed checks (e.g. pw wrong) https://github.com/zitadel/zitadel/issues/5807
+ if cmds, err := checks.Exec(ctx); err != nil {
+ if len(cmds) > 0 {
+ _, pushErr := c.eventstore.Push(ctx, cmds...)
+ logging.OnError(pushErr).Error("unable to store check failures")
+ }
return nil, err
}
checks.ChangeMetadata(ctx, metadata)
diff --git a/internal/command/session_otp.go b/internal/command/session_otp.go
index 859f893400..6b7e20fb1a 100644
--- a/internal/command/session_otp.go
+++ b/internal/command/session_otp.go
@@ -6,9 +6,10 @@ import (
"golang.org/x/text/language"
- "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/session"
+ "github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -21,26 +22,26 @@ func (c *Commands) CreateOTPSMSChallenge() SessionCommand {
}
func (c *Commands) createOTPSMSChallenge(returnCode bool, dst *string) SessionCommand {
- return func(ctx context.Context, cmd *SessionCommands) error {
+ return func(ctx context.Context, cmd *SessionCommands) ([]eventstore.Command, error) {
if cmd.sessionWriteModel.UserID == "" {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-JKL3g", "Errors.User.UserIDMissing")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-JKL3g", "Errors.User.UserIDMissing")
}
writeModel := NewHumanOTPSMSWriteModel(cmd.sessionWriteModel.UserID, "")
if err := cmd.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
- return err
+ return nil, err
}
if !writeModel.OTPAdded() {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-BJ2g3", "Errors.User.MFA.OTP.NotReady")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-BJ2g3", "Errors.User.MFA.OTP.NotReady")
}
code, err := cmd.createCode(ctx, cmd.eventstore.Filter, domain.SecretGeneratorTypeOTPSMS, cmd.otpAlg, c.defaultSecretGenerators.OTPSMS)
if err != nil {
- return err
+ return nil, err
}
if returnCode {
*dst = code.Plain
}
cmd.OTPSMSChallenged(ctx, code.Crypted, code.Expiry, returnCode)
- return nil
+ return nil, nil
}
}
@@ -74,26 +75,26 @@ func (c *Commands) CreateOTPEmailChallenge() SessionCommand {
}
func (c *Commands) createOTPEmailChallenge(returnCode bool, urlTmpl string, dst *string) SessionCommand {
- return func(ctx context.Context, cmd *SessionCommands) error {
+ return func(ctx context.Context, cmd *SessionCommands) ([]eventstore.Command, error) {
if cmd.sessionWriteModel.UserID == "" {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-JK3gp", "Errors.User.UserIDMissing")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-JK3gp", "Errors.User.UserIDMissing")
}
writeModel := NewHumanOTPEmailWriteModel(cmd.sessionWriteModel.UserID, "")
if err := cmd.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
- return err
+ return nil, err
}
if !writeModel.OTPAdded() {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-JKLJ3", "Errors.User.MFA.OTP.NotReady")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-JKLJ3", "Errors.User.MFA.OTP.NotReady")
}
code, err := cmd.createCode(ctx, cmd.eventstore.Filter, domain.SecretGeneratorTypeOTPEmail, cmd.otpAlg, c.defaultSecretGenerators.OTPEmail)
if err != nil {
- return err
+ return nil, err
}
if returnCode {
*dst = code.Plain
}
cmd.OTPEmailChallenged(ctx, code.Crypted, code.Expiry, returnCode, urlTmpl)
- return nil
+ return nil, nil
}
}
@@ -112,37 +113,57 @@ func (c *Commands) OTPEmailSent(ctx context.Context, sessionID, resourceOwner st
}
func CheckOTPSMS(code string) SessionCommand {
- return func(ctx context.Context, cmd *SessionCommands) (err error) {
- if cmd.sessionWriteModel.UserID == "" {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-VDrh3", "Errors.User.UserIDMissing")
+ return func(ctx context.Context, cmd *SessionCommands) (_ []eventstore.Command, err error) {
+ writeModel := func(ctx context.Context, userID string, resourceOwner string) (OTPCodeWriteModel, error) {
+ otpWriteModel := NewHumanOTPSMSCodeWriteModel(cmd.sessionWriteModel.UserID, "")
+ err := cmd.eventstore.FilterToQueryReducer(ctx, otpWriteModel)
+ if err != nil {
+ return nil, err
+ }
+ // explicitly set the challenge from the session write model since the code write model will only check user events
+ otpWriteModel.otpCode = cmd.sessionWriteModel.OTPSMSCodeChallenge
+ return otpWriteModel, nil
}
- challenge := cmd.sessionWriteModel.OTPSMSCodeChallenge
- if challenge == nil {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-SF3tv", "Errors.User.Code.NotFound")
+ succeededEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command {
+ return user.NewHumanOTPSMSCheckSucceededEvent(ctx, aggregate, nil)
}
- err = crypto.VerifyCode(challenge.CreationDate, challenge.Expiry, challenge.Code, code, cmd.otpAlg)
+ failedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command {
+ return user.NewHumanOTPSMSCheckFailedEvent(ctx, aggregate, nil)
+ }
+ commands, err := checkOTP(ctx, cmd.sessionWriteModel.UserID, code, "", nil, writeModel, cmd.eventstore.FilterToQueryReducer, cmd.otpAlg, succeededEvent, failedEvent)
if err != nil {
- return err
+ return commands, err
}
+ cmd.eventCommands = append(cmd.eventCommands, commands...)
cmd.OTPSMSChecked(ctx, cmd.now())
- return nil
+ return nil, nil
}
}
func CheckOTPEmail(code string) SessionCommand {
- return func(ctx context.Context, cmd *SessionCommands) (err error) {
- if cmd.sessionWriteModel.UserID == "" {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-ejo2w", "Errors.User.UserIDMissing")
+ return func(ctx context.Context, cmd *SessionCommands) (_ []eventstore.Command, err error) {
+ writeModel := func(ctx context.Context, userID string, resourceOwner string) (OTPCodeWriteModel, error) {
+ otpWriteModel := NewHumanOTPEmailCodeWriteModel(cmd.sessionWriteModel.UserID, "")
+ err := cmd.eventstore.FilterToQueryReducer(ctx, otpWriteModel)
+ if err != nil {
+ return nil, err
+ }
+ // explicitly set the challenge from the session write model since the code write model will only check user events
+ otpWriteModel.otpCode = cmd.sessionWriteModel.OTPEmailCodeChallenge
+ return otpWriteModel, nil
}
- challenge := cmd.sessionWriteModel.OTPEmailCodeChallenge
- if challenge == nil {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-zF3g3", "Errors.User.Code.NotFound")
+ succeededEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command {
+ return user.NewHumanOTPEmailCheckSucceededEvent(ctx, aggregate, nil)
}
- err = crypto.VerifyCode(challenge.CreationDate, challenge.Expiry, challenge.Code, code, cmd.otpAlg)
+ failedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command {
+ return user.NewHumanOTPEmailCheckFailedEvent(ctx, aggregate, nil)
+ }
+ commands, err := checkOTP(ctx, cmd.sessionWriteModel.UserID, code, "", nil, writeModel, cmd.eventstore.FilterToQueryReducer, cmd.otpAlg, succeededEvent, failedEvent)
if err != nil {
- return err
+ return commands, err
}
+ cmd.eventCommands = append(cmd.eventCommands, commands...)
cmd.OTPEmailChecked(ctx, cmd.now())
- return nil
+ return nil, nil
}
}
diff --git a/internal/command/session_otp_test.go b/internal/command/session_otp_test.go
index c5be0aecae..d6934d0472 100644
--- a/internal/command/session_otp_test.go
+++ b/internal/command/session_otp_test.go
@@ -11,6 +11,7 @@ import (
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -110,8 +111,9 @@ func TestCommands_CreateOTPSMSChallengeReturnCode(t *testing.T) {
now: time.Now,
}
- err := cmd(context.Background(), cmds)
+ gotCmds, err := cmd(context.Background(), cmds)
assert.ErrorIs(t, err, tt.res.err)
+ assert.Empty(t, gotCmds)
assert.Equal(t, tt.res.returnCode, dst)
assert.Equal(t, tt.res.commands, cmds.eventCommands)
})
@@ -210,8 +212,9 @@ func TestCommands_CreateOTPSMSChallenge(t *testing.T) {
now: time.Now,
}
- err := cmd(context.Background(), cmds)
+ gotCmds, err := cmd(context.Background(), cmds)
assert.ErrorIs(t, err, tt.res.err)
+ assert.Empty(t, gotCmds)
assert.Equal(t, tt.res.commands, cmds.eventCommands)
})
}
@@ -410,8 +413,9 @@ func TestCommands_CreateOTPEmailChallengeURLTemplate(t *testing.T) {
now: time.Now,
}
- err = cmd(context.Background(), cmds)
+ gotCmds, err := cmd(context.Background(), cmds)
assert.ErrorIs(t, err, tt.res.err)
+ assert.Empty(t, gotCmds)
assert.Equal(t, tt.res.commands, cmds.eventCommands)
})
}
@@ -511,8 +515,9 @@ func TestCommands_CreateOTPEmailChallengeReturnCode(t *testing.T) {
now: time.Now,
}
- err := cmd(context.Background(), cmds)
+ gotCmds, err := cmd(context.Background(), cmds)
assert.ErrorIs(t, err, tt.res.err)
+ assert.Empty(t, gotCmds)
assert.Equal(t, tt.res.returnCode, dst)
assert.Equal(t, tt.res.commands, cmds.eventCommands)
})
@@ -611,8 +616,9 @@ func TestCommands_CreateOTPEmailChallenge(t *testing.T) {
now: time.Now,
}
- err := cmd(context.Background(), cmds)
+ gotCmds, err := cmd(context.Background(), cmds)
assert.ErrorIs(t, err, tt.res.err)
+ assert.Empty(t, gotCmds)
assert.Equal(t, tt.res.commands, cmds.eventCommands)
})
}
@@ -701,8 +707,9 @@ func TestCheckOTPSMS(t *testing.T) {
code string
}
type res struct {
- err error
- commands []eventstore.Command
+ err error
+ commands []eventstore.Command
+ errorCommands []eventstore.Command
}
tests := []struct {
name string
@@ -720,13 +727,43 @@ func TestCheckOTPSMS(t *testing.T) {
code: "code",
},
res: res{
- err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-VDrh3", "Errors.User.UserIDMissing"),
+ err: zerrors.ThrowInvalidArgument(nil, "COMMAND-S453v", "Errors.User.UserIDMissing"),
+ },
+ },
+ {
+ name: "missing code",
+ fields: fields{
+ eventstore: expectEventstore(),
+ userID: "userID",
+ },
+ args: args{},
+ res: res{
+ err: zerrors.ThrowInvalidArgument(nil, "COMMAND-SJl2g", "Errors.User.Code.Empty"),
+ },
+ },
+ {
+ name: "not set up",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(),
+ ),
+ userID: "userID",
+ },
+ args: args{
+ code: "code",
+ },
+ res: res{
+ err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-d2r52", "Errors.User.MFA.OTP.NotReady"),
},
},
{
name: "missing challenge",
fields: fields{
- eventstore: expectEventstore(),
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
+ ),
+ ),
userID: "userID",
otpCodeChallenge: nil,
},
@@ -734,14 +771,26 @@ func TestCheckOTPSMS(t *testing.T) {
code: "code",
},
res: res{
- err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-SF3tv", "Errors.User.Code.NotFound"),
+ err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound"),
},
},
{
name: "invalid code",
fields: fields{
- eventstore: expectEventstore(),
- userID: "userID",
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
+ ),
+ expectFilter(), // recheck
+ expectFilter(
+ eventFromEventPusher(
+ org.NewLockoutPolicyAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
+ 0, 0, false,
+ ),
+ ),
+ ),
+ ),
+ userID: "userID",
otpCodeChallenge: &OTPCode{
Code: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
@@ -759,13 +808,61 @@ func TestCheckOTPSMS(t *testing.T) {
},
res: res{
err: zerrors.ThrowPreconditionFailed(nil, "CODE-QvUQ4P", "Errors.User.Code.Expired"),
+ errorCommands: []eventstore.Command{
+ user.NewHumanOTPSMSCheckFailedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
+ },
+ },
+ },
+ {
+ name: "invalid code, locked",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
+ ),
+ expectFilter(), // recheck
+ expectFilter(
+ eventFromEventPusher(
+ org.NewLockoutPolicyAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
+ 0, 1, false,
+ ),
+ ),
+ ),
+ ),
+ userID: "userID",
+ otpCodeChallenge: &OTPCode{
+ Code: &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("code"),
+ },
+ Expiry: 5 * time.Minute,
+ CreationDate: testNow.Add(-10 * time.Minute),
+ },
+ otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ code: "code",
+ },
+ res: res{
+ err: zerrors.ThrowPreconditionFailed(nil, "CODE-QvUQ4P", "Errors.User.Code.Expired"),
+ errorCommands: []eventstore.Command{
+ user.NewHumanOTPSMSCheckFailedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
+ user.NewUserLockedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate),
+ },
},
},
{
name: "check ok",
fields: fields{
- eventstore: expectEventstore(),
- userID: "userID",
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
+ ),
+ expectFilter(), // recheck
+ ),
+ userID: "userID",
otpCodeChallenge: &OTPCode{
Code: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
@@ -783,12 +880,44 @@ func TestCheckOTPSMS(t *testing.T) {
},
res: res{
commands: []eventstore.Command{
+ user.NewHumanOTPSMSCheckSucceededEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
session.NewOTPSMSCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
testNow,
),
},
},
},
+ {
+ name: "check ok, locked in the meantime",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(user.NewHumanOTPSMSAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
+ ),
+ expectFilter(
+ user.NewUserLockedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate),
+ ),
+ ),
+ userID: "userID",
+ otpCodeChallenge: &OTPCode{
+ Code: &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("code"),
+ },
+ Expiry: 5 * time.Minute,
+ CreationDate: testNow,
+ },
+ otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ code: "code",
+ },
+ res: res{
+ err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-S6h4R", "Errors.User.Locked"),
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -811,8 +940,9 @@ func TestCheckOTPSMS(t *testing.T) {
},
}
- err := cmd(context.Background(), cmds)
+ gotCmds, err := cmd(context.Background(), cmds)
assert.ErrorIs(t, err, tt.res.err)
+ assert.Equal(t, tt.res.errorCommands, gotCmds)
assert.Equal(t, tt.res.commands, cmds.eventCommands)
})
}
@@ -829,8 +959,9 @@ func TestCheckOTPEmail(t *testing.T) {
code string
}
type res struct {
- err error
- commands []eventstore.Command
+ err error
+ commands []eventstore.Command
+ errorCommands []eventstore.Command
}
tests := []struct {
name string
@@ -848,13 +979,43 @@ func TestCheckOTPEmail(t *testing.T) {
code: "code",
},
res: res{
- err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-ejo2w", "Errors.User.UserIDMissing"),
+ err: zerrors.ThrowInvalidArgument(nil, "COMMAND-S453v", "Errors.User.UserIDMissing"),
+ },
+ },
+ {
+ name: "missing code",
+ fields: fields{
+ eventstore: expectEventstore(),
+ userID: "userID",
+ },
+ args: args{},
+ res: res{
+ err: zerrors.ThrowInvalidArgument(nil, "COMMAND-SJl2g", "Errors.User.Code.Empty"),
+ },
+ },
+ {
+ name: "not set up",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(),
+ ),
+ userID: "userID",
+ },
+ args: args{
+ code: "code",
+ },
+ res: res{
+ err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-d2r52", "Errors.User.MFA.OTP.NotReady"),
},
},
{
name: "missing challenge",
fields: fields{
- eventstore: expectEventstore(),
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(user.NewHumanOTPEmailAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
+ ),
+ ),
userID: "userID",
otpCodeChallenge: nil,
},
@@ -862,14 +1023,26 @@ func TestCheckOTPEmail(t *testing.T) {
code: "code",
},
res: res{
- err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-zF3g3", "Errors.User.Code.NotFound"),
+ err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound"),
},
},
{
name: "invalid code",
fields: fields{
- eventstore: expectEventstore(),
- userID: "userID",
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(user.NewHumanOTPEmailAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
+ ),
+ expectFilter(), // recheck
+ expectFilter(
+ eventFromEventPusher(
+ org.NewLockoutPolicyAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
+ 0, 0, false,
+ ),
+ ),
+ ),
+ ),
+ userID: "userID",
otpCodeChallenge: &OTPCode{
Code: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
@@ -887,13 +1060,61 @@ func TestCheckOTPEmail(t *testing.T) {
},
res: res{
err: zerrors.ThrowPreconditionFailed(nil, "CODE-QvUQ4P", "Errors.User.Code.Expired"),
+ errorCommands: []eventstore.Command{
+ user.NewHumanOTPEmailCheckFailedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
+ },
+ },
+ },
+ {
+ name: "invalid code, locked",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(user.NewHumanOTPEmailAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
+ ),
+ expectFilter(), // recheck
+ expectFilter(
+ eventFromEventPusher(
+ org.NewLockoutPolicyAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
+ 0, 1, false,
+ ),
+ ),
+ ),
+ ),
+ userID: "userID",
+ otpCodeChallenge: &OTPCode{
+ Code: &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("code"),
+ },
+ Expiry: 5 * time.Minute,
+ CreationDate: testNow.Add(-10 * time.Minute),
+ },
+ otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ code: "code",
+ },
+ res: res{
+ err: zerrors.ThrowPreconditionFailed(nil, "CODE-QvUQ4P", "Errors.User.Code.Expired"),
+ errorCommands: []eventstore.Command{
+ user.NewHumanOTPEmailCheckFailedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
+ user.NewUserLockedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate),
+ },
},
},
{
name: "check ok",
fields: fields{
- eventstore: expectEventstore(),
- userID: "userID",
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(user.NewHumanOTPEmailAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
+ ),
+ expectFilter(), // recheck
+ ),
+ userID: "userID",
otpCodeChallenge: &OTPCode{
Code: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
@@ -911,12 +1132,44 @@ func TestCheckOTPEmail(t *testing.T) {
},
res: res{
commands: []eventstore.Command{
+ user.NewHumanOTPEmailCheckSucceededEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
session.NewOTPEmailCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
testNow,
),
},
},
},
+ {
+ name: "check ok, locked in the meantime",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(user.NewHumanOTPEmailAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate)),
+ ),
+ expectFilter(
+ user.NewUserLockedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate),
+ ),
+ ),
+ userID: "userID",
+ otpCodeChallenge: &OTPCode{
+ Code: &crypto.CryptoValue{
+ CryptoType: crypto.TypeEncryption,
+ Algorithm: "enc",
+ KeyID: "id",
+ Crypted: []byte("code"),
+ },
+ Expiry: 5 * time.Minute,
+ CreationDate: testNow,
+ },
+ otpAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
+ },
+ args: args{
+ code: "code",
+ },
+ res: res{
+ err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-S6h4R", "Errors.User.Locked"),
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -939,8 +1192,9 @@ func TestCheckOTPEmail(t *testing.T) {
},
}
- err := cmd(context.Background(), cmds)
+ gotCmds, err := cmd(context.Background(), cmds)
assert.ErrorIs(t, err, tt.res.err)
+ assert.Equal(t, tt.res.errorCommands, gotCmds)
assert.Equal(t, tt.res.commands, cmds.eventCommands)
})
}
diff --git a/internal/command/session_test.go b/internal/command/session_test.go
index b41a886aa2..46cbaeafc3 100644
--- a/internal/command/session_test.go
+++ b/internal/command/session_test.go
@@ -22,6 +22,7 @@ import (
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/idpintent"
+ "github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -31,7 +32,7 @@ func TestSessionCommands_getHumanWriteModel(t *testing.T) {
userAggr := &user.NewAggregate("user1", "org1").Aggregate
type fields struct {
- eventstore *eventstore.Eventstore
+ eventstore func(*testing.T) *eventstore.Eventstore
sessionWriteModel *SessionWriteModel
}
type res struct {
@@ -46,7 +47,7 @@ func TestSessionCommands_getHumanWriteModel(t *testing.T) {
{
name: "missing UID",
fields: fields{
- eventstore: &eventstore.Eventstore{},
+ eventstore: expectEventstore(),
sessionWriteModel: &SessionWriteModel{},
},
res: res{
@@ -57,7 +58,7 @@ func TestSessionCommands_getHumanWriteModel(t *testing.T) {
{
name: "filter error",
fields: fields{
- eventstore: eventstoreExpect(t,
+ eventstore: expectEventstore(
expectFilterError(io.ErrClosedPipe),
),
sessionWriteModel: &SessionWriteModel{
@@ -72,7 +73,7 @@ func TestSessionCommands_getHumanWriteModel(t *testing.T) {
{
name: "removed user",
fields: fields{
- eventstore: eventstoreExpect(t,
+ eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -101,7 +102,7 @@ func TestSessionCommands_getHumanWriteModel(t *testing.T) {
{
name: "ok",
fields: fields{
- eventstore: eventstoreExpect(t,
+ eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
@@ -133,7 +134,7 @@ func TestSessionCommands_getHumanWriteModel(t *testing.T) {
}
for _, tt := range tests {
s := &SessionCommands{
- eventstore: tt.fields.eventstore,
+ eventstore: tt.fields.eventstore(t),
sessionWriteModel: tt.fields.sessionWriteModel,
}
got, err := s.gethumanWriteModel(context.Background())
@@ -271,7 +272,7 @@ func TestCommands_CreateSession(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
- eventstore: eventstoreExpect(t, tt.expect...),
+ eventstore: expectEventstore(tt.expect...)(t),
idGenerator: tt.fields.idGenerator,
sessionTokenCreator: tt.fields.tokenCreator,
}
@@ -284,7 +285,7 @@ func TestCommands_CreateSession(t *testing.T) {
func TestCommands_UpdateSession(t *testing.T) {
type fields struct {
- eventstore *eventstore.Eventstore
+ eventstore func(*testing.T) *eventstore.Eventstore
tokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
}
type args struct {
@@ -307,7 +308,7 @@ func TestCommands_UpdateSession(t *testing.T) {
{
"eventstore failed",
fields{
- eventstore: eventstoreExpect(t,
+ eventstore: expectEventstore(
expectFilterError(zerrors.ThrowInternal(nil, "id", "filter failed")),
),
},
@@ -321,7 +322,7 @@ func TestCommands_UpdateSession(t *testing.T) {
{
"no change",
fields{
- eventstore: eventstoreExpect(t,
+ eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(context.Background(),
@@ -361,7 +362,7 @@ func TestCommands_UpdateSession(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
- eventstore: tt.fields.eventstore,
+ eventstore: tt.fields.eventstore(t),
sessionTokenVerifier: tt.fields.tokenVerifier,
}
got, err := c.UpdateSession(tt.args.ctx, tt.args.sessionID, tt.args.checks, tt.args.metadata, tt.args.lifetime)
@@ -387,7 +388,7 @@ func TestCommands_updateSession(t *testing.T) {
testNow := time.Now()
type fields struct {
- eventstore *eventstore.Eventstore
+ eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@@ -408,7 +409,7 @@ func TestCommands_updateSession(t *testing.T) {
{
"terminated",
fields{
- eventstore: eventstoreExpect(t),
+ eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
@@ -423,15 +424,15 @@ func TestCommands_updateSession(t *testing.T) {
{
"check failed",
fields{
- eventstore: eventstoreExpect(t),
+ eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"),
sessionCommands: []SessionCommand{
- func(ctx context.Context, cmd *SessionCommands) error {
- return zerrors.ThrowInternal(nil, "id", "check failed")
+ func(ctx context.Context, cmd *SessionCommands) ([]eventstore.Command, error) {
+ return nil, zerrors.ThrowInternal(nil, "id", "check failed")
},
},
},
@@ -443,7 +444,7 @@ func TestCommands_updateSession(t *testing.T) {
{
"no change",
fields{
- eventstore: eventstoreExpect(t),
+ eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instance1", "", ""),
@@ -463,14 +464,13 @@ func TestCommands_updateSession(t *testing.T) {
{
"negative lifetime",
fields{
- eventstore: eventstoreExpect(t),
+ eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instance1", "", ""),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"),
sessionCommands: []SessionCommand{},
- eventstore: eventstoreExpect(t),
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
@@ -489,7 +489,7 @@ func TestCommands_updateSession(t *testing.T) {
{
"lifetime set",
fields{
- eventstore: eventstoreExpect(t,
+ eventstore: expectEventstore(
expectPush(
session.NewLifetimeSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
10*time.Minute,
@@ -505,7 +505,6 @@ func TestCommands_updateSession(t *testing.T) {
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"),
sessionCommands: []SessionCommand{},
- eventstore: eventstoreExpect(t),
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
@@ -527,14 +526,75 @@ func TestCommands_updateSession(t *testing.T) {
},
},
},
+ {
+ "set user, invalid password",
+ fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
+ "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
+ ),
+ eventFromEventPusher(
+ user.NewHumanPasswordChangedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
+ "$plain$x$password", false, ""),
+ ),
+ ),
+ expectFilter(), // recheck
+ expectFilter(
+ org.NewLockoutPolicyAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, 0, 0, false),
+ ),
+ expectPush(
+ user.NewHumanPasswordCheckFailedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
+ ),
+ ),
+ },
+ args{
+ ctx: authz.NewMockContext("instance1", "", ""),
+ checks: &SessionCommands{
+ sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"),
+ sessionCommands: []SessionCommand{
+ CheckUser("userID", "org1", &language.Afrikaans),
+ CheckPassword("invalid password"),
+ },
+ createToken: func(sessionID string) (string, string, error) {
+ return "tokenID",
+ "token",
+ nil
+ },
+ hasher: mockPasswordHasher("x"),
+ now: func() time.Time {
+ return testNow
+ },
+ },
+ metadata: map[string][]byte{
+ "key": []byte("value"),
+ },
+ },
+ res{
+ err: zerrors.ThrowInvalidArgument(nil, "COMMAND-3M0fs", "Errors.User.Password.Invalid"),
+ },
+ },
{
"set user, password, metadata and token",
fields{
- eventstore: eventstoreExpect(t,
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
+ "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
+ ),
+ eventFromEventPusher(
+ user.NewHumanPasswordChangedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
+ "$plain$x$password", false, ""),
+ ),
+ ),
+ expectFilter(), // recheck
expectPush(
session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
"userID", "org1", testNow, &language.Afrikaans,
),
+ user.NewHumanPasswordCheckSucceededEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, nil),
session.NewPasswordCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
testNow,
),
@@ -555,18 +615,6 @@ func TestCommands_updateSession(t *testing.T) {
CheckUser("userID", "org1", &language.Afrikaans),
CheckPassword("password"),
},
- eventstore: eventstoreExpect(t,
- expectFilter(
- eventFromEventPusher(
- user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
- "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
- ),
- eventFromEventPusher(
- user.NewHumanPasswordChangedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
- "$plain$x$password", false, ""),
- ),
- ),
- ),
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
@@ -594,7 +642,14 @@ func TestCommands_updateSession(t *testing.T) {
{
"set user, intent not successful",
fields{
- eventstore: eventstoreExpect(t),
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
+ "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
+ ),
+ ),
+ ),
},
args{
ctx: authz.NewMockContext("instance1", "", ""),
@@ -604,14 +659,6 @@ func TestCommands_updateSession(t *testing.T) {
CheckUser("userID", "org1", &language.Afrikaans),
CheckIntent("intent", "aW50ZW50"),
},
- eventstore: eventstoreExpect(t,
- expectFilter(
- eventFromEventPusher(
- user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
- "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
- ),
- ),
- ),
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
@@ -633,7 +680,25 @@ func TestCommands_updateSession(t *testing.T) {
{
"set user, intent not for user",
fields{
- eventstore: eventstoreExpect(t),
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
+ "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
+ ),
+ eventFromEventPusher(
+ idpintent.NewSucceededEvent(context.Background(),
+ &idpintent.NewAggregate("id", "instance1").Aggregate,
+ nil,
+ "idpUserID",
+ "idpUserName",
+ "userID2",
+ nil,
+ "",
+ ),
+ ),
+ ),
+ ),
},
args{
ctx: authz.NewMockContext("instance1", "", ""),
@@ -643,25 +708,6 @@ func TestCommands_updateSession(t *testing.T) {
CheckUser("userID", "org1", &language.Afrikaans),
CheckIntent("intent", "aW50ZW50"),
},
- eventstore: eventstoreExpect(t,
- expectFilter(
- eventFromEventPusher(
- user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
- "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
- ),
- eventFromEventPusher(
- idpintent.NewSucceededEvent(context.Background(),
- &idpintent.NewAggregate("id", "instance1").Aggregate,
- nil,
- "idpUserID",
- "idpUserName",
- "userID2",
- nil,
- "",
- ),
- ),
- ),
- ),
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
@@ -683,7 +729,7 @@ func TestCommands_updateSession(t *testing.T) {
{
"set user, intent incorrect token",
fields{
- eventstore: eventstoreExpect(t),
+ eventstore: expectEventstore(),
},
args{
ctx: authz.NewMockContext("instance1", "", ""),
@@ -693,7 +739,6 @@ func TestCommands_updateSession(t *testing.T) {
CheckUser("userID", "org1", &language.Afrikaans),
CheckIntent("intent2", "aW50ZW50"),
},
- eventstore: eventstoreExpect(t),
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
@@ -715,7 +760,24 @@ func TestCommands_updateSession(t *testing.T) {
{
"set user, intent, metadata and token",
fields{
- eventstore: eventstoreExpect(t,
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
+ "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
+ ),
+ eventFromEventPusher(
+ idpintent.NewSucceededEvent(context.Background(),
+ &idpintent.NewAggregate("id", "instance1").Aggregate,
+ nil,
+ "idpUserID",
+ "idpUsername",
+ "userID",
+ nil,
+ "",
+ ),
+ ),
+ ),
expectPush(
session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
"userID", "org1", testNow, &language.Afrikaans),
@@ -736,25 +798,6 @@ func TestCommands_updateSession(t *testing.T) {
CheckUser("userID", "org1", &language.Afrikaans),
CheckIntent("intent", "aW50ZW50"),
},
- eventstore: eventstoreExpect(t,
- expectFilter(
- eventFromEventPusher(
- user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
- "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
- ),
- eventFromEventPusher(
- idpintent.NewSucceededEvent(context.Background(),
- &idpintent.NewAggregate("id", "instance1").Aggregate,
- nil,
- "idpUserID",
- "idpUsername",
- "userID",
- nil,
- "",
- ),
- ),
- ),
- ),
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
@@ -779,12 +822,90 @@ func TestCommands_updateSession(t *testing.T) {
},
},
},
+ {
+ "set user, intent (user not linked yet)",
+ fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
+ "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
+ ),
+ eventFromEventPusher(
+ idpintent.NewStartedEvent(context.Background(),
+ &idpintent.NewAggregate("id", "instance1").Aggregate,
+ nil,
+ nil,
+ "idpID",
+ ),
+ ),
+ eventFromEventPusher(
+ idpintent.NewSucceededEvent(context.Background(),
+ &idpintent.NewAggregate("id", "instance1").Aggregate,
+ nil,
+ "idpUserID",
+ "idpUsername",
+ "",
+ nil,
+ "",
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ user.NewUserIDPLinkAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
+ "idpID",
+ "idpUsername",
+ "idpUserID",
+ ),
+ ),
+ ),
+ expectPush(
+ session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
+ "userID", "org1", testNow, &language.Afrikaans),
+ session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
+ testNow),
+ session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
+ "tokenID"),
+ ),
+ ),
+ },
+ args{
+ ctx: authz.NewMockContext("instance1", "", ""),
+ checks: &SessionCommands{
+ sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"),
+ sessionCommands: []SessionCommand{
+ CheckUser("userID", "org1", &language.Afrikaans),
+ CheckIntent("intent", "aW50ZW50"),
+ },
+ createToken: func(sessionID string) (string, string, error) {
+ return "tokenID",
+ "token",
+ nil
+ },
+ intentAlg: decryption(nil),
+ now: func() time.Time {
+ return testNow
+ },
+ },
+ },
+ res{
+ want: &SessionChanged{
+ ObjectDetails: &domain.ObjectDetails{
+ ResourceOwner: "instance1",
+ },
+ ID: "sessionID",
+ NewToken: "token",
+ },
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
- eventstore: tt.fields.eventstore,
+ eventstore: tt.fields.eventstore(t),
}
+ tt.args.checks.eventstore = c.eventstore
got, err := c.updateSession(tt.args.ctx, tt.args.checks, tt.args.metadata, tt.args.lifetime)
require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.want, got)
@@ -803,6 +924,7 @@ func TestCheckTOTP(t *testing.T) {
sessAgg := &session.NewAggregate("session1", "instance1").Aggregate
userAgg := &user.NewAggregate("user1", "org1").Aggregate
+ orgAgg := &org.NewAggregate("org1").Aggregate
code, err := totp.GenerateCode(key.Secret(), testNow)
require.NoError(t, err)
@@ -817,6 +939,7 @@ func TestCheckTOTP(t *testing.T) {
code string
fields fields
wantEventCommands []eventstore.Command
+ wantErrorCommands []eventstore.Command
wantErr error
}{
{
@@ -828,7 +951,7 @@ func TestCheckTOTP(t *testing.T) {
},
eventstore: expectEventstore(),
},
- wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Neil7", "Errors.User.UserIDMissing"),
+ wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing"),
},
{
name: "filter error",
@@ -862,7 +985,7 @@ func TestCheckTOTP(t *testing.T) {
),
),
},
- wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-eej1U", "Errors.User.MFA.OTP.NotReady"),
+ wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotReady"),
},
{
name: "otp verify error",
@@ -882,8 +1005,45 @@ func TestCheckTOTP(t *testing.T) {
user.NewHumanOTPVerifiedEvent(ctx, userAgg, "agent1"),
),
),
+ expectFilter(), // recheck
+ expectFilter(
+ eventFromEventPusher(org.NewLockoutPolicyAddedEvent(ctx, orgAgg, 0, 0, false)),
+ ),
),
},
+ wantErrorCommands: []eventstore.Command{
+ user.NewHumanOTPCheckFailedEvent(ctx, userAgg, nil),
+ },
+ wantErr: zerrors.ThrowInvalidArgument(nil, "EVENT-8isk2", "Errors.User.MFA.OTP.InvalidCode"),
+ },
+ {
+ name: "otp verify error, locked",
+ code: "foobar",
+ fields: fields{
+ sessionWriteModel: &SessionWriteModel{
+ UserID: "user1",
+ UserCheckedAt: testNow,
+ aggregate: sessAgg,
+ },
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
+ ),
+ eventFromEventPusher(
+ user.NewHumanOTPVerifiedEvent(ctx, userAgg, "agent1"),
+ ),
+ ),
+ expectFilter(), // recheck
+ expectFilter(
+ eventFromEventPusher(org.NewLockoutPolicyAddedEvent(ctx, orgAgg, 1, 1, false)),
+ ),
+ ),
+ },
+ wantErrorCommands: []eventstore.Command{
+ user.NewHumanOTPCheckFailedEvent(ctx, userAgg, nil),
+ user.NewUserLockedEvent(ctx, userAgg),
+ },
wantErr: zerrors.ThrowInvalidArgument(nil, "EVENT-8isk2", "Errors.User.MFA.OTP.InvalidCode"),
},
{
@@ -904,12 +1064,39 @@ func TestCheckTOTP(t *testing.T) {
user.NewHumanOTPVerifiedEvent(ctx, userAgg, "agent1"),
),
),
+ expectFilter(), // recheck
),
},
wantEventCommands: []eventstore.Command{
+ user.NewHumanOTPCheckSucceededEvent(ctx, userAgg, nil),
session.NewTOTPCheckedEvent(ctx, sessAgg, testNow),
},
},
+ {
+ name: "ok, but locked in the meantime",
+ code: code,
+ fields: fields{
+ sessionWriteModel: &SessionWriteModel{
+ UserID: "user1",
+ UserCheckedAt: testNow,
+ aggregate: sessAgg,
+ },
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
+ ),
+ eventFromEventPusher(
+ user.NewHumanOTPVerifiedEvent(ctx, userAgg, "agent1"),
+ ),
+ ),
+ expectFilter(
+ user.NewUserLockedEvent(ctx, userAgg),
+ ),
+ ),
+ },
+ wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-SF3fg", "Errors.User.Locked"),
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -919,8 +1106,9 @@ func TestCheckTOTP(t *testing.T) {
totpAlg: cryptoAlg,
now: func() time.Time { return testNow },
}
- err := CheckTOTP(tt.code)(ctx, cmd)
+ gotCmds, err := CheckTOTP(tt.code)(ctx, cmd)
require.ErrorIs(t, err, tt.wantErr)
+ assert.Equal(t, tt.wantErrorCommands, gotCmds)
assert.Equal(t, tt.wantEventCommands, cmd.eventCommands)
})
}
diff --git a/internal/command/session_webauhtn.go b/internal/command/session_webauhtn.go
index 680b641a55..0bbb68c99d 100644
--- a/internal/command/session_webauhtn.go
+++ b/internal/command/session_webauhtn.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -41,49 +42,49 @@ func (s *SessionCommands) getHumanWebAuthNTokenReadModel(ctx context.Context, us
}
func (c *Commands) CreateWebAuthNChallenge(userVerification domain.UserVerificationRequirement, rpid string, dst json.Unmarshaler) SessionCommand {
- return func(ctx context.Context, cmd *SessionCommands) error {
+ return func(ctx context.Context, cmd *SessionCommands) ([]eventstore.Command, error) {
humanPasskeys, err := cmd.getHumanWebAuthNTokens(ctx, userVerification)
if err != nil {
- return err
+ return nil, err
}
webAuthNLogin, err := c.webauthnConfig.BeginLogin(ctx, humanPasskeys.human, userVerification, rpid, humanPasskeys.tokens...)
if err != nil {
- return err
+ return nil, err
}
if err = json.Unmarshal(webAuthNLogin.CredentialAssertionData, dst); err != nil {
- return zerrors.ThrowInternal(err, "COMMAND-Yah6A", "Errors.Internal")
+ return nil, zerrors.ThrowInternal(err, "COMMAND-Yah6A", "Errors.Internal")
}
cmd.WebAuthNChallenged(ctx, webAuthNLogin.Challenge, webAuthNLogin.AllowedCredentialIDs, webAuthNLogin.UserVerification, rpid)
- return nil
+ return nil, nil
}
}
func (c *Commands) CheckWebAuthN(credentialAssertionData json.Marshaler) SessionCommand {
- return func(ctx context.Context, cmd *SessionCommands) error {
+ return func(ctx context.Context, cmd *SessionCommands) ([]eventstore.Command, error) {
credentialAssertionData, err := json.Marshal(credentialAssertionData)
if err != nil {
- return zerrors.ThrowInternal(err, "COMMAND-ohG2o", "Errors.Internal")
+ return nil, zerrors.ThrowInternal(err, "COMMAND-ohG2o", "Errors.Internal")
}
challenge := cmd.sessionWriteModel.WebAuthNChallenge
if challenge == nil {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Ioqu5", "Errors.Session.WebAuthN.NoChallenge")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Ioqu5", "Errors.Session.WebAuthN.NoChallenge")
}
webAuthNTokens, err := cmd.getHumanWebAuthNTokens(ctx, challenge.UserVerification)
if err != nil {
- return err
+ return nil, err
}
webAuthN := challenge.WebAuthNLogin(webAuthNTokens.human, credentialAssertionData)
credential, err := c.webauthnConfig.FinishLogin(ctx, webAuthNTokens.human, webAuthN, credentialAssertionData, webAuthNTokens.tokens...)
if err != nil && (credential == nil || credential.ID == nil) {
- return err
+ return nil, err
}
_, token := domain.GetTokenByKeyID(webAuthNTokens.tokens, credential.ID)
if token == nil {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Aej7i", "Errors.User.WebAuthN.NotFound")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Aej7i", "Errors.User.WebAuthN.NotFound")
}
cmd.WebAuthNChecked(ctx, cmd.now(), token.WebAuthNTokenID, credential.Authenticator.SignCount, credential.Flags.UserVerified)
- return nil
+ return nil, nil
}
}
diff --git a/internal/command/system_features.go b/internal/command/system_features.go
index 838fef5b9d..5aa7c8dda1 100644
--- a/internal/command/system_features.go
+++ b/internal/command/system_features.go
@@ -4,6 +4,7 @@ import (
"context"
"github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/feature"
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -15,6 +16,7 @@ type SystemFeatures struct {
TokenExchange *bool
UserSchema *bool
Actions *bool
+ ImprovedPerformance []feature.ImprovedPerformanceType
}
func (m *SystemFeatures) isEmpty() bool {
@@ -23,7 +25,9 @@ func (m *SystemFeatures) isEmpty() bool {
m.LegacyIntrospection == nil &&
m.UserSchema == nil &&
m.TokenExchange == nil &&
- m.Actions == nil
+ m.Actions == nil &&
+ // nil check to allow unset improvements
+ m.ImprovedPerformance == nil
}
func (c *Commands) SetSystemFeatures(ctx context.Context, f *SystemFeatures) (*domain.ObjectDetails, error) {
diff --git a/internal/command/system_features_model.go b/internal/command/system_features_model.go
index d21aa85cbc..46b36d15ad 100644
--- a/internal/command/system_features_model.go
+++ b/internal/command/system_features_model.go
@@ -23,16 +23,23 @@ func NewSystemFeaturesWriteModel() *SystemFeaturesWriteModel {
return m
}
-func (m *SystemFeaturesWriteModel) Reduce() (err error) {
+func (m *SystemFeaturesWriteModel) Reduce() error {
for _, event := range m.Events {
switch e := event.(type) {
case *feature_v2.ResetEvent:
m.reduceReset()
case *feature_v2.SetEvent[bool]:
- err = m.reduceBoolFeature(e)
- }
- if err != nil {
- return err
+ _, key, err := e.FeatureInfo()
+ if err != nil {
+ return err
+ }
+ reduceSystemFeature(&m.SystemFeatures, key, e.Value)
+ case *feature_v2.SetEvent[[]feature.ImprovedPerformanceType]:
+ _, key, err := e.FeatureInfo()
+ if err != nil {
+ return err
+ }
+ reduceSystemFeature(&m.SystemFeatures, key, e.Value)
}
}
return m.WriteModel.Reduce()
@@ -52,41 +59,40 @@ func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.SystemUserSchemaEventType,
feature_v2.SystemTokenExchangeEventType,
feature_v2.SystemActionsEventType,
+ feature_v2.SystemImprovedPerformanceEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
func (m *SystemFeaturesWriteModel) reduceReset() {
- m.LoginDefaultOrg = nil
- m.TriggerIntrospectionProjections = nil
- m.LegacyIntrospection = nil
- m.TokenExchange = nil
- m.UserSchema = nil
- m.Actions = nil
+ m.SystemFeatures = SystemFeatures{}
}
-func (m *SystemFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
- _, key, err := event.FeatureInfo()
- if err != nil {
- return err
- }
+func reduceSystemFeature(features *SystemFeatures, key feature.Key, value any) {
switch key {
case feature.KeyUnspecified:
- return nil
+ return
case feature.KeyLoginDefaultOrg:
- m.LoginDefaultOrg = &event.Value
+ v := value.(bool)
+ features.LoginDefaultOrg = &v
case feature.KeyTriggerIntrospectionProjections:
- m.TriggerIntrospectionProjections = &event.Value
+ v := value.(bool)
+ features.TriggerIntrospectionProjections = &v
case feature.KeyLegacyIntrospection:
- m.LegacyIntrospection = &event.Value
+ v := value.(bool)
+ features.LegacyIntrospection = &v
case feature.KeyUserSchema:
- m.UserSchema = &event.Value
+ v := value.(bool)
+ features.UserSchema = &v
case feature.KeyTokenExchange:
- m.TokenExchange = &event.Value
+ v := value.(bool)
+ features.TokenExchange = &v
case feature.KeyActions:
- m.Actions = &event.Value
+ v := value.(bool)
+ features.Actions = &v
+ case feature.KeyImprovedPerformance:
+ features.ImprovedPerformance = value.([]feature.ImprovedPerformanceType)
}
- return nil
}
func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFeatures) []eventstore.Command {
@@ -98,6 +104,7 @@ func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFe
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.SystemUserSchemaEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.SystemTokenExchangeEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.Actions, f.Actions, feature_v2.SystemActionsEventType)
+ cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.SystemImprovedPerformanceEventType)
return cmds
}
@@ -107,3 +114,15 @@ func appendFeatureUpdate[T comparable](ctx context.Context, cmds []eventstore.Co
}
return cmds
}
+
+func appendFeatureSliceUpdate[T comparable](ctx context.Context, cmds []eventstore.Command, aggregate *feature_v2.Aggregate, oldValues, newValues []T, eventType eventstore.EventType) []eventstore.Command {
+ if len(newValues) != len(oldValues) {
+ return append(cmds, feature_v2.NewSetEvent[[]T](ctx, aggregate, eventType, newValues))
+ }
+ for i, oldValue := range oldValues {
+ if oldValue != newValues[i] {
+ return append(cmds, feature_v2.NewSetEvent[[]T](ctx, aggregate, eventType, newValues))
+ }
+ }
+ return cmds
+}
diff --git a/internal/command/user_human_otp.go b/internal/command/user_human_otp.go
index c368167b3d..39abab3b86 100644
--- a/internal/command/user_human_otp.go
+++ b/internal/command/user_human_otp.go
@@ -161,48 +161,67 @@ func (c *Commands) HumanCheckMFATOTPSetup(ctx context.Context, userID, code, use
}
func (c *Commands) HumanCheckMFATOTP(ctx context.Context, userID, code, resourceOwner string, authRequest *domain.AuthRequest) error {
+ commands, err := checkTOTP(
+ ctx,
+ userID,
+ resourceOwner,
+ code,
+ c.eventstore.FilterToQueryReducer,
+ c.multifactors.OTP.CryptoMFA,
+ authRequestDomainToAuthRequestInfo(authRequest),
+ )
+
+ _, pushErr := c.eventstore.Push(ctx, commands...)
+ logging.OnError(pushErr).Error("error create password check failed event")
+ return err
+}
+
+func checkTOTP(
+ ctx context.Context,
+ userID, resourceOwner, code string,
+ queryReducer func(ctx context.Context, r eventstore.QueryReducer) error,
+ alg crypto.EncryptionAlgorithm,
+ optionalAuthRequestInfo *user.AuthRequestInfo,
+) ([]eventstore.Command, error) {
if userID == "" {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing")
+ return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing")
}
- existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceOwner)
+ existingOTP := NewHumanTOTPWriteModel(userID, resourceOwner)
+ err := queryReducer(ctx, existingOTP)
if err != nil {
- return err
+ return nil, err
}
if existingOTP.State != domain.MFAStateReady {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotReady")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotReady")
}
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
- verifyErr := domain.VerifyTOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA)
+ verifyErr := domain.VerifyTOTP(code, existingOTP.Secret, alg)
// recheck for additional events (failed OTP checks or locks)
- recheckErr := c.eventstore.FilterToQueryReducer(ctx, existingOTP)
+ recheckErr := queryReducer(ctx, existingOTP)
if recheckErr != nil {
- return recheckErr
+ return nil, recheckErr
}
if existingOTP.UserLocked {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-SF3fg", "Errors.User.Locked")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SF3fg", "Errors.User.Locked")
}
// the OTP check succeeded and the user was not locked in the meantime
if verifyErr == nil {
- _, err = c.eventstore.Push(ctx, user.NewHumanOTPCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
- return err
+ return []eventstore.Command{user.NewHumanOTPCheckSucceededEvent(ctx, userAgg, optionalAuthRequestInfo)}, nil
}
// the OTP check failed, therefore check if the limit was reached and the user must additionally be locked
commands := make([]eventstore.Command, 0, 2)
- commands = append(commands, user.NewHumanOTPCheckFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
- lockoutPolicy, err := c.getLockoutPolicy(ctx, resourceOwner)
+ commands = append(commands, user.NewHumanOTPCheckFailedEvent(ctx, userAgg, optionalAuthRequestInfo))
+ lockoutPolicy, err := getLockoutPolicy(ctx, existingOTP.ResourceOwner, queryReducer)
if err != nil {
- return err
+ return nil, err
}
if lockoutPolicy.MaxOTPAttempts > 0 && existingOTP.CheckFailedCount+1 >= lockoutPolicy.MaxOTPAttempts {
commands = append(commands, user.NewUserLockedEvent(ctx, userAgg))
}
-
- _, pushErr := c.eventstore.Push(ctx, commands...)
- logging.OnError(pushErr).Error("error create password check failed event")
- return verifyErr
+ return commands, verifyErr
}
func (c *Commands) HumanRemoveTOTP(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
@@ -342,16 +361,23 @@ func (c *Commands) HumanCheckOTPSMS(ctx context.Context, userID, code, resourceO
failedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command {
return user.NewHumanOTPSMSCheckFailedEvent(ctx, aggregate, authRequestDomainToAuthRequestInfo(authRequest))
}
- return c.humanCheckOTP(
+ commands, err := checkOTP(
ctx,
userID,
code,
resourceOwner,
authRequest,
writeModel,
+ c.eventstore.FilterToQueryReducer,
+ c.userEncryption,
succeededEvent,
failedEvent,
)
+ if len(commands) > 0 {
+ _, pushErr := c.eventstore.Push(ctx, commands...)
+ logging.WithFields("userID", userID).OnError(pushErr).Error("otp failure check push failed")
+ }
+ return err
}
// AddHumanOTPEmail adds the OTP Email factor to a user.
@@ -467,16 +493,23 @@ func (c *Commands) HumanCheckOTPEmail(ctx context.Context, userID, code, resourc
failedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command {
return user.NewHumanOTPEmailCheckFailedEvent(ctx, aggregate, authRequestDomainToAuthRequestInfo(authRequest))
}
- return c.humanCheckOTP(
+ commands, err := checkOTP(
ctx,
userID,
code,
resourceOwner,
authRequest,
writeModel,
+ c.eventstore.FilterToQueryReducer,
+ c.userEncryption,
succeededEvent,
failedEvent,
)
+ if len(commands) > 0 {
+ _, pushErr := c.eventstore.Push(ctx, commands...)
+ logging.WithFields("userID", userID).OnError(pushErr).Error("otp failure check push failed")
+ }
+ return err
}
// sendHumanOTP creates a code for a registered mechanism (sms / email), which is used for a check (during login)
@@ -534,62 +567,57 @@ func (c *Commands) humanOTPSent(
return err
}
-func (c *Commands) humanCheckOTP(
+func checkOTP(
ctx context.Context,
userID, code, resourceOwner string,
authRequest *domain.AuthRequest,
writeModelByID func(ctx context.Context, userID string, resourceOwner string) (OTPCodeWriteModel, error),
- checkSucceededEvent func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command,
- checkFailedEvent func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command,
-) error {
+ queryReducer func(ctx context.Context, r eventstore.QueryReducer) error,
+ alg crypto.EncryptionAlgorithm,
+ checkSucceededEvent, checkFailedEvent func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command,
+) ([]eventstore.Command, error) {
if userID == "" {
- return zerrors.ThrowInvalidArgument(nil, "COMMAND-S453v", "Errors.User.UserIDMissing")
+ return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-S453v", "Errors.User.UserIDMissing")
}
if code == "" {
- return zerrors.ThrowInvalidArgument(nil, "COMMAND-SJl2g", "Errors.User.Code.Empty")
+ return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-SJl2g", "Errors.User.Code.Empty")
}
existingOTP, err := writeModelByID(ctx, userID, resourceOwner)
if err != nil {
- return err
+ return nil, err
}
if !existingOTP.OTPAdded() {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-d2r52", "Errors.User.MFA.OTP.NotReady")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-d2r52", "Errors.User.MFA.OTP.NotReady")
}
if existingOTP.Code() == nil {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound")
}
userAgg := &user.NewAggregate(userID, existingOTP.ResourceOwner()).Aggregate
- verifyErr := crypto.VerifyCode(existingOTP.CodeCreationDate(), existingOTP.CodeExpiry(), existingOTP.Code(), code, c.userEncryption)
+ verifyErr := crypto.VerifyCode(existingOTP.CodeCreationDate(), existingOTP.CodeExpiry(), existingOTP.Code(), code, alg)
// recheck for additional events (failed OTP checks or locks)
- recheckErr := c.eventstore.FilterToQueryReducer(ctx, existingOTP)
+ recheckErr := queryReducer(ctx, existingOTP)
if recheckErr != nil {
- return recheckErr
+ return nil, recheckErr
}
if existingOTP.UserLocked() {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-S6h4R", "Errors.User.Locked")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-S6h4R", "Errors.User.Locked")
}
// the OTP check succeeded and the user was not locked in the meantime
if verifyErr == nil {
- _, err = c.eventstore.Push(ctx, checkSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
- return err
+ return []eventstore.Command{checkSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest))}, nil
}
// the OTP check failed, therefore check if the limit was reached and the user must additionally be locked
commands := make([]eventstore.Command, 0, 2)
commands = append(commands, checkFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
- lockoutPolicy, err := c.getLockoutPolicy(ctx, resourceOwner)
- if err != nil {
- return err
- }
- if lockoutPolicy.MaxOTPAttempts > 0 && existingOTP.CheckFailedCount()+1 >= lockoutPolicy.MaxOTPAttempts {
+ lockoutPolicy, lockoutErr := getLockoutPolicy(ctx, existingOTP.ResourceOwner(), queryReducer)
+ logging.OnError(lockoutErr).Error("unable to get lockout policy")
+ if lockoutPolicy != nil && lockoutPolicy.MaxOTPAttempts > 0 && existingOTP.CheckFailedCount()+1 >= lockoutPolicy.MaxOTPAttempts {
commands = append(commands, user.NewUserLockedEvent(ctx, userAgg))
}
-
- _, pushErr := c.eventstore.Push(ctx, commands...)
- logging.WithFields("userID", userID).OnError(pushErr).Error("otp failure check push failed")
- return verifyErr
+ return commands, verifyErr
}
func (c *Commands) totpWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanTOTPWriteModel, err error) {
diff --git a/internal/command/user_human_otp_model.go b/internal/command/user_human_otp_model.go
index 43abc14349..2780a52d7c 100644
--- a/internal/command/user_human_otp_model.go
+++ b/internal/command/user_human_otp_model.go
@@ -157,24 +157,31 @@ func (wm *HumanOTPSMSWriteModel) Query() *eventstore.SearchQueryBuilder {
type HumanOTPSMSCodeWriteModel struct {
*HumanOTPSMSWriteModel
- code *crypto.CryptoValue
- codeCreationDate time.Time
- codeExpiry time.Duration
+ otpCode *OTPCode
checkFailedCount uint64
userLocked bool
}
func (wm *HumanOTPSMSCodeWriteModel) CodeCreationDate() time.Time {
- return wm.codeCreationDate
+ if wm.otpCode == nil {
+ return time.Time{}
+ }
+ return wm.otpCode.CreationDate
}
func (wm *HumanOTPSMSCodeWriteModel) CodeExpiry() time.Duration {
- return wm.codeExpiry
+ if wm.otpCode == nil {
+ return 0
+ }
+ return wm.otpCode.Expiry
}
func (wm *HumanOTPSMSCodeWriteModel) Code() *crypto.CryptoValue {
- return wm.code
+ if wm.otpCode == nil {
+ return nil
+ }
+ return wm.otpCode.Code
}
func (wm *HumanOTPSMSCodeWriteModel) CheckFailedCount() uint64 {
@@ -195,9 +202,11 @@ func (wm *HumanOTPSMSCodeWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanOTPSMSCodeAddedEvent:
- wm.code = e.Code
- wm.codeCreationDate = e.CreationDate()
- wm.codeExpiry = e.Expiry
+ wm.otpCode = &OTPCode{
+ Code: e.Code,
+ CreationDate: e.CreationDate(),
+ Expiry: e.Expiry,
+ }
case *user.HumanOTPSMSCheckSucceededEvent:
wm.checkFailedCount = 0
case *user.HumanOTPSMSCheckFailedEvent:
@@ -300,24 +309,31 @@ func (wm *HumanOTPEmailWriteModel) Query() *eventstore.SearchQueryBuilder {
type HumanOTPEmailCodeWriteModel struct {
*HumanOTPEmailWriteModel
- code *crypto.CryptoValue
- codeCreationDate time.Time
- codeExpiry time.Duration
+ otpCode *OTPCode
checkFailedCount uint64
userLocked bool
}
func (wm *HumanOTPEmailCodeWriteModel) CodeCreationDate() time.Time {
- return wm.codeCreationDate
+ if wm.otpCode == nil {
+ return time.Time{}
+ }
+ return wm.otpCode.CreationDate
}
func (wm *HumanOTPEmailCodeWriteModel) CodeExpiry() time.Duration {
- return wm.codeExpiry
+ if wm.otpCode == nil {
+ return 0
+ }
+ return wm.otpCode.Expiry
}
func (wm *HumanOTPEmailCodeWriteModel) Code() *crypto.CryptoValue {
- return wm.code
+ if wm.otpCode == nil {
+ return nil
+ }
+ return wm.otpCode.Code
}
func (wm *HumanOTPEmailCodeWriteModel) CheckFailedCount() uint64 {
@@ -338,9 +354,11 @@ func (wm *HumanOTPEmailCodeWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanOTPEmailCodeAddedEvent:
- wm.code = e.Code
- wm.codeCreationDate = e.CreationDate()
- wm.codeExpiry = e.Expiry
+ wm.otpCode = &OTPCode{
+ Code: e.Code,
+ CreationDate: e.CreationDate(),
+ Expiry: e.Expiry,
+ }
case *user.HumanOTPEmailCheckSucceededEvent:
wm.checkFailedCount = 0
case *user.HumanOTPEmailCheckFailedEvent:
diff --git a/internal/command/user_human_password.go b/internal/command/user_human_password.go
index fd8b29a429..839977b384 100644
--- a/internal/command/user_human_password.go
+++ b/internal/command/user_human_password.go
@@ -295,8 +295,8 @@ func (c *Commands) PasswordChangeSent(ctx context.Context, orgID, userID string)
return err
}
-// HumanCheckPassword check password for user with additional informations from authRequest
-func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, password string, authRequest *domain.AuthRequest, lockoutPolicy *domain.LockoutPolicy) (err error) {
+// HumanCheckPassword check password for user with additional information from authRequest
+func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, password string, authRequest *domain.AuthRequest) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -314,56 +314,66 @@ func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, passwo
if !loginPolicy.AllowUsernamePassword {
return zerrors.ThrowPreconditionFailed(err, "COMMAND-Dft32", "Errors.Org.LoginPolicy.UsernamePasswordNotAllowed")
}
-
- wm, err := c.passwordWriteModel(ctx, userID, orgID)
- if err != nil {
+ commands, err := checkPassword(ctx, userID, password, c.eventstore, c.userPasswordHasher, authRequestDomainToAuthRequestInfo(authRequest))
+ if len(commands) == 0 {
return err
}
+ _, pushErr := c.eventstore.Push(ctx, commands...)
+ logging.OnError(pushErr).Error("error create password check failed event")
+ return err
+}
- if !isUserStateExists(wm.UserState) {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.NotFound")
+func checkPassword(ctx context.Context, userID, password string, es *eventstore.Eventstore, hasher *crypto.Hasher, optionalAuthRequestInfo *user.AuthRequestInfo) ([]eventstore.Command, error) {
+ if userID == "" {
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Sfw3f", "Errors.User.UserIDMissing")
+ }
+ wm := NewHumanPasswordWriteModel(userID, "")
+ err := es.FilterToQueryReducer(ctx, wm)
+ if err != nil {
+ return nil, err
+ }
+ if !wm.UserState.Exists() {
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.NotFound")
}
if wm.UserState == domain.UserStateLocked {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-JLK35", "Errors.User.Locked")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-JLK35", "Errors.User.Locked")
}
if wm.EncodedHash == "" {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-3nJ4t", "Errors.User.Password.NotSet")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-3nJ4t", "Errors.User.Password.NotSet")
}
userAgg := UserAggregateFromWriteModel(&wm.WriteModel)
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "passwap.Verify")
- updated, err := c.userPasswordHasher.Verify(wm.EncodedHash, password)
+ updated, err := hasher.Verify(wm.EncodedHash, password)
spanPasswordComparison.EndWithError(err)
err = convertPasswapErr(err)
commands := make([]eventstore.Command, 0, 2)
// recheck for additional events (failed password checks or locks)
- recheckErr := c.eventstore.FilterToQueryReducer(ctx, wm)
+ recheckErr := es.FilterToQueryReducer(ctx, wm)
if recheckErr != nil {
- return recheckErr
+ return nil, recheckErr
}
if wm.UserState == domain.UserStateLocked {
- return zerrors.ThrowPreconditionFailed(nil, "COMMAND-SFA3t", "Errors.User.Locked")
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SFA3t", "Errors.User.Locked")
}
if err == nil {
- commands = append(commands, user.NewHumanPasswordCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
+ commands = append(commands, user.NewHumanPasswordCheckSucceededEvent(ctx, userAgg, optionalAuthRequestInfo))
if updated != "" {
commands = append(commands, user.NewHumanPasswordHashUpdatedEvent(ctx, userAgg, updated))
}
- _, err = c.eventstore.Push(ctx, commands...)
- return err
+ return commands, nil
}
- commands = append(commands, user.NewHumanPasswordCheckFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
- if lockoutPolicy != nil && lockoutPolicy.MaxPasswordAttempts > 0 {
- if wm.PasswordCheckFailedCount+1 >= lockoutPolicy.MaxPasswordAttempts {
- commands = append(commands, user.NewUserLockedEvent(ctx, userAgg))
- }
+ commands = append(commands, user.NewHumanPasswordCheckFailedEvent(ctx, userAgg, optionalAuthRequestInfo))
+
+ lockoutPolicy, lockoutErr := getLockoutPolicy(ctx, wm.ResourceOwner, es.FilterToQueryReducer)
+ logging.OnError(lockoutErr).Error("unable to get lockout policy")
+ if lockoutPolicy != nil && lockoutPolicy.MaxPasswordAttempts > 0 && wm.PasswordCheckFailedCount+1 >= lockoutPolicy.MaxPasswordAttempts {
+ commands = append(commands, user.NewUserLockedEvent(ctx, userAgg))
}
- _, pushErr := c.eventstore.Push(ctx, commands...)
- logging.OnError(pushErr).Error("error create password check failed event")
- return err
+ return commands, err
}
func (c *Commands) passwordWriteModel(ctx context.Context, userID, resourceOwner string) (writeModel *HumanPasswordWriteModel, err error) {
diff --git a/internal/command/user_human_password_test.go b/internal/command/user_human_password_test.go
index 4e7672d8bc..d944f5e1cc 100644
--- a/internal/command/user_human_password_test.go
+++ b/internal/command/user_human_password_test.go
@@ -1456,7 +1456,6 @@ func TestCommandSide_CheckPassword(t *testing.T) {
resourceOwner string
password string
authReq *domain.AuthRequest
- lockoutPolicy *domain.LockoutPolicy
}
type res struct {
err func(error) bool
@@ -1768,6 +1767,13 @@ func TestCommandSide_CheckPassword(t *testing.T) {
"")),
),
expectFilter(),
+ expectFilter(
+ eventFromEventPusher(
+ org.NewLockoutPolicyAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ 0, 0, false,
+ )),
+ ),
expectPush(
user.NewHumanPasswordCheckFailedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
@@ -1789,7 +1795,6 @@ func TestCommandSide_CheckPassword(t *testing.T) {
ID: "request1",
AgentID: "agent1",
},
- lockoutPolicy: &domain.LockoutPolicy{},
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -1852,6 +1857,13 @@ func TestCommandSide_CheckPassword(t *testing.T) {
),
),
expectFilter(),
+ expectFilter(
+ eventFromEventPusher(
+ org.NewLockoutPolicyAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ 1, 1, false,
+ )),
+ ),
expectPush(
user.NewHumanPasswordCheckFailedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
@@ -1876,10 +1888,6 @@ func TestCommandSide_CheckPassword(t *testing.T) {
ID: "request1",
AgentID: "agent1",
},
- lockoutPolicy: &domain.LockoutPolicy{
- MaxPasswordAttempts: 1,
- MaxOTPAttempts: 1,
- },
},
res: res{
err: zerrors.IsErrorInvalidArgument,
@@ -2230,7 +2238,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
eventstore: tt.fields.eventstore(t),
userPasswordHasher: tt.fields.userPasswordHasher,
}
- err := r.HumanCheckPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.password, tt.args.authReq, tt.args.lockoutPolicy)
+ err := r.HumanCheckPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.password, tt.args.authReq)
if tt.res.err == nil {
assert.NoError(t, err)
}
diff --git a/internal/crypto/rsa.go b/internal/crypto/rsa.go
index 38d8e6a1bd..198610d8aa 100644
--- a/internal/crypto/rsa.go
+++ b/internal/crypto/rsa.go
@@ -171,7 +171,7 @@ func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) {
var ErrEmpty = errors.New("cannot decode, empty data")
func BytesToPublicKey(pub []byte) (*rsa.PublicKey, error) {
- if pub == nil {
+ if len(pub) == 0 {
return nil, ErrEmpty
}
block, _ := pem.Decode(pub)
diff --git a/internal/database/cockroach/crdb.go b/internal/database/cockroach/crdb.go
index 48649fac9e..3d72e3904c 100644
--- a/internal/database/cockroach/crdb.go
+++ b/internal/database/cockroach/crdb.go
@@ -14,7 +14,7 @@ import (
)
func init() {
- config := &Config{}
+ config := new(Config)
dialect.Register(config, config, true)
}
@@ -49,11 +49,12 @@ func (c *Config) MatchName(name string) bool {
return false
}
-func (c *Config) Decode(configs []interface{}) (dialect.Connector, error) {
+func (_ *Config) Decode(configs []interface{}) (dialect.Connector, error) {
+ connector := new(Config)
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
WeaklyTypedInput: true,
- Result: c,
+ Result: connector,
})
if err != nil {
return nil, err
@@ -65,7 +66,7 @@ func (c *Config) Decode(configs []interface{}) (dialect.Connector, error) {
}
}
- return c, nil
+ return connector, nil
}
func (c *Config) Connect(useAdmin bool, pusherRatio, spoolerRatio float64, purpose dialect.DBPurpose) (*sql.DB, error) {
diff --git a/internal/database/database.go b/internal/database/database.go
index 77baaa7bd2..f057b5c4e8 100644
--- a/internal/database/database.go
+++ b/internal/database/database.go
@@ -75,7 +75,7 @@ func (db *DB) QueryRow(scan func(*sql.Row) error, query string, args ...any) (er
func (db *DB) QueryRowContext(ctx context.Context, scan func(row *sql.Row) error, query string, args ...any) (err error) {
ctx, spanBeginTx := tracing.NewNamedSpan(ctx, "db.BeginTx")
- tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
+ tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true, Isolation: sql.LevelReadCommitted})
spanBeginTx.EndWithError(err)
if err != nil {
return err
diff --git a/internal/database/postgres/pg.go b/internal/database/postgres/pg.go
index de7942f725..8f70da0703 100644
--- a/internal/database/postgres/pg.go
+++ b/internal/database/postgres/pg.go
@@ -14,7 +14,7 @@ import (
)
func init() {
- config := &Config{}
+ config := new(Config)
dialect.Register(config, config, false)
}
@@ -50,11 +50,12 @@ func (c *Config) MatchName(name string) bool {
return false
}
-func (c *Config) Decode(configs []interface{}) (dialect.Connector, error) {
+func (_ *Config) Decode(configs []interface{}) (dialect.Connector, error) {
+ connector := new(Config)
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
WeaklyTypedInput: true,
- Result: c,
+ Result: connector,
})
if err != nil {
return nil, err
@@ -66,7 +67,7 @@ func (c *Config) Decode(configs []interface{}) (dialect.Connector, error) {
}
}
- return c, nil
+ return connector, nil
}
func (c *Config) Connect(useAdmin bool, pusherRatio, spoolerRatio float64, purpose dialect.DBPurpose) (*sql.DB, error) {
diff --git a/internal/domain/auth_request.go b/internal/domain/auth_request.go
index b7e463ff9a..1aaf2bb63d 100644
--- a/internal/domain/auth_request.go
+++ b/internal/domain/auth_request.go
@@ -1,6 +1,7 @@
package domain
import (
+ "slices"
"strings"
"time"
@@ -81,7 +82,7 @@ func (a *AuthRequest) AuthMethods() []UserAuthMethodType {
for _, mfa := range a.MFAsVerified {
list = append(list, mfa.UserAuthMethodType())
}
- return list
+ return slices.Compact(list)
}
type ExternalUser struct {
diff --git a/internal/domain/user.go b/internal/domain/user.go
index 3204d658da..da32e17490 100644
--- a/internal/domain/user.go
+++ b/internal/domain/user.go
@@ -62,7 +62,8 @@ func HasMFA(methods []UserAuthMethodType) bool {
UserAuthMethodTypeOTPSMS,
UserAuthMethodTypeOTPEmail,
UserAuthMethodTypeIDP,
- UserAuthMethodTypeOTP:
+ UserAuthMethodTypeOTP,
+ UserAuthMethodTypePrivateKey:
factors++
case UserAuthMethodTypeUnspecified,
userAuthMethodTypeCount:
@@ -72,6 +73,30 @@ func HasMFA(methods []UserAuthMethodType) bool {
return factors > 1
}
+// Has2FA checks whether the auth factors provided are a second factor and will return true if at least one is.
+func Has2FA(methods []UserAuthMethodType) bool {
+ var factors int
+ for _, method := range methods {
+ switch method {
+ case
+ UserAuthMethodTypeU2F,
+ UserAuthMethodTypeTOTP,
+ UserAuthMethodTypeOTPSMS,
+ UserAuthMethodTypeOTPEmail,
+ UserAuthMethodTypeOTP:
+ factors++
+ case UserAuthMethodTypeUnspecified,
+ UserAuthMethodTypePassword,
+ UserAuthMethodTypePasswordless,
+ UserAuthMethodTypeIDP,
+ UserAuthMethodTypePrivateKey,
+ userAuthMethodTypeCount:
+ // ignore
+ }
+ }
+ return factors > 0
+}
+
// RequiresMFA checks whether the user requires to authenticate with multiple auth factors based on the LoginPolicy and the authentication type.
// Internal authentication will require MFA if either option is activated.
// External authentication will only require MFA if it's forced generally and not local only.
diff --git a/internal/eventstore/handler/v2/handler.go b/internal/eventstore/handler/v2/handler.go
index 02afc58ca2..0e0f8ae4d1 100644
--- a/internal/eventstore/handler/v2/handler.go
+++ b/internal/eventstore/handler/v2/handler.go
@@ -259,9 +259,6 @@ func (h *Handler) triggerInstances(ctx context.Context, instances []string, trig
for ; err != nil; _, err = h.Trigger(instanceCtx, triggerOpts...) {
time.Sleep(h.retryFailedAfter)
h.log().WithField("instance", instance).OnError(err).Debug("trigger failed")
- if err == nil {
- break
- }
}
}
}
diff --git a/internal/eventstore/local_crdb_test.go b/internal/eventstore/local_crdb_test.go
index fc7a35daf9..6df9e9fd29 100644
--- a/internal/eventstore/local_crdb_test.go
+++ b/internal/eventstore/local_crdb_test.go
@@ -88,7 +88,8 @@ func initDB(db *database.DB) error {
err := initialise.Init(db,
initialise.VerifyUser(config.Username(), ""),
initialise.VerifyDatabase(config.DatabaseName()),
- initialise.VerifyGrant(config.DatabaseName(), config.Username()))
+ initialise.VerifyGrant(config.DatabaseName(), config.Username()),
+ initialise.VerifySettings(config.DatabaseName(), config.Username()))
if err != nil {
return err
}
diff --git a/internal/eventstore/read_model.go b/internal/eventstore/read_model.go
index 10c84358e5..d2c755cc3a 100644
--- a/internal/eventstore/read_model.go
+++ b/internal/eventstore/read_model.go
@@ -13,6 +13,7 @@ type ReadModel struct {
Events []Event `json:"-"`
ResourceOwner string `json:"-"`
InstanceID string `json:"-"`
+ Position float64 `json:"-"`
}
// AppendEvents adds all the events to the read model.
@@ -43,6 +44,7 @@ func (rm *ReadModel) Reduce() error {
}
rm.ChangeDate = rm.Events[len(rm.Events)-1].CreatedAt()
rm.ProcessedSequence = rm.Events[len(rm.Events)-1].Sequence()
+ rm.Position = rm.Events[len(rm.Events)-1].Position()
// all events processed and not needed anymore
rm.Events = rm.Events[0:0]
return nil
diff --git a/internal/eventstore/repository/sql/crdb.go b/internal/eventstore/repository/sql/crdb.go
index 2298f8ee48..9f925d4635 100644
--- a/internal/eventstore/repository/sql/crdb.go
+++ b/internal/eventstore/repository/sql/crdb.go
@@ -282,17 +282,23 @@ func (db *CRDB) db() *database.DB {
return db.DB
}
-func (db *CRDB) orderByEventSequence(desc, useV1 bool) string {
+func (db *CRDB) orderByEventSequence(desc, shouldOrderBySequence, useV1 bool) string {
if useV1 {
if desc {
return ` ORDER BY event_sequence DESC`
}
return ` ORDER BY event_sequence`
}
+ if shouldOrderBySequence {
+ if desc {
+ return ` ORDER BY "sequence" DESC`
+ }
+ return ` ORDER BY "sequence"`
+ }
+
if desc {
return ` ORDER BY "position" DESC, in_tx_order DESC`
}
-
return ` ORDER BY "position", in_tx_order`
}
diff --git a/internal/eventstore/repository/sql/local_crdb_test.go b/internal/eventstore/repository/sql/local_crdb_test.go
index ca5128aaf7..fccb169341 100644
--- a/internal/eventstore/repository/sql/local_crdb_test.go
+++ b/internal/eventstore/repository/sql/local_crdb_test.go
@@ -60,7 +60,8 @@ func initDB(db *database.DB) error {
err := initialise.Init(db,
initialise.VerifyUser(config.Username(), ""),
initialise.VerifyDatabase(config.DatabaseName()),
- initialise.VerifyGrant(config.DatabaseName(), config.Username()))
+ initialise.VerifyGrant(config.DatabaseName(), config.Username()),
+ initialise.VerifySettings(config.DatabaseName(), config.Username()))
if err != nil {
return err
}
diff --git a/internal/eventstore/repository/sql/query.go b/internal/eventstore/repository/sql/query.go
index 145f168d52..3cddcb7924 100644
--- a/internal/eventstore/repository/sql/query.go
+++ b/internal/eventstore/repository/sql/query.go
@@ -28,7 +28,7 @@ type querier interface {
maxSequenceQuery(useV1 bool) string
instanceIDsQuery(useV1 bool) string
db() *database.DB
- orderByEventSequence(desc, useV1 bool) string
+ orderByEventSequence(desc, shouldOrderBySequence, useV1 bool) string
dialect.Database
}
@@ -59,6 +59,7 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search
if err != nil {
return err
}
+
query, rowScanner := prepareColumns(criteria, q.Columns, useV1)
where, values := prepareConditions(criteria, q, useV1)
if where == "" || query == "" {
@@ -78,10 +79,20 @@ func query(ctx context.Context, criteria querier, searchQuery *eventstore.Search
q.Desc = true
}
+ // if there is only one subquery we can optimize the query ordering by ordering by sequence
+ var shouldOrderBySequence bool
+ if len(q.SubQueries) == 1 {
+ for _, filter := range q.SubQueries[0] {
+ if filter.Field == repository.FieldAggregateID {
+ shouldOrderBySequence = filter.Operation == repository.OperationEquals
+ }
+ }
+ }
+
switch q.Columns {
case eventstore.ColumnsEvent,
eventstore.ColumnsMaxSequence:
- query += criteria.orderByEventSequence(q.Desc, useV1)
+ query += criteria.orderByEventSequence(q.Desc, shouldOrderBySequence, useV1)
}
if q.Limit > 0 {
@@ -220,7 +231,7 @@ func eventsScanner(useV1 bool) func(scanner scan, dest interface{}) (err error)
}
}
-func prepareConditions(criteria querier, query *repository.SearchQuery, useV1 bool) (string, []any) {
+func prepareConditions(criteria querier, query *repository.SearchQuery, useV1 bool) (_ string, args []any) {
clauses, args := prepareQuery(criteria, useV1, query.InstanceID, query.InstanceIDs, query.ExcludedInstances)
if clauses != "" && len(query.SubQueries) > 0 {
clauses += " AND "
diff --git a/internal/feature/feature.go b/internal/feature/feature.go
index 5edab4a8ba..dc3e0eb597 100644
--- a/internal/feature/feature.go
+++ b/internal/feature/feature.go
@@ -11,6 +11,7 @@ const (
KeyUserSchema
KeyTokenExchange
KeyActions
+ KeyImprovedPerformance
)
//go:generate enumer -type Level -transform snake -trimprefix Level
@@ -27,10 +28,27 @@ const (
)
type Features struct {
- LoginDefaultOrg bool `json:"login_default_org,omitempty"`
- TriggerIntrospectionProjections bool `json:"trigger_introspection_projections,omitempty"`
- LegacyIntrospection bool `json:"legacy_introspection,omitempty"`
- UserSchema bool `json:"user_schema,omitempty"`
- TokenExchange bool `json:"token_exchange,omitempty"`
- Actions bool `json:"actions,omitempty"`
+ LoginDefaultOrg bool `json:"login_default_org,omitempty"`
+ TriggerIntrospectionProjections bool `json:"trigger_introspection_projections,omitempty"`
+ LegacyIntrospection bool `json:"legacy_introspection,omitempty"`
+ UserSchema bool `json:"user_schema,omitempty"`
+ TokenExchange bool `json:"token_exchange,omitempty"`
+ Actions bool `json:"actions,omitempty"`
+ ImprovedPerformance []ImprovedPerformanceType `json:"improved_performance,omitempty"`
+}
+
+type ImprovedPerformanceType int32
+
+const (
+ ImprovedPerformanceTypeUnknown = iota
+ ImprovedPerformanceTypeOrgByID
+)
+
+func (f Features) ShouldUseImprovedPerformance(typ ImprovedPerformanceType) bool {
+ for _, improvedType := range f.ImprovedPerformance {
+ if improvedType == typ {
+ return true
+ }
+ }
+ return false
}
diff --git a/internal/feature/key_enumer.go b/internal/feature/key_enumer.go
index 172d5f9d01..ca3e156b61 100644
--- a/internal/feature/key_enumer.go
+++ b/internal/feature/key_enumer.go
@@ -7,11 +7,11 @@ import (
"strings"
)
-const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactions"
+const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performance"
-var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113}
+var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113, 133}
-const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactions"
+const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performance"
func (i Key) String() string {
if i < 0 || i >= Key(len(_KeyIndex)-1) {
@@ -31,9 +31,10 @@ func _KeyNoOp() {
_ = x[KeyUserSchema-(4)]
_ = x[KeyTokenExchange-(5)]
_ = x[KeyActions-(6)]
+ _ = x[KeyImprovedPerformance-(7)]
}
-var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions}
+var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance}
var _KeyNameToValueMap = map[string]Key{
_KeyName[0:11]: KeyUnspecified,
@@ -50,6 +51,8 @@ var _KeyNameToValueMap = map[string]Key{
_KeyLowerName[92:106]: KeyTokenExchange,
_KeyName[106:113]: KeyActions,
_KeyLowerName[106:113]: KeyActions,
+ _KeyName[113:133]: KeyImprovedPerformance,
+ _KeyLowerName[113:133]: KeyImprovedPerformance,
}
var _KeyNames = []string{
@@ -60,6 +63,7 @@ var _KeyNames = []string{
_KeyName[81:92],
_KeyName[92:106],
_KeyName[106:113],
+ _KeyName[113:133],
}
// KeyString retrieves an enum value from the enum constants string name.
diff --git a/internal/integration/client.go b/internal/integration/client.go
index 72f50e9988..4b400bfb39 100644
--- a/internal/integration/client.go
+++ b/internal/integration/client.go
@@ -76,7 +76,7 @@ func newClient(cc *grpc.ClientConn) Client {
}
}
-func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx context.Context) (primaryDomain, instanceId string, authenticatedIamOwnerCtx context.Context) {
+func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx context.Context) (primaryDomain, instanceId, adminID string, authenticatedIamOwnerCtx context.Context) {
primaryDomain = RandString(5) + ".integration.localhost"
instance, err := t.Client.System.CreateInstance(systemCtx, &system.CreateInstanceRequest{
InstanceName: "testinstance",
@@ -89,20 +89,23 @@ func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx conte
},
},
})
- if err != nil {
- panic(err)
- }
+ require.NoError(tt, err)
t.createClientConn(iamOwnerCtx, fmt.Sprintf("%s:%d", primaryDomain, t.Config.Port))
instanceId = instance.GetInstanceId()
+ owner, err := t.Queries.GetUserByLoginName(authz.WithInstanceID(iamOwnerCtx, instanceId), true, "owner@"+primaryDomain)
+ require.NoError(tt, err)
t.Users.Set(instanceId, IAMOwner, &User{
+ User: owner,
Token: instance.GetPat(),
})
newCtx := t.WithInstanceAuthorization(iamOwnerCtx, IAMOwner, instanceId)
+ var adminUser *mgmt.ImportHumanUserResponse
// the following serves two purposes:
// 1. it ensures that the instance is ready to be used
// 2. it enables a normal login with the default admin user credentials
require.EventuallyWithT(tt, func(collectT *assert.CollectT) {
- _, importErr := t.Client.Mgmt.ImportHumanUser(newCtx, &mgmt.ImportHumanUserRequest{
+ var importErr error
+ adminUser, importErr = t.Client.Mgmt.ImportHumanUser(newCtx, &mgmt.ImportHumanUserRequest{
UserName: "zitadel-admin@zitadel.localhost",
Email: &mgmt.ImportHumanUserRequest_Email{
Email: "zitadel-admin@zitadel.localhost",
@@ -117,7 +120,7 @@ func (t *Tester) UseIsolatedInstance(tt *testing.T, iamOwnerCtx, systemCtx conte
})
assert.NoError(collectT, importErr)
}, 2*time.Minute, 100*time.Millisecond, "instance not ready")
- return primaryDomain, instanceId, newCtx
+ return primaryDomain, instanceId, adminUser.GetUserId(), newCtx
}
func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse {
diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go
index 3ba655c65b..3e90cb6856 100644
--- a/internal/integration/oidc.go
+++ b/internal/integration/oidc.go
@@ -151,7 +151,10 @@ func (s *Tester) CreateAPIClientBasic(ctx context.Context, projectID string) (*m
const CodeVerifier = "codeVerifier"
func (s *Tester) CreateOIDCAuthRequest(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) {
- provider, err := s.CreateRelyingParty(ctx, clientID, redirectURI, scope...)
+ return s.CreateOIDCAuthRequestWithDomain(ctx, s.Config.ExternalDomain, clientID, loginClient, redirectURI, scope...)
+}
+func (s *Tester) CreateOIDCAuthRequestWithDomain(ctx context.Context, domain, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) {
+ provider, err := s.CreateRelyingPartyForDomain(ctx, domain, clientID, redirectURI, scope...)
if err != nil {
return "", err
}
@@ -212,11 +215,15 @@ func (s *Tester) OIDCIssuer() string {
}
func (s *Tester) CreateRelyingParty(ctx context.Context, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) {
+ return s.CreateRelyingPartyForDomain(ctx, s.Config.ExternalDomain, clientID, redirectURI, scope...)
+}
+
+func (s *Tester) CreateRelyingPartyForDomain(ctx context.Context, domain, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) {
if len(scope) == 0 {
scope = []string{oidc.ScopeOpenID}
}
loginClient := &http.Client{Transport: &loginRoundTripper{http.DefaultTransport}}
- return rp.NewRelyingPartyOIDC(ctx, s.OIDCIssuer(), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient))
+ return rp.NewRelyingPartyOIDC(ctx, http_util.BuildHTTP(domain, s.Config.Port, s.Config.ExternalSecure), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient))
}
type loginRoundTripper struct {
@@ -281,42 +288,42 @@ func CheckRedirect(req *http.Request) (*url.URL, error) {
return resp.Location()
}
-func (s *Tester) CreateOIDCCredentialsClient(ctx context.Context) (userID, clientID, clientSecret string, err error) {
- name := gofakeit.Username()
- user, err := s.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{
+func (s *Tester) CreateOIDCCredentialsClient(ctx context.Context) (machine *management.AddMachineUserResponse, name, clientID, clientSecret string, err error) {
+ name = gofakeit.Username()
+ machine, err = s.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{
Name: name,
UserName: name,
AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT,
})
if err != nil {
- return "", "", "", err
+ return nil, "", "", "", err
}
secret, err := s.Client.Mgmt.GenerateMachineSecret(ctx, &management.GenerateMachineSecretRequest{
- UserId: user.GetUserId(),
+ UserId: machine.GetUserId(),
})
if err != nil {
- return "", "", "", err
+ return nil, "", "", "", err
}
- return user.GetUserId(), secret.GetClientId(), secret.GetClientSecret(), nil
+ return machine, name, secret.GetClientId(), secret.GetClientSecret(), nil
}
-func (s *Tester) CreateOIDCJWTProfileClient(ctx context.Context) (userID string, keyData []byte, err error) {
- name := gofakeit.Username()
- user, err := s.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{
+func (s *Tester) CreateOIDCJWTProfileClient(ctx context.Context) (machine *management.AddMachineUserResponse, name string, keyData []byte, err error) {
+ name = gofakeit.Username()
+ machine, err = s.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{
Name: name,
UserName: name,
AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT,
})
if err != nil {
- return "", nil, err
+ return nil, "", nil, err
}
keyResp, err := s.Client.Mgmt.AddMachineKey(ctx, &management.AddMachineKeyRequest{
- UserId: user.GetUserId(),
+ UserId: machine.GetUserId(),
Type: authn.KeyType_KEY_TYPE_JSON,
ExpirationDate: timestamppb.New(time.Now().Add(time.Hour)),
})
if err != nil {
- return "", nil, err
+ return nil, "", nil, err
}
- return user.GetUserId(), keyResp.GetKeyDetails(), nil
+ return machine, name, keyResp.GetKeyDetails(), nil
}
diff --git a/internal/notification/handlers/quota_notifier_test.go b/internal/notification/handlers/quota_notifier_test.go
index 72991019da..14de4e369c 100644
--- a/internal/notification/handlers/quota_notifier_test.go
+++ b/internal/notification/handlers/quota_notifier_test.go
@@ -21,7 +21,7 @@ import (
)
func TestServer_QuotaNotification_Limit(t *testing.T) {
- _, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
+ _, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
amount := 10
percent := 50
percentAmount := amount * percent / 100
@@ -67,7 +67,7 @@ func TestServer_QuotaNotification_Limit(t *testing.T) {
}
func TestServer_QuotaNotification_NoLimit(t *testing.T) {
- _, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
+ _, instanceID, _, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
amount := 10
percent := 50
percentAmount := amount * percent / 100
diff --git a/internal/notification/handlers/telemetry_pusher_integration_test.go b/internal/notification/handlers/telemetry_pusher_integration_test.go
index 9520253ade..b84f02fa79 100644
--- a/internal/notification/handlers/telemetry_pusher_integration_test.go
+++ b/internal/notification/handlers/telemetry_pusher_integration_test.go
@@ -4,38 +4,126 @@ package handlers_test
import (
"bytes"
+ "context"
"encoding/json"
+ "net/url"
"testing"
"time"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/zitadel/oidc/v3/pkg/client/rp"
+ "github.com/zitadel/oidc/v3/pkg/oidc"
+
+ "github.com/zitadel/zitadel/internal/integration"
+ "github.com/zitadel/zitadel/pkg/grpc/app"
"github.com/zitadel/zitadel/pkg/grpc/management"
+ "github.com/zitadel/zitadel/pkg/grpc/object"
+ oidc_v2 "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
+ "github.com/zitadel/zitadel/pkg/grpc/project"
"github.com/zitadel/zitadel/pkg/grpc/system"
)
func TestServer_TelemetryPushMilestones(t *testing.T) {
- primaryDomain, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
+ primaryDomain, instanceID, adminID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
t.Log("testing against instance with primary domain", primaryDomain)
awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "InstanceCreated")
- project, err := Tester.Client.Mgmt.AddProject(iamOwnerCtx, &management.AddProjectRequest{Name: "integration"})
- if err != nil {
- t.Fatal(err)
- }
+
+ projectAdded, err := Tester.Client.Mgmt.AddProject(iamOwnerCtx, &management.AddProjectRequest{Name: "integration"})
+ require.NoError(t, err)
awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "ProjectCreated")
- if _, err = Tester.Client.Mgmt.AddOIDCApp(iamOwnerCtx, &management.AddOIDCAppRequest{
- ProjectId: project.GetId(),
- Name: "integration",
- }); err != nil {
- t.Fatal(err)
- }
+
+ redirectURI := "http://localhost:8888"
+ application, err := Tester.Client.Mgmt.AddOIDCApp(iamOwnerCtx, &management.AddOIDCAppRequest{
+ ProjectId: projectAdded.GetId(),
+ Name: "integration",
+ RedirectUris: []string{redirectURI},
+ ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
+ GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE},
+ AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB,
+ AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE,
+ DevMode: true,
+ AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT,
+ })
+ require.NoError(t, err)
awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "ApplicationCreated")
- // TODO: trigger and await milestone AuthenticationSucceededOnInstance
- // TODO: trigger and await milestone AuthenticationSucceededOnApplication
- if _, err = Tester.Client.System.RemoveInstance(SystemCTX, &system.RemoveInstanceRequest{InstanceId: instanceID}); err != nil {
- t.Fatal(err)
- }
+
+ // create the session to be used for the authN of the clients
+ sessionID, sessionToken, _, _ := Tester.CreatePasswordSession(t, iamOwnerCtx, adminID, "Password1!")
+
+ console := consoleOIDCConfig(iamOwnerCtx, t)
+ loginToClient(iamOwnerCtx, t, primaryDomain, console.GetClientId(), instanceID, console.GetRedirectUris()[0], sessionID, sessionToken)
+ awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "AuthenticationSucceededOnInstance")
+
+ // make sure the client has been projected
+ require.EventuallyWithT(t, func(collectT *assert.CollectT) {
+ _, err := Tester.Client.Mgmt.GetAppByID(iamOwnerCtx, &management.GetAppByIDRequest{
+ ProjectId: projectAdded.GetId(),
+ AppId: application.GetAppId(),
+ })
+ assert.NoError(collectT, err)
+ }, 1*time.Minute, 100*time.Millisecond, "app not found")
+ loginToClient(iamOwnerCtx, t, primaryDomain, application.GetClientId(), instanceID, redirectURI, sessionID, sessionToken)
+ awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "AuthenticationSucceededOnApplication")
+
+ _, err = Tester.Client.System.RemoveInstance(SystemCTX, &system.RemoveInstanceRequest{InstanceId: instanceID})
+ require.NoError(t, err)
awaitMilestone(t, Tester.MilestoneChan, primaryDomain, "InstanceDeleted")
}
+func loginToClient(iamOwnerCtx context.Context, t *testing.T, primaryDomain, clientID, instanceID, redirectURI, sessionID, sessionToken string) {
+ authRequestID, err := Tester.CreateOIDCAuthRequestWithDomain(iamOwnerCtx, primaryDomain, clientID, Tester.Users.Get(instanceID, integration.IAMOwner).ID, redirectURI, "openid")
+ require.NoError(t, err)
+ callback, err := Tester.Client.OIDCv2.CreateCallback(iamOwnerCtx, &oidc_v2.CreateCallbackRequest{
+ AuthRequestId: authRequestID,
+ CallbackKind: &oidc_v2.CreateCallbackRequest_Session{Session: &oidc_v2.Session{
+ SessionId: sessionID,
+ SessionToken: sessionToken,
+ }},
+ })
+ require.NoError(t, err)
+ provider, err := Tester.CreateRelyingPartyForDomain(iamOwnerCtx, primaryDomain, clientID, redirectURI)
+ require.NoError(t, err)
+ callbackURL, err := url.Parse(callback.GetCallbackUrl())
+ require.NoError(t, err)
+ code := callbackURL.Query().Get("code")
+ _, err = rp.CodeExchange[*oidc.IDTokenClaims](iamOwnerCtx, code, provider, rp.WithCodeVerifier(integration.CodeVerifier))
+ require.NoError(t, err)
+}
+
+func consoleOIDCConfig(iamOwnerCtx context.Context, t *testing.T) *app.OIDCConfig {
+ projects, err := Tester.Client.Mgmt.ListProjects(iamOwnerCtx, &management.ListProjectsRequest{
+ Queries: []*project.ProjectQuery{
+ {
+ Query: &project.ProjectQuery_NameQuery{
+ NameQuery: &project.ProjectNameQuery{
+ Name: "ZITADEL",
+ Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
+ },
+ },
+ },
+ },
+ })
+ require.NoError(t, err)
+ require.Len(t, projects.GetResult(), 1)
+ apps, err := Tester.Client.Mgmt.ListApps(iamOwnerCtx, &management.ListAppsRequest{
+ ProjectId: projects.GetResult()[0].GetId(),
+ Queries: []*app.AppQuery{
+ {
+ Query: &app.AppQuery_NameQuery{
+ NameQuery: &app.AppNameQuery{
+ Name: "Console",
+ Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
+ },
+ },
+ },
+ },
+ })
+ require.NoError(t, err)
+ require.Len(t, apps.GetResult(), 1)
+ return apps.GetResult()[0].GetOidcConfig()
+}
+
func awaitMilestone(t *testing.T, bodies chan []byte, primaryDomain, expectMilestoneType string) {
for {
select {
diff --git a/internal/notification/projections.go b/internal/notification/projections.go
index cdb2a84b26..46434536c2 100644
--- a/internal/notification/projections.go
+++ b/internal/notification/projections.go
@@ -44,6 +44,16 @@ func Start(ctx context.Context) {
}
}
+func ProjectInstance(ctx context.Context) error {
+ for _, projection := range projections {
+ _, err := projection.Trigger(ctx)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
func Projections() []*handler.Handler {
return projections
}
diff --git a/internal/query/access_token.go b/internal/query/access_token.go
index 40d9b1700a..a777a6afc7 100644
--- a/internal/query/access_token.go
+++ b/internal/query/access_token.go
@@ -17,7 +17,7 @@ import (
)
type OIDCSessionAccessTokenReadModel struct {
- eventstore.WriteModel
+ eventstore.ReadModel
UserID string
SessionID string
@@ -39,7 +39,7 @@ type OIDCSessionAccessTokenReadModel struct {
func newOIDCSessionAccessTokenReadModel(id string) *OIDCSessionAccessTokenReadModel {
return &OIDCSessionAccessTokenReadModel{
- WriteModel: eventstore.WriteModel{
+ ReadModel: eventstore.ReadModel{
AggregateID: id,
},
}
@@ -57,13 +57,11 @@ func (wm *OIDCSessionAccessTokenReadModel) Reduce() error {
wm.reduceTokenRevoked(event)
}
}
- return wm.WriteModel.Reduce()
+ return wm.ReadModel.Reduce()
}
func (wm *OIDCSessionAccessTokenReadModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
- AwaitOpenTransactions().
- AllowTimeTravel().
AddQuery().
AggregateTypes(oidcsession.AggregateType).
AggregateIDs(wm.AggregateID).
@@ -120,7 +118,7 @@ func (q *Queries) ActiveAccessTokenByToken(ctx context.Context, token string) (m
if !model.AccessTokenExpiration.After(time.Now()) {
return nil, zerrors.ThrowPermissionDenied(nil, "QUERY-SAF3rf", "Errors.OIDCSession.Token.Expired")
}
- if err = q.checkSessionNotTerminatedAfter(ctx, model.SessionID, model.UserID, model.AccessTokenCreation, model.UserAgent.GetFingerprintID()); err != nil {
+ if err = q.checkSessionNotTerminatedAfter(ctx, model.SessionID, model.UserID, model.Position, model.UserAgent.GetFingerprintID()); err != nil {
return nil, err
}
return model, nil
@@ -142,13 +140,13 @@ func (q *Queries) accessTokenByOIDCSessionAndTokenID(ctx context.Context, oidcSe
// checkSessionNotTerminatedAfter checks if a [session.TerminateType] event (or user events leading to a session termination)
// occurred after a certain time and will return an error if so.
-func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, userID string, creation time.Time, fingerprintID string) (err error) {
+func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID, userID string, position float64, fingerprintID string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
model := &sessionTerminatedModel{
sessionID: sessionID,
- creation: creation,
+ position: position,
userID: userID,
fingerPrintID: fingerprintID,
}
@@ -164,7 +162,7 @@ func (q *Queries) checkSessionNotTerminatedAfter(ctx context.Context, sessionID,
}
type sessionTerminatedModel struct {
- creation time.Time
+ position float64
sessionID string
userID string
fingerPrintID string
@@ -184,8 +182,7 @@ func (s *sessionTerminatedModel) AppendEvents(events ...eventstore.Event) {
func (s *sessionTerminatedModel) Query() *eventstore.SearchQueryBuilder {
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
- AwaitOpenTransactions().
- CreationDateAfter(s.creation).
+ PositionAfter(s.position).
AddQuery().
AggregateTypes(session.AggregateType).
AggregateIDs(s.sessionID).
diff --git a/internal/query/instance_features.go b/internal/query/instance_features.go
index a362b6a5ad..12d8e0d80d 100644
--- a/internal/query/instance_features.go
+++ b/internal/query/instance_features.go
@@ -4,6 +4,7 @@ import (
"context"
"github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/feature"
)
type InstanceFeatures struct {
@@ -14,6 +15,7 @@ type InstanceFeatures struct {
UserSchema FeatureSource[bool]
TokenExchange FeatureSource[bool]
Actions FeatureSource[bool]
+ ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType]
}
func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) {
diff --git a/internal/query/instance_features_model.go b/internal/query/instance_features_model.go
index 068e56a23c..215442c911 100644
--- a/internal/query/instance_features_model.go
+++ b/internal/query/instance_features_model.go
@@ -36,11 +36,14 @@ func (m *InstanceFeaturesReadModel) Reduce() (err error) {
case *feature_v2.ResetEvent:
m.reduceReset()
case *feature_v1.SetEvent[feature_v1.Boolean]:
- err = m.reduceBoolFeature(
+ err = reduceInstanceFeatureSet(
+ m.instance,
feature_v1.DefaultLoginInstanceEventToV2(e),
)
case *feature_v2.SetEvent[bool]:
- err = m.reduceBoolFeature(e)
+ err = reduceInstanceFeatureSet(m.instance, e)
+ case *feature_v2.SetEvent[[]feature.ImprovedPerformanceType]:
+ err = reduceInstanceFeatureSet(m.instance, e)
}
if err != nil {
return err
@@ -63,6 +66,7 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceUserSchemaEventType,
feature_v2.InstanceTokenExchangeEventType,
feature_v2.InstanceActionsEventType,
+ feature_v2.InstanceImprovedPerformanceEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -71,12 +75,8 @@ func (m *InstanceFeaturesReadModel) reduceReset() {
if m.populateFromSystem() {
return
}
- m.instance.LoginDefaultOrg = FeatureSource[bool]{}
- m.instance.TriggerIntrospectionProjections = FeatureSource[bool]{}
- m.instance.LegacyIntrospection = FeatureSource[bool]{}
- m.instance.UserSchema = FeatureSource[bool]{}
- m.instance.TokenExchange = FeatureSource[bool]{}
- m.instance.Actions = FeatureSource[bool]{}
+ m.instance = nil
+ m.instance = new(InstanceFeatures)
}
func (m *InstanceFeaturesReadModel) populateFromSystem() bool {
@@ -89,35 +89,32 @@ func (m *InstanceFeaturesReadModel) populateFromSystem() bool {
m.instance.UserSchema = m.system.UserSchema
m.instance.TokenExchange = m.system.TokenExchange
m.instance.Actions = m.system.Actions
+ m.instance.ImprovedPerformance = m.system.ImprovedPerformance
return true
}
-func (m *InstanceFeaturesReadModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
+func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_v2.SetEvent[T]) error {
level, key, err := event.FeatureInfo()
if err != nil {
return err
}
- var dst *FeatureSource[bool]
-
switch key {
case feature.KeyUnspecified:
return nil
case feature.KeyLoginDefaultOrg:
- dst = &m.instance.LoginDefaultOrg
+ features.LoginDefaultOrg.set(level, event.Value)
case feature.KeyTriggerIntrospectionProjections:
- dst = &m.instance.TriggerIntrospectionProjections
+ features.TriggerIntrospectionProjections.set(level, event.Value)
case feature.KeyLegacyIntrospection:
- dst = &m.instance.LegacyIntrospection
+ features.LegacyIntrospection.set(level, event.Value)
case feature.KeyUserSchema:
- dst = &m.instance.UserSchema
+ features.UserSchema.set(level, event.Value)
case feature.KeyTokenExchange:
- dst = &m.instance.TokenExchange
+ features.TokenExchange.set(level, event.Value)
case feature.KeyActions:
- dst = &m.instance.Actions
- }
- *dst = FeatureSource[bool]{
- Level: level,
- Value: event.Value,
+ features.Actions.set(level, event.Value)
+ case feature.KeyImprovedPerformance:
+ features.ImprovedPerformance.set(level, event.Value)
}
return nil
}
diff --git a/internal/query/org.go b/internal/query/org.go
index 18715d015e..27bdfada76 100644
--- a/internal/query/org.go
+++ b/internal/query/org.go
@@ -13,8 +13,11 @@ import (
"github.com/zitadel/zitadel/internal/api/call"
domain_pkg "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
+ "github.com/zitadel/zitadel/internal/feature"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
+ "github.com/zitadel/zitadel/internal/v2/eventstore"
+ "github.com/zitadel/zitadel/internal/v2/readmodel"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -92,7 +95,44 @@ func (q *OrgSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
return query
}
-func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string) (org *Org, err error) {
+func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string) (_ *Org, err error) {
+ ctx, span := tracing.NewSpan(ctx)
+ defer func() { span.EndWithError(err) }()
+
+ if !authz.GetInstance(ctx).Features().ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeOrgByID) {
+ return q.oldOrgByID(ctx, shouldTriggerBulk, id)
+ }
+
+ foundOrg := readmodel.NewOrg(id)
+ eventCount, err := q.eventStoreV4.Query(
+ ctx,
+ eventstore.NewQuery(
+ authz.GetInstance(ctx).InstanceID(),
+ foundOrg,
+ eventstore.AppendFilters(foundOrg.Filter()...),
+ ),
+ )
+ if err != nil {
+ return nil, zerrors.ThrowInternal(err, "QUERY-AWx52", "Errors.Query.SQLStatement")
+ }
+
+ if eventCount == 0 {
+ return nil, zerrors.ThrowNotFound(nil, "QUERY-leq5Q", "Errors.Org.NotFound")
+ }
+
+ return &Org{
+ ID: foundOrg.ID,
+ CreationDate: foundOrg.CreationDate,
+ ChangeDate: foundOrg.ChangeDate,
+ ResourceOwner: foundOrg.Owner,
+ State: domain_pkg.OrgState(foundOrg.State.State),
+ Sequence: uint64(foundOrg.Sequence),
+ Name: foundOrg.Name,
+ Domain: foundOrg.PrimaryDomain.Domain,
+ }, nil
+}
+
+func (q *Queries) oldOrgByID(ctx context.Context, shouldTriggerBulk bool, id string) (org *Org, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
diff --git a/internal/query/projection/instance_features.go b/internal/query/projection/instance_features.go
index ebb388da89..06090a2f5d 100644
--- a/internal/query/projection/instance_features.go
+++ b/internal/query/projection/instance_features.go
@@ -6,6 +6,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
+ "github.com/zitadel/zitadel/internal/feature"
feature_v1 "github.com/zitadel/zitadel/internal/repository/feature"
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
"github.com/zitadel/zitadel/internal/repository/instance"
@@ -83,6 +84,10 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.InstanceActionsEventType,
Reduce: reduceInstanceSetFeature[bool],
},
+ {
+ Event: feature_v2.InstanceImprovedPerformanceEventType,
+ Reduce: reduceInstanceSetFeature[[]feature.ImprovedPerformanceType],
+ },
{
Event: instance.InstanceRemovedEventType,
Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),
diff --git a/internal/query/projection/milestones.go b/internal/query/projection/milestones.go
index 0ac5e81842..7c344001b3 100644
--- a/internal/query/projection/milestones.go
+++ b/internal/query/projection/milestones.go
@@ -10,6 +10,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/milestone"
+ "github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/user"
)
@@ -104,6 +105,15 @@ func (p *milestoneProjection) Reducers() []handler.AggregateReducer {
},
},
},
+ {
+ Aggregate: oidcsession.AggregateType,
+ EventReducers: []handler.EventReducer{
+ {
+ Event: oidcsession.AddedType,
+ Reduce: p.reduceOIDCSessionAdded,
+ },
+ },
+ },
{
Aggregate: milestone.AggregateType,
EventReducers: []handler.EventReducer{
@@ -217,6 +227,40 @@ func (p *milestoneProjection) reduceUserTokenAdded(event eventstore.Event) (*han
return handler.NewMultiStatement(e, statements...), nil
}
+func (p *milestoneProjection) reduceOIDCSessionAdded(event eventstore.Event) (*handler.Statement, error) {
+ e, err := assertEvent[*oidcsession.AddedEvent](event)
+ if err != nil {
+ return nil, err
+ }
+ statements := []func(eventstore.Event) handler.Exec{
+ handler.AddUpdateStatement(
+ []handler.Column{
+ handler.NewCol(MilestoneColumnReachedDate, event.CreatedAt()),
+ },
+ []handler.Condition{
+ handler.NewCond(MilestoneColumnInstanceID, event.Aggregate().InstanceID),
+ handler.NewCond(MilestoneColumnType, milestone.AuthenticationSucceededOnInstance),
+ handler.NewIsNullCond(MilestoneColumnReachedDate),
+ },
+ ),
+ }
+ // We ignore authentications without app, for example JWT profile or PAT
+ if e.ClientID != "" {
+ statements = append(statements, handler.AddUpdateStatement(
+ []handler.Column{
+ handler.NewCol(MilestoneColumnReachedDate, event.CreatedAt()),
+ },
+ []handler.Condition{
+ handler.NewCond(MilestoneColumnInstanceID, event.Aggregate().InstanceID),
+ handler.NewCond(MilestoneColumnType, milestone.AuthenticationSucceededOnApplication),
+ handler.Not(handler.NewTextArrayContainsCond(MilestoneColumnIgnoreClientIDs, e.ClientID)),
+ handler.NewIsNullCond(MilestoneColumnReachedDate),
+ },
+ ))
+ }
+ return handler.NewMultiStatement(e, statements...), nil
+}
+
func (p *milestoneProjection) reduceInstanceRemoved(event eventstore.Event) (*handler.Statement, error) {
if _, err := assertEvent[*instance.InstanceRemovedEvent](event); err != nil {
return nil, err
diff --git a/internal/query/projection/milestones_test.go b/internal/query/projection/milestones_test.go
index 884c7e27de..f9eb4d40d1 100644
--- a/internal/query/projection/milestones_test.go
+++ b/internal/query/projection/milestones_test.go
@@ -9,6 +9,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/milestone"
+ "github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -294,6 +295,43 @@ func TestMilestonesProjection_reduces(t *testing.T) {
},
},
},
+ {
+ name: "reduceOIDCSessionAdded",
+ args: args{
+ event: getEvent(timedTestEvent(
+ oidcsession.AddedType,
+ oidcsession.AggregateType,
+ []byte(`{"clientID": "client-id"}`),
+ now,
+ ), eventstore.GenericEventMapper[oidcsession.AddedEvent]),
+ },
+ reduce: (&milestoneProjection{}).reduceOIDCSessionAdded,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("oidc_session"),
+ sequence: 15,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "UPDATE projections.milestones SET reached_date = $1 WHERE (instance_id = $2) AND (type = $3) AND (reached_date IS NULL)",
+ expectedArgs: []interface{}{
+ now,
+ "instance-id",
+ milestone.AuthenticationSucceededOnInstance,
+ },
+ },
+ {
+ expectedStmt: "UPDATE projections.milestones SET reached_date = $1 WHERE (instance_id = $2) AND (type = $3) AND (NOT (ignore_client_ids @> $4)) AND (reached_date IS NULL)",
+ expectedArgs: []interface{}{
+ now,
+ "instance-id",
+ milestone.AuthenticationSucceededOnApplication,
+ database.TextArray[string]{"client-id"},
+ },
+ },
+ },
+ },
+ },
+ },
{
name: "reduceInstanceRemoved",
args: args{
diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go
index de7b6135bd..30c1df6870 100644
--- a/internal/query/projection/projection.go
+++ b/internal/query/projection/projection.go
@@ -181,6 +181,16 @@ func Start(ctx context.Context) {
}
}
+func ProjectInstance(ctx context.Context) error {
+ for _, projection := range projections {
+ _, err := projection.Trigger(ctx)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
func ApplyCustomConfig(customConfig CustomConfig) handler.Config {
return applyCustomConfig(projectionConfig, customConfig)
}
diff --git a/internal/query/projection/system_features.go b/internal/query/projection/system_features.go
index 94c6282c49..158da7a616 100644
--- a/internal/query/projection/system_features.go
+++ b/internal/query/projection/system_features.go
@@ -6,6 +6,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
+ "github.com/zitadel/zitadel/internal/feature"
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -75,6 +76,10 @@ func (*systemFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.SystemActionsEventType,
Reduce: reduceSystemSetFeature[bool],
},
+ {
+ Event: feature_v2.SystemImprovedPerformanceEventType,
+ Reduce: reduceSystemSetFeature[[]feature.ImprovedPerformanceType],
+ },
},
}}
}
diff --git a/internal/query/query.go b/internal/query/query.go
index 6bae5f924e..c2fbcb00a3 100644
--- a/internal/query/query.go
+++ b/internal/query/query.go
@@ -19,11 +19,13 @@ import (
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
+ es_v4 "github.com/zitadel/zitadel/internal/v2/eventstore"
)
type Queries struct {
- eventstore *eventstore.Eventstore
- client *database.DB
+ eventstore *eventstore.Eventstore
+ eventStoreV4 es_v4.Querier
+ client *database.DB
keyEncryptionAlgorithm crypto.EncryptionAlgorithm
idpConfigEncryption crypto.EncryptionAlgorithm
@@ -43,6 +45,7 @@ type Queries struct {
func StartQueries(
ctx context.Context,
es *eventstore.Eventstore,
+ esV4 es_v4.Querier,
querySqlClient, projectionSqlClient *database.DB,
projections projection.Config,
defaults sd.SystemDefaults,
@@ -56,6 +59,7 @@ func StartQueries(
) (repo *Queries, err error) {
repo = &Queries{
eventstore: es,
+ eventStoreV4: esV4,
client: querySqlClient,
DefaultLanguage: language.Und,
LoginTranslationFileContents: make(map[string][]byte),
diff --git a/internal/query/system_features.go b/internal/query/system_features.go
index 65087e92dc..33e5f14b3a 100644
--- a/internal/query/system_features.go
+++ b/internal/query/system_features.go
@@ -12,6 +12,11 @@ type FeatureSource[T any] struct {
Value T
}
+func (f *FeatureSource[T]) set(level feature.Level, value any) {
+ f.Level = level
+ f.Value = value.(T)
+}
+
type SystemFeatures struct {
Details *domain.ObjectDetails
@@ -21,6 +26,7 @@ type SystemFeatures struct {
UserSchema FeatureSource[bool]
TokenExchange FeatureSource[bool]
Actions FeatureSource[bool]
+ ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType]
}
func (q *Queries) GetSystemFeatures(ctx context.Context) (_ *SystemFeatures, err error) {
diff --git a/internal/query/system_features_model.go b/internal/query/system_features_model.go
index 243dc4e6c4..9b185c3e8f 100644
--- a/internal/query/system_features_model.go
+++ b/internal/query/system_features_model.go
@@ -28,7 +28,12 @@ func (m *SystemFeaturesReadModel) Reduce() error {
case *feature_v2.ResetEvent:
m.reduceReset()
case *feature_v2.SetEvent[bool]:
- err := m.reduceBoolFeature(e)
+ err := reduceSystemFeatureSet(m.system, e)
+ if err != nil {
+ return err
+ }
+ case *feature_v2.SetEvent[[]feature.ImprovedPerformanceType]:
+ err := reduceSystemFeatureSet(m.system, e)
if err != nil {
return err
}
@@ -51,41 +56,38 @@ func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.SystemUserSchemaEventType,
feature_v2.SystemTokenExchangeEventType,
feature_v2.SystemActionsEventType,
+ feature_v2.SystemImprovedPerformanceEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
func (m *SystemFeaturesReadModel) reduceReset() {
+ m.system = nil
m.system = new(SystemFeatures)
}
-func (m *SystemFeaturesReadModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
+func reduceSystemFeatureSet[T any](features *SystemFeatures, event *feature_v2.SetEvent[T]) error {
level, key, err := event.FeatureInfo()
if err != nil {
return err
}
- var dst *FeatureSource[bool]
-
switch key {
case feature.KeyUnspecified:
return nil
case feature.KeyLoginDefaultOrg:
- dst = &m.system.LoginDefaultOrg
+ features.LoginDefaultOrg.set(level, event.Value)
case feature.KeyTriggerIntrospectionProjections:
- dst = &m.system.TriggerIntrospectionProjections
+ features.TriggerIntrospectionProjections.set(level, event.Value)
case feature.KeyLegacyIntrospection:
- dst = &m.system.LegacyIntrospection
+ features.LegacyIntrospection.set(level, event.Value)
case feature.KeyUserSchema:
- dst = &m.system.UserSchema
+ features.UserSchema.set(level, event.Value)
case feature.KeyTokenExchange:
- dst = &m.system.TokenExchange
+ features.TokenExchange.set(level, event.Value)
case feature.KeyActions:
- dst = &m.system.Actions
- }
-
- *dst = FeatureSource[bool]{
- Level: level,
- Value: event.Value,
+ features.Actions.set(level, event.Value)
+ case feature.KeyImprovedPerformance:
+ features.ImprovedPerformance.set(level, event.Value)
}
return nil
}
diff --git a/internal/query/targets_by_execution_id.sql b/internal/query/targets_by_execution_id.sql
index 6b564104e5..f8248479b0 100644
--- a/internal/query/targets_by_execution_id.sql
+++ b/internal/query/targets_by_execution_id.sql
@@ -1,4 +1,18 @@
WITH RECURSIVE
+ matched AS (SELECT *
+ FROM projections.executions1
+ WHERE instance_id = $1
+ AND id = ANY($2)
+ ORDER BY id DESC
+ LIMIT 1),
+ matched_targets_and_includes AS (SELECT pos.*
+ FROM matched m
+ JOIN
+ projections.executions1_targets pos
+ ON m.id = pos.execution_id
+ AND m.instance_id = pos.instance_id
+ ORDER BY execution_id,
+ position),
dissolved_execution_targets(execution_id, instance_id, position, "include", "target_id")
AS (SELECT execution_id
, instance_id
@@ -16,21 +30,7 @@ WITH RECURSIVE
JOIN projections.executions1_targets p
ON e.instance_id = p.instance_id
AND e.include IS NOT NULL
- AND e.include = p.execution_id),
- matched AS (SELECT *
- FROM projections.executions1
- WHERE instance_id = $1
- AND id = ANY($2)
- ORDER BY id DESC
- LIMIT 1),
- matched_targets_and_includes AS (SELECT pos.*
- FROM matched m
- JOIN
- projections.executions1_targets pos
- ON m.id = pos.execution_id
- AND m.instance_id = pos.instance_id
- ORDER BY execution_id,
- position)
+ AND e.include = p.execution_id)
select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error
FROM dissolved_execution_targets e
JOIN projections.targets1 t
diff --git a/internal/query/targets_by_execution_ids.sql b/internal/query/targets_by_execution_ids.sql
index c1361d9320..749d9387b2 100644
--- a/internal/query/targets_by_execution_ids.sql
+++ b/internal/query/targets_by_execution_ids.sql
@@ -1,22 +1,4 @@
WITH RECURSIVE
- dissolved_execution_targets(execution_id, instance_id, position, "include", "target_id")
- AS (SELECT execution_id
- , instance_id
- , ARRAY [position]
- , "include"
- , "target_id"
- FROM matched_targets_and_includes
- UNION ALL
- SELECT e.execution_id
- , p.instance_id
- , e.position || p.position
- , p."include"
- , p."target_id"
- FROM dissolved_execution_targets e
- JOIN projections.executions1_targets p
- ON e.instance_id = p.instance_id
- AND e.include IS NOT NULL
- AND e.include = p.execution_id),
matched AS ((SELECT *
FROM projections.executions1
WHERE instance_id = $1
@@ -37,7 +19,25 @@ WITH RECURSIVE
ON m.id = pos.execution_id
AND m.instance_id = pos.instance_id
ORDER BY execution_id,
- position)
+ position),
+ dissolved_execution_targets(execution_id, instance_id, position, "include", "target_id")
+ AS (SELECT execution_id
+ , instance_id
+ , ARRAY [position]
+ , "include"
+ , "target_id"
+ FROM matched_targets_and_includes
+ UNION ALL
+ SELECT e.execution_id
+ , p.instance_id
+ , e.position || p.position
+ , p."include"
+ , p."target_id"
+ FROM dissolved_execution_targets e
+ JOIN projections.executions1_targets p
+ ON e.instance_id = p.instance_id
+ AND e.include IS NOT NULL
+ AND e.include = p.execution_id)
select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error
FROM dissolved_execution_targets e
JOIN projections.targets1 t
diff --git a/internal/query/user_auth_method.go b/internal/query/user_auth_method.go
index 3c24948d87..f1e2721bab 100644
--- a/internal/query/user_auth_method.go
+++ b/internal/query/user_auth_method.go
@@ -3,6 +3,7 @@ package query
import (
"context"
"database/sql"
+ "errors"
"time"
sq "github.com/Masterminds/squirrel"
@@ -69,8 +70,12 @@ var (
authMethodTypeTable = userAuthMethodTable.setAlias("auth_method_types")
authMethodTypeUserID = UserAuthMethodColumnUserID.setTable(authMethodTypeTable)
authMethodTypeInstanceID = UserAuthMethodColumnInstanceID.setTable(authMethodTypeTable)
- authMethodTypeTypes = UserAuthMethodColumnMethodType.setTable(authMethodTypeTable)
- authMethodTypeState = UserAuthMethodColumnState.setTable(authMethodTypeTable)
+ authMethodTypeType = UserAuthMethodColumnMethodType.setTable(authMethodTypeTable)
+ authMethodTypeTypes = Column{
+ name: "method_types",
+ table: authMethodTypeTable,
+ }
+ authMethodTypeState = UserAuthMethodColumnState.setTable(authMethodTypeTable)
userIDPsCountTable = idpUserLinkTable.setAlias("user_idps_count")
userIDPsCountUserID = IDPUserLinkUserIDCol.setTable(userIDPsCountTable)
@@ -174,7 +179,6 @@ func (q *Queries) ListActiveUserAuthMethodTypes(ctx context.Context, userID stri
type UserAuthMethodRequirements struct {
UserType domain.UserType
- AuthMethods []domain.UserAuthMethodType
ForceMFA bool
ForceMFALocalOnly bool
}
@@ -199,8 +203,8 @@ func (q *Queries) ListUserAuthMethodTypesRequired(ctx context.Context, userID st
return nil, zerrors.ThrowInvalidArgument(err, "QUERY-E5ut4", "Errors.Query.InvalidRequest")
}
- err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
- requirements, err = scan(rows)
+ err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
+ requirements, err = scan(row)
return err
}, stmt, args...)
if err != nil {
@@ -360,7 +364,7 @@ func prepareActiveUserAuthMethodTypesQuery(ctx context.Context, db prepareDataba
}
return sq.Select(
NotifyPasswordSetCol.identifier(),
- authMethodTypeTypes.identifier(),
+ authMethodTypeType.identifier(),
userIDPsCountCount.identifier()).
From(userTable.identifier()).
LeftJoin(join(NotifyUserIDCol, UserIDCol)).
@@ -411,78 +415,39 @@ func prepareActiveUserAuthMethodTypesQuery(ctx context.Context, db prepareDataba
}
}
-func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserAuthMethodRequirements, error)) {
+func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) {
loginPolicyQuery, err := prepareAuthMethodsForceMFAQuery()
if err != nil {
return sq.SelectBuilder{}, nil
}
- authMethodsQuery, authMethodsArgs, err := prepareAuthMethodQuery()
- if err != nil {
- return sq.SelectBuilder{}, nil
- }
- idpsQuery, err := prepareAuthMethodsIDPsQuery()
- if err != nil {
- return sq.SelectBuilder{}, nil
- }
return sq.Select(
- NotifyPasswordSetCol.identifier(),
- authMethodTypeTypes.identifier(),
- userIDPsCountCount.identifier(),
UserTypeCol.identifier(),
forceMFAForce.identifier(),
forceMFAForceLocalOnly.identifier()).
From(userTable.identifier()).
- LeftJoin(join(NotifyUserIDCol, UserIDCol)).
- LeftJoin("("+authMethodsQuery+") AS "+authMethodTypeTable.alias+" ON "+
- authMethodTypeUserID.identifier()+" = "+UserIDCol.identifier()+" AND "+
- authMethodTypeInstanceID.identifier()+" = "+UserInstanceIDCol.identifier(),
- authMethodsArgs...).
- LeftJoin("(" + idpsQuery + ") AS " + userIDPsCountTable.alias + " ON " +
- userIDPsCountUserID.identifier() + " = " + UserIDCol.identifier() + " AND " +
- userIDPsCountInstanceID.identifier() + " = " + UserInstanceIDCol.identifier()).
LeftJoin("(" + loginPolicyQuery + ") AS " + forceMFATable.alias + " ON " +
"(" + forceMFAOrgID.identifier() + " = " + UserInstanceIDCol.identifier() + " OR " + forceMFAOrgID.identifier() + " = " + UserResourceOwnerCol.identifier() + ") AND " +
- forceMFAInstanceID.identifier() + " = " + UserInstanceIDCol.identifier() + db.Timetravel(call.Took(ctx))).
+ forceMFAInstanceID.identifier() + " = " + UserInstanceIDCol.identifier()).
+ OrderBy(forceMFAIsDefault.identifier()).
+ Limit(1).
PlaceholderFormat(sq.Dollar),
- func(rows *sql.Rows) (*UserAuthMethodRequirements, error) {
- userAuthMethodTypes := make([]domain.UserAuthMethodType, 0)
- var passwordSet sql.NullBool
- var idp sql.NullInt64
+ func(row *sql.Row) (*UserAuthMethodRequirements, error) {
var userType sql.NullInt32
var forceMFA sql.NullBool
var forceMFALocalOnly sql.NullBool
- for rows.Next() {
- var authMethodType sql.NullInt16
- err := rows.Scan(
- &passwordSet,
- &authMethodType,
- &idp,
- &userType,
- &forceMFA,
- &forceMFALocalOnly,
- )
- if err != nil {
- return nil, err
- }
- if authMethodType.Valid {
- userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodType(authMethodType.Int16))
+ err := row.Scan(
+ &userType,
+ &forceMFA,
+ &forceMFALocalOnly,
+ )
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, zerrors.ThrowNotFound(err, "QUERY-SF3h2", "Errors.Internal")
}
+ return nil, zerrors.ThrowInternal(err, "QUERY-Sf3rt", "Errors.Internal")
}
- if passwordSet.Valid && passwordSet.Bool {
- userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodTypePassword)
- }
- if idp.Valid && idp.Int64 > 0 {
- logging.Error("IDP", idp.Int64)
- userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodTypeIDP)
- }
-
- if err := rows.Close(); err != nil {
- return nil, zerrors.ThrowInternal(err, "QUERY-W4zje", "Errors.Query.CloseRows")
- }
-
return &UserAuthMethodRequirements{
UserType: domain.UserType(userType.Int32),
- AuthMethods: userAuthMethodTypes,
ForceMFA: forceMFA.Bool,
ForceMFALocalOnly: forceMFALocalOnly.Bool,
}, nil
@@ -505,7 +470,7 @@ func prepareAuthMethodsIDPsQuery() (string, error) {
func prepareAuthMethodQuery() (string, []interface{}, error) {
return sq.Select(
- "DISTINCT("+authMethodTypeTypes.identifier()+")",
+ "DISTINCT("+authMethodTypeType.identifier()+")",
authMethodTypeUserID.identifier(),
authMethodTypeInstanceID.identifier()).
From(authMethodTypeTable.identifier()).
@@ -519,9 +484,9 @@ func prepareAuthMethodsForceMFAQuery() (string, error) {
forceMFAForceLocalOnly.identifier(),
forceMFAInstanceID.identifier(),
forceMFAOrgID.identifier(),
+ forceMFAIsDefault.identifier(),
).
From(forceMFATable.identifier()).
- OrderBy(forceMFAIsDefault.identifier()).
ToSql()
return loginPolicyQuery, err
}
diff --git a/internal/query/user_auth_method_test.go b/internal/query/user_auth_method_test.go
index 578b14cec5..b75e6fd461 100644
--- a/internal/query/user_auth_method_test.go
+++ b/internal/query/user_auth_method_test.go
@@ -12,6 +12,7 @@ import (
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/zerrors"
)
var (
@@ -56,29 +57,16 @@ var (
"method_type",
"idps_count",
}
- prepareAuthMethodTypesRequiredStmt = `SELECT projections.users12_notifications.password_set,` +
- ` auth_method_types.method_type,` +
- ` user_idps_count.count,` +
- ` projections.users12.type,` +
+ prepareAuthMethodTypesRequiredStmt = `SELECT projections.users12.type,` +
` auth_methods_force_mfa.force_mfa,` +
` auth_methods_force_mfa.force_mfa_local_only` +
` FROM projections.users12` +
- ` LEFT JOIN projections.users12_notifications ON projections.users12.id = projections.users12_notifications.user_id AND projections.users12.instance_id = projections.users12_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.users12.id AND auth_method_types.instance_id = projections.users12.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.users12.id AND user_idps_count.instance_id = projections.users12.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` +
+ ` 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, auth_methods_force_mfa.is_default FROM projections.login_policies5 AS auth_methods_force_mfa) AS auth_methods_force_mfa` +
` ON (auth_methods_force_mfa.aggregate_id = projections.users12.instance_id OR auth_methods_force_mfa.aggregate_id = projections.users12.resource_owner) AND auth_methods_force_mfa.instance_id = projections.users12.instance_id` +
- ` AS OF SYSTEM TIME '-1 ms
+ ` ORDER BY auth_methods_force_mfa.is_default LIMIT 1
`
prepareAuthMethodTypesRequiredCols = []string{
- "password_set",
"type",
- "method_type",
- "idps_count",
"force_mfa",
"force_mfa_local_only",
}
@@ -319,27 +307,33 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
},
{
name: "prepareUserAuthMethodTypesRequiredQuery no result",
- prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserAuthMethodRequirements, error)) {
+ prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) {
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
- return builder, func(rows *sql.Rows) (*UserAuthMethodRequirements, error) {
- return scan(rows)
+ return builder, func(row *sql.Row) (*UserAuthMethodRequirements, error) {
+ return scan(row)
}
},
want: want{
- sqlExpectations: mockQueries(
+ sqlExpectations: mockQueriesScanErr(
regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
nil,
nil,
),
+ err: func(err error) (error, bool) {
+ if !zerrors.IsNotFound(err) {
+ return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
+ }
+ return nil, true
+ },
},
- object: &UserAuthMethodRequirements{AuthMethods: []domain.UserAuthMethodType{}, ForceMFA: false},
+ object: (*UserAuthMethodRequirements)(nil),
},
{
name: "prepareUserAuthMethodTypesRequiredQuery one second factor",
- prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserAuthMethodRequirements, error)) {
+ prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) {
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
- return builder, func(rows *sql.Rows) (*UserAuthMethodRequirements, error) {
- return scan(rows)
+ return builder, func(row *sql.Row) (*UserAuthMethodRequirements, error) {
+ return scan(row)
}
},
want: want{
@@ -348,9 +342,6 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
prepareAuthMethodTypesRequiredCols,
[][]driver.Value{
{
- true,
- domain.UserAuthMethodTypePasswordless,
- 1,
domain.UserTypeHuman,
true,
true,
@@ -359,22 +350,17 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
),
},
object: &UserAuthMethodRequirements{
- UserType: domain.UserTypeHuman,
- AuthMethods: []domain.UserAuthMethodType{
- domain.UserAuthMethodTypePasswordless,
- domain.UserAuthMethodTypePassword,
- domain.UserAuthMethodTypeIDP,
- },
+ UserType: domain.UserTypeHuman,
ForceMFA: true,
ForceMFALocalOnly: true,
},
},
{
name: "prepareUserAuthMethodTypesRequiredQuery multiple second factors",
- prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserAuthMethodRequirements, error)) {
+ prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) {
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
- return builder, func(rows *sql.Rows) (*UserAuthMethodRequirements, error) {
- return scan(rows)
+ return builder, func(row *sql.Row) (*UserAuthMethodRequirements, error) {
+ return scan(row)
}
},
want: want{
@@ -383,17 +369,6 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
prepareAuthMethodTypesRequiredCols,
[][]driver.Value{
{
- true,
- domain.UserAuthMethodTypePasswordless,
- 1,
- domain.UserTypeHuman,
- true,
- true,
- },
- {
- true,
- domain.UserAuthMethodTypeTOTP,
- 1,
domain.UserTypeHuman,
true,
true,
@@ -403,23 +378,17 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
},
object: &UserAuthMethodRequirements{
- UserType: domain.UserTypeHuman,
- AuthMethods: []domain.UserAuthMethodType{
- domain.UserAuthMethodTypePasswordless,
- domain.UserAuthMethodTypeTOTP,
- domain.UserAuthMethodTypePassword,
- domain.UserAuthMethodTypeIDP,
- },
+ UserType: domain.UserTypeHuman,
ForceMFA: true,
ForceMFALocalOnly: true,
},
},
{
name: "prepareUserAuthMethodTypesRequiredQuery sql err",
- prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserAuthMethodRequirements, error)) {
+ prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*UserAuthMethodRequirements, error)) {
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
- return builder, func(rows *sql.Rows) (*UserAuthMethodRequirements, error) {
- return scan(rows)
+ return builder, func(row *sql.Row) (*UserAuthMethodRequirements, error) {
+ return scan(row)
}
},
want: want{
diff --git a/internal/repository/feature/feature_v2/eventstore.go b/internal/repository/feature/feature_v2/eventstore.go
index 26f33661fe..bd8b22eec8 100644
--- a/internal/repository/feature/feature_v2/eventstore.go
+++ b/internal/repository/feature/feature_v2/eventstore.go
@@ -2,6 +2,7 @@ package feature_v2
import (
"github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/feature"
)
func init() {
@@ -12,6 +13,8 @@ func init() {
eventstore.RegisterFilterEventMapper(AggregateType, SystemUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, SystemTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, SystemActionsEventType, eventstore.GenericEventMapper[SetEvent[bool]])
+ eventstore.RegisterFilterEventMapper(AggregateType, SystemImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]])
+
eventstore.RegisterFilterEventMapper(AggregateType, InstanceResetEventType, eventstore.GenericEventMapper[ResetEvent])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceLoginDefaultOrgEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceTriggerIntrospectionProjectionsEventType, eventstore.GenericEventMapper[SetEvent[bool]])
@@ -19,4 +22,5 @@ func init() {
eventstore.RegisterFilterEventMapper(AggregateType, InstanceUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceActionsEventType, eventstore.GenericEventMapper[SetEvent[bool]])
+ eventstore.RegisterFilterEventMapper(AggregateType, InstanceImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]])
}
diff --git a/internal/repository/feature/feature_v2/feature.go b/internal/repository/feature/feature_v2/feature.go
index c1f9add2d7..d5beea8bf4 100644
--- a/internal/repository/feature/feature_v2/feature.go
+++ b/internal/repository/feature/feature_v2/feature.go
@@ -18,6 +18,7 @@ var (
SystemUserSchemaEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyUserSchema)
SystemTokenExchangeEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyTokenExchange)
SystemActionsEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyActions)
+ SystemImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyImprovedPerformance)
InstanceResetEventType = resetEventTypeFromFeature(feature.LevelInstance)
InstanceLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginDefaultOrg)
@@ -26,6 +27,7 @@ var (
InstanceUserSchemaEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyUserSchema)
InstanceTokenExchangeEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTokenExchange)
InstanceActionsEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyActions)
+ InstanceImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyImprovedPerformance)
)
const (
diff --git a/internal/telemetry/tracing/otel/open_telemetry.go b/internal/telemetry/tracing/otel/open_telemetry.go
index 9bffe230e8..4a3f137859 100644
--- a/internal/telemetry/tracing/otel/open_telemetry.go
+++ b/internal/telemetry/tracing/otel/open_telemetry.go
@@ -23,11 +23,10 @@ func NewTracer(sampler sdk_trace.Sampler, exporter sdk_trace.SpanExporter) (*Tra
if err != nil {
return nil, err
}
- spanProcessor := sdk_trace.NewBatchSpanProcessor(exporter)
+
tp := sdk_trace.NewTracerProvider(
sdk_trace.WithSampler(sampler),
sdk_trace.WithBatcher(exporter),
- sdk_trace.WithSpanProcessor(spanProcessor),
sdk_trace.WithResource(resource),
)
diff --git a/internal/user/repository/view/model/user_session.go b/internal/user/repository/view/model/user_session.go
index 4208ea5b53..d761592551 100644
--- a/internal/user/repository/view/model/user_session.go
+++ b/internal/user/repository/view/model/user_session.go
@@ -35,12 +35,12 @@ const (
)
type UserSessionView struct {
- CreationDate time.Time `json:"-" gorm:"column:creation_date"`
- ChangeDate time.Time `json:"-" gorm:"column:change_date"`
- ResourceOwner string `json:"-" gorm:"column:resource_owner"`
- State int32 `json:"-" gorm:"column:state"`
- UserAgentID string `json:"userAgentID" gorm:"column:user_agent_id;primary_key"`
- UserID string `json:"userID" gorm:"column:user_id;primary_key"`
+ CreationDate time.Time `json:"-" gorm:"column:creation_date"`
+ ChangeDate time.Time `json:"-" gorm:"column:change_date"`
+ ResourceOwner string `json:"-" gorm:"column:resource_owner"`
+ State sql.Null[domain.UserSessionState] `json:"-" gorm:"column:state"`
+ UserAgentID string `json:"userAgentID" gorm:"column:user_agent_id;primary_key"`
+ UserID string `json:"userID" gorm:"column:user_id;primary_key"`
// As of https://github.com/zitadel/zitadel/pull/7199 the following 4 attributes
// are not projected in the user session handler anymore
// and are therefore annotated with a `gorm:"-"`.
@@ -79,7 +79,7 @@ func UserSessionToModel(userSession *UserSessionView) *model.UserSessionView {
ChangeDate: userSession.ChangeDate,
CreationDate: userSession.CreationDate,
ResourceOwner: userSession.ResourceOwner,
- State: domain.UserSessionState(userSession.State),
+ State: userSession.State.V,
UserAgentID: userSession.UserAgentID,
UserID: userSession.UserID,
UserName: userSession.UserName.String,
@@ -114,7 +114,7 @@ func (v *UserSessionView) AppendEvent(event eventstore.Event) error {
case user.UserV1PasswordCheckSucceededType,
user.HumanPasswordCheckSucceededType:
v.PasswordVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
- v.State = int32(domain.UserSessionStateActive)
+ v.State.V = domain.UserSessionStateActive
case user.UserIDPLoginCheckSucceededType:
data := new(es_model.AuthRequest)
err := data.SetData(event)
@@ -123,12 +123,12 @@ func (v *UserSessionView) AppendEvent(event eventstore.Event) error {
}
v.ExternalLoginVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
v.SelectedIDPConfigID = sql.NullString{String: data.SelectedIDPConfigID, Valid: true}
- v.State = int32(domain.UserSessionStateActive)
+ v.State.V = domain.UserSessionStateActive
case user.HumanPasswordlessTokenCheckSucceededType:
v.PasswordlessVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
v.MultiFactorVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
v.MultiFactorVerificationType = sql.NullInt32{Int32: int32(domain.MFATypeU2FUserVerification)}
- v.State = int32(domain.UserSessionStateActive)
+ v.State.V = domain.UserSessionStateActive
case user.HumanPasswordlessTokenCheckFailedType,
user.HumanPasswordlessTokenRemovedType:
v.PasswordlessVerification = sql.NullTime{Time: time.Time{}, Valid: true}
@@ -207,7 +207,7 @@ func (v *UserSessionView) AppendEvent(event eventstore.Event) error {
v.MultiFactorVerification = sql.NullTime{Time: time.Time{}, Valid: true}
v.MultiFactorVerificationType = sql.NullInt32{Int32: int32(domain.MFALevelNotSetUp)}
v.ExternalLoginVerification = sql.NullTime{Time: time.Time{}, Valid: true}
- v.State = int32(domain.UserSessionStateTerminated)
+ v.State.V = domain.UserSessionStateTerminated
case user.UserIDPLinkRemovedType, user.UserIDPLinkCascadeRemovedType:
v.ExternalLoginVerification = sql.NullTime{Time: time.Time{}, Valid: true}
v.SelectedIDPConfigID = sql.NullString{String: "", Valid: true}
@@ -218,7 +218,7 @@ func (v *UserSessionView) AppendEvent(event eventstore.Event) error {
func (v *UserSessionView) setSecondFactorVerification(verificationTime time.Time, mfaType domain.MFAType) {
v.SecondFactorVerification = sql.NullTime{Time: verificationTime, Valid: true}
v.SecondFactorVerificationType = sql.NullInt32{Int32: int32(mfaType)}
- v.State = int32(domain.UserSessionStateActive)
+ v.State.V = domain.UserSessionStateActive
}
func (v *UserSessionView) EventTypes() []eventstore.EventType {
diff --git a/internal/user/repository/view/model/user_session_test.go b/internal/user/repository/view/model/user_session_test.go
index 25acd489c7..3e832ae1fd 100644
--- a/internal/user/repository/view/model/user_session_test.go
+++ b/internal/user/repository/view/model/user_session_test.go
@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/crypto"
+ "github.com/zitadel/zitadel/internal/domain"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/user"
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
@@ -209,7 +210,7 @@ func TestAppendEvent(t *testing.T) {
ExternalLoginVerification: sql.NullTime{Time: time.Time{}, Valid: true},
PasswordlessVerification: sql.NullTime{Time: time.Time{}, Valid: true},
MultiFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true},
- State: 1,
+ State: sql.Null[domain.UserSessionState]{V: domain.UserSessionStateTerminated},
},
},
{
@@ -228,7 +229,7 @@ func TestAppendEvent(t *testing.T) {
ExternalLoginVerification: sql.NullTime{Time: time.Time{}, Valid: true},
PasswordlessVerification: sql.NullTime{Time: time.Time{}, Valid: true},
MultiFactorVerification: sql.NullTime{Time: time.Time{}, Valid: true},
- State: 1,
+ State: sql.Null[domain.UserSessionState]{V: domain.UserSessionStateTerminated},
},
},
}
diff --git a/internal/user/repository/view/user_by_id.sql b/internal/user/repository/view/user_by_id.sql
index fa85fc43f4..1e21e59486 100644
--- a/internal/user/repository/view/user_by_id.sql
+++ b/internal/user/repository/view/user_by_id.sql
@@ -33,7 +33,7 @@ SELECT
, (SELECT array_agg(ll.login_name) login_names FROM projections.login_names3 ll
WHERE u.instance_id = ll.instance_id AND u.id = ll.user_id
GROUP BY ll.user_id, ll.instance_id) AS login_names
- , l.login_name
+ , l.login_name as preferred_login_name
, h.first_name
, h.last_name
, h.nick_name
diff --git a/internal/v2/eventstore/postgres/event.go b/internal/v2/eventstore/postgres/event.go
index 18d86c5751..f531c47e9f 100644
--- a/internal/v2/eventstore/postgres/event.go
+++ b/internal/v2/eventstore/postgres/event.go
@@ -29,14 +29,14 @@ func intentToCommands(intent *intent) (commands []*command, err error) {
}
func marshalPayload(payload any) ([]byte, error) {
- if reflect.ValueOf(payload).IsZero() {
+ if payload == nil || reflect.ValueOf(payload).IsZero() {
return nil, nil
}
return json.Marshal(payload)
}
type command struct {
- eventstore.Command
+ *eventstore.Command
intent *intent
diff --git a/internal/v2/eventstore/postgres/push.go b/internal/v2/eventstore/postgres/push.go
index 269d22cdef..0f4c29316c 100644
--- a/internal/v2/eventstore/postgres/push.go
+++ b/internal/v2/eventstore/postgres/push.go
@@ -3,7 +3,9 @@ package postgres
import (
"context"
"database/sql"
+ "fmt"
+ "github.com/cockroachdb/cockroach-go/v2/crdb"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
@@ -28,40 +30,54 @@ func (s *Storage) Push(ctx context.Context, intent *eventstore.PushIntent) (err
}()
}
- // allows smaller wait times on query side for instances which are not actively writing
- if err := setAppName(ctx, tx, "es_pusher_"+intent.Instance()); err != nil {
- return err
- }
+ var retryCount uint32
+ return crdb.Execute(func() (err error) {
+ defer func() {
+ if err == nil {
+ return
+ }
+ if retryCount < s.config.MaxRetries {
+ retryCount++
+ return
+ }
+ logging.WithFields("retry_count", retryCount).WithError(err).Debug("max retry count reached")
+ err = zerrors.ThrowInternal(err, "POSTG-VJfJz", "Errors.Internal")
+ }()
+ // allows smaller wait times on query side for instances which are not actively writing
+ if err := setAppName(ctx, tx, "es_pusher_"+intent.Instance()); err != nil {
+ return err
+ }
- intents, err := lockAggregates(ctx, tx, intent)
- if err != nil {
- return err
- }
-
- if !checkSequences(intents) {
- return zerrors.ThrowInvalidArgument(nil, "POSTG-KOM6E", "Errors.Internal.Eventstore.SequenceNotMatched")
- }
-
- commands := make([]*command, 0, len(intents))
- for _, intent := range intents {
- additionalCommands, err := intentToCommands(intent)
+ intents, err := lockAggregates(ctx, tx, intent)
if err != nil {
return err
}
- commands = append(commands, additionalCommands...)
- }
- err = uniqueConstraints(ctx, tx, commands)
- if err != nil {
- return err
- }
+ if !checkSequences(intents) {
+ return zerrors.ThrowInvalidArgument(nil, "POSTG-KOM6E", "Errors.Internal.Eventstore.SequenceNotMatched")
+ }
- return push(ctx, tx, intent, commands)
+ commands := make([]*command, 0, len(intents))
+ for _, intent := range intents {
+ additionalCommands, err := intentToCommands(intent)
+ if err != nil {
+ return err
+ }
+ commands = append(commands, additionalCommands...)
+ }
+
+ err = uniqueConstraints(ctx, tx, commands)
+ if err != nil {
+ return err
+ }
+
+ return push(ctx, tx, intent, commands)
+ })
}
// setAppName for the the current transaction
func setAppName(ctx context.Context, tx *sql.Tx, name string) error {
- _, err := tx.ExecContext(ctx, "SET LOCAL application_name TO $1", name)
+ _, err := tx.ExecContext(ctx, fmt.Sprintf("SET LOCAL application_name TO '%s'", name))
if err != nil {
logging.WithFields("name", name).WithError(err).Debug("setting app name failed")
return zerrors.ThrowInternal(err, "POSTG-G3OmZ", "Errors.Internal")
@@ -154,7 +170,8 @@ func push(ctx context.Context, tx *sql.Tx, reducer eventstore.Reducer, commands
cmd.sequence,
cmd.position.InPositionOrder,
)
- stmt.WriteString(", statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp())")
+
+ stmt.WriteString(pushPositionStmt)
stmt.WriteString(`)`)
}
stmt.WriteString(` RETURNING created_at, "position"`)
diff --git a/internal/v2/eventstore/postgres/push_test.go b/internal/v2/eventstore/postgres/push_test.go
index b81b3a1517..641b36680d 100644
--- a/internal/v2/eventstore/postgres/push_test.go
+++ b/internal/v2/eventstore/postgres/push_test.go
@@ -36,7 +36,9 @@ func Test_uniqueConstraints(t *testing.T) {
name: "command without constraints",
args: args{
commands: []*command{
- {},
+ {
+ Command: &eventstore.Command{},
+ },
},
expectations: []mock.Expectation{},
},
@@ -53,7 +55,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewAddEventUniqueConstraint("test", "id", "error"),
},
@@ -81,7 +83,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewAddGlobalUniqueConstraint("test", "id", "error"),
},
@@ -109,7 +111,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewAddEventUniqueConstraint("test", "id", "error"),
eventstore.NewAddEventUniqueConstraint("test", "id2", "error"),
@@ -143,7 +145,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewAddEventUniqueConstraint("test", "id", "error"),
},
@@ -156,7 +158,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewAddEventUniqueConstraint("test", "id2", "error"),
},
@@ -189,7 +191,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewRemoveInstanceUniqueConstraints(),
},
@@ -217,7 +219,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewRemoveInstanceUniqueConstraints(),
},
@@ -230,7 +232,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewRemoveInstanceUniqueConstraints(),
},
@@ -263,7 +265,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewRemoveUniqueConstraint("test", "id"),
},
@@ -291,7 +293,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewRemoveGlobalUniqueConstraint("test", "id"),
},
@@ -319,7 +321,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewRemoveUniqueConstraint("test", "id"),
eventstore.NewRemoveUniqueConstraint("test", "id2"),
@@ -353,7 +355,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewRemoveUniqueConstraint("test", "id"),
},
@@ -366,7 +368,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewRemoveUniqueConstraint("test", "id2"),
},
@@ -399,7 +401,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewAddEventUniqueConstraint("test", "id", ""),
},
@@ -433,7 +435,7 @@ func Test_uniqueConstraints(t *testing.T) {
eventstore.AppendAggregate("", "", ""),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
UniqueConstraints: []*eventstore.UniqueConstraint{
eventstore.NewAddEventUniqueConstraint("test", "id", "My.Error"),
},
@@ -786,7 +788,7 @@ func Test_push(t *testing.T) {
eventstore.AppendAggregate("owner", "testType", "testID"),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
Action: eventstore.Action[any]{
Creator: "gigi",
Revision: 1,
@@ -841,7 +843,7 @@ func Test_push(t *testing.T) {
eventstore.AppendAggregate("owner", "testType", "testID"),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
Action: eventstore.Action[any]{
Creator: "gigi",
Revision: 1,
@@ -857,7 +859,7 @@ func Test_push(t *testing.T) {
eventstore.AppendAggregate("owner", "testType", "testID"),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
Action: eventstore.Action[any]{
Creator: "gigi",
Revision: 1,
@@ -926,7 +928,7 @@ func Test_push(t *testing.T) {
eventstore.AppendAggregate("owner", "testType", "testID"),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
Action: eventstore.Action[any]{
Creator: "gigi",
Revision: 1,
@@ -942,7 +944,7 @@ func Test_push(t *testing.T) {
eventstore.AppendAggregate("owner", "type2", "id2"),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
Action: eventstore.Action[any]{
Creator: "gigi",
Revision: 1,
@@ -1011,7 +1013,7 @@ func Test_push(t *testing.T) {
eventstore.AppendAggregate("owner", "testType", "testID"),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
Action: eventstore.Action[any]{
Creator: "gigi",
Revision: 1,
@@ -1067,7 +1069,7 @@ func Test_push(t *testing.T) {
eventstore.AppendAggregate("owner", "testType", "testID"),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
Action: eventstore.Action[any]{
Creator: "gigi",
Revision: 1,
@@ -1123,7 +1125,7 @@ func Test_push(t *testing.T) {
eventstore.AppendAggregate("owner", "testType", "testID"),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
Action: eventstore.Action[any]{
Creator: "gigi",
Revision: 1,
@@ -1139,7 +1141,7 @@ func Test_push(t *testing.T) {
eventstore.AppendAggregate("owner", "testType", "testID"),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
Action: eventstore.Action[any]{
Creator: "gigi",
Revision: 1,
@@ -1214,7 +1216,7 @@ func Test_push(t *testing.T) {
eventstore.AppendAggregate("owner", "testType", "testID"),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
Action: eventstore.Action[any]{
Creator: "gigi",
Revision: 1,
@@ -1230,7 +1232,7 @@ func Test_push(t *testing.T) {
eventstore.AppendAggregate("owner", "testType", "testID"),
).Aggregates()[0],
},
- Command: eventstore.Command{
+ Command: &eventstore.Command{
Action: eventstore.Action[any]{
Creator: "gigi",
Revision: 1,
@@ -1286,6 +1288,7 @@ func Test_push(t *testing.T) {
},
},
}
+ initPushStmt("postgres")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dbMock := mock.NewSQLMock(t, append([]mock.Expectation{mock.ExpectBegin(nil)}, tt.args.expectations...)...)
diff --git a/internal/v2/eventstore/postgres/query.go b/internal/v2/eventstore/postgres/query.go
index ca7a081c75..3545bfb2b6 100644
--- a/internal/v2/eventstore/postgres/query.go
+++ b/internal/v2/eventstore/postgres/query.go
@@ -194,6 +194,7 @@ func writeAggregateFilters(stmt *database.Statement, filters []*eventstore.Aggre
func writeAggregateFilter(stmt *database.Statement, filter *eventstore.AggregateFilter) {
conditions := definedConditions([]*condition{
+ {column: "owner", condition: filter.Owners()},
{column: "aggregate_type", condition: filter.Type()},
{column: "aggregate_id", condition: filter.IDs()},
})
diff --git a/internal/v2/eventstore/postgres/storage.go b/internal/v2/eventstore/postgres/storage.go
index d2bf2a1195..c983cf83f7 100644
--- a/internal/v2/eventstore/postgres/storage.go
+++ b/internal/v2/eventstore/postgres/storage.go
@@ -3,6 +3,8 @@ package postgres
import (
"context"
+ "github.com/zitadel/logging"
+
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/v2/eventstore"
)
@@ -10,15 +12,35 @@ import (
var (
_ eventstore.Pusher = (*Storage)(nil)
_ eventstore.Querier = (*Storage)(nil)
+
+ pushPositionStmt string
)
type Storage struct {
client *database.DB
+ config *Config
}
-func New(client *database.DB) *Storage {
+type Config struct {
+ MaxRetries uint32
+}
+
+func New(client *database.DB, config *Config) *Storage {
+ initPushStmt(client.Type())
return &Storage{
client: client,
+ config: config,
+ }
+}
+
+func initPushStmt(typ string) {
+ switch typ {
+ case "cockroach":
+ pushPositionStmt = ", hlc_to_timestamp(cluster_logical_timestamp()), cluster_logical_timestamp()"
+ case "postgres":
+ pushPositionStmt = ", statement_timestamp(), EXTRACT(EPOCH FROM clock_timestamp())"
+ default:
+ logging.WithFields("database_type", typ).Panic("position statement for type not implemented")
}
}
diff --git a/internal/v2/eventstore/push.go b/internal/v2/eventstore/push.go
index b426078b8e..3864c0d255 100644
--- a/internal/v2/eventstore/push.go
+++ b/internal/v2/eventstore/push.go
@@ -87,7 +87,7 @@ type PushAggregate struct {
// owner of the aggregate
owner string
// Commands is an ordered list of changes on the aggregate
- commands []Command
+ commands []*Command
// CurrentSequence checks the current state of the aggregate.
// The following types match the current sequence of the aggregate as described:
// * nil or [SequenceIgnore]: Not relevant to add the commands
@@ -122,7 +122,7 @@ func (pa *PushAggregate) Owner() string {
return pa.owner
}
-func (pa *PushAggregate) Commands() []Command {
+func (pa *PushAggregate) Commands() []*Command {
return pa.commands
}
@@ -165,7 +165,7 @@ func CurrentSequenceAtLeast(sequence uint32) PushAggregateOpt {
}
}
-func AppendCommands(commands ...Command) PushAggregateOpt {
+func AppendCommands(commands ...*Command) PushAggregateOpt {
return func(pa *PushAggregate) {
pa.commands = append(pa.commands, commands...)
}
diff --git a/internal/v2/eventstore/query.go b/internal/v2/eventstore/query.go
index eddb1aedde..c9b3cecd37 100644
--- a/internal/v2/eventstore/query.go
+++ b/internal/v2/eventstore/query.go
@@ -255,6 +255,7 @@ func NewAggregateFilter(typ string, opts ...AggregateFilterOpt) *AggregateFilter
type AggregateFilter struct {
typ string
ids []string
+ owners *filter[[]string]
events []*EventFilter
}
@@ -273,6 +274,13 @@ func (f *AggregateFilter) IDs() database.Condition {
return database.NewListContains(f.ids...)
}
+func (f *AggregateFilter) Owners() database.Condition {
+ if f.owners == nil {
+ return nil
+ }
+ return f.owners.condition
+}
+
func (f *AggregateFilter) Events() []*EventFilter {
return f.events
}
@@ -298,6 +306,61 @@ func AggregateIDs(ids ...string) AggregateFilterOpt {
}
}
+func AggregateOwnersEqual(owners ...string) AggregateFilterOpt {
+ return func(f *AggregateFilter) {
+ var cond database.Condition
+ switch len(owners) {
+ case 0:
+ return
+ case 1:
+ cond = database.NewTextEqual(owners[0])
+ default:
+ cond = database.NewListEquals(owners...)
+ }
+ f.owners = &filter[[]string]{
+ condition: cond,
+ value: &owners,
+ }
+ }
+}
+
+func AggregateOwnersContains(owners ...string) AggregateFilterOpt {
+ return func(f *AggregateFilter) {
+ var cond database.Condition
+ switch len(owners) {
+ case 0:
+ return
+ case 1:
+ cond = database.NewTextEqual(owners[0])
+ default:
+ cond = database.NewListContains(owners...)
+ }
+
+ f.owners = &filter[[]string]{
+ condition: cond,
+ value: &owners,
+ }
+ }
+}
+
+func AggregateOwnersNotContains(owners ...string) AggregateFilterOpt {
+ return func(f *AggregateFilter) {
+ var cond database.Condition
+ switch len(owners) {
+ case 0:
+ return
+ case 1:
+ cond = database.NewTextUnequal(owners[0])
+ default:
+ cond = database.NewListNotContains(owners...)
+ }
+ f.owners = &filter[[]string]{
+ condition: cond,
+ value: &owners,
+ }
+ }
+}
+
func AppendEvent(opts ...EventFilterOpt) AggregateFilterOpt {
return AppendEvents(NewEventFilter(opts...))
}
diff --git a/internal/v2/projection/highest_position.go b/internal/v2/projection/highest_position.go
new file mode 100644
index 0000000000..180d477809
--- /dev/null
+++ b/internal/v2/projection/highest_position.go
@@ -0,0 +1,15 @@
+package projection
+
+import (
+ "github.com/zitadel/zitadel/internal/v2/eventstore"
+)
+
+type HighestPosition eventstore.GlobalPosition
+
+var _ eventstore.Reducer = (*HighestPosition)(nil)
+
+// Reduce implements eventstore.Reducer.
+func (h *HighestPosition) Reduce(events ...*eventstore.StorageEvent) error {
+ *h = HighestPosition(events[len(events)-1].Position)
+ return nil
+}
diff --git a/internal/v2/projection/org_primary_domain.go b/internal/v2/projection/org_primary_domain.go
new file mode 100644
index 0000000000..3c83cd3152
--- /dev/null
+++ b/internal/v2/projection/org_primary_domain.go
@@ -0,0 +1,57 @@
+package projection
+
+import (
+ "github.com/zitadel/zitadel/internal/v2/eventstore"
+ "github.com/zitadel/zitadel/internal/v2/org"
+)
+
+type OrgPrimaryDomain struct {
+ projection
+
+ id string
+
+ Domain string
+}
+
+func NewOrgPrimaryDomain(id string) *OrgPrimaryDomain {
+ return &OrgPrimaryDomain{
+ id: id,
+ }
+}
+
+func (p *OrgPrimaryDomain) Filter() []*eventstore.Filter {
+ return []*eventstore.Filter{
+ eventstore.NewFilter(
+ eventstore.FilterPagination(
+ eventstore.GlobalPositionGreater(&p.position),
+ ),
+ eventstore.AppendAggregateFilter(
+ org.AggregateType,
+ eventstore.AggregateIDs(p.id),
+ eventstore.AppendEvent(
+ eventstore.SetEventTypes(org.DomainPrimarySetType),
+ ),
+ ),
+ ),
+ }
+}
+
+func (p *OrgPrimaryDomain) Reduce(events ...*eventstore.StorageEvent) error {
+ for _, event := range events {
+ if !p.shouldReduce(event) {
+ continue
+ }
+ if event.Type != org.DomainPrimarySetType {
+ continue
+ }
+ e, err := org.DomainPrimarySetEventFromStorage(event)
+ if err != nil {
+ return err
+ }
+
+ p.Domain = e.Payload.Name
+ p.projection.reduce(event)
+ }
+
+ return nil
+}
diff --git a/internal/v2/projection/org_state.go b/internal/v2/projection/org_state.go
new file mode 100644
index 0000000000..f67b51060a
--- /dev/null
+++ b/internal/v2/projection/org_state.go
@@ -0,0 +1,67 @@
+package projection
+
+import (
+ "github.com/zitadel/zitadel/internal/v2/eventstore"
+ "github.com/zitadel/zitadel/internal/v2/org"
+)
+
+type OrgState struct {
+ projection
+
+ id string
+
+ org.State
+}
+
+func NewStateProjection(id string) *OrgState {
+ // TODO: check buffer for id and return from buffer if exists
+ return &OrgState{
+ id: id,
+ }
+}
+
+func (p *OrgState) Filter() []*eventstore.Filter {
+ return []*eventstore.Filter{
+ eventstore.NewFilter(
+ eventstore.FilterPagination(
+ eventstore.Descending(),
+ eventstore.GlobalPositionGreater(&p.position),
+ ),
+ eventstore.AppendAggregateFilter(
+ org.AggregateType,
+ eventstore.AggregateIDs(p.id),
+ eventstore.AppendEvent(
+ eventstore.SetEventTypes(
+ org.AddedType,
+ org.DeactivatedType,
+ org.ReactivatedType,
+ org.RemovedType,
+ ),
+ ),
+ ),
+ ),
+ }
+}
+
+func (p *OrgState) Reduce(events ...*eventstore.StorageEvent) error {
+ for _, event := range events {
+ if !p.shouldReduce(event) {
+ continue
+ }
+
+ switch event.Type {
+ case org.AddedType:
+ p.State = org.ActiveState
+ case org.DeactivatedType:
+ p.State = org.InactiveState
+ case org.ReactivatedType:
+ p.State = org.ActiveState
+ case org.RemovedType:
+ p.State = org.RemovedState
+ default:
+ continue
+ }
+ p.position = event.Position
+ }
+ return nil
+}
diff --git a/internal/v2/projection/projection.go b/internal/v2/projection/projection.go
new file mode 100644
index 0000000000..980eaecc74
--- /dev/null
+++ b/internal/v2/projection/projection.go
@@ -0,0 +1,20 @@
+package projection
+
+import "github.com/zitadel/zitadel/internal/v2/eventstore"
+
+type projection struct {
+ instance string
+ position eventstore.GlobalPosition
+}
+
+func (p *projection) reduce(event *eventstore.StorageEvent) {
+ if p.instance == "" {
+ p.instance = event.Aggregate.Instance
+ }
+ p.position = event.Position
+}
+
+func (p *projection) shouldReduce(event *eventstore.StorageEvent) bool {
+ shouldReduce := p.instance == "" || p.instance == event.Aggregate.Instance
+ return shouldReduce && p.position.IsLess(event.Position)
+}
diff --git a/internal/v2/readmodel/last_successful_mirror.go b/internal/v2/readmodel/last_successful_mirror.go
new file mode 100644
index 0000000000..80b436b896
--- /dev/null
+++ b/internal/v2/readmodel/last_successful_mirror.go
@@ -0,0 +1,72 @@
+package readmodel
+
+import (
+ "github.com/zitadel/zitadel/internal/v2/eventstore"
+ "github.com/zitadel/zitadel/internal/v2/system"
+ "github.com/zitadel/zitadel/internal/v2/system/mirror"
+)
+
+type LastSuccessfulMirror struct {
+ ID string
+ Position float64
+ source string
+}
+
+func NewLastSuccessfulMirror(source string) *LastSuccessfulMirror {
+ return &LastSuccessfulMirror{
+ source: source,
+ }
+}
+
+var _ eventstore.Reducer = (*LastSuccessfulMirror)(nil)
+
+func (p *LastSuccessfulMirror) Filter() *eventstore.Filter {
+ return eventstore.NewFilter(
+ eventstore.AppendAggregateFilter(
+ system.AggregateType,
+ eventstore.AggregateOwnersEqual(system.AggregateOwner),
+ eventstore.AppendEvent(
+ eventstore.SetEventTypes(
+ mirror.SucceededType,
+ ),
+ eventstore.EventCreatorsEqual(mirror.Creator),
+ ),
+ ),
+ eventstore.FilterPagination(
+ eventstore.Descending(),
+ ),
+ )
+}
+
+// Reduce implements eventstore.Reducer.
+func (h *LastSuccessfulMirror) Reduce(events ...*eventstore.StorageEvent) (err error) {
+ for _, event := range events {
+ if event.Type == mirror.SucceededType {
+ err = h.reduceSucceeded(event)
+ }
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (h *LastSuccessfulMirror) reduceSucceeded(event *eventstore.StorageEvent) error {
+ // if position is set we skip all older events
+ if h.Position > 0 {
+ return nil
+
+ }
+ succeededEvent, err := mirror.SucceededEventFromStorage(event)
+ if err != nil {
+ return err
+ }
+
+ if h.source != succeededEvent.Payload.Source {
+ return nil
+ }
+
+ h.Position = succeededEvent.Payload.Position
+
+ return nil
+}
diff --git a/internal/v2/readmodel/org.go b/internal/v2/readmodel/org.go
new file mode 100644
index 0000000000..94bcb21537
--- /dev/null
+++ b/internal/v2/readmodel/org.go
@@ -0,0 +1,68 @@
+package readmodel
+
+import (
+ "time"
+
+ "github.com/zitadel/zitadel/internal/v2/eventstore"
+ "github.com/zitadel/zitadel/internal/v2/org"
+ "github.com/zitadel/zitadel/internal/v2/projection"
+)
+
+type Org struct {
+ ID string
+ Name string
+ PrimaryDomain *projection.OrgPrimaryDomain
+ State *projection.OrgState
+
+ Sequence uint32
+ CreationDate time.Time
+ ChangeDate time.Time
+ Owner string
+}
+
+func NewOrg(id string) *Org {
+ return &Org{
+ ID: id,
+ State: projection.NewStateProjection(id),
+ PrimaryDomain: projection.NewOrgPrimaryDomain(id),
+ }
+}
+
+func (rm *Org) Filter() []*eventstore.Filter {
+ return []*eventstore.Filter{
+ // we don't need the filters of the projections as we filter all events of the read model
+ eventstore.NewFilter(
+ eventstore.AppendAggregateFilter(
+ org.AggregateType,
+ eventstore.SetAggregateID(rm.ID),
+ ),
+ ),
+ }
+}
+
+func (rm *Org) Reduce(events ...*eventstore.StorageEvent) error {
+ for _, event := range events {
+ switch event.Type {
+ case org.AddedType:
+ added, err := org.AddedEventFromStorage(event)
+ if err != nil {
+ return err
+ }
+ rm.Name = added.Payload.Name
+ rm.Owner = event.Aggregate.Owner
+ rm.CreationDate = event.CreatedAt
+ case org.ChangedType:
+ changed, err := org.ChangedEventFromStorage(event)
+ if err != nil {
+ return err
+ }
+ rm.Name = changed.Payload.Name
+ }
+ rm.Sequence = event.Sequence
+ rm.ChangeDate = event.CreatedAt
+ }
+ if err := rm.State.Reduce(events...); err != nil {
+ return err
+ }
+ return rm.PrimaryDomain.Reduce(events...)
+}
diff --git a/internal/v2/readmodel/query.go b/internal/v2/readmodel/query.go
new file mode 100644
index 0000000000..73ca7cd415
--- /dev/null
+++ b/internal/v2/readmodel/query.go
@@ -0,0 +1,15 @@
+package readmodel
+
+import (
+ "database/sql"
+
+ "github.com/zitadel/zitadel/internal/v2/eventstore"
+)
+
+type QueryOpt func(opts []eventstore.QueryOpt) []eventstore.QueryOpt
+
+func WithTx(tx *sql.Tx) QueryOpt {
+ return func(opts []eventstore.QueryOpt) []eventstore.QueryOpt {
+ return append(opts, eventstore.SetQueryTx(tx))
+ }
+}
diff --git a/internal/v2/system/aggregate.go b/internal/v2/system/aggregate.go
new file mode 100644
index 0000000000..f5fb2ea13b
--- /dev/null
+++ b/internal/v2/system/aggregate.go
@@ -0,0 +1,8 @@
+package system
+
+const (
+ AggregateType = "system"
+ AggregateOwner = "SYSTEM"
+ AggregateInstance = ""
+ EventTypePrefix = AggregateType + "."
+)
diff --git a/internal/v2/system/mirror/aggregate.go b/internal/v2/system/mirror/aggregate.go
new file mode 100644
index 0000000000..2e51b84515
--- /dev/null
+++ b/internal/v2/system/mirror/aggregate.go
@@ -0,0 +1,8 @@
+package mirror
+
+import "github.com/zitadel/zitadel/internal/v2/system"
+
+const (
+ Creator = "MIRROR"
+ eventTypePrefix = system.EventTypePrefix + "mirror."
+)
diff --git a/internal/v2/system/mirror/failed.go b/internal/v2/system/mirror/failed.go
new file mode 100644
index 0000000000..141a45c509
--- /dev/null
+++ b/internal/v2/system/mirror/failed.go
@@ -0,0 +1,52 @@
+package mirror
+
+import (
+ "github.com/zitadel/zitadel/internal/v2/eventstore"
+ "github.com/zitadel/zitadel/internal/zerrors"
+)
+
+type failedPayload struct {
+ Cause string `json:"cause"`
+ // Source is the name of the database data are mirrored to
+ Source string `json:"source"`
+}
+
+const FailedType = eventTypePrefix + "failed"
+
+type FailedEvent eventstore.Event[failedPayload]
+
+var _ eventstore.TypeChecker = (*FailedEvent)(nil)
+
+func (e *FailedEvent) ActionType() string {
+ return FailedType
+}
+
+func FailedEventFromStorage(event *eventstore.StorageEvent) (e *FailedEvent, _ error) {
+ if event.Type != e.ActionType() {
+ return nil, zerrors.ThrowInvalidArgument(nil, "MIRRO-bwB9l", "Errors.Invalid.Event.Type")
+ }
+
+ payload, err := eventstore.UnmarshalPayload[failedPayload](event.Payload)
+ if err != nil {
+ return nil, err
+ }
+
+ return &FailedEvent{
+ StorageEvent: event,
+ Payload: payload,
+ }, nil
+}
+
+func NewFailedCommand(source string, cause error) *eventstore.Command {
+ return &eventstore.Command{
+ Action: eventstore.Action[any]{
+ Creator: Creator,
+ Type: FailedType,
+ Payload: failedPayload{
+ Cause: cause.Error(),
+ Source: source,
+ },
+ Revision: 1,
+ },
+ }
+}
diff --git a/internal/v2/system/mirror/started.go b/internal/v2/system/mirror/started.go
new file mode 100644
index 0000000000..1b18d0a548
--- /dev/null
+++ b/internal/v2/system/mirror/started.go
@@ -0,0 +1,68 @@
+package mirror
+
+import (
+ "github.com/zitadel/zitadel/internal/v2/eventstore"
+ "github.com/zitadel/zitadel/internal/zerrors"
+)
+
+type startedPayload struct {
+ // Destination is the name of the database data are mirrored to
+ Destination string `json:"destination"`
+ // Either Instances or System needs to be set
+ Instances []string `json:"instances,omitempty"`
+ System bool `json:"system,omitempty"`
+}
+
+const StartedType = eventTypePrefix + "started"
+
+type StartedEvent eventstore.Event[startedPayload]
+
+var _ eventstore.TypeChecker = (*StartedEvent)(nil)
+
+func (e *StartedEvent) ActionType() string {
+ return StartedType
+}
+
+func StartedEventFromStorage(event *eventstore.StorageEvent) (e *StartedEvent, _ error) {
+ if event.Type != e.ActionType() {
+ return nil, zerrors.ThrowInvalidArgument(nil, "MIRRO-bwB9l", "Errors.Invalid.Event.Type")
+ }
+
+ payload, err := eventstore.UnmarshalPayload[startedPayload](event.Payload)
+ if err != nil {
+ return nil, err
+ }
+
+ return &StartedEvent{
+ StorageEvent: event,
+ Payload: payload,
+ }, nil
+}
+
+func NewStartedSystemCommand(destination string) *eventstore.Command {
+ return newStartedCommand(&startedPayload{
+ Destination: destination,
+ System: true,
+ })
+}
+
+func NewStartedInstancesCommand(destination string, instances []string) (*eventstore.Command, error) {
+ if len(instances) == 0 {
+ return nil, zerrors.ThrowInvalidArgument(nil, "MIRRO-8YkrE", "Errors.Mirror.NoInstances")
+ }
+ return newStartedCommand(&startedPayload{
+ Destination: destination,
+ Instances: instances,
+ }), nil
+}
+
+func newStartedCommand(payload *startedPayload) *eventstore.Command {
+ return &eventstore.Command{
+ Action: eventstore.Action[any]{
+ Creator: Creator,
+ Type: StartedType,
+ Revision: 1,
+ Payload: *payload,
+ },
+ }
+}
diff --git a/internal/v2/system/mirror/succeeded.go b/internal/v2/system/mirror/succeeded.go
new file mode 100644
index 0000000000..6d0fba2c25
--- /dev/null
+++ b/internal/v2/system/mirror/succeeded.go
@@ -0,0 +1,53 @@
+package mirror
+
+import (
+ "github.com/zitadel/zitadel/internal/v2/eventstore"
+ "github.com/zitadel/zitadel/internal/zerrors"
+)
+
+type succeededPayload struct {
+ // Source is the name of the database data are mirrored from
+ Source string `json:"source"`
+ // Position until data will be mirrored
+ Position float64 `json:"position"`
+}
+
+const SucceededType = eventTypePrefix + "succeeded"
+
+type SucceededEvent eventstore.Event[succeededPayload]
+
+var _ eventstore.TypeChecker = (*SucceededEvent)(nil)
+
+func (e *SucceededEvent) ActionType() string {
+ return SucceededType
+}
+
+func SucceededEventFromStorage(event *eventstore.StorageEvent) (e *SucceededEvent, _ error) {
+ if event.Type != e.ActionType() {
+ return nil, zerrors.ThrowInvalidArgument(nil, "MIRRO-xh5IW", "Errors.Invalid.Event.Type")
+ }
+
+ payload, err := eventstore.UnmarshalPayload[succeededPayload](event.Payload)
+ if err != nil {
+ return nil, err
+ }
+
+ return &SucceededEvent{
+ StorageEvent: event,
+ Payload: payload,
+ }, nil
+}
+
+func NewSucceededCommand(source string, position float64) *eventstore.Command {
+ return &eventstore.Command{
+ Action: eventstore.Action[any]{
+ Creator: Creator,
+ Type: SucceededType,
+ Revision: 1,
+ Payload: succeededPayload{
+ Source: source,
+ Position: position,
+ },
+ },
+ }
+}
diff --git a/proto/zitadel/feature/v2beta/feature.proto b/proto/zitadel/feature/v2beta/feature.proto
index 484bb667fc..082159b95b 100644
--- a/proto/zitadel/feature/v2beta/feature.proto
+++ b/proto/zitadel/feature/v2beta/feature.proto
@@ -33,3 +33,25 @@ message FeatureFlag {
}
];
}
+
+message ImprovedPerformanceFeatureFlag {
+ repeated ImprovedPerformance execution_paths = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[1]";
+ description: "Which of the performance improvements is enabled";
+ }
+ ];
+
+ Source source = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "The source where the setting of the feature was defined. The source may be the resource itself or a resource owner through inheritance.";
+ }
+ ];
+}
+
+enum ImprovedPerformance {
+ IMPROVED_PERFORMANCE_UNSPECIFIED = 0;
+ // Uses the eventstore to query the org by id
+ // instead of the sql table.
+ IMPROVED_PERFORMANCE_ORG_BY_ID = 1;
+}
\ No newline at end of file
diff --git a/proto/zitadel/feature/v2beta/instance.proto b/proto/zitadel/feature/v2beta/instance.proto
index 3cfc2c4506..292fcc5101 100644
--- a/proto/zitadel/feature/v2beta/instance.proto
+++ b/proto/zitadel/feature/v2beta/instance.proto
@@ -49,6 +49,15 @@ message SetInstanceFeaturesRequest{
description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage.";
}
];
+
+ repeated ImprovedPerformance improved_performance = 7 [
+ (validate.rules).repeated.unique = true,
+ (validate.rules).repeated.items.enum = {defined_only: true, not_in: [0]},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[1]";
+ description: "Improves performance of specified execution paths.";
+ }
+ ];
}
message SetInstanceFeaturesResponse {
@@ -113,4 +122,11 @@ message GetInstanceFeaturesResponse {
description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage.";
}
];
+
+ ImprovedPerformanceFeatureFlag improved_performance = 8 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[1]";
+ description: "Improves performance of specified execution paths.";
+ }
+ ];
}
diff --git a/proto/zitadel/feature/v2beta/system.proto b/proto/zitadel/feature/v2beta/system.proto
index 8f98eb0625..048f25391f 100644
--- a/proto/zitadel/feature/v2beta/system.proto
+++ b/proto/zitadel/feature/v2beta/system.proto
@@ -46,13 +46,21 @@ message SetSystemFeaturesRequest{
}
];
-
optional bool actions = 6 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "Actions allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage.";
}
];
+
+ repeated ImprovedPerformance improved_performance = 7 [
+ (validate.rules).repeated.unique = true,
+ (validate.rules).repeated.items.enum = {defined_only: true, not_in: [0]},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[1]";
+ description: "Improves performance of specified execution paths.";
+ }
+ ];
}
message SetSystemFeaturesResponse {
@@ -110,4 +118,11 @@ message GetSystemFeaturesResponse {
description: "Actions v2 allow to manage data executions and targets. If the flag is enabled, you'll be able to use the new API and its features. Note that it is still in an early stage.";
}
];
+
+ ImprovedPerformanceFeatureFlag improved_performance = 8 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[1]";
+ description: "Improves performance of specified execution paths.";
+ }
+ ];
}
diff --git a/release-channels.yaml b/release-channels.yaml
index 496ddee451..873825b841 100644
--- a/release-channels.yaml
+++ b/release-channels.yaml
@@ -1 +1 @@
-stable: "v2.47.10"
+stable: "v2.48.5"