diff --git a/cmd/admin/initialise/sql/09_events_table.sql b/cmd/admin/initialise/sql/09_events_table.sql index e1d79e5ea3..0e672ca34b 100644 --- a/cmd/admin/initialise/sql/09_events_table.sql +++ b/cmd/admin/initialise/sql/09_events_table.sql @@ -12,14 +12,14 @@ CREATE TABLE eventstore.events ( , editor_user TEXT NOT NULL , editor_service TEXT NOT NULL , resource_owner TEXT NOT NULL - , instance_id TEXT + , instance_id TEXT NOT NULL - , PRIMARY KEY (event_sequence DESC) USING HASH WITH BUCKET_COUNT = 10 - , INDEX agg_type_agg_id (aggregate_type, aggregate_id) - , INDEX agg_type (aggregate_type) - , INDEX agg_type_seq (aggregate_type, event_sequence DESC) - STORING (id, event_type, aggregate_id, aggregate_version, previous_aggregate_sequence, creation_date, event_data, editor_user, editor_service, resource_owner, instance_id, previous_aggregate_type_sequence) - , INDEX max_sequence (aggregate_type, aggregate_id, event_sequence DESC) - , CONSTRAINT previous_sequence_unique UNIQUE (previous_aggregate_sequence DESC) - , CONSTRAINT prev_agg_type_seq_unique UNIQUE(previous_aggregate_type_sequence) + , PRIMARY KEY (event_sequence DESC, instance_id) USING HASH WITH BUCKET_COUNT = 10 + , INDEX agg_type_agg_id (aggregate_type, aggregate_id, instance_id) + , INDEX agg_type (aggregate_type, instance_id) + , INDEX agg_type_seq (aggregate_type, event_sequence DESC, instance_id) + STORING (id, event_type, aggregate_id, aggregate_version, previous_aggregate_sequence, creation_date, event_data, editor_user, editor_service, resource_owner, previous_aggregate_type_sequence) + , INDEX max_sequence (aggregate_type, aggregate_id, event_sequence DESC, instance_id) + , CONSTRAINT previous_sequence_unique UNIQUE (previous_aggregate_sequence DESC, instance_id) + , CONSTRAINT prev_agg_type_seq_unique UNIQUE(previous_aggregate_type_sequence, instance_id) ) diff --git a/cmd/admin/setup/02.go b/cmd/admin/setup/02.go index 8b20ebce94..e326cab830 100644 --- a/cmd/admin/setup/02.go +++ b/cmd/admin/setup/02.go @@ -3,7 +3,6 @@ package setup import ( "context" "database/sql" - _ "embed" ) const ( diff --git a/cmd/admin/setup/03.go b/cmd/admin/setup/03.go index 71e3ba7a95..18ec2a4203 100644 --- a/cmd/admin/setup/03.go +++ b/cmd/admin/setup/03.go @@ -2,21 +2,62 @@ package setup import ( "context" + "database/sql" + "fmt" - "github.com/caos/zitadel/internal/command/v2" + "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/command" + "github.com/caos/zitadel/internal/config/systemdefaults" + "github.com/caos/zitadel/internal/crypto" + crypto_db "github.com/caos/zitadel/internal/crypto/database" + "github.com/caos/zitadel/internal/eventstore" ) type DefaultInstance struct { - cmd *command.Command InstanceSetup command.InstanceSetup + + userEncryptionKey *crypto.KeyConfig + masterKey string + db *sql.DB + es *eventstore.Eventstore + domain string + defaults systemdefaults.SystemDefaults + zitadelRoles []authz.RoleMapping } func (mig *DefaultInstance) Execute(ctx context.Context) error { - _, err := mig.cmd.SetUpInstance(ctx, &mig.InstanceSetup) + keyStorage, err := crypto_db.NewKeyStorage(mig.db, mig.masterKey) + if err != nil { + return fmt.Errorf("cannot start key storage: %w", err) + } + if err = verifyKey(mig.userEncryptionKey, keyStorage); err != nil { + return err + } + userAlg, err := crypto.NewAESCrypto(mig.userEncryptionKey, keyStorage) + if err != nil { + return err + } + + cmd := command.NewCommandV2(mig.es, mig.defaults, userAlg, mig.zitadelRoles) + + ctx = authz.WithRequestedDomain(ctx, mig.domain) + _, err = cmd.SetUpInstance(ctx, &mig.InstanceSetup) return err } func (mig *DefaultInstance) String() string { return "03_default_instance" } + +func verifyKey(key *crypto.KeyConfig, storage crypto.KeyStorage) (err error) { + _, err = crypto.LoadKey(key.EncryptionKeyID, storage) + if err == nil { + return nil + } + k, err := crypto.NewKey(key.EncryptionKeyID) + if err != nil { + return err + } + return storage.CreateKeys(k) +} diff --git a/cmd/admin/setup/config.go b/cmd/admin/setup/config.go index 171028031c..75676484a5 100644 --- a/cmd/admin/setup/config.go +++ b/cmd/admin/setup/config.go @@ -10,6 +10,7 @@ import ( "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/config/hook" "github.com/caos/zitadel/internal/config/systemdefaults" + "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/database" ) @@ -21,6 +22,7 @@ type Config struct { ExternalDomain string ExternalSecure bool Log *logging.Config + EncryptionKeys *encryptionKeyConfig } func MustNewConfig(v *viper.Viper) *Config { @@ -40,6 +42,10 @@ type Steps struct { S3DefaultInstance *DefaultInstance } +type encryptionKeyConfig struct { + User *crypto.KeyConfig +} + func MustNewSteps(v *viper.Viper) *Steps { v.SetConfigType("yaml") err := v.ReadConfig(bytes.NewBuffer(defaultSteps)) diff --git a/cmd/admin/setup/setup.go b/cmd/admin/setup/setup.go index 21ca6c850d..18c931dfa9 100644 --- a/cmd/admin/setup/setup.go +++ b/cmd/admin/setup/setup.go @@ -8,8 +8,8 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/caos/zitadel/cmd/admin/key" http_util "github.com/caos/zitadel/internal/api/http" - "github.com/caos/zitadel/internal/command/v2" "github.com/caos/zitadel/internal/database" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/migration" @@ -31,12 +31,15 @@ Requirements: config := MustNewConfig(viper.GetViper()) steps := MustNewSteps(viper.New()) - Setup(config, steps) + masterKey, err := key.MasterKey(cmd) + logging.OnError(err).Panic("No master key provided") + + Setup(config, steps, masterKey) }, } } -func Setup(config *Config, steps *Steps) { +func Setup(config *Config, steps *Steps, masterKey string) { dbClient, err := database.Connect(config.Database) logging.OnError(err).Fatal("unable to connect to database") @@ -44,11 +47,17 @@ func Setup(config *Config, steps *Steps) { logging.OnError(err).Fatal("unable to start eventstore") migration.RegisterMappers(eventstoreClient) - cmd := command.New(eventstoreClient, "localhost", config.SystemDefaults) - steps.s1ProjectionTable = &ProjectionTable{dbClient: dbClient} steps.s2AssetsTable = &AssetTable{dbClient: dbClient} - steps.S3DefaultInstance.cmd = cmd + steps.S3DefaultInstance.es = eventstoreClient + steps.S3DefaultInstance.db = dbClient + steps.S3DefaultInstance.defaults = config.SystemDefaults + steps.S3DefaultInstance.masterKey = masterKey + steps.S3DefaultInstance.domain = config.SystemDefaults.Domain + steps.S3DefaultInstance.zitadelRoles = config.InternalAuthZ.RolePermissionMappings + steps.S3DefaultInstance.userEncryptionKey = config.EncryptionKeys.User + steps.S3DefaultInstance.InstanceSetup.Zitadel.IsDevMode = !config.ExternalSecure + steps.S3DefaultInstance.InstanceSetup.Zitadel.BaseURL = http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure) steps.S3DefaultInstance.InstanceSetup.Zitadel.IsDevMode = !config.ExternalSecure steps.S3DefaultInstance.InstanceSetup.Zitadel.BaseURL = http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure) diff --git a/cmd/admin/setup/steps.yaml b/cmd/admin/setup/steps.yaml index 9d3f5febc7..5089198b5e 100644 --- a/cmd/admin/setup/steps.yaml +++ b/cmd/admin/setup/steps.yaml @@ -8,11 +8,64 @@ S3DefaultInstance: LastName: Admin NickName: DisplayName: - Email: admin@zitadel.ch + Email: + Address: admin@zitadel.ch + Verified: true PreferredLanguage: Gender: Phone: + Number: + Verified: Password: Password1! + SecretGenerators: + PasswordSaltCost: 14 + ClientSecret: + Length: 64 + IncludeLowerLetters: true + IncludeUpperLetters: true + IncludeDigits: true + IncludeSymbols: false + InitializeUserCode: + Length: 6 + Expiry: '72h' + IncludeLowerLetters: false + IncludeUpperLetters: true + IncludeDigits: true + IncludeSymbols: false + EmailVerificationCode: + Length: 6 + Expiry: '1h' + IncludeLowerLetters: false + IncludeUpperLetters: true + IncludeDigits: true + IncludeSymbols: false + PhoneVerificationCode: + Length: 6 + Expiry: '1h' + IncludeLowerLetters: false + IncludeUpperLetters: true + IncludeDigits: true + IncludeSymbols: false + PasswordVerificationCode: + Length: 6 + Expiry: '1h' + IncludeLowerLetters: false + IncludeUpperLetters: true + IncludeDigits: true + IncludeSymbols: false + PasswordlessInitCode: + Length: 12 + Expiry: '1h' + IncludeLowerLetters: true + IncludeUpperLetters: true + IncludeDigits: true + IncludeSymbols: false + DomainVerification: + Length: 32 + IncludeLowerLetters: true + IncludeUpperLetters: true + IncludeDigits: true + IncludeSymbols: false Features: TierName: Default Tier TierDescription: "" diff --git a/cmd/admin/start/encryption_keys.go b/cmd/admin/start/encryption_keys.go index bb3f94aebb..81028fc177 100644 --- a/cmd/admin/start/encryption_keys.go +++ b/cmd/admin/start/encryption_keys.go @@ -32,26 +32,20 @@ type encryptionKeys struct { OIDCKey []byte } -func ensureEncryptionKeys(keyConfig *encryptionKeyConfig, keyStorage crypto.KeyStorage) (*encryptionKeys, error) { - keys, err := keyStorage.ReadKeys() +func ensureEncryptionKeys(keyConfig *encryptionKeyConfig, keyStorage crypto.KeyStorage) (keys *encryptionKeys, err error) { + if err := verifyDefaultKeys(keyStorage); err != nil { + return nil, err + } + keys = new(encryptionKeys) + keys.DomainVerification, err = crypto.NewAESCrypto(keyConfig.DomainVerification, keyStorage) if err != nil { return nil, err } - if len(keys) == 0 { - if err := createDefaultKeys(keyStorage); err != nil { - return nil, err - } - } - encryptionKeys := new(encryptionKeys) - encryptionKeys.DomainVerification, err = crypto.NewAESCrypto(keyConfig.DomainVerification, keyStorage) + keys.IDPConfig, err = crypto.NewAESCrypto(keyConfig.IDPConfig, keyStorage) if err != nil { return nil, err } - encryptionKeys.IDPConfig, err = crypto.NewAESCrypto(keyConfig.IDPConfig, keyStorage) - if err != nil { - return nil, err - } - encryptionKeys.OIDC, err = crypto.NewAESCrypto(keyConfig.OIDC, keyStorage) + keys.OIDC, err = crypto.NewAESCrypto(keyConfig.OIDC, keyStorage) if err != nil { return nil, err } @@ -59,20 +53,20 @@ func ensureEncryptionKeys(keyConfig *encryptionKeyConfig, keyStorage crypto.KeyS if err != nil { return nil, err } - encryptionKeys.OIDCKey = []byte(key) - encryptionKeys.OTP, err = crypto.NewAESCrypto(keyConfig.OTP, keyStorage) + keys.OIDCKey = []byte(key) + keys.OTP, err = crypto.NewAESCrypto(keyConfig.OTP, keyStorage) if err != nil { return nil, err } - encryptionKeys.SMS, err = crypto.NewAESCrypto(keyConfig.SMS, keyStorage) + keys.SMS, err = crypto.NewAESCrypto(keyConfig.SMS, keyStorage) if err != nil { return nil, err } - encryptionKeys.SMTP, err = crypto.NewAESCrypto(keyConfig.SMTP, keyStorage) + keys.SMTP, err = crypto.NewAESCrypto(keyConfig.SMTP, keyStorage) if err != nil { return nil, err } - encryptionKeys.User, err = crypto.NewAESCrypto(keyConfig.User, keyStorage) + keys.User, err = crypto.NewAESCrypto(keyConfig.User, keyStorage) if err != nil { return nil, err } @@ -80,23 +74,30 @@ func ensureEncryptionKeys(keyConfig *encryptionKeyConfig, keyStorage crypto.KeyS if err != nil { return nil, err } - encryptionKeys.CSRFCookieKey = []byte(key) + keys.CSRFCookieKey = []byte(key) key, err = crypto.LoadKey(keyConfig.UserAgentCookieKeyID, keyStorage) if err != nil { return nil, err } - encryptionKeys.UserAgentCookieKey = []byte(key) - return encryptionKeys, nil + keys.UserAgentCookieKey = []byte(key) + return keys, nil } -func createDefaultKeys(keyStorage crypto.KeyStorage) error { - keys := make([]*crypto.Key, len(defaultKeyIDs)) - for i, keyID := range defaultKeyIDs { +func verifyDefaultKeys(keyStorage crypto.KeyStorage) (err error) { + keys := make([]*crypto.Key, 0, len(defaultKeyIDs)) + for _, keyID := range defaultKeyIDs { + _, err := crypto.LoadKey(keyID, keyStorage) + if err == nil { + continue + } key, err := crypto.NewKey(keyID) if err != nil { return err } - keys[i] = key + keys = append(keys, key) + } + if len(keys) == 0 { + return nil } if err := keyStorage.CreateKeys(keys...); err != nil { return caos_errs.ThrowInternal(err, "START-aGBq2", "cannot create default keys") diff --git a/cmd/admin/start/start.go b/cmd/admin/start/start.go index eb58248252..5a0cb6a630 100644 --- a/cmd/admin/start/start.go +++ b/cmd/admin/start/start.go @@ -21,7 +21,6 @@ import ( "golang.org/x/net/http2/h2c" "github.com/caos/zitadel/cmd/admin/key" - admin_es "github.com/caos/zitadel/internal/admin/repository/eventsourcing" "github.com/caos/zitadel/internal/api" "github.com/caos/zitadel/internal/api/assets" @@ -118,7 +117,7 @@ func startZitadel(config *Config, masterKey string) error { Origin: http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), DisplayName: "ZITADEL", } - commands, err := command.StartCommands(eventstoreClient, config.SystemDefaults, config.InternalAuthZ, storage, authZRepo, webAuthNConfig, keys.IDPConfig, keys.OTP, keys.SMTP, keys.SMS, keys.DomainVerification, keys.OIDC) + commands, err := command.StartCommands(eventstoreClient, config.SystemDefaults, config.InternalAuthZ, storage, authZRepo, webAuthNConfig, keys.IDPConfig, keys.OTP, keys.SMTP, keys.SMS, keys.User, keys.DomainVerification, keys.OIDC) if err != nil { return fmt.Errorf("cannot start commands: %w", err) } diff --git a/cmd/admin/start/start_from_init.go b/cmd/admin/start/start_from_init.go index 273707ab2f..bbdf93e0e4 100644 --- a/cmd/admin/start/start_from_init.go +++ b/cmd/admin/start/start_from_init.go @@ -3,6 +3,7 @@ package start import ( "github.com/caos/logging" "github.com/caos/zitadel/cmd/admin/initialise" + "github.com/caos/zitadel/cmd/admin/key" "github.com/caos/zitadel/cmd/admin/setup" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -20,16 +21,18 @@ Last ZITADEL starts. Requirements: - cockroachdb`, Run: func(cmd *cobra.Command, args []string) { + masterKey, err := key.MasterKey(cmd) + logging.OnError(err).Panic("No master key provided") + initialise.InitAll(initialise.MustNewConfig(viper.GetViper())) setupConfig := setup.MustNewConfig(viper.GetViper()) setupSteps := setup.MustNewSteps(viper.New()) - setup.Setup(setupConfig, setupSteps) + setup.Setup(setupConfig, setupSteps, masterKey) startConfig := MustNewConfig(viper.GetViper()) - startMasterKey, _ := cmd.Flags().GetString(flagMasterKey) - err := startZitadel(startConfig, startMasterKey) + err = startZitadel(startConfig, masterKey) logging.OnError(err).Fatal("unable to start zitadel") }, } diff --git a/internal/api/authz/instance.go b/internal/api/authz/instance.go index 5978eabb6f..6501793bb6 100644 --- a/internal/api/authz/instance.go +++ b/internal/api/authz/instance.go @@ -12,6 +12,7 @@ type Instance interface { InstanceID() string ProjectID() string ConsoleClientID() string + RequestedDomain() string } type InstanceVerifier interface { @@ -19,7 +20,8 @@ type InstanceVerifier interface { } type instance struct { - ID string + ID string + Domain string } func (i *instance) InstanceID() string { @@ -34,6 +36,10 @@ func (i *instance) ConsoleClientID() string { return "" } +func (i *instance) RequestedDomain() string { + return i.Domain +} + func GetInstance(ctx context.Context) Instance { instance, ok := ctx.Value(instanceKey).(Instance) if !ok { @@ -49,3 +55,13 @@ func WithInstance(ctx context.Context, instance Instance) context.Context { func WithInstanceID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, instanceKey, &instance{ID: id}) } + +func WithRequestedDomain(ctx context.Context, domain string) context.Context { + i, ok := ctx.Value(instanceKey).(*instance) + if !ok { + i = new(instance) + } + + i.Domain = domain + return context.WithValue(ctx, instanceKey, i) +} diff --git a/internal/api/authz/instance_test.go b/internal/api/authz/instance_test.go index 1e2c625d73..c14f5fdf3a 100644 --- a/internal/api/authz/instance_test.go +++ b/internal/api/authz/instance_test.go @@ -78,3 +78,7 @@ func (m *mockInstance) ProjectID() string { func (m *mockInstance) ConsoleClientID() string { return "consoleID" } + +func (m *mockInstance) RequestedDomain() string { + return "zitadel.cloud" +} diff --git a/internal/api/grpc/management/server.go b/internal/api/grpc/management/server.go index 2bccf2a57f..1e000d9223 100644 --- a/internal/api/grpc/management/server.go +++ b/internal/api/grpc/management/server.go @@ -28,7 +28,8 @@ type Server struct { userCodeAlg crypto.EncryptionAlgorithm } -func CreateServer(command *command.Commands, +func CreateServer( + command *command.Commands, query *query.Queries, sd systemdefaults.SystemDefaults, assetAPIPrefix string, diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 8288babeb3..e889262247 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -4,7 +4,9 @@ import ( "context" "time" + "github.com/caos/logging" "github.com/caos/oidc/pkg/oidc" + "golang.org/x/text/language" "google.golang.org/protobuf/types/known/durationpb" "github.com/caos/zitadel/internal/api/authz" @@ -13,9 +15,9 @@ import ( idp_grpc "github.com/caos/zitadel/internal/api/grpc/idp" "github.com/caos/zitadel/internal/api/grpc/metadata" obj_grpc "github.com/caos/zitadel/internal/api/grpc/object" - "github.com/caos/zitadel/internal/api/grpc/user" user_grpc "github.com/caos/zitadel/internal/api/grpc/user" z_oidc "github.com/caos/zitadel/internal/api/oidc" + "github.com/caos/zitadel/internal/command" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/query" mgmt_pb "github.com/caos/zitadel/pkg/grpc/management" @@ -192,24 +194,40 @@ func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRe } func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) { - initCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeInitCode, s.userCodeAlg) - if err != nil { - return nil, err - } - phoneCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, s.userCodeAlg) - if err != nil { - return nil, err - } - human, err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, AddHumanUserRequestToDomain(req), initCodeGenerator, phoneCodeGenerator) + lang, err := language.Parse(req.Profile.PreferredLanguage) + logging.OnError(err).Debug("unable to parse language") + + details, err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, &command.AddHuman{ + Username: req.UserName, + FirstName: req.Profile.FirstName, + LastName: req.Profile.LastName, + NickName: req.Profile.NickName, + DisplayName: req.Profile.DisplayName, + Email: command.Email{ + Address: req.Email.Email, + Verified: req.Email.IsEmailVerified, + }, + PreferredLang: lang, + Gender: user_grpc.GenderToDomain(req.Profile.Gender), + Phone: command.Phone{ + Number: req.Phone.Phone, + Verified: req.Phone.IsPhoneVerified, + }, + Password: req.InitialPassword, + PasswordChangeRequired: true, + Passwordless: false, + Register: false, + ExternalIDP: false, + }) if err != nil { return nil, err } return &mgmt_pb.AddHumanUserResponse{ - UserId: human.AggregateID, + UserId: details.ID, Details: obj_grpc.AddToDetailsPb( - human.Sequence, - human.ChangeDate, - human.ResourceOwner, + details.Sequence, + details.EventDate, + details.ResourceOwner, ), }, nil } @@ -763,7 +781,7 @@ func (s *Server) GetPersonalAccessTokenByIDs(ctx context.Context, req *mgmt_pb.G return nil, err } return &mgmt_pb.GetPersonalAccessTokenByIDsResponse{ - Token: user.PersonalAccessTokenToPb(token), + Token: user_grpc.PersonalAccessTokenToPb(token), }, nil } diff --git a/internal/api/grpc/server/middleware/instance_interceptor_test.go b/internal/api/grpc/server/middleware/instance_interceptor_test.go index 6ecdcbcf9f..dab78be056 100644 --- a/internal/api/grpc/server/middleware/instance_interceptor_test.go +++ b/internal/api/grpc/server/middleware/instance_interceptor_test.go @@ -172,3 +172,7 @@ func (m *mockInstance) ProjectID() string { func (m *mockInstance) ConsoleClientID() string { return "consoleClientID" } + +func (m *mockInstance) RequestedDomain() string { + return "localhost" +} diff --git a/internal/api/http/middleware/instance_interceptor_test.go b/internal/api/http/middleware/instance_interceptor_test.go index 83778fae98..625d3c748c 100644 --- a/internal/api/http/middleware/instance_interceptor_test.go +++ b/internal/api/http/middleware/instance_interceptor_test.go @@ -256,3 +256,7 @@ func (m *mockInstance) ProjectID() string { func (m *mockInstance) ConsoleClientID() string { return "consoleClientID" } + +func (m *mockInstance) RequestedDomain() string { + return "zitadel.cloud" +} diff --git a/internal/command/command.go b/internal/command/command.go index d59a4b3f8c..de87249723 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -48,6 +48,18 @@ type Commands struct { privateKeyLifetime time.Duration publicKeyLifetime time.Duration tokenVerifier orgFeatureChecker + + v2 *commandNew +} + +type commandNew struct { + es *eventstore.Eventstore + userPasswordAlg crypto.HashAlgorithm + phoneAlg crypto.EncryptionAlgorithm + emailAlg crypto.EncryptionAlgorithm + initCodeAlg crypto.EncryptionAlgorithm + zitadelRoles []authz.RoleMapping + id id.Generator } type orgFeatureChecker interface { @@ -64,6 +76,7 @@ func StartCommands(es *eventstore.Eventstore, otpEncryption, smtpEncryption, smsEncryption, + userEncryption, domainVerificationEncryption, oidcEncryption crypto.EncryptionAlgorithm, ) (repo *Commands, err error) { @@ -81,7 +94,9 @@ func StartCommands(es *eventstore.Eventstore, smsCrypto: smsEncryption, domainVerificationAlg: domainVerificationEncryption, keyAlgorithm: oidcEncryption, + v2: NewCommandV2(es, defaults, userEncryption, authZConfig.RolePermissionMappings), } + instance_repo.RegisterEventMappers(repo.eventstore) org.RegisterEventMappers(repo.eventstore) usr_repo.RegisterEventMappers(repo.eventstore) @@ -113,6 +128,31 @@ func StartCommands(es *eventstore.Eventstore, return repo, nil } +func NewCommandV2( + es *eventstore.Eventstore, + defaults sd.SystemDefaults, + userAlg crypto.EncryptionAlgorithm, + zitadelRoles []authz.RoleMapping, +) *commandNew { + instance_repo.RegisterEventMappers(es) + org.RegisterEventMappers(es) + usr_repo.RegisterEventMappers(es) + usr_grant_repo.RegisterEventMappers(es) + proj_repo.RegisterEventMappers(es) + keypair.RegisterEventMappers(es) + action.RegisterEventMappers(es) + + return &commandNew{ + es: es, + userPasswordAlg: crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost), + initCodeAlg: userAlg, + phoneAlg: userAlg, + emailAlg: userAlg, + zitadelRoles: zitadelRoles, + id: id.SonyFlakeGenerator, + } +} + func AppendAndReduce(object interface { AppendEvents(...eventstore.Event) Reduce() error diff --git a/internal/command/crypto.go b/internal/command/crypto.go new file mode 100644 index 0000000000..fafbe96124 --- /dev/null +++ b/internal/command/crypto.go @@ -0,0 +1,67 @@ +package command + +import ( + "context" + "time" + + "github.com/caos/zitadel/internal/command/preparation" + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" +) + +func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, expiry time.Duration, err error) { + config, err := secretGeneratorConfig(ctx, filter, typ) + if err != nil { + return nil, -1, err + } + + switch a := alg.(type) { + case crypto.HashAlgorithm: + value, _, err = crypto.NewCode(crypto.NewHashGenerator(*config, a)) + case crypto.EncryptionAlgorithm: + value, _, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a)) + default: + return nil, -1, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal") + } + if err != nil { + return nil, -1, err + } + return value, config.Expiry, nil +} + +func newCryptoCodeWithPlain(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, plain string, err error) { + config, err := secretGeneratorConfig(ctx, filter, typ) + if err != nil { + return nil, "", err + } + + switch a := alg.(type) { + case crypto.HashAlgorithm: + return crypto.NewCode(crypto.NewHashGenerator(*config, a)) + case crypto.EncryptionAlgorithm: + return crypto.NewCode(crypto.NewEncryptionGenerator(*config, a)) + } + + return nil, "", errors.ThrowInvalidArgument(nil, "V2-NGESt", "Errors.Internal") +} + +func secretGeneratorConfig(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType) (*crypto.GeneratorConfig, error) { + wm := NewInstanceSecretGeneratorConfigWriteModel(ctx, typ) + events, err := filter(ctx, wm.Query()) + if err != nil { + return nil, err + } + wm.AppendEvents(events...) + if err := wm.Reduce(); err != nil { + return nil, err + } + return &crypto.GeneratorConfig{ + Length: wm.Length, + Expiry: wm.Expiry, + IncludeLowerLetters: wm.IncludeLowerLetters, + IncludeUpperLetters: wm.IncludeUpperLetters, + IncludeDigits: wm.IncludeDigits, + IncludeSymbols: wm.IncludeSymbols, + }, nil +} diff --git a/internal/command/email.go b/internal/command/email.go new file mode 100644 index 0000000000..970b4a606f --- /dev/null +++ b/internal/command/email.go @@ -0,0 +1,23 @@ +package command + +import ( + "context" + "time" + + "github.com/caos/zitadel/internal/command/preparation" + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" +) + +type Email struct { + Address string + Verified bool +} + +func (e *Email) Valid() bool { + return e.Address != "" && domain.EmailRegex.MatchString(e.Address) +} + +func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) { + return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyEmailCode, alg) +} diff --git a/internal/command/v2/instance.go b/internal/command/instance.go similarity index 60% rename from internal/command/v2/instance.go rename to internal/command/instance.go index ba8fdfc6d9..44ec11a98d 100644 --- a/internal/command/v2/instance.go +++ b/internal/command/instance.go @@ -6,8 +6,10 @@ import ( "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/api/ui/console" - "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/command/preparation" + "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/id" "github.com/caos/zitadel/internal/repository/instance" @@ -53,6 +55,16 @@ type InstanceSetup struct { ActionsAllowed domain.ActionsAllowed MaxActions int } + SecretGenerators struct { + PasswordSaltCost uint + ClientSecret *crypto.GeneratorConfig + InitializeUserCode *crypto.GeneratorConfig + EmailVerificationCode *crypto.GeneratorConfig + PhoneVerificationCode *crypto.GeneratorConfig + PasswordVerificationCode *crypto.GeneratorConfig + PasswordlessInitCode *crypto.GeneratorConfig + DomainVerification *crypto.GeneratorConfig + } PasswordComplexityPolicy struct { MinLength uint64 HasLowercase bool @@ -110,15 +122,11 @@ type ZitadelConfig struct { IsDevMode bool BaseURL string - projectID string - mgmtID string - mgmtClientID string - adminID string - adminClientID string - authID string - authClientID string - consoleID string - consoleClientID string + projectID string + mgmtAppID string + adminAppID string + authAppID string + consoleAppID string } func (s *InstanceSetup) generateIDs() (err error) { @@ -127,50 +135,35 @@ func (s *InstanceSetup) generateIDs() (err error) { return err } - s.Zitadel.mgmtID, err = id.SonyFlakeGenerator.Next() - if err != nil { - return err - } - s.Zitadel.mgmtClientID, err = domain.NewClientID(id.SonyFlakeGenerator, zitadelProjectName) + s.Zitadel.mgmtAppID, err = id.SonyFlakeGenerator.Next() if err != nil { return err } - s.Zitadel.adminID, err = id.SonyFlakeGenerator.Next() - if err != nil { - return err - } - s.Zitadel.adminClientID, err = domain.NewClientID(id.SonyFlakeGenerator, zitadelProjectName) + s.Zitadel.adminAppID, err = id.SonyFlakeGenerator.Next() if err != nil { return err } - s.Zitadel.authID, err = id.SonyFlakeGenerator.Next() - if err != nil { - return err - } - s.Zitadel.authClientID, err = domain.NewClientID(id.SonyFlakeGenerator, zitadelProjectName) + s.Zitadel.authAppID, err = id.SonyFlakeGenerator.Next() if err != nil { return err } - s.Zitadel.consoleID, err = id.SonyFlakeGenerator.Next() - if err != nil { - return err - } - s.Zitadel.consoleClientID, err = domain.NewClientID(id.SonyFlakeGenerator, zitadelProjectName) + s.Zitadel.consoleAppID, err = id.SonyFlakeGenerator.Next() if err != nil { return err } return nil } -func (command *Command) SetUpInstance(ctx context.Context, setup *InstanceSetup) (*domain.ObjectDetails, error) { +func (c *commandNew) SetUpInstance(ctx context.Context, setup *InstanceSetup) (*domain.ObjectDetails, error) { instanceID, err := id.SonyFlakeGenerator.Next() if err != nil { return nil, err } - ctx = authz.SetCtxData(authz.WithInstanceID(ctx, instanceID), authz.CtxData{OrgID: instanceID, ResourceOwner: instanceID}) + requestedDomain := authz.GetInstance(ctx).RequestedDomain() + ctx = authz.SetCtxData(authz.WithRequestedDomain(authz.WithInstanceID(ctx, instanceID), requestedDomain), authz.CtxData{OrgID: instanceID, ResourceOwner: instanceID}) orgID, err := id.SonyFlakeGenerator.Next() if err != nil { @@ -219,6 +212,14 @@ func (command *Command) SetUpInstance(ctx context.Context, setup *InstanceSetup) setup.Features.ActionsAllowed, setup.Features.MaxActions, ), + addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeAppSecret, setup.SecretGenerators.ClientSecret), + addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeInitCode, setup.SecretGenerators.InitializeUserCode), + addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeVerifyEmailCode, setup.SecretGenerators.EmailVerificationCode), + addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeVerifyPhoneCode, setup.SecretGenerators.PhoneVerificationCode), + addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypePasswordResetCode, setup.SecretGenerators.PasswordVerificationCode), + addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypePasswordlessInitCode, setup.SecretGenerators.PasswordlessInitCode), + addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeVerifyDomain, setup.SecretGenerators.DomainVerification), + AddPasswordComplexityPolicy( instanceAgg, setup.PasswordComplexityPolicy.MinLength, @@ -280,72 +281,82 @@ func (command *Command) SetUpInstance(ctx context.Context, setup *InstanceSetup) validations = append(validations, SetInstanceCustomTexts(instanceAgg, msg)) } - validations = append(validations, - AddOrg(orgAgg, setup.Org.Name, command.iamDomain), - AddHumanCommand(userAgg, &setup.Org.Human, command.userPasswordAlg), - AddOrgMember(orgAgg, userID, domain.RoleOrgOwner), - AddInstanceMember(instanceAgg, userID, domain.RoleIAMOwner), + console := &addOIDCApp{ + AddApp: AddApp{ + Aggregate: *projectAgg, + ID: setup.Zitadel.consoleAppID, + Name: consoleAppName, + }, + Version: domain.OIDCVersionV1, + RedirectUris: []string{setup.Zitadel.BaseURL + consoleRedirectPath}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectUris: []string{setup.Zitadel.BaseURL + consolePostLogoutPath}, + DevMode: setup.Zitadel.IsDevMode, + AccessTokenType: domain.OIDCTokenTypeBearer, + AccessTokenRoleAssertion: false, + IDTokenRoleAssertion: false, + IDTokenUserinfoAssertion: false, + ClockSkew: 0, + } - AddProject(projectAgg, zitadelProjectName, userID, false, false, false, domain.PrivateLabelingSettingUnspecified), + validations = append(validations, + AddOrgCommand(ctx, orgAgg, setup.Org.Name), + addHumanCommand(userAgg, &setup.Org.Human, c.userPasswordAlg, c.phoneAlg, c.emailAlg, c.initCodeAlg), + c.AddOrgMember(orgAgg, userID, domain.RoleOrgOwner), + c.AddInstanceMember(instanceAgg, userID, domain.RoleIAMOwner), + + AddProjectCommand(projectAgg, zitadelProjectName, userID, false, false, false, domain.PrivateLabelingSettingUnspecified), SetIAMProject(instanceAgg, projectAgg.ID), - AddAPIApp( - *projectAgg, - setup.Zitadel.mgmtID, - mgmtAppName, - setup.Zitadel.mgmtClientID, + AddAPIAppCommand( + &addAPIApp{ + AddApp: AddApp{ + Aggregate: *projectAgg, + ID: setup.Zitadel.mgmtAppID, + Name: mgmtAppName, + }, + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, nil, - domain.APIAuthMethodTypePrivateKeyJWT, ), - AddAPIApp( - *projectAgg, - setup.Zitadel.adminID, - adminAppName, - setup.Zitadel.adminClientID, + AddAPIAppCommand( + &addAPIApp{ + AddApp: AddApp{ + Aggregate: *projectAgg, + ID: setup.Zitadel.adminAppID, + Name: adminAppName, + }, + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, nil, - domain.APIAuthMethodTypePrivateKeyJWT, ), - AddAPIApp( - *projectAgg, - setup.Zitadel.authID, - authAppName, - setup.Zitadel.authClientID, + AddAPIAppCommand( + &addAPIApp{ + AddApp: AddApp{ + Aggregate: *projectAgg, + ID: setup.Zitadel.authAppID, + Name: authAppName, + }, + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, nil, - domain.APIAuthMethodTypePrivateKeyJWT, ), - AddOIDCApp( - *projectAgg, - domain.OIDCVersionV1, - setup.Zitadel.consoleID, - consoleAppName, - setup.Zitadel.consoleClientID, - nil, - []string{setup.Zitadel.BaseURL + consoleRedirectPath}, - []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, - []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - domain.OIDCApplicationTypeUserAgent, - domain.OIDCAuthMethodTypeNone, - []string{setup.Zitadel.BaseURL + consolePostLogoutPath}, - setup.Zitadel.IsDevMode, - domain.OIDCTokenTypeBearer, - false, - false, - false, - 0, - nil, - ), - SetIAMConsoleID(instanceAgg, setup.Zitadel.consoleClientID), + AddOIDCAppCommand(console, nil), + SetIAMConsoleID(instanceAgg, &console.ClientID), ) - cmds, err := preparation.PrepareCommands(ctx, command.es.Filter, validations...) + cmds, err := preparation.PrepareCommands(ctx, c.es.Filter, validations...) if err != nil { return nil, err } - events, err := command.es.Push(ctx, cmds...) + events, err := c.es.Push(ctx, cmds...) if err != nil { return nil, err } @@ -368,7 +379,7 @@ func SetIAMProject(a *instance.Aggregate, projectID string) preparation.Validati } //SetIAMConsoleID defines the command to set the clientID of the Console App onto the instance -func SetIAMConsoleID(a *instance.Aggregate, clientID string) preparation.Validation { +func SetIAMConsoleID(a *instance.Aggregate, clientID *string) preparation.Validation { return func() (preparation.CreateCommands, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { return []eventstore.Command{ @@ -377,3 +388,25 @@ func SetIAMConsoleID(a *instance.Aggregate, clientID string) preparation.Validat }, nil } } + +func (c *Commands) setGlobalOrg(ctx context.Context, iamAgg *eventstore.Aggregate, iamWriteModel *InstanceWriteModel, orgID string) (eventstore.Command, error) { + err := c.eventstore.FilterToQueryReducer(ctx, iamWriteModel) + if err != nil { + return nil, err + } + if iamWriteModel.GlobalOrgID != "" { + return nil, errors.ThrowPreconditionFailed(nil, "IAM-HGG24", "Errors.IAM.GlobalOrgAlreadySet") + } + return instance.NewGlobalOrgSetEventEvent(ctx, iamAgg, orgID), nil +} + +func (c *Commands) setIAMProject(ctx context.Context, iamAgg *eventstore.Aggregate, iamWriteModel *InstanceWriteModel, projectID string) (eventstore.Command, error) { + err := c.eventstore.FilterToQueryReducer(ctx, iamWriteModel) + if err != nil { + return nil, err + } + if iamWriteModel.ProjectID != "" { + return nil, errors.ThrowPreconditionFailed(nil, "IAM-EGbw2", "Errors.IAM.IAMProjectAlreadySet") + } + return instance.NewIAMProjectSetEvent(ctx, iamAgg, projectID), nil +} diff --git a/internal/command/instance_domain.go b/internal/command/instance_domain.go index e9c44fafc4..c0db780047 100644 --- a/internal/command/instance_domain.go +++ b/internal/command/instance_domain.go @@ -5,12 +5,11 @@ import ( "strings" "github.com/caos/zitadel/internal/api/authz" - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/repository/instance" - + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/instance" ) func (c *Commands) AddInstanceDomain(ctx context.Context, instanceDomain string) (*domain.ObjectDetails, error) { @@ -52,7 +51,7 @@ func (c *Commands) RemoveInstanceDomain(ctx context.Context, instanceDomain stri func (c *Commands) addInstanceDomain(a *instance.Aggregate, instanceDomain string, generated bool) preparation.Validation { return func() (preparation.CreateCommands, error) { if instanceDomain = strings.TrimSpace(instanceDomain); instanceDomain == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "INST-28nlD", "Errors.Invalid.Argument") + return nil, errors.ThrowInvalidArgument(nil, "INST-28nlD", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { domainWriteModel, err := c.getInstanceDomainWriteModel(ctx, instanceDomain) @@ -60,7 +59,7 @@ func (c *Commands) addInstanceDomain(a *instance.Aggregate, instanceDomain strin return nil, err } if domainWriteModel.State == domain.InstanceDomainStateActive { - return nil, caos_errs.ThrowAlreadyExists(nil, "INST-i2nl", "Errors.Instance.Domain.AlreadyExists") + return nil, errors.ThrowAlreadyExists(nil, "INST-i2nl", "Errors.Instance.Domain.AlreadyExists") } return []eventstore.Command{instance.NewDomainAddedEvent(ctx, &a.Aggregate, instanceDomain, generated)}, nil }, nil @@ -70,7 +69,7 @@ func (c *Commands) addInstanceDomain(a *instance.Aggregate, instanceDomain strin func (c *Commands) removeInstanceDomain(a *instance.Aggregate, instanceDomain string) preparation.Validation { return func() (preparation.CreateCommands, error) { if instanceDomain = strings.TrimSpace(instanceDomain); instanceDomain == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "INST-39nls", "Errors.Invalid.Argument") + return nil, errors.ThrowInvalidArgument(nil, "INST-39nls", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { domainWriteModel, err := c.getInstanceDomainWriteModel(ctx, instanceDomain) @@ -78,10 +77,10 @@ func (c *Commands) removeInstanceDomain(a *instance.Aggregate, instanceDomain st return nil, err } if domainWriteModel.State != domain.InstanceDomainStateActive { - return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-8ls9f", "Errors.Instance.Domain.NotFound") + return nil, errors.ThrowNotFound(nil, "INSTANCE-8ls9f", "Errors.Instance.Domain.NotFound") } if domainWriteModel.Generated { - return nil, caos_errs.ThrowPreconditionFailed(nil, "INSTANCE-9hn3n", "Errors.Instance.Domain.GeneratedNotRemovable") + return nil, errors.ThrowPreconditionFailed(nil, "INSTANCE-9hn3n", "Errors.Instance.Domain.GeneratedNotRemovable") } return []eventstore.Command{instance.NewDomainRemovedEvent(ctx, &a.Aggregate, instanceDomain)}, nil }, nil diff --git a/internal/command/v2/instance_domain_policy.go b/internal/command/instance_domain_policy.go similarity index 91% rename from internal/command/v2/instance_domain_policy.go rename to internal/command/instance_domain_policy.go index 0a6237c5fb..4bd7a10667 100644 --- a/internal/command/v2/instance_domain_policy.go +++ b/internal/command/instance_domain_policy.go @@ -3,7 +3,7 @@ package command import ( "context" - "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/instance" ) diff --git a/internal/command/v2/instance_email_template.go b/internal/command/instance_email_template.go similarity index 94% rename from internal/command/v2/instance_email_template.go rename to internal/command/instance_email_template.go index 036948da31..45d9500156 100644 --- a/internal/command/v2/instance_email_template.go +++ b/internal/command/instance_email_template.go @@ -5,8 +5,7 @@ import ( "golang.org/x/text/language" - "github.com/caos/zitadel/internal/command" - "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/instance" @@ -95,8 +94,8 @@ func SetInstanceCustomTexts( } } -func existingInstanceCustomMessageText(ctx context.Context, filter preparation.FilterToQueryReducer, textType string, lang language.Tag) (*command.InstanceCustomMessageTextWriteModel, error) { - writeModel := command.NewInstanceCustomMessageTextWriteModel(ctx, textType, lang) +func existingInstanceCustomMessageText(ctx context.Context, filter preparation.FilterToQueryReducer, textType string, lang language.Tag) (*InstanceCustomMessageTextWriteModel, error) { + writeModel := NewInstanceCustomMessageTextWriteModel(ctx, textType, lang) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err diff --git a/internal/command/instance_features.go b/internal/command/instance_features.go index 9bb1713930..215dafac8b 100644 --- a/internal/command/instance_features.go +++ b/internal/command/instance_features.go @@ -2,13 +2,97 @@ package command import ( "context" + "time" - caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/repository/instance" - + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/instance" ) +func SetDefaultFeatures( + a *instance.Aggregate, + tierName, + tierDescription string, + state domain.FeaturesState, + stateDescription string, + retention time.Duration, + loginPolicyFactors, + loginPolicyIDP, + loginPolicyPasswordless, + loginPolicyRegistration, + loginPolicyUsernameLogin, + loginPolicyPasswordReset, + passwordComplexityPolicy, + labelPolicyPrivateLabel, + labelPolicyWatermark, + customDomain, + privacyPolicy, + metadataUser, + customTextMessage, + customTextLogin, + lockoutPolicy bool, + actionsAllowed domain.ActionsAllowed, + maxActions int, +) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if !state.Valid() || state == domain.FeaturesStateUnspecified || state == domain.FeaturesStateRemoved { + return nil, errors.ThrowInvalidArgument(nil, "INSTA-d3r1s", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel, err := defaultFeatures(ctx, filter) + if err != nil { + return nil, err + } + event, hasChanged := writeModel.NewSetEvent(ctx, &a.Aggregate, + tierName, + tierDescription, + state, + stateDescription, + retention, + loginPolicyFactors, + loginPolicyIDP, + loginPolicyPasswordless, + loginPolicyRegistration, + loginPolicyUsernameLogin, + loginPolicyPasswordReset, + passwordComplexityPolicy, + labelPolicyPrivateLabel, + labelPolicyWatermark, + customDomain, + privacyPolicy, + metadataUser, + customTextMessage, + customTextLogin, + lockoutPolicy, + actionsAllowed, + maxActions, + ) + if !hasChanged { + return nil, errors.ThrowPreconditionFailed(nil, "INSTA-GE4h2", "Errors.Features.NotChanged") + } + return []eventstore.Command{ + event, + }, nil + }, nil + } +} + +func defaultFeatures(ctx context.Context, filter preparation.FilterToQueryReducer) (*InstanceFeaturesWriteModel, error) { + features := NewInstanceFeaturesWriteModel(ctx) + events, err := filter(ctx, features.Query()) + if err != nil { + return nil, err + } + if len(events) == 0 { + return features, nil + } + features.AppendEvents(events...) + err = features.Reduce() + return features, err +} + func (c *Commands) SetDefaultFeatures(ctx context.Context, features *domain.Features) (*domain.ObjectDetails, error) { existingFeatures := NewInstanceFeaturesWriteModel(ctx) setEvent, err := c.setDefaultFeatures(ctx, existingFeatures, features) @@ -59,7 +143,7 @@ func (c *Commands) setDefaultFeatures(ctx context.Context, existingFeatures *Ins features.MaxActions, ) if !hasChanged { - return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged") + return nil, errors.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged") } return setEvent, nil } diff --git a/internal/command/v2/instance_features_test.go b/internal/command/instance_features_test.go similarity index 100% rename from internal/command/v2/instance_features_test.go rename to internal/command/instance_features_test.go diff --git a/internal/command/v2/instance_label_policy.go b/internal/command/instance_label_policy.go similarity index 95% rename from internal/command/v2/instance_label_policy.go rename to internal/command/instance_label_policy.go index e48609108d..ba89429f77 100644 --- a/internal/command/v2/instance_label_policy.go +++ b/internal/command/instance_label_policy.go @@ -3,7 +3,7 @@ package command import ( "context" - "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/instance" ) diff --git a/internal/command/v2/instance_lockout_policy.go b/internal/command/instance_lockout_policy.go similarity index 91% rename from internal/command/v2/instance_lockout_policy.go rename to internal/command/instance_lockout_policy.go index 1860894cee..932b4e8789 100644 --- a/internal/command/v2/instance_lockout_policy.go +++ b/internal/command/instance_lockout_policy.go @@ -3,7 +3,7 @@ package command import ( "context" - "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/instance" ) diff --git a/internal/command/v2/instance_login_policy.go b/internal/command/instance_login_policy.go similarity index 97% rename from internal/command/v2/instance_login_policy.go rename to internal/command/instance_login_policy.go index 9da82fac83..4fe15abb09 100644 --- a/internal/command/v2/instance_login_policy.go +++ b/internal/command/instance_login_policy.go @@ -4,7 +4,7 @@ import ( "context" "time" - "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/instance" diff --git a/internal/command/instance_member.go b/internal/command/instance_member.go index a4bbe45f9b..08a9dc45ff 100644 --- a/internal/command/instance_member.go +++ b/internal/command/instance_member.go @@ -4,15 +4,71 @@ import ( "context" "reflect" - "github.com/caos/zitadel/internal/eventstore" - + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/instance" "github.com/caos/zitadel/internal/telemetry/tracing" ) +func (c *commandNew) AddInstanceMember(a *instance.Aggregate, userID string, roles ...string) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if userID == "" { + return nil, errors.ThrowInvalidArgument(nil, "INSTA-SDSfs", "Errors.Invalid.Argument") + } + if len(domain.CheckForInvalidRoles(roles, domain.IAMRolePrefix, c.zitadelRoles)) > 0 { + return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-4m0fS", "Errors.IAM.MemberInvalid") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + if exists, err := ExistsUser(ctx, filter, userID, ""); err != nil || !exists { + return nil, errors.ThrowNotFound(err, "INSTA-GSXOn", "Errors.User.NotFound") + } + if isMember, err := IsInstanceMember(ctx, filter, a.ID, userID); err != nil || isMember { + return nil, errors.ThrowAlreadyExists(err, "INSTA-pFDwe", "Errors.Instance.Member.AlreadyExists") + } + return []eventstore.Command{instance.NewMemberAddedEvent(ctx, &a.Aggregate, userID, roles...)}, nil + }, + nil + } +} + +func IsInstanceMember(ctx context.Context, filter preparation.FilterToQueryReducer, instanceID, userID string) (isMember bool, err error) { + events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + OrderAsc(). + AddQuery(). + AggregateIDs(instanceID). + AggregateTypes(instance.AggregateType). + EventTypes( + instance.MemberAddedEventType, + instance.MemberRemovedEventType, + instance.MemberCascadeRemovedEventType, + ).Builder()) + if err != nil { + return false, err + } + + for _, event := range events { + switch e := event.(type) { + case *instance.MemberAddedEvent: + if e.UserID == userID { + isMember = true + } + case *instance.MemberRemovedEvent: + if e.UserID == userID { + isMember = false + } + case *instance.MemberCascadeRemovedEvent: + if e.UserID == userID { + isMember = false + } + } + } + + return isMember, nil +} + func (c *Commands) AddInstanceMember(ctx context.Context, member *domain.Member) (*domain.Member, error) { if member.UserID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-Mf83b", "Errors.IAM.MemberInvalid") diff --git a/internal/command/v2/instance_password_age_policy.go b/internal/command/instance_password_age_policy.go similarity index 91% rename from internal/command/v2/instance_password_age_policy.go rename to internal/command/instance_password_age_policy.go index 46fc711dcc..964fd5f6c4 100644 --- a/internal/command/v2/instance_password_age_policy.go +++ b/internal/command/instance_password_age_policy.go @@ -3,7 +3,7 @@ package command import ( "context" - "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/instance" ) diff --git a/internal/command/v2/instance_password_complexity_policy.go b/internal/command/instance_password_complexity_policy.go similarity index 92% rename from internal/command/v2/instance_password_complexity_policy.go rename to internal/command/instance_password_complexity_policy.go index 9ce04c2f36..78caef22d8 100644 --- a/internal/command/v2/instance_password_complexity_policy.go +++ b/internal/command/instance_password_complexity_policy.go @@ -3,7 +3,7 @@ package command import ( "context" - "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/instance" ) diff --git a/internal/command/v2/instance_privacy_policy.go b/internal/command/instance_privacy_policy.go similarity index 91% rename from internal/command/v2/instance_privacy_policy.go rename to internal/command/instance_privacy_policy.go index 00a2b7f9c9..c16cabd043 100644 --- a/internal/command/v2/instance_privacy_policy.go +++ b/internal/command/instance_privacy_policy.go @@ -3,7 +3,7 @@ package command import ( "context" - "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/instance" ) diff --git a/internal/command/main_test.go b/internal/command/main_test.go index d064a1dfaa..4cae45781b 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -173,7 +173,7 @@ func eventFromEventPusherWithInstanceID(instanceID string, event eventstore.Comm AggregateID: event.Aggregate().ID, AggregateType: repository.AggregateType(event.Aggregate().Type), ResourceOwner: sql.NullString{String: event.Aggregate().ResourceOwner, Valid: event.Aggregate().ResourceOwner != ""}, - InstanceID: sql.NullString{String: instanceID, Valid: instanceID != ""}, + InstanceID: instanceID, } } diff --git a/internal/command/org.go b/internal/command/org.go index 4c05a014f2..b4c5f746d1 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -2,14 +2,78 @@ package command import ( "context" + "strings" + "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/id" "github.com/caos/zitadel/internal/repository/org" + user_repo "github.com/caos/zitadel/internal/repository/user" ) +type OrgSetup struct { + Name string + Human AddHuman +} + +func (c *commandNew) SetUpOrg(ctx context.Context, o *OrgSetup) (*domain.ObjectDetails, error) { + orgID, err := id.SonyFlakeGenerator.Next() + if err != nil { + return nil, err + } + + userID, err := id.SonyFlakeGenerator.Next() + if err != nil { + return nil, err + } + + orgAgg := org.NewAggregate(orgID, orgID) + userAgg := user_repo.NewAggregate(userID, orgID) + + cmds, err := preparation.PrepareCommands(ctx, c.es.Filter, + AddOrgCommand(ctx, orgAgg, o.Name), + addHumanCommand(userAgg, &o.Human, c.userPasswordAlg, c.phoneAlg, c.emailAlg, c.initCodeAlg), + c.AddOrgMember(orgAgg, userID, domain.RoleOrgOwner), + ) + if err != nil { + return nil, err + } + + events, err := c.es.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: orgID, + }, nil +} + +//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) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if name = strings.TrimSpace(name); name == "" { + return nil, errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument") + } + defaultDomain := domain.NewIAMDomainName(name, authz.GetInstance(ctx).RequestedDomain()) + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + return []eventstore.Command{ + org.NewOrgAddedEvent(ctx, &a.Aggregate, name), + org.NewDomainAddedEvent(ctx, &a.Aggregate, defaultDomain), + org.NewDomainVerifiedEvent(ctx, &a.Aggregate, defaultDomain), + org.NewDomainPrimarySetEvent(ctx, &a.Aggregate, defaultDomain), + }, nil + }, nil + } +} + func (c *Commands) getOrg(ctx context.Context, orgID string) (*domain.Org, error) { writeModel, err := c.getOrgWriteModelByID(ctx, orgID) if err != nil { diff --git a/internal/command/org_domain.go b/internal/command/org_domain.go index 79b715e260..5c3b3cb774 100644 --- a/internal/command/org_domain.go +++ b/internal/command/org_domain.go @@ -2,20 +2,92 @@ package command import ( "context" + "strings" "github.com/caos/logging" + errs "errors" + http_utils "github.com/caos/zitadel/internal/api/http" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/org" ) +func AddOrgDomain(a *org.Aggregate, domain string) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if domain = strings.TrimSpace(domain); domain == "" { + return nil, errors.ThrowInvalidArgument(nil, "ORG-r3h4J", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + existing, err := orgDomain(ctx, filter, a.ID, domain) + if err != nil && !errs.Is(err, errors.ThrowNotFound(nil, "", "")) { + return nil, err + } + if existing != nil && existing.Verified { + return nil, errors.ThrowAlreadyExists(nil, "V2-e1wse", "Errors.Already.Exists") + } + return []eventstore.Command{org.NewDomainAddedEvent(ctx, &a.Aggregate, domain)}, nil + }, nil + } +} + +func VerifyOrgDomain(a *org.Aggregate, domain string) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if domain = strings.TrimSpace(domain); domain == "" { + return nil, errors.ThrowInvalidArgument(nil, "ORG-yqlVQ", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + // no checks required because unique constraints handle it + return []eventstore.Command{org.NewDomainVerifiedEvent(ctx, &a.Aggregate, domain)}, nil + }, nil + } +} + +func SetPrimaryOrgDomain(a *org.Aggregate, domain string) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if domain = strings.TrimSpace(domain); domain == "" { + return nil, errors.ThrowInvalidArgument(nil, "ORG-gmNqY", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + existing, err := orgDomain(ctx, filter, a.ID, domain) + if err != nil { + return nil, errors.ThrowAlreadyExists(err, "V2-d0Gyw", "Errors.Already.Exists") + } + if existing.Primary { + return nil, errors.ThrowPreconditionFailed(nil, "COMMA-FfoZO", "Errors.Org.DomainAlreadyPrimary") + } + if !existing.Verified { + return nil, errors.ThrowPreconditionFailed(nil, "COMMA-yKA80", "Errors.Org.DomainNotVerified") + } + return []eventstore.Command{org.NewDomainPrimarySetEvent(ctx, &a.Aggregate, domain)}, nil + }, nil + } +} + +func orgDomain(ctx context.Context, filter preparation.FilterToQueryReducer, orgID, domain string) (*OrgDomainWriteModel, error) { + wm := NewOrgDomainWriteModel(orgID, domain) + events, err := filter(ctx, wm.Query()) + if err != nil { + return nil, err + } + if len(events) == 0 { + return nil, errors.ThrowNotFound(nil, "COMMA-kFHpQ", "Errors.Org.DomainNotFound") + } + wm.AppendEvents(events...) + if err = wm.Reduce(); err != nil { + return nil, err + } + + return wm, nil +} + func (c *Commands) AddOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain, claimedUserIDs []string) (*domain.OrgDomain, error) { if !orgDomain.IsValid() { - return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain") + return nil, errors.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain") } domainWriteModel := NewOrgDomainWriteModel(orgDomain.AggregateID, orgDomain.Domain) orgAgg := OrgAggregateFromWriteModel(&domainWriteModel.WriteModel) @@ -36,21 +108,21 @@ func (c *Commands) AddOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain func (c *Commands) GenerateOrgDomainValidation(ctx context.Context, orgDomain *domain.OrgDomain) (token, url string, err error) { if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" { - return "", "", caos_errs.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain") + return "", "", errors.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain") } checkType, ok := orgDomain.ValidationType.CheckType() if !ok { - return "", "", caos_errs.ThrowInvalidArgument(nil, "ORG-Gsw31", "Errors.Org.DomainVerificationTypeInvalid") + return "", "", errors.ThrowInvalidArgument(nil, "ORG-Gsw31", "Errors.Org.DomainVerificationTypeInvalid") } domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain) if err != nil { return "", "", err } if domainWriteModel.State != domain.OrgDomainStateActive { - return "", "", caos_errs.ThrowNotFound(nil, "ORG-AGD31", "Errors.Org.DomainNotOnOrg") + return "", "", errors.ThrowNotFound(nil, "ORG-AGD31", "Errors.Org.DomainNotOnOrg") } if domainWriteModel.Verified { - return "", "", caos_errs.ThrowPreconditionFailed(nil, "ORG-HGw21", "Errors.Org.DomainAlreadyVerified") + return "", "", errors.ThrowPreconditionFailed(nil, "ORG-HGw21", "Errors.Org.DomainAlreadyVerified") } token, err = orgDomain.GenerateVerificationCode(c.domainVerificationGenerator) if err != nil { @@ -58,7 +130,7 @@ func (c *Commands) GenerateOrgDomainValidation(ctx context.Context, orgDomain *d } url, err = http_utils.TokenUrl(orgDomain.Domain, token, checkType) if err != nil { - return "", "", caos_errs.ThrowPreconditionFailed(err, "ORG-Bae21", "Errors.Org.DomainVerificationTypeInvalid") + return "", "", errors.ThrowPreconditionFailed(err, "ORG-Bae21", "Errors.Org.DomainVerificationTypeInvalid") } orgAgg := OrgAggregateFromWriteModel(&domainWriteModel.WriteModel) @@ -74,20 +146,20 @@ func (c *Commands) GenerateOrgDomainValidation(ctx context.Context, orgDomain *d func (c *Commands) ValidateOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain, claimedUserIDs []string) (*domain.ObjectDetails, error) { if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain") + return nil, errors.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain") } domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain) if err != nil { return nil, err } if domainWriteModel.State != domain.OrgDomainStateActive { - return nil, caos_errs.ThrowNotFound(nil, "ORG-Sjdi3", "Errors.Org.DomainNotOnOrg") + return nil, errors.ThrowNotFound(nil, "ORG-Sjdi3", "Errors.Org.DomainNotOnOrg") } if domainWriteModel.Verified { - return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-HGw21", "Errors.Org.DomainAlreadyVerified") + return nil, errors.ThrowPreconditionFailed(nil, "ORG-HGw21", "Errors.Org.DomainAlreadyVerified") } if domainWriteModel.ValidationCode == nil || domainWriteModel.ValidationType == domain.OrgDomainValidationTypeUnspecified { - return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-SFBB3", "Errors.Org.DomainVerificationMissing") + return nil, errors.ThrowPreconditionFailed(nil, "ORG-SFBB3", "Errors.Org.DomainVerificationMissing") } validationCode, err := crypto.DecryptString(domainWriteModel.ValidationCode, c.domainVerificationAlg) @@ -122,22 +194,22 @@ func (c *Commands) ValidateOrgDomain(ctx context.Context, orgDomain *domain.OrgD events = append(events, org.NewDomainVerificationFailedEvent(ctx, orgAgg, orgDomain.Domain)) _, err = c.eventstore.Push(ctx, events...) logging.LogWithFields("ORG-dhTE", "orgID", orgAgg.ID, "domain", orgDomain.Domain).OnError(err).Error("NewDomainVerificationFailedEvent push failed") - return nil, caos_errs.ThrowInvalidArgument(err, "ORG-GH3s", "Errors.Org.DomainVerificationFailed") + return nil, errors.ThrowInvalidArgument(err, "ORG-GH3s", "Errors.Org.DomainVerificationFailed") } func (c *Commands) SetPrimaryOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain) (*domain.ObjectDetails, error) { if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SsDG2", "Errors.Org.InvalidDomain") + return nil, errors.ThrowInvalidArgument(nil, "ORG-SsDG2", "Errors.Org.InvalidDomain") } domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain) if err != nil { return nil, err } if domainWriteModel.State != domain.OrgDomainStateActive { - return nil, caos_errs.ThrowNotFound(nil, "ORG-GDfA3", "Errors.Org.DomainNotOnOrg") + return nil, errors.ThrowNotFound(nil, "ORG-GDfA3", "Errors.Org.DomainNotOnOrg") } if !domainWriteModel.Verified { - return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-Ggd32", "Errors.Org.DomainNotVerified") + return nil, errors.ThrowPreconditionFailed(nil, "ORG-Ggd32", "Errors.Org.DomainNotVerified") } orgAgg := OrgAggregateFromWriteModel(&domainWriteModel.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, org.NewDomainPrimarySetEvent(ctx, orgAgg, orgDomain.Domain)) @@ -153,17 +225,17 @@ func (c *Commands) SetPrimaryOrgDomain(ctx context.Context, orgDomain *domain.Or func (c *Commands) RemoveOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain) (*domain.ObjectDetails, error) { if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SJsK3", "Errors.Org.InvalidDomain") + return nil, errors.ThrowInvalidArgument(nil, "ORG-SJsK3", "Errors.Org.InvalidDomain") } domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain) if err != nil { return nil, err } if domainWriteModel.State != domain.OrgDomainStateActive { - return nil, caos_errs.ThrowNotFound(nil, "ORG-GDfA3", "Errors.Org.DomainNotOnOrg") + return nil, errors.ThrowNotFound(nil, "ORG-GDfA3", "Errors.Org.DomainNotOnOrg") } if domainWriteModel.Primary { - return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-Sjdi3", "Errors.Org.PrimaryDomainNotDeletable") + return nil, errors.ThrowPreconditionFailed(nil, "ORG-Sjdi3", "Errors.Org.PrimaryDomainNotDeletable") } orgAgg := OrgAggregateFromWriteModel(&domainWriteModel.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, org.NewDomainRemovedEvent(ctx, orgAgg, orgDomain.Domain, domainWriteModel.Verified)) @@ -183,7 +255,7 @@ func (c *Commands) addOrgDomain(ctx context.Context, orgAgg *eventstore.Aggregat return nil, err } if addedDomain.State == domain.OrgDomainStateActive { - return nil, caos_errs.ThrowAlreadyExists(nil, "COMMA-Bd2jj", "Errors.Org.Domain.AlreadyExists") + return nil, errors.ThrowAlreadyExists(nil, "COMMA-Bd2jj", "Errors.Org.Domain.AlreadyExists") } events := []eventstore.Command{ diff --git a/internal/command/org_domain_test.go b/internal/command/org_domain_test.go index 907d005aa0..9908e3de08 100644 --- a/internal/command/org_domain_test.go +++ b/internal/command/org_domain_test.go @@ -9,9 +9,10 @@ import ( "golang.org/x/text/language" "github.com/caos/zitadel/internal/api/http" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/repository" "github.com/caos/zitadel/internal/eventstore/v1/models" @@ -21,6 +22,206 @@ import ( "github.com/caos/zitadel/internal/repository/user" ) +func TestAddDomain(t *testing.T) { + type args struct { + a *org.Aggregate + domain string + filter preparation.FilterToQueryReducer + } + + agg := org.NewAggregate("test", "test") + + tests := []struct { + name string + args args + want Want + }{ + { + name: "invalid domain", + args: args{ + a: agg, + domain: "", + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-r3h4J", "Errors.Invalid.Argument"), + }, + }, + { + name: "correct", + args: args{ + a: agg, + domain: "domain", + filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return nil, nil + }, + }, + want: Want{ + Commands: []eventstore.Command{ + org.NewDomainAddedEvent(context.Background(), &agg.Aggregate, "domain"), + }, + }, + }, + { + name: "already verified", + args: args{ + a: agg, + domain: "domain", + filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewDomainAddedEvent(ctx, &agg.Aggregate, "domain"), + org.NewDomainVerificationAddedEvent(ctx, &agg.Aggregate, "domain", domain.OrgDomainValidationTypeHTTP, nil), + org.NewDomainVerifiedEvent(ctx, &agg.Aggregate, "domain"), + }, nil + }, + }, + want: Want{ + CreateErr: errors.ThrowAlreadyExists(nil, "", ""), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + AssertValidation(t, AddOrgDomain(tt.args.a, tt.args.domain), tt.args.filter, tt.want) + }) + } +} + +func TestVerifyDomain(t *testing.T) { + type args struct { + a *org.Aggregate + domain string + } + + tests := []struct { + name string + args args + want Want + }{ + { + name: "invalid domain", + args: args{ + a: org.NewAggregate("test", "test"), + domain: "", + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-yqlVQ", "Errors.Invalid.Argument"), + }, + }, + { + name: "correct", + args: args{ + a: org.NewAggregate("test", "test"), + domain: "domain", + }, + want: Want{ + Commands: []eventstore.Command{ + org.NewDomainVerifiedEvent(context.Background(), &org.NewAggregate("test", "test").Aggregate, "domain"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + AssertValidation(t, VerifyOrgDomain(tt.args.a, tt.args.domain), nil, tt.want) + }) + } +} + +func TestSetDomainPrimary(t *testing.T) { + type args struct { + a *org.Aggregate + domain string + filter preparation.FilterToQueryReducer + } + + agg := org.NewAggregate("test", "test") + + tests := []struct { + name string + args args + want Want + }{ + { + name: "invalid domain", + args: args{ + a: agg, + domain: "", + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-gmNqY", "Errors.Invalid.Argument"), + }, + }, + { + name: "not exists", + args: args{ + a: agg, + domain: "domain", + filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return nil, nil + }, + }, + want: Want{ + CreateErr: errors.ThrowNotFound(nil, "", ""), + }, + }, + { + name: "not verified", + args: args{ + a: agg, + domain: "domain", + filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{org.NewDomainAddedEvent(ctx, &agg.Aggregate, "domain")}, nil + }, + }, + want: Want{ + CreateErr: errors.ThrowPreconditionFailed(nil, "", ""), + }, + }, + { + name: "already primary", + args: args{ + a: agg, + domain: "domain", + filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewDomainAddedEvent(ctx, &agg.Aggregate, "domain"), + org.NewDomainVerificationAddedEvent(ctx, &agg.Aggregate, "domain", domain.OrgDomainValidationTypeHTTP, nil), + org.NewDomainVerifiedEvent(ctx, &agg.Aggregate, "domain"), + org.NewDomainPrimarySetEvent(ctx, &agg.Aggregate, "domain"), + }, nil + }, + }, + want: Want{ + CreateErr: errors.ThrowPreconditionFailed(nil, "", ""), + }, + }, + { + name: "correct", + args: args{ + a: agg, + domain: "domain", + filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewDomainAddedEvent(ctx, &agg.Aggregate, "domain"), + org.NewDomainVerificationAddedEvent(ctx, &agg.Aggregate, "domain", domain.OrgDomainValidationTypeHTTP, nil), + org.NewDomainVerifiedEvent(ctx, &agg.Aggregate, "domain"), + }, nil + }, + }, + want: Want{ + Commands: []eventstore.Command{ + org.NewDomainPrimarySetEvent(context.Background(), &agg.Aggregate, "domain"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + AssertValidation(t, SetPrimaryOrgDomain(tt.args.a, tt.args.domain), tt.args.filter, tt.want) + }) + } +} + func TestCommandSide_AddOrgDomain(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore @@ -52,7 +253,7 @@ func TestCommandSide_AddOrgDomain(t *testing.T) { domain: &domain.OrgDomain{}, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -86,7 +287,7 @@ func TestCommandSide_AddOrgDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorAlreadyExists, + err: errors.IsErrorAlreadyExists, }, }, { @@ -187,7 +388,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -204,7 +405,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -224,7 +425,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -253,7 +454,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { }, }, res: res{ - err: caos_errs.IsNotFound, + err: errors.IsNotFound, }, }, { @@ -294,7 +495,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -462,7 +663,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -479,7 +680,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -508,7 +709,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsNotFound, + err: errors.IsNotFound, }, }, { @@ -549,7 +750,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -584,7 +785,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -642,7 +843,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -920,7 +1121,7 @@ func TestCommandSide_SetPrimaryDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -937,7 +1138,7 @@ func TestCommandSide_SetPrimaryDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -966,7 +1167,7 @@ func TestCommandSide_SetPrimaryDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsNotFound, + err: errors.IsNotFound, }, }, { @@ -1000,7 +1201,7 @@ func TestCommandSide_SetPrimaryDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -1107,7 +1308,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -1124,7 +1325,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -1153,7 +1354,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsNotFound, + err: errors.IsNotFound, }, }, { @@ -1199,7 +1400,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -1318,7 +1519,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) { } func invalidDomainVerification(domain, token, verifier string, checkType http.CheckType) error { - return caos_errs.ThrowInvalidArgument(nil, "HTTP-GH422", "Errors.Internal") + return errors.ThrowInvalidArgument(nil, "HTTP-GH422", "Errors.Internal") } func validDomainVerification(domain, token, verifier string, checkType http.CheckType) error { diff --git a/internal/command/org_member.go b/internal/command/org_member.go index 9df8b08396..b05f7d1be7 100644 --- a/internal/command/org_member.go +++ b/internal/command/org_member.go @@ -4,24 +4,84 @@ import ( "context" "reflect" - "github.com/caos/zitadel/internal/eventstore" - + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" - caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/org" "github.com/caos/zitadel/internal/telemetry/tracing" ) +func (c *commandNew) AddOrgMember(a *org.Aggregate, userID string, roles ...string) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if userID == "" { + return nil, errors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument") + } + if len(roles) == 0 { + return nil, errors.ThrowInvalidArgument(nil, "V2-PfYhb", "Errors.Invalid.Argument") + } + + if len(domain.CheckForInvalidRoles(roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(roles, domain.RoleSelfManagementGlobal, c.zitadelRoles)) > 0 { + return nil, errors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + if exists, err := ExistsUser(ctx, filter, userID, a.ID); err != nil || !exists { + return nil, errors.ThrowNotFound(err, "ORG-GoXOn", "Errors.User.NotFound") + } + if isMember, err := IsOrgMember(ctx, filter, a.ID, userID); err != nil || isMember { + return nil, errors.ThrowAlreadyExists(err, "ORG-poWwe", "Errors.Org.Member.AlreadyExists") + } + return []eventstore.Command{org.NewMemberAddedEvent(ctx, &a.Aggregate, userID, roles...)}, nil + }, + nil + } +} + +func IsOrgMember(ctx context.Context, filter preparation.FilterToQueryReducer, orgID, userID string) (isMember bool, err error) { + events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(orgID). + OrderAsc(). + AddQuery(). + AggregateIDs(orgID). + AggregateTypes(org.AggregateType). + EventTypes( + org.MemberAddedEventType, + org.MemberRemovedEventType, + org.MemberCascadeRemovedEventType, + ).Builder()) + if err != nil { + return false, err + } + + for _, event := range events { + switch e := event.(type) { + case *org.MemberAddedEvent: + if e.UserID == userID { + isMember = true + } + case *org.MemberRemovedEvent: + if e.UserID == userID { + isMember = false + } + case *org.MemberCascadeRemovedEvent: + if e.UserID == userID { + isMember = false + } + } + } + + return isMember, nil +} + func (c *Commands) AddOrgMember(ctx context.Context, member *domain.Member) (*domain.Member, error) { if member.UserID == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "Org-u8fkf", "Errors.Org.MemberInvalid") + return nil, errors.ThrowInvalidArgument(nil, "Org-u8fkf", "Errors.Org.MemberInvalid") } addedMember := NewOrgMemberWriteModel(member.AggregateID, member.UserID) orgAgg := OrgAggregateFromWriteModel(&addedMember.WriteModel) err := c.checkUserExists(ctx, addedMember.UserID, "") if err != nil { - return nil, caos_errs.ThrowPreconditionFailed(err, "Org-2H8ds", "Errors.User.NotFound") + return nil, errors.ThrowPreconditionFailed(err, "Org-2H8ds", "Errors.User.NotFound") } event, err := c.addOrgMember(ctx, orgAgg, addedMember, member) if err != nil { @@ -40,10 +100,10 @@ func (c *Commands) AddOrgMember(ctx context.Context, member *domain.Member) (*do func (c *Commands) addOrgMember(ctx context.Context, orgAgg *eventstore.Aggregate, addedMember *OrgMemberWriteModel, member *domain.Member) (eventstore.Command, error) { if !member.IsValid() { - return nil, caos_errs.ThrowInvalidArgument(nil, "Org-W8m4l", "Errors.Org.MemberInvalid") + return nil, errors.ThrowInvalidArgument(nil, "Org-W8m4l", "Errors.Org.MemberInvalid") } if len(domain.CheckForInvalidRoles(member.Roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(member.Roles, domain.RoleSelfManagementGlobal, c.zitadelRoles)) > 0 { - return nil, caos_errs.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid") + return nil, errors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid") } err := c.eventstore.FilterToQueryReducer(ctx, addedMember) if err != nil { @@ -59,10 +119,10 @@ func (c *Commands) addOrgMember(ctx context.Context, orgAgg *eventstore.Aggregat //ChangeOrgMember updates an existing member func (c *Commands) ChangeOrgMember(ctx context.Context, member *domain.Member) (*domain.Member, error) { if !member.IsValid() { - return nil, caos_errs.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid") + return nil, errors.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid") } if len(domain.CheckForInvalidRoles(member.Roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 { - return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-m9fG8", "Errors.Org.MemberInvalid") + return nil, errors.ThrowInvalidArgument(nil, "IAM-m9fG8", "Errors.Org.MemberInvalid") } existingMember, err := c.orgMemberWriteModelByID(ctx, member.AggregateID, member.UserID) @@ -71,7 +131,7 @@ func (c *Commands) ChangeOrgMember(ctx context.Context, member *domain.Member) ( } if reflect.DeepEqual(existingMember.Roles, member.Roles) { - return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-LiaZi", "Errors.Org.Member.RolesNotChanged") + return nil, errors.ThrowPreconditionFailed(nil, "Org-LiaZi", "Errors.Org.Member.RolesNotChanged") } orgAgg := OrgAggregateFromWriteModel(&existingMember.MemberWriteModel.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, org.NewMemberChangedEvent(ctx, orgAgg, member.UserID, member.Roles...)) diff --git a/internal/command/org_member_test.go b/internal/command/org_member_test.go index 689608ebf5..d4a5283905 100644 --- a/internal/command/org_member_test.go +++ b/internal/command/org_member_test.go @@ -8,8 +8,9 @@ import ( "golang.org/x/text/language" "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/repository" "github.com/caos/zitadel/internal/eventstore/v1/models" @@ -19,6 +20,271 @@ import ( "github.com/caos/zitadel/internal/repository/user" ) +func TestAddMember(t *testing.T) { + type args struct { + a *org.Aggregate + userID string + roles []string + zitadelRoles []authz.RoleMapping + filter preparation.FilterToQueryReducer + } + + ctx := context.Background() + agg := org.NewAggregate("test", "test") + + tests := []struct { + name string + args args + want Want + }{ + { + name: "no user id", + args: args{ + a: agg, + userID: "", + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"), + }, + }, + { + name: "no roles", + args: args{ + a: agg, + userID: "12342", + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "V2-PfYhb", "Errors.Invalid.Argument"), + }, + }, + { + name: "TODO: invalid roles", + args: args{ + a: agg, + userID: "123", + roles: []string{"ORG_OWNER"}, + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "Org-4N8es", ""), + }, + }, + { + name: "user not exists", + args: args{ + a: agg, + userID: "userID", + roles: []string{"ORG_OWNER"}, + zitadelRoles: []authz.RoleMapping{ + { + Role: "ORG_OWNER", + }, + }, + filter: NewMultiFilter().Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return nil, nil + }).Filter(), + }, + want: Want{ + CreateErr: errors.ThrowNotFound(nil, "ORG-GoXOn", "Errors.User.NotFound"), + }, + }, + { + name: "already member", + args: args{ + a: agg, + userID: "userID", + roles: []string{"ORG_OWNER"}, + zitadelRoles: []authz.RoleMapping{ + { + Role: "ORG_OWNER", + }, + }, + filter: NewMultiFilter(). + Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + user.NewMachineAddedEvent( + ctx, + &user.NewAggregate("id", "ro").Aggregate, + "userName", + "name", + "description", + true, + ), + }, nil + }). + Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewMemberAddedEvent( + ctx, + &org.NewAggregate("id", "ro").Aggregate, + "userID", + ), + }, nil + }). + Filter(), + }, + want: Want{ + CreateErr: errors.ThrowAlreadyExists(nil, "ORG-poWwe", "Errors.Org.Member.AlreadyExists"), + }, + }, + { + name: "correct", + args: args{ + a: agg, + userID: "userID", + roles: []string{"ORG_OWNER"}, + zitadelRoles: []authz.RoleMapping{ + { + Role: "ORG_OWNER", + }, + }, + filter: NewMultiFilter(). + Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + user.NewMachineAddedEvent( + ctx, + &user.NewAggregate("id", "ro").Aggregate, + "userName", + "name", + "description", + true, + ), + }, nil + }). + Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return nil, nil + }). + Filter(), + }, + want: Want{ + Commands: []eventstore.Command{ + org.NewMemberAddedEvent(ctx, &agg.Aggregate, "userID", "ORG_OWNER"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + AssertValidation(t, (&commandNew{zitadelRoles: tt.args.zitadelRoles}).AddOrgMember(tt.args.a, tt.args.userID, tt.args.roles...), tt.args.filter, tt.want) + }) + } +} + +func TestIsMember(t *testing.T) { + type args struct { + filter preparation.FilterToQueryReducer + orgID string + userID string + } + tests := []struct { + name string + args args + wantExists bool + wantErr bool + }{ + { + name: "no events", + args: args{ + filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{}, nil + }, + orgID: "orgID", + userID: "userID", + }, + wantExists: false, + wantErr: false, + }, + { + name: "member added", + args: args{ + filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewMemberAddedEvent( + context.Background(), + &org.NewAggregate("orgID", "ro").Aggregate, + "userID", + ), + }, nil + }, + orgID: "orgID", + userID: "userID", + }, + wantExists: true, + wantErr: false, + }, + { + name: "member removed", + args: args{ + filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewMemberAddedEvent( + context.Background(), + &org.NewAggregate("orgID", "ro").Aggregate, + "userID", + ), + org.NewMemberRemovedEvent( + context.Background(), + &org.NewAggregate("orgID", "ro").Aggregate, + "userID", + ), + }, nil + }, + orgID: "orgID", + userID: "userID", + }, + wantExists: false, + wantErr: false, + }, + { + name: "member cascade removed", + args: args{ + filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewMemberAddedEvent( + context.Background(), + &org.NewAggregate("orgID", "ro").Aggregate, + "userID", + ), + org.NewMemberCascadeRemovedEvent( + context.Background(), + &org.NewAggregate("orgID", "ro").Aggregate, + "userID", + ), + }, nil + }, + orgID: "orgID", + userID: "userID", + }, + wantExists: false, + wantErr: false, + }, + { + name: "error durring filter", + args: args{ + filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return nil, errors.ThrowInternal(nil, "PROJE-Op26p", "Errors.Internal") + }, + orgID: "orgID", + userID: "userID", + }, + wantExists: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotExists, err := IsOrgMember(context.Background(), tt.args.filter, tt.args.orgID, tt.args.userID) + if (err != nil) != tt.wantErr { + t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotExists != tt.wantExists { + t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists) + } + }) + } +} + func TestCommandSide_AddOrgMember(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore @@ -54,7 +320,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -76,7 +342,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -113,7 +379,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -163,7 +429,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorAlreadyExists, + err: errors.IsErrorAlreadyExists, }, }, { @@ -188,7 +454,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) { ), ), expectFilter(), - expectPushFailed(caos_errs.ThrowAlreadyExists(nil, "ERROR", "internal"), + expectPushFailed(errors.ThrowAlreadyExists(nil, "ERROR", "internal"), []*repository.Event{ eventFromEventPusher(org.NewMemberAddedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, @@ -216,7 +482,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorAlreadyExists, + err: errors.IsErrorAlreadyExists, }, }, { @@ -335,7 +601,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -356,7 +622,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -383,7 +649,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { }, }, res: res{ - err: caos_errs.IsNotFound, + err: errors.IsNotFound, }, }, { @@ -418,7 +684,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -530,7 +796,7 @@ func TestCommandSide_RemoveOrgMember(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -547,7 +813,7 @@ func TestCommandSide_RemoveOrgMember(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { diff --git a/internal/command/org_test.go b/internal/command/org_test.go index 690efefe47..333f5f7fe6 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -9,7 +9,7 @@ import ( "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/repository" "github.com/caos/zitadel/internal/eventstore/v1/models" @@ -20,6 +20,53 @@ import ( "github.com/caos/zitadel/internal/repository/user" ) +func TestAddOrg(t *testing.T) { + type args struct { + a *org.Aggregate + name string + } + + ctx := context.Background() + agg := org.NewAggregate("test", "test") + + tests := []struct { + name string + args args + want Want + }{ + { + name: "invalid domain", + args: args{ + a: agg, + name: "", + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument"), + }, + }, + { + name: "correct", + args: args{ + a: agg, + name: "caos ag", + }, + want: Want{ + Commands: []eventstore.Command{ + org.NewOrgAddedEvent(ctx, &agg.Aggregate, "caos ag"), + org.NewDomainAddedEvent(ctx, &agg.Aggregate, "caos-ag.localhost"), + org.NewDomainVerifiedEvent(ctx, &agg.Aggregate, "caos-ag.localhost"), + org.NewDomainPrimarySetEvent(ctx, &agg.Aggregate, "caos-ag.localhost"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + AssertValidation(t, AddOrgCommand(authz.WithRequestedDomain(context.Background(), "localhost"), tt.args.a, tt.args.name), nil, tt.want) + }) + } +} + func TestCommandSide_AddOrg(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore @@ -57,7 +104,7 @@ func TestCommandSide_AddOrg(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -101,7 +148,7 @@ func TestCommandSide_AddOrg(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -127,7 +174,7 @@ func TestCommandSide_AddOrg(t *testing.T) { ), ), expectFilterOrgMemberNotFound(), - expectPushFailed(caos_errs.ThrowAlreadyExists(nil, "id", "internal"), + expectPushFailed(errors.ThrowAlreadyExists(nil, "id", "internal"), []*repository.Event{ eventFromEventPusher(org.NewOrgAddedEvent( context.Background(), @@ -170,7 +217,7 @@ func TestCommandSide_AddOrg(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorAlreadyExists, + err: errors.IsErrorAlreadyExists, }, }, { @@ -196,7 +243,7 @@ func TestCommandSide_AddOrg(t *testing.T) { ), ), expectFilterOrgMemberNotFound(), - expectPushFailed(caos_errs.ThrowInternal(nil, "id", "internal"), + expectPushFailed(errors.ThrowInternal(nil, "id", "internal"), []*repository.Event{ eventFromEventPusher(org.NewOrgAddedEvent( context.Background(), @@ -239,7 +286,7 @@ func TestCommandSide_AddOrg(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsInternal, + err: errors.IsInternal, }, }, { @@ -374,7 +421,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { orgID: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -391,7 +438,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { name: "org", }, res: res{ - err: caos_errs.IsNotFound, + err: errors.IsNotFound, }, }, { @@ -409,7 +456,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { ), expectFilter(), expectPushFailed( - caos_errs.ThrowInternal(nil, "id", "message"), + errors.ThrowInternal(nil, "id", "message"), []*repository.Event{ eventFromEventPusher(org.NewOrgChangedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, "org", "neworg")), @@ -425,7 +472,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { name: "neworg", }, res: res{ - err: caos_errs.IsInternal, + err: errors.IsInternal, }, }, { @@ -596,7 +643,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { orgID: "org1", }, res: res{ - err: caos_errs.IsNotFound, + err: errors.IsNotFound, }, }, { @@ -622,7 +669,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { orgID: "org1", }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -638,7 +685,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { ), ), expectPushFailed( - caos_errs.ThrowInternal(nil, "id", "message"), + errors.ThrowInternal(nil, "id", "message"), []*repository.Event{ eventFromEventPusher(org.NewOrgDeactivatedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate)), @@ -651,7 +698,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { orgID: "org1", }, res: res{ - err: caos_errs.IsInternal, + err: errors.IsInternal, }, }, { @@ -731,7 +778,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { orgID: "org1", }, res: res{ - err: caos_errs.IsNotFound, + err: errors.IsNotFound, }, }, { @@ -753,7 +800,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { orgID: "org1", }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -774,7 +821,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { ), ), expectPushFailed( - caos_errs.ThrowInternal(nil, "id", "message"), + errors.ThrowInternal(nil, "id", "message"), []*repository.Event{ eventFromEventPusher(org.NewOrgReactivatedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, @@ -788,7 +835,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { orgID: "org1", }, res: res{ - err: caos_errs.IsInternal, + err: errors.IsInternal, }, }, { diff --git a/internal/command/phone.go b/internal/command/phone.go new file mode 100644 index 0000000000..efc9a7a22a --- /dev/null +++ b/internal/command/phone.go @@ -0,0 +1,33 @@ +package command + +import ( + "context" + "time" + + "github.com/caos/zitadel/internal/command/preparation" + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/ttacon/libphonenumber" +) + +type Phone struct { + Number string + Verified bool +} + +func FormatPhoneNumber(number string) (string, error) { + if number == "" { + return "", nil + } + phoneNr, err := libphonenumber.Parse(number, libphonenumber.UNKNOWN_REGION) + if err != nil { + return "", errors.ThrowInvalidArgument(nil, "EVENT-so0wa", "Errors.User.Phone.Invalid") + } + number = libphonenumber.Format(phoneNr, libphonenumber.E164) + return number, nil +} + +func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) { + return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg) +} diff --git a/internal/command/phone_test.go b/internal/command/phone_test.go new file mode 100644 index 0000000000..a993dd2cb4 --- /dev/null +++ b/internal/command/phone_test.go @@ -0,0 +1,57 @@ +package command + +import ( + "testing" + + "github.com/caos/zitadel/internal/errors" +) + +func TestFormatPhoneNumber(t *testing.T) { + type args struct { + number string + } + tests := []struct { + name string + args args + result *Phone + errFunc func(err error) bool + }{ + { + name: "invalid phone number", + args: args{ + number: "PhoneNumber", + }, + errFunc: errors.IsErrorInvalidArgument, + }, + { + name: "format phone +4171 xxx xx xx", + args: args{ + number: "+4171 123 45 67", + }, + result: &Phone{ + Number: "+41711234567", + }, + }, + { + name: "format non swiss phone +4371 xxx xx xx", + args: args{ + number: "+4371 123 45 67", + }, + result: &Phone{ + Number: "+43711234567", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + formatted, err := FormatPhoneNumber(tt.args.number) + + if tt.errFunc == nil && tt.result.Number != formatted { + t.Errorf("got wrong result: expected: %v, actual: %v ", tt.args.number, formatted) + } + if tt.errFunc != nil && !tt.errFunc(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} diff --git a/internal/command/v2/preparation/command.go b/internal/command/preparation/command.go similarity index 100% rename from internal/command/v2/preparation/command.go rename to internal/command/preparation/command.go diff --git a/internal/command/v2/preparation/command_test.go b/internal/command/preparation/command_test.go similarity index 100% rename from internal/command/v2/preparation/command_test.go rename to internal/command/preparation/command_test.go diff --git a/internal/command/v2/preparation_test.go b/internal/command/preparation_test.go similarity index 77% rename from internal/command/v2/preparation_test.go rename to internal/command/preparation_test.go index 5860ec25e0..bc7b79a342 100644 --- a/internal/command/v2/preparation_test.go +++ b/internal/command/preparation_test.go @@ -7,7 +7,7 @@ import ( "reflect" "testing" - "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/eventstore" ) @@ -18,13 +18,17 @@ type Want struct { Commands []eventstore.Command } +type CommandVerifier interface { + Validate(eventstore.Command) bool +} + //AssertValidation checks if the validation works as inteded func AssertValidation(t *testing.T, validation preparation.Validation, filter preparation.FilterToQueryReducer, want Want) { t.Helper() creates, err := validation() if !errors.Is(err, want.ValidationErr) { - t.Errorf("wrong validation err = %v, want %v", err, want.ValidationErr) + t.Errorf("wrong validation err = (%[1]T): %[1]v, want (%[2]T): %[2]v", err, want.ValidationErr) return } if err != nil { @@ -32,7 +36,7 @@ func AssertValidation(t *testing.T, validation preparation.Validation, filter pr } cmds, err := creates(context.Background(), filter) if !errors.Is(err, want.CreateErr) { - t.Errorf("wrong create err = %v, want %v", err, want.CreateErr) + t.Errorf("wrong create err = (%[1]T): %[1]v, want (%[2]T): %[2]v", err, want.CreateErr) return } if err != nil { @@ -45,6 +49,12 @@ func AssertValidation(t *testing.T, validation preparation.Validation, filter pr } for i, cmd := range want.Commands { + if v, ok := cmd.(CommandVerifier); ok { + if verified := v.Validate(cmds[i]); !verified { + t.Errorf("verification failed on command: = %v, want %v", cmds[i], cmd) + } + continue + } if !reflect.DeepEqual(cmd, cmds[i]) { t.Errorf("unexpected command: = %v, want %v", cmds[i], cmd) } diff --git a/internal/command/project.go b/internal/command/project.go index 2335cebfbd..27d186057e 100644 --- a/internal/command/project.go +++ b/internal/command/project.go @@ -2,9 +2,12 @@ package command import ( "context" + "strings" "github.com/caos/logging" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/project" @@ -52,6 +55,57 @@ func (c *Commands) addProject(ctx context.Context, projectAdd *domain.Project, r return events, addedProject, nil } +func AddProjectCommand( + a *project.Aggregate, + name string, + owner string, + projectRoleAssertion bool, + projectRoleCheck bool, + hasProjectCheck bool, + privateLabelingSetting domain.PrivateLabelingSetting, +) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if name = strings.TrimSpace(name); name == "" { + return nil, errors.ThrowInvalidArgument(nil, "PROJE-C01yo", "Errors.Invalid.Argument") + } + if !privateLabelingSetting.Valid() { + return nil, errors.ThrowInvalidArgument(nil, "PROJE-AO52V", "Errors.Invalid.Argument") + } + if owner == "" { + return nil, errors.ThrowPreconditionFailed(nil, "PROJE-hzxwo", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + return []eventstore.Command{ + project.NewProjectAddedEvent(ctx, &a.Aggregate, + name, + projectRoleAssertion, + projectRoleCheck, + hasProjectCheck, + privateLabelingSetting, + ), + project.NewProjectMemberAddedEvent(ctx, &a.Aggregate, + owner, + domain.RoleProjectOwner), + }, nil + }, nil + } +} + +func projectWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer, projectID, resourceOwner string) (project *ProjectWriteModel, err error) { + project = NewProjectWriteModel(projectID, resourceOwner) + events, err := filter(ctx, project.Query()) + if err != nil { + return nil, err + } + + project.AppendEvents(events...) + if err := project.Reduce(); err != nil { + return nil, err + } + + return project, nil +} + func (c *Commands) getProjectByID(ctx context.Context, projectID, resourceOwner string) (*domain.Project, error) { projectWriteModel, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner) if err != nil { diff --git a/internal/command/project_application.go b/internal/command/project_application.go index 80532f0109..3093826d4e 100644 --- a/internal/command/project_application.go +++ b/internal/command/project_application.go @@ -3,11 +3,23 @@ package command import ( "context" + "github.com/caos/zitadel/internal/command/preparation" + "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/repository/project" ) +type AddApp struct { + Aggregate project.Aggregate + ID string + Name string +} + +func newAppClientSecret(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.HashAlgorithm) (value *crypto.CryptoValue, plain string, err error) { + return newCryptoCodeWithPlain(ctx, filter, domain.SecretGeneratorTypeAppSecret, alg) +} + func (c *Commands) ChangeApplication(ctx context.Context, projectID string, appChange domain.Application, resourceOwner string) (*domain.ObjectDetails, error) { if projectID == "" || appChange.GetAppID() == "" || appChange.GetApplicationName() == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.App.Invalid") diff --git a/internal/command/project_application_api.go b/internal/command/project_application_api.go index 86857793da..8660cd3e26 100644 --- a/internal/command/project_application_api.go +++ b/internal/command/project_application_api.go @@ -2,24 +2,84 @@ package command import ( "context" + "strings" "github.com/caos/logging" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/project" + "github.com/caos/zitadel/internal/id" + project_repo "github.com/caos/zitadel/internal/repository/project" "github.com/caos/zitadel/internal/telemetry/tracing" ) +type addAPIApp struct { + AddApp + AuthMethodType domain.APIAuthMethodType + + ClientID string + ClientSecret *crypto.CryptoValue + ClientSecretPlain string +} + +func AddAPIAppCommand(app *addAPIApp, clientSecretAlg crypto.HashAlgorithm) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if app.ID == "" { + return nil, errors.ThrowInvalidArgument(nil, "PROJE-XHsKt", "Errors.Invalid.Argument") + } + if app.Name = strings.TrimSpace(app.Name); app.Name == "" { + return nil, errors.ThrowInvalidArgument(nil, "PROJE-F7g21", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + project, err := projectWriteModel(ctx, filter, app.Aggregate.ID, app.Aggregate.ResourceOwner) + if err != nil || !project.State.Valid() { + return nil, errors.ThrowNotFound(err, "PROJE-Sf2gb", "Errors.Project.NotFound") + } + + app.ClientID, err = domain.NewClientID(id.SonyFlakeGenerator, project.Name) + if err != nil { + return nil, errors.ThrowInternal(err, "V2-f0pgP", "Errors.Internal") + } + + //requires client secret + // TODO(release blocking):we have to return the secret + if app.AuthMethodType == domain.APIAuthMethodTypeBasic { + app.ClientSecret, app.ClientSecretPlain, err = newAppClientSecret(ctx, filter, clientSecretAlg) + if err != nil { + return nil, err + } + } + + return []eventstore.Command{ + project_repo.NewApplicationAddedEvent( + ctx, + &app.Aggregate.Aggregate, + app.ID, + app.Name, + ), + project_repo.NewAPIConfigAddedEvent( + ctx, + &app.Aggregate.Aggregate, + app.ID, + app.ClientID, + app.ClientSecret, + app.AuthMethodType, + ), + }, nil + }, nil + } +} + func (c *Commands) AddAPIApplication(ctx context.Context, application *domain.APIApp, resourceOwner string, appSecretGenerator crypto.Generator) (_ *domain.APIApp, err error) { if application == nil || application.AggregateID == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-5m9E", "Errors.Application.Invalid") + return nil, errors.ThrowInvalidArgument(nil, "PROJECT-5m9E", "Errors.Application.Invalid") } project, err := c.getProjectByID(ctx, application.AggregateID, resourceOwner) if err != nil { - return nil, caos_errs.ThrowPreconditionFailed(err, "PROJECT-9fnsf", "Errors.Project.NotFound") + return nil, errors.ThrowPreconditionFailed(err, "PROJECT-9fnsf", "Errors.Project.NotFound") } addedApplication := NewAPIApplicationWriteModel(application.AggregateID, resourceOwner) projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) @@ -43,7 +103,7 @@ func (c *Commands) AddAPIApplication(ctx context.Context, application *domain.AP func (c *Commands) addAPIApplication(ctx context.Context, projectAgg *eventstore.Aggregate, proj *domain.Project, apiAppApp *domain.APIApp, resourceOwner string, appSecretGenerator crypto.Generator) (events []eventstore.Command, stringPW string, err error) { if !apiAppApp.IsValid() { - return nil, "", caos_errs.ThrowInvalidArgument(nil, "PROJECT-Bff2g", "Errors.Application.Invalid") + return nil, "", errors.ThrowInvalidArgument(nil, "PROJECT-Bff2g", "Errors.Application.Invalid") } apiAppApp.AppID, err = c.idGenerator.Next() if err != nil { @@ -51,7 +111,7 @@ func (c *Commands) addAPIApplication(ctx context.Context, projectAgg *eventstore } events = []eventstore.Command{ - project.NewApplicationAddedEvent(ctx, projectAgg, apiAppApp.AppID, apiAppApp.AppName), + project_repo.NewApplicationAddedEvent(ctx, projectAgg, apiAppApp.AppID, apiAppApp.AppName), } var stringPw string @@ -63,7 +123,7 @@ func (c *Commands) addAPIApplication(ctx context.Context, projectAgg *eventstore if err != nil { return nil, "", err } - events = append(events, project.NewAPIConfigAddedEvent(ctx, + events = append(events, project_repo.NewAPIConfigAddedEvent(ctx, projectAgg, apiAppApp.AppID, apiAppApp.ClientID, @@ -75,7 +135,7 @@ func (c *Commands) addAPIApplication(ctx context.Context, projectAgg *eventstore func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIApp, resourceOwner string) (*domain.APIApp, error) { if apiApp.AppID == "" || apiApp.AggregateID == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-1m900", "Errors.Project.App.APIConfigInvalid") + return nil, errors.ThrowInvalidArgument(nil, "COMMAND-1m900", "Errors.Project.App.APIConfigInvalid") } existingAPI, err := c.getAPIAppWriteModel(ctx, apiApp.AggregateID, apiApp.AppID, resourceOwner) @@ -83,10 +143,10 @@ func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIA return nil, err } if existingAPI.State == domain.AppStateUnspecified || existingAPI.State == domain.AppStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "COMMAND-2n8uU", "Errors.Project.App.NotExisting") + return nil, errors.ThrowNotFound(nil, "COMMAND-2n8uU", "Errors.Project.App.NotExisting") } if !existingAPI.IsAPI() { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Gnwt3", "Errors.Project.App.IsNotAPI") + return nil, errors.ThrowInvalidArgument(nil, "COMMAND-Gnwt3", "Errors.Project.App.IsNotAPI") } projectAgg := ProjectAggregateFromWriteModel(&existingAPI.WriteModel) changedEvent, hasChanged, err := existingAPI.NewChangedEvent( @@ -98,7 +158,7 @@ func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIA return nil, err } if !hasChanged { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-1m88i", "Errors.NoChangesFound") + return nil, errors.ThrowPreconditionFailed(nil, "COMMAND-1m88i", "Errors.NoChangesFound") } pushedEvents, err := c.eventstore.Push(ctx, changedEvent) @@ -115,7 +175,7 @@ func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIA func (c *Commands) ChangeAPIApplicationSecret(ctx context.Context, projectID, appID, resourceOwner string, appSecretGenerator crypto.Generator) (*domain.APIApp, error) { if projectID == "" || appID == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-99i83", "Errors.IDMissing") + return nil, errors.ThrowInvalidArgument(nil, "COMMAND-99i83", "Errors.IDMissing") } existingAPI, err := c.getAPIAppWriteModel(ctx, projectID, appID, resourceOwner) @@ -123,10 +183,10 @@ func (c *Commands) ChangeAPIApplicationSecret(ctx context.Context, projectID, ap return nil, err } if existingAPI.State == domain.AppStateUnspecified || existingAPI.State == domain.AppStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "COMMAND-2g66f", "Errors.Project.App.NotExisting") + return nil, errors.ThrowNotFound(nil, "COMMAND-2g66f", "Errors.Project.App.NotExisting") } if !existingAPI.IsAPI() { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-aeH4", "Errors.Project.App.IsNotAPI") + return nil, errors.ThrowInvalidArgument(nil, "COMMAND-aeH4", "Errors.Project.App.IsNotAPI") } cryptoSecret, stringPW, err := domain.NewClientSecret(appSecretGenerator) if err != nil { @@ -135,7 +195,7 @@ func (c *Commands) ChangeAPIApplicationSecret(ctx context.Context, projectID, ap projectAgg := ProjectAggregateFromWriteModel(&existingAPI.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, project.NewAPIConfigSecretChangedEvent(ctx, projectAgg, appID, cryptoSecret)) + pushedEvents, err := c.eventstore.Push(ctx, project_repo.NewAPIConfigSecretChangedEvent(ctx, projectAgg, appID, cryptoSecret)) if err != nil { return nil, err } @@ -158,13 +218,13 @@ func (c *Commands) VerifyAPIClientSecret(ctx context.Context, projectID, appID, return err } if !app.State.Exists() { - return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-DFnbf", "Errors.Project.App.NoExisting") + return errors.ThrowPreconditionFailed(nil, "COMMAND-DFnbf", "Errors.Project.App.NoExisting") } if !app.IsAPI() { - return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Bf3fw", "Errors.Project.App.IsNotAPI") + return errors.ThrowInvalidArgument(nil, "COMMAND-Bf3fw", "Errors.Project.App.IsNotAPI") } if app.ClientSecret == nil { - return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-D3t5g", "Errors.Project.App.APIConfigInvalid") + return errors.ThrowPreconditionFailed(nil, "COMMAND-D3t5g", "Errors.Project.App.APIConfigInvalid") } projectAgg := ProjectAggregateFromWriteModel(&app.WriteModel) @@ -172,12 +232,12 @@ func (c *Commands) VerifyAPIClientSecret(ctx context.Context, projectID, appID, err = crypto.CompareHash(app.ClientSecret, []byte(secret), c.userPasswordAlg) spanPasswordComparison.EndWithError(err) if err == nil { - _, err = c.eventstore.Push(ctx, project.NewAPIConfigSecretCheckSucceededEvent(ctx, projectAgg, app.AppID)) + _, err = c.eventstore.Push(ctx, project_repo.NewAPIConfigSecretCheckSucceededEvent(ctx, projectAgg, app.AppID)) return err } - _, err = c.eventstore.Push(ctx, project.NewAPIConfigSecretCheckFailedEvent(ctx, projectAgg, app.AppID)) + _, err = c.eventstore.Push(ctx, project_repo.NewAPIConfigSecretCheckFailedEvent(ctx, projectAgg, app.AppID)) logging.Log("COMMAND-g3f12").OnError(err).Error("could not push event APIClientSecretCheckFailed") - return caos_errs.ThrowInvalidArgument(nil, "COMMAND-SADfg", "Errors.Project.App.ClientSecretInvalid") + return errors.ThrowInvalidArgument(nil, "COMMAND-SADfg", "Errors.Project.App.ClientSecretInvalid") } func (c *Commands) getAPIAppWriteModel(ctx context.Context, projectID, appID, resourceOwner string) (*APIApplicationWriteModel, error) { diff --git a/internal/command/project_application_api_test.go b/internal/command/project_application_api_test.go index 507ac3512b..5383713eec 100644 --- a/internal/command/project_application_api_test.go +++ b/internal/command/project_application_api_test.go @@ -4,9 +4,10 @@ import ( "context" "testing" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/repository" "github.com/caos/zitadel/internal/eventstore/v1/models" @@ -16,6 +17,118 @@ import ( "github.com/stretchr/testify/assert" ) +func TestAddAPIConfig(t *testing.T) { + type args struct { + a *project.Aggregate + appID string + name string + filter preparation.FilterToQueryReducer + } + + ctx := context.Background() + agg := project.NewAggregate("test", "test") + + tests := []struct { + name string + args args + want Want + }{ + { + name: "invalid appID", + args: args{ + a: agg, + appID: "", + name: "name", + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-XHsKt", "Errors.Invalid.Argument"), + }, + }, + { + name: "invalid name", + args: args{ + a: agg, + appID: "appID", + name: "", + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-F7g21", "Errors.Invalid.Argument"), + }, + }, + { + name: "project not exists", + args: args{ + a: agg, + appID: "id", + name: "name", + filter: NewMultiFilter(). + Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return nil, nil + }). + Filter(), + }, + want: Want{ + CreateErr: errors.ThrowNotFound(nil, "PROJE-Sf2gb", "Errors.Project.NotFound"), + }, + }, + { + name: "correct without client secret", + args: args{ + a: agg, + appID: "appID", + name: "name", + filter: NewMultiFilter(). + Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + project.NewProjectAddedEvent( + ctx, + &agg.Aggregate, + "project", + false, + false, + false, + domain.PrivateLabelingSettingUnspecified, + ), + }, nil + }). + Filter(), + }, + want: Want{ + Commands: []eventstore.Command{ + project.NewApplicationAddedEvent( + ctx, + &agg.Aggregate, + "appID", + "name", + ), + project.NewAPIConfigAddedEvent(ctx, &agg.Aggregate, + "appID", + "", + nil, + domain.APIAuthMethodTypePrivateKeyJWT, + ), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + AssertValidation(t, + AddAPIAppCommand( + &addAPIApp{ + AddApp: AddApp{ + Aggregate: *tt.args.a, + ID: tt.args.appID, + Name: tt.args.name, + }, + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + nil, + ), tt.args.filter, tt.want) + }) + } +} + func TestCommandSide_AddAPIApplication(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore @@ -50,7 +163,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -73,7 +186,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -103,7 +216,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -295,7 +408,7 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -318,7 +431,7 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -341,7 +454,7 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsNotFound, + err: errors.IsNotFound, }, }, { @@ -381,7 +494,7 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -504,7 +617,7 @@ func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -521,7 +634,7 @@ func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -539,7 +652,7 @@ func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsNotFound, + err: errors.IsNotFound, }, }, { diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go index 1b07eae68f..2a826ece66 100644 --- a/internal/command/project_application_oidc.go +++ b/internal/command/project_application_oidc.go @@ -2,17 +2,121 @@ package command import ( "context" + "strings" + "time" "github.com/caos/logging" + http_util "github.com/caos/zitadel/internal/api/http" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/project" + "github.com/caos/zitadel/internal/id" + project_repo "github.com/caos/zitadel/internal/repository/project" "github.com/caos/zitadel/internal/telemetry/tracing" ) +type addOIDCApp struct { + AddApp + Version domain.OIDCVersion + RedirectUris []string + ResponseTypes []domain.OIDCResponseType + GrantTypes []domain.OIDCGrantType + ApplicationType domain.OIDCApplicationType + AuthMethodType domain.OIDCAuthMethodType + PostLogoutRedirectUris []string + DevMode bool + AccessTokenType domain.OIDCTokenType + AccessTokenRoleAssertion bool + IDTokenRoleAssertion bool + IDTokenUserinfoAssertion bool + ClockSkew time.Duration + AdditionalOrigins []string + + ClientID string + ClientSecret *crypto.CryptoValue + ClientSecretPlain string +} + +//AddOIDCAppCommand prepares the commands to add an oidc app. The ClientID will be set during the CreateCommands +func AddOIDCAppCommand(app *addOIDCApp, clientSecretAlg crypto.HashAlgorithm) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if app.ID == "" { + return nil, errors.ThrowInvalidArgument(nil, "PROJE-NnavI", "Errors.Invalid.Argument") + } + + if app.Name = strings.TrimSpace(app.Name); app.Name == "" { + return nil, errors.ThrowInvalidArgument(nil, "PROJE-Fef31", "Errors.Invalid.Argument") + } + + if app.ClockSkew > time.Second*5 || app.ClockSkew < 0 { + return nil, errors.ThrowInvalidArgument(nil, "V2-PnCMS", "Errors.Invalid.Argument") + } + + for _, origin := range app.AdditionalOrigins { + if !http_util.IsOrigin(origin) { + return nil, errors.ThrowInvalidArgument(nil, "V2-DqWPX", "Errors.Invalid.Argument") + } + } + + if !domain.ContainsRequiredGrantTypes(app.ResponseTypes, app.GrantTypes) { + return nil, errors.ThrowInvalidArgument(nil, "V2-sLpW1", "Errors.Invalid.Argument") + } + + return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) { + project, err := projectWriteModel(ctx, filter, app.Aggregate.ID, app.Aggregate.ResourceOwner) + if err != nil || !project.State.Valid() { + return nil, errors.ThrowNotFound(err, "PROJE-6swVG", "Errors.Project.NotFound") + } + + app.ClientID, err = domain.NewClientID(id.SonyFlakeGenerator, project.Name) + if err != nil { + return nil, errors.ThrowInternal(err, "V2-VMSQ1", "Errors.Internal") + } + + if app.AuthMethodType == domain.OIDCAuthMethodTypeBasic || app.AuthMethodType == domain.OIDCAuthMethodTypePost { + app.ClientSecret, app.ClientSecretPlain, err = newAppClientSecret(ctx, filter, clientSecretAlg) + if err != nil { + return nil, err + } + } + + return []eventstore.Command{ + project_repo.NewApplicationAddedEvent( + ctx, + &app.Aggregate.Aggregate, + app.ID, + app.Name, + ), + project_repo.NewOIDCConfigAddedEvent( + ctx, + &app.Aggregate.Aggregate, + app.Version, + app.ID, + app.ClientID, + app.ClientSecret, + app.RedirectUris, + app.ResponseTypes, + app.GrantTypes, + app.ApplicationType, + app.AuthMethodType, + app.PostLogoutRedirectUris, + app.DevMode, + app.AccessTokenType, + app.AccessTokenRoleAssertion, + app.IDTokenRoleAssertion, + app.IDTokenUserinfoAssertion, + app.ClockSkew, + app.AdditionalOrigins, + ), + }, nil + }, nil + } +} + func (c *Commands) AddOIDCApplication(ctx context.Context, application *domain.OIDCApp, resourceOwner string, appSecretGenerator crypto.Generator) (_ *domain.OIDCApp, err error) { if application == nil || application.AggregateID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-34Fm0", "Errors.Application.Invalid") @@ -52,7 +156,7 @@ func (c *Commands) addOIDCApplication(ctx context.Context, projectAgg *eventstor } events = []eventstore.Command{ - project.NewApplicationAddedEvent(ctx, projectAgg, oidcApp.AppID, oidcApp.AppName), + project_repo.NewApplicationAddedEvent(ctx, projectAgg, oidcApp.AppID, oidcApp.AppName), } var stringPw string @@ -64,7 +168,7 @@ func (c *Commands) addOIDCApplication(ctx context.Context, projectAgg *eventstor if err != nil { return nil, "", err } - events = append(events, project.NewOIDCConfigAddedEvent(ctx, + events = append(events, project_repo.NewOIDCConfigAddedEvent(ctx, projectAgg, oidcApp.OIDCVersion, oidcApp.AppID, @@ -164,7 +268,7 @@ func (c *Commands) ChangeOIDCApplicationSecret(ctx context.Context, projectID, a projectAgg := ProjectAggregateFromWriteModel(&existingOIDC.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, project.NewOIDCConfigSecretChangedEvent(ctx, projectAgg, appID, cryptoSecret)) + pushedEvents, err := c.eventstore.Push(ctx, project_repo.NewOIDCConfigSecretChangedEvent(ctx, projectAgg, appID, cryptoSecret)) if err != nil { return nil, err } @@ -201,10 +305,10 @@ func (c *Commands) VerifyOIDCClientSecret(ctx context.Context, projectID, appID, err = crypto.CompareHash(app.ClientSecret, []byte(secret), c.userPasswordAlg) spanPasswordComparison.EndWithError(err) if err == nil { - _, err = c.eventstore.Push(ctx, project.NewOIDCConfigSecretCheckSucceededEvent(ctx, projectAgg, app.AppID)) + _, err = c.eventstore.Push(ctx, project_repo.NewOIDCConfigSecretCheckSucceededEvent(ctx, projectAgg, app.AppID)) return err } - _, err = c.eventstore.Push(ctx, project.NewOIDCConfigSecretCheckFailedEvent(ctx, projectAgg, app.AppID)) + _, err = c.eventstore.Push(ctx, project_repo.NewOIDCConfigSecretCheckFailedEvent(ctx, projectAgg, app.AppID)) logging.Log("COMMAND-ADfhz").OnError(err).Error("could not push event OIDCClientSecretCheckFailed") return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Bz542", "Errors.Project.App.ClientSecretInvalid") } diff --git a/internal/command/project_application_oidc_test.go b/internal/command/project_application_oidc_test.go index 7a34d0645e..5259f5c44f 100644 --- a/internal/command/project_application_oidc_test.go +++ b/internal/command/project_application_oidc_test.go @@ -7,9 +7,10 @@ import ( "github.com/stretchr/testify/assert" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/repository" "github.com/caos/zitadel/internal/eventstore/v1/models" @@ -18,6 +19,162 @@ import ( "github.com/caos/zitadel/internal/repository/project" ) +func TestAddOIDCApp(t *testing.T) { + type args struct { + app *addOIDCApp + clientSecretAlg crypto.HashAlgorithm + filter preparation.FilterToQueryReducer + } + + ctx := context.Background() + agg := project.NewAggregate("test", "test") + + tests := []struct { + name string + args args + want Want + }{ + { + name: "invalid appID", + args: args{ + app: &addOIDCApp{ + AddApp: AddApp{ + Aggregate: *agg, + ID: "", + Name: "name", + }, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + Version: domain.OIDCVersionV1, + ApplicationType: domain.OIDCApplicationTypeWeb, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + AccessTokenType: domain.OIDCTokenTypeBearer, + }, + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-NnavI", "Errors.Invalid.Argument"), + }, + }, + { + name: "invalid name", + args: args{ + app: &addOIDCApp{ + AddApp: AddApp{ + Aggregate: *agg, + ID: "id", + Name: "", + }, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + Version: domain.OIDCVersionV1, + ApplicationType: domain.OIDCApplicationTypeWeb, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + AccessTokenType: domain.OIDCTokenTypeBearer, + }, + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-Fef31", "Errors.Invalid.Argument"), + }, + }, + { + name: "project not exists", + args: args{ + app: &addOIDCApp{ + AddApp: AddApp{ + Aggregate: *agg, + ID: "id", + Name: "name", + }, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + Version: domain.OIDCVersionV1, + ApplicationType: domain.OIDCApplicationTypeWeb, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + AccessTokenType: domain.OIDCTokenTypeBearer, + }, + filter: NewMultiFilter(). + Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return nil, nil + }). + Filter(), + }, + want: Want{ + CreateErr: errors.ThrowNotFound(nil, "PROJE-6swVG", ""), + }, + }, + { + name: "correct", + args: args{ + app: &addOIDCApp{ + AddApp: AddApp{ + Aggregate: *agg, + ID: "id", + Name: "name", + }, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + Version: domain.OIDCVersionV1, + + ApplicationType: domain.OIDCApplicationTypeWeb, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + AccessTokenType: domain.OIDCTokenTypeBearer, + }, + filter: NewMultiFilter(). + Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + project.NewProjectAddedEvent( + ctx, + &agg.Aggregate, + "project", + false, + false, + false, + domain.PrivateLabelingSettingUnspecified, + ), + }, nil + }). + Filter(), + }, + want: Want{ + Commands: []eventstore.Command{ + project.NewApplicationAddedEvent(ctx, &agg.Aggregate, + "id", + "name", + ), + project.NewOIDCConfigAddedEvent(ctx, &agg.Aggregate, + domain.OIDCVersionV1, + "id", + "", + nil, + nil, + []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + domain.OIDCApplicationTypeWeb, + domain.OIDCAuthMethodTypeNone, + nil, + false, + domain.OIDCTokenTypeBearer, + false, + false, + false, + 0, + nil, + ), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + AssertValidation(t, + AddOIDCAppCommand( + tt.args.app, + tt.args.clientSecretAlg, + ), tt.args.filter, tt.want) + }) + } +} + func TestCommandSide_AddOIDCApplication(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore @@ -52,7 +209,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -75,7 +232,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -105,7 +262,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -274,7 +431,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -298,7 +455,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -322,7 +479,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -347,7 +504,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsNotFound, + err: errors.IsNotFound, }, }, { @@ -418,7 +575,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -580,7 +737,7 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -597,7 +754,7 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -615,7 +772,7 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: caos_errs.IsNotFound, + err: errors.IsNotFound, }, }, { diff --git a/internal/command/project_test.go b/internal/command/project_test.go index 241ed34610..e7c4d78850 100644 --- a/internal/command/project_test.go +++ b/internal/command/project_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/repository" @@ -985,3 +986,186 @@ func newProjectChangedEvent(ctx context.Context, projectID, resourceOwner, oldNa ) return event } + +func TestAddProject(t *testing.T) { + type args struct { + a *project.Aggregate + name string + owner string + privateLabelingSetting domain.PrivateLabelingSetting + } + + ctx := context.Background() + agg := project.NewAggregate("test", "test") + + tests := []struct { + name string + args args + want Want + }{ + { + name: "invalid name", + args: args{ + a: agg, + name: "", + owner: "owner", + privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-C01yo", "Errors.Invalid.Argument"), + }, + }, + { + name: "invalid private labeling setting", + args: args{ + a: agg, + name: "name", + owner: "owner", + privateLabelingSetting: -1, + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-AO52V", "Errors.Invalid.Argument"), + }, + }, + { + name: "invalid owner", + args: args{ + a: agg, + name: "name", + owner: "", + privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + }, + want: Want{ + ValidationErr: errors.ThrowPreconditionFailed(nil, "PROJE-hzxwo", "Errors.Invalid.Argument"), + }, + }, + { + name: "correct", + args: args{ + a: agg, + name: "ZITADEL", + owner: "CAOS AG", + privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + }, + want: Want{ + Commands: []eventstore.Command{ + project.NewProjectAddedEvent(ctx, &agg.Aggregate, + "ZITADEL", + false, + false, + false, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + ), + project.NewProjectMemberAddedEvent(ctx, &agg.Aggregate, + "CAOS AG", + domain.RoleProjectOwner), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + AssertValidation(t, AddProjectCommand(tt.args.a, tt.args.name, tt.args.owner, false, false, false, tt.args.privateLabelingSetting), nil, tt.want) + }) + } +} + +// func TestExistsProject(t *testing.T) { +// type args struct { +// filter preparation.FilterToQueryReducer +// id string +// resourceOwner string +// } +// tests := []struct { +// name string +// args args +// wantExists bool +// wantErr bool +// }{ +// { +// name: "no events", +// args: args{ +// filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { +// return []eventstore.Event{}, nil +// }, +// id: "id", +// resourceOwner: "ro", +// }, +// wantExists: false, +// wantErr: false, +// }, +// { +// name: "project added", +// args: args{ +// filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { +// return []eventstore.Event{ +// project.NewProjectAddedEvent( +// context.Background(), +// &project.NewAggregate("id", "ro").Aggregate, +// "name", +// false, +// false, +// false, +// domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, +// ), +// }, nil +// }, +// id: "id", +// resourceOwner: "ro", +// }, +// wantExists: true, +// wantErr: false, +// }, +// { +// name: "project removed", +// args: args{ +// filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { +// return []eventstore.Event{ +// project.NewProjectAddedEvent( +// context.Background(), +// &project.NewAggregate("id", "ro").Aggregate, +// "name", +// false, +// false, +// false, +// domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, +// ), +// project.NewProjectRemovedEvent( +// context.Background(), +// &project.NewAggregate("id", "ro").Aggregate, +// "name", +// ), +// }, nil +// }, +// id: "id", +// resourceOwner: "ro", +// }, +// wantExists: false, +// wantErr: false, +// }, +// { +// name: "error durring filter", +// args: args{ +// filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { +// return nil, errors.ThrowInternal(nil, "PROJE-Op26p", "Errors.Internal") +// }, +// id: "id", +// resourceOwner: "ro", +// }, +// wantExists: false, +// wantErr: true, +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// gotExists, err := projectWriteModel(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner) +// if (err != nil) != tt.wantErr { +// t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr) +// return +// } +// if gotExists != tt.wantExists { +// t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists) +// } +// }) +// } +// } diff --git a/internal/command/secret_generator.go b/internal/command/secret_generator.go new file mode 100644 index 0000000000..f285a46eef --- /dev/null +++ b/internal/command/secret_generator.go @@ -0,0 +1,73 @@ +package command + +import ( + "context" + + "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/command/preparation" + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/instance" +) + +func (c *commandNew) AddSecretGeneratorConfig(ctx context.Context, typ domain.SecretGeneratorType, config *crypto.GeneratorConfig) (*domain.ObjectDetails, error) { + agg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) + cmds, err := preparation.PrepareCommands(ctx, c.es.Filter, addSecretGeneratorConfig(agg, typ, config)) + if err != nil { + return nil, err + } + + events, err := c.es.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: agg.ResourceOwner, + }, nil +} + +//AddOrg defines the commands to create a new org, +// this includes the verified default domain +func addSecretGeneratorConfig(a *instance.Aggregate, typ domain.SecretGeneratorType, config *crypto.GeneratorConfig) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if !typ.Valid() { + return nil, errors.ThrowInvalidArgument(nil, "V2-FGqVj", "Errors.InvalidArgument") + } + if config.Length < 1 { + return nil, errors.ThrowInvalidArgument(nil, "V2-jEqCt", "Errors.InvalidArgument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel := NewInstanceSecretGeneratorConfigWriteModel(ctx, typ) + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + writeModel.AppendEvents(events...) + if err = writeModel.Reduce(); err != nil { + return nil, err + } + + if writeModel.State == domain.SecretGeneratorStateActive { + return nil, errors.ThrowAlreadyExists(nil, "V2-6CqKo", "Errors.SecretGenerator.AlreadyExists") + } + + return []eventstore.Command{ + instance.NewSecretGeneratorAddedEvent( + ctx, + &a.Aggregate, + typ, + config.Length, + config.Expiry, + config.IncludeLowerLetters, + config.IncludeUpperLetters, + config.IncludeDigits, + config.IncludeSymbols, + ), + }, nil + }, nil + } +} diff --git a/internal/command/user.go b/internal/command/user.go index e4e0735722..2f86ca31c3 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -5,15 +5,15 @@ import ( "fmt" "time" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/query" - - "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/logging" + "github.com/caos/zitadel/internal/command/preparation" + "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/query" "github.com/caos/zitadel/internal/repository/user" "github.com/caos/zitadel/internal/telemetry/tracing" ) @@ -378,3 +378,38 @@ func (c *Commands) userWriteModelByID(ctx context.Context, userID, resourceOwner } return writeModel, nil } + +func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string) (exists bool, err error) { + events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(resourceOwner). + OrderAsc(). + AddQuery(). + AggregateTypes(user.AggregateType). + AggregateIDs(id). + EventTypes( + user.HumanRegisteredType, + user.UserV1RegisteredType, + user.HumanAddedType, + user.UserV1AddedType, + user.MachineAddedEventType, + user.UserRemovedType, + ).Builder()) + if err != nil { + return false, err + } + + for _, event := range events { + switch event.(type) { + case *user.HumanRegisteredEvent, *user.HumanAddedEvent, *user.MachineAddedEvent: + exists = true + case *user.UserRemovedEvent: + exists = false + } + } + + return exists, nil +} + +func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) { + return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeInitCode, alg) +} diff --git a/internal/command/v2/user_domain_policy.go b/internal/command/user_domain_policy.go similarity index 70% rename from internal/command/v2/user_domain_policy.go rename to internal/command/user_domain_policy.go index cc6f5a06c7..15281bec70 100644 --- a/internal/command/v2/user_domain_policy.go +++ b/internal/command/user_domain_policy.go @@ -4,12 +4,11 @@ import ( "context" "github.com/caos/zitadel/internal/api/authz" - "github.com/caos/zitadel/internal/command" - "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/errors" ) -func domainPolicyWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PolicyDomainWriteModel, error) { +func domainPolicyWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer) (*PolicyDomainWriteModel, error) { wm, err := orgDomainPolicy(ctx, filter) if err != nil || wm != nil && wm.State.Exists() { return wm, err @@ -21,8 +20,8 @@ func domainPolicyWriteModel(ctx context.Context, filter preparation.FilterToQuer return nil, errors.ThrowInternal(nil, "USER-Ggk9n", "Errors.Internal") } -func orgDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PolicyDomainWriteModel, error) { - policy := command.NewOrgDomainPolicyWriteModel(authz.GetCtxData(ctx).OrgID) +func orgDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*PolicyDomainWriteModel, error) { + policy := NewOrgDomainPolicyWriteModel(authz.GetCtxData(ctx).OrgID) events, err := filter(ctx, policy.Query()) if err != nil { return nil, err @@ -35,8 +34,8 @@ func orgDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReduce return &policy.PolicyDomainWriteModel, err } -func instanceDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PolicyDomainWriteModel, error) { - policy := command.NewInstanceDomainPolicyWriteModel(ctx) +func instanceDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*PolicyDomainWriteModel, error) { + policy := NewInstanceDomainPolicyWriteModel(ctx) events, err := filter(ctx, policy.Query()) if err != nil { return nil, err diff --git a/internal/command/v2/user_domain_policy_test.go b/internal/command/user_domain_policy_test.go similarity index 94% rename from internal/command/v2/user_domain_policy_test.go rename to internal/command/user_domain_policy_test.go index c02460c390..c6c7bd747c 100644 --- a/internal/command/v2/user_domain_policy_test.go +++ b/internal/command/user_domain_policy_test.go @@ -6,8 +6,7 @@ import ( "testing" "github.com/caos/zitadel/internal/api/authz" - "github.com/caos/zitadel/internal/command" - "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" @@ -22,7 +21,7 @@ func Test_customDomainPolicy(t *testing.T) { tests := []struct { name string args args - want *command.PolicyDomainWriteModel + want *PolicyDomainWriteModel wantErr bool }{ { @@ -58,7 +57,7 @@ func Test_customDomainPolicy(t *testing.T) { }, nil }, }, - want: &command.PolicyDomainWriteModel{ + want: &PolicyDomainWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: "id", ResourceOwner: "ro", @@ -91,7 +90,7 @@ func Test_defaultDomainPolicy(t *testing.T) { tests := []struct { name string args args - want *command.PolicyDomainWriteModel + want *PolicyDomainWriteModel wantErr bool }{ { @@ -127,7 +126,7 @@ func Test_defaultDomainPolicy(t *testing.T) { }, nil }, }, - want: &command.PolicyDomainWriteModel{ + want: &PolicyDomainWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: "INSTANCE", ResourceOwner: "INSTANCE", @@ -160,7 +159,7 @@ func Test_DomainPolicy(t *testing.T) { tests := []struct { name string args args - want *command.PolicyDomainWriteModel + want *PolicyDomainWriteModel wantErr bool }{ { @@ -186,7 +185,7 @@ func Test_DomainPolicy(t *testing.T) { }, nil }, }, - want: &command.PolicyDomainWriteModel{ + want: &PolicyDomainWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: "id", ResourceOwner: "ro", @@ -230,7 +229,7 @@ func Test_DomainPolicy(t *testing.T) { }). Filter(), }, - want: &command.PolicyDomainWriteModel{ + want: &PolicyDomainWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: "INSTANCE", ResourceOwner: "INSTANCE", diff --git a/internal/command/user_human.go b/internal/command/user_human.go index e4e810aecd..8c89e19573 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -4,13 +4,14 @@ import ( "context" "strings" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/crypto" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/v1/models" "github.com/caos/zitadel/internal/repository/user" + "golang.org/x/text/language" ) func (c *Commands) getHuman(ctx context.Context, userID, resourceowner string) (*domain.Human, error) { @@ -19,51 +20,258 @@ func (c *Commands) getHuman(ctx context.Context, userID, resourceowner string) ( return nil, err } if !isUserStateExists(human.UserState) { - return nil, caos_errs.ThrowNotFound(nil, "COMMAND-M9dsd", "Errors.User.NotFound") + return nil, errors.ThrowNotFound(nil, "COMMAND-M9dsd", "Errors.User.NotFound") } return writeModelToHuman(human), nil } -func (c *Commands) AddHuman(ctx context.Context, orgID string, human *domain.Human, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) (*domain.Human, error) { - if orgID == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-XYFk9", "Errors.ResourceOwnerMissing") +type AddHuman struct { + // Username is required + Username string + // FirstName is required + FirstName string + // LastName is required + LastName string + // NickName is required + NickName string + // DisplayName is required + DisplayName string + // Email is required + Email Email + // PreferredLang is required + PreferredLang language.Tag + // Gender is required + Gender domain.Gender + //Phone represents an international phone number + Phone Phone + //Password is optional + Password string + //PasswordChangeRequired is used if the `Password`-field is set + PasswordChangeRequired bool + Passwordless bool + ExternalIDP bool + Register bool +} + +func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman) (*domain.HumanDetails, error) { + return c.v2.AddHuman(ctx, resourceOwner, human) +} + +func (c *commandNew) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman) (*domain.HumanDetails, error) { + if resourceOwner == "" { + return nil, errors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal") } - domainPolicy, err := c.getOrgDomainPolicy(ctx, orgID) - if err != nil { - return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-33M9f", "Errors.Org.DomainPolicy.NotFound") - } - pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, orgID) - if err != nil { - return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-M5Fsd", "Errors.Org.PasswordComplexityPolicy.NotFound") - } - events, addedHuman, err := c.addHuman(ctx, orgID, human, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) + userID, err := c.id.Next() if err != nil { return nil, err } - pushedEvents, err := c.eventstore.Push(ctx, events...) + agg := user.NewAggregate(userID, resourceOwner) + cmds, err := preparation.PrepareCommands(ctx, c.es.Filter, addHumanCommand(agg, human, c.userPasswordAlg, c.phoneAlg, c.emailAlg, c.initCodeAlg)) if err != nil { return nil, err } - err = AppendAndReduce(addedHuman, pushedEvents...) + events, err := c.es.Push(ctx, cmds...) if err != nil { return nil, err } - return writeModelToHuman(addedHuman), nil + return &domain.HumanDetails{ + ID: userID, + ObjectDetails: domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, + }, + }, nil +} + +type humanCreationCommand interface { + eventstore.Command + AddPhoneData(phoneNumber string) + AddPasswordData(secret *crypto.CryptoValue, changeRequired bool) +} + +func addHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm, phoneAlg, emailAlg, initCodeAlg crypto.EncryptionAlgorithm) preparation.Validation { + return func() (_ preparation.CreateCommands, err error) { + if !human.Email.Valid() { + return nil, errors.ThrowInvalidArgument(nil, "USER-Ec7dM", "Errors.Invalid.Argument") + } + if human.Username = strings.TrimSpace(human.Username); human.Username == "" { + return nil, errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument") + } + + if human.FirstName = strings.TrimSpace(human.FirstName); human.FirstName == "" { + return nil, errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.Invalid.Argument") + } + if human.LastName = strings.TrimSpace(human.LastName); human.LastName == "" { + return nil, errors.ThrowInvalidArgument(nil, "USER-DiAq8", "Errors.Invalid.Argument") + } + human.ensureDisplayName() + + if human.Phone.Number, err = FormatPhoneNumber(human.Phone.Number); err != nil { + return nil, errors.ThrowInvalidArgument(nil, "USER-tD6ax", "Errors.Invalid.Argument") + } + + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + domainPolicy, err := domainPolicyWriteModel(ctx, filter) + if err != nil { + return nil, err + } + + if err = userValidateDomain(ctx, a, human.Username, domainPolicy.UserLoginMustBeDomain, filter); err != nil { + return nil, err + } + + var createCmd humanCreationCommand + if human.Register { + createCmd = user.NewHumanRegisteredEvent( + ctx, + &a.Aggregate, + human.Username, + human.FirstName, + human.LastName, + human.NickName, + human.DisplayName, + human.PreferredLang, + human.Gender, + human.Email.Address, + domainPolicy.UserLoginMustBeDomain, + ) + } else { + createCmd = user.NewHumanAddedEvent( + ctx, + &a.Aggregate, + human.Username, + human.FirstName, + human.LastName, + human.NickName, + human.DisplayName, + human.PreferredLang, + human.Gender, + human.Email.Address, + domainPolicy.UserLoginMustBeDomain, + ) + } + + if human.Phone.Number != "" { + createCmd.AddPhoneData(human.Phone.Number) + } + + if human.Password != "" { + if err = humanValidatePassword(ctx, filter, human.Password); err != nil { + return nil, err + } + + secret, err := crypto.Hash([]byte(human.Password), passwordAlg) + if err != nil { + return nil, err + } + createCmd.AddPasswordData(secret, human.PasswordChangeRequired) + } + + cmds := make([]eventstore.Command, 0, 3) + cmds = append(cmds, createCmd) + + //add init code if + // email not verified or + // user not registered and password set + if human.shouldAddInitCode() { + value, expiry, err := newUserInitCode(ctx, filter, initCodeAlg) + if err != nil { + return nil, err + } + cmds = append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, value, expiry)) + } + + if human.Email.Verified { + cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate)) + } else { + value, expiry, err := newEmailCode(ctx, filter, emailAlg) + if err != nil { + return nil, err + } + cmds = append(cmds, user.NewHumanEmailCodeAddedEvent(ctx, &a.Aggregate, value, expiry)) + } + + if human.Phone.Verified { + cmds = append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate)) + } else if human.Phone.Number != "" { + value, expiry, err := newPhoneCode(ctx, filter, phoneAlg) + if err != nil { + return nil, err + } + cmds = append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, value, expiry)) + } + + return cmds, nil + }, nil + } +} + +func userValidateDomain(ctx context.Context, a *user.Aggregate, username string, mustBeDomain bool, filter preparation.FilterToQueryReducer) error { + if mustBeDomain { + return nil + } + + usernameSplit := strings.Split(username, "@") + if len(usernameSplit) != 2 { + return errors.ThrowInvalidArgument(nil, "COMMAND-Dfd21", "Errors.User.Invalid") + } + + domainCheck := NewOrgDomainVerifiedWriteModel(usernameSplit[1]) + events, err := filter(ctx, domainCheck.Query()) + if err != nil { + return err + } + domainCheck.AppendEvents(events...) + if err = domainCheck.Reduce(); err != nil { + return err + } + + if domainCheck.Verified && domainCheck.ResourceOwner != a.ResourceOwner { + return errors.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername") + } + + return nil +} + +func humanValidatePassword(ctx context.Context, filter preparation.FilterToQueryReducer, password string) error { + passwordComplexity, err := passwordComplexityPolicyWriteModel(ctx, filter) + if err != nil { + return err + } + + return passwordComplexity.Validate(password) +} + +func (h *AddHuman) ensureDisplayName() { + if strings.TrimSpace(h.DisplayName) != "" { + return + } + h.DisplayName = h.FirstName + " " + h.LastName +} + +func (h *AddHuman) shouldAddInitCode() bool { + //user without idp + return !h.Email.Verified || + //user with idp + !h.ExternalIDP && + !h.Passwordless && + h.Password != "" } func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) { if orgID == "" { - return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5N8fs", "Errors.ResourceOwnerMissing") + return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-5N8fs", "Errors.ResourceOwnerMissing") } domainPolicy, err := c.getOrgDomainPolicy(ctx, orgID) if err != nil { - return nil, nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-2N9fs", "Errors.Org.DomainPolicy.NotFound") + return nil, nil, errors.ThrowPreconditionFailed(err, "COMMAND-2N9fs", "Errors.Org.DomainPolicy.NotFound") } pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, orgID) if err != nil { - return nil, nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-4N8gs", "Errors.Org.PasswordComplexityPolicy.NotFound") + return nil, nil, errors.ThrowPreconditionFailed(err, "COMMAND-4N8gs", "Errors.Org.PasswordComplexityPolicy.NotFound") } events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) if err != nil { @@ -89,53 +297,24 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. return writeModelToHuman(addedHuman), passwordlessCode, nil } -func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Human, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) { - if orgID == "" || !human.IsValid() { - return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-67Ms8", "Errors.User.Invalid") - } - if human.Password != nil && human.SecretString != "" { - human.ChangeRequired = true - } - return c.createHuman(ctx, orgID, human, nil, false, false, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) -} - -func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { - if orgID == "" || !human.IsValid() { - return nil, nil, nil, "", caos_errs.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Invalid") - } - events, humanWriteModel, err = c.createHuman(ctx, orgID, human, nil, false, passwordless, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) - if err != nil { - return nil, nil, nil, "", err - } - if passwordless { - var codeEvent eventstore.Command - codeEvent, passwordlessCodeWriteModel, code, err = c.humanAddPasswordlessInitCode(ctx, human.AggregateID, orgID, true, passwordlessCodeGenerator) - if err != nil { - return nil, nil, nil, "", err - } - events = append(events, codeEvent) - } - return events, humanWriteModel, passwordlessCodeWriteModel, code, nil -} - func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, orgMemberRoles []string, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) (*domain.Human, error) { if orgID == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-GEdf2", "Errors.ResourceOwnerMissing") + return nil, errors.ThrowInvalidArgument(nil, "COMMAND-GEdf2", "Errors.ResourceOwnerMissing") } domainPolicy, err := c.getOrgDomainPolicy(ctx, orgID) if err != nil { - return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-33M9f", "Errors.Org.DomainPolicy.NotFound") + return nil, errors.ThrowPreconditionFailed(err, "COMMAND-33M9f", "Errors.Org.DomainPolicy.NotFound") } pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, orgID) if err != nil { - return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-M5Fsd", "Errors.Org.PasswordComplexityPolicy.NotFound") + return nil, errors.ThrowPreconditionFailed(err, "COMMAND-M5Fsd", "Errors.Org.PasswordComplexityPolicy.NotFound") } loginPolicy, err := c.getOrgLoginPolicy(ctx, orgID) if err != nil { - return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-Dfg3g", "Errors.Org.LoginPolicy.NotFound") + return nil, errors.ThrowPreconditionFailed(err, "COMMAND-Dfg3g", "Errors.Org.LoginPolicy.NotFound") } if !loginPolicy.AllowRegister { - return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-SAbr3", "Errors.Org.LoginPolicy.RegistrationNotAllowed") + return nil, errors.ThrowPreconditionFailed(err, "COMMAND-SAbr3", "Errors.Org.LoginPolicy.RegistrationNotAllowed") } userEvents, registeredHuman, err := c.registerHuman(ctx, orgID, human, link, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) if err != nil { @@ -171,12 +350,41 @@ func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domai return writeModelToHuman(registeredHuman), nil } +func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Human, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) { + if orgID == "" || !human.IsValid() { + return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-67Ms8", "Errors.User.Invalid") + } + if human.Password != nil && human.SecretString != "" { + human.ChangeRequired = true + } + return c.createHuman(ctx, orgID, human, nil, false, false, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) +} + +func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { + if orgID == "" || !human.IsValid() { + return nil, nil, nil, "", errors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Invalid") + } + events, humanWriteModel, err = c.createHuman(ctx, orgID, human, nil, false, passwordless, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) + if err != nil { + return nil, nil, nil, "", err + } + if passwordless { + var codeEvent eventstore.Command + codeEvent, passwordlessCodeWriteModel, code, err = c.humanAddPasswordlessInitCode(ctx, human.AggregateID, orgID, true, passwordlessCodeGenerator) + if err != nil { + return nil, nil, nil, "", err + } + events = append(events, codeEvent) + } + return events, humanWriteModel, passwordlessCodeWriteModel, code, nil +} + func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) { if human != nil && human.Username == "" { human.Username = human.EmailAddress } if orgID == "" || !human.IsValid() || link == nil && (human.Password == nil || human.SecretString == "") { - return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-9dk45", "Errors.User.Invalid") + return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-9dk45", "Errors.User.Invalid") } if human.Password != nil && human.SecretString != "" { human.ChangeRequired = false @@ -184,21 +392,21 @@ func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domai return c.createHuman(ctx, orgID, human, link, true, false, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) } -func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, selfregister, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) { +func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, selfregister, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, addedHuman *HumanWriteModel, err error) { if err := human.CheckDomainPolicy(domainPolicy); err != nil { return nil, nil, err } if !domainPolicy.UserLoginMustBeDomain { usernameSplit := strings.Split(human.Username, "@") if len(usernameSplit) != 2 { - return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Dfd21", "Errors.User.Invalid") + return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-Dfd21", "Errors.User.Invalid") } domainCheck := NewOrgDomainVerifiedWriteModel(usernameSplit[1]) if err := c.eventstore.FilterToQueryReducer(ctx, domainCheck); err != nil { return nil, nil, err } if domainCheck.Verified && domainCheck.ResourceOwner != orgID { - return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername") + return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername") } } userID, err := c.idGenerator.Next() @@ -213,10 +421,9 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. } } - addedHuman := NewHumanWriteModel(human.AggregateID, orgID) + addedHuman = NewHumanWriteModel(human.AggregateID, orgID) //TODO: adlerhurst maybe we could simplify the code below userAgg := UserAggregateFromWriteModel(&addedHuman.WriteModel) - var events []eventstore.Command if selfregister { events = append(events, createRegisterHumanEvent(ctx, userAgg, human, domainPolicy.UserLoginMustBeDomain)) @@ -259,7 +466,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. func (c *Commands) HumanSkipMFAInit(ctx context.Context, userID, resourceowner string) (err error) { if userID == "" { - return caos_errs.ThrowInvalidArgument(nil, "COMMAND-2xpX9", "Errors.User.UserIDMissing") + return errors.ThrowInvalidArgument(nil, "COMMAND-2xpX9", "Errors.User.UserIDMissing") } existingHuman, err := c.getHumanWriteModelByID(ctx, userID, resourceowner) @@ -267,7 +474,7 @@ func (c *Commands) HumanSkipMFAInit(ctx context.Context, userID, resourceowner s return err } if !isUserStateExists(existingHuman.UserState) { - return caos_errs.ThrowNotFound(nil, "COMMAND-m9cV8", "Errors.User.NotFound") + return errors.ThrowNotFound(nil, "COMMAND-m9cV8", "Errors.User.NotFound") } _, err = c.eventstore.Push(ctx, @@ -340,10 +547,10 @@ func createRegisterHumanEvent(ctx context.Context, aggregate *eventstore.Aggrega func (c *Commands) HumansSignOut(ctx context.Context, agentID string, userIDs []string) error { if agentID == "" { - return caos_errs.ThrowInvalidArgument(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing") + return errors.ThrowInvalidArgument(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing") } if len(userIDs) == 0 { - return caos_errs.ThrowInvalidArgument(nil, "COMMAND-M0od3", "Errors.User.UserIDMissing") + return errors.ThrowInvalidArgument(nil, "COMMAND-M0od3", "Errors.User.UserIDMissing") } events := make([]eventstore.Command, 0) for _, userID := range userIDs { diff --git a/internal/command/user_human_phone_test.go b/internal/command/user_human_phone_test.go index b5d33cabc2..1e743f2856 100644 --- a/internal/command/user_human_phone_test.go +++ b/internal/command/user_human_phone_test.go @@ -71,7 +71,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { ObjectRoot: models.ObjectRoot{ AggregateID: "user1", }, - PhoneNumber: "0711234567", + PhoneNumber: "+41711234567", }, resourceOwner: "org1", }, @@ -114,7 +114,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { ObjectRoot: models.ObjectRoot{ AggregateID: "user1", }, - PhoneNumber: "0711234567", + PhoneNumber: "+41711234567", }, resourceOwner: "org1", }, @@ -172,7 +172,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { ObjectRoot: models.ObjectRoot{ AggregateID: "user1", }, - PhoneNumber: "0719876543", + PhoneNumber: "+41719876543", IsPhoneVerified: true, }, resourceOwner: "org1", @@ -239,7 +239,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { ObjectRoot: models.ObjectRoot{ AggregateID: "user1", }, - PhoneNumber: "0711234567", + PhoneNumber: "+41711234567", }, resourceOwner: "org1", secretGenerator: GetMockSecretGenerator(t), diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index 12e70429b6..17f126b00a 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -9,14 +9,16 @@ import ( "github.com/stretchr/testify/assert" "golang.org/x/text/language" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/repository" "github.com/caos/zitadel/internal/eventstore/v1/models" "github.com/caos/zitadel/internal/id" id_mock "github.com/caos/zitadel/internal/id/mock" + "github.com/caos/zitadel/internal/repository/instance" "github.com/caos/zitadel/internal/repository/org" "github.com/caos/zitadel/internal/repository/user" ) @@ -26,17 +28,24 @@ func TestCommandSide_AddHuman(t *testing.T) { eventstore *eventstore.Eventstore idGenerator id.Generator userPasswordAlg crypto.HashAlgorithm + initCodeAlg crypto.EncryptionAlgorithm + emailCodeAlg crypto.EncryptionAlgorithm + phoneAlg crypto.EncryptionAlgorithm } type args struct { ctx context.Context orgID string - human *domain.Human + human *AddHuman secretGenerator crypto.Generator } type res struct { - want *domain.Human + want *domain.HumanDetails err func(error) bool } + + userAgg := user.NewAggregate("user1", "org1") + instanceAgg := instance.NewAggregate("instance") + tests := []struct { name string fields fields @@ -53,24 +62,23 @@ func TestCommandSide_AddHuman(t *testing.T) { args: args{ ctx: context.Background(), orgID: "", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", }, }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { - name: "org policy not found, precondition error", + name: "domain policy not found, precondition error", fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), eventstore: eventstoreExpect( t, expectFilter(), @@ -80,30 +88,29 @@ func TestCommandSide_AddHuman(t *testing.T) { args: args{ ctx: context.Background(), orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", }, }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsInternal, }, }, { name: "password policy not found, precondition error", fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), eventstore: eventstoreExpect( t, expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + &userAgg.Aggregate, true, ), ), @@ -115,60 +122,39 @@ func TestCommandSide_AddHuman(t *testing.T) { args: args{ ctx: context.Background(), orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Password: "pass", + Email: Email{ + Address: "email@test.ch", + Verified: true, }, }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsInternal, }, }, { name: "user invalid, invalid argument error", fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), eventstore: eventstoreExpect( t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - ), - ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), ), }, args: args{ ctx: context.Background(), orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - }, + human: &AddHuman{ + Username: "username", + FirstName: "firstname", }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -179,20 +165,38 @@ func TestCommandSide_AddHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + &userAgg.Aggregate, true, ), ), ), expectFilter( eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, + instance.NewSecretGeneratorAddedEvent( + context.Background(), + &instanceAgg.Aggregate, + domain.SecretGeneratorTypeInitCode, + 0, + 1*time.Hour, + true, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent( + context.Background(), + &instanceAgg.Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 0, + 1*time.Hour, + true, + true, + true, + true, ), ), ), @@ -200,7 +204,7 @@ func TestCommandSide_AddHuman(t *testing.T) { []*repository.Event{ eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + &userAgg.Aggregate, "username", "firstname", "lastname", @@ -214,12 +218,24 @@ func TestCommandSide_AddHuman(t *testing.T) { ), eventFromEventPusher( user.NewHumanInitialCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + &userAgg.Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", - Crypted: []byte("a"), + Crypted: []byte(""), + }, + time.Hour*1, + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEvent(context.Background(), + &userAgg.Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte(""), }, time.Hour*1, ), @@ -228,40 +244,31 @@ func TestCommandSide_AddHuman(t *testing.T) { uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + initCodeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + emailCodeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ ctx: context.Background(), orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", }, }, secretGenerator: GetMockSecretGenerator(t), }, res: res{ - want: &domain.Human{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "user1", + want: &domain.HumanDetails{ + ID: "user1", + ObjectDetails: domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, ResourceOwner: "org1", }, - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - DisplayName: "firstname lastname", - PreferredLanguage: language.Und, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - State: domain.UserStateInitial, }, }, }, @@ -290,10 +297,39 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &userAgg.Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte(""), + }, + time.Hour*1, + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent( + context.Background(), + &instanceAgg.Aggregate, + domain.SecretGeneratorTypeInitCode, + 0, + 1*time.Hour, + true, + true, + true, + true, + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( - newAddHumanEvent("password", true, ""), + newAddHumanEvent("password", false, ""), ), eventFromEventPusher( user.NewHumanInitialCodeAddedEvent(context.Background(), @@ -302,9 +338,22 @@ func TestCommandSide_AddHuman(t *testing.T) { CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", - Crypted: []byte("a"), + Crypted: []byte(""), }, - time.Hour*1, + 0, + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEvent( + context.Background(), + &userAgg.Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte(""), + }, + 0, ), ), }, @@ -313,42 +362,29 @@ func TestCommandSide_AddHuman(t *testing.T) { ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + initCodeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + emailCodeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ ctx: context.Background(), orgID: "org1", - human: &domain.Human{ - Username: "username", - Password: &domain.Password{ - SecretString: "password", - }, - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", + human: &AddHuman{ + Username: "username", + Password: "password", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", }, }, secretGenerator: GetMockSecretGenerator(t), }, res: res{ - want: &domain.Human{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "user1", + want: &domain.HumanDetails{ + ID: "user1", + ObjectDetails: domain.ObjectDetails{ ResourceOwner: "org1", }, - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - DisplayName: "firstname lastname", - PreferredLanguage: language.Und, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - State: domain.UserStateInitial, }, }, }, @@ -360,7 +396,7 @@ func TestCommandSide_AddHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + &userAgg.Aggregate, true, ), ), @@ -368,7 +404,7 @@ func TestCommandSide_AddHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + &userAgg.Aggregate, 1, false, false, @@ -377,14 +413,42 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent( + context.Background(), + &instanceAgg.Aggregate, + domain.SecretGeneratorTypeInitCode, + 0, + 1*time.Hour, + true, + true, + true, + true, + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( newAddHumanEvent("password", true, ""), ), + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent( + context.Background(), + &userAgg.Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte(""), + }, + 1*time.Hour, + ), + ), eventFromEventPusher( user.NewHumanEmailVerifiedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), + &userAgg.Aggregate), ), }, uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), @@ -392,44 +456,30 @@ func TestCommandSide_AddHuman(t *testing.T) { ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + initCodeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ ctx: context.Background(), orgID: "org1", - human: &domain.Human{ - Username: "username", - Password: &domain.Password{ - SecretString: "password", - }, - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - IsEmailVerified: true, + human: &AddHuman{ + Username: "username", + Password: "password", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", + Verified: true, }, + PasswordChangeRequired: true, }, secretGenerator: GetMockSecretGenerator(t), }, res: res{ - want: &domain.Human{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "user1", + want: &domain.HumanDetails{ + ID: "user1", + ObjectDetails: domain.ObjectDetails{ ResourceOwner: "org1", }, - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - DisplayName: "firstname lastname", - PreferredLanguage: language.Und, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - IsEmailVerified: true, - }, - State: domain.UserStateActive, }, }, }, @@ -441,20 +491,23 @@ func TestCommandSide_AddHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + &userAgg.Aggregate, true, ), ), ), expectFilter( eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, + instance.NewSecretGeneratorAddedEvent( + context.Background(), + &instanceAgg.Aggregate, + domain.SecretGeneratorTypeVerifyPhoneCode, + 0, + 1*time.Hour, + true, + true, + true, + true, ), ), ), @@ -464,25 +517,19 @@ func TestCommandSide_AddHuman(t *testing.T) { newAddHumanEvent("", false, "+41711234567"), ), eventFromEventPusher( - user.NewHumanInitialCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("a"), - }, - time.Hour*1, + user.NewHumanEmailVerifiedEvent( + context.Background(), + &userAgg.Aggregate, ), ), eventFromEventPusher( user.NewHumanPhoneCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + &userAgg.Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", - Crypted: []byte("a"), + Crypted: []byte(""), }, time.Hour*1)), }, @@ -490,45 +537,31 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + phoneAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ ctx: context.Background(), orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", + Verified: true, }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - Phone: &domain.Phone{ - PhoneNumber: "+41711234567", + Phone: Phone{ + Number: "+41711234567", }, }, secretGenerator: GetMockSecretGenerator(t), }, res: res{ - want: &domain.Human{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "user1", + want: &domain.HumanDetails{ + ID: "user1", + ObjectDetails: domain.ObjectDetails{ ResourceOwner: "org1", }, - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - DisplayName: "firstname lastname", - PreferredLanguage: language.Und, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - Phone: &domain.Phone{ - PhoneNumber: "+41711234567", - }, - State: domain.UserStateInitial, }, }, }, @@ -540,20 +573,38 @@ func TestCommandSide_AddHuman(t *testing.T) { expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + &userAgg.Aggregate, true, ), ), ), expectFilter( eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, + instance.NewSecretGeneratorAddedEvent( + context.Background(), + &instanceAgg.Aggregate, + domain.SecretGeneratorTypeInitCode, + 0, + 1*time.Hour, + true, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent( + context.Background(), + &instanceAgg.Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 0, + 1*time.Hour, + true, + true, + true, + true, ), ), ), @@ -563,80 +614,87 @@ func TestCommandSide_AddHuman(t *testing.T) { newAddHumanEvent("", false, "+41711234567"), ), eventFromEventPusher( - user.NewHumanInitialCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, + user.NewHumanInitialCodeAddedEvent( + context.Background(), + &userAgg.Aggregate, &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", - Crypted: []byte("a"), + Crypted: []byte(""), }, - time.Hour*1, + 1*time.Hour, ), ), eventFromEventPusher( - user.NewHumanPhoneVerifiedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), + user.NewHumanEmailCodeAddedEvent( + context.Background(), + &userAgg.Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte(""), + }, + 1*time.Hour, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneVerifiedEvent( + context.Background(), + &userAgg.Aggregate, + ), ), }, uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + initCodeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + emailCodeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ ctx: context.Background(), orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - Phone: &domain.Phone{ - PhoneNumber: "+41711234567", - IsPhoneVerified: true, + Phone: Phone{ + Number: "+41711234567", + Verified: true, }, }, secretGenerator: GetMockSecretGenerator(t), }, res: res{ - want: &domain.Human{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "user1", + want: &domain.HumanDetails{ + ID: "user1", + ObjectDetails: domain.ObjectDetails{ ResourceOwner: "org1", }, - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - DisplayName: "firstname lastname", - PreferredLanguage: language.Und, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - Phone: &domain.Phone{ - PhoneNumber: "+41711234567", - }, - State: domain.UserStateInitial, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, + r := &commandNew{ + es: tt.fields.eventstore, userPasswordAlg: tt.fields.userPasswordAlg, + initCodeAlg: tt.fields.initCodeAlg, + emailAlg: tt.fields.emailCodeAlg, + id: tt.fields.idGenerator, + phoneAlg: tt.fields.phoneAlg, } - got, err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.secretGenerator, tt.args.secretGenerator) + got, err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human) if tt.res.err == nil { - assert.NoError(t, err) + if !assert.NoError(t, err) { + t.FailNow() + } } if tt.res.err != nil && !tt.res.err(err) { t.Errorf("got wrong err: %v ", err) @@ -695,7 +753,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -722,7 +780,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -757,7 +815,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -798,7 +856,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -1455,7 +1513,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -1485,7 +1543,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -1523,7 +1581,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -1569,7 +1627,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -1631,7 +1689,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: errors.IsPreconditionFailed, }, }, { @@ -1693,7 +1751,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -1772,7 +1830,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -2409,7 +2467,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) { userID: "", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -2426,7 +2484,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) { userID: "user1", }, res: res{ - err: caos_errs.IsNotFound, + err: errors.IsNotFound, }, }, { @@ -2523,7 +2581,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { userIDs: []string{"user1"}, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -2539,7 +2597,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { userIDs: []string{}, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: errors.IsErrorInvalidArgument, }, }, { @@ -2739,3 +2797,168 @@ func newRegisterHumanEvent(username, password string, changeRequired bool, phone } return event } + +func TestAddHumanCommand(t *testing.T) { + type args struct { + a *user.Aggregate + human *AddHuman + passwordAlg crypto.HashAlgorithm + filter preparation.FilterToQueryReducer + phoneAlg crypto.EncryptionAlgorithm + emailAlg crypto.EncryptionAlgorithm + initCodeAlg crypto.EncryptionAlgorithm + } + agg := user.NewAggregate("id", "ro") + tests := []struct { + name string + args args + want Want + }{ + { + name: "invalid email", + args: args{ + a: agg, + human: &AddHuman{ + Email: Email{ + Address: "invalid", + }, + }, + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "USER-Ec7dM", "Errors.Invalid.Argument"), + }, + }, + { + name: "invalid first name", + args: args{ + a: agg, + human: &AddHuman{ + Username: "username", + Email: Email{ + Address: "support@zitadel.ch", + }, + }, + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.Invalid.Argument"), + }, + }, + { + name: "invalid last name", + args: args{ + a: agg, + human: &AddHuman{ + Username: "username", + FirstName: "hurst", + Email: Email{Address: "support@zitadel.ch"}, + }, + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "USER-DiAq8", "Errors.Invalid.Argument"), + }, + }, + { + name: "invalid password", + args: args{ + a: agg, + human: &AddHuman{ + Email: Email{Address: "support@zitadel.ch"}, + FirstName: "gigi", + LastName: "giraffe", + Password: "short", + Username: "username", + }, + filter: NewMultiFilter().Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewDomainPolicyAddedEvent( + context.Background(), + &org.NewAggregate("id", "ro").Aggregate, + true, + ), + }, nil + }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewPasswordComplexityPolicyAddedEvent( + context.Background(), + &org.NewAggregate("id", "ro").Aggregate, + 8, + true, + true, + true, + true, + ), + }, nil + }). + Filter(), + }, + want: Want{ + CreateErr: errors.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"), + }, + }, + { + name: "correct", + args: args{ + a: agg, + human: &AddHuman{ + Email: Email{Address: "support@zitadel.ch", Verified: true}, + FirstName: "gigi", + LastName: "giraffe", + Password: "", + Username: "username", + }, + passwordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + filter: NewMultiFilter().Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewDomainPolicyAddedEvent( + context.Background(), + &org.NewAggregate("id", "ro").Aggregate, + true, + ), + }, nil + }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewPasswordComplexityPolicyAddedEvent( + context.Background(), + &org.NewAggregate("id", "ro").Aggregate, + 2, + false, + false, + false, + false, + ), + }, nil + }). + Filter(), + }, + want: Want{ + Commands: []eventstore.Command{ + user.NewHumanAddedEvent( + context.Background(), + &agg.Aggregate, + "username", + "gigi", + "giraffe", + "", + "gigi giraffe", + language.Und, + 0, + "support@zitadel.ch", + true, + ), + user.NewHumanEmailVerifiedEvent(context.Background(), &agg.Aggregate), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + AssertValidation(t, addHumanCommand(tt.args.a, tt.args.human, tt.args.passwordAlg, tt.args.phoneAlg, tt.args.emailAlg, tt.args.initCodeAlg), tt.args.filter, tt.want) + }) + } +} diff --git a/internal/command/v2/user_password_complexity_policy.go b/internal/command/user_password_complexity_policy.go similarity index 67% rename from internal/command/v2/user_password_complexity_policy.go rename to internal/command/user_password_complexity_policy.go index 003c4eba5c..07f2745d02 100644 --- a/internal/command/v2/user_password_complexity_policy.go +++ b/internal/command/user_password_complexity_policy.go @@ -4,12 +4,11 @@ import ( "context" "github.com/caos/zitadel/internal/api/authz" - "github.com/caos/zitadel/internal/command" - "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/errors" ) -func passwordComplexityPolicyWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PasswordComplexityPolicyWriteModel, error) { +func passwordComplexityPolicyWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer) (*PasswordComplexityPolicyWriteModel, error) { wm, err := customPasswordComplexityPolicy(ctx, filter) if err != nil || wm != nil && wm.State.Exists() { return wm, err @@ -21,8 +20,8 @@ func passwordComplexityPolicyWriteModel(ctx context.Context, filter preparation. return nil, errors.ThrowInternal(nil, "USER-uQ96e", "Errors.Internal") } -func customPasswordComplexityPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PasswordComplexityPolicyWriteModel, error) { - policy := command.NewOrgPasswordComplexityPolicyWriteModel(authz.GetCtxData(ctx).OrgID) +func customPasswordComplexityPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*PasswordComplexityPolicyWriteModel, error) { + policy := NewOrgPasswordComplexityPolicyWriteModel(authz.GetCtxData(ctx).OrgID) events, err := filter(ctx, policy.Query()) if err != nil { return nil, err @@ -35,8 +34,8 @@ func customPasswordComplexityPolicy(ctx context.Context, filter preparation.Filt return &policy.PasswordComplexityPolicyWriteModel, err } -func defaultPasswordComplexityPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PasswordComplexityPolicyWriteModel, error) { - policy := command.NewInstancePasswordComplexityPolicyWriteModel(ctx) +func defaultPasswordComplexityPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*PasswordComplexityPolicyWriteModel, error) { + policy := NewInstancePasswordComplexityPolicyWriteModel(ctx) events, err := filter(ctx, policy.Query()) if err != nil { return nil, err diff --git a/internal/command/v2/user_password_complexity_policy_test.go b/internal/command/user_password_complexity_policy_test.go similarity index 93% rename from internal/command/v2/user_password_complexity_policy_test.go rename to internal/command/user_password_complexity_policy_test.go index 3ee8eb7adc..b24fdd5a6a 100644 --- a/internal/command/v2/user_password_complexity_policy_test.go +++ b/internal/command/user_password_complexity_policy_test.go @@ -6,8 +6,7 @@ import ( "testing" "github.com/caos/zitadel/internal/api/authz" - "github.com/caos/zitadel/internal/command" - "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" @@ -22,7 +21,7 @@ func Test_customPasswordComplexityPolicy(t *testing.T) { tests := []struct { name string args args - want *command.PasswordComplexityPolicyWriteModel + want *PasswordComplexityPolicyWriteModel wantErr bool }{ { @@ -62,7 +61,7 @@ func Test_customPasswordComplexityPolicy(t *testing.T) { }, nil }, }, - want: &command.PasswordComplexityPolicyWriteModel{ + want: &PasswordComplexityPolicyWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: "id", ResourceOwner: "ro", @@ -99,7 +98,7 @@ func Test_defaultPasswordComplexityPolicy(t *testing.T) { tests := []struct { name string args args - want *command.PasswordComplexityPolicyWriteModel + want *PasswordComplexityPolicyWriteModel wantErr bool }{ { @@ -139,7 +138,7 @@ func Test_defaultPasswordComplexityPolicy(t *testing.T) { }, nil }, }, - want: &command.PasswordComplexityPolicyWriteModel{ + want: &PasswordComplexityPolicyWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: "INSTANCE", ResourceOwner: "INSTANCE", @@ -176,7 +175,7 @@ func Test_passwordComplexityPolicy(t *testing.T) { tests := []struct { name string args args - want *command.PasswordComplexityPolicyWriteModel + want *PasswordComplexityPolicyWriteModel wantErr bool }{ { @@ -206,7 +205,7 @@ func Test_passwordComplexityPolicy(t *testing.T) { }, nil }, }, - want: &command.PasswordComplexityPolicyWriteModel{ + want: &PasswordComplexityPolicyWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: "id", ResourceOwner: "ro", @@ -258,7 +257,7 @@ func Test_passwordComplexityPolicy(t *testing.T) { }). Filter(), }, - want: &command.PasswordComplexityPolicyWriteModel{ + want: &PasswordComplexityPolicyWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: "INSTANCE", ResourceOwner: "INSTANCE", diff --git a/internal/command/user_test.go b/internal/command/user_test.go index c11f85580b..195948d148 100644 --- a/internal/command/user_test.go +++ b/internal/command/user_test.go @@ -11,7 +11,9 @@ import ( "github.com/stretchr/testify/assert" "golang.org/x/text/language" + "github.com/caos/zitadel/internal/command/preparation" "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/repository" @@ -1577,3 +1579,155 @@ func TestCommandSide_UserDomainClaimedSent(t *testing.T) { }) } } + +func TestExistsUser(t *testing.T) { + type args struct { + filter preparation.FilterToQueryReducer + id string + resourceOwner string + } + tests := []struct { + name string + args args + wantExists bool + wantErr bool + }{ + { + name: "no events", + args: args{ + filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{}, nil + }, + id: "id", + resourceOwner: "ro", + }, + wantExists: false, + wantErr: false, + }, + { + name: "human registered", + args: args{ + filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + user.NewHumanRegisteredEvent( + context.Background(), + &user.NewAggregate("id", "ro").Aggregate, + "userName", + "firstName", + "lastName", + "nickName", + "displayName", + language.German, + domain.GenderFemale, + "support@zitadel.ch", + true, + ), + }, nil + }, + id: "id", + resourceOwner: "ro", + }, + wantExists: true, + wantErr: false, + }, + { + name: "human added", + args: args{ + filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("id", "ro").Aggregate, + "userName", + "firstName", + "lastName", + "nickName", + "displayName", + language.German, + domain.GenderFemale, + "support@zitadel.ch", + true, + ), + }, nil + }, + id: "id", + resourceOwner: "ro", + }, + wantExists: true, + wantErr: false, + }, + { + name: "machine added", + args: args{ + filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + user.NewMachineAddedEvent( + context.Background(), + &user.NewAggregate("id", "ro").Aggregate, + "userName", + "name", + "description", + true, + ), + }, nil + }, + id: "id", + resourceOwner: "ro", + }, + wantExists: true, + wantErr: false, + }, + { + name: "user removed", + args: args{ + filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + user.NewMachineAddedEvent( + context.Background(), + &user.NewAggregate("removed", "ro").Aggregate, + "userName", + "name", + "description", + true, + ), + user.NewUserRemovedEvent( + context.Background(), + &user.NewAggregate("removed", "ro").Aggregate, + "userName", + nil, + true, + ), + }, nil + }, + id: "id", + resourceOwner: "ro", + }, + wantExists: false, + wantErr: false, + }, + { + name: "error durring filter", + args: args{ + filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return nil, errors.ThrowInternal(nil, "USER-Drebn", "Errors.Internal") + }, + id: "id", + resourceOwner: "ro", + }, + wantExists: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner) + if (err != nil) != tt.wantErr { + t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotExists != tt.wantExists { + t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists) + } + }) + } +} diff --git a/internal/command/v2/command.go b/internal/command/v2/command.go deleted file mode 100644 index 3fd4c66903..0000000000 --- a/internal/command/v2/command.go +++ /dev/null @@ -1,36 +0,0 @@ -package command - -import ( - sd "github.com/caos/zitadel/internal/config/systemdefaults" - "github.com/caos/zitadel/internal/crypto" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/action" - iam_repo "github.com/caos/zitadel/internal/repository/instance" - "github.com/caos/zitadel/internal/repository/keypair" - "github.com/caos/zitadel/internal/repository/org" - proj_repo "github.com/caos/zitadel/internal/repository/project" - usr_repo "github.com/caos/zitadel/internal/repository/user" - usr_grant_repo "github.com/caos/zitadel/internal/repository/usergrant" -) - -type Command struct { - es *eventstore.Eventstore - userPasswordAlg crypto.HashAlgorithm - iamDomain string -} - -func New(es *eventstore.Eventstore, iamDomain string, defaults sd.SystemDefaults) *Command { - iam_repo.RegisterEventMappers(es) - org.RegisterEventMappers(es) - usr_repo.RegisterEventMappers(es) - usr_grant_repo.RegisterEventMappers(es) - proj_repo.RegisterEventMappers(es) - keypair.RegisterEventMappers(es) - action.RegisterEventMappers(es) - - return &Command{ - es: es, - iamDomain: iamDomain, - userPasswordAlg: crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost), - } -} diff --git a/internal/command/v2/instance_features.go b/internal/command/v2/instance_features.go deleted file mode 100644 index c175b2aab6..0000000000 --- a/internal/command/v2/instance_features.go +++ /dev/null @@ -1,95 +0,0 @@ -package command - -import ( - "context" - "time" - - "github.com/caos/zitadel/internal/command" - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/domain" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/instance" -) - -func SetDefaultFeatures( - a *instance.Aggregate, - tierName, - tierDescription string, - state domain.FeaturesState, - stateDescription string, - retention time.Duration, - loginPolicyFactors, - loginPolicyIDP, - loginPolicyPasswordless, - loginPolicyRegistration, - loginPolicyUsernameLogin, - loginPolicyPasswordReset, - passwordComplexityPolicy, - labelPolicyPrivateLabel, - labelPolicyWatermark, - customDomain, - privacyPolicy, - metadataUser, - customTextMessage, - customTextLogin, - lockoutPolicy bool, - actionsAllowed domain.ActionsAllowed, - maxActions int, -) preparation.Validation { - return func() (preparation.CreateCommands, error) { - if !state.Valid() || state == domain.FeaturesStateUnspecified || state == domain.FeaturesStateRemoved { - return nil, errors.ThrowInvalidArgument(nil, "INSTA-d3r1s", "Errors.Invalid.Argument") - } - return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := defaultFeatures(ctx, filter) - if err != nil { - return nil, err - } - event, hasChanged := writeModel.NewSetEvent(ctx, &a.Aggregate, - tierName, - tierDescription, - state, - stateDescription, - retention, - loginPolicyFactors, - loginPolicyIDP, - loginPolicyPasswordless, - loginPolicyRegistration, - loginPolicyUsernameLogin, - loginPolicyPasswordReset, - passwordComplexityPolicy, - labelPolicyPrivateLabel, - labelPolicyWatermark, - customDomain, - privacyPolicy, - metadataUser, - customTextMessage, - customTextLogin, - lockoutPolicy, - actionsAllowed, - maxActions, - ) - if !hasChanged { - return nil, errors.ThrowPreconditionFailed(nil, "INSTA-GE4h2", "Errors.Features.NotChanged") - } - return []eventstore.Command{ - event, - }, nil - }, nil - } -} - -func defaultFeatures(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.InstanceFeaturesWriteModel, error) { - features := command.NewInstanceFeaturesWriteModel(ctx) - events, err := filter(ctx, features.Query()) - if err != nil { - return nil, err - } - if len(events) == 0 { - return features, nil - } - features.AppendEvents(events...) - err = features.Reduce() - return features, err -} diff --git a/internal/command/v2/instance_member.go b/internal/command/v2/instance_member.go deleted file mode 100644 index 537fac2698..0000000000 --- a/internal/command/v2/instance_member.go +++ /dev/null @@ -1,64 +0,0 @@ -package command - -import ( - "context" - - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/instance" -) - -func AddInstanceMember(a *instance.Aggregate, userID string, roles ...string) preparation.Validation { - return func() (preparation.CreateCommands, error) { - if userID == "" { - return nil, errors.ThrowInvalidArgument(nil, "INSTA-SDSfs", "Errors.Invalid.Argument") - } - // TODO: check roles - return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - if exists, err := ExistsUser(ctx, filter, userID, ""); err != nil || !exists { - return nil, errors.ThrowNotFound(err, "INSTA-GSXOn", "Errors.User.NotFound") - } - if isMember, err := IsInstanceMember(ctx, filter, a.ID, userID); err != nil || isMember { - return nil, errors.ThrowAlreadyExists(err, "INSTA-pFDwe", "Errors.Instance.Member.AlreadyExists") - } - return []eventstore.Command{instance.NewMemberAddedEvent(ctx, &a.Aggregate, userID, roles...)}, nil - }, - nil - } -} - -func IsInstanceMember(ctx context.Context, filter preparation.FilterToQueryReducer, instanceID, userID string) (isMember bool, err error) { - events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - OrderAsc(). - AddQuery(). - AggregateIDs(instanceID). - AggregateTypes(instance.AggregateType). - EventTypes( - instance.MemberAddedEventType, - instance.MemberRemovedEventType, - instance.MemberCascadeRemovedEventType, - ).Builder()) - if err != nil { - return false, err - } - - for _, event := range events { - switch e := event.(type) { - case *instance.MemberAddedEvent: - if e.UserID == userID { - isMember = true - } - case *instance.MemberRemovedEvent: - if e.UserID == userID { - isMember = false - } - case *instance.MemberCascadeRemovedEvent: - if e.UserID == userID { - isMember = false - } - } - } - - return isMember, nil -} diff --git a/internal/command/v2/org.go b/internal/command/v2/org.go deleted file mode 100644 index 66e8ad8994..0000000000 --- a/internal/command/v2/org.go +++ /dev/null @@ -1,72 +0,0 @@ -package command - -import ( - "context" - "strings" - - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/domain" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/id" - "github.com/caos/zitadel/internal/repository/org" - user_repo "github.com/caos/zitadel/internal/repository/user" -) - -type OrgSetup struct { - Name string - Human AddHuman -} - -func (command *Command) SetUpOrg(ctx context.Context, o *OrgSetup) (*domain.ObjectDetails, error) { - orgID, err := id.SonyFlakeGenerator.Next() - if err != nil { - return nil, err - } - - userID, err := id.SonyFlakeGenerator.Next() - if err != nil { - return nil, err - } - - orgAgg := org.NewAggregate(orgID, orgID) - userAgg := user_repo.NewAggregate(userID, orgID) - - cmds, err := preparation.PrepareCommands(ctx, command.es.Filter, - AddOrg(orgAgg, o.Name, command.iamDomain), - AddHumanCommand(userAgg, &o.Human, command.userPasswordAlg), - AddOrgMember(orgAgg, userID, domain.RoleOrgOwner), - ) - if err != nil { - return nil, err - } - - events, err := command.es.Push(ctx, cmds...) - if err != nil { - return nil, err - } - return &domain.ObjectDetails{ - Sequence: events[len(events)-1].Sequence(), - EventDate: events[len(events)-1].CreationDate(), - ResourceOwner: orgID, - }, nil -} - -//AddOrg defines the commands to create a new org, -// this includes the verified default domain -func AddOrg(a *org.Aggregate, name, iamDomain string) preparation.Validation { - return func() (preparation.CreateCommands, error) { - if name = strings.TrimSpace(name); name == "" { - return nil, errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument") - } - defaultDomain := domain.NewIAMDomainName(name, iamDomain) - return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - return []eventstore.Command{ - org.NewOrgAddedEvent(ctx, &a.Aggregate, name), - org.NewDomainAddedEvent(ctx, &a.Aggregate, defaultDomain), - org.NewDomainVerifiedEvent(ctx, &a.Aggregate, defaultDomain), - org.NewDomainPrimarySetEvent(ctx, &a.Aggregate, defaultDomain), - }, nil - }, nil - } -} diff --git a/internal/command/v2/org_domain.go b/internal/command/v2/org_domain.go deleted file mode 100644 index 9393a1097a..0000000000 --- a/internal/command/v2/org_domain.go +++ /dev/null @@ -1,46 +0,0 @@ -package command - -import ( - "context" - "strings" - - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/org" -) - -func AddOrgDomain(a *org.Aggregate, domain string) preparation.Validation { - return func() (preparation.CreateCommands, error) { - if domain = strings.TrimSpace(domain); domain == "" { - return nil, errors.ThrowInvalidArgument(nil, "ORG-r3h4J", "Errors.Invalid.Argument") - } - return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - return []eventstore.Command{org.NewDomainAddedEvent(ctx, &a.Aggregate, domain)}, nil - }, nil - } -} - -func VerifyOrgDomain(a *org.Aggregate, domain string) preparation.Validation { - return func() (preparation.CreateCommands, error) { - if domain = strings.TrimSpace(domain); domain == "" { - return nil, errors.ThrowInvalidArgument(nil, "ORG-yqlVQ", "Errors.Invalid.Argument") - } - return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - //TODO: check if already exists - return []eventstore.Command{org.NewDomainVerifiedEvent(ctx, &a.Aggregate, domain)}, nil - }, nil - } -} - -func SetPrimaryOrgDomain(a *org.Aggregate, domain string) preparation.Validation { - return func() (preparation.CreateCommands, error) { - if domain = strings.TrimSpace(domain); domain == "" { - return nil, errors.ThrowInvalidArgument(nil, "ORG-gmNqY", "Errors.Invalid.Argument") - } - return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - //TODO: check if already exists and verified - return []eventstore.Command{org.NewDomainPrimarySetEvent(ctx, &a.Aggregate, domain)}, nil - }, nil - } -} diff --git a/internal/command/v2/org_domain_test.go b/internal/command/v2/org_domain_test.go deleted file mode 100644 index 8a5ff01ae1..0000000000 --- a/internal/command/v2/org_domain_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package command - -import ( - "context" - "testing" - - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/org" -) - -func TestAddDomain(t *testing.T) { - type args struct { - a *org.Aggregate - domain string - } - - tests := []struct { - name string - args args - want Want - }{ - { - name: "invalid domain", - args: args{ - a: org.NewAggregate("test", "test"), - domain: "", - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-r3h4J", "Errors.Invalid.Argument"), - }, - }, - { - name: "correct", - args: args{ - a: org.NewAggregate("test", "test"), - domain: "domain", - }, - want: Want{ - Commands: []eventstore.Command{ - org.NewDomainAddedEvent(context.Background(), &org.NewAggregate("test", "test").Aggregate, "domain"), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, AddOrgDomain(tt.args.a, tt.args.domain), nil, tt.want) - }) - } -} - -func TestVerifyDomain(t *testing.T) { - type args struct { - a *org.Aggregate - domain string - } - - tests := []struct { - name string - args args - want Want - }{ - { - name: "invalid domain", - args: args{ - a: org.NewAggregate("test", "test"), - domain: "", - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-yqlVQ", "Errors.Invalid.Argument"), - }, - }, - { - name: "correct", - args: args{ - a: org.NewAggregate("test", "test"), - domain: "domain", - }, - want: Want{ - Commands: []eventstore.Command{ - org.NewDomainVerifiedEvent(context.Background(), &org.NewAggregate("test", "test").Aggregate, "domain"), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, VerifyOrgDomain(tt.args.a, tt.args.domain), nil, tt.want) - }) - } -} - -func TestSetDomainPrimary(t *testing.T) { - type args struct { - a *org.Aggregate - domain string - } - - tests := []struct { - name string - args args - want Want - }{ - { - name: "invalid domain", - args: args{ - a: org.NewAggregate("test", "test"), - domain: "", - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-gmNqY", "Errors.Invalid.Argument"), - }, - }, - { - name: "correct", - args: args{ - a: org.NewAggregate("test", "test"), - domain: "domain", - }, - want: Want{ - Commands: []eventstore.Command{ - org.NewDomainPrimarySetEvent(context.Background(), &org.NewAggregate("test", "test").Aggregate, "domain"), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, SetPrimaryOrgDomain(tt.args.a, tt.args.domain), nil, tt.want) - }) - } -} diff --git a/internal/command/v2/org_member.go b/internal/command/v2/org_member.go deleted file mode 100644 index f5182fb85e..0000000000 --- a/internal/command/v2/org_member.go +++ /dev/null @@ -1,65 +0,0 @@ -package command - -import ( - "context" - - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/org" -) - -func AddOrgMember(a *org.Aggregate, userID string, roles ...string) preparation.Validation { - return func() (preparation.CreateCommands, error) { - if userID == "" { - return nil, errors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument") - } - // TODO: check roles - return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - if exists, err := ExistsUser(ctx, filter, userID, a.ID); err != nil || !exists { - return nil, errors.ThrowNotFound(err, "ORG-GoXOn", "Errors.User.NotFound") - } - if isMember, err := IsOrgMember(ctx, filter, a.ID, userID); err != nil || isMember { - return nil, errors.ThrowAlreadyExists(err, "ORG-poWwe", "Errors.Org.Member.AlreadyExists") - } - return []eventstore.Command{org.NewMemberAddedEvent(ctx, &a.Aggregate, userID, roles...)}, nil - }, - nil - } -} - -func IsOrgMember(ctx context.Context, filter preparation.FilterToQueryReducer, orgID, userID string) (isMember bool, err error) { - events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - ResourceOwner(orgID). - OrderAsc(). - AddQuery(). - AggregateIDs(orgID). - AggregateTypes(org.AggregateType). - EventTypes( - org.MemberAddedEventType, - org.MemberRemovedEventType, - org.MemberCascadeRemovedEventType, - ).Builder()) - if err != nil { - return false, err - } - - for _, event := range events { - switch e := event.(type) { - case *org.MemberAddedEvent: - if e.UserID == userID { - isMember = true - } - case *org.MemberRemovedEvent: - if e.UserID == userID { - isMember = false - } - case *org.MemberCascadeRemovedEvent: - if e.UserID == userID { - isMember = false - } - } - } - - return isMember, nil -} diff --git a/internal/command/v2/org_member_test.go b/internal/command/v2/org_member_test.go deleted file mode 100644 index 88665f628e..0000000000 --- a/internal/command/v2/org_member_test.go +++ /dev/null @@ -1,249 +0,0 @@ -package command - -import ( - "context" - "testing" - - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/org" - "github.com/caos/zitadel/internal/repository/user" -) - -func TestAddMember(t *testing.T) { - type args struct { - a *org.Aggregate - userID string - roles []string - filter preparation.FilterToQueryReducer - } - - ctx := context.Background() - agg := org.NewAggregate("test", "test") - - tests := []struct { - name string - args args - want Want - }{ - { - name: "no user id", - args: args{ - a: agg, - userID: "", - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"), - }, - }, - // { - // name: "TODO: invalid roles", - // args: args{ - // a: agg, - // userID: "", - // roles: []string{""}, - // }, - // want: preparation.Want{ - // ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"), - // }, - // }, - { - name: "user not exists", - args: args{ - a: agg, - userID: "userID", - filter: NewMultiFilter(). - Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return nil, nil - }). - Filter(), - }, - want: Want{ - CreateErr: errors.ThrowNotFound(nil, "ORG-GoXOn", "Errors.User.NotFound"), - }, - }, - { - name: "already member", - args: args{ - a: agg, - userID: "userID", - filter: NewMultiFilter(). - Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - user.NewMachineAddedEvent( - ctx, - &user.NewAggregate("id", "ro").Aggregate, - "userName", - "name", - "description", - true, - ), - }, nil - }). - Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - org.NewMemberAddedEvent( - ctx, - &org.NewAggregate("id", "ro").Aggregate, - "userID", - ), - }, nil - }). - Filter(), - }, - want: Want{ - CreateErr: errors.ThrowAlreadyExists(nil, "ORG-poWwe", "Errors.Org.Member.AlreadyExists"), - }, - }, - { - name: "correct", - args: args{ - a: agg, - userID: "userID", - filter: NewMultiFilter(). - Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - user.NewMachineAddedEvent( - ctx, - &user.NewAggregate("id", "ro").Aggregate, - "userName", - "name", - "description", - true, - ), - }, nil - }). - Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return nil, nil - }). - Filter(), - }, - want: Want{ - Commands: []eventstore.Command{ - org.NewMemberAddedEvent(ctx, &agg.Aggregate, "userID"), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, AddOrgMember(tt.args.a, tt.args.userID, tt.args.roles...), tt.args.filter, tt.want) - }) - } -} - -func TestIsMember(t *testing.T) { - type args struct { - filter preparation.FilterToQueryReducer - orgID string - userID string - } - tests := []struct { - name string - args args - wantExists bool - wantErr bool - }{ - { - name: "no events", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{}, nil - }, - orgID: "orgID", - userID: "userID", - }, - wantExists: false, - wantErr: false, - }, - { - name: "member added", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - org.NewMemberAddedEvent( - context.Background(), - &org.NewAggregate("orgID", "ro").Aggregate, - "userID", - ), - }, nil - }, - orgID: "orgID", - userID: "userID", - }, - wantExists: true, - wantErr: false, - }, - { - name: "member removed", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - org.NewMemberAddedEvent( - context.Background(), - &org.NewAggregate("orgID", "ro").Aggregate, - "userID", - ), - org.NewMemberRemovedEvent( - context.Background(), - &org.NewAggregate("orgID", "ro").Aggregate, - "userID", - ), - }, nil - }, - orgID: "orgID", - userID: "userID", - }, - wantExists: false, - wantErr: false, - }, - { - name: "member cascade removed", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - org.NewMemberAddedEvent( - context.Background(), - &org.NewAggregate("orgID", "ro").Aggregate, - "userID", - ), - org.NewMemberCascadeRemovedEvent( - context.Background(), - &org.NewAggregate("orgID", "ro").Aggregate, - "userID", - ), - }, nil - }, - orgID: "orgID", - userID: "userID", - }, - wantExists: false, - wantErr: false, - }, - { - name: "error durring filter", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return nil, errors.ThrowInternal(nil, "PROJE-Op26p", "Errors.Internal") - }, - orgID: "orgID", - userID: "userID", - }, - wantExists: false, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotExists, err := IsOrgMember(context.Background(), tt.args.filter, tt.args.orgID, tt.args.userID) - if (err != nil) != tt.wantErr { - t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotExists != tt.wantExists { - t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists) - } - }) - } -} diff --git a/internal/command/v2/org_test.go b/internal/command/v2/org_test.go deleted file mode 100644 index b4b5b64478..0000000000 --- a/internal/command/v2/org_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package command - -import ( - "context" - "testing" - - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/org" -) - -func TestAddOrg(t *testing.T) { - type args struct { - a *org.Aggregate - name string - } - - ctx := context.Background() - agg := org.NewAggregate("test", "test") - - tests := []struct { - name string - args args - want Want - }{ - { - name: "invalid domain", - args: args{ - a: agg, - name: "", - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument"), - }, - }, - { - name: "correct", - args: args{ - a: agg, - name: "caos ag", - }, - want: Want{ - Commands: []eventstore.Command{ - org.NewOrgAddedEvent(ctx, &agg.Aggregate, "caos ag"), - org.NewDomainAddedEvent(ctx, &agg.Aggregate, "caos-ag.localhost"), - org.NewDomainVerifiedEvent(ctx, &agg.Aggregate, "caos-ag.localhost"), - org.NewDomainPrimarySetEvent(ctx, &agg.Aggregate, "caos-ag.localhost"), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, AddOrg(tt.args.a, tt.args.name, "localhost"), nil, tt.want) - }) - } -} diff --git a/internal/command/v2/project.go b/internal/command/v2/project.go deleted file mode 100644 index 2e95af474c..0000000000 --- a/internal/command/v2/project.go +++ /dev/null @@ -1,75 +0,0 @@ -package command - -import ( - "context" - "strings" - - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/domain" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/project" -) - -func AddProject( - a *project.Aggregate, - name string, - owner string, - projectRoleAssertion bool, - projectRoleCheck bool, - hasProjectCheck bool, - privateLabelingSetting domain.PrivateLabelingSetting, -) preparation.Validation { - return func() (preparation.CreateCommands, error) { - if name = strings.TrimSpace(name); name == "" { - return nil, errors.ThrowInvalidArgument(nil, "PROJE-C01yo", "Errors.Invalid.Argument") - } - if !privateLabelingSetting.Valid() { - return nil, errors.ThrowInvalidArgument(nil, "PROJE-AO52V", "Errors.Invalid.Argument") - } - if owner == "" { - return nil, errors.ThrowPreconditionFailed(nil, "PROJE-hzxwo", "Errors.Invalid.Argument") - } - return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - return []eventstore.Command{ - project.NewProjectAddedEvent(ctx, &a.Aggregate, - name, - projectRoleAssertion, - projectRoleCheck, - hasProjectCheck, - privateLabelingSetting, - ), - project.NewProjectMemberAddedEvent(ctx, &a.Aggregate, - owner, - domain.RoleProjectOwner), - }, nil - }, nil - } -} - -func ExistsProject(ctx context.Context, filter preparation.FilterToQueryReducer, projectID, resourceOwner string) (exists bool, err error) { - events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - ResourceOwner(resourceOwner). - OrderAsc(). - AddQuery(). - AggregateTypes(project.AggregateType). - AggregateIDs(projectID). - EventTypes( - project.ProjectAddedType, - project.ProjectRemovedType, - ).Builder()) - if err != nil { - return false, err - } - - for _, event := range events { - switch event.(type) { - case *project.ProjectAddedEvent: - exists = true - case *project.ProjectRemovedEvent: - exists = false - } - } - - return exists, nil -} diff --git a/internal/command/v2/project_app.go b/internal/command/v2/project_app.go deleted file mode 100644 index 050485b3f5..0000000000 --- a/internal/command/v2/project_app.go +++ /dev/null @@ -1,155 +0,0 @@ -package command - -import ( - "context" - "strings" - "time" - - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/crypto" - "github.com/caos/zitadel/internal/domain" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/project" -) - -func AddOIDCApp( - a project.Aggregate, - version domain.OIDCVersion, - appID, - name, - clientID string, - clientSecret *crypto.CryptoValue, - redirectUris []string, - responseTypes []domain.OIDCResponseType, - grantTypes []domain.OIDCGrantType, - applicationType domain.OIDCApplicationType, - authMethodType domain.OIDCAuthMethodType, - postLogoutRedirectUris []string, - devMode bool, - accessTokenType domain.OIDCTokenType, - accessTokenRoleAssertion bool, - idTokenRoleAssertion bool, - idTokenUserinfoAssertion bool, - clockSkew time.Duration, - additionalOrigins []string, -) preparation.Validation { - return func() (preparation.CreateCommands, error) { - if appID == "" { - return nil, errors.ThrowInvalidArgument(nil, "PROJE-NnavI", "Errors.Invalid.Argument") - } - if name = strings.TrimSpace(name); name == "" { - return nil, errors.ThrowInvalidArgument(nil, "PROJE-Fef31", "Errors.Invalid.Argument") - } - if clientID == "" { - return nil, errors.ThrowInvalidArgument(nil, "PROJE-ghTsJ", "Errors.Invalid.Argument") - } - return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - if exists, err := ExistsProject(ctx, filter, a.ID, a.ResourceOwner); !exists || err != nil { - return nil, errors.ThrowNotFound(err, "PROJE-5LQ0U", "Errors.Project.NotFound") - } - return []eventstore.Command{ - project.NewApplicationAddedEvent( - ctx, - &a.Aggregate, - appID, - name, - ), - project.NewOIDCConfigAddedEvent( - ctx, - &a.Aggregate, - version, - appID, - clientID, - clientSecret, - redirectUris, - responseTypes, - grantTypes, - applicationType, - authMethodType, - postLogoutRedirectUris, - devMode, - accessTokenType, - accessTokenRoleAssertion, - idTokenRoleAssertion, - idTokenUserinfoAssertion, - clockSkew, - additionalOrigins, - ), - }, nil - }, nil - } -} - -func AddAPIApp( - a project.Aggregate, - appID, - name, - clientID string, - clientSecret *crypto.CryptoValue, - authMethodType domain.APIAuthMethodType, -) preparation.Validation { - return func() (preparation.CreateCommands, error) { - if appID == "" { - return nil, errors.ThrowInvalidArgument(nil, "PROJE-XHsKt", "Errors.Invalid.Argument") - } - if name = strings.TrimSpace(name); name == "" { - return nil, errors.ThrowInvalidArgument(nil, "PROJE-F7g21", "Errors.Invalid.Argument") - } - if clientID == "" { - return nil, errors.ThrowInvalidArgument(nil, "PROJE-XXED5", "Errors.Invalid.Argument") - } - return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - if exists, err := ExistsProject(ctx, filter, a.ID, a.ResourceOwner); !exists || err != nil { - return nil, errors.ThrowNotFound(err, "PROJE-Sf2gb", "Errors.Project.NotFound") - } - return []eventstore.Command{ - project.NewApplicationAddedEvent( - ctx, - &a.Aggregate, - appID, - name, - ), - project.NewAPIConfigAddedEvent( - ctx, - &a.Aggregate, - appID, - clientID, - clientSecret, - authMethodType, - ), - }, nil - }, nil - } -} - -func ExistsApp(ctx context.Context, filter preparation.FilterToQueryReducer, projectID, appID, resourceOwner string) (exists bool, err error) { - events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - ResourceOwner(resourceOwner). - OrderAsc(). - AddQuery(). - AggregateTypes(project.AggregateType). - AggregateIDs(projectID). - EventTypes( - project.ApplicationAddedType, - project.ApplicationRemovedType, - ).Builder()) - if err != nil { - return false, err - } - - for _, event := range events { - switch e := event.(type) { - case *project.ApplicationAddedEvent: - if e.AppID == appID { - exists = true - } - case *project.ApplicationRemovedEvent: - if e.AppID == appID { - exists = false - } - } - } - - return exists, nil -} diff --git a/internal/command/v2/project_app_test.go b/internal/command/v2/project_app_test.go deleted file mode 100644 index dff26631c4..0000000000 --- a/internal/command/v2/project_app_test.go +++ /dev/null @@ -1,386 +0,0 @@ -package command - -import ( - "context" - "testing" - - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/domain" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/project" -) - -func TestAddOIDCApp(t *testing.T) { - type args struct { - a *project.Aggregate - appID string - name string - clientID string - filter preparation.FilterToQueryReducer - } - - ctx := context.Background() - agg := project.NewAggregate("test", "test") - - tests := []struct { - name string - args args - want Want - }{ - { - name: "invalid appID", - args: args{ - a: agg, - appID: "", - name: "name", - clientID: "clientID", - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-NnavI", "Errors.Invalid.Argument"), - }, - }, - { - name: "invalid name", - args: args{ - a: agg, - appID: "appID", - name: "", - clientID: "clientID", - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-Fef31", "Errors.Invalid.Argument"), - }, - }, - { - name: "invalid clientID", - args: args{ - a: agg, - appID: "appID", - name: "name", - clientID: "", - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-ghTsJ", "Errors.Invalid.Argument"), - }, - }, - { - name: "project not exists", - args: args{ - a: agg, - appID: "id", - name: "name", - clientID: "clientID", - filter: NewMultiFilter(). - Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return nil, nil - }). - Filter(), - }, - want: Want{ - CreateErr: errors.ThrowNotFound(nil, "PROJE-5LQ0U", "Errors.Project.NotFound"), - }, - }, - { - name: "correct", - args: args{ - a: agg, - appID: "appID", - name: "name", - clientID: "clientID", - filter: NewMultiFilter(). - Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - project.NewProjectAddedEvent( - ctx, - &agg.Aggregate, - "project", - false, - false, - false, - domain.PrivateLabelingSettingUnspecified, - ), - }, nil - }). - Filter(), - }, - want: Want{ - Commands: []eventstore.Command{ - project.NewApplicationAddedEvent(ctx, &agg.Aggregate, - "appID", - "name", - ), - project.NewOIDCConfigAddedEvent(ctx, &agg.Aggregate, - domain.OIDCVersionV1, - "appID", - "clientID", - nil, - nil, - nil, - nil, - domain.OIDCApplicationTypeWeb, - domain.OIDCAuthMethodTypeBasic, - nil, - false, - domain.OIDCTokenTypeBearer, - false, - false, - false, - 0, - nil, - ), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, - AddOIDCApp(*tt.args.a, - domain.OIDCVersionV1, - tt.args.appID, - tt.args.name, - tt.args.clientID, - nil, - nil, - nil, - nil, - domain.OIDCApplicationTypeWeb, - domain.OIDCAuthMethodTypeBasic, - nil, - false, - domain.OIDCTokenTypeBearer, - false, - false, - false, - 0, - nil, - ), tt.args.filter, tt.want) - }) - } -} - -func TestAddAPIConfig(t *testing.T) { - type args struct { - a *project.Aggregate - appID string - name string - clientID string - filter preparation.FilterToQueryReducer - } - - ctx := context.Background() - agg := project.NewAggregate("test", "test") - - tests := []struct { - name string - args args - want Want - }{ - { - name: "invalid appID", - args: args{ - a: agg, - appID: "", - name: "name", - clientID: "clientID", - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-XHsKt", "Errors.Invalid.Argument"), - }, - }, - { - name: "invalid name", - args: args{ - a: agg, - appID: "appID", - name: "", - clientID: "clientID", - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-F7g21", "Errors.Invalid.Argument"), - }, - }, - { - name: "invalid clientID", - args: args{ - a: agg, - appID: "appID", - name: "name", - clientID: "", - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-XXED5", "Errors.Invalid.Argument"), - }, - }, - { - name: "project not exists", - args: args{ - a: agg, - appID: "id", - name: "name", - clientID: "clientID", - filter: NewMultiFilter(). - Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return nil, nil - }). - Filter(), - }, - want: Want{ - CreateErr: errors.ThrowNotFound(nil, "PROJE-Sf2gb", "Errors.Project.NotFound"), - }, - }, - { - name: "correct", - args: args{ - a: agg, - appID: "appID", - name: "name", - clientID: "clientID", - filter: NewMultiFilter(). - Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - project.NewProjectAddedEvent( - ctx, - &agg.Aggregate, - "project", - false, - false, - false, - domain.PrivateLabelingSettingUnspecified, - ), - }, nil - }). - Filter(), - }, - want: Want{ - Commands: []eventstore.Command{ - project.NewApplicationAddedEvent( - ctx, - &agg.Aggregate, - "appID", - "name", - ), - project.NewAPIConfigAddedEvent(ctx, &agg.Aggregate, - "appID", - "clientID", - nil, - domain.APIAuthMethodTypeBasic, - ), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, - AddAPIApp(*tt.args.a, - tt.args.appID, - tt.args.name, - tt.args.clientID, - nil, - domain.APIAuthMethodTypeBasic, - ), tt.args.filter, tt.want) - }) - } -} - -func TestExistsApp(t *testing.T) { - type args struct { - filter preparation.FilterToQueryReducer - appID string - projectID string - resourceOwner string - } - tests := []struct { - name string - args args - wantExists bool - wantErr bool - }{ - { - name: "no events", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{}, nil - }, - appID: "appID", - projectID: "projectID", - resourceOwner: "ro", - }, - wantExists: false, - wantErr: false, - }, - { - name: "app added", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - project.NewApplicationAddedEvent( - context.Background(), - &project.NewAggregate("id", "ro").Aggregate, - "appID", - "name", - ), - }, nil - }, - appID: "appID", - projectID: "projectID", - resourceOwner: "ro", - }, - wantExists: true, - wantErr: false, - }, - { - name: "app removed", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - project.NewApplicationAddedEvent( - context.Background(), - &project.NewAggregate("id", "ro").Aggregate, - "appID", - "name", - ), - project.NewApplicationRemovedEvent( - context.Background(), - &project.NewAggregate("id", "ro").Aggregate, - "appID", - "name", - ), - }, nil - }, - appID: "appID", - projectID: "projectID", - resourceOwner: "ro", - }, - wantExists: false, - wantErr: false, - }, - { - name: "error durring filter", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return nil, errors.ThrowInternal(nil, "PROJE-Op26p", "Errors.Internal") - }, - appID: "appID", - projectID: "projectID", - resourceOwner: "ro", - }, - wantExists: false, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotExists, err := ExistsApp(context.Background(), tt.args.filter, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) - if (err != nil) != tt.wantErr { - t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotExists != tt.wantExists { - t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists) - } - }) - } -} diff --git a/internal/command/v2/project_test.go b/internal/command/v2/project_test.go deleted file mode 100644 index 8e5fc81a8b..0000000000 --- a/internal/command/v2/project_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package command - -import ( - "context" - "testing" - - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/domain" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/project" -) - -func TestAddProject(t *testing.T) { - type args struct { - a *project.Aggregate - name string - owner string - privateLabelingSetting domain.PrivateLabelingSetting - } - - ctx := context.Background() - agg := project.NewAggregate("test", "test") - - tests := []struct { - name string - args args - want Want - }{ - { - name: "invalid name", - args: args{ - a: agg, - name: "", - owner: "owner", - privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-C01yo", "Errors.Invalid.Argument"), - }, - }, - { - name: "invalid private labeling setting", - args: args{ - a: agg, - name: "name", - owner: "owner", - privateLabelingSetting: -1, - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-AO52V", "Errors.Invalid.Argument"), - }, - }, - { - name: "invalid owner", - args: args{ - a: agg, - name: "name", - owner: "", - privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, - }, - want: Want{ - ValidationErr: errors.ThrowPreconditionFailed(nil, "PROJE-hzxwo", "Errors.Invalid.Argument"), - }, - }, - { - name: "correct", - args: args{ - a: agg, - name: "ZITADEL", - owner: "CAOS AG", - privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, - }, - want: Want{ - Commands: []eventstore.Command{ - project.NewProjectAddedEvent(ctx, &agg.Aggregate, - "ZITADEL", - false, - false, - false, - domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, - ), - project.NewProjectMemberAddedEvent(ctx, &agg.Aggregate, - "CAOS AG", - domain.RoleProjectOwner), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, AddProject(tt.args.a, tt.args.name, tt.args.owner, false, false, false, tt.args.privateLabelingSetting), nil, tt.want) - }) - } -} - -func TestExistsProject(t *testing.T) { - type args struct { - filter preparation.FilterToQueryReducer - id string - resourceOwner string - } - tests := []struct { - name string - args args - wantExists bool - wantErr bool - }{ - { - name: "no events", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{}, nil - }, - id: "id", - resourceOwner: "ro", - }, - wantExists: false, - wantErr: false, - }, - { - name: "project added", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - project.NewProjectAddedEvent( - context.Background(), - &project.NewAggregate("id", "ro").Aggregate, - "name", - false, - false, - false, - domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, - ), - }, nil - }, - id: "id", - resourceOwner: "ro", - }, - wantExists: true, - wantErr: false, - }, - { - name: "project removed", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - project.NewProjectAddedEvent( - context.Background(), - &project.NewAggregate("id", "ro").Aggregate, - "name", - false, - false, - false, - domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, - ), - project.NewProjectRemovedEvent( - context.Background(), - &project.NewAggregate("id", "ro").Aggregate, - "name", - ), - }, nil - }, - id: "id", - resourceOwner: "ro", - }, - wantExists: false, - wantErr: false, - }, - { - name: "error durring filter", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return nil, errors.ThrowInternal(nil, "PROJE-Op26p", "Errors.Internal") - }, - id: "id", - resourceOwner: "ro", - }, - wantExists: false, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotExists, err := ExistsProject(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner) - if (err != nil) != tt.wantErr { - t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotExists != tt.wantExists { - t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists) - } - }) - } -} diff --git a/internal/command/v2/user.go b/internal/command/v2/user.go deleted file mode 100644 index a24cca54fb..0000000000 --- a/internal/command/v2/user.go +++ /dev/null @@ -1,40 +0,0 @@ -package command - -import ( - "context" - - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/user" -) - -func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string) (exists bool, err error) { - events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - ResourceOwner(resourceOwner). - OrderAsc(). - AddQuery(). - AggregateTypes(user.AggregateType). - AggregateIDs(id). - EventTypes( - user.HumanRegisteredType, - user.UserV1RegisteredType, - user.HumanAddedType, - user.UserV1AddedType, - user.MachineAddedEventType, - user.UserRemovedType, - ).Builder()) - if err != nil { - return false, err - } - - for _, event := range events { - switch event.(type) { - case *user.HumanRegisteredEvent, *user.HumanAddedEvent, *user.MachineAddedEvent: - exists = true - case *user.UserRemovedEvent: - exists = false - } - } - - return exists, nil -} diff --git a/internal/command/v2/user_human.go b/internal/command/v2/user_human.go deleted file mode 100644 index 1907647ddc..0000000000 --- a/internal/command/v2/user_human.go +++ /dev/null @@ -1,97 +0,0 @@ -package command - -import ( - "context" - "strings" - - "golang.org/x/text/language" - - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/crypto" - "github.com/caos/zitadel/internal/domain" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/user" -) - -type AddHuman struct { - // Username is required - Username string - // FirstName is required - FirstName string - // LastName is required - LastName string - // NickName is required - NickName string - // DisplayName is required - DisplayName string - // Email is required - Email string - // PreferredLang is required - PreferredLang language.Tag - // Gender is required - Gender domain.Gender - //TODO: can it also be verified? - Phone string - //Password is optional - Password string - //PasswordChangeRequired is used if the `Password`-field is set - PasswordChangeRequired bool -} - -func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm) preparation.Validation { - return func() (preparation.CreateCommands, error) { - if !domain.EmailRegex.MatchString(human.Email) { - return nil, errors.ThrowInvalidArgument(nil, "USER-Ec7dM", "Errors.Invalid.Argument") - } - if human.FirstName = strings.TrimSpace(human.FirstName); human.FirstName == "" { - return nil, errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.Invalid.Argument") - } - if human.LastName = strings.TrimSpace(human.LastName); human.LastName == "" { - return nil, errors.ThrowInvalidArgument(nil, "USER-DiAq8", "Errors.Invalid.Argument") - } - human.Phone = strings.TrimSpace(human.Phone) - - return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - domainPolicy, err := domainPolicyWriteModel(ctx, filter) - if err != nil { - return nil, err - } - - cmd := user.NewHumanAddedEvent( - ctx, - &a.Aggregate, - human.Username, - human.FirstName, - human.LastName, - human.NickName, - human.DisplayName, - human.PreferredLang, - human.Gender, - human.Email, //TODO: pass if verified - domainPolicy.UserLoginMustBeDomain, - ) - if human.Phone != "" { - cmd.AddPhoneData(human.Phone) //TODO: pass if verified - } - if human.Password != "" { - passwordComplexity, err := passwordComplexityPolicyWriteModel(ctx, filter) - if err != nil { - return nil, err - } - - if err = passwordComplexity.Validate(human.Password); err != nil { - return nil, err - } - - secret, err := crypto.Hash([]byte(human.Password), passwordAlg) - if err != nil { - return nil, err - } - cmd.AddPasswordData(secret, human.PasswordChangeRequired) - } - - return []eventstore.Command{cmd}, nil - }, nil - } -} diff --git a/internal/command/v2/user_human_test.go b/internal/command/v2/user_human_test.go deleted file mode 100644 index ee82f3eae9..0000000000 --- a/internal/command/v2/user_human_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package command - -import ( - "context" - "testing" - - "github.com/golang/mock/gomock" - "golang.org/x/text/language" - - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/crypto" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/org" - "github.com/caos/zitadel/internal/repository/user" -) - -func TestAddHumanCommand(t *testing.T) { - type args struct { - a *user.Aggregate - human *AddHuman - passwordAlg crypto.HashAlgorithm - filter preparation.FilterToQueryReducer - } - agg := user.NewAggregate("id", "ro") - tests := []struct { - name string - args args - want Want - }{ - { - name: "invalid email", - args: args{ - a: agg, - human: &AddHuman{ - Email: "invalid", - }, - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "USER-Ec7dM", "Errors.Invalid.Argument"), - }, - }, - { - name: "invalid first name", - args: args{ - a: agg, - human: &AddHuman{ - Email: "support@zitadel.ch", - }, - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.Invalid.Argument"), - }, - }, - { - name: "invalid last name", - args: args{ - a: agg, - human: &AddHuman{ - Email: "support@zitadel.ch", - FirstName: "hurst", - }, - }, - want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "USER-DiAq8", "Errors.Invalid.Argument"), - }, - }, - { - name: "invalid password", - args: args{ - a: agg, - human: &AddHuman{ - Email: "support@zitadel.ch", - FirstName: "gigi", - LastName: "giraffe", - Password: "short", - }, - filter: NewMultiFilter().Append( - func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - org.NewDomainPolicyAddedEvent( - context.Background(), - &org.NewAggregate("id", "ro").Aggregate, - true, - ), - }, nil - }). - Append( - func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - org.NewPasswordComplexityPolicyAddedEvent( - context.Background(), - &org.NewAggregate("id", "ro").Aggregate, - 8, - true, - true, - true, - true, - ), - }, nil - }). - Filter(), - }, - want: Want{ - CreateErr: errors.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"), - }, - }, - { - name: "correct", - args: args{ - a: agg, - human: &AddHuman{ - Email: "support@zitadel.ch", - FirstName: "gigi", - LastName: "giraffe", - Password: "", - }, - passwordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), - filter: NewMultiFilter().Append( - func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - org.NewDomainPolicyAddedEvent( - context.Background(), - &org.NewAggregate("id", "ro").Aggregate, - true, - ), - }, nil - }). - Append( - func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - org.NewPasswordComplexityPolicyAddedEvent( - context.Background(), - &org.NewAggregate("id", "ro").Aggregate, - 2, - false, - false, - false, - false, - ), - }, nil - }). - Filter(), - }, - want: Want{ - Commands: []eventstore.Command{ - user.NewHumanAddedEvent( - context.Background(), - &agg.Aggregate, - "", - "gigi", - "giraffe", - "", - "", - language.Und, - 0, - "support@zitadel.ch", - true, - ), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, AddHumanCommand(tt.args.a, tt.args.human, tt.args.passwordAlg), tt.args.filter, tt.want) - }) - } -} diff --git a/internal/command/v2/user_test.go b/internal/command/v2/user_test.go deleted file mode 100644 index d2f20dcb12..0000000000 --- a/internal/command/v2/user_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package command - -import ( - "context" - "testing" - - "golang.org/x/text/language" - - "github.com/caos/zitadel/internal/command/v2/preparation" - "github.com/caos/zitadel/internal/domain" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/user" -) - -func TestExistsUser(t *testing.T) { - type args struct { - filter preparation.FilterToQueryReducer - id string - resourceOwner string - } - tests := []struct { - name string - args args - wantExists bool - wantErr bool - }{ - { - name: "no events", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{}, nil - }, - id: "id", - resourceOwner: "ro", - }, - wantExists: false, - wantErr: false, - }, - { - name: "human registered", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - user.NewHumanRegisteredEvent( - context.Background(), - &user.NewAggregate("id", "ro").Aggregate, - "userName", - "firstName", - "lastName", - "nickName", - "displayName", - language.German, - domain.GenderFemale, - "support@zitadel.ch", - true, - ), - }, nil - }, - id: "id", - resourceOwner: "ro", - }, - wantExists: true, - wantErr: false, - }, - { - name: "human added", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - user.NewHumanAddedEvent( - context.Background(), - &user.NewAggregate("id", "ro").Aggregate, - "userName", - "firstName", - "lastName", - "nickName", - "displayName", - language.German, - domain.GenderFemale, - "support@zitadel.ch", - true, - ), - }, nil - }, - id: "id", - resourceOwner: "ro", - }, - wantExists: true, - wantErr: false, - }, - { - name: "machine added", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - user.NewMachineAddedEvent( - context.Background(), - &user.NewAggregate("id", "ro").Aggregate, - "userName", - "name", - "description", - true, - ), - }, nil - }, - id: "id", - resourceOwner: "ro", - }, - wantExists: true, - wantErr: false, - }, - { - name: "user removed", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - user.NewMachineAddedEvent( - context.Background(), - &user.NewAggregate("removed", "ro").Aggregate, - "userName", - "name", - "description", - true, - ), - user.NewUserRemovedEvent( - context.Background(), - &user.NewAggregate("removed", "ro").Aggregate, - "userName", - nil, - true, - ), - }, nil - }, - id: "id", - resourceOwner: "ro", - }, - wantExists: false, - wantErr: false, - }, - { - name: "error durring filter", - args: args{ - filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return nil, errors.ThrowInternal(nil, "USER-Drebn", "Errors.Internal") - }, - id: "id", - resourceOwner: "ro", - }, - wantExists: false, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner) - if (err != nil) != tt.wantErr { - t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotExists != tt.wantExists { - t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists) - } - }) - } -} diff --git a/internal/crypto/key.go b/internal/crypto/key.go index b19dc7fb98..61058a4e06 100644 --- a/internal/crypto/key.go +++ b/internal/crypto/key.go @@ -39,7 +39,7 @@ func LoadKey(id string, keyStorage KeyStorage) (string, error) { return key.Value, nil } -func LoadKeys(config *KeyConfig, keyStorage KeyStorage) (map[string]string, []string, error) { +func LoadKeys(config *KeyConfig, keyStorage KeyStorage) (Keys, []string, error) { if config == nil { return nil, nil, errors.ThrowInvalidArgument(nil, "CRYPT-dJK8s", "config must not be nil") } @@ -47,7 +47,7 @@ func LoadKeys(config *KeyConfig, keyStorage KeyStorage) (map[string]string, []st if err != nil { return nil, nil, err } - keys := make(map[string]string) + keys := make(Keys) ids := make([]string, 0, len(config.DecryptionKeyIDs)+1) if config.EncryptionKeyID != "" { key, ok := readKeys[config.EncryptionKeyID] diff --git a/internal/domain/application_oidc.go b/internal/domain/application_oidc.go index 41cee23927..b617d8cd28 100644 --- a/internal/domain/application_oidc.go +++ b/internal/domain/application_oidc.go @@ -146,10 +146,15 @@ func (a *OIDCApp) OriginsValid() bool { return true } -func (a *OIDCApp) getRequiredGrantTypes() []OIDCGrantType { - grantTypes := make([]OIDCGrantType, 0) - implicit := false - for _, r := range a.ResponseTypes { +func ContainsRequiredGrantTypes(responseTypes []OIDCResponseType, grantTypes []OIDCGrantType) bool { + required := RequiredOIDCGrantTypes(responseTypes) + return ContainsOIDCGrantTypes(required, grantTypes) +} + +func RequiredOIDCGrantTypes(responseTypes []OIDCResponseType) (grantTypes []OIDCGrantType) { + var implicit bool + + for _, r := range responseTypes { switch r { case OIDCResponseTypeCode: grantTypes = append(grantTypes, OIDCGrantTypeAuthorizationCode) @@ -160,9 +165,23 @@ func (a *OIDCApp) getRequiredGrantTypes() []OIDCGrantType { } } } + return grantTypes } +func (a *OIDCApp) getRequiredGrantTypes() []OIDCGrantType { + return RequiredOIDCGrantTypes(a.ResponseTypes) +} + +func ContainsOIDCGrantTypes(shouldContain, list []OIDCGrantType) bool { + for _, should := range shouldContain { + if !containsOIDCGrantType(list, should) { + return false + } + } + return true +} + func containsOIDCGrantType(grantTypes []OIDCGrantType, grantType OIDCGrantType) bool { for _, gt := range grantTypes { if gt == grantType { diff --git a/internal/domain/human.go b/internal/domain/human.go index 51b1b3c15a..2b15f70cbc 100644 --- a/internal/domain/human.go +++ b/internal/domain/human.go @@ -8,6 +8,11 @@ import ( es_models "github.com/caos/zitadel/internal/eventstore/v1/models" ) +type HumanDetails struct { + ID string + ObjectDetails +} + type Human struct { es_models.ObjectRoot diff --git a/internal/domain/human_email.go b/internal/domain/human_email.go index 5947a27600..718aee00f0 100644 --- a/internal/domain/human_email.go +++ b/internal/domain/human_email.go @@ -8,7 +8,9 @@ import ( es_models "github.com/caos/zitadel/internal/eventstore/v1/models" ) -var EmailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") +var ( + EmailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") +) type Email struct { es_models.ObjectRoot diff --git a/internal/domain/human_phone.go b/internal/domain/human_phone.go index b349322096..d743e4ecbd 100644 --- a/internal/domain/human_phone.go +++ b/internal/domain/human_phone.go @@ -1,11 +1,12 @@ package domain import ( + "time" + "github.com/caos/zitadel/internal/crypto" caos_errs "github.com/caos/zitadel/internal/errors" es_models "github.com/caos/zitadel/internal/eventstore/v1/models" "github.com/ttacon/libphonenumber" - "time" ) const ( diff --git a/internal/domain/project.go b/internal/domain/project.go index 8952a39c48..dbb43e6039 100644 --- a/internal/domain/project.go +++ b/internal/domain/project.go @@ -22,8 +22,14 @@ const ( ProjectStateActive ProjectStateInactive ProjectStateRemoved + + projectStateMax ) +func (s ProjectState) Valid() bool { + return s > ProjectStateUnspecified && s < projectStateMax +} + type PrivateLabelingSetting int32 const ( diff --git a/internal/domain/secret_generator.go b/internal/domain/secret_generator.go index 83f3ffe1c5..892fd463b8 100644 --- a/internal/domain/secret_generator.go +++ b/internal/domain/secret_generator.go @@ -7,6 +7,7 @@ const ( SecretGeneratorTypeInitCode SecretGeneratorTypeVerifyEmailCode SecretGeneratorTypeVerifyPhoneCode + SecretGeneratorTypeVerifyDomain SecretGeneratorTypePasswordResetCode SecretGeneratorTypePasswordlessInitCode SecretGeneratorTypeAppSecret @@ -14,6 +15,10 @@ const ( secretGeneratorTypeCount ) +func (t SecretGeneratorType) Valid() bool { + return t > SecretGeneratorTypeUnspecified && t < secretGeneratorTypeCount +} + type SecretGeneratorState int32 const ( diff --git a/internal/eventstore/event_base.go b/internal/eventstore/event_base.go index fb0bf0750a..d6feb18fc0 100644 --- a/internal/eventstore/event_base.go +++ b/internal/eventstore/event_base.go @@ -79,7 +79,7 @@ func BaseEventFromRepo(event *repository.Event) *BaseEvent { ID: event.AggregateID, Type: AggregateType(event.AggregateType), ResourceOwner: event.ResourceOwner.String, - InstanceID: event.InstanceID.String, + InstanceID: event.InstanceID, Version: Version(event.Version), }, EventType: EventType(event.Type), diff --git a/internal/eventstore/eventstore.go b/internal/eventstore/eventstore.go index 9f367f9674..47b4778ea6 100644 --- a/internal/eventstore/eventstore.go +++ b/internal/eventstore/eventstore.go @@ -82,7 +82,7 @@ func commandsToRepository(instanceID string, cmds []Command) (events []*reposito AggregateID: cmd.Aggregate().ID, AggregateType: repository.AggregateType(cmd.Aggregate().Type), ResourceOwner: sql.NullString{String: cmd.Aggregate().ResourceOwner, Valid: cmd.Aggregate().ResourceOwner != ""}, - InstanceID: sql.NullString{String: instanceID, Valid: instanceID != ""}, + InstanceID: instanceID, EditorService: cmd.EditorService(), EditorUser: cmd.EditorUser(), Type: repository.EventType(cmd.Type()), @@ -178,7 +178,7 @@ func (es *Eventstore) LatestSequence(ctx context.Context, queryFactory *SearchQu return es.repo.LatestSequence(ctx, query) } -type queryReducer interface { +type QueryReducer interface { reducer //Query returns the SearchQueryFactory for the events needed in reducer Query() *SearchQueryBuilder @@ -186,7 +186,7 @@ type queryReducer interface { //FilterToQueryReducer filters the events based on the search query of the query function, // appends all events to the reducer and calls it's reduce function -func (es *Eventstore) FilterToQueryReducer(ctx context.Context, r queryReducer) error { +func (es *Eventstore) FilterToQueryReducer(ctx context.Context, r QueryReducer) error { events, err := es.Filter(ctx, r.Query()) if err != nil { return err diff --git a/internal/eventstore/eventstore_test.go b/internal/eventstore/eventstore_test.go index f7a252c12d..0e2738955a 100644 --- a/internal/eventstore/eventstore_test.go +++ b/internal/eventstore/eventstore_test.go @@ -380,7 +380,7 @@ func TestEventstore_aggregatesToEvents(t *testing.T) { EditorService: "editorService", EditorUser: "editorUser", ResourceOwner: sql.NullString{String: "caos", Valid: true}, - InstanceID: sql.NullString{String: "instanceID", Valid: true}, + InstanceID: "instanceID", Type: "test.event", Version: "v1", }, @@ -418,7 +418,7 @@ func TestEventstore_aggregatesToEvents(t *testing.T) { EditorService: "editorService", EditorUser: "editorUser", ResourceOwner: sql.NullString{String: "caos", Valid: true}, - InstanceID: sql.NullString{String: "instanceID", Valid: true}, + InstanceID: "instanceID", Type: "test.event", Version: "v1", }, @@ -429,7 +429,7 @@ func TestEventstore_aggregatesToEvents(t *testing.T) { EditorService: "editorService", EditorUser: "editorUser", ResourceOwner: sql.NullString{String: "caos", Valid: true}, - InstanceID: sql.NullString{String: "instanceID", Valid: true}, + InstanceID: "instanceID", Type: "test.event", Version: "v1", }, @@ -585,7 +585,7 @@ func TestEventstore_aggregatesToEvents(t *testing.T) { EditorService: "editorService", EditorUser: "editorUser", ResourceOwner: sql.NullString{String: "", Valid: false}, - InstanceID: sql.NullString{String: "zitadel"}, + InstanceID: "zitadel", Type: "test.event", Version: "v1", }, @@ -630,7 +630,7 @@ func TestEventstore_aggregatesToEvents(t *testing.T) { EditorService: "editorService", EditorUser: "editorUser", ResourceOwner: sql.NullString{String: "caos", Valid: true}, - InstanceID: sql.NullString{String: "zitadel"}, + InstanceID: "zitadel", Type: "test.event", Version: "v1", }, @@ -641,7 +641,7 @@ func TestEventstore_aggregatesToEvents(t *testing.T) { EditorService: "editorService", EditorUser: "editorUser", ResourceOwner: sql.NullString{String: "caos", Valid: true}, - InstanceID: sql.NullString{String: "zitadel"}, + InstanceID: "zitadel", Type: "test.event", Version: "v1", }, @@ -654,7 +654,7 @@ func TestEventstore_aggregatesToEvents(t *testing.T) { EditorService: "editorService", EditorUser: "editorUser", ResourceOwner: sql.NullString{String: "caos", Valid: true}, - InstanceID: sql.NullString{String: "zitadel"}, + InstanceID: "zitadel", Type: "test.event", Version: "v1", }, @@ -772,7 +772,7 @@ func TestEventstore_Push(t *testing.T) { EditorService: "editorService", EditorUser: "editorUser", ResourceOwner: sql.NullString{String: "caos", Valid: true}, - InstanceID: sql.NullString{String: "zitadel"}, + InstanceID: "zitadel", Type: "test.event", Version: "v1", }, @@ -816,7 +816,7 @@ func TestEventstore_Push(t *testing.T) { EditorService: "editorService", EditorUser: "editorUser", ResourceOwner: sql.NullString{String: "caos", Valid: true}, - InstanceID: sql.NullString{String: "zitadel"}, + InstanceID: "zitadel", Type: "test.event", Version: "v1", }, @@ -827,7 +827,7 @@ func TestEventstore_Push(t *testing.T) { EditorService: "editorService", EditorUser: "editorUser", ResourceOwner: sql.NullString{String: "caos", Valid: true}, - InstanceID: sql.NullString{String: "zitadel"}, + InstanceID: "zitadel", Type: "test.event", Version: "v1", }, @@ -882,7 +882,7 @@ func TestEventstore_Push(t *testing.T) { EditorService: "editorService", EditorUser: "editorUser", ResourceOwner: sql.NullString{String: "caos", Valid: true}, - InstanceID: sql.NullString{String: "zitadel"}, + InstanceID: "zitadel", Type: "test.event", Version: "v1", }, @@ -893,7 +893,7 @@ func TestEventstore_Push(t *testing.T) { EditorService: "editorService", EditorUser: "editorUser", ResourceOwner: sql.NullString{String: "caos", Valid: true}, - InstanceID: sql.NullString{String: "zitadel"}, + InstanceID: "zitadel", Type: "test.event", Version: "v1", }, @@ -906,7 +906,7 @@ func TestEventstore_Push(t *testing.T) { EditorService: "editorService", EditorUser: "editorUser", ResourceOwner: sql.NullString{String: "caos", Valid: true}, - InstanceID: sql.NullString{String: "zitadel"}, + InstanceID: "zitadel", Type: "test.event", Version: "v1", }, diff --git a/internal/eventstore/repository/event.go b/internal/eventstore/repository/event.go index 7a6132f0bc..8583c6d3fc 100644 --- a/internal/eventstore/repository/event.go +++ b/internal/eventstore/repository/event.go @@ -58,7 +58,7 @@ type Event struct { ResourceOwner sql.NullString //InstanceID is the instance where this event belongs to // use the ID of the instance - InstanceID sql.NullString + InstanceID string } //EventType is the description of the change diff --git a/internal/eventstore/repository/sql/crdb.go b/internal/eventstore/repository/sql/crdb.go index 21db073ffd..cf5d53443e 100644 --- a/internal/eventstore/repository/sql/crdb.go +++ b/internal/eventstore/repository/sql/crdb.go @@ -71,7 +71,7 @@ const ( " $7::VARCHAR AS editor_service," + " IFNULL((resource_owner), $8::VARCHAR) AS resource_owner," + " $9::VARCHAR AS instance_id," + - " NEXTVAL(CONCAT('eventstore.', IFNULL($9, 'system'), '_seq'))," + + " NEXTVAL(CONCAT('eventstore.', IF($9 <> '', CONCAT('i_', $9), 'system'), '_seq'))," + " aggregate_sequence AS previous_aggregate_sequence," + " aggregate_type_sequence AS previous_aggregate_type_sequence " + "FROM previous_data " + diff --git a/internal/eventstore/repository/sql/query_test.go b/internal/eventstore/repository/sql/query_test.go index 4e26eda709..83a026c889 100644 --- a/internal/eventstore/repository/sql/query_test.go +++ b/internal/eventstore/repository/sql/query_test.go @@ -136,7 +136,7 @@ func Test_prepareColumns(t *testing.T) { }, }, fields: fields{ - dbRow: []interface{}{time.Time{}, repository.EventType(""), uint64(5), Sequence(0), Sequence(0), Data(nil), "", "", sql.NullString{String: ""}, sql.NullString{String: ""}, repository.AggregateType("user"), "hodor", repository.Version("")}, + dbRow: []interface{}{time.Time{}, repository.EventType(""), uint64(5), Sequence(0), Sequence(0), Data(nil), "", "", sql.NullString{String: ""}, "", repository.AggregateType("user"), "hodor", repository.Version("")}, }, }, { diff --git a/internal/query/instance.go b/internal/query/instance.go index cdddc0dadd..7e911da8e6 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -68,6 +68,7 @@ type Instance struct { DefaultLanguage language.Tag SetupStarted domain.Step SetupDone domain.Step + Host string } func (i *Instance) InstanceID() string { @@ -82,6 +83,10 @@ func (i *Instance) ConsoleClientID() string { return i.ConsoleID } +func (i *Instance) RequestedDomain() string { + return i.Host +} + type InstanceSearchQueries struct { SearchRequest Queries []SearchQuery @@ -96,7 +101,7 @@ func (q *InstanceSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder } func (q *Queries) Instance(ctx context.Context) (*Instance, error) { - stmt, scan := prepareIAMQuery() + stmt, scan := prepareInstanceQuery(authz.GetInstance(ctx).RequestedDomain()) query, args, err := stmt.Where(sq.Eq{ InstanceColumnID.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() @@ -109,7 +114,7 @@ func (q *Queries) Instance(ctx context.Context) (*Instance, error) { } func (q *Queries) InstanceByHost(ctx context.Context, host string) (authz.Instance, error) { - stmt, scan := prepareIAMQuery() + stmt, scan := prepareInstanceQuery(host) query, args, err := stmt.Where(sq.Eq{ InstanceColumnID.identifier(): "system", //TODO: change column to domain when available }).ToSql() @@ -129,7 +134,7 @@ func (q *Queries) GetDefaultLanguage(ctx context.Context) language.Tag { return iam.DefaultLanguage } -func prepareIAMQuery() (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) { +func prepareInstanceQuery(host string) (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) { return sq.Select( InstanceColumnID.identifier(), InstanceColumnChangeDate.identifier(), @@ -143,17 +148,17 @@ func prepareIAMQuery() (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) { ). From(instanceTable.identifier()).PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Instance, error) { - iam := new(Instance) + instance := &Instance{Host: host} lang := "" err := row.Scan( - &iam.ID, - &iam.ChangeDate, - &iam.Sequence, - &iam.GlobalOrgID, - &iam.IAMProjectID, - &iam.ConsoleID, - &iam.SetupStarted, - &iam.SetupDone, + &instance.ID, + &instance.ChangeDate, + &instance.Sequence, + &instance.GlobalOrgID, + &instance.IAMProjectID, + &instance.ConsoleID, + &instance.SetupStarted, + &instance.SetupDone, &lang, ) if err != nil { @@ -162,7 +167,7 @@ func prepareIAMQuery() (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) { } return nil, errors.ThrowInternal(err, "QUERY-d9nw", "Errors.Internal") } - iam.DefaultLanguage = language.Make(lang) - return iam, nil + instance.DefaultLanguage = language.Make(lang) + return instance, nil } } diff --git a/internal/query/instance_test.go b/internal/query/instance_test.go index 3757cc7179..b7e653a74c 100644 --- a/internal/query/instance_test.go +++ b/internal/query/instance_test.go @@ -10,6 +10,7 @@ import ( "golang.org/x/text/language" + sq "github.com/Masterminds/squirrel" "github.com/caos/zitadel/internal/domain" errs "github.com/caos/zitadel/internal/errors" ) @@ -26,8 +27,10 @@ func Test_InstancePrepares(t *testing.T) { object interface{} }{ { - name: "prepareInstanceQuery no result", - prepare: prepareIAMQuery, + name: "prepareInstanceQuery no result", + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) { + return prepareInstanceQuery("") + }, want: want{ sqlExpectations: mockQueries( regexp.QuoteMeta(`SELECT projections.instances.id,`+ @@ -53,8 +56,10 @@ func Test_InstancePrepares(t *testing.T) { object: (*Instance)(nil), }, { - name: "prepareInstanceQuery found", - prepare: prepareIAMQuery, + name: "prepareInstanceQuery found", + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) { + return prepareInstanceQuery("") + }, want: want{ sqlExpectations: mockQuery( regexp.QuoteMeta(`SELECT projections.instances.id,`+ @@ -104,8 +109,10 @@ func Test_InstancePrepares(t *testing.T) { }, }, { - name: "prepareInstanceQuery sql err", - prepare: prepareIAMQuery, + name: "prepareInstanceQuery sql err", + prepare: func() (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) { + return prepareInstanceQuery("") + }, want: want{ sqlExpectations: mockQueryErr( regexp.QuoteMeta(`SELECT projections.instances.id,`+ diff --git a/internal/query/projection/event_test.go b/internal/query/projection/event_test.go index 09f54931dd..83b4db6a3c 100644 --- a/internal/query/projection/event_test.go +++ b/internal/query/projection/event_test.go @@ -26,7 +26,7 @@ func testEvent( Version: "v1", AggregateID: "agg-id", ResourceOwner: sql.NullString{String: "ro-id", Valid: true}, - InstanceID: sql.NullString{String: "instance-id", Valid: true}, + InstanceID: "instance-id", ID: "event-id", EditorService: "editor-svc", EditorUser: "editor-user", diff --git a/internal/query/projection/secret_generator.go b/internal/query/projection/secret_generator.go index 6491074a4f..74833f9115 100644 --- a/internal/query/projection/secret_generator.go +++ b/internal/query/projection/secret_generator.go @@ -8,7 +8,6 @@ import ( "github.com/caos/zitadel/internal/eventstore/handler" "github.com/caos/zitadel/internal/eventstore/handler/crdb" "github.com/caos/zitadel/internal/repository/instance" - "github.com/caos/zitadel/internal/repository/project" ) const ( @@ -63,7 +62,7 @@ func NewSecretGeneratorProjection(ctx context.Context, config crdb.StatementHand func (p *SecretGeneratorProjection) reducers() []handler.AggregateReducer { return []handler.AggregateReducer{ { - Aggregate: project.AggregateType, + Aggregate: instance.AggregateType, EventRedusers: []handler.EventReducer{ { Event: instance.SecretGeneratorAddedEventType, diff --git a/internal/repository/instance/event_iam_project_set.go b/internal/repository/instance/event_iam_project_set.go index 47d79ad674..1322ce6cf5 100644 --- a/internal/repository/instance/event_iam_project_set.go +++ b/internal/repository/instance/event_iam_project_set.go @@ -73,7 +73,7 @@ func (e *ConsoleSetEvent) UniqueConstraints() []*eventstore.EventUniqueConstrain func NewIAMConsoleSetEvent( ctx context.Context, aggregate *eventstore.Aggregate, - clientID string, + clientID *string, ) *ConsoleSetEvent { return &ConsoleSetEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -81,7 +81,7 @@ func NewIAMConsoleSetEvent( aggregate, ConsoleSetEventType, ), - ClientID: clientID, + ClientID: *clientID, } } diff --git a/internal/repository/project/api_config.go b/internal/repository/project/api_config.go index 9086edbfd3..6df8aafaf5 100644 --- a/internal/repository/project/api_config.go +++ b/internal/repository/project/api_config.go @@ -58,6 +58,22 @@ func NewAPIConfigAddedEvent( } } +func (e *APIConfigAddedEvent) Validate(cmd eventstore.Command) bool { + c, ok := cmd.(*APIConfigAddedEvent) + if !ok { + return false + } + + if e.AppID != c.AppID { + return false + } + if e.AuthMethodType != c.AuthMethodType { + return false + } + + return true +} + func APIConfigAddedEventMapper(event *repository.Event) (eventstore.Event, error) { e := &APIConfigAddedEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event), diff --git a/internal/repository/project/oidc_config.go b/internal/repository/project/oidc_config.go index 1599a4fc1a..b3e1c61e7f 100644 --- a/internal/repository/project/oidc_config.go +++ b/internal/repository/project/oidc_config.go @@ -97,6 +97,92 @@ func NewOIDCConfigAddedEvent( } } +func (e *OIDCConfigAddedEvent) Validate(cmd eventstore.Command) bool { + c, ok := cmd.(*OIDCConfigAddedEvent) + if !ok { + return false + } + + if e.Version != c.Version { + return false + } + if e.AppID != c.AppID { + return false + } + if e.ClientID != "" && e.ClientID != c.ClientID { + return false + } + if e.ClientSecret != c.ClientSecret { + return false + } + if len(e.RedirectUris) != len(c.RedirectUris) { + return false + } + for i, uri := range e.RedirectUris { + if uri != c.RedirectUris[i] { + return false + } + } + if len(e.ResponseTypes) != len(c.ResponseTypes) { + return false + } + for i, typ := range e.ResponseTypes { + if typ != c.ResponseTypes[i] { + return false + } + } + if len(e.GrantTypes) != len(c.GrantTypes) { + return false + } + for i, typ := range e.GrantTypes { + if typ != c.GrantTypes[i] { + return false + } + } + if e.ApplicationType != c.ApplicationType { + return false + } + if e.AuthMethodType != c.AuthMethodType { + return false + } + if len(e.PostLogoutRedirectUris) != len(c.PostLogoutRedirectUris) { + return false + } + for i, uri := range e.PostLogoutRedirectUris { + if uri != c.PostLogoutRedirectUris[i] { + return false + } + } + if e.DevMode != c.DevMode { + return false + } + if e.AccessTokenType != c.AccessTokenType { + return false + } + if e.AccessTokenRoleAssertion != c.AccessTokenRoleAssertion { + return false + } + if e.IDTokenRoleAssertion != c.IDTokenRoleAssertion { + return false + } + if e.IDTokenUserinfoAssertion != c.IDTokenUserinfoAssertion { + return false + } + if e.ClockSkew != c.ClockSkew { + return false + } + if len(e.AdditionalOrigins) != len(c.AdditionalOrigins) { + return false + } + for i, origin := range e.AdditionalOrigins { + if origin != c.AdditionalOrigins[i] { + return false + } + } + + return true +} + func OIDCConfigAddedEventMapper(event *repository.Event) (eventstore.Event, error) { e := &OIDCConfigAddedEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event),