diff --git a/.gitignore b/.gitignore index b2f4277b2c..8b8a107f07 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ sandbox.go .idea .vscode .DS_STORE +.run # credential google-credentials diff --git a/Makefile b/Makefile index 2a36edfe9f..e3936b8379 100644 --- a/Makefile +++ b/Makefile @@ -103,7 +103,7 @@ core_unit_test: core_integration_setup: go build -o zitadel main.go ./zitadel init --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml - ./zitadel setup --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml + ./zitadel setup --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml --steps internal/integration/config/zitadel.yaml --steps internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml $(RM) zitadel .PHONY: core_integration_test diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 5daeec650e..56424b9e01 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -838,6 +838,11 @@ DefaultInstance: # DisallowPublicOrgRegistration defines if ZITADEL should expose the endpoint /ui/login/register/org # If it is true, the endpoint returns the HTTP status 404 on GET requests, and 409 on POST requests. DisallowPublicOrgRegistration: # ZITADEL_DEFAULTINSTANCE_RESTRICTIONS_DISALLOWPUBLICORGREGISTRATION + # AllowedLanguages restricts the languages that can be used. + # If the list is empty, all supported languages are allowed. + AllowedLanguages: # ZITADEL_DEFAULTINSTANCE_RESTRICTIONS_ALLOWEDLANGUAGES + # - en + # - de Quotas: # Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist. # The following unit types are supported diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index a31c5e9ae2..c1d354d825 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -16,6 +16,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql" new_es "github.com/zitadel/zitadel/internal/eventstore/v3" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/migration" "github.com/zitadel/zitadel/internal/query/projection" ) @@ -64,6 +65,8 @@ func Setup(config *Config, steps *Steps, masterKey string) { ctx := context.Background() logging.Info("setup started") + i18n.MustLoadSupportedLanguagesFromDir() + zitadelDBClient, err := database.Connect(config.Database, false, false) logging.OnError(err).Fatal("unable to connect to database") esPusherDBClient, err := database.Connect(config.Database, false, true) diff --git a/cmd/start/start.go b/cmd/start/start.go index 519772ef7c..794d629a59 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -62,6 +62,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql" new_es "github.com/zitadel/zitadel/internal/eventstore/v3" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/logstore" "github.com/zitadel/zitadel/internal/logstore/emitters/access" @@ -93,7 +94,6 @@ Requirements: if err != nil { return err } - return startZitadel(config, masterKey, server) }, } @@ -123,6 +123,8 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error ctx := context.Background() + i18n.MustLoadSupportedLanguagesFromDir() + zitadelDBClient, err := database.Connect(config.Database, false, false) if err != nil { return fmt.Errorf("cannot start client for projection: %w", err) diff --git a/docs/docs/guides/manage/customize/restrictions.md b/docs/docs/guides/manage/customize/restrictions.md index 443dd0a948..4ad29cef7a 100644 --- a/docs/docs/guides/manage/customize/restrictions.md +++ b/docs/docs/guides/manage/customize/restrictions.md @@ -8,7 +8,11 @@ Users with the role IAM_OWNER can change the restrictions of their instance usin Currently, the following restrictions are available: - *Disallow public organization registrations* - If restricted, only users with the role IAM_OWNERS can create new organizations. The endpoint */ui/login/register/org* returns HTTP status 404 on GET requests, and 409 on POST requests. -- *[Coming soon](https://github.com/zitadel/zitadel/issues/6250): AllowedLanguages* +- *AllowedLanguages* - The following rules apply if languages are restricted: + - Only allowed languages are listed in the OIDC discovery endpoint */.well-kown/openid-configuration*. + - Login UI texts are only rendered in allowed languages. + - Notification message texts are only rendered in allowed languages. + - Custom Texts can be created for disallowed languages as long as ZITADEL supports that language. Therefore, all texts can be customized before allowing a language. Feature restrictions for an instance are intended to be configured by a user that is managed within that instance. However, if you are self-hosting and need to control your virtual instances usage, [read about the APIs for limits and quotas](/self-hosting/manage/usage_control) that are intended to be used by system users. diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 6b1e54fefa..f464a44702 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -12,17 +12,17 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/zitadel/logging" "google.golang.org/api/option" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/durationpb" - "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/authz" action_grpc "github.com/zitadel/zitadel/internal/api/grpc/action" "github.com/zitadel/zitadel/internal/api/grpc/authn" "github.com/zitadel/zitadel/internal/api/grpc/management" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -34,10 +34,10 @@ import ( type importResponse struct { ret *admin_pb.ImportDataResponse - count *count + count *counts err error } -type count struct { +type counts struct { humanUserCount int humanUserLen int machineUserCount int @@ -70,7 +70,7 @@ type count struct { machineKeysCount int } -func (c *count) getProgress() string { +func (c *counts) getProgress() string { return "progress:" + "human_users " + strconv.Itoa(c.humanUserCount) + "/" + strconv.Itoa(c.humanUserLen) + ", " + "machine_users " + strconv.Itoa(c.machineUserCount) + "/" + strconv.Itoa(c.machineUserLen) + ", " + @@ -91,7 +91,6 @@ func (c *count) getProgress() string { func (s *Server) ImportData(ctx context.Context, req *admin_pb.ImportDataRequest) (_ *admin_pb.ImportDataResponse, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if req.GetDataOrgs() != nil || req.GetDataOrgsv1() != nil { timeoutDuration, err := time.ParseDuration(req.Timeout) if err != nil { @@ -293,10 +292,736 @@ func getFileFromGCS(ctx context.Context, input *admin_pb.ImportDataRequest_GCSIn return ioutil.ReadAll(reader) } -func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*admin_pb.ImportDataResponse, *count, error) { +func importOrg1(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, ctxData authz.CtxData, org *admin_pb.DataOrg, success *admin_pb.ImportDataSuccess, count *counts, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode, appSecretGenerator crypto.Generator) error { + _, err := s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), []string{}) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "org", Id: org.GetOrgId(), Message: err.Error()}) + if _, err := s.query.OrgByID(ctx, true, org.OrgId); err != nil { + // TODO: Only nil if err != not found + return nil + } + } + successOrg := &admin_pb.ImportDataSuccessOrg{ + OrgId: org.GetOrgId(), + ProjectIds: []string{}, + OidcAppIds: []string{}, + ApiAppIds: []string{}, + HumanUserIds: []string{}, + MachineUserIds: []string{}, + ActionIds: []string{}, + ProjectGrants: []*admin_pb.ImportDataSuccessProjectGrant{}, + UserGrants: []*admin_pb.ImportDataSuccessUserGrant{}, + OrgMembers: []string{}, + ProjectMembers: []*admin_pb.ImportDataSuccessProjectMember{}, + ProjectGrantMembers: []*admin_pb.ImportDataSuccessProjectGrantMember{}, + } + logging.Debugf("successful org: %s", successOrg.OrgId) + success.Orgs = append(success.Orgs, successOrg) + + domainPolicy := org.GetDomainPolicy() + if org.DomainPolicy != nil { + _, err := s.command.AddOrgDomainPolicy(ctx, org.GetOrgId(), domainPolicy.UserLoginMustBeDomain, domainPolicy.ValidateOrgDomains, domainPolicy.SmtpSenderAddressMatchesInstanceDomain) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "domain_policy", Id: org.GetOrgId(), Message: err.Error()}) + } + } + return importResources(ctx, s, errors, successOrg, org, count, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode, appSecretGenerator) +} + +func importLabelPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) error { + if org.LabelPolicy == nil { + return nil + } + _, err := s.command.AddLabelPolicy(ctx, org.GetOrgId(), management.AddLabelPolicyToDomain(org.GetLabelPolicy())) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "label_policy", Id: org.GetOrgId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + } else { + _, err = s.command.ActivateLabelPolicy(ctx, org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "label_policy", Id: org.GetOrgId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + } + } + return nil +} + +func importLockoutPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.LockoutPolicy == nil { + return + } + _, err := s.command.AddLockoutPolicy(ctx, org.GetOrgId(), management.AddLockoutPolicyToDomain(org.GetLockoutPolicy())) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "lockout_policy", Id: org.GetOrgId(), Message: err.Error()}) + } +} + +func importOidcIdps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg) error { + if org.OidcIdps == nil { + return nil + } + for _, idp := range org.OidcIdps { + logging.Debugf("import oidcidp: %s", idp.IdpId) + _, err := s.command.ImportIDPConfig(ctx, management.AddOIDCIDPRequestToDomain(idp.Idp), idp.IdpId, org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "oidc_idp", Id: idp.IdpId, Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + logging.Debugf("successful oidcidp: %s", idp.GetIdpId()) + successOrg.OidcIpds = append(successOrg.OidcIpds, idp.GetIdpId()) + } + return nil +} + +func importJwtIdps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg) error { + if org.JwtIdps == nil { + return nil + } + for _, idp := range org.JwtIdps { + logging.Debugf("import jwtidp: %s", idp.IdpId) + _, err := s.command.ImportIDPConfig(ctx, management.AddJWTIDPRequestToDomain(idp.Idp), idp.IdpId, org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "jwt_idp", Id: idp.IdpId, Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + logging.Debugf("successful jwtidp: %s", idp.GetIdpId()) + successOrg.JwtIdps = append(successOrg.JwtIdps, idp.GetIdpId()) + } + return nil +} + +func importLoginPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.LoginPolicy == nil { + return + } + _, err := s.command.AddLoginPolicy(ctx, org.GetOrgId(), management.AddLoginPolicyToCommand(org.GetLoginPolicy())) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "login_policy", Id: org.GetOrgId(), Message: err.Error()}) + } +} + +func importPwComlexityPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.PasswordComplexityPolicy == nil { + return + } + _, err := s.command.AddPasswordComplexityPolicy(ctx, org.GetOrgId(), management.AddPasswordComplexityPolicyToDomain(org.GetPasswordComplexityPolicy())) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "password_complexity_policy", Id: org.GetOrgId(), Message: err.Error()}) + } +} + +func importPrivacyPolicy(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.PrivacyPolicy == nil { + return + } + _, err := s.command.AddPrivacyPolicy(ctx, org.GetOrgId(), management.AddPrivacyPolicyToDomain(org.GetPrivacyPolicy())) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "privacy_policy", Id: org.GetOrgId(), Message: err.Error()}) + } +} + +func importHumanUsers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode crypto.Generator) error { + if org.HumanUsers == nil { + return nil + } + for _, user := range org.GetHumanUsers() { + logging.Debugf("import user: %s", user.GetUserId()) + human, passwordless, links := management.ImportHumanUserRequestToDomain(user.User) + human.AggregateID = user.UserId + _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, links, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "human_user", Id: user.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + } else { + count.humanUserCount += 1 + logging.Debugf("successful user %d: %s", count.humanUserCount, user.GetUserId()) + successOrg.HumanUserIds = append(successOrg.HumanUserIds, user.GetUserId()) + } + + if user.User.OtpCode != "" { + logging.Debugf("import user otp: %s", user.GetUserId()) + if err := s.command.ImportHumanTOTP(ctx, user.UserId, "", org.GetOrgId(), user.User.OtpCode); err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "human_user_otp", Id: user.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + } else { + logging.Debugf("successful user otp: %s", user.GetUserId()) + } + } + } + return nil +} + +func importMachineUsers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.MachineUsers == nil { + return nil + } + for _, user := range org.GetMachineUsers() { + logging.Debugf("import user: %s", user.GetUserId()) + _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId())) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "machine_user", Id: user.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.machineUserCount += 1 + logging.Debugf("successful user %d: %s", count.machineUserCount, user.GetUserId()) + successOrg.MachineUserIds = append(successOrg.MachineUserIds, user.GetUserId()) + } + return nil +} + +func importUserMetadata(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.UserMetadata == nil { + return nil + } + for _, userMetadata := range org.GetUserMetadata() { + logging.Debugf("import usermetadata: %s", userMetadata.GetId()+"_"+userMetadata.GetKey()) + _, err := s.command.SetUserMetadata(ctx, &domain.Metadata{Key: userMetadata.GetKey(), Value: userMetadata.GetValue()}, userMetadata.GetId(), org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "user_metadata", Id: userMetadata.GetId() + "_" + userMetadata.GetKey(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.userMetadataCount += 1 + logging.Debugf("successful usermetadata %d: %s", count.userMetadataCount, userMetadata.GetId()+"_"+userMetadata.GetKey()) + successOrg.UserMetadata = append(successOrg.UserMetadata, &admin_pb.ImportDataSuccessUserMetadata{UserId: userMetadata.GetId(), Key: userMetadata.GetKey()}) + } + return nil +} + +func importMachineKeys(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.MachineKeys == nil { + return nil + } + for _, key := range org.GetMachineKeys() { + logging.Debugf("import machine_user_key: %s", key.KeyId) + _, err := s.command.AddUserMachineKey(ctx, &command.MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: key.UserId, + ResourceOwner: org.GetOrgId(), + }, + KeyID: key.KeyId, + Type: authn.KeyTypeToDomain(key.Type), + ExpirationDate: key.ExpirationDate.AsTime(), + PublicKey: key.PublicKey, + }) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "machine_user_key", Id: key.KeyId, Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.machineKeysCount += 1 + logging.Debugf("successful machine_user_key %d: %s", count.machineKeysCount, key.KeyId) + successOrg.MachineKeys = append(successOrg.MachineKeys, key.KeyId) + } + return nil +} + +func importUserLinks(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.UserLinks == nil { + return nil + } + for _, userLinks := range org.GetUserLinks() { + logging.Debugf("import userlink: %s", userLinks.GetUserId()+"_"+userLinks.GetIdpId()+"_"+userLinks.GetProvidedUserId()+"_"+userLinks.GetProvidedUserName()) + externalIDP := &command.AddLink{ + IDPID: userLinks.IdpId, + IDPExternalID: userLinks.ProvidedUserId, + DisplayName: userLinks.ProvidedUserName, + } + if _, err := s.command.AddUserIDPLink(ctx, userLinks.UserId, org.GetOrgId(), externalIDP); err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "user_link", Id: userLinks.UserId + "_" + userLinks.IdpId, Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.userLinksCount += 1 + logging.Debugf("successful userlink %d: %s", count.userLinksCount, userLinks.GetUserId()+"_"+userLinks.GetIdpId()+"_"+userLinks.GetProvidedUserId()+"_"+userLinks.GetProvidedUserName()) + successOrg.UserLinks = append(successOrg.UserLinks, &admin_pb.ImportDataSuccessUserLinks{UserId: userLinks.GetUserId(), IdpId: userLinks.GetIdpId(), ExternalUserId: userLinks.GetProvidedUserId(), DisplayName: userLinks.GetProvidedUserName()}) + } + return nil + +} + +func importProjects(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.Projects == nil { + return nil + } + for _, project := range org.GetProjects() { + logging.Debugf("import project: %s", project.GetProjectId()) + _, err := s.command.AddProjectWithID(ctx, management.ProjectCreateToDomain(project.GetProject()), org.GetOrgId(), project.GetProjectId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "project", Id: project.GetProjectId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.projectCount += 1 + logging.Debugf("successful project %d: %s", count.projectCount, project.GetProjectId()) + successOrg.ProjectIds = append(successOrg.ProjectIds, project.GetProjectId()) + } + return nil +} + +func importOIDCApps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts, appSecretGenerator crypto.Generator) error { + if org.OidcApps == nil { + return nil + } + for _, app := range org.GetOidcApps() { + logging.Debugf("import oidcapplication: %s", app.GetAppId()) + _, err := s.command.AddOIDCApplicationWithID(ctx, management.AddOIDCAppRequestToDomain(app.App), org.GetOrgId(), app.GetAppId(), appSecretGenerator) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "oidc_app", Id: app.GetAppId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.oidcAppCount += 1 + logging.Debugf("successful oidcapplication %d: %s", count.oidcAppCount, app.GetAppId()) + successOrg.OidcAppIds = append(successOrg.OidcAppIds, app.GetAppId()) + } + return nil +} + +func importAPIApps(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts, appSecretGenerator crypto.Generator) error { + if org.ApiApps == nil { + return nil + } + for _, app := range org.GetApiApps() { + logging.Debugf("import apiapplication: %s", app.GetAppId()) + _, err := s.command.AddAPIApplicationWithID(ctx, management.AddAPIAppRequestToDomain(app.GetApp()), org.GetOrgId(), app.GetAppId(), appSecretGenerator) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "api_app", Id: app.GetAppId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.apiAppCount += 1 + logging.Debugf("successful apiapplication %d: %s", count.apiAppCount, app.GetAppId()) + successOrg.ApiAppIds = append(successOrg.ApiAppIds, app.GetAppId()) + } + return nil +} + +func importAppKeys(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.AppKeys == nil { + return nil + } + for _, key := range org.GetAppKeys() { + logging.Debugf("import app_key: %s", key.Id) + _, err := s.command.AddApplicationKeyWithID(ctx, &domain.ApplicationKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: key.ProjectId, + ResourceOwner: org.GetOrgId(), + }, + ApplicationID: key.AppId, + ClientID: key.ClientId, + KeyID: key.Id, + Type: authn.KeyTypeToDomain(key.Type), + ExpirationDate: key.ExpirationDate.AsTime(), + PublicKey: key.PublicKey, + }, org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "app_key", Id: key.Id, Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.appKeysCount += 1 + logging.Debugf("successful app_key %d: %s", count.appKeysCount, key.Id) + successOrg.AppKeys = append(successOrg.AppKeys, key.Id) + } + return nil +} + +func importActions(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.Actions == nil { + return nil + } + for _, action := range org.GetActions() { + logging.Debugf("import action: %s", action.GetActionId()) + _, _, err := s.command.AddActionWithID(ctx, management.CreateActionRequestToDomain(action.GetAction()), org.GetOrgId(), action.GetActionId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "action", Id: action.GetActionId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.actionCount += 1 + logging.Debugf("successful action %d: %s", count.actionCount, action.GetActionId()) + successOrg.ActionIds = append(successOrg.ActionIds, action.ActionId) + } + return nil +} +func importProjectRoles(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) error { + if org.ProjectRoles == nil { + return nil + } + for _, role := range org.GetProjectRoles() { + logging.Debugf("import projectroles: %s", role.ProjectId+"_"+role.RoleKey) + _, err := s.command.AddProjectRole(ctx, management.AddProjectRoleRequestToDomain(role), org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_role", Id: role.ProjectId + "_" + role.RoleKey, Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.projectRolesCount += 1 + logging.Debugf("successful projectroles %d: %s", count.projectRolesCount, role.ProjectId+"_"+role.RoleKey) + successOrg.ProjectRoles = append(successOrg.ProjectRoles, successOrg.ActionIds...) + successOrg.ProjectRoles = append(successOrg.ProjectRoles, role.ProjectId+"_"+role.RoleKey) + } + return nil +} + +func importResources(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode, appSecretGenerator crypto.Generator) error { + if err := importOrgDomains(ctx, s, errors, successOrg, org); err != nil { + return err + } + if err := importLabelPolicy(ctx, s, errors, org); err != nil { + return err + } + importLockoutPolicy(ctx, s, errors, org) + if err := importOidcIdps(ctx, s, errors, successOrg, org); err != nil { + return err + } + if err := importJwtIdps(ctx, s, errors, successOrg, org); err != nil { + return err + } + importLoginPolicy(ctx, s, errors, org) + importPwComlexityPolicy(ctx, s, errors, org) + importPrivacyPolicy(ctx, s, errors, org) + importLoginTexts(ctx, s, errors, org) + importInitMessageTexts(ctx, s, errors, org) + importPWResetMessageTexts(ctx, s, errors, org) + importVerifyEmailMessageTexts(ctx, s, errors, org) + importVerifyPhoneMessageTexts(ctx, s, errors, org) + importDomainClaimedMessageTexts(ctx, s, errors, org) + importPasswordlessRegistrationMessageTexts(ctx, s, errors, org) + if err := importHumanUsers(ctx, s, errors, successOrg, org, count, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode); err != nil { + return err + } + if err := importMachineUsers(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + if err := importUserMetadata(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + if err := importMachineKeys(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + if err := importUserLinks(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + if err := importProjects(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + if err := importOIDCApps(ctx, s, errors, successOrg, org, count, appSecretGenerator); err != nil { + return err + } + if err := importAPIApps(ctx, s, errors, successOrg, org, count, appSecretGenerator); err != nil { + return err + } + if err := importAppKeys(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + if err := importActions(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + if err := importProjectRoles(ctx, s, errors, successOrg, org, count); err != nil { + return err + } + return nil +} + +func importOrgDomains(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg) error { + if org.Domains == nil { + return nil + } + for _, domainR := range org.Domains { + orgDomain := &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: org.GetOrgId(), + }, + Domain: domainR.DomainName, + Verified: domainR.IsVerified, + Primary: domainR.IsPrimary, + } + _, err := s.command.AddOrgDomain(ctx, org.GetOrgId(), domainR.DomainName, []string{}) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "domain", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + logging.Debugf("successful domain: %s", domainR.DomainName) + successOrg.Domains = append(successOrg.Domains, domainR.DomainName) + + if domainR.IsVerified { + if _, err := s.command.VerifyOrgDomain(ctx, org.GetOrgId(), domainR.DomainName); err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "domain_isverified", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) + } + } + if domainR.IsPrimary { + if _, err := s.command.SetPrimaryOrgDomain(ctx, orgDomain); err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "domain_isprimary", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) + } + } + } + return nil +} + +func importLoginTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.LoginTexts == nil { + return + } + for _, text := range org.GetLoginTexts() { + _, err := s.command.SetOrgLoginText(ctx, org.GetOrgId(), management.SetLoginCustomTextToDomain(text)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "login_texts", Id: org.GetOrgId() + "_" + text.Language, Message: err.Error()}) + } + } +} + +func importInitMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.InitMessages == nil { + return + } + for _, message := range org.GetInitMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetInitCustomTextToDomain(message)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "init_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } +} + +func importPWResetMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.PasswordResetMessages == nil { + return + } + for _, message := range org.GetPasswordResetMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetPasswordResetCustomTextToDomain(message)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "password_reset_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } +} + +func importVerifyEmailMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.VerifyEmailMessages == nil { + return + } + for _, message := range org.GetVerifyEmailMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetVerifyEmailCustomTextToDomain(message)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "verify_email_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } +} + +func importVerifyPhoneMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.VerifyPhoneMessages != nil { + return + } + for _, message := range org.GetVerifyPhoneMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetVerifyPhoneCustomTextToDomain(message)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "verify_phone_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } +} + +func importDomainClaimedMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.DomainClaimedMessages == nil { + return + } + for _, message := range org.GetDomainClaimedMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetDomainClaimedCustomTextToDomain(message)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "domain_claimed_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } +} + +func importPasswordlessRegistrationMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) { + if org.PasswordlessRegistrationMessages == nil { + return + } + for _, message := range org.GetPasswordlessRegistrationMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetPasswordlessRegistrationCustomTextToDomain(message)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "passwordless_registration_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } +} + +func importOrg2(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, success *admin_pb.ImportDataSuccess, count *counts, org *admin_pb.DataOrg) error { + successOrg := findOldOrg(success, org.OrgId) + if successOrg == nil { + return nil + } + if org.TriggerActions != nil { + for _, triggerAction := range org.GetTriggerActions() { + _, err := s.command.SetTriggerActions(ctx, action_grpc.FlowTypeToDomain(triggerAction.FlowType), action_grpc.TriggerTypeToDomain(triggerAction.TriggerType), triggerAction.ActionIds, org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "trigger_action", Id: triggerAction.FlowType + "_" + triggerAction.TriggerType, Message: err.Error()}) + continue + } + successOrg.TriggerActions = append(successOrg.TriggerActions, &management_pb.SetTriggerActionsRequest{FlowType: triggerAction.FlowType, TriggerType: triggerAction.TriggerType, ActionIds: triggerAction.GetActionIds()}) + } + } + if org.ProjectGrants != nil { + for _, grant := range org.GetProjectGrants() { + logging.Debugf("import projectgrant: %s", grant.GetGrantId()+"_"+grant.GetProjectGrant().GetProjectId()+"_"+grant.GetProjectGrant().GetGrantedOrgId()) + _, err := s.command.AddProjectGrantWithID(ctx, management.AddProjectGrantRequestToDomain(grant.GetProjectGrant()), grant.GetGrantId(), org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_grant", Id: org.GetOrgId() + "_" + grant.GetProjectGrant().GetProjectId() + "_" + grant.GetProjectGrant().GetGrantedOrgId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.projectGrantCount += 1 + logging.Debugf("successful projectgrant %d: %s", count.projectGrantCount, grant.GetGrantId()+"_"+grant.GetProjectGrant().GetProjectId()+"_"+grant.GetProjectGrant().GetGrantedOrgId()) + successOrg.ProjectGrants = append(successOrg.ProjectGrants, &admin_pb.ImportDataSuccessProjectGrant{GrantId: grant.GetGrantId(), ProjectId: grant.GetProjectGrant().GetProjectId(), OrgId: grant.GetProjectGrant().GetGrantedOrgId()}) + } + } + if org.UserGrants != nil { + for _, grant := range org.GetUserGrants() { + logging.Debugf("import usergrant: %s", grant.GetProjectId()+"_"+grant.GetUserId()) + _, err := s.command.AddUserGrant(ctx, management.AddUserGrantRequestToDomain(grant), org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "user_grant", Id: org.GetOrgId() + "_" + grant.GetProjectId() + "_" + grant.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.userGrantCount += 1 + logging.Debugf("successful usergrant %d: %s", count.userGrantCount, grant.GetProjectId()+"_"+grant.GetUserId()) + successOrg.UserGrants = append(successOrg.UserGrants, &admin_pb.ImportDataSuccessUserGrant{ProjectId: grant.GetProjectId(), UserId: grant.GetUserId()}) + } + } + return nil +} + +func importOrg3(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, success *admin_pb.ImportDataSuccess, count *counts, org *admin_pb.DataOrg) error { + successOrg := findOldOrg(success, org.OrgId) + if successOrg == nil { + return nil + } + if err := importOrgMembers(ctx, s, errors, successOrg, count, org); err != nil { + return err + } + if err := importProjectGrantMembers(ctx, s, errors, successOrg, count, org); err != nil { + return err + } + return importProjectMembers(ctx, s, errors, successOrg, count, org) +} + +func importOrgMembers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, count *counts, org *admin_pb.DataOrg) error { + if org.OrgMembers == nil { + return nil + } + for _, member := range org.GetOrgMembers() { + logging.Debugf("import orgmember: %s", member.GetUserId()) + _, err := s.command.AddOrgMember(ctx, org.GetOrgId(), member.GetUserId(), member.GetRoles()...) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "org_member", Id: org.GetOrgId() + "_" + member.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.orgMemberCount += 1 + logging.Debugf("successful orgmember %d: %s", count.orgMemberCount, member.GetUserId()) + successOrg.OrgMembers = append(successOrg.OrgMembers, member.GetUserId()) + } + return nil +} + +func importProjectGrantMembers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, count *counts, org *admin_pb.DataOrg) error { + if org.ProjectGrantMembers == nil { + return nil + } + for _, member := range org.GetProjectGrantMembers() { + logging.Debugf("import projectgrantmember: %s", member.GetProjectId()+"_"+member.GetGrantId()+"_"+member.GetUserId()) + _, err := s.command.AddProjectGrantMember(ctx, management.AddProjectGrantMemberRequestToDomain(member)) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_grant_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetGrantId() + "_" + member.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.projectGrantMemberCount += 1 + logging.Debugf("successful projectgrantmember %d: %s", count.projectGrantMemberCount, member.GetProjectId()+"_"+member.GetGrantId()+"_"+member.GetUserId()) + successOrg.ProjectGrantMembers = append(successOrg.ProjectGrantMembers, &admin_pb.ImportDataSuccessProjectGrantMember{ProjectId: member.GetProjectId(), GrantId: member.GetGrantId(), UserId: member.GetUserId()}) + } + return nil +} + +func importProjectMembers(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, count *counts, org *admin_pb.DataOrg) error { + if org.ProjectMembers == nil { + return nil + } + for _, member := range org.GetProjectMembers() { + logging.Debugf("import orgmember: %s", member.GetProjectId()+"_"+member.GetUserId()) + _, err := s.command.AddProjectMember(ctx, management.AddProjectMemberRequestToDomain(member), org.GetOrgId()) + if err != nil { + *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return err + } + continue + } + count.projectMembersCount += 1 + logging.Debugf("successful orgmember %d: %s", count.projectMembersCount, member.GetProjectId()+"_"+member.GetUserId()) + successOrg.ProjectMembers = append(successOrg.ProjectMembers, &admin_pb.ImportDataSuccessProjectMember{ProjectId: member.GetProjectId(), UserId: member.GetUserId()}) + } + return nil +} + +func findOldOrg(success *admin_pb.ImportDataSuccess, orgId string) *admin_pb.ImportDataSuccessOrg { + for _, oldOrd := range success.Orgs { + if orgId == oldOrd.OrgId { + return oldOrd + } + } + return nil +} + +func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*admin_pb.ImportDataResponse, *counts, error) { errors := make([]*admin_pb.ImportDataError, 0) success := &admin_pb.ImportDataSuccess{} - count := &count{} + count := &counts{} appSecretGenerator, err := s.query.InitHashGenerator(ctx, domain.SecretGeneratorTypeAppSecret, s.passwordHashAlg) if err != nil { @@ -338,533 +1063,21 @@ func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*adm count.machineKeysCount += len(org.GetMachineKeys()) count.appKeysCount += len(org.GetAppKeys()) } - for _, org := range orgs { - _, err := s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), []string{}) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "org", Id: org.GetOrgId(), Message: err.Error()}) - - if _, err := s.query.OrgByID(ctx, true, org.OrgId); err != nil { - continue - } - } - successOrg := &admin_pb.ImportDataSuccessOrg{ - OrgId: org.GetOrgId(), - ProjectIds: []string{}, - OidcAppIds: []string{}, - ApiAppIds: []string{}, - HumanUserIds: []string{}, - MachineUserIds: []string{}, - ActionIds: []string{}, - ProjectGrants: []*admin_pb.ImportDataSuccessProjectGrant{}, - UserGrants: []*admin_pb.ImportDataSuccessUserGrant{}, - OrgMembers: []string{}, - ProjectMembers: []*admin_pb.ImportDataSuccessProjectMember{}, - ProjectGrantMembers: []*admin_pb.ImportDataSuccessProjectGrantMember{}, - } - logging.Debugf("successful org: %s", successOrg.OrgId) - success.Orgs = append(success.Orgs, successOrg) - - domainPolicy := org.GetDomainPolicy() - if org.DomainPolicy != nil { - _, err := s.command.AddOrgDomainPolicy(ctx, org.GetOrgId(), domainPolicy.UserLoginMustBeDomain, domainPolicy.ValidateOrgDomains, domainPolicy.SmtpSenderAddressMatchesInstanceDomain) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "domain_policy", Id: org.GetOrgId(), Message: err.Error()}) - } - } - if org.Domains != nil { - for _, domainR := range org.Domains { - orgDomain := &domain.OrgDomain{ - ObjectRoot: models.ObjectRoot{ - AggregateID: org.GetOrgId(), - }, - Domain: domainR.DomainName, - Verified: domainR.IsVerified, - Primary: domainR.IsPrimary, - } - _, err := s.command.AddOrgDomain(ctx, org.GetOrgId(), domainR.DomainName, []string{}) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "domain", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - logging.Debugf("successful domain: %s", domainR.DomainName) - successOrg.Domains = append(successOrg.Domains, domainR.DomainName) - - if domainR.IsVerified { - if _, err := s.command.VerifyOrgDomain(ctx, org.GetOrgId(), domainR.DomainName); err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "domain_isverified", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) - } - } - if domainR.IsPrimary { - if _, err := s.command.SetPrimaryOrgDomain(ctx, orgDomain); err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "domain_isprimary", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) - } - } - } - } - if org.LabelPolicy != nil { - _, err = s.command.AddLabelPolicy(ctx, org.GetOrgId(), management.AddLabelPolicyToDomain(org.GetLabelPolicy())) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "label_policy", Id: org.GetOrgId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - } else { - _, err = s.command.ActivateLabelPolicy(ctx, org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "label_policy", Id: org.GetOrgId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - } - } - } - if org.LockoutPolicy != nil { - _, err = s.command.AddLockoutPolicy(ctx, org.GetOrgId(), management.AddLockoutPolicyToDomain(org.GetLockoutPolicy())) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "lockout_policy", Id: org.GetOrgId(), Message: err.Error()}) - } - } - if org.OidcIdps != nil { - for _, idp := range org.OidcIdps { - logging.Debugf("import oidcidp: %s", idp.IdpId) - _, err := s.command.ImportIDPConfig(ctx, management.AddOIDCIDPRequestToDomain(idp.Idp), idp.IdpId, org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "oidc_idp", Id: idp.IdpId, Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - logging.Debugf("successful oidcidp: %s", idp.GetIdpId()) - successOrg.OidcIpds = append(successOrg.OidcIpds, idp.GetIdpId()) - } - } - if org.JwtIdps != nil { - for _, idp := range org.JwtIdps { - logging.Debugf("import jwtidp: %s", idp.IdpId) - _, err := s.command.ImportIDPConfig(ctx, management.AddJWTIDPRequestToDomain(idp.Idp), idp.IdpId, org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "jwt_idp", Id: idp.IdpId, Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - logging.Debugf("successful jwtidp: %s", idp.GetIdpId()) - successOrg.JwtIdps = append(successOrg.JwtIdps, idp.GetIdpId()) - } - } - if org.LoginPolicy != nil { - _, err = s.command.AddLoginPolicy(ctx, org.GetOrgId(), management.AddLoginPolicyToCommand(org.GetLoginPolicy())) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "login_policy", Id: org.GetOrgId(), Message: err.Error()}) - } - } - if org.PasswordComplexityPolicy != nil { - _, err = s.command.AddPasswordComplexityPolicy(ctx, org.GetOrgId(), management.AddPasswordComplexityPolicyToDomain(org.GetPasswordComplexityPolicy())) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "password_complexity_policy", Id: org.GetOrgId(), Message: err.Error()}) - } - } - if org.PrivacyPolicy != nil { - _, err = s.command.AddPrivacyPolicy(ctx, org.GetOrgId(), management.AddPrivacyPolicyToDomain(org.GetPrivacyPolicy())) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "privacy_policy", Id: org.GetOrgId(), Message: err.Error()}) - } - } - if org.LoginTexts != nil { - for _, text := range org.GetLoginTexts() { - _, err := s.command.SetOrgLoginText(ctx, org.GetOrgId(), management.SetLoginCustomTextToDomain(text)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "login_texts", Id: org.GetOrgId() + "_" + text.Language, Message: err.Error()}) - } - } - } - if org.InitMessages != nil { - for _, message := range org.GetInitMessages() { - _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetInitCustomTextToDomain(message)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "init_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) - } - } - } - if org.PasswordResetMessages != nil { - for _, message := range org.GetPasswordResetMessages() { - _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetPasswordResetCustomTextToDomain(message)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "password_reset_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) - } - } - } - if org.VerifyEmailMessages != nil { - for _, message := range org.GetVerifyEmailMessages() { - _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetVerifyEmailCustomTextToDomain(message)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "verify_email_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) - } - } - } - if org.VerifyPhoneMessages != nil { - for _, message := range org.GetVerifyPhoneMessages() { - _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetVerifyPhoneCustomTextToDomain(message)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "verify_phone_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) - } - } - } - if org.DomainClaimedMessages != nil { - for _, message := range org.GetDomainClaimedMessages() { - _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetDomainClaimedCustomTextToDomain(message)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "domain_claimed_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) - } - } - } - if org.PasswordlessRegistrationMessages != nil { - for _, message := range org.GetPasswordlessRegistrationMessages() { - _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetPasswordlessRegistrationCustomTextToDomain(message)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "passwordless_registration_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) - } - } - } - - if org.HumanUsers != nil { - for _, user := range org.GetHumanUsers() { - logging.Debugf("import user: %s", user.GetUserId()) - human, passwordless, links := management.ImportHumanUserRequestToDomain(user.User) - human.AggregateID = user.UserId - _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, links, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "human_user", Id: user.GetUserId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - } else { - count.humanUserCount += 1 - logging.Debugf("successful user %d: %s", count.humanUserCount, user.GetUserId()) - successOrg.HumanUserIds = append(successOrg.HumanUserIds, user.GetUserId()) - } - - if user.User.OtpCode != "" { - logging.Debugf("import user otp: %s", user.GetUserId()) - if err := s.command.ImportHumanTOTP(ctx, user.UserId, "", org.GetOrgId(), user.User.OtpCode); err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "human_user_otp", Id: user.GetUserId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - } else { - logging.Debugf("successful user otp: %s", user.GetUserId()) - } - } - } - } - if org.MachineUsers != nil { - for _, user := range org.GetMachineUsers() { - logging.Debugf("import user: %s", user.GetUserId()) - _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId())) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "machine_user", Id: user.GetUserId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.machineUserCount += 1 - logging.Debugf("successful user %d: %s", count.machineUserCount, user.GetUserId()) - successOrg.MachineUserIds = append(successOrg.MachineUserIds, user.GetUserId()) - } - } - if org.UserMetadata != nil { - for _, userMetadata := range org.GetUserMetadata() { - logging.Debugf("import usermetadata: %s", userMetadata.GetId()+"_"+userMetadata.GetKey()) - _, err := s.command.SetUserMetadata(ctx, &domain.Metadata{Key: userMetadata.GetKey(), Value: userMetadata.GetValue()}, userMetadata.GetId(), org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "user_metadata", Id: userMetadata.GetId() + "_" + userMetadata.GetKey(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.userMetadataCount += 1 - logging.Debugf("successful usermetadata %d: %s", count.userMetadataCount, userMetadata.GetId()+"_"+userMetadata.GetKey()) - successOrg.UserMetadata = append(successOrg.UserMetadata, &admin_pb.ImportDataSuccessUserMetadata{UserId: userMetadata.GetId(), Key: userMetadata.GetKey()}) - } - } - if org.MachineKeys != nil { - for _, key := range org.GetMachineKeys() { - logging.Debugf("import machine_user_key: %s", key.KeyId) - _, err := s.command.AddUserMachineKey(ctx, &command.MachineKey{ - ObjectRoot: models.ObjectRoot{ - AggregateID: key.UserId, - ResourceOwner: org.GetOrgId(), - }, - KeyID: key.KeyId, - Type: authn.KeyTypeToDomain(key.Type), - ExpirationDate: key.ExpirationDate.AsTime(), - PublicKey: key.PublicKey, - }) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "machine_user_key", Id: key.KeyId, Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.machineKeysCount += 1 - logging.Debugf("successful machine_user_key %d: %s", count.machineKeysCount, key.KeyId) - successOrg.MachineKeys = append(successOrg.MachineKeys, key.KeyId) - } - } - if org.UserLinks != nil { - for _, userLinks := range org.GetUserLinks() { - logging.Debugf("import userlink: %s", userLinks.GetUserId()+"_"+userLinks.GetIdpId()+"_"+userLinks.GetProvidedUserId()+"_"+userLinks.GetProvidedUserName()) - externalIDP := &command.AddLink{ - IDPID: userLinks.IdpId, - IDPExternalID: userLinks.ProvidedUserId, - DisplayName: userLinks.ProvidedUserName, - } - if _, err := s.command.AddUserIDPLink(ctx, userLinks.UserId, org.GetOrgId(), externalIDP); err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "user_link", Id: userLinks.UserId + "_" + userLinks.IdpId, Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.userLinksCount += 1 - logging.Debugf("successful userlink %d: %s", count.userLinksCount, userLinks.GetUserId()+"_"+userLinks.GetIdpId()+"_"+userLinks.GetProvidedUserId()+"_"+userLinks.GetProvidedUserName()) - successOrg.UserLinks = append(successOrg.UserLinks, &admin_pb.ImportDataSuccessUserLinks{UserId: userLinks.GetUserId(), IdpId: userLinks.GetIdpId(), ExternalUserId: userLinks.GetProvidedUserId(), DisplayName: userLinks.GetProvidedUserName()}) - } - } - if org.Projects != nil { - for _, project := range org.GetProjects() { - logging.Debugf("import project: %s", project.GetProjectId()) - _, err := s.command.AddProjectWithID(ctx, management.ProjectCreateToDomain(project.GetProject()), org.GetOrgId(), project.GetProjectId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "project", Id: project.GetProjectId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.projectCount += 1 - logging.Debugf("successful project %d: %s", count.projectCount, project.GetProjectId()) - successOrg.ProjectIds = append(successOrg.ProjectIds, project.GetProjectId()) - } - } - if org.OidcApps != nil { - for _, app := range org.GetOidcApps() { - logging.Debugf("import oidcapplication: %s", app.GetAppId()) - _, err := s.command.AddOIDCApplicationWithID(ctx, management.AddOIDCAppRequestToDomain(app.App), org.GetOrgId(), app.GetAppId(), appSecretGenerator) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "oidc_app", Id: app.GetAppId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.oidcAppCount += 1 - logging.Debugf("successful oidcapplication %d: %s", count.oidcAppCount, app.GetAppId()) - successOrg.OidcAppIds = append(successOrg.OidcAppIds, app.GetAppId()) - } - } - if org.ApiApps != nil { - for _, app := range org.GetApiApps() { - logging.Debugf("import apiapplication: %s", app.GetAppId()) - _, err := s.command.AddAPIApplicationWithID(ctx, management.AddAPIAppRequestToDomain(app.GetApp()), org.GetOrgId(), app.GetAppId(), appSecretGenerator) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "api_app", Id: app.GetAppId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.apiAppCount += 1 - logging.Debugf("successful apiapplication %d: %s", count.apiAppCount, app.GetAppId()) - successOrg.ApiAppIds = append(successOrg.ApiAppIds, app.GetAppId()) - } - } - if org.AppKeys != nil { - for _, key := range org.GetAppKeys() { - logging.Debugf("import app_key: %s", key.Id) - _, err := s.command.AddApplicationKeyWithID(ctx, &domain.ApplicationKey{ - ObjectRoot: models.ObjectRoot{ - AggregateID: key.ProjectId, - ResourceOwner: org.GetOrgId(), - }, - ApplicationID: key.AppId, - ClientID: key.ClientId, - KeyID: key.Id, - Type: authn.KeyTypeToDomain(key.Type), - ExpirationDate: key.ExpirationDate.AsTime(), - PublicKey: key.PublicKey, - }, org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "app_key", Id: key.Id, Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.appKeysCount += 1 - logging.Debugf("successful app_key %d: %s", count.appKeysCount, key.Id) - successOrg.AppKeys = append(successOrg.AppKeys, key.Id) - } - } - if org.Actions != nil { - for _, action := range org.GetActions() { - logging.Debugf("import action: %s", action.GetActionId()) - _, _, err := s.command.AddActionWithID(ctx, management.CreateActionRequestToDomain(action.GetAction()), org.GetOrgId(), action.GetActionId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "action", Id: action.GetActionId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.actionCount += 1 - logging.Debugf("successful action %d: %s", count.actionCount, action.GetActionId()) - successOrg.ActionIds = append(successOrg.ActionIds, action.ActionId) - } - } - if org.ProjectRoles != nil { - for _, role := range org.GetProjectRoles() { - logging.Debugf("import projectroles: %s", role.ProjectId+"_"+role.RoleKey) - _, err := s.command.AddProjectRole(ctx, management.AddProjectRoleRequestToDomain(role), org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "project_role", Id: role.ProjectId + "_" + role.RoleKey, Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.projectRolesCount += 1 - logging.Debugf("successful projectroles %d: %s", count.projectRolesCount, role.ProjectId+"_"+role.RoleKey) - successOrg.ProjectRoles = append(successOrg.ActionIds, role.ProjectId+"_"+role.RoleKey) - } + if err = importOrg1(ctx, s, &errors, ctxData, org, success, count, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode, appSecretGenerator); err != nil { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err } } - for _, org := range orgs { - var successOrg *admin_pb.ImportDataSuccessOrg - for _, oldOrd := range success.Orgs { - if org.OrgId == oldOrd.OrgId { - successOrg = oldOrd - } - } - if successOrg == nil { - continue - } - - if org.TriggerActions != nil { - for _, triggerAction := range org.GetTriggerActions() { - _, err := s.command.SetTriggerActions(ctx, action_grpc.FlowTypeToDomain(triggerAction.FlowType), action_grpc.TriggerTypeToDomain(triggerAction.TriggerType), triggerAction.ActionIds, org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "trigger_action", Id: triggerAction.FlowType + "_" + triggerAction.TriggerType, Message: err.Error()}) - continue - } - successOrg.TriggerActions = append(successOrg.TriggerActions, &management_pb.SetTriggerActionsRequest{FlowType: triggerAction.FlowType, TriggerType: triggerAction.TriggerType, ActionIds: triggerAction.GetActionIds()}) - } - } - if org.ProjectGrants != nil { - for _, grant := range org.GetProjectGrants() { - logging.Debugf("import projectgrant: %s", grant.GetGrantId()+"_"+grant.GetProjectGrant().GetProjectId()+"_"+grant.GetProjectGrant().GetGrantedOrgId()) - _, err := s.command.AddProjectGrantWithID(ctx, management.AddProjectGrantRequestToDomain(grant.GetProjectGrant()), grant.GetGrantId(), org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "project_grant", Id: org.GetOrgId() + "_" + grant.GetProjectGrant().GetProjectId() + "_" + grant.GetProjectGrant().GetGrantedOrgId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.projectGrantCount += 1 - logging.Debugf("successful projectgrant %d: %s", count.projectGrantCount, grant.GetGrantId()+"_"+grant.GetProjectGrant().GetProjectId()+"_"+grant.GetProjectGrant().GetGrantedOrgId()) - successOrg.ProjectGrants = append(successOrg.ProjectGrants, &admin_pb.ImportDataSuccessProjectGrant{GrantId: grant.GetGrantId(), ProjectId: grant.GetProjectGrant().GetProjectId(), OrgId: grant.GetProjectGrant().GetGrantedOrgId()}) - } - } - if org.UserGrants != nil { - for _, grant := range org.GetUserGrants() { - logging.Debugf("import usergrant: %s", grant.GetProjectId()+"_"+grant.GetUserId()) - _, err := s.command.AddUserGrant(ctx, management.AddUserGrantRequestToDomain(grant), org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "user_grant", Id: org.GetOrgId() + "_" + grant.GetProjectId() + "_" + grant.GetUserId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.userGrantCount += 1 - logging.Debugf("successful usergrant %d: %s", count.userGrantCount, grant.GetProjectId()+"_"+grant.GetUserId()) - successOrg.UserGrants = append(successOrg.UserGrants, &admin_pb.ImportDataSuccessUserGrant{ProjectId: grant.GetProjectId(), UserId: grant.GetUserId()}) - } + if err = importOrg2(ctx, s, &errors, success, count, org); err != nil { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err } } - for _, org := range orgs { - var successOrg *admin_pb.ImportDataSuccessOrg - for _, oldOrd := range success.Orgs { - if org.OrgId == oldOrd.OrgId { - successOrg = oldOrd - } - } - if successOrg == nil { - continue - } - - if org.OrgMembers != nil { - for _, member := range org.GetOrgMembers() { - logging.Debugf("import orgmember: %s", member.GetUserId()) - _, err := s.command.AddOrgMember(ctx, org.GetOrgId(), member.GetUserId(), member.GetRoles()...) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "org_member", Id: org.GetOrgId() + "_" + member.GetUserId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.orgMemberCount += 1 - logging.Debugf("successful orgmember %d: %s", count.orgMemberCount, member.GetUserId()) - successOrg.OrgMembers = append(successOrg.OrgMembers, member.GetUserId()) - } - } - if org.ProjectGrantMembers != nil { - for _, member := range org.GetProjectGrantMembers() { - logging.Debugf("import projectgrantmember: %s", member.GetProjectId()+"_"+member.GetGrantId()+"_"+member.GetUserId()) - _, err := s.command.AddProjectGrantMember(ctx, management.AddProjectGrantMemberRequestToDomain(member)) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "project_grant_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetGrantId() + "_" + member.GetUserId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.projectGrantMemberCount += 1 - logging.Debugf("successful projectgrantmember %d: %s", count.projectGrantMemberCount, member.GetProjectId()+"_"+member.GetGrantId()+"_"+member.GetUserId()) - successOrg.ProjectGrantMembers = append(successOrg.ProjectGrantMembers, &admin_pb.ImportDataSuccessProjectGrantMember{ProjectId: member.GetProjectId(), GrantId: member.GetGrantId(), UserId: member.GetUserId()}) - } - } - if org.ProjectMembers != nil { - for _, member := range org.GetProjectMembers() { - logging.Debugf("import orgmember: %s", member.GetProjectId()+"_"+member.GetUserId()) - _, err := s.command.AddProjectMember(ctx, management.AddProjectMemberRequestToDomain(member), org.GetOrgId()) - if err != nil { - errors = append(errors, &admin_pb.ImportDataError{Type: "project_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetUserId(), Message: err.Error()}) - if isCtxTimeout(ctx) { - return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err - } - continue - } - count.projectMembersCount += 1 - logging.Debugf("successful orgmember %d: %s", count.projectMembersCount, member.GetProjectId()+"_"+member.GetUserId()) - successOrg.ProjectMembers = append(successOrg.ProjectMembers, &admin_pb.ImportDataSuccessProjectMember{ProjectId: member.GetProjectId(), UserId: member.GetUserId()}) - } + if err = importOrg3(ctx, s, &errors, success, count, org); err != nil { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err } } - return &admin_pb.ImportDataResponse{ Errors: errors, Success: success, diff --git a/internal/api/grpc/admin/language.go b/internal/api/grpc/admin/language.go index 73924a401e..dc3ca055b3 100644 --- a/internal/api/grpc/admin/language.go +++ b/internal/api/grpc/admin/language.go @@ -3,29 +3,23 @@ package admin import ( "context" - "golang.org/x/text/language" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object" - "github.com/zitadel/zitadel/internal/api/grpc/text" - caos_errors "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/i18n" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" ) func (s *Server) GetSupportedLanguages(ctx context.Context, req *admin_pb.GetSupportedLanguagesRequest) (*admin_pb.GetSupportedLanguagesResponse, error) { - langs, err := s.query.Languages(ctx) - if err != nil { - return nil, err - } - return &admin_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil + return &admin_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil } func (s *Server) SetDefaultLanguage(ctx context.Context, req *admin_pb.SetDefaultLanguageRequest) (*admin_pb.SetDefaultLanguageResponse, error) { - lang, err := language.Parse(req.Language) + lang, err := domain.ParseLanguage(req.Language) if err != nil { - return nil, caos_errors.ThrowInvalidArgument(err, "API-39nnf", "Errors.Language.Parse") + return nil, err } - details, err := s.command.SetDefaultLanguage(ctx, lang) + details, err := s.command.SetDefaultLanguage(ctx, lang[0]) if err != nil { return nil, err } diff --git a/internal/api/grpc/admin/language_converter.go b/internal/api/grpc/admin/language_converter.go new file mode 100644 index 0000000000..c36fc229ba --- /dev/null +++ b/internal/api/grpc/admin/language_converter.go @@ -0,0 +1,19 @@ +package admin + +import ( + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/pkg/grpc/admin" +) + +func selectLanguagesToCommand(languages *admin.SelectLanguages) (tags []language.Tag, err error) { + allowedLanguages := languages.GetList() + if allowedLanguages == nil && languages != nil { + allowedLanguages = make([]string, 0) + } + if allowedLanguages == nil { + return nil, nil + } + return domain.ParseLanguage(allowedLanguages...) +} diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index f3beb383e5..23fe94a78e 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -75,7 +75,6 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (* return nil, err } human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine - createdOrg, err := s.command.SetUpOrg(ctx, &command.OrgSetup{ Name: req.Org.Name, CustomDomain: req.Org.Domain, diff --git a/internal/api/grpc/admin/restrictions.go b/internal/api/grpc/admin/restrictions.go index 974f2e3555..ec4b6b7f18 100644 --- a/internal/api/grpc/admin/restrictions.go +++ b/internal/api/grpc/admin/restrictions.go @@ -5,11 +5,19 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/object" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/pkg/grpc/admin" ) func (s *Server) SetRestrictions(ctx context.Context, req *admin.SetRestrictionsRequest) (*admin.SetRestrictionsResponse, error) { - details, err := s.command.SetInstanceRestrictions(ctx, &command.SetRestrictions{DisallowPublicOrgRegistration: req.DisallowPublicOrgRegistration}) + lang, err := selectLanguagesToCommand(req.GetAllowedLanguages()) + if err != nil { + return nil, err + } + details, err := s.command.SetInstanceRestrictions(ctx, &command.SetRestrictions{ + DisallowPublicOrgRegistration: req.DisallowPublicOrgRegistration, + AllowedLanguages: lang, + }) if err != nil { return nil, err } @@ -26,5 +34,6 @@ func (s *Server) GetRestrictions(ctx context.Context, _ *admin.GetRestrictionsRe return &admin.GetRestrictionsResponse{ Details: object.ToViewDetailsPb(restrictions.Sequence, restrictions.CreationDate, restrictions.ChangeDate, restrictions.ResourceOwner), DisallowPublicOrgRegistration: restrictions.DisallowPublicOrgRegistration, + AllowedLanguages: domain.LanguagesToStrings(restrictions.AllowedLanguages), }, nil } diff --git a/internal/api/grpc/admin/restrictions_integration_test.go b/internal/api/grpc/admin/restrictions_integration_allow_public_org_registrations_test.go similarity index 54% rename from internal/api/grpc/admin/restrictions_integration_test.go rename to internal/api/grpc/admin/restrictions_integration_allow_public_org_registrations_test.go index 07a56409e9..b6e075ae39 100644 --- a/internal/api/grpc/admin/restrictions_integration_test.go +++ b/internal/api/grpc/admin/restrictions_integration_allow_public_org_registrations_test.go @@ -6,6 +6,7 @@ import ( "bytes" "context" "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" "io" "net/http" "net/http/cookiejar" @@ -29,19 +30,25 @@ func TestServer_Restrictions_DisallowPublicOrgRegistration(t *testing.T) { jar, err := cookiejar.New(nil) require.NoError(t, err) browserSession := &http.Client{Jar: jar} - // Default should be allowed - csrfToken := awaitAllowed(t, iamOwnerCtx, browserSession, regOrgUrl) - _, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(true)}) - require.NoError(t, err) - awaitDisallowed(t, iamOwnerCtx, browserSession, regOrgUrl, csrfToken) - _, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(false)}) - require.NoError(t, err) - awaitAllowed(t, iamOwnerCtx, browserSession, regOrgUrl) + var csrfToken string + t.Run("public org registration is allowed by default", func(*testing.T) { + csrfToken = awaitPubOrgRegAllowed(t, iamOwnerCtx, browserSession, regOrgUrl) + }) + t.Run("disallowing public org registration disables the endpoints", func(*testing.T) { + _, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(true)}) + require.NoError(t, err) + awaitPubOrgRegDisallowed(t, iamOwnerCtx, browserSession, regOrgUrl, csrfToken) + }) + t.Run("allowing public org registration again re-enables the endpoints", func(*testing.T) { + _, err = Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{DisallowPublicOrgRegistration: gu.Ptr(false)}) + require.NoError(t, err) + awaitPubOrgRegAllowed(t, iamOwnerCtx, browserSession, regOrgUrl) + }) } -// awaitAllowed doesn't accept a CSRF token, as we expected it to always produce a new one -func awaitAllowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL) string { - csrfToken := awaitGetResponse(t, ctx, client, parsedURL, http.StatusOK) +// awaitPubOrgRegAllowed doesn't accept a CSRF token, as we expected it to always produce a new one +func awaitPubOrgRegAllowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL) string { + csrfToken := awaitGetSSRGetResponse(t, ctx, client, parsedURL, http.StatusOK) awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusOK, csrfToken) restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{}) require.NoError(t, err) @@ -49,17 +56,17 @@ func awaitAllowed(t *testing.T, ctx context.Context, client *http.Client, parsed return csrfToken } -// awaitDisallowed accepts an old CSRF token, as we don't expect to get a CSRF token from the GET request anymore -func awaitDisallowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, reuseOldCSRFToken string) { - awaitGetResponse(t, ctx, client, parsedURL, http.StatusNotFound) +// awaitPubOrgRegDisallowed accepts an old CSRF token, as we don't expect to get a CSRF token from the GET request anymore +func awaitPubOrgRegDisallowed(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, reuseOldCSRFToken string) { + awaitGetSSRGetResponse(t, ctx, client, parsedURL, http.StatusNotFound) awaitPostFormResponse(t, ctx, client, parsedURL, http.StatusConflict, reuseOldCSRFToken) restrictions, err := Tester.Client.Admin.GetRestrictions(ctx, &admin.GetRestrictionsRequest{}) require.NoError(t, err) require.True(t, restrictions.DisallowPublicOrgRegistration) } -// awaitGetResponse cuts the CSRF token from the response body if it exists -func awaitGetResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int) string { +// awaitGetSSRGetResponse cuts the CSRF token from the response body if it exists +func awaitGetSSRGetResponse(t *testing.T, ctx context.Context, client *http.Client, parsedURL *url.URL, expectCode int) string { var csrfToken []byte await(t, ctx, func() bool { resp, err := client.Get(parsedURL.String()) @@ -71,7 +78,7 @@ func awaitGetResponse(t *testing.T, ctx context.Context, client *http.Client, pa if hasCsrfToken { csrfToken, _, _ = bytes.Cut(after, []byte(`">`)) } - return resp.StatusCode == expectCode + return assert.Equal(NoopAssertionT, resp.StatusCode, expectCode) }) return string(csrfToken) } @@ -83,24 +90,6 @@ func awaitPostFormResponse(t *testing.T, ctx context.Context, client *http.Clien "gorilla.csrf.Token": {csrfToken}, }) require.NoError(t, err) - return resp.StatusCode == expectCode - + return assert.Equal(NoopAssertionT, resp.StatusCode, expectCode) }) } - -func await(t *testing.T, ctx context.Context, cb func() bool) { - deadline, ok := ctx.Deadline() - require.True(t, ok, "context must have deadline") - require.Eventuallyf( - t, - func() bool { - defer func() { - require.Nil(t, recover(), "panic in await callback") - }() - return cb() - }, - time.Until(deadline), - 100*time.Millisecond, - "awaiting successful callback failed", - ) -} diff --git a/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go b/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go new file mode 100644 index 0000000000..bfe9f0031c --- /dev/null +++ b/internal/api/grpc/admin/restrictions_integration_allowed_languages_test.go @@ -0,0 +1,258 @@ +//go:build integration + +package admin_test + +import ( + "context" + "encoding/json" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/admin" + "github.com/zitadel/zitadel/pkg/grpc/management" + "github.com/zitadel/zitadel/pkg/grpc/text" + "github.com/zitadel/zitadel/pkg/grpc/user" + "golang.org/x/text/language" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "io" + "net/http" + "testing" + "time" +) + +func TestServer_Restrictions_AllowedLanguages(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Hour) + defer cancel() + + var ( + defaultAndAllowedLanguage = language.German + supportedLanguagesStr = []string{language.German.String(), language.English.String(), language.Japanese.String()} + disallowedLanguage = language.Spanish + unsupportedLanguage1 = language.Afrikaans + unsupportedLanguage2 = language.Albanian + ) + + domain, _, iamOwnerCtx := Tester.UseIsolatedInstance(ctx, SystemCTX) + t.Run("assumed defaults are correct", func(tt *testing.T) { + tt.Run("languages are not restricted by default", func(ttt *testing.T) { + restrictions, err := Tester.Client.Admin.GetRestrictions(iamOwnerCtx, &admin.GetRestrictionsRequest{}) + require.NoError(ttt, err) + require.Len(ttt, restrictions.AllowedLanguages, 0) + }) + tt.Run("default language is English by default", func(ttt *testing.T) { + defaultLang, err := Tester.Client.Admin.GetDefaultLanguage(iamOwnerCtx, &admin.GetDefaultLanguageRequest{}) + require.NoError(ttt, err) + require.Equal(ttt, language.Make(defaultLang.Language), language.English) + }) + tt.Run("the discovery endpoint returns all supported languages", func(ttt *testing.T) { + checkDiscoveryEndpoint(ttt, domain, supportedLanguagesStr, nil) + }) + }) + t.Run("restricting the default language fails", func(tt *testing.T) { + _, err := Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{AllowedLanguages: &admin.SelectLanguages{List: []string{defaultAndAllowedLanguage.String()}}}) + expectStatus, ok := status.FromError(err) + require.True(tt, ok) + require.Equal(tt, codes.FailedPrecondition, expectStatus.Code()) + }) + t.Run("not defining any restrictions throws an error", func(tt *testing.T) { + _, err := Tester.Client.Admin.SetRestrictions(iamOwnerCtx, &admin.SetRestrictionsRequest{}) + expectStatus, ok := status.FromError(err) + require.True(tt, ok) + require.Equal(tt, codes.InvalidArgument, expectStatus.Code()) + }) + t.Run("setting the default language works", func(tt *testing.T) { + setAndAwaitDefaultLanguage(iamOwnerCtx, tt, defaultAndAllowedLanguage) + }) + t.Run("restricting allowed languages works", func(tt *testing.T) { + setAndAwaitAllowedLanguages(iamOwnerCtx, tt, []string{defaultAndAllowedLanguage.String()}) + }) + t.Run("setting the default language to a disallowed language fails", func(tt *testing.T) { + _, err := Tester.Client.Admin.SetDefaultLanguage(iamOwnerCtx, &admin.SetDefaultLanguageRequest{Language: disallowedLanguage.String()}) + expectStatus, ok := status.FromError(err) + require.True(tt, ok) + require.Equal(tt, codes.FailedPrecondition, expectStatus.Code()) + }) + t.Run("the list of supported languages includes the disallowed languages", func(tt *testing.T) { + supported, err := Tester.Client.Admin.GetSupportedLanguages(iamOwnerCtx, &admin.GetSupportedLanguagesRequest{}) + require.NoError(tt, err) + require.Condition(tt, contains(supported.GetLanguages(), supportedLanguagesStr)) + }) + t.Run("the disallowed language is not listed in the discovery endpoint", func(tt *testing.T) { + checkDiscoveryEndpoint(tt, domain, []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()}) + }) + t.Run("the login ui is rendered in the default language", func(tt *testing.T) { + checkLoginUILanguage(tt, domain, disallowedLanguage, defaultAndAllowedLanguage, "Allgemeine Geschäftsbedingungen und Datenschutz") + }) + t.Run("preferred languages are not restricted by the supported languages", func(tt *testing.T) { + var importedUser *management.ImportHumanUserResponse + tt.Run("import user", func(ttt *testing.T) { + var err error + importedUser, err = importUser(iamOwnerCtx, unsupportedLanguage1) + require.NoError(ttt, err) + }) + tt.Run("change user profile", func(ttt *testing.T) { + _, err := Tester.Client.Mgmt.UpdateHumanProfile(iamOwnerCtx, &management.UpdateHumanProfileRequest{ + UserId: importedUser.GetUserId(), + FirstName: "hodor", + LastName: "hodor", + NickName: integration.RandString(5), + DisplayName: "hodor", + PreferredLanguage: unsupportedLanguage2.String(), + Gender: user.Gender_GENDER_MALE, + }) + require.NoError(ttt, err) + }) + }) + t.Run("custom texts are only restricted by the supported languages", func(tt *testing.T) { + _, err := Tester.Client.Admin.SetCustomLoginText(iamOwnerCtx, &admin.SetCustomLoginTextsRequest{ + Language: disallowedLanguage.String(), + EmailVerificationText: &text.EmailVerificationScreenText{ + Description: "hodor", + }, + }) + assert.NoError(tt, err) + _, err = Tester.Client.Mgmt.SetCustomLoginText(iamOwnerCtx, &management.SetCustomLoginTextsRequest{ + Language: disallowedLanguage.String(), + EmailVerificationText: &text.EmailVerificationScreenText{ + Description: "hodor", + }, + }) + assert.NoError(tt, err) + _, err = Tester.Client.Mgmt.SetCustomInitMessageText(iamOwnerCtx, &management.SetCustomInitMessageTextRequest{ + Language: disallowedLanguage.String(), + Text: "hodor", + }) + assert.NoError(tt, err) + _, err = Tester.Client.Admin.SetDefaultInitMessageText(iamOwnerCtx, &admin.SetDefaultInitMessageTextRequest{ + Language: disallowedLanguage.String(), + Text: "hodor", + }) + assert.NoError(tt, err) + }) + t.Run("allowing all languages works", func(tt *testing.T) { + tt.Run("restricting allowed languages works", func(ttt *testing.T) { + setAndAwaitAllowedLanguages(iamOwnerCtx, ttt, make([]string, 0)) + }) + }) + + t.Run("allowing the language makes it usable again", func(tt *testing.T) { + tt.Run("the disallowed language is listed in the discovery endpoint again", func(ttt *testing.T) { + checkDiscoveryEndpoint(ttt, domain, []string{defaultAndAllowedLanguage.String()}, []string{disallowedLanguage.String()}) + }) + tt.Run("the login ui is rendered in the allowed language", func(ttt *testing.T) { + checkLoginUILanguage(ttt, domain, disallowedLanguage, disallowedLanguage, "Términos y condiciones") + }) + }) +} + +func setAndAwaitAllowedLanguages(ctx context.Context, t *testing.T, selectLanguages []string) { + _, err := Tester.Client.Admin.SetRestrictions(ctx, &admin.SetRestrictionsRequest{AllowedLanguages: &admin.SelectLanguages{List: selectLanguages}}) + require.NoError(t, err) + awaitCtx, awaitCancel := context.WithTimeout(ctx, 10*time.Second) + defer awaitCancel() + await(t, awaitCtx, func() bool { + restrictions, getErr := Tester.Client.Admin.GetRestrictions(awaitCtx, &admin.GetRestrictionsRequest{}) + expectLanguages := selectLanguages + if len(selectLanguages) == 0 { + expectLanguages = nil + } + return assert.NoError(NoopAssertionT, getErr) && + assert.Equal(NoopAssertionT, expectLanguages, restrictions.GetAllowedLanguages()) + }) +} + +func setAndAwaitDefaultLanguage(ctx context.Context, t *testing.T, lang language.Tag) { + _, err := Tester.Client.Admin.SetDefaultLanguage(ctx, &admin.SetDefaultLanguageRequest{Language: lang.String()}) + require.NoError(t, err) + awaitCtx, awaitCancel := context.WithTimeout(ctx, 10*time.Second) + defer awaitCancel() + await(t, awaitCtx, func() bool { + defaultLang, getErr := Tester.Client.Admin.GetDefaultLanguage(awaitCtx, &admin.GetDefaultLanguageRequest{}) + return assert.NoError(NoopAssertionT, getErr) && + assert.Equal(NoopAssertionT, lang.String(), defaultLang.GetLanguage()) + }) +} + +func importUser(ctx context.Context, preferredLanguage language.Tag) (*management.ImportHumanUserResponse, error) { + random := integration.RandString(5) + return Tester.Client.Mgmt.ImportHumanUser(ctx, &management.ImportHumanUserRequest{ + UserName: "integration-test-user_" + random, + Profile: &management.ImportHumanUserRequest_Profile{ + FirstName: "hodor", + LastName: "hodor", + NickName: "hodor", + PreferredLanguage: preferredLanguage.String(), + }, + Email: &management.ImportHumanUserRequest_Email{ + Email: random + "@hodor.hodor", + IsEmailVerified: true, + }, + PasswordChangeRequired: false, + Password: "Password1!", + }) +} + +func checkDiscoveryEndpoint(t *testing.T, domain string, containsUILocales, notContainsUILocales []string) { + resp, err := http.Get("http://" + domain + ":8080/.well-known/openid-configuration") + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + defer func() { + require.NoError(t, resp.Body.Close()) + }() + require.NoError(t, err) + doc := struct { + UILocalesSupported []string `json:"ui_locales_supported"` + }{} + require.NoError(t, json.Unmarshal(body, &doc)) + if containsUILocales != nil { + assert.Condition(NoopAssertionT, contains(doc.UILocalesSupported, containsUILocales)) + } + if notContainsUILocales != nil { + assert.Condition(NoopAssertionT, not(contains(doc.UILocalesSupported, notContainsUILocales))) + } +} + +func checkLoginUILanguage(t *testing.T, domain string, acceptLanguage language.Tag, expectLang language.Tag, containsText string) { + req, err := http.NewRequest(http.MethodGet, "http://"+domain+":8080/ui/login/register", nil) + req.Header.Set("Accept-Language", acceptLanguage.String()) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + defer func() { + require.NoError(t, resp.Body.Close()) + }() + require.NoError(t, err) + assert.Containsf(t, string(body), containsText, "login ui language is in "+expectLang.String()) +} + +// We would love to use assert.Contains here, but it doesn't work with slices of strings +func contains(container []string, subset []string) assert.Comparison { + return func() bool { + if subset == nil { + return true + } + for _, str := range subset { + var found bool + for _, containerStr := range container { + if str == containerStr { + found = true + break + } + } + if !found { + return false + } + } + return true + } +} + +func not(cmp assert.Comparison) assert.Comparison { + return func() bool { + return !cmp() + } +} diff --git a/internal/api/grpc/admin/server_integration_test.go b/internal/api/grpc/admin/server_integration_test.go index 64a761dd7f..de1b24b4b0 100644 --- a/internal/api/grpc/admin/server_integration_test.go +++ b/internal/api/grpc/admin/server_integration_test.go @@ -8,12 +8,17 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/internal/integration" ) var ( AdminCTX, SystemCTX context.Context Tester *integration.Tester + // NoopAssertionT is useful in combination with assert.Eventuallyf to use testify assertions in a callback + NoopAssertionT = new(noopAssertionT) ) func TestMain(m *testing.M) { @@ -30,3 +35,29 @@ func TestMain(m *testing.M) { return m.Run() }()) } + +func await(t *testing.T, ctx context.Context, cb func() bool) { + deadline, ok := ctx.Deadline() + require.True(t, ok, "context must have deadline") + assert.Eventuallyf( + t, + func() bool { + defer func() { + // Panics are not recovered and don't mark the test as failed, so we need to do that ourselves + require.Nil(t, recover(), "panic in await callback") + }() + return cb() + }, + time.Until(deadline), + 100*time.Millisecond, + "awaiting successful callback failed", + ) +} + +var _ assert.TestingT = (*noopAssertionT)(nil) + +type noopAssertionT struct{} + +func (*noopAssertionT) FailNow() {} + +func (*noopAssertionT) Errorf(string, ...interface{}) {} diff --git a/internal/api/grpc/auth/language.go b/internal/api/grpc/auth/language.go index 91f78cd150..9f1d65bbb7 100644 --- a/internal/api/grpc/auth/language.go +++ b/internal/api/grpc/auth/language.go @@ -2,15 +2,12 @@ package auth import ( "context" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/api/grpc/text" auth_pb "github.com/zitadel/zitadel/pkg/grpc/auth" ) -func (s *Server) GetSupportedLanguages(ctx context.Context, req *auth_pb.GetSupportedLanguagesRequest) (*auth_pb.GetSupportedLanguagesResponse, error) { - langs, err := s.query.Languages(ctx) - if err != nil { - return nil, err - } - return &auth_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil +func (s *Server) GetSupportedLanguages(context.Context, *auth_pb.GetSupportedLanguagesRequest) (*auth_pb.GetSupportedLanguagesResponse, error) { + return &auth_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil } diff --git a/internal/api/grpc/management/language.go b/internal/api/grpc/management/language.go index cab36f63e6..4b13ba5c4c 100644 --- a/internal/api/grpc/management/language.go +++ b/internal/api/grpc/management/language.go @@ -2,15 +2,12 @@ package management import ( "context" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/api/grpc/text" mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management" ) -func (s *Server) GetSupportedLanguages(ctx context.Context, req *mgmt_pb.GetSupportedLanguagesRequest) (*mgmt_pb.GetSupportedLanguagesResponse, error) { - langs, err := s.query.Languages(ctx) - if err != nil { - return nil, err - } - return &mgmt_pb.GetSupportedLanguagesResponse{Languages: text.LanguageTagsToStrings(langs)}, nil +func (s *Server) GetSupportedLanguages(context.Context, *mgmt_pb.GetSupportedLanguagesRequest) (*mgmt_pb.GetSupportedLanguagesResponse, error) { + return &mgmt_pb.GetSupportedLanguagesResponse{Languages: domain.LanguagesToStrings(i18n.SupportedLanguages())}, nil } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 754b02755a..cb2afdcf8e 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -220,8 +220,7 @@ func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRe func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) { human := AddHumanUserRequestToAddHuman(req) - err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true) - if err != nil { + if err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true); err != nil { return nil, err } return &mgmt_pb.AddHumanUserResponse{ diff --git a/internal/api/grpc/management/user_integration_test.go b/internal/api/grpc/management/user_integration_test.go index 6b5fe77d1a..5d612b158f 100644 --- a/internal/api/grpc/management/user_integration_test.go +++ b/internal/api/grpc/management/user_integration_test.go @@ -55,14 +55,14 @@ func TestImport_and_Get(t *testing.T) { // create unique names. lastName := strconv.FormatInt(time.Now().Unix(), 10) userName := strings.Join([]string{firstName, lastName}, "_") - email := strings.Join([]string{userName, "zitadel.com"}, "@") + email := strings.Join([]string{userName, "example.com"}, "@") res, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{ UserName: userName, Profile: &management.ImportHumanUserRequest_Profile{ FirstName: firstName, LastName: lastName, - PreferredLanguage: language.Afrikaans.String(), + PreferredLanguage: language.Japanese.String(), Gender: user.Gender_GENDER_DIVERSE, }, Email: &management.ImportHumanUserRequest_Email{ @@ -82,3 +82,21 @@ func TestImport_and_Get(t *testing.T) { }) } } + +func TestImport_UnparsablePreferredLanguage(t *testing.T) { + random := integration.RandString(5) + _, err := Client.ImportHumanUser(CTX, &management.ImportHumanUserRequest{ + UserName: random, + Profile: &management.ImportHumanUserRequest_Profile{ + FirstName: random, + LastName: random, + PreferredLanguage: "not valid", + Gender: user.Gender_GENDER_DIVERSE, + }, + Email: &management.ImportHumanUserRequest_Email{ + Email: random + "@example.com", + IsEmailVerified: true, + }, + }) + require.NoError(t, err) +} diff --git a/internal/api/grpc/server/middleware/instance_interceptor.go b/internal/api/grpc/server/middleware/instance_interceptor.go index 77302fdf77..68389a0c4f 100644 --- a/internal/api/grpc/server/middleware/instance_interceptor.go +++ b/internal/api/grpc/server/middleware/instance_interceptor.go @@ -24,7 +24,7 @@ const ( ) func InstanceInterceptor(verifier authz.InstanceVerifier, headerName string, explicitInstanceIdServices ...string) grpc.UnaryServerInterceptor { - translator, err := newZitadelTranslator(language.English) + translator, err := i18n.NewZitadelTranslator(language.English) logging.OnError(err).Panic("unable to get translator") return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { return setInstance(ctx, req, info, handler, verifier, headerName, translator, explicitInstanceIdServices...) diff --git a/internal/api/grpc/server/middleware/translation_interceptor.go b/internal/api/grpc/server/middleware/translation_interceptor.go index 996e80acc6..08e1540531 100644 --- a/internal/api/grpc/server/middleware/translation_interceptor.go +++ b/internal/api/grpc/server/middleware/translation_interceptor.go @@ -7,6 +7,7 @@ import ( "google.golang.org/grpc" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/i18n" _ "github.com/zitadel/zitadel/internal/statik" "github.com/zitadel/zitadel/internal/telemetry/tracing" ) @@ -18,17 +19,15 @@ func TranslationHandler() func(ctx context.Context, req interface{}, info *grpc. defer func() { span.EndWithError(err) }() if loc, ok := resp.(localizers); ok && resp != nil { - translator, translatorError := newZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage()) + translator, translatorError := getTranslator(ctx) if translatorError != nil { - logging.New().WithError(translatorError).Error("could not load translator") return resp, err } translateFields(ctx, loc, translator) } if err != nil { - translator, translatorError := newZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage()) + translator, translatorError := getTranslator(ctx) if translatorError != nil { - logging.New().WithError(translatorError).Error("could not load translator") return resp, err } err = translateError(ctx, err, translator) @@ -36,3 +35,11 @@ func TranslationHandler() func(ctx context.Context, req interface{}, info *grpc. return resp, err } } + +func getTranslator(ctx context.Context) (*i18n.Translator, error) { + translator, err := i18n.NewZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage()) + if err != nil { + logging.New().WithError(err).Error("could not load translator") + } + return translator, err +} diff --git a/internal/api/grpc/server/middleware/translator.go b/internal/api/grpc/server/middleware/translator.go index f42741db0b..fa453c682f 100644 --- a/internal/api/grpc/server/middleware/translator.go +++ b/internal/api/grpc/server/middleware/translator.go @@ -4,10 +4,6 @@ import ( "context" "errors" - "github.com/rakyll/statik/fs" - "github.com/zitadel/logging" - "golang.org/x/text/language" - caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/i18n" ) @@ -39,14 +35,3 @@ func translateError(ctx context.Context, err error, translator *i18n.Translator) } return err } - -func newZitadelTranslator(defaultLanguage language.Tag) (*i18n.Translator, error) { - return translatorFromNamespace("zitadel", defaultLanguage) -} - -func translatorFromNamespace(namespace string, defaultLanguage language.Tag) (*i18n.Translator, error) { - dir, err := fs.NewWithNamespace(namespace) - logging.WithFields("namespace", namespace).OnError(err).Panic("unable to get namespace") - - return i18n.NewTranslator(dir, defaultLanguage, "") -} diff --git a/internal/api/grpc/settings/v2/settings.go b/internal/api/grpc/settings/v2/settings.go index ff458fa98d..5e09f8e89a 100644 --- a/internal/api/grpc/settings/v2/settings.go +++ b/internal/api/grpc/settings/v2/settings.go @@ -7,7 +7,8 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" - "github.com/zitadel/zitadel/internal/api/grpc/text" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/query" object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" @@ -116,13 +117,9 @@ func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *settings.G } func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralSettingsRequest) (*settings.GetGeneralSettingsResponse, error) { - langs, err := s.query.Languages(ctx) - if err != nil { - return nil, err - } instance := authz.GetInstance(ctx) return &settings.GetGeneralSettingsResponse{ - SupportedLanguages: text.LanguageTagsToStrings(langs), + SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()), DefaultOrgId: instance.DefaultOrganisationID(), DefaultLanguage: instance.DefaultLanguage().String(), }, nil diff --git a/internal/api/grpc/text/language.go b/internal/api/grpc/text/language.go deleted file mode 100644 index 9ae5b1ed1a..0000000000 --- a/internal/api/grpc/text/language.go +++ /dev/null @@ -1,13 +0,0 @@ -package text - -import ( - "golang.org/x/text/language" -) - -func LanguageTagsToStrings(langs []language.Tag) []string { - result := make([]string, len(langs)) - for i, lang := range langs { - result[i] = lang.String() - } - return result -} diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index 4301bc097d..d7c26e9031 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -28,8 +28,7 @@ func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest return nil, err } orgID := authz.GetCtxData(ctx).OrgID - err = s.command.AddHuman(ctx, orgID, human, false) - if err != nil { + if err = s.command.AddHuman(ctx, orgID, human, false); err != nil { return nil, err } return &user.AddHumanUserResponse{ diff --git a/internal/api/grpc/user/v2/user_integration_test.go b/internal/api/grpc/user/v2/user_integration_test.go index 55f0e76a17..fc00e165f6 100644 --- a/internal/api/grpc/user/v2/user_integration_test.go +++ b/internal/api/grpc/user/v2/user_integration_test.go @@ -677,7 +677,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { parametersEqual: map[string]string{ "client_id": "clientID", "prompt": "select_account", - "redirect_uri": "http://localhost:8080/idps/callback", + "redirect_uri": "http://" + Tester.Config.ExternalDomain + ":8080/idps/callback", "response_type": "code", "scope": "openid profile email", }, @@ -704,7 +704,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { ChangeDate: timestamppb.Now(), ResourceOwner: Tester.Organisation.ID, }, - url: "http://localhost:8000/sso", + url: "http://" + Tester.Config.ExternalDomain + ":8000/sso", parametersExisting: []string{"RelayState", "SAMLRequest"}, }, wantErr: false, @@ -728,7 +728,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { ChangeDate: timestamppb.Now(), ResourceOwner: Tester.Organisation.ID, }, - url: "http://localhost:8000/sso", + url: "http://" + Tester.Config.ExternalDomain + ":8000/sso", parametersExisting: []string{"RelayState", "SAMLRequest"}, }, wantErr: false, diff --git a/internal/api/http/middleware/instance_interceptor.go b/internal/api/http/middleware/instance_interceptor.go index 276037301d..3e4fd41f69 100644 --- a/internal/api/http/middleware/instance_interceptor.go +++ b/internal/api/http/middleware/instance_interceptor.go @@ -8,7 +8,6 @@ import ( "net/url" "strings" - "github.com/rakyll/statik/fs" "github.com/zitadel/logging" "golang.org/x/text/language" @@ -120,10 +119,7 @@ func hostFromOrigin(ctx context.Context) (host string, err error) { } func newZitadelTranslator() *i18n.Translator { - dir, err := fs.NewWithNamespace("zitadel") - logging.WithFields("namespace", "zitadel").OnError(err).Panic("unable to get namespace") - - translator, err := i18n.NewTranslator(dir, language.English, "") + translator, err := i18n.NewZitadelTranslator(language.English) logging.OnError(err).Panic("unable to get translator") return translator } diff --git a/internal/api/http/middleware/middleware_test.go b/internal/api/http/middleware/middleware_test.go new file mode 100644 index 0000000000..4d7cb6636d --- /dev/null +++ b/internal/api/http/middleware/middleware_test.go @@ -0,0 +1,18 @@ +package middleware + +import ( + "testing" + + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/i18n" +) + +var ( + SupportedLanguages = []language.Tag{language.English, language.German} +) + +func TestMain(m *testing.M) { + i18n.SupportLanguages(SupportedLanguages...) + m.Run() +} diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index defa5fdcdc..cdc167ad21 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -6,11 +6,9 @@ import ( "net/http" "time" - "github.com/rakyll/statik/fs" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" "golang.org/x/exp/slog" - "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/assets" http_utils "github.com/zitadel/zitadel/internal/api/http" @@ -23,7 +21,6 @@ import ( caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/crdb" - "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/metrics" ) @@ -167,10 +164,6 @@ func ignoredQuotaLimitEndpoint(endpoints *EndpointConfig) []string { } func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []byte) (*op.Config, error) { - supportedLanguages, err := getSupportedLanguages() - if err != nil { - return nil, err - } opConfig := &op.Config{ DefaultLogoutRedirectURI: defaultLogoutRedirectURI, CodeMethodS256: config.CodeMethodS256, @@ -178,7 +171,6 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey [] AuthMethodPrivateKeyJWT: config.AuthMethodPrivateKeyJWT, GrantTypeRefreshToken: config.GrantTypeRefreshToken, RequestObjectSupported: config.RequestObjectSupported, - SupportedUILocales: supportedLanguages, DeviceAuthorization: config.DeviceAuth.toOPConfig(), } if cryptoLength := len(cryptoKey); cryptoLength != 32 { @@ -211,11 +203,3 @@ func newStorage(config Config, command *command.Commands, query *query.Queries, func (o *OPStorage) Health(ctx context.Context) error { return o.repo.Health(ctx) } - -func getSupportedLanguages() ([]language.Tag, error) { - statikLoginFS, err := fs.NewWithNamespace("login") - if err != nil { - return nil, err - } - return i18n.SupportedLanguages(statikLoginFS) -} diff --git a/internal/api/oidc/server.go b/internal/api/oidc/server.go index fe16078f34..8d04cd62e9 100644 --- a/internal/api/oidc/server.go +++ b/internal/api/oidc/server.go @@ -12,6 +12,7 @@ import ( "github.com/zitadel/zitadel/internal/auth/repository" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/tracing" ) @@ -103,8 +104,15 @@ func (s *Server) Ready(ctx context.Context, r *op.Request[struct{}]) (_ *op.Resp func (s *Server) Discovery(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - - return op.NewResponse(s.createDiscoveryConfig(ctx)), nil + restrictions, err := s.query.GetInstanceRestrictions(ctx) + if err != nil { + return nil, err + } + allowedLanguages := restrictions.AllowedLanguages + if len(allowedLanguages) == 0 { + allowedLanguages = i18n.SupportedLanguages() + } + return op.NewResponse(s.createDiscoveryConfig(ctx, allowedLanguages)), nil } func (s *Server) Keys(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) { @@ -205,7 +213,7 @@ func (s *Server) EndSession(ctx context.Context, r *op.Request[oidc.EndSessionRe return s.LegacyServer.EndSession(ctx, r) } -func (s *Server) createDiscoveryConfig(ctx context.Context) *oidc.DiscoveryConfiguration { +func (s *Server) createDiscoveryConfig(ctx context.Context, supportedUILocales oidc.Locales) *oidc.DiscoveryConfiguration { issuer := op.IssuerFromContext(ctx) return &oidc.DiscoveryConfiguration{ Issuer: issuer, @@ -231,7 +239,7 @@ func (s *Server) createDiscoveryConfig(ctx context.Context) *oidc.DiscoveryConfi RevocationEndpointAuthMethodsSupported: op.AuthMethodsRevocationEndpoint(s.Provider()), ClaimsSupported: op.SupportedClaims(s.Provider()), CodeChallengeMethodsSupported: op.CodeChallengeMethods(s.Provider()), - UILocalesSupported: s.Provider().SupportedUILocales(), + UILocalesSupported: supportedUILocales, RequestParameterSupported: s.Provider().RequestObjectSupported(), } } diff --git a/internal/api/oidc/server_test.go b/internal/api/oidc/server_test.go index d7f258d0d2..c42c11d195 100644 --- a/internal/api/oidc/server_test.go +++ b/internal/api/oidc/server_test.go @@ -16,7 +16,8 @@ func TestServer_createDiscoveryConfig(t *testing.T) { signingKeyAlgorithm string } type args struct { - ctx context.Context + ctx context.Context + supportedUILocales []language.Tag } tests := []struct { name string @@ -36,7 +37,6 @@ func TestServer_createDiscoveryConfig(t *testing.T) { AuthMethodPrivateKeyJWT: true, GrantTypeRefreshToken: true, RequestObjectSupported: true, - SupportedUILocales: []language.Tag{language.English, language.German}, }, nil, ) @@ -56,7 +56,8 @@ func TestServer_createDiscoveryConfig(t *testing.T) { signingKeyAlgorithm: "RS256", }, args{ - ctx: op.ContextWithIssuer(context.Background(), "https://issuer.com"), + ctx: op.ContextWithIssuer(context.Background(), "https://issuer.com"), + supportedUILocales: []language.Tag{language.English, language.German}, }, &oidc.DiscoveryConfiguration{ Issuer: "https://issuer.com", @@ -113,7 +114,7 @@ func TestServer_createDiscoveryConfig(t *testing.T) { LegacyServer: tt.fields.LegacyServer, signingKeyAlgorithm: tt.fields.signingKeyAlgorithm, } - assert.Equalf(t, tt.want, s.createDiscoveryConfig(tt.args.ctx), "createDiscoveryConfig(%v)", tt.args.ctx) + assert.Equalf(t, tt.want, s.createDiscoveryConfig(tt.args.ctx, tt.args.supportedUILocales), "createDiscoveryConfig(%v)", tt.args.ctx) }) } } diff --git a/internal/api/ui/login/change_password_handler.go b/internal/api/ui/login/change_password_handler.go index 08eb8badeb..e85b99dcd5 100644 --- a/internal/api/ui/login/change_password_handler.go +++ b/internal/api/ui/login/change_password_handler.go @@ -36,13 +36,13 @@ func (l *Login) handleChangePassword(w http.ResponseWriter, r *http.Request) { } func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) { - var errID, errMessage string + var errType, errMessage string if err != nil { - errID, errMessage = l.getErrorMessage(r, err) + errType, errMessage = l.getErrorMessage(r, err) } translator := l.getTranslator(r.Context(), authReq) data := passwordData{ - baseData: l.getBaseData(r, authReq, "PasswordChange.Title", "PasswordChange.Description", errID, errMessage), + baseData: l.getBaseData(r, authReq, translator, "PasswordChange.Title", "PasswordChange.Description", errType, errMessage), profileData: l.getProfileData(authReq), } policy := l.getPasswordComplexityPolicy(r, authReq.UserOrgID) @@ -65,8 +65,7 @@ func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, aut } func (l *Login) renderChangePasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) { - var errType, errMessage string translator := l.getTranslator(r.Context(), authReq) - data := l.getUserData(r, authReq, "PasswordChange.Title", "PasswordChange.Description", errType, errMessage) + data := l.getUserData(r, authReq, translator, "PasswordChange.Title", "PasswordChange.Description", "", "") l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangePasswordDone], data, nil) } diff --git a/internal/api/ui/login/device_auth.go b/internal/api/ui/login/device_auth.go index e2322ee04f..ff5031ebcf 100644 --- a/internal/api/ui/login/device_auth.go +++ b/internal/api/ui/login/device_auth.go @@ -28,13 +28,13 @@ func (l *Login) renderDeviceAuthUserCode(w http.ResponseWriter, r *http.Request, logging.WithError(err).Error() errID, errMessage = l.getErrorMessage(r, err) } - - data := l.getBaseData(r, nil, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage) translator := l.getTranslator(r.Context(), nil) + data := l.getBaseData(r, nil, translator, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthUserCode], data, nil) } func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, scopes []string) { + translator := l.getTranslator(r.Context(), authReq) data := &struct { baseData AuthRequestID string @@ -42,14 +42,13 @@ func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, a ClientID string Scopes []string }{ - baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""), + baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""), AuthRequestID: authReq.ID, Username: authReq.UserName, ClientID: authReq.ApplicationID, Scopes: scopes, } - translator := l.getTranslator(r.Context(), authReq) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthAction], data, nil) } @@ -60,14 +59,13 @@ const ( // renderDeviceAuthDone renders success.html when the action was allowed and error.html when it was denied. func (l *Login) renderDeviceAuthDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, action string) { + translator := l.getTranslator(r.Context(), authReq) data := &struct { baseData Message string }{ - baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""), + baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""), } - - translator := l.getTranslator(r.Context(), authReq) switch action { case deviceAuthAllowed: data.Message = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Approved", nil) diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index 3bab7b90e6..a2f0afcbc2 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -549,7 +549,7 @@ func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Requ translator := l.getTranslator(r.Context(), authReq) data := externalNotFoundOptionData{ - baseData: l.getBaseData(r, authReq, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage), + baseData: l.getBaseData(r, authReq, translator, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage), externalNotFoundOptionFormData: externalNotFoundOptionFormData{ externalRegisterFormData: externalRegisterFormData{ Email: human.EmailAddress, diff --git a/internal/api/ui/login/init_password_handler.go b/internal/api/ui/login/init_password_handler.go index e6939e0c09..6c25130635 100644 --- a/internal/api/ui/login/init_password_handler.go +++ b/internal/api/ui/login/init_password_handler.go @@ -122,7 +122,7 @@ func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authR translator := l.getTranslator(r.Context(), authReq) data := initPasswordData{ - baseData: l.getBaseData(r, authReq, "InitPassword.Title", "InitPassword.Description", errID, errMessage), + baseData: l.getBaseData(r, authReq, translator, "InitPassword.Title", "InitPassword.Description", errID, errMessage), profileData: l.getProfileData(authReq), UserID: userID, Code: code, @@ -153,8 +153,8 @@ func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authR } func (l *Login) renderInitPasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) { - data := l.getUserData(r, authReq, "InitPasswordDone.Title", "InitPasswordDone.Description", "", "") translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "InitPasswordDone.Title", "InitPasswordDone.Description", "", "") if authReq == nil { l.customTexts(r.Context(), translator, orgID) } diff --git a/internal/api/ui/login/init_user_handler.go b/internal/api/ui/login/init_user_handler.go index df2f940d6a..850ddb32ae 100644 --- a/internal/api/ui/login/init_user_handler.go +++ b/internal/api/ui/login/init_user_handler.go @@ -118,7 +118,7 @@ func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq * translator := l.getTranslator(r.Context(), authReq) data := initUserData{ - baseData: l.getBaseData(r, authReq, "InitUser.Title", "InitUser.Description", errID, errMessage), + baseData: l.getBaseData(r, authReq, translator, "InitUser.Title", "InitUser.Description", errID, errMessage), profileData: l.getProfileData(authReq), UserID: userID, Code: code, @@ -155,8 +155,8 @@ func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq * } func (l *Login) renderInitUserDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) { - data := l.getUserData(r, authReq, "InitUserDone.Title", "InitUserDone.Description", "", "") translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "InitUserDone.Title", "InitUserDone.Description", "", "") if authReq == nil { l.customTexts(r.Context(), translator, orgID) } diff --git a/internal/api/ui/login/ldap_handler.go b/internal/api/ui/login/ldap_handler.go index 3ec49f4a7f..93590458f6 100644 --- a/internal/api/ui/login/ldap_handler.go +++ b/internal/api/ui/login/ldap_handler.go @@ -35,8 +35,9 @@ func (l *Login) renderLDAPLogin(w http.ResponseWriter, r *http.Request, authReq errID, errMessage = l.getErrorMessage(r, err) } temp := l.renderer.Templates[tmplLDAPLogin] - data := l.getUserData(r, authReq, "Login.Title", "Login.Description", errID, errMessage) - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), temp, data, nil) + translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "Login.Title", "Login.Description", errID, errMessage) + l.renderer.RenderTemplate(w, r, translator, temp, data, nil) } func (l *Login) handleLDAPCallback(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/link_users_handler.go b/internal/api/ui/login/link_users_handler.go index 09d42e91ec..1952ed1213 100644 --- a/internal/api/ui/login/link_users_handler.go +++ b/internal/api/ui/login/link_users_handler.go @@ -19,6 +19,7 @@ func (l *Login) linkUsers(w http.ResponseWriter, r *http.Request, authReq *domai func (l *Login) renderLinkUsersDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) { var errType, errMessage string - data := l.getUserData(r, authReq, "LinkingUsersDone.Title", "LinkingUsersDone.Description", errType, errMessage) - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLinkUsersDone], data, nil) + translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "LinkingUsersDone.Title", "LinkingUsersDone.Description", errType, errMessage) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLinkUsersDone], data, nil) } diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index dc8e834fcd..71bba44d5d 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -2,15 +2,12 @@ package login import ( "context" - "fmt" "net/http" "strings" "time" "github.com/gorilla/csrf" "github.com/gorilla/mux" - "github.com/rakyll/statik/fs" - "github.com/zitadel/zitadel/feature" "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" @@ -93,17 +90,12 @@ func CreateLogin(config Config, userCodeAlg: userCodeAlg, featureCheck: featureCheck, } - statikFS, err := fs.NewWithNamespace("login") - if err != nil { - return nil, fmt.Errorf("unable to create filesystem: %w", err) - } - csrfInterceptor := createCSRFInterceptor(config.CSRFCookieName, csrfCookieKey, externalSecure, login.csrfErrorHandler()) cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache) security := middleware.SecurityHeaders(csp(), login.cspErrorHandler) - login.router = CreateRouter(login, statikFS, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor, accessHandler) - login.renderer = CreateRenderer(HandlerPrefix, statikFS, staticStorage, config.LanguageCookieName) + login.router = CreateRouter(login, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor, accessHandler) + login.renderer = CreateRenderer(HandlerPrefix, staticStorage, config.LanguageCookieName) login.parser = form.NewParser() return login, nil } diff --git a/internal/api/ui/login/login_handler.go b/internal/api/ui/login/login_handler.go index c141600926..2369490638 100644 --- a/internal/api/ui/login/login_handler.go +++ b/internal/api/ui/login/login_handler.go @@ -99,7 +99,8 @@ func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *dom l.handleIDP(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID) return } - data := l.getUserData(r, authReq, "Login.Title", "Login.Description", errID, errMessage) + translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "Login.Title", "Login.Description", errID, errMessage) funcs := map[string]interface{}{ "hasUsernamePasswordLogin": func() bool { return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowUsernamePassword @@ -111,7 +112,7 @@ func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *dom return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowRegister }, } - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLogin], data, funcs) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLogin], data, funcs) } func singleIDPAllowed(authReq *domain.AuthRequest) bool { diff --git a/internal/api/ui/login/login_success_handler.go b/internal/api/ui/login/login_success_handler.go index f05ee48185..8186bfe61b 100644 --- a/internal/api/ui/login/login_success_handler.go +++ b/internal/api/ui/login/login_success_handler.go @@ -41,8 +41,9 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request, if err != nil { errID, errMessage = l.getErrorMessage(r, err) } + translator := l.getTranslator(r.Context(), authReq) data := loginSuccessData{ - userData: l.getUserData(r, authReq, "LoginSuccess.Title", "", errID, errMessage), + userData: l.getUserData(r, authReq, translator, "LoginSuccess.Title", "", errID, errMessage), } if authReq != nil { data.RedirectURI, err = l.authRequestCallback(r.Context(), authReq) @@ -51,7 +52,7 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request, return } } - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplLoginSuccess], data, nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLoginSuccess], data, nil) } func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) { diff --git a/internal/api/ui/login/logout_handler.go b/internal/api/ui/login/logout_handler.go index 2146d47e49..e270cd5541 100644 --- a/internal/api/ui/login/logout_handler.go +++ b/internal/api/ui/login/logout_handler.go @@ -13,6 +13,7 @@ func (l *Login) handleLogoutDone(w http.ResponseWriter, r *http.Request) { } func (l *Login) renderLogoutDone(w http.ResponseWriter, r *http.Request) { - data := l.getUserData(r, nil, "LogoutDone.Title", "LogoutDone.Description", "", "") - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), nil), l.renderer.Templates[tmplLogoutDone], data, nil) + translator := l.getTranslator(r.Context(), nil) + data := l.getUserData(r, nil, translator, "LogoutDone.Title", "LogoutDone.Description", "", "") + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLogoutDone], data, nil) } diff --git a/internal/api/ui/login/mail_verify_handler.go b/internal/api/ui/login/mail_verify_handler.go index bfcb322fa2..50f03df811 100644 --- a/internal/api/ui/login/mail_verify_handler.go +++ b/internal/api/ui/login/mail_verify_handler.go @@ -95,7 +95,7 @@ func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, a translator := l.getTranslator(r.Context(), authReq) data := mailVerificationData{ - baseData: l.getBaseData(r, authReq, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage), + baseData: l.getBaseData(r, authReq, translator, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage), UserID: userID, profileData: l.getProfileData(authReq), } @@ -111,7 +111,7 @@ func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, a func (l *Login) renderMailVerified(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) { translator := l.getTranslator(r.Context(), authReq) data := mailVerificationData{ - baseData: l.getBaseData(r, authReq, "EmailVerificationDone.Title", "EmailVerificationDone.Description", "", ""), + baseData: l.getBaseData(r, authReq, translator, "EmailVerificationDone.Title", "EmailVerificationDone.Description", "", ""), profileData: l.getProfileData(authReq), } if authReq == nil { diff --git a/internal/api/ui/login/mfa_init_done_handler.go b/internal/api/ui/login/mfa_init_done_handler.go index f38927d5e7..437fde29f4 100644 --- a/internal/api/ui/login/mfa_init_done_handler.go +++ b/internal/api/ui/login/mfa_init_done_handler.go @@ -16,7 +16,7 @@ type mfaInitDoneData struct { func (l *Login) renderMFAInitDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaDoneData) { var errType, errMessage string translator := l.getTranslator(r.Context(), authReq) - data.baseData = l.getBaseData(r, authReq, "InitMFADone.Title", "InitMFADone.Description", errType, errMessage) + data.baseData = l.getBaseData(r, authReq, translator, "InitMFADone.Title", "InitMFADone.Description", errType, errMessage) data.profileData = l.getProfileData(authReq) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAInitDone], data, nil) } diff --git a/internal/api/ui/login/mfa_init_sms.go b/internal/api/ui/login/mfa_init_sms.go index 965806c90b..a947918634 100644 --- a/internal/api/ui/login/mfa_init_sms.go +++ b/internal/api/ui/login/mfa_init_sms.go @@ -57,10 +57,11 @@ func (l *Login) renderRegisterSMS(w http.ResponseWriter, r *http.Request, authRe if err != nil { errID, errMessage = l.getErrorMessage(r, err) } - data.baseData = l.getBaseData(r, authReq, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage) + translator := l.getTranslator(r.Context(), authReq) + data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage) data.profileData = l.getProfileData(authReq) data.MFAType = domain.MFATypeOTPSMS - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplMFASMSInit], data, nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFASMSInit], data, nil) } // handleRegisterSMSCheck handles form submissions of the SMS registration. diff --git a/internal/api/ui/login/mfa_init_u2f.go b/internal/api/ui/login/mfa_init_u2f.go index f00b398f0e..2cd1029ee5 100644 --- a/internal/api/ui/login/mfa_init_u2f.go +++ b/internal/api/ui/login/mfa_init_u2f.go @@ -29,14 +29,15 @@ func (l *Login) renderRegisterU2F(w http.ResponseWriter, r *http.Request, authRe if u2f != nil { credentialData = base64.RawURLEncoding.EncodeToString(u2f.CredentialCreationData) } + translator := l.getTranslator(r.Context(), authReq) data := &u2fInitData{ webAuthNData: webAuthNData{ - userData: l.getUserData(r, authReq, "InitMFAU2F.Title", "InitMFAU2F.Description", errID, errMessage), + userData: l.getUserData(r, authReq, translator, "InitMFAU2F.Title", "InitMFAU2F.Description", errID, errMessage), CredentialCreationData: credentialData, }, MFAType: domain.MFATypeU2F, } - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplMFAU2FInit], data, nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAU2FInit], data, nil) } func (l *Login) handleRegisterU2F(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/mfa_init_verify_handler.go b/internal/api/ui/login/mfa_init_verify_handler.go index e6f0749e92..5f0f0e3119 100644 --- a/internal/api/ui/login/mfa_init_verify_handler.go +++ b/internal/api/ui/login/mfa_init_verify_handler.go @@ -71,7 +71,7 @@ func (l *Login) renderMFAInitVerify(w http.ResponseWriter, r *http.Request, auth errID, errMessage = l.getErrorMessage(r, err) } translator := l.getTranslator(r.Context(), authReq) - data.baseData = l.getBaseData(r, authReq, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage) + data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage) data.profileData = l.getProfileData(authReq) if data.MFAType == domain.MFATypeTOTP { code, err := generateQrCode(data.totpData.Url) diff --git a/internal/api/ui/login/mfa_prompt_handler.go b/internal/api/ui/login/mfa_prompt_handler.go index 9f4e8be409..7aba581970 100644 --- a/internal/api/ui/login/mfa_prompt_handler.go +++ b/internal/api/ui/login/mfa_prompt_handler.go @@ -56,7 +56,7 @@ func (l *Login) renderMFAPrompt(w http.ResponseWriter, r *http.Request, authReq } translator := l.getTranslator(r.Context(), authReq) data := mfaData{ - baseData: l.getBaseData(r, authReq, "InitMFAPrompt.Title", "InitMFAPrompt.Description", errID, errMessage), + baseData: l.getBaseData(r, authReq, translator, "InitMFAPrompt.Title", "InitMFAPrompt.Description", errID, errMessage), profileData: l.getProfileData(authReq), } diff --git a/internal/api/ui/login/mfa_verify_handler.go b/internal/api/ui/login/mfa_verify_handler.go index addb7347fb..80f1c94e25 100644 --- a/internal/api/ui/login/mfa_verify_handler.go +++ b/internal/api/ui/login/mfa_verify_handler.go @@ -66,12 +66,12 @@ func (l *Login) renderMFAVerifySelected(w http.ResponseWriter, r *http.Request, if err != nil { errID, errMessage = l.getErrorMessage(r, err) } - data := l.getUserData(r, authReq, "", "", errID, errMessage) + translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "", "", errID, errMessage) if verificationStep == nil { l.renderError(w, r, authReq, err) return } - translator := l.getTranslator(r.Context(), authReq) switch selectedProvider { case domain.MFATypeU2F: diff --git a/internal/api/ui/login/mfa_verify_otp_handler.go b/internal/api/ui/login/mfa_verify_otp_handler.go index 88aa37c947..297485933a 100644 --- a/internal/api/ui/login/mfa_verify_otp_handler.go +++ b/internal/api/ui/login/mfa_verify_otp_handler.go @@ -61,12 +61,13 @@ func (l *Login) renderOTPVerification(w http.ResponseWriter, r *http.Request, au if err != nil { errID, errMessage = l.getErrorMessage(r, err) } + translator := l.getTranslator(r.Context(), authReq) data := &mfaOTPData{ - userData: l.getUserData(r, authReq, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage), + userData: l.getUserData(r, authReq, translator, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage), MFAProviders: removeSelectedProviderFromList(providers, selectedProvider), SelectedProvider: selectedProvider, } - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplOTPVerification], data, nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplOTPVerification], data, nil) } // handleOTPVerificationCheck handles form submissions of the OTP verification. diff --git a/internal/api/ui/login/mfa_verify_u2f_handler.go b/internal/api/ui/login/mfa_verify_u2f_handler.go index 2fc1361b44..c6cbe359ea 100644 --- a/internal/api/ui/login/mfa_verify_u2f_handler.go +++ b/internal/api/ui/login/mfa_verify_u2f_handler.go @@ -37,15 +37,16 @@ func (l *Login) renderU2FVerification(w http.ResponseWriter, r *http.Request, au if webAuthNLogin != nil { credentialData = base64.RawURLEncoding.EncodeToString(webAuthNLogin.CredentialAssertionData) } + translator := l.getTranslator(r.Context(), authReq) data := &mfaU2FData{ webAuthNData: webAuthNData{ - userData: l.getUserData(r, authReq, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage), + userData: l.getUserData(r, authReq, translator, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage), CredentialCreationData: credentialData, }, MFAProviders: providers, SelectedProvider: -1, } - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplU2FVerification], data, nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplU2FVerification], data, nil) } func (l *Login) handleU2FVerification(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/password_handler.go b/internal/api/ui/login/password_handler.go index 9e448842e9..28baf4a1e1 100644 --- a/internal/api/ui/login/password_handler.go +++ b/internal/api/ui/login/password_handler.go @@ -19,7 +19,8 @@ func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq * if err != nil { errID, errMessage = l.getErrorMessage(r, err) } - data := l.getUserData(r, authReq, "Password.Title", "Password.Description", errID, errMessage) + translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "Password.Title", "Password.Description", errID, errMessage) funcs := map[string]interface{}{ "showPasswordReset": func() bool { if authReq.LoginPolicy != nil { @@ -28,7 +29,7 @@ func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq * return true }, } - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPassword], data, funcs) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPassword], data, funcs) } func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/password_reset_handler.go b/internal/api/ui/login/password_reset_handler.go index ea8cd43321..2cd83ab2dc 100644 --- a/internal/api/ui/login/password_reset_handler.go +++ b/internal/api/ui/login/password_reset_handler.go @@ -48,6 +48,7 @@ func (l *Login) renderPasswordResetDone(w http.ResponseWriter, r *http.Request, if err != nil { errID, errMessage = l.getErrorMessage(r, err) } - data := l.getUserData(r, authReq, "PasswordResetDone.Title", "PasswordResetDone.Description", errID, errMessage) - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPasswordResetDone], data, nil) + translator := l.getTranslator(r.Context(), authReq) + data := l.getUserData(r, authReq, translator, "PasswordResetDone.Title", "PasswordResetDone.Description", errID, errMessage) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordResetDone], data, nil) } diff --git a/internal/api/ui/login/passwordless_login_handler.go b/internal/api/ui/login/passwordless_login_handler.go index 58cba32efb..8373a8fbdb 100644 --- a/internal/api/ui/login/passwordless_login_handler.go +++ b/internal/api/ui/login/passwordless_login_handler.go @@ -36,14 +36,15 @@ func (l *Login) renderPasswordlessVerification(w http.ResponseWriter, r *http.Re if passwordSet && authReq.LoginPolicy != nil { passwordSet = authReq.LoginPolicy.AllowUsernamePassword } + translator := l.getTranslator(r.Context(), authReq) data := &passwordlessData{ webAuthNData{ - userData: l.getUserData(r, authReq, "Passwordless.Title", "Passwordless.Description", errID, errMessage), + userData: l.getUserData(r, authReq, translator, "Passwordless.Title", "Passwordless.Description", errID, errMessage), CredentialCreationData: credentialData, }, passwordSet, } - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplPasswordlessVerification], data, nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessVerification], data, nil) } func (l *Login) handlePasswordlessVerification(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/passwordless_prompt_handler.go b/internal/api/ui/login/passwordless_prompt_handler.go index 24a1eabf6c..ee70b76126 100644 --- a/internal/api/ui/login/passwordless_prompt_handler.go +++ b/internal/api/ui/login/passwordless_prompt_handler.go @@ -31,10 +31,9 @@ func (l *Login) renderPasswordlessPrompt(w http.ResponseWriter, r *http.Request, if err != nil { errID, errMessage = l.getErrorMessage(r, err) } - data := &passwordlessPromptData{ - userData: l.getUserData(r, authReq, "PasswordlessPrompt.Title", "PasswordlessPrompt.Description", errID, errMessage), - } - translator := l.getTranslator(r.Context(), authReq) + data := &passwordlessPromptData{ + userData: l.getUserData(r, authReq, translator, "PasswordlessPrompt.Title", "PasswordlessPrompt.Description", errID, errMessage), + } l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessPrompt], data, nil) } diff --git a/internal/api/ui/login/passwordless_registration_handler.go b/internal/api/ui/login/passwordless_registration_handler.go index b70aac0c60..4c2d379b48 100644 --- a/internal/api/ui/login/passwordless_registration_handler.go +++ b/internal/api/ui/login/passwordless_registration_handler.go @@ -99,11 +99,10 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re if webAuthNToken != nil { credentialData = base64.RawURLEncoding.EncodeToString(webAuthNToken.CredentialCreationData) } - translator := l.getTranslator(r.Context(), authReq) data := &passwordlessRegistrationData{ webAuthNData{ - userData: l.getUserData(r, authReq, "PasswordlessRegistration.Title", "PasswordlessRegistration.Description", errID, errMessage), + userData: l.getUserData(r, authReq, translator, "PasswordlessRegistration.Title", "PasswordlessRegistration.Description", errID, errMessage), CredentialCreationData: credentialData, }, code, @@ -117,8 +116,6 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re policy, err := l.query.ActiveLabelPolicyByOrg(r.Context(), orgID, false) logging.Log("HANDL-XjWKE").OnError(err).Error("unable to get active label policy") data.LabelPolicy = labelPolicyToDomain(policy) - - translator, err = l.renderer.NewTranslator(r.Context()) if err == nil { texts, err := l.authRepo.GetLoginText(r.Context(), orgID) logging.Log("LOGIN-HJK4t").OnError(err).Warn("could not get custom texts") @@ -193,9 +190,8 @@ func (l *Login) renderPasswordlessRegistrationDone(w http.ResponseWriter, r *htt errID, errMessage = l.getErrorMessage(r, err) } translator := l.getTranslator(r.Context(), authReq) - data := passwordlessRegistrationDoneDate{ - userData: l.getUserData(r, authReq, "PasswordlessRegistrationDone.Title", "PasswordlessRegistrationDone.Description", errID, errMessage), + userData: l.getUserData(r, authReq, translator, "PasswordlessRegistrationDone.Title", "PasswordlessRegistrationDone.Description", errID, errMessage), HideNextButton: authReq == nil, } if authReq == nil { diff --git a/internal/api/ui/login/register_handler.go b/internal/api/ui/login/register_handler.go index d2b4845db8..99ce94b3d9 100644 --- a/internal/api/ui/login/register_handler.go +++ b/internal/api/ui/login/register_handler.go @@ -96,7 +96,6 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) { l.renderRegister(w, r, authRequest, data, err) return } - user, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, nil, nil, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) if err != nil { l.renderRegister(w, r, authRequest, data, err) @@ -160,7 +159,7 @@ func (l *Login) renderRegister(w http.ResponseWriter, r *http.Request, authReque } data := registerData{ - baseData: l.getBaseData(r, authRequest, "RegistrationUser.Title", "RegistrationUser.Description", errID, errMessage), + baseData: l.getBaseData(r, authRequest, translator, "RegistrationUser.Title", "RegistrationUser.Description", errID, errMessage), registerFormData: *formData, } diff --git a/internal/api/ui/login/register_option_handler.go b/internal/api/ui/login/register_option_handler.go index b2d0c9e16f..7d88f76c6c 100644 --- a/internal/api/ui/login/register_option_handler.go +++ b/internal/api/ui/login/register_option_handler.go @@ -54,7 +54,7 @@ func (l *Login) renderRegisterOption(w http.ResponseWriter, r *http.Request, aut } translator := l.getTranslator(r.Context(), authReq) data := registerOptionData{ - baseData: l.getBaseData(r, authReq, "RegisterOption.Title", "RegisterOption.Description", errID, errMessage), + baseData: l.getBaseData(r, authReq, translator, "RegisterOption.Title", "RegisterOption.Description", errID, errMessage), } funcs := map[string]interface{}{ "hasRegistration": func() bool { diff --git a/internal/api/ui/login/register_org_handler.go b/internal/api/ui/login/register_org_handler.go index 662f683d02..fbea67784f 100644 --- a/internal/api/ui/login/register_org_handler.go +++ b/internal/api/ui/login/register_org_handler.go @@ -1,7 +1,6 @@ package login import ( - "context" "net/http" "github.com/zitadel/zitadel/internal/api/authz" @@ -39,8 +38,12 @@ type registerOrgData struct { } func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) { - disallowed, err := l.publicOrgRegistrationIsDisallowed(r.Context()) - if disallowed || err != nil { + restrictions, err := l.query.GetInstanceRestrictions(r.Context()) + if err != nil { + l.renderError(w, r, nil, err) + return + } + if restrictions.DisallowPublicOrgRegistration { w.WriteHeader(http.StatusNotFound) return } @@ -54,8 +57,12 @@ func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) { } func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) { - disallowed, err := l.publicOrgRegistrationIsDisallowed(r.Context()) - if disallowed || err != nil { + restrictions, err := l.query.GetInstanceRestrictions(r.Context()) + if err != nil { + l.renderError(w, r, nil, err) + return + } + if restrictions.DisallowPublicOrgRegistration { w.WriteHeader(http.StatusConflict) return } @@ -99,7 +106,7 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe } translator := l.getTranslator(r.Context(), authRequest) data := registerOrgData{ - baseData: l.getBaseData(r, authRequest, "RegistrationOrg.Title", "RegistrationOrg.Description", errID, errMessage), + baseData: l.getBaseData(r, authRequest, translator, "RegistrationOrg.Title", "RegistrationOrg.Description", errID, errMessage), registerOrgFormData: *formData, } pwPolicy := l.getPasswordComplexityPolicy(r, "0") @@ -130,11 +137,6 @@ func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRe l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplRegisterOrg], data, nil) } -func (l *Login) publicOrgRegistrationIsDisallowed(ctx context.Context) (bool, error) { - restrictions, err := l.query.GetInstanceRestrictions(ctx) - return restrictions.DisallowPublicOrgRegistration, err -} - func (d registerOrgFormData) toUserDomain() *domain.Human { if d.Username == "" { d.Username = string(d.Email) diff --git a/internal/api/ui/login/renderer.go b/internal/api/ui/login/renderer.go index d81ab4567e..a50ed2d4ea 100644 --- a/internal/api/ui/login/renderer.go +++ b/internal/api/ui/login/renderer.go @@ -39,7 +39,7 @@ type LanguageData struct { Lang string } -func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage static.Storage, cookieName string) *Renderer { +func CreateRenderer(pathPrefix string, staticStorage static.Storage, cookieName string) *Renderer { r := &Renderer{ pathPrefix: pathPrefix, staticStorage: staticStorage, @@ -238,7 +238,6 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage } var err error r.Renderer, err = renderer.NewRenderer( - staticDir, tmplMapping, funcs, cookieName, ) @@ -343,13 +342,14 @@ func (l *Login) renderInternalError(w http.ResponseWriter, r *http.Request, auth _, msg = l.getErrorMessage(r, err) } - data := l.getBaseData(r, authReq, "Errors.Internal", "", "Internal", msg) - l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplError], data, nil) + translator := l.getTranslator(r.Context(), authReq) + data := l.getBaseData(r, authReq, translator, "Errors.Internal", "", "Internal", msg) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplError], data, nil) } -func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) userData { +func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) userData { userData := userData{ - baseData: l.getBaseData(r, authReq, titleI18nKey, descriptionI18nKey, errType, errMessage), + baseData: l.getBaseData(r, authReq, translator, titleI18nKey, descriptionI18nKey, errType, errMessage), profileData: l.getProfileData(authReq), } if authReq != nil && authReq.LinkingUsers != nil { @@ -358,9 +358,7 @@ func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, titleI return userData } -func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) baseData { - translator := l.getTranslator(r.Context(), authReq) - +func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) baseData { title := "" if titleI18nKey != "" { title = translator.LocalizeWithoutArgs(titleI18nKey) @@ -418,7 +416,11 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, titleI } func (l *Login) getTranslator(ctx context.Context, authReq *domain.AuthRequest) *i18n.Translator { - translator, err := l.renderer.NewTranslator(ctx) + restrictions, err := l.query.GetInstanceRestrictions(ctx) + if err != nil { + logging.OnError(err).Warn("cannot load instance restrictions to retrieve allowed languages for creating the translator") + } + translator, err := l.renderer.NewTranslator(ctx, restrictions.AllowedLanguages) logging.OnError(err).Warn("cannot load translator") if authReq != nil { l.addLoginTranslations(translator, authReq.DefaultTranslations) diff --git a/internal/api/ui/login/resources_handler.go b/internal/api/ui/login/resources_handler.go index 6abe666e98..7f263f6b8e 100644 --- a/internal/api/ui/login/resources_handler.go +++ b/internal/api/ui/login/resources_handler.go @@ -7,6 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/api/assets" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/i18n" ) type dynamicResourceData struct { @@ -15,8 +16,8 @@ type dynamicResourceData struct { FileName string `schema:"filename"` } -func (l *Login) handleResources(staticDir http.FileSystem) http.Handler { - return http.FileServer(staticDir) +func (l *Login) handleResources() http.Handler { + return http.FileServer(i18n.LoadFilesystem(i18n.LOGIN)) } func (l *Login) handleDynamicResources(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/router.go b/internal/api/ui/login/router.go index 34a0092607..414ffb1919 100644 --- a/internal/api/ui/login/router.go +++ b/internal/api/ui/login/router.go @@ -64,7 +64,7 @@ var ( } ) -func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.MiddlewareFunc) *mux.Router { +func CreateRouter(login *Login, interceptors ...mux.MiddlewareFunc) *mux.Router { router := mux.NewRouter() router.Use(interceptors...) router.HandleFunc(EndpointRoot, login.handleLogin).Methods(http.MethodGet) @@ -113,7 +113,7 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M router.HandleFunc(EndpointExternalRegisterCallback, login.handleExternalLoginCallback).Methods(http.MethodGet) router.HandleFunc(EndpointLogoutDone, login.handleLogoutDone).Methods(http.MethodGet) router.HandleFunc(EndpointDynamicResources, login.handleDynamicResources).Methods(http.MethodGet) - router.PathPrefix(EndpointResources).Handler(login.handleResources(staticDir)).Methods(http.MethodGet) + router.PathPrefix(EndpointResources).Handler(login.handleResources()).Methods(http.MethodGet) router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrg).Methods(http.MethodGet) router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrgCheck).Methods(http.MethodPost) router.HandleFunc(EndpointLoginSuccess, login.handleLoginSuccess).Methods(http.MethodGet) diff --git a/internal/api/ui/login/select_user_handler.go b/internal/api/ui/login/select_user_handler.go index d1078cbb83..2f9292d7ae 100644 --- a/internal/api/ui/login/select_user_handler.go +++ b/internal/api/ui/login/select_user_handler.go @@ -28,7 +28,7 @@ func (l *Login) renderUserSelection(w http.ResponseWriter, r *http.Request, auth descriptionI18nKey = "SelectAccount.DescriptionLinking" } data := userSelectionData{ - baseData: l.getBaseData(r, authReq, titleI18nKey, descriptionI18nKey, "", ""), + baseData: l.getBaseData(r, authReq, translator, titleI18nKey, descriptionI18nKey, "", ""), Users: selectionData.Users, Linking: linking, } diff --git a/internal/api/ui/login/username_change_handler.go b/internal/api/ui/login/username_change_handler.go index 79affe9705..7a497c4eb5 100644 --- a/internal/api/ui/login/username_change_handler.go +++ b/internal/api/ui/login/username_change_handler.go @@ -21,7 +21,7 @@ func (l *Login) renderChangeUsername(w http.ResponseWriter, r *http.Request, aut errID, errMessage = l.getErrorMessage(r, err) } translator := l.getTranslator(r.Context(), authReq) - data := l.getUserData(r, authReq, "UsernameChange.Title", "UsernameChange.Description", errID, errMessage) + data := l.getUserData(r, authReq, translator, "UsernameChange.Title", "UsernameChange.Description", errID, errMessage) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangeUsername], data, nil) } @@ -43,6 +43,6 @@ func (l *Login) handleChangeUsername(w http.ResponseWriter, r *http.Request) { func (l *Login) renderChangeUsernameDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) { var errType, errMessage string translator := l.getTranslator(r.Context(), authReq) - data := l.getUserData(r, authReq, "UsernameChangeDone.Title", "UsernameChangeDone.Description", errType, errMessage) + data := l.getUserData(r, authReq, translator, "UsernameChangeDone.Title", "UsernameChangeDone.Description", errType, errMessage) l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangeUsernameDone], data, nil) } diff --git a/internal/command/command_test.go b/internal/command/command_test.go new file mode 100644 index 0000000000..a28f5f890a --- /dev/null +++ b/internal/command/command_test.go @@ -0,0 +1,22 @@ +package command + +import ( + "testing" + + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/i18n" +) + +var ( + SupportedLanguages = []language.Tag{language.English, language.German} + OnlyAllowedLanguages = []language.Tag{language.English} + AllowedLanguage = language.English + DisallowedLanguage = language.German + UnsupportedLanguage = language.Spanish +) + +func TestMain(m *testing.M) { + i18n.SupportLanguages(SupportedLanguages...) + m.Run() +} diff --git a/internal/command/instance.go b/internal/command/instance.go index 75325b5b32..6913cdc5bd 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -13,6 +13,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/notification/channels/smtp" "github.com/zitadel/zitadel/internal/repository/feature" @@ -107,18 +108,22 @@ type InstanceSetup struct { EmailTemplate []byte MessageTexts []*domain.CustomMessageText SMTPConfiguration *smtp.Config - OIDCSettings *struct { - AccessTokenLifetime time.Duration - IdTokenLifetime time.Duration - RefreshTokenIdleExpiration time.Duration - RefreshTokenExpiration time.Duration - } - Quotas *struct { - Items []*SetQuota - } - Features map[domain.Feature]any - Limits *SetLimits - Restrictions *SetRestrictions + OIDCSettings *OIDCSettings + Quotas *SetQuotas + Features map[domain.Feature]any + Limits *SetLimits + Restrictions *SetRestrictions +} + +type OIDCSettings struct { + AccessTokenLifetime time.Duration + IdTokenLifetime time.Duration + RefreshTokenIdleExpiration time.Duration + RefreshTokenExpiration time.Duration +} + +type SetQuotas struct { + Items []*SetQuota } type SecretGenerators struct { @@ -289,183 +294,32 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str prepareAddDefaultEmailTemplate(instanceAgg, setup.EmailTemplate), } - - if setup.Quotas != nil { - for _, q := range setup.Quotas.Items { - quotaId, err := c.idGenerator.Next() - if err != nil { - return "", "", nil, nil, err - } - validations = append(validations, c.SetQuotaCommand(quota.NewAggregate(quotaId, instanceID), nil, true, q)) - } + if err := setupQuotas(c, &validations, setup.Quotas, instanceID); err != nil { + return "", "", nil, nil, err } - - for _, msg := range setup.MessageTexts { - validations = append(validations, prepareSetInstanceCustomMessageTexts(instanceAgg, msg)) - } - - console := &addOIDCApp{ - AddApp: AddApp{ - Aggregate: *projectAgg, - ID: setup.zitadel.consoleAppID, - Name: consoleAppName, - }, - Version: domain.OIDCVersionV1, - RedirectUris: []string{}, - ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, - GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeUserAgent, - AuthMethodType: domain.OIDCAuthMethodTypeNone, - PostLogoutRedirectUris: []string{}, - DevMode: !c.externalSecure, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: false, - IDTokenRoleAssertion: false, - IDTokenUserinfoAssertion: false, - ClockSkew: 0, - } - + setupMessageTexts(&validations, setup.MessageTexts, instanceAgg) validations = append(validations, AddOrgCommand(ctx, orgAgg, setup.Org.Name), c.prepareSetDefaultOrg(instanceAgg, orgAgg.ID), ) - - var pat *PersonalAccessToken - var machineKey *MachineKey - // only a human or a machine user should be created as owner - if setup.Org.Machine != nil && setup.Org.Machine.Machine != nil && !setup.Org.Machine.Machine.IsZero() { - validations = append(validations, - AddMachineCommand(userAgg, setup.Org.Machine.Machine), - ) - if setup.Org.Machine.Pat != nil { - pat = NewPersonalAccessToken(orgID, userID, setup.Org.Machine.Pat.ExpirationDate, setup.Org.Machine.Pat.Scopes, domain.UserTypeMachine) - pat.TokenID, err = c.idGenerator.Next() - if err != nil { - return "", "", nil, nil, err - } - validations = append(validations, prepareAddPersonalAccessToken(pat, c.keyAlgorithm)) - } - if setup.Org.Machine.MachineKey != nil { - machineKey = NewMachineKey(orgID, userID, setup.Org.Machine.MachineKey.ExpirationDate, setup.Org.Machine.MachineKey.Type) - machineKey.KeyID, err = c.idGenerator.Next() - if err != nil { - return "", "", nil, nil, err - } - validations = append(validations, prepareAddUserMachineKey(machineKey, c.machineKeySize)) - } - } else if setup.Org.Human != nil { - setup.Org.Human.ID = userID - validations = append(validations, - c.AddHumanCommand(setup.Org.Human, orgID, c.userPasswordHasher, c.userEncryption, true), - ) - } - - validations = append(validations, - c.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner), - c.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMOwner), - AddProjectCommand(projectAgg, zitadelProjectName, userID, false, false, false, domain.PrivateLabelingSettingUnspecified), - SetIAMProject(instanceAgg, projectAgg.ID), - - c.AddAPIAppCommand( - &addAPIApp{ - AddApp: AddApp{ - Aggregate: *projectAgg, - ID: setup.zitadel.mgmtAppID, - Name: mgmtAppName, - }, - AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, - }, - nil, - ), - - c.AddAPIAppCommand( - &addAPIApp{ - AddApp: AddApp{ - Aggregate: *projectAgg, - ID: setup.zitadel.adminAppID, - Name: adminAppName, - }, - AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, - }, - nil, - ), - - c.AddAPIAppCommand( - &addAPIApp{ - AddApp: AddApp{ - Aggregate: *projectAgg, - ID: setup.zitadel.authAppID, - Name: authAppName, - }, - AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, - }, - nil, - ), - - c.AddOIDCAppCommand(console, nil), - SetIAMConsoleID(instanceAgg, &console.ClientID, &setup.zitadel.consoleAppID), - ) - - addGeneratedDomain, err := c.addGeneratedInstanceDomain(ctx, instanceAgg, setup.InstanceName) + pat, machineKey, err := setupAdmin(c, &validations, setup.Org.Machine, setup.Org.Human, orgID, userID, userAgg) if err != nil { return "", "", nil, nil, err } - validations = append(validations, addGeneratedDomain...) - if setup.CustomDomain != "" { - validations = append(validations, - c.addInstanceDomain(instanceAgg, setup.CustomDomain, false), - setPrimaryInstanceDomain(instanceAgg, setup.CustomDomain), - ) + setupMinimalInterfaces(c, &validations, instanceAgg, projectAgg, orgAgg, userID, setup.zitadel) + if err := setupGeneratedDomain(ctx, c, &validations, instanceAgg, setup.InstanceName); err != nil { + return "", "", nil, nil, err } - - if setup.SMTPConfiguration != nil { - validations = append(validations, - c.prepareAddSMTPConfig( - instanceAgg, - setup.SMTPConfiguration.From, - setup.SMTPConfiguration.FromName, - setup.SMTPConfiguration.ReplyToAddress, - setup.SMTPConfiguration.SMTP.Host, - setup.SMTPConfiguration.SMTP.User, - []byte(setup.SMTPConfiguration.SMTP.Password), - setup.SMTPConfiguration.Tls, - ), - ) - } - - if setup.OIDCSettings != nil { - validations = append(validations, - c.prepareAddOIDCSettings( - instanceAgg, - setup.OIDCSettings.AccessTokenLifetime, - setup.OIDCSettings.IdTokenLifetime, - setup.OIDCSettings.RefreshTokenIdleExpiration, - setup.OIDCSettings.RefreshTokenExpiration, - ), - ) - } - - for f, value := range setup.Features { - switch v := value.(type) { - case bool: - wm, err := NewInstanceFeatureWriteModel[feature.Boolean](instanceID, f) - if err != nil { - return "", "", nil, nil, err - } - validations = append(validations, prepareSetFeature(wm, feature.Boolean{Boolean: v}, c.idGenerator)) - default: - return "", "", nil, nil, errors.ThrowInvalidArgument(nil, "INST-GE4tg", "Errors.Feature.TypeNotSupported") - } - } - - if setup.Limits != nil { - validations = append(validations, c.SetLimitsCommand(limitsAgg, &limitsWriteModel{}, setup.Limits)) - } - - if setup.Restrictions != nil { - validations = append(validations, c.SetRestrictionsCommand(restrictionsAgg, &restrictionsWriteModel{}, setup.Restrictions)) + setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain) + setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg) + setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg) + if err := setupFeatures(c, &validations, setup.Features, instanceID); err != nil { + return "", "", nil, nil, err } + setupLimits(c, &validations, limitsAgg, setup.Limits) + setupRestrictions(c, &validations, restrictionsAgg, setup.Restrictions) + //nolint:staticcheck cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) if err != nil { return "", "", nil, nil, err @@ -488,6 +342,205 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str }, nil } +func setupLimits(commands *Commands, validations *[]preparation.Validation, limitsAgg *limits.Aggregate, setLimits *SetLimits) { + if setLimits != nil { + *validations = append(*validations, commands.SetLimitsCommand(limitsAgg, &limitsWriteModel{}, setLimits)) + } +} + +func setupRestrictions(commands *Commands, validations *[]preparation.Validation, restrictionsAgg *restrictions.Aggregate, setRestrictions *SetRestrictions) { + if setRestrictions != nil { + *validations = append(*validations, commands.SetRestrictionsCommand(restrictionsAgg, &restrictionsWriteModel{}, setRestrictions)) + } +} + +func setupQuotas(commands *Commands, validations *[]preparation.Validation, setQuotas *SetQuotas, instanceID string) error { + if setQuotas == nil { + return nil + } + for _, q := range setQuotas.Items { + quotaId, err := commands.idGenerator.Next() + if err != nil { + return err + } + *validations = append(*validations, commands.SetQuotaCommand(quota.NewAggregate(quotaId, instanceID), nil, true, q)) + } + return nil +} + +func setupFeatures(commands *Commands, validations *[]preparation.Validation, enableFeatures map[domain.Feature]any, instanceID string) error { + for f, value := range enableFeatures { + switch v := value.(type) { + case bool: + wm, err := NewInstanceFeatureWriteModel[feature.Boolean](instanceID, f) + if err != nil { + return err + } + *validations = append(*validations, prepareSetFeature(wm, feature.Boolean{Boolean: v}, commands.idGenerator)) + default: + return errors.ThrowInvalidArgument(nil, "INST-GE4tg", "Errors.Feature.TypeNotSupported") + } + } + return nil +} + +func setupOIDCSettings(commands *Commands, validations *[]preparation.Validation, oidcSettings *OIDCSettings, instanceAgg *instance.Aggregate) { + if oidcSettings == nil { + return + } + *validations = append(*validations, + commands.prepareAddOIDCSettings( + instanceAgg, + oidcSettings.AccessTokenLifetime, + oidcSettings.IdTokenLifetime, + oidcSettings.RefreshTokenIdleExpiration, + oidcSettings.RefreshTokenExpiration, + ), + ) +} + +func setupSMTPSettings(commands *Commands, validations *[]preparation.Validation, smtpConfig *smtp.Config, instanceAgg *instance.Aggregate) { + if smtpConfig == nil { + return + } + *validations = append(*validations, + commands.prepareAddSMTPConfig( + instanceAgg, + smtpConfig.From, + smtpConfig.FromName, + smtpConfig.ReplyToAddress, + smtpConfig.SMTP.Host, + smtpConfig.SMTP.User, + []byte(smtpConfig.SMTP.Password), + smtpConfig.Tls, + ), + ) +} + +func setupCustomDomain(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, customDomain string) { + if customDomain == "" { + return + } + *validations = append(*validations, + commands.addInstanceDomain(instanceAgg, customDomain, false), + setPrimaryInstanceDomain(instanceAgg, customDomain), + ) +} + +func setupGeneratedDomain(ctx context.Context, commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, instanceName string) error { + addGeneratedDomain, err := commands.addGeneratedInstanceDomain(ctx, instanceAgg, instanceName) + if err != nil { + return err + } + *validations = append(*validations, addGeneratedDomain...) + return nil +} + +func setupMinimalInterfaces(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, projectAgg *project.Aggregate, orgAgg *org.Aggregate, userID string, ids ZitadelConfig) { + cnsl := &addOIDCApp{ + AddApp: AddApp{ + Aggregate: *projectAgg, + ID: ids.consoleAppID, + Name: consoleAppName, + }, + Version: domain.OIDCVersionV1, + RedirectUris: []string{}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: domain.OIDCApplicationTypeUserAgent, + AuthMethodType: domain.OIDCAuthMethodTypeNone, + PostLogoutRedirectUris: []string{}, + DevMode: !commands.externalSecure, + AccessTokenType: domain.OIDCTokenTypeBearer, + AccessTokenRoleAssertion: false, + IDTokenRoleAssertion: false, + IDTokenUserinfoAssertion: false, + ClockSkew: 0, + } + *validations = append(*validations, + commands.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner), + commands.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMOwner), + AddProjectCommand(projectAgg, zitadelProjectName, userID, false, false, false, domain.PrivateLabelingSettingUnspecified), + SetIAMProject(instanceAgg, projectAgg.ID), + + commands.AddAPIAppCommand( + &addAPIApp{ + AddApp: AddApp{ + Aggregate: *projectAgg, + ID: ids.mgmtAppID, + Name: mgmtAppName, + }, + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + nil, + ), + + commands.AddAPIAppCommand( + &addAPIApp{ + AddApp: AddApp{ + Aggregate: *projectAgg, + ID: ids.adminAppID, + Name: adminAppName, + }, + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + nil, + ), + + commands.AddAPIAppCommand( + &addAPIApp{ + AddApp: AddApp{ + Aggregate: *projectAgg, + ID: ids.authAppID, + Name: authAppName, + }, + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + nil, + ), + + commands.AddOIDCAppCommand(cnsl, nil), + SetIAMConsoleID(instanceAgg, &cnsl.ClientID, &ids.consoleAppID), + ) +} + +func setupAdmin(commands *Commands, validations *[]preparation.Validation, machine *AddMachine, human *AddHuman, orgID, userID string, userAgg *user.Aggregate) (pat *PersonalAccessToken, machineKey *MachineKey, err error) { + // only a human or a machine user should be created as owner + if machine != nil && machine.Machine != nil && !machine.Machine.IsZero() { + *validations = append(*validations, + AddMachineCommand(userAgg, machine.Machine), + ) + if machine.Pat != nil { + pat = NewPersonalAccessToken(orgID, userID, machine.Pat.ExpirationDate, machine.Pat.Scopes, domain.UserTypeMachine) + pat.TokenID, err = commands.idGenerator.Next() + if err != nil { + return nil, nil, err + } + *validations = append(*validations, prepareAddPersonalAccessToken(pat, commands.keyAlgorithm)) + } + if machine.MachineKey != nil { + machineKey = NewMachineKey(orgID, userID, machine.MachineKey.ExpirationDate, machine.MachineKey.Type) + machineKey.KeyID, err = commands.idGenerator.Next() + if err != nil { + return nil, nil, err + } + *validations = append(*validations, prepareAddUserMachineKey(machineKey, commands.machineKeySize)) + } + } else if human != nil { + human.ID = userID + *validations = append(*validations, + commands.AddHumanCommand(human, orgID, commands.userPasswordHasher, commands.userEncryption, true), + ) + } + return pat, machineKey, nil +} + +func setupMessageTexts(validations *[]preparation.Validation, setupMessageTexts []*domain.CustomMessageText, instanceAgg *instance.Aggregate) { + for _, msg := range setupMessageTexts { + *validations = append(*validations, prepareSetInstanceCustomMessageTexts(instanceAgg, msg)) + } +} + func (c *Commands) UpdateInstance(ctx context.Context, name string) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) validation := c.prepareUpdateInstance(instanceAgg, name) @@ -656,16 +709,27 @@ func (c *Commands) prepareUpdateInstance(a *instance.Aggregate, name string) pre func (c *Commands) prepareSetDefaultLanguage(a *instance.Aggregate, defaultLanguage language.Tag) preparation.Validation { return func() (preparation.CreateCommands, error) { - if defaultLanguage == language.Und { - return nil, errors.ThrowInvalidArgument(nil, "INST-28nlD", "Errors.Invalid.Argument") + if err := domain.LanguageIsDefined(defaultLanguage); err != nil { + return nil, err + } + if err := domain.LanguagesAreSupported(i18n.SupportedLanguages(), defaultLanguage); err != nil { + return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { writeModel, err := getInstanceWriteModel(ctx, filter) + if writeModel.DefaultLanguage == defaultLanguage { + return nil, errors.ThrowPreconditionFailed(nil, "INST-DS3rq", "Errors.Instance.NotChanged") + } + instanceID := authz.GetInstance(ctx).InstanceID() + restrictionsWM, err := c.getRestrictionsWriteModel(ctx, instanceID, instanceID) if err != nil { return nil, err } - if writeModel.DefaultLanguage == defaultLanguage { - return nil, errors.ThrowPreconditionFailed(nil, "INST-DS3rq", "Errors.Instance.NotChanged") + if err := domain.LanguageIsAllowed(false, restrictionsWM.allowedLanguages, defaultLanguage); err != nil { + return nil, err + } + if err != nil { + return nil, err } return []eventstore.Command{instance.NewDefaultLanguageSetEvent(ctx, &a.Aggregate, defaultLanguage)}, nil }, nil diff --git a/internal/command/instance_custom_login_text.go b/internal/command/instance_custom_login_text.go index 7b332b0a15..321d159768 100644 --- a/internal/command/instance_custom_login_text.go +++ b/internal/command/instance_custom_login_text.go @@ -2,16 +2,18 @@ package command import ( "context" - - "github.com/zitadel/zitadel/internal/api/authz" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/repository/instance" ) +// SetCustomInstanceLoginText only validates if the language is supported, not if it is allowed. +// This enables setting texts before allowing a language func (c *Commands) SetCustomInstanceLoginText(ctx context.Context, loginText *domain.CustomLoginText) (*domain.ObjectDetails, error) { iamAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) events, existingMailText, err := c.setCustomInstanceLoginText(ctx, &iamAgg.Aggregate, loginText) @@ -53,8 +55,8 @@ func (c *Commands) RemoveCustomInstanceLoginTexts(ctx context.Context, lang lang } func (c *Commands) setCustomInstanceLoginText(ctx context.Context, instanceAgg *eventstore.Aggregate, text *domain.CustomLoginText) ([]eventstore.Command, *InstanceCustomLoginTextReadModel, error) { - if !text.IsValid() { - return nil, nil, caos_errs.ThrowInvalidArgument(nil, "Instance-kd9fs", "Errors.CustomText.Invalid") + if err := text.IsValid(i18n.SupportedLanguages()); err != nil { + return nil, nil, err } existingLoginText, err := c.defaultLoginTextWriteModelByID(ctx, text.Language) if err != nil { diff --git a/internal/command/instance_custom_login_text_test.go b/internal/command/instance_custom_login_text_test.go index fdb47dff17..8b5603be06 100644 --- a/internal/command/instance_custom_login_text_test.go +++ b/internal/command/instance_custom_login_text_test.go @@ -33,20 +33,54 @@ func TestCommandSide_SetCustomIAMLoginText(t *testing.T) { res res }{ { - name: "invalid custom login text, error", + name: "empty custom login text, success", fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), + expectPush(), ), }, args: args{ - ctx: context.Background(), + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + config: &domain.CustomLoginText{ + Language: AllowedLanguage, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "undefined language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), config: &domain.CustomLoginText{}, }, res: res{ err: caos_errs.IsErrorInvalidArgument, }, }, + { + name: "unsupported language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + config: &domain.CustomLoginText{ + Language: UnsupportedLanguage, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, { name: "custom login text set all fields, ok", fields: fields{ diff --git a/internal/command/instance_custom_message_text.go b/internal/command/instance_custom_message_text.go index a70b6c5de8..2f67c6eb2a 100644 --- a/internal/command/instance_custom_message_text.go +++ b/internal/command/instance_custom_message_text.go @@ -9,9 +9,12 @@ import ( "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/repository/instance" ) +// SetDefaultMessageText only validates if the language is supported, not if it is allowed. +// This enables setting texts before allowing a language func (c *Commands) SetDefaultMessageText(ctx context.Context, instanceID string, messageText *domain.CustomMessageText) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(instanceID) events, existingMessageText, err := c.setDefaultMessageText(ctx, &instanceAgg.Aggregate, messageText) @@ -30,8 +33,8 @@ func (c *Commands) SetDefaultMessageText(ctx context.Context, instanceID string, } func (c *Commands) setDefaultMessageText(ctx context.Context, instanceAgg *eventstore.Aggregate, msg *domain.CustomMessageText) ([]eventstore.Command, *InstanceCustomMessageTextWriteModel, error) { - if !msg.IsValid() { - return nil, nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-kd9fs", "Errors.CustomMessageText.Invalid") + if err := msg.IsValid(i18n.SupportedLanguages()); err != nil { + return nil, nil, err } existingMessageText, err := c.defaultCustomMessageTextWriteModelByID(ctx, msg.MessageTextType, msg.Language) @@ -129,8 +132,8 @@ func prepareSetInstanceCustomMessageTexts( msg *domain.CustomMessageText, ) preparation.Validation { return func() (preparation.CreateCommands, error) { - if !msg.IsValid() { - return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-kd9fs", "Errors.CustomMessageText.Invalid") + if err := msg.IsValid(i18n.SupportedLanguages()); err != nil { + return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { existing, err := existingInstanceCustomMessageText(ctx, filter, msg.MessageTextType, msg.Language) diff --git a/internal/command/instance_custom_message_text_test.go b/internal/command/instance_custom_message_text_test.go index b1c8539cab..c7eef8b775 100644 --- a/internal/command/instance_custom_message_text_test.go +++ b/internal/command/instance_custom_message_text_test.go @@ -9,7 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" - caos_errs "github.com/zitadel/zitadel/internal/errors" + zitadel_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/instance" ) @@ -34,19 +34,68 @@ func TestCommandSide_SetDefaultMessageText(t *testing.T) { res res }{ { - name: "invalid custom text, error", + name: "empty message type, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + config: &domain.CustomMessageText{ + Language: AllowedLanguage, + }, + }, + res: res{ + err: zitadel_errs.IsErrorInvalidArgument, + }, + }, + { + name: "empty custom message text, success", fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), + expectPush(), ), }, args: args{ - ctx: context.Background(), - instanceID: "INSTANCE", - config: &domain.CustomMessageText{}, + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + config: &domain.CustomMessageText{ + MessageTextType: "Some type", // TODO: check the type! + Language: AllowedLanguage, + }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "undefined language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + config: &domain.CustomMessageText{}, + }, + res: res{ + err: zitadel_errs.IsErrorInvalidArgument, + }, + }, + { + name: "unsupported language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + config: &domain.CustomMessageText{ + Language: UnsupportedLanguage, + }, + }, + res: res{ + err: zitadel_errs.IsErrorInvalidArgument, }, }, { diff --git a/internal/command/main_test.go b/internal/command/main_test.go index 6c34dfa1e9..7b7ef67d6f 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -212,7 +212,7 @@ func (m *mockInstance) ConsoleApplicationID() string { } func (m *mockInstance) DefaultLanguage() language.Tag { - return language.English + return AllowedLanguage } func (m *mockInstance) DefaultOrganisationID() string { diff --git a/internal/command/org_custom_login_text.go b/internal/command/org_custom_login_text.go index 83a846d247..9c2a709560 100644 --- a/internal/command/org_custom_login_text.go +++ b/internal/command/org_custom_login_text.go @@ -8,9 +8,12 @@ import ( "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/repository/org" ) +// SetOrgLoginText only validates if the language is supported, not if it is allowed. +// This enables setting texts before allowing a language func (c *Commands) SetOrgLoginText(ctx context.Context, resourceOwner string, loginText *domain.CustomLoginText) (*domain.ObjectDetails, error) { if resourceOwner == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-m29rF", "Errors.ResourceOwnerMissing") @@ -32,10 +35,9 @@ func (c *Commands) SetOrgLoginText(ctx context.Context, resourceOwner string, lo } func (c *Commands) setOrgLoginText(ctx context.Context, orgAgg *eventstore.Aggregate, loginText *domain.CustomLoginText) ([]eventstore.Command, *OrgCustomLoginTextReadModel, error) { - if !loginText.IsValid() { - return nil, nil, caos_errs.ThrowInvalidArgument(nil, "ORG-PPo2w", "Errors.CustomText.Invalid") + if err := loginText.IsValid(i18n.SupportedLanguages()); err != nil { + return nil, nil, err } - existingLoginText, err := c.orgCustomLoginTextWriteModelByID(ctx, orgAgg.ID, loginText.Language) if err != nil { return nil, nil, err diff --git a/internal/command/org_custom_login_text_test.go b/internal/command/org_custom_login_text_test.go index 19ddb189cf..419ee724d6 100644 --- a/internal/command/org_custom_login_text_test.go +++ b/internal/command/org_custom_login_text_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" @@ -40,22 +41,44 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { ), }, args: args{ - ctx: context.Background(), - config: &domain.CustomLoginText{}, + ctx: authz.WithInstanceID(context.Background(), "org1"), + config: &domain.CustomLoginText{ + Language: AllowedLanguage, + }, }, res: res{ err: caos_errs.IsErrorInvalidArgument, }, }, { - name: "invalid custom login text, error", + name: "empty custom login text, success", fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), + expectPush(), ), }, args: args{ - ctx: context.Background(), + ctx: authz.WithInstanceID(context.Background(), "org1"), + resourceOwner: "org1", + config: &domain.CustomLoginText{ + Language: AllowedLanguage, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "undefined language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "org1"), resourceOwner: "org1", config: &domain.CustomLoginText{}, }, @@ -63,6 +86,22 @@ func TestCommandSide_SetCustomOrgLoginText(t *testing.T) { err: caos_errs.IsErrorInvalidArgument, }, }, + { + name: "unsupported language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "org1"), + resourceOwner: "org1", + config: &domain.CustomLoginText{ + Language: UnsupportedLanguage, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, { name: "custom login text set all fields, ok", fields: fields{ diff --git a/internal/command/org_custom_message_text.go b/internal/command/org_custom_message_text.go index 7aacd6a35a..8676fefad0 100644 --- a/internal/command/org_custom_message_text.go +++ b/internal/command/org_custom_message_text.go @@ -8,9 +8,12 @@ import ( "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/repository/org" ) +// SetOrgMessageText only validates if the language is supported, not if it is allowed. +// This enables setting texts before allowing a language func (c *Commands) SetOrgMessageText(ctx context.Context, resourceOwner string, messageText *domain.CustomMessageText) (*domain.ObjectDetails, error) { if resourceOwner == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-2biiR", "Errors.ResourceOwnerMissing") @@ -32,10 +35,9 @@ func (c *Commands) SetOrgMessageText(ctx context.Context, resourceOwner string, } func (c *Commands) setOrgMessageText(ctx context.Context, orgAgg *eventstore.Aggregate, message *domain.CustomMessageText) ([]eventstore.Command, *OrgCustomMessageTextReadModel, error) { - if !message.IsValid() { - return nil, nil, caos_errs.ThrowInvalidArgument(nil, "ORG-2jfsf", "Errors.CustomText.Invalid") + if err := message.IsValid(i18n.SupportedLanguages()); err != nil { + return nil, nil, err } - existingMessageText, err := c.orgCustomMessageTextWriteModelByID(ctx, orgAgg.ID, message.MessageTextType, message.Language) if err != nil { return nil, nil, err diff --git a/internal/command/org_custom_message_text_test.go b/internal/command/org_custom_message_text_test.go index 61a562e456..8b24ff5c53 100644 --- a/internal/command/org_custom_message_text_test.go +++ b/internal/command/org_custom_message_text_test.go @@ -8,7 +8,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" - caos_errs "github.com/zitadel/zitadel/internal/errors" + zitadel_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/org" ) @@ -35,32 +35,83 @@ func TestCommandSide_SetCustomMessageText(t *testing.T) { { name: "no resource owner, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: eventstoreExpect(t), }, args: args{ ctx: context.Background(), config: &domain.CustomMessageText{}, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { - name: "invalid custom text, error", + name: "empty message type, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + config: &domain.CustomMessageText{ + Language: AllowedLanguage, + }, + }, + res: res{ + err: zitadel_errs.IsErrorInvalidArgument, + }, + }, + { + name: "empty custom message text, success", fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), + expectPush(), ), }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + config: &domain.CustomMessageText{ + MessageTextType: "Some type", // TODO: check the type! + Language: AllowedLanguage, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "undefined language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, args: args{ ctx: context.Background(), resourceOwner: "org1", config: &domain.CustomMessageText{}, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, + }, + }, + { + name: "unsupported language, error", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + config: &domain.CustomMessageText{ + Language: UnsupportedLanguage, + }, + }, + res: res{ + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -345,7 +396,7 @@ func TestCommandSide_RemoveCustomMessageText(t *testing.T) { lang: language.English, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -361,7 +412,7 @@ func TestCommandSide_RemoveCustomMessageText(t *testing.T) { lang: language.English, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -377,7 +428,7 @@ func TestCommandSide_RemoveCustomMessageText(t *testing.T) { mailTextType: "Template", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -471,6 +522,43 @@ func TestCommandSide_RemoveCustomMessageText(t *testing.T) { }, }, }, + { + name: "remove unsupported language ok, especially because we never validated whether a language is supported in previous ZITADEL versions", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewCustomTextSetEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "Template", + domain.MessageGreeting, + "Greeting", + UnsupportedLanguage, + ), + ), + ), + expectPush( + org.NewCustomTextTemplateRemovedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "Template", + UnsupportedLanguage, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + mailTextType: "Template", + lang: UnsupportedLanguage, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/command/org_test.go b/internal/command/org_test.go index abaeb8d351..9bfe32a1c7 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -141,7 +141,7 @@ func TestCommandSide_AddOrg(t *testing.T) { "lastname1", "nickname1", "displayname1", - language.German, + language.English, domain.GenderMale, "email1", true, @@ -185,7 +185,7 @@ func TestCommandSide_AddOrg(t *testing.T) { "lastname1", "nickname1", "displayname1", - language.German, + language.English, domain.GenderMale, "email1", true, @@ -253,7 +253,7 @@ func TestCommandSide_AddOrg(t *testing.T) { "lastname1", "nickname1", "displayname1", - language.German, + language.English, domain.GenderMale, "email1", true, @@ -321,7 +321,7 @@ func TestCommandSide_AddOrg(t *testing.T) { "lastname1", "nickname1", "displayname1", - language.German, + language.English, domain.GenderMale, "email1", true, @@ -392,7 +392,7 @@ func TestCommandSide_AddOrg(t *testing.T) { "lastname1", "nickname1", "displayname1", - language.German, + language.English, domain.GenderMale, "email1", true, @@ -1181,7 +1181,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { "lastname1", "nickname1", "displayname1", - language.German, + language.English, domain.GenderMale, "email1", false, diff --git a/internal/command/restrictions.go b/internal/command/restrictions.go index 1658c99592..b386ce8287 100644 --- a/internal/command/restrictions.go +++ b/internal/command/restrictions.go @@ -3,16 +3,38 @@ package command import ( "context" + "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/errors" + zitadel_errors "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/repository/restrictions" ) type SetRestrictions struct { DisallowPublicOrgRegistration *bool + AllowedLanguages []language.Tag +} + +func (s *SetRestrictions) Validate(defaultLanguage language.Tag) error { + if s == nil || (s.DisallowPublicOrgRegistration == nil && s.AllowedLanguages == nil) { + return zitadel_errors.ThrowInvalidArgument(nil, "COMMAND-oASwj", "Errors.Restrictions.NoneSpecified") + } + if s.AllowedLanguages != nil { + if err := domain.LanguagesHaveDuplicates(s.AllowedLanguages); err != nil { + return err + } + if err := domain.LanguagesAreSupported(i18n.SupportedLanguages(), s.AllowedLanguages...); err != nil { + return err + } + if err := domain.LanguageIsAllowed(false, s.AllowedLanguages, defaultLanguage); err != nil { + return zitadel_errors.ThrowPreconditionFailedf(err, "COMMAND-L0m2u", "Errors.Restrictions.DefaultLanguageMustBeAllowed") + } + } + return nil } // SetRestrictions creates new restrictions or updates existing restrictions. @@ -60,10 +82,10 @@ func (c *Commands) getRestrictionsWriteModel(ctx context.Context, instanceId, re func (c *Commands) SetRestrictionsCommand(a *restrictions.Aggregate, wm *restrictionsWriteModel, setRestrictions *SetRestrictions) preparation.Validation { return func() (preparation.CreateCommands, error) { - if setRestrictions == nil || setRestrictions.DisallowPublicOrgRegistration == nil { - return nil, errors.ThrowInvalidArgument(nil, "COMMAND-oASwj", "Errors.Restrictions.NoneSpecified") - } return func(ctx context.Context, _ preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + if err := setRestrictions.Validate(authz.GetInstance(ctx).DefaultLanguage()); err != nil { + return nil, err + } changes := wm.NewChanges(setRestrictions) if len(changes) == 0 { return nil, nil diff --git a/internal/command/restrictions_model.go b/internal/command/restrictions_model.go index cabf1981ac..81ada1f4f1 100644 --- a/internal/command/restrictions_model.go +++ b/internal/command/restrictions_model.go @@ -1,13 +1,17 @@ package command import ( + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/restrictions" ) type restrictionsWriteModel struct { eventstore.WriteModel - disallowPublicOrgRegistrations bool + disallowPublicOrgRegistration bool + allowedLanguages []language.Tag } // newRestrictionsWriteModel aggregateId is filled by reducing unit matching events @@ -34,8 +38,15 @@ func (wm *restrictionsWriteModel) Query() *eventstore.SearchQueryBuilder { func (wm *restrictionsWriteModel) Reduce() error { for _, event := range wm.Events { wm.ChangeDate = event.CreatedAt() - if e, ok := event.(*restrictions.SetEvent); ok && e.DisallowPublicOrgRegistrations != nil { - wm.disallowPublicOrgRegistrations = *e.DisallowPublicOrgRegistrations + e, ok := event.(*restrictions.SetEvent) + if !ok { + continue + } + if e.DisallowPublicOrgRegistration != nil { + wm.disallowPublicOrgRegistration = *e.DisallowPublicOrgRegistration + } + if e.AllowedLanguages != nil { + wm.allowedLanguages = *e.AllowedLanguages } } return wm.WriteModel.Reduce() @@ -48,8 +59,11 @@ func (wm *restrictionsWriteModel) NewChanges(setRestrictions *SetRestrictions) ( return nil } changes = make([]restrictions.RestrictionsChange, 0, 1) - if setRestrictions.DisallowPublicOrgRegistration != nil && (wm.disallowPublicOrgRegistrations != *setRestrictions.DisallowPublicOrgRegistration) { - changes = append(changes, restrictions.ChangePublicOrgRegistrations(*setRestrictions.DisallowPublicOrgRegistration)) + if setRestrictions.DisallowPublicOrgRegistration != nil && (wm.disallowPublicOrgRegistration != *setRestrictions.DisallowPublicOrgRegistration) { + changes = append(changes, restrictions.ChangeDisallowPublicOrgRegistration(*setRestrictions.DisallowPublicOrgRegistration)) + } + if setRestrictions.AllowedLanguages != nil && domain.LanguagesDiffer(wm.allowedLanguages, setRestrictions.AllowedLanguages) { + changes = append(changes, restrictions.ChangeAllowedLanguages(setRestrictions.AllowedLanguages)) } return changes } diff --git a/internal/command/restrictions_test.go b/internal/command/restrictions_test.go index 1fcefb8065..6f4bf4ee9d 100644 --- a/internal/command/restrictions_test.go +++ b/internal/command/restrictions_test.go @@ -6,6 +6,7 @@ import ( "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" + "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" @@ -19,7 +20,6 @@ import ( func TestSetRestrictions(t *testing.T) { type fields func(*testing.T) (*eventstore.Eventstore, id.Generator) type args struct { - ctx context.Context setRestrictions *SetRestrictions } type res struct { @@ -40,14 +40,14 @@ func TestSetRestrictions(t *testing.T) { expectFilter(), expectPush( eventFromEventPusherWithInstanceID( - "instance1", + "INSTANCE", restrictions.NewSetEvent( eventstore.NewBaseEventForPush( context.Background(), - &restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate, + &restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate, restrictions.SetEventType, ), - restrictions.ChangePublicOrgRegistrations(true), + restrictions.ChangeDisallowPublicOrgRegistration(true), ), ), ), @@ -55,14 +55,13 @@ func TestSetRestrictions(t *testing.T) { id_mock.NewIDGeneratorExpectIDs(t, "restrictions1") }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "instance1"), setRestrictions: &SetRestrictions{ DisallowPublicOrgRegistration: gu.Ptr(true), }, }, res: res{ want: &domain.ObjectDetails{ - ResourceOwner: "instance1", + ResourceOwner: "INSTANCE", }, }, }, @@ -76,23 +75,23 @@ func TestSetRestrictions(t *testing.T) { restrictions.NewSetEvent( eventstore.NewBaseEventForPush( context.Background(), - &restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate, + &restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate, restrictions.SetEventType, ), - restrictions.ChangePublicOrgRegistrations(true), + restrictions.ChangeDisallowPublicOrgRegistration(true), ), ), ), expectPush( eventFromEventPusherWithInstanceID( - "instance1", + "INSTANCE", restrictions.NewSetEvent( eventstore.NewBaseEventForPush( context.Background(), - &restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate, + &restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate, restrictions.SetEventType, ), - restrictions.ChangePublicOrgRegistrations(false), + restrictions.ChangeDisallowPublicOrgRegistration(false), ), ), ), @@ -100,14 +99,13 @@ func TestSetRestrictions(t *testing.T) { nil }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "instance1"), setRestrictions: &SetRestrictions{ DisallowPublicOrgRegistration: gu.Ptr(false), }, }, res: res{ want: &domain.ObjectDetails{ - ResourceOwner: "instance1", + ResourceOwner: "INSTANCE", }, }, }, @@ -121,10 +119,10 @@ func TestSetRestrictions(t *testing.T) { restrictions.NewSetEvent( eventstore.NewBaseEventForPush( context.Background(), - &restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate, + &restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate, restrictions.SetEventType, ), - restrictions.ChangePublicOrgRegistrations(true), + restrictions.ChangeDisallowPublicOrgRegistration(true), ), ), ), @@ -132,14 +130,13 @@ func TestSetRestrictions(t *testing.T) { nil }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "instance1"), setRestrictions: &SetRestrictions{ DisallowPublicOrgRegistration: gu.Ptr(true), }, }, res: res{ want: &domain.ObjectDetails{ - ResourceOwner: "instance1", + ResourceOwner: "INSTANCE", }, }, }, @@ -152,29 +149,82 @@ func TestSetRestrictions(t *testing.T) { restrictions.NewSetEvent( eventstore.NewBaseEventForPush( context.Background(), - &restrictions.NewAggregate("restrictions1", "instance1", "instance1").Aggregate, + &restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate, restrictions.SetEventType, ), - restrictions.ChangePublicOrgRegistrations(true), + restrictions.ChangeDisallowPublicOrgRegistration(true), ), ), ), ), nil }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "instance1"), setRestrictions: &SetRestrictions{}, }, res: res{ err: zitadel_errs.IsErrorInvalidArgument, }, }, + { + name: "unsupported language restricted", + fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) { + return eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + restrictions.NewSetEvent( + eventstore.NewBaseEventForPush( + context.Background(), + &restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate, + restrictions.SetEventType, + ), + restrictions.ChangeAllowedLanguages(SupportedLanguages), + ), + ), + ), + ), nil + }, + args: args{ + setRestrictions: &SetRestrictions{ + AllowedLanguages: []language.Tag{AllowedLanguage, UnsupportedLanguage}, + }, + }, + res: res{ + err: zitadel_errs.IsErrorInvalidArgument, + }, + }, + { + name: "default language not allowed", + fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) { + return eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + restrictions.NewSetEvent( + eventstore.NewBaseEventForPush( + context.Background(), + &restrictions.NewAggregate("restrictions1", "INSTANCE", "INSTANCE").Aggregate, + restrictions.SetEventType, + ), + restrictions.ChangeAllowedLanguages(OnlyAllowedLanguages), + ), + ), + ), + ), nil + }, + args: args{ + setRestrictions: &SetRestrictions{ + AllowedLanguages: []language.Tag{DisallowedLanguage}, + }, + }, + res: res{ + err: zitadel_errs.IsPreconditionFailed, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := new(Commands) r.eventstore, r.idGenerator = tt.fields(t) - got, err := r.SetInstanceRestrictions(tt.args.ctx, tt.args.setRestrictions) + got, err := r.SetInstanceRestrictions(authz.WithInstance(context.Background(), &mockInstance{}), tt.args.setRestrictions) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_human_profile_test.go b/internal/command/user_human_profile_test.go index aea3ef562e..e8849adf11 100644 --- a/internal/command/user_human_profile_test.go +++ b/internal/command/user_human_profile_test.go @@ -8,7 +8,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" - caos_errs "github.com/zitadel/zitadel/internal/errors" + zitadel_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/user" @@ -36,8 +36,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: eventstoreExpect(t, expectFilter(), ), }, @@ -51,13 +50,13 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { LastName: "lastname", NickName: "nickname", DisplayName: "displayname", - PreferredLanguage: language.German, + PreferredLanguage: AllowedLanguage, Gender: domain.GenderFemale, }, resourceOwner: "org1", }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { @@ -74,7 +73,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { "lastname", "nickname", "displayname", - language.German, + AllowedLanguage, domain.GenderFemale, "email", true, @@ -93,13 +92,13 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { LastName: "lastname", NickName: "nickname", DisplayName: "displayname", - PreferredLanguage: language.German, + PreferredLanguage: AllowedLanguage, Gender: domain.GenderFemale, }, resourceOwner: "org1", }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { @@ -116,7 +115,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { "lastname", "nickname", "displayname", - language.German, + DisallowedLanguage, domain.GenderUnspecified, "email", true, @@ -130,7 +129,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { "lastname2", "nickname2", "displayname2", - language.English, + AllowedLanguage, domain.GenderMale, ), ), @@ -146,7 +145,7 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { LastName: "lastname2", NickName: "nickname2", DisplayName: "displayname2", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, Gender: domain.GenderMale, }, resourceOwner: "org1", @@ -161,7 +160,133 @@ func TestCommandSide_ChangeHumanProfile(t *testing.T) { LastName: "lastname2", NickName: "nickname2", DisplayName: "displayname2", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, + Gender: domain.GenderMale, + }, + }, + }, + { + name: "undefined preferred language, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + DisallowedLanguage, + domain.GenderUnspecified, + "email", + true, + ), + ), + ), + expectPush( + newProfileChangedEvent(context.Background(), + "user1", "org1", + "firstname2", + "lastname2", + "nickname2", + "displayname2", + language.Und, + domain.GenderMale, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + address: &domain.Profile{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + FirstName: "firstname2", + LastName: "lastname2", + NickName: "nickname2", + DisplayName: "displayname2", + Gender: domain.GenderMale, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.Profile{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + FirstName: "firstname2", + LastName: "lastname2", + NickName: "nickname2", + DisplayName: "displayname2", + PreferredLanguage: language.Und, + Gender: domain.GenderMale, + }, + }, + }, { + name: "unsupported preferred language, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + DisallowedLanguage, + domain.GenderUnspecified, + "email", + true, + ), + ), + ), + expectPush( + newProfileChangedEvent(context.Background(), + "user1", "org1", + "firstname2", + "lastname2", + "nickname2", + "displayname2", + UnsupportedLanguage, + domain.GenderMale, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + address: &domain.Profile{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + FirstName: "firstname2", + LastName: "lastname2", + NickName: "nickname2", + DisplayName: "displayname2", + PreferredLanguage: UnsupportedLanguage, + Gender: domain.GenderMale, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.Profile{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + FirstName: "firstname2", + LastName: "lastname2", + NickName: "nickname2", + DisplayName: "displayname2", + PreferredLanguage: UnsupportedLanguage, Gender: domain.GenderMale, }, }, diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index 6e630f8885..d8ee4f1333 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -14,7 +14,7 @@ import ( "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" - caos_errs "github.com/zitadel/zitadel/internal/errors" + zitadel_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/id" @@ -74,7 +74,7 @@ func TestCommandSide_AddHuman(t *testing.T) { }, res: res{ err: func(err error) bool { - return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")) + return errors.Is(err, zitadel_errs.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")) }, }, }, @@ -94,7 +94,7 @@ func TestCommandSide_AddHuman(t *testing.T) { }, res: res{ err: func(err error) bool { - return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty")) + return errors.Is(err, zitadel_errs.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty")) }, }, }, @@ -104,7 +104,7 @@ func TestCommandSide_AddHuman(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - newAddHumanEvent("$plain$x$password", true, true, ""), + newAddHumanEvent("$plain$x$password", true, true, "", AllowedLanguage), ), ), ), @@ -120,18 +120,19 @@ func TestCommandSide_AddHuman(t *testing.T) { Email: Email{ Address: "email@test.ch", }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, allowInitMail: true, }, res: res{ err: func(err error) bool { - return errors.Is(err, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting")) + return errors.Is(err, zitadel_errs.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting")) }, }, }, { name: "domain policy not found, precondition error", + fields: fields{ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), eventstore: expectEventstore( @@ -150,13 +151,13 @@ func TestCommandSide_AddHuman(t *testing.T) { Email: Email{ Address: "email@test.ch", }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, allowInitMail: true, }, res: res{ err: func(err error) bool { - return errors.Is(err, caos_errs.ThrowInternal(nil, "USER-Ggk9n", "Errors.Internal")) + return errors.Is(err, zitadel_errs.ThrowInternal(nil, "USER-Ggk9n", "Errors.Internal")) }, }, }, @@ -192,18 +193,18 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", Verified: true, }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, allowInitMail: true, }, res: res{ err: func(err error) bool { - return errors.Is(err, caos_errs.ThrowInternal(nil, "USER-uQ96e", "Errors.Internal")) + return errors.Is(err, zitadel_errs.ThrowInternal(nil, "USER-uQ96e", "Errors.Internal")) }, }, }, { - name: "add human (with initial code), ok", + name: "add human with undefined preferred language, ok", fields: fields{ eventstore: expectEventstore( expectFilter(), @@ -225,7 +226,7 @@ func TestCommandSide_AddHuman(t *testing.T) { "lastname", "", "firstname lastname", - language.English, + language.Und, domain.GenderUnspecified, "email@test.ch", true, @@ -256,7 +257,142 @@ func TestCommandSide_AddHuman(t *testing.T) { Email: Email{ Address: "email@test.ch", }, - PreferredLanguage: language.English, + }, + secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + wantID: "user1", + }, + }, + { + name: "add human with unsupported preferred language, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &userAgg.Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewHumanAddedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + UnsupportedLanguage, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &userAgg.Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("userinit"), + }, + time.Hour*1, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + newCode: mockCode("userinit", time.Hour), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", + }, + PreferredLanguage: UnsupportedLanguage, + }, + secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + wantID: "user1", + }, + }, + { + name: "add human (with initial code), ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &userAgg.Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewHumanAddedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + AllowedLanguage, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &userAgg.Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("userinit"), + }, + time.Hour*1, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + newCode: mockCode("userinit", time.Hour), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", + }, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -298,7 +434,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", false, true, ""), + newAddHumanEvent("$plain$x$password", false, true, "", AllowedLanguage), user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -327,7 +463,7 @@ func TestCommandSide_AddHuman(t *testing.T) { Email: Email{ Address: "email@test.ch", }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -367,7 +503,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", false, true, ""), + newAddHumanEvent("$plain$x$password", false, true, "", AllowedLanguage), user.NewHumanEmailCodeAddedEventV2(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -399,7 +535,7 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", URLTemplate: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}", }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: false, @@ -439,7 +575,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", false, true, ""), + newAddHumanEvent("$plain$x$password", false, true, "", AllowedLanguage), user.NewHumanEmailCodeAddedEventV2(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -471,7 +607,7 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", ReturnCode: true, }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: false, @@ -512,7 +648,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", true, true, ""), + newAddHumanEvent("$plain$x$password", true, true, "", AllowedLanguage), user.NewHumanEmailVerifiedEvent(context.Background(), &userAgg.Aggregate, ), @@ -534,8 +670,8 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", Verified: true, }, - PreferredLanguage: language.English, PasswordChangeRequired: true, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -575,7 +711,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", true, true, ""), + newAddHumanEvent("$plain$x$password", true, true, "", AllowedLanguage), user.NewHumanEmailVerifiedEvent(context.Background(), &userAgg.Aggregate, ), @@ -597,8 +733,8 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", Verified: true, }, - PreferredLanguage: language.English, PasswordChangeRequired: true, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -638,7 +774,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", true, false, ""), + newAddHumanEvent("$plain$x$password", true, false, "", AllowedLanguage), user.NewHumanEmailVerifiedEvent(context.Background(), &userAgg.Aggregate, ), @@ -660,8 +796,8 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", Verified: true, }, - PreferredLanguage: language.English, PasswordChangeRequired: true, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -713,15 +849,15 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", Verified: true, }, - PreferredLanguage: language.English, PasswordChangeRequired: true, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, }, res: res{ err: func(err error) bool { - return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername")) + return errors.Is(err, zitadel_errs.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername")) }, }, }, @@ -769,7 +905,7 @@ func TestCommandSide_AddHuman(t *testing.T) { "lastname", "", "firstname lastname", - language.English, + AllowedLanguage, domain.GenderUnspecified, "email@test.ch", false, @@ -798,8 +934,8 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", Verified: true, }, - PreferredLanguage: language.English, PasswordChangeRequired: true, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -840,7 +976,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", false, true, "+41711234567"), + newAddHumanEvent("$plain$x$password", false, true, "+41711234567", AllowedLanguage), user.NewHumanEmailVerifiedEvent( context.Background(), &userAgg.Aggregate, @@ -877,7 +1013,7 @@ func TestCommandSide_AddHuman(t *testing.T) { Phone: Phone{ Number: "+41711234567", }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -905,7 +1041,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("", false, true, "+41711234567"), + newAddHumanEvent("", false, true, "+41711234567", AllowedLanguage), user.NewHumanInitialCodeAddedEvent( context.Background(), &userAgg.Aggregate, @@ -941,7 +1077,7 @@ func TestCommandSide_AddHuman(t *testing.T) { Number: "+41711234567", Verified: true, }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -980,7 +1116,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("$plain$x$password", false, true, "+41711234567"), + newAddHumanEvent("$plain$x$password", false, true, "+41711234567", AllowedLanguage), user.NewHumanEmailVerifiedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate), user.NewHumanPhoneCodeAddedEventV2( @@ -1018,7 +1154,7 @@ func TestCommandSide_AddHuman(t *testing.T) { Number: "+41711234567", ReturnCode: true, }, - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -1046,7 +1182,7 @@ func TestCommandSide_AddHuman(t *testing.T) { ), ), expectPush( - newAddHumanEvent("", false, true, ""), + newAddHumanEvent("", false, true, "", AllowedLanguage), user.NewHumanInitialCodeAddedEvent( context.Background(), &userAgg.Aggregate, @@ -1080,13 +1216,13 @@ func TestCommandSide_AddHuman(t *testing.T) { Email: Email{ Address: "email@test.ch", }, - PreferredLanguage: language.English, Metadata: []*AddMetadataEntry{ { Key: "testKey", Value: []byte("testValue"), }, }, + PreferredLanguage: AllowedLanguage, }, secretGenerator: GetMockSecretGenerator(t), allowInitMail: true, @@ -1147,206 +1283,218 @@ func TestCommandSide_ImportHuman(t *testing.T) { err func(error) bool } tests := []struct { - name string - fields fields - args args - res res + name string + given func(t *testing.T) (fields, args) + res res }{ { name: "orgid missing, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - orgID: "", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + ), }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - }, + args{ + ctx: context.Background(), + orgID: "", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + } }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { name: "org policy not found, precondition error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - expectFilter(), - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter(), + ), }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + } }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { name: "password policy not found, precondition error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), ), + expectFilter(), + expectFilter(), ), - ), - expectFilter(), - expectFilter(), - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + } }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { name: "user invalid, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", }, - }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + PreferredLanguage: AllowedLanguage, + }, + }, + } }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, - }, - { + }, { name: "add human (with password and initial code), ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + newAddHumanEvent("$plain$x$password", true, true, "", AllowedLanguage), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectPush( - newAddHumanEvent("$plain$x$password", true, true, ""), - user.NewHumanInitialCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("a"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: true, }, - time.Hour*1, - ), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Password: &domain.Password{ - SecretString: "password", - ChangeRequired: true, - }, - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - }, - secretGenerator: GetMockSecretGenerator(t), + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } }, res: res{ wantHuman: &domain.Human{ @@ -1359,7 +1507,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -1370,61 +1518,63 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, { name: "add human email verified password change not required, ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + newAddHumanEvent("$plain$x$password", false, true, "", AllowedLanguage), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectPush( - newAddHumanEvent("$plain$x$password", false, true, ""), - user.NewHumanEmailVerifiedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - ), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Password: &domain.Password{ - SecretString: "password", - ChangeRequired: false, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), }, - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - IsEmailVerified: true, - }, - }, - secretGenerator: GetMockSecretGenerator(t), + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: false, + }, + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } }, res: res{ wantHuman: &domain.Human{ @@ -1437,7 +1587,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -1449,70 +1599,72 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, { name: "add human email verified passwordless only, ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter(), + expectPush( + newAddHumanEvent("", false, true, "", AllowedLanguage), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + user.NewHumanPasswordlessInitCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "code1", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour, + ), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectFilter(), - expectPush( - newAddHumanEvent("", false, true, ""), - user.NewHumanEmailVerifiedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), - user.NewHumanPasswordlessInitCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - "code1", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("a"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, - time.Hour, - ), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - IsEmailVerified: true, - }, - }, - passwordless: true, - secretGenerator: GetMockSecretGenerator(t), - passwordlessInitCode: GetMockSecretGenerator(t), + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + passwordless: true, + secretGenerator: GetMockSecretGenerator(t), + passwordlessInitCode: GetMockSecretGenerator(t), + } }, res: res{ wantHuman: &domain.Human{ @@ -1525,7 +1677,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -1547,74 +1699,76 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, { name: "add human email verified passwordless and password change not required, ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter(), + expectPush( + newAddHumanEvent("$plain$x$password", false, true, "", AllowedLanguage), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + user.NewHumanPasswordlessInitCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "code1", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour, + ), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectFilter(), - expectPush( - newAddHumanEvent("$plain$x$password", false, true, ""), - user.NewHumanEmailVerifiedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), - user.NewHumanPasswordlessInitCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - "code1", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("a"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: false, }, - time.Hour, - ), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Password: &domain.Password{ - SecretString: "password", - ChangeRequired: false, - }, - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - IsEmailVerified: true, - }, - }, - passwordless: true, - secretGenerator: GetMockSecretGenerator(t), - passwordlessInitCode: GetMockSecretGenerator(t), + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + passwordless: true, + secretGenerator: GetMockSecretGenerator(t), + passwordlessInitCode: GetMockSecretGenerator(t), + } }, res: res{ wantHuman: &domain.Human{ @@ -1627,7 +1781,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -1649,79 +1803,81 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, { name: "add human (with phone), ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + newAddHumanEvent("$plain$x$password", false, true, "+41711234567", AllowedLanguage), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + user.NewHumanPhoneCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectPush( - newAddHumanEvent("$plain$x$password", false, true, "+41711234567"), - user.NewHumanInitialCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("a"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, - time.Hour*1, - ), - user.NewHumanPhoneCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("a"), + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: false, }, - time.Hour*1), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, - }, - Password: &domain.Password{ - SecretString: "password", - ChangeRequired: false, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - Phone: &domain.Phone{ - PhoneNumber: "+41711234567", - }, - }, - secretGenerator: GetMockSecretGenerator(t), + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Phone: &domain.Phone{ + PhoneNumber: "+41711234567", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } }, res: res{ wantHuman: &domain.Human{ @@ -1734,7 +1890,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -1748,73 +1904,75 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, { name: "add human (with verified phone), ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + newAddHumanEvent("$plain$x$password", false, true, "+41711234567", AllowedLanguage), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + user.NewHumanPhoneVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectPush( - newAddHumanEvent("$plain$x$password", false, true, "+41711234567"), - user.NewHumanInitialCodeAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("a"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, - time.Hour*1, - ), - user.NewHumanPhoneVerifiedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, - }, - Password: &domain.Password{ - SecretString: "password", - ChangeRequired: false, - }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - }, - Phone: &domain.Phone{ - PhoneNumber: "+41711234567", - IsPhoneVerified: true, - }, - }, - secretGenerator: GetMockSecretGenerator(t), + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: false, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Phone: &domain.Phone{ + PhoneNumber: "+41711234567", + IsPhoneVerified: true, + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } }, res: res{ wantHuman: &domain.Human{ @@ -1827,7 +1985,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.English, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -1840,127 +1998,69 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, { - name: "add human (with idp), ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + name: "add human (with undefined preferred language), ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + newAddHumanEvent("$plain$x$password", false, true, "", language.Und), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectFilter( - eventFromEventPusher( - org.NewIDPConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "idpID", - "name", - domain.IDPConfigTypeOIDC, - domain.IDPConfigStylingTypeUnspecified, - false, - ), - ), - eventFromEventPusher( - org.NewIDPOIDCConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "clientID", - "idpID", - "issuer", - "authEndpoint", - "tokenEndpoint", - nil, - domain.OIDCMappingFieldUnspecified, - domain.OIDCMappingFieldUnspecified, - ), - ), - ), - expectFilter( - eventFromEventPusher( - org.NewIDPConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "idpID", - "name", - domain.IDPConfigTypeOIDC, - domain.IDPConfigStylingTypeUnspecified, - false, - ), - ), - eventFromEventPusher( - org.NewIDPOIDCConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "clientID", - "idpID", - "issuer", - "authEndpoint", - "tokenEndpoint", - nil, - domain.OIDCMappingFieldUnspecified, - domain.OIDCMappingFieldUnspecified, - ), - ), - eventFromEventPusher( - org.NewIdentityProviderAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "idpID", - domain.IdentityProviderTypeOrg, - ), - ), - ), - expectPush( - newAddHumanEvent("", false, true, ""), - user.NewUserIDPLinkAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - "idpID", - "name", - "externalID", - ), - user.NewHumanEmailVerifiedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - IsEmailVerified: true, - }, - }, - links: []*domain.UserIDPLink{ - { - IDPConfigID: "idpID", - ExternalUserID: "externalID", - DisplayName: "name", - }, - }, - secretGenerator: GetMockSecretGenerator(t), + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: false, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } }, res: res{ wantHuman: &domain.Human{ @@ -1973,7 +2073,238 @@ func TestCommandSide_ImportHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.English, + PreferredLanguage: language.Und, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + State: domain.UserStateInitial, + }, + }, + }, + { + name: "add human (with unsupported preferred language), ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + newAddHumanEvent("$plain$x$password", false, true, "", UnsupportedLanguage), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: UnsupportedLanguage, + }, + Password: &domain.Password{ + SecretString: "password", + ChangeRequired: false, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } + }, + res: res{ + wantHuman: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: UnsupportedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + State: domain.UserStateInitial, + }, + }, + }, + { + name: "add human (with idp), ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + domain.IdentityProviderTypeOrg, + ), + ), + ), + expectPush( + newAddHumanEvent("", false, true, "", AllowedLanguage), + user.NewUserIDPLinkAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "idpID", + "name", + "externalID", + ), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + links: []*domain.UserIDPLink{ + { + IDPConfigID: "idpID", + ExternalUserID: "externalID", + DisplayName: "name", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } + }, + res: res{ + wantHuman: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -1985,153 +2316,155 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, { name: "add human (with idp, creation not allowed), precondition error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), + }, + ) + return e + }(), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), + }, + ) + return e + }(), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + domain.IdentityProviderTypeOrg, + ), + ), ), ), - ), - expectFilter( - eventFromEventPusher( - org.NewPasswordComplexityPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - 1, - false, - false, - false, - false, - ), - ), - ), - expectFilter( - eventFromEventPusher( - org.NewIDPConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "idpID", - "name", - domain.IDPConfigTypeOIDC, - domain.IDPConfigStylingTypeUnspecified, - false, - ), - ), - eventFromEventPusher( - org.NewIDPOIDCConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "clientID", - "idpID", - "issuer", - "authEndpoint", - "tokenEndpoint", - nil, - domain.OIDCMappingFieldUnspecified, - domain.OIDCMappingFieldUnspecified, - ), - ), - eventFromEventPusher( - func() eventstore.Command { - e, _ := org.NewOIDCIDPChangedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "config1", - []idp.OIDCIDPChanges{ - idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), - }, - ) - return e - }(), - ), - ), - expectFilter( - eventFromEventPusher( - org.NewIDPConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "idpID", - "name", - domain.IDPConfigTypeOIDC, - domain.IDPConfigStylingTypeUnspecified, - false, - ), - ), - eventFromEventPusher( - org.NewIDPOIDCConfigAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "clientID", - "idpID", - "issuer", - "authEndpoint", - "tokenEndpoint", - nil, - domain.OIDCMappingFieldUnspecified, - domain.OIDCMappingFieldUnspecified, - ), - ), - eventFromEventPusher( - func() eventstore.Command { - e, _ := org.NewOIDCIDPChangedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "config1", - []idp.OIDCIDPChanges{ - idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), - }, - ) - return e - }(), - ), - eventFromEventPusher( - org.NewIdentityProviderAddedEvent(context.Background(), - &org.NewAggregate("org1").Aggregate, - "idpID", - domain.IdentityProviderTypeOrg, - ), - ), - ), - ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - userPasswordHasher: mockPasswordHasher("x"), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &domain.Human{ - Username: "username", - Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", - PreferredLanguage: language.English, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), }, - Email: &domain.Email{ - EmailAddress: "email@test.ch", - IsEmailVerified: true, - }, - }, - links: []*domain.UserIDPLink{ - { - IDPConfigID: "idpID", - ExternalUserID: "externalID", - DisplayName: "name", - }, - }, - secretGenerator: GetMockSecretGenerator(t), + args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + links: []*domain.UserIDPLink{ + { + IDPConfigID: "idpID", + ExternalUserID: "externalID", + DisplayName: "name", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + f, a := tt.given(t) r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, - userPasswordHasher: tt.fields.userPasswordHasher, + eventstore: f.eventstore, + idGenerator: f.idGenerator, + userPasswordHasher: f.userPasswordHasher, } - gotHuman, gotCode, err := r.ImportHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.passwordless, tt.args.links, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator) + gotHuman, gotCode, err := r.ImportHuman(a.ctx, a.orgID, a.human, a.passwordless, a.links, a.secretGenerator, a.secretGenerator, a.secretGenerator, a.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } @@ -2139,7 +2472,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.wantHuman, gotHuman) + assert.Equal(t, tt.res.wantHuman.PreferredLanguage, gotHuman.PreferredLanguage) assert.Equal(t, tt.res.wantCode, gotCode) } }) @@ -2192,7 +2525,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -2222,7 +2555,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { @@ -2262,7 +2595,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { @@ -2310,7 +2643,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { @@ -2380,7 +2713,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsPreconditionFailed, + err: zitadel_errs.IsPreconditionFailed, }, }, { @@ -2450,7 +2783,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -2537,7 +2870,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -2625,7 +2958,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), expectPush( - newRegisterHumanEvent("email@test.ch", "$plain$x$password", false, false, ""), + newRegisterHumanEvent("email@test.ch", "$plain$x$password", false, false, "", AllowedLanguage), user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -2649,8 +2982,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) { SecretString: "password", }, Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2669,7 +3003,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.Und, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2730,7 +3064,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), expectPush( - newRegisterHumanEvent("username", "$plain$x$password", false, false, ""), + newRegisterHumanEvent("username", "$plain$x$password", false, false, "", AllowedLanguage), user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -2754,8 +3088,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) { SecretString: "password", }, Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2775,7 +3110,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.Und, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2836,7 +3171,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), expectPush( - newRegisterHumanEvent("username", "$plain$x$password", false, true, ""), + newRegisterHumanEvent("username", "$plain$x$password", false, true, "", AllowedLanguage), user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -2861,8 +3196,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) { SecretString: "password", }, Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2881,7 +3217,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.Und, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2942,7 +3278,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), expectPush( - newRegisterHumanEvent("username", "$plain$x$password", false, true, ""), + newRegisterHumanEvent("username", "$plain$x$password", false, true, "", AllowedLanguage), user.NewHumanEmailVerifiedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate), ), @@ -2959,8 +3295,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) { SecretString: "password", }, Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -2980,7 +3317,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.Und, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -3042,7 +3379,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), expectPush( - newRegisterHumanEvent("username", "$plain$x$password", false, true, "+41711234567"), + newRegisterHumanEvent("username", "$plain$x$password", false, true, "+41711234567", AllowedLanguage), user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -3074,8 +3411,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) { human: &domain.Human{ Username: "username", Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -3100,7 +3438,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.Und, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -3164,7 +3502,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), expectPush( - newRegisterHumanEvent("username", "$plain$x$password", false, true, "+41711234567"), + newRegisterHumanEvent("username", "$plain$x$password", false, true, "+41711234567", AllowedLanguage), user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, &crypto.CryptoValue{ @@ -3189,8 +3527,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) { human: &domain.Human{ Username: "username", Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -3216,7 +3555,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.Und, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -3228,6 +3567,218 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, }, + { + name: "add human (with unsupported preferred language), ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + domain.PasswordlessTypeNotAllowed, + "", + time.Hour*1, + time.Hour*2, + time.Hour*3, + time.Hour*4, + time.Hour*5, + ), + ), + ), + expectPush( + newRegisterHumanEvent("username", "$plain$x$password", false, true, "", UnsupportedLanguage), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: UnsupportedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Password: &domain.Password{ + SecretString: "password", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + }, + res: res{ + want: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: UnsupportedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + State: domain.UserStateInitial, + }, + }, + }, + { + name: "add human (with undefined preferred language), ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + domain.PasswordlessTypeNotAllowed, + "", + time.Hour*1, + time.Hour*2, + time.Hour*3, + time.Hour*4, + time.Hour*5, + ), + ), + ), + expectPush( + newRegisterHumanEvent("username", "$plain$x$password", false, true, "", language.Und), + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Password: &domain.Password{ + SecretString: "password", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + }, + res: res{ + want: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + State: domain.UserStateInitial, + }, + }, + }, { name: "add with idp link, email verified, ok", fields: fields{ @@ -3337,7 +3888,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { ), ), expectPush( - newRegisterHumanEvent("username", "$plain$x$password", false, true, ""), + newRegisterHumanEvent("username", "$plain$x$password", false, true, "", AllowedLanguage), user.NewUserIDPLinkAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "idpID", @@ -3361,8 +3912,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) { SecretString: "password", }, Profile: &domain.Profile{ - FirstName: "firstname", - LastName: "lastname", + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -3387,7 +3939,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { FirstName: "firstname", LastName: "lastname", DisplayName: "firstname lastname", - PreferredLanguage: language.Und, + PreferredLanguage: AllowedLanguage, }, Email: &domain.Email{ EmailAddress: "email@test.ch", @@ -3453,7 +4005,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) { userID: "", }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -3470,7 +4022,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) { userID: "user1", }, res: res{ - err: caos_errs.IsNotFound, + err: zitadel_errs.IsNotFound, }, }, { @@ -3487,7 +4039,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) { "lastname", "nickname", "displayname", - language.German, + AllowedLanguage, domain.GenderUnspecified, "email@test.ch", true, @@ -3563,7 +4115,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { userIDs: []string{"user1"}, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -3579,7 +4131,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { userIDs: []string{}, }, res: res{ - err: caos_errs.IsErrorInvalidArgument, + err: zitadel_errs.IsErrorInvalidArgument, }, }, { @@ -3611,7 +4163,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { "lastname", "nickname", "displayname", - language.German, + AllowedLanguage, domain.GenderUnspecified, "email@test.ch", true, @@ -3651,7 +4203,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { "lastname", "nickname", "displayname", - language.German, + AllowedLanguage, domain.GenderUnspecified, "email@test.ch", true, @@ -3667,7 +4219,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { "lastname", "nickname", "displayname", - language.German, + AllowedLanguage, domain.GenderUnspecified, "email@test.ch", true, @@ -3714,7 +4266,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { } } -func newAddHumanEvent(password string, changeRequired, userLoginMustBeDomain bool, phone string) *user.HumanAddedEvent { +func newAddHumanEvent(password string, changeRequired, userLoginMustBeDomain bool, phone string, preferredLanguage language.Tag) *user.HumanAddedEvent { event := user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, "username", @@ -3722,7 +4274,7 @@ func newAddHumanEvent(password string, changeRequired, userLoginMustBeDomain boo "lastname", "", "firstname lastname", - language.English, + preferredLanguage, domain.GenderUnspecified, "email@test.ch", userLoginMustBeDomain, @@ -3736,7 +4288,7 @@ func newAddHumanEvent(password string, changeRequired, userLoginMustBeDomain boo return event } -func newRegisterHumanEvent(username, password string, changeRequired, userLoginMustBeUnique bool, phone string) *user.HumanRegisteredEvent { +func newRegisterHumanEvent(username, password string, changeRequired, userLoginMustBeUnique bool, phone string, preferredLanguage language.Tag) *user.HumanRegisteredEvent { event := user.NewHumanRegisteredEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, username, @@ -3744,7 +4296,7 @@ func newRegisterHumanEvent(username, password string, changeRequired, userLoginM "lastname", "", "firstname lastname", - language.Und, + preferredLanguage, domain.GenderUnspecified, "email@test.ch", userLoginMustBeUnique, @@ -3784,27 +4336,28 @@ func TestAddHumanCommand(t *testing.T) { Email: Email{ Address: "invalid", }, + PreferredLanguage: AllowedLanguage, }, orgID: "ro", }, want: Want{ - ValidationErr: caos_errs.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"), + ValidationErr: zitadel_errs.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"), }, }, { name: "invalid first name", args: args{ human: &AddHuman{ - Username: "username", - PreferredLanguage: language.English, + Username: "username", Email: Email{ Address: "support@zitadel.com", }, + PreferredLanguage: AllowedLanguage, }, orgID: "ro", }, want: Want{ - ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"), + ValidationErr: zitadel_errs.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"), }, }, { @@ -3812,14 +4365,14 @@ func TestAddHumanCommand(t *testing.T) { args: args{ human: &AddHuman{ Username: "username", - PreferredLanguage: language.English, FirstName: "hurst", Email: Email{Address: "support@zitadel.com"}, + PreferredLanguage: AllowedLanguage, }, orgID: "ro", }, want: Want{ - ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"), + ValidationErr: zitadel_errs.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"), }, }, { @@ -3827,17 +4380,17 @@ func TestAddHumanCommand(t *testing.T) { args: args{ human: &AddHuman{ Email: Email{Address: "support@zitadel.com", Verified: true}, - PreferredLanguage: language.English, FirstName: "gigi", LastName: "giraffe", EncodedPasswordHash: "$foo$x$password", Username: "username", + PreferredLanguage: AllowedLanguage, }, orgID: "ro", hasher: mockPasswordHasher("x"), }, want: Want{ - ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.User.Password.NotSupported"), + ValidationErr: zitadel_errs.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.User.Password.NotSupported"), }, }, { @@ -3848,11 +4401,11 @@ func TestAddHumanCommand(t *testing.T) { args: args{ human: &AddHuman{ Email: Email{Address: "support@zitadel.com"}, - PreferredLanguage: language.English, FirstName: "gigi", LastName: "giraffe", Password: "short", Username: "username", + PreferredLanguage: AllowedLanguage, }, orgID: "ro", filter: NewMultiFilter().Append( @@ -3888,7 +4441,7 @@ func TestAddHumanCommand(t *testing.T) { Filter(), }, want: Want{ - CreateErr: caos_errs.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"), + CreateErr: zitadel_errs.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"), }, }, { @@ -3899,11 +4452,11 @@ func TestAddHumanCommand(t *testing.T) { args: args{ human: &AddHuman{ Email: Email{Address: "support@zitadel.com", Verified: true}, - PreferredLanguage: language.English, FirstName: "gigi", LastName: "giraffe", Password: "password", Username: "username", + PreferredLanguage: AllowedLanguage, }, orgID: "ro", hasher: mockPasswordHasher("x"), @@ -3951,7 +4504,150 @@ func TestAddHumanCommand(t *testing.T) { "giraffe", "", "gigi giraffe", - language.English, + AllowedLanguage, + 0, + "support@zitadel.com", + true, + ) + event.AddPasswordData("$plain$x$password", false) + return event + }(), + user.NewHumanEmailVerifiedEvent(context.Background(), &agg.Aggregate), + }, + }, + }, + { + name: "undefined preferred language, ok", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"), + }, + args: args{ + human: &AddHuman{ + Email: Email{Address: "support@zitadel.com", Verified: true}, + FirstName: "gigi", + LastName: "giraffe", + Password: "password", + Username: "username", + }, + orgID: "ro", + hasher: mockPasswordHasher("x"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + filter: NewMultiFilter().Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{}, nil + }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewDomainPolicyAddedEvent( + ctx, + &org.NewAggregate("id").Aggregate, + true, + true, + true, + ), + }, nil + }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewPasswordComplexityPolicyAddedEvent( + ctx, + &org.NewAggregate("id").Aggregate, + 2, + false, + false, + false, + false, + ), + }, nil + }). + Filter(), + }, + want: Want{ + Commands: []eventstore.Command{ + func() *user.HumanAddedEvent { + event := user.NewHumanAddedEvent( + context.Background(), + &agg.Aggregate, + "username", + "gigi", + "giraffe", + "", + "gigi giraffe", + language.Und, + 0, + "support@zitadel.com", + true, + ) + event.AddPasswordData("$plain$x$password", false) + return event + }(), + user.NewHumanEmailVerifiedEvent(context.Background(), &agg.Aggregate), + }, + }, + }, + { + name: "unsupported preferred language, ok", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"), + }, + args: args{ + human: &AddHuman{ + Email: Email{Address: "support@zitadel.com", Verified: true}, + FirstName: "gigi", + LastName: "giraffe", + Password: "password", + Username: "username", + PreferredLanguage: UnsupportedLanguage, + }, + orgID: "ro", + hasher: mockPasswordHasher("x"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + filter: NewMultiFilter().Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{}, nil + }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewDomainPolicyAddedEvent( + ctx, + &org.NewAggregate("id").Aggregate, + true, + true, + true, + ), + }, nil + }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewPasswordComplexityPolicyAddedEvent( + ctx, + &org.NewAggregate("id").Aggregate, + 2, + false, + false, + false, + false, + ), + }, nil + }). + Filter(), + }, + want: Want{ + Commands: []eventstore.Command{ + func() *user.HumanAddedEvent { + event := user.NewHumanAddedEvent( + context.Background(), + &agg.Aggregate, + "username", + "gigi", + "giraffe", + "", + "gigi giraffe", + UnsupportedLanguage, 0, "support@zitadel.com", true, @@ -3971,11 +4667,11 @@ func TestAddHumanCommand(t *testing.T) { args: args{ human: &AddHuman{ Email: Email{Address: "support@zitadel.com", Verified: true}, - PreferredLanguage: language.English, FirstName: "gigi", LastName: "giraffe", EncodedPasswordHash: "$plain$x$password", Username: "username", + PreferredLanguage: AllowedLanguage, }, orgID: "ro", hasher: mockPasswordHasher("x"), @@ -4023,7 +4719,7 @@ func TestAddHumanCommand(t *testing.T) { "giraffe", "", "gigi giraffe", - language.English, + AllowedLanguage, 0, "support@zitadel.com", true, diff --git a/internal/command/user_v2_password_test.go b/internal/command/user_v2_password_test.go index 2465c240f3..a88fd6e6aa 100644 --- a/internal/command/user_v2_password_test.go +++ b/internal/command/user_v2_password_test.go @@ -5,11 +5,11 @@ import ( "testing" "time" + "golang.org/x/text/language" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/text/language" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" diff --git a/internal/config/hook/tag_to_language.go b/internal/config/hook/tag_to_language.go index b8ac8c4f39..e7e5f3acac 100644 --- a/internal/config/hook/tag_to_language.go +++ b/internal/config/hook/tag_to_language.go @@ -5,6 +5,8 @@ import ( "github.com/mitchellh/mapstructure" "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/domain" ) func TagToLanguageHookFunc() mapstructure.DecodeHookFuncType { @@ -21,6 +23,7 @@ func TagToLanguageHookFunc() mapstructure.DecodeHookFuncType { return data, nil } - return language.Parse(data.(string)) + lang, err := domain.ParseLanguage(data.(string)) + return lang[0], err } } diff --git a/internal/domain/custom_login_text.go b/internal/domain/custom_login_text.go index 18d999ce93..63a5599eb8 100644 --- a/internal/domain/custom_login_text.go +++ b/internal/domain/custom_login_text.go @@ -343,8 +343,11 @@ type CustomLoginText struct { Footer FooterText } -func (m *CustomLoginText) IsValid() bool { - return m.Language != language.Und +func (m *CustomLoginText) IsValid(supportedLanguages []language.Tag) error { + if err := LanguageIsDefined(m.Language); err != nil { + return err + } + return LanguagesAreSupported(supportedLanguages, m.Language) } type SelectAccountScreenText struct { diff --git a/internal/domain/custom_message_text.go b/internal/domain/custom_message_text.go index 263cd57794..dbfbb56254 100644 --- a/internal/domain/custom_message_text.go +++ b/internal/domain/custom_message_text.go @@ -3,6 +3,7 @@ package domain import ( "golang.org/x/text/language" + zitadel_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore/v1/models" ) @@ -51,8 +52,14 @@ type CustomMessageText struct { FooterText string } -func (m *CustomMessageText) IsValid() bool { - return m.MessageTextType != "" && m.Language != language.Und +func (m *CustomMessageText) IsValid(supportedLanguages []language.Tag) error { + if m.MessageTextType == "" { + return zitadel_errs.ThrowInvalidArgument(nil, "INSTANCE-kd9fs", "Errors.CustomMessageText.Invalid") + } + if err := LanguageIsDefined(m.Language); err != nil { + return err + } + return LanguagesAreSupported(supportedLanguages, m.Language) } func IsMessageTextType(textType string) bool { diff --git a/internal/domain/language.go b/internal/domain/language.go new file mode 100644 index 0000000000..ffb00b20e0 --- /dev/null +++ b/internal/domain/language.go @@ -0,0 +1,130 @@ +package domain + +import ( + "errors" + + "golang.org/x/text/language" + + z_errors "github.com/zitadel/zitadel/internal/errors" +) + +func StringsToLanguages(langs []string) []language.Tag { + return GenericMapSlice(langs, language.Make) +} + +func LanguagesToStrings(langs []language.Tag) []string { + return GenericMapSlice(langs, func(lang language.Tag) string { return lang.String() }) +} + +func GenericMapSlice[T any, U any](from []T, mapTo func(T) U) []U { + if from == nil { + return nil + } + result := make([]U, len(from)) + for i, lang := range from { + result[i] = mapTo(lang) + } + return result +} + +// LanguagesDiffer returns true if the languages differ. +func LanguagesDiffer(left, right []language.Tag) bool { + if left == nil && right == nil { + return false + } + if left == nil || right == nil || len(left) != len(right) { + return true + } + return !languagesAreContained(left, right) +} + +func LanguageIsAllowed(allowUndefined bool, allowedLanguages []language.Tag, lang language.Tag) error { + err := LanguageIsDefined(lang) + if err != nil && allowUndefined { + return nil + } + if err != nil { + return err + } + if len(allowedLanguages) > 0 && !languageIsContained(allowedLanguages, lang) { + return z_errors.ThrowPreconditionFailed(nil, "LANG-2M9fs", "Errors.Language.NotAllowed") + } + return nil +} + +func LanguagesAreSupported(supportedLanguages []language.Tag, lang ...language.Tag) error { + unsupported := make([]language.Tag, 0) + for _, l := range lang { + if l.IsRoot() { + continue + } + if !languageIsContained(supportedLanguages, l) { + unsupported = append(unsupported, l) + } + } + if len(unsupported) == 0 { + return nil + } + if len(unsupported) == 1 { + return z_errors.ThrowInvalidArgument(nil, "LANG-lg4DP", "Errors.Language.NotSupported") + } + return z_errors.ThrowInvalidArgumentf(nil, "LANG-XHiK5", "Errors.Languages.NotSupported: %s", LanguagesToStrings(unsupported)) +} + +func LanguageIsDefined(lang language.Tag) error { + if lang.IsRoot() { + return z_errors.ThrowInvalidArgument(nil, "LANG-3M9f2", "Errors.Language.Undefined") + } + return nil +} + +// LanguagesHaveDuplicates returns an error if the passed slices contains duplicates. +// The error lists the duplicates. +func LanguagesHaveDuplicates(langs []language.Tag) error { + unique := make(map[language.Tag]struct{}) + duplicates := make([]language.Tag, 0) + for _, lang := range langs { + if _, ok := unique[lang]; ok { + duplicates = append(duplicates, lang) + } + unique[lang] = struct{}{} + } + if len(duplicates) == 0 { + return nil + } + if len(duplicates) > 1 { + return z_errors.ThrowInvalidArgument(nil, "LANG-3M9f2", "Errors.Language.Duplicate") + } + return z_errors.ThrowInvalidArgumentf(nil, "LANG-XHiK5", "Errors.Languages.Duplicate: %s", LanguagesToStrings(duplicates)) +} + +func ParseLanguage(lang ...string) (tags []language.Tag, err error) { + tags = make([]language.Tag, len(lang)) + for i := range lang { + var parseErr error + tags[i], parseErr = language.Parse(lang[i]) + err = errors.Join(err, parseErr) + } + if err != nil { + err = z_errors.ThrowInvalidArgument(err, "LANG-jc8Sq", "Errors.Language.NotParsed") + } + return tags, err +} + +func languagesAreContained(languages, search []language.Tag) bool { + for _, s := range search { + if !languageIsContained(languages, s) { + return false + } + } + return true +} + +func languageIsContained(languages []language.Tag, search language.Tag) bool { + for _, lang := range languages { + if lang == search { + return true + } + } + return false +} diff --git a/internal/eventstore/aggregate.go b/internal/eventstore/aggregate.go index 30053079da..9939d8335c 100644 --- a/internal/eventstore/aggregate.go +++ b/internal/eventstore/aggregate.go @@ -55,6 +55,7 @@ func AggregateFromWriteModel( version Version, ) *Aggregate { return NewAggregate( + // TODO: the linter complains if this function is called without passing a context context.Background(), wm.AggregateID, typ, diff --git a/internal/i18n/bundle.go b/internal/i18n/bundle.go new file mode 100644 index 0000000000..78b4550d83 --- /dev/null +++ b/internal/i18n/bundle.go @@ -0,0 +1,60 @@ +package i18n + +import ( + "encoding/json" + "io" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/BurntSushi/toml" + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" + "sigs.k8s.io/yaml" + + "github.com/zitadel/zitadel/internal/domain" + zitadel_errors "github.com/zitadel/zitadel/internal/errors" +) + +const i18nPath = "/i18n" + +func newBundle(dir http.FileSystem, defaultLanguage language.Tag, allowedLanguages []language.Tag) (*i18n.Bundle, error) { + bundle := i18n.NewBundle(defaultLanguage) + bundle.RegisterUnmarshalFunc("yaml", func(data []byte, v interface{}) error { return yaml.Unmarshal(data, v) }) + bundle.RegisterUnmarshalFunc("json", json.Unmarshal) + bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) + i18nDir, err := dir.Open(i18nPath) + if err != nil { + return nil, zitadel_errors.ThrowNotFound(err, "I18N-MnXRie", "path not found") + } + defer i18nDir.Close() + files, err := i18nDir.Readdir(0) + if err != nil { + return nil, zitadel_errors.ThrowNotFound(err, "I18N-Gew23", "cannot read dir") + } + for _, file := range files { + fileLang, _ := strings.CutSuffix(file.Name(), filepath.Ext(file.Name())) + if err = domain.LanguageIsAllowed(false, allowedLanguages, language.Make(fileLang)); err != nil { + continue + } + if err := addFileFromFileSystemToBundle(dir, bundle, file); err != nil { + return nil, zitadel_errors.ThrowNotFoundf(err, "I18N-ZS2AW", "cannot append file %s to Bundle", file.Name()) + } + } + return bundle, nil +} + +func addFileFromFileSystemToBundle(dir http.FileSystem, bundle *i18n.Bundle, file os.FileInfo) error { + f, err := dir.Open("/i18n/" + file.Name()) + if err != nil { + return err + } + defer f.Close() + content, err := io.ReadAll(f) + if err != nil { + return err + } + _, err = bundle.ParseMessageFileBytes(content, file.Name()) + return err +} diff --git a/internal/i18n/fs.go b/internal/i18n/fs.go new file mode 100644 index 0000000000..eac34ba8e6 --- /dev/null +++ b/internal/i18n/fs.go @@ -0,0 +1,48 @@ +package i18n + +import ( + "net/http" + + "github.com/rakyll/statik/fs" + "github.com/zitadel/logging" +) + +var zitadelFS, loginFS, notificationFS http.FileSystem + +type Namespace string + +const ( + ZITADEL Namespace = "zitadel" + LOGIN Namespace = "login" + NOTIFICATION Namespace = "notification" +) + +func LoadFilesystem(ns Namespace) http.FileSystem { + var err error + defer func() { + if err != nil { + logging.WithFields("namespace", ns).OnError(err).Panic("unable to get namespace") + } + }() + switch ns { + case ZITADEL: + if zitadelFS != nil { + return zitadelFS + } + zitadelFS, err = fs.NewWithNamespace(string(ns)) + return zitadelFS + case LOGIN: + if loginFS != nil { + return loginFS + } + loginFS, err = fs.NewWithNamespace(string(ns)) + return loginFS + case NOTIFICATION: + if notificationFS != nil { + return notificationFS + } + notificationFS, err = fs.NewWithNamespace(string(ns)) + return notificationFS + } + return nil +} diff --git a/internal/i18n/languages.go b/internal/i18n/languages.go new file mode 100644 index 0000000000..f3acdb4eba --- /dev/null +++ b/internal/i18n/languages.go @@ -0,0 +1,51 @@ +package i18n + +import ( + "errors" + "strings" + + "golang.org/x/text/language" +) + +var supportedLanguages []language.Tag + +func SupportedLanguages() []language.Tag { + if supportedLanguages == nil { + panic("supported languages not loaded") + } + return supportedLanguages +} + +func SupportLanguages(languages ...language.Tag) { + supportedLanguages = languages +} + +func MustLoadSupportedLanguagesFromDir() { + var err error + defer func() { + if err != nil { + panic("failed to load supported languages: " + err.Error()) + } + }() + if supportedLanguages != nil { + return + } + i18nDir, err := LoadFilesystem(LOGIN).Open(i18nPath) + if err != nil { + return + } + defer func() { + err = errors.Join(err, i18nDir.Close()) + }() + files, err := i18nDir.Readdir(0) + if err != nil { + return + } + supportedLanguages = make([]language.Tag, 0, len(files)) + for _, file := range files { + lang := language.Make(strings.TrimSuffix(file.Name(), ".yaml")) + if lang != language.Und { + supportedLanguages = append(supportedLanguages, lang) + } + } +} diff --git a/internal/i18n/i18n.go b/internal/i18n/translator.go similarity index 63% rename from internal/i18n/i18n.go rename to internal/i18n/translator.go index a399ae1fc7..a60932bd6c 100644 --- a/internal/i18n/i18n.go +++ b/internal/i18n/translator.go @@ -2,26 +2,15 @@ package i18n import ( "context" - "encoding/json" - "io/ioutil" "net/http" - "os" - "strings" - "github.com/BurntSushi/toml" "github.com/grpc-ecosystem/go-grpc-middleware/util/metautils" "github.com/nicksnyder/go-i18n/v2/i18n" "github.com/zitadel/logging" "golang.org/x/text/language" - "sigs.k8s.io/yaml" "github.com/zitadel/zitadel/internal/api/authz" http_util "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/errors" -) - -const ( - i18nPath = "/i18n" ) type Translator struct { @@ -29,6 +18,7 @@ type Translator struct { cookieName string cookieHandler *http_util.CookieHandler preferredLanguages []string + allowedLanguages []language.Tag } type TranslatorConfig struct { @@ -41,10 +31,27 @@ type Message struct { Text string } -func NewTranslator(dir http.FileSystem, defaultLanguage language.Tag, cookieName string) (*Translator, error) { +// NewZitadelTranslator translates to all supported languages, as the ZITADEL texts are not customizable. +func NewZitadelTranslator(defaultLanguage language.Tag) (*Translator, error) { + return newTranslator(ZITADEL, defaultLanguage, SupportedLanguages(), "") +} + +func NewNotificationTranslator(defaultLanguage language.Tag, allowedLanguages []language.Tag) (*Translator, error) { + return newTranslator(NOTIFICATION, defaultLanguage, allowedLanguages, "") +} + +func NewLoginTranslator(defaultLanguage language.Tag, allowedLanguages []language.Tag, cookieName string) (*Translator, error) { + return newTranslator(LOGIN, defaultLanguage, allowedLanguages, cookieName) +} + +func newTranslator(ns Namespace, defaultLanguage language.Tag, allowedLanguages []language.Tag, cookieName string) (*Translator, error) { t := new(Translator) var err error - t.bundle, err = newBundle(dir, defaultLanguage) + t.allowedLanguages = allowedLanguages + if len(t.allowedLanguages) == 0 { + t.allowedLanguages = SupportedLanguages() + } + t.bundle, err = newBundle(LoadFilesystem(ns), defaultLanguage, t.allowedLanguages) if err != nil { return nil, err } @@ -53,64 +60,8 @@ func NewTranslator(dir http.FileSystem, defaultLanguage language.Tag, cookieName return t, nil } -func newBundle(dir http.FileSystem, defaultLanguage language.Tag) (*i18n.Bundle, error) { - bundle := i18n.NewBundle(defaultLanguage) - bundle.RegisterUnmarshalFunc("yaml", func(data []byte, v interface{}) error { return yaml.Unmarshal(data, v) }) - bundle.RegisterUnmarshalFunc("json", json.Unmarshal) - bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) - i18nDir, err := dir.Open(i18nPath) - if err != nil { - return nil, errors.ThrowNotFound(err, "I18N-MnXRie", "path not found") - } - defer i18nDir.Close() - files, err := i18nDir.Readdir(0) - if err != nil { - return nil, errors.ThrowNotFound(err, "I18N-Gew23", "cannot read dir") - } - for _, file := range files { - if err := addFileFromFileSystemToBundle(dir, bundle, file); err != nil { - return nil, errors.ThrowNotFoundf(err, "I18N-ZS2AW", "cannot append file %s to Bundle", file.Name()) - } - } - return bundle, nil -} - -func addFileFromFileSystemToBundle(dir http.FileSystem, bundle *i18n.Bundle, file os.FileInfo) error { - f, err := dir.Open("/i18n/" + file.Name()) - if err != nil { - return err - } - defer f.Close() - content, err := ioutil.ReadAll(f) - if err != nil { - return err - } - _, err = bundle.ParseMessageFileBytes(content, file.Name()) - return err -} - -func SupportedLanguages(dir http.FileSystem) ([]language.Tag, error) { - i18nDir, err := dir.Open("/i18n") - if err != nil { - return nil, errors.ThrowNotFound(err, "I18N-Dbt42", "cannot open dir") - } - defer i18nDir.Close() - files, err := i18nDir.Readdir(0) - if err != nil { - return nil, errors.ThrowNotFound(err, "I18N-Gh4zk", "cannot read dir") - } - languages := make([]language.Tag, 0, len(files)) - for _, file := range files { - lang := language.Make(strings.TrimSuffix(file.Name(), ".yaml")) - if lang != language.Und { - languages = append(languages, lang) - } - } - return languages, nil -} - func (t *Translator) SupportedLanguages() []language.Tag { - return t.bundle.LanguageTags() + return t.allowedLanguages } func (t *Translator) AddMessages(tag language.Tag, messages ...Message) error { @@ -144,7 +95,7 @@ func (t *Translator) LocalizeWithoutArgs(id string, langs ...string) string { } func (t *Translator) Lang(r *http.Request) language.Tag { - matcher := language.NewMatcher(t.bundle.LanguageTags()) + matcher := language.NewMatcher(t.allowedLanguages) tag, _ := language.MatchStrings(matcher, t.langsFromRequest(r)...) return tag } diff --git a/internal/integration/client.go b/internal/integration/client.go index cc4ccc75bd..4e5f2677dc 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -60,7 +60,7 @@ func newClient(cc *grpc.ClientConn) Client { } func (t *Tester) UseIsolatedInstance(iamOwnerCtx, systemCtx context.Context) (primaryDomain, instanceId string, authenticatedIamOwnerCtx context.Context) { - primaryDomain = randString(5) + ".integration.localhost" + primaryDomain = RandString(5) + ".integration.localhost" instance, err := t.Client.System.CreateInstance(systemCtx, &system.CreateInstanceRequest{ InstanceName: "testinstance", CustomDomain: primaryDomain, @@ -85,8 +85,8 @@ func (t *Tester) UseIsolatedInstance(iamOwnerCtx, systemCtx context.Context) (pr func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse { resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{ - Organisation: &object.Organisation{ - Org: &object.Organisation_OrgId{ + Organization: &object.Organization{ + Org: &object.Organization_OrgId{ OrgId: s.Organisation.ID, }, }, diff --git a/internal/integration/config/zitadel.yaml b/internal/integration/config/zitadel.yaml index 5bf6937af9..8768e8e513 100644 --- a/internal/integration/config/zitadel.yaml +++ b/internal/integration/config/zitadel.yaml @@ -51,3 +51,9 @@ DefaultInstance: SystemAPIUsers: - tester: KeyData: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aStGRlNKTDdmNXl3NEtUd3pnTQpQMzRlUEd5Y20vTStrVDBNN1Y0Q2d4NVYzRWFESXZUUUtUTGZCYUVCNDV6YjlMdGpJWHpEdzByWFJvUzJoTzZ0CmgrQ1lRQ3ozS0N2aDA5QzBJenhaaUIySVMzSC9hVCs1Qng5RUZZK3ZuQWtaamNjYnlHNVlOUnZtdE9sbnZJZUkKSDdxWjB0RXdrUGZGNUdFWk5QSlB0bXkzVUdWN2lvZmRWUVMxeFJqNzMrYU13NXJ2SDREOElkeWlBQzNWZWtJYgpwdDBWajBTVVgzRHdLdG9nMzM3QnpUaVBrM2FYUkYwc2JGaFFvcWRKUkk4TnFnWmpDd2pxOXlmSTV0eXhZc3duCitKR3pIR2RIdlczaWRPRGxtd0V0NUsycGFzaVJJV0syT0dmcSt3MEVjbHRRSGFidXFFUGdabG1oQ2tSZE5maXgKQndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" + Memberships: + - MemberType: System + Roles: + - "SYSTEM_OWNER" + - "IAM_OWNER" + - "ORG_OWNER" diff --git a/internal/integration/rand.go b/internal/integration/rand.go index 4425c97c8c..d4f01b51c8 100644 --- a/internal/integration/rand.go +++ b/internal/integration/rand.go @@ -11,7 +11,7 @@ func init() { var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz") -func randString(n int) string { +func RandString(n int) string { b := make([]rune, n) for i := range b { b[i] = letterRunes[rand.Intn(len(letterRunes))] diff --git a/internal/notification/handlers/mock/queries.mock.go b/internal/notification/handlers/mock/queries.mock.go index 4d4c5516ac..e31aacad0f 100644 --- a/internal/notification/handlers/mock/queries.mock.go +++ b/internal/notification/handlers/mock/queries.mock.go @@ -85,6 +85,21 @@ func (mr *MockQueriesMockRecorder) GetDefaultLanguage(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultLanguage", reflect.TypeOf((*MockQueries)(nil).GetDefaultLanguage), arg0) } +// GetInstanceRestrictions mocks base method. +func (m *MockQueries) GetInstanceRestrictions(arg0 context.Context) (query.Restrictions, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetInstanceRestrictions", arg0) + ret0, _ := ret[0].(query.Restrictions) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetInstanceRestrictions indicates an expected call of GetInstanceRestrictions. +func (mr *MockQueriesMockRecorder) GetInstanceRestrictions(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceRestrictions", reflect.TypeOf((*MockQueries)(nil).GetInstanceRestrictions), arg0) +} + // GetNotifyUserByID mocks base method. func (m *MockQueries) GetNotifyUserByID(arg0 context.Context, arg1 bool, arg2 string, arg3 ...query.SearchQuery) (*query.NotifyUser, error) { m.ctrl.T.Helper() diff --git a/internal/notification/handlers/queries.go b/internal/notification/handlers/queries.go index cd50a26b41..eadfbd7573 100644 --- a/internal/notification/handlers/queries.go +++ b/internal/notification/handlers/queries.go @@ -2,8 +2,6 @@ package handlers import ( "context" - "net/http" - "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/crypto" @@ -25,6 +23,7 @@ type Queries interface { SMSProviderConfig(ctx context.Context, queries ...query.SearchQuery) (*query.SMSConfig, error) SMTPConfigByAggregateID(ctx context.Context, aggregateID string) (*query.SMTPConfig, error) GetDefaultLanguage(ctx context.Context) language.Tag + GetInstanceRestrictions(ctx context.Context) (restrictions query.Restrictions, err error) } type NotificationQueries struct { @@ -37,7 +36,6 @@ type NotificationQueries struct { UserDataCrypto crypto.EncryptionAlgorithm SMTPPasswordCrypto crypto.EncryptionAlgorithm SMSTokenCrypto crypto.EncryptionAlgorithm - statikDir http.FileSystem } func NewNotificationQueries( @@ -50,7 +48,6 @@ func NewNotificationQueries( userDataCrypto crypto.EncryptionAlgorithm, smtpPasswordCrypto crypto.EncryptionAlgorithm, smsTokenCrypto crypto.EncryptionAlgorithm, - statikDir http.FileSystem, ) *NotificationQueries { return &NotificationQueries{ Queries: baseQueries, @@ -62,6 +59,5 @@ func NewNotificationQueries( UserDataCrypto: userDataCrypto, SMTPPasswordCrypto: smtpPasswordCrypto, SMSTokenCrypto: smsTokenCrypto, - statikDir: statikDir, } } diff --git a/internal/notification/handlers/translator.go b/internal/notification/handlers/translator.go index 627bb42a27..d805985795 100644 --- a/internal/notification/handlers/translator.go +++ b/internal/notification/handlers/translator.go @@ -10,7 +10,11 @@ import ( ) func (n *NotificationQueries) GetTranslatorWithOrgTexts(ctx context.Context, orgID, textType string) (*i18n.Translator, error) { - translator, err := i18n.NewTranslator(n.statikDir, n.GetDefaultLanguage(ctx), "") + restrictions, err := n.Queries.GetInstanceRestrictions(ctx) + if err != nil { + return nil, err + } + translator, err := i18n.NewNotificationTranslator(n.GetDefaultLanguage(ctx), restrictions.AllowedLanguages) if err != nil { return nil, err } diff --git a/internal/notification/handlers/user_notifier_test.go b/internal/notification/handlers/user_notifier_test.go index 1ca10a3d15..86b843fc52 100644 --- a/internal/notification/handlers/user_notifier_test.go +++ b/internal/notification/handlers/user_notifier_test.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "fmt" - "net/http" "testing" "time" @@ -12,7 +11,6 @@ import ( "github.com/zitadel/zitadel/internal/notification/messages" - statik_fs "github.com/rakyll/statik/fs" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "golang.org/x/text/language" @@ -202,15 +200,13 @@ func Test_userNotifier_reduceInitCodeAdded(t *testing.T) { }, }} // TODO: Why don't we have an url template on user.HumanInitialCodeAddedEvent? - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reduceInitCodeAdded(a.event) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceInitCodeAdded(a.event) if w.err != nil { w.err(t, err) } else { @@ -423,15 +419,13 @@ func Test_userNotifier_reduceEmailCodeAdded(t *testing.T) { }, w }, }} - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reduceEmailCodeAdded(a.event) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceEmailCodeAdded(a.event) if w.err != nil { w.err(t, err) } else { @@ -644,15 +638,13 @@ func Test_userNotifier_reducePasswordCodeAdded(t *testing.T) { }, w }, }} - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reducePasswordCodeAdded(a.event) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordCodeAdded(a.event) if w.err != nil { w.err(t, err) } else { @@ -737,15 +729,13 @@ func Test_userNotifier_reduceDomainClaimed(t *testing.T) { }, w }, }} - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reduceDomainClaimed(a.event) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceDomainClaimed(a.event) if w.err != nil { w.err(t, err) } else { @@ -963,15 +953,13 @@ func Test_userNotifier_reducePasswordlessCodeRequested(t *testing.T) { }, w }, }} - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reducePasswordlessCodeRequested(a.event) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordlessCodeRequested(a.event) if w.err != nil { w.err(t, err) } else { @@ -1062,15 +1050,13 @@ func Test_userNotifier_reducePasswordChanged(t *testing.T) { }, w }, }} - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - stmt, err := newUserNotifier(t, ctrl, queries, fs, f, a, w).reducePasswordChanged(a.event) + stmt, err := newUserNotifier(t, ctrl, queries, f, a, w).reducePasswordChanged(a.event) if w.err != nil { w.err(t, err) } else { @@ -1287,15 +1273,13 @@ func Test_userNotifier_reduceOTPEmailChallenged(t *testing.T) { }, w }, }} - fs, err := statik_fs.NewWithNamespace("notification") - assert.NoError(t, err) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) queries := mock.NewMockQueries(ctrl) commands := mock.NewMockCommands(ctrl) f, a, w := tt.test(ctrl, queries, commands) - _, err = newUserNotifier(t, ctrl, queries, fs, f, a, w).reduceSessionOTPEmailChallenged(a.event) + _, err := newUserNotifier(t, ctrl, queries, f, a, w).reduceSessionOTPEmailChallenged(a.event) if w.err != nil { w.err(t, err) } else { @@ -1320,7 +1304,7 @@ type want struct { err assert.ErrorAssertionFunc } -func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, fs http.FileSystem, f fields, a args, w want) *userNotifier { +func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQueries, f fields, a args, w want) *userNotifier { queries.EXPECT().NotificationProviderByIDAndType(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&query.DebugNotificationProvider{}, nil) smtpAlg, _ := cryptoValue(t, ctrl, "smtppw") channel := channel_mock.NewMockNotificationChannel(ctrl) @@ -1340,7 +1324,6 @@ func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQu f.userDataCrypto, smtpAlg, f.SMSTokenCrypto, - fs, ), otpEmailTmpl: defaultOTPEmailTemplate, channels: &channels{Chain: *senders.ChainChannels(channel)}, @@ -1366,6 +1349,9 @@ func (c *channels) Webhook(context.Context, webhook.Config) (*senders.Chain, err } func expectTemplateQueries(queries *mock.MockQueries, template string) { + queries.EXPECT().GetInstanceRestrictions(gomock.Any()).Return(query.Restrictions{ + AllowedLanguages: []language.Tag{language.English}, + }, nil) queries.EXPECT().ActiveLabelPolicyByOrg(gomock.Any(), gomock.Any(), gomock.Any()).Return(&query.LabelPolicy{ ID: policyID, Light: query.Theme{ diff --git a/internal/notification/projections.go b/internal/notification/projections.go index b2630d2330..341e351461 100644 --- a/internal/notification/projections.go +++ b/internal/notification/projections.go @@ -3,9 +3,6 @@ package notification import ( "context" - statik_fs "github.com/rakyll/statik/fs" - "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" @@ -29,9 +26,7 @@ func Start( fileSystemPath string, userEncryption, smtpEncryption, smsEncryption crypto.EncryptionAlgorithm, ) { - statikFS, err := statik_fs.NewWithNamespace("notification") - logging.OnError(err).Panic("unable to start listener") - q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption, statikFS) + q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption) c := newChannels(q) handlers.NewUserNotifier(ctx, projection.ApplyCustomConfig(userHandlerCustomConfig), commands, q, c, otpEmailTmpl).Start(ctx) handlers.NewQuotaNotifier(ctx, projection.ApplyCustomConfig(quotaHandlerCustomConfig), commands, q, c).Start(ctx) diff --git a/internal/query/custom_text.go b/internal/query/custom_text.go index 794271f3ec..6641b1b205 100644 --- a/internal/query/custom_text.go +++ b/internal/query/custom_text.go @@ -17,6 +17,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" ) @@ -217,9 +218,9 @@ func (q *Queries) readLoginTranslationFile(ctx context.Context, lang string) ([] contents, ok := q.LoginTranslationFileContents[lang] var err error if !ok { - contents, err = q.readTranslationFile(q.LoginDir, fmt.Sprintf("/i18n/%s.yaml", lang)) + contents, err = q.readTranslationFile(i18n.LOGIN, fmt.Sprintf("/i18n/%s.yaml", lang)) if errors.IsNotFound(err) { - contents, err = q.readTranslationFile(q.LoginDir, fmt.Sprintf("/i18n/%s.yaml", authz.GetInstance(ctx).DefaultLanguage().String())) + contents, err = q.readTranslationFile(i18n.LOGIN, fmt.Sprintf("/i18n/%s.yaml", authz.GetInstance(ctx).DefaultLanguage().String())) } if err != nil { return nil, err diff --git a/internal/query/languages.go b/internal/query/languages.go deleted file mode 100644 index f12c29677e..0000000000 --- a/internal/query/languages.go +++ /dev/null @@ -1,22 +0,0 @@ -package query - -import ( - "context" - - "github.com/zitadel/logging" - "golang.org/x/text/language" - - "github.com/zitadel/zitadel/internal/i18n" -) - -func (q *Queries) Languages(ctx context.Context) ([]language.Tag, error) { - if len(q.supportedLangs) == 0 { - langs, err := i18n.SupportedLanguages(q.LoginDir) - if err != nil { - logging.Log("ADMIN-tiMWs").WithError(err).Debug("unable to parse language") - return nil, err - } - q.supportedLangs = langs - } - return q.supportedLangs, nil -} diff --git a/internal/query/message_text.go b/internal/query/message_text.go index 4bc656c0cc..ab7674c7ae 100644 --- a/internal/query/message_text.go +++ b/internal/query/message_text.go @@ -7,7 +7,6 @@ import ( errs "errors" "fmt" "io/ioutil" - "net/http" "os" "time" @@ -19,6 +18,7 @@ import ( "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" ) @@ -236,9 +236,9 @@ func (q *Queries) readNotificationTextMessages(ctx context.Context, language str var err error contents, ok := q.NotificationTranslationFileContents[language] if !ok { - contents, err = q.readTranslationFile(q.NotificationDir, fmt.Sprintf("/i18n/%s.yaml", language)) + contents, err = q.readTranslationFile(i18n.NOTIFICATION, fmt.Sprintf("/i18n/%s.yaml", language)) if errors.IsNotFound(err) { - contents, err = q.readTranslationFile(q.NotificationDir, fmt.Sprintf("/i18n/%s.yaml", authz.GetInstance(ctx).DefaultLanguage().String())) + contents, err = q.readTranslationFile(i18n.NOTIFICATION, fmt.Sprintf("/i18n/%s.yaml", authz.GetInstance(ctx).DefaultLanguage().String())) } if err != nil { return nil, err @@ -311,8 +311,8 @@ func prepareMessageTextQuery(ctx context.Context, db prepareDatabase) (sq.Select } } -func (q *Queries) readTranslationFile(dir http.FileSystem, filename string) ([]byte, error) { - r, err := dir.Open(filename) +func (q *Queries) readTranslationFile(namespace i18n.Namespace, filename string) ([]byte, error) { + r, err := i18n.LoadFilesystem(namespace).Open(filename) if os.IsNotExist(err) { return nil, errors.ThrowNotFound(err, "QUERY-sN9wg", "Errors.TranslationFile.NotFound") } diff --git a/internal/query/projection/restrictions.go b/internal/query/projection/restrictions.go index 597a6fac64..81f8bc7cc2 100644 --- a/internal/query/projection/restrictions.go +++ b/internal/query/projection/restrictions.go @@ -3,6 +3,7 @@ package projection import ( "context" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -21,6 +22,7 @@ const ( RestrictionsColumnSequence = "sequence" RestrictionsColumnDisallowPublicOrgRegistration = "disallow_public_org_registration" + RestrictionsColumnAllowedLanguages = "allowed_languages" ) type restrictionsProjection struct{} @@ -42,7 +44,8 @@ func (*restrictionsProjection) Init() *old_handler.Check { handler.NewColumn(RestrictionsColumnResourceOwner, handler.ColumnTypeText), handler.NewColumn(RestrictionsColumnInstanceID, handler.ColumnTypeText), handler.NewColumn(RestrictionsColumnSequence, handler.ColumnTypeInt64), - handler.NewColumn(RestrictionsColumnDisallowPublicOrgRegistration, handler.ColumnTypeBool), + handler.NewColumn(RestrictionsColumnDisallowPublicOrgRegistration, handler.ColumnTypeBool, handler.Nullable()), + handler.NewColumn(RestrictionsColumnAllowedLanguages, handler.ColumnTypeTextArray, handler.Nullable()), }, handler.NewPrimaryKey(RestrictionsColumnInstanceID, RestrictionsColumnResourceOwner), ), @@ -89,8 +92,11 @@ func (p *restrictionsProjection) reduceRestrictionsSet(event eventstore.Event) ( handler.NewCol(RestrictionsColumnSequence, e.Sequence()), handler.NewCol(RestrictionsColumnAggregateID, e.Aggregate().ID), } - if e.DisallowPublicOrgRegistrations != nil { - updateCols = append(updateCols, handler.NewCol(RestrictionsColumnDisallowPublicOrgRegistration, *e.DisallowPublicOrgRegistrations)) + if e.DisallowPublicOrgRegistration != nil { + updateCols = append(updateCols, handler.NewCol(RestrictionsColumnDisallowPublicOrgRegistration, *e.DisallowPublicOrgRegistration)) + } + if e.AllowedLanguages != nil { + updateCols = append(updateCols, handler.NewCol(RestrictionsColumnAllowedLanguages, domain.LanguagesToStrings(*e.AllowedLanguages))) } return handler.NewUpsertStatement(e, conflictCols, updateCols), nil } diff --git a/internal/query/projection/restrictions_test.go b/internal/query/projection/restrictions_test.go index f018ab00c0..6a0a678f70 100644 --- a/internal/query/projection/restrictions_test.go +++ b/internal/query/projection/restrictions_test.go @@ -25,7 +25,7 @@ func TestRestrictionsProjection_reduces(t *testing.T) { event: getEvent(testEvent( restrictions.SetEventType, restrictions.AggregateType, - []byte(`{ "disallowPublicOrgRegistrations": true }`), + []byte(`{ "disallowPublicOrgRegistration": true }`), ), restrictions.SetEventMapper), }, reduce: (&restrictionsProjection{}).reduceRestrictionsSet, diff --git a/internal/query/query.go b/internal/query/query.go index 69cb422849..c2538414bb 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -3,12 +3,10 @@ package query import ( "context" "fmt" - "net/http" "regexp" "sync" "time" - "github.com/rakyll/statik/fs" "github.com/zitadel/logging" "golang.org/x/text/language" @@ -47,8 +45,6 @@ type Queries struct { checkPermission domain.PermissionCheck DefaultLanguage language.Tag - LoginDir http.FileSystem - NotificationDir http.FileSystem mutex sync.Mutex LoginTranslationFileContents map[string][]byte NotificationTranslationFileContents map[string][]byte @@ -71,22 +67,10 @@ func StartQueries( defaultAuditLogRetention time.Duration, systemAPIUsers map[string]*authz.SystemAPIUser, ) (repo *Queries, err error) { - statikLoginFS, err := fs.NewWithNamespace("login") - if err != nil { - return nil, fmt.Errorf("unable to start login statik dir") - } - - statikNotificationFS, err := fs.NewWithNamespace("notification") - if err != nil { - return nil, fmt.Errorf("unable to start notification statik dir") - } - repo = &Queries{ eventstore: es, client: sqlClient, DefaultLanguage: language.Und, - LoginDir: statikLoginFS, - NotificationDir: statikNotificationFS, LoginTranslationFileContents: make(map[string][]byte), NotificationTranslationFileContents: make(map[string][]byte), zitadelRoles: zitadelRoles, diff --git a/internal/query/restrictions.go b/internal/query/restrictions.go index 80217164ab..e24a5e4092 100644 --- a/internal/query/restrictions.go +++ b/internal/query/restrictions.go @@ -7,9 +7,12 @@ import ( "time" sq "github.com/Masterminds/squirrel" + "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/call" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" zitade_errors "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -44,10 +47,14 @@ var ( name: projection.RestrictionsColumnSequence, table: restrictionsTable, } - RestrictionsColumnDisallowPublicOrgRegistrations = Column{ + RestrictionsColumnDisallowPublicOrgRegistration = Column{ name: projection.RestrictionsColumnDisallowPublicOrgRegistration, table: restrictionsTable, } + RestrictionsColumnAllowedLanguages = Column{ + name: projection.RestrictionsColumnAllowedLanguages, + table: restrictionsTable, + } ) type Restrictions struct { @@ -58,6 +65,7 @@ type Restrictions struct { Sequence uint64 DisallowPublicOrgRegistration bool + AllowedLanguages []language.Tag } func (q *Queries) GetInstanceRestrictions(ctx context.Context) (restrictions Restrictions, err error) { @@ -91,18 +99,25 @@ func prepareRestrictionsQuery(ctx context.Context, db prepareDatabase) (sq.Selec RestrictionsColumnChangeDate.identifier(), RestrictionsColumnResourceOwner.identifier(), RestrictionsColumnSequence.identifier(), - RestrictionsColumnDisallowPublicOrgRegistrations.identifier(), + RestrictionsColumnDisallowPublicOrgRegistration.identifier(), + RestrictionsColumnAllowedLanguages.identifier(), ). From(restrictionsTable.identifier() + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (restrictions Restrictions, err error) { - return restrictions, row.Scan( + allowedLanguages := database.TextArray[string](make([]string, 0)) + disallowPublicOrgRegistration := sql.NullBool{} + err = row.Scan( &restrictions.AggregateID, &restrictions.CreationDate, &restrictions.ChangeDate, &restrictions.ResourceOwner, &restrictions.Sequence, - &restrictions.DisallowPublicOrgRegistration, + &disallowPublicOrgRegistration, + &allowedLanguages, ) + restrictions.DisallowPublicOrgRegistration = disallowPublicOrgRegistration.Bool + restrictions.AllowedLanguages = domain.StringsToLanguages(allowedLanguages) + return restrictions, err } } diff --git a/internal/query/restrictions_test.go b/internal/query/restrictions_test.go index 83e6d9a8fe..db53bf36fd 100644 --- a/internal/query/restrictions_test.go +++ b/internal/query/restrictions_test.go @@ -7,6 +7,10 @@ import ( "fmt" "regexp" "testing" + + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/database" ) var ( @@ -15,7 +19,8 @@ var ( " projections.restrictions.change_date," + " projections.restrictions.resource_owner," + " projections.restrictions.sequence," + - " projections.restrictions.disallow_public_org_registration" + + " projections.restrictions.disallow_public_org_registration," + + " projections.restrictions.allowed_languages" + " FROM projections.restrictions" + " AS OF SYSTEM TIME '-1 ms'", ) @@ -27,6 +32,7 @@ var ( "resource_owner", "sequence", "disallow_public_org_registration", + "allowed_languages", } ) @@ -56,7 +62,9 @@ func Test_RestrictionsPrepare(t *testing.T) { } return nil, true }, - object: Restrictions{}, + object: Restrictions{ + AllowedLanguages: make([]language.Tag, 0), + }, }, }, { @@ -73,6 +81,7 @@ func Test_RestrictionsPrepare(t *testing.T) { "instance1", 0, true, + database.TextArray[string]([]string{"en", "de", "ru"}), }, ), object: Restrictions{ @@ -82,6 +91,7 @@ func Test_RestrictionsPrepare(t *testing.T) { ResourceOwner: "instance1", Sequence: 0, DisallowPublicOrgRegistration: true, + AllowedLanguages: []language.Tag{language.Make("en"), language.Make("de"), language.Make("ru")}, }, }, }, diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go index 72da287a36..ea889c1045 100644 --- a/internal/renderer/renderer.go +++ b/internal/renderer/renderer.go @@ -23,17 +23,13 @@ const ( type Renderer struct { Templates map[string]*template.Template - dir http.FileSystem cookieName string } -func NewRenderer(dir http.FileSystem, tmplMapping map[string]string, funcs map[string]interface{}, cookieName string) (*Renderer, error) { +func NewRenderer(tmplMapping map[string]string, funcs map[string]interface{}, cookieName string) (*Renderer, error) { var err error - r := &Renderer{ - dir: dir, - cookieName: cookieName, - } - err = r.loadTemplates(dir, nil, tmplMapping, funcs) + r := &Renderer{cookieName: cookieName} + err = r.loadTemplates(i18n.LoadFilesystem(i18n.LOGIN), nil, tmplMapping, funcs) if err != nil { return nil, err } @@ -47,8 +43,8 @@ func (r *Renderer) RenderTemplate(w http.ResponseWriter, req *http.Request, tran } } -func (r *Renderer) NewTranslator(ctx context.Context) (*i18n.Translator, error) { - return i18n.NewTranslator(r.dir, authz.GetInstance(ctx).DefaultLanguage(), r.cookieName) +func (r *Renderer) NewTranslator(ctx context.Context, allowedLanguages []language.Tag) (*i18n.Translator, error) { + return i18n.NewLoginTranslator(authz.GetInstance(ctx).DefaultLanguage(), allowedLanguages, r.cookieName) } func (r *Renderer) Localize(translator *i18n.Translator, id string, args map[string]interface{}) string { diff --git a/internal/repository/restrictions/events.go b/internal/repository/restrictions/events.go index e15fd7c767..7b28af3c30 100644 --- a/internal/repository/restrictions/events.go +++ b/internal/repository/restrictions/events.go @@ -2,6 +2,7 @@ package restrictions import ( "github.com/muhlemmer/gu" + "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/eventstore" ) @@ -13,8 +14,9 @@ const ( // SetEvent describes that restrictions are added or modified and contains only changed properties type SetEvent struct { - *eventstore.BaseEvent `json:"-"` - DisallowPublicOrgRegistrations *bool `json:"disallowPublicOrgRegistrations,omitempty"` + *eventstore.BaseEvent `json:"-"` + DisallowPublicOrgRegistration *bool `json:"disallowPublicOrgRegistration,omitempty"` + AllowedLanguages *[]language.Tag `json:"allowedLanguages,omitempty"` } func (e *SetEvent) Payload() any { @@ -44,9 +46,15 @@ func NewSetEvent( type RestrictionsChange func(*SetEvent) -func ChangePublicOrgRegistrations(disallow bool) RestrictionsChange { +func ChangeDisallowPublicOrgRegistration(disallow bool) RestrictionsChange { return func(e *SetEvent) { - e.DisallowPublicOrgRegistrations = gu.Ptr(disallow) + e.DisallowPublicOrgRegistration = gu.Ptr(disallow) + } +} + +func ChangeAllowedLanguages(allowedLanguages []language.Tag) RestrictionsChange { + return func(e *SetEvent) { + e.AllowedLanguages = &allowedLanguages } } diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index ff7f86601a..773eb93f64 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Не са посочени лимити Restrictions: NoneSpecified: Не са посочени ограничения + DefaultLanguageMustBeAllowed: Езикът по подразбиране трябва да бъде разрешен Language: NotParsed: Езикът не можа да бъде анализиран синтактично + NotSupported: Езикът не се поддържа + NotAllowed: Езикът не е разрешен + Undefined: Езикът е неопределен + Duplicate: Езиците имат дубликати OIDCSettings: NotFound: Конфигурацията на OIDC не е намерена AlreadyExists: OIDC конфигурацията вече съществува diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index 4230dc7749..55bc9b7616 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Nebyly určeny žádné limity Restrictions: NoneSpecified: Nebyla určena žádná omezení + DefaultLanguageMustBeAllowed: Výchozí jazyk musí být povolen Language: NotParsed: Jazyk nelze určit + NotSupported: Jazyk není podporován + NotAllowed: Jazyk není povolen + Undefined: Jazyk není definován + Duplicate: Jazyky mají duplikáty OIDCSettings: NotFound: Konfigurace OIDC nebyla nalezena AlreadyExists: Konfigurace OIDC již existuje diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 65cd5b1168..e959737285 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Keine Limits angegeben Restrictions: NoneSpecified: Keine Restriktionen angegeben + DefaultLanguageMustBeAllowed: Default Sprache muss erlaubt sein Language: NotParsed: Sprache konnte nicht gemapped werden + NotSupported: Sprache wird nicht unterstützt + NotAllowed: Sprache ist nicht erlaubt + Undefined: Sprache ist nicht definiert + Duplicate: Sprachen haben Duplikate OIDCSettings: NotFound: OIDC Konfiguration konnte nicht gefunden werden AlreadyExists: OIDC Konfiguration existiert bereits diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index e3cdbcf2e8..536d81e7d9 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: No limits specified Restrictions: NoneSpecified: No restrictions specified + DefaultLanguageMustBeAllowed: The default language must be allowed Language: NotParsed: Could not parse language + NotSupported: Language is not supported + NotAllowed: Language is not allowed + Undefined: Language is undefined + Duplicate: Languages have duplicates OIDCSettings: NotFound: OIDC Configuration not found AlreadyExists: OIDC configuration already exists diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 7a5d0ab297..c85456b3b6 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: No se especificaron límites Restrictions: NoneSpecified: No se especificaron restricciones + DefaultLanguageMustBeAllowed: El idioma por defecto debe estar permitido Language: NotParsed: No pude analizar el idioma + NotSupported: El idioma no está soportado + NotAllowed: El idioma no está permitido + Undefined: El idioma no está definido + Duplicate: Idiomas duplicados OIDCSettings: NotFound: Configuración OIDC no encontrada AlreadyExists: La configuración OIDC ya existe diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 43f8b8453b..3a75c2fa03 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Aucune limite spécifiée Restrictions: NoneSpecified: Aucune restriction spécifiée + DefaultLanguageMustBeAllowed: La langue par défaut doit être autorisée Language: NotParsed: Impossible d'analyser la langue + NotSupported: Langue non prise en charge + NotAllowed: Langue non autorisée + Undefined: Langue non définie + Duplicate: Langues en double OIDCSettings: NotFound: Configuration OIDC non trouvée AlreadyExists: La configuration OIDC existe déjà diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 045a7b06bc..352c691a5b 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Nessun limite specificato Restrictions: NoneSpecified: Nessuna restrizione specificata + DefaultLanguageMustBeAllowed: La lingua predefinita deve essere consentita Language: NotParsed: Impossibile analizzare la lingua + NotSupported: Lingua non supportata + NotAllowed: Lingua non consentita + Undefined: Lingua non definita + Duplicate: Lingue duplicate OIDCSettings: NotFound: Impossibile trovare la configurazione OIDC AlreadyExists: La configurazione OIDC esiste già diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index aa6e591987..c2a353f1c8 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: 制限が指定されていません Restrictions: NoneSpecified: 制限が指定されていません + DefaultLanguageMustBeAllowed: デフォルト言語は許可されている必要があります Language: NotParsed: 言語のパースに失敗しました + NotSupported: 言語はサポートされていません + NotAllowed: 言語は許可されていません + Undefined: 言語は未定義です + Duplicate: 言語に重複があります OIDCSettings: NotFound: OIDC構成が見つかりません AlreadyExists: すでに存在するOIDC構成です diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index a28190f645..ba1dbd47b6 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Не се наведени лимити Restrictions: NoneSpecified: Не се наведени ограничувања + DefaultLanguageMustBeAllowed: Стандардниот јазик мора да биде дозволен Language: NotParsed: Јазикот не може да се парсира + NotSupported: Јазикот не е поддржан + NotAllowed: Јазикот не е дозволен + Undefined: Јазикот е недефиниран + Duplicate: Јазиците имаат дупликати OIDCSettings: NotFound: OIDC конфигурацијата не е пронајдена AlreadyExists: OIDC конфигурацијата веќе постои diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index d9489dbb9e..322f25b2f8 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Geen limieten gespecificeerd Restrictions: NoneSpecified: Geen beperkingen gespecificeerd + DefaultLanguageMustBeAllowed: De standaardtaal moet worden toegestaan Language: NotParsed: Kon taal niet parsen + NotSupported: Taal wordt niet ondersteund + NotAllowed: Taal is niet toegestaan + Undefined: Taal is niet gedefinieerd + Duplicate: Talen hebben duplicaten OIDCSettings: NotFound: OIDC-configuratie niet gevonden AlreadyExists: OIDC-configuratie bestaat al diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 2359c4a6d3..6a06939d55 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Nie określono limitów Restrictions: NoneSpecified: Nie określono ograniczeń + DefaultLanguageMustBeAllowed: Domyślny język musi być dozwolony Language: NotParsed: Nie można przeanalizować języka + NotSupported: Język nie jest obsługiwany + NotAllowed: Język nie jest dozwolony + Undefined: Język jest niezdefiniowany + Duplicate: Języki mają duplikaty OIDCSettings: NotFound: Konfiguracja OIDC nie znaleziona AlreadyExists: Konfiguracja OIDC już istnieje diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 1633181539..4a0e01e40c 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Nenhum limite especificado Restrictions: NoneSpecified: Nenhuma restrição especificada + DefaultLanguageMustBeAllowed: O idioma padrão deve ser permitido Language: NotParsed: Não foi possível analisar o idioma + NotSupported: Idioma não suportado + NotAllowed: Idioma não permitido + Undefined: Idioma indefinido + Duplicate: Idiomas têm duplicatas OIDCSettings: NotFound: Configuração OIDC não encontrada AlreadyExists: Configuração OIDC já existe diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 21d8dd9a60..8d46081cf0 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: Не указаны лимиты Restrictions: NoneSpecified: Не указаны ограничения + DefaultLanguageMustBeAllowed: Язык по умолчанию должен быть разрешен Language: NotParsed: Не удалось разобрать язык + NotSupported: Язык не поддерживается + NotAllowed: Язык не разрешен + Undefined: Язык не определен + Duplicate: Языки имеют дубликаты OIDCSettings: NotFound: Конфигурация OIDC не найдена AlreadyExists: Конфигурация OIDC уже существует diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 5b3ee8cab2..7e22dc5d57 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -33,8 +33,13 @@ Errors: NoneSpecified: 未指定限制 Restrictions: NoneSpecified: 未指定限制 + DefaultLanguageMustBeAllowed: 默认语言必须被允许 Language: NotParsed: 无法解析语言 + NotSupported: 语言不支持 + NotAllowed: 语言不被允许 + Undefined: 语言未定义 + Duplicate: 语言有重复 OIDCSettings: NotFound: OIDC 配置未找到 AlreadyExists: OIDC 配置已存在 diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 448fcca303..3e1d56fdda 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -3843,7 +3843,7 @@ service AdminService { responses: { key: "200"; value: { - description: "The status 200 is also returned if no restrictions were ever set. In this case, all feature restrictions have zero values."; + description: "The status 200 is also returned if no restrictions were ever set. In this case, all feature restrictions are undefined."; }; }; }; @@ -7994,6 +7994,20 @@ message SetRestrictionsRequest { description: "defines if ZITADEL should expose the endpoint /ui/login/register/org. If it is true, the org registration endpoint returns the HTTP status 404 on GET requests, and 409 on POST requests."; } ]; + optional SelectLanguages allowed_languages = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "restricts the allowed languages. If allowed_languages is undefined, the allowed languages are not changed."; + } + ]; +} + +// We have to wrap the languages list into a message so we can serialize empty lists. +message SelectLanguages { + repeated string list = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which languages to select. An empty list means all languages are selected."; + } + ]; } message SetRestrictionsResponse { @@ -8009,5 +8023,10 @@ message GetRestrictionsResponse { description: "defines if ZITADEL should expose the endpoint /ui/login/register/org. If it is true, the org registration endpoint returns the HTTP status 404 on GET requests, and 409 on POST requests."; } ]; + repeated string allowed_languages = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines the allowed languages. If allowed_languages has one or more entries, only these languages are allowed. If it has no entries, all supported languages are allowed"; + } + ]; }