diff --git a/cmd/admin/start/start.go b/cmd/admin/start/start.go index 7623ec6d4a..fb06b80234 100644 --- a/cmd/admin/start/start.go +++ b/cmd/admin/start/start.go @@ -142,6 +142,9 @@ func startZitadel(config *startConfig) error { if err != nil { return fmt.Errorf("cannot start eventstore for queries: %w", err) } + smtpPasswordCrypto, err := crypto.NewAESCrypto(config.SystemDefaults.SMTPPasswordVerificationKey) + logging.Log("MAIN-en9ew").OnError(err).Fatal("cannot create smtp crypto") + queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections.Config, config.SystemDefaults, config.Projections.KeyConfig, keyChan, config.InternalAuthZ.RolePermissionMappings) if err != nil { return fmt.Errorf("cannot start queries: %w", err) @@ -156,12 +159,12 @@ func startZitadel(config *startConfig) error { Origin: http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), DisplayName: "ZITADEL", } - commands, err := command.StartCommands(eventstoreClient, config.SystemDefaults, config.InternalAuthZ, storage, authZRepo, config.OIDC.KeyConfig, webAuthNConfig) + commands, err := command.StartCommands(eventstoreClient, config.SystemDefaults, config.InternalAuthZ, storage, authZRepo, config.OIDC.KeyConfig, smtpPasswordCrypto, webAuthNConfig) if err != nil { return fmt.Errorf("cannot start commands: %w", err) } - notification.Start(config.Notification, config.SystemDefaults, commands, queries, dbClient, assets.HandlerPrefix) + notification.Start(config.Notification, config.SystemDefaults, commands, queries, dbClient, assets.HandlerPrefix, smtpPasswordCrypto) router := mux.NewRouter() err = startAPIs(ctx, router, commands, queries, eventstoreClient, dbClient, keyChan, config, storage, authZRepo) @@ -181,9 +184,12 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman } verifier := internal_authz.Start(repo) - apis := api.New(config.Port, router, &repo, config.InternalAuthZ, config.SystemDefaults, config.ExternalSecure) - - authRepo, err := auth_es.Start(config.Auth, config.SystemDefaults, commands, queries, dbClient, config.OIDC.KeyConfig, assets.HandlerPrefix) + apis := api.New(config.Port, router, &repo, config.InternalAuthZ, config.ExternalSecure) + userEncryptionAlgorithm, err := crypto.NewAESCrypto(config.SystemDefaults.UserVerificationKey) + if err != nil { + return nil + } + authRepo, err := auth_es.Start(config.Auth, config.SystemDefaults, commands, queries, dbClient, config.OIDC.KeyConfig, assets.HandlerPrefix, userEncryptionAlgorithm) if err != nil { return fmt.Errorf("error starting auth repo: %w", err) } @@ -191,13 +197,13 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman if err != nil { return fmt.Errorf("error starting admin repo: %w", err) } - if err := apis.RegisterServer(ctx, admin.CreateServer(commands, queries, adminRepo, config.SystemDefaults.Domain, assets.HandlerPrefix)); err != nil { + if err := apis.RegisterServer(ctx, admin.CreateServer(commands, queries, adminRepo, config.SystemDefaults.Domain, assets.HandlerPrefix, userEncryptionAlgorithm)); err != nil { return err } - if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, assets.HandlerPrefix)); err != nil { + if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, assets.HandlerPrefix, userEncryptionAlgorithm)); err != nil { return err } - if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, assets.HandlerPrefix)); err != nil { + if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, assets.HandlerPrefix, userEncryptionAlgorithm)); err != nil { return err } @@ -231,7 +237,7 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman } apis.RegisterHandler(console.HandlerPrefix, c) - l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, config.SystemDefaults, console.HandlerPrefix, config.ExternalDomain, oidc.AuthCallback, config.ExternalSecure, userAgentInterceptor) + l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, config.SystemDefaults, console.HandlerPrefix, config.ExternalDomain, oidc.AuthCallback, config.ExternalSecure, userAgentInterceptor, userEncryptionAlgorithm) if err != nil { return fmt.Errorf("unable to start login: %w", err) } diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 322bf220b4..be85496a74 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -141,6 +141,8 @@ SystemDefaults: EncryptionKeyID: $ZITADEL_USER_VERIFICATION_KEY IDPConfigVerificationKey: EncryptionKeyID: $ZITADEL_IDP_CONFIG_VERIFICATION_KEY + SMTPPasswordVerificationKey: + EncryptionKeyID: $ZITADEL_SMTP_PASSWORD_VERIFICATION_KEY SecretGenerators: PasswordSaltCost: 14 ClientSecretGenerator: @@ -197,7 +199,6 @@ SystemDefaults: MFAInitSkip: 720h #30d SecondFactorCheck: 18h MultiFactorCheck: 12h - IamID: 'IAM' DomainVerification: VerificationKey: EncryptionKeyID: $ZITADEL_DOMAIN_VERIFICATION_KEY @@ -240,42 +241,6 @@ SystemDefaults: Url: $CHAT_URL # Compact: $CHAT_COMPACT SplitCount: 4000 - TemplateData: - InitCode: - Title: 'InitCode.Title' - PreHeader: 'InitCode.PreHeader' - Subject: 'InitCode.Subject' - Greeting: 'InitCode.Greeting' - Text: 'InitCode.Text' - ButtonText: 'InitCode.ButtonText' - PasswordReset: - Title: 'PasswordReset.Title' - PreHeader: 'PasswordReset.PreHeader' - Subject: 'PasswordReset.Subject' - Greeting: 'PasswordReset.Greeting' - Text: 'PasswordReset.Text' - ButtonText: 'PasswordReset.ButtonText' - VerifyEmail: - Title: 'VerifyEmail.Title' - PreHeader: 'VerifyEmail.PreHeader' - Subject: 'VerifyEmail.Subject' - Greeting: 'VerifyEmail.Greeting' - Text: 'VerifyEmail.Text' - ButtonText: 'VerifyEmail.ButtonText' - VerifyPhone: - Title: 'VerifyPhone.Title' - PreHeader: 'VerifyPhone.PreHeader' - Subject: 'VerifyPhone.Subject' - Greeting: 'VerifyPhone.Greeting' - Text: 'VerifyPhone.Text' - ButtonText: 'VerifyPhone.ButtonText' - DomainClaimed: - Title: 'DomainClaimed.Title' - PreHeader: 'DomainClaimed.PreHeader' - Subject: 'DomainClaimed.Subject' - Greeting: 'DomainClaimed.Greeting' - Text: 'DomainClaimed.Text' - ButtonText: 'DomainClaimed.ButtonText' KeyConfig: Size: 2048 PrivateKeyLifetime: 6h diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index 2c95815ed1..1b1c1e12cb 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -32,6 +32,102 @@ Returns the default languages GET: /languages +### SetDefaultLanguage + +> **rpc** SetDefaultLanguage([SetDefaultLanguageRequest](#setdefaultlanguagerequest)) +[SetDefaultLanguageResponse](#setdefaultlanguageresponse) + +Set the default language + + + + PUT: /languages/default/{language} + + +### GetDefaultLanguage + +> **rpc** GetDefaultLanguage([GetDefaultLanguageRequest](#getdefaultlanguagerequest)) +[GetDefaultLanguageResponse](#getdefaultlanguageresponse) + +Set the default language + + + + GET: /languages/default + + +### ListSecretGenerators + +> **rpc** ListSecretGenerators([ListSecretGeneratorsRequest](#listsecretgeneratorsrequest)) +[ListSecretGeneratorsResponse](#listsecretgeneratorsresponse) + +Set the default language + + + + POST: /secretgenerators/_search + + +### GetSecretGenerator + +> **rpc** GetSecretGenerator([GetSecretGeneratorRequest](#getsecretgeneratorrequest)) +[GetSecretGeneratorResponse](#getsecretgeneratorresponse) + +Get Secret Generator by type (e.g PasswordResetCode) + + + + GET: /secretgenerators/{generator_type} + + +### UpdateSecretGenerator + +> **rpc** UpdateSecretGenerator([UpdateSecretGeneratorRequest](#updatesecretgeneratorrequest)) +[UpdateSecretGeneratorResponse](#updatesecretgeneratorresponse) + +Update secret generator configuration + + + + PUT: /secretgenerators/{generator_type} + + +### GetSMTPConfig + +> **rpc** GetSMTPConfig([GetSMTPConfigRequest](#getsmtpconfigrequest)) +[GetSMTPConfigResponse](#getsmtpconfigresponse) + +Get system smtp configuration + + + + GET: /smtp + + +### UpdateSMTPConfig + +> **rpc** UpdateSMTPConfig([UpdateSMTPConfigRequest](#updatesmtpconfigrequest)) +[UpdateSMTPConfigResponse](#updatesmtpconfigresponse) + +Update system smtp configuration + + + + PUT: /smtp + + +### UpdateSMTPConfigPassword + +> **rpc** UpdateSMTPConfigPassword([UpdateSMTPConfigPasswordRequest](#updatesmtpconfigpasswordrequest)) +[UpdateSMTPConfigPasswordResponse](#updatesmtpconfigpasswordresponse) + +Update system smtp configuration password for host + + + + PUT: /smtp/password + + ### GetOrgByID > **rpc** GetOrgByID([GetOrgByIDRequest](#getorgbyidrequest)) @@ -1665,6 +1761,23 @@ This is an empty response +### GetDefaultLanguageRequest +This is an empty request + + + + +### GetDefaultLanguageResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | | + + + + ### GetDefaultLoginTextsRequest @@ -1977,6 +2090,45 @@ This is an empty request +### GetSMTPConfigRequest +This is an empty request + + + + +### GetSMTPConfigResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| smtp_config | zitadel.settings.v1.SMTPConfig | - | | + + + + +### GetSecretGeneratorRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| generator_type | zitadel.settings.v1.SecretGeneratorType | - | enum.defined_only: true
enum.not_in: [0]
| + + + + +### GetSecretGeneratorResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| secret_generator | zitadel.settings.v1.SecretGenerator | - | | + + + + ### GetSupportedLanguagesRequest This is an empty request @@ -2211,6 +2363,30 @@ This is an empty request +### ListSecretGeneratorsRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| query | zitadel.v1.ListQuery | list limitations and ordering | | +| queries | repeated zitadel.settings.v1.SecretGeneratorQuery | criterias the client is looking for | | + + + + +### ListSecretGeneratorsResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ListDetails | - | | +| result | repeated zitadel.settings.v1.SecretGenerator | - | | + + + + ### ListViewsRequest This is an empty request @@ -2820,6 +2996,28 @@ This is an empty request +### SetDefaultLanguageRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| language | string | - | string.min_len: 1
string.max_len: 10
| + + + + +### SetDefaultLanguageResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### SetDefaultPasswordResetMessageTextRequest @@ -3374,6 +3572,82 @@ This is an empty request +### UpdateSMTPConfigPasswordRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| password | string | - | | + + + + +### UpdateSMTPConfigPasswordResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + +### UpdateSMTPConfigRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| sender_address | string | - | string.min_len: 1
string.max_len: 200
| +| sender_name | string | - | string.min_len: 1
string.max_len: 200
| +| tls | bool | - | | +| host | string | - | string.min_len: 1
string.max_len: 500
| +| user | string | - | | + + + + +### UpdateSMTPConfigResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + +### UpdateSecretGeneratorRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| generator_type | zitadel.settings.v1.SecretGeneratorType | - | enum.defined_only: true
enum.not_in: [0]
| +| length | uint32 | - | | +| expiry | google.protobuf.Duration | - | | +| include_lower_letters | bool | - | | +| include_upper_letters | bool | - | | +| include_digits | bool | - | | +| include_symbols | bool | - | | + + + + +### UpdateSecretGeneratorResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### View diff --git a/go.mod b/go.mod index 9615d7a70f..40b4e7d838 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,6 @@ require ( github.com/pquerna/otp v1.3.0 github.com/rakyll/statik v0.1.7 github.com/rs/cors v1.8.0 - github.com/sirupsen/logrus v1.8.1 github.com/sony/sonyflake v1.0.0 github.com/spf13/cobra v1.3.0 github.com/spf13/viper v1.10.1 @@ -140,6 +139,7 @@ require ( github.com/prometheus/procfs v0.6.0 // indirect github.com/rs/xid v1.2.1 // indirect github.com/satori/go.uuid v1.2.0 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/afero v1.8.1 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect diff --git a/go.sum b/go.sum index a3b0b63b54..13c1281d47 100644 --- a/go.sum +++ b/go.sum @@ -124,8 +124,6 @@ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBW github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= -github.com/caos/logging v0.3.0 h1:PMKd47Aqr98FjAPcAE8v3SCe8eqTEclytruND/zc7QU= -github.com/caos/logging v0.3.0/go.mod h1:B8QNS0WDmR2Keac52Fw+XN4ZJkzLDGrcRIPB2Ux4uRo= github.com/caos/logging v0.3.1 h1:892AMeHs09D0e3ZcGB+QDRsZ5+2xtPAsAhOy8eKfztc= github.com/caos/logging v0.3.1/go.mod h1:B8QNS0WDmR2Keac52Fw+XN4ZJkzLDGrcRIPB2Ux4uRo= github.com/caos/oidc v1.0.1 h1:8UHAPynCObwaqortppDtJFktjqLDLYSLidkNy0Num4o= diff --git a/internal/api/api.go b/internal/api/api.go index 98e2e6c1a4..ed5978488f 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -15,7 +15,6 @@ import ( "github.com/caos/zitadel/internal/api/grpc/server" http_util "github.com/caos/zitadel/internal/api/http" "github.com/caos/zitadel/internal/authz/repository" - "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/query" @@ -44,7 +43,6 @@ func New( *query.Queries }, authZ internal_authz.Config, - sd systemdefaults.SystemDefaults, externalSecure bool, ) *API { verifier := internal_authz.Start(repo) @@ -55,7 +53,7 @@ func New( router: router, externalSecure: externalSecure, } - api.grpcServer = server.CreateServer(api.verifier, authZ, sd.DefaultLanguage) + api.grpcServer = server.CreateServer(api.verifier, authZ, repo.Queries) api.routeGRPC() api.RegisterHandler("/debug", api.healthHandler()) diff --git a/internal/api/grpc/admin/iam_settings.go b/internal/api/grpc/admin/iam_settings.go new file mode 100644 index 0000000000..c64d38839b --- /dev/null +++ b/internal/api/grpc/admin/iam_settings.go @@ -0,0 +1,82 @@ +package admin + +import ( + "context" + + "github.com/caos/zitadel/internal/api/grpc/object" + "github.com/caos/zitadel/internal/domain" + admin_pb "github.com/caos/zitadel/pkg/grpc/admin" +) + +func (s *Server) ListSecretGenerators(ctx context.Context, req *admin_pb.ListSecretGeneratorsRequest) (*admin_pb.ListSecretGeneratorsResponse, error) { + queries, err := listSecretGeneratorToModel(req) + if err != nil { + return nil, err + } + result, err := s.query.SearchSecretGenerators(ctx, queries) + if err != nil { + return nil, err + } + return &admin_pb.ListSecretGeneratorsResponse{ + Details: object.ToListDetails(result.Count, result.Sequence, result.Timestamp), + }, nil +} + +func (s *Server) GetSecretGenerator(ctx context.Context, req *admin_pb.GetSecretGeneratorRequest) (*admin_pb.GetSecretGeneratorResponse, error) { + generator, err := s.query.SecretGeneratorByType(ctx, SecretGeneratorTypeToDomain(req.GeneratorType)) + if err != nil { + return nil, err + } + return &admin_pb.GetSecretGeneratorResponse{ + SecretGenerator: SecretGeneratorToPb(generator), + }, nil +} + +func (s *Server) UpdateSecretGenerator(ctx context.Context, req *admin_pb.UpdateSecretGeneratorRequest) (*admin_pb.UpdateSecretGeneratorResponse, error) { + details, err := s.command.ChangeSecretGeneratorConfig(ctx, SecretGeneratorTypeToDomain(req.GeneratorType), UpdateSecretGeneratorToConfig(req)) + if err != nil { + return nil, err + } + return &admin_pb.UpdateSecretGeneratorResponse{ + Details: object.ChangeToDetailsPb( + details.Sequence, + details.EventDate, + details.ResourceOwner), + }, nil +} + +func (s *Server) GetSMTPConfig(ctx context.Context, req *admin_pb.GetSMTPConfigRequest) (*admin_pb.GetSMTPConfigResponse, error) { + smtp, err := s.query.SMTPConfigByAggregateID(ctx, domain.IAMID) + if err != nil { + return nil, err + } + return &admin_pb.GetSMTPConfigResponse{ + SmtpConfig: SMTPConfigToPb(smtp), + }, nil +} + +func (s *Server) UpdateSMTPConfig(ctx context.Context, req *admin_pb.UpdateSMTPConfigRequest) (*admin_pb.UpdateSMTPConfigResponse, error) { + details, err := s.command.ChangeSMTPConfig(ctx, UpdateSMTPToConfig(req)) + if err != nil { + return nil, err + } + return &admin_pb.UpdateSMTPConfigResponse{ + Details: object.ChangeToDetailsPb( + details.Sequence, + details.EventDate, + details.ResourceOwner), + }, nil +} + +func (s *Server) UpdateSMTPConfigPassword(ctx context.Context, req *admin_pb.UpdateSMTPConfigPasswordRequest) (*admin_pb.UpdateSMTPConfigPasswordResponse, error) { + details, err := s.command.ChangeSMTPConfigPassword(ctx, req.Password) + if err != nil { + return nil, err + } + return &admin_pb.UpdateSMTPConfigPasswordResponse{ + Details: object.ChangeToDetailsPb( + details.Sequence, + details.EventDate, + details.ResourceOwner), + }, nil +} diff --git a/internal/api/grpc/admin/iam_settings_converter.go b/internal/api/grpc/admin/iam_settings_converter.go new file mode 100644 index 0000000000..69f2581b84 --- /dev/null +++ b/internal/api/grpc/admin/iam_settings_converter.go @@ -0,0 +1,138 @@ +package admin + +import ( + "github.com/caos/zitadel/internal/domain" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/caos/zitadel/internal/api/grpc/object" + obj_grpc "github.com/caos/zitadel/internal/api/grpc/object" + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/notification/channels/smtp" + "github.com/caos/zitadel/internal/query" + admin_pb "github.com/caos/zitadel/pkg/grpc/admin" + settings_pb "github.com/caos/zitadel/pkg/grpc/settings" +) + +func listSecretGeneratorToModel(req *admin_pb.ListSecretGeneratorsRequest) (*query.SecretGeneratorSearchQueries, error) { + offset, limit, asc := object.ListQueryToModel(req.Query) + queries, err := SecretGeneratorQueriesToModel(req.Queries) + if err != nil { + return nil, err + } + return &query.SecretGeneratorSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + Queries: queries, + }, nil +} + +func SecretGeneratorQueriesToModel(queries []*settings_pb.SecretGeneratorQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = SecretGeneratorQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func SecretGeneratorQueryToModel(apiQuery *settings_pb.SecretGeneratorQuery) (query.SearchQuery, error) { + switch q := apiQuery.Query.(type) { + case *settings_pb.SecretGeneratorQuery_TypeQuery: + domainType := SecretGeneratorTypeToDomain(q.TypeQuery.GeneratorType) + return query.NewSecretGeneratorTypeSearchQuery(int32(domainType)) + default: + return nil, errors.ThrowInvalidArgument(nil, "ORG-fm9es", "List.Query.Invalid") + } +} + +func UpdateSecretGeneratorToConfig(req *admin_pb.UpdateSecretGeneratorRequest) *crypto.GeneratorConfig { + return &crypto.GeneratorConfig{ + Length: uint(req.Length), + Expiry: req.Expiry.AsDuration(), + IncludeUpperLetters: req.IncludeUpperLetters, + IncludeLowerLetters: req.IncludeLowerLetters, + IncludeDigits: req.IncludeDigits, + IncludeSymbols: req.IncludeSymbols, + } +} + +func SecretGeneratorToPb(generator *query.SecretGenerator) *settings_pb.SecretGenerator { + mapped := &settings_pb.SecretGenerator{ + Length: uint32(generator.Length), + Expiry: durationpb.New(generator.Expiry), + IncludeUpperLetters: generator.IncludeUpperLetters, + IncludeLowerLetters: generator.IncludeLowerLetters, + IncludeDigits: generator.IncludeDigits, + IncludeSymbols: generator.IncludeSymbols, + Details: obj_grpc.ToViewDetailsPb(generator.Sequence, generator.CreationDate, generator.ChangeDate, generator.AggregateID), + } + return mapped +} + +func SecretGeneratorTypeToPb(generatorType domain.SecretGeneratorType) settings_pb.SecretGeneratorType { + switch generatorType { + case domain.SecretGeneratorTypeInitCode: + return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_INIT_CODE + case domain.SecretGeneratorTypeVerifyEmailCode: + return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_VERIFY_EMAIL_CODE + case domain.SecretGeneratorTypeVerifyPhoneCode: + return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_VERIFY_PHONE_CODE + case domain.SecretGeneratorTypePasswordResetCode: + return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_PASSWORD_RESET_CODE + case domain.SecretGeneratorTypePasswordlessInitCode: + return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_PASSWORDLESS_INIT_CODE + case domain.SecretGeneratorTypeAppSecret: + return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_APP_SECRET + default: + return settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_UNSPECIFIED + } +} + +func SecretGeneratorTypeToDomain(generatorType settings_pb.SecretGeneratorType) domain.SecretGeneratorType { + switch generatorType { + case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_INIT_CODE: + return domain.SecretGeneratorTypeInitCode + case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_VERIFY_EMAIL_CODE: + return domain.SecretGeneratorTypeVerifyEmailCode + case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_VERIFY_PHONE_CODE: + return domain.SecretGeneratorTypeVerifyPhoneCode + case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_PASSWORD_RESET_CODE: + return domain.SecretGeneratorTypePasswordResetCode + case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_PASSWORDLESS_INIT_CODE: + return domain.SecretGeneratorTypePasswordlessInitCode + case settings_pb.SecretGeneratorType_SECRET_GENERATOR_TYPE_APP_SECRET: + return domain.SecretGeneratorTypeAppSecret + default: + return domain.SecretGeneratorTypeUnspecified + } +} + +func UpdateSMTPToConfig(req *admin_pb.UpdateSMTPConfigRequest) *smtp.EmailConfig { + return &smtp.EmailConfig{ + Tls: req.Tls, + From: req.SenderAddress, + FromName: req.SenderName, + SMTP: smtp.SMTP{ + Host: req.Host, + User: req.User, + }, + } +} + +func SMTPConfigToPb(smtp *query.SMTPConfig) *settings_pb.SMTPConfig { + mapped := &settings_pb.SMTPConfig{ + Tls: smtp.TLS, + SenderAddress: smtp.SenderAddress, + SenderName: smtp.SenderName, + Host: smtp.Host, + User: smtp.User, + Details: obj_grpc.ToViewDetailsPb(smtp.Sequence, smtp.CreationDate, smtp.ChangeDate, smtp.AggregateID), + } + return mapped +} diff --git a/internal/api/grpc/admin/language.go b/internal/api/grpc/admin/language.go index 67fa950d79..fff5b43381 100644 --- a/internal/api/grpc/admin/language.go +++ b/internal/api/grpc/admin/language.go @@ -3,8 +3,12 @@ package admin import ( "context" + "github.com/caos/zitadel/internal/api/grpc/object" "github.com/caos/zitadel/internal/api/grpc/text" + "github.com/caos/zitadel/internal/domain" + caos_errors "github.com/caos/zitadel/internal/errors" admin_pb "github.com/caos/zitadel/pkg/grpc/admin" + "golang.org/x/text/language" ) func (s *Server) GetSupportedLanguages(ctx context.Context, req *admin_pb.GetSupportedLanguagesRequest) (*admin_pb.GetSupportedLanguagesResponse, error) { @@ -14,3 +18,25 @@ func (s *Server) GetSupportedLanguages(ctx context.Context, req *admin_pb.GetSup } return &admin_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil } + +func (s *Server) SetDefaultLanguage(ctx context.Context, req *admin_pb.SetDefaultLanguageRequest) (*admin_pb.SetDefaultLanguageResponse, error) { + lang, err := language.Parse(req.Language) + if err != nil { + return nil, caos_errors.ThrowInvalidArgument(err, "API-39nnf", "Errors.Language.Parse") + } + details, err := s.command.SetDefaultLanguage(ctx, lang) + if err != nil { + return nil, err + } + return &admin_pb.SetDefaultLanguageResponse{ + Details: object.DomainToChangeDetailsPb(details), + }, nil +} + +func (s *Server) GetDefaultLanguage(ctx context.Context, req *admin_pb.GetDefaultLanguageRequest) (*admin_pb.GetDefaultLanguageResponse, error) { + iam, err := s.query.IAMByID(ctx, domain.IAMID) + if err != nil { + return nil, err + } + return &admin_pb.GetDefaultLanguageResponse{Language: iam.DefaultLanguage.String()}, nil +} diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index 9a9daa889c..a660d2aa58 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -53,7 +53,15 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (* human := setUpOrgHumanToDomain(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine org := setUpOrgOrgToDomain(req.Org) - objectDetails, err := s.command.SetUpOrg(ctx, org, human, userIDs, false) + 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 + } + objectDetails, err := s.command.SetUpOrg(ctx, org, human, initCodeGenerator, phoneCodeGenerator, userIDs, false) if err != nil { return nil, err } diff --git a/internal/api/grpc/admin/server.go b/internal/api/grpc/admin/server.go index 18818f1401..dfcdc23e6d 100644 --- a/internal/api/grpc/admin/server.go +++ b/internal/api/grpc/admin/server.go @@ -1,6 +1,7 @@ package admin import ( + "github.com/caos/zitadel/internal/crypto" "google.golang.org/grpc" "github.com/caos/zitadel/internal/admin/repository" @@ -25,19 +26,22 @@ type Server struct { administrator repository.AdministratorRepository iamDomain string assetsAPIDomain string + + UserCodeAlg crypto.EncryptionAlgorithm } type Config struct { Repository eventsourcing.Config } -func CreateServer(command *command.Commands, query *query.Queries, repo repository.Repository, iamDomain, assetsAPIDomain string) *Server { +func CreateServer(command *command.Commands, query *query.Queries, repo repository.Repository, iamDomain, assetsAPIDomain string, userCrypto *crypto.AESCrypto) *Server { return &Server{ command: command, query: query, administrator: repo, iamDomain: iamDomain, assetsAPIDomain: assetsAPIDomain, + UserCodeAlg: userCrypto, } } diff --git a/internal/api/grpc/auth/email.go b/internal/api/grpc/auth/email.go index 6636c64850..465f14ac29 100644 --- a/internal/api/grpc/auth/email.go +++ b/internal/api/grpc/auth/email.go @@ -6,6 +6,7 @@ import ( "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/api/grpc/object" "github.com/caos/zitadel/internal/api/grpc/user" + "github.com/caos/zitadel/internal/domain" auth_pb "github.com/caos/zitadel/pkg/grpc/auth" ) @@ -26,7 +27,11 @@ func (s *Server) GetMyEmail(ctx context.Context, _ *auth_pb.GetMyEmailRequest) ( } func (s *Server) SetMyEmail(ctx context.Context, req *auth_pb.SetMyEmailRequest) (*auth_pb.SetMyEmailResponse, error) { - email, err := s.command.ChangeHumanEmail(ctx, UpdateMyEmailToDomain(ctx, req)) + emailCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyEmailCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + email, err := s.command.ChangeHumanEmail(ctx, UpdateMyEmailToDomain(ctx, req), emailCodeGenerator) if err != nil { return nil, err } @@ -40,8 +45,12 @@ func (s *Server) SetMyEmail(ctx context.Context, req *auth_pb.SetMyEmailRequest) } func (s *Server) VerifyMyEmail(ctx context.Context, req *auth_pb.VerifyMyEmailRequest) (*auth_pb.VerifyMyEmailResponse, error) { + emailCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyEmailCode, s.UserCodeAlg) + if err != nil { + return nil, err + } ctxData := authz.GetCtxData(ctx) - objectDetails, err := s.command.VerifyHumanEmail(ctx, ctxData.UserID, req.Code, ctxData.ResourceOwner) + objectDetails, err := s.command.VerifyHumanEmail(ctx, ctxData.UserID, req.Code, ctxData.ResourceOwner, emailCodeGenerator) if err != nil { return nil, err } @@ -52,7 +61,11 @@ func (s *Server) VerifyMyEmail(ctx context.Context, req *auth_pb.VerifyMyEmailRe func (s *Server) ResendMyEmailVerification(ctx context.Context, _ *auth_pb.ResendMyEmailVerificationRequest) (*auth_pb.ResendMyEmailVerificationResponse, error) { ctxData := authz.GetCtxData(ctx) - objectDetails, err := s.command.CreateHumanEmailVerificationCode(ctx, ctxData.UserID, ctxData.ResourceOwner) + emailCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyEmailCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + objectDetails, err := s.command.CreateHumanEmailVerificationCode(ctx, ctxData.UserID, ctxData.ResourceOwner, emailCodeGenerator) if err != nil { return nil, err } diff --git a/internal/api/grpc/auth/passwordless.go b/internal/api/grpc/auth/passwordless.go index 1d1453c974..fee5c0c7e6 100644 --- a/internal/api/grpc/auth/passwordless.go +++ b/internal/api/grpc/auth/passwordless.go @@ -57,7 +57,11 @@ func (s *Server) AddMyPasswordless(ctx context.Context, _ *auth_pb.AddMyPassword func (s *Server) AddMyPasswordlessLink(ctx context.Context, _ *auth_pb.AddMyPasswordlessLinkRequest) (*auth_pb.AddMyPasswordlessLinkResponse, error) { ctxData := authz.GetCtxData(ctx) - initCode, err := s.command.HumanAddPasswordlessInitCode(ctx, ctxData.UserID, ctxData.ResourceOwner) + passwordlessInitCode, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypePasswordlessInitCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + initCode, err := s.command.HumanAddPasswordlessInitCode(ctx, ctxData.UserID, ctxData.ResourceOwner, passwordlessInitCode) if err != nil { return nil, err } @@ -70,7 +74,11 @@ func (s *Server) AddMyPasswordlessLink(ctx context.Context, _ *auth_pb.AddMyPass func (s *Server) SendMyPasswordlessLink(ctx context.Context, _ *auth_pb.SendMyPasswordlessLinkRequest) (*auth_pb.SendMyPasswordlessLinkResponse, error) { ctxData := authz.GetCtxData(ctx) - initCode, err := s.command.HumanSendPasswordlessInitCode(ctx, ctxData.UserID, ctxData.ResourceOwner) + passwordlessInitCode, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypePasswordlessInitCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + initCode, err := s.command.HumanSendPasswordlessInitCode(ctx, ctxData.UserID, ctxData.ResourceOwner, passwordlessInitCode) if err != nil { return nil, err } diff --git a/internal/api/grpc/auth/phone.go b/internal/api/grpc/auth/phone.go index dc1b1fa23a..60efca999d 100644 --- a/internal/api/grpc/auth/phone.go +++ b/internal/api/grpc/auth/phone.go @@ -6,6 +6,7 @@ import ( "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/api/grpc/object" "github.com/caos/zitadel/internal/api/grpc/user" + "github.com/caos/zitadel/internal/domain" auth_pb "github.com/caos/zitadel/pkg/grpc/auth" ) @@ -26,7 +27,11 @@ func (s *Server) GetMyPhone(ctx context.Context, _ *auth_pb.GetMyPhoneRequest) ( } func (s *Server) SetMyPhone(ctx context.Context, req *auth_pb.SetMyPhoneRequest) (*auth_pb.SetMyPhoneResponse, error) { - phone, err := s.command.ChangeHumanPhone(ctx, UpdateMyPhoneToDomain(ctx, req), authz.GetCtxData(ctx).ResourceOwner) + phoneCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + phone, err := s.command.ChangeHumanPhone(ctx, UpdateMyPhoneToDomain(ctx, req), authz.GetCtxData(ctx).ResourceOwner, phoneCodeGenerator) if err != nil { return nil, err } @@ -41,7 +46,11 @@ func (s *Server) SetMyPhone(ctx context.Context, req *auth_pb.SetMyPhoneRequest) func (s *Server) VerifyMyPhone(ctx context.Context, req *auth_pb.VerifyMyPhoneRequest) (*auth_pb.VerifyMyPhoneResponse, error) { ctxData := authz.GetCtxData(ctx) - objectDetails, err := s.command.VerifyHumanPhone(ctx, ctxData.UserID, req.Code, ctxData.ResourceOwner) + phoneCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + objectDetails, err := s.command.VerifyHumanPhone(ctx, ctxData.UserID, req.Code, ctxData.ResourceOwner, phoneCodeGenerator) if err != nil { return nil, err } @@ -53,7 +62,11 @@ func (s *Server) VerifyMyPhone(ctx context.Context, req *auth_pb.VerifyMyPhoneRe func (s *Server) ResendMyPhoneVerification(ctx context.Context, _ *auth_pb.ResendMyPhoneVerificationRequest) (*auth_pb.ResendMyPhoneVerificationResponse, error) { ctxData := authz.GetCtxData(ctx) - objectDetails, err := s.command.CreateHumanPhoneVerificationCode(ctx, ctxData.UserID, ctxData.ResourceOwner) + phoneCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + objectDetails, err := s.command.CreateHumanPhoneVerificationCode(ctx, ctxData.UserID, ctxData.ResourceOwner, phoneCodeGenerator) if err != nil { return nil, err } diff --git a/internal/api/grpc/auth/server.go b/internal/api/grpc/auth/server.go index b027ef797f..c40295ac8b 100644 --- a/internal/api/grpc/auth/server.go +++ b/internal/api/grpc/auth/server.go @@ -1,6 +1,7 @@ package auth import ( + "github.com/caos/zitadel/internal/crypto" "google.golang.org/grpc" "github.com/caos/zitadel/internal/api/authz" @@ -26,19 +27,21 @@ type Server struct { repo repository.Repository defaults systemdefaults.SystemDefaults assetsAPIDomain string + UserCodeAlg crypto.EncryptionAlgorithm } type Config struct { Repository eventsourcing.Config } -func CreateServer(command *command.Commands, query *query.Queries, authRepo repository.Repository, defaults systemdefaults.SystemDefaults, assetsAPIDomain string) *Server { +func CreateServer(command *command.Commands, query *query.Queries, authRepo repository.Repository, defaults systemdefaults.SystemDefaults, assetsAPIDomain string, userCrypto *crypto.AESCrypto) *Server { return &Server{ command: command, query: query, repo: authRepo, defaults: defaults, assetsAPIDomain: assetsAPIDomain, + UserCodeAlg: userCrypto, } } diff --git a/internal/api/grpc/management/iam.go b/internal/api/grpc/management/iam.go index 006ada73c4..03aa70a91a 100644 --- a/internal/api/grpc/management/iam.go +++ b/internal/api/grpc/management/iam.go @@ -3,11 +3,12 @@ package management import ( "context" + "github.com/caos/zitadel/internal/domain" mgmt_pb "github.com/caos/zitadel/pkg/grpc/management" ) func (s *Server) GetIAM(ctx context.Context, req *mgmt_pb.GetIAMRequest) (*mgmt_pb.GetIAMResponse, error) { - iam, err := s.query.IAMByID(ctx, s.systemDefaults.IamID) + iam, err := s.query.IAMByID(ctx, domain.IAMID) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/project_application.go b/internal/api/grpc/management/project_application.go index f4cbd6e87d..11886acbf0 100644 --- a/internal/api/grpc/management/project_application.go +++ b/internal/api/grpc/management/project_application.go @@ -8,6 +8,7 @@ import ( change_grpc "github.com/caos/zitadel/internal/api/grpc/change" object_grpc "github.com/caos/zitadel/internal/api/grpc/object" project_grpc "github.com/caos/zitadel/internal/api/grpc/project" + "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/query" mgmt_pb "github.com/caos/zitadel/pkg/grpc/management" ) @@ -57,7 +58,11 @@ func (s *Server) ListAppChanges(ctx context.Context, req *mgmt_pb.ListAppChanges } func (s *Server) AddOIDCApp(ctx context.Context, req *mgmt_pb.AddOIDCAppRequest) (*mgmt_pb.AddOIDCAppResponse, error) { - app, err := s.command.AddOIDCApplication(ctx, AddOIDCAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + appSecretGenerator, err := s.query.InitHashGenerator(ctx, domain.SecretGeneratorTypeAppSecret, s.PasswordHashAlg) + if err != nil { + return nil, err + } + app, err := s.command.AddOIDCApplication(ctx, AddOIDCAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID, appSecretGenerator) if err != nil { return nil, err } @@ -72,7 +77,11 @@ func (s *Server) AddOIDCApp(ctx context.Context, req *mgmt_pb.AddOIDCAppRequest) } func (s *Server) AddAPIApp(ctx context.Context, req *mgmt_pb.AddAPIAppRequest) (*mgmt_pb.AddAPIAppResponse, error) { - app, err := s.command.AddAPIApplication(ctx, AddAPIAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + appSecretGenerator, err := s.query.InitHashGenerator(ctx, domain.SecretGeneratorTypeAppSecret, s.PasswordHashAlg) + if err != nil { + return nil, err + } + app, err := s.command.AddAPIApplication(ctx, AddAPIAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID, appSecretGenerator) if err != nil { return nil, err } @@ -153,7 +162,11 @@ func (s *Server) RemoveApp(ctx context.Context, req *mgmt_pb.RemoveAppRequest) ( } func (s *Server) RegenerateOIDCClientSecret(ctx context.Context, req *mgmt_pb.RegenerateOIDCClientSecretRequest) (*mgmt_pb.RegenerateOIDCClientSecretResponse, error) { - config, err := s.command.ChangeOIDCApplicationSecret(ctx, req.ProjectId, req.AppId, authz.GetCtxData(ctx).OrgID) + appSecretGenerator, err := s.query.InitHashGenerator(ctx, domain.SecretGeneratorTypeAppSecret, s.PasswordHashAlg) + if err != nil { + return nil, err + } + config, err := s.command.ChangeOIDCApplicationSecret(ctx, req.ProjectId, req.AppId, authz.GetCtxData(ctx).OrgID, appSecretGenerator) if err != nil { return nil, err } @@ -168,7 +181,11 @@ func (s *Server) RegenerateOIDCClientSecret(ctx context.Context, req *mgmt_pb.Re } func (s *Server) RegenerateAPIClientSecret(ctx context.Context, req *mgmt_pb.RegenerateAPIClientSecretRequest) (*mgmt_pb.RegenerateAPIClientSecretResponse, error) { - config, err := s.command.ChangeAPIApplicationSecret(ctx, req.ProjectId, req.AppId, authz.GetCtxData(ctx).OrgID) + appSecretGenerator, err := s.query.InitHashGenerator(ctx, domain.SecretGeneratorTypeAppSecret, s.PasswordHashAlg) + if err != nil { + return nil, err + } + config, err := s.command.ChangeAPIApplicationSecret(ctx, req.ProjectId, req.AppId, authz.GetCtxData(ctx).OrgID, appSecretGenerator) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/server.go b/internal/api/grpc/management/server.go index 67baa1e510..56778acb74 100644 --- a/internal/api/grpc/management/server.go +++ b/internal/api/grpc/management/server.go @@ -1,6 +1,7 @@ package management import ( + "github.com/caos/zitadel/internal/crypto" "google.golang.org/grpc" "github.com/caos/zitadel/internal/api/authz" @@ -19,18 +20,22 @@ var _ management.ManagementServiceServer = (*Server)(nil) type Server struct { management.UnimplementedManagementServiceServer - command *command.Commands - query *query.Queries - systemDefaults systemdefaults.SystemDefaults - assetAPIPrefix string + command *command.Commands + query *query.Queries + systemDefaults systemdefaults.SystemDefaults + assetAPIPrefix string + PasswordHashAlg crypto.HashAlgorithm + UserCodeAlg crypto.EncryptionAlgorithm } -func CreateServer(command *command.Commands, query *query.Queries, sd systemdefaults.SystemDefaults, assetAPIPrefix string) *Server { +func CreateServer(command *command.Commands, query *query.Queries, sd systemdefaults.SystemDefaults, assetAPIPrefix string, userCrypto *crypto.AESCrypto) *Server { return &Server{ - command: command, - query: query, - systemDefaults: sd, - assetAPIPrefix: assetAPIPrefix, + command: command, + query: query, + systemDefaults: sd, + assetAPIPrefix: assetAPIPrefix, + PasswordHashAlg: crypto.NewBCrypt(sd.SecretGenerators.PasswordSaltCost), + UserCodeAlg: userCrypto, } } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 3a829a86f6..a9cd0161ee 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -192,7 +192,15 @@ func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRe } func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) { - human, err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, AddHumanUserRequestToDomain(req)) + 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) if err != nil { return nil, err } @@ -208,7 +216,19 @@ func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUserRequest) (*mgmt_pb.ImportHumanUserResponse, error) { human, passwordless := ImportHumanUserRequestToDomain(req) - addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless) + 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 + } + passwordlessInitCode, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypePasswordlessInitCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless, initCodeGenerator, phoneCodeGenerator, passwordlessInitCode) if err != nil { return nil, err } @@ -388,7 +408,11 @@ func (s *Server) GetHumanEmail(ctx context.Context, req *mgmt_pb.GetHumanEmailRe } func (s *Server) UpdateHumanEmail(ctx context.Context, req *mgmt_pb.UpdateHumanEmailRequest) (*mgmt_pb.UpdateHumanEmailResponse, error) { - email, err := s.command.ChangeHumanEmail(ctx, UpdateHumanEmailRequestToDomain(ctx, req)) + emailCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyEmailCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + email, err := s.command.ChangeHumanEmail(ctx, UpdateHumanEmailRequestToDomain(ctx, req), emailCodeGenerator) if err != nil { return nil, err } @@ -402,7 +426,11 @@ func (s *Server) UpdateHumanEmail(ctx context.Context, req *mgmt_pb.UpdateHumanE } func (s *Server) ResendHumanInitialization(ctx context.Context, req *mgmt_pb.ResendHumanInitializationRequest) (*mgmt_pb.ResendHumanInitializationResponse, error) { - details, err := s.command.ResendInitialMail(ctx, req.UserId, req.Email, authz.GetCtxData(ctx).OrgID) + initCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeInitCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + details, err := s.command.ResendInitialMail(ctx, req.UserId, req.Email, authz.GetCtxData(ctx).OrgID, initCodeGenerator) if err != nil { return nil, err } @@ -412,7 +440,11 @@ func (s *Server) ResendHumanInitialization(ctx context.Context, req *mgmt_pb.Res } func (s *Server) ResendHumanEmailVerification(ctx context.Context, req *mgmt_pb.ResendHumanEmailVerificationRequest) (*mgmt_pb.ResendHumanEmailVerificationResponse, error) { - objectDetails, err := s.command.CreateHumanEmailVerificationCode(ctx, req.UserId, authz.GetCtxData(ctx).OrgID) + emailCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyEmailCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + objectDetails, err := s.command.CreateHumanEmailVerificationCode(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, emailCodeGenerator) if err != nil { return nil, err } @@ -442,7 +474,11 @@ func (s *Server) GetHumanPhone(ctx context.Context, req *mgmt_pb.GetHumanPhoneRe } func (s *Server) UpdateHumanPhone(ctx context.Context, req *mgmt_pb.UpdateHumanPhoneRequest) (*mgmt_pb.UpdateHumanPhoneResponse, error) { - phone, err := s.command.ChangeHumanPhone(ctx, UpdateHumanPhoneRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + phoneCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + phone, err := s.command.ChangeHumanPhone(ctx, UpdateHumanPhoneRequestToDomain(req), authz.GetCtxData(ctx).OrgID, phoneCodeGenerator) if err != nil { return nil, err } @@ -466,7 +502,11 @@ func (s *Server) RemoveHumanPhone(ctx context.Context, req *mgmt_pb.RemoveHumanP } func (s *Server) ResendHumanPhoneVerification(ctx context.Context, req *mgmt_pb.ResendHumanPhoneVerificationRequest) (*mgmt_pb.ResendHumanPhoneVerificationResponse, error) { - objectDetails, err := s.command.CreateHumanPhoneVerificationCode(ctx, req.UserId, authz.GetCtxData(ctx).OrgID) + phoneCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + objectDetails, err := s.command.CreateHumanPhoneVerificationCode(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, phoneCodeGenerator) if err != nil { return nil, err } @@ -507,7 +547,11 @@ func (s *Server) SetHumanPassword(ctx context.Context, req *mgmt_pb.SetHumanPass } func (s *Server) SendHumanResetPasswordNotification(ctx context.Context, req *mgmt_pb.SendHumanResetPasswordNotificationRequest) (*mgmt_pb.SendHumanResetPasswordNotificationResponse, error) { - objectDetails, err := s.command.RequestSetPassword(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, notifyTypeToDomain(req.Type)) + passwordCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypePasswordResetCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + objectDetails, err := s.command.RequestSetPassword(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, notifyTypeToDomain(req.Type), passwordCodeGenerator) if err != nil { return nil, err } @@ -584,7 +628,11 @@ func (s *Server) ListHumanPasswordless(ctx context.Context, req *mgmt_pb.ListHum func (s *Server) AddPasswordlessRegistration(ctx context.Context, req *mgmt_pb.AddPasswordlessRegistrationRequest) (*mgmt_pb.AddPasswordlessRegistrationResponse, error) { ctxData := authz.GetCtxData(ctx) - initCode, err := s.command.HumanAddPasswordlessInitCode(ctx, req.UserId, ctxData.OrgID) + passwordlessInitCode, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypePasswordlessInitCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + initCode, err := s.command.HumanAddPasswordlessInitCode(ctx, req.UserId, ctxData.OrgID, passwordlessInitCode) if err != nil { return nil, err } @@ -597,7 +645,11 @@ func (s *Server) AddPasswordlessRegistration(ctx context.Context, req *mgmt_pb.A func (s *Server) SendPasswordlessRegistration(ctx context.Context, req *mgmt_pb.SendPasswordlessRegistrationRequest) (*mgmt_pb.SendPasswordlessRegistrationResponse, error) { ctxData := authz.GetCtxData(ctx) - initCode, err := s.command.HumanSendPasswordlessInitCode(ctx, req.UserId, ctxData.OrgID) + passwordlessInitCode, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypePasswordlessInitCode, s.UserCodeAlg) + if err != nil { + return nil, err + } + initCode, err := s.command.HumanSendPasswordlessInitCode(ctx, req.UserId, ctxData.OrgID, passwordlessInitCode) if err != nil { return nil, err } diff --git a/internal/api/grpc/server/middleware/translation_interceptor.go b/internal/api/grpc/server/middleware/translation_interceptor.go index ed760c53f7..09a2f37e18 100644 --- a/internal/api/grpc/server/middleware/translation_interceptor.go +++ b/internal/api/grpc/server/middleware/translation_interceptor.go @@ -3,15 +3,14 @@ package middleware import ( "context" - "golang.org/x/text/language" - + "github.com/caos/zitadel/internal/query" "google.golang.org/grpc" _ "github.com/caos/zitadel/internal/statik" ) -func TranslationHandler(defaultLanguage language.Tag) func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - translator := newZitadelTranslator(defaultLanguage) +func TranslationHandler(query *query.Queries) func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + translator := newZitadelTranslator(query.GetDefaultLanguage(context.Background())) return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { resp, err := handler(ctx, req) diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index 419b47f6bc..2776831607 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -2,10 +2,10 @@ package server import ( grpc_api "github.com/caos/zitadel/internal/api/grpc" + "github.com/caos/zitadel/internal/query" "github.com/caos/zitadel/internal/telemetry/metrics" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" - "golang.org/x/text/language" "google.golang.org/grpc" "github.com/caos/zitadel/internal/api/authz" @@ -20,7 +20,7 @@ type Server interface { AuthMethods() authz.MethodMapping } -func CreateServer(verifier *authz.TokenVerifier, authConfig authz.Config, lang language.Tag) *grpc.Server { +func CreateServer(verifier *authz.TokenVerifier, authConfig authz.Config, queries *query.Queries) *grpc.Server { metricTypes := []metrics.MetricType{metrics.MetricTypeTotalCount, metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode} return grpc.NewServer( grpc.UnaryInterceptor( @@ -31,7 +31,7 @@ func CreateServer(verifier *authz.TokenVerifier, authConfig authz.Config, lang l middleware.NoCacheInterceptor(), middleware.ErrorHandler(), middleware.AuthorizationInterceptor(verifier, authConfig), - middleware.TranslationHandler(lang), + middleware.TranslationHandler(queries), middleware.ValidationHandler(), middleware.ServiceHandler(), ), diff --git a/internal/api/ui/login/external_register_handler.go b/internal/api/ui/login/external_register_handler.go index 668692a6bc..dd4a844839 100644 --- a/internal/api/ui/login/external_register_handler.go +++ b/internal/api/ui/login/external_register_handler.go @@ -145,7 +145,17 @@ func (l *Login) registerExternalUser(w http.ResponseWriter, r *http.Request, aut memberRoles = nil resourceOwner = authReq.RequestedOrgID } - _, err := l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, externalIDP, memberRoles) + initCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeInitCode, l.UserCodeAlg) + if err != nil { + l.renderRegisterOption(w, r, authReq, err) + return + } + phoneCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyPhoneCode, l.UserCodeAlg) + if err != nil { + l.renderRegisterOption(w, r, authReq, err) + return + } + _, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, externalIDP, memberRoles, initCodeGenerator, phoneCodeGenerator) if err != nil { l.renderRegisterOption(w, r, authReq, err) return @@ -216,7 +226,17 @@ func (l *Login) handleExternalRegisterCheck(w http.ResponseWriter, r *http.Reque l.renderRegisterOption(w, r, authReq, err) return } - _, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, externalIDP, memberRoles) + initCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeInitCode, l.UserCodeAlg) + if err != nil { + l.renderRegisterOption(w, r, authReq, err) + return + } + phoneCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyPhoneCode, l.UserCodeAlg) + if err != nil { + l.renderRegisterOption(w, r, authReq, err) + return + } + _, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, externalIDP, memberRoles, initCodeGenerator, phoneCodeGenerator) if err != nil { l.renderRegisterOption(w, r, authReq, err) return diff --git a/internal/api/ui/login/init_password_handler.go b/internal/api/ui/login/init_password_handler.go index fcec051e70..238bf46431 100644 --- a/internal/api/ui/login/init_password_handler.go +++ b/internal/api/ui/login/init_password_handler.go @@ -70,7 +70,12 @@ func (l *Login) checkPWCode(w http.ResponseWriter, r *http.Request, authReq *dom userOrg = authReq.UserOrgID } userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) - err = l.command.SetPasswordWithVerifyCode(setContext(r.Context(), userOrg), userOrg, data.UserID, data.Code, data.Password, userAgentID) + passwordCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypePasswordResetCode, l.UserCodeAlg) + if err != nil { + l.renderInitPassword(w, r, authReq, data.UserID, "", err) + return + } + err = l.command.SetPasswordWithVerifyCode(setContext(r.Context(), userOrg), userOrg, data.UserID, data.Code, data.Password, userAgentID, passwordCodeGenerator) if err != nil { l.renderInitPassword(w, r, authReq, data.UserID, "", err) return @@ -92,12 +97,17 @@ func (l *Login) resendPasswordSet(w http.ResponseWriter, r *http.Request, authRe l.renderInitPassword(w, r, authReq, authReq.UserID, "", err) return } + passwordCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypePasswordResetCode, l.UserCodeAlg) + if err != nil { + l.renderInitPassword(w, r, authReq, authReq.UserID, "", err) + return + } user, err := l.query.GetUser(setContext(r.Context(), userOrg), loginName) if err != nil { l.renderInitPassword(w, r, authReq, authReq.UserID, "", err) return } - _, err = l.command.RequestSetPassword(setContext(r.Context(), userOrg), user.ID, user.ResourceOwner, domain.NotificationTypeEmail) + _, err = l.command.RequestSetPassword(setContext(r.Context(), userOrg), user.ID, user.ResourceOwner, domain.NotificationTypeEmail, passwordCodeGenerator) l.renderInitPassword(w, r, authReq, authReq.UserID, "", err) } diff --git a/internal/api/ui/login/init_user_handler.go b/internal/api/ui/login/init_user_handler.go index c3dd7fb91d..31fd70eac3 100644 --- a/internal/api/ui/login/init_user_handler.go +++ b/internal/api/ui/login/init_user_handler.go @@ -73,7 +73,12 @@ func (l *Login) checkUserInitCode(w http.ResponseWriter, r *http.Request, authRe if authReq != nil { userOrgID = authReq.UserOrgID } - err = l.command.HumanVerifyInitCode(setContext(r.Context(), userOrgID), data.UserID, userOrgID, data.Code, data.Password) + initCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeInitCode, l.UserCodeAlg) + if err != nil { + l.renderInitUser(w, r, authReq, data.UserID, "", data.PasswordSet, err) + return + } + err = l.command.HumanVerifyInitCode(setContext(r.Context(), userOrgID), data.UserID, userOrgID, data.Code, data.Password, initCodeGenerator) if err != nil { l.renderInitUser(w, r, authReq, data.UserID, "", data.PasswordSet, err) return @@ -86,7 +91,12 @@ func (l *Login) resendUserInit(w http.ResponseWriter, r *http.Request, authReq * if authReq != nil { userOrgID = authReq.UserOrgID } - _, err := l.command.ResendInitialMail(setContext(r.Context(), userOrgID), userID, "", userOrgID) + initCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeInitCode, l.UserCodeAlg) + if err != nil { + l.renderInitUser(w, r, authReq, userID, "", showPassword, err) + return + } + _, err = l.command.ResendInitialMail(setContext(r.Context(), userOrgID), userID, "", userOrgID, initCodeGenerator) l.renderInitUser(w, r, authReq, userID, "", showPassword, err) } diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index e184d1077c..057f3bed2d 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -38,6 +38,7 @@ type Login struct { zitadelURL string oidcAuthCallbackURL string IDPConfigAesCrypto crypto.EncryptionAlgorithm + UserCodeAlg crypto.EncryptionAlgorithm iamDomain string } @@ -59,7 +60,7 @@ const ( DefaultLoggedOutPath = HandlerPrefix + EndpointLogoutDone ) -func CreateLogin(config Config, command *command.Commands, query *query.Queries, authRepo *eventsourcing.EsRepository, staticStorage static.Storage, systemDefaults systemdefaults.SystemDefaults, zitadelURL, domain, oidcAuthCallbackURL string, externalSecure bool, userAgentCookie mux.MiddlewareFunc) (*Login, error) { +func CreateLogin(config Config, command *command.Commands, query *query.Queries, authRepo *eventsourcing.EsRepository, staticStorage static.Storage, systemDefaults systemdefaults.SystemDefaults, zitadelURL, domain, oidcAuthCallbackURL string, externalSecure bool, userAgentCookie mux.MiddlewareFunc, userCrypto *crypto.AESCrypto) (*Login, error) { aesCrypto, err := crypto.NewAESCrypto(systemDefaults.IDPConfigVerificationKey) if err != nil { return nil, fmt.Errorf("error create new aes crypto: %w", err) @@ -74,6 +75,7 @@ func CreateLogin(config Config, command *command.Commands, query *query.Queries, authRepo: authRepo, IDPConfigAesCrypto: aesCrypto, iamDomain: domain, + UserCodeAlg: userCrypto, } //TODO: enable when storage is implemented again //login.staticCache, err = config.StaticCache.Config.NewCache() diff --git a/internal/api/ui/login/mail_verify_handler.go b/internal/api/ui/login/mail_verify_handler.go index 6cd4cde2c4..fdfb80e69a 100644 --- a/internal/api/ui/login/mail_verify_handler.go +++ b/internal/api/ui/login/mail_verify_handler.go @@ -51,7 +51,12 @@ func (l *Login) handleMailVerificationCheck(w http.ResponseWriter, r *http.Reque if authReq != nil { userOrg = authReq.UserOrgID } - _, err = l.command.CreateHumanEmailVerificationCode(setContext(r.Context(), userOrg), data.UserID, userOrg) + emailCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyEmailCode, l.UserCodeAlg) + if err != nil { + l.checkMailCode(w, r, authReq, data.UserID, data.Code) + return + } + _, err = l.command.CreateHumanEmailVerificationCode(setContext(r.Context(), userOrg), data.UserID, userOrg, emailCodeGenerator) l.renderMailVerification(w, r, authReq, data.UserID, err) } @@ -61,7 +66,12 @@ func (l *Login) checkMailCode(w http.ResponseWriter, r *http.Request, authReq *d userID = authReq.UserID userOrg = authReq.UserOrgID } - _, err := l.command.VerifyHumanEmail(setContext(r.Context(), userOrg), userID, code, userOrg) + emailCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyEmailCode, l.UserCodeAlg) + if err != nil { + l.renderMailVerification(w, r, authReq, userID, err) + return + } + _, err = l.command.VerifyHumanEmail(setContext(r.Context(), userOrg), userID, code, userOrg, emailCodeGenerator) if err != nil { l.renderMailVerification(w, r, authReq, userID, err) return diff --git a/internal/api/ui/login/password_reset_handler.go b/internal/api/ui/login/password_reset_handler.go index 2741841c7e..8f28ff914e 100644 --- a/internal/api/ui/login/password_reset_handler.go +++ b/internal/api/ui/login/password_reset_handler.go @@ -27,7 +27,12 @@ func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) { l.renderPasswordResetDone(w, r, authReq, err) return } - _, err = l.command.RequestSetPassword(setContext(r.Context(), authReq.UserOrgID), user.ID, authReq.UserOrgID, domain.NotificationTypeEmail) + passwordCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypePasswordResetCode, l.UserCodeAlg) + if err != nil { + l.renderPasswordResetDone(w, r, authReq, err) + return + } + _, err = l.command.RequestSetPassword(setContext(r.Context(), authReq.UserOrgID), user.ID, authReq.UserOrgID, domain.NotificationTypeEmail, passwordCodeGenerator) l.renderPasswordResetDone(w, r, authReq, err) } diff --git a/internal/api/ui/login/register_handler.go b/internal/api/ui/login/register_handler.go index 754e72714d..546663378a 100644 --- a/internal/api/ui/login/register_handler.go +++ b/internal/api/ui/login/register_handler.go @@ -74,7 +74,17 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) { memberRoles = nil resourceOwner = authRequest.RequestedOrgID } - user, err := l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, data.toHumanDomain(), nil, memberRoles) + initCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeInitCode, l.UserCodeAlg) + if err != nil { + l.renderRegister(w, r, authRequest, data, err) + return + } + phoneCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyPhoneCode, l.UserCodeAlg) + if err != nil { + l.renderRegister(w, r, authRequest, data, err) + return + } + user, err := l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, data.toHumanDomain(), nil, memberRoles, initCodeGenerator, phoneCodeGenerator) if err != nil { l.renderRegister(w, r, authRequest, data, err) return diff --git a/internal/api/ui/login/register_org_handler.go b/internal/api/ui/login/register_org_handler.go index 2970aa39f4..2e0854f7c6 100644 --- a/internal/api/ui/login/register_org_handler.go +++ b/internal/api/ui/login/register_org_handler.go @@ -65,7 +65,17 @@ func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) { l.renderRegisterOrg(w, r, authRequest, data, err) return } - _, err = l.command.SetUpOrg(ctx, data.toOrgDomain(), data.toUserDomain(), userIDs, true) + initCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypePasswordlessInitCode, l.UserCodeAlg) + if err != nil { + l.renderRegisterOrg(w, r, authRequest, data, err) + return + } + phoneCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyPhoneCode, l.UserCodeAlg) + if err != nil { + l.renderRegisterOrg(w, r, authRequest, data, err) + return + } + _, err = l.command.SetUpOrg(ctx, data.toOrgDomain(), data.toUserDomain(), initCodeGenerator, phoneCodeGenerator, userIDs, true) if err != nil { l.renderRegisterOrg(w, r, authRequest, data, err) return diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 92b2a5c1dd..0aedc1f26f 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -5,6 +5,7 @@ import ( "time" "github.com/caos/logging" + "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" @@ -32,6 +33,7 @@ type AuthRequestRepo struct { AuthRequests cache.AuthRequestCache View *view.View Eventstore v1.Eventstore + UserCodeAlg crypto.EncryptionAlgorithm LabelPolicyProvider labelPolicyProvider UserSessionViewProvider userSessionViewProvider @@ -54,8 +56,6 @@ type AuthRequestRepo struct { MFAInitSkippedLifeTime time.Duration SecondFactorCheckLifeTime time.Duration MultiFactorCheckLifeTime time.Duration - - IAMID string } type labelPolicyProvider interface { @@ -381,13 +381,21 @@ func (repo *AuthRequestRepo) VerifyPasswordlessSetup(ctx context.Context, userID func (repo *AuthRequestRepo) BeginPasswordlessInitCodeSetup(ctx context.Context, userID, resourceOwner, codeID, verificationCode string, preferredPlatformType domain.AuthenticatorAttachment) (login *domain.WebAuthNToken, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - return repo.Command.HumanAddPasswordlessSetupInitCode(ctx, userID, resourceOwner, codeID, verificationCode, preferredPlatformType) + passwordlessInitCode, err := repo.Query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypePasswordlessInitCode, repo.UserCodeAlg) + if err != nil { + return nil, err + } + return repo.Command.HumanAddPasswordlessSetupInitCode(ctx, userID, resourceOwner, codeID, verificationCode, preferredPlatformType, passwordlessInitCode) } func (repo *AuthRequestRepo) VerifyPasswordlessInitCodeSetup(ctx context.Context, userID, resourceOwner, userAgentID, tokenName, codeID, verificationCode string, credentialData []byte) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - _, err = repo.Command.HumanPasswordlessSetupInitCode(ctx, userID, resourceOwner, tokenName, userAgentID, codeID, verificationCode, credentialData) + passwordlessInitCode, err := repo.Query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypePasswordlessInitCode, repo.UserCodeAlg) + if err != nil { + return err + } + _, err = repo.Command.HumanPasswordlessSetupInitCode(ctx, userID, resourceOwner, tokenName, userAgentID, codeID, verificationCode, credentialData, passwordlessInitCode) return err } @@ -447,7 +455,15 @@ func (repo *AuthRequestRepo) AutoRegisterExternalUser(ctx context.Context, regis if err != nil { return err } - human, err := repo.Command.RegisterHuman(ctx, resourceOwner, registerUser, externalIDP, orgMemberRoles) + initCodeGenerator, err := repo.Query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeInitCode, repo.UserCodeAlg) + if err != nil { + return err + } + phoneCodeGenerator, err := repo.Query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, repo.UserCodeAlg) + if err != nil { + return err + } + human, err := repo.Command.RegisterHuman(ctx, resourceOwner, registerUser, externalIDP, orgMemberRoles, initCodeGenerator, phoneCodeGenerator) if err != nil { return err } @@ -519,7 +535,7 @@ func (repo *AuthRequestRepo) getLoginPolicyAndIDPProviders(ctx context.Context, if !policy.AllowExternalIDPs { return policy, nil, nil } - idpProviders, err := getLoginPolicyIDPProviders(repo.IDPProviderViewProvider, repo.IAMID, orgID, policy.IsDefault) + idpProviders, err := getLoginPolicyIDPProviders(repo.IDPProviderViewProvider, domain.IAMID, orgID, policy.IsDefault) if err != nil { return nil, nil, err } @@ -534,7 +550,7 @@ func (repo *AuthRequestRepo) fillPolicies(ctx context.Context, request *domain.A orgID = request.UserOrgID } if orgID == "" { - orgID = repo.IAMID + orgID = domain.IAMID } loginPolicy, idpProviders, err := repo.getLoginPolicyAndIDPProviders(ctx, orgID) diff --git a/internal/auth/repository/eventsourcing/eventstore/org.go b/internal/auth/repository/eventsourcing/eventstore/org.go index 678ee3be8e..613536b929 100644 --- a/internal/auth/repository/eventsourcing/eventstore/org.go +++ b/internal/auth/repository/eventsourcing/eventstore/org.go @@ -53,5 +53,5 @@ func (repo *OrgRepository) GetLoginText(ctx context.Context, orgID string) ([]*d } func (p *OrgRepository) getIAMEvents(ctx context.Context, sequence uint64) ([]*models.Event, error) { - return p.Eventstore.FilterEvents(ctx, models.NewSearchQuery().AggregateIDFilter(p.SystemDefaults.IamID).AggregateTypeFilter(iam.AggregateType)) + return p.Eventstore.FilterEvents(ctx, models.NewSearchQuery().AggregateIDFilter(domain.IAMID).AggregateTypeFilter(iam.AggregateType)) } diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go index 0afab0affe..26abb93e6d 100644 --- a/internal/auth/repository/eventsourcing/handler/handler.go +++ b/internal/auth/repository/eventsourcing/handler/handler.go @@ -32,8 +32,7 @@ func (h *handler) Eventstore() v1.Eventstore { func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es v1.Eventstore, systemDefaults sd.SystemDefaults, queries *query2.Queries) []query.Handler { return []query.Handler{ newUser( - handler{view, bulkLimit, configs.cycleDuration("User"), errorCount, es}, - systemDefaults.IamID, queries), + handler{view, bulkLimit, configs.cycleDuration("User"), errorCount, es}, queries), newUserSession( handler{view, bulkLimit, configs.cycleDuration("UserSession"), errorCount, es}), newToken( diff --git a/internal/auth/repository/eventsourcing/handler/idp_providers.go b/internal/auth/repository/eventsourcing/handler/idp_providers.go index 2465db6639..734e8dcec5 100644 --- a/internal/auth/repository/eventsourcing/handler/idp_providers.go +++ b/internal/auth/repository/eventsourcing/handler/idp_providers.go @@ -111,7 +111,7 @@ func (i *IDPProvider) processIdpProvider(event *es_models.Event) (err error) { case model.IDPConfigChanged, org_es_model.IDPConfigChanged: esConfig := new(iam_view_model.IDPConfigView) providerType := iam_model.IDPProviderTypeSystem - if event.AggregateID != i.systemDefaults.IamID { + if event.AggregateID != domain.IAMID { providerType = iam_model.IDPProviderTypeOrg } esConfig.AppendEvent(providerType, event) @@ -120,7 +120,7 @@ func (i *IDPProvider) processIdpProvider(event *es_models.Event) (err error) { return err } config := new(query2.IDP) - if event.AggregateID == i.systemDefaults.IamID { + if event.AggregateID == domain.IAMID { config, err = i.getDefaultIDPConfig(context.TODO(), esConfig.IDPConfigID) } else { config, err = i.getOrgIDPConfig(context.TODO(), event.AggregateID, esConfig.IDPConfigID) diff --git a/internal/auth/repository/eventsourcing/handler/user.go b/internal/auth/repository/eventsourcing/handler/user.go index bd058058ce..8463777200 100644 --- a/internal/auth/repository/eventsourcing/handler/user.go +++ b/internal/auth/repository/eventsourcing/handler/user.go @@ -25,19 +25,16 @@ const ( type User struct { handler - iamID string subscription *v1.Subscription queries *query2.Queries } func newUser( handler handler, - iamID string, queries *query2.Queries, ) *User { h := &User{ handler: handler, - iamID: iamID, queries: queries, } diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index 003d0fca03..d3c635d9b3 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -33,7 +33,7 @@ type EsRepository struct { eventstore.OrgRepository } -func Start(conf Config, systemDefaults sd.SystemDefaults, command *command.Commands, queries *query.Queries, dbClient *sql.DB, keyConfig *crypto.KeyConfig, assetsPrefix string) (*EsRepository, error) { +func Start(conf Config, systemDefaults sd.SystemDefaults, command *command.Commands, queries *query.Queries, dbClient *sql.DB, keyConfig *crypto.KeyConfig, assetsPrefix string, userCrypto *crypto.AESCrypto) (*EsRepository, error) { es, err := v1.Start(dbClient) if err != nil { return nil, err @@ -80,6 +80,7 @@ func Start(conf Config, systemDefaults sd.SystemDefaults, command *command.Comma AuthRequests: authReq, View: view, Eventstore: es, + UserCodeAlg: userCrypto, UserSessionViewProvider: view, UserViewProvider: view, UserCommandProvider: command, @@ -96,7 +97,6 @@ func Start(conf Config, systemDefaults sd.SystemDefaults, command *command.Comma MFAInitSkippedLifeTime: systemDefaults.VerificationLifetimes.MFAInitSkip, SecondFactorCheckLifeTime: systemDefaults.VerificationLifetimes.SecondFactorCheck, MultiFactorCheckLifeTime: systemDefaults.VerificationLifetimes.MultiFactorCheck, - IAMID: systemDefaults.IamID, }, eventstore.TokenRepo{ View: view, diff --git a/internal/authz/repository/eventsourcing/handler/handler.go b/internal/authz/repository/eventsourcing/handler/handler.go index c90af02098..8fd9014e25 100644 --- a/internal/authz/repository/eventsourcing/handler/handler.go +++ b/internal/authz/repository/eventsourcing/handler/handler.go @@ -31,8 +31,7 @@ func (h *handler) Eventstore() v1.Eventstore { func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es v1.Eventstore, systemDefaults sd.SystemDefaults) []query.Handler { return []query.Handler{ newUserGrant( - handler{view, bulkLimit, configs.cycleDuration("UserGrants"), errorCount, es}, - systemDefaults.IamID), + handler{view, bulkLimit, configs.cycleDuration("UserGrants"), errorCount, es}), newUserMembership( handler{view, bulkLimit, configs.cycleDuration("UserMemberships"), errorCount, es}), } diff --git a/internal/authz/repository/eventsourcing/handler/user_grant.go b/internal/authz/repository/eventsourcing/handler/user_grant.go index ec47e331cd..e0dd933739 100644 --- a/internal/authz/repository/eventsourcing/handler/user_grant.go +++ b/internal/authz/repository/eventsourcing/handler/user_grant.go @@ -29,18 +29,15 @@ const ( type UserGrant struct { handler - iamID string iamProjectID string subscription *v1.Subscription } func newUserGrant( handler handler, - iamID string, ) *UserGrant { h := &UserGrant{ handler: handler, - iamID: iamID, } h.subscribe() @@ -151,15 +148,15 @@ func (u *UserGrant) processIAMMember(event *es_models.Event, rolePrefix string, case iam_es_model.IAMMemberAdded, iam_es_model.IAMMemberChanged: member.SetData(event) - grant, err := u.view.UserGrantByIDs(u.iamID, u.iamProjectID, member.UserID) + grant, err := u.view.UserGrantByIDs(domain.IAMID, u.iamProjectID, member.UserID) if err != nil && !errors.IsNotFound(err) { return err } if errors.IsNotFound(err) { grant = &view_model.UserGrantView{ ID: u.iamProjectID + member.UserID, - ResourceOwner: u.iamID, - OrgName: u.iamID, + ResourceOwner: domain.IAMID, + OrgName: domain.IAMID, ProjectID: u.iamProjectID, UserID: member.UserID, RoleKeys: member.Roles, @@ -182,7 +179,7 @@ func (u *UserGrant) processIAMMember(event *es_models.Event, rolePrefix string, case iam_es_model.IAMMemberRemoved, iam_es_model.IAMMemberCascadeRemoved: member.SetData(event) - grant, err := u.view.UserGrantByIDs(u.iamID, u.iamProjectID, member.UserID) + grant, err := u.view.UserGrantByIDs(domain.IAMID, u.iamProjectID, member.UserID) if err != nil { return err } diff --git a/internal/command/command.go b/internal/command/command.go index 30ccd80621..e8f222e989 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -20,7 +20,6 @@ import ( usr_repo "github.com/caos/zitadel/internal/repository/user" usr_grant_repo "github.com/caos/zitadel/internal/repository/usergrant" "github.com/caos/zitadel/internal/static" - "github.com/caos/zitadel/internal/telemetry/tracing" webauthn_helper "github.com/caos/zitadel/internal/webauthn" ) @@ -32,17 +31,11 @@ type Commands struct { zitadelRoles []authz.RoleMapping idpConfigSecretCrypto crypto.EncryptionAlgorithm + smtpPasswordCrypto crypto.EncryptionAlgorithm userPasswordAlg crypto.HashAlgorithm - initializeUserCode crypto.Generator - emailVerificationCode crypto.Generator - phoneVerificationCode crypto.Generator - passwordVerificationCode crypto.Generator - passwordlessInitCode crypto.Generator - machineKeyAlg crypto.EncryptionAlgorithm machineKeySize int applicationKeySize int - applicationSecretGenerator crypto.Generator domainVerificationAlg crypto.EncryptionAlgorithm domainVerificationGenerator crypto.Generator domainVerificationValidator func(domain, token, verifier string, checkType http.CheckType) error @@ -60,7 +53,16 @@ type orgFeatureChecker interface { CheckOrgFeatures(ctx context.Context, orgID string, requiredFeatures ...string) error } -func StartCommands(es *eventstore.Eventstore, defaults sd.SystemDefaults, authZConfig authz.Config, staticStore static.Storage, authZRepo authz_repo.Repository, keyConfig *crypto.KeyConfig, webAuthN webauthn_helper.Config) (repo *Commands, err error) { +func StartCommands( + es *eventstore.Eventstore, + defaults sd.SystemDefaults, + authZConfig authz.Config, + staticStore static.Storage, + authZRepo authz_repo.Repository, + keyConfig *crypto.KeyConfig, + smtpPasswordEncAlg crypto.EncryptionAlgorithm, + webAuthN webauthn_helper.Config, +) (repo *Commands, err error) { repo = &Commands{ eventstore: es, static: staticStore, @@ -70,6 +72,7 @@ func StartCommands(es *eventstore.Eventstore, defaults sd.SystemDefaults, authZC keySize: defaults.KeyConfig.Size, privateKeyLifetime: defaults.KeyConfig.PrivateKeyLifetime, publicKeyLifetime: defaults.KeyConfig.PublicKeyLifetime, + smtpPasswordCrypto: smtpPasswordEncAlg, } iam_repo.RegisterEventMappers(repo.eventstore) org.RegisterEventMappers(repo.eventstore) @@ -83,17 +86,8 @@ func StartCommands(es *eventstore.Eventstore, defaults sd.SystemDefaults, authZC if err != nil { return nil, err } - userEncryptionAlgorithm, err := crypto.NewAESCrypto(defaults.UserVerificationKey) - if err != nil { - return nil, err - } - repo.initializeUserCode = crypto.NewEncryptionGenerator(defaults.SecretGenerators.InitializeUserCode, userEncryptionAlgorithm) - repo.emailVerificationCode = crypto.NewEncryptionGenerator(defaults.SecretGenerators.EmailVerificationCode, userEncryptionAlgorithm) - repo.phoneVerificationCode = crypto.NewEncryptionGenerator(defaults.SecretGenerators.PhoneVerificationCode, userEncryptionAlgorithm) - repo.passwordVerificationCode = crypto.NewEncryptionGenerator(defaults.SecretGenerators.PasswordVerificationCode, userEncryptionAlgorithm) - repo.passwordlessInitCode = crypto.NewEncryptionGenerator(defaults.SecretGenerators.PasswordlessInitCode, userEncryptionAlgorithm) + repo.userPasswordAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost) - repo.machineKeyAlg = userEncryptionAlgorithm repo.machineKeySize = int(defaults.SecretGenerators.MachineKeySize) repo.applicationKeySize = int(defaults.SecretGenerators.ApplicationKeySize) @@ -107,8 +101,6 @@ func StartCommands(es *eventstore.Eventstore, defaults sd.SystemDefaults, authZC Issuer: defaults.Multifactors.OTP.Issuer, }, } - passwordAlg := crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost) - repo.applicationSecretGenerator = crypto.NewHashGenerator(defaults.SecretGenerators.ClientSecretGenerator, passwordAlg) repo.domainVerificationAlg, err = crypto.NewAESCrypto(defaults.DomainVerification.VerificationKey) if err != nil { @@ -132,19 +124,6 @@ func StartCommands(es *eventstore.Eventstore, defaults sd.SystemDefaults, authZC return repo, nil } -func (c *Commands) getIAMWriteModel(ctx context.Context) (_ *IAMWriteModel, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - writeModel := NewIAMWriteModel() - err = c.eventstore.FilterToQueryReducer(ctx, writeModel) - if err != nil { - return nil, err - } - - return writeModel, nil -} - func AppendAndReduce(object interface { AppendEvents(...eventstore.Event) Reduce() error diff --git a/internal/command/iam.go b/internal/command/iam.go index a234647b12..4847dd6f52 100644 --- a/internal/command/iam.go +++ b/internal/command/iam.go @@ -7,6 +7,8 @@ import ( caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/telemetry/tracing" + "golang.org/x/text/language" ) //TODO: private as soon as setup uses query @@ -40,3 +42,33 @@ func (c *Commands) setIAMProject(ctx context.Context, iamAgg *eventstore.Aggrega } return iam.NewIAMProjectSetEvent(ctx, iamAgg, projectID), nil } + +func (c *Commands) SetDefaultLanguage(ctx context.Context, language language.Tag) (*domain.ObjectDetails, error) { + iamWriteModel, err := c.getIAMWriteModel(ctx) + if err != nil { + return nil, err + } + iamAgg := IAMAggregateFromWriteModel(&iamWriteModel.WriteModel) + pushedEvents, err := c.eventstore.Push(ctx, iam.NewDefaultLanguageSetEvent(ctx, iamAgg, language)) + if err != nil { + return nil, err + } + err = AppendAndReduce(iamWriteModel, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&iamWriteModel.WriteModel), nil +} + +func (c *Commands) getIAMWriteModel(ctx context.Context) (_ *IAMWriteModel, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + writeModel := NewIAMWriteModel() + err = c.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + + return writeModel, nil +} diff --git a/internal/command/iam_idp_config.go b/internal/command/iam_idp_config.go index 5fd35d5ad4..3feb5537f5 100644 --- a/internal/command/iam_idp_config.go +++ b/internal/command/iam_idp_config.go @@ -15,9 +15,8 @@ import ( func (c *Commands) AddDefaultIDPConfig(ctx context.Context, config *domain.IDPConfig) (*domain.IDPConfig, error) { if config.OIDCConfig == nil && config.JWTConfig == nil { - return nil, errors.ThrowInvalidArgument(nil, "IAM-eUpQU", "Errors.idp.config.notset") + return nil, caos_errs.ThrowInvalidArgument(nil, "IDP-s8nn3", "Errors.IDPConfig.Invalid") } - idpConfigID, err := c.idGenerator.Next() if err != nil { return nil, err diff --git a/internal/command/iam_model.go b/internal/command/iam_model.go index 855fd74e4d..9d13156a67 100644 --- a/internal/command/iam_model.go +++ b/internal/command/iam_model.go @@ -4,6 +4,7 @@ import ( "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/iam" + "golang.org/x/text/language" ) type IAMWriteModel struct { @@ -12,8 +13,9 @@ type IAMWriteModel struct { SetUpStarted domain.Step SetUpDone domain.Step - GlobalOrgID string - ProjectID string + GlobalOrgID string + ProjectID string + DefaultLanguage language.Tag } func NewIAMWriteModel() *IAMWriteModel { @@ -32,6 +34,8 @@ func (wm *IAMWriteModel) Reduce() error { wm.ProjectID = e.ProjectID case *iam.GlobalOrgSetEvent: wm.GlobalOrgID = e.OrgID + case *iam.DefaultLanguageSetEvent: + wm.DefaultLanguage = e.Language case *iam.SetupStepEvent: if e.Done { wm.SetUpDone = e.Step @@ -52,6 +56,7 @@ func (wm *IAMWriteModel) Query() *eventstore.SearchQueryBuilder { EventTypes( iam.ProjectSetEventType, iam.GlobalOrgSetEventType, + iam.DefaultLanguageSetEventType, iam.SetupStartedEventType, iam.SetupDoneEventType). Builder() diff --git a/internal/command/iam_secret_generator_model.go b/internal/command/iam_secret_generator_model.go new file mode 100644 index 0000000000..f9caf7555d --- /dev/null +++ b/internal/command/iam_secret_generator_model.go @@ -0,0 +1,140 @@ +package command + +import ( + "context" + "time" + + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/iam" +) + +type IAMSecretGeneratorConfigWriteModel struct { + eventstore.WriteModel + + GeneratorType domain.SecretGeneratorType + Length uint + Expiry time.Duration + IncludeLowerLetters bool + IncludeUpperLetters bool + IncludeDigits bool + IncludeSymbols bool + State domain.SecretGeneratorState +} + +func NewIAMSecretGeneratorConfigWriteModel(GeneratorType domain.SecretGeneratorType) *IAMSecretGeneratorConfigWriteModel { + return &IAMSecretGeneratorConfigWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: domain.IAMID, + ResourceOwner: domain.IAMID, + }, + GeneratorType: GeneratorType, + } +} + +func (wm *IAMSecretGeneratorConfigWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *iam.SecretGeneratorAddedEvent: + if wm.GeneratorType != e.GeneratorType { + continue + } + wm.Length = e.Length + wm.Expiry = e.Expiry + wm.IncludeLowerLetters = e.IncludeLowerLetters + wm.IncludeUpperLetters = e.IncludeUpperLetters + wm.IncludeDigits = e.IncludeDigits + wm.IncludeSymbols = e.IncludeDigits + wm.State = domain.SecretGeneratorStateActive + case *iam.SecretGeneratorChangedEvent: + if wm.GeneratorType != e.GeneratorType { + continue + } + if e.Length != nil { + wm.Length = *e.Length + } + if e.Expiry != nil { + wm.Expiry = *e.Expiry + } + if e.IncludeUpperLetters != nil { + wm.IncludeUpperLetters = *e.IncludeUpperLetters + } + if e.IncludeLowerLetters != nil { + wm.IncludeLowerLetters = *e.IncludeLowerLetters + } + if e.IncludeDigits != nil { + wm.IncludeDigits = *e.IncludeDigits + } + if e.IncludeSymbols != nil { + wm.IncludeSymbols = *e.IncludeSymbols + } + case *iam.SecretGeneratorRemovedEvent: + if wm.GeneratorType != e.GeneratorType { + continue + } + wm.State = domain.SecretGeneratorStateRemoved + wm.Length = 0 + wm.Expiry = 0 + wm.IncludeLowerLetters = false + wm.IncludeUpperLetters = false + wm.IncludeDigits = false + wm.IncludeSymbols = false + } + } + return wm.WriteModel.Reduce() +} + +func (wm *IAMSecretGeneratorConfigWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(iam.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + iam.SecretGeneratorAddedEventType, + iam.SecretGeneratorChangedEventType, + iam.SecretGeneratorRemovedEventType). + Builder() +} + +func (wm *IAMSecretGeneratorConfigWriteModel) NewChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + generatorType domain.SecretGeneratorType, + length uint, + expiry time.Duration, + includeLowerLetters, + includeUpperLetters, + includeDigits, + includeSymbols bool, +) (*iam.SecretGeneratorChangedEvent, bool, error) { + changes := make([]iam.SecretGeneratorChanges, 0) + var err error + + if wm.Length != length { + changes = append(changes, iam.ChangeSecretGeneratorLength(length)) + } + if wm.Expiry != expiry { + changes = append(changes, iam.ChangeSecretGeneratorExpiry(expiry)) + } + if wm.IncludeLowerLetters != includeLowerLetters { + changes = append(changes, iam.ChangeSecretGeneratorIncludeLowerLetters(includeLowerLetters)) + } + if wm.IncludeUpperLetters != includeUpperLetters { + changes = append(changes, iam.ChangeSecretGeneratorIncludeUpperLetters(includeUpperLetters)) + } + if wm.IncludeDigits != includeDigits { + changes = append(changes, iam.ChangeSecretGeneratorIncludeDigits(includeDigits)) + } + if wm.IncludeSymbols != includeSymbols { + changes = append(changes, iam.ChangeSecretGeneratorIncludeSymbols(includeSymbols)) + } + if len(changes) == 0 { + return nil, false, nil + } + changeEvent, err := iam.NewSecretGeneratorChangeEvent(ctx, aggregate, generatorType, changes) + if err != nil { + return nil, false, err + } + return changeEvent, true, nil +} diff --git a/internal/command/iam_settings.go b/internal/command/iam_settings.go new file mode 100644 index 0000000000..8a73718995 --- /dev/null +++ b/internal/command/iam_settings.go @@ -0,0 +1,118 @@ +package command + +import ( + "context" + + "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/iam" +) + +func (c *Commands) AddSecretGeneratorConfig(ctx context.Context, generatorType domain.SecretGeneratorType, config *crypto.GeneratorConfig) (*domain.ObjectDetails, error) { + if generatorType == domain.SecretGeneratorTypeUnspecified { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-0pkwf", "Errors.SecretGenerator.TypeMissing") + } + + generatorWriteModel, err := c.getSecretConfig(ctx, generatorType) + if err != nil { + return nil, err + } + if generatorWriteModel.State == domain.SecretGeneratorStateActive { + return nil, caos_errs.ThrowAlreadyExists(nil, "COMMAND-3n9ls", "Errors.SecretGenerator.AlreadyExists") + } + iamAgg := IAMAggregateFromWriteModel(&generatorWriteModel.WriteModel) + pushedEvents, err := c.eventstore.Push(ctx, iam.NewSecretGeneratorAddedEvent( + ctx, + iamAgg, + generatorType, + config.Length, + config.Expiry, + config.IncludeLowerLetters, + config.IncludeUpperLetters, + config.IncludeDigits, + config.IncludeSymbols)) + if err != nil { + return nil, err + } + err = AppendAndReduce(generatorWriteModel, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&generatorWriteModel.WriteModel), nil +} + +func (c *Commands) ChangeSecretGeneratorConfig(ctx context.Context, generatorType domain.SecretGeneratorType, config *crypto.GeneratorConfig) (*domain.ObjectDetails, error) { + if generatorType == domain.SecretGeneratorTypeUnspecified { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-33k9f", "Errors.SecretGenerator.TypeMissing") + } + + generatorWriteModel, err := c.getSecretConfig(ctx, generatorType) + if err != nil { + return nil, err + } + if generatorWriteModel.State == domain.SecretGeneratorStateUnspecified || generatorWriteModel.State == domain.SecretGeneratorStateRemoved { + return nil, caos_errs.ThrowNotFound(nil, "COMMAND-3n9ls", "Errors.SecretGenerator.NotFound") + } + iamAgg := IAMAggregateFromWriteModel(&generatorWriteModel.WriteModel) + + changedEvent, hasChanged, err := generatorWriteModel.NewChangedEvent( + ctx, + iamAgg, + generatorType, + config.Length, + config.Expiry, + config.IncludeLowerLetters, + config.IncludeUpperLetters, + config.IncludeDigits, + config.IncludeSymbols) + if err != nil { + return nil, err + } + if !hasChanged { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-m0o3f", "Errors.NoChangesFound") + } + pushedEvents, err := c.eventstore.Push(ctx, changedEvent) + if err != nil { + return nil, err + } + err = AppendAndReduce(generatorWriteModel, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&generatorWriteModel.WriteModel), nil +} + +func (c *Commands) RemoveSecretGeneratorConfig(ctx context.Context, generatorType domain.SecretGeneratorType) (*domain.ObjectDetails, error) { + if generatorType == domain.SecretGeneratorTypeUnspecified { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-2j9lw", "Errors.SecretGenerator.TypeMissing") + } + + generatorWriteModel, err := c.getSecretConfig(ctx, generatorType) + if err != nil { + return nil, err + } + if generatorWriteModel.State == domain.SecretGeneratorStateUnspecified || generatorWriteModel.State == domain.SecretGeneratorStateRemoved { + return nil, caos_errs.ThrowNotFound(nil, "COMMAND-b8les", "Errors.SecretGenerator.NotFound") + } + iamAgg := IAMAggregateFromWriteModel(&generatorWriteModel.WriteModel) + pushedEvents, err := c.eventstore.Push(ctx, iam.NewSecretGeneratorRemovedEvent(ctx, iamAgg, generatorType)) + if err != nil { + return nil, err + } + err = AppendAndReduce(generatorWriteModel, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&generatorWriteModel.WriteModel), nil +} + +func (c *Commands) getSecretConfig(ctx context.Context, generatorType domain.SecretGeneratorType) (_ *IAMSecretGeneratorConfigWriteModel, err error) { + writeModel := NewIAMSecretGeneratorConfigWriteModel(generatorType) + err = c.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + + return writeModel, nil +} diff --git a/internal/command/iam_settings_test.go b/internal/command/iam_settings_test.go new file mode 100644 index 0000000000..db25f5d6d8 --- /dev/null +++ b/internal/command/iam_settings_test.go @@ -0,0 +1,512 @@ +package command + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "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/repository" + "github.com/caos/zitadel/internal/repository/iam" +) + +func TestCommandSide_AddSecretGenerator(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + generator *crypto.GeneratorConfig + generatorType domain.SecretGeneratorType + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid empty type, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + generator: &crypto.GeneratorConfig{}, + generatorType: domain.SecretGeneratorTypeUnspecified, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "secret generator config, error already exists", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewSecretGeneratorAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecretGeneratorTypeInitCode, + 4, + time.Hour*1, + true, + true, + true, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + generator: &crypto.GeneratorConfig{ + Length: 4, + Expiry: 1 * time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + generatorType: domain.SecretGeneratorTypeInitCode, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add secret generator, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher(iam.NewSecretGeneratorAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecretGeneratorTypeInitCode, + 4, + time.Hour*1, + true, + true, + true, + true, + ), + ), + }, + uniqueConstraintsFromEventConstraint(iam.NewAddSecretGeneratorTypeUniqueConstraint(domain.SecretGeneratorTypeInitCode)), + ), + ), + }, + args: args{ + ctx: context.Background(), + generator: &crypto.GeneratorConfig{ + Length: 4, + Expiry: 1 * time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + generatorType: domain.SecretGeneratorTypeInitCode, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddSecretGeneratorConfig(tt.args.ctx, tt.args.generatorType, tt.args.generator) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangeSecretGenerator(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + generator *crypto.GeneratorConfig + generatorType domain.SecretGeneratorType + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "empty generatortype, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + generator: &crypto.GeneratorConfig{}, + generatorType: domain.SecretGeneratorTypeUnspecified, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "generator not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + generatorType: domain.SecretGeneratorTypeInitCode, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "generator removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewSecretGeneratorAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecretGeneratorTypeInitCode, + 4, + time.Hour*1, + true, + true, + true, + true, + ), + ), + eventFromEventPusher( + iam.NewSecretGeneratorRemovedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecretGeneratorTypeInitCode), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + generatorType: domain.SecretGeneratorTypeInitCode, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewSecretGeneratorAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecretGeneratorTypeInitCode, + 4, + time.Hour*1, + true, + true, + true, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + generator: &crypto.GeneratorConfig{ + Length: 4, + Expiry: 1 * time.Hour, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + generatorType: domain.SecretGeneratorTypeInitCode, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "secret generator change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewSecretGeneratorAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecretGeneratorTypeInitCode, + 4, + time.Hour*1, + true, + true, + true, + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newSecretGeneratorChangedEvent(context.Background(), + domain.SecretGeneratorTypeInitCode, + 8, + time.Hour*2, + false, + false, + false, + false), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + generator: &crypto.GeneratorConfig{ + Length: 8, + Expiry: 2 * time.Hour, + IncludeLowerLetters: false, + IncludeUpperLetters: false, + IncludeDigits: false, + IncludeSymbols: false, + }, + generatorType: domain.SecretGeneratorTypeInitCode, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeSecretGeneratorConfig(tt.args.ctx, tt.args.generatorType, tt.args.generator) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveSecretGenerator(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + generatorType domain.SecretGeneratorType + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "empty type, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + generatorType: domain.SecretGeneratorTypeUnspecified, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "generator not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + generatorType: domain.SecretGeneratorTypeInitCode, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "generator removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewSecretGeneratorAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecretGeneratorTypeInitCode, + 4, + time.Hour*1, + true, + true, + true, + true, + ), + ), + eventFromEventPusher( + iam.NewSecretGeneratorRemovedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecretGeneratorTypeInitCode), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + generatorType: domain.SecretGeneratorTypeInitCode, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "generator config remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewSecretGeneratorAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecretGeneratorTypeInitCode, + 4, + time.Hour*1, + true, + true, + true, + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewSecretGeneratorRemovedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecretGeneratorTypeInitCode), + ), + }, + uniqueConstraintsFromEventConstraint(iam.NewRemoveSecretGeneratorTypeUniqueConstraint(domain.SecretGeneratorTypeInitCode)), + ), + ), + }, + args: args{ + ctx: context.Background(), + generatorType: domain.SecretGeneratorTypeInitCode, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveSecretGeneratorConfig(tt.args.ctx, tt.args.generatorType) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newSecretGeneratorChangedEvent(ctx context.Context, generatorType domain.SecretGeneratorType, length uint, expiry time.Duration, lowerCase, upperCase, digits, symbols bool) *iam.SecretGeneratorChangedEvent { + changes := []iam.SecretGeneratorChanges{ + iam.ChangeSecretGeneratorLength(length), + iam.ChangeSecretGeneratorExpiry(expiry), + iam.ChangeSecretGeneratorIncludeLowerLetters(lowerCase), + iam.ChangeSecretGeneratorIncludeUpperLetters(upperCase), + iam.ChangeSecretGeneratorIncludeDigits(digits), + iam.ChangeSecretGeneratorIncludeSymbols(symbols), + } + event, _ := iam.NewSecretGeneratorChangeEvent(ctx, + &iam.NewAggregate().Aggregate, + generatorType, + changes, + ) + return event +} diff --git a/internal/command/iam_smtp_config_model.go b/internal/command/iam_smtp_config_model.go new file mode 100644 index 0000000000..fc778ac5ad --- /dev/null +++ b/internal/command/iam_smtp_config_model.go @@ -0,0 +1,106 @@ +package command + +import ( + "context" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/iam" +) + +type IAMSMTPConfigWriteModel struct { + eventstore.WriteModel + + SenderAddress string + SenderName string + TLS bool + Host string + User string + Password *crypto.CryptoValue + State domain.SMTPConfigState +} + +func NewIAMSMTPConfigWriteModel() *IAMSMTPConfigWriteModel { + return &IAMSMTPConfigWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: domain.IAMID, + ResourceOwner: domain.IAMID, + }, + } +} + +func (wm *IAMSMTPConfigWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *iam.SMTPConfigAddedEvent: + wm.TLS = e.TLS + wm.SenderAddress = e.SenderAddress + wm.SenderName = e.SenderName + wm.Host = e.Host + wm.User = e.User + wm.Password = e.Password + wm.State = domain.SMTPConfigStateActive + case *iam.SMTPConfigChangedEvent: + if e.TLS != nil { + wm.TLS = *e.TLS + } + if e.FromAddress != nil { + wm.SenderAddress = *e.FromAddress + } + if e.FromName != nil { + wm.SenderName = *e.FromName + } + if e.Host != nil { + wm.Host = *e.Host + } + if e.User != nil { + wm.User = *e.User + } + } + } + return wm.WriteModel.Reduce() +} + +func (wm *IAMSMTPConfigWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + AddQuery(). + AggregateTypes(iam.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes( + iam.SMTPConfigAddedEventType, + iam.SMTPConfigChangedEventType, + iam.SMTPConfigPasswordChangedEventType). + Builder() +} + +func (wm *IAMSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, tls bool, fromAddress, fromName, smtpHost, smtpUser string) (*iam.SMTPConfigChangedEvent, bool, error) { + changes := make([]iam.SMTPConfigChanges, 0) + var err error + + if wm.TLS != tls { + changes = append(changes, iam.ChangeSMTPConfigTLS(tls)) + } + if wm.SenderAddress != fromAddress { + changes = append(changes, iam.ChangeSMTPConfigFromAddress(fromAddress)) + } + if wm.SenderName != fromName { + changes = append(changes, iam.ChangeSMTPConfigFromName(fromName)) + } + if wm.Host != smtpHost { + changes = append(changes, iam.ChangeSMTPConfigSMTPHost(smtpHost)) + } + if wm.User != smtpUser { + changes = append(changes, iam.ChangeSMTPConfigSMTPUser(smtpUser)) + } + + if len(changes) == 0 { + return nil, false, nil + } + changeEvent, err := iam.NewSMTPConfigChangeEvent(ctx, aggregate, changes) + if err != nil { + return nil, false, err + } + return changeEvent, true, nil +} diff --git a/internal/command/org.go b/internal/command/org.go index 288f804a6c..0f8fe083f3 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -3,6 +3,7 @@ package command import ( "context" + "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" @@ -31,7 +32,7 @@ func (c *Commands) checkOrgExists(ctx context.Context, orgID string) error { return nil } -func (c *Commands) SetUpOrg(ctx context.Context, organisation *domain.Org, admin *domain.Human, claimedUserIDs []string, selfregistered bool) (*domain.ObjectDetails, error) { +func (c *Commands) SetUpOrg(ctx context.Context, organisation *domain.Org, admin *domain.Human, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator, claimedUserIDs []string, selfregistered bool) (*domain.ObjectDetails, error) { orgIAMPolicy, err := c.getDefaultOrgIAMPolicy(ctx) if err != nil { return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-33M9f", "Errors.IAM.OrgIAMPolicy.NotFound") @@ -40,7 +41,7 @@ func (c *Commands) SetUpOrg(ctx context.Context, organisation *domain.Org, admin if err != nil { return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-M5Fsd", "Errors.IAM.PasswordComplexity.NotFound") } - _, orgWriteModel, _, _, events, err := c.setUpOrg(ctx, organisation, admin, orgIAMPolicy, pwPolicy, claimedUserIDs, selfregistered) + _, orgWriteModel, _, _, events, err := c.setUpOrg(ctx, organisation, admin, orgIAMPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator, claimedUserIDs, selfregistered) if err != nil { return nil, err } @@ -168,6 +169,8 @@ func (c *Commands) setUpOrg( admin *domain.Human, loginPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy, + initCodeGenerator crypto.Generator, + phoneCodeGenerator crypto.Generator, claimedUserIDs []string, selfregistered bool, ) (orgAgg *eventstore.Aggregate, org *OrgWriteModel, human *HumanWriteModel, orgMember *OrgMemberWriteModel, events []eventstore.Command, err error) { @@ -178,9 +181,9 @@ func (c *Commands) setUpOrg( var userEvents []eventstore.Command if selfregistered { - userEvents, human, err = c.registerHuman(ctx, orgAgg.ID, admin, nil, loginPolicy, pwPolicy) + userEvents, human, err = c.registerHuman(ctx, orgAgg.ID, admin, nil, loginPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) } else { - userEvents, human, err = c.addHuman(ctx, orgAgg.ID, admin, loginPolicy, pwPolicy) + userEvents, human, err = c.addHuman(ctx, orgAgg.ID, admin, loginPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) } if err != nil { return nil, nil, nil, nil, nil, err diff --git a/internal/command/project_application_api.go b/internal/command/project_application_api.go index 4006f1bce7..86857793da 100644 --- a/internal/command/project_application_api.go +++ b/internal/command/project_application_api.go @@ -13,7 +13,7 @@ import ( "github.com/caos/zitadel/internal/telemetry/tracing" ) -func (c *Commands) AddAPIApplication(ctx context.Context, application *domain.APIApp, resourceOwner string) (_ *domain.APIApp, err error) { +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") } @@ -23,7 +23,7 @@ func (c *Commands) AddAPIApplication(ctx context.Context, application *domain.AP } addedApplication := NewAPIApplicationWriteModel(application.AggregateID, resourceOwner) projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) - events, stringPw, err := c.addAPIApplication(ctx, projectAgg, project, application, resourceOwner) + events, stringPw, err := c.addAPIApplication(ctx, projectAgg, project, application, resourceOwner, appSecretGenerator) if err != nil { return nil, err } @@ -41,7 +41,7 @@ func (c *Commands) AddAPIApplication(ctx context.Context, application *domain.AP return result, nil } -func (c *Commands) addAPIApplication(ctx context.Context, projectAgg *eventstore.Aggregate, proj *domain.Project, apiAppApp *domain.APIApp, resourceOwner string) (events []eventstore.Command, stringPW string, err error) { +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") } @@ -59,7 +59,7 @@ func (c *Commands) addAPIApplication(ctx context.Context, projectAgg *eventstore if err != nil { return nil, "", err } - stringPw, err = domain.SetNewClientSecretIfNeeded(apiAppApp, c.applicationSecretGenerator) + stringPw, err = domain.SetNewClientSecretIfNeeded(apiAppApp, appSecretGenerator) if err != nil { return nil, "", err } @@ -113,7 +113,7 @@ func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIA return apiWriteModelToAPIConfig(existingAPI), nil } -func (c *Commands) ChangeAPIApplicationSecret(ctx context.Context, projectID, appID, resourceOwner string) (*domain.APIApp, error) { +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") } @@ -128,7 +128,7 @@ func (c *Commands) ChangeAPIApplicationSecret(ctx context.Context, projectID, ap if !existingAPI.IsAPI() { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-aeH4", "Errors.Project.App.IsNotAPI") } - cryptoSecret, stringPW, err := domain.NewClientSecret(c.applicationSecretGenerator) + cryptoSecret, stringPW, err := domain.NewClientSecret(appSecretGenerator) if err != nil { return nil, err } diff --git a/internal/command/project_application_api_test.go b/internal/command/project_application_api_test.go index 6de1636741..507ac3512b 100644 --- a/internal/command/project_application_api_test.go +++ b/internal/command/project_application_api_test.go @@ -2,6 +2,8 @@ package command import ( "context" + "testing" + "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" @@ -12,19 +14,18 @@ import ( id_mock "github.com/caos/zitadel/internal/id/mock" "github.com/caos/zitadel/internal/repository/project" "github.com/stretchr/testify/assert" - "testing" ) func TestCommandSide_AddAPIApplication(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator - secretGenerator crypto.Generator + eventstore *eventstore.Eventstore + idGenerator id.Generator } type args struct { - ctx context.Context - apiApp *domain.APIApp - resourceOwner string + ctx context.Context + apiApp *domain.APIApp + resourceOwner string + secretGenerator crypto.Generator } type res struct { want *domain.APIApp @@ -144,8 +145,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { uniqueConstraintsFromEventConstraint(project.NewAddApplicationUniqueConstraint("app", "project1")), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"), - secretGenerator: GetMockSecretGenerator(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"), }, args: args{ ctx: context.Background(), @@ -156,7 +156,8 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { AppName: "app", AuthMethodType: domain.APIAuthMethodTypeBasic, }, - resourceOwner: "org1", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.APIApp{ @@ -238,11 +239,10 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, - applicationSecretGenerator: tt.fields.secretGenerator, + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, } - got, err := r.AddAPIApplication(tt.args.ctx, tt.args.apiApp, tt.args.resourceOwner) + got, err := r.AddAPIApplication(tt.args.ctx, tt.args.apiApp, tt.args.resourceOwner, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } @@ -472,14 +472,14 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) { func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - secretGenerator crypto.Generator + eventstore *eventstore.Eventstore } type args struct { - ctx context.Context - appID string - projectID string - resourceOwner string + ctx context.Context + appID string + projectID string + resourceOwner string + secretGenerator crypto.Generator } type res struct { want *domain.APIApp @@ -585,13 +585,13 @@ func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - projectID: "project1", - appID: "app1", - resourceOwner: "org1", + ctx: context.Background(), + projectID: "project1", + appID: "app1", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.APIApp{ @@ -612,10 +612,9 @@ func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - applicationSecretGenerator: tt.fields.secretGenerator, + eventstore: tt.fields.eventstore, } - got, err := r.ChangeAPIApplicationSecret(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) + got, err := r.ChangeAPIApplicationSecret(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/project_application_key_test.go b/internal/command/project_application_key_test.go index cc4ead3889..3643e1ebfd 100644 --- a/internal/command/project_application_key_test.go +++ b/internal/command/project_application_key_test.go @@ -2,6 +2,8 @@ package command import ( "context" + "testing" + "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" @@ -11,15 +13,13 @@ import ( id_mock "github.com/caos/zitadel/internal/id/mock" "github.com/caos/zitadel/internal/repository/project" "github.com/stretchr/testify/assert" - "testing" ) func TestCommandSide_AddAPIApplicationKey(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator - secretGenerator crypto.Generator - keySize int + eventstore *eventstore.Eventstore + idGenerator id.Generator + keySize int } type args struct { ctx context.Context @@ -126,8 +126,7 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"), - secretGenerator: GetMockSecretGenerator(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"), }, args: args{ ctx: context.Background(), @@ -173,9 +172,8 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"), - secretGenerator: GetMockSecretGenerator(t), - keySize: 10, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"), + keySize: 10, }, args: args{ ctx: context.Background(), @@ -195,10 +193,9 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, - applicationSecretGenerator: tt.fields.secretGenerator, - applicationKeySize: tt.fields.keySize, + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + applicationKeySize: tt.fields.keySize, } got, err := r.AddApplicationKey(tt.args.ctx, tt.args.key, tt.args.resourceOwner) if tt.res.err == nil { diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go index e26cd7d7df..1b07eae68f 100644 --- a/internal/command/project_application_oidc.go +++ b/internal/command/project_application_oidc.go @@ -13,7 +13,7 @@ import ( "github.com/caos/zitadel/internal/telemetry/tracing" ) -func (c *Commands) AddOIDCApplication(ctx context.Context, application *domain.OIDCApp, resourceOwner string) (_ *domain.OIDCApp, err error) { +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") } @@ -23,7 +23,7 @@ func (c *Commands) AddOIDCApplication(ctx context.Context, application *domain.O } addedApplication := NewOIDCApplicationWriteModel(application.AggregateID, resourceOwner) projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) - events, stringPw, err := c.addOIDCApplication(ctx, projectAgg, project, application, resourceOwner) + events, stringPw, err := c.addOIDCApplication(ctx, projectAgg, project, application, resourceOwner, appSecretGenerator) if err != nil { return nil, err } @@ -42,7 +42,7 @@ func (c *Commands) AddOIDCApplication(ctx context.Context, application *domain.O return result, nil } -func (c *Commands) addOIDCApplication(ctx context.Context, projectAgg *eventstore.Aggregate, proj *domain.Project, oidcApp *domain.OIDCApp, resourceOwner string) (events []eventstore.Command, stringPW string, err error) { +func (c *Commands) addOIDCApplication(ctx context.Context, projectAgg *eventstore.Aggregate, proj *domain.Project, oidcApp *domain.OIDCApp, resourceOwner string, appSecretGenerator crypto.Generator) (events []eventstore.Command, stringPW string, err error) { if oidcApp.AppName == "" || !oidcApp.IsValid() { return nil, "", caos_errs.ThrowInvalidArgument(nil, "PROJECT-1n8df", "Errors.Application.Invalid") } @@ -60,7 +60,7 @@ func (c *Commands) addOIDCApplication(ctx context.Context, projectAgg *eventstor if err != nil { return nil, "", err } - stringPw, err = domain.SetNewClientSecretIfNeeded(oidcApp, c.applicationSecretGenerator) + stringPw, err = domain.SetNewClientSecretIfNeeded(oidcApp, appSecretGenerator) if err != nil { return nil, "", err } @@ -142,7 +142,7 @@ func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCA return result, nil } -func (c *Commands) ChangeOIDCApplicationSecret(ctx context.Context, projectID, appID, resourceOwner string) (*domain.OIDCApp, error) { +func (c *Commands) ChangeOIDCApplicationSecret(ctx context.Context, projectID, appID, resourceOwner string, appSecretGenerator crypto.Generator) (*domain.OIDCApp, error) { if projectID == "" || appID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-99i83", "Errors.IDMissing") } @@ -157,7 +157,7 @@ func (c *Commands) ChangeOIDCApplicationSecret(ctx context.Context, projectID, a if !existingOIDC.IsOIDC() { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Ghrh3", "Errors.Project.App.IsNotOIDC") } - cryptoSecret, stringPW, err := domain.NewClientSecret(c.applicationSecretGenerator) + cryptoSecret, stringPW, err := domain.NewClientSecret(appSecretGenerator) if err != nil { return nil, err } diff --git a/internal/command/project_application_oidc_test.go b/internal/command/project_application_oidc_test.go index d690bbe8da..7a34d0645e 100644 --- a/internal/command/project_application_oidc_test.go +++ b/internal/command/project_application_oidc_test.go @@ -20,14 +20,14 @@ import ( func TestCommandSide_AddOIDCApplication(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator - secretGenerator crypto.Generator + eventstore *eventstore.Eventstore + idGenerator id.Generator } type args struct { - ctx context.Context - oidcApp *domain.OIDCApp - resourceOwner string + ctx context.Context + oidcApp *domain.OIDCApp + resourceOwner string + secretGenerator crypto.Generator } type res struct { want *domain.OIDCApp @@ -160,8 +160,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { uniqueConstraintsFromEventConstraint(project.NewAddApplicationUniqueConstraint("app", "project1")), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"), - secretGenerator: GetMockSecretGenerator(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"), }, args: args{ ctx: context.Background(), @@ -185,7 +184,8 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { ClockSkew: time.Second * 1, AdditionalOrigins: []string{"https://sub.test.ch"}, }, - resourceOwner: "org1", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.OIDCApp{ @@ -220,11 +220,10 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, - applicationSecretGenerator: tt.fields.secretGenerator, + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, } - got, err := r.AddOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner) + got, err := r.AddOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } @@ -549,14 +548,14 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - secretGenerator crypto.Generator + eventstore *eventstore.Eventstore } type args struct { - ctx context.Context - appID string - projectID string - resourceOwner string + ctx context.Context + appID string + projectID string + resourceOwner string + secretGenerator crypto.Generator } type res struct { want *domain.OIDCApp @@ -675,13 +674,13 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - projectID: "project1", - appID: "app1", - resourceOwner: "org1", + ctx: context.Background(), + projectID: "project1", + appID: "app1", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.OIDCApp{ @@ -715,10 +714,9 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - applicationSecretGenerator: tt.fields.secretGenerator, + eventstore: tt.fields.eventstore, } - got, err := r.ChangeOIDCApplicationSecret(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) + got, err := r.ChangeOIDCApplicationSecret(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/setup_step1.go b/internal/command/setup_step1.go index 78bb0a041e..e05db49f94 100644 --- a/internal/command/setup_step1.go +++ b/internal/command/setup_step1.go @@ -3,6 +3,7 @@ package command import ( "context" + "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/v1/models" @@ -134,7 +135,10 @@ func (c *Commands) SetupStep1(ctx context.Context, step1 *Step1) error { EmailAddress: organisation.Owner.Email, IsEmailVerified: true, }, - }, orgIAMPolicy, pwPolicy, nil, false) + }, orgIAMPolicy, pwPolicy, + nil, //TODO: Code Generator missing! Should be setuped in step1 create iam + nil, //TODO: Code Generator missing! Should be setuped in step1 create iam + nil, false) if err != nil { return err } @@ -180,14 +184,16 @@ func (c *Commands) SetupStep1(ctx context.Context, step1 *Step1) error { } //create applications for _, app := range proj.OIDCApps { - applicationEvents, err := setUpOIDCApplication(ctx, c, projectWriteModel, project, app, orgAgg.ID) + //TODO: Add Secret Generator + applicationEvents, err := setUpOIDCApplication(ctx, c, projectWriteModel, project, app, orgAgg.ID, nil) if err != nil { return err } events = append(events, applicationEvents...) } for _, app := range proj.APIs { - applicationEvents, err := setUpAPI(ctx, c, projectWriteModel, project, app, orgAgg.ID) + //TODO: Add Secret Generator + applicationEvents, err := setUpAPI(ctx, c, projectWriteModel, project, app, orgAgg.ID, nil) if err != nil { return err } @@ -205,7 +211,7 @@ func (c *Commands) SetupStep1(ctx context.Context, step1 *Step1) error { return nil } -func setUpOIDCApplication(ctx context.Context, r *Commands, projectWriteModel *ProjectWriteModel, project *domain.Project, oidcApp OIDCApp, resourceOwner string) ([]eventstore.Command, error) { +func setUpOIDCApplication(ctx context.Context, r *Commands, projectWriteModel *ProjectWriteModel, project *domain.Project, oidcApp OIDCApp, resourceOwner string, appSecretGenerator crypto.Generator) ([]eventstore.Command, error) { app := &domain.OIDCApp{ ObjectRoot: models.ObjectRoot{ AggregateID: projectWriteModel.AggregateID, @@ -220,7 +226,7 @@ func setUpOIDCApplication(ctx context.Context, r *Commands, projectWriteModel *P } projectAgg := ProjectAggregateFromWriteModel(&projectWriteModel.WriteModel) - events, _, err := r.addOIDCApplication(ctx, projectAgg, project, app, resourceOwner) + events, _, err := r.addOIDCApplication(ctx, projectAgg, project, app, resourceOwner, appSecretGenerator) if err != nil { return nil, err } @@ -228,7 +234,7 @@ func setUpOIDCApplication(ctx context.Context, r *Commands, projectWriteModel *P return events, nil } -func setUpAPI(ctx context.Context, r *Commands, projectWriteModel *ProjectWriteModel, project *domain.Project, apiApp API, resourceOwner string) ([]eventstore.Command, error) { +func setUpAPI(ctx context.Context, r *Commands, projectWriteModel *ProjectWriteModel, project *domain.Project, apiApp API, resourceOwner string, appSecretGenerator crypto.Generator) ([]eventstore.Command, error) { app := &domain.APIApp{ ObjectRoot: models.ObjectRoot{ AggregateID: projectWriteModel.AggregateID, @@ -238,7 +244,7 @@ func setUpAPI(ctx context.Context, r *Commands, projectWriteModel *ProjectWriteM } projectAgg := ProjectAggregateFromWriteModel(&projectWriteModel.WriteModel) - events, _, err := r.addAPIApplication(ctx, projectAgg, project, app, resourceOwner) + events, _, err := r.addAPIApplication(ctx, projectAgg, project, app, resourceOwner, appSecretGenerator) if err != nil { return nil, err } diff --git a/internal/command/smtp.go b/internal/command/smtp.go new file mode 100644 index 0000000000..173197e19f --- /dev/null +++ b/internal/command/smtp.go @@ -0,0 +1,119 @@ +package command + +import ( + "context" + + "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/notification/channels/smtp" + "github.com/caos/zitadel/internal/repository/iam" +) + +func (c *Commands) AddSMTPConfig(ctx context.Context, config *smtp.EmailConfig) (*domain.ObjectDetails, error) { + smtpConfigWriteModel, err := c.getSMTPConfig(ctx) + if err != nil { + return nil, err + } + if smtpConfigWriteModel.State == domain.SMTPConfigStateActive { + return nil, caos_errs.ThrowAlreadyExists(nil, "COMMAND-en9lw", "Errors.SMTPConfig.AlreadyExists") + } + var smtpPassword *crypto.CryptoValue + if config.SMTP.Password != "" { + smtpPassword, err = crypto.Encrypt([]byte(config.SMTP.Password), c.smtpPasswordCrypto) + if err != nil { + return nil, err + } + } + + iamAgg := IAMAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel) + pushedEvents, err := c.eventstore.Push(ctx, iam.NewSMTPConfigAddedEvent( + ctx, + iamAgg, + config.Tls, + config.From, + config.FromName, + config.SMTP.Host, + config.SMTP.User, + smtpPassword)) + if err != nil { + return nil, err + } + err = AppendAndReduce(smtpConfigWriteModel, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel), nil +} + +func (c *Commands) ChangeSMTPConfig(ctx context.Context, config *smtp.EmailConfig) (*domain.ObjectDetails, error) { + smtpConfigWriteModel, err := c.getSMTPConfig(ctx) + if err != nil { + return nil, err + } + if smtpConfigWriteModel.State == domain.SMTPConfigStateUnspecified { + return nil, caos_errs.ThrowNotFound(nil, "COMMAND-3n9ls", "Errors.SMTPConfig.NotFound") + } + iamAgg := IAMAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel) + + changedEvent, hasChanged, err := smtpConfigWriteModel.NewChangedEvent( + ctx, + iamAgg, + config.Tls, + config.From, + config.FromName, + config.SMTP.Host, + config.SMTP.User) + if err != nil { + return nil, err + } + if !hasChanged { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-m0o3f", "Errors.NoChangesFound") + } + pushedEvents, err := c.eventstore.Push(ctx, changedEvent) + if err != nil { + return nil, err + } + err = AppendAndReduce(smtpConfigWriteModel, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel), nil +} + +func (c *Commands) ChangeSMTPConfigPassword(ctx context.Context, password string) (*domain.ObjectDetails, error) { + smtpConfigWriteModel, err := c.getSMTPConfig(ctx) + if err != nil { + return nil, err + } + if smtpConfigWriteModel.State == domain.SMTPConfigStateUnspecified { + return nil, caos_errs.ThrowNotFound(nil, "COMMAND-3n9ls", "Errors.SMTPConfig.NotFound") + } + iamAgg := IAMAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel) + newPW, err := crypto.Encrypt([]byte(password), c.smtpPasswordCrypto) + if err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, iam.NewSMTPConfigPasswordChangedEvent( + ctx, + iamAgg, + newPW)) + if err != nil { + return nil, err + } + err = AppendAndReduce(smtpConfigWriteModel, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel), nil +} + +func (c *Commands) getSMTPConfig(ctx context.Context) (_ *IAMSMTPConfigWriteModel, err error) { + writeModel := NewIAMSMTPConfigWriteModel() + err = c.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + + return writeModel, nil +} diff --git a/internal/command/smtp_test.go b/internal/command/smtp_test.go new file mode 100644 index 0000000000..c297c831e5 --- /dev/null +++ b/internal/command/smtp_test.go @@ -0,0 +1,398 @@ +package command + +import ( + "context" + "testing" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/notification/channels/smtp" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "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/repository" + "github.com/caos/zitadel/internal/repository/iam" +) + +func TestCommandSide_AddSMTPConfig(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + alg crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + smtp *smtp.EmailConfig + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "smtp config, error already exists", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewSMTPConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + true, + "from", + "name", + "host", + "user", + &crypto.CryptoValue{}, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + smtp: &smtp.EmailConfig{ + Tls: true, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add smtp config, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher(iam.NewSMTPConfigAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + true, + "from", + "name", + "host", + "user", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("password"), + }, + ), + ), + }, + ), + ), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + smtp: &smtp.EmailConfig{ + Tls: true, + From: "from", + FromName: "name", + SMTP: smtp.SMTP{ + Host: "host", + User: "user", + Password: "password", + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + smtpPasswordCrypto: tt.fields.alg, + } + got, err := r.AddSMTPConfig(tt.args.ctx, tt.args.smtp) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangeSMTPConfig(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + smtp *smtp.EmailConfig + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "smtp not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + smtp: &smtp.EmailConfig{}, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewSMTPConfigAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + true, + "from", + "name", + "host", + "user", + &crypto.CryptoValue{}, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + smtp: &smtp.EmailConfig{ + Tls: true, + From: "from", + FromName: "name", + SMTP: smtp.SMTP{ + Host: "host", + User: "user", + }, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "smtp config change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewSMTPConfigAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + true, + "from", + "name", + "host", + "user", + &crypto.CryptoValue{}, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newSMTPConfigChangedEvent( + context.Background(), + false, + "from2", + "name2", + "host2", + "user2", + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + smtp: &smtp.EmailConfig{ + Tls: false, + From: "from2", + FromName: "name2", + SMTP: smtp.SMTP{ + Host: "host2", + User: "user2", + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeSMTPConfig(tt.args.ctx, tt.args.smtp) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangeSMTPConfigPassword(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + alg crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + password string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "smtp config, error not found", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + password: "", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "change smtp config password, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewSMTPConfigAddedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + true, + "from", + "name", + "host", + "user", + &crypto.CryptoValue{}, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(iam.NewSMTPConfigPasswordChangedEvent( + context.Background(), + &iam.NewAggregate().Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("password"), + }, + ), + ), + }, + ), + ), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + password: "password", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + smtpPasswordCrypto: tt.fields.alg, + } + got, err := r.ChangeSMTPConfigPassword(tt.args.ctx, tt.args.password) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newSMTPConfigChangedEvent(ctx context.Context, tls bool, fromAddress, fromName, host, user string) *iam.SMTPConfigChangedEvent { + changes := []iam.SMTPConfigChanges{ + iam.ChangeSMTPConfigTLS(tls), + iam.ChangeSMTPConfigFromAddress(fromAddress), + iam.ChangeSMTPConfigFromName(fromName), + iam.ChangeSMTPConfigSMTPHost(host), + iam.ChangeSMTPConfigSMTPUser(user), + } + event, _ := iam.NewSMTPConfigChangeEvent(ctx, + &iam.NewAggregate().Aggregate, + changes, + ) + return event +} diff --git a/internal/command/user_human.go b/internal/command/user_human.go index c65d3a95c4..e93fe3183b 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -4,6 +4,7 @@ import ( "context" "strings" + "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/domain" @@ -23,7 +24,7 @@ func (c *Commands) getHuman(ctx context.Context, userID, resourceowner string) ( return writeModelToHuman(human), nil } -func (c *Commands) AddHuman(ctx context.Context, orgID string, human *domain.Human) (*domain.Human, error) { +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") } @@ -35,7 +36,7 @@ func (c *Commands) AddHuman(ctx context.Context, orgID string, human *domain.Hum if err != nil { return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-M5Fsd", "Errors.Org.PasswordComplexity.NotFound") } - events, addedHuman, err := c.addHuman(ctx, orgID, human, orgIAMPolicy, pwPolicy) + events, addedHuman, err := c.addHuman(ctx, orgID, human, orgIAMPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) if err != nil { return nil, err } @@ -52,7 +53,7 @@ func (c *Commands) AddHuman(ctx context.Context, orgID string, human *domain.Hum return writeModelToHuman(addedHuman), nil } -func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) { +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") } @@ -64,7 +65,7 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. if err != nil { return nil, nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-4N8gs", "Errors.Org.PasswordComplexity.NotFound") } - events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, orgIAMPolicy, pwPolicy) + events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, orgIAMPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) if err != nil { return nil, nil, err } @@ -88,27 +89,27 @@ 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, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy) ([]eventstore.Command, *HumanWriteModel, error) { +func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Human, orgIAMPolicy *domain.OrgIAMPolicy, 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, orgIAMPolicy, pwPolicy) + return c.createHuman(ctx, orgID, human, nil, false, false, orgIAMPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) } -func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { +func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, orgIAMPolicy *domain.OrgIAMPolicy, 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, orgIAMPolicy, pwPolicy) + events, humanWriteModel, err = c.createHuman(ctx, orgID, human, nil, false, passwordless, orgIAMPolicy, 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) + codeEvent, passwordlessCodeWriteModel, code, err = c.humanAddPasswordlessInitCode(ctx, human.AggregateID, orgID, true, passwordlessCodeGenerator) if err != nil { return nil, nil, nil, "", err } @@ -117,7 +118,7 @@ func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain. return events, humanWriteModel, passwordlessCodeWriteModel, code, nil } -func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, orgMemberRoles []string) (*domain.Human, error) { +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") } @@ -136,7 +137,7 @@ func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domai if !loginPolicy.AllowRegister { return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-SAbr3", "Errors.Org.LoginPolicy.RegistrationNotAllowed") } - userEvents, registeredHuman, err := c.registerHuman(ctx, orgID, human, link, orgIAMPolicy, pwPolicy) + userEvents, registeredHuman, err := c.registerHuman(ctx, orgID, human, link, orgIAMPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) if err != nil { return nil, err } @@ -170,7 +171,7 @@ func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domai return writeModelToHuman(registeredHuman), nil } -func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy) ([]eventstore.Command, *HumanWriteModel, error) { +func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) { if human != nil && human.Username == "" { human.Username = human.EmailAddress } @@ -180,10 +181,10 @@ func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domai if human.Password != nil && human.SecretString != "" { human.ChangeRequired = false } - return c.createHuman(ctx, orgID, human, link, true, false, orgIAMPolicy, pwPolicy) + return c.createHuman(ctx, orgID, human, link, true, false, orgIAMPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) } -func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, selfregister, passwordless bool, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy) ([]eventstore.Command, *HumanWriteModel, error) { +func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, selfregister, passwordless bool, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) { if err := human.CheckOrgIAMPolicy(orgIAMPolicy); err != nil { return nil, nil, err } @@ -232,7 +233,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. } if human.IsInitialState(passwordless, link != nil) { - initCode, err := domain.NewInitUserCode(c.initializeUserCode) + initCode, err := domain.NewInitUserCode(initCodeGenerator) if err != nil { return nil, nil, err } @@ -244,7 +245,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. } if human.Phone != nil && human.PhoneNumber != "" && !human.IsPhoneVerified { - phoneCode, err := domain.NewPhoneCode(c.phoneVerificationCode) + phoneCode, err := domain.NewPhoneCode(phoneCodeGenerator) if err != nil { return nil, nil, err } diff --git a/internal/command/user_human_email.go b/internal/command/user_human_email.go index 5a4863ac0b..a786310627 100644 --- a/internal/command/user_human_email.go +++ b/internal/command/user_human_email.go @@ -12,7 +12,7 @@ import ( "github.com/caos/zitadel/internal/telemetry/tracing" ) -func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email) (*domain.Email, error) { +func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email, emailCodeGenerator crypto.Generator) (*domain.Email, error) { if !email.IsValid() || email.AggregateID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M9sf", "Errors.Email.Invalid") } @@ -38,7 +38,7 @@ func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email) (* if email.IsEmailVerified { events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg)) } else { - emailCode, err := domain.NewEmailCode(c.emailVerificationCode) + emailCode, err := domain.NewEmailCode(emailCodeGenerator) if err != nil { return nil, err } @@ -56,7 +56,7 @@ func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email) (* return writeModelToEmail(existingEmail), nil } -func (c *Commands) VerifyHumanEmail(ctx context.Context, userID, code, resourceowner string) (*domain.ObjectDetails, error) { +func (c *Commands) VerifyHumanEmail(ctx context.Context, userID, code, resourceowner string, emailCodeGenerator crypto.Generator) (*domain.ObjectDetails, error) { if userID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing") } @@ -73,7 +73,7 @@ func (c *Commands) VerifyHumanEmail(ctx context.Context, userID, code, resourceo } userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel) - err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, c.emailVerificationCode) + err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, emailCodeGenerator) if err == nil { pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanEmailVerifiedEvent(ctx, userAgg)) if err != nil { @@ -91,7 +91,7 @@ func (c *Commands) VerifyHumanEmail(ctx context.Context, userID, code, resourceo return nil, caos_errs.ThrowInvalidArgument(err, "COMMAND-Gdsgs", "Errors.User.Code.Invalid") } -func (c *Commands) CreateHumanEmailVerificationCode(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) { +func (c *Commands) CreateHumanEmailVerificationCode(ctx context.Context, userID, resourceOwner string, emailCodeGenerator crypto.Generator) (*domain.ObjectDetails, error) { if userID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing") } @@ -110,7 +110,7 @@ func (c *Commands) CreateHumanEmailVerificationCode(ctx context.Context, userID, return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M9ds", "Errors.User.Email.AlreadyVerified") } userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel) - emailCode, err := domain.NewEmailCode(c.emailVerificationCode) + emailCode, err := domain.NewEmailCode(emailCodeGenerator) if err != nil { return nil, err } diff --git a/internal/command/user_human_email_test.go b/internal/command/user_human_email_test.go index eadb20d040..7d9cac2d02 100644 --- a/internal/command/user_human_email_test.go +++ b/internal/command/user_human_email_test.go @@ -19,13 +19,13 @@ import ( func TestCommandSide_ChangeHumanEmail(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - secretGenerator crypto.Generator + eventstore *eventstore.Eventstore } type args struct { - ctx context.Context - email *domain.Email - resourceOwner string + ctx context.Context + email *domain.Email + resourceOwner string + secretGenerator crypto.Generator } type res struct { want *domain.Email @@ -263,7 +263,6 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ ctx: context.Background(), @@ -273,7 +272,8 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) { }, EmailAddress: "email-changed@test.ch", }, - resourceOwner: "org1", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.Email{ @@ -289,10 +289,9 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - emailVerificationCode: tt.fields.secretGenerator, + eventstore: tt.fields.eventstore, } - got, err := r.ChangeHumanEmail(tt.args.ctx, tt.args.email) + got, err := r.ChangeHumanEmail(tt.args.ctx, tt.args.email, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } @@ -308,14 +307,14 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) { func TestCommandSide_VerifyHumanEmail(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - secretGenerator crypto.Generator + eventstore *eventstore.Eventstore } type args struct { - ctx context.Context - userID string - code string - resourceOwner string + ctx context.Context + userID string + code string + resourceOwner string + secretGenerator crypto.Generator } type res struct { want *domain.ObjectDetails @@ -453,13 +452,13 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - userID: "user1", - code: "test", - resourceOwner: "org1", + ctx: context.Background(), + userID: "user1", + code: "test", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ err: caos_errs.IsErrorInvalidArgument, @@ -508,13 +507,13 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - userID: "user1", - code: "a", - resourceOwner: "org1", + ctx: context.Background(), + userID: "user1", + code: "a", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.ObjectDetails{ @@ -526,10 +525,9 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - emailVerificationCode: tt.fields.secretGenerator, + eventstore: tt.fields.eventstore, } - got, err := r.VerifyHumanEmail(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner) + got, err := r.VerifyHumanEmail(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } @@ -545,13 +543,13 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) { func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - secretGenerator crypto.Generator + eventstore *eventstore.Eventstore } type args struct { - ctx context.Context - userID string - resourceOwner string + ctx context.Context + userID string + resourceOwner string + secretGenerator crypto.Generator } type res struct { want *domain.ObjectDetails @@ -719,12 +717,12 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.ObjectDetails{ @@ -736,10 +734,9 @@ func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - emailVerificationCode: tt.fields.secretGenerator, + eventstore: tt.fields.eventstore, } - got, err := r.CreateHumanEmailVerificationCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) + got, err := r.CreateHumanEmailVerificationCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_human_init.go b/internal/command/user_human_init.go index f4669fde5d..8a72b5c7d4 100644 --- a/internal/command/user_human_init.go +++ b/internal/command/user_human_init.go @@ -12,7 +12,7 @@ import ( ) //ResendInitialMail resend inital mail and changes email if provided -func (c *Commands) ResendInitialMail(ctx context.Context, userID, email, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) { +func (c *Commands) ResendInitialMail(ctx context.Context, userID, email, resourceOwner string, initCodeGenerator crypto.Generator) (objectDetails *domain.ObjectDetails, err error) { if userID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing") } @@ -33,7 +33,7 @@ func (c *Commands) ResendInitialMail(ctx context.Context, userID, email, resourc changedEvent, _ := existingCode.NewChangedEvent(ctx, userAgg, email) events = append(events, changedEvent) } - initCode, err := domain.NewInitUserCode(c.initializeUserCode) + initCode, err := domain.NewInitUserCode(initCodeGenerator) if err != nil { return nil, err } @@ -49,7 +49,7 @@ func (c *Commands) ResendInitialMail(ctx context.Context, userID, email, resourc return writeModelToObjectDetails(&existingCode.WriteModel), nil } -func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwner, code, passwordString string) error { +func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwner, code, passwordString string, initCodeGenerator crypto.Generator) error { if userID == "" { return caos_errs.ThrowInvalidArgument(nil, "COMMAND-mkM9f", "Errors.User.UserIDMissing") } @@ -66,7 +66,7 @@ func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwne } userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel) - err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, c.initializeUserCode) + err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, initCodeGenerator) if err != nil { _, err = c.eventstore.Push(ctx, user.NewHumanInitializedCheckFailedEvent(ctx, userAgg)) logging.LogWithFields("COMMAND-Dg2z5", "userID", userAgg.ID).OnError(err).Error("NewHumanInitializedCheckFailedEvent push failed") diff --git a/internal/command/user_human_init_test.go b/internal/command/user_human_init_test.go index f0089fbf5a..bcac84c32c 100644 --- a/internal/command/user_human_init_test.go +++ b/internal/command/user_human_init_test.go @@ -20,14 +20,14 @@ import ( func TestCommandSide_ResendInitialMail(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - secretGenerator crypto.Generator + eventstore *eventstore.Eventstore } type args struct { - ctx context.Context - userID string - email string - resourceOwner string + ctx context.Context + userID string + email string + resourceOwner string + secretGenerator crypto.Generator } type res struct { want *domain.ObjectDetails @@ -150,13 +150,13 @@ func TestCommandSide_ResendInitialMail(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - email: "email@test.ch", + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + email: "email@test.ch", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.ObjectDetails{ @@ -208,12 +208,12 @@ func TestCommandSide_ResendInitialMail(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.ObjectDetails{ @@ -269,13 +269,13 @@ func TestCommandSide_ResendInitialMail(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - email: "email2@test.ch", + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + email: "email2@test.ch", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.ObjectDetails{ @@ -287,10 +287,9 @@ func TestCommandSide_ResendInitialMail(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - initializeUserCode: tt.fields.secretGenerator, + eventstore: tt.fields.eventstore, } - got, err := r.ResendInitialMail(tt.args.ctx, tt.args.userID, tt.args.email, tt.args.resourceOwner) + got, err := r.ResendInitialMail(tt.args.ctx, tt.args.userID, tt.args.email, tt.args.resourceOwner, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } @@ -307,15 +306,15 @@ func TestCommandSide_ResendInitialMail(t *testing.T) { func TestCommandSide_VerifyInitCode(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore - secretGenerator crypto.Generator userPasswordAlg crypto.HashAlgorithm } type args struct { - ctx context.Context - userID string - code string - resourceOwner string - password string + ctx context.Context + userID string + code string + resourceOwner string + password string + secretGenerator crypto.Generator } type res struct { want *domain.ObjectDetails @@ -453,13 +452,13 @@ func TestCommandSide_VerifyInitCode(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - userID: "user1", - code: "test", - resourceOwner: "org1", + ctx: context.Background(), + userID: "user1", + code: "test", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ err: caos_errs.IsErrorInvalidArgument, @@ -513,13 +512,13 @@ func TestCommandSide_VerifyInitCode(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - userID: "user1", - code: "a", - resourceOwner: "org1", + ctx: context.Background(), + userID: "user1", + code: "a", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.ObjectDetails{ @@ -596,15 +595,15 @@ func TestCommandSide_VerifyInitCode(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ - ctx: context.Background(), - userID: "user1", - code: "a", - resourceOwner: "org1", - password: "password", + ctx: context.Background(), + userID: "user1", + code: "a", + resourceOwner: "org1", + password: "password", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.ObjectDetails{ @@ -616,11 +615,10 @@ func TestCommandSide_VerifyInitCode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - initializeUserCode: tt.fields.secretGenerator, - userPasswordAlg: tt.fields.userPasswordAlg, + eventstore: tt.fields.eventstore, + userPasswordAlg: tt.fields.userPasswordAlg, } - err := r.HumanVerifyInitCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.code, tt.args.password) + err := r.HumanVerifyInitCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.code, tt.args.password, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_human_password.go b/internal/command/user_human_password.go index 5a12113f65..0d07eae6dd 100644 --- a/internal/command/user_human_password.go +++ b/internal/command/user_human_password.go @@ -46,7 +46,7 @@ func (c *Commands) SetPassword(ctx context.Context, orgID, userID, passwordStrin return writeModelToObjectDetails(&existingPassword.WriteModel), nil } -func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, passwordString, userAgentID string) (err error) { +func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, passwordString, userAgentID string, passwordVerificationCode crypto.Generator) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -65,7 +65,7 @@ func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound") } - err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, c.passwordVerificationCode) + err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, passwordVerificationCode) if err != nil { return err } @@ -148,7 +148,7 @@ func (c *Commands) changePassword(ctx context.Context, userAgentID string, passw return user.NewHumanPasswordChangedEvent(ctx, userAgg, password.SecretCrypto, password.ChangeRequired, userAgentID), nil } -func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner string, notifyType domain.NotificationType) (objectDetails *domain.ObjectDetails, err error) { +func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner string, notifyType domain.NotificationType, passwordVerificationCode crypto.Generator) (objectDetails *domain.ObjectDetails, err error) { if userID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-M00oL", "Errors.User.UserIDMissing") } @@ -164,7 +164,7 @@ func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9sd", "Errors.User.NotInitialised") } userAgg := UserAggregateFromWriteModel(&existingHuman.WriteModel) - passwordCode, err := domain.NewPasswordCode(c.passwordVerificationCode) + passwordCode, err := domain.NewPasswordCode(passwordVerificationCode) if err != nil { return nil, err } diff --git a/internal/command/user_human_password_test.go b/internal/command/user_human_password_test.go index a33ee1a2c8..fa95a9c3ec 100644 --- a/internal/command/user_human_password_test.go +++ b/internal/command/user_human_password_test.go @@ -239,15 +239,15 @@ func TestCommandSide_SetPassword(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore userPasswordAlg crypto.HashAlgorithm - secretGenerator crypto.Generator } type args struct { - ctx context.Context - userID string - code string - resourceOwner string - password string - agentID string + ctx context.Context + userID string + code string + resourceOwner string + password string + agentID string + secretGenerator crypto.Generator } type res struct { want *domain.ObjectDetails @@ -377,14 +377,14 @@ func TestCommandSide_SetPassword(t *testing.T) { ), ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - userID: "user1", - code: "test", - resourceOwner: "org1", - password: "password", + ctx: context.Background(), + userID: "user1", + code: "test", + resourceOwner: "org1", + password: "password", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ err: caos_errs.IsPreconditionFailed, @@ -459,15 +459,15 @@ func TestCommandSide_SetPassword(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - password: "password", - code: "a", + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + password: "password", + code: "a", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.ObjectDetails{ @@ -479,11 +479,10 @@ func TestCommandSide_SetPassword(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - userPasswordAlg: tt.fields.userPasswordAlg, - passwordVerificationCode: tt.fields.secretGenerator, + eventstore: tt.fields.eventstore, + userPasswordAlg: tt.fields.userPasswordAlg, } - err := r.SetPasswordWithVerifyCode(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password, tt.args.agentID) + err := r.SetPasswordWithVerifyCode(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password, tt.args.agentID, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } @@ -778,14 +777,14 @@ func TestCommandSide_ChangePassword(t *testing.T) { func TestCommandSide_RequestSetPassword(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - secretGenerator crypto.Generator + eventstore *eventstore.Eventstore } type args struct { - ctx context.Context - userID string - resourceOwner string - notifyType domain.NotificationType + ctx context.Context + userID string + resourceOwner string + notifyType domain.NotificationType + secretGenerator crypto.Generator } type res struct { want *domain.ObjectDetails @@ -925,12 +924,12 @@ func TestCommandSide_RequestSetPassword(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.ObjectDetails{ @@ -942,10 +941,9 @@ func TestCommandSide_RequestSetPassword(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - passwordVerificationCode: tt.fields.secretGenerator, + eventstore: tt.fields.eventstore, } - got, err := r.RequestSetPassword(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.notifyType) + got, err := r.RequestSetPassword(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.notifyType, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_human_phone.go b/internal/command/user_human_phone.go index abaa03742f..42c02c2de1 100644 --- a/internal/command/user_human_phone.go +++ b/internal/command/user_human_phone.go @@ -13,7 +13,7 @@ import ( "github.com/caos/zitadel/internal/telemetry/tracing" ) -func (c *Commands) ChangeHumanPhone(ctx context.Context, phone *domain.Phone, resourceOwner string) (*domain.Phone, error) { +func (c *Commands) ChangeHumanPhone(ctx context.Context, phone *domain.Phone, resourceOwner string, phoneCodeGenerator crypto.Generator) (*domain.Phone, error) { if !phone.IsValid() { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-6M0ds", "Errors.Phone.Invalid") } @@ -36,7 +36,7 @@ func (c *Commands) ChangeHumanPhone(ctx context.Context, phone *domain.Phone, re if phone.IsPhoneVerified { events = append(events, user.NewHumanPhoneVerifiedEvent(ctx, userAgg)) } else { - phoneCode, err := domain.NewPhoneCode(c.phoneVerificationCode) + phoneCode, err := domain.NewPhoneCode(phoneCodeGenerator) if err != nil { return nil, err } @@ -55,7 +55,7 @@ func (c *Commands) ChangeHumanPhone(ctx context.Context, phone *domain.Phone, re return writeModelToPhone(existingPhone), nil } -func (c *Commands) VerifyHumanPhone(ctx context.Context, userID, code, resourceowner string) (*domain.ObjectDetails, error) { +func (c *Commands) VerifyHumanPhone(ctx context.Context, userID, code, resourceowner string, phoneCodeGenerator crypto.Generator) (*domain.ObjectDetails, error) { if userID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Km9ds", "Errors.User.UserIDMissing") } @@ -75,7 +75,7 @@ func (c *Commands) VerifyHumanPhone(ctx context.Context, userID, code, resourceo } userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel) - err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, c.phoneVerificationCode) + err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, phoneCodeGenerator) if err == nil { pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanPhoneVerifiedEvent(ctx, userAgg)) if err != nil { @@ -92,7 +92,7 @@ func (c *Commands) VerifyHumanPhone(ctx context.Context, userID, code, resourceo return nil, caos_errs.ThrowInvalidArgument(err, "COMMAND-sM0cs", "Errors.User.Code.Invalid") } -func (c *Commands) CreateHumanPhoneVerificationCode(ctx context.Context, userID, resourceowner string) (*domain.ObjectDetails, error) { +func (c *Commands) CreateHumanPhoneVerificationCode(ctx context.Context, userID, resourceowner string, phoneCodeGenerator crypto.Generator) (*domain.ObjectDetails, error) { if userID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing") } @@ -112,7 +112,7 @@ func (c *Commands) CreateHumanPhoneVerificationCode(ctx context.Context, userID, return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9sf", "Errors.User.Phone.AlreadyVerified") } - phoneCode, err := domain.NewPhoneCode(c.phoneVerificationCode) + phoneCode, err := domain.NewPhoneCode(phoneCodeGenerator) if err != nil { return nil, err } diff --git a/internal/command/user_human_phone_test.go b/internal/command/user_human_phone_test.go index 60eaa86330..b5d33cabc2 100644 --- a/internal/command/user_human_phone_test.go +++ b/internal/command/user_human_phone_test.go @@ -19,13 +19,13 @@ import ( func TestCommandSide_ChangeHumanPhone(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - secretGenerator crypto.Generator + eventstore *eventstore.Eventstore } type args struct { - ctx context.Context - email *domain.Phone - resourceOwner string + ctx context.Context + email *domain.Phone + resourceOwner string + secretGenerator crypto.Generator } type res struct { want *domain.Phone @@ -232,7 +232,6 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ ctx: context.Background(), @@ -242,7 +241,8 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { }, PhoneNumber: "0711234567", }, - resourceOwner: "org1", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.Phone{ @@ -258,10 +258,9 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - phoneVerificationCode: tt.fields.secretGenerator, + eventstore: tt.fields.eventstore, } - got, err := r.ChangeHumanPhone(tt.args.ctx, tt.args.email, tt.args.resourceOwner) + got, err := r.ChangeHumanPhone(tt.args.ctx, tt.args.email, tt.args.resourceOwner, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } @@ -277,14 +276,14 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) { func TestCommandSide_VerifyHumanPhone(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - secretGenerator crypto.Generator + eventstore *eventstore.Eventstore } type args struct { - ctx context.Context - userID string - code string - resourceOwner string + ctx context.Context + userID string + code string + resourceOwner string + secretGenerator crypto.Generator } type res struct { want *domain.ObjectDetails @@ -428,13 +427,13 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - userID: "user1", - code: "test", - resourceOwner: "org1", + ctx: context.Background(), + userID: "user1", + code: "test", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ err: caos_errs.IsErrorInvalidArgument, @@ -489,13 +488,13 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - userID: "user1", - code: "a", - resourceOwner: "org1", + ctx: context.Background(), + userID: "user1", + code: "a", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.ObjectDetails{ @@ -507,10 +506,9 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - phoneVerificationCode: tt.fields.secretGenerator, + eventstore: tt.fields.eventstore, } - got, err := r.VerifyHumanPhone(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner) + got, err := r.VerifyHumanPhone(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } @@ -526,13 +524,13 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) { func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - secretGenerator crypto.Generator + eventstore *eventstore.Eventstore } type args struct { - ctx context.Context - userID string - resourceOwner string + ctx context.Context + userID string + resourceOwner string + secretGenerator crypto.Generator } type res struct { want *domain.ObjectDetails @@ -663,12 +661,12 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) { }, ), ), - secretGenerator: GetMockSecretGenerator(t), }, args: args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.ObjectDetails{ @@ -680,10 +678,9 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - phoneVerificationCode: tt.fields.secretGenerator, + eventstore: tt.fields.eventstore, } - got, err := r.CreateHumanPhoneVerificationCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) + got, err := r.CreateHumanPhoneVerificationCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index f17df3ec25..f84a0c8a3a 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -25,13 +25,13 @@ func TestCommandSide_AddHuman(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore idGenerator id.Generator - secretGenerator crypto.Generator userPasswordAlg crypto.HashAlgorithm } type args struct { - ctx context.Context - orgID string - human *domain.Human + ctx context.Context + orgID string + human *domain.Human + secretGenerator crypto.Generator } type res struct { want *domain.Human @@ -228,8 +228,7 @@ func TestCommandSide_AddHuman(t *testing.T) { uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - secretGenerator: GetMockSecretGenerator(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), }, args: args{ ctx: context.Background(), @@ -244,6 +243,7 @@ func TestCommandSide_AddHuman(t *testing.T) { EmailAddress: "email@test.ch", }, }, + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.Human{ @@ -312,7 +312,6 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - secretGenerator: GetMockSecretGenerator(t), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ @@ -331,6 +330,7 @@ func TestCommandSide_AddHuman(t *testing.T) { EmailAddress: "email@test.ch", }, }, + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.Human{ @@ -391,7 +391,6 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - secretGenerator: GetMockSecretGenerator(t), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ @@ -411,6 +410,7 @@ func TestCommandSide_AddHuman(t *testing.T) { IsEmailVerified: true, }, }, + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.Human{ @@ -489,8 +489,7 @@ func TestCommandSide_AddHuman(t *testing.T) { uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - secretGenerator: GetMockSecretGenerator(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), }, args: args{ ctx: context.Background(), @@ -508,6 +507,7 @@ func TestCommandSide_AddHuman(t *testing.T) { PhoneNumber: "+41711234567", }, }, + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.Human{ @@ -582,8 +582,7 @@ func TestCommandSide_AddHuman(t *testing.T) { uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - secretGenerator: GetMockSecretGenerator(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), }, args: args{ ctx: context.Background(), @@ -602,6 +601,7 @@ func TestCommandSide_AddHuman(t *testing.T) { IsPhoneVerified: true, }, }, + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.Human{ @@ -630,13 +630,11 @@ func TestCommandSide_AddHuman(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, - initializeUserCode: tt.fields.secretGenerator, - phoneVerificationCode: tt.fields.secretGenerator, - userPasswordAlg: tt.fields.userPasswordAlg, + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + userPasswordAlg: tt.fields.userPasswordAlg, } - got, err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human) + got, err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.secretGenerator, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } @@ -652,17 +650,17 @@ func TestCommandSide_AddHuman(t *testing.T) { func TestCommandSide_ImportHuman(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator - secretGenerator crypto.Generator - userPasswordAlg crypto.HashAlgorithm - passwordlessInitCode crypto.Generator + eventstore *eventstore.Eventstore + idGenerator id.Generator + userPasswordAlg crypto.HashAlgorithm } type args struct { - ctx context.Context - orgID string - human *domain.Human - passwordless bool + ctx context.Context + orgID string + human *domain.Human + passwordless bool + secretGenerator crypto.Generator + passwordlessInitCode crypto.Generator } type res struct { wantHuman *domain.Human @@ -850,7 +848,6 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - secretGenerator: GetMockSecretGenerator(t), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ @@ -870,6 +867,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { EmailAddress: "email@test.ch", }, }, + secretGenerator: GetMockSecretGenerator(t), }, res: res{ wantHuman: &domain.Human{ @@ -930,7 +928,6 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - secretGenerator: GetMockSecretGenerator(t), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ @@ -951,6 +948,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { IsEmailVerified: true, }, }, + secretGenerator: GetMockSecretGenerator(t), }, res: res{ wantHuman: &domain.Human{ @@ -1025,10 +1023,8 @@ func TestCommandSide_ImportHuman(t *testing.T) { uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"), - secretGenerator: GetMockSecretGenerator(t), - userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), - passwordlessInitCode: GetMockSecretGenerator(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ ctx: context.Background(), @@ -1044,7 +1040,9 @@ func TestCommandSide_ImportHuman(t *testing.T) { IsEmailVerified: true, }, }, - passwordless: true, + passwordless: true, + secretGenerator: GetMockSecretGenerator(t), + passwordlessInitCode: GetMockSecretGenerator(t), }, res: res{ wantHuman: &domain.Human{ @@ -1129,10 +1127,8 @@ func TestCommandSide_ImportHuman(t *testing.T) { uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"), - secretGenerator: GetMockSecretGenerator(t), - userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), - passwordlessInitCode: GetMockSecretGenerator(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ ctx: context.Background(), @@ -1152,7 +1148,9 @@ func TestCommandSide_ImportHuman(t *testing.T) { IsEmailVerified: true, }, }, - passwordless: true, + passwordless: true, + secretGenerator: GetMockSecretGenerator(t), + passwordlessInitCode: GetMockSecretGenerator(t), }, res: res{ wantHuman: &domain.Human{ @@ -1242,7 +1240,6 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - secretGenerator: GetMockSecretGenerator(t), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ @@ -1265,6 +1262,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { PhoneNumber: "+41711234567", }, }, + secretGenerator: GetMockSecretGenerator(t), }, res: res{ wantHuman: &domain.Human{ @@ -1340,7 +1338,6 @@ func TestCommandSide_ImportHuman(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - secretGenerator: GetMockSecretGenerator(t), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ @@ -1364,6 +1361,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { IsPhoneVerified: true, }, }, + secretGenerator: GetMockSecretGenerator(t), }, res: res{ wantHuman: &domain.Human{ @@ -1392,14 +1390,11 @@ func TestCommandSide_ImportHuman(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, - initializeUserCode: tt.fields.secretGenerator, - phoneVerificationCode: tt.fields.secretGenerator, - userPasswordAlg: tt.fields.userPasswordAlg, - passwordlessInitCode: tt.fields.passwordlessInitCode, + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + userPasswordAlg: tt.fields.userPasswordAlg, } - gotHuman, gotCode, err := r.ImportHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.passwordless) + gotHuman, gotCode, err := r.ImportHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.passwordless, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } @@ -1418,15 +1413,15 @@ func TestCommandSide_RegisterHuman(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore idGenerator id.Generator - secretGenerator crypto.Generator userPasswordAlg crypto.HashAlgorithm } type args struct { - ctx context.Context - orgID string - human *domain.Human - link *domain.UserIDPLink - orgMemberRoles []string + ctx context.Context + orgID string + human *domain.Human + link *domain.UserIDPLink + orgMemberRoles []string + secretGenerator crypto.Generator } type res struct { want *domain.Human @@ -1858,7 +1853,6 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - secretGenerator: GetMockSecretGenerator(t), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ @@ -1876,6 +1870,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { EmailAddress: "email@test.ch", }, }, + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.Human{ @@ -1957,7 +1952,6 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - secretGenerator: GetMockSecretGenerator(t), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ @@ -1976,6 +1970,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { EmailAddress: "email@test.ch", }, }, + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.Human{ @@ -2049,7 +2044,6 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - secretGenerator: GetMockSecretGenerator(t), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ @@ -2069,6 +2063,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { IsEmailVerified: true, }, }, + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.Human{ @@ -2161,7 +2156,6 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - secretGenerator: GetMockSecretGenerator(t), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ @@ -2183,6 +2177,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { SecretString: "password", }, }, + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.Human{ @@ -2271,7 +2266,6 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - secretGenerator: GetMockSecretGenerator(t), userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), }, args: args{ @@ -2294,6 +2288,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { SecretString: "password", }, }, + secretGenerator: GetMockSecretGenerator(t), }, res: res{ want: &domain.Human{ @@ -2322,13 +2317,11 @@ func TestCommandSide_RegisterHuman(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, - initializeUserCode: tt.fields.secretGenerator, - phoneVerificationCode: tt.fields.secretGenerator, - userPasswordAlg: tt.fields.userPasswordAlg, + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + userPasswordAlg: tt.fields.userPasswordAlg, } - got, err := r.RegisterHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.link, tt.args.orgMemberRoles) + got, err := r.RegisterHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.link, tt.args.orgMemberRoles, tt.args.secretGenerator, tt.args.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_human_webauthn.go b/internal/command/user_human_webauthn.go index d2d4110224..a12a5cbdd8 100644 --- a/internal/command/user_human_webauthn.go +++ b/internal/command/user_human_webauthn.go @@ -129,8 +129,8 @@ func (c *Commands) HumanAddPasswordlessSetup(ctx context.Context, userID, resour return createdWebAuthN, nil } -func (c *Commands) HumanAddPasswordlessSetupInitCode(ctx context.Context, userID, resourceowner, codeID, verificationCode string, preferredPlatformType domain.AuthenticatorAttachment) (*domain.WebAuthNToken, error) { - err := c.humanVerifyPasswordlessInitCode(ctx, userID, resourceowner, codeID, verificationCode) +func (c *Commands) HumanAddPasswordlessSetupInitCode(ctx context.Context, userID, resourceowner, codeID, verificationCode string, preferredPlatformType domain.AuthenticatorAttachment, passwordlessCodeGenerator crypto.Generator) (*domain.WebAuthNToken, error) { + err := c.humanVerifyPasswordlessInitCode(ctx, userID, resourceowner, codeID, verificationCode, passwordlessCodeGenerator) if err != nil { return nil, err } @@ -208,8 +208,8 @@ func (c *Commands) HumanVerifyU2FSetup(ctx context.Context, userID, resourceowne return writeModelToObjectDetails(&verifyWebAuthN.WriteModel), nil } -func (c *Commands) HumanPasswordlessSetupInitCode(ctx context.Context, userID, resourceowner, tokenName, userAgentID, codeID, verificationCode string, credentialData []byte) (*domain.ObjectDetails, error) { - err := c.humanVerifyPasswordlessInitCode(ctx, userID, resourceowner, codeID, verificationCode) +func (c *Commands) HumanPasswordlessSetupInitCode(ctx context.Context, userID, resourceowner, tokenName, userAgentID, codeID, verificationCode string, credentialData []byte, passwordlessCodeGenerator crypto.Generator) (*domain.ObjectDetails, error) { + err := c.humanVerifyPasswordlessInitCode(ctx, userID, resourceowner, codeID, verificationCode, passwordlessCodeGenerator) if err != nil { return nil, err } @@ -483,8 +483,8 @@ func (c *Commands) HumanRemovePasswordless(ctx context.Context, userID, webAuthN return c.removeHumanWebAuthN(ctx, userID, webAuthNID, resourceOwner, event) } -func (c *Commands) HumanAddPasswordlessInitCode(ctx context.Context, userID, resourceOwner string) (*domain.PasswordlessInitCode, error) { - codeEvent, initCode, code, err := c.humanAddPasswordlessInitCode(ctx, userID, resourceOwner, true) +func (c *Commands) HumanAddPasswordlessInitCode(ctx context.Context, userID, resourceOwner string, passwordlessCodeGenerator crypto.Generator) (*domain.PasswordlessInitCode, error) { + codeEvent, initCode, code, err := c.humanAddPasswordlessInitCode(ctx, userID, resourceOwner, true, passwordlessCodeGenerator) pushedEvents, err := c.eventstore.Push(ctx, codeEvent) if err != nil { return nil, err @@ -496,8 +496,8 @@ func (c *Commands) HumanAddPasswordlessInitCode(ctx context.Context, userID, res return writeModelToPasswordlessInitCode(initCode, code), nil } -func (c *Commands) HumanSendPasswordlessInitCode(ctx context.Context, userID, resourceOwner string) (*domain.PasswordlessInitCode, error) { - codeEvent, initCode, code, err := c.humanAddPasswordlessInitCode(ctx, userID, resourceOwner, false) +func (c *Commands) HumanSendPasswordlessInitCode(ctx context.Context, userID, resourceOwner string, passwordlessCodeGenerator crypto.Generator) (*domain.PasswordlessInitCode, error) { + codeEvent, initCode, code, err := c.humanAddPasswordlessInitCode(ctx, userID, resourceOwner, false, passwordlessCodeGenerator) if err != nil { return nil, err } @@ -512,7 +512,7 @@ func (c *Commands) HumanSendPasswordlessInitCode(ctx context.Context, userID, re return writeModelToPasswordlessInitCode(initCode, code), nil } -func (c *Commands) humanAddPasswordlessInitCode(ctx context.Context, userID, resourceOwner string, direct bool) (eventstore.Command, *HumanPasswordlessInitCodeWriteModel, string, error) { +func (c *Commands) humanAddPasswordlessInitCode(ctx context.Context, userID, resourceOwner string, direct bool, passwordlessCodeGenerator crypto.Generator) (eventstore.Command, *HumanPasswordlessInitCodeWriteModel, string, error) { if userID == "" { return nil, nil, "", caos_errs.ThrowPreconditionFailed(nil, "COMMAND-GVfg3", "Errors.IDMissing") } @@ -527,7 +527,7 @@ func (c *Commands) humanAddPasswordlessInitCode(ctx context.Context, userID, res return nil, nil, "", err } - cryptoCode, code, err := crypto.NewCode(c.passwordlessInitCode) + cryptoCode, code, err := crypto.NewCode(passwordlessCodeGenerator) if err != nil { return nil, nil, "", err } @@ -539,7 +539,7 @@ func (c *Commands) humanAddPasswordlessInitCode(ctx context.Context, userID, res return usr_repo.NewHumanPasswordlessInitCodeRequestedEvent(ctx, agg, id, cryptoCode, exp) } } - codeEvent := codeEventCreator(ctx, UserAggregateFromWriteModel(&initCode.WriteModel), codeID, cryptoCode, c.passwordlessInitCode.Expiry()) + codeEvent := codeEventCreator(ctx, UserAggregateFromWriteModel(&initCode.WriteModel), codeID, cryptoCode, passwordlessCodeGenerator.Expiry()) return codeEvent, initCode, code, nil } @@ -563,7 +563,7 @@ func (c *Commands) HumanPasswordlessInitCodeSent(ctx context.Context, userID, re return err } -func (c *Commands) humanVerifyPasswordlessInitCode(ctx context.Context, userID, resourceOwner, codeID, verificationCode string) error { +func (c *Commands) humanVerifyPasswordlessInitCode(ctx context.Context, userID, resourceOwner, codeID, verificationCode string, passwordlessCodeGenerator crypto.Generator) error { if userID == "" || codeID == "" { return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-GVfg3", "Errors.IDMissing") } @@ -572,7 +572,7 @@ func (c *Commands) humanVerifyPasswordlessInitCode(ctx context.Context, userID, if err != nil { return err } - err = crypto.VerifyCode(initCode.ChangeDate, initCode.Expiration, initCode.CryptoCode, verificationCode, c.passwordlessInitCode) + err = crypto.VerifyCode(initCode.ChangeDate, initCode.Expiration, initCode.CryptoCode, verificationCode, passwordlessCodeGenerator) if err != nil || initCode.State != domain.PasswordlessInitCodeStateActive { userAgg := UserAggregateFromWriteModel(&initCode.WriteModel) _, err = c.eventstore.Push(ctx, usr_repo.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, codeID)) diff --git a/internal/config/systemdefaults/system_defaults.go b/internal/config/systemdefaults/system_defaults.go index 5125a64ca5..6f2f1ba535 100644 --- a/internal/config/systemdefaults/system_defaults.go +++ b/internal/config/systemdefaults/system_defaults.go @@ -9,24 +9,23 @@ import ( "github.com/caos/zitadel/internal/notification/channels/chat" "github.com/caos/zitadel/internal/notification/channels/fs" "github.com/caos/zitadel/internal/notification/channels/log" - "github.com/caos/zitadel/internal/notification/channels/smtp" "github.com/caos/zitadel/internal/notification/channels/twilio" "github.com/caos/zitadel/internal/notification/templates" ) type SystemDefaults struct { - DefaultLanguage language.Tag - Domain string - ZitadelDocs ZitadelDocs - SecretGenerators SecretGenerators - UserVerificationKey *crypto.KeyConfig - IDPConfigVerificationKey *crypto.KeyConfig - Multifactors MultifactorConfig - VerificationLifetimes VerificationLifetimes - DomainVerification DomainVerification - IamID string - Notifications Notifications - KeyConfig KeyConfig + DefaultLanguage language.Tag + Domain string + ZitadelDocs ZitadelDocs + SecretGenerators SecretGenerators + UserVerificationKey *crypto.KeyConfig + IDPConfigVerificationKey *crypto.KeyConfig + SMTPPasswordVerificationKey *crypto.KeyConfig + Multifactors MultifactorConfig + VerificationLifetimes VerificationLifetimes + DomainVerification DomainVerification + Notifications Notifications + KeyConfig KeyConfig } type ZitadelDocs struct { @@ -35,15 +34,9 @@ type ZitadelDocs struct { } type SecretGenerators struct { - PasswordSaltCost int - ClientSecretGenerator crypto.GeneratorConfig - InitializeUserCode crypto.GeneratorConfig - EmailVerificationCode crypto.GeneratorConfig - PhoneVerificationCode crypto.GeneratorConfig - PasswordVerificationCode crypto.GeneratorConfig - PasswordlessInitCode crypto.GeneratorConfig - MachineKeySize uint32 - ApplicationKeySize uint32 + PasswordSaltCost int + MachineKeySize uint32 + ApplicationKeySize uint32 } type MultifactorConfig struct { @@ -69,10 +62,9 @@ type DomainVerification struct { } type Notifications struct { - DebugMode bool - Endpoints Endpoints - Providers Channels - TemplateData TemplateData + DebugMode bool + Endpoints Endpoints + Providers Channels } type Endpoints struct { @@ -85,7 +77,6 @@ type Endpoints struct { type Channels struct { Chat chat.ChatConfig - Email smtp.EmailConfig Twilio twilio.TwilioConfig FileSystem fs.FSConfig Log log.LogConfig diff --git a/internal/domain/secret_generator.go b/internal/domain/secret_generator.go new file mode 100644 index 0000000000..83f3ffe1c5 --- /dev/null +++ b/internal/domain/secret_generator.go @@ -0,0 +1,23 @@ +package domain + +type SecretGeneratorType int32 + +const ( + SecretGeneratorTypeUnspecified SecretGeneratorType = iota + SecretGeneratorTypeInitCode + SecretGeneratorTypeVerifyEmailCode + SecretGeneratorTypeVerifyPhoneCode + SecretGeneratorTypePasswordResetCode + SecretGeneratorTypePasswordlessInitCode + SecretGeneratorTypeAppSecret + + secretGeneratorTypeCount +) + +type SecretGeneratorState int32 + +const ( + SecretGeneratorStateUnspecified SecretGeneratorState = iota + SecretGeneratorStateActive + SecretGeneratorStateRemoved +) diff --git a/internal/domain/smtp.go b/internal/domain/smtp.go new file mode 100644 index 0000000000..aa190ab076 --- /dev/null +++ b/internal/domain/smtp.go @@ -0,0 +1,8 @@ +package domain + +type SMTPConfigState int32 + +const ( + SMTPConfigStateUnspecified SMTPConfigState = iota + SMTPConfigStateActive +) diff --git a/internal/notification/channels/smtp/channel.go b/internal/notification/channels/smtp/channel.go index d57ee63968..251e66efa8 100644 --- a/internal/notification/channels/smtp/channel.go +++ b/internal/notification/channels/smtp/channel.go @@ -1,26 +1,30 @@ package smtp import ( + "context" "crypto/tls" "net" "net/smtp" - "github.com/caos/zitadel/internal/notification/messages" - "github.com/caos/logging" + "github.com/pkg/errors" + caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/notification/channels" - "github.com/pkg/errors" + "github.com/caos/zitadel/internal/notification/messages" ) var _ channels.NotificationChannel = (*Email)(nil) type Email struct { - smtpClient *smtp.Client + smtpClient *smtp.Client + senderAddress string + senderName string } -func InitSMTPChannel(config EmailConfig) (*Email, error) { - client, err := config.SMTP.connectToSMTP(config.Tls) +func InitSMTPChannel(ctx context.Context, getSMTPConfig func(ctx context.Context) (*EmailConfig, error)) (*Email, error) { + smtpConfig, err := getSMTPConfig(ctx) + client, err := smtpConfig.SMTP.connectToSMTP(smtpConfig.Tls) if err != nil { return nil, err } @@ -42,12 +46,12 @@ func (email *Email) HandleMessage(message channels.Message) error { if emailMsg.Content == "" || emailMsg.Subject == "" || len(emailMsg.Recipients) == 0 { return caos_errs.ThrowInternalf(nil, "EMAIL-zGemZ", "subject, recipients and content must be set but got subject %s, recipients length %d and content length %d", emailMsg.Subject, len(emailMsg.Recipients), len(emailMsg.Content)) } - + emailMsg.SenderEmail = email.senderAddress + emailMsg.SenderName = email.senderName // To && From if err := email.smtpClient.Mail(emailMsg.SenderEmail); err != nil { return caos_errs.ThrowInternalf(err, "EMAIL-s3is3", "could not set sender: %v", emailMsg.SenderEmail) } - for _, recp := range append(append(emailMsg.Recipients, emailMsg.CC...), emailMsg.BCC...) { if err := email.smtpClient.Rcpt(recp); err != nil { return caos_errs.ThrowInternalf(err, "EMAIL-s4is4", "could not set recipient: %v", recp) diff --git a/internal/notification/messages/email.go b/internal/notification/messages/email.go index 51e151c1d5..5525e8fd15 100644 --- a/internal/notification/messages/email.go +++ b/internal/notification/messages/email.go @@ -20,13 +20,18 @@ type Email struct { BCC []string CC []string SenderEmail string + SenderName string Subject string Content string } func (msg *Email) GetContent() string { headers := make(map[string]string) - headers["From"] = msg.SenderEmail + from := msg.SenderEmail + if msg.SenderName != "" { + from = fmt.Sprintf("%s <%s>", msg.SenderName, msg.SenderEmail) + } + headers["From"] = from headers["To"] = strings.Join(msg.Recipients, ", ") headers["Cc"] = strings.Join(msg.CC, ", ") diff --git a/internal/notification/notification.go b/internal/notification/notification.go index fe1e747401..4a2169442a 100644 --- a/internal/notification/notification.go +++ b/internal/notification/notification.go @@ -4,6 +4,7 @@ import ( "database/sql" "github.com/caos/logging" + "github.com/caos/zitadel/internal/crypto" "github.com/rakyll/statik/fs" "github.com/caos/zitadel/internal/command" @@ -17,10 +18,10 @@ type Config struct { Repository eventsourcing.Config } -func Start(config Config, systemDefaults sd.SystemDefaults, command *command.Commands, queries *query.Queries, dbClient *sql.DB, assetsPrefix string) { +func Start(config Config, systemDefaults sd.SystemDefaults, command *command.Commands, queries *query.Queries, dbClient *sql.DB, assetsPrefix string, smtpPasswordEncAlg crypto.EncryptionAlgorithm) { statikFS, err := fs.NewWithNamespace("notification") logging.OnError(err).Panic("unable to start listener") - _, err = eventsourcing.Start(config.Repository, statikFS, systemDefaults, command, queries, dbClient, assetsPrefix) + _, err = eventsourcing.Start(config.Repository, statikFS, systemDefaults, command, queries, dbClient, assetsPrefix, smtpPasswordEncAlg) logging.OnError(err).Panic("unable to start app") } diff --git a/internal/notification/repository/eventsourcing/handler/handler.go b/internal/notification/repository/eventsourcing/handler/handler.go index 0d6eba58e8..2182c881c5 100644 --- a/internal/notification/repository/eventsourcing/handler/handler.go +++ b/internal/notification/repository/eventsourcing/handler/handler.go @@ -34,14 +34,13 @@ func (h *handler) Eventstore() v1.Eventstore { return h.es } -func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es v1.Eventstore, command *command.Commands, queries *query.Queries, systemDefaults sd.SystemDefaults, dir http.FileSystem, assetsPrefix string) []queryv1.Handler { +func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es v1.Eventstore, command *command.Commands, queries *query.Queries, systemDefaults sd.SystemDefaults, dir http.FileSystem, assetsPrefix string, smtpPasswordEncAlg crypto.EncryptionAlgorithm) []queryv1.Handler { aesCrypto, err := crypto.NewAESCrypto(systemDefaults.UserVerificationKey) logging.OnError(err).Fatal("error create new aes crypto") return []queryv1.Handler{ newNotifyUser( handler{view, bulkLimit, configs.cycleDuration("User"), errorCount, es}, - systemDefaults.IamID, queries, ), newNotification( @@ -52,6 +51,7 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es aesCrypto, dir, assetsPrefix, + smtpPasswordEncAlg, ), } } diff --git a/internal/notification/repository/eventsourcing/handler/notification.go b/internal/notification/repository/eventsourcing/handler/notification.go index be4d9f24a9..c151861fd5 100644 --- a/internal/notification/repository/eventsourcing/handler/notification.go +++ b/internal/notification/repository/eventsourcing/handler/notification.go @@ -19,6 +19,7 @@ import ( queryv1 "github.com/caos/zitadel/internal/eventstore/v1/query" "github.com/caos/zitadel/internal/eventstore/v1/spooler" "github.com/caos/zitadel/internal/i18n" + "github.com/caos/zitadel/internal/notification/channels/smtp" "github.com/caos/zitadel/internal/notification/types" "github.com/caos/zitadel/internal/query" user_repo "github.com/caos/zitadel/internal/repository/user" @@ -34,13 +35,14 @@ const ( type Notification struct { handler - command *command.Commands - systemDefaults sd.SystemDefaults - AesCrypto crypto.EncryptionAlgorithm - statikDir http.FileSystem - subscription *v1.Subscription - assetsPrefix string - queries *query.Queries + command *command.Commands + systemDefaults sd.SystemDefaults + AesCrypto crypto.EncryptionAlgorithm + statikDir http.FileSystem + subscription *v1.Subscription + assetsPrefix string + queries *query.Queries + smtpPasswordCrypto crypto.EncryptionAlgorithm } func newNotification( @@ -51,15 +53,17 @@ func newNotification( aesCrypto crypto.EncryptionAlgorithm, statikDir http.FileSystem, assetsPrefix string, + smtpPasswordEncAlg crypto.EncryptionAlgorithm, ) *Notification { h := &Notification{ - handler: handler, - command: command, - systemDefaults: defaults, - statikDir: statikDir, - AesCrypto: aesCrypto, - assetsPrefix: assetsPrefix, - queries: query, + handler: handler, + command: command, + systemDefaults: defaults, + statikDir: statikDir, + AesCrypto: aesCrypto, + assetsPrefix: assetsPrefix, + queries: query, + smtpPasswordCrypto: smtpPasswordEncAlg, } h.subscribe() @@ -161,7 +165,7 @@ func (n *Notification) handleInitUserCode(event *models.Event) (err error) { return err } - err = types.SendUserInitCode(string(template.Template), translator, user, initCode, n.systemDefaults, n.AesCrypto, colors, n.assetsPrefix) + err = types.SendUserInitCode(ctx, string(template.Template), translator, user, initCode, n.systemDefaults, n.getSMTPConfig, n.AesCrypto, colors, n.assetsPrefix) if err != nil { return err } @@ -199,7 +203,7 @@ func (n *Notification) handlePasswordCode(event *models.Event) (err error) { if err != nil { return err } - err = types.SendPasswordCode(string(template.Template), translator, user, pwCode, n.systemDefaults, n.AesCrypto, colors, n.assetsPrefix) + err = types.SendPasswordCode(ctx, string(template.Template), translator, user, pwCode, n.systemDefaults, n.getSMTPConfig, n.AesCrypto, colors, n.assetsPrefix) if err != nil { return err } @@ -238,7 +242,7 @@ func (n *Notification) handleEmailVerificationCode(event *models.Event) (err err return err } - err = types.SendEmailVerificationCode(string(template.Template), translator, user, emailCode, n.systemDefaults, n.AesCrypto, colors, n.assetsPrefix) + err = types.SendEmailVerificationCode(ctx, string(template.Template), translator, user, emailCode, n.systemDefaults, n.getSMTPConfig, n.AesCrypto, colors, n.assetsPrefix) if err != nil { return err } @@ -303,7 +307,8 @@ func (n *Notification) handleDomainClaimed(event *models.Event) (err error) { if err != nil { return err } - err = types.SendDomainClaimed(string(template.Template), translator, user, data["userName"], n.systemDefaults, colors, n.assetsPrefix) + + err = types.SendDomainClaimed(ctx, string(template.Template), translator, user, data["userName"], n.systemDefaults, n.getSMTPConfig, colors, n.assetsPrefix) if err != nil { return err } @@ -349,7 +354,8 @@ func (n *Notification) handlePasswordlessRegistrationLink(event *models.Event) ( if err != nil { return err } - err = types.SendPasswordlessRegistrationLink(string(template.Template), translator, user, addedEvent, n.systemDefaults, n.AesCrypto, colors, n.assetsPrefix) + + err = types.SendPasswordlessRegistrationLink(ctx, string(template.Template), translator, user, addedEvent, n.systemDefaults, n.getSMTPConfig, n.AesCrypto, colors, n.assetsPrefix) if err != nil { return err } @@ -410,12 +416,34 @@ func (n *Notification) getMailTemplate(ctx context.Context) (*query.MailTemplate return n.queries.MailTemplateByOrg(ctx, authz.GetCtxData(ctx).OrgID) } -func (n *Notification) getTranslatorWithOrgTexts(orgID, textType string) (*i18n.Translator, error) { - translator, err := i18n.NewTranslator(n.statikDir, i18n.TranslatorConfig{DefaultLanguage: n.systemDefaults.DefaultLanguage}) +// Read iam smtp config +func (n *Notification) getSMTPConfig(ctx context.Context) (*smtp.EmailConfig, error) { + config, err := n.queries.SMTPConfigByAggregateID(ctx, domain.IAMID) if err != nil { return nil, err } - ctx := context.TODO() + password, err := crypto.Decrypt(config.Password, n.smtpPasswordCrypto) + if err != nil { + return nil, err + } + return &smtp.EmailConfig{ + From: config.SenderAddress, + FromName: config.SenderName, + SMTP: smtp.SMTP{ + Host: config.Host, + User: config.User, + Password: string(password), + }, + }, nil +} + +func (n *Notification) getTranslatorWithOrgTexts(orgID, textType string) (*i18n.Translator, error) { + ctx := context.Background() + translator, err := i18n.NewTranslator(n.statikDir, i18n.TranslatorConfig{DefaultLanguage: n.queries.GetDefaultLanguage(ctx)}) + if err != nil { + return nil, err + } + allCustomTexts, err := n.queries.CustomTextListByTemplate(ctx, domain.IAMID, textType) if err != nil { return translator, nil diff --git a/internal/notification/repository/eventsourcing/handler/notify_user.go b/internal/notification/repository/eventsourcing/handler/notify_user.go index 8ed3ae84a0..4cef14f6f8 100644 --- a/internal/notification/repository/eventsourcing/handler/notify_user.go +++ b/internal/notification/repository/eventsourcing/handler/notify_user.go @@ -26,19 +26,16 @@ const ( type NotifyUser struct { handler - iamID string subscription *v1.Subscription queries *query2.Queries } func newNotifyUser( handler handler, - iamID string, queries *query2.Queries, ) *NotifyUser { h := &NotifyUser{ handler: handler, - iamID: iamID, queries: queries, } diff --git a/internal/notification/repository/eventsourcing/repository.go b/internal/notification/repository/eventsourcing/repository.go index 142c688779..d9b4d4a9b3 100644 --- a/internal/notification/repository/eventsourcing/repository.go +++ b/internal/notification/repository/eventsourcing/repository.go @@ -6,6 +6,7 @@ import ( "github.com/caos/zitadel/internal/command" sd "github.com/caos/zitadel/internal/config/systemdefaults" + "github.com/caos/zitadel/internal/crypto" v1 "github.com/caos/zitadel/internal/eventstore/v1" es_spol "github.com/caos/zitadel/internal/eventstore/v1/spooler" "github.com/caos/zitadel/internal/notification/repository/eventsourcing/spooler" @@ -21,7 +22,7 @@ type EsRepository struct { spooler *es_spol.Spooler } -func Start(conf Config, dir http.FileSystem, systemDefaults sd.SystemDefaults, command *command.Commands, queries *query.Queries, dbClient *sql.DB, assetsPrefix string) (*EsRepository, error) { +func Start(conf Config, dir http.FileSystem, systemDefaults sd.SystemDefaults, command *command.Commands, queries *query.Queries, dbClient *sql.DB, assetsPrefix string, smtpPasswordEncAlg crypto.EncryptionAlgorithm) (*EsRepository, error) { es, err := v1.Start(dbClient) if err != nil { return nil, err @@ -32,7 +33,7 @@ func Start(conf Config, dir http.FileSystem, systemDefaults sd.SystemDefaults, c return nil, err } - spool := spooler.StartSpooler(conf.Spooler, es, view, dbClient, command, queries, systemDefaults, dir, assetsPrefix) + spool := spooler.StartSpooler(conf.Spooler, es, view, dbClient, command, queries, systemDefaults, dir, assetsPrefix, smtpPasswordEncAlg) return &EsRepository{ spool, diff --git a/internal/notification/repository/eventsourcing/spooler/spooler.go b/internal/notification/repository/eventsourcing/spooler/spooler.go index 5954d59c4e..ac264e6de7 100644 --- a/internal/notification/repository/eventsourcing/spooler/spooler.go +++ b/internal/notification/repository/eventsourcing/spooler/spooler.go @@ -6,6 +6,7 @@ import ( "github.com/caos/zitadel/internal/command" sd "github.com/caos/zitadel/internal/config/systemdefaults" + "github.com/caos/zitadel/internal/crypto" v1 "github.com/caos/zitadel/internal/eventstore/v1" "github.com/caos/zitadel/internal/eventstore/v1/spooler" "github.com/caos/zitadel/internal/notification/repository/eventsourcing/handler" @@ -20,12 +21,12 @@ type SpoolerConfig struct { Handlers handler.Configs } -func StartSpooler(c SpoolerConfig, es v1.Eventstore, view *view.View, sql *sql.DB, command *command.Commands, queries *query.Queries, systemDefaults sd.SystemDefaults, dir http.FileSystem, assetsPrefix string) *spooler.Spooler { +func StartSpooler(c SpoolerConfig, es v1.Eventstore, view *view.View, sql *sql.DB, command *command.Commands, queries *query.Queries, systemDefaults sd.SystemDefaults, dir http.FileSystem, assetsPrefix string, smtpPasswordEncAlg crypto.EncryptionAlgorithm) *spooler.Spooler { spoolerConfig := spooler.Config{ Eventstore: es, Locker: &locker{dbClient: sql}, ConcurrentWorkers: c.ConcurrentWorkers, - ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, command, queries, systemDefaults, dir, assetsPrefix), + ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, command, queries, systemDefaults, dir, assetsPrefix, smtpPasswordEncAlg), } spool := spoolerConfig.New() spool.Start() diff --git a/internal/notification/senders/email.go b/internal/notification/senders/email.go index 725be49af8..90240b5b53 100644 --- a/internal/notification/senders/email.go +++ b/internal/notification/senders/email.go @@ -1,12 +1,14 @@ package senders import ( + "context" + "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/notification/channels" "github.com/caos/zitadel/internal/notification/channels/smtp" ) -func EmailChannels(config systemdefaults.Notifications) (channels.NotificationChannel, error) { +func EmailChannels(ctx context.Context, config systemdefaults.Notifications, emailConfig func(ctx context.Context) (*smtp.EmailConfig, error)) (channels.NotificationChannel, error) { debug, err := debugChannels(config) if err != nil { @@ -14,7 +16,7 @@ func EmailChannels(config systemdefaults.Notifications) (channels.NotificationCh } if !config.DebugMode { - p, err := smtp.InitSMTPChannel(config.Providers.Email) + p, err := smtp.InitSMTPChannel(ctx, emailConfig) if err != nil { return nil, err } diff --git a/internal/notification/types/domain_claimed.go b/internal/notification/types/domain_claimed.go index 430ad831fb..e22a689c16 100644 --- a/internal/notification/types/domain_claimed.go +++ b/internal/notification/types/domain_claimed.go @@ -1,11 +1,13 @@ package types import ( + "context" "strings" "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/i18n" + "github.com/caos/zitadel/internal/notification/channels/smtp" "github.com/caos/zitadel/internal/notification/templates" "github.com/caos/zitadel/internal/query" view_model "github.com/caos/zitadel/internal/user/repository/view/model" @@ -16,7 +18,7 @@ type DomainClaimedData struct { URL string } -func SendDomainClaimed(mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, username string, systemDefaults systemdefaults.SystemDefaults, colors *query.LabelPolicy, assetsPrefix string) error { +func SendDomainClaimed(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, username string, systemDefaults systemdefaults.SystemDefaults, emailConfig func(ctx context.Context) (*smtp.EmailConfig, error), colors *query.LabelPolicy, assetsPrefix string) error { url, err := templates.ParseTemplateText(systemDefaults.Notifications.Endpoints.DomainClaimed, &UrlData{UserID: user.ID}) if err != nil { return err @@ -33,5 +35,5 @@ func SendDomainClaimed(mailhtml string, translator *i18n.Translator, user *view_ if err != nil { return err } - return generateEmail(user, domainClaimedData.Subject, template, systemDefaults.Notifications, true) + return generateEmail(ctx, user, domainClaimedData.Subject, template, systemDefaults.Notifications, emailConfig, true) } diff --git a/internal/notification/types/email_verification_code.go b/internal/notification/types/email_verification_code.go index e667483ec6..37f4045253 100644 --- a/internal/notification/types/email_verification_code.go +++ b/internal/notification/types/email_verification_code.go @@ -1,10 +1,13 @@ package types import ( + "context" + "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/i18n" + "github.com/caos/zitadel/internal/notification/channels/smtp" "github.com/caos/zitadel/internal/notification/templates" "github.com/caos/zitadel/internal/query" es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" @@ -16,7 +19,7 @@ type EmailVerificationCodeData struct { URL string } -func SendEmailVerificationCode(mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.EmailCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string) error { +func SendEmailVerificationCode(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.EmailCode, systemDefaults systemdefaults.SystemDefaults, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string) error { codeString, err := crypto.DecryptString(code.Code, alg) if err != nil { return err @@ -38,5 +41,5 @@ func SendEmailVerificationCode(mailhtml string, translator *i18n.Translator, use if err != nil { return err } - return generateEmail(user, emailCodeData.Subject, template, systemDefaults.Notifications, true) + return generateEmail(ctx, user, emailCodeData.Subject, template, systemDefaults.Notifications, smtpConfig, true) } diff --git a/internal/notification/types/init_code.go b/internal/notification/types/init_code.go index c55bfa8e94..bf46e4c3c8 100644 --- a/internal/notification/types/init_code.go +++ b/internal/notification/types/init_code.go @@ -1,10 +1,13 @@ package types import ( + "context" + "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/i18n" + "github.com/caos/zitadel/internal/notification/channels/smtp" "github.com/caos/zitadel/internal/notification/templates" "github.com/caos/zitadel/internal/query" es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" @@ -22,7 +25,7 @@ type UrlData struct { PasswordSet bool } -func SendUserInitCode(mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.InitUserCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string) error { +func SendUserInitCode(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.InitUserCode, systemDefaults systemdefaults.SystemDefaults, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string) error { codeString, err := crypto.DecryptString(code.Code, alg) if err != nil { return err @@ -42,5 +45,5 @@ func SendUserInitCode(mailhtml string, translator *i18n.Translator, user *view_m if err != nil { return err } - return generateEmail(user, initCodeData.Subject, template, systemDefaults.Notifications, true) + return generateEmail(ctx, user, initCodeData.Subject, template, systemDefaults.Notifications, smtpConfig, true) } diff --git a/internal/notification/types/password_code.go b/internal/notification/types/password_code.go index 7eceb6ae43..2c98211c1a 100644 --- a/internal/notification/types/password_code.go +++ b/internal/notification/types/password_code.go @@ -1,10 +1,13 @@ package types import ( + "context" + "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/i18n" + "github.com/caos/zitadel/internal/notification/channels/smtp" "github.com/caos/zitadel/internal/notification/templates" "github.com/caos/zitadel/internal/query" es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" @@ -18,7 +21,7 @@ type PasswordCodeData struct { URL string } -func SendPasswordCode(mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.PasswordCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string) error { +func SendPasswordCode(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.PasswordCode, systemDefaults systemdefaults.SystemDefaults, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string) error { codeString, err := crypto.DecryptString(code.Code, alg) if err != nil { return err @@ -43,6 +46,6 @@ func SendPasswordCode(mailhtml string, translator *i18n.Translator, user *view_m if code.NotificationType == int32(domain.NotificationTypeSms) { return generateSms(user, passwordResetData.Text, systemDefaults.Notifications, false) } - return generateEmail(user, passwordResetData.Subject, template, systemDefaults.Notifications, true) + return generateEmail(ctx, user, passwordResetData.Subject, template, systemDefaults.Notifications, smtpConfig, true) } diff --git a/internal/notification/types/passwordless_registration_link.go b/internal/notification/types/passwordless_registration_link.go index 5801459ae7..5aa0461999 100644 --- a/internal/notification/types/passwordless_registration_link.go +++ b/internal/notification/types/passwordless_registration_link.go @@ -1,10 +1,13 @@ package types import ( + "context" + "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/i18n" + "github.com/caos/zitadel/internal/notification/channels/smtp" "github.com/caos/zitadel/internal/notification/templates" "github.com/caos/zitadel/internal/query" "github.com/caos/zitadel/internal/repository/user" @@ -16,7 +19,7 @@ type PasswordlessRegistrationLinkData struct { URL string } -func SendPasswordlessRegistrationLink(mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *user.HumanPasswordlessInitCodeRequestedEvent, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string) error { +func SendPasswordlessRegistrationLink(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *user.HumanPasswordlessInitCodeRequestedEvent, systemDefaults systemdefaults.SystemDefaults, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix string) error { codeString, err := crypto.DecryptString(code.Code, alg) if err != nil { return err @@ -33,5 +36,5 @@ func SendPasswordlessRegistrationLink(mailhtml string, translator *i18n.Translat if err != nil { return err } - return generateEmail(user, emailCodeData.Subject, template, systemDefaults.Notifications, true) + return generateEmail(ctx, user, emailCodeData.Subject, template, systemDefaults.Notifications, smtpConfig, true) } diff --git a/internal/notification/types/user_email.go b/internal/notification/types/user_email.go index 9a54142c1d..d5ea7c483c 100644 --- a/internal/notification/types/user_email.go +++ b/internal/notification/types/user_email.go @@ -1,8 +1,10 @@ package types import ( + "context" "html" + "github.com/caos/zitadel/internal/notification/channels/smtp" "github.com/caos/zitadel/internal/notification/messages" "github.com/caos/zitadel/internal/notification/senders" @@ -10,19 +12,18 @@ import ( view_model "github.com/caos/zitadel/internal/user/repository/view/model" ) -func generateEmail(user *view_model.NotifyUser, subject, content string, config systemdefaults.Notifications, lastEmail bool) error { +func generateEmail(ctx context.Context, user *view_model.NotifyUser, subject, content string, config systemdefaults.Notifications, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), lastEmail bool) error { content = html.UnescapeString(content) message := &messages.Email{ - SenderEmail: config.Providers.Email.From, - Recipients: []string{user.VerifiedEmail}, - Subject: subject, - Content: content, + Recipients: []string{user.VerifiedEmail}, + Subject: subject, + Content: content, } if lastEmail { message.Recipients = []string{user.LastEmail} } - channels, err := senders.EmailChannels(config) + channels, err := senders.EmailChannels(ctx, config, smtpConfig) if err != nil { return err } diff --git a/internal/query/custom_text.go b/internal/query/custom_text.go index 1fe8e4f70e..e809afd236 100644 --- a/internal/query/custom_text.go +++ b/internal/query/custom_text.go @@ -186,7 +186,7 @@ func (q *Queries) readLoginTranslationFile(lang string) ([]byte, error) { if !ok { contents, err = q.readTranslationFile(q.LoginDir, fmt.Sprintf("/i18n/%s.yaml", lang)) if errors.IsNotFound(err) { - contents, err = q.readTranslationFile(q.LoginDir, fmt.Sprintf("/i18n/%s.yaml", q.DefaultLanguage.String())) + contents, err = q.readTranslationFile(q.LoginDir, fmt.Sprintf("/i18n/%s.yaml", q.GetDefaultLanguage(context.Background()).String())) } if err != nil { return nil, err diff --git a/internal/query/iam.go b/internal/query/iam.go index a1dfb8460a..9554d0a4c5 100644 --- a/internal/query/iam.go +++ b/internal/query/iam.go @@ -10,6 +10,7 @@ import ( "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/query/projection" + "golang.org/x/text/language" ) var ( @@ -44,6 +45,10 @@ var ( name: projection.IAMColumnSetUpDone, table: iamTable, } + IAMColumnDefaultLanguage = Column{ + name: projection.IAMColumnDefaultLanguage, + table: iamTable, + } ) type IAM struct { @@ -51,10 +56,11 @@ type IAM struct { ChangeDate time.Time Sequence uint64 - GlobalOrgID string - IAMProjectID string - SetupStarted domain.Step - SetupDone domain.Step + GlobalOrgID string + IAMProjectID string + DefaultLanguage language.Tag + SetupStarted domain.Step + SetupDone domain.Step } type IAMSearchQueries struct { @@ -83,6 +89,14 @@ func (q *Queries) IAMByID(ctx context.Context, id string) (*IAM, error) { return scan(row) } +func (q *Queries) GetDefaultLanguage(ctx context.Context) language.Tag { + iam, err := q.IAMByID(ctx, domain.IAMID) + if err != nil { + return language.Und + } + return iam.DefaultLanguage +} + func prepareIAMQuery() (sq.SelectBuilder, func(*sql.Row) (*IAM, error)) { return sq.Select( IAMColumnID.identifier(), @@ -92,18 +106,21 @@ func prepareIAMQuery() (sq.SelectBuilder, func(*sql.Row) (*IAM, error)) { IAMColumnProjectID.identifier(), IAMColumnSetupStarted.identifier(), IAMColumnSetupDone.identifier(), + IAMColumnDefaultLanguage.identifier(), ). From(iamTable.identifier()).PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*IAM, error) { - o := new(IAM) + iam := new(IAM) + lang := "" err := row.Scan( - &o.ID, - &o.ChangeDate, - &o.Sequence, - &o.GlobalOrgID, - &o.IAMProjectID, - &o.SetupStarted, - &o.SetupDone, + &iam.ID, + &iam.ChangeDate, + &iam.Sequence, + &iam.GlobalOrgID, + &iam.IAMProjectID, + &iam.SetupStarted, + &iam.SetupDone, + &lang, ) if err != nil { if errs.Is(err, sql.ErrNoRows) { @@ -111,6 +128,7 @@ func prepareIAMQuery() (sq.SelectBuilder, func(*sql.Row) (*IAM, error)) { } return nil, errors.ThrowInternal(err, "QUERY-d9nw", "Errors.Internal") } - return o, nil + iam.DefaultLanguage = language.Make(lang) + return iam, nil } } diff --git a/internal/query/iam_test.go b/internal/query/iam_test.go index e5f7a92593..7bfc1630af 100644 --- a/internal/query/iam_test.go +++ b/internal/query/iam_test.go @@ -10,6 +10,7 @@ import ( "github.com/caos/zitadel/internal/domain" errs "github.com/caos/zitadel/internal/errors" + "golang.org/x/text/language" ) func Test_IAMPrepares(t *testing.T) { @@ -34,7 +35,8 @@ func Test_IAMPrepares(t *testing.T) { ` zitadel.projections.iam.global_org_id,`+ ` zitadel.projections.iam.iam_project_id,`+ ` zitadel.projections.iam.setup_started,`+ - ` zitadel.projections.iam.setup_done`+ + ` zitadel.projections.iam.setup_done,`+ + ` zitadel.projections.iam.default_language`+ ` FROM zitadel.projections.iam`), nil, nil, @@ -59,7 +61,8 @@ func Test_IAMPrepares(t *testing.T) { ` zitadel.projections.iam.global_org_id,`+ ` zitadel.projections.iam.iam_project_id,`+ ` zitadel.projections.iam.setup_started,`+ - ` zitadel.projections.iam.setup_done`+ + ` zitadel.projections.iam.setup_done,`+ + ` zitadel.projections.iam.default_language`+ ` FROM zitadel.projections.iam`), []string{ "id", @@ -69,6 +72,7 @@ func Test_IAMPrepares(t *testing.T) { "iam_project_id", "setup_started", "setup_done", + "default_language", }, []driver.Value{ "id", @@ -78,17 +82,19 @@ func Test_IAMPrepares(t *testing.T) { "project-id", domain.Step2, domain.Step1, + "en", }, ), }, object: &IAM{ - ID: "id", - ChangeDate: testNow, - Sequence: 20211108, - GlobalOrgID: "global-org-id", - IAMProjectID: "project-id", - SetupStarted: domain.Step2, - SetupDone: domain.Step1, + ID: "id", + ChangeDate: testNow, + Sequence: 20211108, + GlobalOrgID: "global-org-id", + IAMProjectID: "project-id", + SetupStarted: domain.Step2, + SetupDone: domain.Step1, + DefaultLanguage: language.English, }, }, { @@ -102,7 +108,8 @@ func Test_IAMPrepares(t *testing.T) { ` zitadel.projections.iam.global_org_id,`+ ` zitadel.projections.iam.iam_project_id,`+ ` zitadel.projections.iam.setup_started,`+ - ` zitadel.projections.iam.setup_done`+ + ` zitadel.projections.iam.setup_done,`+ + ` zitadel.projections.iam.default_language`+ ` FROM zitadel.projections.iam`), sql.ErrConnDone, ), diff --git a/internal/query/idp.go b/internal/query/idp.go index 3831a06b3a..0ef6e4f17f 100644 --- a/internal/query/idp.go +++ b/internal/query/idp.go @@ -8,7 +8,7 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/lib/pq" - + "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" @@ -186,7 +186,7 @@ func (q *Queries) IDPByIDAndResourceOwner(ctx context.Context, id, resourceOwner IDPResourceOwnerCol.identifier(): resourceOwner, }, sq.Eq{ - IDPResourceOwnerCol.identifier(): q.iamID, + IDPResourceOwnerCol.identifier(): domain.IAMID, }, }, }, diff --git a/internal/query/label_policy.go b/internal/query/label_policy.go index ff073861f1..0f53124e20 100644 --- a/internal/query/label_policy.go +++ b/internal/query/label_policy.go @@ -47,7 +47,7 @@ func (q *Queries) ActiveLabelPolicyByOrg(ctx context.Context, orgID string) (*La LabelPolicyColID.identifier(): orgID, }, sq.Eq{ - LabelPolicyColID.identifier(): q.iamID, + LabelPolicyColID.identifier(): domain.IAMID, }, }, sq.Eq{ @@ -73,7 +73,7 @@ func (q *Queries) PreviewLabelPolicyByOrg(ctx context.Context, orgID string) (*L LabelPolicyColID.identifier(): orgID, }, sq.Eq{ - LabelPolicyColID.identifier(): q.iamID, + LabelPolicyColID.identifier(): domain.IAMID, }, }, sq.Eq{ @@ -93,7 +93,7 @@ func (q *Queries) PreviewLabelPolicyByOrg(ctx context.Context, orgID string) (*L func (q *Queries) DefaultActiveLabelPolicy(ctx context.Context) (*LabelPolicy, error) { stmt, scan := prepareLabelPolicyQuery() query, args, err := stmt.Where(sq.Eq{ - LabelPolicyColID.identifier(): q.iamID, + LabelPolicyColID.identifier(): domain.IAMID, LabelPolicyColState.identifier(): domain.LabelPolicyStateActive, }). OrderBy(LabelPolicyColIsDefault.identifier()). @@ -109,7 +109,7 @@ func (q *Queries) DefaultActiveLabelPolicy(ctx context.Context) (*LabelPolicy, e func (q *Queries) DefaultPreviewLabelPolicy(ctx context.Context) (*LabelPolicy, error) { stmt, scan := prepareLabelPolicyQuery() query, args, err := stmt.Where(sq.Eq{ - LabelPolicyColID.identifier(): q.iamID, + LabelPolicyColID.identifier(): domain.IAMID, LabelPolicyColState.identifier(): domain.LabelPolicyStatePreview, }). OrderBy(LabelPolicyColIsDefault.identifier()). diff --git a/internal/query/lockout_policy.go b/internal/query/lockout_policy.go index 0a4b570a51..db5d474708 100644 --- a/internal/query/lockout_policy.go +++ b/internal/query/lockout_policy.go @@ -76,7 +76,7 @@ func (q *Queries) LockoutPolicyByOrg(ctx context.Context, orgID string) (*Lockou LockoutColID.identifier(): orgID, }, sq.Eq{ - LockoutColID.identifier(): q.iamID, + LockoutColID.identifier(): domain.IAMID, }, }). OrderBy(LockoutColIsDefault.identifier()). @@ -92,7 +92,7 @@ func (q *Queries) LockoutPolicyByOrg(ctx context.Context, orgID string) (*Lockou func (q *Queries) DefaultLockoutPolicy(ctx context.Context) (*LockoutPolicy, error) { stmt, scan := prepareLockoutPolicyQuery() query, args, err := stmt.Where(sq.Eq{ - LockoutColID.identifier(): q.iamID, + LockoutColID.identifier(): domain.IAMID, }). OrderBy(LockoutColIsDefault.identifier()). Limit(1).ToSql() diff --git a/internal/query/mail_template.go b/internal/query/mail_template.go index b55ad9c8c7..daba2b7f25 100644 --- a/internal/query/mail_template.go +++ b/internal/query/mail_template.go @@ -65,7 +65,7 @@ func (q *Queries) MailTemplateByOrg(ctx context.Context, orgID string) (*MailTem MailTemplateColAggregateID.identifier(): orgID, }, sq.Eq{ - MailTemplateColAggregateID.identifier(): q.iamID, + MailTemplateColAggregateID.identifier(): domain.IAMID, }, }). OrderBy(MailTemplateColIsDefault.identifier()). @@ -81,7 +81,7 @@ func (q *Queries) MailTemplateByOrg(ctx context.Context, orgID string) (*MailTem func (q *Queries) DefaultMailTemplate(ctx context.Context) (*MailTemplate, error) { stmt, scan := prepareMailTemplateQuery() query, args, err := stmt.Where(sq.Eq{ - MailTemplateColAggregateID.identifier(): q.iamID, + MailTemplateColAggregateID.identifier(): domain.IAMID, }). OrderBy(MailTemplateColIsDefault.identifier()). Limit(1).ToSql() diff --git a/internal/query/message_text.go b/internal/query/message_text.go index 6c9edb8668..ecdc67487d 100644 --- a/internal/query/message_text.go +++ b/internal/query/message_text.go @@ -119,7 +119,7 @@ func (q *Queries) MessageTextByOrg(ctx context.Context, orgID string) (*MessageT MessageTextColAggregateID.identifier(): orgID, }, sq.Eq{ - MessageTextColAggregateID.identifier(): q.iamID, + MessageTextColAggregateID.identifier(): domain.IAMID, }, }). OrderBy(MessageTextColAggregateID.identifier()). @@ -135,7 +135,7 @@ func (q *Queries) MessageTextByOrg(ctx context.Context, orgID string) (*MessageT func (q *Queries) DefaultMessageText(ctx context.Context) (*MessageText, error) { stmt, scan := prepareMessageTextQuery() query, args, err := stmt.Where(sq.Eq{ - MessageTextColAggregateID.identifier(): q.iamID, + MessageTextColAggregateID.identifier(): domain.IAMID, }). Limit(1).ToSql() if err != nil { @@ -230,7 +230,7 @@ func (q *Queries) readNotificationTextMessages(language string) ([]byte, error) if !ok { contents, err = q.readTranslationFile(q.NotificationDir, fmt.Sprintf("/i18n/%s.yaml", language)) if errors.IsNotFound(err) { - contents, err = q.readTranslationFile(q.NotificationDir, fmt.Sprintf("/i18n/%s.yaml", q.DefaultLanguage.String())) + contents, err = q.readTranslationFile(q.NotificationDir, fmt.Sprintf("/i18n/%s.yaml", q.GetDefaultLanguage(context.Background()).String())) } if err != nil { return nil, err diff --git a/internal/query/org_iam_policy.go b/internal/query/org_iam_policy.go index 3f85efaaad..f31439dfa5 100644 --- a/internal/query/org_iam_policy.go +++ b/internal/query/org_iam_policy.go @@ -71,7 +71,7 @@ func (q *Queries) OrgIAMPolicyByOrg(ctx context.Context, orgID string) (*OrgIAMP OrgIAMColID.identifier(): orgID, }, sq.Eq{ - OrgIAMColID.identifier(): q.iamID, + OrgIAMColID.identifier(): domain.IAMID, }, }). OrderBy(OrgIAMColIsDefault.identifier()). @@ -87,7 +87,7 @@ func (q *Queries) OrgIAMPolicyByOrg(ctx context.Context, orgID string) (*OrgIAMP func (q *Queries) DefaultOrgIAMPolicy(ctx context.Context) (*OrgIAMPolicy, error) { stmt, scan := prepareOrgIAMPolicyQuery() query, args, err := stmt.Where(sq.Eq{ - OrgIAMColID.identifier(): q.iamID, + OrgIAMColID.identifier(): domain.IAMID, }). OrderBy(OrgIAMColIsDefault.identifier()). Limit(1).ToSql() diff --git a/internal/query/password_age_policy.go b/internal/query/password_age_policy.go index adbf220036..c0acf97508 100644 --- a/internal/query/password_age_policy.go +++ b/internal/query/password_age_policy.go @@ -76,7 +76,7 @@ func (q *Queries) PasswordAgePolicyByOrg(ctx context.Context, orgID string) (*Pa PasswordAgeColID.identifier(): orgID, }, sq.Eq{ - PasswordAgeColID.identifier(): q.iamID, + PasswordAgeColID.identifier(): domain.IAMID, }, }). OrderBy(PasswordAgeColIsDefault.identifier()). @@ -92,7 +92,7 @@ func (q *Queries) PasswordAgePolicyByOrg(ctx context.Context, orgID string) (*Pa func (q *Queries) DefaultPasswordAgePolicy(ctx context.Context) (*PasswordAgePolicy, error) { stmt, scan := preparePasswordAgePolicyQuery() query, args, err := stmt.Where(sq.Eq{ - PasswordAgeColID.identifier(): q.iamID, + PasswordAgeColID.identifier(): domain.IAMID, }). OrderBy(PasswordAgeColIsDefault.identifier()). Limit(1).ToSql() diff --git a/internal/query/password_complexity_policy.go b/internal/query/password_complexity_policy.go index 409370b2b5..b3c1b0c48b 100644 --- a/internal/query/password_complexity_policy.go +++ b/internal/query/password_complexity_policy.go @@ -37,7 +37,7 @@ func (q *Queries) PasswordComplexityPolicyByOrg(ctx context.Context, orgID strin PasswordComplexityColID.identifier(): orgID, }, sq.Eq{ - PasswordComplexityColID.identifier(): q.iamID, + PasswordComplexityColID.identifier(): domain.IAMID, }, }). OrderBy(PasswordComplexityColIsDefault.identifier()). @@ -53,7 +53,7 @@ func (q *Queries) PasswordComplexityPolicyByOrg(ctx context.Context, orgID strin func (q *Queries) DefaultPasswordComplexityPolicy(ctx context.Context) (*PasswordComplexityPolicy, error) { stmt, scan := preparePasswordComplexityPolicyQuery() query, args, err := stmt.Where(sq.Eq{ - PasswordComplexityColID.identifier(): q.iamID, + PasswordComplexityColID.identifier(): domain.IAMID, }). OrderBy(PasswordComplexityColIsDefault.identifier()). Limit(1).ToSql() diff --git a/internal/query/privacy_policy.go b/internal/query/privacy_policy.go index e21054e6b4..7c4c613a0f 100644 --- a/internal/query/privacy_policy.go +++ b/internal/query/privacy_policy.go @@ -76,7 +76,7 @@ func (q *Queries) PrivacyPolicyByOrg(ctx context.Context, orgID string) (*Privac PrivacyColID.identifier(): orgID, }, sq.Eq{ - PrivacyColID.identifier(): q.iamID, + PrivacyColID.identifier(): domain.IAMID, }, }). OrderBy(PrivacyColIsDefault.identifier()). @@ -92,7 +92,7 @@ func (q *Queries) PrivacyPolicyByOrg(ctx context.Context, orgID string) (*Privac func (q *Queries) DefaultPrivacyPolicy(ctx context.Context) (*PrivacyPolicy, error) { stmt, scan := preparePrivacyPolicyQuery() query, args, err := stmt.Where(sq.Eq{ - PrivacyColID.identifier(): q.iamID, + PrivacyColID.identifier(): domain.IAMID, }). OrderBy(PrivacyColIsDefault.identifier()). Limit(1).ToSql() diff --git a/internal/query/projection/iam.go b/internal/query/projection/iam.go index 7de91f3efd..cf565e9df3 100644 --- a/internal/query/projection/iam.go +++ b/internal/query/projection/iam.go @@ -40,6 +40,10 @@ func (p *IAMProjection) reducers() []handler.AggregateReducer { Event: iam.ProjectSetEventType, Reduce: p.reduceIAMProjectSet, }, + { + Event: iam.DefaultLanguageSetEventType, + Reduce: p.reduceDefaultLanguageSet, + }, { Event: iam.SetupStartedEventType, Reduce: p.reduceSetupEvent, @@ -56,13 +60,14 @@ func (p *IAMProjection) reducers() []handler.AggregateReducer { type IAMColumn string const ( - IAMColumnID = "id" - IAMColumnChangeDate = "change_date" - IAMColumnGlobalOrgID = "global_org_id" - IAMColumnProjectID = "iam_project_id" - IAMColumnSequence = "sequence" - IAMColumnSetUpStarted = "setup_started" - IAMColumnSetUpDone = "setup_done" + IAMColumnID = "id" + IAMColumnChangeDate = "change_date" + IAMColumnGlobalOrgID = "global_org_id" + IAMColumnProjectID = "iam_project_id" + IAMColumnSequence = "sequence" + IAMColumnSetUpStarted = "setup_started" + IAMColumnSetUpDone = "setup_done" + IAMColumnDefaultLanguage = "default_language" ) func (p *IAMProjection) reduceGlobalOrgSet(event eventstore.Event) (*handler.Statement, error) { @@ -99,6 +104,23 @@ func (p *IAMProjection) reduceIAMProjectSet(event eventstore.Event) (*handler.St ), nil } +func (p *IAMProjection) reduceDefaultLanguageSet(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*iam.DefaultLanguageSetEvent) + if !ok { + logging.LogWithFields("HANDL-3n9le", "seq", event.Sequence(), "expectedType", iam.DefaultLanguageSetEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-30o0e", "reduce.wrong.event.type") + } + return crdb.NewUpsertStatement( + e, + []handler.Column{ + handler.NewCol(IAMColumnID, e.Aggregate().ID), + handler.NewCol(IAMColumnChangeDate, e.CreationDate()), + handler.NewCol(IAMColumnSequence, e.Sequence()), + handler.NewCol(IAMColumnDefaultLanguage, e.Language.String()), + }, + ), nil +} + func (p *IAMProjection) reduceSetupEvent(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*iam.SetupStepEvent) if !ok { diff --git a/internal/query/projection/iam_test.go b/internal/query/projection/iam_test.go index 148fe1f02f..b3bd0ce012 100644 --- a/internal/query/projection/iam_test.go +++ b/internal/query/projection/iam_test.go @@ -52,7 +52,7 @@ func TestIAMProjection_reduces(t *testing.T) { }, }, { - name: "reduceGlobalOrgSet", + name: "reduceProjectIDSet", args: args{ event: getEvent(testEvent( repository.EventType(iam.ProjectSetEventType), @@ -81,6 +81,36 @@ func TestIAMProjection_reduces(t *testing.T) { }, }, }, + { + name: "reduceDefaultLanguageSet", + args: args{ + event: getEvent(testEvent( + repository.EventType(iam.DefaultLanguageSetEventType), + iam.AggregateType, + []byte(`{"language": "en"}`), + ), iam.DefaultLanguageSetMapper), + }, + reduce: (&IAMProjection{}).reduceDefaultLanguageSet, + want: wantReduce{ + projection: IAMProjectionTable, + aggregateType: eventstore.AggregateType("iam"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPSERT INTO zitadel.projections.iam (id, change_date, sequence, default_language) VALUES ($1, $2, $3, $4)", + expectedArgs: []interface{}{ + "agg-id", + anyArg{}, + uint64(15), + "en", + }, + }, + }, + }, + }, + }, { name: "reduceSetupStarted", args: args{ diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 41d5f5462d..6004f7411a 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -69,6 +69,8 @@ func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, co NewUserMetadataProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_metadata"])) NewUserAuthMethodProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_auth_method"])) NewIAMProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["iam"])) + NewSecretGeneratorProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["secret_generators"])) + NewSMTPConfigProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["smtp_configs"])) _, err := NewKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["keys"]), keyConfig, keyChan) return err diff --git a/internal/query/projection/secret_generator.go b/internal/query/projection/secret_generator.go new file mode 100644 index 0000000000..4ce6cc841e --- /dev/null +++ b/internal/query/projection/secret_generator.go @@ -0,0 +1,144 @@ +package projection + +import ( + "context" + + "github.com/caos/logging" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/handler/crdb" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/project" +) + +type SecretGeneratorProjection struct { + crdb.StatementHandler +} + +const ( + SecretGeneratorProjectionTable = "zitadel.projections.secret_generators" +) + +func NewSecretGeneratorProjection(ctx context.Context, config crdb.StatementHandlerConfig) *SecretGeneratorProjection { + p := &SecretGeneratorProjection{} + config.ProjectionName = SecretGeneratorProjectionTable + config.Reducers = p.reducers() + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + return p +} + +func (p *SecretGeneratorProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: project.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: iam.SecretGeneratorAddedEventType, + Reduce: p.reduceSecretGeneratorAdded, + }, + { + Event: iam.SecretGeneratorChangedEventType, + Reduce: p.reduceSecretGeneratorChanged, + }, + { + Event: iam.SecretGeneratorRemovedEventType, + Reduce: p.reduceSecretGeneratorRemoved, + }, + }, + }, + } +} + +const ( + SecretGeneratorColumnGeneratorType = "generator_type" + SecretGeneratorColumnAggregateID = "aggregate_id" + SecretGeneratorColumnCreationDate = "creation_date" + SecretGeneratorColumnChangeDate = "change_date" + SecretGeneratorColumnResourceOwner = "resource_owner" + SecretGeneratorColumnSequence = "sequence" + SecretGeneratorColumnLength = "length" + SecretGeneratorColumnExpiry = "expiry" + SecretGeneratorColumnIncludeLowerLetters = "include_lower_letters" + SecretGeneratorColumnIncludeUpperLetters = "include_upper_letters" + SecretGeneratorColumnIncludeDigits = "include_digits" + SecretGeneratorColumnIncludeSymbols = "include_symbols" +) + +func (p *SecretGeneratorProjection) reduceSecretGeneratorAdded(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*iam.SecretGeneratorAddedEvent) + if !ok { + logging.LogWithFields("HANDL-nf9sl", "seq", event.Sequence(), "expectedType", iam.SecretGeneratorAddedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-sk99F", "reduce.wrong.event.type") + } + return crdb.NewCreateStatement( + e, + []handler.Column{ + handler.NewCol(SecretGeneratorColumnAggregateID, e.Aggregate().ID), + handler.NewCol(SecretGeneratorColumnGeneratorType, e.GeneratorType), + handler.NewCol(SecretGeneratorColumnCreationDate, e.CreationDate()), + handler.NewCol(SecretGeneratorColumnChangeDate, e.CreationDate()), + handler.NewCol(SecretGeneratorColumnResourceOwner, e.Aggregate().ResourceOwner), + handler.NewCol(SecretGeneratorColumnSequence, e.Sequence()), + handler.NewCol(SecretGeneratorColumnLength, e.Length), + handler.NewCol(SecretGeneratorColumnExpiry, e.Expiry), + handler.NewCol(SecretGeneratorColumnIncludeLowerLetters, e.IncludeLowerLetters), + handler.NewCol(SecretGeneratorColumnIncludeUpperLetters, e.IncludeUpperLetters), + handler.NewCol(SecretGeneratorColumnIncludeDigits, e.IncludeDigits), + handler.NewCol(SecretGeneratorColumnIncludeSymbols, e.IncludeSymbols), + }, + ), nil +} + +func (p *SecretGeneratorProjection) reduceSecretGeneratorChanged(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*iam.SecretGeneratorChangedEvent) + if !ok { + logging.LogWithFields("HANDL-sn9jd", "seq", event.Sequence(), "expected", iam.SecretGeneratorChangedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-s00Fs", "reduce.wrong.event.type") + } + + columns := make([]handler.Column, 0, 7) + columns = append(columns, handler.NewCol(SecretGeneratorColumnChangeDate, e.CreationDate()), + handler.NewCol(SecretGeneratorColumnSequence, e.Sequence())) + if e.Length != nil { + columns = append(columns, handler.NewCol(SecretGeneratorColumnLength, *e.Length)) + } + if e.Expiry != nil { + columns = append(columns, handler.NewCol(SecretGeneratorColumnExpiry, *e.Expiry)) + } + if e.IncludeLowerLetters != nil { + columns = append(columns, handler.NewCol(SecretGeneratorColumnIncludeLowerLetters, *e.IncludeLowerLetters)) + } + if e.IncludeUpperLetters != nil { + columns = append(columns, handler.NewCol(SecretGeneratorColumnIncludeUpperLetters, *e.IncludeUpperLetters)) + } + if e.IncludeDigits != nil { + columns = append(columns, handler.NewCol(SecretGeneratorColumnIncludeDigits, *e.IncludeDigits)) + } + if e.IncludeSymbols != nil { + columns = append(columns, handler.NewCol(SecretGeneratorColumnIncludeSymbols, *e.IncludeSymbols)) + } + return crdb.NewUpdateStatement( + e, + columns, + []handler.Condition{ + handler.NewCond(SecretGeneratorColumnAggregateID, e.Aggregate().ID), + handler.NewCond(SecretGeneratorColumnGeneratorType, e.GeneratorType), + }, + ), nil +} + +func (p *SecretGeneratorProjection) reduceSecretGeneratorRemoved(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*iam.SecretGeneratorRemovedEvent) + if !ok { + logging.LogWithFields("HANDL-30oEF", "seq", event.Sequence(), "expectedType", iam.SecretGeneratorRemovedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-fmiIf", "reduce.wrong.event.type") + } + return crdb.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(SecretGeneratorColumnAggregateID, e.Aggregate().ID), + handler.NewCond(SecretGeneratorColumnGeneratorType, e.GeneratorType), + }, + ), nil +} diff --git a/internal/query/projection/secret_generator_test.go b/internal/query/projection/secret_generator_test.go new file mode 100644 index 0000000000..36c8217d3e --- /dev/null +++ b/internal/query/projection/secret_generator_test.go @@ -0,0 +1,141 @@ +package projection + +import ( + "testing" + "time" + + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/iam" +) + +func TestSecretGeneratorProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.Event + } + tests := []struct { + name string + args args + reduce func(event eventstore.Event) (*handler.Statement, error) + want wantReduce + }{ + { + name: "reduceSecretGeneratorRemoved", + args: args{ + event: getEvent(testEvent( + repository.EventType(iam.SecretGeneratorRemovedEventType), + iam.AggregateType, + []byte(`{"generatorType": 1}`), + ), iam.SecretGeneratorRemovedEventMapper), + }, + reduce: (&SecretGeneratorProjection{}).reduceSecretGeneratorRemoved, + want: wantReduce{ + projection: SecretGeneratorProjectionTable, + aggregateType: eventstore.AggregateType("iam"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.secret_generators WHERE (aggregate_id = $1) AND (generator_type = $2)", + expectedArgs: []interface{}{ + "agg-id", + domain.SecretGeneratorTypeInitCode, + }, + }, + }, + }, + }, + }, + { + name: "reduceSecretGeneratorChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(iam.SecretGeneratorChangedEventType), + iam.AggregateType, + []byte(`{"generatorType": 1, "length": 4, "expiry": 10000000, "includeLowerLetters": true, "includeUpperLetters": true, "includeDigits": true, "includeSymbols": true}`), + ), iam.SecretGeneratorChangedEventMapper), + }, + reduce: (&SecretGeneratorProjection{}).reduceSecretGeneratorChanged, + want: wantReduce{ + projection: SecretGeneratorProjectionTable, + aggregateType: eventstore.AggregateType("iam"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.secret_generators SET (change_date, sequence, length, expiry, include_lower_letters, include_upper_letters, include_digits, include_symbols) = ($1, $2, $3, $4, $5, $6, $7, $8) WHERE (aggregate_id = $9) AND (generator_type = $10)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + uint(4), + time.Millisecond * 10, + true, + true, + true, + true, + "agg-id", + domain.SecretGeneratorTypeInitCode, + }, + }, + }, + }, + }, + }, + { + name: "reduceSecretGeneratorAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(iam.SecretGeneratorAddedEventType), + iam.AggregateType, + []byte(`{"generatorType": 1, "length": 4, "expiry": 10000000, "includeLowerLetters": true, "includeUpperLetters": true, "includeDigits": true, "includeSymbols": true}`), + ), iam.SecretGeneratorAddedEventMapper), + }, + reduce: (&SecretGeneratorProjection{}).reduceSecretGeneratorAdded, + want: wantReduce{ + projection: SecretGeneratorProjectionTable, + aggregateType: eventstore.AggregateType("iam"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.secret_generators (aggregate_id, generator_type, creation_date, change_date, resource_owner, sequence, length, expiry, include_lower_letters, include_upper_letters, include_digits, include_symbols) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", + expectedArgs: []interface{}{ + "agg-id", + domain.SecretGeneratorTypeInitCode, + anyArg{}, + anyArg{}, + "ro-id", + uint64(15), + uint(4), + time.Millisecond * 10, + true, + true, + true, + true, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if _, ok := err.(errors.InvalidArgument); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, tt.want) + }) + } +} diff --git a/internal/query/projection/smtp.go b/internal/query/projection/smtp.go new file mode 100644 index 0000000000..a31399ff2a --- /dev/null +++ b/internal/query/projection/smtp.go @@ -0,0 +1,143 @@ +package projection + +import ( + "context" + + "github.com/caos/logging" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/handler/crdb" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/project" +) + +type SMTPConfigProjection struct { + crdb.StatementHandler +} + +const ( + SMTPConfigProjectionTable = "zitadel.projections.smtp_configs" +) + +func NewSMTPConfigProjection(ctx context.Context, config crdb.StatementHandlerConfig) *SMTPConfigProjection { + p := &SMTPConfigProjection{} + config.ProjectionName = SMTPConfigProjectionTable + config.Reducers = p.reducers() + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + return p +} + +func (p *SMTPConfigProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: project.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: iam.SMTPConfigAddedEventType, + Reduce: p.reduceSMTPConfigAdded, + }, + { + Event: iam.SMTPConfigChangedEventType, + Reduce: p.reduceSMTPConfigChanged, + }, + { + Event: iam.SMTPConfigPasswordChangedEventType, + Reduce: p.reduceSMTPConfigPasswordChanged, + }, + }, + }, + } +} + +const ( + SMTPConfigColumnAggregateID = "aggregate_id" + SMTPConfigColumnCreationDate = "creation_date" + SMTPConfigColumnChangeDate = "change_date" + SMTPConfigColumnResourceOwner = "resource_owner" + SMTPConfigColumnSequence = "sequence" + SMTPConfigColumnTLS = "tls" + SMTPConfigColumnFromAddress = "sender_address" + SMTPConfigColumnFromName = "sender_name" + SMTPConfigColumnSMTPHost = "host" + SMTPConfigColumnSMTPUser = "username" + SMTPConfigColumnSMTPPassword = "password" +) + +func (p *SMTPConfigProjection) reduceSMTPConfigAdded(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*iam.SMTPConfigAddedEvent) + if !ok { + logging.LogWithFields("HANDL-wkofs", "seq", event.Sequence(), "expectedType", iam.SMTPConfigAddedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-sk99F", "reduce.wrong.event.type") + } + return crdb.NewCreateStatement( + e, + []handler.Column{ + handler.NewCol(SMTPConfigColumnAggregateID, e.Aggregate().ID), + handler.NewCol(SMTPConfigColumnCreationDate, e.CreationDate()), + handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()), + handler.NewCol(SMTPConfigColumnResourceOwner, e.Aggregate().ResourceOwner), + handler.NewCol(SMTPConfigColumnSequence, e.Sequence()), + handler.NewCol(SMTPConfigColumnTLS, e.TLS), + handler.NewCol(SMTPConfigColumnFromAddress, e.SenderAddress), + handler.NewCol(SMTPConfigColumnFromName, e.SenderName), + handler.NewCol(SMTPConfigColumnSMTPHost, e.Host), + handler.NewCol(SMTPConfigColumnSMTPUser, e.User), + handler.NewCol(SMTPConfigColumnSMTPPassword, e.Password), + }, + ), nil +} + +func (p *SMTPConfigProjection) reduceSMTPConfigChanged(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*iam.SMTPConfigChangedEvent) + if !ok { + logging.LogWithFields("HANDL-wo00f", "seq", event.Sequence(), "expected", iam.SMTPConfigChangedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-wl0wd", "reduce.wrong.event.type") + } + + columns := make([]handler.Column, 0, 7) + columns = append(columns, handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()), + handler.NewCol(SMTPConfigColumnSequence, e.Sequence())) + if e.TLS != nil { + columns = append(columns, handler.NewCol(SMTPConfigColumnTLS, *e.TLS)) + } + if e.FromAddress != nil { + columns = append(columns, handler.NewCol(SMTPConfigColumnFromAddress, *e.FromAddress)) + } + if e.FromName != nil { + columns = append(columns, handler.NewCol(SMTPConfigColumnFromName, *e.FromName)) + } + if e.Host != nil { + columns = append(columns, handler.NewCol(SMTPConfigColumnSMTPHost, *e.Host)) + } + if e.User != nil { + columns = append(columns, handler.NewCol(SMTPConfigColumnSMTPUser, *e.User)) + } + return crdb.NewUpdateStatement( + e, + columns, + []handler.Condition{ + handler.NewCond(SMTPConfigColumnAggregateID, e.Aggregate().ID), + }, + ), nil +} + +func (p *SMTPConfigProjection) reduceSMTPConfigPasswordChanged(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*iam.SMTPConfigPasswordChangedEvent) + if !ok { + logging.LogWithFields("HANDL-f92sf", "seq", event.Sequence(), "expected", iam.SMTPConfigChangedEventType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "HANDL-fk02f", "reduce.wrong.event.type") + } + + return crdb.NewUpdateStatement( + e, + []handler.Column{ + handler.NewCol(SMTPConfigColumnChangeDate, e.CreationDate()), + handler.NewCol(SMTPConfigColumnSequence, e.Sequence()), + handler.NewCol(SMTPConfigColumnSMTPPassword, e.Password), + }, + []handler.Condition{ + handler.NewCond(SMTPConfigColumnAggregateID, e.Aggregate().ID), + }, + ), nil +} diff --git a/internal/query/projection/smtp_test.go b/internal/query/projection/smtp_test.go new file mode 100644 index 0000000000..5c6343fa36 --- /dev/null +++ b/internal/query/projection/smtp_test.go @@ -0,0 +1,162 @@ +package projection + +import ( + "testing" + + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/iam" +) + +func TestSMTPConfigProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.Event + } + tests := []struct { + name string + args args + reduce func(event eventstore.Event) (*handler.Statement, error) + want wantReduce + }{ + { + name: "reduceSMTPConfigChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(iam.SMTPConfigChangedEventType), + iam.AggregateType, + []byte(`{ + "tls": true, + "senderAddress": "sender", + "senderName": "name", + "host": "host", + "user": "user" + }`, + ), + ), iam.SMTPConfigChangedEventMapper), + }, + reduce: (&SMTPConfigProjection{}).reduceSMTPConfigChanged, + want: wantReduce{ + projection: SMTPConfigProjectionTable, + aggregateType: eventstore.AggregateType("iam"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.smtp_configs SET (change_date, sequence, tls, sender_address, sender_name, host, username) = ($1, $2, $3, $4, $5, $6, $7) WHERE (aggregate_id = $8)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + true, + "sender", + "name", + "host", + "user", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceSMTPConfigAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(iam.SMTPConfigAddedEventType), + iam.AggregateType, + []byte(`{ + "tls": true, + "senderAddress": "sender", + "senderName": "name", + "host": "host", + "user": "user", + "password": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + } + }`), + ), iam.SMTPConfigAddedEventMapper), + }, + reduce: (&SMTPConfigProjection{}).reduceSMTPConfigAdded, + want: wantReduce{ + projection: SMTPConfigProjectionTable, + aggregateType: eventstore.AggregateType("iam"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.smtp_configs (aggregate_id, creation_date, change_date, resource_owner, sequence, tls, sender_address, sender_name, host, username, password) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", + expectedArgs: []interface{}{ + "agg-id", + anyArg{}, + anyArg{}, + "ro-id", + uint64(15), + true, + "sender", + "name", + "host", + "user", + anyArg{}, + }, + }, + }, + }, + }, + }, + { + name: "reduceSMTPConfigPasswordChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(iam.SMTPConfigPasswordChangedEventType), + iam.AggregateType, + []byte(`{ + "password": { + "cryptoType": 0, + "algorithm": "RSA-265", + "keyId": "key-id" + } + }`), + ), iam.SMTPConfigPasswordChangedEventMapper), + }, + reduce: (&SMTPConfigProjection{}).reduceSMTPConfigPasswordChanged, + want: wantReduce{ + projection: SMTPConfigProjectionTable, + aggregateType: eventstore.AggregateType("iam"), + sequence: 15, + previousSequence: 10, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.smtp_configs SET (change_date, sequence, password) = ($1, $2, $3) WHERE (aggregate_id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + anyArg{}, + "agg-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if _, ok := err.(errors.InvalidArgument); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, tt.want) + }) + } +} diff --git a/internal/query/query.go b/internal/query/query.go index fe4d562838..99f29c20d9 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -25,7 +25,6 @@ import ( ) type Queries struct { - iamID string eventstore *eventstore.Eventstore client *sql.DB @@ -51,10 +50,9 @@ func StartQueries(ctx context.Context, es *eventstore.Eventstore, sqlClient *sql } repo = &Queries{ - iamID: defaults.IamID, eventstore: es, client: sqlClient, - DefaultLanguage: defaults.DefaultLanguage, + DefaultLanguage: language.Und, LoginDir: statikLoginFS, NotificationDir: statikNotificationFS, LoginTranslationFileContents: make(map[string][]byte), diff --git a/internal/query/secret_generator_test.go b/internal/query/secret_generator_test.go new file mode 100644 index 0000000000..3f4617d1e7 --- /dev/null +++ b/internal/query/secret_generator_test.go @@ -0,0 +1,386 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" + "time" + + "github.com/caos/zitadel/internal/domain" + errs "github.com/caos/zitadel/internal/errors" +) + +func Test_SecretGeneratorsPrepares(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareSecretGeneratorsQuery no result", + prepare: prepareSecretGeneratorsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(`SELECT zitadel.projections.secret_generators.aggregate_id,`+ + ` zitadel.projections.secret_generators.generator_type,`+ + ` zitadel.projections.secret_generators.creation_date,`+ + ` zitadel.projections.secret_generators.change_date,`+ + ` zitadel.projections.secret_generators.resource_owner,`+ + ` zitadel.projections.secret_generators.sequence,`+ + ` zitadel.projections.secret_generators.length,`+ + ` zitadel.projections.secret_generators.expiry,`+ + ` zitadel.projections.secret_generators.include_lower_letters,`+ + ` zitadel.projections.secret_generators.include_upper_letters,`+ + ` zitadel.projections.secret_generators.include_digits,`+ + ` zitadel.projections.secret_generators.include_symbols,`+ + ` COUNT(*) OVER ()`+ + ` FROM zitadel.projections.secret_generators`), + nil, + nil, + ), + }, + object: &SecretGenerators{SecretGenerators: []*SecretGenerator{}}, + }, + { + name: "prepareSecretGeneratorsQuery one result", + prepare: prepareSecretGeneratorsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(`SELECT zitadel.projections.secret_generators.aggregate_id,`+ + ` zitadel.projections.secret_generators.generator_type,`+ + ` zitadel.projections.secret_generators.creation_date,`+ + ` zitadel.projections.secret_generators.change_date,`+ + ` zitadel.projections.secret_generators.resource_owner,`+ + ` zitadel.projections.secret_generators.sequence,`+ + ` zitadel.projections.secret_generators.length,`+ + ` zitadel.projections.secret_generators.expiry,`+ + ` zitadel.projections.secret_generators.include_lower_letters,`+ + ` zitadel.projections.secret_generators.include_upper_letters,`+ + ` zitadel.projections.secret_generators.include_digits,`+ + ` zitadel.projections.secret_generators.include_symbols,`+ + ` COUNT(*) OVER ()`+ + ` FROM zitadel.projections.secret_generators`), + []string{ + "aggregate_id", + "generator_type", + "creation_date", + "change_date", + "resource_owner", + "sequence", + "length", + "expiry", + "include_lower_letters", + "include_upper_letters", + "include_digits", + "include_symbols", + "count", + }, + [][]driver.Value{ + { + "agg-id", + domain.SecretGeneratorTypeInitCode, + testNow, + testNow, + "ro", + uint64(20211108), + 4, + time.Minute * 1, + true, + true, + true, + true, + }, + }, + ), + }, + object: &SecretGenerators{ + SearchResponse: SearchResponse{ + Count: 1, + }, + SecretGenerators: []*SecretGenerator{ + { + AggregateID: "agg-id", + GeneratorType: 1, + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211108, + Length: 4, + Expiry: time.Minute * 1, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + }, + }, + }, + { + name: "prepareSecretGeneratorsQuery multiple result", + prepare: prepareSecretGeneratorsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(`SELECT zitadel.projections.secret_generators.aggregate_id,`+ + ` zitadel.projections.secret_generators.generator_type,`+ + ` zitadel.projections.secret_generators.creation_date,`+ + ` zitadel.projections.secret_generators.change_date,`+ + ` zitadel.projections.secret_generators.resource_owner,`+ + ` zitadel.projections.secret_generators.sequence,`+ + ` zitadel.projections.secret_generators.length,`+ + ` zitadel.projections.secret_generators.expiry,`+ + ` zitadel.projections.secret_generators.include_lower_letters,`+ + ` zitadel.projections.secret_generators.include_upper_letters,`+ + ` zitadel.projections.secret_generators.include_digits,`+ + ` zitadel.projections.secret_generators.include_symbols,`+ + ` COUNT(*) OVER ()`+ + ` FROM zitadel.projections.secret_generators`), + []string{ + "aggregate_id", + "generator_type", + "creation_date", + "change_date", + "resource_owner", + "sequence", + "length", + "expiry", + "include_lower_letters", + "include_upper_letters", + "include_digits", + "include_symbols", + "count", + }, + [][]driver.Value{ + { + "agg-id", + domain.SecretGeneratorTypeInitCode, + testNow, + testNow, + "ro", + uint64(20211108), + 4, + time.Minute * 1, + true, + true, + true, + true, + }, + { + "agg-id", + domain.SecretGeneratorTypeVerifyEmailCode, + testNow, + testNow, + "ro", + uint64(20211108), + 4, + time.Minute * 1, + true, + true, + true, + true, + }, + }, + ), + }, + object: &SecretGenerators{ + SearchResponse: SearchResponse{ + Count: 2, + }, + SecretGenerators: []*SecretGenerator{ + { + AggregateID: "agg-id", + GeneratorType: 1, + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211108, + Length: 4, + Expiry: time.Minute * 1, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + { + AggregateID: "agg-id", + GeneratorType: 2, + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211108, + Length: 4, + Expiry: time.Minute * 1, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + }, + }, + }, + { + name: "prepareSecretGeneratorsQuery sql err", + prepare: prepareSecretGeneratorsQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(`SELECT zitadel.projections.secret_generators.aggregate_id,`+ + ` zitadel.projections.secret_generators.generator_type,`+ + ` zitadel.projections.secret_generators.creation_date,`+ + ` zitadel.projections.secret_generators.change_date,`+ + ` zitadel.projections.secret_generators.resource_owner,`+ + ` zitadel.projections.secret_generators.sequence,`+ + ` zitadel.projections.secret_generators.length,`+ + ` zitadel.projections.secret_generators.expiry,`+ + ` zitadel.projections.secret_generators.include_lower_letters,`+ + ` zitadel.projections.secret_generators.include_upper_letters,`+ + ` zitadel.projections.secret_generators.include_digits,`+ + ` zitadel.projections.secret_generators.include_symbols,`+ + ` COUNT(*) OVER ()`+ + ` FROM zitadel.projections.secret_generators`), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + { + name: "prepareSecretGeneratorQuery no result", + prepare: prepareSecretGeneratorQuery, + want: want{ + sqlExpectations: mockQueries( + `SELECT zitadel.projections.secret_generators.aggregate_id,`+ + ` zitadel.projections.secret_generators.generator_type,`+ + ` zitadel.projections.secret_generators.creation_date,`+ + ` zitadel.projections.secret_generators.change_date,`+ + ` zitadel.projections.secret_generators.resource_owner,`+ + ` zitadel.projections.secret_generators.sequence,`+ + ` zitadel.projections.secret_generators.length,`+ + ` zitadel.projections.secret_generators.expiry,`+ + ` zitadel.projections.secret_generators.include_lower_letters,`+ + ` zitadel.projections.secret_generators.include_upper_letters,`+ + ` zitadel.projections.secret_generators.include_digits,`+ + ` zitadel.projections.secret_generators.include_symbols`+ + ` FROM zitadel.projections.secret_generators`, + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*SecretGenerator)(nil), + }, + { + name: "prepareSecretGeneratorQuery found", + prepare: prepareSecretGeneratorQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(`SELECT zitadel.projections.secret_generators.aggregate_id,`+ + ` zitadel.projections.secret_generators.generator_type,`+ + ` zitadel.projections.secret_generators.creation_date,`+ + ` zitadel.projections.secret_generators.change_date,`+ + ` zitadel.projections.secret_generators.resource_owner,`+ + ` zitadel.projections.secret_generators.sequence,`+ + ` zitadel.projections.secret_generators.length,`+ + ` zitadel.projections.secret_generators.expiry,`+ + ` zitadel.projections.secret_generators.include_lower_letters,`+ + ` zitadel.projections.secret_generators.include_upper_letters,`+ + ` zitadel.projections.secret_generators.include_digits,`+ + ` zitadel.projections.secret_generators.include_symbols`+ + ` FROM zitadel.projections.secret_generators`), + []string{ + "aggregate_id", + "generator_type", + "creation_date", + "change_date", + "resource_owner", + "sequence", + "length", + "expiry", + "include_lower_letters", + "include_upper_letters", + "include_digits", + "include_symbols", + }, + []driver.Value{ + "agg-id", + domain.SecretGeneratorTypeInitCode, + testNow, + testNow, + "ro", + uint64(20211108), + 4, + time.Minute * 1, + true, + true, + true, + true, + }, + ), + }, + object: &SecretGenerator{ + AggregateID: "agg-id", + GeneratorType: domain.SecretGeneratorTypeInitCode, + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211108, + Length: 4, + Expiry: time.Minute * 1, + IncludeLowerLetters: true, + IncludeUpperLetters: true, + IncludeDigits: true, + IncludeSymbols: true, + }, + }, + { + name: "prepareSecretGeneratorQuery sql err", + prepare: prepareSecretGeneratorQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(`SELECT zitadel.projections.secret_generators.aggregate_id,`+ + ` zitadel.projections.secret_generators.generator_type,`+ + ` zitadel.projections.secret_generators.creation_date,`+ + ` zitadel.projections.secret_generators.change_date,`+ + ` zitadel.projections.secret_generators.resource_owner,`+ + ` zitadel.projections.secret_generators.sequence,`+ + ` zitadel.projections.secret_generators.length,`+ + ` zitadel.projections.secret_generators.expiry,`+ + ` zitadel.projections.secret_generators.include_lower_letters,`+ + ` zitadel.projections.secret_generators.include_upper_letters,`+ + ` zitadel.projections.secret_generators.include_digits,`+ + ` zitadel.projections.secret_generators.include_symbols`+ + ` FROM zitadel.projections.secret_generators`), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) + }) + } +} diff --git a/internal/query/secret_generators.go b/internal/query/secret_generators.go new file mode 100644 index 0000000000..f029c07fd8 --- /dev/null +++ b/internal/query/secret_generators.go @@ -0,0 +1,268 @@ +package query + +import ( + "context" + "database/sql" + errs "errors" + "time" + + sq "github.com/Masterminds/squirrel" + "github.com/caos/zitadel/internal/domain" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/query/projection" + + "github.com/caos/zitadel/internal/errors" +) + +var ( + secretGeneratorsTable = table{ + name: projection.SecretGeneratorProjectionTable, + } + SecretGeneratorColumnAggregateID = Column{ + name: projection.SecretGeneratorColumnAggregateID, + table: secretGeneratorsTable, + } + SecretGeneratorColumnGeneratorType = Column{ + name: projection.SecretGeneratorColumnGeneratorType, + table: secretGeneratorsTable, + } + SecretGeneratorColumnCreationDate = Column{ + name: projection.SecretGeneratorColumnCreationDate, + table: secretGeneratorsTable, + } + SecretGeneratorColumnChangeDate = Column{ + name: projection.SecretGeneratorColumnChangeDate, + table: secretGeneratorsTable, + } + SecretGeneratorColumnResourceOwner = Column{ + name: projection.SecretGeneratorColumnResourceOwner, + table: secretGeneratorsTable, + } + SecretGeneratorColumnSequence = Column{ + name: projection.SecretGeneratorColumnSequence, + table: secretGeneratorsTable, + } + SecretGeneratorColumnLength = Column{ + name: projection.SecretGeneratorColumnLength, + table: secretGeneratorsTable, + } + SecretGeneratorColumnExpiry = Column{ + name: projection.SecretGeneratorColumnExpiry, + table: secretGeneratorsTable, + } + SecretGeneratorColumnIncludeLowerLetters = Column{ + name: projection.SecretGeneratorColumnIncludeLowerLetters, + table: secretGeneratorsTable, + } + SecretGeneratorColumnIncludeUpperLetters = Column{ + name: projection.SecretGeneratorColumnIncludeUpperLetters, + table: secretGeneratorsTable, + } + SecretGeneratorColumnIncludeDigits = Column{ + name: projection.SecretGeneratorColumnIncludeDigits, + table: secretGeneratorsTable, + } + SecretGeneratorColumnIncludeSymbols = Column{ + name: projection.SecretGeneratorColumnIncludeSymbols, + table: secretGeneratorsTable, + } +) + +type SecretGenerators struct { + SearchResponse + SecretGenerators []*SecretGenerator +} + +type SecretGenerator struct { + AggregateID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + Sequence uint64 + + GeneratorType domain.SecretGeneratorType + Length uint + Expiry time.Duration + IncludeLowerLetters bool + IncludeUpperLetters bool + IncludeDigits bool + IncludeSymbols bool +} + +type SecretGeneratorSearchQueries struct { + SearchRequest + Queries []SearchQuery +} + +func (q *Queries) InitEncryptionGenerator(ctx context.Context, generatorType domain.SecretGeneratorType, algorithm crypto.EncryptionAlgorithm) (crypto.Generator, error) { + generatorConfig, err := q.SecretGeneratorByType(ctx, generatorType) + if err != nil { + return nil, err + } + cryptoConfig := crypto.GeneratorConfig{ + Length: generatorConfig.Length, + Expiry: generatorConfig.Expiry, + IncludeLowerLetters: generatorConfig.IncludeLowerLetters, + IncludeUpperLetters: generatorConfig.IncludeUpperLetters, + IncludeDigits: generatorConfig.IncludeDigits, + IncludeSymbols: generatorConfig.IncludeSymbols, + } + return crypto.NewEncryptionGenerator(cryptoConfig, algorithm), nil +} + +func (q *Queries) InitHashGenerator(ctx context.Context, generatorType domain.SecretGeneratorType, algorithm crypto.HashAlgorithm) (crypto.Generator, error) { + generatorConfig, err := q.SecretGeneratorByType(ctx, generatorType) + if err != nil { + return nil, err + } + cryptoConfig := crypto.GeneratorConfig{ + Length: generatorConfig.Length, + Expiry: generatorConfig.Expiry, + IncludeLowerLetters: generatorConfig.IncludeLowerLetters, + IncludeUpperLetters: generatorConfig.IncludeUpperLetters, + IncludeDigits: generatorConfig.IncludeDigits, + IncludeSymbols: generatorConfig.IncludeSymbols, + } + return crypto.NewHashGenerator(cryptoConfig, algorithm), nil +} + +func (q *Queries) SecretGeneratorByType(ctx context.Context, generatorType domain.SecretGeneratorType) (*SecretGenerator, error) { + stmt, scan := prepareSecretGeneratorQuery() + query, args, err := stmt.Where(sq.Eq{ + SecretGeneratorColumnGeneratorType.identifier(): generatorType, + }).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-3k99f", "Errors.Query.SQLStatment") + } + + row := q.client.QueryRowContext(ctx, query, args...) + return scan(row) +} + +func (q *Queries) SearchSecretGenerators(ctx context.Context, queries *SecretGeneratorSearchQueries) (secretGenerators *SecretGenerators, err error) { + query, scan := prepareSecretGeneratorsQuery() + stmt, args, err := queries.toQuery(query).ToSql() + if err != nil { + return nil, errors.ThrowInvalidArgument(err, "QUERY-sn9lw", "Errors.Query.InvalidRequest") + } + + rows, err := q.client.QueryContext(ctx, stmt, args...) + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-4miii", "Errors.Internal") + } + secretGenerators, err = scan(rows) + if err != nil { + return nil, err + } + secretGenerators.LatestSequence, err = q.latestSequence(ctx, secretGeneratorsTable) + return secretGenerators, err +} + +func (q *SecretGeneratorSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +func NewSecretGeneratorTypeSearchQuery(value int32) (SearchQuery, error) { + return NewNumberQuery(SecretGeneratorColumnGeneratorType, value, NumberEquals) +} + +func prepareSecretGeneratorQuery() (sq.SelectBuilder, func(*sql.Row) (*SecretGenerator, error)) { + return sq.Select( + SecretGeneratorColumnAggregateID.identifier(), + SecretGeneratorColumnGeneratorType.identifier(), + SecretGeneratorColumnCreationDate.identifier(), + SecretGeneratorColumnChangeDate.identifier(), + SecretGeneratorColumnResourceOwner.identifier(), + SecretGeneratorColumnSequence.identifier(), + SecretGeneratorColumnLength.identifier(), + SecretGeneratorColumnExpiry.identifier(), + SecretGeneratorColumnIncludeLowerLetters.identifier(), + SecretGeneratorColumnIncludeUpperLetters.identifier(), + SecretGeneratorColumnIncludeDigits.identifier(), + SecretGeneratorColumnIncludeSymbols.identifier()). + From(secretGeneratorsTable.identifier()).PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*SecretGenerator, error) { + secretGenerator := new(SecretGenerator) + err := row.Scan( + &secretGenerator.AggregateID, + &secretGenerator.GeneratorType, + &secretGenerator.CreationDate, + &secretGenerator.ChangeDate, + &secretGenerator.ResourceOwner, + &secretGenerator.Sequence, + &secretGenerator.Length, + &secretGenerator.Expiry, + &secretGenerator.IncludeLowerLetters, + &secretGenerator.IncludeUpperLetters, + &secretGenerator.IncludeDigits, + &secretGenerator.IncludeSymbols, + ) + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-m9wff", "Errors.SecretGenerator.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-2k99d", "Errors.Internal") + } + return secretGenerator, nil + } +} + +func prepareSecretGeneratorsQuery() (sq.SelectBuilder, func(*sql.Rows) (*SecretGenerators, error)) { + return sq.Select( + SecretGeneratorColumnAggregateID.identifier(), + SecretGeneratorColumnGeneratorType.identifier(), + SecretGeneratorColumnCreationDate.identifier(), + SecretGeneratorColumnChangeDate.identifier(), + SecretGeneratorColumnResourceOwner.identifier(), + SecretGeneratorColumnSequence.identifier(), + SecretGeneratorColumnLength.identifier(), + SecretGeneratorColumnExpiry.identifier(), + SecretGeneratorColumnIncludeLowerLetters.identifier(), + SecretGeneratorColumnIncludeUpperLetters.identifier(), + SecretGeneratorColumnIncludeDigits.identifier(), + SecretGeneratorColumnIncludeSymbols.identifier(), + countColumn.identifier()). + From(secretGeneratorsTable.identifier()).PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*SecretGenerators, error) { + secretGenerators := make([]*SecretGenerator, 0) + var count uint64 + for rows.Next() { + secretGenerator := new(SecretGenerator) + err := rows.Scan( + &secretGenerator.AggregateID, + &secretGenerator.GeneratorType, + &secretGenerator.CreationDate, + &secretGenerator.ChangeDate, + &secretGenerator.ResourceOwner, + &secretGenerator.Sequence, + &secretGenerator.Length, + &secretGenerator.Expiry, + &secretGenerator.IncludeLowerLetters, + &secretGenerator.IncludeUpperLetters, + &secretGenerator.IncludeDigits, + &secretGenerator.IncludeSymbols, + &count, + ) + if err != nil { + return nil, err + } + secretGenerators = append(secretGenerators, secretGenerator) + } + + if err := rows.Close(); err != nil { + return nil, errors.ThrowInternal(err, "QUERY-em9fs", "Errors.Query.CloseRows") + } + + return &SecretGenerators{ + SecretGenerators: secretGenerators, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} diff --git a/internal/query/smtp.go b/internal/query/smtp.go new file mode 100644 index 0000000000..7386e65b49 --- /dev/null +++ b/internal/query/smtp.go @@ -0,0 +1,139 @@ +package query + +import ( + "context" + "database/sql" + errs "errors" + "time" + + sq "github.com/Masterminds/squirrel" + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/query/projection" + + "github.com/caos/zitadel/internal/errors" +) + +var ( + smtpConfigsTable = table{ + name: projection.SMTPConfigProjectionTable, + } + SMTPConfigColumnAggregateID = Column{ + name: projection.SMTPConfigColumnAggregateID, + table: smtpConfigsTable, + } + SMTPConfigColumnCreationDate = Column{ + name: projection.SMTPConfigColumnCreationDate, + table: smtpConfigsTable, + } + SMTPConfigColumnChangeDate = Column{ + name: projection.SMTPConfigColumnChangeDate, + table: smtpConfigsTable, + } + SMTPConfigColumnResourceOwner = Column{ + name: projection.SMTPConfigColumnResourceOwner, + table: smtpConfigsTable, + } + SMTPConfigColumnSequence = Column{ + name: projection.SMTPConfigColumnSequence, + table: smtpConfigsTable, + } + SMTPConfigColumnTLS = Column{ + name: projection.SMTPConfigColumnTLS, + table: smtpConfigsTable, + } + SMTPConfigColumnFromAddress = Column{ + name: projection.SMTPConfigColumnFromAddress, + table: smtpConfigsTable, + } + SMTPConfigColumnFromName = Column{ + name: projection.SMTPConfigColumnFromName, + table: smtpConfigsTable, + } + SMTPConfigColumnSMTPHost = Column{ + name: projection.SMTPConfigColumnSMTPHost, + table: smtpConfigsTable, + } + SMTPConfigColumnSMTPUser = Column{ + name: projection.SMTPConfigColumnSMTPUser, + table: smtpConfigsTable, + } + SMTPConfigColumnSMTPPassword = Column{ + name: projection.SMTPConfigColumnSMTPPassword, + table: smtpConfigsTable, + } +) + +type SMTPConfigs struct { + SearchResponse + SMTPConfigs []*SMTPConfig +} + +type SMTPConfig struct { + AggregateID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + Sequence uint64 + + TLS bool + SenderAddress string + SenderName string + Host string + User string + Password *crypto.CryptoValue +} + +func (q *Queries) SMTPConfigByAggregateID(ctx context.Context, aggregateID string) (*SMTPConfig, error) { + stmt, scan := prepareSMTPConfigQuery() + query, args, err := stmt.Where(sq.Eq{ + SMTPConfigColumnAggregateID.identifier(): aggregateID, + }).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-3m9sl", "Errors.Query.SQLStatment") + } + + row := q.client.QueryRowContext(ctx, query, args...) + return scan(row) +} + +func prepareSMTPConfigQuery() (sq.SelectBuilder, func(*sql.Row) (*SMTPConfig, error)) { + password := new(crypto.CryptoValue) + + return sq.Select( + SMTPConfigColumnAggregateID.identifier(), + SMTPConfigColumnCreationDate.identifier(), + SMTPConfigColumnChangeDate.identifier(), + SMTPConfigColumnResourceOwner.identifier(), + SMTPConfigColumnSequence.identifier(), + SMTPConfigColumnTLS.identifier(), + SMTPConfigColumnFromAddress.identifier(), + SMTPConfigColumnFromName.identifier(), + SMTPConfigColumnSMTPHost.identifier(), + SMTPConfigColumnSMTPUser.identifier(), + SMTPConfigColumnSMTPPassword.identifier()). + From(smtpConfigsTable.identifier()).PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*SMTPConfig, error) { + config := new(SMTPConfig) + err := row.Scan( + &config.AggregateID, + &config.CreationDate, + &config.ChangeDate, + &config.ResourceOwner, + &config.Sequence, + &config.TLS, + &config.SenderAddress, + &config.SenderName, + &config.Host, + &config.User, + &password, + ) + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-fwofw", "Errors.SMTPConfig.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-9k87F", "Errors.Internal") + } + config.Password = password + return config, nil + } +} diff --git a/internal/query/smtp_test.go b/internal/query/smtp_test.go new file mode 100644 index 0000000000..b818bc82af --- /dev/null +++ b/internal/query/smtp_test.go @@ -0,0 +1,148 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" + + "github.com/caos/zitadel/internal/crypto" + errs "github.com/caos/zitadel/internal/errors" +) + +func Test_SMTPConfigsPrepares(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareSMTPConfigQuery no result", + prepare: prepareSMTPConfigQuery, + want: want{ + sqlExpectations: mockQueries( + `SELECT zitadel.projections.smtp_configs.aggregate_id,`+ + ` zitadel.projections.smtp_configs.creation_date,`+ + ` zitadel.projections.smtp_configs.change_date,`+ + ` zitadel.projections.smtp_configs.resource_owner,`+ + ` zitadel.projections.smtp_configs.sequence,`+ + ` zitadel.projections.smtp_configs.tls,`+ + ` zitadel.projections.smtp_configs.sender_address,`+ + ` zitadel.projections.smtp_configs.sender_name,`+ + ` zitadel.projections.smtp_configs.host,`+ + ` zitadel.projections.smtp_configs.username,`+ + ` zitadel.projections.smtp_configs.password`+ + ` FROM zitadel.projections.smtp_configs`, + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*SMTPConfig)(nil), + }, + { + name: "prepareSMTPConfigQuery found", + prepare: prepareSMTPConfigQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(`SELECT zitadel.projections.smtp_configs.aggregate_id,`+ + ` zitadel.projections.smtp_configs.creation_date,`+ + ` zitadel.projections.smtp_configs.change_date,`+ + ` zitadel.projections.smtp_configs.resource_owner,`+ + ` zitadel.projections.smtp_configs.sequence,`+ + ` zitadel.projections.smtp_configs.tls,`+ + ` zitadel.projections.smtp_configs.sender_address,`+ + ` zitadel.projections.smtp_configs.sender_name,`+ + ` zitadel.projections.smtp_configs.host,`+ + ` zitadel.projections.smtp_configs.username,`+ + ` zitadel.projections.smtp_configs.password`+ + ` FROM zitadel.projections.smtp_configs`), + []string{ + "aggregate_id", + "creation_date", + "change_date", + "resource_owner", + "sequence", + "tls", + "sender_address", + "sender_name", + "smtp_host", + "smtp_user", + "smtp_password", + }, + []driver.Value{ + "agg-id", + testNow, + testNow, + "ro", + uint64(20211108), + true, + "sender", + "name", + "host", + "user", + &crypto.CryptoValue{}, + }, + ), + }, + object: &SMTPConfig{ + AggregateID: "agg-id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211108, + TLS: true, + SenderAddress: "sender", + SenderName: "name", + Host: "host", + User: "user", + Password: &crypto.CryptoValue{}, + }, + }, + { + name: "prepareSMTPConfigQuery sql err", + prepare: prepareSMTPConfigQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(`SELECT zitadel.projections.smtp_configs.aggregate_id,`+ + ` zitadel.projections.smtp_configs.creation_date,`+ + ` zitadel.projections.smtp_configs.change_date,`+ + ` zitadel.projections.smtp_configs.resource_owner,`+ + ` zitadel.projections.smtp_configs.sequence,`+ + ` zitadel.projections.smtp_configs.tls,`+ + ` zitadel.projections.smtp_configs.sender_address,`+ + ` zitadel.projections.smtp_configs.sender_name,`+ + ` zitadel.projections.smtp_configs.host,`+ + ` zitadel.projections.smtp_configs.username,`+ + ` zitadel.projections.smtp_configs.password`+ + ` FROM zitadel.projections.smtp_configs`), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) + }) + } +} diff --git a/internal/repository/iam/event_default_language.go b/internal/repository/iam/event_default_language.go new file mode 100644 index 0000000000..d45320d8db --- /dev/null +++ b/internal/repository/iam/event_default_language.go @@ -0,0 +1,57 @@ +package iam + +import ( + "context" + "encoding/json" + + "github.com/caos/zitadel/internal/eventstore" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/repository" +) + +const ( + DefaultLanguageSetEventType eventstore.EventType = "iam.default.language.set" +) + +type DefaultLanguageSetEvent struct { + eventstore.BaseEvent `json:"-"` + + Language language.Tag `json:"language"` +} + +func (e *DefaultLanguageSetEvent) Data() interface{} { + return e +} + +func (e *DefaultLanguageSetEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewDefaultLanguageSetEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + language language.Tag, +) *DefaultLanguageSetEvent { + return &DefaultLanguageSetEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + DefaultLanguageSetEventType, + ), + Language: language, + } +} + +func DefaultLanguageSetMapper(event *repository.Event) (eventstore.Event, error) { + e := &DefaultLanguageSetEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IAM-3j9fs", "unable to unmarshal default language set") + } + + return e, nil +} diff --git a/internal/repository/iam/eventstore.go b/internal/repository/iam/eventstore.go index c173a0eef3..764cf4cc94 100644 --- a/internal/repository/iam/eventstore.go +++ b/internal/repository/iam/eventstore.go @@ -9,6 +9,13 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(SetupDoneEventType, SetupStepMapper). RegisterFilterEventMapper(GlobalOrgSetEventType, GlobalOrgSetMapper). RegisterFilterEventMapper(ProjectSetEventType, ProjectSetMapper). + RegisterFilterEventMapper(DefaultLanguageSetEventType, DefaultLanguageSetMapper). + RegisterFilterEventMapper(SecretGeneratorAddedEventType, SecretGeneratorAddedEventMapper). + RegisterFilterEventMapper(SecretGeneratorChangedEventType, SecretGeneratorChangedEventMapper). + RegisterFilterEventMapper(SecretGeneratorRemovedEventType, SecretGeneratorRemovedEventMapper). + RegisterFilterEventMapper(SMTPConfigAddedEventType, SMTPConfigAddedEventMapper). + RegisterFilterEventMapper(SMTPConfigChangedEventType, SMTPConfigChangedEventMapper). + RegisterFilterEventMapper(SMTPConfigPasswordChangedEventType, SMTPConfigPasswordChangedEventMapper). RegisterFilterEventMapper(UniqueConstraintsMigratedEventType, MigrateUniqueConstraintEventMapper). RegisterFilterEventMapper(LabelPolicyAddedEventType, LabelPolicyAddedEventMapper). RegisterFilterEventMapper(LabelPolicyChangedEventType, LabelPolicyChangedEventMapper). diff --git a/internal/repository/iam/secret_generator.go b/internal/repository/iam/secret_generator.go new file mode 100644 index 0000000000..c53e0cf40a --- /dev/null +++ b/internal/repository/iam/secret_generator.go @@ -0,0 +1,228 @@ +package iam + +import ( + "context" + "encoding/json" + "time" + + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" +) + +const ( + UniqueSecretGeneratorType = "secret_generator" + secretGeneratorPrefix = "secret.generator." + SecretGeneratorAddedEventType = iamEventTypePrefix + secretGeneratorPrefix + "added" + SecretGeneratorChangedEventType = iamEventTypePrefix + secretGeneratorPrefix + "changed" + SecretGeneratorRemovedEventType = iamEventTypePrefix + secretGeneratorPrefix + "removed" +) + +func NewAddSecretGeneratorTypeUniqueConstraint(generatorType domain.SecretGeneratorType) *eventstore.EventUniqueConstraint { + return eventstore.NewAddEventUniqueConstraint( + UniqueSecretGeneratorType, + string(generatorType), + "Errors.SecretGenerator.AlreadyExists") +} + +func NewRemoveSecretGeneratorTypeUniqueConstraint(generatorType domain.SecretGeneratorType) *eventstore.EventUniqueConstraint { + return eventstore.NewRemoveEventUniqueConstraint( + UniqueSecretGeneratorType, + string(generatorType)) +} + +type SecretGeneratorAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + GeneratorType domain.SecretGeneratorType `json:"generatorType"` + Length uint `json:"length,omitempty"` + Expiry time.Duration `json:"expiry,omitempty"` + IncludeLowerLetters bool `json:"includeLowerLetters,omitempty"` + IncludeUpperLetters bool `json:"includeUpperLetters,omitempty"` + IncludeDigits bool `json:"includeDigits,omitempty"` + IncludeSymbols bool `json:"includeSymbols,omitempty"` +} + +func NewSecretGeneratorAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + generatorType domain.SecretGeneratorType, + length uint, + expiry time.Duration, + includeLowerLetters, + includeUpperLetters, + includeDigits, + includeSymbols bool, +) *SecretGeneratorAddedEvent { + return &SecretGeneratorAddedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + SecretGeneratorAddedEventType, + ), + GeneratorType: generatorType, + Length: length, + Expiry: expiry, + IncludeLowerLetters: includeLowerLetters, + IncludeUpperLetters: includeUpperLetters, + IncludeDigits: includeDigits, + IncludeSymbols: includeSymbols, + } +} + +func (e *SecretGeneratorAddedEvent) Data() interface{} { + return e +} + +func (e *SecretGeneratorAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return []*eventstore.EventUniqueConstraint{NewAddSecretGeneratorTypeUniqueConstraint(e.GeneratorType)} +} + +func SecretGeneratorAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + secretGeneratorAdded := &SecretGeneratorAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := json.Unmarshal(event.Data, secretGeneratorAdded) + if err != nil { + return nil, errors.ThrowInternal(err, "IAM-en9f4", "unable to unmarshal secret generator added") + } + + return secretGeneratorAdded, nil +} + +type SecretGeneratorChangedEvent struct { + eventstore.BaseEvent `json:"-"` + + GeneratorType domain.SecretGeneratorType `json:"generatorType"` + Length *uint `json:"length,omitempty"` + Expiry *time.Duration `json:"expiry,omitempty"` + IncludeLowerLetters *bool `json:"includeLowerLetters,omitempty"` + IncludeUpperLetters *bool `json:"includeUpperLetters,omitempty"` + IncludeDigits *bool `json:"includeDigits,omitempty"` + IncludeSymbols *bool `json:"includeSymbols,omitempty"` +} + +func (e *SecretGeneratorChangedEvent) Data() interface{} { + return e +} + +func (e *SecretGeneratorChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewSecretGeneratorChangeEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + generatorType domain.SecretGeneratorType, + changes []SecretGeneratorChanges, +) (*SecretGeneratorChangedEvent, error) { + if len(changes) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "IAM-j2jfw", "Errors.NoChangesFound") + } + changeEvent := &SecretGeneratorChangedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + SecretGeneratorChangedEventType, + ), + GeneratorType: generatorType, + } + for _, change := range changes { + change(changeEvent) + } + return changeEvent, nil +} + +type SecretGeneratorChanges func(event *SecretGeneratorChangedEvent) + +func ChangeSecretGeneratorLength(length uint) func(event *SecretGeneratorChangedEvent) { + return func(e *SecretGeneratorChangedEvent) { + e.Length = &length + } +} + +func ChangeSecretGeneratorExpiry(expiry time.Duration) func(event *SecretGeneratorChangedEvent) { + return func(e *SecretGeneratorChangedEvent) { + e.Expiry = &expiry + } +} + +func ChangeSecretGeneratorIncludeLowerLetters(includeLowerLetters bool) func(event *SecretGeneratorChangedEvent) { + return func(e *SecretGeneratorChangedEvent) { + e.IncludeLowerLetters = &includeLowerLetters + } +} + +func ChangeSecretGeneratorIncludeUpperLetters(includeUpperLetters bool) func(event *SecretGeneratorChangedEvent) { + return func(e *SecretGeneratorChangedEvent) { + e.IncludeUpperLetters = &includeUpperLetters + } +} + +func ChangeSecretGeneratorIncludeDigits(includeDigits bool) func(event *SecretGeneratorChangedEvent) { + return func(e *SecretGeneratorChangedEvent) { + e.IncludeDigits = &includeDigits + } +} + +func ChangeSecretGeneratorIncludeSymbols(includeSymbols bool) func(event *SecretGeneratorChangedEvent) { + return func(e *SecretGeneratorChangedEvent) { + e.IncludeDigits = &includeSymbols + } +} + +func SecretGeneratorChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &SecretGeneratorChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IAM-2m09e", "unable to unmarshal secret generator changed") + } + + return e, nil +} + +type SecretGeneratorRemovedEvent struct { + eventstore.BaseEvent `json:"-"` + + GeneratorType domain.SecretGeneratorType `json:"generatorType"` +} + +func (e *SecretGeneratorRemovedEvent) Data() interface{} { + return e +} + +func (e *SecretGeneratorRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return []*eventstore.EventUniqueConstraint{NewRemoveSecretGeneratorTypeUniqueConstraint(e.GeneratorType)} +} + +func NewSecretGeneratorRemovedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + generatorType domain.SecretGeneratorType, +) *SecretGeneratorRemovedEvent { + return &SecretGeneratorRemovedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + SecretGeneratorRemovedEventType, + ), + GeneratorType: generatorType, + } +} + +func SecretGeneratorRemovedEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &SecretGeneratorRemovedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IAM-m09ke", "unable to unmarshal secret generator removed") + } + + return e, nil +} diff --git a/internal/repository/iam/smtp_config.go b/internal/repository/iam/smtp_config.go new file mode 100644 index 0000000000..91e2c26e6f --- /dev/null +++ b/internal/repository/iam/smtp_config.go @@ -0,0 +1,199 @@ +package iam + +import ( + "context" + "encoding/json" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" +) + +const ( + smtpConfigPrefix = "smtp.config" + SMTPConfigAddedEventType = iamEventTypePrefix + smtpConfigPrefix + "added" + SMTPConfigChangedEventType = iamEventTypePrefix + smtpConfigPrefix + "changed" + SMTPConfigPasswordChangedEventType = iamEventTypePrefix + smtpConfigPrefix + "password.changed" +) + +type SMTPConfigAddedEvent struct { + eventstore.BaseEvent `json:"-"` + + SenderAddress string `json:"senderAddress,omitempty"` + SenderName string `json:"senderName,omitempty"` + TLS bool `json:"tls,omitempty"` + Host string `json:"host,omitempty"` + User string `json:"user,omitempty"` + Password *crypto.CryptoValue `json:"password,omitempty"` +} + +func NewSMTPConfigAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + tls bool, + senderAddress, + senderName, + host, + user string, + password *crypto.CryptoValue, +) *SMTPConfigAddedEvent { + return &SMTPConfigAddedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + SMTPConfigAddedEventType, + ), + TLS: tls, + SenderAddress: senderAddress, + SenderName: senderName, + Host: host, + User: user, + Password: password, + } +} + +func (e *SMTPConfigAddedEvent) Data() interface{} { + return e +} + +func (e *SMTPConfigAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func SMTPConfigAddedEventMapper(event *repository.Event) (eventstore.Event, error) { + smtpConfigAdded := &SMTPConfigAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := json.Unmarshal(event.Data, smtpConfigAdded) + if err != nil { + return nil, errors.ThrowInternal(err, "IAM-39fks", "unable to unmarshal smtp config added") + } + + return smtpConfigAdded, nil +} + +type SMTPConfigChangedEvent struct { + eventstore.BaseEvent `json:"-"` + + FromAddress *string `json:"senderAddress,omitempty"` + FromName *string `json:"senderName,omitempty"` + TLS *bool `json:"tls,omitempty"` + Host *string `json:"host,omitempty"` + User *string `json:"user,omitempty"` +} + +func (e *SMTPConfigChangedEvent) Data() interface{} { + return e +} + +func (e *SMTPConfigChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewSMTPConfigChangeEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + changes []SMTPConfigChanges, +) (*SMTPConfigChangedEvent, error) { + if len(changes) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "IAM-o0pWf", "Errors.NoChangesFound") + } + changeEvent := &SMTPConfigChangedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + SMTPConfigChangedEventType, + ), + } + for _, change := range changes { + change(changeEvent) + } + return changeEvent, nil +} + +type SMTPConfigChanges func(event *SMTPConfigChangedEvent) + +func ChangeSMTPConfigTLS(tls bool) func(event *SMTPConfigChangedEvent) { + return func(e *SMTPConfigChangedEvent) { + e.TLS = &tls + } +} + +func ChangeSMTPConfigFromAddress(senderAddress string) func(event *SMTPConfigChangedEvent) { + return func(e *SMTPConfigChangedEvent) { + e.FromAddress = &senderAddress + } +} + +func ChangeSMTPConfigFromName(senderName string) func(event *SMTPConfigChangedEvent) { + return func(e *SMTPConfigChangedEvent) { + e.FromName = &senderName + } +} + +func ChangeSMTPConfigSMTPHost(smtpHost string) func(event *SMTPConfigChangedEvent) { + return func(e *SMTPConfigChangedEvent) { + e.Host = &smtpHost + } +} + +func ChangeSMTPConfigSMTPUser(smtpUser string) func(event *SMTPConfigChangedEvent) { + return func(e *SMTPConfigChangedEvent) { + e.User = &smtpUser + } +} + +func SMTPConfigChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &SMTPConfigChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IAM-m09oo", "unable to unmarshal smtp changed") + } + + return e, nil +} + +type SMTPConfigPasswordChangedEvent struct { + eventstore.BaseEvent `json:"-"` + + Password *crypto.CryptoValue `json:"password,omitempty"` +} + +func NewSMTPConfigPasswordChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + password *crypto.CryptoValue, +) *SMTPConfigPasswordChangedEvent { + return &SMTPConfigPasswordChangedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + SMTPConfigPasswordChangedEventType, + ), + Password: password, + } +} + +func (e *SMTPConfigPasswordChangedEvent) Data() interface{} { + return e +} + +func (e *SMTPConfigPasswordChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func SMTPConfigPasswordChangedEventMapper(event *repository.Event) (eventstore.Event, error) { + smtpConfigPasswordChagned := &SMTPConfigPasswordChangedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := json.Unmarshal(event.Data, smtpConfigPasswordChagned) + if err != nil { + return nil, errors.ThrowInternal(err, "IAM-99iNF", "unable to unmarshal smtp config password changed") + } + + return smtpConfigPasswordChagned, nil +} diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 8752ea4622..01eecb3bb9 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -27,6 +27,8 @@ Errors: RemoveFailed: Objekt konnte nicht gelöscht werden Limit: ExceedsDefault: Limit überschreitet default Limit + Language: + NotParsed: Sprache konnte nicht gemapped werde User: NotFound: Benutzer konnte nicht gefunden werden AlreadyExists: Benutzer existierts bereits diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 8041c9c19f..70bb408df2 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -27,6 +27,8 @@ Errors: RemoveFailed: Object could not be removed Limit: ExceedsDefault: Limit exceeds default limit + Language: + NotParsed: Could not parse languge User: NotFound: User could not be found AlreadyExists: User already exists diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 0e0d88c964..92ea347056 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -27,6 +27,8 @@ Errors: RemoveFailed: L'oggetto non può essere rimosso Limit: ExceedsDefault: Il limite supera quello predefinito + Language: + NotParsed: Impossibile analizzare la lingua User: NotFound: L'utente non è stato trovato AlreadyExists: L'utente già esistente diff --git a/migrations/cockroach/V1.111__settings.sql b/migrations/cockroach/V1.111__settings.sql new file mode 100644 index 0000000000..3a5de4368a --- /dev/null +++ b/migrations/cockroach/V1.111__settings.sql @@ -0,0 +1,36 @@ +ALTER TABLE zitadel.projections.iam ADD COLUMN default_language TEXT DEFAULT ''; + +CREATE TABLE zitadel.projections.secret_generators ( + generator_type INT2 NOT NULL + , aggregate_id STRING NOT NULL + , creation_date TIMESTAMPTZ NOT NULL + , change_date TIMESTAMPTZ NOT NULL + , resource_owner STRING NOT NULL + , sequence INT8 NOT NULL + + , length BIGINT NOT NULL + , expiry BIGINT NOT NULL + , include_lower_letters BOOLEAN NOT NULL + , include_upper_letters BOOLEAN NOT NULL + , include_digits BOOLEAN NOT NULL + , include_symbols BOOLEAN NOT NULL + + , PRIMARY KEY (generator_type, aggregate_id) +); + +CREATE TABLE zitadel.projections.smtp_configs ( + aggregate_id STRING NOT NULL + , creation_date TIMESTAMPTZ NOT NULL + , change_date TIMESTAMPTZ NOT NULL + , resource_owner STRING NOT NULL + , sequence INT8 NOT NULL + + , tls BOOLEAN NOT NULL + , sender_address STRING NOT NULL + , sender_name STRING NOT NULL + , host STRING NOT NULL + , username STRING NOT NULL DEFAULT '' + , password JSONB + + , PRIMARY KEY (aggregate_id) +); diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index a6b89d739f..daedc780ce 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -2,6 +2,7 @@ syntax = "proto3"; import "zitadel/idp.proto"; import "zitadel/user.proto"; +import "zitadel/settings.proto"; import "zitadel/object.proto"; import "zitadel/options.proto"; import "zitadel/org.proto"; @@ -161,6 +162,98 @@ service AdminService { }; } + // Set the default language + rpc SetDefaultLanguage(SetDefaultLanguageRequest) returns (SetDefaultLanguageResponse) { + option (google.api.http) = { + put: "/languages/default/{language}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + } + + // Set the default language + rpc GetDefaultLanguage(GetDefaultLanguageRequest) returns (GetDefaultLanguageResponse) { + option (google.api.http) = { + get: "/languages/default"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.read"; + }; + } + + // Set the default language + rpc ListSecretGenerators(ListSecretGeneratorsRequest) returns (ListSecretGeneratorsResponse) { + option (google.api.http) = { + post: "/secretgenerators/_search" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.read"; + }; + } + + // Get Secret Generator by type (e.g PasswordResetCode) + rpc GetSecretGenerator(GetSecretGeneratorRequest) returns (GetSecretGeneratorResponse) { + option (google.api.http) = { + get: "/secretgenerators/{generator_type}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.read"; + }; + } + + // Update secret generator configuration + rpc UpdateSecretGenerator(UpdateSecretGeneratorRequest) returns (UpdateSecretGeneratorResponse) { + option (google.api.http) = { + put: "/secretgenerators/{generator_type}"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + } + + // Get system smtp configuration + rpc GetSMTPConfig(GetSMTPConfigRequest) returns (GetSMTPConfigResponse) { + option (google.api.http) = { + get: "/smtp"; + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.read"; + }; + } + + // Update system smtp configuration + rpc UpdateSMTPConfig(UpdateSMTPConfigRequest) returns (UpdateSMTPConfigResponse) { + option (google.api.http) = { + put: "/smtp"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + } + + // Update system smtp configuration password for host + rpc UpdateSMTPConfigPassword(UpdateSMTPConfigPasswordRequest) returns (UpdateSMTPConfigPasswordResponse) { + option (google.api.http) = { + put: "/smtp/password"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + } + // Returns an organisation by id rpc GetOrgByID(GetOrgByIDRequest) returns (GetOrgByIDResponse) { option (google.api.http) = { @@ -2262,6 +2355,82 @@ message GetSupportedLanguagesResponse { repeated string languages = 1; } +message SetDefaultLanguageRequest { + string language = 1 [(validate.rules).string = {min_len: 1, max_len: 10}]; +} + +message SetDefaultLanguageResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message GetDefaultLanguageRequest {} + +message GetDefaultLanguageResponse { + string language = 1; +} + +message ListSecretGeneratorsRequest { + //list limitations and ordering + zitadel.v1.ListQuery query = 1; + //criterias the client is looking for + repeated zitadel.settings.v1.SecretGeneratorQuery queries = 2; +} + +message ListSecretGeneratorsResponse { + zitadel.v1.ListDetails details = 1; + repeated zitadel.settings.v1.SecretGenerator result = 3; +} + +message GetSecretGeneratorRequest { + zitadel.settings.v1.SecretGeneratorType generator_type = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; +} + +message GetSecretGeneratorResponse { + zitadel.settings.v1.SecretGenerator secret_generator = 1; +} + +message UpdateSecretGeneratorRequest { + zitadel.settings.v1.SecretGeneratorType generator_type = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; + uint32 length = 2; + google.protobuf.Duration expiry = 3; + bool include_lower_letters = 4; + bool include_upper_letters = 5; + bool include_digits = 6; + bool include_symbols = 7; +} + +message UpdateSecretGeneratorResponse { + zitadel.v1.ObjectDetails details = 1; +} + +//This is an empty request +message GetSMTPConfigRequest {} + +message GetSMTPConfigResponse { + zitadel.settings.v1.SMTPConfig smtp_config = 1; +} + +message UpdateSMTPConfigRequest { + string sender_address = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string sender_name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + bool tls = 3; + string host = 4 [(validate.rules).string = {min_len: 1, max_len: 500}]; + string user = 5; +} + +message UpdateSMTPConfigResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message UpdateSMTPConfigPasswordRequest { + string password = 1; +} + +message UpdateSMTPConfigPasswordResponse { + zitadel.v1.ObjectDetails details = 1; +} + // if name or domain is already in use, org is not unique message IsOrgUniqueRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 48c9c3402e..0e83dc66d6 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -290,6 +290,7 @@ service ManagementService { rpc UpdateUserName(UpdateUserNameRequest) returns (UpdateUserNameResponse) { option (google.api.http) = { put: "/users/{user_id}/username" + body: "*" }; option (zitadel.v1.auth_option) = { diff --git a/proto/zitadel/settings.proto b/proto/zitadel/settings.proto new file mode 100644 index 0000000000..81de52fd96 --- /dev/null +++ b/proto/zitadel/settings.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; + +import "zitadel/object.proto"; +import "validate/validate.proto"; +import "google/protobuf/duration.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +package zitadel.settings.v1; + +option go_package ="github.com/caos/zitadel/pkg/grpc/settings"; + +message SecretGenerator { + SecretGeneratorType generator_type = 1; + zitadel.v1.ObjectDetails details = 2; + uint32 length = 3; + google.protobuf.Duration expiry = 4; + bool include_lower_letters = 5; + bool include_upper_letters = 6; + bool include_digits = 7; + bool include_symbols = 8; +} + + +message SecretGeneratorQuery { + oneof query { + option (validate.required) = true; + + SecretGeneratorTypeQuery type_query = 1; + } +} + +message SecretGeneratorTypeQuery { + SecretGeneratorType generator_type = 1; +} + +enum SecretGeneratorType { + SECRET_GENERATOR_TYPE_UNSPECIFIED = 0; + SECRET_GENERATOR_TYPE_INIT_CODE = 1; + SECRET_GENERATOR_TYPE_VERIFY_EMAIL_CODE = 2; + SECRET_GENERATOR_TYPE_VERIFY_PHONE_CODE = 3; + SECRET_GENERATOR_TYPE_PASSWORD_RESET_CODE = 4; + SECRET_GENERATOR_TYPE_PASSWORDLESS_INIT_CODE = 5; + SECRET_GENERATOR_TYPE_APP_SECRET = 6; +} + +message SMTPConfig { + zitadel.v1.ObjectDetails details = 1; + string sender_address = 2; + string sender_name = 3; + bool tls = 4; + string host = 5; + string user = 6; +}