From 320ddfa46d7822a9df33e8a22f6c16af322031d3 Mon Sep 17 00:00:00 2001 From: Fabi <38692350+fgerschwiler@users.noreply.github.com> Date: Fri, 18 Sep 2020 13:26:28 +0200 Subject: [PATCH] feat: Identity brokering (#730) * feat: add/ remove external idps * feat: external idp add /remove * fix: auth proto * fix: handle login * feat: loginpolicy on authrequest * feat: idp providers on login * feat: link external idp * fix: check login policy on check username * feat: add mapping fields for idp config * feat: use user org id if existing * feat: use user org id if existing * feat: register external user * feat: register external user * feat: user linking * feat: user linking * feat: design external login * feat: design external login * fix: tests * fix: regenerate login design * feat: next step test linking process * feat: next step test linking process * feat: cascade remove external idps on user * fix: tests * fix: tests * feat: external idp requsts on users * fix: generate protos * feat: login styles * feat: login styles * fix: link user * fix: register user on specifig org * fix: user linking * fix: register external, linking auto * fix: remove unnecessary request from proto * fix: tests * fix: new oidc package * fix: migration version * fix: policy permissions * Update internal/ui/login/static/i18n/en.yaml Co-authored-by: Livio Amstutz * Update internal/ui/login/static/i18n/en.yaml Co-authored-by: Livio Amstutz * Update internal/ui/login/handler/renderer.go Co-authored-by: Livio Amstutz * Update internal/ui/login/handler/renderer.go Co-authored-by: Livio Amstutz * fix: pr requests * Update internal/ui/login/handler/link_users_handler.go Co-authored-by: Livio Amstutz * fix: pr requests * fix: pr requests * fix: pr requests * fix: login name size * fix: profile image light * fix: colors * fix: pr requests * fix: remove redirect uri validator * fix: remove redirect uri validator Co-authored-by: Livio Amstutz --- cmd/zitadel/main.go | 2 +- cmd/zitadel/startup.yaml | 1 + .../policy-grid/policy-grid.component.html | 4 +- .../eventsourcing/eventstore/iam.go | 43 +- .../handler/user_external_idps.go | 126 + .../repository/eventsourcing/repository.go | 1 + .../eventsourcing/view/external_idps.go | 73 + .../api/grpc/admin/idp_config_converter.go | 52 +- internal/api/grpc/auth/user.go | 13 + internal/api/grpc/auth/user_converter.go | 64 +- .../grpc/management/idp_config_converter.go | 60 +- internal/api/grpc/management/user.go | 14 +- .../api/grpc/management/user_converter.go | 58 +- internal/auth/repository/auth_request.go | 6 + .../eventsourcing/eventstore/auth_request.go | 308 +- .../eventstore/auth_request_test.go | 100 + .../eventsourcing/eventstore/org.go | 23 +- .../eventsourcing/eventstore/user.go | 49 +- .../eventsourcing/handler/handler.go | 4 + .../eventsourcing/handler/idp_config.go | 82 + .../eventsourcing/handler/idp_providers.go | 120 + .../eventsourcing/handler/login_policy.go | 67 + .../handler/user_external_idps.go | 126 + .../repository/eventsourcing/repository.go | 6 + .../eventsourcing/view/external_idps.go | 73 + .../eventsourcing/view/idp_configs.go | 57 + .../eventsourcing/view/idp_providers.go | 77 + .../eventsourcing/view/login_policies.go | 48 + .../repository/eventsourcing/view/user.go | 4 + internal/auth/repository/org.go | 3 + internal/auth/repository/user.go | 5 + internal/auth_request/model/auth_request.go | 56 +- internal/auth_request/model/next_step.go | 14 + internal/auth_request/model/request.go | 4 + internal/iam/model/idp_config.go | 22 +- internal/iam/model/idp_config_view.go | 12 +- .../repository/eventsourcing/eventstore.go | 28 +- internal/iam/repository/eventsourcing/iam.go | 18 +- .../iam/repository/eventsourcing/iam_test.go | 2 +- .../eventsourcing/model/idp_config.go | 11 +- .../eventsourcing/model/oidc_idp_config.go | 48 +- .../iam/repository/view/idp_provider_view.go | 23 +- internal/iam/repository/view/idp_view.go | 17 + .../iam/repository/view/model/idp_config.go | 72 +- .../iam/repository/view/model/idp_provider.go | 2 + .../eventsourcing/eventstore/org.go | 45 +- .../eventsourcing/eventstore/user.go | 25 + .../eventsourcing/handler/handler.go | 1 + .../handler/user_external_idps.go | 126 + .../eventsourcing/view/external_idps.go | 73 + internal/management/repository/user.go | 3 + .../repository/eventsourcing/eventstore.go | 4 + internal/org/repository/eventsourcing/org.go | 2 + internal/static/i18n/de.yaml | 6 + internal/static/i18n/en.yaml | 8 +- internal/ui/login/handler/auth_request.go | 4 + .../login/handler/external_login_handler.go | 269 + .../handler/external_register_handler.go | 155 + .../ui/login/handler/link_users_handler.go | 24 + internal/ui/login/handler/login.go | 12 +- internal/ui/login/handler/login_handler.go | 10 +- .../login/handler/org_iam_policy_handler.go | 10 - internal/ui/login/handler/policy_handler.go | 18 + internal/ui/login/handler/register_handler.go | 7 +- .../login/handler/register_option_handler.go | 53 + internal/ui/login/handler/renderer.go | 114 +- internal/ui/login/handler/router.go | 53 +- .../ui/login/handler/select_user_handler.go | 1 + internal/ui/login/login.go | 5 +- internal/ui/login/static/i18n/de.yaml | 23 + internal/ui/login/static/i18n/en.yaml | 25 +- .../static/resources/themes/caos/css/dark.css | 61 +- .../resources/themes/caos/css/dark.css.map | 2 +- .../resources/themes/caos/css/light.css | 143 +- .../resources/themes/caos/css/light.css.map | 2 +- .../static/resources/themes/scss/light.scss | 56 +- .../static/resources/themes/scss/main.scss | 51 +- .../resources/themes/scss/variables.scss | 8 +- .../resources/themes/zitadel/css/dark.css | 61 +- .../resources/themes/zitadel/css/dark.css.map | 2 +- .../resources/themes/zitadel/css/light.css | 142 +- .../themes/zitadel/css/light.css.map | 2 +- .../static/templates/change_password.html | 10 +- .../templates/change_password_done.html | 10 +- .../static/templates/change_username.html | 10 +- .../templates/change_username_done.html | 6 +- .../templates/external_not_found_option.html | 31 + .../login/static/templates/init_password.html | 10 +- .../static/templates/init_password_done.html | 10 +- .../ui/login/static/templates/init_user.html | 10 +- .../static/templates/init_user_done.html | 10 +- .../static/templates/link_users_done.html | 25 + internal/ui/login/static/templates/login.html | 33 +- .../login/static/templates/logout_done.html | 7 +- .../static/templates/mail_verification.html | 10 +- .../login/static/templates/mail_verified.html | 10 +- .../login/static/templates/mfa_init_done.html | 10 +- .../static/templates/mfa_init_verify.html | 14 +- .../ui/login/static/templates/mfa_prompt.html | 10 +- .../ui/login/static/templates/mfa_verify.html | 10 +- .../ui/login/static/templates/password.html | 10 +- .../static/templates/password_reset_done.html | 6 +- .../ui/login/static/templates/register.html | 13 +- .../static/templates/register_option.html | 40 + .../login/static/templates/register_org.html | 10 +- .../login/static/templates/select_user.html | 11 +- .../login/static/templates/user_profile.html | 20 +- internal/user/model/external_idp.go | 17 + internal/user/model/external_idp_view.go | 61 + internal/user/model/user_human.go | 14 +- .../repository/eventsourcing/eventstore.go | 115 +- .../eventsourcing/eventstore_mock_test.go | 24 + .../eventsourcing/eventstore_test.go | 185 + .../eventsourcing/model/external_idp.go | 96 + .../eventsourcing/model/external_idp_test.go | 89 + .../repository/eventsourcing/model/types.go | 12 +- .../eventsourcing/model/user_human.go | 21 +- .../user/repository/eventsourcing/user.go | 356 +- .../repository/eventsourcing/user_test.go | 194 +- .../user/repository/view/external_idp_view.go | 117 + .../view/model/external_idp_query.go | 65 + .../repository/view/model/external_idps.go | 91 + internal/user/repository/view/user_view.go | 20 + .../cockroach/V1.14__auth_loginpolicy.sql | 105 + pkg/grpc/admin/admin.pb.go | 1571 +- pkg/grpc/admin/admin.pb.validate.go | 12 + pkg/grpc/admin/admin.swagger.json | 27 + pkg/grpc/admin/proto/admin.proto | 12 + pkg/grpc/auth/auth.pb.authoptions.go | 10 + pkg/grpc/auth/auth.pb.go | 6757 ++-- pkg/grpc/auth/auth.pb.gw.go | 203 +- pkg/grpc/auth/auth.pb.validate.go | 412 + pkg/grpc/auth/auth.swagger.json | 420 +- pkg/grpc/auth/mock/auth.proto.mock.go | 40 + pkg/grpc/auth/proto/auth.proto | 58 + .../management/management.pb.authoptions.go | 10 + pkg/grpc/management/management.pb.go | 26754 ++++++++++------ pkg/grpc/management/management.pb.gw.go | 311 +- pkg/grpc/management/management.pb.validate.go | 357 + .../management/mock/management.proto.mock.go | 40 + pkg/grpc/management/proto/management.proto | 66 + 141 files changed, 30057 insertions(+), 12535 deletions(-) create mode 100644 internal/admin/repository/eventsourcing/handler/user_external_idps.go create mode 100644 internal/admin/repository/eventsourcing/view/external_idps.go create mode 100644 internal/auth/repository/eventsourcing/handler/idp_config.go create mode 100644 internal/auth/repository/eventsourcing/handler/idp_providers.go create mode 100644 internal/auth/repository/eventsourcing/handler/login_policy.go create mode 100644 internal/auth/repository/eventsourcing/handler/user_external_idps.go create mode 100644 internal/auth/repository/eventsourcing/view/external_idps.go create mode 100644 internal/auth/repository/eventsourcing/view/idp_configs.go create mode 100644 internal/auth/repository/eventsourcing/view/idp_providers.go create mode 100644 internal/auth/repository/eventsourcing/view/login_policies.go create mode 100644 internal/management/repository/eventsourcing/handler/user_external_idps.go create mode 100644 internal/management/repository/eventsourcing/view/external_idps.go create mode 100644 internal/ui/login/handler/external_login_handler.go create mode 100644 internal/ui/login/handler/external_register_handler.go create mode 100644 internal/ui/login/handler/link_users_handler.go delete mode 100644 internal/ui/login/handler/org_iam_policy_handler.go create mode 100644 internal/ui/login/handler/policy_handler.go create mode 100644 internal/ui/login/handler/register_option_handler.go create mode 100644 internal/ui/login/static/templates/external_not_found_option.html create mode 100644 internal/ui/login/static/templates/link_users_done.html create mode 100644 internal/ui/login/static/templates/register_option.html create mode 100644 internal/user/model/external_idp.go create mode 100644 internal/user/model/external_idp_view.go create mode 100644 internal/user/repository/eventsourcing/model/external_idp.go create mode 100644 internal/user/repository/eventsourcing/model/external_idp_test.go create mode 100644 internal/user/repository/view/external_idp_view.go create mode 100644 internal/user/repository/view/model/external_idp_query.go create mode 100644 internal/user/repository/view/model/external_idps.go create mode 100644 migrations/cockroach/V1.14__auth_loginpolicy.sql diff --git a/cmd/zitadel/main.go b/cmd/zitadel/main.go index a3f0a8e5cc..776fd3bb5b 100644 --- a/cmd/zitadel/main.go +++ b/cmd/zitadel/main.go @@ -113,7 +113,7 @@ func startZitadel(configPaths []string) { func startUI(ctx context.Context, conf *Config, authRepo *auth_es.EsRepository) { uis := ui.Create(conf.UI) if *loginEnabled { - login, prefix := login.Start(conf.UI.Login, authRepo, *localDevMode) + login, prefix := login.Start(conf.UI.Login, authRepo, conf.SystemDefaults, *localDevMode) uis.RegisterHandler(prefix, login.Handler()) } if *consoleEnabled { diff --git a/cmd/zitadel/startup.yaml b/cmd/zitadel/startup.yaml index c152c29231..71fa66bb27 100644 --- a/cmd/zitadel/startup.yaml +++ b/cmd/zitadel/startup.yaml @@ -222,6 +222,7 @@ UI: Port: 50003 Login: Handler: + BaseURL: '$ZITADEL_ACCOUNTS' OidcAuthCallbackURL: '$ZITADEL_AUTHORIZE/authorize/' ZitadelURL: '$ZITADEL_CONSOLE' LanguageCookieName: 'caos.zitadel.login.lang' diff --git a/console/src/app/pages/orgs/policy-grid/policy-grid.component.html b/console/src/app/pages/orgs/policy-grid/policy-grid.component.html index e45010f835..4a404001c5 100644 --- a/console/src/app/pages/orgs/policy-grid/policy-grid.component.html +++ b/console/src/app/pages/orgs/policy-grid/policy-grid.component.html @@ -81,7 +81,7 @@
- +
- \ No newline at end of file + diff --git a/internal/admin/repository/eventsourcing/eventstore/iam.go b/internal/admin/repository/eventsourcing/eventstore/iam.go index bed72cc182..130eabe886 100644 --- a/internal/admin/repository/eventsourcing/eventstore/iam.go +++ b/internal/admin/repository/eventsourcing/eventstore/iam.go @@ -9,6 +9,8 @@ import ( es_sdk "github.com/caos/zitadel/internal/eventstore/sdk" iam_es_model "github.com/caos/zitadel/internal/iam/repository/view/model" org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing" + usr_model "github.com/caos/zitadel/internal/user/model" + usr_es "github.com/caos/zitadel/internal/user/repository/eventsourcing" "strings" iam_model "github.com/caos/zitadel/internal/iam/model" @@ -19,6 +21,7 @@ type IAMRepository struct { SearchLimit uint64 *iam_es.IAMEventstore OrgEvents *org_es.OrgEventstore + UserEvents *usr_es.UserEventstore View *admin_view.View SystemDefaults systemdefaults.SystemDefaults Roles []string @@ -83,7 +86,7 @@ func (repo *IAMRepository) IDPConfigByID(ctx context.Context, idpConfigID string if err != nil { return nil, err } - return iam_es_model.IdpConfigViewToModel(idp), nil + return iam_es_model.IDPConfigViewToModel(idp), nil } func (repo *IAMRepository) AddOIDCIDPConfig(ctx context.Context, idp *iam_model.IDPConfig) (*iam_model.IDPConfig, error) { idp.AggregateID = repo.SystemDefaults.IamID @@ -128,7 +131,19 @@ func (repo *IAMRepository) RemoveIDPConfig(ctx context.Context, idpConfigID stri } aggregates = append(aggregates, providerAgg) } - + externalIDPs, err := repo.View.ExternalIDPsByIDPConfigID(idpConfigID) + if err != nil { + return err + } + for _, externalIDP := range externalIDPs { + idpRemove := &usr_model.ExternalIDP{ObjectRoot: es_models.ObjectRoot{AggregateID: externalIDP.UserID}, IDPConfigID: externalIDP.IDPConfigID, UserID: externalIDP.ExternalUserID} + idpAgg := make([]*es_models.Aggregate, 0) + _, idpAgg, err = repo.UserEvents.PrepareRemoveExternalIDP(ctx, idpRemove, true) + if err != nil { + return err + } + aggregates = append(aggregates, idpAgg...) + } return es_sdk.PushAggregates(ctx, repo.Eventstore.PushAggregates, nil, aggregates...) } @@ -203,7 +218,27 @@ func (repo *IAMRepository) AddIDPProviderToLoginPolicy(ctx context.Context, prov return repo.IAMEventstore.AddIDPProviderToLoginPolicy(ctx, provider) } -func (repo *IAMRepository) RemoveIdpProviderFromIdpProvider(ctx context.Context, provider *iam_model.IDPProvider) error { +func (repo *IAMRepository) RemoveIDPProviderFromIDPProvider(ctx context.Context, provider *iam_model.IDPProvider) error { + aggregates := make([]*es_models.Aggregate, 0) provider.AggregateID = repo.SystemDefaults.IamID - return repo.IAMEventstore.RemoveIDPProviderFromLoginPolicy(ctx, provider) + _, removeAgg, err := repo.IAMEventstore.PrepareRemoveIDPProviderFromLoginPolicy(ctx, provider) + if err != nil { + return err + } + aggregates = append(aggregates, removeAgg) + + externalIDPs, err := repo.View.ExternalIDPsByIDPConfigID(provider.IdpConfigID) + if err != nil { + return err + } + for _, externalIDP := range externalIDPs { + idpRemove := &usr_model.ExternalIDP{ObjectRoot: es_models.ObjectRoot{AggregateID: externalIDP.UserID}, IDPConfigID: externalIDP.IDPConfigID, UserID: externalIDP.ExternalUserID} + idpAgg := make([]*es_models.Aggregate, 0) + _, idpAgg, err = repo.UserEvents.PrepareRemoveExternalIDP(ctx, idpRemove, true) + if err != nil { + return err + } + aggregates = append(aggregates, idpAgg...) + } + return es_sdk.PushAggregates(ctx, repo.Eventstore.PushAggregates, nil, aggregates...) } diff --git a/internal/admin/repository/eventsourcing/handler/user_external_idps.go b/internal/admin/repository/eventsourcing/handler/user_external_idps.go new file mode 100644 index 0000000000..1d64fb5a9f --- /dev/null +++ b/internal/admin/repository/eventsourcing/handler/user_external_idps.go @@ -0,0 +1,126 @@ +package handler + +import ( + "context" + "github.com/caos/logging" + "github.com/caos/zitadel/internal/config/systemdefaults" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/iam/repository/eventsourcing" + org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing" + org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" + "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" + usr_view_model "github.com/caos/zitadel/internal/user/repository/view/model" + + "github.com/caos/zitadel/internal/eventstore/models" + es_models "github.com/caos/zitadel/internal/eventstore/models" + "github.com/caos/zitadel/internal/eventstore/spooler" + iam_model "github.com/caos/zitadel/internal/iam/model" + iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" +) + +type ExternalIDP struct { + handler + systemDefaults systemdefaults.SystemDefaults + iamEvents *eventsourcing.IAMEventstore + orgEvents *org_es.OrgEventstore +} + +const ( + externalIDPTable = "adminapi.user_external_idps" +) + +func (m *ExternalIDP) ViewModel() string { + return externalIDPTable +} + +func (m *ExternalIDP) EventQuery() (*models.SearchQuery, error) { + sequence, err := m.view.GetLatestExternalIDPSequence() + if err != nil { + return nil, err + } + return es_models.NewSearchQuery(). + AggregateTypeFilter(model.UserAggregate, iam_es_model.IAMAggregate, org_es_model.OrgAggregate). + LatestSequenceFilter(sequence.CurrentSequence), nil +} + +func (m *ExternalIDP) Reduce(event *models.Event) (err error) { + switch event.AggregateType { + case model.UserAggregate: + err = m.processUser(event) + case iam_es_model.IAMAggregate, org_es_model.OrgAggregate: + err = m.processIdpConfig(event) + } + return err +} + +func (m *ExternalIDP) processUser(event *models.Event) (err error) { + externalIDP := new(usr_view_model.ExternalIDPView) + switch event.Type { + case model.HumanExternalIDPAdded: + err = externalIDP.AppendEvent(event) + if err != nil { + return err + } + err = m.fillData(externalIDP) + case model.HumanExternalIDPRemoved, model.HumanExternalIDPCascadeRemoved: + err = externalIDP.SetData(event) + if err != nil { + return err + } + return m.view.DeleteExternalIDP(externalIDP.ExternalUserID, externalIDP.IDPConfigID, event.Sequence) + default: + return m.view.ProcessedExternalIDPSequence(event.Sequence) + } + if err != nil { + return err + } + return m.view.PutExternalIDP(externalIDP, externalIDP.Sequence) +} + +func (m *ExternalIDP) processIdpConfig(event *models.Event) (err error) { + switch event.Type { + case iam_es_model.IDPConfigChanged, org_es_model.IDPConfigChanged: + config := new(iam_model.IDPConfig) + config.AppendEvent(event) + exterinalIDPs, err := m.view.ExternalIDPsByIDPConfigID(config.IDPConfigID) + if err != nil { + return err + } + if event.AggregateType == iam_es_model.IAMAggregate { + config, err = m.iamEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID) + } else { + config, err = m.orgEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID) + } + if err != nil { + return err + } + for _, provider := range exterinalIDPs { + m.fillConfigData(provider, config) + } + return m.view.PutExternalIDPs(event.Sequence, exterinalIDPs...) + default: + return m.view.ProcessedExternalIDPSequence(event.Sequence) + } + return nil +} + +func (m *ExternalIDP) fillData(externalIDP *usr_view_model.ExternalIDPView) error { + config, err := m.orgEvents.GetIDPConfig(context.Background(), externalIDP.ResourceOwner, externalIDP.IDPConfigID) + if caos_errs.IsNotFound(err) { + config, err = m.iamEvents.GetIDPConfig(context.Background(), m.systemDefaults.IamID, externalIDP.IDPConfigID) + } + if err != nil { + return err + } + m.fillConfigData(externalIDP, config) + return nil +} + +func (m *ExternalIDP) fillConfigData(externalIDP *usr_view_model.ExternalIDPView, config *iam_model.IDPConfig) { + externalIDP.IDPName = config.Name +} + +func (m *ExternalIDP) OnError(event *models.Event, err error) error { + logging.LogWithFields("SPOOL-4Rsu8", "id", event.AggregateID).WithError(err).Warn("something went wrong in idp provider handler") + return spooler.HandleError(event, err, m.view.GetLatestExternalIDPFailedEvent, m.view.ProcessedExternalIDPFailedEvent, m.view.ProcessedExternalIDPSequence, m.errorCountUntilSkip) +} diff --git a/internal/admin/repository/eventsourcing/repository.go b/internal/admin/repository/eventsourcing/repository.go index e1c2d0d675..28eb20f654 100644 --- a/internal/admin/repository/eventsourcing/repository.go +++ b/internal/admin/repository/eventsourcing/repository.go @@ -87,6 +87,7 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, r IAMRepository: eventstore.IAMRepository{ IAMEventstore: iam, OrgEvents: org, + UserEvents: user, View: view, SystemDefaults: systemDefaults, SearchLimit: conf.SearchLimit, diff --git a/internal/admin/repository/eventsourcing/view/external_idps.go b/internal/admin/repository/eventsourcing/view/external_idps.go new file mode 100644 index 0000000000..103f4f76ef --- /dev/null +++ b/internal/admin/repository/eventsourcing/view/external_idps.go @@ -0,0 +1,73 @@ +package view + +import ( + "github.com/caos/zitadel/internal/errors" + usr_model "github.com/caos/zitadel/internal/user/model" + "github.com/caos/zitadel/internal/user/repository/view" + "github.com/caos/zitadel/internal/user/repository/view/model" + global_view "github.com/caos/zitadel/internal/view/repository" +) + +const ( + externalIDPTable = "adminapi.user_external_idps" +) + +func (v *View) ExternalIDPByExternalUserIDAndIDPConfigID(externalUserID, idpConfigID string) (*model.ExternalIDPView, error) { + return view.ExternalIDPByExternalUserIDAndIDPConfigID(v.Db, externalIDPTable, externalUserID, idpConfigID) +} + +func (v *View) ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(externalUserID, idpConfigID, resourceOwner string) (*model.ExternalIDPView, error) { + return view.ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(v.Db, externalIDPTable, externalUserID, idpConfigID, resourceOwner) +} + +func (v *View) ExternalIDPsByIDPConfigID(idpConfigID string) ([]*model.ExternalIDPView, error) { + return view.ExternalIDPsByIDPConfigID(v.Db, externalIDPTable, idpConfigID) +} + +func (v *View) ExternalIDPsByUserID(userID string) ([]*model.ExternalIDPView, error) { + return view.ExternalIDPsByUserID(v.Db, externalIDPTable, userID) +} + +func (v *View) SearchExternalIDPs(request *usr_model.ExternalIDPSearchRequest) ([]*model.ExternalIDPView, uint64, error) { + return view.SearchExternalIDPs(v.Db, externalIDPTable, request) +} + +func (v *View) PutExternalIDP(externalIDP *model.ExternalIDPView, sequence uint64) error { + err := view.PutExternalIDP(v.Db, externalIDPTable, externalIDP) + if err != nil { + return err + } + return v.ProcessedExternalIDPSequence(sequence) +} + +func (v *View) PutExternalIDPs(sequence uint64, externalIDPs ...*model.ExternalIDPView) error { + err := view.PutExternalIDPs(v.Db, externalIDPTable, externalIDPs...) + if err != nil { + return err + } + return v.ProcessedExternalIDPSequence(sequence) +} + +func (v *View) DeleteExternalIDP(externalUserID, idpConfigID string, eventSequence uint64) error { + err := view.DeleteExternalIDP(v.Db, externalIDPTable, externalUserID, idpConfigID) + if err != nil && !errors.IsNotFound(err) { + return err + } + return v.ProcessedExternalIDPSequence(eventSequence) +} + +func (v *View) GetLatestExternalIDPSequence() (*global_view.CurrentSequence, error) { + return v.latestSequence(externalIDPTable) +} + +func (v *View) ProcessedExternalIDPSequence(eventSequence uint64) error { + return v.saveCurrentSequence(externalIDPTable, eventSequence) +} + +func (v *View) GetLatestExternalIDPFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { + return v.latestFailedEvent(externalIDPTable, sequence) +} + +func (v *View) ProcessedExternalIDPFailedEvent(failedEvent *global_view.FailedEvent) error { + return v.saveFailedEvent(failedEvent) +} diff --git a/internal/api/grpc/admin/idp_config_converter.go b/internal/api/grpc/admin/idp_config_converter.go index c772dbd689..f387a0fa84 100644 --- a/internal/api/grpc/admin/idp_config_converter.go +++ b/internal/api/grpc/admin/idp_config_converter.go @@ -13,10 +13,12 @@ func createOidcIdpToModel(idp *admin.OidcIdpConfigCreate) *iam_model.IDPConfig { LogoSrc: idp.LogoSrc, Type: iam_model.IDPConfigTypeOIDC, OIDCConfig: &iam_model.OIDCIDPConfig{ - ClientID: idp.ClientId, - ClientSecretString: idp.ClientSecret, - Issuer: idp.Issuer, - Scopes: idp.Scopes, + ClientID: idp.ClientId, + ClientSecretString: idp.ClientSecret, + Issuer: idp.Issuer, + Scopes: idp.Scopes, + IDPDisplayNameMapping: oidcMappingFieldToModel(idp.IdpDisplayNameMapping), + UsernameMapping: oidcMappingFieldToModel(idp.UsernameMapping), }, } } @@ -31,11 +33,13 @@ func updateIdpToModel(idp *admin.IdpUpdate) *iam_model.IDPConfig { func updateOidcIdpToModel(idp *admin.OidcIdpConfigUpdate) *iam_model.OIDCIDPConfig { return &iam_model.OIDCIDPConfig{ - IDPConfigID: idp.IdpId, - ClientID: idp.ClientId, - ClientSecretString: idp.ClientSecret, - Issuer: idp.Issuer, - Scopes: idp.Scopes, + IDPConfigID: idp.IdpId, + ClientID: idp.ClientId, + ClientSecretString: idp.ClientSecret, + Issuer: idp.Issuer, + Scopes: idp.Scopes, + IDPDisplayNameMapping: oidcMappingFieldToModel(idp.IdpDisplayNameMapping), + UsernameMapping: oidcMappingFieldToModel(idp.UsernameMapping), } } @@ -105,9 +109,11 @@ func idpConfigViewFromModel(idp *iam_model.IDPConfigView) *admin.IdpView_OidcCon func oidcIdpConfigViewFromModel(idp *iam_model.IDPConfigView) *admin.OidcIdpConfigView { return &admin.OidcIdpConfigView{ - ClientId: idp.OIDCClientID, - Issuer: idp.OIDCIssuer, - Scopes: idp.OIDCScopes, + ClientId: idp.OIDCClientID, + Issuer: idp.OIDCIssuer, + Scopes: idp.OIDCScopes, + IdpDisplayNameMapping: oidcMappingFieldFromModel(idp.OIDCIDPDisplayNameMapping), + UsernameMapping: oidcMappingFieldFromModel(idp.OIDCUsernameMapping), } } @@ -122,6 +128,28 @@ func idpConfigStateFromModel(state iam_model.IDPConfigState) admin.IdpState { } } +func oidcMappingFieldFromModel(field iam_model.OIDCMappingField) admin.OIDCMappingField { + switch field { + case iam_model.OIDCMappingFieldPreferredLoginName: + return admin.OIDCMappingField_OIDCMAPPINGFIELD_PREFERRED_USERNAME + case iam_model.OIDCMappingFieldEmail: + return admin.OIDCMappingField_OIDCMAPPINGFIELD_EMAIL + default: + return admin.OIDCMappingField_OIDCMAPPINGFIELD_UNSPECIFIED + } +} + +func oidcMappingFieldToModel(field admin.OIDCMappingField) iam_model.OIDCMappingField { + switch field { + case admin.OIDCMappingField_OIDCMAPPINGFIELD_PREFERRED_USERNAME: + return iam_model.OIDCMappingFieldPreferredLoginName + case admin.OIDCMappingField_OIDCMAPPINGFIELD_EMAIL: + return iam_model.OIDCMappingFieldEmail + default: + return iam_model.OIDCMappingFieldUnspecified + } +} + func idpConfigSearchRequestToModel(request *admin.IdpSearchRequest) *iam_model.IDPConfigSearchRequest { return &iam_model.IDPConfigSearchRequest{ Limit: request.Limit, diff --git a/internal/api/grpc/auth/user.go b/internal/api/grpc/auth/user.go index ed3c013d45..ad0dba523f 100644 --- a/internal/api/grpc/auth/user.go +++ b/internal/api/grpc/auth/user.go @@ -122,6 +122,19 @@ func (s *Server) ChangeMyPassword(ctx context.Context, request *auth.PasswordCha return &empty.Empty{}, err } +func (s *Server) SearchMyExternalIDPs(ctx context.Context, request *auth.ExternalIDPSearchRequest) (*auth.ExternalIDPSearchResponse, error) { + externalIDP, err := s.repo.SearchMyExternalIDPs(ctx, externalIDPSearchRequestToModel(request)) + if err != nil { + return nil, err + } + return externalIDPSearchResponseFromModel(externalIDP), nil +} + +func (s *Server) RemoveMyExternalIDP(ctx context.Context, request *auth.ExternalIDPRemoveRequest) (*empty.Empty, error) { + err := s.repo.RemoveMyExternalIDP(ctx, externalIDPRemoveToModel(ctx, request)) + return &empty.Empty{}, err +} + func (s *Server) GetMyPasswordComplexityPolicy(ctx context.Context, _ *empty.Empty) (*auth.PasswordComplexityPolicy, error) { policy, err := s.repo.GetMyPasswordComplexityPolicy(ctx) if err != nil { diff --git a/internal/api/grpc/auth/user_converter.go b/internal/api/grpc/auth/user_converter.go index f65dc32bd4..e5c585e8eb 100644 --- a/internal/api/grpc/auth/user_converter.go +++ b/internal/api/grpc/auth/user_converter.go @@ -3,7 +3,6 @@ package auth import ( "context" "encoding/json" - "github.com/caos/logging" "github.com/golang/protobuf/ptypes" "golang.org/x/text/language" @@ -242,6 +241,69 @@ func updateAddressToModel(ctx context.Context, address *auth.UpdateUserAddressRe } } +func externalIDPSearchRequestToModel(request *auth.ExternalIDPSearchRequest) *usr_model.ExternalIDPSearchRequest { + return &usr_model.ExternalIDPSearchRequest{ + Limit: request.Limit, + Offset: request.Offset, + } +} + +func externalIDPRemoveToModel(ctx context.Context, idp *auth.ExternalIDPRemoveRequest) *usr_model.ExternalIDP { + return &usr_model.ExternalIDP{ + ObjectRoot: models.ObjectRoot{AggregateID: authz.GetCtxData(ctx).UserID}, + IDPConfigID: idp.IdpConfigId, + UserID: idp.ExternalUserId, + } +} + +func externalIDPResponseFromModel(idp *usr_model.ExternalIDP) *auth.ExternalIDPResponse { + return &auth.ExternalIDPResponse{ + IdpConfigId: idp.IDPConfigID, + UserId: idp.UserID, + DisplayName: idp.DisplayName, + } +} + +func externalIDPSearchResponseFromModel(response *usr_model.ExternalIDPSearchResponse) *auth.ExternalIDPSearchResponse { + viewTimestamp, err := ptypes.TimestampProto(response.Timestamp) + logging.Log("GRPC-3h8is").OnError(err).Debug("unable to parse timestamp") + + return &auth.ExternalIDPSearchResponse{ + Offset: response.Offset, + Limit: response.Limit, + TotalResult: response.TotalResult, + ProcessedSequence: response.Sequence, + ViewTimestamp: viewTimestamp, + Result: externalIDPViewsFromModel(response.Result), + } +} + +func externalIDPViewsFromModel(externalIDPs []*usr_model.ExternalIDPView) []*auth.ExternalIDPView { + converted := make([]*auth.ExternalIDPView, len(externalIDPs)) + for i, externalIDP := range externalIDPs { + converted[i] = externalIDPViewFromModel(externalIDP) + } + return converted +} + +func externalIDPViewFromModel(externalIDP *usr_model.ExternalIDPView) *auth.ExternalIDPView { + creationDate, err := ptypes.TimestampProto(externalIDP.CreationDate) + logging.Log("GRPC-Sj8dw").OnError(err).Debug("unable to parse timestamp") + + changeDate, err := ptypes.TimestampProto(externalIDP.ChangeDate) + logging.Log("GRPC-Nf8ue").OnError(err).Debug("unable to parse timestamp") + + return &auth.ExternalIDPView{ + UserId: externalIDP.UserID, + IdpConfigId: externalIDP.IDPConfigID, + ExternalUserId: externalIDP.ExternalUserID, + ExternalUserDisplayName: externalIDP.UserDisplayName, + IdpName: externalIDP.IDPName, + CreationDate: creationDate, + ChangeDate: changeDate, + } +} + func otpFromModel(otp *usr_model.OTP) *auth.MfaOtpResponse { return &auth.MfaOtpResponse{ UserId: otp.AggregateID, diff --git a/internal/api/grpc/management/idp_config_converter.go b/internal/api/grpc/management/idp_config_converter.go index 32f35dc332..db1297d76b 100644 --- a/internal/api/grpc/management/idp_config_converter.go +++ b/internal/api/grpc/management/idp_config_converter.go @@ -13,10 +13,12 @@ func createOidcIdpToModel(idp *management.OidcIdpConfigCreate) *iam_model.IDPCon LogoSrc: idp.LogoSrc, Type: iam_model.IDPConfigTypeOIDC, OIDCConfig: &iam_model.OIDCIDPConfig{ - ClientID: idp.ClientId, - ClientSecretString: idp.ClientSecret, - Issuer: idp.Issuer, - Scopes: idp.Scopes, + ClientID: idp.ClientId, + ClientSecretString: idp.ClientSecret, + Issuer: idp.Issuer, + Scopes: idp.Scopes, + IDPDisplayNameMapping: oidcMappingFieldToModel(idp.IdpDisplayNameMapping), + UsernameMapping: oidcMappingFieldToModel(idp.UsernameMapping), }, } } @@ -31,11 +33,13 @@ func updateIdpToModel(idp *management.IdpUpdate) *iam_model.IDPConfig { func updateOidcIdpToModel(idp *management.OidcIdpConfigUpdate) *iam_model.OIDCIDPConfig { return &iam_model.OIDCIDPConfig{ - IDPConfigID: idp.IdpId, - ClientID: idp.ClientId, - ClientSecretString: idp.ClientSecret, - Issuer: idp.Issuer, - Scopes: idp.Scopes, + IDPConfigID: idp.IdpId, + ClientID: idp.ClientId, + ClientSecretString: idp.ClientSecret, + Issuer: idp.Issuer, + Scopes: idp.Scopes, + IDPDisplayNameMapping: oidcMappingFieldToModel(idp.IdpDisplayNameMapping), + UsernameMapping: oidcMappingFieldToModel(idp.UsernameMapping), } } @@ -89,9 +93,11 @@ func idpConfigFromModel(idp *iam_model.IDPConfig) *management.Idp_OidcConfig { func oidcIdpConfigFromModel(idp *iam_model.OIDCIDPConfig) *management.OidcIdpConfig { return &management.OidcIdpConfig{ - ClientId: idp.ClientID, - Issuer: idp.Issuer, - Scopes: idp.Scopes, + ClientId: idp.ClientID, + Issuer: idp.Issuer, + Scopes: idp.Scopes, + IdpDisplayNameMapping: oidcMappingFieldFromModel(idp.IDPDisplayNameMapping), + UsernameMapping: oidcMappingFieldFromModel(idp.UsernameMapping), } } @@ -106,9 +112,11 @@ func idpConfigViewFromModel(idp *iam_model.IDPConfigView) *management.IdpView_Oi func oidcIdpConfigViewFromModel(idp *iam_model.IDPConfigView) *management.OidcIdpConfigView { return &management.OidcIdpConfigView{ - ClientId: idp.OIDCClientID, - Issuer: idp.OIDCIssuer, - Scopes: idp.OIDCScopes, + ClientId: idp.OIDCClientID, + Issuer: idp.OIDCIssuer, + Scopes: idp.OIDCScopes, + IdpDisplayNameMapping: oidcMappingFieldFromModel(idp.OIDCIDPDisplayNameMapping), + UsernameMapping: oidcMappingFieldFromModel(idp.OIDCUsernameMapping), } } @@ -181,3 +189,25 @@ func idpConfigsFromView(viewIdps []*iam_model.IDPConfigView) []*management.IdpVi } return idps } + +func oidcMappingFieldFromModel(field iam_model.OIDCMappingField) management.OIDCMappingField { + switch field { + case iam_model.OIDCMappingFieldPreferredLoginName: + return management.OIDCMappingField_OIDCMAPPINGFIELD_PREFERRED_USERNAME + case iam_model.OIDCMappingFieldEmail: + return management.OIDCMappingField_OIDCMAPPINGFIELD_EMAIL + default: + return management.OIDCMappingField_OIDCMAPPINGFIELD_UNSPECIFIED + } +} + +func oidcMappingFieldToModel(field management.OIDCMappingField) iam_model.OIDCMappingField { + switch field { + case management.OIDCMappingField_OIDCMAPPINGFIELD_PREFERRED_USERNAME: + return iam_model.OIDCMappingFieldPreferredLoginName + case management.OIDCMappingField_OIDCMAPPINGFIELD_EMAIL: + return iam_model.OIDCMappingFieldEmail + default: + return iam_model.OIDCMappingFieldUnspecified + } +} diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index a299026937..acd1b0ef24 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -2,7 +2,6 @@ package management import ( "context" - "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/pkg/grpc/management" @@ -196,6 +195,19 @@ func (s *Server) SetInitialPassword(ctx context.Context, request *management.Pas return &empty.Empty{}, err } +func (s *Server) SearchUserExternalIDPs(ctx context.Context, request *management.ExternalIDPSearchRequest) (*management.ExternalIDPSearchResponse, error) { + externalIDP, err := s.user.SearchExternalIDPs(ctx, externalIDPSearchRequestToModel(request)) + if err != nil { + return nil, err + } + return externalIDPSearchResponseFromModel(externalIDP), nil +} + +func (s *Server) RemoveExternalIDP(ctx context.Context, request *management.ExternalIDPRemoveRequest) (*empty.Empty, error) { + err := s.user.RemoveExternalIDP(ctx, externalIDPRemoveToModel(request)) + return &empty.Empty{}, err +} + func (s *Server) GetUserMfas(ctx context.Context, userID *management.UserID) (*management.MultiFactors, error) { mfas, err := s.user.UserMfas(ctx, userID.Id) if err != nil { diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go index 2b748680d9..0a4c90ef29 100644 --- a/internal/api/grpc/management/user_converter.go +++ b/internal/api/grpc/management/user_converter.go @@ -2,8 +2,8 @@ package management import ( "encoding/json" - "github.com/caos/logging" + "github.com/caos/zitadel/internal/model" "github.com/golang/protobuf/ptypes" "golang.org/x/text/language" "google.golang.org/protobuf/encoding/protojson" @@ -66,6 +66,62 @@ func passwordRequestToModel(r *management.PasswordRequest) *usr_model.Password { } } +func externalIDPSearchRequestToModel(request *management.ExternalIDPSearchRequest) *usr_model.ExternalIDPSearchRequest { + return &usr_model.ExternalIDPSearchRequest{ + Limit: request.Limit, + Offset: request.Offset, + Queries: []*usr_model.ExternalIDPSearchQuery{{Key: usr_model.ExternalIDPSearchKeyUserID, Method: model.SearchMethodEquals, Value: request.UserId}}, + } +} + +func externalIDPRemoveToModel(idp *management.ExternalIDPRemoveRequest) *usr_model.ExternalIDP { + return &usr_model.ExternalIDP{ + ObjectRoot: models.ObjectRoot{AggregateID: idp.UserId}, + IDPConfigID: idp.IdpConfigId, + UserID: idp.ExternalUserId, + } +} + +func externalIDPSearchResponseFromModel(response *usr_model.ExternalIDPSearchResponse) *management.ExternalIDPSearchResponse { + viewTimestamp, err := ptypes.TimestampProto(response.Timestamp) + logging.Log("GRPC-3h8is").OnError(err).Debug("unable to parse timestamp") + + return &management.ExternalIDPSearchResponse{ + Offset: response.Offset, + Limit: response.Limit, + TotalResult: response.TotalResult, + ProcessedSequence: response.Sequence, + ViewTimestamp: viewTimestamp, + Result: externalIDPViewsFromModel(response.Result), + } +} + +func externalIDPViewsFromModel(externalIDPs []*usr_model.ExternalIDPView) []*management.ExternalIDPView { + converted := make([]*management.ExternalIDPView, len(externalIDPs)) + for i, externalIDP := range externalIDPs { + converted[i] = externalIDPViewFromModel(externalIDP) + } + return converted +} + +func externalIDPViewFromModel(externalIDP *usr_model.ExternalIDPView) *management.ExternalIDPView { + creationDate, err := ptypes.TimestampProto(externalIDP.CreationDate) + logging.Log("GRPC-Fdu8s").OnError(err).Debug("unable to parse timestamp") + + changeDate, err := ptypes.TimestampProto(externalIDP.ChangeDate) + logging.Log("GRPC-Was7u").OnError(err).Debug("unable to parse timestamp") + + return &management.ExternalIDPView{ + UserId: externalIDP.UserID, + IdpConfigId: externalIDP.IDPConfigID, + ExternalUserId: externalIDP.ExternalUserID, + ExternalUserDisplayName: externalIDP.UserDisplayName, + IdpName: externalIDP.IDPName, + CreationDate: creationDate, + ChangeDate: changeDate, + } +} + func userSearchRequestsToModel(project *management.UserSearchRequest) *usr_model.UserSearchRequest { return &usr_model.UserSearchRequest{ Offset: project.Offset, diff --git a/internal/auth/repository/auth_request.go b/internal/auth/repository/auth_request.go index 6e3781ed22..8779b7fb7c 100644 --- a/internal/auth/repository/auth_request.go +++ b/internal/auth/repository/auth_request.go @@ -2,6 +2,8 @@ package repository import ( "context" + org_model "github.com/caos/zitadel/internal/org/model" + user_model "github.com/caos/zitadel/internal/user/model" "github.com/caos/zitadel/internal/auth_request/model" ) @@ -14,7 +16,11 @@ type AuthRequestRepository interface { SaveAuthCode(ctx context.Context, id, code, userAgentID string) error DeleteAuthRequest(ctx context.Context, id string) error CheckLoginName(ctx context.Context, id, loginName, userAgentID string) error + CheckExternalUserLogin(ctx context.Context, authReqID, userAgentID string, user *model.ExternalUser) error SelectUser(ctx context.Context, id, userID, userAgentID string) error + SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string) error VerifyPassword(ctx context.Context, id, userID, password, userAgentID string, info *model.BrowserInfo) error VerifyMfaOTP(ctx context.Context, agentID, authRequestID, code, userAgentID string, info *model.BrowserInfo) error + LinkExternalUsers(ctx context.Context, authReqID, userAgentID string) error + AutoRegisterExternalUser(ctx context.Context, user *user_model.User, externalIDP *user_model.ExternalIDP, member *org_model.OrgMember, authReqID, userAgentID, resourceOwner string) error } diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 4392baf6a0..2deb258719 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -2,6 +2,12 @@ package eventstore import ( "context" + "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/eventstore/sdk" + iam_model "github.com/caos/zitadel/internal/iam/model" + iam_es_model "github.com/caos/zitadel/internal/iam/repository/view/model" + org_event "github.com/caos/zitadel/internal/org/repository/eventsourcing" + policy_event "github.com/caos/zitadel/internal/policy/repository/eventsourcing" "time" "github.com/caos/logging" @@ -11,6 +17,7 @@ import ( cache "github.com/caos/zitadel/internal/auth_request/repository" "github.com/caos/zitadel/internal/errors" es_models "github.com/caos/zitadel/internal/eventstore/models" + iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model" "github.com/caos/zitadel/internal/id" org_model "github.com/caos/zitadel/internal/org/model" org_view_model "github.com/caos/zitadel/internal/org/repository/view/model" @@ -22,6 +29,8 @@ import ( type AuthRequestRepo struct { UserEvents *user_event.UserEventstore + OrgEvents *org_event.OrgEventstore + PolicyEvents *policy_event.PolicyEventstore AuthRequests cache.AuthRequestCache View *view.View @@ -29,6 +38,8 @@ type AuthRequestRepo struct { UserViewProvider userViewProvider UserEventProvider userEventProvider OrgViewProvider orgViewProvider + LoginPolicyViewProvider loginPolicyViewProvider + IDPProviderViewProvider idpProviderViewProvider IdGenerator id.Generator @@ -36,6 +47,8 @@ type AuthRequestRepo struct { MfaInitSkippedLifeTime time.Duration MfaSoftwareCheckLifeTime time.Duration MfaHardwareCheckLifeTime time.Duration + + IAMID string } type userSessionViewProvider interface { @@ -46,8 +59,17 @@ type userViewProvider interface { UserByID(string) (*user_view_model.UserView, error) } +type loginPolicyViewProvider interface { + LoginPolicyByAggregateID(string) (*iam_view_model.LoginPolicyView, error) +} + +type idpProviderViewProvider interface { + IDPProvidersByAggregateID(string) ([]*iam_view_model.IDPProviderView, error) +} + type userEventProvider interface { UserEventsByID(ctx context.Context, id string, sequence uint64) ([]*es_models.Event, error) + BulkAddExternalIDPs(ctx context.Context, userID string, externalIDPs []*user_model.ExternalIDP) error } type orgViewProvider interface { @@ -73,7 +95,7 @@ func (repo *AuthRequestRepo) CreateAuthRequest(ctx context.Context, request *mod } request.Audience = ids if request.LoginHint != "" { - err = repo.checkLoginName(request, request.LoginHint) + err = repo.checkLoginName(ctx, request, request.LoginHint) logging.LogWithFields("EVENT-aG311", "login name", request.LoginHint, "id", request.ID, "applicationID", request.ApplicationID).Debug("login hint invalid") } err = repo.AuthRequests.SaveAuthRequest(ctx, request) @@ -122,13 +144,45 @@ func (repo *AuthRequestRepo) CheckLoginName(ctx context.Context, id, loginName, if err != nil { return err } - err = repo.checkLoginName(request, loginName) + err = repo.checkLoginName(ctx, request, loginName) if err != nil { return err } return repo.AuthRequests.UpdateAuthRequest(ctx, request) } +func (repo *AuthRequestRepo) SelectExternalIDP(ctx context.Context, authReqID, idpConfigID, userAgentID string) error { + request, err := repo.getAuthRequest(ctx, authReqID, userAgentID) + if err != nil { + return err + } + err = repo.checkSelectedExternalIDP(request, idpConfigID) + if err != nil { + return err + } + return repo.AuthRequests.UpdateAuthRequest(ctx, request) +} + +func (repo *AuthRequestRepo) CheckExternalUserLogin(ctx context.Context, authReqID, userAgentID string, externalUser *model.ExternalUser) error { + request, err := repo.getAuthRequest(ctx, authReqID, userAgentID) + if err != nil { + return err + } + err = repo.checkExternalUserLogin(request, externalUser.IDPConfigID, externalUser.ExternalUserID) + if errors.IsNotFound(err) { + return repo.setLinkingUser(ctx, request, externalUser) + } + if err != nil { + return err + } + return repo.AuthRequests.UpdateAuthRequest(ctx, request) +} + +func (repo *AuthRequestRepo) setLinkingUser(ctx context.Context, request *model.AuthRequest, externalUser *model.ExternalUser) error { + request.LinkingUsers = append(request.LinkingUsers, externalUser) + return repo.AuthRequests.UpdateAuthRequest(ctx, request) +} + func (repo *AuthRequestRepo) SelectUser(ctx context.Context, id, userID, userAgentID string) error { request, err := repo.getAuthRequest(ctx, id, userAgentID) if err != nil { @@ -164,6 +218,59 @@ func (repo *AuthRequestRepo) VerifyMfaOTP(ctx context.Context, authRequestID, us return repo.UserEvents.CheckMfaOTP(ctx, userID, code, request.WithCurrentInfo(info)) } +func (repo *AuthRequestRepo) LinkExternalUsers(ctx context.Context, authReqID, userAgentID string) error { + request, err := repo.getAuthRequest(ctx, authReqID, userAgentID) + if err != nil { + return err + } + err = linkExternalIDPs(ctx, repo.UserEventProvider, request) + if err != nil { + return err + } + request.LinkingUsers = nil + return repo.AuthRequests.UpdateAuthRequest(ctx, request) +} + +func (repo *AuthRequestRepo) AutoRegisterExternalUser(ctx context.Context, registerUser *user_model.User, externalIDP *user_model.ExternalIDP, orgMember *org_model.OrgMember, authReqID, userAgentID, resourceOwner string) error { + request, err := repo.getAuthRequest(ctx, authReqID, userAgentID) + if err != nil { + return err + } + policyResourceOwner := authz.GetCtxData(ctx).OrgID + if resourceOwner != "" { + policyResourceOwner = resourceOwner + } + pwPolicy, err := repo.PolicyEvents.GetPasswordComplexityPolicy(ctx, policyResourceOwner) + if err != nil { + return err + } + orgPolicy, err := repo.OrgEvents.GetOrgIAMPolicy(ctx, policyResourceOwner) + if err != nil { + return err + } + user, aggregates, err := repo.UserEvents.PrepareRegisterUser(ctx, registerUser, externalIDP, pwPolicy, orgPolicy, resourceOwner) + if err != nil { + return err + } + if orgMember != nil { + orgMember.UserID = user.AggregateID + _, memberAggregate, err := repo.OrgEvents.PrepareAddOrgMember(ctx, orgMember, policyResourceOwner) + if err != nil { + return err + } + aggregates = append(aggregates, memberAggregate) + } + + err = sdk.PushAggregates(ctx, repo.UserEvents.PushAggregates, user.AppendEvents, aggregates...) + if err != nil { + return err + } + request.UserID = user.AggregateID + request.SelectedIDPConfigID = externalIDP.IDPConfigID + request.LinkingUsers = nil + return repo.AuthRequests.UpdateAuthRequest(ctx, request) +} + func (repo *AuthRequestRepo) getAuthRequestNextSteps(ctx context.Context, id, userAgentID string, checkLoggedIn bool) (*model.AuthRequest, error) { request, err := repo.getAuthRequest(ctx, id, userAgentID) if err != nil { @@ -185,18 +292,114 @@ func (repo *AuthRequestRepo) getAuthRequest(ctx context.Context, id, userAgentID if request.AgentID != userAgentID { return nil, errors.ThrowPermissionDenied(nil, "EVENT-adk13", "Errors.AuthRequest.UserAgentNotCorresponding") } + err = repo.fillLoginPolicy(ctx, request) + if err != nil { + return nil, err + } return request, nil } -func (repo *AuthRequestRepo) checkLoginName(request *model.AuthRequest, loginName string) error { - user, err := repo.View.UserByLoginName(loginName) +func (repo *AuthRequestRepo) getLoginPolicyAndIDPProviders(ctx context.Context, orgID string) (*iam_model.LoginPolicyView, []*iam_model.IDPProviderView, error) { + policy, err := repo.getLoginPolicy(ctx, orgID) + if err != nil { + return nil, nil, err + } + if !policy.AllowExternalIDP { + return policy, nil, nil + } + idpProviders, err := getLoginPolicyIDPProviders(repo.IDPProviderViewProvider, repo.IAMID, orgID, policy.Default) + if err != nil { + return nil, nil, err + } + return policy, idpProviders, nil +} + +func (repo *AuthRequestRepo) fillLoginPolicy(ctx context.Context, request *model.AuthRequest) error { + orgID := request.UserOrgID + if orgID == "" { + orgID = request.GetScopeOrgID() + } + if orgID == "" { + orgID = repo.IAMID + } + + policy, idpProviders, err := repo.getLoginPolicyAndIDPProviders(ctx, orgID) if err != nil { return err } + request.LoginPolicy = policy + if idpProviders != nil { + request.AllowedExternalIDPs = idpProviders + } + return nil +} + +func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *model.AuthRequest, loginName string) (err error) { + orgID := request.GetScopeOrgID() + user := new(user_view_model.UserView) + if orgID != "" { + user, err = repo.View.UserByLoginNameAndResourceOwner(loginName, orgID) + } else { + user, err = repo.View.UserByLoginName(loginName) + if err == nil { + err = repo.checkLoginPolicyWithResourceOwner(ctx, request, user) + if err != nil { + return err + } + } + } + if err != nil { + return err + } + request.SetUserInfo(user.ID, loginName, "", user.ResourceOwner) return nil } +func (repo AuthRequestRepo) checkLoginPolicyWithResourceOwner(ctx context.Context, request *model.AuthRequest, user *user_view_model.UserView) error { + loginPolicy, idpProviders, err := repo.getLoginPolicyAndIDPProviders(ctx, user.ResourceOwner) + if err != nil { + return err + } + if len(request.LinkingUsers) != 0 && !loginPolicy.AllowExternalIDP { + return errors.ThrowInvalidArgument(nil, "LOGIN-s9sio", "Errors.User.NotAllowedToLink") + } + if len(request.LinkingUsers) != 0 { + exists := linkingIDPConfigExistingInAllowedIDPs(request.LinkingUsers, idpProviders) + if !exists { + return errors.ThrowInvalidArgument(nil, "LOGIN-Dj89o", "Errors.User.NotAllowedToLink") + } + } + request.LoginPolicy = loginPolicy + request.AllowedExternalIDPs = idpProviders + return nil +} + +func (repo *AuthRequestRepo) checkSelectedExternalIDP(request *model.AuthRequest, idpConfigID string) error { + for _, externalIDP := range request.AllowedExternalIDPs { + if externalIDP.IDPConfigID == idpConfigID { + request.SelectedIDPConfigID = idpConfigID + return nil + } + } + return errors.ThrowNotFound(nil, "LOGIN-Nsm8r", "Errors.User.ExternalIDP.NotAllowed") +} + +func (repo *AuthRequestRepo) checkExternalUserLogin(request *model.AuthRequest, idpConfigID, externalUserID string) (err error) { + orgID := request.GetScopeOrgID() + externalIDP := new(user_view_model.ExternalIDPView) + if orgID != "" { + externalIDP, err = repo.View.ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(externalUserID, idpConfigID, orgID) + } else { + externalIDP, err = repo.View.ExternalIDPByExternalUserIDAndIDPConfigID(externalUserID, idpConfigID) + } + if err != nil { + return err + } + request.SetUserInfo(externalIDP.UserID, "", "", externalIDP.ResourceOwner) + return nil +} + func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *model.AuthRequest, checkLoggedIn bool) ([]model.NextStep, error) { if request == nil { return nil, errors.ThrowInvalidArgument(nil, "EVENT-ds27a", "Errors.Internal") @@ -206,7 +409,11 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *model.AuthR return append(steps, &model.RedirectToCallbackStep{}), nil } if request.UserID == "" { - steps = append(steps, &model.LoginStep{}) + if request.LinkingUsers != nil && len(request.LinkingUsers) > 0 { + steps = append(steps, new(model.ExternalNotFoundOptionStep)) + return steps, nil + } + steps = append(steps, new(model.LoginStep)) if request.Prompt == model.PromptSelectAccount || request.Prompt == model.PromptUnspecified { users, err := repo.usersForUserSelection(request) if err != nil { @@ -222,23 +429,26 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *model.AuthR if err != nil { return nil, err } + request.LoginName = user.PreferredLoginName userSession, err := userSessionByIDs(ctx, repo.UserSessionViewProvider, repo.UserEventProvider, request.AgentID, user) if err != nil { return nil, err } - if user.InitRequired { - return append(steps, &model.InitUserStep{PasswordSet: user.PasswordSet}), nil - } - if !user.PasswordSet { - return append(steps, &model.InitPasswordStep{}), nil - } + if request.SelectedIDPConfigID == "" { + if user.InitRequired { + return append(steps, &model.InitUserStep{PasswordSet: user.PasswordSet}), nil + } + if !user.PasswordSet { + return append(steps, &model.InitPasswordStep{}), nil + } - if !checkVerificationTime(userSession.PasswordVerification, repo.PasswordCheckLifeTime) { - return append(steps, &model.PasswordStep{}), nil + if !checkVerificationTime(userSession.PasswordVerification, repo.PasswordCheckLifeTime) { + return append(steps, &model.PasswordStep{}), nil + } + request.PasswordVerified = true + request.AuthTime = userSession.PasswordVerification } - request.PasswordVerified = true - request.AuthTime = userSession.PasswordVerification if step, ok := repo.mfaChecked(userSession, request, user); !ok { return append(steps, step), nil @@ -258,6 +468,10 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *model.AuthR return steps, nil } + if request.LinkingUsers != nil && len(request.LinkingUsers) != 0 { + return append(steps, &model.LinkUsersStep{}), nil + + } //PLANNED: consent step return append(steps, &model.RedirectToCallbackStep{}), nil } @@ -322,6 +536,36 @@ func (repo *AuthRequestRepo) mfaSkippedOrSetUp(user *user_model.UserView) bool { return checkVerificationTime(user.MfaInitSkipped, repo.MfaInitSkippedLifeTime) } +func (repo *AuthRequestRepo) getLoginPolicy(ctx context.Context, orgID string) (*iam_model.LoginPolicyView, error) { + policy, err := repo.View.LoginPolicyByAggregateID(orgID) + if errors.IsNotFound(err) { + policy, err = repo.View.LoginPolicyByAggregateID(repo.IAMID) + if err != nil { + return nil, err + } + policy.Default = true + } + if err != nil { + return nil, err + } + return iam_es_model.LoginPolicyViewToModel(policy), err +} + +func getLoginPolicyIDPProviders(provider idpProviderViewProvider, iamID, orgID string, defaultPolicy bool) ([]*iam_model.IDPProviderView, error) { + if defaultPolicy { + idpProviders, err := provider.IDPProvidersByAggregateID(iamID) + if err != nil { + return nil, err + } + return iam_es_model.IDPProviderViewsToModel(idpProviders), nil + } + idpProviders, err := provider.IDPProvidersByAggregateID(orgID) + if err != nil { + return nil, err + } + return iam_es_model.IDPProviderViewsToModel(idpProviders), nil +} + func checkVerificationTime(verificationTime time.Time, lifetime time.Duration) bool { return verificationTime.Add(lifetime).After(time.Now().UTC()) } @@ -422,3 +666,37 @@ func userByID(ctx context.Context, viewProvider userViewProvider, eventProvider } return user_view_model.UserToModel(&userCopy), nil } + +func linkExternalIDPs(ctx context.Context, userEventProvider userEventProvider, request *model.AuthRequest) error { + externalIDPs := make([]*user_model.ExternalIDP, len(request.LinkingUsers)) + for i, linkingUser := range request.LinkingUsers { + externalIDP := &user_model.ExternalIDP{ + ObjectRoot: es_models.ObjectRoot{AggregateID: request.UserID}, + IDPConfigID: linkingUser.IDPConfigID, + UserID: linkingUser.ExternalUserID, + DisplayName: linkingUser.DisplayName, + } + externalIDPs[i] = externalIDP + } + data := authz.CtxData{ + UserID: "LOGIN", + OrgID: request.UserOrgID, + } + return userEventProvider.BulkAddExternalIDPs(authz.SetCtxData(ctx, data), request.UserID, externalIDPs) +} + +func linkingIDPConfigExistingInAllowedIDPs(linkingUsers []*model.ExternalUser, idpProviders []*iam_model.IDPProviderView) bool { + for _, linkingUser := range linkingUsers { + exists := false + for _, idp := range idpProviders { + if idp.IDPConfigID == linkingUser.IDPConfigID { + exists = true + continue + } + } + if !exists { + return false + } + } + return true +} diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index defda5dd3b..848b615b42 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -88,12 +88,20 @@ func (m *mockEventUser) UserEventsByID(ctx context.Context, id string, sequence return events, nil } +func (m *mockEventUser) BulkAddExternalIDPs(ctx context.Context, userID string, externalIDPs []*user_model.ExternalIDP) error { + return nil +} + type mockEventErrUser struct{} func (m *mockEventErrUser) UserEventsByID(ctx context.Context, id string, sequence uint64) ([]*es_models.Event, error) { return nil, errors.ThrowInternal(nil, "id", "internal error") } +func (m *mockEventErrUser) BulkAddExternalIDPs(ctx context.Context, userID string, externalIDPs []*user_model.ExternalIDP) error { + return errors.ThrowInternal(nil, "id", "internal error") +} + type mockViewUser struct { InitRequired bool PasswordSet bool @@ -185,6 +193,15 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { []model.NextStep{&model.LoginStep{}}, nil, }, + { + "user not set no active session, linking users, external user not found option", + fields{ + userSessionViewProvider: &mockViewNoUserSession{}, + }, + args{&model.AuthRequest{LinkingUsers: []*model.ExternalUser{{IDPConfigID: "IDPConfigID", ExternalUserID: "ExternalUserID"}}}, false}, + []model.NextStep{&model.ExternalNotFoundOptionStep{}}, + nil, + }, { "user not set, prompt select account and internal error, internal error", fields{ @@ -363,6 +380,24 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { []model.NextStep{&model.InitPasswordStep{}}, nil, }, + { + "external user (no password set), callback", + fields{ + userSessionViewProvider: &mockViewUserSession{ + MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute), + }, + userViewProvider: &mockViewUser{ + IsEmailVerified: true, + MfaMaxSetUp: int32(model.MfaLevelSoftware), + }, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, + MfaSoftwareCheckLifeTime: 18 * time.Hour, + }, + args{&model.AuthRequest{UserID: "UserID", SelectedIDPConfigID: "IDPConfigID"}, false}, + []model.NextStep{&model.RedirectToCallbackStep{}}, + nil, + }, { "password not verified, password check step", fields{ @@ -378,6 +413,25 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { []model.NextStep{&model.PasswordStep{}}, nil, }, + { + "external user (no password check needed), callback", + fields{ + userSessionViewProvider: &mockViewUserSession{ + MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute), + }, + userViewProvider: &mockViewUser{ + PasswordSet: true, + IsEmailVerified: true, + MfaMaxSetUp: int32(model.MfaLevelSoftware), + }, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, + MfaSoftwareCheckLifeTime: 18 * time.Hour, + }, + args{&model.AuthRequest{UserID: "UserID", SelectedIDPConfigID: "IDPConfigID"}, false}, + []model.NextStep{&model.RedirectToCallbackStep{}}, + nil, + }, { "mfa not verified, mfa check step", fields{ @@ -400,6 +454,28 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }}, nil, }, + { + "external user, mfa not verified, mfa check step", + fields{ + userSessionViewProvider: &mockViewUserSession{ + PasswordVerification: time.Now().UTC().Add(-5 * time.Minute), + }, + userViewProvider: &mockViewUser{ + PasswordSet: true, + OTPState: int32(user_model.MfaStateReady), + MfaMaxSetUp: int32(model.MfaLevelSoftware), + }, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, + PasswordCheckLifeTime: 10 * 24 * time.Hour, + MfaSoftwareCheckLifeTime: 18 * time.Hour, + }, + args{&model.AuthRequest{UserID: "UserID", SelectedIDPConfigID: "IDPConfigID"}, false}, + []model.NextStep{&model.MfaVerificationStep{ + MfaProviders: []model.MfaType{model.MfaTypeOTP}, + }}, + nil, + }, { "password change required and email verified, password change step", fields{ @@ -505,6 +581,30 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { []model.NextStep{&model.RedirectToCallbackStep{}}, nil, }, + { + "linking users, link users step", + fields{ + userSessionViewProvider: &mockViewUserSession{ + MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute), + }, + userViewProvider: &mockViewUser{ + PasswordSet: true, + IsEmailVerified: true, + MfaMaxSetUp: int32(model.MfaLevelSoftware), + }, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive}, + MfaSoftwareCheckLifeTime: 18 * time.Hour, + }, + args{ + &model.AuthRequest{ + UserID: "UserID", + SelectedIDPConfigID: "IDPConfigID", + LinkingUsers: []*model.ExternalUser{{IDPConfigID: "IDPConfigID", ExternalUserID: "UserID", DisplayName: "DisplayName"}}, + }, false}, + []model.NextStep{&model.LinkUsersStep{}}, + nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/auth/repository/eventsourcing/eventstore/org.go b/internal/auth/repository/eventsourcing/eventstore/org.go index 28f4df949c..fb4f46c7c4 100644 --- a/internal/auth/repository/eventsourcing/eventstore/org.go +++ b/internal/auth/repository/eventsourcing/eventstore/org.go @@ -2,8 +2,9 @@ package eventstore import ( "context" - "github.com/caos/logging" + iam_model "github.com/caos/zitadel/internal/iam/model" + iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model" auth_model "github.com/caos/zitadel/internal/auth/model" auth_view "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" @@ -41,7 +42,7 @@ func (repo *OrgRepository) SearchOrgs(ctx context.Context, request *org_model.Or result := &org_model.OrgSearchResult{ Offset: request.Offset, Limit: request.Limit, - TotalResult: uint64(count), + TotalResult: count, Result: model.OrgsToModel(members), } if err == nil { @@ -71,7 +72,7 @@ func (repo *OrgRepository) RegisterOrg(ctx context.Context, register *auth_model if err != nil { return nil, err } - user, userAggregates, err := repo.UserEventstore.PrepareRegisterUser(ctx, register.User, pwPolicy, orgPolicy, org.AggregateID) + user, userAggregates, err := repo.UserEventstore.PrepareRegisterUser(ctx, register.User, nil, pwPolicy, orgPolicy, org.AggregateID) if err != nil { return nil, err } @@ -94,6 +95,18 @@ func (repo *OrgRepository) RegisterOrg(ctx context.Context, register *auth_model return RegisterToModel(registerModel), nil } -func (repo *OrgRepository) GetOrgIamPolicy(ctx context.Context, orgID string) (*org_model.OrgIAMPolicy, error) { - return repo.OrgEventstore.GetOrgIAMPolicy(ctx, policy_model.DefaultPolicy) +func (repo *OrgRepository) GetDefaultOrgIamPolicy(ctx context.Context) *org_model.OrgIAMPolicy { + return repo.OrgEventstore.GetDefaultOrgIAMPolicy(ctx) +} + +func (repo *OrgRepository) GetOrgIamPolicy(ctx context.Context, orgID string) (*org_model.OrgIAMPolicy, error) { + return repo.OrgEventstore.GetOrgIAMPolicy(ctx, orgID) +} + +func (repo *OrgRepository) GetIDPConfigByID(ctx context.Context, idpConfigID string) (*iam_model.IDPConfigView, error) { + idpConfig, err := repo.View.IDPConfigByID(idpConfigID) + if err != nil { + return nil, err + } + return iam_view_model.IDPConfigViewToModel(idpConfig), nil } diff --git a/internal/auth/repository/eventsourcing/eventstore/user.go b/internal/auth/repository/eventsourcing/eventstore/user.go index c532b72c31..616027bf25 100644 --- a/internal/auth/repository/eventsourcing/eventstore/user.go +++ b/internal/auth/repository/eventsourcing/eventstore/user.go @@ -21,6 +21,7 @@ import ( ) type UserRepo struct { + SearchLimit uint64 Eventstore eventstore.Eventstore UserEvents *user_event.UserEventstore OrgEvents *org_event.OrgEventstore @@ -32,7 +33,15 @@ func (repo *UserRepo) Health(ctx context.Context) error { return repo.UserEvents.Health(ctx) } -func (repo *UserRepo) Register(ctx context.Context, registerUser *model.User, orgMember *org_model.OrgMember, resourceOwner string) (*model.User, error) { +func (repo *UserRepo) Register(ctx context.Context, user *model.User, orgMember *org_model.OrgMember, resourceOwner string) (*model.User, error) { + return repo.registerUser(ctx, user, nil, orgMember, resourceOwner) +} + +func (repo *UserRepo) RegisterExternalUser(ctx context.Context, user *model.User, externalIDP *model.ExternalIDP, orgMember *org_model.OrgMember, resourceOwner string) (*model.User, error) { + return repo.registerUser(ctx, user, externalIDP, orgMember, resourceOwner) +} + +func (repo *UserRepo) registerUser(ctx context.Context, registerUser *model.User, externalIDP *model.ExternalIDP, orgMember *org_model.OrgMember, resourceOwner string) (*model.User, error) { policyResourceOwner := authz.GetCtxData(ctx).OrgID if resourceOwner != "" { policyResourceOwner = resourceOwner @@ -45,7 +54,7 @@ func (repo *UserRepo) Register(ctx context.Context, registerUser *model.User, or if err != nil { return nil, err } - user, aggregates, err := repo.UserEvents.PrepareRegisterUser(ctx, registerUser, pwPolicy, orgPolicy, resourceOwner) + user, aggregates, err := repo.UserEvents.PrepareRegisterUser(ctx, registerUser, externalIDP, pwPolicy, orgPolicy, resourceOwner) if err != nil { return nil, err } @@ -87,6 +96,42 @@ func (repo *UserRepo) ChangeMyProfile(ctx context.Context, profile *model.Profil return repo.UserEvents.ChangeProfile(ctx, profile) } +func (repo *UserRepo) SearchMyExternalIDPs(ctx context.Context, request *model.ExternalIDPSearchRequest) (*model.ExternalIDPSearchResponse, error) { + request.EnsureLimit(repo.SearchLimit) + sequence, seqErr := repo.View.GetLatestExternalIDPSequence() + logging.Log("EVENT-5Jsi8").OnError(seqErr).Warn("could not read latest user sequence") + request.AppendUserQuery(authz.GetCtxData(ctx).UserID) + externalIDPS, count, err := repo.View.SearchExternalIDPs(request) + if err != nil { + return nil, err + } + result := &model.ExternalIDPSearchResponse{ + Offset: request.Offset, + Limit: request.Limit, + TotalResult: count, + Result: usr_view_model.ExternalIDPViewsToModel(externalIDPS), + } + if seqErr == nil { + result.Sequence = sequence.CurrentSequence + result.Timestamp = sequence.CurrentTimestamp + } + return result, nil +} + +func (repo *UserRepo) AddMyExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) (*model.ExternalIDP, error) { + if err := checkIDs(ctx, externalIDP.ObjectRoot); err != nil { + return nil, err + } + return repo.UserEvents.AddExternalIDP(ctx, externalIDP) +} + +func (repo *UserRepo) RemoveMyExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) error { + if err := checkIDs(ctx, externalIDP.ObjectRoot); err != nil { + return err + } + return repo.UserEvents.RemoveExternalIDP(ctx, externalIDP) +} + func (repo *UserRepo) MyEmail(ctx context.Context) (*model.Email, error) { user, err := repo.UserByID(ctx, authz.GetCtxData(ctx).UserID) if err != nil { diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go index ccb94a7128..52f078cced 100644 --- a/internal/auth/repository/eventsourcing/handler/handler.go +++ b/internal/auth/repository/eventsourcing/handler/handler.go @@ -53,6 +53,10 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, ev iamEvents: repos.IamEvents, iamID: systemDefaults.IamID}, &MachineKeys{handler: handler{view, bulkLimit, configs.cycleDuration("MachineKey"), errorCount}}, + &LoginPolicy{handler: handler{view, bulkLimit, configs.cycleDuration("LoginPolicy"), errorCount}}, + &IDPConfig{handler: handler{view, bulkLimit, configs.cycleDuration("IDPConfig"), errorCount}}, + &IDPProvider{handler: handler{view, bulkLimit, configs.cycleDuration("IDPProvider"), errorCount}, systemDefaults: systemDefaults, orgEvents: repos.OrgEvents, iamEvents: repos.IamEvents}, + &ExternalIDP{handler: handler{view, bulkLimit, configs.cycleDuration("ExternalIDP"), errorCount}, systemDefaults: systemDefaults, orgEvents: repos.OrgEvents, iamEvents: repos.IamEvents}, } } diff --git a/internal/auth/repository/eventsourcing/handler/idp_config.go b/internal/auth/repository/eventsourcing/handler/idp_config.go new file mode 100644 index 0000000000..0460b32a9b --- /dev/null +++ b/internal/auth/repository/eventsourcing/handler/idp_config.go @@ -0,0 +1,82 @@ +package handler + +import ( + "github.com/caos/logging" + "github.com/caos/zitadel/internal/eventstore/models" + es_models "github.com/caos/zitadel/internal/eventstore/models" + "github.com/caos/zitadel/internal/eventstore/spooler" + iam_model "github.com/caos/zitadel/internal/iam/model" + iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" + iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model" + "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" +) + +type IDPConfig struct { + handler +} + +const ( + idpConfigTable = "auth.idp_configs" +) + +func (m *IDPConfig) ViewModel() string { + return idpConfigTable +} + +func (m *IDPConfig) EventQuery() (*models.SearchQuery, error) { + sequence, err := m.view.GetLatestIDPConfigSequence() + if err != nil { + return nil, err + } + return es_models.NewSearchQuery(). + AggregateTypeFilter(model.OrgAggregate, iam_es_model.IAMAggregate). + LatestSequenceFilter(sequence.CurrentSequence), nil +} + +func (m *IDPConfig) Reduce(event *models.Event) (err error) { + switch event.AggregateType { + case model.OrgAggregate: + err = m.processIdpConfig(iam_model.IDPProviderTypeOrg, event) + case iam_es_model.IAMAggregate: + err = m.processIdpConfig(iam_model.IDPProviderTypeSystem, event) + } + return err +} + +func (m *IDPConfig) processIdpConfig(providerType iam_model.IDPProviderType, event *models.Event) (err error) { + idp := new(iam_view_model.IDPConfigView) + switch event.Type { + case model.IDPConfigAdded, + iam_es_model.IDPConfigAdded: + err = idp.AppendEvent(providerType, event) + case model.IDPConfigChanged, iam_es_model.IDPConfigChanged, + model.OIDCIDPConfigAdded, iam_es_model.OIDCIDPConfigAdded, + model.OIDCIDPConfigChanged, iam_es_model.OIDCIDPConfigChanged: + err = idp.SetData(event) + if err != nil { + return err + } + idp, err = m.view.IDPConfigByID(idp.IDPConfigID) + if err != nil { + return err + } + err = idp.AppendEvent(providerType, event) + case model.IDPConfigRemoved, iam_es_model.IDPConfigRemoved: + err = idp.SetData(event) + if err != nil { + return err + } + return m.view.DeleteIDPConfig(idp.IDPConfigID, event.Sequence) + default: + return m.view.ProcessedIDPConfigSequence(event.Sequence) + } + if err != nil { + return err + } + return m.view.PutIDPConfig(idp, idp.Sequence) +} + +func (m *IDPConfig) OnError(event *models.Event, err error) error { + logging.LogWithFields("SPOOL-Ejf8s", "id", event.AggregateID).WithError(err).Warn("something went wrong in idp config handler") + return spooler.HandleError(event, err, m.view.GetLatestIDPConfigFailedEvent, m.view.ProcessedIDPConfigFailedEvent, m.view.ProcessedIDPConfigSequence, m.errorCountUntilSkip) +} diff --git a/internal/auth/repository/eventsourcing/handler/idp_providers.go b/internal/auth/repository/eventsourcing/handler/idp_providers.go new file mode 100644 index 0000000000..538e6c0918 --- /dev/null +++ b/internal/auth/repository/eventsourcing/handler/idp_providers.go @@ -0,0 +1,120 @@ +package handler + +import ( + "context" + "github.com/caos/logging" + "github.com/caos/zitadel/internal/config/systemdefaults" + "github.com/caos/zitadel/internal/iam/repository/eventsourcing" + org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing" + org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" + + "github.com/caos/zitadel/internal/eventstore/models" + es_models "github.com/caos/zitadel/internal/eventstore/models" + "github.com/caos/zitadel/internal/eventstore/spooler" + iam_model "github.com/caos/zitadel/internal/iam/model" + "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" + iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model" +) + +type IDPProvider struct { + handler + systemDefaults systemdefaults.SystemDefaults + iamEvents *eventsourcing.IAMEventstore + orgEvents *org_es.OrgEventstore +} + +const ( + idpProviderTable = "auth.idp_providers" +) + +func (m *IDPProvider) ViewModel() string { + return idpProviderTable +} + +func (m *IDPProvider) EventQuery() (*models.SearchQuery, error) { + sequence, err := m.view.GetLatestIDPProviderSequence() + if err != nil { + return nil, err + } + return es_models.NewSearchQuery(). + AggregateTypeFilter(model.IAMAggregate, org_es_model.OrgAggregate). + LatestSequenceFilter(sequence.CurrentSequence), nil +} + +func (m *IDPProvider) Reduce(event *models.Event) (err error) { + switch event.AggregateType { + case model.IAMAggregate, org_es_model.OrgAggregate: + err = m.processIdpProvider(event) + } + return err +} + +func (m *IDPProvider) processIdpProvider(event *models.Event) (err error) { + provider := new(iam_view_model.IDPProviderView) + switch event.Type { + case model.LoginPolicyIDPProviderAdded, org_es_model.LoginPolicyIDPProviderAdded: + err = provider.AppendEvent(event) + if err != nil { + return err + } + err = m.fillData(provider) + case model.LoginPolicyIDPProviderRemoved, model.LoginPolicyIDPProviderCascadeRemoved, + org_es_model.LoginPolicyIDPProviderRemoved, org_es_model.LoginPolicyIDPProviderCascadeRemoved: + err = provider.SetData(event) + if err != nil { + return err + } + return m.view.DeleteIDPProvider(event.AggregateID, provider.IDPConfigID, event.Sequence) + case model.IDPConfigChanged, org_es_model.IDPConfigChanged: + config := new(iam_model.IDPConfig) + config.AppendEvent(event) + providers, err := m.view.IDPProvidersByIDPConfigID(config.IDPConfigID) + if err != nil { + return err + } + if provider.IDPProviderType == int32(iam_model.IDPProviderTypeSystem) { + config, err = m.iamEvents.GetIDPConfig(context.Background(), provider.AggregateID, config.IDPConfigID) + } else { + config, err = m.orgEvents.GetIDPConfig(context.Background(), provider.AggregateID, provider.IDPConfigID) + } + if err != nil { + return err + } + for _, provider := range providers { + m.fillConfigData(provider, config) + } + return m.view.PutIDPProviders(event.Sequence, providers...) + case org_es_model.LoginPolicyRemoved: + return m.view.DeleteIDPProvidersByAggregateID(event.AggregateID, event.Sequence) + default: + return m.view.ProcessedIDPProviderSequence(event.Sequence) + } + if err != nil { + return err + } + return m.view.PutIDPProvider(provider, provider.Sequence) +} + +func (m *IDPProvider) fillData(provider *iam_view_model.IDPProviderView) (err error) { + var config *iam_model.IDPConfig + if provider.IDPProviderType == int32(iam_model.IDPProviderTypeSystem) { + config, err = m.iamEvents.GetIDPConfig(context.Background(), m.systemDefaults.IamID, provider.IDPConfigID) + } else { + config, err = m.orgEvents.GetIDPConfig(context.Background(), provider.AggregateID, provider.IDPConfigID) + } + if err != nil { + return err + } + m.fillConfigData(provider, config) + return nil +} + +func (m *IDPProvider) fillConfigData(provider *iam_view_model.IDPProviderView, config *iam_model.IDPConfig) { + provider.Name = config.Name + provider.IDPConfigType = int32(config.Type) +} + +func (m *IDPProvider) OnError(event *models.Event, err error) error { + logging.LogWithFields("SPOOL-Fjd89", "id", event.AggregateID).WithError(err).Warn("something went wrong in idp provider handler") + return spooler.HandleError(event, err, m.view.GetLatestIDPProviderFailedEvent, m.view.ProcessedIDPProviderFailedEvent, m.view.ProcessedIDPProviderSequence, m.errorCountUntilSkip) +} diff --git a/internal/auth/repository/eventsourcing/handler/login_policy.go b/internal/auth/repository/eventsourcing/handler/login_policy.go new file mode 100644 index 0000000000..824de772e3 --- /dev/null +++ b/internal/auth/repository/eventsourcing/handler/login_policy.go @@ -0,0 +1,67 @@ +package handler + +import ( + "github.com/caos/logging" + iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" + + "github.com/caos/zitadel/internal/eventstore/models" + es_models "github.com/caos/zitadel/internal/eventstore/models" + "github.com/caos/zitadel/internal/eventstore/spooler" + iam_model "github.com/caos/zitadel/internal/iam/repository/view/model" + "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" +) + +type LoginPolicy struct { + handler +} + +const ( + loginPolicyTable = "auth.login_policies" +) + +func (m *LoginPolicy) ViewModel() string { + return loginPolicyTable +} + +func (m *LoginPolicy) EventQuery() (*models.SearchQuery, error) { + sequence, err := m.view.GetLatestLoginPolicySequence() + if err != nil { + return nil, err + } + return es_models.NewSearchQuery(). + AggregateTypeFilter(model.OrgAggregate, iam_es_model.IAMAggregate). + LatestSequenceFilter(sequence.CurrentSequence), nil +} + +func (m *LoginPolicy) Reduce(event *models.Event) (err error) { + switch event.AggregateType { + case model.OrgAggregate, iam_es_model.IAMAggregate: + err = m.processLoginPolicy(event) + } + return err +} + +func (m *LoginPolicy) processLoginPolicy(event *models.Event) (err error) { + policy := new(iam_model.LoginPolicyView) + switch event.Type { + case iam_es_model.LoginPolicyAdded, model.LoginPolicyAdded: + err = policy.AppendEvent(event) + case iam_es_model.LoginPolicyChanged, model.LoginPolicyChanged: + policy, err = m.view.LoginPolicyByAggregateID(event.AggregateID) + if err != nil { + return err + } + err = policy.AppendEvent(event) + default: + return m.view.ProcessedLoginPolicySequence(event.Sequence) + } + if err != nil { + return err + } + return m.view.PutLoginPolicy(policy, policy.Sequence) +} + +func (m *LoginPolicy) OnError(event *models.Event, err error) error { + logging.LogWithFields("SPOOL-5id9s", "id", event.AggregateID).WithError(err).Warn("something went wrong in login policy handler") + return spooler.HandleError(event, err, m.view.GetLatestLoginPolicyFailedEvent, m.view.ProcessedLoginPolicyFailedEvent, m.view.ProcessedLoginPolicySequence, m.errorCountUntilSkip) +} diff --git a/internal/auth/repository/eventsourcing/handler/user_external_idps.go b/internal/auth/repository/eventsourcing/handler/user_external_idps.go new file mode 100644 index 0000000000..44432a1c87 --- /dev/null +++ b/internal/auth/repository/eventsourcing/handler/user_external_idps.go @@ -0,0 +1,126 @@ +package handler + +import ( + "context" + "github.com/caos/logging" + "github.com/caos/zitadel/internal/config/systemdefaults" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/iam/repository/eventsourcing" + org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing" + org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" + "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" + usr_view_model "github.com/caos/zitadel/internal/user/repository/view/model" + + "github.com/caos/zitadel/internal/eventstore/models" + es_models "github.com/caos/zitadel/internal/eventstore/models" + "github.com/caos/zitadel/internal/eventstore/spooler" + iam_model "github.com/caos/zitadel/internal/iam/model" + iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" +) + +type ExternalIDP struct { + handler + systemDefaults systemdefaults.SystemDefaults + iamEvents *eventsourcing.IAMEventstore + orgEvents *org_es.OrgEventstore +} + +const ( + externalIDPTable = "auth.user_external_idps" +) + +func (m *ExternalIDP) ViewModel() string { + return externalIDPTable +} + +func (m *ExternalIDP) EventQuery() (*models.SearchQuery, error) { + sequence, err := m.view.GetLatestExternalIDPSequence() + if err != nil { + return nil, err + } + return es_models.NewSearchQuery(). + AggregateTypeFilter(model.UserAggregate, iam_es_model.IAMAggregate, org_es_model.OrgAggregate). + LatestSequenceFilter(sequence.CurrentSequence), nil +} + +func (m *ExternalIDP) Reduce(event *models.Event) (err error) { + switch event.AggregateType { + case model.UserAggregate: + err = m.processUser(event) + case iam_es_model.IAMAggregate, org_es_model.OrgAggregate: + err = m.processIdpConfig(event) + } + return err +} + +func (m *ExternalIDP) processUser(event *models.Event) (err error) { + externalIDP := new(usr_view_model.ExternalIDPView) + switch event.Type { + case model.HumanExternalIDPAdded: + err = externalIDP.AppendEvent(event) + if err != nil { + return err + } + err = m.fillData(externalIDP) + case model.HumanExternalIDPRemoved, model.HumanExternalIDPCascadeRemoved: + err = externalIDP.SetData(event) + if err != nil { + return err + } + return m.view.DeleteExternalIDP(externalIDP.ExternalUserID, externalIDP.IDPConfigID, event.Sequence) + default: + return m.view.ProcessedExternalIDPSequence(event.Sequence) + } + if err != nil { + return err + } + return m.view.PutExternalIDP(externalIDP, externalIDP.Sequence) +} + +func (m *ExternalIDP) processIdpConfig(event *models.Event) (err error) { + switch event.Type { + case iam_es_model.IDPConfigChanged, org_es_model.IDPConfigChanged: + config := new(iam_model.IDPConfig) + config.AppendEvent(event) + exterinalIDPs, err := m.view.ExternalIDPsByIDPConfigID(config.IDPConfigID) + if err != nil { + return err + } + if event.AggregateType == iam_es_model.IAMAggregate { + config, err = m.iamEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID) + } else { + config, err = m.orgEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID) + } + if err != nil { + return err + } + for _, provider := range exterinalIDPs { + m.fillConfigData(provider, config) + } + return m.view.PutExternalIDPs(event.Sequence, exterinalIDPs...) + default: + return m.view.ProcessedExternalIDPSequence(event.Sequence) + } + return nil +} + +func (m *ExternalIDP) fillData(externalIDP *usr_view_model.ExternalIDPView) error { + config, err := m.orgEvents.GetIDPConfig(context.Background(), externalIDP.ResourceOwner, externalIDP.IDPConfigID) + if caos_errs.IsNotFound(err) { + config, err = m.iamEvents.GetIDPConfig(context.Background(), m.systemDefaults.IamID, externalIDP.IDPConfigID) + } + if err != nil { + return err + } + m.fillConfigData(externalIDP, config) + return nil +} + +func (m *ExternalIDP) fillConfigData(externalIDP *usr_view_model.ExternalIDPView, config *iam_model.IDPConfig) { + externalIDP.IDPName = config.Name +} + +func (m *ExternalIDP) OnError(event *models.Event, err error) error { + logging.LogWithFields("SPOOL-4Rsu8", "id", event.AggregateID).WithError(err).Warn("something went wrong in idp provider handler") + return spooler.HandleError(event, err, m.view.GetLatestExternalIDPFailedEvent, m.view.ProcessedExternalIDPFailedEvent, m.view.ProcessedExternalIDPSequence, m.errorCountUntilSkip) +} diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index 739803fe5b..3da02dcf05 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -128,6 +128,7 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, au return &EsRepository{ spool, eventstore.UserRepo{ + SearchLimit: conf.SearchLimit, Eventstore: es, UserEvents: user, OrgEvents: org, @@ -136,17 +137,22 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, au }, eventstore.AuthRequestRepo{ UserEvents: user, + OrgEvents: org, + PolicyEvents: policy, AuthRequests: authReq, View: view, UserSessionViewProvider: view, UserViewProvider: view, UserEventProvider: user, OrgViewProvider: view, + IDPProviderViewProvider: view, + LoginPolicyViewProvider: view, IdGenerator: idGenerator, PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration, MfaInitSkippedLifeTime: systemDefaults.VerificationLifetimes.MfaInitSkip.Duration, MfaSoftwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaSoftwareCheck.Duration, MfaHardwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaHardwareCheck.Duration, + IAMID: systemDefaults.IamID, }, eventstore.TokenRepo{View: view}, eventstore.KeyRepository{ diff --git a/internal/auth/repository/eventsourcing/view/external_idps.go b/internal/auth/repository/eventsourcing/view/external_idps.go new file mode 100644 index 0000000000..70a382b3bc --- /dev/null +++ b/internal/auth/repository/eventsourcing/view/external_idps.go @@ -0,0 +1,73 @@ +package view + +import ( + "github.com/caos/zitadel/internal/errors" + usr_model "github.com/caos/zitadel/internal/user/model" + "github.com/caos/zitadel/internal/user/repository/view" + "github.com/caos/zitadel/internal/user/repository/view/model" + global_view "github.com/caos/zitadel/internal/view/repository" +) + +const ( + externalIDPTable = "auth.user_external_idps" +) + +func (v *View) ExternalIDPByExternalUserIDAndIDPConfigID(externalUserID, idpConfigID string) (*model.ExternalIDPView, error) { + return view.ExternalIDPByExternalUserIDAndIDPConfigID(v.Db, externalIDPTable, externalUserID, idpConfigID) +} + +func (v *View) ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(externalUserID, idpConfigID, resourceOwner string) (*model.ExternalIDPView, error) { + return view.ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(v.Db, externalIDPTable, externalUserID, idpConfigID, resourceOwner) +} + +func (v *View) ExternalIDPsByIDPConfigID(idpConfigID string) ([]*model.ExternalIDPView, error) { + return view.ExternalIDPsByIDPConfigID(v.Db, externalIDPTable, idpConfigID) +} + +func (v *View) ExternalIDPsByUserID(userID string) ([]*model.ExternalIDPView, error) { + return view.ExternalIDPsByUserID(v.Db, externalIDPTable, userID) +} + +func (v *View) SearchExternalIDPs(request *usr_model.ExternalIDPSearchRequest) ([]*model.ExternalIDPView, uint64, error) { + return view.SearchExternalIDPs(v.Db, externalIDPTable, request) +} + +func (v *View) PutExternalIDP(externalIDP *model.ExternalIDPView, sequence uint64) error { + err := view.PutExternalIDP(v.Db, externalIDPTable, externalIDP) + if err != nil { + return err + } + return v.ProcessedExternalIDPSequence(sequence) +} + +func (v *View) PutExternalIDPs(sequence uint64, externalIDPs ...*model.ExternalIDPView) error { + err := view.PutExternalIDPs(v.Db, externalIDPTable, externalIDPs...) + if err != nil { + return err + } + return v.ProcessedExternalIDPSequence(sequence) +} + +func (v *View) DeleteExternalIDP(externalUserID, idpConfigID string, eventSequence uint64) error { + err := view.DeleteExternalIDP(v.Db, externalIDPTable, externalUserID, idpConfigID) + if err != nil && !errors.IsNotFound(err) { + return err + } + return v.ProcessedExternalIDPSequence(eventSequence) +} + +func (v *View) GetLatestExternalIDPSequence() (*global_view.CurrentSequence, error) { + return v.latestSequence(externalIDPTable) +} + +func (v *View) ProcessedExternalIDPSequence(eventSequence uint64) error { + return v.saveCurrentSequence(externalIDPTable, eventSequence) +} + +func (v *View) GetLatestExternalIDPFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { + return v.latestFailedEvent(externalIDPTable, sequence) +} + +func (v *View) ProcessedExternalIDPFailedEvent(failedEvent *global_view.FailedEvent) error { + return v.saveFailedEvent(failedEvent) +} diff --git a/internal/auth/repository/eventsourcing/view/idp_configs.go b/internal/auth/repository/eventsourcing/view/idp_configs.go new file mode 100644 index 0000000000..0a218f08d6 --- /dev/null +++ b/internal/auth/repository/eventsourcing/view/idp_configs.go @@ -0,0 +1,57 @@ +package view + +import ( + "github.com/caos/zitadel/internal/errors" + iam_model "github.com/caos/zitadel/internal/iam/model" + "github.com/caos/zitadel/internal/iam/repository/view" + iam_es_model "github.com/caos/zitadel/internal/iam/repository/view/model" + global_view "github.com/caos/zitadel/internal/view/repository" +) + +const ( + idpConfigTable = "auth.idp_configs" +) + +func (v *View) IDPConfigByID(idpID string) (*iam_es_model.IDPConfigView, error) { + return view.IDPByID(v.Db, idpConfigTable, idpID) +} + +func (v *View) GetIDPConfigsByAggregateID(aggregateID string) ([]*iam_es_model.IDPConfigView, error) { + return view.GetIDPConfigsByAggregateID(v.Db, idpConfigTable, aggregateID) +} + +func (v *View) SearchIDPConfigs(request *iam_model.IDPConfigSearchRequest) ([]*iam_es_model.IDPConfigView, uint64, error) { + return view.SearchIDPs(v.Db, idpConfigTable, request) +} + +func (v *View) PutIDPConfig(idp *iam_es_model.IDPConfigView, sequence uint64) error { + err := view.PutIDP(v.Db, idpConfigTable, idp) + if err != nil { + return err + } + return v.ProcessedIDPConfigSequence(sequence) +} + +func (v *View) DeleteIDPConfig(idpID string, eventSequence uint64) error { + err := view.DeleteIDP(v.Db, idpConfigTable, idpID) + if err != nil && !errors.IsNotFound(err) { + return err + } + return v.ProcessedIDPConfigSequence(eventSequence) +} + +func (v *View) GetLatestIDPConfigSequence() (*global_view.CurrentSequence, error) { + return v.latestSequence(idpConfigTable) +} + +func (v *View) ProcessedIDPConfigSequence(eventSequence uint64) error { + return v.saveCurrentSequence(idpConfigTable, eventSequence) +} + +func (v *View) GetLatestIDPConfigFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { + return v.latestFailedEvent(idpConfigTable, sequence) +} + +func (v *View) ProcessedIDPConfigFailedEvent(failedEvent *global_view.FailedEvent) error { + return v.saveFailedEvent(failedEvent) +} diff --git a/internal/auth/repository/eventsourcing/view/idp_providers.go b/internal/auth/repository/eventsourcing/view/idp_providers.go new file mode 100644 index 0000000000..fc3eae06ab --- /dev/null +++ b/internal/auth/repository/eventsourcing/view/idp_providers.go @@ -0,0 +1,77 @@ +package view + +import ( + "github.com/caos/zitadel/internal/errors" + iam_model "github.com/caos/zitadel/internal/iam/model" + "github.com/caos/zitadel/internal/iam/repository/view" + "github.com/caos/zitadel/internal/iam/repository/view/model" + global_view "github.com/caos/zitadel/internal/view/repository" +) + +const ( + idpProviderTable = "auth.idp_providers" +) + +func (v *View) IDPProviderByAggregateAndIDPConfigID(aggregateID, idpConfigID string) (*model.IDPProviderView, error) { + return view.GetIDPProviderByAggregateIDAndConfigID(v.Db, idpProviderTable, aggregateID, idpConfigID) +} + +func (v *View) IDPProvidersByIDPConfigID(idpConfigID string) ([]*model.IDPProviderView, error) { + return view.IDPProvidersByIdpConfigID(v.Db, idpProviderTable, idpConfigID) +} + +func (v *View) IDPProvidersByAggregateID(aggregateID string) ([]*model.IDPProviderView, error) { + return view.IDPProvidersByAggregateID(v.Db, idpProviderTable, aggregateID) +} + +func (v *View) SearchIDPProviders(request *iam_model.IDPProviderSearchRequest) ([]*model.IDPProviderView, uint64, error) { + return view.SearchIDPProviders(v.Db, idpProviderTable, request) +} + +func (v *View) PutIDPProvider(provider *model.IDPProviderView, sequence uint64) error { + err := view.PutIDPProvider(v.Db, idpProviderTable, provider) + if err != nil { + return err + } + return v.ProcessedIDPProviderSequence(sequence) +} + +func (v *View) PutIDPProviders(sequence uint64, providers ...*model.IDPProviderView) error { + err := view.PutIDPProviders(v.Db, idpProviderTable, providers...) + if err != nil { + return err + } + return v.ProcessedIDPProviderSequence(sequence) +} + +func (v *View) DeleteIDPProvider(aggregateID, idpConfigID string, eventSequence uint64) error { + err := view.DeleteIDPProvider(v.Db, idpProviderTable, aggregateID, idpConfigID) + if err != nil && !errors.IsNotFound(err) { + return err + } + return v.ProcessedIDPProviderSequence(eventSequence) +} + +func (v *View) DeleteIDPProvidersByAggregateID(aggregateID string, eventSequence uint64) error { + err := view.DeleteIDPProvidersByAggregateID(v.Db, idpProviderTable, aggregateID) + if err != nil && !errors.IsNotFound(err) { + return err + } + return v.ProcessedIDPProviderSequence(eventSequence) +} + +func (v *View) GetLatestIDPProviderSequence() (*global_view.CurrentSequence, error) { + return v.latestSequence(idpProviderTable) +} + +func (v *View) ProcessedIDPProviderSequence(eventSequence uint64) error { + return v.saveCurrentSequence(idpProviderTable, eventSequence) +} + +func (v *View) GetLatestIDPProviderFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { + return v.latestFailedEvent(idpProviderTable, sequence) +} + +func (v *View) ProcessedIDPProviderFailedEvent(failedEvent *global_view.FailedEvent) error { + return v.saveFailedEvent(failedEvent) +} diff --git a/internal/auth/repository/eventsourcing/view/login_policies.go b/internal/auth/repository/eventsourcing/view/login_policies.go new file mode 100644 index 0000000000..3a2133ddee --- /dev/null +++ b/internal/auth/repository/eventsourcing/view/login_policies.go @@ -0,0 +1,48 @@ +package view + +import ( + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/iam/repository/view" + "github.com/caos/zitadel/internal/iam/repository/view/model" + global_view "github.com/caos/zitadel/internal/view/repository" +) + +const ( + loginPolicyTable = "auth.login_policies" +) + +func (v *View) LoginPolicyByAggregateID(aggregateID string) (*model.LoginPolicyView, error) { + return view.GetLoginPolicyByAggregateID(v.Db, loginPolicyTable, aggregateID) +} + +func (v *View) PutLoginPolicy(policy *model.LoginPolicyView, sequence uint64) error { + err := view.PutLoginPolicy(v.Db, loginPolicyTable, policy) + if err != nil { + return err + } + return v.ProcessedLoginPolicySequence(sequence) +} + +func (v *View) DeleteLoginPolicy(aggregateID string, eventSequence uint64) error { + err := view.DeleteLoginPolicy(v.Db, loginPolicyTable, aggregateID) + if err != nil && !errors.IsNotFound(err) { + return err + } + return v.ProcessedLoginPolicySequence(eventSequence) +} + +func (v *View) GetLatestLoginPolicySequence() (*global_view.CurrentSequence, error) { + return v.latestSequence(loginPolicyTable) +} + +func (v *View) ProcessedLoginPolicySequence(eventSequence uint64) error { + return v.saveCurrentSequence(loginPolicyTable, eventSequence) +} + +func (v *View) GetLatestLoginPolicyFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { + return v.latestFailedEvent(loginPolicyTable, sequence) +} + +func (v *View) ProcessedLoginPolicyFailedEvent(failedEvent *global_view.FailedEvent) error { + return v.saveFailedEvent(failedEvent) +} diff --git a/internal/auth/repository/eventsourcing/view/user.go b/internal/auth/repository/eventsourcing/view/user.go index 87a8dd6168..5472eb628d 100644 --- a/internal/auth/repository/eventsourcing/view/user.go +++ b/internal/auth/repository/eventsourcing/view/user.go @@ -23,6 +23,10 @@ func (v *View) UserByLoginName(loginName string) (*model.UserView, error) { return view.UserByLoginName(v.Db, userTable, loginName) } +func (v *View) UserByLoginNameAndResourceOwner(loginName, resourceOwner string) (*model.UserView, error) { + return view.UserByLoginNameAndResourceOwner(v.Db, userTable, loginName, resourceOwner) +} + func (v *View) UsersByOrgID(orgID string) ([]*model.UserView, error) { return view.UsersByOrgID(v.Db, userTable, orgID) } diff --git a/internal/auth/repository/org.go b/internal/auth/repository/org.go index 2c9f4cfad1..54b75b2643 100644 --- a/internal/auth/repository/org.go +++ b/internal/auth/repository/org.go @@ -3,10 +3,13 @@ package repository import ( "context" auth_model "github.com/caos/zitadel/internal/auth/model" + iam_model "github.com/caos/zitadel/internal/iam/model" org_model "github.com/caos/zitadel/internal/org/model" ) type OrgRepository interface { RegisterOrg(context.Context, *auth_model.RegisterOrg) (*auth_model.RegisterOrg, error) GetOrgIamPolicy(ctx context.Context, orgID string) (*org_model.OrgIAMPolicy, error) + GetDefaultOrgIamPolicy(ctx context.Context) *org_model.OrgIAMPolicy + GetIDPConfigByID(ctx context.Context, idpConfigID string) (*iam_model.IDPConfigView, error) } diff --git a/internal/auth/repository/user.go b/internal/auth/repository/user.go index 9b6d332483..164ca23c35 100644 --- a/internal/auth/repository/user.go +++ b/internal/auth/repository/user.go @@ -10,6 +10,7 @@ import ( type UserRepository interface { Register(ctx context.Context, user *model.User, member *org_model.OrgMember, resourceOwner string) (*model.User, error) + RegisterExternalUser(ctx context.Context, user *model.User, externalIDP *model.ExternalIDP, member *org_model.OrgMember, resourceOwner string) (*model.User, error) myUserRepo SkipMfaInit(ctx context.Context, userID string) error @@ -58,6 +59,10 @@ type myUserRepo interface { ChangeMyPassword(ctx context.Context, old, new string) error + SearchMyExternalIDPs(ctx context.Context, request *model.ExternalIDPSearchRequest) (*model.ExternalIDPSearchResponse, error) + AddMyExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) (*model.ExternalIDP, error) + RemoveMyExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) error + MyUserMfas(ctx context.Context) ([]*model.MultiFactor, error) AddMyMfaOTP(ctx context.Context) (*model.OTP, error) VerifyMyMfaOTPSetup(ctx context.Context, code string) error diff --git a/internal/auth_request/model/auth_request.go b/internal/auth_request/model/auth_request.go index a834adc34d..b1f56accf1 100644 --- a/internal/auth_request/model/auth_request.go +++ b/internal/auth_request/model/auth_request.go @@ -1,6 +1,9 @@ package model import ( + "github.com/caos/zitadel/internal/iam/model" + "golang.org/x/text/language" + "strings" "time" "github.com/caos/zitadel/internal/errors" @@ -22,17 +25,36 @@ type AuthRequest struct { MaxAuthAge uint32 Request Request - levelOfAssurance LevelOfAssurance - UserID string - LoginName string - DisplayName string - UserOrgID string - PossibleSteps []NextStep - PasswordVerified bool - MfasVerified []MfaType - Audience []string - AuthTime time.Time - Code string + levelOfAssurance LevelOfAssurance + UserID string + LoginName string + DisplayName string + UserOrgID string + SelectedIDPConfigID string + LinkingUsers []*ExternalUser + PossibleSteps []NextStep + PasswordVerified bool + MfasVerified []MfaType + Audience []string + AuthTime time.Time + Code string + LoginPolicy *model.LoginPolicyView + AllowedExternalIDPs []*model.IDPProviderView +} + +type ExternalUser struct { + IDPConfigID string + ExternalUserID string + DisplayName string + PreferredUsername string + FirstName string + LastName string + NickName string + Email string + IsEmailVerified bool + PreferredLanguage language.Tag + Phone string + IsPhoneVerified bool } type Prompt int32 @@ -103,3 +125,15 @@ func (a *AuthRequest) SetUserInfo(userID, loginName, displayName, userOrgID stri a.DisplayName = displayName a.UserOrgID = userOrgID } + +func (a *AuthRequest) GetScopeOrgID() string { + switch request := a.Request.(type) { + case *AuthRequestOIDC: + for _, scope := range request.Scopes { + if strings.HasPrefix(scope, OrgIDScope) { + strings.TrimPrefix(scope, OrgIDScope) + } + } + } + return "" +} diff --git a/internal/auth_request/model/next_step.go b/internal/auth_request/model/next_step.go index b0abc5b0ac..cbaead8e46 100644 --- a/internal/auth_request/model/next_step.go +++ b/internal/auth_request/model/next_step.go @@ -19,6 +19,8 @@ const ( NextStepMfaVerify NextStepRedirectToCallback NextStepChangeUsername + NextStepLinkUsers + NextStepExternalNotFoundOption ) type UserSessionState int32 @@ -53,6 +55,12 @@ type InitUserStep struct { PasswordSet bool } +type ExternalNotFoundOptionStep struct{} + +func (s *ExternalNotFoundOptionStep) Type() NextStepType { + return NextStepExternalNotFoundOption +} + func (s *InitUserStep) Type() NextStepType { return NextStepInitUser } @@ -104,6 +112,12 @@ func (s *MfaVerificationStep) Type() NextStepType { return NextStepMfaVerify } +type LinkUsersStep struct{} + +func (s *LinkUsersStep) Type() NextStepType { + return NextStepLinkUsers +} + type RedirectToCallbackStep struct{} func (s *RedirectToCallbackStep) Type() NextStepType { diff --git a/internal/auth_request/model/request.go b/internal/auth_request/model/request.go index 34d09d5355..5bf1f9a507 100644 --- a/internal/auth_request/model/request.go +++ b/internal/auth_request/model/request.go @@ -18,6 +18,10 @@ const ( AuthRequestTypeSAML ) +const ( + OrgIDScope = "urn:zitadel:organisation:id:" +) + type AuthRequestOIDC struct { Scopes []string ResponseType OIDCResponseType diff --git a/internal/iam/model/idp_config.go b/internal/iam/model/idp_config.go index fbc6c510b9..bf6ecd45e6 100644 --- a/internal/iam/model/idp_config.go +++ b/internal/iam/model/idp_config.go @@ -17,12 +17,14 @@ type IDPConfig struct { type OIDCIDPConfig struct { es_models.ObjectRoot - IDPConfigID string - ClientID string - ClientSecret *crypto.CryptoValue - ClientSecretString string - Issuer string - Scopes []string + IDPConfigID string + ClientID string + ClientSecret *crypto.CryptoValue + ClientSecretString string + Issuer string + Scopes []string + IDPDisplayNameMapping OIDCMappingField + UsernameMapping OIDCMappingField } type IdpConfigType int32 @@ -40,6 +42,14 @@ const ( IDPConfigStateRemoved ) +type OIDCMappingField int32 + +const ( + OIDCMappingFieldUnspecified OIDCMappingField = iota + OIDCMappingFieldPreferredLoginName + OIDCMappingFieldEmail +) + func NewIDPConfig(iamID, idpID string) *IDPConfig { return &IDPConfig{ObjectRoot: es_models.ObjectRoot{AggregateID: iamID}, IDPConfigID: idpID} } diff --git a/internal/iam/model/idp_config_view.go b/internal/iam/model/idp_config_view.go index 18dba650f9..61770b8dfa 100644 --- a/internal/iam/model/idp_config_view.go +++ b/internal/iam/model/idp_config_view.go @@ -17,11 +17,13 @@ type IDPConfigView struct { Sequence uint64 IDPProviderType IDPProviderType - IsOIDC bool - OIDCClientID string - OIDCClientSecret *crypto.CryptoValue - OIDCIssuer string - OIDCScopes []string + IsOIDC bool + OIDCClientID string + OIDCClientSecret *crypto.CryptoValue + OIDCIssuer string + OIDCScopes []string + OIDCIDPDisplayNameMapping OIDCMappingField + OIDCUsernameMapping OIDCMappingField } type IDPConfigSearchRequest struct { diff --git a/internal/iam/repository/eventsourcing/eventstore.go b/internal/iam/repository/eventsourcing/eventstore.go index 09e232cfed..405ee87f42 100644 --- a/internal/iam/repository/eventsourcing/eventstore.go +++ b/internal/iam/repository/eventsourcing/eventstore.go @@ -380,7 +380,10 @@ func (es *IAMEventstore) ChangeIDPOIDCConfig(ctx context.Context, config *iam_mo return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-Fms8w", "Errors.IAM.IdpIsNotOIDC") } if config.ClientSecretString != "" { - err = idp.OIDCConfig.CryptSecret(es.secretCrypto) + err = config.CryptSecret(es.secretCrypto) + if err != nil { + return nil, err + } } else { config.ClientSecret = nil } @@ -467,20 +470,31 @@ func (es *IAMEventstore) AddIDPProviderToLoginPolicy(ctx context.Context, provid return nil, caos_errs.ThrowInternal(nil, "EVENT-Slf9s", "Errors.Internal") } -func (es *IAMEventstore) RemoveIDPProviderFromLoginPolicy(ctx context.Context, provider *iam_model.IDPProvider) error { +func (es *IAMEventstore) PrepareRemoveIDPProviderFromLoginPolicy(ctx context.Context, provider *iam_model.IDPProvider) (*model.IAM, *models.Aggregate, error) { if provider == nil || !provider.IsValid() { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-Esi8c", "Errors.IdpProviderInvalid") + return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-Esi8c", "Errors.IdpProviderInvalid") } iam, err := es.IAMByID(ctx, provider.AggregateID) if err != nil { - return err + return nil, nil, err } if _, m := iam.DefaultLoginPolicy.GetIdpProvider(provider.IdpConfigID); m == nil { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-29skr", "Errors.IAM.LoginPolicy.IdpProviderNotExisting") + return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-29skr", "Errors.IAM.LoginPolicy.IdpProviderNotExisting") } repoIam := model.IAMFromModel(iam) - addAggregate := LoginPolicyIDPProviderRemovedAggregate(es.Eventstore.AggregateCreator(), repoIam, &model.IDPProviderID{provider.IdpConfigID}) - err = es_sdk.Push(ctx, es.PushAggregates, repoIam.AppendEvents, addAggregate) + removeAgg, err := LoginPolicyIDPProviderRemovedAggregate(ctx, es.Eventstore.AggregateCreator(), repoIam, &model.IDPProviderID{provider.IdpConfigID}) + if err != nil { + return nil, nil, err + } + return repoIam, removeAgg, nil +} + +func (es *IAMEventstore) RemoveIDPProviderFromLoginPolicy(ctx context.Context, provider *iam_model.IDPProvider) error { + repoIam, removeAgg, err := es.PrepareRemoveIDPProviderFromLoginPolicy(ctx, provider) + if err != nil { + return err + } + err = es_sdk.PushAggregates(ctx, es.PushAggregates, repoIam.AppendEvents, removeAgg) if err != nil { return err } diff --git a/internal/iam/repository/eventsourcing/iam.go b/internal/iam/repository/eventsourcing/iam.go index ba963b145a..3e3a0c2064 100644 --- a/internal/iam/repository/eventsourcing/iam.go +++ b/internal/iam/repository/eventsourcing/iam.go @@ -285,17 +285,15 @@ func LoginPolicyIDPProviderAddedAggregate(aggCreator *es_models.AggregateCreator } } -func LoginPolicyIDPProviderRemovedAggregate(aggCreator *es_models.AggregateCreator, existing *model.IAM, provider *model.IDPProviderID) func(ctx context.Context) (*es_models.Aggregate, error) { - return func(ctx context.Context) (*es_models.Aggregate, error) { - if provider == nil || existing == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Sml9d", "Errors.Internal") - } - agg, err := IAMAggregate(ctx, aggCreator, existing) - if err != nil { - return nil, err - } - return agg.AppendEvent(model.LoginPolicyIDPProviderRemoved, provider) +func LoginPolicyIDPProviderRemovedAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, existing *model.IAM, provider *model.IDPProviderID) (*es_models.Aggregate, error) { + if provider == nil || existing == nil { + return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Sml9d", "Errors.Internal") } + agg, err := IAMAggregate(ctx, aggCreator, existing) + if err != nil { + return nil, err + } + return agg.AppendEvent(model.LoginPolicyIDPProviderRemoved, provider) } func checkExistingLoginPolicyValidation() func(...*es_models.Event) error { diff --git a/internal/iam/repository/eventsourcing/iam_test.go b/internal/iam/repository/eventsourcing/iam_test.go index 92f90530a7..4f48f5edf0 100644 --- a/internal/iam/repository/eventsourcing/iam_test.go +++ b/internal/iam/repository/eventsourcing/iam_test.go @@ -1447,7 +1447,7 @@ func TestLoginPolicyIdpProviderRemovedAggregate(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - agg, err := LoginPolicyIDPProviderRemovedAggregate(tt.args.aggCreator, tt.args.existingIAM, tt.args.newProviderID)(tt.args.ctx) + agg, err := LoginPolicyIDPProviderRemovedAggregate(tt.args.ctx, tt.args.aggCreator, tt.args.existingIAM, tt.args.newProviderID) if !tt.res.wantErr && len(agg.Events) != tt.res.eventLen { t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events)) diff --git a/internal/iam/repository/eventsourcing/model/idp_config.go b/internal/iam/repository/eventsourcing/model/idp_config.go index 96c7425c39..433b25351a 100644 --- a/internal/iam/repository/eventsourcing/model/idp_config.go +++ b/internal/iam/repository/eventsourcing/model/idp_config.go @@ -10,11 +10,12 @@ import ( type IDPConfig struct { es_models.ObjectRoot - IDPConfigID string `json:"idpConfigId"` - State int32 `json:"-"` - Name string `json:"name,omitempty"` - Type int32 `json:"idpType,omitempty"` - LogoSrc []byte `json:"logoSrc,omitempty"` + IDPConfigID string `json:"idpConfigId"` + State int32 `json:"-"` + Name string `json:"name,omitempty"` + Type int32 `json:"idpType,omitempty"` + LogoSrc []byte `json:"logoSrc,omitempty"` + OIDCIDPConfig *OIDCIDPConfig `json:"-"` } diff --git a/internal/iam/repository/eventsourcing/model/oidc_idp_config.go b/internal/iam/repository/eventsourcing/model/oidc_idp_config.go index d334ba41b9..ecdda1b32c 100644 --- a/internal/iam/repository/eventsourcing/model/oidc_idp_config.go +++ b/internal/iam/repository/eventsourcing/model/oidc_idp_config.go @@ -12,11 +12,13 @@ import ( type OIDCIDPConfig struct { es_models.ObjectRoot - IDPConfigID string `json:"idpConfigId"` - ClientID string `json:"clientId"` - ClientSecret *crypto.CryptoValue `json:"clientSecret,omitempty"` - Issuer string `json:"issuer,omitempty"` - Scopes pq.StringArray `json:"scopes,omitempty"` + IDPConfigID string `json:"idpConfigId"` + ClientID string `json:"clientId"` + ClientSecret *crypto.CryptoValue `json:"clientSecret,omitempty"` + Issuer string `json:"issuer,omitempty"` + Scopes pq.StringArray `json:"scopes,omitempty"` + IDPDisplayNameMapping int32 `json:"idpDisplayNameMapping,omitempty"` + UsernameMapping int32 `json:"usernameMapping,omitempty"` } func (c *OIDCIDPConfig) Changes(changed *OIDCIDPConfig) map[string]interface{} { @@ -25,7 +27,7 @@ func (c *OIDCIDPConfig) Changes(changed *OIDCIDPConfig) map[string]interface{} { if c.ClientID != changed.ClientID { changes["clientId"] = changed.ClientID } - if c.ClientSecret != nil { + if c.ClientSecret != nil && c.ClientSecret != changed.ClientSecret { changes["clientSecret"] = changed.ClientSecret } if c.Issuer != changed.Issuer { @@ -34,28 +36,38 @@ func (c *OIDCIDPConfig) Changes(changed *OIDCIDPConfig) map[string]interface{} { if !reflect.DeepEqual(c.Scopes, changed.Scopes) { changes["scopes"] = changed.Scopes } + if c.IDPDisplayNameMapping != changed.IDPDisplayNameMapping { + changes["idpDisplayNameMapping"] = changed.IDPDisplayNameMapping + } + if c.UsernameMapping != changed.UsernameMapping { + changes["usernameMapping"] = changed.UsernameMapping + } return changes } func OIDCIDPConfigFromModel(config *model.OIDCIDPConfig) *OIDCIDPConfig { return &OIDCIDPConfig{ - ObjectRoot: config.ObjectRoot, - IDPConfigID: config.IDPConfigID, - ClientID: config.ClientID, - ClientSecret: config.ClientSecret, - Issuer: config.Issuer, - Scopes: config.Scopes, + ObjectRoot: config.ObjectRoot, + IDPConfigID: config.IDPConfigID, + ClientID: config.ClientID, + ClientSecret: config.ClientSecret, + Issuer: config.Issuer, + Scopes: config.Scopes, + IDPDisplayNameMapping: int32(config.IDPDisplayNameMapping), + UsernameMapping: int32(config.UsernameMapping), } } func OIDCIDPConfigToModel(config *OIDCIDPConfig) *model.OIDCIDPConfig { return &model.OIDCIDPConfig{ - ObjectRoot: config.ObjectRoot, - IDPConfigID: config.IDPConfigID, - ClientID: config.ClientID, - ClientSecret: config.ClientSecret, - Issuer: config.Issuer, - Scopes: config.Scopes, + ObjectRoot: config.ObjectRoot, + IDPConfigID: config.IDPConfigID, + ClientID: config.ClientID, + ClientSecret: config.ClientSecret, + Issuer: config.Issuer, + Scopes: config.Scopes, + IDPDisplayNameMapping: model.OIDCMappingField(config.IDPDisplayNameMapping), + UsernameMapping: model.OIDCMappingField(config.UsernameMapping), } } diff --git a/internal/iam/repository/view/idp_provider_view.go b/internal/iam/repository/view/idp_provider_view.go index b7a4bff024..88301dd3ee 100644 --- a/internal/iam/repository/view/idp_provider_view.go +++ b/internal/iam/repository/view/idp_provider_view.go @@ -22,7 +22,7 @@ func GetIDPProviderByAggregateIDAndConfigID(db *gorm.DB, table, aggregateID, idp } func IDPProvidersByIdpConfigID(db *gorm.DB, table string, idpConfigID string) ([]*model.IDPProviderView, error) { - members := make([]*model.IDPProviderView, 0) + providers := make([]*model.IDPProviderView, 0) queries := []*iam_model.IDPProviderSearchQuery{ { Key: iam_model.IDPProviderSearchKeyIdpConfigID, @@ -31,11 +31,28 @@ func IDPProvidersByIdpConfigID(db *gorm.DB, table string, idpConfigID string) ([ }, } query := repository.PrepareSearchQuery(table, model.IDPProviderSearchRequest{Queries: queries}) - _, err := query(db, &members) + _, err := query(db, &providers) if err != nil { return nil, err } - return members, nil + return providers, nil +} + +func IDPProvidersByAggregateID(db *gorm.DB, table string, aggregateID string) ([]*model.IDPProviderView, error) { + providers := make([]*model.IDPProviderView, 0) + queries := []*iam_model.IDPProviderSearchQuery{ + { + Key: iam_model.IDPProviderSearchKeyAggregateID, + Value: aggregateID, + Method: global_model.SearchMethodEquals, + }, + } + query := repository.PrepareSearchQuery(table, model.IDPProviderSearchRequest{Queries: queries}) + _, err := query(db, &providers) + if err != nil { + return nil, err + } + return providers, nil } func SearchIDPProviders(db *gorm.DB, table string, req *iam_model.IDPProviderSearchRequest) ([]*model.IDPProviderView, uint64, error) { diff --git a/internal/iam/repository/view/idp_view.go b/internal/iam/repository/view/idp_view.go index dc6fd73424..44fcd93edf 100644 --- a/internal/iam/repository/view/idp_view.go +++ b/internal/iam/repository/view/idp_view.go @@ -20,6 +20,23 @@ func IDPByID(db *gorm.DB, table, idpID string) (*model.IDPConfigView, error) { return idp, err } +func GetIDPConfigsByAggregateID(db *gorm.DB, table string, aggregateID string) ([]*model.IDPConfigView, error) { + idps := make([]*model.IDPConfigView, 0) + queries := []*iam_model.IDPConfigSearchQuery{ + { + Key: iam_model.IDPConfigSearchKeyAggregateID, + Value: aggregateID, + Method: global_model.SearchMethodEquals, + }, + } + query := repository.PrepareSearchQuery(table, model.IDPConfigSearchRequest{Queries: queries}) + _, err := query(db, &idps) + if err != nil { + return nil, err + } + return idps, nil +} + func SearchIDPs(db *gorm.DB, table string, req *iam_model.IDPConfigSearchRequest) ([]*model.IDPConfigView, uint64, error) { idps := make([]*model.IDPConfigView, 0) query := repository.PrepareSearchQuery(table, model.IDPConfigSearchRequest{Limit: req.Limit, Offset: req.Offset, Queries: req.Queries}) diff --git a/internal/iam/repository/view/model/idp_config.go b/internal/iam/repository/view/model/idp_config.go index d85181a04d..5587e8af47 100644 --- a/internal/iam/repository/view/model/idp_config.go +++ b/internal/iam/repository/view/model/idp_config.go @@ -32,55 +32,61 @@ type IDPConfigView struct { IDPState int32 `json:"-" gorm:"column:idp_state"` IDPProviderType int32 `json:"-" gorm:"column:idp_provider_type"` - IsOIDC bool `json:"-" gorm:"column:is_oidc"` - OIDCClientID string `json:"clientId" gorm:"column:oidc_client_id"` - OIDCClientSecret *crypto.CryptoValue `json:"clientSecret" gorm:"column:oidc_client_secret"` - OIDCIssuer string `json:"issuer" gorm:"column:oidc_issuer"` - OIDCScopes pq.StringArray `json:"scopes" gorm:"column:oidc_scopes"` + IsOIDC bool `json:"-" gorm:"column:is_oidc"` + OIDCClientID string `json:"clientId" gorm:"column:oidc_client_id"` + OIDCClientSecret *crypto.CryptoValue `json:"clientSecret" gorm:"column:oidc_client_secret"` + OIDCIssuer string `json:"issuer" gorm:"column:oidc_issuer"` + OIDCScopes pq.StringArray `json:"scopes" gorm:"column:oidc_scopes"` + OIDCIDPDisplayNameMapping int32 `json:"idpDisplayNameMapping" gorm:"column:oidc_idp_display_name_mapping"` + OIDCUsernameMapping int32 `json:"usernameMapping" gorm:"column:oidc_idp_username_mapping"` Sequence uint64 `json:"-" gorm:"column:sequence"` } func IDPConfigViewFromModel(idp *model.IDPConfigView) *IDPConfigView { return &IDPConfigView{ - IDPConfigID: idp.IDPConfigID, - AggregateID: idp.AggregateID, - Name: idp.Name, - LogoSrc: idp.LogoSrc, - Sequence: idp.Sequence, - CreationDate: idp.CreationDate, - ChangeDate: idp.ChangeDate, - IDPProviderType: int32(idp.IDPProviderType), - IsOIDC: idp.IsOIDC, - OIDCClientID: idp.OIDCClientID, - OIDCClientSecret: idp.OIDCClientSecret, - OIDCIssuer: idp.OIDCIssuer, - OIDCScopes: idp.OIDCScopes, + IDPConfigID: idp.IDPConfigID, + AggregateID: idp.AggregateID, + Name: idp.Name, + LogoSrc: idp.LogoSrc, + Sequence: idp.Sequence, + CreationDate: idp.CreationDate, + ChangeDate: idp.ChangeDate, + IDPProviderType: int32(idp.IDPProviderType), + IsOIDC: idp.IsOIDC, + OIDCClientID: idp.OIDCClientID, + OIDCClientSecret: idp.OIDCClientSecret, + OIDCIssuer: idp.OIDCIssuer, + OIDCScopes: idp.OIDCScopes, + OIDCIDPDisplayNameMapping: int32(idp.OIDCIDPDisplayNameMapping), + OIDCUsernameMapping: int32(idp.OIDCUsernameMapping), } } -func IdpConfigViewToModel(idp *IDPConfigView) *model.IDPConfigView { +func IDPConfigViewToModel(idp *IDPConfigView) *model.IDPConfigView { return &model.IDPConfigView{ - IDPConfigID: idp.IDPConfigID, - AggregateID: idp.AggregateID, - Name: idp.Name, - LogoSrc: idp.LogoSrc, - Sequence: idp.Sequence, - CreationDate: idp.CreationDate, - ChangeDate: idp.ChangeDate, - IDPProviderType: model.IDPProviderType(idp.IDPProviderType), - IsOIDC: idp.IsOIDC, - OIDCClientID: idp.OIDCClientID, - OIDCClientSecret: idp.OIDCClientSecret, - OIDCIssuer: idp.OIDCIssuer, - OIDCScopes: idp.OIDCScopes, + IDPConfigID: idp.IDPConfigID, + AggregateID: idp.AggregateID, + Name: idp.Name, + LogoSrc: idp.LogoSrc, + Sequence: idp.Sequence, + CreationDate: idp.CreationDate, + ChangeDate: idp.ChangeDate, + IDPProviderType: model.IDPProviderType(idp.IDPProviderType), + IsOIDC: idp.IsOIDC, + OIDCClientID: idp.OIDCClientID, + OIDCClientSecret: idp.OIDCClientSecret, + OIDCIssuer: idp.OIDCIssuer, + OIDCScopes: idp.OIDCScopes, + OIDCIDPDisplayNameMapping: model.OIDCMappingField(idp.OIDCIDPDisplayNameMapping), + OIDCUsernameMapping: model.OIDCMappingField(idp.OIDCUsernameMapping), } } func IdpConfigViewsToModel(idps []*IDPConfigView) []*model.IDPConfigView { result := make([]*model.IDPConfigView, len(idps)) for i, idp := range idps { - result[i] = IdpConfigViewToModel(idp) + result[i] = IDPConfigViewToModel(idp) } return result } diff --git a/internal/iam/repository/view/model/idp_provider.go b/internal/iam/repository/view/model/idp_provider.go index c6b5fe280e..57fa265419 100644 --- a/internal/iam/repository/view/model/idp_provider.go +++ b/internal/iam/repository/view/model/idp_provider.go @@ -39,6 +39,7 @@ func IDPProviderViewFromModel(policy *model.IDPProviderView) *IDPProviderView { CreationDate: policy.CreationDate, ChangeDate: policy.ChangeDate, Name: policy.Name, + IDPConfigID: policy.IDPConfigID, IDPConfigType: int32(policy.IDPConfigType), IDPProviderType: int32(policy.IDPProviderType), } @@ -51,6 +52,7 @@ func IDPProviderViewToModel(policy *IDPProviderView) *model.IDPProviderView { CreationDate: policy.CreationDate, ChangeDate: policy.ChangeDate, Name: policy.Name, + IDPConfigID: policy.IDPConfigID, IDPConfigType: model.IdpConfigType(policy.IDPConfigType), IDPProviderType: model.IDPProviderType(policy.IDPProviderType), } diff --git a/internal/management/repository/eventsourcing/eventstore/org.go b/internal/management/repository/eventsourcing/eventstore/org.go index 462d5724b3..2108fabedf 100644 --- a/internal/management/repository/eventsourcing/eventstore/org.go +++ b/internal/management/repository/eventsourcing/eventstore/org.go @@ -8,6 +8,7 @@ import ( iam_model "github.com/caos/zitadel/internal/iam/model" iam_es_model "github.com/caos/zitadel/internal/iam/repository/view/model" iam_view_model "github.com/caos/zitadel/internal/iam/repository/view/model" + usr_model "github.com/caos/zitadel/internal/user/model" "strings" "github.com/caos/zitadel/internal/api/authz" @@ -218,7 +219,7 @@ func (repo *OrgRepository) IDPConfigByID(ctx context.Context, idpConfigID string if err != nil { return nil, err } - return iam_view_model.IdpConfigViewToModel(idp), nil + return iam_view_model.IDPConfigViewToModel(idp), nil } func (repo *OrgRepository) AddOIDCIDPConfig(ctx context.Context, idp *iam_model.IDPConfig) (*iam_model.IDPConfig, error) { idp.AggregateID = authz.GetCtxData(ctx).OrgID @@ -239,8 +240,27 @@ func (repo *OrgRepository) ReactivateIDPConfig(ctx context.Context, idpConfigID } func (repo *OrgRepository) RemoveIDPConfig(ctx context.Context, idpConfigID string) error { + aggregates := make([]*es_models.Aggregate, 0) idp := iam_model.NewIDPConfig(authz.GetCtxData(ctx).OrgID, idpConfigID) - return repo.OrgEventstore.RemoveIDPConfig(ctx, idp) + _, agg, err := repo.OrgEventstore.PrepareRemoveIDPConfig(ctx, idp) + if err != nil { + + } + aggregates = append(aggregates, agg) + externalIDPs, err := repo.View.ExternalIDPsByIDPConfigID(idpConfigID) + if err != nil { + return err + } + for _, externalIDP := range externalIDPs { + idpRemove := &usr_model.ExternalIDP{ObjectRoot: es_models.ObjectRoot{AggregateID: externalIDP.UserID}, IDPConfigID: externalIDP.IDPConfigID, UserID: externalIDP.ExternalUserID} + idpAgg := make([]*es_models.Aggregate, 0) + _, idpAgg, err = repo.UserEvents.PrepareRemoveExternalIDP(ctx, idpRemove, true) + if err != nil { + return err + } + aggregates = append(aggregates, idpAgg...) + } + return sdk.PushAggregates(ctx, repo.Eventstore.PushAggregates, nil, aggregates...) } func (repo *OrgRepository) ChangeOIDCIDPConfig(ctx context.Context, oidcConfig *iam_model.OIDCIDPConfig) (*iam_model.OIDCIDPConfig, error) { @@ -338,6 +358,25 @@ func (repo *OrgRepository) AddIDPProviderToLoginPolicy(ctx context.Context, prov } func (repo *OrgRepository) RemoveIDPProviderFromIdpProvider(ctx context.Context, provider *iam_model.IDPProvider) error { + aggregates := make([]*es_models.Aggregate, 0) provider.AggregateID = authz.GetCtxData(ctx).OrgID - return repo.OrgEventstore.RemoveIDPProviderFromLoginPolicy(ctx, provider) + _, agg, err := repo.OrgEventstore.PrepareRemoveIDPProviderFromLoginPolicy(ctx, provider, false) + if err != nil { + return err + } + aggregates = append(aggregates, agg) + externalIDPs, err := repo.View.ExternalIDPsByIDPConfigID(provider.IdpConfigID) + if err != nil { + return err + } + for _, externalIDP := range externalIDPs { + idpRemove := &usr_model.ExternalIDP{ObjectRoot: es_models.ObjectRoot{AggregateID: externalIDP.UserID}, IDPConfigID: externalIDP.IDPConfigID, UserID: externalIDP.ExternalUserID} + idpAgg := make([]*es_models.Aggregate, 0) + _, idpAgg, err = repo.UserEvents.PrepareRemoveExternalIDP(ctx, idpRemove, true) + if err != nil { + return err + } + aggregates = append(aggregates, idpAgg...) + } + return sdk.PushAggregates(ctx, repo.Eventstore.PushAggregates, nil, aggregates...) } diff --git a/internal/management/repository/eventsourcing/eventstore/user.go b/internal/management/repository/eventsourcing/eventstore/user.go index e8e285c316..6dc41cb564 100644 --- a/internal/management/repository/eventsourcing/eventstore/user.go +++ b/internal/management/repository/eventsourcing/eventstore/user.go @@ -181,6 +181,31 @@ func (repo *UserRepo) ProfileByID(ctx context.Context, userID string) (*usr_mode return user.GetProfile() } +func (repo *UserRepo) SearchExternalIDPs(ctx context.Context, request *usr_model.ExternalIDPSearchRequest) (*usr_model.ExternalIDPSearchResponse, error) { + request.EnsureLimit(repo.SearchLimit) + sequence, seqErr := repo.View.GetLatestExternalIDPSequence() + logging.Log("EVENT-Qs7uf").OnError(seqErr).Warn("could not read latest external idp sequence") + externalIDPS, count, err := repo.View.SearchExternalIDPs(request) + if err != nil { + return nil, err + } + result := &usr_model.ExternalIDPSearchResponse{ + Offset: request.Offset, + Limit: request.Limit, + TotalResult: count, + Result: model.ExternalIDPViewsToModel(externalIDPS), + } + if seqErr == nil { + result.Sequence = sequence.CurrentSequence + result.Timestamp = sequence.CurrentTimestamp + } + return result, nil +} + +func (repo *UserRepo) RemoveExternalIDP(ctx context.Context, externalIDP *usr_model.ExternalIDP) error { + return repo.UserEvents.RemoveExternalIDP(ctx, externalIDP) +} + func (repo *UserRepo) ChangeMachine(ctx context.Context, machine *usr_model.Machine) (*usr_model.Machine, error) { return repo.UserEvents.ChangeMachine(ctx, machine) } diff --git a/internal/management/repository/eventsourcing/handler/handler.go b/internal/management/repository/eventsourcing/handler/handler.go index 5f2074210e..8b34cfe490 100644 --- a/internal/management/repository/eventsourcing/handler/handler.go +++ b/internal/management/repository/eventsourcing/handler/handler.go @@ -52,6 +52,7 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, ev &IDPConfig{handler: handler{view, bulkLimit, configs.cycleDuration("IDPConfig"), errorCount}}, &LoginPolicy{handler: handler{view, bulkLimit, configs.cycleDuration("LoginPolicy"), errorCount}}, &IDPProvider{handler: handler{view, bulkLimit, configs.cycleDuration("IDPProvider"), errorCount}, systemDefaults: defaults, iamEvents: repos.IamEvents, orgEvents: repos.OrgEvents}, + &ExternalIDP{handler: handler{view, bulkLimit, configs.cycleDuration("ExternalIDP"), errorCount}, systemDefaults: defaults, iamEvents: repos.IamEvents, orgEvents: repos.OrgEvents}, } } diff --git a/internal/management/repository/eventsourcing/handler/user_external_idps.go b/internal/management/repository/eventsourcing/handler/user_external_idps.go new file mode 100644 index 0000000000..91e8fa91b1 --- /dev/null +++ b/internal/management/repository/eventsourcing/handler/user_external_idps.go @@ -0,0 +1,126 @@ +package handler + +import ( + "context" + "github.com/caos/logging" + "github.com/caos/zitadel/internal/config/systemdefaults" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/iam/repository/eventsourcing" + org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing" + org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" + "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" + usr_view_model "github.com/caos/zitadel/internal/user/repository/view/model" + + "github.com/caos/zitadel/internal/eventstore/models" + es_models "github.com/caos/zitadel/internal/eventstore/models" + "github.com/caos/zitadel/internal/eventstore/spooler" + iam_model "github.com/caos/zitadel/internal/iam/model" + iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" +) + +type ExternalIDP struct { + handler + systemDefaults systemdefaults.SystemDefaults + iamEvents *eventsourcing.IAMEventstore + orgEvents *org_es.OrgEventstore +} + +const ( + externalIDPTable = "management.user_external_idps" +) + +func (m *ExternalIDP) ViewModel() string { + return externalIDPTable +} + +func (m *ExternalIDP) EventQuery() (*models.SearchQuery, error) { + sequence, err := m.view.GetLatestExternalIDPSequence() + if err != nil { + return nil, err + } + return es_models.NewSearchQuery(). + AggregateTypeFilter(model.UserAggregate, iam_es_model.IAMAggregate, org_es_model.OrgAggregate). + LatestSequenceFilter(sequence.CurrentSequence), nil +} + +func (m *ExternalIDP) Reduce(event *models.Event) (err error) { + switch event.AggregateType { + case model.UserAggregate: + err = m.processUser(event) + case iam_es_model.IAMAggregate, org_es_model.OrgAggregate: + err = m.processIdpConfig(event) + } + return err +} + +func (m *ExternalIDP) processUser(event *models.Event) (err error) { + externalIDP := new(usr_view_model.ExternalIDPView) + switch event.Type { + case model.HumanExternalIDPAdded: + err = externalIDP.AppendEvent(event) + if err != nil { + return err + } + err = m.fillData(externalIDP) + case model.HumanExternalIDPRemoved, model.HumanExternalIDPCascadeRemoved: + err = externalIDP.SetData(event) + if err != nil { + return err + } + return m.view.DeleteExternalIDP(externalIDP.ExternalUserID, externalIDP.IDPConfigID, event.Sequence) + default: + return m.view.ProcessedExternalIDPSequence(event.Sequence) + } + if err != nil { + return err + } + return m.view.PutExternalIDP(externalIDP, externalIDP.Sequence) +} + +func (m *ExternalIDP) processIdpConfig(event *models.Event) (err error) { + switch event.Type { + case iam_es_model.IDPConfigChanged, org_es_model.IDPConfigChanged: + config := new(iam_model.IDPConfig) + config.AppendEvent(event) + exterinalIDPs, err := m.view.ExternalIDPsByIDPConfigID(config.IDPConfigID) + if err != nil { + return err + } + if event.AggregateType == iam_es_model.IAMAggregate { + config, err = m.iamEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID) + } else { + config, err = m.orgEvents.GetIDPConfig(context.Background(), config.AggregateID, config.IDPConfigID) + } + if err != nil { + return err + } + for _, provider := range exterinalIDPs { + m.fillConfigData(provider, config) + } + return m.view.PutExternalIDPs(event.Sequence, exterinalIDPs...) + default: + return m.view.ProcessedExternalIDPSequence(event.Sequence) + } + return nil +} + +func (m *ExternalIDP) fillData(externalIDP *usr_view_model.ExternalIDPView) error { + config, err := m.orgEvents.GetIDPConfig(context.Background(), externalIDP.ResourceOwner, externalIDP.IDPConfigID) + if caos_errs.IsNotFound(err) { + config, err = m.iamEvents.GetIDPConfig(context.Background(), m.systemDefaults.IamID, externalIDP.IDPConfigID) + } + if err != nil { + return err + } + m.fillConfigData(externalIDP, config) + return nil +} + +func (m *ExternalIDP) fillConfigData(externalIDP *usr_view_model.ExternalIDPView, config *iam_model.IDPConfig) { + externalIDP.IDPName = config.Name +} + +func (m *ExternalIDP) OnError(event *models.Event, err error) error { + logging.LogWithFields("SPOOL-4Rsu8", "id", event.AggregateID).WithError(err).Warn("something went wrong in idp provider handler") + return spooler.HandleError(event, err, m.view.GetLatestExternalIDPFailedEvent, m.view.ProcessedExternalIDPFailedEvent, m.view.ProcessedExternalIDPSequence, m.errorCountUntilSkip) +} diff --git a/internal/management/repository/eventsourcing/view/external_idps.go b/internal/management/repository/eventsourcing/view/external_idps.go new file mode 100644 index 0000000000..650c8111db --- /dev/null +++ b/internal/management/repository/eventsourcing/view/external_idps.go @@ -0,0 +1,73 @@ +package view + +import ( + "github.com/caos/zitadel/internal/errors" + usr_model "github.com/caos/zitadel/internal/user/model" + "github.com/caos/zitadel/internal/user/repository/view" + "github.com/caos/zitadel/internal/user/repository/view/model" + global_view "github.com/caos/zitadel/internal/view/repository" +) + +const ( + externalIDPTable = "management.user_external_idps" +) + +func (v *View) ExternalIDPByExternalUserIDAndIDPConfigID(externalUserID, idpConfigID string) (*model.ExternalIDPView, error) { + return view.ExternalIDPByExternalUserIDAndIDPConfigID(v.Db, externalIDPTable, externalUserID, idpConfigID) +} + +func (v *View) ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(externalUserID, idpConfigID, resourceOwner string) (*model.ExternalIDPView, error) { + return view.ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(v.Db, externalIDPTable, externalUserID, idpConfigID, resourceOwner) +} + +func (v *View) ExternalIDPsByIDPConfigID(idpConfigID string) ([]*model.ExternalIDPView, error) { + return view.ExternalIDPsByIDPConfigID(v.Db, externalIDPTable, idpConfigID) +} + +func (v *View) ExternalIDPsByUserID(userID string) ([]*model.ExternalIDPView, error) { + return view.ExternalIDPsByUserID(v.Db, externalIDPTable, userID) +} + +func (v *View) SearchExternalIDPs(request *usr_model.ExternalIDPSearchRequest) ([]*model.ExternalIDPView, uint64, error) { + return view.SearchExternalIDPs(v.Db, externalIDPTable, request) +} + +func (v *View) PutExternalIDP(externalIDP *model.ExternalIDPView, sequence uint64) error { + err := view.PutExternalIDP(v.Db, externalIDPTable, externalIDP) + if err != nil { + return err + } + return v.ProcessedExternalIDPSequence(sequence) +} + +func (v *View) PutExternalIDPs(sequence uint64, externalIDPs ...*model.ExternalIDPView) error { + err := view.PutExternalIDPs(v.Db, externalIDPTable, externalIDPs...) + if err != nil { + return err + } + return v.ProcessedExternalIDPSequence(sequence) +} + +func (v *View) DeleteExternalIDP(externalUserID, idpConfigID string, eventSequence uint64) error { + err := view.DeleteExternalIDP(v.Db, externalIDPTable, externalUserID, idpConfigID) + if err != nil && !errors.IsNotFound(err) { + return err + } + return v.ProcessedExternalIDPSequence(eventSequence) +} + +func (v *View) GetLatestExternalIDPSequence() (*global_view.CurrentSequence, error) { + return v.latestSequence(externalIDPTable) +} + +func (v *View) ProcessedExternalIDPSequence(eventSequence uint64) error { + return v.saveCurrentSequence(externalIDPTable, eventSequence) +} + +func (v *View) GetLatestExternalIDPFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { + return v.latestFailedEvent(externalIDPTable, sequence) +} + +func (v *View) ProcessedExternalIDPFailedEvent(failedEvent *global_view.FailedEvent) error { + return v.saveFailedEvent(failedEvent) +} diff --git a/internal/management/repository/user.go b/internal/management/repository/user.go index 8cb90db9ff..8b75ee119e 100644 --- a/internal/management/repository/user.go +++ b/internal/management/repository/user.go @@ -31,6 +31,9 @@ type UserRepository interface { UserMfas(ctx context.Context, userID string) ([]*model.MultiFactor, error) + SearchExternalIDPs(ctx context.Context, request *model.ExternalIDPSearchRequest) (*model.ExternalIDPSearchResponse, error) + RemoveExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) error + SearchMachineKeys(ctx context.Context, request *model.MachineKeySearchRequest) (*model.MachineKeySearchResponse, error) GetMachineKey(ctx context.Context, userID, keyID string) (*model.MachineKeyView, error) ChangeMachine(ctx context.Context, machine *model.Machine) (*model.Machine, error) diff --git a/internal/org/repository/eventsourcing/eventstore.go b/internal/org/repository/eventsourcing/eventstore.go index 083310122a..b65e5aac3d 100644 --- a/internal/org/repository/eventsourcing/eventstore.go +++ b/internal/org/repository/eventsourcing/eventstore.go @@ -464,6 +464,10 @@ func (es *OrgEventstore) RemoveOrgMember(ctx context.Context, member *org_model. return es_sdk.Push(ctx, es.PushAggregates, repoMember.AppendEvents, orgAggregate) } +func (es *OrgEventstore) GetDefaultOrgIAMPolicy(ctx context.Context) *org_model.OrgIAMPolicy { + return es.defaultOrgIamPolicy +} + func (es *OrgEventstore) GetOrgIAMPolicy(ctx context.Context, orgID string) (*org_model.OrgIAMPolicy, error) { existingOrg, err := es.OrgByID(ctx, org_model.NewOrg(orgID)) if err != nil && !errors.IsNotFound(err) { diff --git a/internal/org/repository/eventsourcing/org.go b/internal/org/repository/eventsourcing/org.go index 97c6f1956e..aa035a16a3 100644 --- a/internal/org/repository/eventsourcing/org.go +++ b/internal/org/repository/eventsourcing/org.go @@ -386,7 +386,9 @@ func IDPConfigRemovedAggregate(ctx context.Context, aggCreator *es_models.Aggreg if err != nil { return nil, err } + agg, err = agg.AppendEvent(model.IDPConfigRemoved, &iam_es_model.IDPConfigID{IDPConfigID: idp.IDPConfigID}) + if err != nil { return nil, err } diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index fb0212ca48..c890aac789 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -26,6 +26,7 @@ Errors: AddressNotFound: Addresse nicht gefunden NotHuman: Der Benutzer muss eine Person sein NotMachine: Der Benutzer muss technisch sein + NotAllowedToLink: Der Benutzer darf nicht mit einem externen Login Provider verlinkt werden Username: Reservied: Benutzername ist bereits vergeben Code: @@ -45,6 +46,11 @@ Errors: HasUpper: Passwort beinhaltet keinen Grossbuchstaben HasNumber: Passwort beinhaltet keine Nummer HasSymbol: Passwort beinhaltet kein Symbol + ExternalIDP: + Invalid: Externer IDP ungültig + IDPConfigNotExisting: IDP Provider ungültig für diese Organisation + NotAllowed: Externer IDP ist auf dieser Organisation nicht erlaubt. + MinimumExternalIDPNeeded: Mindestens ein IDP muss hinzugefügt werden. Mfa: Otp: AlreadyReady: Multifaktor OTP (OneTimePassword) ist bereits eingerichtet diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 583063d5ca..2dfdb41373 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -26,6 +26,7 @@ Errors: AddressNotFound: Address not found NotHuman: The User must be personal NotMachine: The User must be technical + NotAllowedToLink: User is not allowed to link with external login provider Username: Reservied: Username is already taken Code: @@ -45,6 +46,11 @@ Errors: HasUpper: Password must contain upper case HasNumber: Password must contain number HasSymbol: Password must contain symbol + ExternalIDP: + Invalid: Externer IDP invalid + IDPConfigNotExisting: IDP provider invalid for this organisation + NotAllowed: External IDP not allowed on this organisation + MinimumExternalIDPNeeded: At least one IDP must be added Mfa: Otp: AlreadyReady: Multifactor OTP (OneTimePassword) is already set up @@ -97,7 +103,7 @@ Errors: MemberInvalid: Project member is invalid MemberAlreadyExists: Project member already exists MemberNotExisting: Project member doesn't exist - MinimumOneRoleNeeded: At least one role should be added + MinimumOneRoleNeeded: At least one role must be added RoleAlreadyExists: Role already exists RoleInvalid: Role is invalid RoleNotExisting: Role doesn't exist diff --git a/internal/ui/login/handler/auth_request.go b/internal/ui/login/handler/auth_request.go index 0fd9da68cd..f3c3d4b8d1 100644 --- a/internal/ui/login/handler/auth_request.go +++ b/internal/ui/login/handler/auth_request.go @@ -28,3 +28,7 @@ func (l *Login) getAuthRequestAndParseData(r *http.Request, data interface{}) (* err = l.parser.Parse(r, data) return authReq, err } + +func (l *Login) getParseData(r *http.Request, data interface{}) error { + return l.parser.Parse(r, data) +} diff --git a/internal/ui/login/handler/external_login_handler.go b/internal/ui/login/handler/external_login_handler.go new file mode 100644 index 0000000000..80265a94e7 --- /dev/null +++ b/internal/ui/login/handler/external_login_handler.go @@ -0,0 +1,269 @@ +package handler + +import ( + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/rp" + http_mw "github.com/caos/zitadel/internal/api/http/middleware" + "github.com/caos/zitadel/internal/auth_request/model" + "github.com/caos/zitadel/internal/crypto" + caos_errors "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/models" + iam_model "github.com/caos/zitadel/internal/iam/model" + org_model "github.com/caos/zitadel/internal/org/model" + usr_model "github.com/caos/zitadel/internal/user/model" + "net/http" + "strings" + "time" +) + +const ( + queryIDPConfigID = "idpConfigID" + tmplExternalNotFoundOption = "externalnotfoundoption" +) + +type externalIDPData struct { + IDPConfigID string `schema:"idpConfigID"` +} + +type externalIDPCallbackData struct { + State string `schema:"state"` + Code string `schema:"code"` +} + +type externalNotFoundOptionFormData struct { + Link bool `schema:"link"` + AutoRegister bool `schema:"autoregister"` +} + +type externalNotFoundOptionData struct { + baseData +} + +func (l *Login) handleExternalLogin(w http.ResponseWriter, r *http.Request) { + data := new(externalIDPData) + authReq, err := l.getAuthRequestAndParseData(r, data) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + if authReq == nil { + http.Redirect(w, r, l.zitadelURL, http.StatusFound) + return + } + idpConfig, err := l.getIDPConfigByID(r, data.IDPConfigID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, idpConfig.IDPConfigID, userAgentID) + if err != nil { + l.renderLogin(w, r, authReq, err) + return + } + if !idpConfig.IsOIDC { + l.renderError(w, r, authReq, caos_errors.ThrowInternal(nil, "LOGIN-Rio9s", "Errors.User.ExternalIDP.IDPTypeNotImplemented")) + return + } + l.handleOIDCAuthorize(w, r, authReq, idpConfig, EndpointExternalLoginCallback) +} + +func (l *Login) handleOIDCAuthorize(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, idpConfig *iam_model.IDPConfigView, callbackEndpoint string) { + provider := l.getRPConfig(w, r, authReq, idpConfig, callbackEndpoint) + http.Redirect(w, r, rp.AuthURL(authReq.ID, provider), http.StatusFound) +} + +func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Request) { + data := new(externalIDPCallbackData) + err := l.getParseData(r, data) + if err != nil { + l.renderError(w, r, nil, err) + return + } + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + provider := l.getRPConfig(w, r, authReq, idpConfig, EndpointExternalLoginCallback) + tokens, err := rp.CodeExchange(r.Context(), data.Code, provider) + if err != nil { + l.renderLogin(w, r, authReq, err) + return + } + l.handleExternalUserAuthenticated(w, r, authReq, idpConfig, userAgentID, tokens) +} + +func (l *Login) getRPConfig(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, idpConfig *iam_model.IDPConfigView, callbackEndpoint string) rp.RelayingParty { + oidcClientSecret, err := crypto.DecryptString(idpConfig.OIDCClientSecret, l.IDPConfigAesCrypto) + if err != nil { + l.renderError(w, r, authReq, err) + return nil + } + provider, err := rp.NewRelayingPartyOIDC(idpConfig.OIDCIssuer, idpConfig.OIDCClientID, oidcClientSecret, l.baseURL+callbackEndpoint, idpConfig.OIDCScopes, rp.WithVerifierOpts(rp.WithIssuedAtOffset(3*time.Second))) + if err != nil { + l.renderError(w, r, authReq, err) + return nil + } + return provider +} + +func (l *Login) handleExternalUserAuthenticated(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, idpConfig *iam_model.IDPConfigView, userAgentID string, tokens *oidc.Tokens) { + externalUser := l.mapTokenToLoginUser(tokens, idpConfig) + err := l.authRepo.CheckExternalUserLogin(r.Context(), authReq.ID, userAgentID, externalUser) + if err != nil { + l.renderExternalNotFoundOption(w, r, authReq, nil) + return + } + l.renderNextStep(w, r, authReq) +} + +func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) { + var errType, errMessage string + if err != nil { + errMessage = l.getErrorMessage(r, err) + } + data := externalNotFoundOptionData{ + baseData: l.getBaseData(r, authReq, "ExternalNotFoundOption", errType, errMessage), + } + l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplExternalNotFoundOption], data, nil) +} + +func (l *Login) handleExternalNotFoundOptionCheck(w http.ResponseWriter, r *http.Request) { + data := new(externalNotFoundOptionFormData) + authReq, err := l.getAuthRequestAndParseData(r, data) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + if data.Link { + l.renderLogin(w, r, authReq, nil) + return + } + l.handleAutoRegister(w, r, authReq) +} + +func (l *Login) handleAutoRegister(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) { + orgIamPolicy, err := l.getOrgIamPolicy(r, authReq.GetScopeOrgID()) + if err != nil { + l.renderExternalNotFoundOption(w, r, authReq, err) + return + } + iam, err := l.authRepo.GetIAM(r.Context()) + if err != nil { + l.renderExternalNotFoundOption(w, r, authReq, err) + return + } + resourceOwner := iam.GlobalOrgID + member := &org_model.OrgMember{ + ObjectRoot: models.ObjectRoot{AggregateID: iam.GlobalOrgID}, + Roles: []string{orgProjectCreatorRole}, + } + if authReq.GetScopeOrgID() != iam.GlobalOrgID && authReq.GetScopeOrgID() != "" { + member = nil + resourceOwner = authReq.GetScopeOrgID() + } + + idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID) + if err != nil { + l.renderExternalNotFoundOption(w, r, authReq, err) + return + } + + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + user, externalIDP := l.mapExternalUserToLoginUser(orgIamPolicy, authReq.LinkingUsers[len(authReq.LinkingUsers)-1], idpConfig) + err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, member, authReq.ID, userAgentID, resourceOwner) + if err != nil { + l.renderExternalNotFoundOption(w, r, authReq, err) + return + } + l.renderNextStep(w, r, authReq) +} + +func (l *Login) mapTokenToLoginUser(tokens *oidc.Tokens, idpConfig *iam_model.IDPConfigView) *model.ExternalUser { + displayName := tokens.IDTokenClaims.PreferredUsername + switch idpConfig.OIDCIDPDisplayNameMapping { + case iam_model.OIDCMappingFieldEmail: + if tokens.IDTokenClaims.EmailVerified && tokens.IDTokenClaims.Email != "" { + displayName = tokens.IDTokenClaims.Email + } + } + + externalUser := &model.ExternalUser{ + IDPConfigID: idpConfig.IDPConfigID, + ExternalUserID: tokens.IDTokenClaims.Subject, + PreferredUsername: tokens.IDTokenClaims.PreferredUsername, + DisplayName: displayName, + FirstName: tokens.IDTokenClaims.GivenName, + LastName: tokens.IDTokenClaims.FamilyName, + NickName: tokens.IDTokenClaims.Nickname, + Email: tokens.IDTokenClaims.Email, + IsEmailVerified: tokens.IDTokenClaims.EmailVerified, + } + + if tokens.IDTokenClaims.PhoneNumber != "" { + externalUser.Phone = tokens.IDTokenClaims.PhoneNumber + externalUser.IsPhoneVerified = tokens.IDTokenClaims.PhoneNumberVerified + } + return externalUser +} + +func (l *Login) mapExternalUserToLoginUser(orgIamPolicy *org_model.OrgIAMPolicy, linkingUser *model.ExternalUser, idpConfig *iam_model.IDPConfigView) (*usr_model.User, *usr_model.ExternalIDP) { + username := linkingUser.PreferredUsername + switch idpConfig.OIDCUsernameMapping { + case iam_model.OIDCMappingFieldEmail: + if linkingUser.IsEmailVerified && linkingUser.Email != "" { + username = linkingUser.Email + } + } + + if orgIamPolicy.UserLoginMustBeDomain { + splittedUsername := strings.Split(username, "@") + if len(splittedUsername) > 1 { + username = splittedUsername[0] + } + } + + user := &usr_model.User{ + UserName: username, + Human: &usr_model.Human{ + Profile: &usr_model.Profile{ + FirstName: linkingUser.FirstName, + LastName: linkingUser.LastName, + PreferredLanguage: linkingUser.PreferredLanguage, + NickName: linkingUser.NickName, + }, + Email: &usr_model.Email{ + EmailAddress: linkingUser.Email, + IsEmailVerified: linkingUser.IsEmailVerified, + }, + }, + } + if linkingUser.Phone != "" { + user.Phone = &usr_model.Phone{ + PhoneNumber: linkingUser.Phone, + IsPhoneVerified: linkingUser.IsPhoneVerified, + } + } + + displayName := linkingUser.PreferredUsername + switch idpConfig.OIDCIDPDisplayNameMapping { + case iam_model.OIDCMappingFieldEmail: + if linkingUser.IsEmailVerified && linkingUser.Email != "" { + displayName = linkingUser.Email + } + } + + externalIDP := &usr_model.ExternalIDP{ + IDPConfigID: idpConfig.IDPConfigID, + UserID: linkingUser.ExternalUserID, + DisplayName: displayName, + } + return user, externalIDP +} diff --git a/internal/ui/login/handler/external_register_handler.go b/internal/ui/login/handler/external_register_handler.go new file mode 100644 index 0000000000..e5cdc8bb04 --- /dev/null +++ b/internal/ui/login/handler/external_register_handler.go @@ -0,0 +1,155 @@ +package handler + +import ( + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/rp" + http_mw "github.com/caos/zitadel/internal/api/http/middleware" + "github.com/caos/zitadel/internal/auth_request/model" + caos_errors "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/models" + iam_model "github.com/caos/zitadel/internal/iam/model" + org_model "github.com/caos/zitadel/internal/org/model" + usr_model "github.com/caos/zitadel/internal/user/model" + "net/http" + "strings" +) + +func (l *Login) handleExternalRegister(w http.ResponseWriter, r *http.Request) { + data := new(externalIDPData) + authReq, err := l.getAuthRequestAndParseData(r, data) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + if authReq == nil { + http.Redirect(w, r, l.zitadelURL, http.StatusFound) + return + } + idpConfig, err := l.getIDPConfigByID(r, data.IDPConfigID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, idpConfig.IDPConfigID, userAgentID) + if err != nil { + l.renderLogin(w, r, authReq, err) + return + } + if !idpConfig.IsOIDC { + l.renderError(w, r, authReq, caos_errors.ThrowInternal(nil, "LOGIN-Rio9s", "Errors.User.ExternalIDP.IDPTypeNotImplemented")) + return + } + l.handleOIDCAuthorize(w, r, authReq, idpConfig, EndpointExternalRegisterCallback) +} + +func (l *Login) handleExternalRegisterCallback(w http.ResponseWriter, r *http.Request) { + data := new(externalIDPCallbackData) + err := l.getParseData(r, data) + if err != nil { + l.renderError(w, r, nil, err) + return + } + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + provider := l.getRPConfig(w, r, authReq, idpConfig, EndpointExternalRegisterCallback) + tokens, err := rp.CodeExchange(r.Context(), data.Code, provider) + if err != nil { + l.renderRegisterOption(w, r, authReq, err) + return + } + l.handleExternalUserRegister(w, r, authReq, idpConfig, userAgentID, tokens) +} + +func (l *Login) handleExternalUserRegister(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, idpConfig *iam_model.IDPConfigView, userAgentID string, tokens *oidc.Tokens) { + orgIamPolicy, err := l.getOrgIamPolicy(r, authReq.GetScopeOrgID()) + if err != nil { + l.renderRegisterOption(w, r, authReq, err) + return + } + iam, err := l.authRepo.GetIAM(r.Context()) + if err != nil { + l.renderRegisterOption(w, r, authReq, err) + return + } + resourceOwner := iam.GlobalOrgID + member := &org_model.OrgMember{ + ObjectRoot: models.ObjectRoot{AggregateID: iam.GlobalOrgID}, + Roles: []string{orgProjectCreatorRole}, + } + if authReq.GetScopeOrgID() != iam.GlobalOrgID && authReq.GetScopeOrgID() != "" { + member = nil + resourceOwner = authReq.GetScopeOrgID() + } + + user, externalIDP := l.mapTokenToLoginUserAndExternalIDP(orgIamPolicy, tokens, idpConfig) + _, err = l.authRepo.RegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, member, resourceOwner) + if err != nil { + l.renderRegisterOption(w, r, authReq, err) + return + } + l.renderNextStep(w, r, authReq) +} + +func (l *Login) mapTokenToLoginUserAndExternalIDP(orgIamPolicy *org_model.OrgIAMPolicy, tokens *oidc.Tokens, idpConfig *iam_model.IDPConfigView) (*usr_model.User, *usr_model.ExternalIDP) { + username := tokens.IDTokenClaims.PreferredUsername + switch idpConfig.OIDCUsernameMapping { + case iam_model.OIDCMappingFieldEmail: + if tokens.IDTokenClaims.EmailVerified && tokens.IDTokenClaims.Email != "" { + username = tokens.IDTokenClaims.Email + } + } + + if orgIamPolicy.UserLoginMustBeDomain { + splittedUsername := strings.Split(username, "@") + if len(splittedUsername) > 1 { + username = splittedUsername[0] + } + } + + user := &usr_model.User{ + UserName: username, + Human: &usr_model.Human{ + Profile: &usr_model.Profile{ + FirstName: tokens.IDTokenClaims.GivenName, + LastName: tokens.IDTokenClaims.FamilyName, + PreferredLanguage: tokens.IDTokenClaims.Locale, + NickName: tokens.IDTokenClaims.Nickname, + }, + Email: &usr_model.Email{ + EmailAddress: tokens.IDTokenClaims.Email, + IsEmailVerified: tokens.IDTokenClaims.EmailVerified, + }, + }, + } + if tokens.IDTokenClaims.PhoneNumber != "" { + user.Phone = &usr_model.Phone{ + PhoneNumber: tokens.IDTokenClaims.PhoneNumber, + IsPhoneVerified: tokens.IDTokenClaims.PhoneNumberVerified, + } + } + + displayName := tokens.IDTokenClaims.PreferredUsername + switch idpConfig.OIDCIDPDisplayNameMapping { + case iam_model.OIDCMappingFieldEmail: + if tokens.IDTokenClaims.EmailVerified && tokens.IDTokenClaims.Email != "" { + displayName = tokens.IDTokenClaims.Email + } + } + + externalIDP := &usr_model.ExternalIDP{ + IDPConfigID: idpConfig.IDPConfigID, + UserID: tokens.IDTokenClaims.Subject, + DisplayName: displayName, + } + return user, externalIDP +} diff --git a/internal/ui/login/handler/link_users_handler.go b/internal/ui/login/handler/link_users_handler.go new file mode 100644 index 0000000000..ca238486bb --- /dev/null +++ b/internal/ui/login/handler/link_users_handler.go @@ -0,0 +1,24 @@ +package handler + +import ( + http_mw "github.com/caos/zitadel/internal/api/http/middleware" + "net/http" + + "github.com/caos/zitadel/internal/auth_request/model" +) + +const ( + tmplLinkUsersDone = "linkusersdone" +) + +func (l *Login) linkUsers(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) { + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + err = l.authRepo.LinkExternalUsers(setContext(r.Context(), authReq.UserOrgID), authReq.ID, userAgentID) + l.renderLinkUsersDone(w, r, authReq, err) +} + +func (l *Login) renderLinkUsersDone(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) { + var errType, errMessage string + data := l.getUserData(r, authReq, "Linking Users Done", errType, errMessage) + l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplLinkUsersDone], data, nil) +} diff --git a/internal/ui/login/handler/login.go b/internal/ui/login/handler/login.go index 1ca76e0f81..4322c9c27f 100644 --- a/internal/ui/login/handler/login.go +++ b/internal/ui/login/handler/login.go @@ -2,6 +2,7 @@ package handler import ( "context" + "github.com/caos/zitadel/internal/config/systemdefaults" "net" "net/http" @@ -28,11 +29,14 @@ type Login struct { renderer *Renderer parser *form.Parser authRepo auth_repository.Repository + baseURL string zitadelURL string oidcAuthCallbackURL string + IDPConfigAesCrypto crypto.EncryptionAlgorithm } type Config struct { + BaseURL string OidcAuthCallbackURL string ZitadelURL string LanguageCookieName string @@ -53,11 +57,17 @@ const ( handlerPrefix = "/login" ) -func CreateLogin(config Config, authRepo *eventsourcing.EsRepository, localDevMode bool) (*Login, string) { +func CreateLogin(config Config, authRepo *eventsourcing.EsRepository, systemDefaults systemdefaults.SystemDefaults, localDevMode bool) (*Login, string) { + aesCrypto, err := crypto.NewAESCrypto(systemDefaults.IDPConfigVerificationKey) + if err != nil { + logging.Log("HANDL-s90ew").WithError(err).Debug("error create new aes crypto") + } login := &Login{ oidcAuthCallbackURL: config.OidcAuthCallbackURL, + baseURL: config.BaseURL, zitadelURL: config.ZitadelURL, authRepo: authRepo, + IDPConfigAesCrypto: aesCrypto, } prefix := "" if localDevMode { diff --git a/internal/ui/login/handler/login_handler.go b/internal/ui/login/handler/login_handler.go index 6fc7930c6e..08e950116e 100644 --- a/internal/ui/login/handler/login_handler.go +++ b/internal/ui/login/handler/login_handler.go @@ -30,12 +30,12 @@ func (l *Login) handleLogin(w http.ResponseWriter, r *http.Request) { } func (l *Login) handleLoginName(w http.ResponseWriter, r *http.Request) { - authSession, err := l.getAuthRequest(r) + authReq, err := l.getAuthRequest(r) if err != nil { - l.renderError(w, r, authSession, err) + l.renderError(w, r, authReq, err) return } - l.renderLogin(w, r, authSession, nil) + l.renderLogin(w, r, authReq, nil) } func (l *Login) handleLoginNameCheck(w http.ResponseWriter, r *http.Request) { @@ -46,6 +46,10 @@ func (l *Login) handleLoginNameCheck(w http.ResponseWriter, r *http.Request) { return } if data.Register { + if authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowExternalIDP && authReq.AllowedExternalIDPs != nil && len(authReq.AllowedExternalIDPs) > 0 { + l.handleRegisterOption(w, r) + return + } l.handleRegister(w, r) return } diff --git a/internal/ui/login/handler/org_iam_policy_handler.go b/internal/ui/login/handler/org_iam_policy_handler.go deleted file mode 100644 index 3fd4b4179c..0000000000 --- a/internal/ui/login/handler/org_iam_policy_handler.go +++ /dev/null @@ -1,10 +0,0 @@ -package handler - -import ( - org_model "github.com/caos/zitadel/internal/org/model" - "net/http" -) - -func (l *Login) getOrgIamPolicy(r *http.Request, orgID string) (*org_model.OrgIAMPolicy, error) { - return l.authRepo.GetOrgIamPolicy(r.Context(), orgID) -} diff --git a/internal/ui/login/handler/policy_handler.go b/internal/ui/login/handler/policy_handler.go new file mode 100644 index 0000000000..b9741240ee --- /dev/null +++ b/internal/ui/login/handler/policy_handler.go @@ -0,0 +1,18 @@ +package handler + +import ( + iam_model "github.com/caos/zitadel/internal/iam/model" + org_model "github.com/caos/zitadel/internal/org/model" + "net/http" +) + +func (l *Login) getOrgIamPolicy(r *http.Request, orgID string) (*org_model.OrgIAMPolicy, error) { + if orgID == "" { + return l.authRepo.GetDefaultOrgIamPolicy(r.Context()), nil + } + return l.authRepo.GetOrgIamPolicy(r.Context(), orgID) +} + +func (l *Login) getIDPConfigByID(r *http.Request, idpConfigID string) (*iam_model.IDPConfigView, error) { + return l.authRepo.GetIDPConfigByID(r.Context(), idpConfigID) +} diff --git a/internal/ui/login/handler/register_handler.go b/internal/ui/login/handler/register_handler.go index 0d1b6f2f39..e8ffdf0508 100644 --- a/internal/ui/login/handler/register_handler.go +++ b/internal/ui/login/handler/register_handler.go @@ -66,11 +66,16 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) { return } + resourceOwner := iam.GlobalOrgID member := &org_model.OrgMember{ ObjectRoot: models.ObjectRoot{AggregateID: iam.GlobalOrgID}, Roles: []string{orgProjectCreatorRole}, } - user, err := l.authRepo.Register(setContext(r.Context(), iam.GlobalOrgID), data.toUserModel(), member, iam.GlobalOrgID) + if authRequest.GetScopeOrgID() != "" && authRequest.GetScopeOrgID() != iam.GlobalOrgID { + member = nil + resourceOwner = authRequest.GetScopeOrgID() + } + user, err := l.authRepo.Register(setContext(r.Context(), resourceOwner), data.toUserModel(), member, resourceOwner) if err != nil { l.renderRegister(w, r, authRequest, data, err) return diff --git a/internal/ui/login/handler/register_option_handler.go b/internal/ui/login/handler/register_option_handler.go new file mode 100644 index 0000000000..b915da4269 --- /dev/null +++ b/internal/ui/login/handler/register_option_handler.go @@ -0,0 +1,53 @@ +package handler + +import ( + "github.com/caos/zitadel/internal/auth_request/model" + "net/http" +) + +const ( + tmplRegisterOption = "registeroption" +) + +type registerOptionFormData struct { + UsernamePassword bool `schema:"usernamepassword"` +} + +type registerOptionData struct { + baseData +} + +func (l *Login) handleRegisterOption(w http.ResponseWriter, r *http.Request) { + data := new(registerOptionFormData) + authRequest, err := l.getAuthRequestAndParseData(r, data) + if err != nil { + l.renderError(w, r, authRequest, err) + return + } + l.renderRegisterOption(w, r, authRequest, nil) +} + +func (l *Login) renderRegisterOption(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) { + var errType, errMessage string + if err != nil { + errMessage = l.getErrorMessage(r, err) + } + data := registerOptionData{ + baseData: l.getBaseData(r, authReq, "RegisterOption", errType, errMessage), + } + l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplRegisterOption], data, nil) +} + +func (l *Login) handleRegisterOptionCheck(w http.ResponseWriter, r *http.Request) { + data := new(registerOptionFormData) + authReq, err := l.getAuthRequestAndParseData(r, data) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + if data.UsernamePassword { + l.handleRegister(w, r) + return + } + l.handleRegisterOption(w, r) +} diff --git a/internal/ui/login/handler/renderer.go b/internal/ui/login/handler/renderer.go index 76f19e2872..01fd185353 100644 --- a/internal/ui/login/handler/renderer.go +++ b/internal/ui/login/handler/renderer.go @@ -3,14 +3,14 @@ package handler import ( "errors" "fmt" + "github.com/caos/logging" + iam_model "github.com/caos/zitadel/internal/iam/model" + "github.com/gorilla/csrf" + "golang.org/x/text/language" "html/template" "net/http" "path" - "github.com/caos/logging" - "github.com/gorilla/csrf" - "golang.org/x/text/language" - http_mw "github.com/caos/zitadel/internal/api/http/middleware" "github.com/caos/zitadel/internal/auth_request/model" caos_errs "github.com/caos/zitadel/internal/errors" @@ -32,28 +32,31 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str pathPrefix: pathPrefix, } tmplMapping := map[string]string{ - tmplError: "error.html", - tmplLogin: "login.html", - tmplUserSelection: "select_user.html", - tmplPassword: "password.html", - tmplMfaVerify: "mfa_verify.html", - tmplMfaPrompt: "mfa_prompt.html", - tmplMfaInitVerify: "mfa_init_verify.html", - tmplMfaInitDone: "mfa_init_done.html", - tmplMailVerification: "mail_verification.html", - tmplMailVerified: "mail_verified.html", - tmplInitPassword: "init_password.html", - tmplInitPasswordDone: "init_password_done.html", - tmplInitUser: "init_user.html", - tmplInitUserDone: "init_user_done.html", - tmplPasswordResetDone: "password_reset_done.html", - tmplChangePassword: "change_password.html", - tmplChangePasswordDone: "change_password_done.html", - tmplRegister: "register.html", - tmplLogoutDone: "logout_done.html", - tmplRegisterOrg: "register_org.html", - tmplChangeUsername: "change_username.html", - tmplChangeUsernameDone: "change_username_done.html", + tmplError: "error.html", + tmplLogin: "login.html", + tmplUserSelection: "select_user.html", + tmplPassword: "password.html", + tmplMfaVerify: "mfa_verify.html", + tmplMfaPrompt: "mfa_prompt.html", + tmplMfaInitVerify: "mfa_init_verify.html", + tmplMfaInitDone: "mfa_init_done.html", + tmplMailVerification: "mail_verification.html", + tmplMailVerified: "mail_verified.html", + tmplInitPassword: "init_password.html", + tmplInitPasswordDone: "init_password_done.html", + tmplInitUser: "init_user.html", + tmplInitUserDone: "init_user_done.html", + tmplPasswordResetDone: "password_reset_done.html", + tmplChangePassword: "change_password.html", + tmplChangePasswordDone: "change_password_done.html", + tmplRegisterOption: "register_option.html", + tmplRegister: "register.html", + tmplLogoutDone: "logout_done.html", + tmplRegisterOrg: "register_org.html", + tmplChangeUsername: "change_username.html", + tmplChangeUsernameDone: "change_username_done.html", + tmplLinkUsersDone: "link_users_done.html", + tmplExternalNotFoundOption: "external_not_found_option.html", } funcs := map[string]interface{}{ "resourceUrl": func(file string) string { @@ -65,6 +68,12 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str "loginUrl": func() string { return path.Join(r.pathPrefix, EndpointLogin) }, + "externalIDPAuthURL": func(authReqID, idpConfigID string) string { + return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s&%s=%s", EndpointExternalLogin, queryAuthRequestID, authReqID, queryIDPConfigID, idpConfigID)) + }, + "externalIDPRegisterURL": func(authReqID, idpConfigID string) string { + return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s&%s=%s", EndpointExternalRegister, queryAuthRequestID, authReqID, queryIDPConfigID, idpConfigID)) + }, "registerUrl": func(id string) string { return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s", EndpointRegister, queryAuthRequestID, id)) }, @@ -107,6 +116,9 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str "changePasswordUrl": func() string { return path.Join(r.pathPrefix, EndpointChangePassword) }, + "registerOptionUrl": func() string { + return path.Join(r.pathPrefix, EndpointRegisterOption) + }, "registrationUrl": func() string { return path.Join(r.pathPrefix, EndpointRegister) }, @@ -116,6 +128,9 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str "changeUsernameUrl": func() string { return path.Join(r.pathPrefix, EndpointChangeUsername) }, + "externalNotFoundOptionUrl": func() string { + return path.Join(r.pathPrefix, EndpointExternalNotFoundOption) + }, "selectedLanguage": func(l string) bool { return false }, @@ -186,6 +201,10 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq * l.renderInitUser(w, r, authReq, "", "", step.PasswordSet, nil) case *model.ChangeUsernameStep: l.renderChangeUsername(w, r, authReq, nil) + case *model.LinkUsersStep: + l.linkUsers(w, r, authReq, err) + case *model.ExternalNotFoundOptionStep: + l.renderExternalNotFoundOption(w, r, authReq, err) default: l.renderInternalError(w, r, authReq, caos_errs.ThrowInternal(nil, "APP-ds3QF", "step no possible")) } @@ -204,11 +223,12 @@ func (l *Login) getUserData(r *http.Request, authReq *model.AuthRequest, title s return userData{ baseData: l.getBaseData(r, authReq, title, errType, errMessage), profileData: l.getProfileData(authReq), + Linking: len(authReq.LinkingUsers) > 0, } } func (l *Login) getBaseData(r *http.Request, authReq *model.AuthRequest, title string, errType, errMessage string) baseData { - return baseData{ + baseData := baseData{ errorData: errorData{ ErrType: errType, ErrMessage: errMessage, @@ -217,10 +237,16 @@ func (l *Login) getBaseData(r *http.Request, authReq *model.AuthRequest, title s Title: title, Theme: l.getTheme(r), ThemeMode: l.getThemeMode(r), + OrgID: l.getOrgID(authReq), AuthReqID: getRequestID(authReq, r), CSRF: csrf.TemplateField(r), Nonce: http_mw.GetNonce(r), } + if authReq != nil { + baseData.LoginPolicy = authReq.LoginPolicy + baseData.IDPProviders = authReq.AllowedExternalIDPs + } + return baseData } func (l *Login) getProfileData(authReq *model.AuthRequest) profileData { @@ -253,6 +279,19 @@ func (l *Login) getThemeMode(r *http.Request) string { return "" //TODO: impl } +func (l *Login) getOrgID(authReq *model.AuthRequest) string { + if authReq == nil { + return "" + } + if authReq.UserOrgID != "" { + return authReq.UserOrgID + } + if authReq.Request == nil { + return "" + } + return authReq.GetScopeOrgID() +} + func getRequestID(authReq *model.AuthRequest, r *http.Request) string { if authReq != nil { return authReq.ID @@ -275,13 +314,16 @@ func (l *Login) cspErrorHandler(err error) http.Handler { type baseData struct { errorData - Lang string - Title string - Theme string - ThemeMode string - AuthReqID string - CSRF template.HTML - Nonce string + Lang string + Title string + Theme string + ThemeMode string + OrgID string + AuthReqID string + CSRF template.HTML + Nonce string + LoginPolicy *iam_model.LoginPolicyView + IDPProviders []*iam_model.IDPProviderView } type errorData struct { @@ -295,6 +337,7 @@ type userData struct { PasswordChecked string MfaProviders []model.MfaType SelectedMfaProvider model.MfaType + Linking bool } type profileData struct { @@ -315,7 +358,8 @@ type passwordData struct { type userSelectionData struct { baseData - Users []model.UserSelection + Users []model.UserSelection + Linking bool } type mfaData struct { diff --git a/internal/ui/login/handler/router.go b/internal/ui/login/handler/router.go index abea7b49b6..7b1651b9eb 100644 --- a/internal/ui/login/handler/router.go +++ b/internal/ui/login/handler/router.go @@ -7,26 +7,32 @@ import ( ) const ( - EndpointRoot = "/" - EndpointHealthz = "/healthz" - EndpointReadiness = "/ready" - EndpointLogin = "/login" - EndpointLoginName = "/loginname" - EndpointUserSelection = "/userselection" - EndpointChangeUsername = "/username/change" - EndpointPassword = "/password" - EndpointInitPassword = "/password/init" - EndpointChangePassword = "/password/change" - EndpointPasswordReset = "/password/reset" - EndpointInitUser = "/user/init" - EndpointMfaVerify = "/mfa/verify" - EndpointMfaPrompt = "/mfa/prompt" - EndpointMfaInitVerify = "/mfa/init/verify" - EndpointMailVerification = "/mail/verification" - EndpointMailVerified = "/mail/verified" - EndpointRegister = "/register" - EndpointRegisterOrg = "/register/org" - EndpointLogoutDone = "/logout/done" + EndpointRoot = "/" + EndpointHealthz = "/healthz" + EndpointReadiness = "/ready" + EndpointLogin = "/login" + EndpointExternalLogin = "/login/externalidp" + EndpointExternalLoginCallback = "/login/externalidp/callback" + EndpointLoginName = "/loginname" + EndpointUserSelection = "/userselection" + EndpointChangeUsername = "/username/change" + EndpointPassword = "/password" + EndpointInitPassword = "/password/init" + EndpointChangePassword = "/password/change" + EndpointPasswordReset = "/password/reset" + EndpointInitUser = "/user/init" + EndpointMfaVerify = "/mfa/verify" + EndpointMfaPrompt = "/mfa/prompt" + EndpointMfaInitVerify = "/mfa/init/verify" + EndpointMailVerification = "/mail/verification" + EndpointMailVerified = "/mail/verified" + EndpointRegisterOption = "/register/option" + EndpointRegister = "/register" + EndpointExternalRegister = "/register/externalidp" + EndpointExternalRegisterCallback = "/register/externalidp/callback" + EndpointRegisterOrg = "/register/org" + EndpointLogoutDone = "/logout/done" + EndpointExternalNotFoundOption = "/externaluser/option" EndpointResources = "/resources" ) @@ -38,6 +44,8 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M router.HandleFunc(EndpointHealthz, login.handleHealthz).Methods(http.MethodGet) router.HandleFunc(EndpointReadiness, login.handleReadiness).Methods(http.MethodGet) router.HandleFunc(EndpointLogin, login.handleLogin).Methods(http.MethodGet, http.MethodPost) + router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet) + router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).Methods(http.MethodGet) router.HandleFunc(EndpointLoginName, login.handleLoginName).Methods(http.MethodGet) router.HandleFunc(EndpointLoginName, login.handleLoginNameCheck).Methods(http.MethodPost) router.HandleFunc(EndpointUserSelection, login.handleSelectUser).Methods(http.MethodPost) @@ -55,8 +63,13 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M router.HandleFunc(EndpointMailVerification, login.handleMailVerification).Methods(http.MethodGet) router.HandleFunc(EndpointMailVerification, login.handleMailVerificationCheck).Methods(http.MethodPost) router.HandleFunc(EndpointChangePassword, login.handleChangePassword).Methods(http.MethodPost) + router.HandleFunc(EndpointRegisterOption, login.handleRegisterOption).Methods(http.MethodGet) + router.HandleFunc(EndpointRegisterOption, login.handleRegisterOptionCheck).Methods(http.MethodPost) + router.HandleFunc(EndpointExternalNotFoundOption, login.handleExternalNotFoundOptionCheck).Methods(http.MethodPost) router.HandleFunc(EndpointRegister, login.handleRegister).Methods(http.MethodGet) router.HandleFunc(EndpointRegister, login.handleRegisterCheck).Methods(http.MethodPost) + router.HandleFunc(EndpointExternalRegister, login.handleExternalRegister).Methods(http.MethodGet) + router.HandleFunc(EndpointExternalRegisterCallback, login.handleExternalRegisterCallback).Methods(http.MethodGet) router.HandleFunc(EndpointLogoutDone, login.handleLogoutDone).Methods(http.MethodGet) router.PathPrefix(EndpointResources).Handler(login.handleResources(staticDir)).Methods(http.MethodGet) router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrg).Methods(http.MethodGet) diff --git a/internal/ui/login/handler/select_user_handler.go b/internal/ui/login/handler/select_user_handler.go index 1327c62298..088c963439 100644 --- a/internal/ui/login/handler/select_user_handler.go +++ b/internal/ui/login/handler/select_user_handler.go @@ -19,6 +19,7 @@ func (l *Login) renderUserSelection(w http.ResponseWriter, r *http.Request, auth data := userSelectionData{ baseData: l.getBaseData(r, authReq, "Select User", "", ""), Users: selectionData.Users, + Linking: len(authReq.LinkingUsers) > 0, } l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplUserSelection], data, nil) } diff --git a/internal/ui/login/login.go b/internal/ui/login/login.go index b076b21a79..0d28b56006 100644 --- a/internal/ui/login/login.go +++ b/internal/ui/login/login.go @@ -2,6 +2,7 @@ package login import ( "github.com/caos/zitadel/internal/auth/repository/eventsourcing" + "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/ui/login/handler" ) @@ -9,6 +10,6 @@ type Config struct { Handler handler.Config } -func Start(config Config, authRepo *eventsourcing.EsRepository, localDevMode bool) (*handler.Login, string) { - return handler.CreateLogin(config.Handler, authRepo, localDevMode) +func Start(config Config, authRepo *eventsourcing.EsRepository, systemdefaults systemdefaults.SystemDefaults, localDevMode bool) (*handler.Login, string) { + return handler.CreateLogin(config.Handler, authRepo, systemdefaults, localDevMode) } diff --git a/internal/ui/login/static/i18n/de.yaml b/internal/ui/login/static/i18n/de.yaml index 46b2839594..a88ecf6757 100644 --- a/internal/ui/login/static/i18n/de.yaml +++ b/internal/ui/login/static/i18n/de.yaml @@ -11,12 +11,17 @@ Password: Login: Title: Anmeldung Description: Gib deine Benutzerdaten ein. + TitleLinking: Anmeldung für Benutzer Linking + DescriptionLinking: Gib deine Benutzerdaten ein um den externen Benutzer mit einem ZITADEL Benutzer zu linken. Loginname: Loginname LoginnamePlaceHolder: username@domain + ExternalLogin: Melde dich mit einem externen Benutzer an UserSelection: Title: Account auswählen Description: Wähle deinen Account aus. + TitleLinking: Account auswählen um zu verlinken + DescriptionLinking: Wähle deinen Account, um diesen mit deinem externen Benutzer zu verlinken. OtherUser: Anderer Benutzer SessionState0: aktiv SessionState1: inaktiv @@ -103,6 +108,11 @@ EmailVerificationDone: Title: E-Mail Verifizierung Description: Deine E-Mail Adresse wurde erfolgreich verifiziert. +RegisterOption: + Title: Registrations Möglichkeiten + Description: Wähle aus wie du dich registrieren möchtest. + RegisterUsernamePassword: Mit Benutzername Passwort + Registration: Title: Registration Description: Gib deine Benutzerangaben an. Die E-Mail Adresse wird als Benutzernamen verwendet. @@ -147,6 +157,16 @@ RegistrationOrg: TosLinkText: AGBs TosLink: https://zitadel.ch/pdf/agb.pdf +LinkingUsersDone: + Title: Benutzerlinking + Description: Benuzterlinking erledigt. + +ExternalNotFoundOption: + Title: Externer Benutzer + Description: Externer Benutzer konnte nicht gefunden werden. Willst du deinen Benutzer mit einem bestehenden verlinken oder diesen als neuen Benutzer registrieren. + Link: Verlinken + AutoRegister: Automatisches registrieren + LogoutDone: Title: Ausgeloggt Description: Du wurdest erfolgreich ausgeloggt. @@ -173,6 +193,7 @@ Errors: UserIDMissing: UserID ist leer Invalid: Userdaten sind ungültig DomainNotAllowedAsUsername: Domäne ist bereits reserviert und kann nicht verwendet werden + NotAllowedToLink: Der Benutzer darf nicht mit einem externen Login Provider verlinkt werden Password: ConfirmationWrong: Passwort Bestätigung stimmt nicht überein Empty: Passwort ist leer @@ -202,5 +223,7 @@ Errors: NotReady: Multifaktor OTP (OneTimePassword) ist nicht bereit Locked: Benutzer ist gesperrt NotActive: Benutzer ist nicht aktiv + ExternalIDP: + IDPTypeNotImplemented: IDP Typ ist nicht implementiert optional: (optional) diff --git a/internal/ui/login/static/i18n/en.yaml b/internal/ui/login/static/i18n/en.yaml index 0300789ce4..c21db87b1c 100644 --- a/internal/ui/login/static/i18n/en.yaml +++ b/internal/ui/login/static/i18n/en.yaml @@ -1,12 +1,17 @@ Login: Title: Login Description: Enter your logindata. + TitleLinking: Login for userlinking + DescriptionLinking: Enter your login data to link your external user with a ZITADEL user. Loginname: Loginname LoginnamePlaceHolder: username@domain + ExternalLogin: Login with an external user. UserSelection: Title: Select account Description: Select your account. + TitleLinking: Select account for userlinking + DescriptionLinking: Select your account to link with your external user. OtherUser: Other User SessionState0: active SessionState1: inactive @@ -103,6 +108,11 @@ EmailVerificationDone: Title: E-Mail Verification Description: Your email address has been successfully verified. +RegistrationOption: + Title: Registration Options + Description: Choose how you'd like to register + RegisterUsernamePassword: With username password + Registration: Title: Registration Description: Enter your Userdata. Your email address will be used as loginname. @@ -147,11 +157,20 @@ RegistrationOrg: TosLinkText: TOS TosLink: https://zitadel.ch/pdf/tos.pdf - LogoutDone: Title: Logged out Description: You have logged out successfully. +LinkingUsersDone: + Title: Userlinking + Description: Userlinking done. + +ExternalNotFoundOption: + Title: External User + Description: External user not found. Do you want to link your user or auto register a new one. + Link: Link + AutoRegister: Auto register + Actions: Login: login Next: next @@ -163,7 +182,6 @@ Actions: Cancel: cancel Save: save - Errors: Internal: An internal error occured AuthRequest: @@ -175,6 +193,7 @@ Errors: UserIDMissing: UserID is empty Invalid: Invalid userdata DomainNotAllowedAsUsername: Domain is already reserved and cannot be used + NotAllowedToLink: User is not allowed to link with external login provider Password: ConfirmationWrong: Passwordconfirmation is wrong Empty: Password is empty @@ -204,6 +223,8 @@ Errors: NotReady: Multifactor OTP (OneTimePassword) isn't ready Locked: User is locked NotActive: User is not active + ExternalIDP: + IDPTypeNotImplemented: IDP Type is not implemented optional: (optional) diff --git a/internal/ui/login/static/resources/themes/caos/css/dark.css b/internal/ui/login/static/resources/themes/caos/css/dark.css index 1e2926e23e..ea9bdd761a 100644 --- a/internal/ui/login/static/resources/themes/caos/css/dark.css +++ b/internal/ui/login/static/resources/themes/caos/css/dark.css @@ -73,7 +73,7 @@ *, *::before, *::after { box-sizing: border-box; font-family: Lato; - font-size: 18px; + font-size: 16px; font-weight: 400; } @@ -97,6 +97,7 @@ h1 { font-family: Aileron; font-weight: 300; font-size: 40px; + text-align: center; } h2 { @@ -122,7 +123,13 @@ header .logo { margin: 30px; } -.content { +.head { + width: 100%; + max-width: 1000px; + margin: auto; +} + +.content form { margin: auto; padding: 20px; width: 100%; @@ -137,11 +144,14 @@ a { a:hover { color: #f60075; } +a.tos-link { + font-size: 14px; +} -button { +button, .button { background-color: #282828; color: #760038; - border: 2px solid #760038; + border: 1px solid #760038; border-radius: 5px; width: 100%; max-width: 600px; @@ -149,36 +159,39 @@ button { transition: all 0.3s ease 0s; cursor: pointer; outline: none; + display: inline-block; + text-align: center; + line-height: 40px; } -button:hover { +button:hover, .button:hover { background-color: #f60075; color: #282828; - border: 2px solid #f60075; + border: 1px solid #f60075; } -button.primary { +button.primary, .button.primary { background-color: #760038; color: white; border: none; } -button.primary:hover { +button.primary:hover, .button.primary:hover { background-color: #f60075; } -button:disabled { - background-color: #505050; - border: 2px solid #505050; +button:disabled, .button:disabled { + background-color: #999999; + border: 1px solid #999999; } -button:disabled:hover { - background-color: #505050; - border: 2px solid #505050; +button:disabled:hover, .button:disabled:hover { + background-color: #999999; + border: 1px solid #999999; } input:not([type=radio]), select { background-color: #252525; color: white; height: 50px; - border: 2px solid #505050; + border: 1px solid #999999; border-radius: 5px; - padding-left: 15px; + padding-left: 8px; } form button.user-selection .profile-image, .login-profile .profile-image { @@ -213,7 +226,7 @@ form button.user-selection:hover .profile-image, .login-profile:hover .profile-i text-align: center; } .login-profile .names div:first-of-type { - font-size: 40px; + font-size: 26px; font-weight: 300; } .login-profile .names div:nth-of-type(2) { @@ -244,20 +257,21 @@ form .field.check-box { display: flex; } form .field.check-box input[type=checkbox] { - height: 20px; + height: 16px; vertical-align: middle; } form .field.check-box label { - height: 20px; + height: 16px; text-transform: inherit; display: inline-block; padding: 2px 0 0 15px; width: 100%; + color: white; } form label { color: #898989; text-transform: uppercase; - font-size: 0.9rem; + font-size: 0.8rem; margin-bottom: 3px; } form label span.optional { @@ -301,7 +315,7 @@ form button.user-selection .sessionstate { height: 20px; width: 20px; border-radius: 20px; - border-color: #505050; + border-color: #999999; border-style: solid; border-width: 1px; position: absolute; @@ -362,6 +376,7 @@ form ul#passwordcomplexity { flex-wrap: wrap; padding: 0; list-style: none; + margin-bottom: 0; } form ul#passwordcomplexity li { flex: 1 0 50%; @@ -444,4 +459,8 @@ footer { padding: 10px; } +.error { + color: #F20D6B; +} + /*# sourceMappingURL=dark.css.map */ diff --git a/internal/ui/login/static/resources/themes/caos/css/dark.css.map b/internal/ui/login/static/resources/themes/caos/css/dark.css.map index 468dd95445..ff3a62044f 100644 --- a/internal/ui/login/static/resources/themes/caos/css/dark.css.map +++ b/internal/ui/login/static/resources/themes/caos/css/dark.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/caos/variables.scss","../../scss/variables.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCMW;EDLX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCDc;EDEd,OCDQ;EDER;EACA;EACA;;;AAMJ;EACI,OCXQ;EDYR,aClBS;EDmBT;EACA,WEzBS;;;AF4Bb;EACI,OClBQ;EDmBR,aCzBS;ED0BT;EACA,WE/BU;;;AFkCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OChDW;EDiDX;EACA;;AAEA;EACI,OCpDY;;;ADwDpB;EACI,kBC5Dc;ED6Dd,OC3DW;ED4DX;EACA;EACA;EACA;EACA,QE7EU;EF8EV;EACA;EACA;;AACA;EACI,kBCpEY;EDqEZ,OCxEU;EDyEV;;AAGJ;EACI,kBC3EO;ED4EP,OC7EI;ED8EJ;;AACA;EACI,kBC9EQ;;ADkFhB;EACI,kBEzEW;EF0EX;;AAEA;EACI,kBE7EO;EF8EP;;;AAKZ;EACI,kBEnFmB;EFoFnB,OCjGQ;EDkGR,QE9GU;EF+GV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;EE9GN;;AACA;EFyGE;IExGA;IACA;;;AF+GA;EElHF;;AACA;EFiHE;IEhHA;IACA;;;;AFsHA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WE7IC;EF8ID;;AAGJ;EACI;EACA;EACA;EACA,OE/HC;;;AFqIT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAIR;EACI,OE5KK;EF6KL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OCjNI;EDkNJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBE5MW;;AF+Mf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cErOO;EFsOP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OEjQP;;AFwQL;EACI;;AAEJ;EACI;EACA;EACA;EACA;EE3RV;;AACA;EFsRM;IErRJ;IACA;;;AF6RQ;EACI;EACA;EElSd;;AACA;EF+RU;IE9RR;IACA;;;AFoSI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OE3SN;;AFgTE;EACI,OElTL;;AFuTP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MC1VI;;AD6VR;EACI,MC/VU;;;ADoWd;EACI;EACA;;;AAIR;EAEQ;EAEJ;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA","file":"dark.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/caos/variables.scss","../../scss/variables.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCMW;EDLX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCDc;EDEd,OCDQ;EDER;EACA;EACA;;;AAMJ;EACI,OCXQ;EDYR,aClBS;EDmBT;EACA,WEzBS;EF0BT;;;AAGJ;EACI,OCnBQ;EDoBR,aC1BS;ED2BT;EACA,WEhCU;;;AFmCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OCvDW;EDwDX;EACA;;AAEA;EACI,OC3DY;;AD8DhB;EACI;;;AAIR;EACI,kBCvEc;EDwEd,OCtEW;EDuEX;EACA;EACA;EACA;EACA,QExFU;EFyFV;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI,kBCnFY;EDoFZ,OCvFU;EDwFV;;AAGJ;EACI,kBC1FO;ED2FP,OC5FI;ED6FJ;;AACA;EACI,kBC7FQ;;ADiGhB;EACI,kBEvFW;EFwFX;;AAEA;EACI,kBE3FO;EF4FP;;;AAOZ;EACI,kBEnGmB;EFoGnB,OClHQ;EDmHR,QE/HU;EFgIV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;EE9HN;;AACA;EFyHE;IExHA;IACA;;;AF+HA;EElIF;;AACA;EFiIE;IEhIA;IACA;;;;AFsIA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WE5JE;EF6JF;;AAGJ;EACI;EACA;EACA;EACA,OE/IC;;;AFqJT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA,OCpMA;;ADwMR;EACI,OE7LK;EF8LL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OCnOI;EDoOJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBE7NW;;AFgOf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cEtPO;EFuPP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OElRP;;AFyRL;EACI;;AAEJ;EACI;EACA;EACA;EACA;EE5SV;;AACA;EFuSM;IEtSJ;IACA;;;AF8SQ;EACI;EACA;EEnTd;;AACA;EFgTU;IE/SR;IACA;;;AFqTI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OE7TN;;AFkUE;EACI,OEpUL;;AFyUP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MC7WI;;ADgXR;EACI,MClXU;;;ADuXd;EACI;EACA;;;AAIR;EAEQ;EAEJ;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI,OEpZO","file":"dark.css"} \ No newline at end of file diff --git a/internal/ui/login/static/resources/themes/caos/css/light.css b/internal/ui/login/static/resources/themes/caos/css/light.css index 38002f6538..1f1a0b1767 100644 --- a/internal/ui/login/static/resources/themes/caos/css/light.css +++ b/internal/ui/login/static/resources/themes/caos/css/light.css @@ -73,7 +73,7 @@ *, *::before, *::after { box-sizing: border-box; font-family: Lato; - font-size: 18px; + font-size: 16px; font-weight: 400; } @@ -97,6 +97,7 @@ h1 { font-family: Aileron; font-weight: 300; font-size: 40px; + text-align: center; } h2 { @@ -122,7 +123,13 @@ header .logo { margin: 30px; } -.content { +.head { + width: 100%; + max-width: 1000px; + margin: auto; +} + +.content form { margin: auto; padding: 20px; width: 100%; @@ -137,11 +144,14 @@ a { a:hover { color: #f60075; } +a.tos-link { + font-size: 14px; +} -button { +button, .button { background-color: #282828; color: #760038; - border: 2px solid #760038; + border: 1px solid #760038; border-radius: 5px; width: 100%; max-width: 600px; @@ -149,36 +159,39 @@ button { transition: all 0.3s ease 0s; cursor: pointer; outline: none; + display: inline-block; + text-align: center; + line-height: 40px; } -button:hover { +button:hover, .button:hover { background-color: #f60075; color: #282828; - border: 2px solid #f60075; + border: 1px solid #f60075; } -button.primary { +button.primary, .button.primary { background-color: #760038; color: white; border: none; } -button.primary:hover { +button.primary:hover, .button.primary:hover { background-color: #f60075; } -button:disabled { - background-color: #505050; - border: 2px solid #505050; +button:disabled, .button:disabled { + background-color: #999999; + border: 1px solid #999999; } -button:disabled:hover { - background-color: #505050; - border: 2px solid #505050; +button:disabled:hover, .button:disabled:hover { + background-color: #999999; + border: 1px solid #999999; } input:not([type=radio]), select { background-color: #252525; color: white; height: 50px; - border: 2px solid #505050; + border: 1px solid #999999; border-radius: 5px; - padding-left: 15px; + padding-left: 8px; } form button.user-selection .profile-image, .login-profile .profile-image { @@ -213,7 +226,7 @@ form button.user-selection:hover .profile-image, .login-profile:hover .profile-i text-align: center; } .login-profile .names div:first-of-type { - font-size: 40px; + font-size: 26px; font-weight: 300; } .login-profile .names div:nth-of-type(2) { @@ -244,20 +257,21 @@ form .field.check-box { display: flex; } form .field.check-box input[type=checkbox] { - height: 20px; + height: 16px; vertical-align: middle; } form .field.check-box label { - height: 20px; + height: 16px; text-transform: inherit; display: inline-block; padding: 2px 0 0 15px; width: 100%; + color: white; } form label { color: #898989; text-transform: uppercase; - font-size: 0.9rem; + font-size: 0.8rem; margin-bottom: 3px; } form label span.optional { @@ -301,7 +315,7 @@ form button.user-selection .sessionstate { height: 20px; width: 20px; border-radius: 20px; - border-color: #505050; + border-color: #999999; border-style: solid; border-width: 1px; position: absolute; @@ -362,6 +376,7 @@ form ul#passwordcomplexity { flex-wrap: wrap; padding: 0; list-style: none; + margin-bottom: 0; } form ul#passwordcomplexity li { flex: 1 0 50%; @@ -444,6 +459,10 @@ footer { padding: 10px; } +.error { + color: #F20D6B; +} + html { background-color: white; color: #282828; @@ -454,66 +473,75 @@ html header .logo { html h1, html h2 { color: #282828; } -html button { +html button, html .button { background-color: white; color: #760038; - border: 2px solid #760038; + border: 1px solid #760038; } -html button:hover { +html button:hover, html .button:hover { background-color: #f60075; - border: 2px solid #f60075; + border: 1px solid #f60075; + color: #FFFFFF; } -html button.primary { +html button.primary, html .button.primary { background-color: #760038; - color: white; + color: #FFFFFF; border: none; box-shadow: 0px 10px 30px #760038; } -html button.primary:hover { +html button.primary:hover, html .button.primary:hover { background-color: #f60075; } -html button.clean { +html button:disabled, html .button:disabled { + background-color: #999999; + border: 1px solid #999999; +} +html button:disabled:hover, html .button:disabled:hover { + background-color: #999999; + border: 1px solid #999999; +} +html button.clean, html .button.clean { color: #282828; } -html button.clean:hover { +html button.clean:hover, html .button.clean:hover { border: none; background-color: #FFFFFF; } -html button.user-selection .profile-image { +html button.user-selection .profile-image, html .button.user-selection .profile-image { background-image: url("../../../images/icon-user-light.png"); } @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { - html button.user-selection .profile-image { + html button.user-selection .profile-image, html .button.user-selection .profile-image { background-image: url("../../../images/icon-user-light@2x.png"); background-size: 80px 80px; } } -html button.user-selection:hover { +html button.user-selection:hover, html .button.user-selection:hover { background-color: #FFFFFF; } -html button.user-selection:hover .profile-image { +html button.user-selection:hover .profile-image, html .button.user-selection:hover .profile-image { background-image: url("../../../images/icon-user-light-hover.png"); } @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { - html button.user-selection:hover .profile-image { + html button.user-selection:hover .profile-image, html .button.user-selection:hover .profile-image { background-image: url("../../../images/icon-user-light-hover@2x.png"); background-size: 80px 80px; } } -html button.other-user .other-user-image { +html button.other-user .other-user-image, html .button.other-user .other-user-image { background-image: url("../../../images/icon-newuser-light.png"); } @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { - html button.other-user .other-user-image { + html button.other-user .other-user-image, html .button.other-user .other-user-image { background-image: url("../../../images/icon-newuser-light@2x.png"); background-size: 80px 60px; } } -html button.other-user:hover .other-user-image { +html button.other-user:hover .other-user-image, html .button.other-user:hover .other-user-image { background-image: url("../../../images/icon-newuser-light-hover.png"); } @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { - html button.other-user:hover .other-user-image { + html button.other-user:hover .other-user-image, html .button.other-user:hover .other-user-image { background-image: url("../../../images/icon-newuser-light-hover@2x.png"); background-size: 80px 60px; } @@ -532,4 +560,41 @@ html footer { background-image: url("../gradientdeco-full.svg"); } +form .field.check-box label { + color: #282828; +} +form ul#passwordcomplexity li i { + color: #50CA3D; +} +form ul#passwordcomplexity li.invalid i { + color: #F20D6B; +} + +.login-profile .profile-image, form button.user-selection .profile-image { + background-image: url("../../../images/icon-user-light.png"); +} +@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { + .login-profile .profile-image, form button.user-selection .profile-image { + background-image: url("../../../images/icon-user-light@2x.png"); + background-size: 80px 80px; + } +} +.login-profile:hover .profile-image, form button.user-selection:hover .profile-image { + background-image: url("../../../images/icon-user-light-hover.png"); +} +@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { + .login-profile:hover .profile-image, form button.user-selection:hover .profile-image { + background-image: url("../../../images/icon-user-light-hover@2x.png"); + background-size: 80px 80px; + } +} + +.free-tier { + border: 2px solid #F20D6B; +} + +.error { + color: #F20D6B; +} + /*# sourceMappingURL=light.css.map */ diff --git a/internal/ui/login/static/resources/themes/caos/css/light.css.map b/internal/ui/login/static/resources/themes/caos/css/light.css.map index 3ee84bba40..5bf7e126f4 100644 --- a/internal/ui/login/static/resources/themes/caos/css/light.css.map +++ b/internal/ui/login/static/resources/themes/caos/css/light.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/caos/variables.scss","../../scss/variables.scss","../../scss/light.scss"],"names":[],"mappings":"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","file":"light.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/caos/variables.scss","../../scss/variables.scss","../../scss/light.scss"],"names":[],"mappings":"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","file":"light.css"} \ No newline at end of file diff --git a/internal/ui/login/static/resources/themes/scss/light.scss b/internal/ui/login/static/resources/themes/scss/light.scss index e017c7778c..0b55a06ab6 100644 --- a/internal/ui/login/static/resources/themes/scss/light.scss +++ b/internal/ui/login/static/resources/themes/scss/light.scss @@ -14,19 +14,20 @@ html { color: $fontColorLight; } - button { + button, .button { background-color: $backgroundColorLight; color: $primaryColorLight; - border: 2px solid $primaryColorLight; + border: 1px solid $primaryColorLight; &:hover { background-color: $primaryColorHoverLight; - border: 2px solid $primaryColorHoverLight; + border: 1px solid $primaryColorHoverLight; + color: $buttonBackgroundColorHoverLight } &.primary { background-color: $primaryColor; - color: $fontColor; + color: $buttonBackgroundColorHoverLight; border: none; box-shadow: 0px 10px 30px $primaryColor; &:hover { @@ -34,6 +35,16 @@ html { } } + &:disabled { + background-color: $inputBorderColor; + border: 1px solid $inputBorderColor; + + &:hover { + background-color: $inputBorderColor; + border: 1px solid $inputBorderColor; + } + } + &.clean { color: $fontColorLight; @@ -90,4 +101,41 @@ html { background-image: url($footerimgLight); } } +} + +form { + .field.check-box label { + color: $fontColorLight; + } + + ul#passwordcomplexity li { + i { + color: $okColorLight; + } + + &.invalid { + i { + color: $nokColorLight; + } + } + } +} + + +%profile-image { + .profile-image { + @include retina-background-image($profileImgLight, "png", false, 80px, 80px); + } + + &:hover .profile-image { + @include retina-background-image($profileImgLight, "png", true, 80px, 80px); + } +} + +.free-tier { + border: 2px solid $nokColorLight; +} + +.error { + color: $nokColorLight; } \ No newline at end of file diff --git a/internal/ui/login/static/resources/themes/scss/main.scss b/internal/ui/login/static/resources/themes/scss/main.scss index 9e8a0a6e1e..1b21a4fba8 100644 --- a/internal/ui/login/static/resources/themes/scss/main.scss +++ b/internal/ui/login/static/resources/themes/scss/main.scss @@ -3,7 +3,7 @@ *, *::before, *::after { box-sizing: border-box; font-family: $standardFont; - font-size: 18px; + font-size: 16px; font-weight: 400; } @@ -30,6 +30,7 @@ h1 { font-family: $headerFont; font-weight: 300; font-size: $headerSize; + text-align: center; } h2 { @@ -56,7 +57,13 @@ header { } } -.content { +.head { + width: 100%; + max-width: 1000px; + margin: auto; +} + +.content form { margin: auto; padding: 20px; width: 100%; @@ -71,12 +78,16 @@ a { &:hover { color: $primaryColorHover; } + + &.tos-link { + font-size: 14px; + } } -button { +button, .button { background-color: $backgroundColor; color: $primaryColor; - border: 2px solid $primaryColor; + border: 1px solid $primaryColor; border-radius: 5px; width: 100%; max-width: 600px; @@ -84,10 +95,14 @@ button { transition: all 0.3s ease 0s; cursor: pointer; outline: none; + display: inline-block; + text-align: center; + line-height: 40px; + &:hover { background-color: $primaryColorHover; color: $backgroundColor; - border: 2px solid $primaryColorHover; + border: 1px solid $primaryColorHover; } &.primary { @@ -101,22 +116,24 @@ button { &:disabled { background-color: $inputBorderColor; - border: 2px solid $inputBorderColor; + border: 1px solid $inputBorderColor; &:hover { background-color: $inputBorderColor; - border: 2px solid $inputBorderColor; + border: 1px solid $inputBorderColor; } } } + + input:not([type='radio']), select { background-color: $inputBackgroundColor; color: $fontColor; height: $inputHeight; - border: 2px solid $inputBorderColor; + border: 1px solid $inputBorderColor; border-radius: 5px; - padding-left: 15px; + padding-left: 8px; } %profile-image { @@ -145,7 +162,7 @@ input:not([type='radio']), select { text-align: center; div:first-of-type { - font-size: $headerSize; + font-size: $header3Size; font-weight: 300; } @@ -184,23 +201,24 @@ form { display: flex; input[type='checkbox'] { - height: 20px; + height: 16px; vertical-align: middle; } & label { - height: 20px; + height: 16px; text-transform: inherit; display: inline-block; padding: 2px 0 0 15px; width: 100%; + color: $fontColor; } } label { color: $labelColor; text-transform: uppercase; - font-size: 0.9rem; + font-size: 0.8rem; margin-bottom: 3px; span.optional { @@ -320,6 +338,7 @@ form { flex-wrap: wrap; padding: 0; list-style: none; + margin-bottom: 0; li { flex: 1 0 50%; @@ -412,7 +431,11 @@ footer { } .free-tier { - border: 2px solid #F20D6B; + border: 2px solid $nokColor; border-radius: 5px; padding: 10px; +} + +.error { + color: $nokColor; } \ No newline at end of file diff --git a/internal/ui/login/static/resources/themes/scss/variables.scss b/internal/ui/login/static/resources/themes/scss/variables.scss index f1e7c2df67..1cbd3c65b5 100644 --- a/internal/ui/login/static/resources/themes/scss/variables.scss +++ b/internal/ui/login/static/resources/themes/scss/variables.scss @@ -6,6 +6,7 @@ $headerFont: Lato; $inputHeight: 50px; $headerSize: 40px; $header2Size: 30px; +$header3Size: 26px; $retina: "only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx)"; @mixin retina-background-image($file, $type, $hover, $width, $height) { @@ -27,13 +28,14 @@ $fontColor: #BBBBC8; $primaryColor: #3574C6; $primaryColorHover: lighten($primaryColor, 10%); $labelColor: #898989; -$inputBorderColor: #505050; +$inputBorderColor: #999999; $inputBackgroundColor: #252525; $buttonBackgroundColorHover: $inputBackgroundColor; $profileImgDark: "../../../images/icon-user-dark"; $otherUserImgDark: "../../../images/icon-newuser-dark"; $nokColor: #F20D6B; $okColor: #0DF279; +$errorColor: red; // ----- LIGHT-THEME -------- @@ -44,4 +46,6 @@ $primaryColorHoverLight: lighten($primaryColorLight, 10%); $inputBackgroundColorLight: #FFFFFF; $buttonBackgroundColorHoverLight: $inputBackgroundColorLight; $profileImgLight: "../../../images/icon-user-light"; -$otherUserImgLight: "../../../images/icon-newuser-light"; \ No newline at end of file +$otherUserImgLight: "../../../images/icon-newuser-light"; +$nokColorLight: #F20D6B; +$okColorLight: #50CA3D; \ No newline at end of file diff --git a/internal/ui/login/static/resources/themes/zitadel/css/dark.css b/internal/ui/login/static/resources/themes/zitadel/css/dark.css index 3e1296f671..037a0e6b6e 100644 --- a/internal/ui/login/static/resources/themes/zitadel/css/dark.css +++ b/internal/ui/login/static/resources/themes/zitadel/css/dark.css @@ -73,7 +73,7 @@ *, *::before, *::after { box-sizing: border-box; font-family: Lato; - font-size: 18px; + font-size: 16px; font-weight: 400; } @@ -98,6 +98,7 @@ h1 { font-family: Lato; font-weight: 300; font-size: 40px; + text-align: center; } h2 { @@ -123,7 +124,13 @@ header .logo { margin: 30px; } -.content { +.head { + width: 100%; + max-width: 1000px; + margin: auto; +} + +.content form { margin: auto; padding: 20px; width: 100%; @@ -138,11 +145,14 @@ a { a:hover { color: #5b8fd3; } +a.tos-link { + font-size: 14px; +} -button { +button, .button { background-color: #282828; color: #3574C6; - border: 2px solid #3574C6; + border: 1px solid #3574C6; border-radius: 5px; width: 100%; max-width: 600px; @@ -150,36 +160,39 @@ button { transition: all 0.3s ease 0s; cursor: pointer; outline: none; + display: inline-block; + text-align: center; + line-height: 40px; } -button:hover { +button:hover, .button:hover { background-color: #5b8fd3; color: #282828; - border: 2px solid #5b8fd3; + border: 1px solid #5b8fd3; } -button.primary { +button.primary, .button.primary { background-color: #3574C6; color: #BBBBC8; border: none; } -button.primary:hover { +button.primary:hover, .button.primary:hover { background-color: #5b8fd3; } -button:disabled { - background-color: #505050; - border: 2px solid #505050; +button:disabled, .button:disabled { + background-color: #999999; + border: 1px solid #999999; } -button:disabled:hover { - background-color: #505050; - border: 2px solid #505050; +button:disabled:hover, .button:disabled:hover { + background-color: #999999; + border: 1px solid #999999; } input:not([type=radio]), select { background-color: #252525; color: #BBBBC8; height: 50px; - border: 2px solid #505050; + border: 1px solid #999999; border-radius: 5px; - padding-left: 15px; + padding-left: 8px; } form button.user-selection .profile-image, .login-profile .profile-image { @@ -214,7 +227,7 @@ form button.user-selection:hover .profile-image, .login-profile:hover .profile-i text-align: center; } .login-profile .names div:first-of-type { - font-size: 40px; + font-size: 26px; font-weight: 300; } .login-profile .names div:nth-of-type(2) { @@ -245,20 +258,21 @@ form .field.check-box { display: flex; } form .field.check-box input[type=checkbox] { - height: 20px; + height: 16px; vertical-align: middle; } form .field.check-box label { - height: 20px; + height: 16px; text-transform: inherit; display: inline-block; padding: 2px 0 0 15px; width: 100%; + color: #BBBBC8; } form label { color: #898989; text-transform: uppercase; - font-size: 0.9rem; + font-size: 0.8rem; margin-bottom: 3px; } form label span.optional { @@ -302,7 +316,7 @@ form button.user-selection .sessionstate { height: 20px; width: 20px; border-radius: 20px; - border-color: #505050; + border-color: #999999; border-style: solid; border-width: 1px; position: absolute; @@ -363,6 +377,7 @@ form ul#passwordcomplexity { flex-wrap: wrap; padding: 0; list-style: none; + margin-bottom: 0; } form ul#passwordcomplexity li { flex: 1 0 50%; @@ -444,4 +459,8 @@ footer { padding: 10px; } +.error { + color: #F20D6B; +} + /*# sourceMappingURL=dark.css.map */ diff --git a/internal/ui/login/static/resources/themes/zitadel/css/dark.css.map b/internal/ui/login/static/resources/themes/zitadel/css/dark.css.map index a3cfe47781..3e41e2309a 100644 --- a/internal/ui/login/static/resources/themes/zitadel/css/dark.css.map +++ b/internal/ui/login/static/resources/themes/zitadel/css/dark.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/variables.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCHW;EDIX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCOc;EDNd,OCOQ;EDNR;EACA;EACA;EAEI;;;AAIR;EACI,OCHQ;EDIR,aC3BS;ED4BT;EACA,WCzBS;;;AD4Bb;EACI,OCVQ;EDWR,aClCS;EDmCT;EACA,WC/BU;;;ADkCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OCxCW;EDyCX;EACA;;AAEA;EACI,OC5CY;;;ADgDpB;EACI,kBCpDc;EDqDd,OCnDW;EDoDX;EACA;EACA;EACA;EACA,QC7EU;ED8EV;EACA;EACA;;AACA;EACI,kBC5DY;ED6DZ,OChEU;EDiEV;;AAGJ;EACI,kBCnEO;EDoEP,OCrEI;EDsEJ;;AACA;EACI,kBCtEQ;;AD0EhB;EACI,kBCzEW;ED0EX;;AAEA;EACI,kBC7EO;ED8EP;;;AAKZ;EACI,kBCnFmB;EDoFnB,OCzFQ;ED0FR,QC9GU;ED+GV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;EC9GN;;AACA;EDyGE;ICxGA;IACA;;;AD+GA;EClHF;;AACA;EDiHE;IChHA;IACA;;;;ADsHA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WC7IC;ED8ID;;AAGJ;EACI;EACA;EACA;EACA,OC/HC;;;ADqIT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAIR;EACI,OC5KK;ED6KL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OCzMI;ED0MJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBC5MW;;AD+Mf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cCrOO;EDsOP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OCjQP;;ADwQL;EACI;;AAEJ;EACI;EACA;EACA;EACA;EC3RV;;AACA;EDsRM;ICrRJ;IACA;;;AD6RQ;EACI;EACA;EClSd;;AACA;ED+RU;IC9RR;IACA;;;ADoSI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OC3SN;;ADgTE;EACI,OClTL;;ADuTP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MClVI;;ADqVR;EACI,MCvVU;;;AD4Vd;EACI;EACA;;;AAIR;EAII;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA","file":"dark.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/variables.scss"],"names":[],"mappings":"AACA;EACI;EACA;;AAIJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AAIJ;EACI;EACA;EACA;EACA;AAA6D;EAC7D;;AC5EJ;EACI;EACA,aCHW;EDIX;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA,kBCQc;EDPd,OCQQ;EDPR;EACA;EACA;EAEI;;;AAIR;EACI,OCFQ;EDGR,aC3BS;ED4BT;EACA,WCzBS;ED0BT;;;AAGJ;EACI,OCVQ;EDWR,aCnCS;EDoCT;EACA,WChCU;;;ADmCd;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;;AAIR;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI,OC9CW;ED+CX;EACA;;AAEA;EACI,OClDY;;ADqDhB;EACI;;;AAIR;EACI,kBC9Dc;ED+Dd,OC7DW;ED8DX;EACA;EACA;EACA;EACA,QCxFU;EDyFV;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI,kBC1EY;ED2EZ,OC9EU;ED+EV;;AAGJ;EACI,kBCjFO;EDkFP,OCnFI;EDoFJ;;AACA;EACI,kBCpFQ;;ADwFhB;EACI,kBCvFW;EDwFX;;AAEA;EACI,kBC3FO;ED4FP;;;AAOZ;EACI,kBCnGmB;EDoGnB,OCzGQ;ED0GR,QC/HU;EDgIV;EACA;EACA;;;AAIA;EACI;EACA;EACA;EACA;EC9HN;;AACA;EDyHE;ICxHA;IACA;;;AD+HA;EClIF;;AACA;EDiIE;IChIA;IACA;;;;ADsIA;EACI;EACA;;AAGJ;EACI;EACA;;AAEA;EACI,WC5JE;ED6JF;;AAGJ;EACI;EACA;EACA;EACA,OC/IC;;;ADqJT;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA,OC3LA;;AD+LR;EACI,OC7LK;ED8LL;EACA;EACA;;AAEA;EACI;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;;AAIR;EACI;EACA;EACA,OC1NI;ED2NJ;EACA;EACA;EACA;;AAEA;EACI;EACA,kBC7NW;;ADgOf;EACI;;AAIR;EACI;;AAKA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA,cCtPO;EDuPP;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAKR;EACI;;AAEA;EACI;;AAEA;EACI;;AAEJ;EACI,OClRP;;ADyRL;EACI;;AAEJ;EACI;EACA;EACA;EACA;EC5SV;;AACA;EDuSM;ICtSJ;IACA;;;AD8SQ;EACI;EACA;ECnTd;;AACA;EDgTU;IC/SR;IACA;;;ADqTI;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA,OC7TN;;ADkUE;EACI,OCpUL;;ADyUP;EACI;;AACA;EACI;EACA;;;AAKZ;EACI;EACA;;;AAGJ;EACI;;AAEA;EACI,MCpWI;;ADuWR;EACI,MCzWU;;;AD8Wd;EACI;EACA;;;AAIR;EAII;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;AAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;AACA;EACA;AAEA;EACA;AAEA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI,OCpZO","file":"dark.css"} \ No newline at end of file diff --git a/internal/ui/login/static/resources/themes/zitadel/css/light.css b/internal/ui/login/static/resources/themes/zitadel/css/light.css index db877bbd37..3b6763a38d 100644 --- a/internal/ui/login/static/resources/themes/zitadel/css/light.css +++ b/internal/ui/login/static/resources/themes/zitadel/css/light.css @@ -73,7 +73,7 @@ *, *::before, *::after { box-sizing: border-box; font-family: Lato; - font-size: 18px; + font-size: 16px; font-weight: 400; } @@ -98,6 +98,7 @@ h1 { font-family: Lato; font-weight: 300; font-size: 40px; + text-align: center; } h2 { @@ -123,7 +124,13 @@ header .logo { margin: 30px; } -.content { +.head { + width: 100%; + max-width: 1000px; + margin: auto; +} + +.content form { margin: auto; padding: 20px; width: 100%; @@ -138,11 +145,14 @@ a { a:hover { color: #5b8fd3; } +a.tos-link { + font-size: 14px; +} -button { +button, .button { background-color: #282828; color: #3574C6; - border: 2px solid #3574C6; + border: 1px solid #3574C6; border-radius: 5px; width: 100%; max-width: 600px; @@ -150,36 +160,39 @@ button { transition: all 0.3s ease 0s; cursor: pointer; outline: none; + display: inline-block; + text-align: center; + line-height: 40px; } -button:hover { +button:hover, .button:hover { background-color: #5b8fd3; color: #282828; - border: 2px solid #5b8fd3; + border: 1px solid #5b8fd3; } -button.primary { +button.primary, .button.primary { background-color: #3574C6; color: #BBBBC8; border: none; } -button.primary:hover { +button.primary:hover, .button.primary:hover { background-color: #5b8fd3; } -button:disabled { - background-color: #505050; - border: 2px solid #505050; +button:disabled, .button:disabled { + background-color: #999999; + border: 1px solid #999999; } -button:disabled:hover { - background-color: #505050; - border: 2px solid #505050; +button:disabled:hover, .button:disabled:hover { + background-color: #999999; + border: 1px solid #999999; } input:not([type=radio]), select { background-color: #252525; color: #BBBBC8; height: 50px; - border: 2px solid #505050; + border: 1px solid #999999; border-radius: 5px; - padding-left: 15px; + padding-left: 8px; } form button.user-selection .profile-image, .login-profile .profile-image { @@ -214,7 +227,7 @@ form button.user-selection:hover .profile-image, .login-profile:hover .profile-i text-align: center; } .login-profile .names div:first-of-type { - font-size: 40px; + font-size: 26px; font-weight: 300; } .login-profile .names div:nth-of-type(2) { @@ -245,20 +258,21 @@ form .field.check-box { display: flex; } form .field.check-box input[type=checkbox] { - height: 20px; + height: 16px; vertical-align: middle; } form .field.check-box label { - height: 20px; + height: 16px; text-transform: inherit; display: inline-block; padding: 2px 0 0 15px; width: 100%; + color: #BBBBC8; } form label { color: #898989; text-transform: uppercase; - font-size: 0.9rem; + font-size: 0.8rem; margin-bottom: 3px; } form label span.optional { @@ -302,7 +316,7 @@ form button.user-selection .sessionstate { height: 20px; width: 20px; border-radius: 20px; - border-color: #505050; + border-color: #999999; border-style: solid; border-width: 1px; position: absolute; @@ -363,6 +377,7 @@ form ul#passwordcomplexity { flex-wrap: wrap; padding: 0; list-style: none; + margin-bottom: 0; } form ul#passwordcomplexity li { flex: 1 0 50%; @@ -444,6 +459,10 @@ footer { padding: 10px; } +.error { + color: #F20D6B; +} + html { background-color: #f5f5f5; color: #282828; @@ -455,66 +474,75 @@ html header .logo { html h1, html h2 { color: #282828; } -html button { +html button, html .button { background-color: #f5f5f5; color: #3574C6; - border: 2px solid #3574C6; + border: 1px solid #3574C6; } -html button:hover { +html button:hover, html .button:hover { background-color: #5b8fd3; - border: 2px solid #5b8fd3; + border: 1px solid #5b8fd3; + color: #FFFFFF; } -html button.primary { +html button.primary, html .button.primary { background-color: #3574C6; - color: #BBBBC8; + color: #FFFFFF; border: none; box-shadow: 0px 10px 30px #3574C6; } -html button.primary:hover { +html button.primary:hover, html .button.primary:hover { background-color: #5b8fd3; } -html button.clean { +html button:disabled, html .button:disabled { + background-color: #999999; + border: 1px solid #999999; +} +html button:disabled:hover, html .button:disabled:hover { + background-color: #999999; + border: 1px solid #999999; +} +html button.clean, html .button.clean { color: #282828; } -html button.clean:hover { +html button.clean:hover, html .button.clean:hover { border: none; background-color: #FFFFFF; } -html button.user-selection .profile-image { +html button.user-selection .profile-image, html .button.user-selection .profile-image { background-image: url("../../../images/icon-user-light.png"); } @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { - html button.user-selection .profile-image { + html button.user-selection .profile-image, html .button.user-selection .profile-image { background-image: url("../../../images/icon-user-light@2x.png"); background-size: 80px 80px; } } -html button.user-selection:hover { +html button.user-selection:hover, html .button.user-selection:hover { background-color: #FFFFFF; } -html button.user-selection:hover .profile-image { +html button.user-selection:hover .profile-image, html .button.user-selection:hover .profile-image { background-image: url("../../../images/icon-user-light-hover.png"); } @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { - html button.user-selection:hover .profile-image { + html button.user-selection:hover .profile-image, html .button.user-selection:hover .profile-image { background-image: url("../../../images/icon-user-light-hover@2x.png"); background-size: 80px 80px; } } -html button.other-user .other-user-image { +html button.other-user .other-user-image, html .button.other-user .other-user-image { background-image: url("../../../images/icon-newuser-light.png"); } @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { - html button.other-user .other-user-image { + html button.other-user .other-user-image, html .button.other-user .other-user-image { background-image: url("../../../images/icon-newuser-light@2x.png"); background-size: 80px 60px; } } -html button.other-user:hover .other-user-image { +html button.other-user:hover .other-user-image, html .button.other-user:hover .other-user-image { background-image: url("../../../images/icon-newuser-light-hover.png"); } @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { - html button.other-user:hover .other-user-image { + html button.other-user:hover .other-user-image, html .button.other-user:hover .other-user-image { background-image: url("../../../images/icon-newuser-light-hover@2x.png"); background-size: 80px 60px; } @@ -529,5 +557,41 @@ html #qrcode svg rect.color { html #qrcode svg rect.bg-color { fill: #f5f5f5; } +form .field.check-box label { + color: #282828; +} +form ul#passwordcomplexity li i { + color: #50CA3D; +} +form ul#passwordcomplexity li.invalid i { + color: #F20D6B; +} + +.login-profile .profile-image, form button.user-selection .profile-image { + background-image: url("../../../images/icon-user-light.png"); +} +@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { + .login-profile .profile-image, form button.user-selection .profile-image { + background-image: url("../../../images/icon-user-light@2x.png"); + background-size: 80px 80px; + } +} +.login-profile:hover .profile-image, form button.user-selection:hover .profile-image { + background-image: url("../../../images/icon-user-light-hover.png"); +} +@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { + .login-profile:hover .profile-image, form button.user-selection:hover .profile-image { + background-image: url("../../../images/icon-user-light-hover@2x.png"); + background-size: 80px 80px; + } +} + +.free-tier { + border: 2px solid #F20D6B; +} + +.error { + color: #F20D6B; +} /*# sourceMappingURL=light.css.map */ diff --git a/internal/ui/login/static/resources/themes/zitadel/css/light.css.map b/internal/ui/login/static/resources/themes/zitadel/css/light.css.map index 7f5b22b3c7..ddbb08b558 100644 --- a/internal/ui/login/static/resources/themes/zitadel/css/light.css.map +++ b/internal/ui/login/static/resources/themes/zitadel/css/light.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/variables.scss","../../scss/light.scss"],"names":[],"mappings":"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","file":"light.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../../scss/fonts.scss","../../scss/main.scss","../../scss/variables.scss","../../scss/light.scss"],"names":[],"mappings":"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","file":"light.css"} \ No newline at end of file diff --git a/internal/ui/login/static/templates/change_password.html b/internal/ui/login/static/templates/change_password.html index 03316276e7..5bb0d8c0a8 100644 --- a/internal/ui/login/static/templates/change_password.html +++ b/internal/ui/login/static/templates/change_password.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "PasswordChange.Description"}}

+

{{t "PasswordChange.Description"}}

+
@@ -42,8 +44,8 @@
diff --git a/internal/ui/login/static/templates/change_password_done.html b/internal/ui/login/static/templates/change_password_done.html index 2e06b42630..c450468c5d 100644 --- a/internal/ui/login/static/templates/change_password_done.html +++ b/internal/ui/login/static/templates/change_password_done.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "PasswordChangeDone.Description"}}

+

{{t "PasswordChangeDone.Description"}}

+
@@ -13,8 +15,8 @@
diff --git a/internal/ui/login/static/templates/change_username.html b/internal/ui/login/static/templates/change_username.html index 06b7aa7214..9dd7cfa136 100644 --- a/internal/ui/login/static/templates/change_username.html +++ b/internal/ui/login/static/templates/change_username.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "UsernameChange.Description"}}

+

{{t "UsernameChange.Description"}}

+
@@ -21,8 +23,8 @@
diff --git a/internal/ui/login/static/templates/change_username_done.html b/internal/ui/login/static/templates/change_username_done.html index b75d8f0d5a..dcd5d89613 100644 --- a/internal/ui/login/static/templates/change_username_done.html +++ b/internal/ui/login/static/templates/change_username_done.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "UsernameChangeDone.Description"}}

+

{{t "UsernameChangeDone.Description"}}

+
diff --git a/internal/ui/login/static/templates/external_not_found_option.html b/internal/ui/login/static/templates/external_not_found_option.html new file mode 100644 index 0000000000..a56525c689 --- /dev/null +++ b/internal/ui/login/static/templates/external_not_found_option.html @@ -0,0 +1,31 @@ + +{{template "main-top" .}} + +
+

{{t "ExternalNotFoundOption.Title"}}

+

{{t "ExternalNotFoundOption.Description"}}

+
+ + + + {{ .CSRF }} + + + +
+ + + + {{t "Actions.Back"}} + +
+ + {{template "error-message" .}} +
+ + + + + +{{template "main-bottom" .}} + \ No newline at end of file diff --git a/internal/ui/login/static/templates/init_password.html b/internal/ui/login/static/templates/init_password.html index b25505330d..446bca767c 100644 --- a/internal/ui/login/static/templates/init_password.html +++ b/internal/ui/login/static/templates/init_password.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "InitPassword.Description" }}

+

{{t "InitPassword.Description" }}

+
@@ -47,8 +49,8 @@
- - + + {{t "Actions.Cancel"}}
diff --git a/internal/ui/login/static/templates/init_password_done.html b/internal/ui/login/static/templates/init_password_done.html index d47891313a..05d4b14ca2 100644 --- a/internal/ui/login/static/templates/init_password_done.html +++ b/internal/ui/login/static/templates/init_password_done.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "PasswordSetDone.Description"}}

+

{{t "PasswordSetDone.Description"}}

+
@@ -12,8 +14,8 @@
diff --git a/internal/ui/login/static/templates/init_user.html b/internal/ui/login/static/templates/init_user.html index 6d20f81d37..09c919c725 100644 --- a/internal/ui/login/static/templates/init_user.html +++ b/internal/ui/login/static/templates/init_user.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "InitUser.Description" }}

+

{{t "InitUser.Description" }}

+
@@ -49,8 +51,8 @@ value="false" class="primary right">{{t "Actions.Next"}} - - + + {{t "Actions.Cancel"}}
diff --git a/internal/ui/login/static/templates/init_user_done.html b/internal/ui/login/static/templates/init_user_done.html index d44ea84d81..4b7e2527ec 100644 --- a/internal/ui/login/static/templates/init_user_done.html +++ b/internal/ui/login/static/templates/init_user_done.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "InitUserDone.Description"}}

+

{{t "InitUserDone.Description"}}

+
@@ -12,8 +14,8 @@
diff --git a/internal/ui/login/static/templates/link_users_done.html b/internal/ui/login/static/templates/link_users_done.html new file mode 100644 index 0000000000..6101d08422 --- /dev/null +++ b/internal/ui/login/static/templates/link_users_done.html @@ -0,0 +1,25 @@ +{{template "main-top" .}} + +
+ {{ template "user-profile" . }} + +

{{t "LinkingUsersDone.Description"}}

+
+ +
+ + {{ .CSRF }} + + + +
+ + + {{t "Actions.Cancel"}} + +
+
+ + +{{template "main-bottom" .}} + \ No newline at end of file diff --git a/internal/ui/login/static/templates/login.html b/internal/ui/login/static/templates/login.html index d2743d76ba..ab28a8eb77 100644 --- a/internal/ui/login/static/templates/login.html +++ b/internal/ui/login/static/templates/login.html @@ -1,8 +1,16 @@ {{template "main-top" .}} -

{{t "Login.Title"}}

-

{{t "Login.Description"}}

+
+ {{if .Linking}} +

{{t "Login.TitleLinking"}}

+

{{t "Login.DescriptionLinking"}}

+ {{else}} +

{{t "Login.Title"}}

+

{{t "Login.Description"}}

+ {{end}} +
+
@@ -10,19 +18,36 @@ + {{if .LoginPolicy.AllowUsernamePassword }}
+ {{end}} {{template "error-message" .}}
- - + + {{if .LoginPolicy.AllowRegister}} + + {{end}}
+ + {{if .LoginPolicy.AllowExternalIDP}} +
+

{{t "Login.ExternalLogin"}}

+ + {{ $reqid := .AuthReqID}} + {{range $provider := .IDPProviders}} + + {{$provider.Name}} + + {{end}} +
+ {{end}}
diff --git a/internal/ui/login/static/templates/logout_done.html b/internal/ui/login/static/templates/logout_done.html index 0519a42284..4c492356e2 100644 --- a/internal/ui/login/static/templates/logout_done.html +++ b/internal/ui/login/static/templates/logout_done.html @@ -1,8 +1,9 @@ {{template "main-top" .}} - -

{{t "LogoutDone.Title"}}

-

{{t "LogoutDone.Description"}}

+
+

{{t "LogoutDone.Title"}}

+

{{t "LogoutDone.Description"}}

+
{{ .CSRF }} diff --git a/internal/ui/login/static/templates/mail_verification.html b/internal/ui/login/static/templates/mail_verification.html index dcc1faf81a..916aab69e9 100644 --- a/internal/ui/login/static/templates/mail_verification.html +++ b/internal/ui/login/static/templates/mail_verification.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "EmailVerification.Description"}}

+

{{t "EmailVerification.Description"}}

+
@@ -25,8 +27,8 @@ {{ if .UserID }} {{ end }} - - + + {{t "Actions.Cancel"}}
diff --git a/internal/ui/login/static/templates/mail_verified.html b/internal/ui/login/static/templates/mail_verified.html index a90c705b44..7aa54d49cb 100644 --- a/internal/ui/login/static/templates/mail_verified.html +++ b/internal/ui/login/static/templates/mail_verified.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "EmailVerificationDone.Description"}}

+

{{t "EmailVerificationDone.Description"}}

+
@@ -13,8 +15,8 @@
- - + + {{t "Actions.Cancel"}}
diff --git a/internal/ui/login/static/templates/mfa_init_done.html b/internal/ui/login/static/templates/mfa_init_done.html index f78fc564c3..0a233c35ba 100644 --- a/internal/ui/login/static/templates/mfa_init_done.html +++ b/internal/ui/login/static/templates/mfa_init_done.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "MfaInitDone.Description"}}

+

{{t "MfaInitDone.Description"}}

+
@@ -13,8 +15,8 @@
diff --git a/internal/ui/login/static/templates/mfa_init_verify.html b/internal/ui/login/static/templates/mfa_init_verify.html index 2c05d015ba..ab70e39fe6 100644 --- a/internal/ui/login/static/templates/mfa_init_verify.html +++ b/internal/ui/login/static/templates/mfa_init_verify.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "MfaInitVerify.Description"}}

+

{{t "MfaInitVerify.Description"}}

+
@@ -35,11 +37,11 @@
diff --git a/internal/ui/login/static/templates/mfa_prompt.html b/internal/ui/login/static/templates/mfa_prompt.html index 6f5515702b..cf9dac3b68 100644 --- a/internal/ui/login/static/templates/mfa_prompt.html +++ b/internal/ui/login/static/templates/mfa_prompt.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "MfaPrompt.Description"}}

+

{{t "MfaPrompt.Description"}}

+
@@ -25,8 +27,8 @@ {{if not .MfaRequired}} {{end}} - - + + {{t "Actions.Cancel"}}
diff --git a/internal/ui/login/static/templates/mfa_verify.html b/internal/ui/login/static/templates/mfa_verify.html index c905d86479..eae77ba47e 100644 --- a/internal/ui/login/static/templates/mfa_verify.html +++ b/internal/ui/login/static/templates/mfa_verify.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "MfaVerify.Description"}}

+

{{t "MfaVerify.Description"}}

+
@@ -22,8 +24,8 @@
diff --git a/internal/ui/login/static/templates/password.html b/internal/ui/login/static/templates/password.html index 0cdaf65b61..511da72dec 100644 --- a/internal/ui/login/static/templates/password.html +++ b/internal/ui/login/static/templates/password.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "Password.Description"}}

+

{{t "Password.Description"}}

+
@@ -25,8 +27,8 @@ - - + + {{t "Actions.ForgotPassword"}}
diff --git a/internal/ui/login/static/templates/password_reset_done.html b/internal/ui/login/static/templates/password_reset_done.html index ba93b24852..55a3cc9b57 100644 --- a/internal/ui/login/static/templates/password_reset_done.html +++ b/internal/ui/login/static/templates/password_reset_done.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -{{ template "user-profile" . }} +
+ {{ template "user-profile" . }} -

{{t "PasswordResetDone.Description"}}

+

{{t "PasswordResetDone.Description"}}

+
diff --git a/internal/ui/login/static/templates/register.html b/internal/ui/login/static/templates/register.html index f96606e861..2a5d987321 100644 --- a/internal/ui/login/static/templates/register.html +++ b/internal/ui/login/static/templates/register.html @@ -1,7 +1,10 @@ {{template "main-top" .}} -

{{t "Registration.Title"}}

-

{{t "Registration.Description"}}

+
+

{{t "Registration.Title"}}

+

{{t "Registration.Description"}}

+
+ @@ -62,7 +65,7 @@ @@ -72,8 +75,8 @@
diff --git a/internal/ui/login/static/templates/register_option.html b/internal/ui/login/static/templates/register_option.html new file mode 100644 index 0000000000..6e5d8e889a --- /dev/null +++ b/internal/ui/login/static/templates/register_option.html @@ -0,0 +1,40 @@ + +{{template "main-top" .}} + +
+

{{t "RegisterOption.Title"}}

+

{{t "RegisterOption.Description"}}

+
+ +
+ + {{ .CSRF }} + + + +
+ {{if .LoginPolicy.AllowUsernamePassword }} + + {{end}} + {{if .LoginPolicy.AllowExternalIDP}} + {{ $reqid := .AuthReqID}} + {{range $provider := .IDPProviders}} + + {{$provider.Name}} + + {{end}} + {{end}} + + {{t "Actions.Back"}} + +
+ + {{template "error-message" .}} +
+ + + + + +{{template "main-bottom" .}} + \ No newline at end of file diff --git a/internal/ui/login/static/templates/register_org.html b/internal/ui/login/static/templates/register_org.html index 128eaaad7f..fc0aeea156 100644 --- a/internal/ui/login/static/templates/register_org.html +++ b/internal/ui/login/static/templates/register_org.html @@ -1,8 +1,10 @@ {{template "main-top" .}} -

{{t "RegistrationOrg.Title"}}

-

{{t "RegistrationOrg.FreeTillEndOfYear"}}

-

{{t "RegistrationOrg.Description"}}

+
+

{{t "RegistrationOrg.Title"}}

+

{{t "RegistrationOrg.FreeTillEndOfYear"}}

+

{{t "RegistrationOrg.Description"}}

+
@@ -61,7 +63,7 @@ diff --git a/internal/ui/login/static/templates/select_user.html b/internal/ui/login/static/templates/select_user.html index ea439da96f..26f7c02ec1 100644 --- a/internal/ui/login/static/templates/select_user.html +++ b/internal/ui/login/static/templates/select_user.html @@ -1,8 +1,15 @@ {{template "main-top" .}} +
+ {{if .Linking}} +

{{t "UserSelection.TitleLinking"}}

+

{{t "UserSelection.DescriptionLinking"}}

+ {{else}} +

{{t "UserSelection.Title"}}

+

{{t "UserSelection.Description"}}

+ {{end}} +
-

{{t "UserSelection.Title"}}

-

{{t "UserSelection.Description"}}

diff --git a/internal/ui/login/static/templates/user_profile.html b/internal/ui/login/static/templates/user_profile.html index 7728af0d4c..4cb5641487 100644 --- a/internal/ui/login/static/templates/user_profile.html +++ b/internal/ui/login/static/templates/user_profile.html @@ -1,13 +1,13 @@ {{define "user-profile"}} -{{if .LoginName}} -