From b0b1e94090e6d3a5ebf4b0d800a4aa8671d3abbe Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Mon, 17 Oct 2022 21:19:15 +0200 Subject: [PATCH] feat(login): additionally use email/phone for authentication (#4563) * feat: add ability to disable login by email and phone * feat: check login by email and phone * fix: set verified email / phone correctly on notify users * update projection version * fix merge * fix email/phone verified reduce tests * fix user tests * loginname check * cleanup * fix: update user projection version to handle fixed statement --- .../login-policy/login-policy.component.html | 44 +++ .../login-policy/login-policy.component.ts | 6 + console/src/assets/i18n/de.json | 2 + console/src/assets/i18n/en.json | 2 + console/src/assets/i18n/fr.json | 2 + console/src/assets/i18n/it.json | 2 + console/src/assets/i18n/zh.json | 2 + docs/docs/apis/proto/admin.md | 2 + docs/docs/apis/proto/management.md | 4 + docs/docs/apis/proto/policy.md | 2 + internal/api/grpc/admin/import.go | 2 +- internal/api/grpc/admin/login_policy.go | 4 +- .../api/grpc/admin/login_policy_converter.go | 8 +- internal/api/grpc/management/policy_login.go | 8 +- .../grpc/management/policy_login_converter.go | 26 +- internal/api/grpc/policy/login_policy.go | 2 + .../eventsourcing/eventstore/auth_request.go | 84 +++++- .../repository/eventsourcing/view/user.go | 44 ++- internal/command/command.go | 27 ++ internal/command/idp_config_model.go | 4 + internal/command/instance.go | 4 + internal/command/instance_policy_login.go | 91 +++--- .../command/instance_policy_login_model.go | 10 +- .../command/instance_policy_login_test.go | 63 +++-- internal/command/org_policy_login.go | 264 +++++++++++------- internal/command/org_policy_login_model.go | 10 +- internal/command/org_policy_login_test.go | 219 ++++++--------- internal/command/policy_login_model.go | 14 + internal/command/user_human_password_test.go | 12 + internal/command/user_human_test.go | 16 ++ internal/domain/idp_config.go | 2 +- internal/domain/policy_login.go | 2 + internal/eventstore/handler/crdb/statement.go | 22 +- .../eventstore/handler/crdb/statement_test.go | 26 ++ internal/notification/projection.go | 12 +- internal/query/iam_member_test.go | 20 +- internal/query/login_policy.go | 14 + internal/query/login_policy_test.go | 182 ++++++------ internal/query/org_member_test.go | 20 +- internal/query/project_grant_member_test.go | 20 +- internal/query/project_member_test.go | 20 +- internal/query/projection/login_policy.go | 14 +- .../query/projection/login_policy_test.go | 42 ++- internal/query/projection/user.go | 44 +-- internal/query/projection/user_test.go | 154 +++++----- internal/query/user.go | 62 +++- internal/query/user_grant_test.go | 40 +-- internal/query/user_test.go | 260 ++++++++--------- internal/repository/instance/policy_login.go | 6 +- internal/repository/org/policy_login.go | 9 +- internal/repository/policy/login.go | 22 +- proto/zitadel/admin.proto | 10 + proto/zitadel/management.proto | 20 ++ proto/zitadel/policy.proto | 10 + 54 files changed, 1245 insertions(+), 768 deletions(-) diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.html b/console/src/app/modules/policies/login-policy/login-policy.component.html index 003f6464f8..82a0064833 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.html +++ b/console/src/app/modules/policies/login-policy/login-policy.component.html @@ -329,6 +329,50 @@ +
+ +
+ +
+ +
+
{{ 'POLICY.DATA.DEFAULTREDIRECTURI' | translate }} diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.ts b/console/src/app/modules/policies/login-policy/login-policy.component.ts index 40ff25d8dd..dfea830837 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.ts +++ b/console/src/app/modules/policies/login-policy/login-policy.component.ts @@ -155,6 +155,8 @@ export class LoginPolicyComponent implements OnInit { mgmtreq.setHidePasswordReset(this.loginData.hidePasswordReset); mgmtreq.setMultiFactorsList(this.loginData.multiFactorsList); mgmtreq.setSecondFactorsList(this.loginData.secondFactorsList); + mgmtreq.setDisableLoginWithEmail(this.loginData.disableLoginWithEmail); + mgmtreq.setDisableLoginWithPhone(this.loginData.disableLoginWithPhone); const pcl = new Duration().setSeconds((this.passwordCheckLifetime?.value ?? 240) * 60 * 60); mgmtreq.setPasswordCheckLifetime(pcl); @@ -184,6 +186,8 @@ export class LoginPolicyComponent implements OnInit { mgmtreq.setForceMfa(this.loginData.forceMfa); mgmtreq.setPasswordlessType(this.loginData.passwordlessType); mgmtreq.setHidePasswordReset(this.loginData.hidePasswordReset); + mgmtreq.setDisableLoginWithEmail(this.loginData.disableLoginWithEmail); + mgmtreq.setDisableLoginWithPhone(this.loginData.disableLoginWithPhone); const pcl = new Duration().setSeconds((this.passwordCheckLifetime?.value ?? 240) * 60 * 60); mgmtreq.setPasswordCheckLifetime(pcl); @@ -214,6 +218,8 @@ export class LoginPolicyComponent implements OnInit { adminreq.setForceMfa(this.loginData.forceMfa); adminreq.setPasswordlessType(this.loginData.passwordlessType); adminreq.setHidePasswordReset(this.loginData.hidePasswordReset); + adminreq.setDisableLoginWithEmail(this.loginData.disableLoginWithEmail); + adminreq.setDisableLoginWithPhone(this.loginData.disableLoginWithPhone); const admin_pcl = new Duration().setSeconds((this.passwordCheckLifetime?.value ?? 240) * 60 * 60); adminreq.setPasswordCheckLifetime(admin_pcl); diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 0264e18294..abfff4ca04 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -1173,6 +1173,8 @@ "IGNOREUNKNOWNUSERNAMES_DESC": "Ist die Option gewählt, wird der Passwort Schritt im Login auch angezeigt wenn der User nicht gefunden wurde. Dem Benutzer wird auf bei der Passwortprüfung nicht angezeigt ob der Username oder das Passwort falsch war.", "ALLOWDOMAINDISCOVERY": "Domänenentdeckung erlauben", "ALLOWDOMAINDISCOVERY_DESC": "Ist die Option gewählt, wird die Endung (@domain.com) eines unbekannten Benutzernamens im Login mit den Organisationsdomänen verglichen. Bei Übereinstimmung wird der Benutzer auf die Registrierung dieser Organisation weitergeleitet.", + "DISABLELOGINWITHEMAIL": "Login mittels E-Mailadresse deaktivieren", + "DISABLELOGINWITHPHONE": "Login mittels Telefonnummer deaktivieren", "DEFAULTREDIRECTURI": "Default Redirect URI", "DEFAULTREDIRECTURI_DESC": "Definiert, wohin der Benutzer umgeleitet wird, wenn die Anmeldung ohne App-Kontext gestartet wurde (z. B. von Mail)", "ERRORMSGPOPUP": "Fehler als Dialog Fenster", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 5c54b2f022..bf4984571d 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1173,6 +1173,8 @@ "IGNOREUNKNOWNUSERNAMES_DESC": "If the option is selected, the password screen will be displayed in the login process even if the user was not found. The error on the password check will not disclose if the username or password was wrong.", "ALLOWDOMAINDISCOVERY": "Domain discovery allowed", "ALLOWDOMAINDISCOVERY_DESC": "If the option is selected, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the organization domains and will redirect to the registration of that organisation on success.", + "DISABLELOGINWITHEMAIL": "Disable login with email address", + "DISABLELOGINWITHPHONE": "Disable login with phone number", "DEFAULTREDIRECTURI": "Default Redirect URI", "DEFAULTREDIRECTURI_DESC": "Defines where the user will be redirected to if the login has started without an app context (e.g. from mail)", "ERRORMSGPOPUP": "Show Error in Dialog", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index de409e9501..c37ddf1743 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -1173,6 +1173,8 @@ "IGNOREUNKNOWNUSERNAMES_DESC": "Si l'option est sélectionnée, l'écran du mot de passe sera affiché dans le processus de connexion même si l'utilisateur n'a pas été trouvé. L'erreur sur la vérification du mot de passe ne révélera pas si le nom d'utilisateur ou le mot de passe était erroné.", "ALLOWDOMAINDISCOVERY": "Découverte du domaine autorisée", "ALLOWDOMAINDISCOVERY_DESC": "Si l'option est sélectionnée, le suffixe (@domain.com) d'un nom d'utilisateur inconnu saisi sur l'écran de connexion sera comparé aux domaines organisation et redirigera vers l'enregistrement de cette organisation en cas de succès.", + "DISABLELOGINWITHEMAIL": "Désactiver la connexion avec l'adresse e-mail", + "DISABLELOGINWITHPHONE": "Désactiver la connexion avec le numéro de téléphone", "DEFAULTREDIRECTURI": "URI de redirection par défaut", "DEFAULTREDIRECTURI_DESC": "Définit l'endroit où l'utilisateur sera redirigé si la connexion a commencé sans contexte d'application (par exemple, à partir du courrier électronique).", "ERRORMSGPOPUP": "Afficher l'erreur dans la boîte de dialogue", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 779325339e..fd5409f7ca 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -1173,6 +1173,8 @@ "IGNOREUNKNOWNUSERNAMES_DESC": "Se l'opzione \u00e8 selezionata, l'inserimento della password viene mostrato anche se nessun utente è stato trovato. Nota che dopo il controllo della password, non viene mostrato se il nome utente o la password erano errati.", "ALLOWDOMAINDISCOVERY": "Scoperta del dominio consentita", "ALLOWDOMAINDISCOVERY_DESC": "Se l'opzione è selezionata, il suffisso (@domain.com) di un nome utente sconosciuto inserito nel login verrà confrontato con i domini organizzazione e, in caso di successo, verrà reindirizzato alla registrazione di tale organizzazione", + "DISABLELOGINWITHEMAIL": "Disabilita il login con l'indirizzo e-mail", + "DISABLELOGINWITHPHONE": "Disabilita l'accesso con il numero di telefono", "DEFAULTREDIRECTURI": "Default Redirect URI", "DEFAULTREDIRECTURI_DESC": "Definisce dove verrà reindirizzato l'utente se l'accesso è stato avviato senza un contesto dell'app (ad es. dall' email)", "ERRORMSGPOPUP": "Mostra l'errore nella finestra di dialogo", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 9e67158b0a..fa86d9497a 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -1172,6 +1172,8 @@ "IGNOREUNKNOWNUSERNAMES_DESC": "如果选择该选项,即使未找到用户,登录过程中也会显示密码屏幕。如果用户名或密码错误,密码检查的错误不会透露。", "ALLOWDOMAINDISCOVERY": "允许域名发现", "ALLOWDOMAINDISCOVERY_DESC": "如果选择该选项,在登录屏幕上输入的未知用户名的后缀(@domain.com)将与组织的域名进行匹配,成功后将重定向到组织的注册。", + "DISABLELOGINWITHEMAIL": "禁止用电子邮件地址登录", + "DISABLELOGINWITHPHONE": "禁止用电话号码登录", "DEFAULTREDIRECTURI": "默认重定向 URI", "DEFAULTREDIRECTURI_DESC": "定义如果在没有应用程序上下文的情况下开始登录(例如来自邮件),用户将被重定向到哪里。", "ERRORMSGPOPUP": "在对话框中显示错误", diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index 2c5c57d822..72c0ab5f8f 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -4360,6 +4360,8 @@ this is en empty request | second_factor_check_lifetime | google.protobuf.Duration | - | | | multi_factor_check_lifetime | google.protobuf.Duration | - | | | allow_domain_discovery | bool | If set to true, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the org domains and will redirect to the registration of that organisation on success. | | +| disable_login_with_email | bool | - | | +| disable_login_with_phone | bool | - | | diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md index e267eceb8d..43eebe443e 100644 --- a/docs/docs/apis/proto/management.md +++ b/docs/docs/apis/proto/management.md @@ -3184,6 +3184,8 @@ This is an empty request | multi_factors | repeated zitadel.policy.v1.MultiFactorType | - | | | idps | repeated AddCustomLoginPolicyRequest.IDP | - | | | allow_domain_discovery | bool | If set to true, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the org domains and will redirect to the registration of that organisation on success. | | +| disable_login_with_email | bool | - | | +| disable_login_with_phone | bool | - | | @@ -8171,6 +8173,8 @@ This is an empty request | second_factor_check_lifetime | google.protobuf.Duration | - | | | multi_factor_check_lifetime | google.protobuf.Duration | - | | | allow_domain_discovery | bool | If set to true, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the org domains and will redirect to the registration of that organisation on success. | | +| disable_login_with_email | bool | - | | +| disable_login_with_phone | bool | - | | diff --git a/docs/docs/apis/proto/policy.md b/docs/docs/apis/proto/policy.md index 28d0a5312f..8af9ffbda5 100644 --- a/docs/docs/apis/proto/policy.md +++ b/docs/docs/apis/proto/policy.md @@ -89,6 +89,8 @@ title: zitadel/policy.proto | multi_factors | repeated MultiFactorType | - | | | idps | repeated zitadel.idp.v1.IDPLoginPolicyLink | - | | | allow_domain_discovery | bool | If set to true, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the org domains and will redirect to the registration of that organisation on success. | | +| disable_login_with_email | bool | - | | +| disable_login_with_phone | bool | - | | diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 60b29619d6..42b3d25a5c 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -442,7 +442,7 @@ func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*adm } } if org.LoginPolicy != nil { - _, err = s.command.AddLoginPolicy(ctx, org.GetOrgId(), management.AddLoginPolicyToDomain(org.GetLoginPolicy())) + _, err = s.command.AddLoginPolicy(ctx, org.GetOrgId(), management.AddLoginPolicyToCommand(org.GetLoginPolicy())) if err != nil { errors = append(errors, &admin_pb.ImportDataError{Type: "login_policy", Id: org.GetOrgId(), Message: err.Error()}) } diff --git a/internal/api/grpc/admin/login_policy.go b/internal/api/grpc/admin/login_policy.go index 4a8c2cbd67..aa39ac7367 100644 --- a/internal/api/grpc/admin/login_policy.go +++ b/internal/api/grpc/admin/login_policy.go @@ -22,14 +22,14 @@ func (s *Server) GetLoginPolicy(ctx context.Context, _ *admin_pb.GetLoginPolicyR } func (s *Server) UpdateLoginPolicy(ctx context.Context, p *admin_pb.UpdateLoginPolicyRequest) (*admin_pb.UpdateLoginPolicyResponse, error) { - policy, err := s.command.ChangeDefaultLoginPolicy(ctx, updateLoginPolicyToDomain(p)) + policy, err := s.command.ChangeDefaultLoginPolicy(ctx, updateLoginPolicyToCommand(p)) if err != nil { return nil, err } return &admin_pb.UpdateLoginPolicyResponse{ Details: object.ChangeToDetailsPb( policy.Sequence, - policy.ChangeDate, + policy.EventDate, policy.ResourceOwner, ), }, nil diff --git a/internal/api/grpc/admin/login_policy_converter.go b/internal/api/grpc/admin/login_policy_converter.go index e6b97ebc40..24fee2d00c 100644 --- a/internal/api/grpc/admin/login_policy_converter.go +++ b/internal/api/grpc/admin/login_policy_converter.go @@ -3,13 +3,13 @@ package admin import ( "github.com/zitadel/zitadel/internal/api/grpc/object" policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy" - "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" ) -func updateLoginPolicyToDomain(p *admin_pb.UpdateLoginPolicyRequest) *domain.LoginPolicy { - return &domain.LoginPolicy{ +func updateLoginPolicyToCommand(p *admin_pb.UpdateLoginPolicyRequest) *command.ChangeLoginPolicy { + return &command.ChangeLoginPolicy{ AllowUsernamePassword: p.AllowUsernamePassword, AllowRegister: p.AllowRegister, AllowExternalIDP: p.AllowExternalIdp, @@ -18,6 +18,8 @@ func updateLoginPolicyToDomain(p *admin_pb.UpdateLoginPolicyRequest) *domain.Log HidePasswordReset: p.HidePasswordReset, IgnoreUnknownUsernames: p.IgnoreUnknownUsernames, AllowDomainDiscovery: p.AllowDomainDiscovery, + DisableLoginWithEmail: p.DisableLoginWithEmail, + DisableLoginWithPhone: p.DisableLoginWithPhone, DefaultRedirectURI: p.DefaultRedirectUri, PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(), ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(), diff --git a/internal/api/grpc/management/policy_login.go b/internal/api/grpc/management/policy_login.go index 78029c79a5..ecfa3073bd 100644 --- a/internal/api/grpc/management/policy_login.go +++ b/internal/api/grpc/management/policy_login.go @@ -30,28 +30,28 @@ func (s *Server) GetDefaultLoginPolicy(ctx context.Context, req *mgmt_pb.GetDefa } func (s *Server) AddCustomLoginPolicy(ctx context.Context, req *mgmt_pb.AddCustomLoginPolicyRequest) (*mgmt_pb.AddCustomLoginPolicyResponse, error) { - policy, err := s.command.AddLoginPolicy(ctx, authz.GetCtxData(ctx).OrgID, AddLoginPolicyToDomain(req)) + policy, err := s.command.AddLoginPolicy(ctx, authz.GetCtxData(ctx).OrgID, AddLoginPolicyToCommand(req)) if err != nil { return nil, err } return &mgmt_pb.AddCustomLoginPolicyResponse{ Details: object.AddToDetailsPb( policy.Sequence, - policy.ChangeDate, + policy.EventDate, policy.ResourceOwner, ), }, nil } func (s *Server) UpdateCustomLoginPolicy(ctx context.Context, req *mgmt_pb.UpdateCustomLoginPolicyRequest) (*mgmt_pb.UpdateCustomLoginPolicyResponse, error) { - policy, err := s.command.ChangeLoginPolicy(ctx, authz.GetCtxData(ctx).OrgID, updateLoginPolicyToDomain(req)) + policy, err := s.command.ChangeLoginPolicy(ctx, authz.GetCtxData(ctx).OrgID, updateLoginPolicyToCommand(req)) if err != nil { return nil, err } return &mgmt_pb.UpdateCustomLoginPolicyResponse{ Details: object.ChangeToDetailsPb( policy.Sequence, - policy.ChangeDate, + policy.EventDate, policy.ResourceOwner, ), }, nil diff --git a/internal/api/grpc/management/policy_login_converter.go b/internal/api/grpc/management/policy_login_converter.go index 3cf1d0a97f..2e41d1c8f5 100644 --- a/internal/api/grpc/management/policy_login_converter.go +++ b/internal/api/grpc/management/policy_login_converter.go @@ -4,13 +4,13 @@ import ( idp_grpc "github.com/zitadel/zitadel/internal/api/grpc/idp" "github.com/zitadel/zitadel/internal/api/grpc/object" policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy" - "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management" ) -func AddLoginPolicyToDomain(p *mgmt_pb.AddCustomLoginPolicyRequest) *domain.LoginPolicy { - return &domain.LoginPolicy{ +func AddLoginPolicyToCommand(p *mgmt_pb.AddCustomLoginPolicyRequest) *command.AddLoginPolicy { + return &command.AddLoginPolicy{ AllowUsernamePassword: p.AllowUsernamePassword, AllowRegister: p.AllowRegister, AllowExternalIDP: p.AllowExternalIdp, @@ -27,22 +27,24 @@ func AddLoginPolicyToDomain(p *mgmt_pb.AddCustomLoginPolicyRequest) *domain.Logi MultiFactorCheckLifetime: p.MultiFactorCheckLifetime.AsDuration(), SecondFactors: policy_grpc.SecondFactorsTypesToDomain(p.SecondFactors), MultiFactors: policy_grpc.MultiFactorsTypesToDomain(p.MultiFactors), - IDPProviders: addLoginPolicyIDPsToDomain(p.Idps), + IDPProviders: addLoginPolicyIDPsToCommand(p.Idps), + DisableLoginWithEmail: p.DisableLoginWithEmail, + DisableLoginWithPhone: p.DisableLoginWithPhone, } } -func addLoginPolicyIDPsToDomain(idps []*mgmt_pb.AddCustomLoginPolicyRequest_IDP) []*domain.IDPProvider { - providers := make([]*domain.IDPProvider, len(idps)) +func addLoginPolicyIDPsToCommand(idps []*mgmt_pb.AddCustomLoginPolicyRequest_IDP) []*command.AddLoginPolicyIDP { + providers := make([]*command.AddLoginPolicyIDP, len(idps)) for i, idp := range idps { - providers[i] = &domain.IDPProvider{ - Type: idp_grpc.IDPProviderTypeFromPb(idp.OwnerType), - IDPConfigID: idp.IdpId, + providers[i] = &command.AddLoginPolicyIDP{ + Type: idp_grpc.IDPProviderTypeFromPb(idp.OwnerType), + ConfigID: idp.IdpId, } } return providers } -func updateLoginPolicyToDomain(p *mgmt_pb.UpdateCustomLoginPolicyRequest) *domain.LoginPolicy { - return &domain.LoginPolicy{ +func updateLoginPolicyToCommand(p *mgmt_pb.UpdateCustomLoginPolicyRequest) *command.ChangeLoginPolicy { + return &command.ChangeLoginPolicy{ AllowUsernamePassword: p.AllowUsernamePassword, AllowRegister: p.AllowRegister, AllowExternalIDP: p.AllowExternalIdp, @@ -51,6 +53,8 @@ func updateLoginPolicyToDomain(p *mgmt_pb.UpdateCustomLoginPolicyRequest) *domai HidePasswordReset: p.HidePasswordReset, IgnoreUnknownUsernames: p.IgnoreUnknownUsernames, AllowDomainDiscovery: p.AllowDomainDiscovery, + DisableLoginWithEmail: p.DisableLoginWithEmail, + DisableLoginWithPhone: p.DisableLoginWithPhone, DefaultRedirectURI: p.DefaultRedirectUri, PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(), ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(), diff --git a/internal/api/grpc/policy/login_policy.go b/internal/api/grpc/policy/login_policy.go index 498005f06b..3cd191a915 100644 --- a/internal/api/grpc/policy/login_policy.go +++ b/internal/api/grpc/policy/login_policy.go @@ -22,6 +22,8 @@ func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy { HidePasswordReset: policy.HidePasswordReset, IgnoreUnknownUsernames: policy.IgnoreUnknownUsernames, AllowDomainDiscovery: policy.AllowDomainDiscovery, + DisableLoginWithEmail: policy.DisableLoginWithEmail, + DisableLoginWithPhone: policy.DisableLoginWithPhone, DefaultRedirectUri: policy.DefaultRedirectURI, PasswordCheckLifetime: durationpb.New(policy.PasswordCheckLifetime), ExternalLoginCheckLifetime: durationpb.New(policy.ExternalLoginCheckLifetime), diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 30e0cb05a3..88d4e337b1 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -641,15 +641,9 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain preferredLoginName += "@" + request.RequestedPrimaryDomain } } - user, err = repo.View.UserByLoginNameAndResourceOwner(preferredLoginName, request.RequestedOrgID, request.InstanceID) + user, err = repo.checkLoginNameInputForResourceOwner(request, preferredLoginName) } else { - user, err = repo.View.UserByLoginName(loginName, request.InstanceID) - if err == nil { - err = repo.checkLoginPolicyWithResourceOwner(ctx, request, user) - if err != nil { - return err - } - } + user, err = repo.checkLoginNameInput(ctx, request, preferredLoginName) } // return any error apart from not found ones directly if err != nil && !errors.IsNotFound(err) { @@ -720,8 +714,74 @@ func (repo *AuthRequestRepo) checkDomainDiscovery(ctx context.Context, request * return true } -func (repo *AuthRequestRepo) checkLoginPolicyWithResourceOwner(ctx context.Context, request *domain.AuthRequest, user *user_view_model.UserView) error { - loginPolicy, idpProviders, err := repo.getLoginPolicyAndIDPProviders(ctx, user.ResourceOwner) +func (repo *AuthRequestRepo) checkLoginNameInput(ctx context.Context, request *domain.AuthRequest, loginNameInput string) (*user_view_model.UserView, error) { + // always check the loginname first + user, err := repo.View.UserByLoginName(loginNameInput, request.InstanceID) + if err == nil { + // and take the user regardless if there would be a user with that email or phone + return user, repo.checkLoginPolicyWithResourceOwner(ctx, request, user.ResourceOwner) + } + user, emailErr := repo.View.UserByEmail(loginNameInput, request.InstanceID) + if emailErr == nil { + // if there was a single user with the specified email + // load and check the login policy + if emailErr = repo.checkLoginPolicyWithResourceOwner(ctx, request, user.ResourceOwner); emailErr != nil { + return nil, emailErr + } + // and in particular if the login with email is possible + // if so take the user (and ignore possible phone matches) + if !request.LoginPolicy.DisableLoginWithEmail { + return user, nil + } + } + user, phoneErr := repo.View.UserByPhone(loginNameInput, request.InstanceID) + if phoneErr == nil { + // if there was a single user with the specified phone + // load and check the login policy + if phoneErr = repo.checkLoginPolicyWithResourceOwner(ctx, request, user.ResourceOwner); phoneErr != nil { + return nil, phoneErr + } + // and in particular if the login with phone is possible + // if so take the user + if !request.LoginPolicy.DisableLoginWithPhone { + return user, nil + } + } + // if we get here the user was not found by loginname + // and either there was no match for email or phone as well, or they have been both disabled + return nil, err +} + +func (repo *AuthRequestRepo) checkLoginNameInputForResourceOwner(request *domain.AuthRequest, loginNameInput string) (*user_view_model.UserView, error) { + // always check the loginname first + user, err := repo.View.UserByLoginNameAndResourceOwner(loginNameInput, request.RequestedOrgID, request.InstanceID) + if err == nil { + // and take the user regardless if there would be a user with that email or phone + return user, nil + } + if request.LoginPolicy != nil && !request.LoginPolicy.DisableLoginWithEmail { + // if login by email is allowed and there was a single user with the specified email + // take that user (and ignore possible phone number matches) + user, emailErr := repo.View.UserByEmailAndResourceOwner(loginNameInput, request.RequestedOrgID, request.InstanceID) + if emailErr == nil { + return user, nil + } + } + if request.LoginPolicy != nil && !request.LoginPolicy.DisableLoginWithPhone { + // if login by phone is allowed and there was a single user with the specified phone + // take that user + user, phoneErr := repo.View.UserByPhoneAndResourceOwner(loginNameInput, request.RequestedOrgID, request.InstanceID) + if phoneErr == nil { + return user, nil + } + } + // if we get here the user was not found by loginname + // and either there was no match for email or phone as well or they have been both disabled + return nil, err +} + +func (repo *AuthRequestRepo) checkLoginPolicyWithResourceOwner(ctx context.Context, request *domain.AuthRequest, resourceOwner string) error { + loginPolicy, idpProviders, err := repo.getLoginPolicyAndIDPProviders(ctx, resourceOwner) if err != nil { return err } @@ -758,11 +818,15 @@ func queryLoginPolicyToDomain(policy *query.LoginPolicy) *domain.LoginPolicy { PasswordlessType: policy.PasswordlessType, HidePasswordReset: policy.HidePasswordReset, IgnoreUnknownUsernames: policy.IgnoreUnknownUsernames, + AllowDomainDiscovery: policy.AllowDomainDiscovery, + DefaultRedirectURI: policy.DefaultRedirectURI, PasswordCheckLifetime: policy.PasswordCheckLifetime, ExternalLoginCheckLifetime: policy.ExternalLoginCheckLifetime, MFAInitSkipLifetime: policy.MFAInitSkipLifetime, SecondFactorCheckLifetime: policy.SecondFactorCheckLifetime, MultiFactorCheckLifetime: policy.MultiFactorCheckLifetime, + DisableLoginWithEmail: policy.DisableLoginWithEmail, + DisableLoginWithPhone: policy.DisableLoginWithPhone, } } diff --git a/internal/auth/repository/eventsourcing/view/user.go b/internal/auth/repository/eventsourcing/view/user.go index dd56664a9d..e9e14a1893 100644 --- a/internal/auth/repository/eventsourcing/view/user.go +++ b/internal/auth/repository/eventsourcing/view/user.go @@ -52,10 +52,52 @@ func (v *View) UserByLoginNameAndResourceOwner(loginName, resourceOwner, instanc return v.userByID(instanceID, loginNameQuery, resourceOwnerQuery) } +func (v *View) UserByEmail(email, instanceID string) (*model.UserView, error) { + emailQuery, err := query.NewUserVerifiedEmailSearchQuery(email, query.TextEquals) + if err != nil { + return nil, err + } + return v.userByID(instanceID, emailQuery) +} + +func (v *View) UserByEmailAndResourceOwner(email, resourceOwner, instanceID string) (*model.UserView, error) { + emailQuery, err := query.NewUserVerifiedEmailSearchQuery(email, query.TextEquals) + if err != nil { + return nil, err + } + resourceOwnerQuery, err := query.NewUserResourceOwnerSearchQuery(resourceOwner, query.TextEquals) + if err != nil { + return nil, err + } + + return v.userByID(instanceID, emailQuery, resourceOwnerQuery) +} + +func (v *View) UserByPhone(phone, instanceID string) (*model.UserView, error) { + phoneQuery, err := query.NewUserVerifiedPhoneSearchQuery(phone, query.TextEquals) + if err != nil { + return nil, err + } + return v.userByID(instanceID, phoneQuery) +} + +func (v *View) UserByPhoneAndResourceOwner(phone, resourceOwner, instanceID string) (*model.UserView, error) { + phoneQuery, err := query.NewUserVerifiedPhoneSearchQuery(phone, query.TextEquals) + if err != nil { + return nil, err + } + resourceOwnerQuery, err := query.NewUserResourceOwnerSearchQuery(resourceOwner, query.TextEquals) + if err != nil { + return nil, err + } + + return v.userByID(instanceID, phoneQuery, resourceOwnerQuery) +} + func (v *View) userByID(instanceID string, queries ...query.SearchQuery) (*model.UserView, error) { ctx := authz.WithInstanceID(context.Background(), instanceID) - queriedUser, err := v.query.GetUser(ctx, true, queries...) + queriedUser, err := v.query.GetNotifyUser(ctx, true, queries...) if err != nil { return nil, err } diff --git a/internal/command/command.go b/internal/command/command.go index 9ca70163f5..f68bc6efa6 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -1,11 +1,13 @@ package command import ( + "context" "net/http" "time" "github.com/zitadel/zitadel/internal/api/authz" api_http "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/command/preparation" sd "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -132,3 +134,28 @@ func AppendAndReduce(object interface { object.AppendEvents(events...) return object.Reduce() } + +func queryAndReduce(ctx context.Context, filter preparation.FilterToQueryReducer, wm eventstore.QueryReducer) error { + events, err := filter(ctx, wm.Query()) + if err != nil { + return err + } + if len(events) == 0 { + return nil + } + wm.AppendEvents(events...) + return wm.Reduce() +} + +type existsWriteModel interface { + Exists() bool + eventstore.QueryReducer +} + +func exists(ctx context.Context, filter preparation.FilterToQueryReducer, wm existsWriteModel) (bool, error) { + err := queryAndReduce(ctx, filter, wm) + if err != nil { + return false, err + } + return wm.Exists(), nil +} diff --git a/internal/command/idp_config_model.go b/internal/command/idp_config_model.go index 8d37ab275c..74654c244b 100644 --- a/internal/command/idp_config_model.go +++ b/internal/command/idp_config_model.go @@ -62,3 +62,7 @@ func (rm *IDPConfigWriteModel) reduceConfigChangedEvent(e *idpconfig.IDPConfigCh func (rm *IDPConfigWriteModel) reduceConfigStateChanged(configID string, state domain.IDPConfigState) { rm.State = state } + +func (rm *IDPConfigWriteModel) Exists() bool { + return rm.State.Exists() +} diff --git a/internal/command/instance.go b/internal/command/instance.go index 7d9878785e..2c9283c603 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -72,6 +72,8 @@ type InstanceSetup struct { HidePasswordReset bool IgnoreUnknownUsername bool AllowDomainDiscovery bool + DisableLoginWithEmail bool + DisableLoginWithPhone bool PasswordlessType domain.PasswordlessType DefaultRedirectURI string PasswordCheckLifetime time.Duration @@ -219,6 +221,8 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str setup.LoginPolicy.HidePasswordReset, setup.LoginPolicy.IgnoreUnknownUsername, setup.LoginPolicy.AllowDomainDiscovery, + setup.LoginPolicy.DisableLoginWithEmail, + setup.LoginPolicy.DisableLoginWithPhone, setup.LoginPolicy.PasswordlessType, setup.LoginPolicy.DefaultRedirectURI, setup.LoginPolicy.PasswordCheckLifetime, diff --git a/internal/command/instance_policy_login.go b/internal/command/instance_policy_login.go index 34008536a9..395c107230 100644 --- a/internal/command/instance_policy_login.go +++ b/internal/command/instance_policy_login.go @@ -15,56 +15,17 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" ) -func (c *Commands) ChangeDefaultLoginPolicy(ctx context.Context, policy *domain.LoginPolicy) (*domain.LoginPolicy, error) { - existingPolicy := NewInstanceLoginPolicyWriteModel(ctx) - instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.LoginPolicyWriteModel.WriteModel) - event, err := c.changeDefaultLoginPolicy(ctx, instanceAgg, existingPolicy, policy) +func (c *Commands) ChangeDefaultLoginPolicy(ctx context.Context, policy *ChangeLoginPolicy) (*domain.ObjectDetails, error) { + instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareChangeDefaultLoginPolicy(instanceAgg, policy)) if err != nil { return nil, err } - pushedEvents, err := c.eventstore.Push(ctx, event) + pushedEvents, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err } - err = AppendAndReduce(existingPolicy, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToLoginPolicy(&existingPolicy.LoginPolicyWriteModel), nil -} - -func (c *Commands) changeDefaultLoginPolicy(ctx context.Context, instanceAgg *eventstore.Aggregate, existingPolicy *InstanceLoginPolicyWriteModel, policy *domain.LoginPolicy) (eventstore.Command, error) { - if ok := domain.ValidateDefaultRedirectURI(policy.DefaultRedirectURI); !ok { - return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-SFdqd", "Errors.IAM.LoginPolicy.RedirectURIInvalid") - } - err := c.defaultLoginPolicyWriteModelByID(ctx, existingPolicy) - if err != nil { - return nil, err - } - if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-M0sif", "Errors.IAM.LoginPolicy.NotFound") - } - changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, - instanceAgg, - policy.AllowUsernamePassword, - policy.AllowRegister, - policy.AllowExternalIDP, - policy.ForceMFA, - policy.HidePasswordReset, - policy.IgnoreUnknownUsernames, - policy.AllowDomainDiscovery, - policy.PasswordlessType, - policy.DefaultRedirectURI, - policy.PasswordCheckLifetime, - policy.ExternalLoginCheckLifetime, - policy.MFAInitSkipLifetime, - policy.SecondFactorCheckLifetime, - policy.MultiFactorCheckLifetime, - ) - if !hasChanged { - return nil, caos_errs.ThrowPreconditionFailed(nil, "INSTANCE-5M9vdd", "Errors.IAM.LoginPolicy.NotChanged") - } - return changedEvent, nil + return pushedEventsToObjectDetails(pushedEvents), nil } func (c *Commands) AddIDPProviderToDefaultLoginPolicy(ctx context.Context, idpProvider *domain.IDPProvider) (*domain.IDPProvider, error) { @@ -255,6 +216,44 @@ func (c *Commands) getDefaultLoginPolicy(ctx context.Context) (*domain.LoginPoli return policy, nil } +func prepareChangeDefaultLoginPolicy(a *instance.Aggregate, policy *ChangeLoginPolicy) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if ok := domain.ValidateDefaultRedirectURI(policy.DefaultRedirectURI); !ok { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-SFdqd", "Errors.IAM.LoginPolicy.RedirectURIInvalid") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + wm := NewInstanceLoginPolicyWriteModel(ctx) + if err := queryAndReduce(ctx, filter, wm); err != nil { + return nil, err + } + if !wm.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-M0sif", "Errors.IAM.LoginPolicy.NotFound") + } + changedEvent, hasChanged := wm.NewChangedEvent(ctx, &a.Aggregate, + policy.AllowUsernamePassword, + policy.AllowRegister, + policy.AllowExternalIDP, + policy.ForceMFA, + policy.HidePasswordReset, + policy.IgnoreUnknownUsernames, + policy.AllowDomainDiscovery, + policy.DisableLoginWithEmail, + policy.DisableLoginWithPhone, + policy.PasswordlessType, + policy.DefaultRedirectURI, + policy.PasswordCheckLifetime, + policy.ExternalLoginCheckLifetime, + policy.MFAInitSkipLifetime, + policy.SecondFactorCheckLifetime, + policy.MultiFactorCheckLifetime) + if !hasChanged { + return nil, caos_errs.ThrowPreconditionFailed(nil, "INSTANCE-5M9vdd", "Errors.IAM.LoginPolicy.NotChanged") + } + return []eventstore.Command{changedEvent}, nil + }, nil + } +} + func prepareAddDefaultLoginPolicy( a *instance.Aggregate, allowUsernamePassword bool, @@ -264,6 +263,8 @@ func prepareAddDefaultLoginPolicy( hidePasswordReset bool, ignoreUnknownUsernames bool, allowDomainDiscovery bool, + disableLoginWithEmail bool, + disableLoginWithPhone bool, passwordlessType domain.PasswordlessType, defaultRedirectURI string, passwordCheckLifetime time.Duration, @@ -295,6 +296,8 @@ func prepareAddDefaultLoginPolicy( hidePasswordReset, ignoreUnknownUsernames, allowDomainDiscovery, + disableLoginWithEmail, + disableLoginWithPhone, passwordlessType, defaultRedirectURI, passwordCheckLifetime, diff --git a/internal/command/instance_policy_login_model.go b/internal/command/instance_policy_login_model.go index 972265b26d..3f04616aa4 100644 --- a/internal/command/instance_policy_login_model.go +++ b/internal/command/instance_policy_login_model.go @@ -67,7 +67,9 @@ func (wm *InstanceLoginPolicyWriteModel) NewChangedEvent( forceMFA, hidePasswordReset, ignoreUnknownUsernames, - allowDomainDiscovery bool, + allowDomainDiscovery, + disableLoginWithEmail, + disableLoginWithPhone bool, passwordlessType domain.PasswordlessType, defaultRedirectURI string, passwordCheckLifetime, @@ -120,6 +122,12 @@ func (wm *InstanceLoginPolicyWriteModel) NewChangedEvent( if wm.MultiFactorCheckLifetime != multiFactorCheckLifetime { changes = append(changes, policy.ChangeMultiFactorCheckLifetime(multiFactorCheckLifetime)) } + if wm.DisableLoginWithEmail != disableLoginWithEmail { + changes = append(changes, policy.ChangeDisableLoginWithEmail(disableLoginWithEmail)) + } + if wm.DisableLoginWithPhone != disableLoginWithPhone { + changes = append(changes, policy.ChangeDisableLoginWithPhone(disableLoginWithPhone)) + } if len(changes) == 0 { return nil, false } diff --git a/internal/command/instance_policy_login_test.go b/internal/command/instance_policy_login_test.go index 3e1518ec90..639fc4a65b 100644 --- a/internal/command/instance_policy_login_test.go +++ b/internal/command/instance_policy_login_test.go @@ -24,10 +24,10 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { } type args struct { ctx context.Context - policy *domain.LoginPolicy + policy *ChangeLoginPolicy } type res struct { - want *domain.LoginPolicy + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -46,7 +46,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { }, args: args{ ctx: context.Background(), - policy: &domain.LoginPolicy{ + policy: &ChangeLoginPolicy{ AllowRegister: true, AllowExternalIDP: true, }, @@ -71,6 +71,8 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -85,7 +87,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { }, args: args{ ctx: context.Background(), - policy: &domain.LoginPolicy{ + policy: &ChangeLoginPolicy{ AllowRegister: true, AllowUsernamePassword: true, AllowExternalIDP: true, @@ -93,6 +95,8 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { HidePasswordReset: true, IgnoreUnknownUsernames: true, AllowDomainDiscovery: true, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -123,6 +127,8 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -145,6 +151,8 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*10, @@ -159,7 +167,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { }, args: args{ ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - policy: &domain.LoginPolicy{ + policy: &ChangeLoginPolicy{ AllowRegister: false, AllowUsernamePassword: false, AllowExternalIDP: false, @@ -167,6 +175,8 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { HidePasswordReset: false, IgnoreUnknownUsernames: false, AllowDomainDiscovery: false, + DisableLoginWithEmail: false, + DisableLoginWithPhone: false, PasswordlessType: domain.PasswordlessTypeNotAllowed, DefaultRedirectURI: "", PasswordCheckLifetime: time.Hour * 10, @@ -177,26 +187,8 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { }, }, res: res{ - want: &domain.LoginPolicy{ - ObjectRoot: models.ObjectRoot{ - InstanceID: "INSTANCE", - AggregateID: "INSTANCE", - ResourceOwner: "INSTANCE", - }, - AllowRegister: false, - AllowUsernamePassword: false, - AllowExternalIDP: false, - ForceMFA: false, - HidePasswordReset: false, - IgnoreUnknownUsernames: false, - AllowDomainDiscovery: false, - PasswordlessType: domain.PasswordlessTypeNotAllowed, - DefaultRedirectURI: "", - PasswordCheckLifetime: time.Hour * 10, - ExternalLoginCheckLifetime: time.Hour * 20, - MFAInitSkipLifetime: time.Hour * 30, - SecondFactorCheckLifetime: time.Hour * 40, - MultiFactorCheckLifetime: time.Hour * 50, + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", }, }, }, @@ -287,6 +279,8 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -326,6 +320,8 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -385,6 +381,8 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -526,6 +524,8 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -565,6 +565,8 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -617,6 +619,8 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -674,6 +678,8 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -739,6 +745,8 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -1293,7 +1301,8 @@ func TestCommandSide_RemoveMultiFactorDefaultLoginPolicy(t *testing.T) { } } -func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allowUsernamePassword, allowExternalIDP, forceMFA, hidePasswordReset, ignoreUnknownUsernames, allowDomainDiscovery bool, +func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allowUsernamePassword, allowExternalIDP, forceMFA, + hidePasswordReset, ignoreUnknownUsernames, allowDomainDiscovery, disableLoginWithEmail, disableLoginWithPhone bool, passwordlessType domain.PasswordlessType, redirectURI string, passwordLifetime, externalLoginLifetime, mfaInitSkipLifetime, secondFactorLifetime, multiFactorLifetime time.Duration) *instance.LoginPolicyChangedEvent { @@ -1307,6 +1316,8 @@ func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allow policy.ChangeHidePasswordReset(hidePasswordReset), policy.ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames), policy.ChangeAllowDomainDiscovery(allowDomainDiscovery), + policy.ChangeDisableLoginWithEmail(disableLoginWithEmail), + policy.ChangeDisableLoginWithPhone(disableLoginWithPhone), policy.ChangePasswordlessType(passwordlessType), policy.ChangeDefaultRedirectURI(redirectURI), policy.ChangePasswordCheckLifetime(passwordLifetime), diff --git a/internal/command/org_policy_login.go b/internal/command/org_policy_login.go index e85d6f2a27..bee73e079c 100644 --- a/internal/command/org_policy_login.go +++ b/internal/command/org_policy_login.go @@ -2,9 +2,12 @@ package command import ( "context" + "time" "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" @@ -12,74 +15,63 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" ) -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") - } - if ok := domain.ValidateDefaultRedirectURI(policy.DefaultRedirectURI); !ok { - return nil, caos_errs.ThrowInvalidArgument(nil, "Org-WSfdq", "Errors.Org.LoginPolicy.RedirectURIInvalid") - } - addedPolicy := NewOrgLoginPolicyWriteModel(resourceOwner) - err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) +type AddLoginPolicy struct { + AllowUsernamePassword bool + AllowRegister bool + AllowExternalIDP bool + IDPProviders []*AddLoginPolicyIDP + ForceMFA bool + SecondFactors []domain.SecondFactorType + MultiFactors []domain.MultiFactorType + PasswordlessType domain.PasswordlessType + HidePasswordReset bool + IgnoreUnknownUsernames bool + AllowDomainDiscovery bool + DefaultRedirectURI string + PasswordCheckLifetime time.Duration + ExternalLoginCheckLifetime time.Duration + MFAInitSkipLifetime time.Duration + SecondFactorCheckLifetime time.Duration + MultiFactorCheckLifetime time.Duration + DisableLoginWithEmail bool + DisableLoginWithPhone bool +} + +type AddLoginPolicyIDP struct { + ConfigID string + Type domain.IdentityProviderType +} + +type ChangeLoginPolicy struct { + AllowUsernamePassword bool + AllowRegister bool + AllowExternalIDP bool + ForceMFA bool + PasswordlessType domain.PasswordlessType + HidePasswordReset bool + IgnoreUnknownUsernames bool + AllowDomainDiscovery bool + DefaultRedirectURI string + PasswordCheckLifetime time.Duration + ExternalLoginCheckLifetime time.Duration + MFAInitSkipLifetime time.Duration + SecondFactorCheckLifetime time.Duration + MultiFactorCheckLifetime time.Duration + DisableLoginWithEmail bool + DisableLoginWithPhone bool +} + +func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, policy *AddLoginPolicy) (*domain.ObjectDetails, error) { + orgAgg := org.NewAggregate(resourceOwner) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareAddLoginPolicy(orgAgg, policy)) if err != nil { return nil, err } - if addedPolicy.State == domain.PolicyStateActive { - return nil, caos_errs.ThrowAlreadyExists(nil, "Org-Dgfb2", "Errors.Org.LoginPolicy.AlreadyExists") - } - - orgAgg := OrgAggregateFromWriteModel(&addedPolicy.WriteModel) - cmds := []eventstore.Command{ - org.NewLoginPolicyAddedEvent( - ctx, - orgAgg, - policy.AllowUsernamePassword, - policy.AllowRegister, - policy.AllowExternalIDP, - policy.ForceMFA, - policy.HidePasswordReset, - policy.IgnoreUnknownUsernames, - policy.AllowDomainDiscovery, - policy.PasswordlessType, - policy.DefaultRedirectURI, - policy.PasswordCheckLifetime, - policy.ExternalLoginCheckLifetime, - policy.MFAInitSkipLifetime, - policy.SecondFactorCheckLifetime, - policy.MultiFactorCheckLifetime), - } - for _, factor := range policy.SecondFactors { - if !factor.Valid() { - return nil, caos_errs.ThrowInvalidArgument(nil, "Org-SFeea", "Errors.Org.LoginPolicy.MFA.Unspecified") - } - cmds = append(cmds, org.NewLoginPolicySecondFactorAddedEvent(ctx, orgAgg, factor)) - } - for _, factor := range policy.MultiFactors { - if !factor.Valid() { - return nil, caos_errs.ThrowInvalidArgument(nil, "Org-WSfrg", "Errors.Org.LoginPolicy.MFA.Unspecified") - } - cmds = append(cmds, org.NewLoginPolicyMultiFactorAddedEvent(ctx, orgAgg, factor)) - } - for _, provider := range policy.IDPProviders { - if provider.Type == domain.IdentityProviderTypeOrg { - _, err = c.getOrgIDPConfigByID(ctx, provider.IDPConfigID, resourceOwner) - } else { - _, err = c.getInstanceIDPConfigByID(ctx, provider.IDPConfigID) - } - if err != nil { - return nil, caos_errs.ThrowPreconditionFailed(err, "Org-FEd32", "Errors.IDPConfig.NotExisting") - } - cmds = append(cmds, org.NewIdentityProviderAddedEvent(ctx, orgAgg, provider.IDPConfigID, provider.Type)) - } pushedEvents, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err } - err = AppendAndReduce(addedPolicy, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToLoginPolicy(&addedPolicy.LoginPolicyWriteModel), nil + return pushedEventsToObjectDetails(pushedEvents), nil } func (c *Commands) orgLoginPolicyWriteModelByID(ctx context.Context, orgID string) (*OrgLoginPolicyWriteModel, error) { @@ -102,54 +94,17 @@ func (c *Commands) getOrgLoginPolicy(ctx context.Context, orgID string) (*domain return c.getDefaultLoginPolicy(ctx) } -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") - } - if ok := domain.ValidateDefaultRedirectURI(policy.DefaultRedirectURI); !ok { - return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Sfd21", "Errors.Org.LoginPolicy.RedirectURIInvalid") - } - existingPolicy := NewOrgLoginPolicyWriteModel(resourceOwner) - err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) +func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string, policy *ChangeLoginPolicy) (*domain.ObjectDetails, error) { + orgAgg := org.NewAggregate(resourceOwner) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareChangeLoginPolicy(orgAgg, policy)) if err != nil { return nil, err } - if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "Org-M0sif", "Errors.Org.LoginPolicy.NotFound") - } - - orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LoginPolicyWriteModel.WriteModel) - changedEvent, hasChanged := existingPolicy.NewChangedEvent( - ctx, - orgAgg, - policy.AllowUsernamePassword, - policy.AllowRegister, - policy.AllowExternalIDP, - policy.ForceMFA, - policy.HidePasswordReset, - policy.IgnoreUnknownUsernames, - policy.AllowDomainDiscovery, - policy.PasswordlessType, - policy.DefaultRedirectURI, - policy.PasswordCheckLifetime, - policy.ExternalLoginCheckLifetime, - policy.MFAInitSkipLifetime, - policy.SecondFactorCheckLifetime, - policy.MultiFactorCheckLifetime) - - if !hasChanged { - return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-5M9vdd", "Errors.Org.LoginPolicy.NotChanged") - } - - pushedEvents, err := c.eventstore.Push(ctx, changedEvent) + pushedEvents, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err } - err = AppendAndReduce(existingPolicy, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToLoginPolicy(&existingPolicy.LoginPolicyWriteModel), nil + return pushedEventsToObjectDetails(pushedEvents), nil } func (c *Commands) RemoveLoginPolicy(ctx context.Context, orgID string) (*domain.ObjectDetails, error) { @@ -437,3 +392,106 @@ func (c *Commands) orgLoginPolicyAuthFactorsWriteModel(ctx context.Context, orgI } return writeModel, nil } + +func prepareAddLoginPolicy(a *org.Aggregate, policy *AddLoginPolicy) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if ok := domain.ValidateDefaultRedirectURI(policy.DefaultRedirectURI); !ok { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-WSfdq", "Errors.Org.LoginPolicy.RedirectURIInvalid") + } + for _, factor := range policy.SecondFactors { + if !factor.Valid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-SFeea", "Errors.Org.LoginPolicy.MFA.Unspecified") + } + } + for _, factor := range policy.MultiFactors { + if !factor.Valid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-WSfrg", "Errors.Org.LoginPolicy.MFA.Unspecified") + } + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + if exists, err := exists(ctx, filter, NewOrgLoginPolicyWriteModel(a.ID)); exists || err != nil { + return nil, caos_errs.ThrowAlreadyExists(nil, "Org-Dgfb2", "Errors.Org.LoginPolicy.AlreadyExists") + } + for _, idp := range policy.IDPProviders { + exists, err := idpExists(ctx, filter, idp) + if !exists || err != nil { + return nil, caos_errs.ThrowPreconditionFailed(err, "Org-FEd32", "Errors.IDPConfig.NotExisting") + } + } + cmds := make([]eventstore.Command, 0, len(policy.SecondFactors)+len(policy.MultiFactors)+len(policy.IDPProviders)+1) + cmds = append(cmds, org.NewLoginPolicyAddedEvent(ctx, &a.Aggregate, + policy.AllowUsernamePassword, + policy.AllowRegister, + policy.AllowExternalIDP, + policy.ForceMFA, + policy.HidePasswordReset, + policy.IgnoreUnknownUsernames, + policy.AllowDomainDiscovery, + policy.DisableLoginWithEmail, + policy.DisableLoginWithPhone, + policy.PasswordlessType, + policy.DefaultRedirectURI, + policy.PasswordCheckLifetime, + policy.ExternalLoginCheckLifetime, + policy.MFAInitSkipLifetime, + policy.SecondFactorCheckLifetime, + policy.MultiFactorCheckLifetime, + )) + for _, factor := range policy.SecondFactors { + cmds = append(cmds, org.NewLoginPolicySecondFactorAddedEvent(ctx, &a.Aggregate, factor)) + } + for _, factor := range policy.MultiFactors { + cmds = append(cmds, org.NewLoginPolicyMultiFactorAddedEvent(ctx, &a.Aggregate, factor)) + } + for _, idp := range policy.IDPProviders { + cmds = append(cmds, org.NewIdentityProviderAddedEvent(ctx, &a.Aggregate, idp.ConfigID, idp.Type)) + } + return cmds, nil + }, nil + } +} + +func prepareChangeLoginPolicy(a *org.Aggregate, policy *ChangeLoginPolicy) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if ok := domain.ValidateDefaultRedirectURI(policy.DefaultRedirectURI); !ok { + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Sfd21", "Errors.Org.LoginPolicy.RedirectURIInvalid") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + wm := NewOrgLoginPolicyWriteModel(a.ID) + if err := queryAndReduce(ctx, filter, wm); err != nil { + return nil, err + } + if !wm.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "Org-M0sif", "Errors.Org.LoginPolicy.NotFound") + } + changedEvent, hasChanged := wm.NewChangedEvent(ctx, &a.Aggregate, + policy.AllowUsernamePassword, + policy.AllowRegister, + policy.AllowExternalIDP, + policy.ForceMFA, + policy.HidePasswordReset, + policy.IgnoreUnknownUsernames, + policy.AllowDomainDiscovery, + policy.DisableLoginWithEmail, + policy.DisableLoginWithPhone, + policy.PasswordlessType, + policy.DefaultRedirectURI, + policy.PasswordCheckLifetime, + policy.ExternalLoginCheckLifetime, + policy.MFAInitSkipLifetime, + policy.SecondFactorCheckLifetime, + policy.MultiFactorCheckLifetime) + if !hasChanged { + return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-5M9vdd", "Errors.Org.LoginPolicy.NotChanged") + } + return []eventstore.Command{changedEvent}, nil + }, nil + } +} + +func idpExists(ctx context.Context, filter preparation.FilterToQueryReducer, idp *AddLoginPolicyIDP) (bool, error) { + if idp.Type == domain.IdentityProviderTypeSystem { + return exists(ctx, filter, NewInstanceIDPConfigWriteModel(ctx, idp.ConfigID)) + } + return exists(ctx, filter, NewOrgIDPConfigWriteModel(idp.ConfigID, authz.GetCtxData(ctx).ResourceOwner)) +} diff --git a/internal/command/org_policy_login_model.go b/internal/command/org_policy_login_model.go index 4eaccbae5d..1f546ea99c 100644 --- a/internal/command/org_policy_login_model.go +++ b/internal/command/org_policy_login_model.go @@ -69,7 +69,9 @@ func (wm *OrgLoginPolicyWriteModel) NewChangedEvent( forceMFA, hidePasswordReset, ignoreUnknownUsernames, - allowDomainDiscovery bool, + allowDomainDiscovery, + disableLoginWithEmail, + disableLoginWithPhone bool, passwordlessType domain.PasswordlessType, defaultRedirectURI string, passwordCheckLifetime, @@ -122,6 +124,12 @@ func (wm *OrgLoginPolicyWriteModel) NewChangedEvent( if wm.DefaultRedirectURI != defaultRedirectURI { changes = append(changes, policy.ChangeDefaultRedirectURI(defaultRedirectURI)) } + if wm.DisableLoginWithEmail != disableLoginWithEmail { + changes = append(changes, policy.ChangeDisableLoginWithEmail(disableLoginWithEmail)) + } + if wm.DisableLoginWithPhone != disableLoginWithPhone { + changes = append(changes, policy.ChangeDisableLoginWithPhone(disableLoginWithPhone)) + } if len(changes) == 0 { return nil, false } diff --git a/internal/command/org_policy_login_test.go b/internal/command/org_policy_login_test.go index 9dfaeb0c49..e4a3c0a2f5 100644 --- a/internal/command/org_policy_login_test.go +++ b/internal/command/org_policy_login_test.go @@ -33,10 +33,10 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { type args struct { ctx context.Context orgID string - policy *domain.LoginPolicy + policy *AddLoginPolicy } type res struct { - want *domain.LoginPolicy + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -45,25 +45,6 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { 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{ @@ -80,6 +61,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { true, true, true, + false, + false, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -95,7 +78,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { args: args{ ctx: context.Background(), orgID: "org1", - policy: &domain.LoginPolicy{ + policy: &AddLoginPolicy{ AllowRegister: true, AllowUsernamePassword: true, AllowExternalIDP: true, @@ -133,6 +116,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -149,7 +134,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { args: args{ ctx: context.Background(), orgID: "org1", - policy: &domain.LoginPolicy{ + policy: &AddLoginPolicy{ AllowRegister: true, AllowUsernamePassword: true, AllowExternalIDP: true, @@ -157,6 +142,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { HidePasswordReset: true, IgnoreUnknownUsernames: true, AllowDomainDiscovery: true, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -167,25 +154,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { }, }, res: res{ - want: &domain.LoginPolicy{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - ResourceOwner: "org1", - }, - AllowRegister: true, - AllowUsernamePassword: true, - AllowExternalIDP: true, - ForceMFA: true, - HidePasswordReset: true, - IgnoreUnknownUsernames: true, - AllowDomainDiscovery: true, - PasswordlessType: domain.PasswordlessTypeAllowed, - DefaultRedirectURI: "https://example.com/redirect", - PasswordCheckLifetime: time.Hour * 1, - ExternalLoginCheckLifetime: time.Hour * 2, - MFAInitSkipLifetime: time.Hour * 3, - SecondFactorCheckLifetime: time.Hour * 4, - MultiFactorCheckLifetime: time.Hour * 5, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -194,13 +164,12 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { fields: fields{ eventstore: eventstoreExpect( t, - expectFilter(), ), }, args: args{ ctx: context.Background(), orgID: "org1", - policy: &domain.LoginPolicy{ + policy: &AddLoginPolicy{ AllowRegister: true, AllowUsernamePassword: true, AllowExternalIDP: true, @@ -208,6 +177,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { HidePasswordReset: true, IgnoreUnknownUsernames: true, AllowDomainDiscovery: true, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -240,6 +211,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -268,7 +241,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { args: args{ ctx: context.Background(), orgID: "org1", - policy: &domain.LoginPolicy{ + policy: &AddLoginPolicy{ AllowRegister: true, AllowUsernamePassword: true, AllowExternalIDP: true, @@ -276,6 +249,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { HidePasswordReset: true, IgnoreUnknownUsernames: true, AllowDomainDiscovery: true, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -288,25 +263,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { }, }, res: res{ - want: &domain.LoginPolicy{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - ResourceOwner: "org1", - }, - AllowRegister: true, - AllowUsernamePassword: true, - AllowExternalIDP: true, - ForceMFA: true, - HidePasswordReset: true, - IgnoreUnknownUsernames: true, - AllowDomainDiscovery: true, - PasswordlessType: domain.PasswordlessTypeAllowed, - DefaultRedirectURI: "https://example.com/redirect", - PasswordCheckLifetime: time.Hour * 1, - ExternalLoginCheckLifetime: time.Hour * 2, - MFAInitSkipLifetime: time.Hour * 3, - SecondFactorCheckLifetime: time.Hour * 4, - MultiFactorCheckLifetime: time.Hour * 5, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -322,7 +280,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { args: args{ ctx: context.Background(), orgID: "org1", - policy: &domain.LoginPolicy{ + policy: &AddLoginPolicy{ AllowRegister: true, AllowUsernamePassword: true, AllowExternalIDP: true, @@ -330,6 +288,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { HidePasswordReset: true, IgnoreUnknownUsernames: true, AllowDomainDiscovery: true, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -337,10 +297,10 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { MFAInitSkipLifetime: time.Hour * 3, SecondFactorCheckLifetime: time.Hour * 4, MultiFactorCheckLifetime: time.Hour * 5, - IDPProviders: []*domain.IDPProvider{ + IDPProviders: []*AddLoginPolicyIDP{ { - Type: domain.IdentityProviderTypeSystem, - IDPConfigID: "invalid", + Type: domain.IdentityProviderTypeSystem, + ConfigID: "invalid", }, }, }, @@ -379,6 +339,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -402,7 +364,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { args: args{ ctx: context.Background(), orgID: "org1", - policy: &domain.LoginPolicy{ + policy: &AddLoginPolicy{ AllowRegister: true, AllowUsernamePassword: true, AllowExternalIDP: true, @@ -410,6 +372,8 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { HidePasswordReset: true, IgnoreUnknownUsernames: true, AllowDomainDiscovery: true, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -417,34 +381,17 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { MFAInitSkipLifetime: time.Hour * 3, SecondFactorCheckLifetime: time.Hour * 4, MultiFactorCheckLifetime: time.Hour * 5, - IDPProviders: []*domain.IDPProvider{ + IDPProviders: []*AddLoginPolicyIDP{ { - Type: domain.IdentityProviderTypeSystem, - IDPConfigID: "config1", + Type: domain.IdentityProviderTypeSystem, + ConfigID: "config1", }, }, }, }, res: res{ - want: &domain.LoginPolicy{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - ResourceOwner: "org1", - }, - AllowRegister: true, - AllowUsernamePassword: true, - AllowExternalIDP: true, - ForceMFA: true, - HidePasswordReset: true, - IgnoreUnknownUsernames: true, - AllowDomainDiscovery: true, - PasswordlessType: domain.PasswordlessTypeAllowed, - DefaultRedirectURI: "https://example.com/redirect", - PasswordCheckLifetime: time.Hour * 1, - ExternalLoginCheckLifetime: time.Hour * 2, - MFAInitSkipLifetime: time.Hour * 3, - SecondFactorCheckLifetime: time.Hour * 4, - MultiFactorCheckLifetime: time.Hour * 5, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -475,10 +422,10 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { type args struct { ctx context.Context orgID string - policy *domain.LoginPolicy + policy *ChangeLoginPolicy } type res struct { - want *domain.LoginPolicy + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -487,30 +434,6 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { 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, - IgnoreUnknownUsernames: true, - AllowDomainDiscovery: true, - PasswordlessType: domain.PasswordlessTypeAllowed, - DefaultRedirectURI: "https://example.com/redirect", - }, - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, { name: "loginpolicy not existing, not found error", fields: fields{ @@ -522,13 +445,15 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { args: args{ ctx: context.Background(), orgID: "org1", - policy: &domain.LoginPolicy{ + policy: &ChangeLoginPolicy{ AllowRegister: true, AllowUsernamePassword: true, AllowExternalIDP: true, ForceMFA: true, IgnoreUnknownUsernames: true, AllowDomainDiscovery: true, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", }, @@ -553,6 +478,8 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -568,7 +495,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { args: args{ ctx: context.Background(), orgID: "org1", - policy: &domain.LoginPolicy{ + policy: &ChangeLoginPolicy{ AllowRegister: true, AllowUsernamePassword: true, AllowExternalIDP: true, @@ -576,6 +503,8 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { HidePasswordReset: true, IgnoreUnknownUsernames: true, AllowDomainDiscovery: true, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -605,6 +534,8 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -627,6 +558,8 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", &duration10, @@ -643,13 +576,15 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { args: args{ ctx: context.Background(), orgID: "org1", - policy: &domain.LoginPolicy{ + policy: &ChangeLoginPolicy{ AllowRegister: false, AllowUsernamePassword: false, AllowExternalIDP: false, ForceMFA: false, IgnoreUnknownUsernames: false, AllowDomainDiscovery: false, + DisableLoginWithEmail: false, + DisableLoginWithPhone: false, PasswordlessType: domain.PasswordlessTypeNotAllowed, DefaultRedirectURI: "", PasswordCheckLifetime: time.Hour * 10, @@ -660,25 +595,8 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { }, }, res: res{ - want: &domain.LoginPolicy{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - ResourceOwner: "org1", - }, - AllowRegister: false, - AllowUsernamePassword: false, - AllowExternalIDP: false, - ForceMFA: false, - HidePasswordReset: false, - IgnoreUnknownUsernames: false, - AllowDomainDiscovery: false, - PasswordlessType: domain.PasswordlessTypeNotAllowed, - DefaultRedirectURI: "", - PasswordCheckLifetime: time.Hour * 10, - ExternalLoginCheckLifetime: time.Hour * 20, - MFAInitSkipLifetime: time.Hour * 30, - SecondFactorCheckLifetime: time.Hour * 40, - MultiFactorCheckLifetime: time.Hour * 50, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -766,6 +684,8 @@ func TestCommandSide_RemoveLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -907,6 +827,8 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -949,6 +871,8 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -1011,6 +935,8 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -1176,6 +1102,8 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -1218,6 +1146,8 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -1272,6 +1202,8 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -1333,6 +1265,8 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -1402,6 +1336,8 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -2020,7 +1956,8 @@ func TestCommandSide_RemoveMultiFactorLoginPolicy(t *testing.T) { } } -func newLoginPolicyChangedEvent(ctx context.Context, orgID string, usernamePassword, register, externalIDP, mfa, passwordReset, ignoreUnknownUsernames, allowDomainDiscovery bool, +func newLoginPolicyChangedEvent(ctx context.Context, orgID string, + usernamePassword, register, externalIDP, mfa, passwordReset, ignoreUnknownUsernames, allowDomainDiscovery, disableLoginWithEmail, disableLoginWithPhone bool, passwordlessType domain.PasswordlessType, redirectURI string, passwordLifetime, externalLoginLifetime, mfaInitSkipLifetime, secondFactorLifetime, multiFactorLifetime *time.Duration) *org.LoginPolicyChangedEvent { @@ -2034,6 +1971,8 @@ func newLoginPolicyChangedEvent(ctx context.Context, orgID string, usernamePassw policy.ChangeAllowDomainDiscovery(allowDomainDiscovery), policy.ChangePasswordlessType(passwordlessType), policy.ChangeDefaultRedirectURI(redirectURI), + policy.ChangeDisableLoginWithEmail(disableLoginWithEmail), + policy.ChangeDisableLoginWithPhone(disableLoginWithPhone), } if passwordLifetime != nil { changes = append(changes, policy.ChangePasswordCheckLifetime(*passwordLifetime)) diff --git a/internal/command/policy_login_model.go b/internal/command/policy_login_model.go index 1c49fc6ab4..50ece0f2f5 100644 --- a/internal/command/policy_login_model.go +++ b/internal/command/policy_login_model.go @@ -18,6 +18,8 @@ type LoginPolicyWriteModel struct { HidePasswordReset bool IgnoreUnknownUsernames bool AllowDomainDiscovery bool + DisableLoginWithEmail bool + DisableLoginWithPhone bool PasswordlessType domain.PasswordlessType DefaultRedirectURI string PasswordCheckLifetime time.Duration @@ -40,6 +42,8 @@ func (wm *LoginPolicyWriteModel) Reduce() error { wm.HidePasswordReset = e.HidePasswordReset wm.IgnoreUnknownUsernames = e.IgnoreUnknownUsernames wm.AllowDomainDiscovery = e.AllowDomainDiscovery + wm.DisableLoginWithEmail = e.DisableLoginWithEmail + wm.DisableLoginWithPhone = e.DisableLoginWithPhone wm.DefaultRedirectURI = e.DefaultRedirectURI wm.PasswordCheckLifetime = e.PasswordCheckLifetime wm.ExternalLoginCheckLifetime = e.ExternalLoginCheckLifetime @@ -90,9 +94,19 @@ func (wm *LoginPolicyWriteModel) Reduce() error { if e.MultiFactorCheckLifetime != nil { wm.MultiFactorCheckLifetime = *e.MultiFactorCheckLifetime } + if e.DisableLoginWithEmail != nil { + wm.DisableLoginWithEmail = *e.DisableLoginWithEmail + } + if e.DisableLoginWithPhone != nil { + wm.DisableLoginWithPhone = *e.DisableLoginWithPhone + } case *policy.LoginPolicyRemovedEvent: wm.State = domain.PolicyStateRemoved } } return wm.WriteModel.Reduce() } + +func (wm *LoginPolicyWriteModel) Exists() bool { + return wm.State.Exists() +} diff --git a/internal/command/user_human_password_test.go b/internal/command/user_human_password_test.go index 3162aeedc7..883959f40a 100644 --- a/internal/command/user_human_password_test.go +++ b/internal/command/user_human_password_test.go @@ -1158,6 +1158,8 @@ func TestCommandSide_CheckPassword(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1196,6 +1198,8 @@ func TestCommandSide_CheckPassword(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1235,6 +1239,8 @@ func TestCommandSide_CheckPassword(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1290,6 +1296,8 @@ func TestCommandSide_CheckPassword(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1379,6 +1387,8 @@ func TestCommandSide_CheckPassword(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1475,6 +1485,8 @@ func TestCommandSide_CheckPassword(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index 2558e5945b..c19beaac05 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -1680,6 +1680,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1747,6 +1749,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1814,6 +1818,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1898,6 +1904,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -2040,6 +2048,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -2150,6 +2160,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -2254,6 +2266,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -2380,6 +2394,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, diff --git a/internal/domain/idp_config.go b/internal/domain/idp_config.go index 86dde0d18f..762d0da5d6 100644 --- a/internal/domain/idp_config.go +++ b/internal/domain/idp_config.go @@ -101,7 +101,7 @@ func (s IDPConfigState) Valid() bool { } func (s IDPConfigState) Exists() bool { - return s != IDPConfigStateUnspecified || s == IDPConfigStateRemoved + return s != IDPConfigStateUnspecified && s != IDPConfigStateRemoved } type IDPConfigStylingType int32 diff --git a/internal/domain/policy_login.go b/internal/domain/policy_login.go index b265d87094..823a51220c 100644 --- a/internal/domain/policy_login.go +++ b/internal/domain/policy_login.go @@ -28,6 +28,8 @@ type LoginPolicy struct { MFAInitSkipLifetime time.Duration SecondFactorCheckLifetime time.Duration MultiFactorCheckLifetime time.Duration + DisableLoginWithEmail bool + DisableLoginWithPhone bool } func ValidateDefaultRedirectURI(rawURL string) bool { diff --git a/internal/eventstore/handler/crdb/statement.go b/internal/eventstore/handler/crdb/statement.go index 1549289bc0..8601630cdd 100644 --- a/internal/eventstore/handler/crdb/statement.go +++ b/internal/eventstore/handler/crdb/statement.go @@ -123,7 +123,7 @@ func getUpdateCols(cols, conflictTarget []string) (updateCols, updateVals []stri func NewUpdateStatement(event eventstore.Event, values []handler.Column, conditions []handler.Condition, opts ...execOption) *handler.Statement { cols, params, args := columnsToQuery(values) - wheres, whereArgs := conditionsToWhere(conditions, len(params)) + wheres, whereArgs := conditionsToWhere(conditions, len(args)) args = append(args, whereArgs...) config := execConfig{ @@ -278,6 +278,13 @@ func NewArrayIntersectCol(column string, value interface{}) handler.Column { } } +func NewCopyCol(column, from string) handler.Column { + return handler.Column{ + Name: column, + Value: handler.NewCol(from, nil), + } +} + // NewCopyStatement creates a new upsert statement which updates a column from an existing row // cols represent the columns which are objective to change. // if the value of a col is empty the data will be copied from the selected row @@ -359,15 +366,22 @@ func columnsToQuery(cols []handler.Column) (names []string, parameters []string, names = make([]string, len(cols)) values = make([]interface{}, len(cols)) parameters = make([]string, len(cols)) + var parameterIndex int for i, col := range cols { names[i] = col.Name - values[i] = col.Value - parameters[i] = "$" + strconv.Itoa(i+1) + if c, ok := col.Value.(handler.Column); ok { + parameters[i] = c.Name + continue + } else { + values[parameterIndex] = col.Value + } + parameters[i] = "$" + strconv.Itoa(parameterIndex+1) if col.ParameterOpt != nil { parameters[i] = col.ParameterOpt(parameters[i]) } + parameterIndex++ } - return names, parameters, values + return names, parameters, values[:parameterIndex] } func conditionsToWhere(cols []handler.Condition, paramOffset int) (wheres []string, values []interface{}) { diff --git a/internal/eventstore/handler/crdb/statement_test.go b/internal/eventstore/handler/crdb/statement_test.go index efc2704936..fd9f451d94 100644 --- a/internal/eventstore/handler/crdb/statement_test.go +++ b/internal/eventstore/handler/crdb/statement_test.go @@ -1352,6 +1352,32 @@ func Test_columnsToQuery(t *testing.T) { values: []interface{}{1, 3.14}, }, }, + { + name: "with copy column", + args: args{ + cols: []handler.Column{ + { + Name: "col1", + Value: 1, + }, + { + Name: "col2", + Value: handler.Column{ + Name: "col1", + }, + }, + { + Name: "col3", + Value: "something", + }, + }, + }, + want: want{ + names: []string{"col1", "col2", "col3"}, + params: []string{"$1", "col1", "$2"}, + values: []interface{}{1, "something"}, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/notification/projection.go b/internal/notification/projection.go index 78fbfb3f06..9f0aefe5ad 100644 --- a/internal/notification/projection.go +++ b/internal/notification/projection.go @@ -169,7 +169,7 @@ func (p *notificationsProjection) reduceInitCodeAdded(event eventstore.Event) (* return nil, err } - notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID) + notifyUser, err := p.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) if err != nil { return nil, err } @@ -232,7 +232,7 @@ func (p *notificationsProjection) reduceEmailCodeAdded(event eventstore.Event) ( return nil, err } - notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID) + notifyUser, err := p.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) if err != nil { return nil, err } @@ -295,7 +295,7 @@ func (p *notificationsProjection) reducePasswordCodeAdded(event eventstore.Event return nil, err } - notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID) + notifyUser, err := p.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) if err != nil { return nil, err } @@ -366,7 +366,7 @@ func (p *notificationsProjection) reduceDomainClaimed(event eventstore.Event) (* return nil, err } - notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID) + notifyUser, err := p.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) if err != nil { return nil, err } @@ -427,7 +427,7 @@ func (p *notificationsProjection) reducePasswordlessCodeRequested(event eventsto return nil, err } - notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID) + notifyUser, err := p.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) if err != nil { return nil, err } @@ -485,7 +485,7 @@ func (p *notificationsProjection) reducePhoneCodeAdded(event eventstore.Event) ( return nil, err } - notifyUser, err := p.queries.GeNotifyUser(ctx, true, e.Aggregate().ID) + notifyUser, err := p.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID) if err != nil { return nil, err } diff --git a/internal/query/iam_member_test.go b/internal/query/iam_member_test.go index e4755bb2a9..52877242cc 100644 --- a/internal/query/iam_member_test.go +++ b/internal/query/iam_member_test.go @@ -20,18 +20,18 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names.login_name" + - ", projections.users3_humans.email" + - ", projections.users3_humans.first_name" + - ", projections.users3_humans.last_name" + - ", projections.users3_humans.display_name" + - ", projections.users3_machines.name" + - ", projections.users3_humans.avatar_key" + + ", projections.users4_humans.email" + + ", projections.users4_humans.first_name" + + ", projections.users4_humans.last_name" + + ", projections.users4_humans.display_name" + + ", projections.users4_machines.name" + + ", projections.users4_humans.avatar_key" + ", COUNT(*) OVER () " + "FROM projections.instance_members2 AS members " + - "LEFT JOIN projections.users3_humans " + - "ON members.user_id = projections.users3_humans.user_id " + - "LEFT JOIN projections.users3_machines " + - "ON members.user_id = projections.users3_machines.user_id " + + "LEFT JOIN projections.users4_humans " + + "ON members.user_id = projections.users4_humans.user_id " + + "LEFT JOIN projections.users4_machines " + + "ON members.user_id = projections.users4_machines.user_id " + "LEFT JOIN projections.login_names " + "ON members.user_id = projections.login_names.user_id " + "WHERE projections.login_names.is_primary = $1") diff --git a/internal/query/login_policy.go b/internal/query/login_policy.go index bd7326a3b6..d25117e8d7 100644 --- a/internal/query/login_policy.go +++ b/internal/query/login_policy.go @@ -31,6 +31,8 @@ type LoginPolicy struct { HidePasswordReset bool IgnoreUnknownUsernames bool AllowDomainDiscovery bool + DisableLoginWithEmail bool + DisableLoginWithPhone bool DefaultRedirectURI string PasswordCheckLifetime time.Duration ExternalLoginCheckLifetime time.Duration @@ -118,6 +120,14 @@ var ( name: projection.AllowDomainDiscovery, table: loginPolicyTable, } + LoginPolicyColumnDisableLoginWithEmail = Column{ + name: projection.DisableLoginWithEmail, + table: loginPolicyTable, + } + LoginPolicyColumnDisableLoginWithPhone = Column{ + name: projection.DisableLoginWithPhone, + table: loginPolicyTable, + } LoginPolicyColumnDefaultRedirectURI = Column{ name: projection.DefaultRedirectURI, table: loginPolicyTable, @@ -311,6 +321,8 @@ func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Rows) (*LoginPolicy, LoginPolicyColumnHidePasswordReset.identifier(), LoginPolicyColumnIgnoreUnknownUsernames.identifier(), LoginPolicyColumnAllowDomainDiscovery.identifier(), + LoginPolicyColumnDisableLoginWithEmail.identifier(), + LoginPolicyColumnDisableLoginWithPhone.identifier(), LoginPolicyColumnDefaultRedirectURI.identifier(), LoginPolicyColumnPasswordCheckLifetime.identifier(), LoginPolicyColumnExternalLoginCheckLifetime.identifier(), @@ -350,6 +362,8 @@ func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Rows) (*LoginPolicy, &p.HidePasswordReset, &p.IgnoreUnknownUsernames, &p.AllowDomainDiscovery, + &p.DisableLoginWithEmail, + &p.DisableLoginWithPhone, &defaultRedirectURI, &p.PasswordCheckLifetime, &p.ExternalLoginCheckLifetime, diff --git a/internal/query/login_policy_test.go b/internal/query/login_policy_test.go index 221e4024fd..c5b05ed032 100644 --- a/internal/query/login_policy_test.go +++ b/internal/query/login_policy_test.go @@ -30,33 +30,35 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicyQuery, want: want{ sqlExpectations: mockQueries( - regexp.QuoteMeta(`SELECT projections.login_policies2.aggregate_id,`+ - ` projections.login_policies2.creation_date,`+ - ` projections.login_policies2.change_date,`+ - ` projections.login_policies2.sequence,`+ - ` projections.login_policies2.allow_register,`+ - ` projections.login_policies2.allow_username_password,`+ - ` projections.login_policies2.allow_external_idps,`+ - ` projections.login_policies2.force_mfa,`+ - ` projections.login_policies2.second_factors,`+ - ` projections.login_policies2.multi_factors,`+ - ` projections.login_policies2.passwordless_type,`+ - ` projections.login_policies2.is_default,`+ - ` projections.login_policies2.hide_password_reset,`+ - ` projections.login_policies2.ignore_unknown_usernames,`+ - ` projections.login_policies2.allow_domain_discovery,`+ - ` projections.login_policies2.default_redirect_uri,`+ - ` projections.login_policies2.password_check_lifetime,`+ - ` projections.login_policies2.external_login_check_lifetime,`+ - ` projections.login_policies2.mfa_init_skip_lifetime,`+ - ` projections.login_policies2.second_factor_check_lifetime,`+ - ` projections.login_policies2.multi_factor_check_lifetime,`+ + regexp.QuoteMeta(`SELECT projections.login_policies3.aggregate_id,`+ + ` projections.login_policies3.creation_date,`+ + ` projections.login_policies3.change_date,`+ + ` projections.login_policies3.sequence,`+ + ` projections.login_policies3.allow_register,`+ + ` projections.login_policies3.allow_username_password,`+ + ` projections.login_policies3.allow_external_idps,`+ + ` projections.login_policies3.force_mfa,`+ + ` projections.login_policies3.second_factors,`+ + ` projections.login_policies3.multi_factors,`+ + ` projections.login_policies3.passwordless_type,`+ + ` projections.login_policies3.is_default,`+ + ` projections.login_policies3.hide_password_reset,`+ + ` projections.login_policies3.ignore_unknown_usernames,`+ + ` projections.login_policies3.allow_domain_discovery,`+ + ` projections.login_policies3.disable_login_with_email,`+ + ` projections.login_policies3.disable_login_with_phone,`+ + ` projections.login_policies3.default_redirect_uri,`+ + ` projections.login_policies3.password_check_lifetime,`+ + ` projections.login_policies3.external_login_check_lifetime,`+ + ` projections.login_policies3.mfa_init_skip_lifetime,`+ + ` projections.login_policies3.second_factor_check_lifetime,`+ + ` projections.login_policies3.multi_factor_check_lifetime,`+ ` projections.idp_login_policy_links3.idp_id,`+ ` projections.idps2.name,`+ ` projections.idps2.type`+ - ` FROM projections.login_policies2`+ + ` FROM projections.login_policies3`+ ` LEFT JOIN projections.idp_login_policy_links3 ON `+ - ` projections.login_policies2.aggregate_id = projections.idp_login_policy_links3.aggregate_id`+ + ` projections.login_policies3.aggregate_id = projections.idp_login_policy_links3.aggregate_id`+ ` LEFT JOIN projections.idps2 ON`+ ` projections.idp_login_policy_links3.idp_id = projections.idps2.id`), nil, @@ -76,33 +78,35 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicyQuery, want: want{ sqlExpectations: mockQuery( - regexp.QuoteMeta(`SELECT projections.login_policies2.aggregate_id,`+ - ` projections.login_policies2.creation_date,`+ - ` projections.login_policies2.change_date,`+ - ` projections.login_policies2.sequence,`+ - ` projections.login_policies2.allow_register,`+ - ` projections.login_policies2.allow_username_password,`+ - ` projections.login_policies2.allow_external_idps,`+ - ` projections.login_policies2.force_mfa,`+ - ` projections.login_policies2.second_factors,`+ - ` projections.login_policies2.multi_factors,`+ - ` projections.login_policies2.passwordless_type,`+ - ` projections.login_policies2.is_default,`+ - ` projections.login_policies2.hide_password_reset,`+ - ` projections.login_policies2.ignore_unknown_usernames,`+ - ` projections.login_policies2.allow_domain_discovery,`+ - ` projections.login_policies2.default_redirect_uri,`+ - ` projections.login_policies2.password_check_lifetime,`+ - ` projections.login_policies2.external_login_check_lifetime,`+ - ` projections.login_policies2.mfa_init_skip_lifetime,`+ - ` projections.login_policies2.second_factor_check_lifetime,`+ - ` projections.login_policies2.multi_factor_check_lifetime,`+ + regexp.QuoteMeta(`SELECT projections.login_policies3.aggregate_id,`+ + ` projections.login_policies3.creation_date,`+ + ` projections.login_policies3.change_date,`+ + ` projections.login_policies3.sequence,`+ + ` projections.login_policies3.allow_register,`+ + ` projections.login_policies3.allow_username_password,`+ + ` projections.login_policies3.allow_external_idps,`+ + ` projections.login_policies3.force_mfa,`+ + ` projections.login_policies3.second_factors,`+ + ` projections.login_policies3.multi_factors,`+ + ` projections.login_policies3.passwordless_type,`+ + ` projections.login_policies3.is_default,`+ + ` projections.login_policies3.hide_password_reset,`+ + ` projections.login_policies3.ignore_unknown_usernames,`+ + ` projections.login_policies3.allow_domain_discovery,`+ + ` projections.login_policies3.disable_login_with_email,`+ + ` projections.login_policies3.disable_login_with_phone,`+ + ` projections.login_policies3.default_redirect_uri,`+ + ` projections.login_policies3.password_check_lifetime,`+ + ` projections.login_policies3.external_login_check_lifetime,`+ + ` projections.login_policies3.mfa_init_skip_lifetime,`+ + ` projections.login_policies3.second_factor_check_lifetime,`+ + ` projections.login_policies3.multi_factor_check_lifetime,`+ ` projections.idp_login_policy_links3.idp_id,`+ ` projections.idps2.name,`+ ` projections.idps2.type`+ - ` FROM projections.login_policies2`+ + ` FROM projections.login_policies3`+ ` LEFT JOIN projections.idp_login_policy_links3 ON `+ - ` projections.login_policies2.aggregate_id = projections.idp_login_policy_links3.aggregate_id`+ + ` projections.login_policies3.aggregate_id = projections.idp_login_policy_links3.aggregate_id`+ ` LEFT JOIN projections.idps2 ON`+ ` projections.idp_login_policy_links3.idp_id = projections.idps2.id`), []string{ @@ -121,6 +125,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { "hide_password_reset", "ignore_unknown_usernames", "allow_domain_discovery", + "disable_login_with_email", + "disable_login_with_phone", "default_redirect_uri", "password_check_lifetime", "external_login_check_lifetime", @@ -147,6 +153,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { true, true, true, + true, + true, "https://example.com/redirect", time.Hour * 2, time.Hour * 2, @@ -175,6 +183,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { HidePasswordReset: true, IgnoreUnknownUsernames: true, AllowDomainDiscovery: true, + DisableLoginWithEmail: true, + DisableLoginWithPhone: true, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 2, ExternalLoginCheckLifetime: time.Hour * 2, @@ -195,33 +205,35 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicyQuery, want: want{ sqlExpectations: mockQueryErr( - regexp.QuoteMeta(`SELECT projections.login_policies2.aggregate_id,`+ - ` projections.login_policies2.creation_date,`+ - ` projections.login_policies2.change_date,`+ - ` projections.login_policies2.sequence,`+ - ` projections.login_policies2.allow_register,`+ - ` projections.login_policies2.allow_username_password,`+ - ` projections.login_policies2.allow_external_idps,`+ - ` projections.login_policies2.force_mfa,`+ - ` projections.login_policies2.second_factors,`+ - ` projections.login_policies2.multi_factors,`+ - ` projections.login_policies2.passwordless_type,`+ - ` projections.login_policies2.is_default,`+ - ` projections.login_policies2.hide_password_reset,`+ - ` projections.login_policies2.ignore_unknown_usernames,`+ - ` projections.login_policies2.allow_domain_discovery,`+ - ` projections.login_policies2.default_redirect_uri,`+ - ` projections.login_policies2.password_check_lifetime,`+ - ` projections.login_policies2.external_login_check_lifetime,`+ - ` projections.login_policies2.mfa_init_skip_lifetime,`+ - ` projections.login_policies2.second_factor_check_lifetime,`+ - ` projections.login_policies2.multi_factor_check_lifetime,`+ + regexp.QuoteMeta(`SELECT projections.login_policies3.aggregate_id,`+ + ` projections.login_policies3.creation_date,`+ + ` projections.login_policies3.change_date,`+ + ` projections.login_policies3.sequence,`+ + ` projections.login_policies3.allow_register,`+ + ` projections.login_policies3.allow_username_password,`+ + ` projections.login_policies3.allow_external_idps,`+ + ` projections.login_policies3.force_mfa,`+ + ` projections.login_policies3.second_factors,`+ + ` projections.login_policies3.multi_factors,`+ + ` projections.login_policies3.passwordless_type,`+ + ` projections.login_policies3.is_default,`+ + ` projections.login_policies3.hide_password_reset,`+ + ` projections.login_policies3.ignore_unknown_usernames,`+ + ` projections.login_policies3.allow_domain_discovery,`+ + ` projections.login_policies3.disable_login_with_email,`+ + ` projections.login_policies3.disable_login_with_phone,`+ + ` projections.login_policies3.default_redirect_uri,`+ + ` projections.login_policies3.password_check_lifetime,`+ + ` projections.login_policies3.external_login_check_lifetime,`+ + ` projections.login_policies3.mfa_init_skip_lifetime,`+ + ` projections.login_policies3.second_factor_check_lifetime,`+ + ` projections.login_policies3.multi_factor_check_lifetime,`+ ` projections.idp_login_policy_links3.idp_id,`+ ` projections.idps2.name,`+ ` projections.idps2.type`+ - ` FROM projections.login_policies2`+ + ` FROM projections.login_policies3`+ ` LEFT JOIN projections.idp_login_policy_links3 ON `+ - ` projections.login_policies2.aggregate_id = projections.idp_login_policy_links3.aggregate_id`+ + ` projections.login_policies3.aggregate_id = projections.idp_login_policy_links3.aggregate_id`+ ` LEFT JOIN projections.idps2 ON`+ ` projections.idp_login_policy_links3.idp_id = projections.idps2.id`), sql.ErrConnDone, @@ -240,8 +252,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicy2FAsQuery, want: want{ sqlExpectations: mockQuery( - regexp.QuoteMeta(`SELECT projections.login_policies2.second_factors`+ - ` FROM projections.login_policies2`), + regexp.QuoteMeta(`SELECT projections.login_policies3.second_factors`+ + ` FROM projections.login_policies3`), []string{ "second_factors", }, @@ -261,8 +273,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicy2FAsQuery, want: want{ sqlExpectations: mockQuery( - regexp.QuoteMeta(`SELECT projections.login_policies2.second_factors`+ - ` FROM projections.login_policies2`), + regexp.QuoteMeta(`SELECT projections.login_policies3.second_factors`+ + ` FROM projections.login_policies3`), []string{ "second_factors", }, @@ -283,8 +295,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicy2FAsQuery, want: want{ sqlExpectations: mockQuery( - regexp.QuoteMeta(`SELECT projections.login_policies2.second_factors`+ - ` FROM projections.login_policies2`), + regexp.QuoteMeta(`SELECT projections.login_policies3.second_factors`+ + ` FROM projections.login_policies3`), []string{ "second_factors", }, @@ -300,8 +312,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicy2FAsQuery, want: want{ sqlExpectations: mockQueryErr( - regexp.QuoteMeta(`SELECT projections.login_policies2.second_factors`+ - ` FROM projections.login_policies2`), + regexp.QuoteMeta(`SELECT projections.login_policies3.second_factors`+ + ` FROM projections.login_policies3`), sql.ErrConnDone, ), err: func(err error) (error, bool) { @@ -318,8 +330,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicyMFAsQuery, want: want{ sqlExpectations: mockQuery( - regexp.QuoteMeta(`SELECT projections.login_policies2.multi_factors`+ - ` FROM projections.login_policies2`), + regexp.QuoteMeta(`SELECT projections.login_policies3.multi_factors`+ + ` FROM projections.login_policies3`), []string{ "multi_factors", }, @@ -339,8 +351,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicyMFAsQuery, want: want{ sqlExpectations: mockQuery( - regexp.QuoteMeta(`SELECT projections.login_policies2.multi_factors`+ - ` FROM projections.login_policies2`), + regexp.QuoteMeta(`SELECT projections.login_policies3.multi_factors`+ + ` FROM projections.login_policies3`), []string{ "multi_factors", }, @@ -361,8 +373,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicyMFAsQuery, want: want{ sqlExpectations: mockQuery( - regexp.QuoteMeta(`SELECT projections.login_policies2.multi_factors`+ - ` FROM projections.login_policies2`), + regexp.QuoteMeta(`SELECT projections.login_policies3.multi_factors`+ + ` FROM projections.login_policies3`), []string{ "multi_factors", }, @@ -378,8 +390,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicyMFAsQuery, want: want{ sqlExpectations: mockQueryErr( - regexp.QuoteMeta(`SELECT projections.login_policies2.multi_factors`+ - ` FROM projections.login_policies2`), + regexp.QuoteMeta(`SELECT projections.login_policies3.multi_factors`+ + ` FROM projections.login_policies3`), sql.ErrConnDone, ), err: func(err error) (error, bool) { diff --git a/internal/query/org_member_test.go b/internal/query/org_member_test.go index 185ed451c5..c0e683da59 100644 --- a/internal/query/org_member_test.go +++ b/internal/query/org_member_test.go @@ -20,18 +20,18 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names.login_name" + - ", projections.users3_humans.email" + - ", projections.users3_humans.first_name" + - ", projections.users3_humans.last_name" + - ", projections.users3_humans.display_name" + - ", projections.users3_machines.name" + - ", projections.users3_humans.avatar_key" + + ", projections.users4_humans.email" + + ", projections.users4_humans.first_name" + + ", projections.users4_humans.last_name" + + ", projections.users4_humans.display_name" + + ", projections.users4_machines.name" + + ", projections.users4_humans.avatar_key" + ", COUNT(*) OVER () " + "FROM projections.org_members2 AS members " + - "LEFT JOIN projections.users3_humans " + - "ON members.user_id = projections.users3_humans.user_id " + - "LEFT JOIN projections.users3_machines " + - "ON members.user_id = projections.users3_machines.user_id " + + "LEFT JOIN projections.users4_humans " + + "ON members.user_id = projections.users4_humans.user_id " + + "LEFT JOIN projections.users4_machines " + + "ON members.user_id = projections.users4_machines.user_id " + "LEFT JOIN projections.login_names " + "ON members.user_id = projections.login_names.user_id " + "WHERE projections.login_names.is_primary = $1") diff --git a/internal/query/project_grant_member_test.go b/internal/query/project_grant_member_test.go index 3ca6783cc3..40ec90bf02 100644 --- a/internal/query/project_grant_member_test.go +++ b/internal/query/project_grant_member_test.go @@ -20,18 +20,18 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names.login_name" + - ", projections.users3_humans.email" + - ", projections.users3_humans.first_name" + - ", projections.users3_humans.last_name" + - ", projections.users3_humans.display_name" + - ", projections.users3_machines.name" + - ", projections.users3_humans.avatar_key" + + ", projections.users4_humans.email" + + ", projections.users4_humans.first_name" + + ", projections.users4_humans.last_name" + + ", projections.users4_humans.display_name" + + ", projections.users4_machines.name" + + ", projections.users4_humans.avatar_key" + ", COUNT(*) OVER () " + "FROM projections.project_grant_members2 AS members " + - "LEFT JOIN projections.users3_humans " + - "ON members.user_id = projections.users3_humans.user_id " + - "LEFT JOIN projections.users3_machines " + - "ON members.user_id = projections.users3_machines.user_id " + + "LEFT JOIN projections.users4_humans " + + "ON members.user_id = projections.users4_humans.user_id " + + "LEFT JOIN projections.users4_machines " + + "ON members.user_id = projections.users4_machines.user_id " + "LEFT JOIN projections.login_names " + "ON members.user_id = projections.login_names.user_id " + "LEFT JOIN projections.project_grants2 " + diff --git a/internal/query/project_member_test.go b/internal/query/project_member_test.go index 610b976b0f..c894857279 100644 --- a/internal/query/project_member_test.go +++ b/internal/query/project_member_test.go @@ -20,18 +20,18 @@ var ( ", members.user_id" + ", members.roles" + ", projections.login_names.login_name" + - ", projections.users3_humans.email" + - ", projections.users3_humans.first_name" + - ", projections.users3_humans.last_name" + - ", projections.users3_humans.display_name" + - ", projections.users3_machines.name" + - ", projections.users3_humans.avatar_key" + + ", projections.users4_humans.email" + + ", projections.users4_humans.first_name" + + ", projections.users4_humans.last_name" + + ", projections.users4_humans.display_name" + + ", projections.users4_machines.name" + + ", projections.users4_humans.avatar_key" + ", COUNT(*) OVER () " + "FROM projections.project_members2 AS members " + - "LEFT JOIN projections.users3_humans " + - "ON members.user_id = projections.users3_humans.user_id " + - "LEFT JOIN projections.users3_machines " + - "ON members.user_id = projections.users3_machines.user_id " + + "LEFT JOIN projections.users4_humans " + + "ON members.user_id = projections.users4_humans.user_id " + + "LEFT JOIN projections.users4_machines " + + "ON members.user_id = projections.users4_machines.user_id " + "LEFT JOIN projections.login_names " + "ON members.user_id = projections.login_names.user_id " + "WHERE projections.login_names.is_primary = $1") diff --git a/internal/query/projection/login_policy.go b/internal/query/projection/login_policy.go index 0900d90269..9ba12ec800 100644 --- a/internal/query/projection/login_policy.go +++ b/internal/query/projection/login_policy.go @@ -13,7 +13,7 @@ import ( ) const ( - LoginPolicyTable = "projections.login_policies2" + LoginPolicyTable = "projections.login_policies3" LoginPolicyIDCol = "aggregate_id" LoginPolicyInstanceIDCol = "instance_id" @@ -31,6 +31,8 @@ const ( LoginPolicyHidePWResetCol = "hide_password_reset" IgnoreUnknownUsernames = "ignore_unknown_usernames" AllowDomainDiscovery = "allow_domain_discovery" + DisableLoginWithEmail = "disable_login_with_email" + DisableLoginWithPhone = "disable_login_with_phone" DefaultRedirectURI = "default_redirect_uri" PasswordCheckLifetimeCol = "password_check_lifetime" ExternalLoginCheckLifetimeCol = "external_login_check_lifetime" @@ -65,6 +67,8 @@ func newLoginPolicyProjection(ctx context.Context, config crdb.StatementHandlerC crdb.NewColumn(LoginPolicyHidePWResetCol, crdb.ColumnTypeBool), crdb.NewColumn(IgnoreUnknownUsernames, crdb.ColumnTypeBool), crdb.NewColumn(AllowDomainDiscovery, crdb.ColumnTypeBool), + crdb.NewColumn(DisableLoginWithEmail, crdb.ColumnTypeBool), + crdb.NewColumn(DisableLoginWithPhone, crdb.ColumnTypeBool), crdb.NewColumn(DefaultRedirectURI, crdb.ColumnTypeText, crdb.Nullable()), crdb.NewColumn(PasswordCheckLifetimeCol, crdb.ColumnTypeInt64), crdb.NewColumn(ExternalLoginCheckLifetimeCol, crdb.ColumnTypeInt64), @@ -175,6 +179,8 @@ func (p *loginPolicyProjection) reduceLoginPolicyAdded(event eventstore.Event) ( handler.NewCol(LoginPolicyHidePWResetCol, policyEvent.HidePasswordReset), handler.NewCol(IgnoreUnknownUsernames, policyEvent.IgnoreUnknownUsernames), handler.NewCol(AllowDomainDiscovery, policyEvent.AllowDomainDiscovery), + handler.NewCol(DisableLoginWithEmail, policyEvent.DisableLoginWithEmail), + handler.NewCol(DisableLoginWithPhone, policyEvent.DisableLoginWithPhone), handler.NewCol(DefaultRedirectURI, policyEvent.DefaultRedirectURI), handler.NewCol(PasswordCheckLifetimeCol, policyEvent.PasswordCheckLifetime), handler.NewCol(ExternalLoginCheckLifetimeCol, policyEvent.ExternalLoginCheckLifetime), @@ -223,6 +229,12 @@ func (p *loginPolicyProjection) reduceLoginPolicyChanged(event eventstore.Event) if policyEvent.AllowDomainDiscovery != nil { cols = append(cols, handler.NewCol(AllowDomainDiscovery, *policyEvent.AllowDomainDiscovery)) } + if policyEvent.DisableLoginWithEmail != nil { + cols = append(cols, handler.NewCol(DisableLoginWithEmail, *policyEvent.DisableLoginWithEmail)) + } + if policyEvent.DisableLoginWithPhone != nil { + cols = append(cols, handler.NewCol(DisableLoginWithPhone, *policyEvent.DisableLoginWithPhone)) + } if policyEvent.DefaultRedirectURI != nil { cols = append(cols, handler.NewCol(DefaultRedirectURI, *policyEvent.DefaultRedirectURI)) } diff --git a/internal/query/projection/login_policy_test.go b/internal/query/projection/login_policy_test.go index cecec6f2f0..ef5f5a33f1 100644 --- a/internal/query/projection/login_policy_test.go +++ b/internal/query/projection/login_policy_test.go @@ -37,6 +37,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { "hidePasswordReset": true, "ignoreUnknownUsernames": true, "allowDomainDiscovery": true, + "disableLoginWithEmail": true, + "disableLoginWithPhone": true, "passwordlessType": 1, "defaultRedirectURI": "https://example.com/redirect", "passwordCheckLifetime": 10000000, @@ -56,7 +58,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.login_policies2 (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)", + expectedStmt: "INSERT INTO projections.login_policies3 (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -72,6 +74,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { true, true, true, + true, + true, "https://example.com/redirect", time.Millisecond * 10, time.Millisecond * 10, @@ -99,6 +103,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { "hidePasswordReset": true, "ignoreUnknownUsernames": true, "allowDomainDiscovery": true, + "disableLoginWithEmail": true, + "disableLoginWithPhone": true, "passwordlessType": 1, "defaultRedirectURI": "https://example.com/redirect", "passwordCheckLifetime": 10000000, @@ -117,7 +123,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) WHERE (aggregate_id = $17)", + expectedStmt: "UPDATE projections.login_policies3 SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) WHERE (aggregate_id = $19)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -129,6 +135,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { true, true, true, + true, + true, "https://example.com/redirect", time.Millisecond * 10, time.Millisecond * 10, @@ -162,7 +170,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies3 SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -194,7 +202,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies3 SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -224,7 +232,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.login_policies2 WHERE (aggregate_id = $1)", + expectedStmt: "DELETE FROM projections.login_policies3 WHERE (aggregate_id = $1)", expectedArgs: []interface{}{ "agg-id", }, @@ -253,7 +261,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies3 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -285,7 +293,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies3 SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -312,6 +320,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { "hidePasswordReset": true, "ignoreUnknownUsernames": true, "allowDomainDiscovery": true, + "disableLoginWithEmail": true, + "disableLoginWithPhone": true, "passwordlessType": 1, "defaultRedirectURI": "https://example.com/redirect", "passwordCheckLifetime": 10000000, @@ -330,7 +340,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.login_policies2 (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)", + expectedStmt: "INSERT INTO projections.login_policies3 (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -346,6 +356,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { true, true, true, + true, + true, "https://example.com/redirect", time.Millisecond * 10, time.Millisecond * 10, @@ -373,6 +385,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { "hidePasswordReset": true, "ignoreUnknownUsernames": true, "allowDomainDiscovery": true, + "disableLoginWithEmail": true, + "disableLoginWithPhone": true, "passwordlessType": 1, "defaultRedirectURI": "https://example.com/redirect" }`), @@ -386,7 +400,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, default_redirect_uri) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) WHERE (aggregate_id = $12)", + expectedStmt: "UPDATE projections.login_policies3 SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, hide_password_reset, ignore_unknown_usernames, allow_domain_discovery, disable_login_with_email, disable_login_with_phone, default_redirect_uri) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) WHERE (aggregate_id = $14)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -398,6 +412,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { true, true, true, + true, + true, "https://example.com/redirect", "agg-id", }, @@ -426,7 +442,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies3 SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -458,7 +474,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies3 SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -490,7 +506,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies3 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -522,7 +538,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies3 SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), diff --git a/internal/query/projection/user.go b/internal/query/projection/user.go index 9d1c72c615..6f646d46ac 100644 --- a/internal/query/projection/user.go +++ b/internal/query/projection/user.go @@ -17,7 +17,7 @@ type userProjection struct { } const ( - UserTable = "projections.users3" + UserTable = "projections.users4" UserHumanTable = UserTable + "_" + UserHumanSuffix UserMachineTable = UserTable + "_" + UserMachineSuffix UserNotifyTable = UserTable + "_" + UserNotifySuffix @@ -88,9 +88,9 @@ func newUserProjection(ctx context.Context, config crdb.StatementHandlerConfig) crdb.NewColumn(UserTypeCol, crdb.ColumnTypeEnum), }, crdb.NewPrimaryKey(UserIDCol, UserInstanceIDCol), - crdb.WithIndex(crdb.NewIndex("username_idx", []string{UserUsernameCol})), - crdb.WithIndex(crdb.NewIndex("user_ro_idx", []string{UserResourceOwnerCol})), - crdb.WithConstraint(crdb.NewConstraint("user_id_unique", []string{UserIDCol})), + crdb.WithIndex(crdb.NewIndex("username_idx4", []string{UserUsernameCol})), + crdb.WithIndex(crdb.NewIndex("user_ro_idx4", []string{UserResourceOwnerCol})), + crdb.WithConstraint(crdb.NewConstraint("user_id_unique4", []string{UserIDCol})), ), crdb.NewSuffixedTable([]*crdb.Column{ crdb.NewColumn(HumanUserIDCol, crdb.ColumnTypeText), @@ -109,7 +109,7 @@ func newUserProjection(ctx context.Context, config crdb.StatementHandlerConfig) }, crdb.NewPrimaryKey(HumanUserIDCol, HumanUserInstanceIDCol), UserHumanSuffix, - crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys("fk_human_ref_user")), + crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys("fk_human_ref_user4")), ), crdb.NewSuffixedTable([]*crdb.Column{ crdb.NewColumn(MachineUserIDCol, crdb.ColumnTypeText), @@ -119,7 +119,7 @@ func newUserProjection(ctx context.Context, config crdb.StatementHandlerConfig) }, crdb.NewPrimaryKey(MachineUserIDCol, MachineUserInstanceIDCol), UserMachineSuffix, - crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys("fk_machine_ref_user")), + crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys("fk_machine_ref_user4")), ), crdb.NewSuffixedTable([]*crdb.Column{ crdb.NewColumn(NotifyUserIDCol, crdb.ColumnTypeText), @@ -132,7 +132,7 @@ func newUserProjection(ctx context.Context, config crdb.StatementHandlerConfig) }, crdb.NewPrimaryKey(NotifyUserIDCol, NotifyInstanceIDCol), UserNotifySuffix, - crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys("fk_notify_ref_user")), + crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys("fk_notify_ref_user4")), ), ) p.StatementHandler = crdb.NewStatementHandler(ctx, config) @@ -708,20 +708,9 @@ func (p *userProjection) reduceHumanPhoneVerified(event eventstore.Event) (*hand }, crdb.WithTableSuffix(UserHumanSuffix), ), - crdb.AddCopyStatement( + crdb.AddUpdateStatement( []handler.Column{ - handler.NewCol(NotifyUserIDCol, nil), - handler.NewCol(NotifyInstanceIDCol, nil), - }, - []handler.Column{ - handler.NewCol(NotifyUserIDCol, nil), - handler.NewCol(NotifyInstanceIDCol, nil), - handler.NewCol(NotifyLastPhoneCol, nil), - }, - []handler.Column{ - handler.NewCol(NotifyUserIDCol, nil), - handler.NewCol(NotifyInstanceIDCol, nil), - handler.NewCol(NotifyVerifiedPhoneCol, nil), + crdb.NewCopyCol(NotifyVerifiedPhoneCol, NotifyLastPhoneCol), }, []handler.Condition{ handler.NewCond(NotifyUserIDCol, e.Aggregate().ID), @@ -802,20 +791,9 @@ func (p *userProjection) reduceHumanEmailVerified(event eventstore.Event) (*hand }, crdb.WithTableSuffix(UserHumanSuffix), ), - crdb.AddCopyStatement( + crdb.AddUpdateStatement( []handler.Column{ - handler.NewCol(NotifyUserIDCol, nil), - handler.NewCol(NotifyInstanceIDCol, nil), - }, - []handler.Column{ - handler.NewCol(NotifyUserIDCol, nil), - handler.NewCol(NotifyInstanceIDCol, nil), - handler.NewCol(NotifyLastEmailCol, nil), - }, - []handler.Column{ - handler.NewCol(NotifyUserIDCol, nil), - handler.NewCol(NotifyInstanceIDCol, nil), - handler.NewCol(NotifyVerifiedEmailCol, nil), + crdb.NewCopyCol(NotifyVerifiedEmailCol, NotifyLastEmailCol), }, []handler.Condition{ handler.NewCond(NotifyUserIDCol, e.Aggregate().ID), diff --git a/internal/query/projection/user_test.go b/internal/query/projection/user_test.go index e7846a258d..54cebee31e 100644 --- a/internal/query/projection/user_test.go +++ b/internal/query/projection/user_test.go @@ -50,7 +50,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users3 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users4 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -64,7 +64,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users4_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -79,7 +79,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users4_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -120,7 +120,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users3 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users4 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -134,7 +134,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users4_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -149,7 +149,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users4_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -185,7 +185,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users3 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users4 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -199,7 +199,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users4_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -214,7 +214,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users4_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -255,7 +255,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users3 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users4 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -269,7 +269,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users4_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -284,7 +284,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users4_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -325,7 +325,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users3 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users4 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -339,7 +339,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users4_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -354,7 +354,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users4_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -390,7 +390,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users3 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users4 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -404,7 +404,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.users4_humans (user_id, instance_id, first_name, last_name, nick_name, display_name, preferred_language, gender, email, phone) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -419,7 +419,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", + expectedStmt: "INSERT INTO projections.users4_notifications (user_id, instance_id, last_email, last_phone, password_set) VALUES ($1, $2, $3, $4, $5)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -450,7 +450,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateInitial, "agg-id", @@ -479,7 +479,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateInitial, "agg-id", @@ -508,7 +508,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateActive, "agg-id", @@ -537,7 +537,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4 SET state = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ domain.UserStateActive, "agg-id", @@ -566,7 +566,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users4 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateLocked, @@ -597,7 +597,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users4 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateActive, @@ -628,7 +628,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users4 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateInactive, @@ -659,7 +659,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users4 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserStateActive, @@ -690,7 +690,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.users3 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.users4 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -720,7 +720,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users4 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, "username", @@ -753,7 +753,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.users4 SET (change_date, username, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, "id@temporary.domain", @@ -791,7 +791,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -800,7 +800,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", + expectedStmt: "UPDATE projections.users4_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ "first-name", "last-name", @@ -841,7 +841,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -850,7 +850,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", + expectedStmt: "UPDATE projections.users4_humans SET (first_name, last_name, nick_name, display_name, preferred_language, gender) = ($1, $2, $3, $4, $5, $6) WHERE (user_id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ "first-name", "last-name", @@ -886,7 +886,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -895,7 +895,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "+41 00 000 00 00", false, @@ -904,7 +904,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "+41 00 000 00 00", Valid: true}, "agg-id", @@ -935,7 +935,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -944,7 +944,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "+41 00 000 00 00", false, @@ -953,7 +953,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4_notifications SET last_phone = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "+41 00 000 00 00", Valid: true}, "agg-id", @@ -982,7 +982,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -991,7 +991,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -1000,7 +1000,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -1030,7 +1030,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1039,7 +1039,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4_humans SET (phone, is_phone_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -1048,7 +1048,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4_notifications SET (last_phone, verified_phone) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ nil, nil, @@ -1078,7 +1078,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1087,7 +1087,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1095,7 +1095,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_notifications (user_id, instance_id, verified_phone) SELECT user_id, instance_id, last_phone FROM projections.users3_notifications AS copy_table WHERE copy_table.user_id = $1 AND copy_table.instance_id = $2 ON CONFLICT (user_id, instance_id) DO UPDATE SET (user_id, instance_id, verified_phone) = (EXCLUDED.user_id, EXCLUDED.instance_id, EXCLUDED.last_phone)", + expectedStmt: "UPDATE projections.users4_notifications SET verified_phone = last_phone WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1123,7 +1123,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1132,7 +1132,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4_humans SET is_phone_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1140,7 +1140,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_notifications (user_id, instance_id, verified_phone) SELECT user_id, instance_id, last_phone FROM projections.users3_notifications AS copy_table WHERE copy_table.user_id = $1 AND copy_table.instance_id = $2 ON CONFLICT (user_id, instance_id) DO UPDATE SET (user_id, instance_id, verified_phone) = (EXCLUDED.user_id, EXCLUDED.instance_id, EXCLUDED.last_phone)", + expectedStmt: "UPDATE projections.users4_notifications SET verified_phone = last_phone WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1170,7 +1170,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1179,7 +1179,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "email@zitadel.com", false, @@ -1188,7 +1188,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "email@zitadel.com", Valid: true}, "agg-id", @@ -1219,7 +1219,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1228,7 +1228,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4_humans SET (email, is_email_verified) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "email@zitadel.com", false, @@ -1237,7 +1237,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4_notifications SET last_email = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ &sql.NullString{String: "email@zitadel.com", Valid: true}, "agg-id", @@ -1266,7 +1266,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1275,7 +1275,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1283,7 +1283,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_notifications (user_id, instance_id, verified_email) SELECT user_id, instance_id, last_email FROM projections.users3_notifications AS copy_table WHERE copy_table.user_id = $1 AND copy_table.instance_id = $2 ON CONFLICT (user_id, instance_id) DO UPDATE SET (user_id, instance_id, verified_email) = (EXCLUDED.user_id, EXCLUDED.instance_id, EXCLUDED.last_email)", + expectedStmt: "UPDATE projections.users4_notifications SET verified_email = last_email WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1311,7 +1311,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1320,7 +1320,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4_humans SET is_email_verified = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ true, "agg-id", @@ -1328,7 +1328,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_notifications (user_id, instance_id, verified_email) SELECT user_id, instance_id, last_email FROM projections.users3_notifications AS copy_table WHERE copy_table.user_id = $1 AND copy_table.instance_id = $2 ON CONFLICT (user_id, instance_id) DO UPDATE SET (user_id, instance_id, verified_email) = (EXCLUDED.user_id, EXCLUDED.instance_id, EXCLUDED.last_email)", + expectedStmt: "UPDATE projections.users4_notifications SET verified_email = last_email WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1358,7 +1358,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1367,7 +1367,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "users/agg-id/avatar", "agg-id", @@ -1396,7 +1396,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1405,7 +1405,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4_humans SET avatar_key = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ nil, "agg-id", @@ -1437,7 +1437,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users3 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users4 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -1451,7 +1451,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_machines (user_id, instance_id, name, description) VALUES ($1, $2, $3, $4)", + expectedStmt: "INSERT INTO projections.users4_machines (user_id, instance_id, name, description) VALUES ($1, $2, $3, $4)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1485,7 +1485,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.users3 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.users4 (id, creation_date, change_date, resource_owner, instance_id, state, sequence, username, type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -1499,7 +1499,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.users3_machines (user_id, instance_id, name, description) VALUES ($1, $2, $3, $4)", + expectedStmt: "INSERT INTO projections.users4_machines (user_id, instance_id, name, description) VALUES ($1, $2, $3, $4)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -1532,7 +1532,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1541,7 +1541,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_machines SET (name, description) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4_machines SET (name, description) = ($1, $2) WHERE (user_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "machine-name", "description", @@ -1573,7 +1573,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1582,7 +1582,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_machines SET name = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4_machines SET name = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "machine-name", "agg-id", @@ -1613,7 +1613,7 @@ func TestUserProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.users3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.users4 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1622,7 +1622,7 @@ func TestUserProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.users3_machines SET description = $1 WHERE (user_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.users4_machines SET description = $1 WHERE (user_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "description", "agg-id", diff --git a/internal/query/user.go b/internal/query/user.go index d001a66255..f75bab56ee 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -388,7 +388,7 @@ func (q *Queries) GetHumanPhone(ctx context.Context, userID string, queries ...S return scan(row) } -func (q *Queries) GeNotifyUser(ctx context.Context, shouldTriggered bool, userID string, queries ...SearchQuery) (*NotifyUser, error) { +func (q *Queries) GetNotifyUserByID(ctx context.Context, shouldTriggered bool, userID string, queries ...SearchQuery) (*NotifyUser, error) { if shouldTriggered { projection.UserProjection.Trigger(ctx) projection.LoginNameProjection.Trigger(ctx) @@ -411,6 +411,28 @@ func (q *Queries) GeNotifyUser(ctx context.Context, shouldTriggered bool, userID return scan(row) } +func (q *Queries) GetNotifyUser(ctx context.Context, shouldTriggered bool, queries ...SearchQuery) (*NotifyUser, error) { + if shouldTriggered { + projection.UserProjection.Trigger(ctx) + projection.LoginNameProjection.Trigger(ctx) + } + + instanceID := authz.GetInstance(ctx).InstanceID() + query, scan := prepareNotifyUserQuery(instanceID) + for _, q := range queries { + query = q.toQuery(query) + } + stmt, args, err := query.Where(sq.Eq{ + UserInstanceIDCol.identifier(): instanceID, + }).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-Err3g", "Errors.Query.SQLStatment") + } + + row := q.client.QueryRowContext(ctx, stmt, args...) + return scan(row) +} + func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries) (*Users, error) { query, scan := prepareUsersQuery() stmt, args, err := queries.toQuery(query). @@ -492,27 +514,39 @@ func NewUserResourceOwnerSearchQuery(value string, comparison TextComparison) (S } func NewUserUsernameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { - return NewTextQuery(Column(UserUsernameCol), value, comparison) + return NewTextQuery(UserUsernameCol, value, comparison) } func NewUserFirstNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { - return NewTextQuery(Column(HumanFirstNameCol), value, comparison) + return NewTextQuery(HumanFirstNameCol, value, comparison) } func NewUserLastNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { - return NewTextQuery(Column(HumanLastNameCol), value, comparison) + return NewTextQuery(HumanLastNameCol, value, comparison) } func NewUserNickNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { - return NewTextQuery(Column(HumanNickNameCol), value, comparison) + return NewTextQuery(HumanNickNameCol, value, comparison) } func NewUserDisplayNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { - return NewTextQuery(Column(HumanDisplayNameCol), value, comparison) + return NewTextQuery(HumanDisplayNameCol, value, comparison) } func NewUserEmailSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { - return NewTextQuery(Column(HumanEmailCol), value, comparison) + return NewTextQuery(HumanEmailCol, value, comparison) +} + +func NewUserPhoneSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { + return NewTextQuery(HumanPhoneCol, value, comparison) +} + +func NewUserVerifiedEmailSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { + return NewTextQuery(NotifyVerifiedEmailCol, value, comparison) +} + +func NewUserVerifiedPhoneSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { + return NewTextQuery(NotifyVerifiedPhoneCol, value, comparison) } func NewUserStateSearchQuery(value int32) (SearchQuery, error) { @@ -580,6 +614,7 @@ func prepareUserQuery(instanceID string) (sq.SelectBuilder, func(*sql.Row) (*Use MachineUserIDCol.identifier(), MachineNameCol.identifier(), MachineDescriptionCol.identifier(), + countColumn.identifier(), ). From(userTable.identifier()). LeftJoin(join(HumanUserIDCol, UserIDCol)). @@ -589,6 +624,7 @@ func prepareUserQuery(instanceID string) (sq.SelectBuilder, func(*sql.Row) (*Use PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*User, error) { u := new(User) + var count int preferredLoginName := sql.NullString{} humanID := sql.NullString{} @@ -634,10 +670,11 @@ func prepareUserQuery(instanceID string) (sq.SelectBuilder, func(*sql.Row) (*Use &machineID, &name, &description, + &count, ) - if err != nil { - if errs.Is(err, sql.ErrNoRows) { + if err != nil || count != 1 { + if errs.Is(err, sql.ErrNoRows) || count != 1 { return nil, errors.ThrowNotFound(err, "QUERY-Dfbg2", "Errors.User.NotFound") } return nil, errors.ThrowInternal(err, "QUERY-Bgah2", "Errors.Internal") @@ -877,6 +914,7 @@ func prepareNotifyUserQuery(instanceID string) (sq.SelectBuilder, func(*sql.Row) NotifyPhoneCol.identifier(), NotifyVerifiedPhoneCol.identifier(), NotifyPasswordSetCol.identifier(), + countColumn.identifier(), ). From(userTable.identifier()). LeftJoin(join(HumanUserIDCol, UserIDCol)). @@ -886,6 +924,7 @@ func prepareNotifyUserQuery(instanceID string) (sq.SelectBuilder, func(*sql.Row) PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*NotifyUser, error) { u := new(NotifyUser) + var count int loginNames := database.StringArray{} preferredLoginName := sql.NullString{} @@ -930,10 +969,11 @@ func prepareNotifyUserQuery(instanceID string) (sq.SelectBuilder, func(*sql.Row) ¬ifyPhone, ¬ifyVerifiedPhone, ¬ifyPasswordSet, + &count, ) - if err != nil { - if errs.Is(err, sql.ErrNoRows) { + if err != nil || count != 1 { + if errs.Is(err, sql.ErrNoRows) || count != 1 { return nil, errors.ThrowNotFound(err, "QUERY-Dgqd2", "Errors.User.NotFound") } return nil, errors.ThrowInternal(err, "QUERY-Dbwsg", "Errors.Internal") diff --git a/internal/query/user_grant_test.go b/internal/query/user_grant_test.go index 37f19512cc..d06fd0ad6a 100644 --- a/internal/query/user_grant_test.go +++ b/internal/query/user_grant_test.go @@ -23,14 +23,14 @@ var ( ", projections.user_grants2.roles" + ", projections.user_grants2.state" + ", projections.user_grants2.user_id" + - ", projections.users3.username" + - ", projections.users3.type" + - ", projections.users3.resource_owner" + - ", projections.users3_humans.first_name" + - ", projections.users3_humans.last_name" + - ", projections.users3_humans.email" + - ", projections.users3_humans.display_name" + - ", projections.users3_humans.avatar_key" + + ", projections.users4.username" + + ", projections.users4.type" + + ", projections.users4.resource_owner" + + ", projections.users4_humans.first_name" + + ", projections.users4_humans.last_name" + + ", projections.users4_humans.email" + + ", projections.users4_humans.display_name" + + ", projections.users4_humans.avatar_key" + ", projections.login_names.login_name" + ", projections.user_grants2.resource_owner" + ", projections.orgs.name" + @@ -38,8 +38,8 @@ var ( ", projections.user_grants2.project_id" + ", projections.projects2.name" + " FROM projections.user_grants2" + - " LEFT JOIN projections.users3 ON projections.user_grants2.user_id = projections.users3.id" + - " LEFT JOIN projections.users3_humans ON projections.user_grants2.user_id = projections.users3_humans.user_id" + + " LEFT JOIN projections.users4 ON projections.user_grants2.user_id = projections.users4.id" + + " LEFT JOIN projections.users4_humans ON projections.user_grants2.user_id = projections.users4_humans.user_id" + " LEFT JOIN projections.orgs ON projections.user_grants2.resource_owner = projections.orgs.id" + " LEFT JOIN projections.projects2 ON projections.user_grants2.project_id = projections.projects2.id" + " LEFT JOIN projections.login_names ON projections.user_grants2.user_id = projections.login_names.user_id" + @@ -77,14 +77,14 @@ var ( ", projections.user_grants2.roles" + ", projections.user_grants2.state" + ", projections.user_grants2.user_id" + - ", projections.users3.username" + - ", projections.users3.type" + - ", projections.users3.resource_owner" + - ", projections.users3_humans.first_name" + - ", projections.users3_humans.last_name" + - ", projections.users3_humans.email" + - ", projections.users3_humans.display_name" + - ", projections.users3_humans.avatar_key" + + ", projections.users4.username" + + ", projections.users4.type" + + ", projections.users4.resource_owner" + + ", projections.users4_humans.first_name" + + ", projections.users4_humans.last_name" + + ", projections.users4_humans.email" + + ", projections.users4_humans.display_name" + + ", projections.users4_humans.avatar_key" + ", projections.login_names.login_name" + ", projections.user_grants2.resource_owner" + ", projections.orgs.name" + @@ -93,8 +93,8 @@ var ( ", projections.projects2.name" + ", COUNT(*) OVER ()" + " FROM projections.user_grants2" + - " LEFT JOIN projections.users3 ON projections.user_grants2.user_id = projections.users3.id" + - " LEFT JOIN projections.users3_humans ON projections.user_grants2.user_id = projections.users3_humans.user_id" + + " LEFT JOIN projections.users4 ON projections.user_grants2.user_id = projections.users4.id" + + " LEFT JOIN projections.users4_humans ON projections.user_grants2.user_id = projections.users4_humans.user_id" + " LEFT JOIN projections.orgs ON projections.user_grants2.resource_owner = projections.orgs.id" + " LEFT JOIN projections.projects2 ON projections.user_grants2.project_id = projections.projects2.id" + " LEFT JOIN projections.login_names ON projections.user_grants2.user_id = projections.login_names.user_id" + diff --git a/internal/query/user_test.go b/internal/query/user_test.go index d9c2ac5412..109399c836 100644 --- a/internal/query/user_test.go +++ b/internal/query/user_test.go @@ -17,43 +17,44 @@ import ( ) var ( - userQuery = `SELECT projections.users3.id,` + - ` projections.users3.creation_date,` + - ` projections.users3.change_date,` + - ` projections.users3.resource_owner,` + - ` projections.users3.sequence,` + - ` projections.users3.state,` + - ` projections.users3.type,` + - ` projections.users3.username,` + + userQuery = `SELECT projections.users4.id,` + + ` projections.users4.creation_date,` + + ` projections.users4.change_date,` + + ` projections.users4.resource_owner,` + + ` projections.users4.sequence,` + + ` projections.users4.state,` + + ` projections.users4.type,` + + ` projections.users4.username,` + ` login_names.loginnames,` + ` preferred_login_name.login_name,` + - ` projections.users3_humans.user_id,` + - ` projections.users3_humans.first_name,` + - ` projections.users3_humans.last_name,` + - ` projections.users3_humans.nick_name,` + - ` projections.users3_humans.display_name,` + - ` projections.users3_humans.preferred_language,` + - ` projections.users3_humans.gender,` + - ` projections.users3_humans.avatar_key,` + - ` projections.users3_humans.email,` + - ` projections.users3_humans.is_email_verified,` + - ` projections.users3_humans.phone,` + - ` projections.users3_humans.is_phone_verified,` + - ` projections.users3_machines.user_id,` + - ` projections.users3_machines.name,` + - ` projections.users3_machines.description` + - ` FROM projections.users3` + - ` LEFT JOIN projections.users3_humans ON projections.users3.id = projections.users3_humans.user_id` + - ` LEFT JOIN projections.users3_machines ON projections.users3.id = projections.users3_machines.user_id` + + ` projections.users4_humans.user_id,` + + ` projections.users4_humans.first_name,` + + ` projections.users4_humans.last_name,` + + ` projections.users4_humans.nick_name,` + + ` projections.users4_humans.display_name,` + + ` projections.users4_humans.preferred_language,` + + ` projections.users4_humans.gender,` + + ` projections.users4_humans.avatar_key,` + + ` projections.users4_humans.email,` + + ` projections.users4_humans.is_email_verified,` + + ` projections.users4_humans.phone,` + + ` projections.users4_humans.is_phone_verified,` + + ` projections.users4_machines.user_id,` + + ` projections.users4_machines.name,` + + ` projections.users4_machines.description,` + + ` COUNT(*) OVER ()` + + ` FROM projections.users4` + + ` LEFT JOIN projections.users4_humans ON projections.users4.id = projections.users4_humans.user_id` + + ` LEFT JOIN projections.users4_machines ON projections.users4.id = projections.users4_machines.user_id` + ` LEFT JOIN` + ` (SELECT login_names.user_id, ARRAY_AGG(login_names.login_name)::TEXT[] AS loginnames` + ` FROM projections.login_names AS login_names` + ` WHERE login_names.instance_id = $1` + ` GROUP BY login_names.user_id) AS login_names` + - ` ON login_names.user_id = projections.users3.id` + + ` ON login_names.user_id = projections.users4.id` + ` LEFT JOIN` + ` (SELECT preferred_login_name.user_id, preferred_login_name.login_name FROM projections.login_names AS preferred_login_name WHERE preferred_login_name.instance_id = $2 AND preferred_login_name.is_primary = $3) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users3.id` + ` ON preferred_login_name.user_id = projections.users4.id` userCols = []string{ "id", "creation_date", @@ -82,22 +83,23 @@ var ( "user_id", "name", "description", + "count", } - profileQuery = `SELECT projections.users3.id,` + - ` projections.users3.creation_date,` + - ` projections.users3.change_date,` + - ` projections.users3.resource_owner,` + - ` projections.users3.sequence,` + - ` projections.users3_humans.user_id,` + - ` projections.users3_humans.first_name,` + - ` projections.users3_humans.last_name,` + - ` projections.users3_humans.nick_name,` + - ` projections.users3_humans.display_name,` + - ` projections.users3_humans.preferred_language,` + - ` projections.users3_humans.gender,` + - ` projections.users3_humans.avatar_key` + - ` FROM projections.users3` + - ` LEFT JOIN projections.users3_humans ON projections.users3.id = projections.users3_humans.user_id` + profileQuery = `SELECT projections.users4.id,` + + ` projections.users4.creation_date,` + + ` projections.users4.change_date,` + + ` projections.users4.resource_owner,` + + ` projections.users4.sequence,` + + ` projections.users4_humans.user_id,` + + ` projections.users4_humans.first_name,` + + ` projections.users4_humans.last_name,` + + ` projections.users4_humans.nick_name,` + + ` projections.users4_humans.display_name,` + + ` projections.users4_humans.preferred_language,` + + ` projections.users4_humans.gender,` + + ` projections.users4_humans.avatar_key` + + ` FROM projections.users4` + + ` LEFT JOIN projections.users4_humans ON projections.users4.id = projections.users4_humans.user_id` profileCols = []string{ "id", "creation_date", @@ -113,16 +115,16 @@ var ( "gender", "avatar_key", } - emailQuery = `SELECT projections.users3.id,` + - ` projections.users3.creation_date,` + - ` projections.users3.change_date,` + - ` projections.users3.resource_owner,` + - ` projections.users3.sequence,` + - ` projections.users3_humans.user_id,` + - ` projections.users3_humans.email,` + - ` projections.users3_humans.is_email_verified` + - ` FROM projections.users3` + - ` LEFT JOIN projections.users3_humans ON projections.users3.id = projections.users3_humans.user_id` + emailQuery = `SELECT projections.users4.id,` + + ` projections.users4.creation_date,` + + ` projections.users4.change_date,` + + ` projections.users4.resource_owner,` + + ` projections.users4.sequence,` + + ` projections.users4_humans.user_id,` + + ` projections.users4_humans.email,` + + ` projections.users4_humans.is_email_verified` + + ` FROM projections.users4` + + ` LEFT JOIN projections.users4_humans ON projections.users4.id = projections.users4_humans.user_id` emailCols = []string{ "id", "creation_date", @@ -133,16 +135,16 @@ var ( "email", "is_email_verified", } - phoneQuery = `SELECT projections.users3.id,` + - ` projections.users3.creation_date,` + - ` projections.users3.change_date,` + - ` projections.users3.resource_owner,` + - ` projections.users3.sequence,` + - ` projections.users3_humans.user_id,` + - ` projections.users3_humans.phone,` + - ` projections.users3_humans.is_phone_verified` + - ` FROM projections.users3` + - ` LEFT JOIN projections.users3_humans ON projections.users3.id = projections.users3_humans.user_id` + phoneQuery = `SELECT projections.users4.id,` + + ` projections.users4.creation_date,` + + ` projections.users4.change_date,` + + ` projections.users4.resource_owner,` + + ` projections.users4.sequence,` + + ` projections.users4_humans.user_id,` + + ` projections.users4_humans.phone,` + + ` projections.users4_humans.is_phone_verified` + + ` FROM projections.users4` + + ` LEFT JOIN projections.users4_humans ON projections.users4.id = projections.users4_humans.user_id` phoneCols = []string{ "id", "creation_date", @@ -153,14 +155,14 @@ var ( "phone", "is_phone_verified", } - userUniqueQuery = `SELECT projections.users3.id,` + - ` projections.users3.state,` + - ` projections.users3.username,` + - ` projections.users3_humans.user_id,` + - ` projections.users3_humans.email,` + - ` projections.users3_humans.is_email_verified` + - ` FROM projections.users3` + - ` LEFT JOIN projections.users3_humans ON projections.users3.id = projections.users3_humans.user_id` + userUniqueQuery = `SELECT projections.users4.id,` + + ` projections.users4.state,` + + ` projections.users4.username,` + + ` projections.users4_humans.user_id,` + + ` projections.users4_humans.email,` + + ` projections.users4_humans.is_email_verified` + + ` FROM projections.users4` + + ` LEFT JOIN projections.users4_humans ON projections.users4.id = projections.users4_humans.user_id` userUniqueCols = []string{ "id", "state", @@ -169,42 +171,43 @@ var ( "email", "is_email_verified", } - notifyUserQuery = `SELECT projections.users3.id,` + - ` projections.users3.creation_date,` + - ` projections.users3.change_date,` + - ` projections.users3.resource_owner,` + - ` projections.users3.sequence,` + - ` projections.users3.state,` + - ` projections.users3.type,` + - ` projections.users3.username,` + + notifyUserQuery = `SELECT projections.users4.id,` + + ` projections.users4.creation_date,` + + ` projections.users4.change_date,` + + ` projections.users4.resource_owner,` + + ` projections.users4.sequence,` + + ` projections.users4.state,` + + ` projections.users4.type,` + + ` projections.users4.username,` + ` login_names.loginnames,` + ` preferred_login_name.login_name,` + - ` projections.users3_humans.user_id,` + - ` projections.users3_humans.first_name,` + - ` projections.users3_humans.last_name,` + - ` projections.users3_humans.nick_name,` + - ` projections.users3_humans.display_name,` + - ` projections.users3_humans.preferred_language,` + - ` projections.users3_humans.gender,` + - ` projections.users3_humans.avatar_key,` + - ` projections.users3_notifications.user_id,` + - ` projections.users3_notifications.last_email,` + - ` projections.users3_notifications.verified_email,` + - ` projections.users3_notifications.last_phone,` + - ` projections.users3_notifications.verified_phone,` + - ` projections.users3_notifications.password_set` + - ` FROM projections.users3` + - ` LEFT JOIN projections.users3_humans ON projections.users3.id = projections.users3_humans.user_id` + - ` LEFT JOIN projections.users3_notifications ON projections.users3.id = projections.users3_notifications.user_id` + + ` projections.users4_humans.user_id,` + + ` projections.users4_humans.first_name,` + + ` projections.users4_humans.last_name,` + + ` projections.users4_humans.nick_name,` + + ` projections.users4_humans.display_name,` + + ` projections.users4_humans.preferred_language,` + + ` projections.users4_humans.gender,` + + ` projections.users4_humans.avatar_key,` + + ` projections.users4_notifications.user_id,` + + ` projections.users4_notifications.last_email,` + + ` projections.users4_notifications.verified_email,` + + ` projections.users4_notifications.last_phone,` + + ` projections.users4_notifications.verified_phone,` + + ` projections.users4_notifications.password_set,` + + ` COUNT(*) OVER ()` + + ` FROM projections.users4` + + ` LEFT JOIN projections.users4_humans ON projections.users4.id = projections.users4_humans.user_id` + + ` LEFT JOIN projections.users4_notifications ON projections.users4.id = projections.users4_notifications.user_id` + ` LEFT JOIN` + ` (SELECT login_names.user_id, ARRAY_AGG(login_names.login_name) AS loginnames` + ` FROM projections.login_names AS login_names` + ` WHERE login_names.instance_id = $1` + ` GROUP BY login_names.user_id) AS login_names` + - ` ON login_names.user_id = projections.users3.id` + + ` ON login_names.user_id = projections.users4.id` + ` LEFT JOIN` + ` (SELECT preferred_login_name.user_id, preferred_login_name.login_name FROM projections.login_names AS preferred_login_name WHERE preferred_login_name.instance_id = $2 AND preferred_login_name.is_primary = $3) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users3.id` + ` ON preferred_login_name.user_id = projections.users4.id` notifyUserCols = []string{ "id", "creation_date", @@ -232,44 +235,45 @@ var ( "last_phone", "verified_phone", "password_set", + "count", } - usersQuery = `SELECT projections.users3.id,` + - ` projections.users3.creation_date,` + - ` projections.users3.change_date,` + - ` projections.users3.resource_owner,` + - ` projections.users3.sequence,` + - ` projections.users3.state,` + - ` projections.users3.type,` + - ` projections.users3.username,` + + usersQuery = `SELECT projections.users4.id,` + + ` projections.users4.creation_date,` + + ` projections.users4.change_date,` + + ` projections.users4.resource_owner,` + + ` projections.users4.sequence,` + + ` projections.users4.state,` + + ` projections.users4.type,` + + ` projections.users4.username,` + ` login_names.loginnames,` + ` preferred_login_name.login_name,` + - ` projections.users3_humans.user_id,` + - ` projections.users3_humans.first_name,` + - ` projections.users3_humans.last_name,` + - ` projections.users3_humans.nick_name,` + - ` projections.users3_humans.display_name,` + - ` projections.users3_humans.preferred_language,` + - ` projections.users3_humans.gender,` + - ` projections.users3_humans.avatar_key,` + - ` projections.users3_humans.email,` + - ` projections.users3_humans.is_email_verified,` + - ` projections.users3_humans.phone,` + - ` projections.users3_humans.is_phone_verified,` + - ` projections.users3_machines.user_id,` + - ` projections.users3_machines.name,` + - ` projections.users3_machines.description,` + + ` projections.users4_humans.user_id,` + + ` projections.users4_humans.first_name,` + + ` projections.users4_humans.last_name,` + + ` projections.users4_humans.nick_name,` + + ` projections.users4_humans.display_name,` + + ` projections.users4_humans.preferred_language,` + + ` projections.users4_humans.gender,` + + ` projections.users4_humans.avatar_key,` + + ` projections.users4_humans.email,` + + ` projections.users4_humans.is_email_verified,` + + ` projections.users4_humans.phone,` + + ` projections.users4_humans.is_phone_verified,` + + ` projections.users4_machines.user_id,` + + ` projections.users4_machines.name,` + + ` projections.users4_machines.description,` + ` COUNT(*) OVER ()` + - ` FROM projections.users3` + - ` LEFT JOIN projections.users3_humans ON projections.users3.id = projections.users3_humans.user_id` + - ` LEFT JOIN projections.users3_machines ON projections.users3.id = projections.users3_machines.user_id` + + ` FROM projections.users4` + + ` LEFT JOIN projections.users4_humans ON projections.users4.id = projections.users4_humans.user_id` + + ` LEFT JOIN projections.users4_machines ON projections.users4.id = projections.users4_machines.user_id` + ` LEFT JOIN` + ` (SELECT login_names.user_id, ARRAY_AGG(login_names.login_name) AS loginnames` + ` FROM projections.login_names AS login_names` + ` GROUP BY login_names.user_id) AS login_names` + - ` ON login_names.user_id = projections.users3.id` + + ` ON login_names.user_id = projections.users4.id` + ` LEFT JOIN` + ` (SELECT preferred_login_name.user_id, preferred_login_name.login_name FROM projections.login_names AS preferred_login_name WHERE preferred_login_name.is_primary = $1) AS preferred_login_name` + - ` ON preferred_login_name.user_id = projections.users3.id` + ` ON preferred_login_name.user_id = projections.users4.id` usersCols = []string{ "id", "creation_date", @@ -370,6 +374,7 @@ func Test_UserPrepares(t *testing.T) { nil, nil, nil, + 1, }, ), }, @@ -436,6 +441,7 @@ func Test_UserPrepares(t *testing.T) { "id", "name", "description", + 1, }, ), }, @@ -879,6 +885,7 @@ func Test_UserPrepares(t *testing.T) { "lastPhone", "verifiedPhone", true, + 1, }, ), }, @@ -942,6 +949,7 @@ func Test_UserPrepares(t *testing.T) { nil, nil, nil, + 1, }, ), err: func(err error) (error, bool) { diff --git a/internal/repository/instance/policy_login.go b/internal/repository/instance/policy_login.go index 279ace4e3c..fad06fa715 100644 --- a/internal/repository/instance/policy_login.go +++ b/internal/repository/instance/policy_login.go @@ -29,7 +29,9 @@ func NewLoginPolicyAddedEvent( forceMFA, hidePasswordReset, ignoreUnknownUsernames, - allowDomainDiscovery bool, + allowDomainDiscovery, + disableLoginWithEmail, + disableLoginWithPhone bool, passwordlessType domain.PasswordlessType, defaultRedirectURI string, passwordCheckLifetime, @@ -51,6 +53,8 @@ func NewLoginPolicyAddedEvent( hidePasswordReset, ignoreUnknownUsernames, allowDomainDiscovery, + disableLoginWithEmail, + disableLoginWithPhone, passwordlessType, defaultRedirectURI, passwordCheckLifetime, diff --git a/internal/repository/org/policy_login.go b/internal/repository/org/policy_login.go index 915be8718a..449e3e10a7 100644 --- a/internal/repository/org/policy_login.go +++ b/internal/repository/org/policy_login.go @@ -30,7 +30,9 @@ func NewLoginPolicyAddedEvent( forceMFA, hidePasswordReset, ignoreUnknownUsernames, - allowDomainDiscovery bool, + allowDomainDiscovery, + disableLoginWithEmail, + disableLoginWithPhone bool, passwordlessType domain.PasswordlessType, defaultRedirectURI string, passwordCheckLifetime, @@ -52,13 +54,16 @@ func NewLoginPolicyAddedEvent( hidePasswordReset, ignoreUnknownUsernames, allowDomainDiscovery, + disableLoginWithEmail, + disableLoginWithPhone, passwordlessType, defaultRedirectURI, passwordCheckLifetime, externalLoginCheckLifetime, mfaInitSkipLifetime, secondFactorCheckLifetime, - multiFactorCheckLifetime), + multiFactorCheckLifetime, + ), } } diff --git a/internal/repository/policy/login.go b/internal/repository/policy/login.go index e86472ab80..dfd2d0afd4 100644 --- a/internal/repository/policy/login.go +++ b/internal/repository/policy/login.go @@ -27,6 +27,8 @@ type LoginPolicyAddedEvent struct { HidePasswordReset bool `json:"hidePasswordReset,omitempty"` IgnoreUnknownUsernames bool `json:"ignoreUnknownUsernames,omitempty"` AllowDomainDiscovery bool `json:"allowDomainDiscovery,omitempty"` + DisableLoginWithEmail bool `json:"disableLoginWithEmail,omitempty"` + DisableLoginWithPhone bool `json:"disableLoginWithPhone,omitempty"` PasswordlessType domain.PasswordlessType `json:"passwordlessType,omitempty"` DefaultRedirectURI string `json:"defaultRedirectURI,omitempty"` PasswordCheckLifetime time.Duration `json:"passwordCheckLifetime,omitempty"` @@ -52,7 +54,9 @@ func NewLoginPolicyAddedEvent( forceMFA, hidePasswordReset, ignoreUnknownUsernames, - allowDomainDiscovery bool, + allowDomainDiscovery, + disableLoginWithEmail, + disableLoginWithPhone bool, passwordlessType domain.PasswordlessType, defaultRedirectURI string, passwordCheckLifetime, @@ -77,6 +81,8 @@ func NewLoginPolicyAddedEvent( MFAInitSkipLifetime: mfaInitSkipLifetime, SecondFactorCheckLifetime: secondFactorCheckLifetime, MultiFactorCheckLifetime: multiFactorCheckLifetime, + DisableLoginWithEmail: disableLoginWithEmail, + DisableLoginWithPhone: disableLoginWithPhone, } } @@ -103,6 +109,8 @@ type LoginPolicyChangedEvent struct { HidePasswordReset *bool `json:"hidePasswordReset,omitempty"` IgnoreUnknownUsernames *bool `json:"ignoreUnknownUsernames,omitempty"` AllowDomainDiscovery *bool `json:"allowDomainDiscovery,omitempty"` + DisableLoginWithEmail *bool `json:"disableLoginWithEmail,omitempty"` + DisableLoginWithPhone *bool `json:"disableLoginWithPhone,omitempty"` PasswordlessType *domain.PasswordlessType `json:"passwordlessType,omitempty"` DefaultRedirectURI *string `json:"defaultRedirectURI,omitempty"` PasswordCheckLifetime *time.Duration `json:"passwordCheckLifetime,omitempty"` @@ -222,6 +230,18 @@ func ChangeDefaultRedirectURI(defaultRedirectURI string) func(*LoginPolicyChange } } +func ChangeDisableLoginWithEmail(disableLoginWithEmail bool) func(*LoginPolicyChangedEvent) { + return func(e *LoginPolicyChangedEvent) { + e.DisableLoginWithEmail = &disableLoginWithEmail + } +} + +func ChangeDisableLoginWithPhone(DisableLoginWithPhone bool) func(*LoginPolicyChangedEvent) { + return func(e *LoginPolicyChangedEvent) { + e.DisableLoginWithPhone = &DisableLoginWithPhone + } +} + func LoginPolicyChangedEventMapper(event *repository.Event) (eventstore.Event, error) { e := &LoginPolicyChangedEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event), diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 2178f50d5c..9585fea108 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -3916,6 +3916,16 @@ message UpdateLoginPolicyRequest { description: "If set to true, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the org domains and will redirect to the registration of that organisation on success." } ]; + bool disable_login_with_email = 15 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if user can additionally (to the loginname) be identified by their verified email address" + } + ]; + bool disable_login_with_phone = 16 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if user can additionally (to the loginname) be identified by their verified phone number" + } + ]; } message UpdateLoginPolicyResponse { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index cbf4021dad..ce0f28f7fa 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -4611,6 +4611,16 @@ message AddCustomLoginPolicyRequest { description: "If set to true, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the org domains and will redirect to the registration of that organisation on success." } ]; + bool disable_login_with_email = 18 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if user can additionally (to the loginname) be identified by their verified email address" + } + ]; + bool disable_login_with_phone = 19 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if user can additionally (to the loginname) be identified by their verified phone number" + } + ]; } message AddCustomLoginPolicyResponse { @@ -4645,6 +4655,16 @@ message UpdateCustomLoginPolicyRequest { description: "If set to true, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the org domains and will redirect to the registration of that organisation on success." } ]; + bool disable_login_with_email = 15 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if user can additionally (to the loginname) be identified by their verified email address" + } + ]; + bool disable_login_with_phone = 16 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if user can additionally (to the loginname) be identified by their verified phone number" + } + ]; } message UpdateCustomLoginPolicyResponse { diff --git a/proto/zitadel/policy.proto b/proto/zitadel/policy.proto index 34920eefa2..c2d0215ae7 100644 --- a/proto/zitadel/policy.proto +++ b/proto/zitadel/policy.proto @@ -179,6 +179,16 @@ message LoginPolicy { description: "If set to true, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the org domains and will redirect to the registration of that organisation on success." } ]; + bool disable_login_with_email = 20 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if user can additionally (to the loginname) be identified by their verified email address" + } + ]; + bool disable_login_with_phone = 21 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines if user can additionally (to the loginname) be identified by their verified phone number" + } + ]; } enum SecondFactorType {