diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index ab9179aa1d..6841d8c8b1 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -399,6 +399,7 @@ DefaultInstance: ForceMFA: false HidePasswordReset: false IgnoreUnknownUsernames: false + AllowDomainDiscovery: false PasswordlessType: 1 #1: allowed 0: not allowed DefaultRedirectURI: #empty because we use the Console UI PasswordCheckLifetime: 240h #10d diff --git a/console/src/app/modules/idp-table/idp-table.component.ts b/console/src/app/modules/idp-table/idp-table.component.ts index 7dbef285f9..2a1ec25fcd 100644 --- a/console/src/app/modules/idp-table/idp-table.component.ts +++ b/console/src/app/modules/idp-table/idp-table.component.ts @@ -271,6 +271,7 @@ export class IdpTableComponent implements OnInit { .setNanos(this.loginPolicy.multiFactorCheckLifetime?.nanos ?? 0); mgmtreq.setMultiFactorCheckLifetime(mficl); + mgmtreq.setAllowDomainDiscovery(this.loginPolicy.allowDomainDiscovery); mgmtreq.setIgnoreUnknownUsernames(this.loginPolicy.ignoreUnknownUsernames); mgmtreq.setDefaultRedirectUri(this.loginPolicy.defaultRedirectUri); 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 ea45c36197..003f6464f8 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 @@ -285,6 +285,28 @@ --> +
+ + {{ 'POLICY.DATA.ALLOWDOMAINDISCOVERY' | translate }} + +
+
| -| hide_login_name_suffix | bool | hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this scope in https://docs.zitadel.com/concepts#Reserved_Scopes | | +| hide_login_name_suffix | bool | hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set | | | warn_color | string | - | string.max_len: 50
| | background_color | string | - | string.max_len: 50
| | font_color | string | - | string.max_len: 50
| @@ -3159,6 +3159,7 @@ This is an empty request | second_factors | repeated zitadel.policy.v1.SecondFactorType | - | | | 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. | | @@ -8106,6 +8107,7 @@ This is an empty request | mfa_init_skip_lifetime | google.protobuf.Duration | - | | | 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. | | diff --git a/docs/docs/apis/proto/policy.md b/docs/docs/apis/proto/policy.md index 026836f72f..28d0a5312f 100644 --- a/docs/docs/apis/proto/policy.md +++ b/docs/docs/apis/proto/policy.md @@ -33,7 +33,7 @@ title: zitadel/policy.proto | details | zitadel.v1.ObjectDetails | - | | | primary_color | string | hex value for primary color | | | is_default | bool | defines if the organisation's admin changed the policy | | -| hide_login_name_suffix | bool | hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this scope in https://docs.zitadel.com/concepts#Reserved_Scopes | | +| hide_login_name_suffix | bool | hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set | | | warn_color | string | hex value for secondary color | | | background_color | string | hex value for background color | | | font_color | string | hex value for font color | | @@ -88,6 +88,7 @@ title: zitadel/policy.proto | second_factors | repeated SecondFactorType | - | | | 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. | | diff --git a/internal/api/grpc/admin/login_policy_converter.go b/internal/api/grpc/admin/login_policy_converter.go index 84e23d1fec..e6b97ebc40 100644 --- a/internal/api/grpc/admin/login_policy_converter.go +++ b/internal/api/grpc/admin/login_policy_converter.go @@ -17,6 +17,7 @@ func updateLoginPolicyToDomain(p *admin_pb.UpdateLoginPolicyRequest) *domain.Log PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType), HidePasswordReset: p.HidePasswordReset, IgnoreUnknownUsernames: p.IgnoreUnknownUsernames, + AllowDomainDiscovery: p.AllowDomainDiscovery, DefaultRedirectURI: p.DefaultRedirectUri, PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(), ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(), diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go index b6063929c2..36560ef3c5 100644 --- a/internal/api/grpc/management/org.go +++ b/internal/api/grpc/management/org.go @@ -26,7 +26,7 @@ func (s *Server) GetMyOrg(ctx context.Context, req *mgmt_pb.GetMyOrgRequest) (*m } func (s *Server) GetOrgByDomainGlobal(ctx context.Context, req *mgmt_pb.GetOrgByDomainGlobalRequest) (*mgmt_pb.GetOrgByDomainGlobalResponse, error) { - org, err := s.query.OrgByDomainGlobal(ctx, req.Domain) + org, err := s.query.OrgByPrimaryDomain(ctx, req.Domain) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/policy_login_converter.go b/internal/api/grpc/management/policy_login_converter.go index 8f7a632ece..3cf1d0a97f 100644 --- a/internal/api/grpc/management/policy_login_converter.go +++ b/internal/api/grpc/management/policy_login_converter.go @@ -18,6 +18,7 @@ func AddLoginPolicyToDomain(p *mgmt_pb.AddCustomLoginPolicyRequest) *domain.Logi PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType), HidePasswordReset: p.HidePasswordReset, IgnoreUnknownUsernames: p.IgnoreUnknownUsernames, + AllowDomainDiscovery: p.AllowDomainDiscovery, DefaultRedirectURI: p.DefaultRedirectUri, PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(), ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(), @@ -49,6 +50,7 @@ func updateLoginPolicyToDomain(p *mgmt_pb.UpdateCustomLoginPolicyRequest) *domai PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType), HidePasswordReset: p.HidePasswordReset, IgnoreUnknownUsernames: p.IgnoreUnknownUsernames, + AllowDomainDiscovery: p.AllowDomainDiscovery, 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 b425bfc915..498005f06b 100644 --- a/internal/api/grpc/policy/login_policy.go +++ b/internal/api/grpc/policy/login_policy.go @@ -21,6 +21,7 @@ func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy { PasswordlessType: ModelPasswordlessTypeToPb(policy.PasswordlessType), HidePasswordReset: policy.HidePasswordReset, IgnoreUnknownUsernames: policy.IgnoreUnknownUsernames, + AllowDomainDiscovery: policy.AllowDomainDiscovery, DefaultRedirectUri: policy.DefaultRedirectURI, PasswordCheckLifetime: durationpb.New(policy.PasswordCheckLifetime), ExternalLoginCheckLifetime: durationpb.New(policy.ExternalLoginCheckLifetime), diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 3a76eb1dcb..f1d8ec6f12 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -91,7 +91,7 @@ func (o *OPStorage) ValidateJWTProfileScopes(ctx context.Context, subject string scope := scopes[i] if strings.HasPrefix(scope, domain.OrgDomainPrimaryScope) { var orgID string - org, err := o.query.OrgByDomainGlobal(ctx, strings.TrimPrefix(scope, domain.OrgDomainPrimaryScope)) + org, err := o.query.OrgByPrimaryDomain(ctx, strings.TrimPrefix(scope, domain.OrgDomainPrimaryScope)) if err == nil { orgID = org.ID } diff --git a/internal/api/ui/login/external_register_handler.go b/internal/api/ui/login/external_register_handler.go index db440f62ce..8cd239f58c 100644 --- a/internal/api/ui/login/external_register_handler.go +++ b/internal/api/ui/login/external_register_handler.go @@ -59,11 +59,15 @@ func (l *Login) handleExternalRegister(w http.ResponseWriter, r *http.Request) { l.renderError(w, r, authReq, err) return } + l.handleExternalRegisterByConfigID(w, r, authReq, data.IDPConfigID) +} + +func (l *Login) handleExternalRegisterByConfigID(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, configID string) { if authReq == nil { l.defaultRedirect(w, r) return } - idpConfig, err := l.getIDPConfigByID(r, data.IDPConfigID) + idpConfig, err := l.getIDPConfigByID(r, configID) if err != nil { l.renderError(w, r, authReq, err) return diff --git a/internal/api/ui/login/register_option_handler.go b/internal/api/ui/login/register_option_handler.go index 55f9e28e89..b9d9cb5a23 100644 --- a/internal/api/ui/login/register_option_handler.go +++ b/internal/api/ui/login/register_option_handler.go @@ -33,12 +33,29 @@ func (l *Login) renderRegisterOption(w http.ResponseWriter, r *http.Request, aut if err != nil { errID, errMessage = l.getErrorMessage(r, err) } + allowed := registrationAllowed(authReq) + externalAllowed := externalRegistrationAllowed(authReq) + if err == nil { + // if only external allowed with a single idp then use that + if !allowed && externalAllowed && len(authReq.AllowedExternalIDPs) == 1 { + l.handleExternalRegisterByConfigID(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID) + return + } + // if only direct registration is allowed, show the form + if allowed && !externalAllowed { + l.renderRegister(w, r, authReq, nil, nil) + return + } + } data := registerOptionData{ baseData: l.getBaseData(r, authReq, "RegisterOption", errID, errMessage), } funcs := map[string]interface{}{ + "hasRegistration": func() bool { + return allowed + }, "hasExternalLogin": func() bool { - return authReq.LoginPolicy.AllowExternalIDP && authReq.AllowedExternalIDPs != nil && len(authReq.AllowedExternalIDPs) > 0 + return externalAllowed }, } translator := l.getTranslator(r.Context(), authReq) @@ -58,3 +75,11 @@ func (l *Login) handleRegisterOptionCheck(w http.ResponseWriter, r *http.Request } l.handleRegisterOption(w, r) } + +func registrationAllowed(authReq *domain.AuthRequest) bool { + return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowRegister +} + +func externalRegistrationAllowed(authReq *domain.AuthRequest) bool { + return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowExternalIDP && authReq.AllowedExternalIDPs != nil && len(authReq.AllowedExternalIDPs) > 0 +} diff --git a/internal/api/ui/login/static/templates/register_option.html b/internal/api/ui/login/static/templates/register_option.html index c9fedb3f1e..7e247af416 100644 --- a/internal/api/ui/login/static/templates/register_option.html +++ b/internal/api/ui/login/static/templates/register_option.html @@ -17,7 +17,7 @@
- {{if .LoginPolicy.AllowUsernamePassword }} + {{if hasRegistration }} {{end}} @@ -42,4 +42,4 @@ -{{template "main-bottom" .}} \ No newline at end of file +{{template "main-bottom" .}} diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 2ba00d9701..70f13dabd6 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -93,7 +93,7 @@ type userCommandProvider interface { type orgViewProvider interface { OrgByID(context.Context, bool, string) (*query.Org, error) - OrgByDomainGlobal(context.Context, string) (*query.Org, error) + OrgByPrimaryDomain(context.Context, string) (*query.Org, error) } type userGrantProvider interface { @@ -651,23 +651,57 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain } } } - if request.LoginPolicy != nil && request.LoginPolicy.IgnoreUnknownUsernames { - if errors.IsNotFound(err) || (user != nil && user.State == int32(domain.UserStateInactive)) { - if request.LabelPolicy != nil && request.LabelPolicy.HideLoginNameSuffix { - preferredLoginName = loginName - } - request.SetUserInfo(unknownUserID, preferredLoginName, preferredLoginName, preferredLoginName, "", request.RequestedOrgID) - return nil - } - } - if err != nil { + // return any error apart from not found ones directly + if err != nil && !errors.IsNotFound(err) { return err } - if user.State == int32(domain.UserStateInactive) { + // if there's an active user, let's use it + if user != nil && user.State == int32(domain.UserStateActive) { + request.SetUserInfo(user.ID, loginName, user.PreferredLoginName, "", "", user.ResourceOwner) + return nil + } + // the user was either not found or not active + // so check if the loginname suffix matches a verified org domain + if repo.checkDomainDiscovery(ctx, request, loginName) { + return nil + } + // let's just check for if unknown usernames are ignored + if request.LoginPolicy != nil && request.LoginPolicy.IgnoreUnknownUsernames { + if request.LabelPolicy != nil && request.LabelPolicy.HideLoginNameSuffix { + preferredLoginName = loginName + } + request.SetUserInfo(unknownUserID, preferredLoginName, preferredLoginName, preferredLoginName, "", request.RequestedOrgID) + return nil + } + // there was no policy that allowed unknown loginnames in any case + // let's once again check if the user was just inactive + if user != nil && user.State == int32(domain.UserStateInactive) { return errors.ThrowPreconditionFailed(nil, "AUTH-2n8fs", "Errors.User.Inactive") } - request.SetUserInfo(user.ID, loginName, user.PreferredLoginName, "", "", user.ResourceOwner) - return nil + // user was not found + return err +} + +func (repo *AuthRequestRepo) checkDomainDiscovery(ctx context.Context, request *domain.AuthRequest, loginName string) bool { + // check if there's a suffix in the loginname + split := strings.Split(loginName, "@") + if len(split) < 2 { + return false + } + // check if the suffix matches a verified domain + org, err := repo.Query.OrgByVerifiedDomain(ctx, split[len(split)-1]) + if err != nil { + return false + } + // and if the login policy allows domain discovery + policy, err := repo.Query.LoginPolicyByID(ctx, true, org.ID) + if err != nil || !policy.AllowDomainDiscovery { + return false + } + // discovery was allowed, so set the org as requested org + request.SetOrgInformation(org.ID, org.Name, org.Domain, false) + request.Prompt = append(request.Prompt, domain.PromptCreate) + return true } func (repo *AuthRequestRepo) checkLoginPolicyWithResourceOwner(ctx context.Context, request *domain.AuthRequest, user *user_view_model.UserView) error { @@ -1075,9 +1109,7 @@ func setOrgID(ctx context.Context, orgViewProvider orgViewProvider, request *dom if err != nil { return err } - request.RequestedOrgID = org.ID - request.RequestedOrgName = org.Name - request.RequestedPrimaryDomain = org.Domain + request.SetOrgInformation(org.ID, org.Name, org.Domain, false) return nil } @@ -1086,14 +1118,11 @@ func setOrgID(ctx context.Context, orgViewProvider orgViewProvider, request *dom return nil } - org, err := orgViewProvider.OrgByDomainGlobal(ctx, primaryDomain) + org, err := orgViewProvider.OrgByPrimaryDomain(ctx, primaryDomain) if err != nil { return err } - request.RequestedOrgID = org.ID - request.RequestedOrgName = org.Name - request.RequestedPrimaryDomain = primaryDomain - request.RequestedOrgDomain = true + request.SetOrgInformation(org.ID, org.Name, primaryDomain, true) return nil } diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index 09aa65716c..bc6b519d93 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -174,7 +174,7 @@ func (m *mockViewOrg) OrgByID(context.Context, bool, string) (*query.Org, error) }, nil } -func (m *mockViewOrg) OrgByDomainGlobal(context.Context, string) (*query.Org, error) { +func (m *mockViewOrg) OrgByPrimaryDomain(context.Context, string) (*query.Org, error) { return &query.Org{ State: m.State, }, nil @@ -186,7 +186,7 @@ func (m *mockViewErrOrg) OrgByID(context.Context, bool, string) (*query.Org, err return nil, errors.ThrowInternal(nil, "id", "internal error") } -func (m *mockViewErrOrg) OrgByDomainGlobal(context.Context, string) (*query.Org, error) { +func (m *mockViewErrOrg) OrgByPrimaryDomain(context.Context, string) (*query.Org, error) { return nil, errors.ThrowInternal(nil, "id", "internal error") } diff --git a/internal/command/instance.go b/internal/command/instance.go index efe07e9302..7d9878785e 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -71,6 +71,7 @@ type InstanceSetup struct { ForceMFA bool HidePasswordReset bool IgnoreUnknownUsername bool + AllowDomainDiscovery bool PasswordlessType domain.PasswordlessType DefaultRedirectURI string PasswordCheckLifetime time.Duration @@ -217,6 +218,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str setup.LoginPolicy.ForceMFA, setup.LoginPolicy.HidePasswordReset, setup.LoginPolicy.IgnoreUnknownUsername, + setup.LoginPolicy.AllowDomainDiscovery, setup.LoginPolicy.PasswordlessType, setup.LoginPolicy.DefaultRedirectURI, setup.LoginPolicy.PasswordCheckLifetime, diff --git a/internal/command/instance_converter.go b/internal/command/instance_converter.go index 428696560e..2eda0acb4e 100644 --- a/internal/command/instance_converter.go +++ b/internal/command/instance_converter.go @@ -32,6 +32,7 @@ func writeModelToLoginPolicy(wm *LoginPolicyWriteModel) *domain.LoginPolicy { AllowExternalIDP: wm.AllowExternalIDP, HidePasswordReset: wm.HidePasswordReset, IgnoreUnknownUsernames: wm.IgnoreUnknownUsernames, + AllowDomainDiscovery: wm.AllowDomainDiscovery, ForceMFA: wm.ForceMFA, PasswordlessType: wm.PasswordlessType, DefaultRedirectURI: wm.DefaultRedirectURI, diff --git a/internal/command/instance_policy_login.go b/internal/command/instance_policy_login.go index 9a6dac96ee..34008536a9 100644 --- a/internal/command/instance_policy_login.go +++ b/internal/command/instance_policy_login.go @@ -15,37 +15,6 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" ) -func (c *Commands) AddDefaultLoginPolicy( - ctx context.Context, - allowUsernamePassword, allowRegister, allowExternalIDP, forceMFA, hidePasswordReset, ignoreUnknownUsernames bool, - passwordlessType domain.PasswordlessType, - defaultRedirectURI string, - passwordCheckLifetime, externalLoginCheckLifetime, mfaInitSkipLifetime, secondFactorCheckLifetime, multiFactorCheckLifetime time.Duration, -) (*domain.ObjectDetails, error) { - instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareAddDefaultLoginPolicy(instanceAgg, allowUsernamePassword, - allowRegister, - allowExternalIDP, - forceMFA, - hidePasswordReset, - ignoreUnknownUsernames, - passwordlessType, - defaultRedirectURI, - passwordCheckLifetime, - externalLoginCheckLifetime, - mfaInitSkipLifetime, - secondFactorCheckLifetime, - multiFactorCheckLifetime)) - if err != nil { - return nil, err - } - pushedEvents, err := c.eventstore.Push(ctx, cmds...) - if err != nil { - return nil, err - } - return pushedEventsToObjectDetails(pushedEvents), nil -} - func (c *Commands) ChangeDefaultLoginPolicy(ctx context.Context, policy *domain.LoginPolicy) (*domain.LoginPolicy, error) { existingPolicy := NewInstanceLoginPolicyWriteModel(ctx) instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.LoginPolicyWriteModel.WriteModel) @@ -83,6 +52,7 @@ func (c *Commands) changeDefaultLoginPolicy(ctx context.Context, instanceAgg *ev policy.ForceMFA, policy.HidePasswordReset, policy.IgnoreUnknownUsernames, + policy.AllowDomainDiscovery, policy.PasswordlessType, policy.DefaultRedirectURI, policy.PasswordCheckLifetime, @@ -293,6 +263,7 @@ func prepareAddDefaultLoginPolicy( forceMFA bool, hidePasswordReset bool, ignoreUnknownUsernames bool, + allowDomainDiscovery bool, passwordlessType domain.PasswordlessType, defaultRedirectURI string, passwordCheckLifetime time.Duration, @@ -323,6 +294,7 @@ func prepareAddDefaultLoginPolicy( forceMFA, hidePasswordReset, ignoreUnknownUsernames, + allowDomainDiscovery, passwordlessType, defaultRedirectURI, passwordCheckLifetime, diff --git a/internal/command/instance_policy_login_model.go b/internal/command/instance_policy_login_model.go index 1e30528ed1..972265b26d 100644 --- a/internal/command/instance_policy_login_model.go +++ b/internal/command/instance_policy_login_model.go @@ -66,7 +66,8 @@ func (wm *InstanceLoginPolicyWriteModel) NewChangedEvent( allowExternalIDP, forceMFA, hidePasswordReset, - ignoreUnknownUsernames bool, + ignoreUnknownUsernames, + allowDomainDiscovery bool, passwordlessType domain.PasswordlessType, defaultRedirectURI string, passwordCheckLifetime, @@ -98,6 +99,9 @@ func (wm *InstanceLoginPolicyWriteModel) NewChangedEvent( if wm.IgnoreUnknownUsernames != ignoreUnknownUsernames { changes = append(changes, policy.ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames)) } + if wm.AllowDomainDiscovery != allowDomainDiscovery { + changes = append(changes, policy.ChangeAllowDomainDiscovery(allowDomainDiscovery)) + } if wm.DefaultRedirectURI != defaultRedirectURI { changes = append(changes, policy.ChangeDefaultRedirectURI(defaultRedirectURI)) } diff --git a/internal/command/instance_policy_login_test.go b/internal/command/instance_policy_login_test.go index 956a204d2b..3e1518ec90 100644 --- a/internal/command/instance_policy_login_test.go +++ b/internal/command/instance_policy_login_test.go @@ -18,161 +18,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) { - type fields struct { - eventstore *eventstore.Eventstore - } - type args struct { - ctx context.Context - allowUsernamePassword bool - allowRegister bool - allowExternalIDP bool - forceMFA bool - hidePasswordReset bool - ignoreUnknownUsernames bool - passwordlessType domain.PasswordlessType - defaultRedirectURI string - passwordCheckLifetime time.Duration - externalLoginCheckLifetime time.Duration - mfaInitSkipLifetime time.Duration - secondFactorCheckLifetime time.Duration - multiFactorCheckLifetime time.Duration - } - type res struct { - want *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - name: "loginpolicy already existing, already exists error", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - instance.NewLoginPolicyAddedEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, - true, - true, - false, - false, - false, - false, - domain.PasswordlessTypeAllowed, - "", - time.Hour*1, - time.Hour*1, - time.Hour*1, - time.Hour*1, - time.Hour*1, - ), - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - allowRegister: true, - allowUsernamePassword: true, - passwordlessType: domain.PasswordlessTypeAllowed, - }, - res: res{ - err: caos_errs.IsErrorAlreadyExists, - }, - }, - { - name: "add policy,ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - expectPush( - []*repository.Event{ - eventFromEventPusherWithInstanceID( - "INSTANCE", - instance.NewLoginPolicyAddedEvent(context.Background(), - &instance.NewAggregate("INSTANCE").Aggregate, - true, - true, - true, - true, - true, - true, - domain.PasswordlessTypeAllowed, - "https://example.com/redirect", - time.Hour*1, - time.Hour*2, - time.Hour*3, - time.Hour*4, - time.Hour*5, - ), - ), - }, - ), - ), - }, - args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - allowRegister: true, - allowUsernamePassword: true, - allowExternalIDP: true, - forceMFA: true, - hidePasswordReset: true, - ignoreUnknownUsernames: 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, - }, - res: res{ - want: &domain.ObjectDetails{ - ResourceOwner: "INSTANCE", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &Commands{ - eventstore: tt.fields.eventstore, - } - got, err := r.AddDefaultLoginPolicy( - tt.args.ctx, - tt.args.allowUsernamePassword, - tt.args.allowRegister, - tt.args.allowExternalIDP, - tt.args.forceMFA, - tt.args.hidePasswordReset, - tt.args.ignoreUnknownUsernames, - tt.args.passwordlessType, - tt.args.defaultRedirectURI, - tt.args.passwordCheckLifetime, - tt.args.externalLoginCheckLifetime, - tt.args.mfaInitSkipLifetime, - tt.args.secondFactorCheckLifetime, - tt.args.multiFactorCheckLifetime, - ) - if tt.res.err == nil { - assert.NoError(t, err) - } - if tt.res.err != nil && !tt.res.err(err) { - t.Errorf("got wrong err: %v ", err) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) - } - }) - } -} - func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore @@ -225,6 +70,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -246,6 +92,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { ForceMFA: true, HidePasswordReset: true, IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -275,6 +122,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -296,6 +144,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*10, @@ -317,6 +166,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { ForceMFA: false, HidePasswordReset: false, IgnoreUnknownUsernames: false, + AllowDomainDiscovery: false, PasswordlessType: domain.PasswordlessTypeNotAllowed, DefaultRedirectURI: "", PasswordCheckLifetime: time.Hour * 10, @@ -339,6 +189,7 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) { ForceMFA: false, HidePasswordReset: false, IgnoreUnknownUsernames: false, + AllowDomainDiscovery: false, PasswordlessType: domain.PasswordlessTypeNotAllowed, DefaultRedirectURI: "", PasswordCheckLifetime: time.Hour * 10, @@ -435,6 +286,7 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -473,6 +325,7 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -531,6 +384,7 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -671,6 +525,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -709,6 +564,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -760,6 +616,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -816,6 +673,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -880,6 +738,7 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -1434,7 +1293,7 @@ func TestCommandSide_RemoveMultiFactorDefaultLoginPolicy(t *testing.T) { } } -func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allowUsernamePassword, allowExternalIDP, forceMFA, hidePasswordReset, ignoreUnknownUsernames bool, +func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allowUsernamePassword, allowExternalIDP, forceMFA, hidePasswordReset, ignoreUnknownUsernames, allowDomainDiscovery bool, passwordlessType domain.PasswordlessType, redirectURI string, passwordLifetime, externalLoginLifetime, mfaInitSkipLifetime, secondFactorLifetime, multiFactorLifetime time.Duration) *instance.LoginPolicyChangedEvent { @@ -1447,6 +1306,7 @@ func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allow policy.ChangeAllowUserNamePassword(allowUsernamePassword), policy.ChangeHidePasswordReset(hidePasswordReset), policy.ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames), + policy.ChangeAllowDomainDiscovery(allowDomainDiscovery), 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 6cbcfc0538..e85d6f2a27 100644 --- a/internal/command/org_policy_login.go +++ b/internal/command/org_policy_login.go @@ -39,6 +39,7 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol policy.ForceMFA, policy.HidePasswordReset, policy.IgnoreUnknownUsernames, + policy.AllowDomainDiscovery, policy.PasswordlessType, policy.DefaultRedirectURI, policy.PasswordCheckLifetime, @@ -127,6 +128,7 @@ func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string, policy.ForceMFA, policy.HidePasswordReset, policy.IgnoreUnknownUsernames, + policy.AllowDomainDiscovery, policy.PasswordlessType, policy.DefaultRedirectURI, policy.PasswordCheckLifetime, diff --git a/internal/command/org_policy_login_model.go b/internal/command/org_policy_login_model.go index 06236188e6..4eaccbae5d 100644 --- a/internal/command/org_policy_login_model.go +++ b/internal/command/org_policy_login_model.go @@ -68,7 +68,8 @@ func (wm *OrgLoginPolicyWriteModel) NewChangedEvent( allowExternalIDP, forceMFA, hidePasswordReset, - ignoreUnknownUsernames bool, + ignoreUnknownUsernames, + allowDomainDiscovery bool, passwordlessType domain.PasswordlessType, defaultRedirectURI string, passwordCheckLifetime, @@ -97,6 +98,9 @@ func (wm *OrgLoginPolicyWriteModel) NewChangedEvent( if wm.IgnoreUnknownUsernames != ignoreUnknownUsernames { changes = append(changes, policy.ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames)) } + if wm.AllowDomainDiscovery != allowDomainDiscovery { + changes = append(changes, policy.ChangeAllowDomainDiscovery(allowDomainDiscovery)) + } if wm.PasswordCheckLifetime != passwordCheckLifetime { changes = append(changes, policy.ChangePasswordCheckLifetime(passwordCheckLifetime)) } diff --git a/internal/command/org_policy_login_test.go b/internal/command/org_policy_login_test.go index 98b528cb0a..9dfaeb0c49 100644 --- a/internal/command/org_policy_login_test.go +++ b/internal/command/org_policy_login_test.go @@ -79,6 +79,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -100,6 +101,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { AllowExternalIDP: true, ForceMFA: true, IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -130,6 +132,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -153,6 +156,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { ForceMFA: true, HidePasswordReset: true, IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -174,6 +178,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { ForceMFA: true, HidePasswordReset: true, IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -202,6 +207,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { ForceMFA: true, HidePasswordReset: true, IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -233,6 +239,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -268,6 +275,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { ForceMFA: true, HidePasswordReset: true, IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -291,6 +299,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { ForceMFA: true, HidePasswordReset: true, IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -320,6 +329,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { ForceMFA: true, HidePasswordReset: true, IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -368,6 +378,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -398,6 +409,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { ForceMFA: true, HidePasswordReset: true, IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -425,6 +437,7 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) { ForceMFA: true, HidePasswordReset: true, IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -489,6 +502,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { AllowExternalIDP: true, ForceMFA: true, IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", }, @@ -514,6 +528,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { AllowExternalIDP: true, ForceMFA: true, IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", }, @@ -537,6 +552,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -559,6 +575,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { ForceMFA: true, HidePasswordReset: true, IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, PasswordlessType: domain.PasswordlessTypeAllowed, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 1, @@ -587,6 +604,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "https://example.com/redirect", time.Hour*1, @@ -608,6 +626,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", &duration10, @@ -630,6 +649,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { AllowExternalIDP: false, ForceMFA: false, IgnoreUnknownUsernames: false, + AllowDomainDiscovery: false, PasswordlessType: domain.PasswordlessTypeNotAllowed, DefaultRedirectURI: "", PasswordCheckLifetime: time.Hour * 10, @@ -651,6 +671,7 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) { ForceMFA: false, HidePasswordReset: false, IgnoreUnknownUsernames: false, + AllowDomainDiscovery: false, PasswordlessType: domain.PasswordlessTypeNotAllowed, DefaultRedirectURI: "", PasswordCheckLifetime: time.Hour * 10, @@ -744,6 +765,7 @@ func TestCommandSide_RemoveLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -884,6 +906,7 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -925,6 +948,7 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -986,6 +1010,7 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -1150,6 +1175,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -1191,6 +1217,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -1244,6 +1271,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -1304,6 +1332,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -1372,6 +1401,7 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) { true, true, true, + true, domain.PasswordlessTypeAllowed, "", time.Hour*1, @@ -1990,7 +2020,7 @@ func TestCommandSide_RemoveMultiFactorLoginPolicy(t *testing.T) { } } -func newLoginPolicyChangedEvent(ctx context.Context, orgID string, usernamePassword, register, externalIDP, mfa, passwordReset, ignoreUnknownUsernames bool, +func newLoginPolicyChangedEvent(ctx context.Context, orgID string, usernamePassword, register, externalIDP, mfa, passwordReset, ignoreUnknownUsernames, allowDomainDiscovery bool, passwordlessType domain.PasswordlessType, redirectURI string, passwordLifetime, externalLoginLifetime, mfaInitSkipLifetime, secondFactorLifetime, multiFactorLifetime *time.Duration) *org.LoginPolicyChangedEvent { @@ -2001,6 +2031,7 @@ func newLoginPolicyChangedEvent(ctx context.Context, orgID string, usernamePassw policy.ChangeForceMFA(mfa), policy.ChangeHidePasswordReset(passwordReset), policy.ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames), + policy.ChangeAllowDomainDiscovery(allowDomainDiscovery), policy.ChangePasswordlessType(passwordlessType), policy.ChangeDefaultRedirectURI(redirectURI), } diff --git a/internal/command/policy_login_model.go b/internal/command/policy_login_model.go index 6206f99a43..1c49fc6ab4 100644 --- a/internal/command/policy_login_model.go +++ b/internal/command/policy_login_model.go @@ -17,6 +17,7 @@ type LoginPolicyWriteModel struct { ForceMFA bool HidePasswordReset bool IgnoreUnknownUsernames bool + AllowDomainDiscovery bool PasswordlessType domain.PasswordlessType DefaultRedirectURI string PasswordCheckLifetime time.Duration @@ -38,6 +39,7 @@ func (wm *LoginPolicyWriteModel) Reduce() error { wm.PasswordlessType = e.PasswordlessType wm.HidePasswordReset = e.HidePasswordReset wm.IgnoreUnknownUsernames = e.IgnoreUnknownUsernames + wm.AllowDomainDiscovery = e.AllowDomainDiscovery wm.DefaultRedirectURI = e.DefaultRedirectURI wm.PasswordCheckLifetime = e.PasswordCheckLifetime wm.ExternalLoginCheckLifetime = e.ExternalLoginCheckLifetime @@ -64,6 +66,9 @@ func (wm *LoginPolicyWriteModel) Reduce() error { if e.IgnoreUnknownUsernames != nil { wm.IgnoreUnknownUsernames = *e.IgnoreUnknownUsernames } + if e.AllowDomainDiscovery != nil { + wm.AllowDomainDiscovery = *e.AllowDomainDiscovery + } if e.PasswordlessType != nil { wm.PasswordlessType = *e.PasswordlessType } diff --git a/internal/command/user_human_password_test.go b/internal/command/user_human_password_test.go index b4c2ec0caa..3162aeedc7 100644 --- a/internal/command/user_human_password_test.go +++ b/internal/command/user_human_password_test.go @@ -1157,6 +1157,7 @@ func TestCommandSide_CheckPassword(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1194,6 +1195,7 @@ func TestCommandSide_CheckPassword(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1232,6 +1234,7 @@ func TestCommandSide_CheckPassword(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1286,6 +1289,7 @@ func TestCommandSide_CheckPassword(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1374,6 +1378,7 @@ func TestCommandSide_CheckPassword(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1469,6 +1474,7 @@ func TestCommandSide_CheckPassword(t *testing.T) { 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 a84a09fa03..2558e5945b 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -1679,6 +1679,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1745,6 +1746,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1811,6 +1813,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -1894,6 +1897,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -2035,6 +2039,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -2144,6 +2149,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -2247,6 +2253,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, @@ -2372,6 +2379,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { false, false, false, + false, domain.PasswordlessTypeNotAllowed, "", time.Hour*1, diff --git a/internal/domain/auth_request.go b/internal/domain/auth_request.go index 1862584ac6..38ff10fd6f 100644 --- a/internal/domain/auth_request.go +++ b/internal/domain/auth_request.go @@ -140,6 +140,13 @@ func (a *AuthRequest) SetUserInfo(userID, userName, loginName, displayName, avat a.UserOrgID = userOrgID } +func (a *AuthRequest) SetOrgInformation(id, name, primaryDomain string, requestedByDomain bool) { + a.RequestedOrgID = id + a.RequestedOrgName = name + a.RequestedPrimaryDomain = primaryDomain + a.RequestedOrgDomain = requestedByDomain +} + func (a *AuthRequest) MFALevel() MFALevel { return -1 //PLANNED: check a.PossibleLOAs (and Prompt Login?) diff --git a/internal/domain/policy_login.go b/internal/domain/policy_login.go index 24073e0f65..b265d87094 100644 --- a/internal/domain/policy_login.go +++ b/internal/domain/policy_login.go @@ -21,6 +21,7 @@ type LoginPolicy struct { PasswordlessType PasswordlessType HidePasswordReset bool IgnoreUnknownUsernames bool + AllowDomainDiscovery bool DefaultRedirectURI string PasswordCheckLifetime time.Duration ExternalLoginCheckLifetime time.Duration diff --git a/internal/query/login_policy.go b/internal/query/login_policy.go index db279f0b89..bd7326a3b6 100644 --- a/internal/query/login_policy.go +++ b/internal/query/login_policy.go @@ -30,6 +30,7 @@ type LoginPolicy struct { IsDefault bool HidePasswordReset bool IgnoreUnknownUsernames bool + AllowDomainDiscovery bool DefaultRedirectURI string PasswordCheckLifetime time.Duration ExternalLoginCheckLifetime time.Duration @@ -113,6 +114,10 @@ var ( name: projection.IgnoreUnknownUsernames, table: loginPolicyTable, } + LoginPolicyColumnAllowDomainDiscovery = Column{ + name: projection.AllowDomainDiscovery, + table: loginPolicyTable, + } LoginPolicyColumnDefaultRedirectURI = Column{ name: projection.DefaultRedirectURI, table: loginPolicyTable, @@ -305,6 +310,7 @@ func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Rows) (*LoginPolicy, LoginPolicyColumnIsDefault.identifier(), LoginPolicyColumnHidePasswordReset.identifier(), LoginPolicyColumnIgnoreUnknownUsernames.identifier(), + LoginPolicyColumnAllowDomainDiscovery.identifier(), LoginPolicyColumnDefaultRedirectURI.identifier(), LoginPolicyColumnPasswordCheckLifetime.identifier(), LoginPolicyColumnExternalLoginCheckLifetime.identifier(), @@ -343,6 +349,7 @@ func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Rows) (*LoginPolicy, &p.IsDefault, &p.HidePasswordReset, &p.IgnoreUnknownUsernames, + &p.AllowDomainDiscovery, &defaultRedirectURI, &p.PasswordCheckLifetime, &p.ExternalLoginCheckLifetime, diff --git a/internal/query/login_policy_test.go b/internal/query/login_policy_test.go index a6e333d123..221e4024fd 100644 --- a/internal/query/login_policy_test.go +++ b/internal/query/login_policy_test.go @@ -30,32 +30,33 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicyQuery, want: want{ sqlExpectations: mockQueries( - regexp.QuoteMeta(`SELECT projections.login_policies.aggregate_id,`+ - ` projections.login_policies.creation_date,`+ - ` projections.login_policies.change_date,`+ - ` projections.login_policies.sequence,`+ - ` projections.login_policies.allow_register,`+ - ` projections.login_policies.allow_username_password,`+ - ` projections.login_policies.allow_external_idps,`+ - ` projections.login_policies.force_mfa,`+ - ` projections.login_policies.second_factors,`+ - ` projections.login_policies.multi_factors,`+ - ` projections.login_policies.passwordless_type,`+ - ` projections.login_policies.is_default,`+ - ` projections.login_policies.hide_password_reset,`+ - ` projections.login_policies.ignore_unknown_usernames,`+ - ` projections.login_policies.default_redirect_uri,`+ - ` projections.login_policies.password_check_lifetime,`+ - ` projections.login_policies.external_login_check_lifetime,`+ - ` projections.login_policies.mfa_init_skip_lifetime,`+ - ` projections.login_policies.second_factor_check_lifetime,`+ - ` projections.login_policies.multi_factor_check_lifetime,`+ + 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,`+ ` projections.idp_login_policy_links3.idp_id,`+ ` projections.idps2.name,`+ ` projections.idps2.type`+ - ` FROM projections.login_policies`+ + ` FROM projections.login_policies2`+ ` LEFT JOIN projections.idp_login_policy_links3 ON `+ - ` projections.login_policies.aggregate_id = projections.idp_login_policy_links3.aggregate_id`+ + ` projections.login_policies2.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, @@ -75,32 +76,33 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicyQuery, want: want{ sqlExpectations: mockQuery( - regexp.QuoteMeta(`SELECT projections.login_policies.aggregate_id,`+ - ` projections.login_policies.creation_date,`+ - ` projections.login_policies.change_date,`+ - ` projections.login_policies.sequence,`+ - ` projections.login_policies.allow_register,`+ - ` projections.login_policies.allow_username_password,`+ - ` projections.login_policies.allow_external_idps,`+ - ` projections.login_policies.force_mfa,`+ - ` projections.login_policies.second_factors,`+ - ` projections.login_policies.multi_factors,`+ - ` projections.login_policies.passwordless_type,`+ - ` projections.login_policies.is_default,`+ - ` projections.login_policies.hide_password_reset,`+ - ` projections.login_policies.ignore_unknown_usernames,`+ - ` projections.login_policies.default_redirect_uri,`+ - ` projections.login_policies.password_check_lifetime,`+ - ` projections.login_policies.external_login_check_lifetime,`+ - ` projections.login_policies.mfa_init_skip_lifetime,`+ - ` projections.login_policies.second_factor_check_lifetime,`+ - ` projections.login_policies.multi_factor_check_lifetime,`+ + 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,`+ ` projections.idp_login_policy_links3.idp_id,`+ ` projections.idps2.name,`+ ` projections.idps2.type`+ - ` FROM projections.login_policies`+ + ` FROM projections.login_policies2`+ ` LEFT JOIN projections.idp_login_policy_links3 ON `+ - ` projections.login_policies.aggregate_id = projections.idp_login_policy_links3.aggregate_id`+ + ` projections.login_policies2.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{ @@ -118,6 +120,7 @@ func Test_LoginPolicyPrepares(t *testing.T) { "is_default", "hide_password_reset", "ignore_unknown_usernames", + "allow_domain_discovery", "default_redirect_uri", "password_check_lifetime", "external_login_check_lifetime", @@ -143,6 +146,7 @@ func Test_LoginPolicyPrepares(t *testing.T) { true, true, true, + true, "https://example.com/redirect", time.Hour * 2, time.Hour * 2, @@ -170,6 +174,7 @@ func Test_LoginPolicyPrepares(t *testing.T) { IsDefault: true, HidePasswordReset: true, IgnoreUnknownUsernames: true, + AllowDomainDiscovery: true, DefaultRedirectURI: "https://example.com/redirect", PasswordCheckLifetime: time.Hour * 2, ExternalLoginCheckLifetime: time.Hour * 2, @@ -190,32 +195,33 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicyQuery, want: want{ sqlExpectations: mockQueryErr( - regexp.QuoteMeta(`SELECT projections.login_policies.aggregate_id,`+ - ` projections.login_policies.creation_date,`+ - ` projections.login_policies.change_date,`+ - ` projections.login_policies.sequence,`+ - ` projections.login_policies.allow_register,`+ - ` projections.login_policies.allow_username_password,`+ - ` projections.login_policies.allow_external_idps,`+ - ` projections.login_policies.force_mfa,`+ - ` projections.login_policies.second_factors,`+ - ` projections.login_policies.multi_factors,`+ - ` projections.login_policies.passwordless_type,`+ - ` projections.login_policies.is_default,`+ - ` projections.login_policies.hide_password_reset,`+ - ` projections.login_policies.ignore_unknown_usernames,`+ - ` projections.login_policies.default_redirect_uri,`+ - ` projections.login_policies.password_check_lifetime,`+ - ` projections.login_policies.external_login_check_lifetime,`+ - ` projections.login_policies.mfa_init_skip_lifetime,`+ - ` projections.login_policies.second_factor_check_lifetime,`+ - ` projections.login_policies.multi_factor_check_lifetime,`+ + 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,`+ ` projections.idp_login_policy_links3.idp_id,`+ ` projections.idps2.name,`+ ` projections.idps2.type`+ - ` FROM projections.login_policies`+ + ` FROM projections.login_policies2`+ ` LEFT JOIN projections.idp_login_policy_links3 ON `+ - ` projections.login_policies.aggregate_id = projections.idp_login_policy_links3.aggregate_id`+ + ` projections.login_policies2.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, @@ -234,8 +240,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicy2FAsQuery, want: want{ sqlExpectations: mockQuery( - regexp.QuoteMeta(`SELECT projections.login_policies.second_factors`+ - ` FROM projections.login_policies`), + regexp.QuoteMeta(`SELECT projections.login_policies2.second_factors`+ + ` FROM projections.login_policies2`), []string{ "second_factors", }, @@ -255,8 +261,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicy2FAsQuery, want: want{ sqlExpectations: mockQuery( - regexp.QuoteMeta(`SELECT projections.login_policies.second_factors`+ - ` FROM projections.login_policies`), + regexp.QuoteMeta(`SELECT projections.login_policies2.second_factors`+ + ` FROM projections.login_policies2`), []string{ "second_factors", }, @@ -277,8 +283,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicy2FAsQuery, want: want{ sqlExpectations: mockQuery( - regexp.QuoteMeta(`SELECT projections.login_policies.second_factors`+ - ` FROM projections.login_policies`), + regexp.QuoteMeta(`SELECT projections.login_policies2.second_factors`+ + ` FROM projections.login_policies2`), []string{ "second_factors", }, @@ -294,8 +300,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicy2FAsQuery, want: want{ sqlExpectations: mockQueryErr( - regexp.QuoteMeta(`SELECT projections.login_policies.second_factors`+ - ` FROM projections.login_policies`), + regexp.QuoteMeta(`SELECT projections.login_policies2.second_factors`+ + ` FROM projections.login_policies2`), sql.ErrConnDone, ), err: func(err error) (error, bool) { @@ -312,8 +318,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicyMFAsQuery, want: want{ sqlExpectations: mockQuery( - regexp.QuoteMeta(`SELECT projections.login_policies.multi_factors`+ - ` FROM projections.login_policies`), + regexp.QuoteMeta(`SELECT projections.login_policies2.multi_factors`+ + ` FROM projections.login_policies2`), []string{ "multi_factors", }, @@ -333,8 +339,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicyMFAsQuery, want: want{ sqlExpectations: mockQuery( - regexp.QuoteMeta(`SELECT projections.login_policies.multi_factors`+ - ` FROM projections.login_policies`), + regexp.QuoteMeta(`SELECT projections.login_policies2.multi_factors`+ + ` FROM projections.login_policies2`), []string{ "multi_factors", }, @@ -355,8 +361,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicyMFAsQuery, want: want{ sqlExpectations: mockQuery( - regexp.QuoteMeta(`SELECT projections.login_policies.multi_factors`+ - ` FROM projections.login_policies`), + regexp.QuoteMeta(`SELECT projections.login_policies2.multi_factors`+ + ` FROM projections.login_policies2`), []string{ "multi_factors", }, @@ -372,8 +378,8 @@ func Test_LoginPolicyPrepares(t *testing.T) { prepare: prepareLoginPolicyMFAsQuery, want: want{ sqlExpectations: mockQueryErr( - regexp.QuoteMeta(`SELECT projections.login_policies.multi_factors`+ - ` FROM projections.login_policies`), + regexp.QuoteMeta(`SELECT projections.login_policies2.multi_factors`+ + ` FROM projections.login_policies2`), sql.ErrConnDone, ), err: func(err error) (error, bool) { diff --git a/internal/query/org.go b/internal/query/org.go index 4d2dcb966f..2db90d5bda 100644 --- a/internal/query/org.go +++ b/internal/query/org.go @@ -104,7 +104,7 @@ func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string return scan(row) } -func (q *Queries) OrgByDomainGlobal(ctx context.Context, domain string) (*Org, error) { +func (q *Queries) OrgByPrimaryDomain(ctx context.Context, domain string) (*Org, error) { stmt, scan := prepareOrgQuery() query, args, err := stmt.Where(sq.Eq{ OrgColumnDomain.identifier(): domain, @@ -118,6 +118,21 @@ func (q *Queries) OrgByDomainGlobal(ctx context.Context, domain string) (*Org, e return scan(row) } +func (q *Queries) OrgByVerifiedDomain(ctx context.Context, domain string) (*Org, error) { + stmt, scan := prepareOrgWithDomainsQuery() + query, args, err := stmt.Where(sq.Eq{ + OrgDomainDomainCol.identifier(): domain, + OrgDomainIsVerifiedCol.identifier(): true, + OrgColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + }).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-TYUCE", "Errors.Query.SQLStatement") + } + + row := q.client.QueryRowContext(ctx, query, args...) + return scan(row) +} + func (q *Queries) IsOrgUnique(ctx context.Context, name, domain string) (isUnique bool, err error) { if name == "" && domain == "" { return false, errors.ThrowInvalidArgument(nil, "QUERY-DGqfd", "Errors.Query.InvalidRequest") @@ -268,6 +283,42 @@ func prepareOrgQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { } } +func prepareOrgWithDomainsQuery() (sq.SelectBuilder, func(*sql.Row) (*Org, error)) { + return sq.Select( + OrgColumnID.identifier(), + OrgColumnCreationDate.identifier(), + OrgColumnChangeDate.identifier(), + OrgColumnResourceOwner.identifier(), + OrgColumnState.identifier(), + OrgColumnSequence.identifier(), + OrgColumnName.identifier(), + OrgColumnDomain.identifier(), + ). + From(orgsTable.identifier()). + LeftJoin(join(OrgDomainOrgIDCol, OrgColumnID)). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*Org, error) { + o := new(Org) + err := row.Scan( + &o.ID, + &o.CreationDate, + &o.ChangeDate, + &o.ResourceOwner, + &o.State, + &o.Sequence, + &o.Name, + &o.Domain, + ) + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-iTTGJ", "Errors.Org.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-pWS5H", "Errors.Internal") + } + return o, nil + } +} + func prepareOrgUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, error)) { return sq.Select(uniqueColumn.identifier()). From(orgsTable.identifier()).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/projection/login_policy.go b/internal/query/projection/login_policy.go index b856fcf454..0900d90269 100644 --- a/internal/query/projection/login_policy.go +++ b/internal/query/projection/login_policy.go @@ -13,7 +13,7 @@ import ( ) const ( - LoginPolicyTable = "projections.login_policies" + LoginPolicyTable = "projections.login_policies2" LoginPolicyIDCol = "aggregate_id" LoginPolicyInstanceIDCol = "instance_id" @@ -30,6 +30,7 @@ const ( LoginPolicyPasswordlessTypeCol = "passwordless_type" LoginPolicyHidePWResetCol = "hide_password_reset" IgnoreUnknownUsernames = "ignore_unknown_usernames" + AllowDomainDiscovery = "allow_domain_discovery" DefaultRedirectURI = "default_redirect_uri" PasswordCheckLifetimeCol = "password_check_lifetime" ExternalLoginCheckLifetimeCol = "external_login_check_lifetime" @@ -63,6 +64,7 @@ func newLoginPolicyProjection(ctx context.Context, config crdb.StatementHandlerC crdb.NewColumn(LoginPolicyPasswordlessTypeCol, crdb.ColumnTypeEnum), crdb.NewColumn(LoginPolicyHidePWResetCol, crdb.ColumnTypeBool), crdb.NewColumn(IgnoreUnknownUsernames, crdb.ColumnTypeBool), + crdb.NewColumn(AllowDomainDiscovery, crdb.ColumnTypeBool), crdb.NewColumn(DefaultRedirectURI, crdb.ColumnTypeText, crdb.Nullable()), crdb.NewColumn(PasswordCheckLifetimeCol, crdb.ColumnTypeInt64), crdb.NewColumn(ExternalLoginCheckLifetimeCol, crdb.ColumnTypeInt64), @@ -172,6 +174,7 @@ func (p *loginPolicyProjection) reduceLoginPolicyAdded(event eventstore.Event) ( handler.NewCol(LoginPolicyIsDefaultCol, isDefault), handler.NewCol(LoginPolicyHidePWResetCol, policyEvent.HidePasswordReset), handler.NewCol(IgnoreUnknownUsernames, policyEvent.IgnoreUnknownUsernames), + handler.NewCol(AllowDomainDiscovery, policyEvent.AllowDomainDiscovery), handler.NewCol(DefaultRedirectURI, policyEvent.DefaultRedirectURI), handler.NewCol(PasswordCheckLifetimeCol, policyEvent.PasswordCheckLifetime), handler.NewCol(ExternalLoginCheckLifetimeCol, policyEvent.ExternalLoginCheckLifetime), @@ -217,6 +220,9 @@ func (p *loginPolicyProjection) reduceLoginPolicyChanged(event eventstore.Event) if policyEvent.IgnoreUnknownUsernames != nil { cols = append(cols, handler.NewCol(IgnoreUnknownUsernames, *policyEvent.IgnoreUnknownUsernames)) } + if policyEvent.AllowDomainDiscovery != nil { + cols = append(cols, handler.NewCol(AllowDomainDiscovery, *policyEvent.AllowDomainDiscovery)) + } 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 c69c5eadf8..cecec6f2f0 100644 --- a/internal/query/projection/login_policy_test.go +++ b/internal/query/projection/login_policy_test.go @@ -24,7 +24,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { want wantReduce }{ { - name: "org.reduceLoginPolicyAdded", + name: "org reduceLoginPolicyAdded", args: args{ event: getEvent(testEvent( repository.EventType(org.LoginPolicyAddedEventType), @@ -36,6 +36,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { "forceMFA": false, "hidePasswordReset": true, "ignoreUnknownUsernames": true, + "allowDomainDiscovery": true, "passwordlessType": 1, "defaultRedirectURI": "https://example.com/redirect", "passwordCheckLifetime": 10000000, @@ -55,7 +56,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.login_policies (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, 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)", + 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)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -70,6 +71,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { false, true, true, + true, "https://example.com/redirect", time.Millisecond * 10, time.Millisecond * 10, @@ -83,7 +85,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { }, }, { - name: "org.reduceLoginPolicyChanged", + name: "org reduceLoginPolicyChanged", reduce: (&loginPolicyProjection{}).reduceLoginPolicyChanged, args: args{ event: getEvent(testEvent( @@ -96,6 +98,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { "forceMFA": true, "hidePasswordReset": true, "ignoreUnknownUsernames": true, + "allowDomainDiscovery": true, "passwordlessType": 1, "defaultRedirectURI": "https://example.com/redirect", "passwordCheckLifetime": 10000000, @@ -114,7 +117,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, hide_password_reset, ignore_unknown_usernames, 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) WHERE (aggregate_id = $16)", + 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)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -125,6 +128,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { domain.PasswordlessTypeAllowed, true, true, + true, "https://example.com/redirect", time.Millisecond * 10, time.Millisecond * 10, @@ -139,7 +143,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { }, }, { - name: "org.reduceMFAAdded", + name: "org reduceMFAAdded", reduce: (&loginPolicyProjection{}).reduceMFAAdded, args: args{ event: getEvent(testEvent( @@ -158,7 +162,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -171,7 +175,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { }, }, { - name: "org.reduceMFARemoved", + name: "org reduceMFARemoved", reduce: (&loginPolicyProjection{}).reduceMFARemoved, args: args{ event: getEvent(testEvent( @@ -190,7 +194,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -203,7 +207,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { }, }, { - name: "org.reduceLoginPolicyRemoved", + name: "org reduceLoginPolicyRemoved", reduce: (&loginPolicyProjection{}).reduceLoginPolicyRemoved, args: args{ event: getEvent(testEvent( @@ -220,7 +224,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.login_policies WHERE (aggregate_id = $1)", + expectedStmt: "DELETE FROM projections.login_policies2 WHERE (aggregate_id = $1)", expectedArgs: []interface{}{ "agg-id", }, @@ -230,7 +234,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { }, }, { - name: "org.reduce2FAAdded", + name: "org reduce2FAAdded", reduce: (&loginPolicyProjection{}).reduce2FAAdded, args: args{ event: getEvent(testEvent( @@ -249,7 +253,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -262,7 +266,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { }, }, { - name: "org.reduce2FARemoved", + name: "org reduce2FARemoved", reduce: (&loginPolicyProjection{}).reduce2FARemoved, args: args{ event: getEvent(testEvent( @@ -281,7 +285,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -294,7 +298,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { }, }, { - name: "instance.reduceLoginPolicyAdded", + name: "instance reduceLoginPolicyAdded", reduce: (&loginPolicyProjection{}).reduceLoginPolicyAdded, args: args{ event: getEvent(testEvent( @@ -307,6 +311,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { "forceMFA": false, "hidePasswordReset": true, "ignoreUnknownUsernames": true, + "allowDomainDiscovery": true, "passwordlessType": 1, "defaultRedirectURI": "https://example.com/redirect", "passwordCheckLifetime": 10000000, @@ -325,7 +330,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.login_policies (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, 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)", + 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)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -340,6 +345,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { true, true, true, + true, "https://example.com/redirect", time.Millisecond * 10, time.Millisecond * 10, @@ -353,7 +359,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { }, }, { - name: "instance.reduceLoginPolicyChanged", + name: "instance reduceLoginPolicyChanged", reduce: (&loginPolicyProjection{}).reduceLoginPolicyChanged, args: args{ event: getEvent(testEvent( @@ -366,6 +372,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { "forceMFA": true, "hidePasswordReset": true, "ignoreUnknownUsernames": true, + "allowDomainDiscovery": true, "passwordlessType": 1, "defaultRedirectURI": "https://example.com/redirect" }`), @@ -379,7 +386,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, hide_password_reset, ignore_unknown_usernames, default_redirect_uri) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) WHERE (aggregate_id = $11)", + 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)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -390,6 +397,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { domain.PasswordlessTypeAllowed, true, true, + true, "https://example.com/redirect", "agg-id", }, @@ -399,7 +407,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { }, }, { - name: "instance.reduceMFAAdded", + name: "instance reduceMFAAdded", reduce: (&loginPolicyProjection{}).reduceMFAAdded, args: args{ event: getEvent(testEvent( @@ -418,7 +426,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, multi_factors) = ($1, $2, array_append(multi_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -431,7 +439,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { }, }, { - name: "instance.reduceMFARemoved", + name: "instance reduceMFARemoved", reduce: (&loginPolicyProjection{}).reduceMFARemoved, args: args{ event: getEvent(testEvent( @@ -450,7 +458,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, multi_factors) = ($1, $2, array_remove(multi_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -463,7 +471,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { }, }, { - name: "instance.reduce2FAAdded", + name: "instance reduce2FAAdded", reduce: (&loginPolicyProjection{}).reduce2FAAdded, args: args{ event: getEvent(testEvent( @@ -482,7 +490,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies2 SET (change_date, sequence, second_factors) = ($1, $2, array_append(second_factors, $3)) WHERE (aggregate_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -495,7 +503,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { }, }, { - name: "instance.reduce2FARemoved", + name: "instance reduce2FARemoved", reduce: (&loginPolicyProjection{}).reduce2FARemoved, args: args{ event: getEvent(testEvent( @@ -514,7 +522,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.login_policies SET (change_date, sequence, second_factors) = ($1, $2, array_remove(second_factors, $3)) WHERE (aggregate_id = $4)", + expectedStmt: "UPDATE projections.login_policies2 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/repository/instance/policy_login.go b/internal/repository/instance/policy_login.go index 83631e4753..279ace4e3c 100644 --- a/internal/repository/instance/policy_login.go +++ b/internal/repository/instance/policy_login.go @@ -28,7 +28,8 @@ func NewLoginPolicyAddedEvent( allowExternalIDP, forceMFA, hidePasswordReset, - ignoreUnknownUsernames bool, + ignoreUnknownUsernames, + allowDomainDiscovery bool, passwordlessType domain.PasswordlessType, defaultRedirectURI string, passwordCheckLifetime, @@ -49,6 +50,7 @@ func NewLoginPolicyAddedEvent( forceMFA, hidePasswordReset, ignoreUnknownUsernames, + allowDomainDiscovery, passwordlessType, defaultRedirectURI, passwordCheckLifetime, diff --git a/internal/repository/org/policy_login.go b/internal/repository/org/policy_login.go index d05d39086b..915be8718a 100644 --- a/internal/repository/org/policy_login.go +++ b/internal/repository/org/policy_login.go @@ -29,7 +29,8 @@ func NewLoginPolicyAddedEvent( allowExternalIDP, forceMFA, hidePasswordReset, - ignoreUnknownUsernames bool, + ignoreUnknownUsernames, + allowDomainDiscovery bool, passwordlessType domain.PasswordlessType, defaultRedirectURI string, passwordCheckLifetime, @@ -50,6 +51,7 @@ func NewLoginPolicyAddedEvent( forceMFA, hidePasswordReset, ignoreUnknownUsernames, + allowDomainDiscovery, passwordlessType, defaultRedirectURI, passwordCheckLifetime, diff --git a/internal/repository/policy/login.go b/internal/repository/policy/login.go index c6165aad15..e86472ab80 100644 --- a/internal/repository/policy/login.go +++ b/internal/repository/policy/login.go @@ -26,6 +26,7 @@ type LoginPolicyAddedEvent struct { ForceMFA bool `json:"forceMFA,omitempty"` HidePasswordReset bool `json:"hidePasswordReset,omitempty"` IgnoreUnknownUsernames bool `json:"ignoreUnknownUsernames,omitempty"` + AllowDomainDiscovery bool `json:"allowDomainDiscovery,omitempty"` PasswordlessType domain.PasswordlessType `json:"passwordlessType,omitempty"` DefaultRedirectURI string `json:"defaultRedirectURI,omitempty"` PasswordCheckLifetime time.Duration `json:"passwordCheckLifetime,omitempty"` @@ -50,7 +51,8 @@ func NewLoginPolicyAddedEvent( allowExternalIDP, forceMFA, hidePasswordReset, - ignoreUnknownUsernames bool, + ignoreUnknownUsernames, + allowDomainDiscovery bool, passwordlessType domain.PasswordlessType, defaultRedirectURI string, passwordCheckLifetime, @@ -68,6 +70,7 @@ func NewLoginPolicyAddedEvent( PasswordlessType: passwordlessType, HidePasswordReset: hidePasswordReset, IgnoreUnknownUsernames: ignoreUnknownUsernames, + AllowDomainDiscovery: allowDomainDiscovery, DefaultRedirectURI: defaultRedirectURI, PasswordCheckLifetime: passwordCheckLifetime, ExternalLoginCheckLifetime: externalLoginCheckLifetime, @@ -99,6 +102,7 @@ type LoginPolicyChangedEvent struct { ForceMFA *bool `json:"forceMFA,omitempty"` HidePasswordReset *bool `json:"hidePasswordReset,omitempty"` IgnoreUnknownUsernames *bool `json:"ignoreUnknownUsernames,omitempty"` + AllowDomainDiscovery *bool `json:"allowDomainDiscovery,omitempty"` PasswordlessType *domain.PasswordlessType `json:"passwordlessType,omitempty"` DefaultRedirectURI *string `json:"defaultRedirectURI,omitempty"` PasswordCheckLifetime *time.Duration `json:"passwordCheckLifetime,omitempty"` @@ -206,6 +210,12 @@ func ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames bool) func(*LoginPolicy } } +func ChangeAllowDomainDiscovery(allowDomainDiscovery bool) func(*LoginPolicyChangedEvent) { + return func(e *LoginPolicyChangedEvent) { + e.AllowDomainDiscovery = &allowDomainDiscovery + } +} + func ChangeDefaultRedirectURI(defaultRedirectURI string) func(*LoginPolicyChangedEvent) { return func(e *LoginPolicyChangedEvent) { e.DefaultRedirectURI = &defaultRedirectURI diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 3caaaf5403..2178f50d5c 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -3798,7 +3798,7 @@ message UpdateLabelPolicyRequest { ]; bool hide_login_name_suffix = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this scope in https://docs.zitadel.com/concepts#Reserved_Scopes"; + description: "hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set"; } ]; string warn_color = 4 [(validate.rules).string = {max_len: 50}]; @@ -3910,6 +3910,12 @@ message UpdateLoginPolicyRequest { google.protobuf.Duration mfa_init_skip_lifetime = 11; google.protobuf.Duration second_factor_check_lifetime = 12; google.protobuf.Duration multi_factor_check_lifetime = 13; + // 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 allow_domain_discovery = 14 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + 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." + } + ]; } message UpdateLoginPolicyResponse { @@ -4852,4 +4858,4 @@ message ExportDataRequest { message ExportDataResponse { repeated DataOrg orgs = 1; -} \ No newline at end of file +} diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index a0226dd2c6..e1c6d804f1 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -4585,6 +4585,12 @@ message AddCustomLoginPolicyRequest { repeated zitadel.policy.v1.SecondFactorType second_factors = 14; repeated zitadel.policy.v1.MultiFactorType multi_factors = 15; repeated IDP idps = 16; + // 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 allow_domain_discovery = 17 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + 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." + } + ]; } message AddCustomLoginPolicyResponse { @@ -4613,6 +4619,12 @@ message UpdateCustomLoginPolicyRequest { google.protobuf.Duration mfa_init_skip_lifetime = 11; google.protobuf.Duration second_factor_check_lifetime = 12; google.protobuf.Duration multi_factor_check_lifetime = 13; + // 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 allow_domain_discovery = 14 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + 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." + } + ]; } message UpdateCustomLoginPolicyResponse { @@ -4890,10 +4902,10 @@ message GetDefaultLabelPolicyResponse { message AddCustomLabelPolicyRequest { string primary_color = 1 [(validate.rules).string = {max_len: 50}]; - // hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this scope in https://docs.zitadel.com/concepts#Reserved_Scopes + // hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set bool hide_login_name_suffix = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this scope in https://docs.zitadel.com/concepts#Reserved_Scopes"; + description: "hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set"; } ]; string warn_color = 4 [(validate.rules).string = {max_len: 50}]; @@ -4914,7 +4926,7 @@ message UpdateCustomLabelPolicyRequest { string primary_color = 1 [(validate.rules).string = {max_len: 50}]; bool hide_login_name_suffix = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this scope in https://docs.zitadel.com/concepts#Reserved_Scopes"; + description: "hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set"; } ]; string warn_color = 4 [(validate.rules).string = {max_len: 50}]; diff --git a/proto/zitadel/policy.proto b/proto/zitadel/policy.proto index de41d680bc..34920eefa2 100644 --- a/proto/zitadel/policy.proto +++ b/proto/zitadel/policy.proto @@ -62,10 +62,10 @@ message LabelPolicy { description: "defines if the organisation's admin changed the policy" } ]; - // hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this scope in https://docs.zitadel.com/concepts#Reserved_Scopes + // hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set bool hide_login_name_suffix = 5 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set. Details about this scope in https://docs.zitadel.com/concepts#Reserved_Scopes"; + description: "hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set"; } ]; // hex value for secondary color @@ -173,6 +173,12 @@ message LoginPolicy { repeated SecondFactorType second_factors = 16; repeated MultiFactorType multi_factors = 17; repeated zitadel.idp.v1.IDPLoginPolicyLink idps = 18; + // 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 allow_domain_discovery = 19 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + 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." + } + ]; } enum SecondFactorType {