From 3f345b1adec24db0387f932e03c2ae67a3cf72b3 Mon Sep 17 00:00:00 2001 From: Fabi <38692350+fgerschwiler@users.noreply.github.com> Date: Fri, 19 Mar 2021 11:12:56 +0100 Subject: [PATCH] feat: new es testing2 (#1428) * fix: org tests * fix: org tests * fix: user grant test * fix: user grant test * fix: project and project role test * fix: project grant test * fix: project grant test * fix: project member, grant member, app changed tests * fix: application tests * fix: application tests * fix: add oidc app test * fix: add oidc app test * fix: add api keys test * fix: iam policies * fix: iam and org member tests * fix: idp config tests * fix: iam tests * fix: user tests * fix: user tests * fix: user tests * fix: user tests * fix: user tests * fix: user tests * fix: user tests * fix: user tests * fix: user tests * fix: user tests * fix: org domain test * fix: org tests * fix: org tests * fix: implement org idps * fix: pr requests * fix: email tests * fix: fix idp check * fix: fix user profile --- .../eventsourcing/handler/idp_providers.go | 4 +- .../handler/user_external_idps.go | 4 +- internal/api/grpc/idp/converter.go | 11 + internal/api/grpc/management/idp.go | 69 +- internal/api/grpc/management/idp_converter.go | 121 ++ .../api/grpc/management/idp_converter_test.go | 149 ++ internal/api/grpc/management/policy_login.go | 2 +- .../eventsourcing/handler/idp_providers.go | 4 +- internal/command/command.go | 4 +- internal/command/iam_converter.go | 1 + internal/command/iam_idp_config.go | 11 +- internal/command/iam_idp_config_test.go | 294 +++ internal/command/iam_idp_oidc_config.go | 5 +- internal/command/iam_idp_oidc_config_test.go | 295 +++ internal/command/iam_policy_login.go | 41 +- .../command/iam_policy_login_factors_model.go | 26 +- internal/command/iam_policy_login_test.go | 913 ++++++++++ internal/command/iam_policy_org_iam.go | 5 +- .../command/iam_policy_password_complexity.go | 3 + internal/command/main_test.go | 6 + internal/command/org_domain.go | 31 +- internal/command/org_domain_test.go | 1318 ++++++++++++++ internal/command/org_idp_config.go | 14 +- internal/command/org_idp_config_test.go | 343 ++++ internal/command/org_idp_oidc_config.go | 12 +- internal/command/org_idp_oidc_config_model.go | 2 +- internal/command/org_idp_oidc_config_test.go | 319 ++++ internal/command/org_policy_label.go | 15 + internal/command/org_policy_label_test.go | 429 +++++ internal/command/org_policy_login.go | 64 +- .../command/org_policy_login_factors_model.go | 27 +- ...rg_policy_login_identity_provider_model.go | 16 +- internal/command/org_policy_login_test.go | 1487 +++++++++++++++ internal/command/org_policy_mail_template.go | 9 + .../command/org_policy_mail_template_test.go | 381 ++++ internal/command/org_policy_mail_text.go | 16 +- internal/command/org_policy_mail_text_test.go | 563 ++++++ internal/command/org_policy_org_iam.go | 11 +- internal/command/org_policy_org_iam_test.go | 381 ++++ internal/command/org_policy_password_age.go | 9 + .../command/org_policy_password_age_test.go | 399 ++++ .../command/org_policy_password_complexity.go | 9 + .../org_policy_password_complexity_test.go | 429 +++++ .../command/org_policy_password_lockout.go | 9 + .../org_policy_password_lockout_test.go | 396 ++++ internal/command/setup_step7.go | 2 +- internal/command/setup_step8.go | 2 +- internal/command/setup_step9.go | 2 +- internal/command/user.go | 21 +- internal/command/user_converter.go | 16 +- internal/command/user_human.go | 37 +- internal/command/user_human_address.go | 7 +- internal/command/user_human_address_model.go | 33 +- internal/command/user_human_adress_test.go | 209 +++ internal/command/user_human_email.go | 13 +- internal/command/user_human_email_model.go | 9 +- internal/command/user_human_email_test.go | 822 +++++++++ internal/command/user_human_externalidp.go | 20 +- .../command/user_human_externalidp_test.go | 582 ++++++ internal/command/user_human_init.go | 11 +- internal/command/user_human_init_model.go | 9 +- internal/command/user_human_init_test.go | 720 ++++++++ internal/command/user_human_otp.go | 10 +- internal/command/user_human_otp_test.go | 339 ++++ internal/command/user_human_password.go | 46 +- internal/command/user_human_password_test.go | 1248 +++++++++++++ internal/command/user_human_phone.go | 32 +- internal/command/user_human_phone_model.go | 18 +- internal/command/user_human_phone_test.go | 962 ++++++++++ internal/command/user_human_profile.go | 8 +- internal/command/user_human_profile_model.go | 38 +- internal/command/user_human_profile_test.go | 207 +++ internal/command/user_human_test.go | 1605 +++++++++++++++++ internal/command/user_machine.go | 22 +- internal/command/user_machine_model.go | 22 +- internal/command/user_machine_test.go | 350 ++++ internal/command/user_test.go | 1275 +++++++++++++ internal/domain/factors.go | 12 + internal/domain/human.go | 9 +- internal/domain/human_email.go | 5 +- internal/domain/human_email_test.go | 74 + internal/domain/human_external_idp.go | 2 +- internal/domain/human_phone.go | 2 +- internal/domain/human_phone_test.go | 107 ++ internal/domain/machine.go | 2 +- internal/domain/org_domain.go | 2 +- internal/domain/policy.go | 4 + internal/domain/policy_login.go | 4 + internal/domain/user.go | 4 + .../iam/repository/view/idp_provider_view.go | 2 +- internal/iam/repository/view/idp_view.go | 2 +- .../handler/user_external_idps.go | 2 +- .../iam/policy_login_identity_provider.go | 3 +- internal/repository/org/domain.go | 5 +- internal/repository/user/human_address.go | 47 +- internal/repository/user/human_email.go | 3 +- internal/repository/user/human_phone.go | 3 +- internal/repository/user/human_profile.go | 53 +- internal/repository/user/machine.go | 32 +- internal/static/i18n/de.yaml | 7 +- internal/static/i18n/en.yaml | 9 +- proto/zitadel/management.proto | 1 + 102 files changed, 17481 insertions(+), 269 deletions(-) create mode 100644 internal/api/grpc/management/idp_converter.go create mode 100644 internal/api/grpc/management/idp_converter_test.go create mode 100644 internal/command/iam_idp_config_test.go create mode 100644 internal/command/iam_idp_oidc_config_test.go create mode 100644 internal/command/org_domain_test.go create mode 100644 internal/command/org_idp_config_test.go create mode 100644 internal/command/org_idp_oidc_config_test.go create mode 100644 internal/command/org_policy_label_test.go create mode 100644 internal/command/org_policy_login_test.go create mode 100644 internal/command/org_policy_mail_template_test.go create mode 100644 internal/command/org_policy_mail_text_test.go create mode 100644 internal/command/org_policy_org_iam_test.go create mode 100644 internal/command/org_policy_password_age_test.go create mode 100644 internal/command/org_policy_password_complexity_test.go create mode 100644 internal/command/org_policy_password_lockout_test.go create mode 100644 internal/command/user_human_adress_test.go create mode 100644 internal/command/user_human_email_test.go create mode 100644 internal/command/user_human_externalidp_test.go create mode 100644 internal/command/user_human_init_test.go create mode 100644 internal/command/user_human_otp_test.go create mode 100644 internal/command/user_human_password_test.go create mode 100644 internal/command/user_human_phone_test.go create mode 100644 internal/command/user_human_profile_test.go create mode 100644 internal/command/user_human_test.go create mode 100644 internal/command/user_machine_test.go create mode 100644 internal/command/user_test.go create mode 100644 internal/domain/human_email_test.go create mode 100644 internal/domain/human_phone_test.go diff --git a/internal/admin/repository/eventsourcing/handler/idp_providers.go b/internal/admin/repository/eventsourcing/handler/idp_providers.go index e3941aec3e..572119f67c 100644 --- a/internal/admin/repository/eventsourcing/handler/idp_providers.go +++ b/internal/admin/repository/eventsourcing/handler/idp_providers.go @@ -170,7 +170,7 @@ func (i *IDPProvider) getOrgIDPConfig(ctx context.Context, aggregateID, idpConfi if _, i := existing.GetIDP(idpConfigID); i != nil { return i, nil } - return nil, errors.ThrowNotFound(nil, "EVENT-4m0fs", "Errors.Org.IdpNotExisting") + return nil, errors.ThrowNotFound(nil, "EVENT-4m0fs", "Errors.IDP.NotExisting") } func (i *IDPProvider) getOrgByID(ctx context.Context, orgID string) (*org_model.Org, error) { @@ -220,5 +220,5 @@ func (u *IDPProvider) getDefaultIDPConfig(ctx context.Context, idpConfigID strin if _, existingIDP := existing.GetIDP(idpConfigID); existingIDP != nil { return existingIDP, nil } - return nil, errors.ThrowNotFound(nil, "EVENT-4M=Fs", "Errors.IAM.IdpNotExisting") + return nil, errors.ThrowNotFound(nil, "EVENT-4M=Fs", "Errors.IDP.NotExisting") } diff --git a/internal/admin/repository/eventsourcing/handler/user_external_idps.go b/internal/admin/repository/eventsourcing/handler/user_external_idps.go index 536dad8d77..02d9e1ba2f 100644 --- a/internal/admin/repository/eventsourcing/handler/user_external_idps.go +++ b/internal/admin/repository/eventsourcing/handler/user_external_idps.go @@ -181,7 +181,7 @@ func (i *ExternalIDP) getOrgIDPConfig(ctx context.Context, aggregateID, idpConfi if _, i := existing.GetIDP(idpConfigID); i != nil { return i, nil } - return nil, caos_errs.ThrowNotFound(nil, "EVENT-2n8Fh", "Errors.Org.IdpNotExisting") + return nil, caos_errs.ThrowNotFound(nil, "EVENT-2n8Fh", "Errors.IDP.NotExisting") } func (i *ExternalIDP) getOrgByID(ctx context.Context, orgID string) (*org_model.Org, error) { @@ -231,5 +231,5 @@ func (u *ExternalIDP) getDefaultIDPConfig(ctx context.Context, idpConfigID strin if _, existingIDP := existing.GetIDP(idpConfigID); existingIDP != nil { return existingIDP, nil } - return nil, caos_errs.ThrowNotFound(nil, "EVENT-49O0f", "Errors.IAM.IdpNotExisting") + return nil, caos_errs.ThrowNotFound(nil, "EVENT-49O0f", "Errors.IDP.NotExisting") } diff --git a/internal/api/grpc/idp/converter.go b/internal/api/grpc/idp/converter.go index cf0fe2c0af..30d4fdd428 100644 --- a/internal/api/grpc/idp/converter.go +++ b/internal/api/grpc/idp/converter.go @@ -221,6 +221,17 @@ func ModelIDPProviderTypeToPb(typ iam_model.IDPProviderType) idp_pb.IDPOwnerType } } +func IDPProviderTypeFromPb(typ idp_pb.IDPOwnerType) domain.IdentityProviderType { + switch typ { + case idp_pb.IDPOwnerType_IDP_OWNER_TYPE_ORG: + return domain.IdentityProviderTypeOrg + case idp_pb.IDPOwnerType_IDP_OWNER_TYPE_SYSTEM: + return domain.IdentityProviderTypeSystem + default: + return domain.IdentityProviderTypeOrg + } +} + func IDPIDQueryToModel(query *idp_pb.IDPIDQuery) *iam_model.IDPConfigSearchQuery { return &iam_model.IDPConfigSearchQuery{ Key: iam_model.IDPConfigSearchKeyIdpConfigID, //TODO: whats the difference between idpconfigid and aggregateid search key? diff --git a/internal/api/grpc/management/idp.go b/internal/api/grpc/management/idp.go index 08e742f2c3..4acc4b62cd 100644 --- a/internal/api/grpc/management/idp.go +++ b/internal/api/grpc/management/idp.go @@ -6,29 +6,84 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "github.com/caos/zitadel/internal/api/authz" + idp_grpc "github.com/caos/zitadel/internal/api/grpc/idp" + object_pb "github.com/caos/zitadel/internal/api/grpc/object" mgmt_pb "github.com/caos/zitadel/pkg/grpc/management" ) func (s *Server) GetOrgIDPByID(ctx context.Context, req *mgmt_pb.GetOrgIDPByIDRequest) (*mgmt_pb.GetOrgIDPByIDResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetOrgIDPByID not implemented") + idp, err := s.org.IDPConfigByID(ctx, req.Id) + if err != nil { + return nil, err + } + return &mgmt_pb.GetOrgIDPByIDResponse{Idp: idp_grpc.ModelIDPViewToPb(idp)}, nil } func (s *Server) ListOrgIDPs(ctx context.Context, req *mgmt_pb.ListOrgIDPsRequest) (*mgmt_pb.ListOrgIDPsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ListOrgIDPs not implemented") + resp, err := s.org.SearchIDPConfigs(ctx, listIDPsToModel(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.ListOrgIDPsResponse{ + Result: idp_grpc.IDPViewsToPb(resp.Result), + Details: object_pb.ToListDetails(resp.TotalResult, resp.Sequence, resp.Timestamp), + }, nil } func (s *Server) AddOrgOIDCIDP(ctx context.Context, req *mgmt_pb.AddOrgOIDCIDPRequest) (*mgmt_pb.AddOrgOIDCIDPResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method AddOrgOIDCIDP not implemented") + config, err := s.command.AddDefaultIDPConfig(ctx, addOIDCIDPRequestToDomain(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.AddOrgOIDCIDPResponse{ + IdpId: config.AggregateID, + Details: object_pb.ToDetailsPb( + config.Sequence, + config.ChangeDate, + config.ResourceOwner, + ), + }, nil } func (s *Server) DeactivateOrgIDP(ctx context.Context, req *mgmt_pb.DeactivateOrgIDPRequest) (*mgmt_pb.DeactivateOrgIDPResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeactivateOrgIDP not implemented") + objectDetails, err := s.command.DeactivateDefaultIDPConfig(ctx, req.IdpId) + if err != nil { + return nil, err + } + return &mgmt_pb.DeactivateOrgIDPResponse{Details: object_pb.DomainToDetailsPb(objectDetails)}, nil } func (s *Server) ReactivateOrgIDP(ctx context.Context, req *mgmt_pb.ReactivateOrgIDPRequest) (*mgmt_pb.ReactivateOrgIDPResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ReactivateOrgIDP not implemented") + objectDetails, err := s.command.ReactivateDefaultIDPConfig(ctx, req.IdpId) + if err != nil { + return nil, err + } + return &mgmt_pb.ReactivateOrgIDPResponse{Details: object_pb.DomainToDetailsPb(objectDetails)}, nil } func (s *Server) RemoveOrgIDP(ctx context.Context, req *mgmt_pb.RemoveOrgIDPRequest) (*mgmt_pb.RemoveOrgIDPResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method RemoveOrgIDP not implemented") + idpProviders, err := s.org.GetIDPProvidersByIDPConfigID(ctx, authz.GetCtxData(ctx).OrgID, req.IdpId) + if err != nil { + return nil, err + } + externalIDPs, err := s.user.ExternalIDPsByIDPConfigID(ctx, req.IdpId) + if err != nil { + return nil, err + } + _, err = s.command.RemoveDefaultIDPConfig(ctx, req.IdpId, idpProviderViewsToDomain(idpProviders), externalIDPViewsToDomain(externalIDPs)...) + if err != nil { + return nil, err + } + return &mgmt_pb.RemoveOrgIDPResponse{}, nil } func (s *Server) UpdateOrgIDP(ctx context.Context, req *mgmt_pb.UpdateOrgIDPRequest) (*mgmt_pb.UpdateOrgIDPResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateOrgIDP not implemented") + config, err := s.command.ChangeDefaultIDPConfig(ctx, updateIDPToDomain(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.UpdateOrgIDPResponse{ + Details: object_pb.ToDetailsPb( + config.Sequence, + config.ChangeDate, + config.ResourceOwner, + ), + }, nil } func (s *Server) UpdateOrgIDPOIDCConfig(ctx context.Context, req *mgmt_pb.UpdateOrgIDPOIDCConfigRequest) (*mgmt_pb.UpdateOrgIDPOIDCConfigResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateOrgIDPOIDCConfig not implemented") diff --git a/internal/api/grpc/management/idp_converter.go b/internal/api/grpc/management/idp_converter.go new file mode 100644 index 0000000000..e374f4db1a --- /dev/null +++ b/internal/api/grpc/management/idp_converter.go @@ -0,0 +1,121 @@ +package management + +import ( + idp_grpc "github.com/caos/zitadel/internal/api/grpc/idp" + "github.com/caos/zitadel/internal/api/grpc/object" + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/eventstore/v1/models" + iam_model "github.com/caos/zitadel/internal/iam/model" + user_model "github.com/caos/zitadel/internal/user/model" + mgmt_pb "github.com/caos/zitadel/pkg/grpc/management" +) + +func addOIDCIDPRequestToDomain(req *mgmt_pb.AddOrgOIDCIDPRequest) *domain.IDPConfig { + return &domain.IDPConfig{ + Name: req.Name, + OIDCConfig: addOIDCIDPRequestToDomainOIDCIDPConfig(req), + StylingType: idp_grpc.IDPStylingTypeToDomain(req.StylingType), + Type: domain.IDPConfigTypeOIDC, + } +} + +func addOIDCIDPRequestToDomainOIDCIDPConfig(req *mgmt_pb.AddOrgOIDCIDPRequest) *domain.OIDCIDPConfig { + return &domain.OIDCIDPConfig{ + ClientID: req.ClientId, + ClientSecretString: req.ClientSecret, + Issuer: req.Issuer, + Scopes: req.Scopes, + IDPDisplayNameMapping: idp_grpc.MappingFieldToDomain(req.DisplayNameMapping), + UsernameMapping: idp_grpc.MappingFieldToDomain(req.UsernameMapping), + } +} + +func updateIDPToDomain(req *mgmt_pb.UpdateOrgIDPRequest) *domain.IDPConfig { + return &domain.IDPConfig{ + IDPConfigID: req.IdpId, + Name: req.Name, + StylingType: idp_grpc.IDPStylingTypeToDomain(req.StylingType), + } +} + +func updateOIDCConfigToDomain(req *mgmt_pb.UpdateOrgIDPOIDCConfigRequest) *domain.OIDCIDPConfig { + return &domain.OIDCIDPConfig{ + IDPConfigID: req.IdpId, + ClientID: req.ClientId, + ClientSecretString: req.ClientSecret, + Issuer: req.Issuer, + Scopes: req.Scopes, + IDPDisplayNameMapping: idp_grpc.MappingFieldToDomain(req.DisplayNameMapping), + UsernameMapping: idp_grpc.MappingFieldToDomain(req.UsernameMapping), + } +} + +func listIDPsToModel(req *mgmt_pb.ListOrgIDPsRequest) *iam_model.IDPConfigSearchRequest { + offset, limit, asc := object.ListQueryToModel(req.Query) + return &iam_model.IDPConfigSearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: idp_grpc.FieldNameToModel(req.SortingColumn), + Queries: idpQueriesToModel(req.Queries), + } +} + +func idpQueriesToModel(queries []*mgmt_pb.IDPQuery) []*iam_model.IDPConfigSearchQuery { + q := make([]*iam_model.IDPConfigSearchQuery, len(queries)) + for i, query := range queries { + q[i] = idpQueryToModel(query) + } + + return q +} + +func idpQueryToModel(query *mgmt_pb.IDPQuery) *iam_model.IDPConfigSearchQuery { + switch q := query.Query.(type) { + case *mgmt_pb.IDPQuery_IdpNameQuery: + return idp_grpc.IDPNameQueryToModel(q.IdpNameQuery) + case *mgmt_pb.IDPQuery_IdpIdQuery: + return idp_grpc.IDPIDQueryToModel(q.IdpIdQuery) + default: + return nil + } +} + +func idpProviderViewsToDomain(idps []*iam_model.IDPProviderView) []*domain.IDPProvider { + idpProvider := make([]*domain.IDPProvider, len(idps)) + for i, idp := range idps { + idpProvider[i] = &domain.IDPProvider{ + ObjectRoot: models.ObjectRoot{ + AggregateID: idp.AggregateID, + }, + IDPConfigID: idp.IDPConfigID, + Type: idpConfigTypeToDomain(idp.IDPProviderType), + } + } + return idpProvider +} + +func idpConfigTypeToDomain(idpType iam_model.IDPProviderType) domain.IdentityProviderType { + switch idpType { + case iam_model.IDPProviderTypeOrg: + return domain.IdentityProviderTypeOrg + default: + return domain.IdentityProviderTypeSystem + } +} + +func externalIDPViewsToDomain(idps []*user_model.ExternalIDPView) []*domain.ExternalIDP { + externalIDPs := make([]*domain.ExternalIDP, len(idps)) + for i, idp := range idps { + externalIDPs[i] = &domain.ExternalIDP{ + ObjectRoot: models.ObjectRoot{ + AggregateID: idp.UserID, + ResourceOwner: idp.ResourceOwner, + }, + IDPConfigID: idp.IDPConfigID, + ExternalUserID: idp.ExternalUserID, + DisplayName: idp.UserDisplayName, + } + } + return externalIDPs +} diff --git a/internal/api/grpc/management/idp_converter_test.go b/internal/api/grpc/management/idp_converter_test.go new file mode 100644 index 0000000000..b7571d0dae --- /dev/null +++ b/internal/api/grpc/management/idp_converter_test.go @@ -0,0 +1,149 @@ +package management + +import ( + "testing" + + "github.com/caos/zitadel/internal/test" + "github.com/caos/zitadel/pkg/grpc/idp" + mgmt_pb "github.com/caos/zitadel/pkg/grpc/management" +) + +func Test_addOIDCIDPRequestToDomain(t *testing.T) { + type args struct { + req *mgmt_pb.AddOrgOIDCIDPRequest + } + tests := []struct { + name string + args args + }{ + { + name: "all fields filled", + args: args{ + req: &mgmt_pb.AddOrgOIDCIDPRequest{ + Name: "ZITADEL", + StylingType: idp.IDPStylingType_STYLING_TYPE_GOOGLE, + ClientId: "test1234", + ClientSecret: "test4321", + Issuer: "zitadel.ch", + Scopes: []string{"email", "profile"}, + DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL, + UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := addOIDCIDPRequestToDomain(tt.args.req) + test.AssertFieldsMapped(t, got, + "ObjectRoot", + "OIDCConfig.ClientSecret", + "OIDCConfig.ObjectRoot", + "OIDCConfig.IDPConfigID", + "IDPConfigID", + "State", + "Type", //TODO: default (0) is oidc + ) + }) + } +} + +func Test_addOIDCIDPRequestToDomainOIDCIDPConfig(t *testing.T) { + type args struct { + req *mgmt_pb.AddOrgOIDCIDPRequest + } + tests := []struct { + name string + args args + }{ + { + name: "all fields filled", + args: args{ + req: &mgmt_pb.AddOrgOIDCIDPRequest{ + ClientId: "test1234", + ClientSecret: "test4321", + Issuer: "zitadel.ch", + Scopes: []string{"email", "profile"}, + DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL, + UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := addOIDCIDPRequestToDomainOIDCIDPConfig(tt.args.req) + test.AssertFieldsMapped(t, got, + "ObjectRoot", + "ClientSecret", //TODO: is client secret string enough for backend? + "IDPConfigID", + ) + }) + } +} + +func Test_updateIDPToDomain(t *testing.T) { + type args struct { + req *mgmt_pb.UpdateOrgIDPRequest + } + tests := []struct { + name string + args args + }{ + { + name: "all fields filled", + args: args{ + req: &mgmt_pb.UpdateOrgIDPRequest{ + IdpId: "13523", + Name: "new name", + StylingType: idp.IDPStylingType_STYLING_TYPE_GOOGLE, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := updateIDPToDomain(tt.args.req) + test.AssertFieldsMapped(t, got, + "ObjectRoot", + "OIDCConfig", + "State", + "Type", //TODO: type should not be changeable + ) + }) + } +} + +func Test_updateOIDCConfigToDomain(t *testing.T) { + type args struct { + req *mgmt_pb.UpdateOrgIDPOIDCConfigRequest + } + tests := []struct { + name string + args args + }{ + { + name: "all fields filled", + args: args{ + req: &mgmt_pb.UpdateOrgIDPOIDCConfigRequest{ + IdpId: "4208", + Issuer: "zitadel.ch", + ClientId: "ZITEADEL", + ClientSecret: "i'm so secret", + Scopes: []string{"profile"}, + DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL, + UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := updateOIDCConfigToDomain(tt.args.req) + test.AssertFieldsMapped(t, got, + "ObjectRoot", + "ClientSecret", + ) + }) + } +} diff --git a/internal/api/grpc/management/policy_login.go b/internal/api/grpc/management/policy_login.go index 908583ce06..c8675a9be7 100644 --- a/internal/api/grpc/management/policy_login.go +++ b/internal/api/grpc/management/policy_login.go @@ -79,7 +79,7 @@ func (s *Server) ListLoginPolicyIDPs(ctx context.Context, req *mgmt_pb.ListLogin } func (s *Server) AddIDPToLoginPolicy(ctx context.Context, req *mgmt_pb.AddIDPToLoginPolicyRequest) (*mgmt_pb.AddIDPToLoginPolicyResponse, error) { - idp, err := s.command.AddIDPProviderToLoginPolicy(ctx, authz.GetCtxData(ctx).OrgID, &domain.IDPProvider{IDPConfigID: req.IdpId}) //TODO: old way was to also add type but this doesnt make sense in my point of view + idp, err := s.command.AddIDPProviderToLoginPolicy(ctx, authz.GetCtxData(ctx).OrgID, &domain.IDPProvider{IDPConfigID: req.IdpId, Type: idp.IDPProviderTypeFromPb(req.OwnerType)}) if err != nil { return nil, err } diff --git a/internal/auth/repository/eventsourcing/handler/idp_providers.go b/internal/auth/repository/eventsourcing/handler/idp_providers.go index e07c606074..3b104ab4f6 100644 --- a/internal/auth/repository/eventsourcing/handler/idp_providers.go +++ b/internal/auth/repository/eventsourcing/handler/idp_providers.go @@ -178,7 +178,7 @@ func (i *IDPProvider) getOrgIDPConfig(ctx context.Context, aggregateID, idpConfi if _, i := existing.GetIDP(idpConfigID); i != nil { return i, nil } - return nil, errors.ThrowNotFound(nil, "EVENT-2m9fS", "Errors.Org.IdpNotExisting") + return nil, errors.ThrowNotFound(nil, "EVENT-2m9fS", "Errors.IDP.NotExisting") } func (i *IDPProvider) getOrgByID(ctx context.Context, orgID string) (*org_model.Org, error) { @@ -228,5 +228,5 @@ func (u *IDPProvider) getDefaultIDPConfig(ctx context.Context, idpConfigID strin if _, existingIDP := existing.GetIDP(idpConfigID); existingIDP != nil { return existingIDP, nil } - return nil, errors.ThrowNotFound(nil, "EVENT-49O0f", "Errors.IAM.IdpNotExisting") + return nil, errors.ThrowNotFound(nil, "EVENT-49O0f", "Errors.IDP.NotExisting") } diff --git a/internal/command/command.go b/internal/command/command.go index 10353945f1..11a2117e41 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -28,7 +28,7 @@ type Commands struct { iamDomain string zitadelRoles []authz.RoleMapping - idpConfigSecretCrypto crypto.Crypto + idpConfigSecretCrypto crypto.EncryptionAlgorithm userPasswordAlg crypto.HashAlgorithm initializeUserCode crypto.Generator @@ -39,7 +39,7 @@ type Commands struct { machineKeySize int applicationKeySize int applicationSecretGenerator crypto.Generator - domainVerificationAlg *crypto.AESCrypto + domainVerificationAlg crypto.EncryptionAlgorithm domainVerificationGenerator crypto.Generator domainVerificationValidator func(domain, token, verifier string, checkType http.CheckType) error multifactors domain.MultifactorConfigs diff --git a/internal/command/iam_converter.go b/internal/command/iam_converter.go index d9e26f5b12..4525ac0ead 100644 --- a/internal/command/iam_converter.go +++ b/internal/command/iam_converter.go @@ -70,6 +70,7 @@ func writeModelToMailText(wm *MailTextWriteModel) *domain.MailText { Greeting: wm.Greeting, Text: wm.Text, ButtonText: wm.ButtonText, + State: wm.State, } } diff --git a/internal/command/iam_idp_config.go b/internal/command/iam_idp_config.go index 50f35b7e4e..22745e1e41 100644 --- a/internal/command/iam_idp_config.go +++ b/internal/command/iam_idp_config.go @@ -23,7 +23,7 @@ func (c *Commands) AddDefaultIDPConfig(ctx context.Context, config *domain.IDPCo } addedConfig := NewIAMIDPConfigWriteModel(idpConfigID) - clientSecret, err := crypto.Crypt([]byte(config.OIDCConfig.ClientSecretString), c.idpConfigSecretCrypto) + clientSecret, err := crypto.Encrypt([]byte(config.OIDCConfig.ClientSecretString), c.idpConfigSecretCrypto) if err != nil { return nil, err } @@ -63,12 +63,15 @@ func (c *Commands) AddDefaultIDPConfig(ctx context.Context, config *domain.IDPCo } func (c *Commands) ChangeDefaultIDPConfig(ctx context.Context, config *domain.IDPConfig) (*domain.IDPConfig, error) { + if config.IDPConfigID == "" { + return nil, errors.ThrowInvalidArgument(nil, "IAM-4m9gs", "Errors.IDMissing") + } existingIDP, err := c.iamIDPConfigWriteModelByID(ctx, config.IDPConfigID) if err != nil { return nil, err } if existingIDP.State == domain.IDPConfigStateRemoved || existingIDP.State == domain.IDPConfigStateUnspecified { - return nil, caos_errs.ThrowNotFound(nil, "IAM-4M9so", "Errors.IAM.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "IAM-4M9so", "Errors.IDPConfig.NotExisting") } iamAgg := IAMAggregateFromWriteModel(&existingIDP.WriteModel) @@ -133,7 +136,7 @@ func (c *Commands) RemoveDefaultIDPConfig(ctx context.Context, idpID string, idp return nil, err } if existingIDP.State == domain.IDPConfigStateRemoved || existingIDP.State == domain.IDPConfigStateUnspecified { - return nil, caos_errs.ThrowNotFound(nil, "IAM-4M0xy", "Errors.IAM.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "IAM-4M0xy", "Errors.IDPConfig.NotExisting") } iamAgg := IAMAggregateFromWriteModel(&existingIDP.WriteModel) @@ -168,7 +171,7 @@ func (c *Commands) getIAMIDPConfigByID(ctx context.Context, idpID string) (*doma return nil, err } if !config.State.Exists() { - return nil, caos_errs.ThrowNotFound(nil, "IAM-4M9so", "Errors.IAM.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "IAM-4M9so", "Errors.IDPConfig.NotExisting") } return writeModelToIDPConfig(&config.IDPConfigWriteModel), nil } diff --git a/internal/command/iam_idp_config_test.go b/internal/command/iam_idp_config_test.go new file mode 100644 index 0000000000..86fced7d52 --- /dev/null +++ b/internal/command/iam_idp_config_test.go @@ -0,0 +1,294 @@ +package command + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/id" + id_mock "github.com/caos/zitadel/internal/id/mock" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/idpconfig" +) + +func TestCommandSide_AddDefaultIDPConfig(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + config *domain.IDPConfig + } + type res struct { + want *domain.IDPConfig + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid config, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.IDPConfig{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "idp config oidc add, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewIDPConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "name1", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeGoogle, + ), + ), + eventFromEventPusher( + iam.NewIDPOIDCConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "clientid1", + "config1", + "issuer", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("secret"), + }, + domain.OIDCMappingFieldEmail, + domain.OIDCMappingFieldEmail, + "scope", + ), + ), + }, + uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("name1", "IAM")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "config1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + config: &domain.IDPConfig{ + Name: "name1", + StylingType: domain.IDPConfigStylingTypeGoogle, + OIDCConfig: &domain.OIDCIDPConfig{ + ClientID: "clientid1", + Issuer: "issuer", + ClientSecretString: "secret", + Scopes: []string{"scope"}, + IDPDisplayNameMapping: domain.OIDCMappingFieldEmail, + UsernameMapping: domain.OIDCMappingFieldEmail, + }, + }, + }, + res: res{ + want: &domain.IDPConfig{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + IDPConfigID: "config1", + Name: "name1", + StylingType: domain.IDPConfigStylingTypeGoogle, + State: domain.IDPConfigStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + idpConfigSecretCrypto: tt.fields.secretCrypto, + } + got, err := r.AddDefaultIDPConfig(tt.args.ctx, tt.args.config) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangeDefaultIDPConfig(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + config *domain.IDPConfig + } + type res struct { + want *domain.IDPConfig + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid config, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.IDPConfig{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "config not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.IDPConfig{ + IDPConfigID: "config1", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "idp config change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewIDPConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "name1", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeGoogle, + ), + ), + eventFromEventPusher( + iam.NewIDPOIDCConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "clientid1", + "config1", + "issuer", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + domain.OIDCMappingFieldEmail, + domain.OIDCMappingFieldEmail, + "scope", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newDefaultIDPConfigChangedEvent(context.Background(), "config1", "name1", "name2", domain.IDPConfigStylingTypeUnspecified), + ), + }, + uniqueConstraintsFromEventConstraint(idpconfig.NewRemoveIDPConfigNameUniqueConstraint("name1", "IAM")), + uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("name2", "IAM")), + ), + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.IDPConfig{ + IDPConfigID: "config1", + Name: "name2", + StylingType: domain.IDPConfigStylingTypeUnspecified, + }, + }, + res: res{ + want: &domain.IDPConfig{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + IDPConfigID: "config1", + Name: "name2", + StylingType: domain.IDPConfigStylingTypeUnspecified, + State: domain.IDPConfigStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeDefaultIDPConfig(tt.args.ctx, tt.args.config) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newDefaultIDPConfigChangedEvent(ctx context.Context, configID, oldName, newName string, stylingType domain.IDPConfigStylingType) *iam.IDPConfigChangedEvent { + event, _ := iam.NewIDPConfigChangedEvent(ctx, + &iam.NewAggregate().Aggregate, + configID, + oldName, + []idpconfig.IDPConfigChanges{ + idpconfig.ChangeName(newName), + idpconfig.ChangeStyleType(stylingType), + }, + ) + return event +} diff --git a/internal/command/iam_idp_oidc_config.go b/internal/command/iam_idp_oidc_config.go index ec75a9dd79..9838bf3bb7 100644 --- a/internal/command/iam_idp_oidc_config.go +++ b/internal/command/iam_idp_oidc_config.go @@ -7,6 +7,9 @@ import ( ) func (c *Commands) ChangeDefaultIDPOIDCConfig(ctx context.Context, config *domain.OIDCIDPConfig) (*domain.OIDCIDPConfig, error) { + if config.IDPConfigID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-9djf8", "Errors.IDMissing") + } existingConfig := NewIAMIDPOIDCConfigWriteModel(config.IDPConfigID) err := c.eventstore.FilterToQueryReducer(ctx, existingConfig) if err != nil { @@ -14,7 +17,7 @@ func (c *Commands) ChangeDefaultIDPOIDCConfig(ctx context.Context, config *domai } if existingConfig.State == domain.IDPConfigStateRemoved || existingConfig.State == domain.IDPConfigStateUnspecified { - return nil, caos_errs.ThrowAlreadyExists(nil, "IAM-67J9d", "Errors.IAM.IDPConfig.AlreadyExists") + return nil, caos_errs.ThrowNotFound(nil, "IAM-67J9d", "Errors.IAM.IDPConfig.AlreadyExists") } iamAgg := IAMAggregateFromWriteModel(&existingConfig.WriteModel) diff --git a/internal/command/iam_idp_oidc_config_test.go b/internal/command/iam_idp_oidc_config_test.go new file mode 100644 index 0000000000..479b417f17 --- /dev/null +++ b/internal/command/iam_idp_oidc_config_test.go @@ -0,0 +1,295 @@ +package command + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/idpconfig" +) + +func TestCommandSide_ChangeDefaultIDPOIDCConfig(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type ( + args struct { + ctx context.Context + config *domain.OIDCIDPConfig + } + ) + type res struct { + want *domain.OIDCIDPConfig + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid config, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.OIDCIDPConfig{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "idp config not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.OIDCIDPConfig{ + IDPConfigID: "config1", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "idp config removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewIDPConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "name1", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeGoogle, + ), + ), + eventFromEventPusher( + iam.NewIDPOIDCConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "clientid1", + "config1", + "issuer", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + domain.OIDCMappingFieldEmail, + domain.OIDCMappingFieldEmail, + "scope", + ), + ), + eventFromEventPusher( + iam.NewIDPConfigRemovedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "name", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.OIDCIDPConfig{ + IDPConfigID: "config1", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewIDPConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "name1", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeGoogle, + ), + ), + eventFromEventPusher( + iam.NewIDPOIDCConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "clientid1", + "config1", + "issuer", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("secret"), + }, + domain.OIDCMappingFieldEmail, + domain.OIDCMappingFieldEmail, + "scope", + ), + ), + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + config: &domain.OIDCIDPConfig{ + IDPConfigID: "config1", + ClientID: "clientid1", + Issuer: "issuer", + Scopes: []string{"scope"}, + IDPDisplayNameMapping: domain.OIDCMappingFieldEmail, + UsernameMapping: domain.OIDCMappingFieldEmail, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "idp config oidc add, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewIDPConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "name1", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeGoogle, + ), + ), + eventFromEventPusher( + iam.NewIDPOIDCConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "clientid1", + "config1", + "issuer", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("secret"), + }, + domain.OIDCMappingFieldEmail, + domain.OIDCMappingFieldEmail, + "scope", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newDefaultIDPOIDCConfigChangedEvent(context.Background(), + "config1", + "clientid-changed", + "issuer-changed", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("secret-changed"), + }, + domain.OIDCMappingFieldPreferredLoginName, + domain.OIDCMappingFieldPreferredLoginName, + []string{"scope", "scope2"}, + ), + ), + }, + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + config: &domain.OIDCIDPConfig{ + IDPConfigID: "config1", + ClientID: "clientid-changed", + Issuer: "issuer-changed", + ClientSecretString: "secret-changed", + Scopes: []string{"scope", "scope2"}, + IDPDisplayNameMapping: domain.OIDCMappingFieldPreferredLoginName, + UsernameMapping: domain.OIDCMappingFieldPreferredLoginName, + }, + }, + res: res{ + want: &domain.OIDCIDPConfig{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + IDPConfigID: "config1", + ClientID: "clientid-changed", + Issuer: "issuer-changed", + Scopes: []string{"scope", "scope2"}, + IDPDisplayNameMapping: domain.OIDCMappingFieldPreferredLoginName, + UsernameMapping: domain.OIDCMappingFieldPreferredLoginName, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigSecretCrypto: tt.fields.secretCrypto, + } + got, err := r.ChangeDefaultIDPOIDCConfig(tt.args.ctx, tt.args.config) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newDefaultIDPOIDCConfigChangedEvent(ctx context.Context, configID, clientID, issuer string, secret *crypto.CryptoValue, displayMapping, usernameMapping domain.OIDCMappingField, scopes []string) *iam.IDPOIDCConfigChangedEvent { + event, _ := iam.NewIDPOIDCConfigChangedEvent(ctx, + &iam.NewAggregate().Aggregate, + configID, + []idpconfig.OIDCConfigChanges{ + idpconfig.ChangeClientID(clientID), + idpconfig.ChangeIssuer(issuer), + idpconfig.ChangeClientSecret(secret), + idpconfig.ChangeIDPDisplayNameMapping(displayMapping), + idpconfig.ChangeUserNameMapping(usernameMapping), + idpconfig.ChangeScopes(scopes), + }, + ) + return event +} diff --git a/internal/command/iam_policy_login.go b/internal/command/iam_policy_login.go index 28ff1b70a6..b9563dda9d 100644 --- a/internal/command/iam_policy_login.go +++ b/internal/command/iam_policy_login.go @@ -85,8 +85,15 @@ func (c *Commands) changeDefaultLoginPolicy(ctx context.Context, iamAgg *eventst } func (c *Commands) AddIDPProviderToDefaultLoginPolicy(ctx context.Context, idpProvider *domain.IDPProvider) (*domain.IDPProvider, error) { + if !idpProvider.IsValid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-9nf88", "Errors.IAM.LoginPolicy.IDP.Invalid") + } + _, err := c.getIAMIDPConfigByID(ctx, idpProvider.IDPConfigID) + if err != nil { + return nil, caos_errs.ThrowPreconditionFailed(err, "IAM-m8fsd", "Errors.IDPConfig.NotExisting") + } idpModel := NewIAMIdentityProviderWriteModel(idpProvider.IDPConfigID) - err := c.eventstore.FilterToQueryReducer(ctx, idpModel) + err = c.eventstore.FilterToQueryReducer(ctx, idpModel) if err != nil { return nil, err } @@ -95,7 +102,7 @@ func (c *Commands) AddIDPProviderToDefaultLoginPolicy(ctx context.Context, idpPr } iamAgg := IAMAggregateFromWriteModel(&idpModel.WriteModel) - pushedEvents, err := c.eventstore.PushEvents(ctx, iam_repo.NewIdentityProviderAddedEvent(ctx, iamAgg, idpProvider.IDPConfigID, idpProvider.Type)) + pushedEvents, err := c.eventstore.PushEvents(ctx, iam_repo.NewIdentityProviderAddedEvent(ctx, iamAgg, idpProvider.IDPConfigID)) if err != nil { return nil, err } @@ -107,6 +114,9 @@ func (c *Commands) AddIDPProviderToDefaultLoginPolicy(ctx context.Context, idpPr } func (c *Commands) RemoveIDPProviderFromDefaultLoginPolicy(ctx context.Context, idpProvider *domain.IDPProvider, cascadeExternalIDPs ...*domain.ExternalIDP) (*domain.ObjectDetails, error) { + if !idpProvider.IsValid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-66m9s", "Errors.IAM.LoginPolicy.IDP.Invalid") + } idpModel := NewIAMIdentityProviderWriteModel(idpProvider.IDPConfigID) err := c.eventstore.FilterToQueryReducer(ctx, idpModel) if err != nil { @@ -117,12 +127,7 @@ func (c *Commands) RemoveIDPProviderFromDefaultLoginPolicy(ctx context.Context, } iamAgg := IAMAggregateFromWriteModel(&idpModel.IdentityProviderWriteModel.WriteModel) - events := []eventstore.EventPusher{ - iam_repo.NewIdentityProviderRemovedEvent(ctx, iamAgg, idpProvider.IDPConfigID), - } - - userEvents := c.removeIDPProviderFromDefaultLoginPolicy(ctx, iamAgg, idpProvider, false, cascadeExternalIDPs...) - events = append(events, userEvents...) + events := c.removeIDPProviderFromDefaultLoginPolicy(ctx, iamAgg, idpProvider, false, cascadeExternalIDPs...) pushedEvents, err := c.eventstore.PushEvents(ctx, events...) if err != nil { return nil, err @@ -154,7 +159,10 @@ func (c *Commands) removeIDPProviderFromDefaultLoginPolicy(ctx context.Context, } func (c *Commands) AddSecondFactorToDefaultLoginPolicy(ctx context.Context, secondFactor domain.SecondFactorType) (domain.SecondFactorType, *domain.ObjectDetails, error) { - secondFactorModel := NewIAMSecondFactorWriteModel() + if !secondFactor.Valid() { + return domain.SecondFactorTypeUnspecified, nil, caos_errs.ThrowInvalidArgument(nil, "IAM-5m9fs", "Errors.IAM.LoginPolicy.MFA.Unspecified") + } + secondFactorModel := NewIAMSecondFactorWriteModel(secondFactor) iamAgg := IAMAggregateFromWriteModel(&secondFactorModel.SecondFactorWriteModel.WriteModel) event, err := c.addSecondFactorToDefaultLoginPolicy(ctx, iamAgg, secondFactorModel, secondFactor) if err != nil { @@ -185,7 +193,10 @@ func (c *Commands) addSecondFactorToDefaultLoginPolicy(ctx context.Context, iamA } func (c *Commands) RemoveSecondFactorFromDefaultLoginPolicy(ctx context.Context, secondFactor domain.SecondFactorType) (*domain.ObjectDetails, error) { - secondFactorModel := NewIAMSecondFactorWriteModel() + if !secondFactor.Valid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-55n8s", "Errors.IAM.LoginPolicy.MFA.Unspecified") + } + secondFactorModel := NewIAMSecondFactorWriteModel(secondFactor) err := c.eventstore.FilterToQueryReducer(ctx, secondFactorModel) if err != nil { return nil, err @@ -206,7 +217,10 @@ func (c *Commands) RemoveSecondFactorFromDefaultLoginPolicy(ctx context.Context, } func (c *Commands) AddMultiFactorToDefaultLoginPolicy(ctx context.Context, multiFactor domain.MultiFactorType) (domain.MultiFactorType, *domain.ObjectDetails, error) { - multiFactorModel := NewIAMMultiFactorWriteModel() + if !multiFactor.Valid() { + return domain.MultiFactorTypeUnspecified, nil, caos_errs.ThrowInvalidArgument(nil, "IAM-5m9fs", "Errors.IAM.LoginPolicy.MFA.Unspecified") + } + multiFactorModel := NewIAMMultiFactorWriteModel(multiFactor) iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel) event, err := c.addMultiFactorToDefaultLoginPolicy(ctx, iamAgg, multiFactorModel, multiFactor) if err != nil { @@ -237,7 +251,10 @@ func (c *Commands) addMultiFactorToDefaultLoginPolicy(ctx context.Context, iamAg } func (c *Commands) RemoveMultiFactorFromDefaultLoginPolicy(ctx context.Context, multiFactor domain.MultiFactorType) (*domain.ObjectDetails, error) { - multiFactorModel := NewIAMMultiFactorWriteModel() + if !multiFactor.Valid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-33m9F", "Errors.IAM.LoginPolicy.MFA.Unspecified") + } + multiFactorModel := NewIAMMultiFactorWriteModel(multiFactor) err := c.eventstore.FilterToQueryReducer(ctx, multiFactorModel) if err != nil { return nil, err diff --git a/internal/command/iam_policy_login_factors_model.go b/internal/command/iam_policy_login_factors_model.go index b09d880da9..561edcd508 100644 --- a/internal/command/iam_policy_login_factors_model.go +++ b/internal/command/iam_policy_login_factors_model.go @@ -10,13 +10,14 @@ type IAMSecondFactorWriteModel struct { SecondFactorWriteModel } -func NewIAMSecondFactorWriteModel() *IAMSecondFactorWriteModel { +func NewIAMSecondFactorWriteModel(factorType domain.SecondFactorType) *IAMSecondFactorWriteModel { return &IAMSecondFactorWriteModel{ SecondFactorWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: domain.IAMID, ResourceOwner: domain.IAMID, }, + MFAType: factorType, }, } } @@ -25,15 +26,19 @@ func (wm *IAMSecondFactorWriteModel) AppendEvents(events ...eventstore.EventRead for _, event := range events { switch e := event.(type) { case *iam.LoginPolicySecondFactorAddedEvent: - wm.WriteModel.AppendEvents(&e.SecondFactorAddedEvent) + if wm.MFAType == e.MFAType { + wm.WriteModel.AppendEvents(&e.SecondFactorAddedEvent) + } case *iam.LoginPolicySecondFactorRemovedEvent: - wm.WriteModel.AppendEvents(&e.SecondFactorRemovedEvent) + if wm.MFAType == e.MFAType { + wm.WriteModel.AppendEvents(&e.SecondFactorRemovedEvent) + } } } } func (wm *IAMSecondFactorWriteModel) Reduce() error { - return wm.WriteModel.Reduce() + return wm.SecondFactorWriteModel.Reduce() } func (wm *IAMSecondFactorWriteModel) Query() *eventstore.SearchQueryBuilder { @@ -49,13 +54,14 @@ type IAMMultiFactorWriteModel struct { MultiFactoryWriteModel } -func NewIAMMultiFactorWriteModel() *IAMMultiFactorWriteModel { +func NewIAMMultiFactorWriteModel(factorType domain.MultiFactorType) *IAMMultiFactorWriteModel { return &IAMMultiFactorWriteModel{ MultiFactoryWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: domain.IAMID, ResourceOwner: domain.IAMID, }, + MFAType: factorType, }, } } @@ -64,15 +70,19 @@ func (wm *IAMMultiFactorWriteModel) AppendEvents(events ...eventstore.EventReade for _, event := range events { switch e := event.(type) { case *iam.LoginPolicyMultiFactorAddedEvent: - wm.WriteModel.AppendEvents(&e.MultiFactorAddedEvent) + if wm.MFAType == e.MFAType { + wm.WriteModel.AppendEvents(&e.MultiFactorAddedEvent) + } case *iam.LoginPolicyMultiFactorRemovedEvent: - wm.WriteModel.AppendEvents(&e.MultiFactorRemovedEvent) + if wm.MFAType == e.MFAType { + wm.WriteModel.AppendEvents(&e.MultiFactorRemovedEvent) + } } } } func (wm *IAMMultiFactorWriteModel) Reduce() error { - return wm.WriteModel.Reduce() + return wm.MultiFactoryWriteModel.Reduce() } func (wm *IAMMultiFactorWriteModel) Query() *eventstore.SearchQueryBuilder { diff --git a/internal/command/iam_policy_login_test.go b/internal/command/iam_policy_login_test.go index b5055f86eb..6bc560c764 100644 --- a/internal/command/iam_policy_login_test.go +++ b/internal/command/iam_policy_login_test.go @@ -9,6 +9,8 @@ import ( "github.com/caos/zitadel/internal/eventstore/v1/models" "github.com/caos/zitadel/internal/repository/iam" "github.com/caos/zitadel/internal/repository/policy" + "github.com/caos/zitadel/internal/repository/user" + "github.com/stretchr/testify/assert" "testing" ) @@ -268,6 +270,917 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { } } +func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + provider *domain.IDPProvider + } + type res struct { + want *domain.IDPProvider + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "provider invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + provider: &domain.IDPProvider{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "config not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "provider already exists, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewIDPConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + eventFromEventPusher( + iam.NewIdentityProviderAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add provider, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewIDPConfigAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + ), + ), + ), + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewIdentityProviderAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1"), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + }, + }, + res: res{ + want: &domain.IDPProvider{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + IDPConfigID: "config1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddIDPProviderToDefaultLoginPolicy(tt.args.ctx, tt.args.provider) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + provider *domain.IDPProvider + cascadeExternalIDPs []*domain.ExternalIDP + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "provider invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + provider: &domain.IDPProvider{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "provider not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "provider removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + eventFromEventPusher( + iam.NewIdentityProviderAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + ), + ), + eventFromEventPusher( + iam.NewIdentityProviderRemovedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove provider, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + eventFromEventPusher( + iam.NewIdentityProviderAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewIdentityProviderRemovedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1"), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + { + name: "remove provider external idp not found, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + eventFromEventPusher( + iam.NewIdentityProviderAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewIdentityProviderRemovedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1"), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + }, + cascadeExternalIDPs: []*domain.ExternalIDP{ + { + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "config1", + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + { + name: "remove provider with external idps, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + eventFromEventPusher( + iam.NewIdentityProviderAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1", + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanExternalIDPAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "config1", "", "externaluser1"), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewIdentityProviderRemovedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "config1"), + ), + eventFromEventPusher( + user.NewHumanExternalIDPCascadeRemovedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "config1", "externaluser1")), + }, + uniqueConstraintsFromEventConstraint(user.NewRemoveExternalIDPUniqueConstraint("config1", "externaluser1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + }, + cascadeExternalIDPs: []*domain.ExternalIDP{ + { + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "config1", + ExternalUserID: "externaluser1", + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveIDPProviderFromDefaultLoginPolicy(tt.args.ctx, tt.args.provider, tt.args.cascadeExternalIDPs...) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_AddSecondFactorDefaultLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + factor domain.SecondFactorType + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "factor invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeUnspecified, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "factor already exists, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicySecondFactorAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecondFactorTypeOTP, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeOTP, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add factor, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewLoginPolicySecondFactorAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecondFactorTypeOTP), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeOTP, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + _, got, err := r.AddSecondFactorToDefaultLoginPolicy(tt.args.ctx, tt.args.factor) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveSecondFactorDefaultLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + factor domain.SecondFactorType + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "factor invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeUnspecified, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "factor not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeOTP, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "factor removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicySecondFactorAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecondFactorTypeOTP, + ), + ), + eventFromEventPusher( + iam.NewLoginPolicySecondFactorRemovedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecondFactorTypeOTP, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeOTP, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "add factor, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicySecondFactorAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecondFactorTypeOTP, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewLoginPolicySecondFactorRemovedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.SecondFactorTypeOTP), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeOTP, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveSecondFactorFromDefaultLoginPolicy(tt.args.ctx, tt.args.factor) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_AddMultiFactorDefaultLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + factor domain.MultiFactorType + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "factor invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeUnspecified, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "factor already exists, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyMultiFactorAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.MultiFactorTypeU2FWithPIN, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeU2FWithPIN, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add factor, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewLoginPolicyMultiFactorAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.MultiFactorTypeU2FWithPIN), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeU2FWithPIN, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + _, got, err := r.AddMultiFactorToDefaultLoginPolicy(tt.args.ctx, tt.args.factor) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveMultiFactorDefaultLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + factor domain.MultiFactorType + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "factor invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeUnspecified, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "factor not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeU2FWithPIN, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "factor removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyMultiFactorAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.MultiFactorTypeU2FWithPIN, + ), + ), + eventFromEventPusher( + iam.NewLoginPolicyMultiFactorRemovedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.MultiFactorTypeU2FWithPIN, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeU2FWithPIN, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "add factor, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyMultiFactorAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.MultiFactorTypeU2FWithPIN, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewLoginPolicyMultiFactorRemovedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + domain.MultiFactorTypeU2FWithPIN), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeU2FWithPIN, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveMultiFactorFromDefaultLoginPolicy(tt.args.ctx, tt.args.factor) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allowUsernamePassword, allowExternalIDP, forceMFA bool, passwordlessType domain.PasswordlessType) *iam.LoginPolicyChangedEvent { event, _ := iam.NewLoginPolicyChangedEvent(ctx, &iam.NewAggregate().Aggregate, diff --git a/internal/command/iam_policy_org_iam.go b/internal/command/iam_policy_org_iam.go index 053e3ec8d3..40ce2b1ad1 100644 --- a/internal/command/iam_policy_org_iam.go +++ b/internal/command/iam_policy_org_iam.go @@ -44,7 +44,7 @@ func (c *Commands) ChangeDefaultOrgIAMPolicy(ctx context.Context, policy *domain if err != nil { return nil, err } - if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { + if !existingPolicy.State.Exists() { return nil, caos_errs.ThrowNotFound(nil, "IAM-0Pl0d", "Errors.IAM.OrgIAMPolicy.NotFound") } @@ -70,6 +70,9 @@ func (c *Commands) getDefaultOrgIAMPolicy(ctx context.Context) (*domain.OrgIAMPo if err != nil { return nil, err } + if !policyWriteModel.State.Exists() { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-3n8fs", "Errors.IAM.PasswordComplexityPolicy.NotFound") + } policy := writeModelToOrgIAMPolicy(policyWriteModel) policy.Default = true return policy, nil diff --git a/internal/command/iam_policy_password_complexity.go b/internal/command/iam_policy_password_complexity.go index 6efd6cb8d5..f804864703 100644 --- a/internal/command/iam_policy_password_complexity.go +++ b/internal/command/iam_policy_password_complexity.go @@ -15,6 +15,9 @@ func (c *Commands) getDefaultPasswordComplexityPolicy(ctx context.Context) (*dom if err != nil { return nil, err } + if !policyWriteModel.State.Exists() { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-M0gsf", "Errors.IAM.OrgIAMPolicy.NotFound") + } policy := writeModelToPasswordComplexityPolicy(&policyWriteModel.PasswordComplexityPolicyWriteModel) policy.Default = true return policy, nil diff --git a/internal/command/main_test.go b/internal/command/main_test.go index a9b79e1cf7..dfcc3c3f56 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -147,6 +147,12 @@ func eventFromEventPusher(event eventstore.EventPusher) *repository.Event { } } +func eventFromEventPusherWithCreationDateNow(event eventstore.EventPusher) *repository.Event { + e := eventFromEventPusher(event) + e.CreationDate = time.Now() + return e +} + func uniqueConstraintsFromEventConstraint(constraint *eventstore.EventUniqueConstraint) *repository.UniqueConstraint { return &repository.UniqueConstraint{ UniqueType: constraint.UniqueType, diff --git a/internal/command/org_domain.go b/internal/command/org_domain.go index e17ded8765..8627a2908b 100644 --- a/internal/command/org_domain.go +++ b/internal/command/org_domain.go @@ -13,6 +13,9 @@ import ( ) func (c *Commands) AddOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain) (*domain.OrgDomain, error) { + if !orgDomain.IsValid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain") + } domainWriteModel := NewOrgDomainWriteModel(orgDomain.AggregateID, orgDomain.Domain) orgAgg := OrgAggregateFromWriteModel(&domainWriteModel.WriteModel) events, err := c.addOrgDomain(ctx, orgAgg, domainWriteModel, orgDomain) @@ -31,19 +34,19 @@ func (c *Commands) AddOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain } func (c *Commands) GenerateOrgDomainValidation(ctx context.Context, orgDomain *domain.OrgDomain) (token, url string, err error) { - if orgDomain == nil || !orgDomain.IsValid() { - return "", "", caos_errs.ThrowPreconditionFailed(nil, "ORG-R24hb", "Errors.Org.InvalidDomain") + if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" { + return "", "", caos_errs.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain") } checkType, ok := orgDomain.ValidationType.CheckType() if !ok { - return "", "", caos_errs.ThrowPreconditionFailed(nil, "ORG-Gsw31", "Errors.Org.DomainVerificationTypeInvalid") + return "", "", caos_errs.ThrowInvalidArgument(nil, "ORG-Gsw31", "Errors.Org.DomainVerificationTypeInvalid") } domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain) if err != nil { return "", "", err } if domainWriteModel.State != domain.OrgDomainStateActive { - return "", "", caos_errs.ThrowPreconditionFailed(nil, "ORG-AGD31", "Errors.Org.DomainNotOnOrg") + return "", "", caos_errs.ThrowNotFound(nil, "ORG-AGD31", "Errors.Org.DomainNotOnOrg") } if domainWriteModel.Verified { return "", "", caos_errs.ThrowPreconditionFailed(nil, "ORG-HGw21", "Errors.Org.DomainAlreadyVerified") @@ -69,15 +72,15 @@ func (c *Commands) GenerateOrgDomainValidation(ctx context.Context, orgDomain *d } func (c *Commands) ValidateOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain, claimedUserIDs ...string) (*domain.ObjectDetails, error) { - if orgDomain == nil || !orgDomain.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-R24hb", "Errors.Org.InvalidDomain") + if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain") } domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain) if err != nil { return nil, err } if domainWriteModel.State != domain.OrgDomainStateActive { - return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-Sjdi3", "Errors.Org.DomainNotOnOrg") + return nil, caos_errs.ThrowNotFound(nil, "ORG-Sjdi3", "Errors.Org.DomainNotOnOrg") } if domainWriteModel.Verified { return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-HGw21", "Errors.Org.DomainAlreadyVerified") @@ -122,15 +125,15 @@ func (c *Commands) ValidateOrgDomain(ctx context.Context, orgDomain *domain.OrgD } func (c *Commands) SetPrimaryOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain) (*domain.ObjectDetails, error) { - if orgDomain == nil || !orgDomain.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-SsDG2", "Errors.Org.InvalidDomain") + if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SsDG2", "Errors.Org.InvalidDomain") } domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain) if err != nil { return nil, err } if domainWriteModel.State != domain.OrgDomainStateActive { - return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-GDfA3", "Errors.Org.DomainNotOnOrg") + return nil, caos_errs.ThrowNotFound(nil, "ORG-GDfA3", "Errors.Org.DomainNotOnOrg") } if !domainWriteModel.Verified { return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-Ggd32", "Errors.Org.DomainNotVerified") @@ -148,21 +151,21 @@ func (c *Commands) SetPrimaryOrgDomain(ctx context.Context, orgDomain *domain.Or } func (c *Commands) RemoveOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain) (*domain.ObjectDetails, error) { - if orgDomain == nil || !orgDomain.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-SJsK3", "Errors.Org.InvalidDomain") + if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SJsK3", "Errors.Org.InvalidDomain") } domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain) if err != nil { return nil, err } if domainWriteModel.State != domain.OrgDomainStateActive { - return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-GDfA3", "Errors.Org.DomainNotOnOrg") + return nil, caos_errs.ThrowNotFound(nil, "ORG-GDfA3", "Errors.Org.DomainNotOnOrg") } if domainWriteModel.Primary { return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-Sjdi3", "Errors.Org.PrimaryDomainNotDeletable") } orgAgg := OrgAggregateFromWriteModel(&domainWriteModel.WriteModel) - pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewDomainRemovedEvent(ctx, orgAgg, orgDomain.Domain)) + pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewDomainRemovedEvent(ctx, orgAgg, orgDomain.Domain, domainWriteModel.Verified)) if err != nil { return nil, err } diff --git a/internal/command/org_domain_test.go b/internal/command/org_domain_test.go new file mode 100644 index 0000000000..9dd4094d40 --- /dev/null +++ b/internal/command/org_domain_test.go @@ -0,0 +1,1318 @@ +package command + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/api/http" + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/id" + id_mock "github.com/caos/zitadel/internal/id/mock" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestCommandSide_AddOrgDomain(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + domain *domain.OrgDomain + } + type res struct { + want *domain.OrgDomain + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid domain, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "domain already exists, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "domain add, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewDomainAddedEvent(context.Background(), + &org.NewAggregate("", "").Aggregate, + "domain.ch", + )), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + Domain: "domain.ch", + }, + }, + res: res{ + want: &domain.OrgDomain{ + Domain: "domain.ch", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddOrgDomain(tt.args.ctx, tt.args.domain) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretGenerator crypto.Generator + } + type args struct { + ctx context.Context + domain *domain.OrgDomain + } + type res struct { + wantToken string + wantURL string + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid domain, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "missing aggregateid, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + Domain: "domain.ch", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid validation type, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "domain not exists, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "domain already verified, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + eventFromEventPusher( + org.NewDomainVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "add dns validation, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewDomainVerificationAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "domain.ch", + domain.OrgDomainValidationTypeDNS, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + )), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + }, + res: res{ + wantToken: "a", + wantURL: "_zitadel-challenge.domain.ch", + }, + }, + { + name: "add http validation, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewDomainVerificationAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "domain.ch", + domain.OrgDomainValidationTypeHTTP, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + )), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeHTTP, + }, + }, + res: res{ + wantToken: "a", + wantURL: "https://domain.ch/.well-known/zitadel-challenge/a", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + domainVerificationGenerator: tt.fields.secretGenerator, + } + token, url, err := r.GenerateOrgDomainValidation(tt.args.ctx, tt.args.domain) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.wantToken, token) + assert.Equal(t, tt.res.wantURL, url) + } + }) + } +} + +func TestCommandSide_ValidateOrgDomain(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretGenerator crypto.Generator + alg crypto.EncryptionAlgorithm + domainValidationFunc func(domain, token, verifier string, checkType http.CheckType) error + } + type args struct { + ctx context.Context + domain *domain.OrgDomain + claimedUserIDs []string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid domain, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "missing aggregateid, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + Domain: "domain.ch", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "domain not exists, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "domain already verified, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + eventFromEventPusher( + org.NewDomainVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "no code existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "invalid domain verification, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + eventFromEventPusher( + org.NewDomainVerificationAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + domain.OrgDomainValidationTypeDNS, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewDomainVerificationFailedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "domain.ch", + )), + }, + ), + ), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + domainValidationFunc: invalidDomainVerification, + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "domain verification, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + eventFromEventPusher( + org.NewDomainVerificationAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + domain.OrgDomainValidationTypeDNS, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewDomainVerifiedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "domain.ch", + )), + }, + uniqueConstraintsFromEventConstraint(org.NewAddOrgDomainUniqueConstraint("domain.ch")), + ), + ), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + domainValidationFunc: validDomainVerification, + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "domain verification, claimed users not found, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + eventFromEventPusher( + org.NewDomainVerificationAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + domain.OrgDomainValidationTypeDNS, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + ), + ), + ), + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewDomainVerifiedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "domain.ch", + )), + }, + uniqueConstraintsFromEventConstraint(org.NewAddOrgDomainUniqueConstraint("domain.ch")), + ), + ), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + domainValidationFunc: validDomainVerification, + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + claimedUserIDs: []string{"user1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "domain verification, claimed users, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + eventFromEventPusher( + org.NewDomainVerificationAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + domain.OrgDomainValidationTypeDNS, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org2").Aggregate, + "username@domain.ch", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + false))), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewDomainVerifiedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "domain.ch", + )), + eventFromEventPusher(user.NewDomainClaimedEvent(context.Background(), + &user.NewAggregate("user1", "org2").Aggregate, + "tempid@temporary.zitadel.ch", + "username@domain.ch", + false, + )), + }, + uniqueConstraintsFromEventConstraint(org.NewAddOrgDomainUniqueConstraint("domain.ch")), + uniqueConstraintsFromEventConstraint(user.NewRemoveUsernameUniqueConstraint("username@domain.ch", "org2", false)), + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("tempid@temporary.zitadel.ch", "org2", false)), + ), + ), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + domainValidationFunc: validDomainVerification, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "tempid"), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + claimedUserIDs: []string{"user1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + domainVerificationGenerator: tt.fields.secretGenerator, + domainVerificationAlg: tt.fields.alg, + domainVerificationValidator: tt.fields.domainValidationFunc, + iamDomain: "zitadel.ch", + idGenerator: tt.fields.idGenerator, + } + got, err := r.ValidateOrgDomain(tt.args.ctx, tt.args.domain, tt.args.claimedUserIDs...) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_SetPrimaryDomain(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + domain *domain.OrgDomain + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid domain, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "missing aggregateid, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + Domain: "domain.ch", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "domain not exists, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "domain not verified, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "set primary, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + eventFromEventPusher( + org.NewDomainVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "domain.ch", + )), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.SetPrimaryOrgDomain(tt.args.ctx, tt.args.domain) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveOrgDomain(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + domain *domain.OrgDomain + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid domain, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "missing aggregateid, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + Domain: "domain.ch", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "domain not exists, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + ValidationType: domain.OrgDomainValidationTypeDNS, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove verified domain, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + eventFromEventPusher( + org.NewDomainVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + eventFromEventPusher( + org.NewDomainPrimarySetEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "remove domain, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewDomainRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "domain.ch", false, + )), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "remove verified domain, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "name", + ), + ), + eventFromEventPusher( + org.NewDomainAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + eventFromEventPusher( + org.NewDomainVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "domain.ch", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewDomainRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "domain.ch", true, + )), + }, + uniqueConstraintsFromEventConstraint(org.NewRemoveOrgDomainUniqueConstraint("domain.ch")), + ), + ), + }, + args: args{ + ctx: context.Background(), + domain: &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + Domain: "domain.ch", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveOrgDomain(tt.args.ctx, tt.args.domain) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func invalidDomainVerification(domain, token, verifier string, checkType http.CheckType) error { + return caos_errs.ThrowInvalidArgument(nil, "HTTP-GH422", "Errors.Internal") +} + +func validDomainVerification(domain, token, verifier string, checkType http.CheckType) error { + return nil +} diff --git a/internal/command/org_idp_config.go b/internal/command/org_idp_config.go index 2fe4c7d837..f6df7c2dc2 100644 --- a/internal/command/org_idp_config.go +++ b/internal/command/org_idp_config.go @@ -12,7 +12,10 @@ import ( org_repo "github.com/caos/zitadel/internal/repository/org" ) -func (c *Commands) AddIDPConfig(ctx context.Context, config *domain.IDPConfig) (*domain.IDPConfig, error) { +func (c *Commands) AddIDPConfig(ctx context.Context, config *domain.IDPConfig, resourceOwner string) (*domain.IDPConfig, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-0j8gs", "Errors.ResourceOwnerMissing") + } if config.OIDCConfig == nil { return nil, errors.ThrowInvalidArgument(nil, "Org-eUpQU", "Errors.idp.config.notset") } @@ -21,7 +24,7 @@ func (c *Commands) AddIDPConfig(ctx context.Context, config *domain.IDPConfig) ( if err != nil { return nil, err } - addedConfig := NewOrgIDPConfigWriteModel(idpConfigID, config.AggregateID) + addedConfig := NewOrgIDPConfigWriteModel(idpConfigID, resourceOwner) clientSecret, err := crypto.Crypt([]byte(config.OIDCConfig.ClientSecretString), c.idpConfigSecretCrypto) if err != nil { @@ -60,7 +63,10 @@ func (c *Commands) AddIDPConfig(ctx context.Context, config *domain.IDPConfig) ( return writeModelToIDPConfig(&addedConfig.IDPConfigWriteModel), nil } -func (c *Commands) ChangeIDPConfig(ctx context.Context, config *domain.IDPConfig) (*domain.IDPConfig, error) { +func (c *Commands) ChangeIDPConfig(ctx context.Context, config *domain.IDPConfig, resourceOwner string) (*domain.IDPConfig, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Gh8ds", "Errors.ResourceOwnerMissing") + } existingIDP, err := c.orgIDPConfigWriteModelByID(ctx, config.IDPConfigID, config.AggregateID) if err != nil { return nil, err @@ -149,7 +155,7 @@ func (c *Commands) getOrgIDPConfigByID(ctx context.Context, idpID, orgID string) return nil, err } if !config.State.Exists() { - return nil, caos_errs.ThrowNotFound(nil, "IAM-4M9so", "Errors.Org.IDPConfig.NotExisting") + return nil, caos_errs.ThrowNotFound(nil, "ORG-4M9so", "Errors.Org.IDPConfig.NotExisting") } return writeModelToIDPConfig(&config.IDPConfigWriteModel), nil } diff --git a/internal/command/org_idp_config_test.go b/internal/command/org_idp_config_test.go new file mode 100644 index 0000000000..8ffbbad682 --- /dev/null +++ b/internal/command/org_idp_config_test.go @@ -0,0 +1,343 @@ +package command + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/id" + id_mock "github.com/caos/zitadel/internal/id/mock" + "github.com/caos/zitadel/internal/repository/idpconfig" + "github.com/caos/zitadel/internal/repository/org" +) + +func TestCommandSide_AddIDPConfig(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretCrypto crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + config *domain.IDPConfig + resourceOwner string + } + type res struct { + want *domain.IDPConfig + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "resourceowner missing, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.IDPConfig{ + Name: "name1", + StylingType: domain.IDPConfigStylingTypeGoogle, + OIDCConfig: &domain.OIDCIDPConfig{ + ClientID: "clientid1", + Issuer: "issuer", + ClientSecretString: "secret", + Scopes: []string{"scope"}, + IDPDisplayNameMapping: domain.OIDCMappingFieldEmail, + UsernameMapping: domain.OIDCMappingFieldEmail, + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid config, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + config: &domain.IDPConfig{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "idp config oidc add, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name1", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeGoogle, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "clientid1", + "config1", + "issuer", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("secret"), + }, + domain.OIDCMappingFieldEmail, + domain.OIDCMappingFieldEmail, + "scope", + ), + ), + }, + uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("name1", "org1")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "config1"), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + config: &domain.IDPConfig{ + Name: "name1", + StylingType: domain.IDPConfigStylingTypeGoogle, + OIDCConfig: &domain.OIDCIDPConfig{ + ClientID: "clientid1", + Issuer: "issuer", + ClientSecretString: "secret", + Scopes: []string{"scope"}, + IDPDisplayNameMapping: domain.OIDCMappingFieldEmail, + UsernameMapping: domain.OIDCMappingFieldEmail, + }, + }, + }, + res: res{ + want: &domain.IDPConfig{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + IDPConfigID: "config1", + Name: "name1", + StylingType: domain.IDPConfigStylingTypeGoogle, + State: domain.IDPConfigStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + idpConfigSecretCrypto: tt.fields.secretCrypto, + } + got, err := r.AddIDPConfig(tt.args.ctx, tt.args.config, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangeIDPConfig(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + resourceOwner string + config *domain.IDPConfig + } + type res struct { + want *domain.IDPConfig + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "missing resourceowner, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.IDPConfig{ + IDPConfigID: "config1", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid config, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.IDPConfig{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "config not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + config: &domain.IDPConfig{ + IDPConfigID: "config1", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "idp config change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name1", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeGoogle, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "clientid1", + "config1", + "issuer", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + domain.OIDCMappingFieldEmail, + domain.OIDCMappingFieldEmail, + "scope", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newIDPConfigChangedEvent(context.Background(), "org1", "config1", "name1", "name2", domain.IDPConfigStylingTypeUnspecified), + ), + }, + uniqueConstraintsFromEventConstraint(idpconfig.NewRemoveIDPConfigNameUniqueConstraint("name1", "org1")), + uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("name2", "org1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + config: &domain.IDPConfig{ + IDPConfigID: "config1", + Name: "name2", + StylingType: domain.IDPConfigStylingTypeUnspecified, + }, + }, + res: res{ + want: &domain.IDPConfig{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + IDPConfigID: "config1", + Name: "name2", + StylingType: domain.IDPConfigStylingTypeUnspecified, + State: domain.IDPConfigStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeIDPConfig(tt.args.ctx, tt.args.config, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newIDPConfigChangedEvent(ctx context.Context, orgID, configID, oldName, newName string, stylingType domain.IDPConfigStylingType) *org.IDPConfigChangedEvent { + event, _ := org.NewIDPConfigChangedEvent(ctx, + &org.NewAggregate(orgID, orgID).Aggregate, + configID, + oldName, + []idpconfig.IDPConfigChanges{ + idpconfig.ChangeName(newName), + idpconfig.ChangeStyleType(stylingType), + }, + ) + return event +} diff --git a/internal/command/org_idp_oidc_config.go b/internal/command/org_idp_oidc_config.go index ce7bcc2840..9e3531dbdc 100644 --- a/internal/command/org_idp_oidc_config.go +++ b/internal/command/org_idp_oidc_config.go @@ -6,15 +6,21 @@ import ( caos_errs "github.com/caos/zitadel/internal/errors" ) -func (c *Commands) ChangeIDPOIDCConfig(ctx context.Context, config *domain.OIDCIDPConfig) (*domain.OIDCIDPConfig, error) { - existingConfig := NewOrgIDPOIDCConfigWriteModel(config.IDPConfigID, config.AggregateID) +func (c *Commands) ChangeIDPOIDCConfig(ctx context.Context, config *domain.OIDCIDPConfig, resourceOwner string) (*domain.OIDCIDPConfig, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-4n8f2", "Errors.ResourceOwnerMissing") + } + if config.IDPConfigID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-66Qwj", "Errors.IDMissing") + } + existingConfig := NewOrgIDPOIDCConfigWriteModel(config.IDPConfigID, resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, existingConfig) if err != nil { return nil, err } if existingConfig.State == domain.IDPConfigStateRemoved || existingConfig.State == domain.IDPConfigStateUnspecified { - return nil, caos_errs.ThrowAlreadyExists(nil, "Org-67J9d", "Errors.Org.IDPConfig.AlreadyExists") + return nil, caos_errs.ThrowNotFound(nil, "Org-67J9d", "Errors.Org.IDPConfig.AlreadyExists") } orgAgg := OrgAggregateFromWriteModel(&existingConfig.WriteModel) diff --git a/internal/command/org_idp_oidc_config_model.go b/internal/command/org_idp_oidc_config_model.go index a82cf299ec..1f5f424d5a 100644 --- a/internal/command/org_idp_oidc_config_model.go +++ b/internal/command/org_idp_oidc_config_model.go @@ -115,7 +115,7 @@ func (wm *IDPOIDCConfigWriteModel) NewChangedEvent( if userNameMapping.Valid() && wm.UserNameMapping != userNameMapping { changes = append(changes, idpconfig.ChangeUserNameMapping(userNameMapping)) } - if reflect.DeepEqual(wm.Scopes, scopes) { + if !reflect.DeepEqual(wm.Scopes, scopes) { changes = append(changes, idpconfig.ChangeScopes(scopes)) } if len(changes) == 0 { diff --git a/internal/command/org_idp_oidc_config_test.go b/internal/command/org_idp_oidc_config_test.go new file mode 100644 index 0000000000..30a94380e6 --- /dev/null +++ b/internal/command/org_idp_oidc_config_test.go @@ -0,0 +1,319 @@ +package command + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/idpconfig" + "github.com/caos/zitadel/internal/repository/org" +) + +func TestCommandSide_ChangeIDPOIDCConfig(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretCrypto crypto.EncryptionAlgorithm + } + type ( + args struct { + ctx context.Context + config *domain.OIDCIDPConfig + resourceOwner string + } + ) + type res struct { + want *domain.OIDCIDPConfig + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "resourceowner missing, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.OIDCIDPConfig{ + IDPConfigID: "config1", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid config, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.OIDCIDPConfig{}, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "idp config not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.OIDCIDPConfig{ + IDPConfigID: "config1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "idp config removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name1", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeGoogle, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "clientid1", + "config1", + "issuer", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + domain.OIDCMappingFieldEmail, + domain.OIDCMappingFieldEmail, + "scope", + ), + ), + eventFromEventPusher( + org.NewIDPConfigRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + config: &domain.OIDCIDPConfig{ + IDPConfigID: "config1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name1", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeGoogle, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "clientid1", + "config1", + "issuer", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("secret"), + }, + domain.OIDCMappingFieldEmail, + domain.OIDCMappingFieldEmail, + "scope", + ), + ), + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + config: &domain.OIDCIDPConfig{ + IDPConfigID: "config1", + ClientID: "clientid1", + Issuer: "issuer", + Scopes: []string{"scope"}, + IDPDisplayNameMapping: domain.OIDCMappingFieldEmail, + UsernameMapping: domain.OIDCMappingFieldEmail, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "idp config oidc add, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name1", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeGoogle, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "clientid1", + "config1", + "issuer", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("secret"), + }, + domain.OIDCMappingFieldEmail, + domain.OIDCMappingFieldEmail, + "scope", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newIDPOIDCConfigChangedEvent(context.Background(), + "org1", + "config1", + "clientid-changed", + "issuer-changed", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("secret-changed"), + }, + domain.OIDCMappingFieldPreferredLoginName, + domain.OIDCMappingFieldPreferredLoginName, + []string{"scope", "scope2"}, + ), + ), + }, + ), + ), + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + config: &domain.OIDCIDPConfig{ + IDPConfigID: "config1", + ClientID: "clientid-changed", + Issuer: "issuer-changed", + ClientSecretString: "secret-changed", + Scopes: []string{"scope", "scope2"}, + IDPDisplayNameMapping: domain.OIDCMappingFieldPreferredLoginName, + UsernameMapping: domain.OIDCMappingFieldPreferredLoginName, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.OIDCIDPConfig{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + IDPConfigID: "config1", + ClientID: "clientid-changed", + Issuer: "issuer-changed", + Scopes: []string{"scope", "scope2"}, + IDPDisplayNameMapping: domain.OIDCMappingFieldPreferredLoginName, + UsernameMapping: domain.OIDCMappingFieldPreferredLoginName, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigSecretCrypto: tt.fields.secretCrypto, + } + got, err := r.ChangeIDPOIDCConfig(tt.args.ctx, tt.args.config, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newIDPOIDCConfigChangedEvent(ctx context.Context, orgID, configID, clientID, issuer string, secret *crypto.CryptoValue, displayMapping, usernameMapping domain.OIDCMappingField, scopes []string) *org.IDPOIDCConfigChangedEvent { + event, _ := org.NewIDPOIDCConfigChangedEvent(ctx, + &org.NewAggregate(orgID, orgID).Aggregate, + configID, + []idpconfig.OIDCConfigChanges{ + idpconfig.ChangeClientID(clientID), + idpconfig.ChangeIssuer(issuer), + idpconfig.ChangeClientSecret(secret), + idpconfig.ChangeIDPDisplayNameMapping(displayMapping), + idpconfig.ChangeUserNameMapping(usernameMapping), + idpconfig.ChangeScopes(scopes), + }, + ) + return event +} diff --git a/internal/command/org_policy_label.go b/internal/command/org_policy_label.go index bfbb26494b..212e0712d6 100644 --- a/internal/command/org_policy_label.go +++ b/internal/command/org_policy_label.go @@ -9,6 +9,12 @@ import ( ) func (c *Commands) AddLabelPolicy(ctx context.Context, resourceOwner string, policy *domain.LabelPolicy) (*domain.LabelPolicy, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Fn8ds", "Errors.ResourceOwnerMissing") + } + if !policy.IsValid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Md9sf", "Errors.Org.LabelPolicy.Invalid") + } addedPolicy := NewOrgLabelPolicyWriteModel(resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) if err != nil { @@ -31,6 +37,12 @@ func (c *Commands) AddLabelPolicy(ctx context.Context, resourceOwner string, pol } func (c *Commands) ChangeLabelPolicy(ctx context.Context, resourceOwner string, policy *domain.LabelPolicy) (*domain.LabelPolicy, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-3N9fs", "Errors.ResourceOwnerMissing") + } + if !policy.IsValid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-dM9fs", "Errors.Org.LabelPolicy.Invalid") + } existingPolicy := NewOrgLabelPolicyWriteModel(resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) if err != nil { @@ -58,6 +70,9 @@ func (c *Commands) ChangeLabelPolicy(ctx context.Context, resourceOwner string, } func (c *Commands) RemoveLabelPolicy(ctx context.Context, orgID string) error { + if orgID == "" { + return caos_errs.ThrowInvalidArgument(nil, "Org-Mf9sf", "Errors.ResourceOwnerMissing") + } existingPolicy := NewOrgLabelPolicyWriteModel(orgID) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) if err != nil { diff --git a/internal/command/org_policy_label_test.go b/internal/command/org_policy_label_test.go new file mode 100644 index 0000000000..4f779c3642 --- /dev/null +++ b/internal/command/org_policy_label_test.go @@ -0,0 +1,429 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/policy" +) + +func TestCommandSide_AddLabelPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.LabelPolicy + } + type res struct { + want *domain.LabelPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LabelPolicy{ + PrimaryColor: "", + SecondaryColor: "secondary-color", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "labelpolicy invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.LabelPolicy{ + PrimaryColor: "", + SecondaryColor: "secondary-color", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "labelpolicy already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLabelPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "primary-color", + "secondary-color", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.LabelPolicy{ + PrimaryColor: "primary-color", + SecondaryColor: "secondary-color", + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewLabelPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "primary-color", + "secondary-color", + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.LabelPolicy{ + PrimaryColor: "primary-color", + SecondaryColor: "secondary-color", + }, + }, + res: res{ + want: &domain.LabelPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + PrimaryColor: "primary-color", + SecondaryColor: "secondary-color", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddLabelPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangeLabelPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.LabelPolicy + } + type res struct { + want *domain.LabelPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LabelPolicy{ + PrimaryColor: "primary-color", + SecondaryColor: "secondary-color", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "labelpolicy invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.LabelPolicy{ + PrimaryColor: "", + SecondaryColor: "secondary-color", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "labelpolicy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.LabelPolicy{ + PrimaryColor: "primary-color", + SecondaryColor: "secondary-color", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLabelPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "primary-color", + "secondary-color", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.LabelPolicy{ + PrimaryColor: "primary-color", + SecondaryColor: "secondary-color", + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLabelPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "primary-color", + "secondary-color", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newLabelPolicyChangedEvent(context.Background(), "org1", "primary-color-change", "secondary-color-change"), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.LabelPolicy{ + PrimaryColor: "primary-color-change", + SecondaryColor: "secondary-color-change", + }, + }, + res: res{ + want: &domain.LabelPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + PrimaryColor: "primary-color-change", + SecondaryColor: "secondary-color-change", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeLabelPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveLabelPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + } + type res struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "labelpolicy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLabelPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "primary-color", + "secondary-color", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewLabelPolicyRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.RemoveLabelPolicy(tt.args.ctx, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func newLabelPolicyChangedEvent(ctx context.Context, orgID, primaryColor, secondaryColor string) *org.LabelPolicyChangedEvent { + event, _ := org.NewLabelPolicyChangedEvent(ctx, + &org.NewAggregate(orgID, orgID).Aggregate, + []policy.LabelPolicyChanges{ + policy.ChangePrimaryColor(primaryColor), + policy.ChangeSecondaryColor(secondaryColor), + }, + ) + return event +} diff --git a/internal/command/org_policy_login.go b/internal/command/org_policy_login.go index 7f4e8ffcb6..6b98c87600 100644 --- a/internal/command/org_policy_login.go +++ b/internal/command/org_policy_login.go @@ -10,6 +10,9 @@ import ( ) func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, policy *domain.LoginPolicy) (*domain.LoginPolicy, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Fn8ds", "Errors.ResourceOwnerMissing") + } addedPolicy := NewOrgLoginPolicyWriteModel(resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) if err != nil { @@ -41,6 +44,9 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol } func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string, policy *domain.LoginPolicy) (*domain.LoginPolicy, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Mf9sf", "Errors.ResourceOwnerMissing") + } existingPolicy := NewOrgLoginPolicyWriteModel(resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) if err != nil { @@ -67,6 +73,9 @@ func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string, } func (c *Commands) RemoveLoginPolicy(ctx context.Context, orgID string) (*domain.ObjectDetails, error) { + if orgID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-55Mg9", "Errors.ResourceOwnerMissing") + } existingPolicy := NewOrgLoginPolicyWriteModel(orgID) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) if err != nil { @@ -88,8 +97,23 @@ func (c *Commands) RemoveLoginPolicy(ctx context.Context, orgID string) (*domain } func (c *Commands) AddIDPProviderToLoginPolicy(ctx context.Context, resourceOwner string, idpProvider *domain.IDPProvider) (*domain.IDPProvider, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-M0fs9", "Errors.ResourceOwnerMissing") + } + if !idpProvider.IsValid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-9nf88", "Errors.Org.LoginPolicy.IDP.") + } + var err error + if idpProvider.Type == domain.IdentityProviderTypeOrg { + _, err = c.getOrgIDPConfigByID(ctx, idpProvider.IDPConfigID, resourceOwner) + } else { + _, err = c.getIAMIDPConfigByID(ctx, idpProvider.IDPConfigID) + } + if err != nil { + return nil, caos_errs.ThrowPreconditionFailed(err, "Org-3N9fs", "Errors.IDPConfig.NotExisting") + } idpModel := NewOrgIdentityProviderWriteModel(resourceOwner, idpProvider.IDPConfigID) - err := c.eventstore.FilterToQueryReducer(ctx, idpModel) + err = c.eventstore.FilterToQueryReducer(ctx, idpModel) if err != nil { return nil, err } @@ -110,6 +134,12 @@ func (c *Commands) AddIDPProviderToLoginPolicy(ctx context.Context, resourceOwne } func (c *Commands) RemoveIDPProviderFromLoginPolicy(ctx context.Context, resourceOwner string, idpProvider *domain.IDPProvider, cascadeExternalIDPs ...*domain.ExternalIDP) (*domain.ObjectDetails, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-M0fs9", "Errors.ResourceOwnerMissing") + } + if !idpProvider.IsValid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-66m9s", "Errors.Org.LoginPolicy.IDP.Invalid") + } idpModel := NewOrgIdentityProviderWriteModel(resourceOwner, idpProvider.IDPConfigID) err := c.eventstore.FilterToQueryReducer(ctx, idpModel) if err != nil { @@ -153,7 +183,13 @@ func (c *Commands) removeIDPProviderFromLoginPolicy(ctx context.Context, orgAgg } func (c *Commands) AddSecondFactorToLoginPolicy(ctx context.Context, secondFactor domain.SecondFactorType, orgID string) (domain.SecondFactorType, error) { - secondFactorModel := NewOrgSecondFactorWriteModel(orgID) + if orgID == "" { + return domain.SecondFactorTypeUnspecified, caos_errs.ThrowInvalidArgument(nil, "Org-M0fs9", "Errors.ResourceOwnerMissing") + } + if !secondFactor.Valid() { + return domain.SecondFactorTypeUnspecified, caos_errs.ThrowInvalidArgument(nil, "Org-5m9fs", "Errors.Org.LoginPolicy.MFA.Unspecified") + } + secondFactorModel := NewOrgSecondFactorWriteModel(orgID, secondFactor) err := c.eventstore.FilterToQueryReducer(ctx, secondFactorModel) if err != nil { return domain.SecondFactorTypeUnspecified, err @@ -173,7 +209,13 @@ func (c *Commands) AddSecondFactorToLoginPolicy(ctx context.Context, secondFacto } func (c *Commands) RemoveSecondFactorFromLoginPolicy(ctx context.Context, secondFactor domain.SecondFactorType, orgID string) error { - secondFactorModel := NewOrgSecondFactorWriteModel(orgID) + if orgID == "" { + return caos_errs.ThrowInvalidArgument(nil, "Org-fM0gs", "Errors.ResourceOwnerMissing") + } + if !secondFactor.Valid() { + return caos_errs.ThrowInvalidArgument(nil, "Org-55n8s", "Errors.Org.LoginPolicy.MFA.Unspecified") + } + secondFactorModel := NewOrgSecondFactorWriteModel(orgID, secondFactor) err := c.eventstore.FilterToQueryReducer(ctx, secondFactorModel) if err != nil { return err @@ -188,7 +230,13 @@ func (c *Commands) RemoveSecondFactorFromLoginPolicy(ctx context.Context, second } func (c *Commands) AddMultiFactorToLoginPolicy(ctx context.Context, multiFactor domain.MultiFactorType, orgID string) (domain.MultiFactorType, error) { - multiFactorModel := NewOrgMultiFactorWriteModel(orgID) + if orgID == "" { + return domain.MultiFactorTypeUnspecified, caos_errs.ThrowInvalidArgument(nil, "Org-M0fsf", "Errors.ResourceOwnerMissing") + } + if !multiFactor.Valid() { + return domain.MultiFactorTypeUnspecified, caos_errs.ThrowInvalidArgument(nil, "Org-5m9fs", "Errors.Org.LoginPolicy.MFA.Unspecified") + } + multiFactorModel := NewOrgMultiFactorWriteModel(orgID, multiFactor) err := c.eventstore.FilterToQueryReducer(ctx, multiFactorModel) if err != nil { return domain.MultiFactorTypeUnspecified, err @@ -207,7 +255,13 @@ func (c *Commands) AddMultiFactorToLoginPolicy(ctx context.Context, multiFactor } func (c *Commands) RemoveMultiFactorFromLoginPolicy(ctx context.Context, multiFactor domain.MultiFactorType, orgID string) error { - multiFactorModel := NewOrgMultiFactorWriteModel(orgID) + if orgID == "" { + return caos_errs.ThrowInvalidArgument(nil, "Org-M0fsf", "Errors.ResourceOwnerMissing") + } + if !multiFactor.Valid() { + return caos_errs.ThrowInvalidArgument(nil, "Org-5m9fs", "Errors.Org.LoginPolicy.MFA.Unspecified") + } + multiFactorModel := NewOrgMultiFactorWriteModel(orgID, multiFactor) err := c.eventstore.FilterToQueryReducer(ctx, multiFactorModel) if err != nil { return err diff --git a/internal/command/org_policy_login_factors_model.go b/internal/command/org_policy_login_factors_model.go index 8c086e1717..d54540b688 100644 --- a/internal/command/org_policy_login_factors_model.go +++ b/internal/command/org_policy_login_factors_model.go @@ -1,6 +1,7 @@ package command import ( + "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/org" ) @@ -9,13 +10,14 @@ type OrgSecondFactorWriteModel struct { SecondFactorWriteModel } -func NewOrgSecondFactorWriteModel(orgID string) *OrgSecondFactorWriteModel { +func NewOrgSecondFactorWriteModel(orgID string, factorType domain.SecondFactorType) *OrgSecondFactorWriteModel { return &OrgSecondFactorWriteModel{ SecondFactorWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: orgID, ResourceOwner: orgID, }, + MFAType: factorType, }, } } @@ -24,15 +26,19 @@ func (wm *OrgSecondFactorWriteModel) AppendEvents(events ...eventstore.EventRead for _, event := range events { switch e := event.(type) { case *org.LoginPolicySecondFactorAddedEvent: - wm.WriteModel.AppendEvents(&e.SecondFactorAddedEvent) + if wm.MFAType == e.MFAType { + wm.WriteModel.AppendEvents(&e.SecondFactorAddedEvent) + } case *org.LoginPolicySecondFactorRemovedEvent: - wm.WriteModel.AppendEvents(&e.SecondFactorRemovedEvent) + if wm.MFAType == e.MFAType { + wm.WriteModel.AppendEvents(&e.SecondFactorRemovedEvent) + } } } } func (wm *OrgSecondFactorWriteModel) Reduce() error { - return wm.WriteModel.Reduce() + return wm.SecondFactorWriteModel.Reduce() } func (wm *OrgSecondFactorWriteModel) Query() *eventstore.SearchQueryBuilder { @@ -48,13 +54,14 @@ type OrgMultiFactorWriteModel struct { MultiFactoryWriteModel } -func NewOrgMultiFactorWriteModel(orgID string) *OrgMultiFactorWriteModel { +func NewOrgMultiFactorWriteModel(orgID string, factorType domain.MultiFactorType) *OrgMultiFactorWriteModel { return &OrgMultiFactorWriteModel{ MultiFactoryWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: orgID, ResourceOwner: orgID, }, + MFAType: factorType, }, } } @@ -63,15 +70,19 @@ func (wm *OrgMultiFactorWriteModel) AppendEvents(events ...eventstore.EventReade for _, event := range events { switch e := event.(type) { case *org.LoginPolicyMultiFactorAddedEvent: - wm.WriteModel.AppendEvents(&e.MultiFactorAddedEvent) + if wm.MFAType == e.MFAType { + wm.WriteModel.AppendEvents(&e.MultiFactorAddedEvent) + } case *org.LoginPolicyMultiFactorRemovedEvent: - wm.WriteModel.AppendEvents(&e.MultiFactorRemovedEvent) + if wm.MFAType == e.MFAType { + wm.WriteModel.AppendEvents(&e.MultiFactorRemovedEvent) + } } } } func (wm *OrgMultiFactorWriteModel) Reduce() error { - return wm.WriteModel.Reduce() + return wm.MultiFactoryWriteModel.Reduce() } func (wm *OrgMultiFactorWriteModel) Query() *eventstore.SearchQueryBuilder { diff --git a/internal/command/org_policy_login_identity_provider_model.go b/internal/command/org_policy_login_identity_provider_model.go index bff3cc0f42..c5c6fbe10d 100644 --- a/internal/command/org_policy_login_identity_provider_model.go +++ b/internal/command/org_policy_login_identity_provider_model.go @@ -2,7 +2,7 @@ package command import ( "github.com/caos/zitadel/internal/eventstore" - "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/org" ) type OrgIdentityProviderWriteModel struct { @@ -24,11 +24,16 @@ func NewOrgIdentityProviderWriteModel(orgID, idpConfigID string) *OrgIdentityPro func (wm *OrgIdentityProviderWriteModel) AppendEvents(events ...eventstore.EventReader) { for _, event := range events { switch e := event.(type) { - case *iam.IdentityProviderAddedEvent: + case *org.IdentityProviderAddedEvent: if e.IDPConfigID != wm.IDPConfigID { continue } wm.IdentityProviderWriteModel.AppendEvents(&e.IdentityProviderAddedEvent) + case *org.IdentityProviderRemovedEvent: + if e.IDPConfigID != wm.IDPConfigID { + continue + } + wm.IdentityProviderWriteModel.AppendEvents(&e.IdentityProviderRemovedEvent) } } } @@ -38,7 +43,10 @@ func (wm *OrgIdentityProviderWriteModel) Reduce() error { } func (wm *OrgIdentityProviderWriteModel) Query() *eventstore.SearchQueryBuilder { - return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, iam.AggregateType). + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, org.AggregateType). AggregateIDs(wm.AggregateID). - ResourceOwner(wm.ResourceOwner) + ResourceOwner(wm.ResourceOwner). + EventTypes( + org.LoginPolicyIDPProviderAddedEventType, + org.LoginPolicyIDPProviderRemovedEventType) } diff --git a/internal/command/org_policy_login_test.go b/internal/command/org_policy_login_test.go new file mode 100644 index 0000000000..099cf0d068 --- /dev/null +++ b/internal/command/org_policy_login_test.go @@ -0,0 +1,1487 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/policy" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestCommandSide_AddLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.LoginPolicy + } + type res struct { + want *domain.LoginPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LoginPolicy{ + AllowRegister: true, + AllowUsernamePassword: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "loginpolicy already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.LoginPolicy{ + AllowRegister: true, + AllowUsernamePassword: true, + AllowExternalIDP: true, + ForceMFA: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewLoginPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.LoginPolicy{ + AllowRegister: true, + AllowUsernamePassword: true, + AllowExternalIDP: true, + ForceMFA: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + }, + }, + res: res{ + want: &domain.LoginPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + AllowRegister: true, + AllowUsernamePassword: true, + AllowExternalIDP: true, + ForceMFA: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddLoginPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangeLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.LoginPolicy + } + type res struct { + want *domain.LoginPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LoginPolicy{ + AllowRegister: true, + AllowUsernamePassword: true, + AllowExternalIDP: true, + ForceMFA: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "loginpolicy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.LoginPolicy{ + AllowRegister: true, + AllowUsernamePassword: true, + AllowExternalIDP: true, + ForceMFA: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.LoginPolicy{ + AllowRegister: true, + AllowUsernamePassword: true, + AllowExternalIDP: true, + ForceMFA: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newLoginPolicyChangedEvent(context.Background(), "org1", false, false, false, false, domain.PasswordlessTypeNotAllowed), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.LoginPolicy{ + AllowRegister: false, + AllowUsernamePassword: false, + AllowExternalIDP: false, + ForceMFA: false, + PasswordlessType: domain.PasswordlessTypeNotAllowed, + }, + }, + res: res{ + want: &domain.LoginPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + AllowRegister: false, + AllowUsernamePassword: false, + AllowExternalIDP: false, + ForceMFA: false, + PasswordlessType: domain.PasswordlessTypeNotAllowed, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeLoginPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewLoginPolicyRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveLoginPolicy(tt.args.ctx, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + provider *domain.IDPProvider + resourceOwner string + } + type res struct { + want *domain.IDPProvider + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "resourceowner missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + Name: "name", + Type: domain.IdentityProviderTypeOrg, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "provider invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: &domain.IDPProvider{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "config not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + Name: "name", + Type: domain.IdentityProviderTypeOrg, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "provider already exists, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1", "or1").Aggregate, + "config1", + domain.IdentityProviderTypeOrg, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + Name: "name", + Type: domain.IdentityProviderTypeOrg, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add provider, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + ), + ), + ), + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + domain.IdentityProviderTypeOrg), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + Name: "name", + Type: domain.IdentityProviderTypeOrg, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.IDPProvider{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + IDPConfigID: "config1", + Type: domain.IdentityProviderTypeOrg, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddIDPProviderToLoginPolicy(tt.args.ctx, tt.args.resourceOwner, tt.args.provider) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + resourceOwner string + provider *domain.IDPProvider + cascadeExternalIDPs []*domain.ExternalIDP + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "resourceowner missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + Name: "name", + Type: domain.IdentityProviderTypeOrg, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "provider invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: &domain.IDPProvider{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "provider not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + Name: "name", + Type: domain.IdentityProviderTypeOrg, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "provider removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + domain.IdentityProviderTypeOrg, + ), + ), + eventFromEventPusher( + org.NewIdentityProviderRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove provider, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + domain.IdentityProviderTypeOrg, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewIdentityProviderRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1"), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + Name: "name", + Type: domain.IdentityProviderTypeOrg, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "remove provider external idp not found, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + domain.IdentityProviderTypeOrg, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewIdentityProviderRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1"), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + Name: "name", + Type: domain.IdentityProviderTypeOrg, + }, + cascadeExternalIDPs: []*domain.ExternalIDP{ + { + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "config1", + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "remove provider with external idps, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + domain.IdentityProviderTypeOrg, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanExternalIDPAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "config1", "", "externaluser1"), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewIdentityProviderRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1"), + ), + eventFromEventPusher( + user.NewHumanExternalIDPCascadeRemovedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "config1", "externaluser1")), + }, + uniqueConstraintsFromEventConstraint(user.NewRemoveExternalIDPUniqueConstraint("config1", "externaluser1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: &domain.IDPProvider{ + IDPConfigID: "config1", + }, + cascadeExternalIDPs: []*domain.ExternalIDP{ + { + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "config1", + ExternalUserID: "externaluser1", + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveIDPProviderFromLoginPolicy(tt.args.ctx, tt.args.resourceOwner, tt.args.provider, tt.args.cascadeExternalIDPs...) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_AddSecondFactorLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + factor domain.SecondFactorType + resourceOwner string + } + type res struct { + want domain.SecondFactorType + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "resourceowner missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeU2F, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "factor invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeUnspecified, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "factor already exists, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLoginPolicySecondFactorAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + domain.SecondFactorTypeOTP, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeOTP, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add factor, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewLoginPolicySecondFactorAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + domain.SecondFactorTypeOTP), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeOTP, + resourceOwner: "org1", + }, + res: res{ + want: domain.SecondFactorTypeOTP, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddSecondFactorToLoginPolicy(tt.args.ctx, tt.args.factor, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveSecondFactoroginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + factor domain.SecondFactorType + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "resourceowner missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeOTP, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "factor invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeUnspecified, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "factor not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeOTP, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "factor removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLoginPolicySecondFactorAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + domain.SecondFactorTypeOTP, + ), + ), + eventFromEventPusher( + org.NewLoginPolicySecondFactorRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + domain.SecondFactorTypeOTP, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeOTP, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "add factor, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLoginPolicySecondFactorAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + domain.SecondFactorTypeOTP, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewLoginPolicySecondFactorRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + domain.SecondFactorTypeOTP), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.SecondFactorTypeOTP, + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.RemoveSecondFactorFromLoginPolicy(tt.args.ctx, tt.args.factor, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func TestCommandSide_AddMultiFactorLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + factor domain.MultiFactorType + resourceOwner string + } + type res struct { + want domain.MultiFactorType + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "resourceowner missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeU2FWithPIN, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "factor invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeUnspecified, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "factor already exists, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyMultiFactorAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + domain.MultiFactorTypeU2FWithPIN, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeU2FWithPIN, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add factor, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewLoginPolicyMultiFactorAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + domain.MultiFactorTypeU2FWithPIN), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeU2FWithPIN, + resourceOwner: "org1", + }, + res: res{ + want: domain.MultiFactorTypeU2FWithPIN, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddMultiFactorToLoginPolicy(tt.args.ctx, tt.args.factor, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveMultiFactorLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + factor domain.MultiFactorType + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "resourceowner missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeU2FWithPIN, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "factor invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeUnspecified, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "factor not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeU2FWithPIN, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "factor removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyMultiFactorAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + domain.MultiFactorTypeU2FWithPIN, + ), + ), + eventFromEventPusher( + org.NewLoginPolicyMultiFactorRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + domain.MultiFactorTypeU2FWithPIN, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeU2FWithPIN, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "add factor, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLoginPolicyMultiFactorAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + domain.MultiFactorTypeU2FWithPIN, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewLoginPolicyMultiFactorRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + domain.MultiFactorTypeU2FWithPIN), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + factor: domain.MultiFactorTypeU2FWithPIN, + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.RemoveMultiFactorFromLoginPolicy(tt.args.ctx, tt.args.factor, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func newLoginPolicyChangedEvent(ctx context.Context, orgID string, usernamePassword, register, externalIDP, mfa bool, passwordlessType domain.PasswordlessType) *org.LoginPolicyChangedEvent { + event, _ := org.NewLoginPolicyChangedEvent(ctx, + &org.NewAggregate(orgID, orgID).Aggregate, + []policy.LoginPolicyChanges{ + policy.ChangeAllowUserNamePassword(usernamePassword), + policy.ChangeAllowRegister(register), + policy.ChangeAllowExternalIDP(externalIDP), + policy.ChangeForceMFA(mfa), + policy.ChangePasswordlessType(passwordlessType), + }, + ) + return event +} diff --git a/internal/command/org_policy_mail_template.go b/internal/command/org_policy_mail_template.go index 2196104f1d..d5585f8381 100644 --- a/internal/command/org_policy_mail_template.go +++ b/internal/command/org_policy_mail_template.go @@ -9,6 +9,9 @@ import ( ) func (c *Commands) AddMailTemplate(ctx context.Context, resourceOwner string, policy *domain.MailTemplate) (*domain.MailTemplate, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-M8dfs", "Errors.ResourceOwnerMissing") + } if !policy.IsValid() { return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-3m9fs", "Errors.Org.MailTemplate.Invalid") } @@ -34,6 +37,9 @@ func (c *Commands) AddMailTemplate(ctx context.Context, resourceOwner string, po } func (c *Commands) ChangeMailTemplate(ctx context.Context, resourceOwner string, policy *domain.MailTemplate) (*domain.MailTemplate, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-M9fFs", "Errors.ResourceOwnerMissing") + } if !policy.IsValid() { return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-9f9ds", "Errors.Org.MailTemplate.Invalid") } @@ -64,6 +70,9 @@ func (c *Commands) ChangeMailTemplate(ctx context.Context, resourceOwner string, } func (c *Commands) RemoveMailTemplate(ctx context.Context, orgID string) error { + if orgID == "" { + return caos_errs.ThrowInvalidArgument(nil, "Org-5Jgis", "Errors.ResourceOwnerMissing") + } existingPolicy := NewOrgMailTemplateWriteModel(orgID) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) if err != nil { diff --git a/internal/command/org_policy_mail_template_test.go b/internal/command/org_policy_mail_template_test.go new file mode 100644 index 0000000000..cd5b158aa3 --- /dev/null +++ b/internal/command/org_policy_mail_template_test.go @@ -0,0 +1,381 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/policy" +) + +func TestCommandSide_AddMailTemplate(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.MailTemplate + } + type res struct { + want *domain.MailTemplate + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailTemplate{ + Template: []byte("template"), + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "mail template already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewMailTemplateAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + []byte("template"), + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.MailTemplate{ + Template: []byte("template"), + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewMailTemplateAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + []byte("template"), + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.MailTemplate{ + Template: []byte("template"), + }, + }, + res: res{ + want: &domain.MailTemplate{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + Template: []byte("template"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddMailTemplate(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangeMailTemplate(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.MailTemplate + } + type res struct { + want *domain.MailTemplate + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailTemplate{ + Template: []byte("template"), + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "mail template not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.MailTemplate{ + Template: []byte("template"), + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewMailTemplateAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + []byte("template"), + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.MailTemplate{ + Template: []byte("template"), + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewMailTemplateAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + []byte("template"), + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newMailTemplateChangedEvent(context.Background(), "org1", "template2"), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.MailTemplate{ + Template: []byte("template2"), + }, + }, + res: res{ + want: &domain.MailTemplate{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + Template: []byte("template2"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeMailTemplate(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveMailTemplate(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewMailTemplateAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + []byte("template"), + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewMailTemplateRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.RemoveMailTemplate(tt.args.ctx, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func newMailTemplateChangedEvent(ctx context.Context, orgID string, template string) *org.MailTemplateChangedEvent { + event, _ := org.NewMailTemplateChangedEvent(ctx, + &org.NewAggregate(orgID, orgID).Aggregate, + []policy.MailTemplateChanges{ + policy.ChangeTemplate([]byte(template)), + }, + ) + return event +} diff --git a/internal/command/org_policy_mail_text.go b/internal/command/org_policy_mail_text.go index 0d0cff4d1c..c325c6091e 100644 --- a/internal/command/org_policy_mail_text.go +++ b/internal/command/org_policy_mail_text.go @@ -9,8 +9,11 @@ import ( ) func (c *Commands) AddMailText(ctx context.Context, resourceOwner string, mailText *domain.MailText) (*domain.MailText, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-MFiig", "Errors.ResourceOwnerMissing") + } if !mailText.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-4778u", "Errors.Org.MailText.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-4778u", "Errors.Org.MailText.Invalid") } addedPolicy := NewOrgMailTextWriteModel(resourceOwner, mailText.MailTextType, mailText.Language) err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) @@ -47,8 +50,11 @@ func (c *Commands) AddMailText(ctx context.Context, resourceOwner string, mailTe } func (c *Commands) ChangeMailText(ctx context.Context, resourceOwner string, mailText *domain.MailText) (*domain.MailText, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-NFus3", "Errors.ResourceOwnerMissing") + } if !mailText.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-3m9fs", "Errors.Org.MailText.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-3m9fs", "Errors.Org.MailText.Invalid") } existingPolicy := NewOrgMailTextWriteModel(resourceOwner, mailText.MailTextType, mailText.Language) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) @@ -88,6 +94,12 @@ func (c *Commands) ChangeMailText(ctx context.Context, resourceOwner string, mai } func (c *Commands) RemoveMailText(ctx context.Context, resourceOwner, mailTextType, language string) error { + if resourceOwner == "" { + return caos_errs.ThrowInvalidArgument(nil, "Org-2N7fd", "Errors.ResourceOwnerMissing") + } + if mailTextType == "" || language == "" { + return caos_errs.ThrowInvalidArgument(nil, "Org-N8fsf", "Errors.Org.MailText.Invalid") + } existingPolicy := NewOrgMailTextWriteModel(resourceOwner, mailTextType, language) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) if err != nil { diff --git a/internal/command/org_policy_mail_text_test.go b/internal/command/org_policy_mail_text_test.go new file mode 100644 index 0000000000..b8ddfcbcc8 --- /dev/null +++ b/internal/command/org_policy_mail_text_test.go @@ -0,0 +1,563 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/policy" +) + +func TestCommandSide_AddMailText(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.MailText + } + type res struct { + want *domain.MailText + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailText{ + MailTextType: "mail-text-type", + Language: "de", + Title: "title", + PreHeader: "pre-header", + Subject: "subject", + Greeting: "greeting", + Text: "text", + ButtonText: "button-text", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "mail text already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewMailTextAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "mail-text-type", + "de", + "title", + "pre-header", + "subject", + "greeting", + "text", + "button-text", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.MailText{ + MailTextType: "mail-text-type", + Language: "de", + Title: "title", + PreHeader: "pre-header", + Subject: "subject", + Greeting: "greeting", + Text: "text", + ButtonText: "button-text", + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "mail text already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewMailTextAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "mail-text-type", + "de", + "title", + "pre-header", + "subject", + "greeting", + "text", + "button-text", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.MailText{ + MailTextType: "mail-text-type", + Language: "de", + Title: "title", + PreHeader: "pre-header", + Subject: "subject", + Greeting: "greeting", + Text: "text", + ButtonText: "button-text", + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewMailTextAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "mail-text-type", + "de", + "title", + "pre-header", + "subject", + "greeting", + "text", + "button-text", + ), + ), + }, + uniqueConstraintsFromEventConstraint(policy.NewAddMailTextUniqueConstraint("org1", "mail-text-type", "de")), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.MailText{ + MailTextType: "mail-text-type", + Language: "de", + Title: "title", + PreHeader: "pre-header", + Subject: "subject", + Greeting: "greeting", + Text: "text", + ButtonText: "button-text", + }, + }, + res: res{ + want: &domain.MailText{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + MailTextType: "mail-text-type", + Language: "de", + Title: "title", + PreHeader: "pre-header", + Subject: "subject", + Greeting: "greeting", + Text: "text", + ButtonText: "button-text", + State: domain.PolicyStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddMailText(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangeMailText(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.MailText + } + type res struct { + want *domain.MailText + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailText{ + MailTextType: "mail-text-type", + Language: "de", + Title: "title", + PreHeader: "pre-header", + Subject: "subject", + Greeting: "greeting", + Text: "text", + ButtonText: "button-text", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "mailtext invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailText{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "mail template not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.MailText{ + MailTextType: "mail-text-type", + Language: "de", + Title: "title", + PreHeader: "pre-header", + Subject: "subject", + Greeting: "greeting", + Text: "text", + ButtonText: "button-text", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewMailTextAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "mail-text-type", + "de", + "title", + "pre-header", + "subject", + "greeting", + "text", + "button-text", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.MailText{ + MailTextType: "mail-text-type", + Language: "de", + Title: "title", + PreHeader: "pre-header", + Subject: "subject", + Greeting: "greeting", + Text: "text", + ButtonText: "button-text", + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewMailTextAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "mail-text-type", + "de", + "title", + "pre-header", + "subject", + "greeting", + "text", + "button-text", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newMailTextChangedEvent( + context.Background(), + "org1", + "mail-text-type", + "de", + "title-change", + "pre-header-change", + "subject-change", + "greeting-change", + "text-change", + "button-text-change"), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.MailText{ + MailTextType: "mail-text-type", + Language: "de", + Title: "title-change", + PreHeader: "pre-header-change", + Subject: "subject-change", + Greeting: "greeting-change", + Text: "text-change", + ButtonText: "button-text-change", + }, + }, + res: res{ + want: &domain.MailText{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + MailTextType: "mail-text-type", + Language: "de", + Title: "title-change", + PreHeader: "pre-header-change", + Subject: "subject-change", + Greeting: "greeting-change", + Text: "text-change", + ButtonText: "button-text-change", + State: domain.PolicyStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeMailText(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveMailText(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + mailTextType string + language string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + mailTextType: "mail-text-type", + language: "de", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewMailTextAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "mail-text-type", + "de", + "title", + "pre-header", + "subject", + "greeting", + "text", + "button-text", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewMailTextRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "mail-text-type", + "de"), + ), + }, + uniqueConstraintsFromEventConstraint(policy.NewRemoveMailTextUniqueConstraint("org1", "mail-text-type", "de")), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + mailTextType: "mail-text-type", + language: "de", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.RemoveMailText(tt.args.ctx, tt.args.orgID, tt.args.mailTextType, tt.args.language) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func newMailTextChangedEvent(ctx context.Context, orgID, mailTextType, language, title, preHeader, subject, greeting, text, buttonText string) *org.MailTextChangedEvent { + event, _ := org.NewMailTextChangedEvent(ctx, + &org.NewAggregate(orgID, orgID).Aggregate, + mailTextType, + language, + []policy.MailTextChanges{ + policy.ChangeTitle(title), + policy.ChangePreHeader(preHeader), + policy.ChangeSubject(subject), + policy.ChangeGreeting(greeting), + policy.ChangeText(text), + policy.ChangeButtonText(buttonText), + }, + ) + return event +} diff --git a/internal/command/org_policy_org_iam.go b/internal/command/org_policy_org_iam.go index 8eb8491d62..b092aa0f56 100644 --- a/internal/command/org_policy_org_iam.go +++ b/internal/command/org_policy_org_iam.go @@ -10,6 +10,9 @@ import ( ) func (c *Commands) AddOrgIAMPolicy(ctx context.Context, resourceOwner string, policy *domain.OrgIAMPolicy) (*domain.OrgIAMPolicy, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-4Jfsf", "Errors.ResourceOwnerMissing") + } addedPolicy := NewORGOrgIAMPolicyWriteModel(resourceOwner) orgAgg := OrgAggregateFromWriteModel(&addedPolicy.PolicyOrgIAMWriteModel.WriteModel) event, err := c.addOrgIAMPolicy(ctx, orgAgg, addedPolicy, policy) @@ -39,6 +42,9 @@ func (c *Commands) addOrgIAMPolicy(ctx context.Context, orgAgg *eventstore.Aggre } func (c *Commands) ChangeOrgIAMPolicy(ctx context.Context, resourceOwner string, policy *domain.OrgIAMPolicy) (*domain.OrgIAMPolicy, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-5H8fs", "Errors.ResourceOwnerMissing") + } existingPolicy, err := c.orgIAMPolicyWriteModelByID(ctx, resourceOwner) if err != nil { return nil, err @@ -65,12 +71,15 @@ func (c *Commands) ChangeOrgIAMPolicy(ctx context.Context, resourceOwner string, } func (c *Commands) RemoveOrgIAMPolicy(ctx context.Context, orgID string) error { + if orgID == "" { + return caos_errs.ThrowInvalidArgument(nil, "Org-3H8fs", "Errors.ResourceOwnerMissing") + } existingPolicy, err := c.orgIAMPolicyWriteModelByID(ctx, orgID) if err != nil { return err } if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { - return caos_errs.ThrowNotFound(nil, "ORG-Dvsh3", "Errors.Org.OrgIAMPolicy.NotFound") + return caos_errs.ThrowNotFound(nil, "ORG-Dvsh3", "Errors.Org.OrgIAM.NotFound") } orgAgg := OrgAggregateFromWriteModel(&existingPolicy.PolicyOrgIAMWriteModel.WriteModel) diff --git a/internal/command/org_policy_org_iam_test.go b/internal/command/org_policy_org_iam_test.go new file mode 100644 index 0000000000..9d3a7aa67e --- /dev/null +++ b/internal/command/org_policy_org_iam_test.go @@ -0,0 +1,381 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/policy" +) + +func TestCommandSide_AddOrgIAMPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.OrgIAMPolicy + } + type res struct { + want *domain.OrgIAMPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.OrgIAMPolicy{ + UserLoginMustBeDomain: true, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "mail template already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.OrgIAMPolicy{ + UserLoginMustBeDomain: true, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.OrgIAMPolicy{ + UserLoginMustBeDomain: true, + }, + }, + res: res{ + want: &domain.OrgIAMPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + UserLoginMustBeDomain: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddOrgIAMPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangeOrgIAMPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.OrgIAMPolicy + } + type res struct { + want *domain.OrgIAMPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.OrgIAMPolicy{ + UserLoginMustBeDomain: true, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.OrgIAMPolicy{ + UserLoginMustBeDomain: true, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.OrgIAMPolicy{ + UserLoginMustBeDomain: true, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newOrgIAMPolicyChangedEvent(context.Background(), "org1", false), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.OrgIAMPolicy{ + UserLoginMustBeDomain: false, + }, + }, + res: res{ + want: &domain.OrgIAMPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + UserLoginMustBeDomain: false, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeOrgIAMPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveOrgIAMPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewOrgIAMPolicyRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.RemoveOrgIAMPolicy(tt.args.ctx, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func newOrgIAMPolicyChangedEvent(ctx context.Context, orgID string, userLoginMustBeDomain bool) *org.OrgIAMPolicyChangedEvent { + event, _ := org.NewOrgIAMPolicyChangedEvent(ctx, + &org.NewAggregate(orgID, orgID).Aggregate, + []policy.OrgIAMPolicyChanges{ + policy.ChangeUserLoginMustBeDomain(userLoginMustBeDomain), + }, + ) + return event +} diff --git a/internal/command/org_policy_password_age.go b/internal/command/org_policy_password_age.go index 7fee2a7254..48fa754f44 100644 --- a/internal/command/org_policy_password_age.go +++ b/internal/command/org_policy_password_age.go @@ -9,6 +9,9 @@ import ( ) func (c *Commands) AddPasswordAgePolicy(ctx context.Context, resourceOwner string, policy *domain.PasswordAgePolicy) (*domain.PasswordAgePolicy, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-M9fsd", "Errors.ResourceOwnerMissing") + } addedPolicy := NewOrgPasswordAgePolicyWriteModel(resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) if err != nil { @@ -31,6 +34,9 @@ func (c *Commands) AddPasswordAgePolicy(ctx context.Context, resourceOwner strin } func (c *Commands) ChangePasswordAgePolicy(ctx context.Context, resourceOwner string, policy *domain.PasswordAgePolicy) (*domain.PasswordAgePolicy, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-57tGs", "Errors.ResourceOwnerMissing") + } existingPolicy := NewOrgPasswordAgePolicyWriteModel(resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) if err != nil { @@ -58,6 +64,9 @@ func (c *Commands) ChangePasswordAgePolicy(ctx context.Context, resourceOwner st } func (c *Commands) RemovePasswordAgePolicy(ctx context.Context, orgID string) (*domain.ObjectDetails, error) { + if orgID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-2N8fs", "Errors.ResourceOwnerMissing") + } existingPolicy := NewOrgPasswordAgePolicyWriteModel(orgID) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) if err != nil { diff --git a/internal/command/org_policy_password_age_test.go b/internal/command/org_policy_password_age_test.go new file mode 100644 index 0000000000..81eccaee51 --- /dev/null +++ b/internal/command/org_policy_password_age_test.go @@ -0,0 +1,399 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/policy" +) + +func TestCommandSide_AddPasswordAgePolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.PasswordAgePolicy + } + type res struct { + want *domain.PasswordAgePolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordAgePolicy{ + MaxAgeDays: 365, + ExpireWarnDays: 10, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "mail template already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPasswordAgePolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 365, + 10, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordAgePolicy{ + MaxAgeDays: 365, + ExpireWarnDays: 10, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewPasswordAgePolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 10, + 365, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordAgePolicy{ + MaxAgeDays: 365, + ExpireWarnDays: 10, + }, + }, + res: res{ + want: &domain.PasswordAgePolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + MaxAgeDays: 365, + ExpireWarnDays: 10, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddPasswordAgePolicy(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangePasswordAgePolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.PasswordAgePolicy + } + type res struct { + want *domain.PasswordAgePolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordAgePolicy{ + MaxAgeDays: 365, + ExpireWarnDays: 10, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordAgePolicy{ + MaxAgeDays: 365, + ExpireWarnDays: 10, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPasswordAgePolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 10, + 365, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordAgePolicy{ + MaxAgeDays: 365, + ExpireWarnDays: 10, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPasswordAgePolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 10, + 365, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newPasswordAgePolicyChangedEvent(context.Background(), "org1", 150, 5), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordAgePolicy{ + MaxAgeDays: 150, + ExpireWarnDays: 5, + }, + }, + res: res{ + want: &domain.PasswordAgePolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + MaxAgeDays: 150, + ExpireWarnDays: 5, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangePasswordAgePolicy(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemovePasswordAgePolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPasswordAgePolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 10, + 365, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewPasswordAgePolicyRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemovePasswordAgePolicy(tt.args.ctx, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newPasswordAgePolicyChangedEvent(ctx context.Context, orgID string, maxAgeDays, expireWarnDays uint64) *org.PasswordAgePolicyChangedEvent { + event, _ := org.NewPasswordAgePolicyChangedEvent(ctx, + &org.NewAggregate(orgID, orgID).Aggregate, + []policy.PasswordAgePolicyChanges{ + policy.ChangeMaxAgeDays(maxAgeDays), + policy.ChangeExpireWarnDays(expireWarnDays), + }, + ) + return event +} diff --git a/internal/command/org_policy_password_complexity.go b/internal/command/org_policy_password_complexity.go index d9db695ea6..9bba17d644 100644 --- a/internal/command/org_policy_password_complexity.go +++ b/internal/command/org_policy_password_complexity.go @@ -21,6 +21,9 @@ func (c *Commands) getOrgPasswordComplexityPolicy(ctx context.Context, orgID str } func (c *Commands) AddPasswordComplexityPolicy(ctx context.Context, resourceOwner string, policy *domain.PasswordComplexityPolicy) (*domain.PasswordComplexityPolicy, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-7ufEs", "Errors.ResourceOwnerMissing") + } if err := policy.IsValid(); err != nil { return nil, err } @@ -55,6 +58,9 @@ func (c *Commands) AddPasswordComplexityPolicy(ctx context.Context, resourceOwne } func (c *Commands) ChangePasswordComplexityPolicy(ctx context.Context, resourceOwner string, policy *domain.PasswordComplexityPolicy) (*domain.PasswordComplexityPolicy, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-3J8fs", "Errors.ResourceOwnerMissing") + } if err := policy.IsValid(); err != nil { return nil, err } @@ -86,6 +92,9 @@ func (c *Commands) ChangePasswordComplexityPolicy(ctx context.Context, resourceO } func (c *Commands) RemovePasswordComplexityPolicy(ctx context.Context, orgID string) (*domain.ObjectDetails, error) { + if orgID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-J8fsf", "Errors.ResourceOwnerMissing") + } existingPolicy := NewOrgPasswordComplexityPolicyWriteModel(orgID) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) if err != nil { diff --git a/internal/command/org_policy_password_complexity_test.go b/internal/command/org_policy_password_complexity_test.go new file mode 100644 index 0000000000..5438ed3622 --- /dev/null +++ b/internal/command/org_policy_password_complexity_test.go @@ -0,0 +1,429 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/policy" +) + +func TestCommandSide_AddPasswordComplexityPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.PasswordComplexityPolicy + } + type res struct { + want *domain.PasswordComplexityPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordComplexityPolicy{ + MinLength: 0, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 8, + true, true, true, true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordComplexityPolicy{ + MinLength: 8, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 8, + true, true, true, true, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordComplexityPolicy{ + MinLength: 8, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }, + }, + res: res{ + want: &domain.PasswordComplexityPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + MinLength: 8, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddPasswordComplexityPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangePasswordComplexityPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.PasswordComplexityPolicy + } + type res struct { + want *domain.PasswordComplexityPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordComplexityPolicy{ + MinLength: 0, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordComplexityPolicy{ + MinLength: 8, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 8, + true, true, true, true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordComplexityPolicy{ + MinLength: 8, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 8, + true, true, true, true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newPasswordComplexityPolicyChangedEvent(context.Background(), "org1", 10, false, false, false, false), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordComplexityPolicy{ + MinLength: 10, + HasUppercase: false, + HasLowercase: false, + HasNumber: false, + HasSymbol: false, + }, + }, + res: res{ + want: &domain.PasswordComplexityPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + MinLength: 10, + HasUppercase: false, + HasLowercase: false, + HasNumber: false, + HasSymbol: false, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangePasswordComplexityPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemovePasswordComplexityPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 8, + true, true, true, true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewPasswordComplexityPolicyRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemovePasswordComplexityPolicy(tt.args.ctx, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newPasswordComplexityPolicyChangedEvent(ctx context.Context, orgID string, minLength uint64, hasUpper, hasLower, hasNumber, hasSymbol bool) *org.PasswordComplexityPolicyChangedEvent { + event, _ := org.NewPasswordComplexityPolicyChangedEvent(ctx, + &org.NewAggregate(orgID, orgID).Aggregate, + []policy.PasswordComplexityPolicyChanges{ + policy.ChangeMinLength(minLength), + policy.ChangeHasUppercase(hasUpper), + policy.ChangeHasLowercase(hasLower), + policy.ChangeHasSymbol(hasNumber), + policy.ChangeHasNumber(hasSymbol), + }, + ) + return event +} diff --git a/internal/command/org_policy_password_lockout.go b/internal/command/org_policy_password_lockout.go index 0394321862..822a4fedf3 100644 --- a/internal/command/org_policy_password_lockout.go +++ b/internal/command/org_policy_password_lockout.go @@ -8,6 +8,9 @@ import ( ) func (c *Commands) AddPasswordLockoutPolicy(ctx context.Context, resourceOwner string, policy *domain.PasswordLockoutPolicy) (*domain.PasswordLockoutPolicy, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-8fJif", "Errors.ResourceOwnerMissing") + } addedPolicy := NewOrgPasswordLockoutPolicyWriteModel(resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) if err != nil { @@ -30,6 +33,9 @@ func (c *Commands) AddPasswordLockoutPolicy(ctx context.Context, resourceOwner s } func (c *Commands) ChangePasswordLockoutPolicy(ctx context.Context, resourceOwner string, policy *domain.PasswordLockoutPolicy) (*domain.PasswordLockoutPolicy, error) { + if resourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-3J9fs", "Errors.ResourceOwnerMissing") + } existingPolicy := NewOrgPasswordLockoutPolicyWriteModel(resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) if err != nil { @@ -57,6 +63,9 @@ func (c *Commands) ChangePasswordLockoutPolicy(ctx context.Context, resourceOwne } func (c *Commands) RemovePasswordLockoutPolicy(ctx context.Context, orgID string) error { + if orgID == "" { + return caos_errs.ThrowInvalidArgument(nil, "Org-4J9fs", "Errors.ResourceOwnerMissing") + } existingPolicy := NewOrgPasswordLockoutPolicyWriteModel(orgID) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) if err != nil { diff --git a/internal/command/org_policy_password_lockout_test.go b/internal/command/org_policy_password_lockout_test.go new file mode 100644 index 0000000000..5e611fcfb3 --- /dev/null +++ b/internal/command/org_policy_password_lockout_test.go @@ -0,0 +1,396 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/policy" +) + +func TestCommandSide_AddPasswordLockoutPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.PasswordLockoutPolicy + } + type res struct { + want *domain.PasswordLockoutPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordLockoutPolicy{ + MaxAttempts: 10, + ShowLockOutFailures: true, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "mail template already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPasswordLockoutPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 10, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordLockoutPolicy{ + MaxAttempts: 10, + ShowLockOutFailures: true, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewPasswordLockoutPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 10, + true, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordLockoutPolicy{ + MaxAttempts: 10, + ShowLockOutFailures: true, + }, + }, + res: res{ + want: &domain.PasswordLockoutPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + MaxAttempts: 10, + ShowLockOutFailures: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddPasswordLockoutPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangePasswordLockoutPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + policy *domain.PasswordLockoutPolicy + } + type res struct { + want *domain.PasswordLockoutPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordLockoutPolicy{ + MaxAttempts: 10, + ShowLockOutFailures: true, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordLockoutPolicy{ + MaxAttempts: 10, + ShowLockOutFailures: true, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPasswordLockoutPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 10, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordLockoutPolicy{ + MaxAttempts: 10, + ShowLockOutFailures: true, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPasswordLockoutPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 10, + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newPasswordLockoutPolicyChangedEvent(context.Background(), "org1", 5, false), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + policy: &domain.PasswordLockoutPolicy{ + MaxAttempts: 5, + ShowLockOutFailures: false, + }, + }, + res: res{ + want: &domain.PasswordLockoutPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + ResourceOwner: "org1", + }, + MaxAttempts: 5, + ShowLockOutFailures: false, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangePasswordLockoutPolicy(tt.args.ctx, tt.args.orgID, tt.args.policy) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemovePasswordLockoutPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewPasswordLockoutPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + 10, + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + org.NewPasswordLockoutPolicyRemovedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.RemovePasswordLockoutPolicy(tt.args.ctx, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func newPasswordLockoutPolicyChangedEvent(ctx context.Context, orgID string, maxAttempts uint64, showLockoutFailure bool) *org.PasswordLockoutPolicyChangedEvent { + event, _ := org.NewPasswordLockoutPolicyChangedEvent(ctx, + &org.NewAggregate(orgID, orgID).Aggregate, + []policy.PasswordLockoutPolicyChanges{ + policy.ChangeMaxAttempts(maxAttempts), + policy.ChangeShowLockOutFailures(showLockoutFailure), + }, + ) + return event +} diff --git a/internal/command/setup_step7.go b/internal/command/setup_step7.go index 326b4ebd06..6661f0a965 100644 --- a/internal/command/setup_step7.go +++ b/internal/command/setup_step7.go @@ -22,7 +22,7 @@ func (s *Step7) execute(ctx context.Context, commandSide *Commands) error { func (c *Commands) SetupStep7(ctx context.Context, step *Step7) error { fn := func(iam *IAMWriteModel) ([]eventstore.EventPusher, error) { - secondFactorModel := NewIAMSecondFactorWriteModel() + secondFactorModel := NewIAMSecondFactorWriteModel(domain.SecondFactorTypeOTP) iamAgg := IAMAggregateFromWriteModel(&secondFactorModel.SecondFactorWriteModel.WriteModel) if !step.OTP { return []eventstore.EventPusher{}, nil diff --git a/internal/command/setup_step8.go b/internal/command/setup_step8.go index e7273eb2c4..4c54e3b048 100644 --- a/internal/command/setup_step8.go +++ b/internal/command/setup_step8.go @@ -22,7 +22,7 @@ func (s *Step8) execute(ctx context.Context, commandSide *Commands) error { func (c *Commands) SetupStep8(ctx context.Context, step *Step8) error { fn := func(iam *IAMWriteModel) ([]eventstore.EventPusher, error) { - secondFactorModel := NewIAMSecondFactorWriteModel() + secondFactorModel := NewIAMSecondFactorWriteModel(domain.SecondFactorTypeU2F) iamAgg := IAMAggregateFromWriteModel(&secondFactorModel.SecondFactorWriteModel.WriteModel) if !step.U2F { return []eventstore.EventPusher{}, nil diff --git a/internal/command/setup_step9.go b/internal/command/setup_step9.go index 1138fe608f..c94e4355b2 100644 --- a/internal/command/setup_step9.go +++ b/internal/command/setup_step9.go @@ -22,7 +22,7 @@ func (s *Step9) execute(ctx context.Context, commandSide *Commands) error { func (c *Commands) SetupStep9(ctx context.Context, step *Step9) error { fn := func(iam *IAMWriteModel) ([]eventstore.EventPusher, error) { - multiFactorModel := NewIAMMultiFactorWriteModel() + multiFactorModel := NewIAMMultiFactorWriteModel(domain.MultiFactorTypeU2FWithPIN) iamAgg := IAMAggregateFromWriteModel(&multiFactorModel.MultiFactoryWriteModel.WriteModel) if !step.Passwordless { return []eventstore.EventPusher{}, nil diff --git a/internal/command/user.go b/internal/command/user.go index 652e55621b..4a85df671e 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -17,7 +17,7 @@ import ( func (c *Commands) ChangeUsername(ctx context.Context, orgID, userID, userName string) (*domain.ObjectDetails, error) { if orgID == "" || userID == "" || userName == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2N9fs", "Errors.IDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-2N9fs", "Errors.IDMissing") } existingUser, err := c.userWriteModelByID(ctx, userID, orgID) @@ -35,7 +35,7 @@ func (c *Commands) ChangeUsername(ctx context.Context, orgID, userID, userName s orgIAMPolicy, err := c.getOrgIAMPolicy(ctx, orgID) if err != nil { - return nil, err + return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-38fnu", "Errors.Org.OrgIAM.NotExisting") } if err := CheckOrgIAMPolicyForUserName(userName, orgIAMPolicy); err != nil { @@ -57,7 +57,7 @@ func (c *Commands) ChangeUsername(ctx context.Context, orgID, userID, userName s func (c *Commands) DeactivateUser(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) { if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-m0gDf", "Errors.User.UserIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-m0gDf", "Errors.User.UserIDMissing") } existingUser, err := c.userWriteModelByID(ctx, userID, resourceOwner) @@ -85,7 +85,7 @@ func (c *Commands) DeactivateUser(ctx context.Context, userID, resourceOwner str func (c *Commands) ReactivateUser(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) { if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4M9ds", "Errors.User.UserIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M9ds", "Errors.User.UserIDMissing") } existingUser, err := c.userWriteModelByID(ctx, userID, resourceOwner) @@ -113,7 +113,7 @@ func (c *Commands) ReactivateUser(ctx context.Context, userID, resourceOwner str func (c *Commands) LockUser(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) { if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M0sd", "Errors.User.UserIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-2M0sd", "Errors.User.UserIDMissing") } existingUser, err := c.userWriteModelByID(ctx, userID, resourceOwner) @@ -141,7 +141,7 @@ func (c *Commands) LockUser(ctx context.Context, userID, resourceOwner string) ( func (c *Commands) UnlockUser(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) { if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-M0dse", "Errors.User.UserIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-M0dse", "Errors.User.UserIDMissing") } existingUser, err := c.userWriteModelByID(ctx, userID, resourceOwner) @@ -169,7 +169,7 @@ func (c *Commands) UnlockUser(ctx context.Context, userID, resourceOwner string) func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string, cascadingGrantIDs ...string) (*domain.ObjectDetails, error) { if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing") } existingUser, err := c.userWriteModelByID(ctx, userID, resourceOwner) @@ -182,7 +182,7 @@ func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string, orgIAMPolicy, err := c.getOrgIAMPolicy(ctx, existingUser.ResourceOwner) if err != nil { - return nil, err + return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-3M9fs", "Errors.Org.OrgIAM.NotExisting") } var events []eventstore.EventPusher userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel) @@ -210,7 +210,7 @@ func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string, func (c *Commands) AddUserToken(ctx context.Context, orgID, agentID, clientID, userID string, audience, scopes []string, lifetime time.Duration) (*domain.Token, error) { if orgID == "" || userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-55n8M", "Errors.IDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-55n8M", "Errors.IDMissing") } existingUser, err := c.userWriteModelByID(ctx, userID, orgID) @@ -286,6 +286,9 @@ func (c *Commands) userDomainClaimed(ctx context.Context, userID string) (events } func (c *Commands) UserDomainClaimedSent(ctx context.Context, orgID, userID string) (err error) { + if userID == "" { + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-5m0fs", "Errors.IDMissing") + } existingUser, err := c.userWriteModelByID(ctx, userID, orgID) if err != nil { return err diff --git a/internal/command/user_converter.go b/internal/command/user_converter.go index 6793ff2697..2832a2be0c 100644 --- a/internal/command/user_converter.go +++ b/internal/command/user_converter.go @@ -6,7 +6,7 @@ import ( ) func writeModelToHuman(wm *HumanWriteModel) *domain.Human { - return &domain.Human{ + human := &domain.Human{ ObjectRoot: writeModelToObjectRoot(wm.WriteModel), Username: wm.UserName, State: wm.UserState, @@ -22,14 +22,22 @@ func writeModelToHuman(wm *HumanWriteModel) *domain.Human { EmailAddress: wm.Email, IsEmailVerified: wm.IsEmailVerified, }, - Address: &domain.Address{ + } + if wm.Phone != "" { + human.Phone = &domain.Phone{ + PhoneNumber: wm.Phone, + } + } + if wm.Country != "" || wm.Locality != "" || wm.PostalCode != "" || wm.Region != "" || wm.StreetAddress != "" { + human.Address = &domain.Address{ Country: wm.Country, Locality: wm.Locality, PostalCode: wm.PostalCode, Region: wm.Region, StreetAddress: wm.StreetAddress, - }, + } } + return human } func writeModelToProfile(wm *HumanProfileWriteModel) *domain.Profile { @@ -73,8 +81,10 @@ func writeModelToAddress(wm *HumanAddressWriteModel) *domain.Address { func writeModelToMachine(wm *MachineWriteModel) *domain.Machine { return &domain.Machine{ ObjectRoot: writeModelToObjectRoot(wm.WriteModel), + Username: wm.UserName, Name: wm.Name, Description: wm.Description, + State: wm.UserState, } } diff --git a/internal/command/user_human.go b/internal/command/user_human.go index 0025795283..873f79414e 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -40,7 +40,7 @@ func (c *Commands) AddHuman(ctx context.Context, orgID string, human *domain.Hum } func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Human) ([]eventstore.EventPusher, *HumanWriteModel, error) { - if !human.IsValid() { + if orgID == "" || !human.IsValid() { return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M90d", "Errors.User.Invalid") } return c.createHuman(ctx, orgID, human, nil, false) @@ -82,8 +82,8 @@ func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domai } func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domain.Human, externalIDP *domain.ExternalIDP) ([]eventstore.EventPusher, *HumanWriteModel, error) { - if !human.IsValid() || externalIDP == nil && (human.Password == nil || human.SecretString == "") { - return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-9dk45", "Errors.User.Invalid") + if orgID == "" || !human.IsValid() || externalIDP == nil && (human.Password == nil || human.SecretString == "") { + return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-9dk45", "Errors.User.Invalid") } return c.createHuman(ctx, orgID, human, externalIDP, true) } @@ -96,17 +96,17 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. human.AggregateID = userID orgIAMPolicy, err := c.getOrgIAMPolicy(ctx, orgID) if err != nil { - return nil, nil, err - } - pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, orgID) - if err != nil { - return nil, nil, err + return nil, nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-33M9f", "Errors.Org.OrgIAMPolicy.NotFound") } if err := human.CheckOrgIAMPolicy(orgIAMPolicy); err != nil { return nil, nil, err } + pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, orgID) + if err != nil { + return nil, nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-M5Fsd", "Errors.Org.PasswordComplexity.NotFound") + } human.SetNamesAsDisplayname() - if err := human.HashPasswordIfExisting(pwPolicy, c.userPasswordAlg, true); err != nil { + if err := human.HashPasswordIfExisting(pwPolicy, c.userPasswordAlg, !selfregister); err != nil { return nil, nil, err } addedHuman := NewHumanWriteModel(human.AggregateID, orgID) @@ -155,7 +155,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. func (c *Commands) HumanSkipMFAInit(ctx context.Context, userID, resourceowner string) (err error) { if userID == "" { - return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2xpX9", "Errors.User.UserIDMissing") + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-2xpX9", "Errors.User.UserIDMissing") } existingHuman, err := c.getHumanWriteModelByID(ctx, userID, resourceowner) @@ -236,10 +236,13 @@ func createRegisterHumanEvent(ctx context.Context, aggregate *eventstore.Aggrega func (c *Commands) HumansSignOut(ctx context.Context, agentID string, userIDs []string) error { if agentID == "" { - return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing") + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing") } - events := make([]eventstore.EventPusher, len(userIDs)) - for i, userID := range userIDs { + if len(userIDs) == 0 { + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-M0od3", "Errors.User.UserIDMissing") + } + events := make([]eventstore.EventPusher, 0) + for _, userID := range userIDs { existingUser, err := c.getHumanWriteModelByID(ctx, userID, "") if err != nil { return err @@ -247,12 +250,14 @@ func (c *Commands) HumansSignOut(ctx context.Context, agentID string, userIDs [] if !isUserStateExists(existingUser.UserState) { continue } - events[i] = user.NewHumanSignedOutEvent( + events = append(events, user.NewHumanSignedOutEvent( ctx, UserAggregateFromWriteModel(&existingUser.WriteModel), - agentID) + agentID)) + } + if len(events) == 0 { + return nil } - _, err := c.eventstore.PushEvents(ctx, events...) return err } diff --git a/internal/command/user_human_address.go b/internal/command/user_human_address.go index e24f4c8eeb..50daa0cf70 100644 --- a/internal/command/user_human_address.go +++ b/internal/command/user_human_address.go @@ -13,10 +13,13 @@ func (c *Commands) ChangeHumanAddress(ctx context.Context, address *domain.Addre return nil, err } if existingAddress.State == domain.AddressStateUnspecified || existingAddress.State == domain.AddressStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "COMMAND-0pLdo", "Errors.User.Address.NotFound") + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-0pLdo", "Errors.User.Address.NotFound") } userAgg := UserAggregateFromWriteModel(&existingAddress.WriteModel) - changedEvent, hasChanged := existingAddress.NewChangedEvent(ctx, userAgg, address.Country, address.Locality, address.PostalCode, address.Region, address.StreetAddress) + changedEvent, hasChanged, err := existingAddress.NewChangedEvent(ctx, userAgg, address.Country, address.Locality, address.PostalCode, address.Region, address.StreetAddress) + if err != nil { + return nil, err + } if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M0cs", "Errors.User.Address.NotChanged") } diff --git a/internal/command/user_human_address_model.go b/internal/command/user_human_address_model.go index 990d818960..14341e656d 100644 --- a/internal/command/user_human_address_model.go +++ b/internal/command/user_human_address_model.go @@ -2,9 +2,9 @@ package command import ( "context" - "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/user" ) @@ -87,28 +87,31 @@ func (wm *HumanAddressWriteModel) NewChangedEvent( postalCode, region, streetAddress string, -) (*user.HumanAddressChangedEvent, bool) { - hasChanged := false - changedEvent := user.NewHumanAddressChangedEvent(ctx, aggregate) +) (*user.HumanAddressChangedEvent, bool, error) { + changes := make([]user.AddressChanges, 0) + var err error + if wm.Country != country { - hasChanged = true - changedEvent.Country = &country + changes = append(changes, user.ChangeCountry(country)) } if wm.Locality != locality { - hasChanged = true - changedEvent.Locality = &locality + changes = append(changes, user.ChangeLocality(locality)) } if wm.PostalCode != postalCode { - hasChanged = true - changedEvent.PostalCode = &postalCode + changes = append(changes, user.ChangePostalCode(postalCode)) } if wm.Region != region { - hasChanged = true - changedEvent.Region = ®ion + changes = append(changes, user.ChangeRegion(region)) } if wm.StreetAddress != streetAddress { - hasChanged = true - changedEvent.StreetAddress = &streetAddress + changes = append(changes, user.ChangeStreetAddress(streetAddress)) } - return changedEvent, hasChanged + if len(changes) == 0 { + return nil, false, nil + } + changeEvent, err := user.NewAddressChangedEvent(ctx, aggregate, changes) + if err != nil { + return nil, false, err + } + return changeEvent, true, nil } diff --git a/internal/command/user_human_adress_test.go b/internal/command/user_human_adress_test.go new file mode 100644 index 0000000000..5997f5eb09 --- /dev/null +++ b/internal/command/user_human_adress_test.go @@ -0,0 +1,209 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestCommandSide_ChangeHumanAddress(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + address *domain.Address + resourceOwner string + } + type res struct { + want *domain.Address + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + address: &domain.Address{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + Country: "Switzerland", + Locality: "St. Gallen", + PostalCode: "9000", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "address not changed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email", + true, + ), + ), + eventFromEventPusher( + newAddressChangedEvent(context.Background(), + "user1", "org1", + "country", + "locality", + "postalcode", + "region", + "street", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + address: &domain.Address{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + Country: "country", + Locality: "locality", + PostalCode: "postalcode", + Region: "region", + StreetAddress: "street", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "address changed, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newAddressChangedEvent(context.Background(), + "user1", "org1", + "country", + "locality", + "postalcode", + "region", + "street", + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + address: &domain.Address{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + Country: "country", + Locality: "locality", + PostalCode: "postalcode", + Region: "region", + StreetAddress: "street", + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.Address{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Country: "country", + Locality: "locality", + PostalCode: "postalcode", + Region: "region", + StreetAddress: "street", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeHumanAddress(tt.args.ctx, tt.args.address) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newAddressChangedEvent(ctx context.Context, userID, resourceOwner, country, locality, postalCode, region, street string) *user.HumanAddressChangedEvent { + event, _ := user.NewAddressChangedEvent(ctx, + &user.NewAggregate(userID, resourceOwner).Aggregate, + []user.AddressChanges{ + user.ChangeCountry(country), + user.ChangeLocality(locality), + user.ChangePostalCode(postalCode), + user.ChangeRegion(region), + user.ChangeStreetAddress(street), + }, + ) + return event +} diff --git a/internal/command/user_human_email.go b/internal/command/user_human_email.go index 02bdab3748..96aab9a587 100644 --- a/internal/command/user_human_email.go +++ b/internal/command/user_human_email.go @@ -13,7 +13,7 @@ import ( func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email) (*domain.Email, error) { if !email.IsValid() || email.AggregateID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4M9sf", "Errors.Email.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M9sf", "Errors.Email.Invalid") } existingEmail, err := c.emailWriteModel(ctx, email.AggregateID, email.ResourceOwner) @@ -21,7 +21,7 @@ func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email) (* return nil, err } if existingEmail.UserState == domain.UserStateUnspecified || existingEmail.UserState == domain.UserStateDeleted { - return nil, caos_errs.ThrowNotFound(nil, "COMMAND-0Pe4r", "Errors.User.Email.NotFound") + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-0Pe4r", "Errors.User.Email.NotFound") } userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel) changedEvent, hasChanged := existingEmail.NewChangedEvent(ctx, userAgg, email.EmailAddress) @@ -54,10 +54,10 @@ func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email) (* func (c *Commands) VerifyHumanEmail(ctx context.Context, userID, code, resourceowner string) (*domain.ObjectDetails, error) { if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing") } if code == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-çm0ds", "Errors.User.Code.Empty") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-çm0ds", "Errors.User.Code.Empty") } existingCode, err := c.emailWriteModel(ctx, userID, resourceowner) @@ -89,7 +89,7 @@ func (c *Commands) VerifyHumanEmail(ctx context.Context, userID, code, resourceo func (c *Commands) CreateHumanEmailVerificationCode(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) { if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing") } existingEmail, err := c.emailWriteModel(ctx, userID, resourceOwner) @@ -122,6 +122,9 @@ func (c *Commands) CreateHumanEmailVerificationCode(ctx context.Context, userID, } func (c *Commands) HumanEmailVerificationCodeSent(ctx context.Context, orgID, userID string) (err error) { + if userID == "" { + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-4m9fs", "Errors.IDMissing") + } existingEmail, err := c.emailWriteModel(ctx, userID, orgID) if err != nil { return err diff --git a/internal/command/user_human_email_model.go b/internal/command/user_human_email_model.go index ee23317b07..8c9ebbd1dc 100644 --- a/internal/command/user_human_email_model.go +++ b/internal/command/user_human_email_model.go @@ -82,11 +82,8 @@ func (wm *HumanEmailWriteModel) NewChangedEvent( aggregate *eventstore.Aggregate, email string, ) (*user.HumanEmailChangedEvent, bool) { - hasChanged := false - changedEvent := user.NewHumanEmailChangedEvent(ctx, aggregate) - if wm.Email != email { - hasChanged = true - changedEvent.EmailAddress = email + if wm.Email == email { + return nil, false } - return changedEvent, hasChanged + return user.NewHumanEmailChangedEvent(ctx, aggregate, email), true } diff --git a/internal/command/user_human_email_test.go b/internal/command/user_human_email_test.go new file mode 100644 index 0000000000..e22517a8bf --- /dev/null +++ b/internal/command/user_human_email_test.go @@ -0,0 +1,822 @@ +package command + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestCommandSide_ChangeHumanEmail(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretGenerator crypto.Generator + } + type args struct { + ctx context.Context + email *domain.Email + resourceOwner string + } + type res struct { + want *domain.Email + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid email, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + email: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + email: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + EmailAddress: "email@test.com", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "email not changed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + email: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + EmailAddress: "email@test.ch", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "verified email changed, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanEmailChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "email-changed@test.ch", + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + email: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + EmailAddress: "email-changed@test.ch", + IsEmailVerified: true, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + EmailAddress: "email-changed@test.ch", + IsEmailVerified: true, + }, + }, + }, + { + name: "email changed with code, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanEmailChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "email-changed@test.ch", + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + email: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + EmailAddress: "email-changed@test.ch", + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + EmailAddress: "email-changed@test.ch", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + emailVerificationCode: tt.fields.secretGenerator, + } + got, err := r.ChangeHumanEmail(tt.args.ctx, tt.args.email) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_VerifyHumanEmail(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretGenerator crypto.Generator + } + type args struct { + ctx context.Context + userID string + code string + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + code: "aa", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "code missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "aa", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "code not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "aa", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "invalid code, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanEmailVerificationFailedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "test", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "valid code, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanEmailCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "a", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + emailVerificationCode: tt.fields.secretGenerator, + } + got, err := r.VerifyHumanEmail(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_CreateVerificationCodeHumanEmail(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretGenerator crypto.Generator + } + type args struct { + ctx context.Context + userID string + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "user not initialized, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "email already verified, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "new code, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanEmailChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "email2@test.ch", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanEmailCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + emailVerificationCode: tt.fields.secretGenerator, + } + got, err := r.CreateHumanEmailVerificationCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_EmailVerificationCodeSent(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userID string + resourceOwner string + } + type res struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "code sent, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanEmailChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "email2@test.ch", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanEmailCodeSentEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.HumanEmailVerificationCodeSent(tt.args.ctx, tt.args.resourceOwner, tt.args.userID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} diff --git a/internal/command/user_human_externalidp.go b/internal/command/user_human_externalidp.go index 636dfd785e..68eeec17b8 100644 --- a/internal/command/user_human_externalidp.go +++ b/internal/command/user_human_externalidp.go @@ -11,8 +11,11 @@ import ( ) func (c *Commands) BulkAddedHumanExternalIDP(ctx context.Context, userID, resourceOwner string, externalIDPs []*domain.ExternalIDP) (err error) { + if userID == "" { + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-03j8f", "Errors.IDMissing") + } if len(externalIDPs) == 0 { - return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Ek9s", "Errors.User.ExternalIDP.MinimumExternalIDPNeeded") + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Ek9s", "Errors.User.ExternalIDP.MinimumExternalIDPNeeded") } events := make([]eventstore.EventPusher, len(externalIDPs)) @@ -31,15 +34,18 @@ func (c *Commands) BulkAddedHumanExternalIDP(ctx context.Context, userID, resour } func (c *Commands) addHumanExternalIDP(ctx context.Context, humanAgg *eventstore.Aggregate, externalIDP *domain.ExternalIDP) (eventstore.EventPusher, error) { + if externalIDP.AggregateID != "" && humanAgg.ID != externalIDP.AggregateID { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-33M0g", "Errors.IDMissing") + } if !externalIDP.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6m9Kd", "Errors.User.ExternalIDP.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-6m9Kd", "Errors.User.ExternalIDP.Invalid") } _, err := c.getOrgIDPConfigByID(ctx, externalIDP.IDPConfigID, humanAgg.ResourceOwner) if caos_errs.IsNotFound(err) { _, err = c.getIAMIDPConfigByID(ctx, externalIDP.IDPConfigID) } if err != nil { - return nil, err + return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-39nfs", "Errors.IDPConfig.NotExisting") } return user.NewHumanExternalIDPAddedEvent(ctx, humanAgg, externalIDP.IDPConfigID, externalIDP.DisplayName, externalIDP.ExternalUserID), nil } @@ -61,8 +67,8 @@ func (c *Commands) RemoveHumanExternalIDP(ctx context.Context, externalIDP *doma } func (c *Commands) removeHumanExternalIDP(ctx context.Context, externalIDP *domain.ExternalIDP, cascade bool) (eventstore.EventPusher, *HumanExternalIDPWriteModel, error) { - if externalIDP.IsValid() { - return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M9ds", "Errors.IDMissing") + if !externalIDP.IsValid() || externalIDP.AggregateID == "" { + return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M9ds", "Errors.IDMissing") } existingExternalIDP, err := c.externalIDPWriteModelByID(ctx, externalIDP.AggregateID, externalIDP.IDPConfigID, externalIDP.ExternalUserID, externalIDP.ResourceOwner) @@ -81,7 +87,7 @@ func (c *Commands) removeHumanExternalIDP(ctx context.Context, externalIDP *doma func (c *Commands) HumanExternalLoginChecked(ctx context.Context, orgID, userID string, authRequest *domain.AuthRequest) (err error) { if userID == "" { - return caos_errs.ThrowNotFound(nil, "COMMAND-5n8sM", "Errors.IDMissing") + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-5n8sM", "Errors.IDMissing") } existingHuman, err := c.getHumanWriteModelByID(ctx, userID, orgID) @@ -89,7 +95,7 @@ func (c *Commands) HumanExternalLoginChecked(ctx context.Context, orgID, userID return err } if existingHuman.UserState == domain.UserStateUnspecified || existingHuman.UserState == domain.UserStateDeleted { - return caos_errs.ThrowNotFound(nil, "COMMAND-dn88J", "Errors.User.NotFound") + return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-dn88J", "Errors.User.NotFound") } userAgg := UserAggregateFromWriteModel(&existingHuman.WriteModel) diff --git a/internal/command/user_human_externalidp_test.go b/internal/command/user_human_externalidp_test.go new file mode 100644 index 0000000000..9438ed45cb --- /dev/null +++ b/internal/command/user_human_externalidp_test.go @@ -0,0 +1,582 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestCommandSide_BulkAddExternalIDPs(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userID string + resourceOwner string + externalIDPs []*domain.ExternalIDP + } + type res struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "missing userid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "", + externalIDPs: []*domain.ExternalIDP{ + { + IDPConfigID: "config1", + ExternalUserID: "externaluser1", + }, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "no external idps, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "userID doesnt match aggregate id, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + externalIDPs: []*domain.ExternalIDP{ + { + ObjectRoot: models.ObjectRoot{ + AggregateID: "user2", + }, + IDPConfigID: "config1", + ExternalUserID: "externaluser1", + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid external idp, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + externalIDPs: []*domain.ExternalIDP{ + { + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "", + ExternalUserID: "externaluser1", + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "config not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + externalIDPs: []*domain.ExternalIDP{ + { + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "config1", + ExternalUserID: "externaluser1", + }, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "add external idp org config, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanExternalIDPAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "config1", + "name", + "externaluser1", + ), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewAddExternalIDPUniqueConstraint("config1", "externaluser1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + externalIDPs: []*domain.ExternalIDP{ + { + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "config1", + DisplayName: "name", + ExternalUserID: "externaluser1", + }, + }, + }, + res: res{}, + }, + { + name: "add external idp iam config, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + iam.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "config1", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanExternalIDPAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "config1", + "name", + "externaluser1", + ), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewAddExternalIDPUniqueConstraint("config1", "externaluser1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + externalIDPs: []*domain.ExternalIDP{ + { + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "config1", + DisplayName: "name", + ExternalUserID: "externaluser1", + }, + }, + }, + res: res{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.BulkAddedHumanExternalIDP(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.externalIDPs) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func TestCommandSide_RemoveExternalIDP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + externalIDP *domain.ExternalIDP + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid idp, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + externalIDP: &domain.ExternalIDP{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "", + ExternalUserID: "externaluser1", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "aggregate id missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + externalIDP: &domain.ExternalIDP{ + IDPConfigID: "config1", + ExternalUserID: "externaluser1", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanExternalIDPAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "config1", + "name", + "externaluser1", + ), + ), + eventFromEventPusher( + user.NewUserRemovedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + externalIDP: &domain.ExternalIDP{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "config1", + ExternalUserID: "externaluser1", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "external idp not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + externalIDP: &domain.ExternalIDP{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "config1", + ExternalUserID: "externaluser1", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove external idp, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanExternalIDPAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "config1", + "name", + "externaluser1", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanExternalIDPRemovedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "config1", + "externaluser1", + ), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewRemoveExternalIDPUniqueConstraint("config1", "externaluser1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + externalIDP: &domain.ExternalIDP{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + IDPConfigID: "config1", + ExternalUserID: "externaluser1", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveHumanExternalIDP(tt.args.ctx, tt.args.externalIDP) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ExternalLoginCheck(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + userID string + authRequest *domain.AuthRequest + } + type res struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanExternalIDPAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "config1", + "name", + "externaluser1", + ), + ), + eventFromEventPusher( + user.NewUserRemovedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "external login check, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanExternalIDPCheckSucceededEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &user.AuthRequestInfo{ + ID: "request1", + UserAgentID: "useragent1", + SelectedIDPConfigID: "config1", + }, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + authRequest: &domain.AuthRequest{ + ID: "request1", + AgentID: "useragent1", + SelectedIDPConfigID: "config1", + }, + }, + res: res{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.HumanExternalLoginChecked(tt.args.ctx, tt.args.orgID, tt.args.userID, tt.args.authRequest) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} diff --git a/internal/command/user_human_init.go b/internal/command/user_human_init.go index 4916fac724..0e0128394d 100644 --- a/internal/command/user_human_init.go +++ b/internal/command/user_human_init.go @@ -13,7 +13,7 @@ import ( //ResendInitialMail resend inital mail and changes email if provided func (c *Commands) ResendInitialMail(ctx context.Context, userID, email, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) { if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing") } existingCode, err := c.getHumanInitWriteModelByID(ctx, userID, resourceOwner) @@ -50,10 +50,10 @@ func (c *Commands) ResendInitialMail(ctx context.Context, userID, email, resourc func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwner, code, passwordString string) error { if userID == "" { - return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-mkM9f", "Errors.User.UserIDMissing") + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-mkM9f", "Errors.User.UserIDMissing") } if code == "" { - return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-44G8s", "Errors.User.Code.Empty") + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-44G8s", "Errors.User.Code.Empty") } existingCode, err := c.getHumanInitWriteModelByID(ctx, userID, resourceOwner) @@ -79,6 +79,7 @@ func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwne } if passwordString != "" { passwordWriteModel := NewHumanPasswordWriteModel(userID, existingCode.ResourceOwner) + passwordWriteModel.UserState = domain.UserStateActive password := &domain.Password{ SecretString: passwordString, ChangeRequired: false, @@ -89,12 +90,14 @@ func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwne } events = append(events, passwordEvent) } - events = append(events, user.NewHumanInitialCodeSentEvent(ctx, userAgg)) _, err = c.eventstore.PushEvents(ctx, events...) return err } func (c *Commands) HumanInitCodeSent(ctx context.Context, orgID, userID string) (err error) { + if userID == "" { + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M9fs", "Errors.IDMissing") + } existingInitCode, err := c.getHumanInitWriteModelByID(ctx, userID, orgID) if err != nil { return err diff --git a/internal/command/user_human_init_model.go b/internal/command/user_human_init_model.go index 5a69a9673b..58e6963e76 100644 --- a/internal/command/user_human_init_model.go +++ b/internal/command/user_human_init_model.go @@ -84,11 +84,6 @@ func (wm *HumanInitCodeWriteModel) NewChangedEvent( aggregate *eventstore.Aggregate, email string, ) (*user.HumanEmailChangedEvent, bool) { - hasChanged := false - changedEvent := user.NewHumanEmailChangedEvent(ctx, aggregate) - if wm.Email != email { - hasChanged = true - changedEvent.EmailAddress = email - } - return changedEvent, hasChanged + changedEvent := user.NewHumanEmailChangedEvent(ctx, aggregate, email) + return changedEvent, wm.Email != email } diff --git a/internal/command/user_human_init_test.go b/internal/command/user_human_init_test.go new file mode 100644 index 0000000000..260c81e71c --- /dev/null +++ b/internal/command/user_human_init_test.go @@ -0,0 +1,720 @@ +package command + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestCommandSide_ResendInitialMail(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretGenerator crypto.Generator + } + type args struct { + ctx context.Context + userID string + email string + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "user not initialized, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate)), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "new code email not changed, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + email: "email@test.ch", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "new code, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "new code with change email, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanEmailChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "email2@test.ch")), + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + email: "email2@test.ch", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + initializeUserCode: tt.fields.secretGenerator, + } + got, err := r.ResendInitialMail(tt.args.ctx, tt.args.userID, tt.args.email, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_VerifyInitCode(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretGenerator crypto.Generator + userPasswordAlg crypto.HashAlgorithm + } + type args struct { + ctx context.Context + userID string + code string + resourceOwner string + password string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + code: "aa", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "code missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "aa", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "code not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "aa", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "invalid code, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanInitializedCheckFailedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "test", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "valid code, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanInitializedCheckSucceededEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "a", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "valid code with password, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate)), + eventFromEventPusherWithCreationDateNow( + 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( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanInitializedCheckSucceededEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + KeyID: "", + Crypted: []byte("password"), + }, + false, + "")), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "a", + resourceOwner: "org1", + password: "password", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + initializeUserCode: tt.fields.secretGenerator, + userPasswordAlg: tt.fields.userPasswordAlg, + } + err := r.HumanVerifyInitCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.code, tt.args.password) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func TestCommandSide_InitCodeSent(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userID string + resourceOwner string + } + type res struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "code sent, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanInitialCodeSentEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.HumanInitCodeSent(tt.args.ctx, tt.args.resourceOwner, tt.args.userID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} diff --git a/internal/command/user_human_otp.go b/internal/command/user_human_otp.go index e34d573a85..fdc88a070c 100644 --- a/internal/command/user_human_otp.go +++ b/internal/command/user_human_otp.go @@ -12,22 +12,22 @@ import ( func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string) (*domain.OTP, error) { if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing") } human, err := c.getHuman(ctx, userID, resourceowner) if err != nil { logging.Log("COMMAND-DAqe1").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get human for loginname") - return nil, err + return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-MM9fs", "Errors.User.NotFound") } org, err := c.getOrg(ctx, human.ResourceOwner) if err != nil { logging.Log("COMMAND-Cm0ds").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org for loginname") - return nil, err + return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-55M9f", "Errors.Org.NotFound") } orgPolicy, err := c.getOrgIAMPolicy(ctx, org.AggregateID) if err != nil { logging.Log("COMMAND-y5zv9").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org policy for loginname") - return nil, err + return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-8ugTs", "Errors.Org.OrgIAM.NotFound") } otpWriteModel, err := c.otpWriteModelByID(ctx, userID, resourceowner) if err != nil { @@ -114,7 +114,7 @@ func (c *Commands) HumanCheckMFAOTP(ctx context.Context, userID, code, resourceo func (c *Commands) HumanRemoveOTP(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) { if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing") } existingOTP, err := c.otpWriteModelByID(ctx, userID, resourceOwner) diff --git a/internal/command/user_human_otp_test.go b/internal/command/user_human_otp_test.go new file mode 100644 index 0000000000..a92094bb23 --- /dev/null +++ b/internal/command/user_human_otp_test.go @@ -0,0 +1,339 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestCommandSide_AddHumanOTP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type ( + args struct { + ctx context.Context + orgID string + userID string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "org not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "org iam policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("org1", "org1").Aggregate, + "org", + ), + ), + ), + expectFilter(), + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "otp already exists, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("org1", "org1").Aggregate, + "org", + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanOTPAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }), + ), + eventFromEventPusher( + user.NewHumanOTPVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "agent1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddHumanOTP(tt.args.ctx, tt.args.userID, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveHumanOTP(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type ( + args struct { + ctx context.Context + orgID string + userID string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "otp not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "otp not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanOTPAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + nil, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanOTPRemovedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.HumanRemoveOTP(tt.args.ctx, tt.args.userID, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} diff --git a/internal/command/user_human_password.go b/internal/command/user_human_password.go index 1051127132..3572650c35 100644 --- a/internal/command/user_human_password.go +++ b/internal/command/user_human_password.go @@ -14,11 +14,16 @@ import ( func (c *Commands) SetOneTimePassword(ctx context.Context, orgID, userID, passwordString string) (objectDetails *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - + if userID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M0fs", "Errors.IDMissing") + } existingPassword, err := c.passwordWriteModel(ctx, userID, orgID) if err != nil { return nil, err } + if !existingPassword.UserState.Exists() { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M0fs", "Errors.User.NotFound") + } password := &domain.Password{ SecretString: passwordString, ChangeRequired: true, @@ -43,16 +48,22 @@ func (c *Commands) SetPassword(ctx context.Context, orgID, userID, code, passwor ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() + if userID == "" { + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M9fs", "Errors.IDMissing") + } + if passwordString == "" { + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Mf0sd", "Errors.User.Password.Empty") + } existingCode, err := c.passwordWriteModel(ctx, userID, orgID) if err != nil { return err } if existingCode.Code == nil || existingCode.UserState == domain.UserStateUnspecified || existingCode.UserState == domain.UserStateDeleted { - return caos_errs.ThrowNotFound(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound") + return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound") } - err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, c.emailVerificationCode) + err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, c.passwordVerificationCode) if err != nil { return err } @@ -74,6 +85,12 @@ func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPasswor ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() + if userID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M0fs", "Errors.IDMissing") + } + if oldPassword == "" || newPassword == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M0fs", "Errors.User.Password.Empty") + } existingPassword, err := c.passwordWriteModel(ctx, userID, orgID) if err != nil { return nil, err @@ -114,7 +131,7 @@ func (c *Commands) changePassword(ctx context.Context, userAgentID string, passw defer func() { span.EndWithError(err) }() if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted { - return nil, caos_errs.ThrowNotFound(nil, "COMMAND-G8dh3", "Errors.User.Email.NotFound") + return nil, caos_errs.ThrowNotFound(nil, "COMMAND-G8dh3", "Errors.User.Password.NotFound") } if existingPassword.UserState == domain.UserStateInitial { return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-M9dse", "Errors.User.NotInitialised") @@ -130,12 +147,16 @@ func (c *Commands) changePassword(ctx context.Context, userAgentID string, passw } func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner string, notifyType domain.NotificationType) (objectDetails *domain.ObjectDetails, err error) { + if userID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-M00oL", "Errors.User.UserIDMissing") + } + existingHuman, err := c.userWriteModelByID(ctx, userID, resourceOwner) if err != nil { return nil, err } if existingHuman.UserState == domain.UserStateUnspecified || existingHuman.UserState == domain.UserStateDeleted { - return nil, caos_errs.ThrowNotFound(nil, "COMMAND-Hj9ds", "Errors.User.NotFound") + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Hj9ds", "Errors.User.NotFound") } if existingHuman.UserState == domain.UserStateInitial { return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9sd", "Errors.User.NotInitialised") @@ -157,12 +178,16 @@ func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner } func (c *Commands) PasswordCodeSent(ctx context.Context, orgID, userID string) (err error) { + if userID == "" { + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-MM9fs", "Errors.User.UserIDMissing") + } + existingPassword, err := c.passwordWriteModel(ctx, userID, orgID) if err != nil { return err } if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted { - return caos_errs.ThrowNotFound(nil, "COMMAND-3n77z", "Errors.User.NotFound") + return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.NotFound") } userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel) _, err = c.eventstore.PushEvents(ctx, user.NewHumanPasswordCodeSentEvent(ctx, userAgg)) @@ -173,8 +198,11 @@ func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, passwo ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() + if userID == "" { + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-4Mfsf", "Errors.User.UserIDMissing") + } if password == "" { - return caos_errs.ThrowNotFound(nil, "COMMAND-3n8fs", "Errors.User.Password.Empty") + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-3n8fs", "Errors.User.Password.Empty") } existingPassword, err := c.passwordWriteModel(ctx, userID, orgID) @@ -182,11 +210,11 @@ func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, passwo return err } if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted { - return caos_errs.ThrowNotFound(nil, "COMMAND-3n77z", "Errors.User.NotFound") + return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.NotFound") } if existingPassword.Secret == nil { - return caos_errs.ThrowNotFound(nil, "COMMAND-3n77z", "Errors.User.Password.NotSet") + return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.Password.NotSet") } userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel) diff --git a/internal/command/user_human_password_test.go b/internal/command/user_human_password_test.go new file mode 100644 index 0000000000..c04bc794da --- /dev/null +++ b/internal/command/user_human_password_test.go @@ -0,0 +1,1248 @@ +package command + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestCommandSide_SetOneTimePassword(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + userPasswordAlg crypto.HashAlgorithm + } + type args struct { + ctx context.Context + userID string + resourceOwner string + password string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change password, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + 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( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + KeyID: "", + Crypted: []byte("password"), + }, + true, + "", + ), + ), + }, + ), + ), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + password: "password", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + userPasswordAlg: tt.fields.userPasswordAlg, + } + got, err := r.SetOneTimePassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.password) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_SetPassword(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + userPasswordAlg crypto.HashAlgorithm + secretGenerator crypto.Generator + } + type args struct { + ctx context.Context + userID string + code string + resourceOwner string + password string + agentID string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "password missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + password: "password", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "code not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "aa", + resourceOwner: "org1", + password: "string", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "invalid code, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanPasswordCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + domain.NotificationTypeEmail, + ), + ), + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "test", + resourceOwner: "org1", + password: "password", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "set password, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanPasswordCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + domain.NotificationTypeEmail, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + KeyID: "", + Crypted: []byte("password"), + }, + false, + "", + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + password: "password", + code: "a", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + userPasswordAlg: tt.fields.userPasswordAlg, + passwordVerificationCode: tt.fields.secretGenerator, + } + err := r.SetPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password, tt.args.agentID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func TestCommandSide_ChangePassword(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + userPasswordAlg crypto.HashAlgorithm + } + type args struct { + ctx context.Context + userID string + resourceOwner string + oldPassword string + newPassword string + agentID string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + oldPassword: "password", + newPassword: "password1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "old password missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + newPassword: "password1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "new password missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + oldPassword: "password", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + oldPassword: "password", + newPassword: "password1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "existing password empty, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + oldPassword: "password", + newPassword: "password1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "password not matching, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + KeyID: "", + Crypted: []byte("password"), + }, + false, + "")), + ), + ), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + oldPassword: "password-old", + newPassword: "password1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "change password, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + KeyID: "", + Crypted: []byte("password"), + }, + false, + "")), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + KeyID: "", + Crypted: []byte("password1"), + }, + false, + "", + ), + ), + }, + ), + ), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + oldPassword: "password", + newPassword: "password1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + userPasswordAlg: tt.fields.userPasswordAlg, + } + got, err := r.ChangePassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.oldPassword, tt.args.newPassword, tt.args.agentID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RequestSetPassword(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretGenerator crypto.Generator + } + type args struct { + ctx context.Context + userID string + resourceOwner string + notifyType domain.NotificationType + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "phone already verified, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+411234567", + ), + ), + eventFromEventPusher( + user.NewHumanPhoneVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "new code, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanInitializedCheckSucceededEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate)), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPasswordCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + domain.NotificationTypeEmail, + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + passwordVerificationCode: tt.fields.secretGenerator, + } + got, err := r.RequestSetPassword(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.notifyType) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_PasswordCodeSent(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userID string + resourceOwner string + } + type res struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "code sent, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+411234567", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPasswordCodeSentEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.PasswordCodeSent(tt.args.ctx, tt.args.resourceOwner, tt.args.userID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func TestCommandSide_CheckPassword(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + userPasswordAlg crypto.HashAlgorithm + } + type args struct { + ctx context.Context + userID string + resourceOwner string + password string + authReq *domain.AuthRequest + } + type res struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + password: "password", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "password missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + password: "password", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "existing password empty, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + password: "password", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "password not matching, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + KeyID: "", + Crypted: []byte("password"), + }, + false, + "")), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPasswordCheckFailedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &user.AuthRequestInfo{ + ID: "request1", + UserAgentID: "agent1", + }, + ), + ), + }, + ), + ), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + password: "password1", + resourceOwner: "org1", + authReq: &domain.AuthRequest{ + ID: "request1", + AgentID: "agent1", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "check password, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanPasswordChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + KeyID: "", + Crypted: []byte("password"), + }, + false, + "")), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPasswordCheckSucceededEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &user.AuthRequestInfo{ + ID: "request1", + UserAgentID: "agent1", + }, + ), + ), + }, + ), + ), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + password: "password", + authReq: &domain.AuthRequest{ + ID: "request1", + AgentID: "agent1", + }, + }, + res: res{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + userPasswordAlg: tt.fields.userPasswordAlg, + } + err := r.HumanCheckPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.password, tt.args.authReq) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} diff --git a/internal/command/user_human_phone.go b/internal/command/user_human_phone.go index 5554055e0c..19ec3d553d 100644 --- a/internal/command/user_human_phone.go +++ b/internal/command/user_human_phone.go @@ -14,15 +14,15 @@ import ( func (c *Commands) ChangeHumanPhone(ctx context.Context, phone *domain.Phone) (*domain.Phone, error) { if !phone.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6M0ds", "Errors.Phone.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-6M0ds", "Errors.Phone.Invalid") } existingPhone, err := c.phoneWriteModelByID(ctx, phone.AggregateID, phone.ResourceOwner) if err != nil { return nil, err } - if !existingPhone.State.Exists() { - return nil, caos_errs.ThrowNotFound(nil, "COMMAND-aM9cs", "Errors.User.Phone.NotFound") + if !existingPhone.UserState.Exists() { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M0fs", "Errors.User.NotFound") } userAgg := UserAggregateFromWriteModel(&existingPhone.WriteModel) @@ -56,17 +56,20 @@ func (c *Commands) ChangeHumanPhone(ctx context.Context, phone *domain.Phone) (* func (c *Commands) VerifyHumanPhone(ctx context.Context, userID, code, resourceowner string) (*domain.ObjectDetails, error) { if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Km9ds", "Errors.User.UserIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Km9ds", "Errors.User.UserIDMissing") } if code == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-wMe9f", "Errors.User.Code.Empty") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-wMe9f", "Errors.User.Code.Empty") } existingCode, err := c.phoneWriteModelByID(ctx, userID, resourceowner) if err != nil { return nil, err } - if !existingCode.State.Exists() { + if !existingCode.UserState.Exists() { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Rsj8c", "Errors.User.NotFound") + } + if !existingCode.State.Exists() || existingCode.Code == nil { return nil, caos_errs.ThrowNotFound(nil, "COMMAND-Rsj8c", "Errors.User.Code.NotFound") } @@ -90,7 +93,7 @@ func (c *Commands) VerifyHumanPhone(ctx context.Context, userID, code, resourceo func (c *Commands) CreateHumanPhoneVerificationCode(ctx context.Context, userID, resourceowner string) (*domain.ObjectDetails, error) { if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing") } existingPhone, err := c.phoneWriteModelByID(ctx, userID, resourceowner) @@ -98,6 +101,9 @@ func (c *Commands) CreateHumanPhoneVerificationCode(ctx context.Context, userID, return nil, err } + if !existingPhone.UserState.Exists() { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M0fs", "Errors.User.NotFound") + } if !existingPhone.State.Exists() { return nil, caos_errs.ThrowNotFound(nil, "COMMAND-2b7Hf", "Errors.User.Phone.NotFound") } @@ -123,10 +129,17 @@ func (c *Commands) CreateHumanPhoneVerificationCode(ctx context.Context, userID, } func (c *Commands) HumanPhoneVerificationCodeSent(ctx context.Context, orgID, userID string) (err error) { + if userID == "" { + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-3m9Fs", "Errors.User.UserIDMissing") + } + existingPhone, err := c.phoneWriteModelByID(ctx, userID, orgID) if err != nil { return err } + if !existingPhone.UserState.Exists() { + return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M9fs", "Errors.User.NotFound") + } if !existingPhone.State.Exists() { return caos_errs.ThrowNotFound(nil, "COMMAND-66n8J", "Errors.User.Phone.NotFound") } @@ -138,13 +151,16 @@ func (c *Commands) HumanPhoneVerificationCodeSent(ctx context.Context, orgID, us func (c *Commands) RemoveHumanPhone(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) { if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6M0ds", "Errors.User.UserIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-6M0ds", "Errors.User.UserIDMissing") } existingPhone, err := c.phoneWriteModelByID(ctx, userID, resourceOwner) if err != nil { return nil, err } + if !existingPhone.UserState.Exists() { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M9fs", "Errors.User.NotFound") + } if !existingPhone.State.Exists() { return nil, caos_errs.ThrowNotFound(nil, "COMMAND-p6rsc", "Errors.User.Phone.NotFound") } diff --git a/internal/command/user_human_phone_model.go b/internal/command/user_human_phone_model.go index 3f651c8634..29f1a28a3b 100644 --- a/internal/command/user_human_phone_model.go +++ b/internal/command/user_human_phone_model.go @@ -20,7 +20,8 @@ type HumanPhoneWriteModel struct { CodeCreationDate time.Time CodeExpiry time.Duration - State domain.PhoneState + State domain.PhoneState + UserState domain.UserState } func NewHumanPhoneWriteModel(userID, resourceOwner string) *HumanPhoneWriteModel { @@ -38,13 +39,15 @@ func (wm *HumanPhoneWriteModel) Reduce() error { case *user.HumanAddedEvent: if e.PhoneNumber != "" { wm.Phone = e.PhoneNumber + wm.State = domain.PhoneStateActive } - wm.State = domain.PhoneStateActive + wm.UserState = domain.UserStateActive case *user.HumanRegisteredEvent: if e.PhoneNumber != "" { wm.Phone = e.PhoneNumber wm.State = domain.PhoneStateActive } + wm.UserState = domain.UserStateActive case *user.HumanPhoneChangedEvent: wm.Phone = e.PhoneNumber wm.IsPhoneVerified = false @@ -60,7 +63,7 @@ func (wm *HumanPhoneWriteModel) Reduce() error { case *user.HumanPhoneRemovedEvent: wm.State = domain.PhoneStateRemoved case *user.UserRemovedEvent: - wm.State = domain.PhoneStateRemoved + wm.UserState = domain.UserStateDeleted } } return wm.WriteModel.Reduce() @@ -84,11 +87,6 @@ func (wm *HumanPhoneWriteModel) NewChangedEvent( aggregate *eventstore.Aggregate, phone string, ) (*user.HumanPhoneChangedEvent, bool) { - hasChanged := false - changedEvent := user.NewHumanPhoneChangedEvent(ctx, aggregate) - if wm.Phone != phone { - hasChanged = true - changedEvent.PhoneNumber = phone - } - return changedEvent, hasChanged + changedEvent := user.NewHumanPhoneChangedEvent(ctx, aggregate, phone) + return changedEvent, phone != wm.Phone } diff --git a/internal/command/user_human_phone_test.go b/internal/command/user_human_phone_test.go new file mode 100644 index 0000000000..9bbbb3e9e0 --- /dev/null +++ b/internal/command/user_human_phone_test.go @@ -0,0 +1,962 @@ +package command + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestCommandSide_ChangeHumanPhone(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretGenerator crypto.Generator + } + type args struct { + ctx context.Context + email *domain.Phone + resourceOwner string + } + type res struct { + want *domain.Phone + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid phone, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + email: &domain.Phone{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + email: &domain.Phone{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + PhoneNumber: "0711234567", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "phone not changed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+41711234567", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + email: &domain.Phone{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + PhoneNumber: "0711234567", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "verified phone changed, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+41711234567", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+41719876543", + ), + ), + eventFromEventPusher( + user.NewHumanPhoneVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + email: &domain.Phone{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + PhoneNumber: "0719876543", + IsPhoneVerified: true, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.Phone{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + PhoneNumber: "+41719876543", + IsPhoneVerified: true, + }, + }, + }, + { + name: "phone changed with code, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+41711234567", + ), + ), + eventFromEventPusher( + user.NewHumanPhoneCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + email: &domain.Phone{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + PhoneNumber: "0711234567", + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.Phone{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + PhoneNumber: "+41711234567", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + phoneVerificationCode: tt.fields.secretGenerator, + } + got, err := r.ChangeHumanPhone(tt.args.ctx, tt.args.email) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_VerifyHumanPhone(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretGenerator crypto.Generator + } + type args struct { + ctx context.Context + userID string + code string + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + code: "aa", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "code missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "aa", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "code not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "aa", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "invalid code, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+411234567", + ), + ), + eventFromEventPusher( + user.NewHumanPhoneCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPhoneVerificationFailedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "test", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "valid code, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+411234567", + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanPhoneCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPhoneVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + code: "a", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + phoneVerificationCode: tt.fields.secretGenerator, + } + got, err := r.VerifyHumanPhone(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretGenerator crypto.Generator + } + type args struct { + ctx context.Context + userID string + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "phone already verified, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+411234567", + ), + ), + eventFromEventPusher( + user.NewHumanPhoneVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "new code, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+411234567", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPhoneCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + phoneVerificationCode: tt.fields.secretGenerator, + } + got, err := r.CreateHumanPhoneVerificationCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_PhoneVerificationCodeSent(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userID string + resourceOwner string + } + type res struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "code sent, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+411234567", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPhoneCodeSentEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.HumanPhoneVerificationCodeSent(tt.args.ctx, tt.args.resourceOwner, tt.args.userID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func TestCommandSide_RemoveHumanPhone(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userID string + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "phone not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "remove phone, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanPhoneChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "+411234567", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanPhoneRemovedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveHumanPhone(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} diff --git a/internal/command/user_human_profile.go b/internal/command/user_human_profile.go index ee2d9a9096..67bfcffc6f 100644 --- a/internal/command/user_human_profile.go +++ b/internal/command/user_human_profile.go @@ -18,9 +18,13 @@ func (c *Commands) ChangeHumanProfile(ctx context.Context, profile *domain.Profi return nil, err } if existingProfile.UserState == domain.UserStateUnspecified || existingProfile.UserState == domain.UserStateDeleted { - return nil, caos_errs.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.User.Profile.NotFound") + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M9sd", "Errors.User.Profile.NotFound") + } + userAgg := UserAggregateFromWriteModel(&existingProfile.WriteModel) + changedEvent, hasChanged, err := existingProfile.NewChangedEvent(ctx, userAgg, profile.FirstName, profile.LastName, profile.NickName, profile.DisplayName, profile.PreferredLanguage, profile.Gender) + if err != nil { + return nil, err } - changedEvent, hasChanged := existingProfile.NewChangedEvent(ctx, profile.FirstName, profile.LastName, profile.NickName, profile.DisplayName, profile.PreferredLanguage, profile.Gender) if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M0fs", "Errors.User.Profile.NotChanged") } diff --git a/internal/command/user_human_profile_model.go b/internal/command/user_human_profile_model.go index c48625cd01..54c766f5fa 100644 --- a/internal/command/user_human_profile_model.go +++ b/internal/command/user_human_profile_model.go @@ -89,39 +89,41 @@ func (wm *HumanProfileWriteModel) Query() *eventstore.SearchQueryBuilder { func (wm *HumanProfileWriteModel) NewChangedEvent( ctx context.Context, + aggregate *eventstore.Aggregate, firstName, lastName, nickName, displayName string, preferredLanguage language.Tag, gender domain.Gender, -) (*user.HumanProfileChangedEvent, bool) { - hasChanged := false - changedEvent := user.NewHumanProfileChangedEvent(ctx, UserAggregateFromWriteModel(&wm.WriteModel)) +) (*user.HumanProfileChangedEvent, bool, error) { + changes := make([]user.ProfileChanges, 0) + var err error + if wm.FirstName != firstName { - hasChanged = true - changedEvent.FirstName = firstName + changes = append(changes, user.ChangeFirstName(firstName)) } if wm.LastName != lastName { - hasChanged = true - changedEvent.LastName = lastName + changes = append(changes, user.ChangeLastName(lastName)) } if wm.NickName != nickName { - hasChanged = true - changedEvent.NickName = &nickName + changes = append(changes, user.ChangeNickName(nickName)) } if wm.DisplayName != displayName { - hasChanged = true - changedEvent.DisplayName = &displayName + changes = append(changes, user.ChangeDisplayName(displayName)) } if wm.PreferredLanguage != preferredLanguage { - hasChanged = true - changedEvent.PreferredLanguage = &preferredLanguage + changes = append(changes, user.ChangePreferredLanguage(preferredLanguage)) } - if gender.Valid() && wm.Gender != gender { - hasChanged = true - changedEvent.Gender = &gender + if wm.Gender != gender { + changes = append(changes, user.ChangeGender(gender)) } - - return changedEvent, hasChanged + if len(changes) == 0 { + return nil, false, nil + } + changeEvent, err := user.NewHumanProfileChangedEvent(ctx, aggregate, changes) + if err != nil { + return nil, false, err + } + return changeEvent, true, nil } diff --git a/internal/command/user_human_profile_test.go b/internal/command/user_human_profile_test.go new file mode 100644 index 0000000000..6542d00c31 --- /dev/null +++ b/internal/command/user_human_profile_test.go @@ -0,0 +1,207 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestCommandSide_ChangeHumanProfile(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + address *domain.Profile + resourceOwner string + } + type res struct { + want *domain.Profile + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + address: &domain.Profile{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + FirstName: "firstname", + LastName: "lastname", + NickName: "nickname", + DisplayName: "displayname", + PreferredLanguage: language.German, + Gender: domain.GenderFemale, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "profile not changed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderFemale, + "email", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + address: &domain.Profile{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + FirstName: "firstname", + LastName: "lastname", + NickName: "nickname", + DisplayName: "displayname", + PreferredLanguage: language.German, + Gender: domain.GenderFemale, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "profile changed, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newProfileChangedEvent(context.Background(), + "user1", "org1", + "firstname2", + "lastname2", + "nickname2", + "displayname2", + language.English, + domain.GenderMale, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + address: &domain.Profile{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + FirstName: "firstname2", + LastName: "lastname2", + NickName: "nickname2", + DisplayName: "displayname2", + PreferredLanguage: language.English, + 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.English, + Gender: domain.GenderMale, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeHumanProfile(tt.args.ctx, tt.args.address) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newProfileChangedEvent(ctx context.Context, userID, resourceOwner, fistName, lastName, nickName, displayName string, lang language.Tag, gender domain.Gender) *user.HumanProfileChangedEvent { + event, _ := user.NewHumanProfileChangedEvent(ctx, + &user.NewAggregate(userID, resourceOwner).Aggregate, + []user.ProfileChanges{ + user.ChangeFirstName(fistName), + user.ChangeLastName(lastName), + user.ChangeNickName(nickName), + user.ChangeDisplayName(displayName), + user.ChangePreferredLanguage(lang), + user.ChangeGender(gender), + }, + ) + return event +} diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go new file mode 100644 index 0000000000..c27c164eb5 --- /dev/null +++ b/internal/command/user_human_test.go @@ -0,0 +1,1605 @@ +package command + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/id" + id_mock "github.com/caos/zitadel/internal/id/mock" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestCommandSide_AddHuman(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretGenerator crypto.Generator + userPasswordAlg crypto.HashAlgorithm + } + type args struct { + ctx context.Context + orgID string + human *domain.Human + } + type res struct { + want *domain.Human + err func(error) bool + } + tests := []struct { + name string + fields fields + args 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", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "org policy not found, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter(), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + 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", + }, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "org policy check failed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "email@test.ch", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "password policy not found, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + expectFilter(), + expectFilter(), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + 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", + }, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "add human (with initial code), ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + }, + 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: language.Und, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + State: domain.UserStateInitial, + }, + }, + }, + { + name: "add human (with password and initial code), ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newAddHumanEvent("password", true, ""), + ), + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + secretGenerator: GetMockSecretGenerator(t), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Password: &domain.Password{ + SecretString: "password", + }, + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + }, + 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: language.Und, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + State: domain.UserStateInitial, + }, + }, + }, + { + name: "add human email verified, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newAddHumanEvent("password", true, ""), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + secretGenerator: GetMockSecretGenerator(t), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Password: &domain.Password{ + SecretString: "password", + }, + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + }, + 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: language.Und, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + State: domain.UserStateInitial, + }, + }, + }, + { + name: "add human (with phone), ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newAddHumanEvent("", false, "+41711234567"), + ), + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1)), + }, + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Phone: &domain.Phone{ + PhoneNumber: "+41711234567", + }, + }, + }, + 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: language.Und, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Phone: &domain.Phone{ + PhoneNumber: "+41711234567", + }, + State: domain.UserStateInitial, + }, + }, + }, + { + name: "add human (with verified phone), ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newAddHumanEvent("", false, "+41711234567"), + ), + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Phone: &domain.Phone{ + PhoneNumber: "+41711234567", + IsPhoneVerified: true, + }, + }, + }, + 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: language.Und, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Phone: &domain.Phone{ + PhoneNumber: "+41711234567", + }, + State: domain.UserStateInitial, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + initializeUserCode: tt.fields.secretGenerator, + phoneVerificationCode: tt.fields.secretGenerator, + userPasswordAlg: tt.fields.userPasswordAlg, + } + got, err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RegisterHuman(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretGenerator crypto.Generator + userPasswordAlg crypto.HashAlgorithm + } + type args struct { + ctx context.Context + orgID string + human *domain.Human + externalIDP *domain.ExternalIDP + orgMemberRoles []string + } + type res struct { + want *domain.Human + err func(error) bool + } + tests := []struct { + name string + fields fields + args 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", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + }, + Password: &domain.Password{ + SecretString: "password", + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "org policy not found, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter(), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + 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", + }, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "org policy check failed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "email@test.ch", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Password: &domain.Password{ + SecretString: "password", + }, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "password policy not found, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + expectFilter(), + expectFilter(), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + 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", + }, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "add human (with password and initial code), ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newRegisterHumanEvent("password", false, ""), + ), + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + secretGenerator: GetMockSecretGenerator(t), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Password: &domain.Password{ + SecretString: "password", + }, + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + }, + }, + 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: language.Und, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + State: domain.UserStateInitial, + }, + }, + }, + { + name: "add human email verified, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newRegisterHumanEvent("password", false, ""), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + secretGenerator: GetMockSecretGenerator(t), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Password: &domain.Password{ + SecretString: "password", + }, + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + }, + 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: language.Und, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + State: domain.UserStateInitial, + }, + }, + }, + { + name: "add human (with phone), ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newRegisterHumanEvent("password", false, "+41711234567"), + ), + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1)), + }, + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + secretGenerator: GetMockSecretGenerator(t), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Phone: &domain.Phone{ + PhoneNumber: "+41711234567", + }, + Password: &domain.Password{ + SecretString: "password", + }, + }, + }, + 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: language.Und, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Phone: &domain.Phone{ + PhoneNumber: "+41711234567", + }, + State: domain.UserStateInitial, + }, + }, + }, + { + name: "add human (with verified phone), ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newRegisterHumanEvent("password", false, "+41711234567"), + ), + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + ), + ), + eventFromEventPusher( + user.NewHumanPhoneVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + secretGenerator: GetMockSecretGenerator(t), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Phone: &domain.Phone{ + PhoneNumber: "+41711234567", + IsPhoneVerified: true, + }, + Password: &domain.Password{ + SecretString: "password", + }, + }, + }, + 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: language.Und, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + }, + Phone: &domain.Phone{ + PhoneNumber: "+41711234567", + }, + State: domain.UserStateInitial, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + initializeUserCode: tt.fields.secretGenerator, + phoneVerificationCode: tt.fields.secretGenerator, + userPasswordAlg: tt.fields.userPasswordAlg, + } + got, err := r.RegisterHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.externalIDP, tt.args.orgMemberRoles) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_HumanMFASkip(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type ( + args struct { + ctx context.Context + orgID string + userID string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "skip mfa init, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanMFAInitSkippedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.HumanSkipMFAInit(tt.args.ctx, tt.args.userID, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func TestCommandSide_HumanSignOut(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type ( + args struct { + ctx context.Context + agentID string + userIDs []string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "agentid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + agentID: "", + userIDs: []string{"user1"}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "userids missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + agentID: "agent1", + userIDs: []string{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + agentID: "agent1", + userIDs: []string{"user1"}, + }, + res: res{}, + }, + { + name: "human sign out, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanSignedOutEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "agent1", + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + agentID: "agent1", + userIDs: []string{"user1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "human sign out multiple users, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user2", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanSignedOutEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "agent1", + ), + ), + eventFromEventPusher( + user.NewHumanSignedOutEvent(context.Background(), + &user.NewAggregate("user2", "org1").Aggregate, + "agent1", + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + agentID: "agent1", + userIDs: []string{"user1", "user2"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.HumansSignOut(tt.args.ctx, tt.args.agentID, tt.args.userIDs) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func newAddHumanEvent(password string, changeRequired bool, phone string) *user.HumanAddedEvent { + event := user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ) + if password != "" { + event.AddPasswordData(&crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + KeyID: "", + Crypted: []byte(password), + }, + changeRequired) + } + if phone != "" { + event.AddPhoneData(phone) + } + return event +} + +func newRegisterHumanEvent(password string, changeRequired bool, phone string) *user.HumanRegisteredEvent { + event := user.NewHumanRegisteredEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ) + if password != "" { + event.AddPasswordData(&crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + KeyID: "", + Crypted: []byte(password), + }, + changeRequired) + } + if phone != "" { + event.AddPhoneData(phone) + } + return event +} diff --git a/internal/command/user_machine.go b/internal/command/user_machine.go index 0fda97c059..3f330d8603 100644 --- a/internal/command/user_machine.go +++ b/internal/command/user_machine.go @@ -13,21 +13,18 @@ func (c *Commands) AddMachine(ctx context.Context, orgID string, machine *domain if !machine.IsValid() { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-bm9Ds", "Errors.User.Invalid") } - + orgIAMPolicy, err := c.getOrgIAMPolicy(ctx, orgID) + if err != nil { + return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-3M9fs", "Errors.Org.OrgIAMPolicy.NotFound") + } + if !orgIAMPolicy.UserLoginMustBeDomain { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6M0ds", "Errors.User.Invalid") + } userID, err := c.idGenerator.Next() if err != nil { return nil, err } machine.AggregateID = userID - - orgIAMPolicy, err := c.getOrgIAMPolicy(ctx, orgID) - if err != nil { - return nil, err - } - if !orgIAMPolicy.UserLoginMustBeDomain { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-6M0ds", "Errors.User.Invalid") - } - addedMachine := NewMachineWriteModel(machine.AggregateID, orgID) userAgg := UserAggregateFromWriteModel(&addedMachine.WriteModel) events, err := c.eventstore.PushEvents(ctx, user.NewMachineAddedEvent( @@ -58,7 +55,10 @@ func (c *Commands) ChangeMachine(ctx context.Context, machine *domain.Machine) ( } userAgg := UserAggregateFromWriteModel(&existingMachine.WriteModel) - changedEvent, hasChanged := existingMachine.NewChangedEvent(ctx, userAgg, machine.Name, machine.Description) + changedEvent, hasChanged, err := existingMachine.NewChangedEvent(ctx, userAgg, machine.Name, machine.Description) + if err != nil { + return nil, err + } if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2n8vs", "Errors.User.NotChanged") } diff --git a/internal/command/user_machine_model.go b/internal/command/user_machine_model.go index 6ed5b96a2c..5bc46c029c 100644 --- a/internal/command/user_machine_model.go +++ b/internal/command/user_machine_model.go @@ -86,16 +86,22 @@ func (wm *MachineWriteModel) NewChangedEvent( aggregate *eventstore.Aggregate, name, description string, -) (*user.MachineChangedEvent, bool) { - hasChanged := false - changedEvent := user.NewMachineChangedEvent(ctx, aggregate) +) (*user.MachineChangedEvent, bool, error) { + changes := make([]user.MachineChanges, 0) + var err error + if wm.Name != name { - hasChanged = true - changedEvent.Name = &name + changes = append(changes, user.ChangeName(name)) } if wm.Description != description { - hasChanged = true - changedEvent.Description = &description + changes = append(changes, user.ChangeDescription(description)) } - return changedEvent, hasChanged + if len(changes) == 0 { + return nil, false, nil + } + changeEvent, err := user.NewMachineChangedEvent(ctx, aggregate, changes) + if err != nil { + return nil, false, err + } + return changeEvent, true, nil } diff --git a/internal/command/user_machine_test.go b/internal/command/user_machine_test.go new file mode 100644 index 0000000000..2e9e0a83cd --- /dev/null +++ b/internal/command/user_machine_test.go @@ -0,0 +1,350 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/id" + id_mock "github.com/caos/zitadel/internal/id/mock" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestCommandSide_AddMachine(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + } + type args struct { + ctx context.Context + orgID string + machine *domain.Machine + } + type res struct { + want *domain.Machine + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "user invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &domain.Machine{ + Username: "username", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "org policy not found, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &domain.Machine{ + Username: "username", + Name: "name", + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "org policy global, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + false, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &domain.Machine{ + Username: "username", + Name: "name", + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "add machine, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + ), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &domain.Machine{ + Username: "username", + Description: "description", + Name: "name", + }, + }, + res: res{ + want: &domain.Machine{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Name: "name", + Description: "description", + State: domain.UserStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + } + got, err := r.AddMachine(tt.args.ctx, tt.args.orgID, tt.args.machine) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ChangeMachine(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + orgID string + machine *domain.Machine + } + type res struct { + want *domain.Machine + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "user invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &domain.Machine{ + Username: "username", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &domain.Machine{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + Username: "username", + Name: "name", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &domain.Machine{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + Name: "name", + Description: "description", + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change machine, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newMachineChangedEvent(context.Background(), "user1", "org1", "name1", "description1"), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &domain.Machine{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + }, + Description: "description1", + Name: "name1", + }, + }, + res: res{ + want: &domain.Machine{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Name: "name1", + Description: "description1", + State: domain.UserStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeMachine(tt.args.ctx, tt.args.machine) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func newMachineChangedEvent(ctx context.Context, userID, resourceOwner, name, description string) *user.MachineChangedEvent { + event, _ := user.NewMachineChangedEvent(ctx, + &user.NewAggregate(userID, resourceOwner).Aggregate, + []user.MachineChanges{ + user.ChangeName(name), + user.ChangeDescription(description), + }, + ) + return event +} diff --git a/internal/command/user_test.go b/internal/command/user_test.go new file mode 100644 index 0000000000..061f71c2fc --- /dev/null +++ b/internal/command/user_test.go @@ -0,0 +1,1275 @@ +package command + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/id" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestCommandSide_UsernameChange(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type ( + args struct { + ctx context.Context + orgID string + userID string + username string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "", + username: "username", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "orgid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "", + userID: "user1", + username: "username", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "username missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + username: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + username: "username", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "username not changed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + username: "username", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "org iam policy not found, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + username: "username", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "invalid username, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter(), + expectFilter( + eventFromEventPusher( + iam.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + username: "test@test.ch", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change username, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter(), + expectFilter( + eventFromEventPusher( + iam.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewUsernameChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "username1", + true, + ), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewRemoveUsernameUniqueConstraint("username", "org1", true)), + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username1", "org1", true)), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + username: "username1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeUsername(tt.args.ctx, tt.args.orgID, tt.args.userID, tt.args.username) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_DeactivateUser(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type ( + args struct { + ctx context.Context + orgID string + userID string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "user already inactive, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewUserDeactivatedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "deactivate user, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewUserDeactivatedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.DeactivateUser(tt.args.ctx, tt.args.userID, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_ReactivateUser(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type ( + args struct { + ctx context.Context + orgID string + userID string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "user already active, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "reactivate user, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewUserDeactivatedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewUserReactivatedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ReactivateUser(tt.args.ctx, tt.args.userID, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_LockUser(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type ( + args struct { + ctx context.Context + orgID string + userID string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "user already locked, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewUserLockedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "lock user, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewUserLockedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.LockUser(tt.args.ctx, tt.args.userID, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_UnlockUser(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type ( + args struct { + ctx context.Context + orgID string + userID string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "user already active, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "unlock user, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewUserLockedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewUserUnlockedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.UnlockUser(tt.args.ctx, tt.args.userID, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveUser(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type ( + args struct { + ctx context.Context + orgID string + userID string + } + ) + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "org iam policy not found, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter(), + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "remove user, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter(), + expectFilter( + eventFromEventPusher( + iam.NewOrgIAMPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewUserRemovedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + true, + ), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewRemoveUsernameUniqueConstraint("username", "org1", true)), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveUser(tt.args.ctx, tt.args.userID, tt.args.orgID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_AddUserToken(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + } + type ( + args struct { + ctx context.Context + orgID string + agentID string + clientID string + userID string + audience []string + scopes []string + lifetime time.Duration + } + ) + type res struct { + want *domain.Token + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "orgid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "", + userID: "user1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + } + got, err := r.AddUserToken(tt.args.ctx, tt.args.orgID, tt.args.agentID, tt.args.clientID, tt.args.userID, tt.args.audience, tt.args.scopes, tt.args.lifetime) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_UserDomainClaimedSent(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userID string + resourceOwner string + } + type res struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "userid missing, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "code sent, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewDomainClaimedSentEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.UserDomainClaimedSent(tt.args.ctx, tt.args.resourceOwner, tt.args.userID) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} diff --git a/internal/domain/factors.go b/internal/domain/factors.go index 7965ae7e37..1f8f0ec41d 100644 --- a/internal/domain/factors.go +++ b/internal/domain/factors.go @@ -6,6 +6,8 @@ const ( SecondFactorTypeUnspecified SecondFactorType = iota SecondFactorTypeOTP SecondFactorTypeU2F + + secondFactorCount ) type MultiFactorType int32 @@ -13,6 +15,8 @@ type MultiFactorType int32 const ( MultiFactorTypeUnspecified MultiFactorType = iota MultiFactorTypeU2FWithPIN + + multiFactorCount ) type FactorState int32 @@ -25,6 +29,14 @@ const ( factorStateCount ) +func (f SecondFactorType) Valid() bool { + return f > 0 && f < secondFactorCount +} + +func (f MultiFactorType) Valid() bool { + return f > 0 && f < multiFactorCount +} + func (f FactorState) Valid() bool { return f >= 0 && f < factorStateCount } diff --git a/internal/domain/human.go b/internal/domain/human.go index 27a63a3d88..5d10980b41 100644 --- a/internal/domain/human.go +++ b/internal/domain/human.go @@ -18,12 +18,7 @@ type Human struct { *Email *Phone *Address - ExternalIDPs []*ExternalIDP - OTP *OTP - U2FTokens []*WebAuthNToken - PasswordlessTokens []*WebAuthNToken - U2FLogins []*WebAuthNLogin - PasswordlessLogins []*WebAuthNLogin + ExternalIDPs []*ExternalIDP } func (h Human) GetUsername() string { @@ -57,7 +52,7 @@ func (f Gender) Valid() bool { } func (u *Human) IsValid() bool { - return u.Profile != nil && u.FirstName != "" && u.LastName != "" && u.Email != nil && u.Email.IsValid() && u.Phone == nil || (u.Phone != nil && u.Phone.PhoneNumber != "" && u.Phone.IsValid()) + return u.Profile != nil && u.Profile.IsValid() && u.Email != nil && u.Email.IsValid() && u.Phone == nil || (u.Phone != nil && u.Phone.PhoneNumber != "" && u.Phone.IsValid()) } func (u *Human) CheckOrgIAMPolicy(policy *OrgIAMPolicy) error { diff --git a/internal/domain/human_email.go b/internal/domain/human_email.go index bd0a55a4c3..097e056666 100644 --- a/internal/domain/human_email.go +++ b/internal/domain/human_email.go @@ -3,9 +3,12 @@ package domain import ( "github.com/caos/zitadel/internal/crypto" es_models "github.com/caos/zitadel/internal/eventstore/v1/models" + "regexp" "time" ) +var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") + type Email struct { es_models.ObjectRoot @@ -21,7 +24,7 @@ type EmailCode struct { } func (e *Email) IsValid() bool { - return e.EmailAddress != "" + return e.EmailAddress != "" && emailRegex.MatchString(e.EmailAddress) } func NewEmailCode(emailGenerator crypto.Generator) (*EmailCode, error) { diff --git a/internal/domain/human_email_test.go b/internal/domain/human_email_test.go new file mode 100644 index 0000000000..77474252c3 --- /dev/null +++ b/internal/domain/human_email_test.go @@ -0,0 +1,74 @@ +package domain + +import ( + "testing" +) + +func TestEmailValid(t *testing.T) { + type args struct { + email *Email + } + tests := []struct { + name string + args args + result bool + }{ + { + name: "empty email, invalid", + args: args{ + email: &Email{}, + }, + result: false, + }, + { + name: "only letters email, invalid", + args: args{ + email: &Email{EmailAddress: "testemail"}, + }, + result: false, + }, + { + name: "nothing after @, invalid", + args: args{ + email: &Email{EmailAddress: "testemail@"}, + }, + result: false, + }, + { + name: "email, valid", + args: args{ + email: &Email{EmailAddress: "testemail@gmail.com"}, + }, + result: true, + }, + { + name: "email, valid", + args: args{ + email: &Email{EmailAddress: "test.email@gmail.com"}, + }, + result: true, + }, + { + name: "email, valid", + args: args{ + email: &Email{EmailAddress: "test/email@gmail.com"}, + }, + result: true, + }, + { + name: "email, valid", + args: args{ + email: &Email{EmailAddress: "test/email@gmail.com"}, + }, + result: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.args.email.IsValid() + if result != tt.result { + t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, result) + } + }) + } +} diff --git a/internal/domain/human_external_idp.go b/internal/domain/human_external_idp.go index 839453a5dc..ebd68ff1d1 100644 --- a/internal/domain/human_external_idp.go +++ b/internal/domain/human_external_idp.go @@ -11,7 +11,7 @@ type ExternalIDP struct { } func (idp *ExternalIDP) IsValid() bool { - return idp.AggregateID != "" && idp.IDPConfigID != "" && idp.ExternalUserID != "" + return idp.IDPConfigID != "" && idp.ExternalUserID != "" } type ExternalIDPState int32 diff --git a/internal/domain/human_phone.go b/internal/domain/human_phone.go index 375718f3be..b349322096 100644 --- a/internal/domain/human_phone.go +++ b/internal/domain/human_phone.go @@ -34,7 +34,7 @@ func (p *Phone) IsValid() bool { func (p *Phone) formatPhone() error { phoneNr, err := libphonenumber.Parse(p.PhoneNumber, defaultRegion) if err != nil { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-so0wa", "Errors.User.Phone.Invalid") + return caos_errs.ThrowInvalidArgument(nil, "EVENT-so0wa", "Errors.User.Phone.Invalid") } p.PhoneNumber = libphonenumber.Format(phoneNr, libphonenumber.E164) return nil diff --git a/internal/domain/human_phone_test.go b/internal/domain/human_phone_test.go new file mode 100644 index 0000000000..d807ff6895 --- /dev/null +++ b/internal/domain/human_phone_test.go @@ -0,0 +1,107 @@ +package domain + +import ( + "testing" + + caos_errs "github.com/caos/zitadel/internal/errors" +) + +func TestFormatPhoneNumber(t *testing.T) { + type args struct { + phone *Phone + } + tests := []struct { + name string + args args + result *Phone + errFunc func(err error) bool + }{ + { + name: "invalid phone number", + args: args{ + phone: &Phone{ + PhoneNumber: "PhoneNumber", + }, + }, + errFunc: caos_errs.IsErrorInvalidArgument, + }, + { + name: "format phone 071...", + args: args{ + phone: &Phone{ + PhoneNumber: "0711234567", + }, + }, + result: &Phone{ + PhoneNumber: "+41711234567", + }, + }, + { + name: "format phone 0041...", + args: args{ + phone: &Phone{ + PhoneNumber: "0041711234567", + }, + }, + result: &Phone{ + PhoneNumber: "+41711234567", + }, + }, + { + name: "format phone 071 xxx xx xx", + args: args{ + phone: &Phone{ + PhoneNumber: "071 123 45 67", + }, + }, + result: &Phone{ + PhoneNumber: "+41711234567", + }, + }, + { + name: "format phone +4171 xxx xx xx", + args: args{ + phone: &Phone{ + PhoneNumber: "+4171 123 45 67", + }, + }, + result: &Phone{ + PhoneNumber: "+41711234567", + }, + }, + { + name: "format phone 004171 xxx xx xx", + args: args{ + phone: &Phone{ + PhoneNumber: "004171 123 45 67", + }, + }, + result: &Phone{ + PhoneNumber: "+41711234567", + }, + }, + { + name: "format non swiss phone 004371 xxx xx xx", + args: args{ + phone: &Phone{ + PhoneNumber: "004371 123 45 67", + }, + }, + result: &Phone{ + PhoneNumber: "+43711234567", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.phone.formatPhone() + + if tt.errFunc == nil && tt.result.PhoneNumber != tt.args.phone.PhoneNumber { + t.Errorf("got wrong result: expected: %v, actual: %v ", tt.args.phone.PhoneNumber, tt.result.PhoneNumber) + } + if tt.errFunc != nil && !tt.errFunc(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} diff --git a/internal/domain/machine.go b/internal/domain/machine.go index 519061530c..f9a92be250 100644 --- a/internal/domain/machine.go +++ b/internal/domain/machine.go @@ -20,5 +20,5 @@ func (m Machine) GetState() UserState { } func (sa *Machine) IsValid() bool { - return sa.Name != "" + return sa.Name != "" && sa.Username != "" } diff --git a/internal/domain/org_domain.go b/internal/domain/org_domain.go index bc85388563..765c5033e6 100644 --- a/internal/domain/org_domain.go +++ b/internal/domain/org_domain.go @@ -17,7 +17,7 @@ type OrgDomain struct { } func (domain *OrgDomain) IsValid() bool { - return domain.AggregateID != "" && domain.Domain != "" + return domain.Domain != "" } func (domain *OrgDomain) GenerateVerificationCode(codeGenerator crypto.Generator) (string, error) { diff --git a/internal/domain/policy.go b/internal/domain/policy.go index c5bb817687..457152f3c3 100644 --- a/internal/domain/policy.go +++ b/internal/domain/policy.go @@ -13,3 +13,7 @@ const ( func (f PolicyState) Valid() bool { return f >= 0 && f < policyStateCount } + +func (s PolicyState) Exists() bool { + return s != PolicyStateUnspecified && s != PolicyStateRemoved +} diff --git a/internal/domain/policy_login.go b/internal/domain/policy_login.go index 6cf5e989d6..dd6c57a0e5 100644 --- a/internal/domain/policy_login.go +++ b/internal/domain/policy_login.go @@ -27,6 +27,10 @@ type IDPProvider struct { IDPState IDPConfigState } +func (p IDPProvider) IsValid() bool { + return p.IDPConfigID != "" +} + type PasswordlessType int32 const ( diff --git a/internal/domain/user.go b/internal/domain/user.go index 3e9e013f1b..132d7a0e34 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -22,3 +22,7 @@ const ( func (f UserState) Valid() bool { return f >= 0 && f < userStateCount } + +func (s UserState) Exists() bool { + return s != UserStateUnspecified && s != UserStateDeleted +} diff --git a/internal/iam/repository/view/idp_provider_view.go b/internal/iam/repository/view/idp_provider_view.go index a11d12fc00..54f59a8226 100644 --- a/internal/iam/repository/view/idp_provider_view.go +++ b/internal/iam/repository/view/idp_provider_view.go @@ -16,7 +16,7 @@ func GetIDPProviderByAggregateIDAndConfigID(db *gorm.DB, table, aggregateID, idp query := repository.PrepareGetByQuery(table, aggIDQuery, idpConfigIDQuery) err := query(db, policy) if caos_errs.IsNotFound(err) { - return nil, caos_errs.ThrowNotFound(nil, "VIEW-Skvi8", "Errors.IAM.LoginPolicy.IdpProviderNotExisting") + return nil, caos_errs.ThrowNotFound(nil, "VIEW-Skvi8", "Errors.IAM.LoginPolicy.IDP.NotExisting") } return policy, err } diff --git a/internal/iam/repository/view/idp_view.go b/internal/iam/repository/view/idp_view.go index b4cd914a55..3687d4e59e 100644 --- a/internal/iam/repository/view/idp_view.go +++ b/internal/iam/repository/view/idp_view.go @@ -15,7 +15,7 @@ func IDPByID(db *gorm.DB, table, idpID string) (*model.IDPConfigView, error) { query := repository.PrepareGetByQuery(table, idpIDQuery) err := query(db, idp) if caos_errs.IsNotFound(err) { - return nil, caos_errs.ThrowNotFound(nil, "VIEW-Ahq2s", "Errors.IAM.IdpNotExisting") + return nil, caos_errs.ThrowNotFound(nil, "VIEW-Ahq2s", "Errors.IDP.NotExisting") } return idp, err } diff --git a/internal/management/repository/eventsourcing/handler/user_external_idps.go b/internal/management/repository/eventsourcing/handler/user_external_idps.go index 14f10f680d..0a8ae55105 100644 --- a/internal/management/repository/eventsourcing/handler/user_external_idps.go +++ b/internal/management/repository/eventsourcing/handler/user_external_idps.go @@ -184,7 +184,7 @@ func (i *ExternalIDP) getOrgIDPConfig(ctx context.Context, aggregateID, idpConfi if _, i := existing.GetIDP(idpConfigID); i != nil { return i, nil } - return nil, caos_errs.ThrowNotFound(nil, "EVENT-22n7G", "Errors.Org.IdpNotExisting") + return nil, caos_errs.ThrowNotFound(nil, "EVENT-22n7G", "Errors.IDP.NotExisting") } func (i *ExternalIDP) getOrgByID(ctx context.Context, orgID string) (*org_model.Org, error) { diff --git a/internal/repository/iam/policy_login_identity_provider.go b/internal/repository/iam/policy_login_identity_provider.go index a09b598fe5..766846fb0e 100644 --- a/internal/repository/iam/policy_login_identity_provider.go +++ b/internal/repository/iam/policy_login_identity_provider.go @@ -23,7 +23,6 @@ func NewIdentityProviderAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate, idpConfigID string, - idpProviderType domain.IdentityProviderType, ) *IdentityProviderAddedEvent { return &IdentityProviderAddedEvent{ @@ -33,7 +32,7 @@ func NewIdentityProviderAddedEvent( aggregate, LoginPolicyIDPProviderAddedEventType), idpConfigID, - idpProviderType), + domain.IdentityProviderTypeSystem), } } diff --git a/internal/repository/org/domain.go b/internal/repository/org/domain.go index aea166ae7b..5e694116d4 100644 --- a/internal/repository/org/domain.go +++ b/internal/repository/org/domain.go @@ -247,14 +247,15 @@ func (e *DomainRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstr return []*eventstore.EventUniqueConstraint{NewRemoveOrgDomainUniqueConstraint(e.Domain)} } -func NewDomainRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate, domain string) *DomainRemovedEvent { +func NewDomainRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate, domain string, verified bool) *DomainRemovedEvent { return &DomainRemovedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, aggregate, OrgDomainRemovedEventType, ), - Domain: domain, + Domain: domain, + isVerified: verified, } } diff --git a/internal/repository/user/human_address.go b/internal/repository/user/human_address.go index 934609b5ab..c902f80948 100644 --- a/internal/repository/user/human_address.go +++ b/internal/repository/user/human_address.go @@ -32,14 +32,57 @@ func (e *HumanAddressChangedEvent) UniqueConstraints() []*eventstore.EventUnique return nil } -func NewHumanAddressChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanAddressChangedEvent { - return &HumanAddressChangedEvent{ +func NewAddressChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + changes []AddressChanges, +) (*HumanAddressChangedEvent, error) { + if len(changes) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "USER-3n8fs", "Errors.NoChangesFound") + } + changeEvent := &HumanAddressChangedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, aggregate, HumanAddressChangedType, ), } + for _, change := range changes { + change(changeEvent) + } + return changeEvent, nil +} + +type AddressChanges func(event *HumanAddressChangedEvent) + +func ChangeCountry(country string) func(event *HumanAddressChangedEvent) { + return func(e *HumanAddressChangedEvent) { + e.Country = &country + } +} + +func ChangeLocality(locality string) func(event *HumanAddressChangedEvent) { + return func(e *HumanAddressChangedEvent) { + e.Locality = &locality + } +} + +func ChangePostalCode(code string) func(event *HumanAddressChangedEvent) { + return func(e *HumanAddressChangedEvent) { + e.PostalCode = &code + } +} + +func ChangeRegion(region string) func(event *HumanAddressChangedEvent) { + return func(e *HumanAddressChangedEvent) { + e.Region = ®ion + } +} + +func ChangeStreetAddress(street string) func(event *HumanAddressChangedEvent) { + return func(e *HumanAddressChangedEvent) { + e.StreetAddress = &street + } } func HumanAddressChangedEventMapper(event *repository.Event) (eventstore.EventReader, error) { diff --git a/internal/repository/user/human_email.go b/internal/repository/user/human_email.go index 7765c5dc67..07e4955394 100644 --- a/internal/repository/user/human_email.go +++ b/internal/repository/user/human_email.go @@ -34,13 +34,14 @@ func (e *HumanEmailChangedEvent) UniqueConstraints() []*eventstore.EventUniqueCo return nil } -func NewHumanEmailChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanEmailChangedEvent { +func NewHumanEmailChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, emailAddress string) *HumanEmailChangedEvent { return &HumanEmailChangedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, aggregate, HumanEmailChangedType, ), + EmailAddress: emailAddress, } } diff --git a/internal/repository/user/human_phone.go b/internal/repository/user/human_phone.go index 1643a6ebc3..9204cb8e4e 100644 --- a/internal/repository/user/human_phone.go +++ b/internal/repository/user/human_phone.go @@ -35,13 +35,14 @@ func (e *HumanPhoneChangedEvent) UniqueConstraints() []*eventstore.EventUniqueCo return nil } -func NewHumanPhoneChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanPhoneChangedEvent { +func NewHumanPhoneChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, phone string) *HumanPhoneChangedEvent { return &HumanPhoneChangedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, aggregate, HumanPhoneChangedType, ), + PhoneNumber: phone, } } diff --git a/internal/repository/user/human_profile.go b/internal/repository/user/human_profile.go index 2f5f8f9629..1ee977ec32 100644 --- a/internal/repository/user/human_profile.go +++ b/internal/repository/user/human_profile.go @@ -35,14 +35,63 @@ func (e *HumanProfileChangedEvent) UniqueConstraints() []*eventstore.EventUnique return nil } -func NewHumanProfileChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanProfileChangedEvent { - return &HumanProfileChangedEvent{ +func NewHumanProfileChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + changes []ProfileChanges, +) (*HumanProfileChangedEvent, error) { + if len(changes) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "USER-33n8F", "Errors.NoChangesFound") + } + changeEvent := &HumanProfileChangedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, aggregate, HumanProfileChangedType, ), } + for _, change := range changes { + change(changeEvent) + } + return changeEvent, nil +} + +type ProfileChanges func(event *HumanProfileChangedEvent) + +func ChangeFirstName(firstName string) func(event *HumanProfileChangedEvent) { + return func(e *HumanProfileChangedEvent) { + e.FirstName = firstName + } +} + +func ChangeLastName(lastName string) func(event *HumanProfileChangedEvent) { + return func(e *HumanProfileChangedEvent) { + e.LastName = lastName + } +} + +func ChangeNickName(nickName string) func(event *HumanProfileChangedEvent) { + return func(e *HumanProfileChangedEvent) { + e.NickName = &nickName + } +} + +func ChangeDisplayName(displayName string) func(event *HumanProfileChangedEvent) { + return func(e *HumanProfileChangedEvent) { + e.DisplayName = &displayName + } +} + +func ChangePreferredLanguage(language language.Tag) func(event *HumanProfileChangedEvent) { + return func(e *HumanProfileChangedEvent) { + e.PreferredLanguage = &language + } +} + +func ChangeGender(gender domain.Gender) func(event *HumanProfileChangedEvent) { + return func(e *HumanProfileChangedEvent) { + e.Gender = &gender + } } func HumanProfileChangedEventMapper(event *repository.Event) (eventstore.EventReader, error) { diff --git a/internal/repository/user/machine.go b/internal/repository/user/machine.go index 9e067cb327..3b23304d54 100644 --- a/internal/repository/user/machine.go +++ b/internal/repository/user/machine.go @@ -19,7 +19,7 @@ type MachineAddedEvent struct { eventstore.BaseEvent `json:"-"` UserName string `json:"userName"` - UserLoginMustBeDomain bool + userLoginMustBeDomain bool `json:"-"` Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` @@ -30,7 +30,7 @@ func (e *MachineAddedEvent) Data() interface{} { } func (e *MachineAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { - return []*eventstore.EventUniqueConstraint{NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.UserLoginMustBeDomain)} + return []*eventstore.EventUniqueConstraint{NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.userLoginMustBeDomain)} } func NewMachineAddedEvent( @@ -50,7 +50,7 @@ func NewMachineAddedEvent( UserName: userName, Name: name, Description: description, - UserLoginMustBeDomain: userLoginMustBeDomain, + userLoginMustBeDomain: userLoginMustBeDomain, } } @@ -86,14 +86,36 @@ func (e *MachineChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConst func NewMachineChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, -) *MachineChangedEvent { - return &MachineChangedEvent{ + changes []MachineChanges, +) (*MachineChangedEvent, error) { + if len(changes) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "USER-3M9fs", "Errors.NoChangesFound") + } + changeEvent := &MachineChangedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, aggregate, MachineChangedEventType, ), } + for _, change := range changes { + change(changeEvent) + } + return changeEvent, nil +} + +type MachineChanges func(event *MachineChangedEvent) + +func ChangeName(name string) func(event *MachineChangedEvent) { + return func(e *MachineChangedEvent) { + e.Name = &name + } +} + +func ChangeDescription(description string) func(event *MachineChangedEvent) { + return func(e *MachineChangedEvent) { + e.Description = &description + } } func MachineChangedEventMapper(event *repository.Event) (eventstore.EventReader, error) { diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index ccd990f144..186b173d6f 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -3,6 +3,7 @@ Errors: NoChangesFound: Keine Änderungen gefunden OriginNotAllowed: Dieser "Origin" ist nicht freigeschaltet IDMissing: ID fehlt + ResourceOwnerMissing: Organisation fehlt User: NotFound: Benutzer konnte nicht gefunden werden AlreadyExists: Benutzer existierts bereits @@ -225,14 +226,11 @@ Errors: IdpIsNotOIDC: IDP Konfiguration ist nicht vom Typ OIDC LoginPolicyInvalid: Login Policy ist ungültig LoginPolicyNotExisting: Login Policy nicht vorhanden - IdpProviderInvalid: Idp Provider ist ungültig LoginPolicy: NotFound: Default Login Policy konnte nicht gefunden NotChanged: Default Login Policy wurde nicht verändert NotExisting: Default Login Policy existiert nicht AlreadyExists: Default Login Policy existiert bereits - IdpProviderAlreadyExisting: Idp Provider existiert bereits - IdpProviderNotExisting: Idp Provider existiert nicht MFA: AlreadyExists: Multifaktor existiert bereits NotExisting: Multifaktor existiert nicht @@ -240,9 +238,9 @@ Errors: IDP: AlreadyExists: Identitäts Provider existiert bereits NotExisting: Identitäts Provider existiert nicht + Invalid: Idp Provider ist ungültig IDPConfig: AlreadyExists: Identitäts Provider Konfiguration existiert bereits - NotExisting: Identitäts Provider Konfiguration existiert nicht NotInactive: Identitäts Provider Konfiguration nicht inaktive NotActive: Identitäts Provider Konfiguration nicht aktive LabelPolicy: @@ -298,6 +296,7 @@ Errors: AlreadyExists: Member existiert bereits IDPConfig: AlreadyExists: IDP Konfiguration mit diesem Name existiert bereits + NotExisting: Identitäts Provider Konfiguration existiert nicht Changes: NotFound: Es konnte kein Änderungsverlauf gefunden werden Token: diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 8723fd5a55..620e4fe1dc 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -3,6 +3,7 @@ Errors: NoChangesFound: No changes OriginNotAllowed: This "Origin" is not allowed IDMissing: ID missing + ResourceOwnerMissing: Resource Owner Organisation missing User: NotFound: User could not be found AlreadyExists: User already exists @@ -231,15 +232,16 @@ Errors: NotChanged: Default Login Policy has not been changed NotExisting: Default Login Policy not existig AlreadyExists: Default Login Policy already exists - IdpProviderAlreadyExisting: Idp Provider already existing - IdpProviderNotExisting: Idp Provider not existing MFA: AlreadyExists: Multifactor already exists NotExisting: Multifactor not existing Unspecified: Multifactor invalid + IDP: + AlreadyExists: Identity provider already exists + NotExisting: Identity provider doesn't exist + Invalid: Identity Provider invalid IDPConfig: AlreadyExists: Identity Provider Configuration already exists - NotExisting: Identity Provider Configuration doesn't exist NotInactive: Identity Provider Configuration not inactive NotActive: Identity Provider Configuration not active LabelPolicy: @@ -295,6 +297,7 @@ Errors: AlreadyExists: Member already exists IDPConfig: AlreadyExists: IDP Configuration with this name already exists + NotExisting: Identity Provider Configuration doesn't exist Changes: NotFound: No history found Token: diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 99f7976316..c0e5fa8046 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -2922,6 +2922,7 @@ message ListLoginPolicyIDPsResponse { message AddIDPToLoginPolicyRequest { string idp_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + zitadel.idp.v1.IDPOwnerType ownerType = 2 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; } message AddIDPToLoginPolicyResponse {