diff --git a/go.mod b/go.mod index e388abc2a6..8a1d1c0e1a 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/sony/sonyflake v1.1.0 github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.15.0 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.2 github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 github.com/zitadel/logging v0.3.4 diff --git a/go.sum b/go.sum index e2f474e886..30b15fcc10 100644 --- a/go.sum +++ b/go.sum @@ -1087,8 +1087,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= diff --git a/internal/api/grpc/admin/idp_converter.go b/internal/api/grpc/admin/idp_converter.go index 0cb4ae499d..7aeb7ec332 100644 --- a/internal/api/grpc/admin/idp_converter.go +++ b/internal/api/grpc/admin/idp_converter.go @@ -407,32 +407,34 @@ func updateGoogleProviderToCommand(req *admin_pb.UpdateGoogleProviderRequest) co func addLDAPProviderToCommand(req *admin_pb.AddLDAPProviderRequest) command.LDAPProvider { return command.LDAPProvider{ - Name: req.Name, - Host: req.Host, - Port: req.Port, - TLS: req.Tls, - BaseDN: req.BaseDn, - UserObjectClass: req.UserObjectClass, - UserUniqueAttribute: req.UserUniqueAttribute, - Admin: req.Admin, - Password: req.Password, - LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes), - IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + Name: req.Name, + Servers: req.Servers, + StartTLS: req.StartTls, + BaseDN: req.BaseDn, + BindDN: req.BindDn, + BindPassword: req.BindPassword, + UserBase: req.UserBase, + UserObjectClasses: req.UserObjectClasses, + UserFilters: req.UserFilters, + Timeout: req.Timeout.AsDuration(), + LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes), + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } func updateLDAPProviderToCommand(req *admin_pb.UpdateLDAPProviderRequest) command.LDAPProvider { return command.LDAPProvider{ - Name: req.Name, - Host: req.Host, - Port: req.Port, - TLS: req.Tls, - BaseDN: req.BaseDn, - UserObjectClass: req.UserObjectClass, - UserUniqueAttribute: req.UserUniqueAttribute, - Admin: req.Admin, - Password: req.Password, - LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes), - IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + Name: req.Name, + Servers: req.Servers, + StartTLS: req.StartTls, + BaseDN: req.BaseDn, + BindDN: req.BindDn, + BindPassword: req.BindPassword, + UserBase: req.UserBase, + UserObjectClasses: req.UserObjectClasses, + UserFilters: req.UserFilters, + Timeout: req.Timeout.AsDuration(), + LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes), + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } diff --git a/internal/api/grpc/idp/converter.go b/internal/api/grpc/idp/converter.go index 4ff09dec94..c3980bd035 100644 --- a/internal/api/grpc/idp/converter.go +++ b/internal/api/grpc/idp/converter.go @@ -1,6 +1,8 @@ package idp import ( + "google.golang.org/protobuf/types/known/durationpb" + obj_grpc "github.com/zitadel/zitadel/internal/api/grpc/object" "github.com/zitadel/zitadel/internal/domain" iam_model "github.com/zitadel/zitadel/internal/iam/model" @@ -582,16 +584,21 @@ func googleConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.Goo } func ldapConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.LDAPIDPTemplate) { + var timeout *durationpb.Duration + if template.Timeout != 0 { + timeout = durationpb.New(template.Timeout) + } providerConfig.Config = &idp_pb.ProviderConfig_Ldap{ Ldap: &idp_pb.LDAPConfig{ - Host: template.Host, - Port: template.Port, - Tls: template.TLS, - BaseDn: template.BaseDN, - UserObjectClass: template.UserObjectClass, - UserUniqueAttribute: template.UserUniqueAttribute, - Admin: template.Admin, - Attributes: ldapAttributesToPb(template.LDAPAttributes), + Servers: template.Servers, + StartTls: template.StartTLS, + BaseDn: template.BaseDN, + BindDn: template.BindDN, + UserBase: template.UserBase, + UserObjectClasses: template.UserObjectClasses, + UserFilters: template.UserFilters, + Timeout: timeout, + Attributes: ldapAttributesToPb(template.LDAPAttributes), }, } } diff --git a/internal/api/grpc/management/idp_converter.go b/internal/api/grpc/management/idp_converter.go index ad78492668..70bcc5dd08 100644 --- a/internal/api/grpc/management/idp_converter.go +++ b/internal/api/grpc/management/idp_converter.go @@ -422,32 +422,34 @@ func updateGoogleProviderToCommand(req *mgmt_pb.UpdateGoogleProviderRequest) com func addLDAPProviderToCommand(req *mgmt_pb.AddLDAPProviderRequest) command.LDAPProvider { return command.LDAPProvider{ - Name: req.Name, - Host: req.Host, - Port: req.Port, - TLS: req.Tls, - BaseDN: req.BaseDn, - UserObjectClass: req.UserObjectClass, - UserUniqueAttribute: req.UserUniqueAttribute, - Admin: req.Admin, - Password: req.Password, - LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes), - IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + Name: req.Name, + Servers: req.Servers, + StartTLS: req.StartTls, + BaseDN: req.BaseDn, + BindDN: req.BindDn, + BindPassword: req.BindPassword, + UserBase: req.UserBase, + UserObjectClasses: req.UserObjectClasses, + UserFilters: req.UserFilters, + Timeout: req.Timeout.AsDuration(), + LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes), + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } func updateLDAPProviderToCommand(req *mgmt_pb.UpdateLDAPProviderRequest) command.LDAPProvider { return command.LDAPProvider{ - Name: req.Name, - Host: req.Host, - Port: req.Port, - TLS: req.Tls, - BaseDN: req.BaseDn, - UserObjectClass: req.UserObjectClass, - UserUniqueAttribute: req.UserUniqueAttribute, - Admin: req.Admin, - Password: req.Password, - LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes), - IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), + Name: req.Name, + Servers: req.Servers, + StartTLS: req.StartTls, + BaseDN: req.BaseDn, + BindDN: req.BindDn, + BindPassword: req.BindPassword, + UserBase: req.UserBase, + UserObjectClasses: req.UserObjectClasses, + UserFilters: req.UserFilters, + Timeout: req.Timeout.AsDuration(), + LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes), + IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), } } diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index 3886c0f0ad..cac82900ab 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -23,6 +23,7 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/gitlab" "github.com/zitadel/zitadel/internal/idp/providers/google" "github.com/zitadel/zitadel/internal/idp/providers/jwt" + "github.com/zitadel/zitadel/internal/idp/providers/ldap" "github.com/zitadel/zitadel/internal/idp/providers/oauth" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" "github.com/zitadel/zitadel/internal/query" @@ -157,8 +158,9 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai provider, err = l.gitlabSelfHostedProvider(r.Context(), identityProvider) case domain.IDPTypeGoogle: provider, err = l.googleProvider(r.Context(), identityProvider) - case domain.IDPTypeLDAP, - domain.IDPTypeUnspecified: + case domain.IDPTypeLDAP: + provider, err = l.ldapProvider(r.Context(), identityProvider) + case domain.IDPTypeUnspecified: fallthrough default: l.renderLogin(w, r, authReq, errors.ThrowInvalidArgument(nil, "LOGIN-AShek", "Errors.ExternalIDP.IDPTypeNotImplemented")) @@ -604,6 +606,69 @@ func (l *Login) updateExternalUser(ctx context.Context, authReq *domain.AuthRequ return nil } +func (l *Login) ldapProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*ldap.Provider, error) { + password, err := crypto.DecryptString(identityProvider.LDAPIDPTemplate.BindPassword, l.idpConfigAlg) + if err != nil { + return nil, err + } + var opts []ldap.ProviderOpts + if !identityProvider.LDAPIDPTemplate.StartTLS { + opts = append(opts, ldap.WithoutStartTLS()) + } + if identityProvider.LDAPIDPTemplate.LDAPAttributes.IDAttribute != "" { + opts = append(opts, ldap.WithCustomIDAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.IDAttribute)) + } + if identityProvider.LDAPIDPTemplate.LDAPAttributes.FirstNameAttribute != "" { + opts = append(opts, ldap.WithFirstNameAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.FirstNameAttribute)) + } + if identityProvider.LDAPIDPTemplate.LDAPAttributes.LastNameAttribute != "" { + opts = append(opts, ldap.WithLastNameAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.LastNameAttribute)) + } + if identityProvider.LDAPIDPTemplate.LDAPAttributes.DisplayNameAttribute != "" { + opts = append(opts, ldap.WithDisplayNameAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.DisplayNameAttribute)) + } + if identityProvider.LDAPIDPTemplate.LDAPAttributes.NickNameAttribute != "" { + opts = append(opts, ldap.WithNickNameAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.NickNameAttribute)) + } + if identityProvider.LDAPIDPTemplate.LDAPAttributes.PreferredUsernameAttribute != "" { + opts = append(opts, ldap.WithPreferredUsernameAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.PreferredUsernameAttribute)) + } + if identityProvider.LDAPIDPTemplate.LDAPAttributes.EmailAttribute != "" { + opts = append(opts, ldap.WithEmailAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.EmailAttribute)) + } + if identityProvider.LDAPIDPTemplate.LDAPAttributes.EmailVerifiedAttribute != "" { + opts = append(opts, ldap.WithEmailVerifiedAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.EmailVerifiedAttribute)) + } + if identityProvider.LDAPIDPTemplate.LDAPAttributes.PhoneAttribute != "" { + opts = append(opts, ldap.WithPhoneAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.PhoneAttribute)) + } + if identityProvider.LDAPIDPTemplate.LDAPAttributes.PhoneVerifiedAttribute != "" { + opts = append(opts, ldap.WithPhoneVerifiedAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.PhoneVerifiedAttribute)) + } + if identityProvider.LDAPIDPTemplate.LDAPAttributes.PreferredLanguageAttribute != "" { + opts = append(opts, ldap.WithPreferredLanguageAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.PreferredLanguageAttribute)) + } + if identityProvider.LDAPIDPTemplate.LDAPAttributes.AvatarURLAttribute != "" { + opts = append(opts, ldap.WithAvatarURLAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.AvatarURLAttribute)) + } + if identityProvider.LDAPIDPTemplate.LDAPAttributes.ProfileAttribute != "" { + opts = append(opts, ldap.WithProfileAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.ProfileAttribute)) + } + return ldap.New( + identityProvider.Name, + identityProvider.Servers, + identityProvider.BaseDN, + identityProvider.BindDN, + password, + identityProvider.UserBase, + identityProvider.UserObjectClasses, + identityProvider.UserFilters, + identityProvider.Timeout, + l.baseURL(ctx)+EndpointLDAPLogin+"?"+QueryAuthRequestID+"=", + opts..., + ), nil +} + func (l *Login) googleProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*google.Provider, error) { errorHandler := func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) { logging.Errorf("token exchanged failed: %s - %s (state: %s)", errorType, errorType, state) diff --git a/internal/api/ui/login/ldap_handler.go b/internal/api/ui/login/ldap_handler.go new file mode 100644 index 0000000000..1804a4884d --- /dev/null +++ b/internal/api/ui/login/ldap_handler.go @@ -0,0 +1,83 @@ +package login + +import ( + "net/http" + + "github.com/zitadel/logging" + + http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/idp/providers/ldap" +) + +const ( + tmplLDAPLogin = "ldap_login" +) + +type ldapFormData struct { + Username string `schema:"ldapusername"` + Password string `schema:"ldappassword"` + ResetExternalIDP bool `schema:"resetexternalidp"` +} + +func (l *Login) handleLDAP(w http.ResponseWriter, r *http.Request) { + authReq, err := l.getAuthRequest(r) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + l.renderLDAPLogin(w, r, authReq, nil) +} + +func (l *Login) renderLDAPLogin(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) { + var errID, errMessage string + if err != nil { + errID, errMessage = l.getErrorMessage(r, err) + } + temp := l.renderer.Templates[tmplLDAPLogin] + data := l.getUserData(r, authReq, "Login.Title", "Login.Description", errID, errMessage) + l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), temp, data, nil) +} + +func (l *Login) handleLDAPCallback(w http.ResponseWriter, r *http.Request) { + data := new(ldapFormData) + authReq, err := l.getAuthRequestAndParseData(r, data) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + if data.ResetExternalIDP { + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + err := l.authRepo.ResetSelectedIDP(r.Context(), authReq.ID, userAgentID) + if err != nil { + l.renderLDAPLogin(w, r, authReq, err) + return + } + + l.handleLoginName(w, r) + return + } + + identityProvider, err := l.getIDPByID(r, authReq.SelectedIDPConfigID) + if err != nil { + l.renderLDAPLogin(w, r, authReq, err) + return + } + + provider, err := l.ldapProvider(r.Context(), identityProvider) + if err != nil { + l.renderLDAPLogin(w, r, authReq, err) + return + } + session := &ldap.Session{Provider: provider, User: data.Username, Password: data.Password} + + user, err := session.FetchUser(r.Context()) + if err != nil { + if _, actionErr := l.runPostExternalAuthenticationActions(new(domain.ExternalUser), nil, authReq, r, nil, err); actionErr != nil { + logging.WithError(err).Error("both external user authentication and action post authentication failed") + } + l.renderLDAPLogin(w, r, authReq, err) + return + } + l.handleExternalUserAuthenticated(w, r, authReq, identityProvider, session, user, l.renderNextStep) +} diff --git a/internal/api/ui/login/renderer.go b/internal/api/ui/login/renderer.go index e0d42aabd7..b08d452ac2 100644 --- a/internal/api/ui/login/renderer.go +++ b/internal/api/ui/login/renderer.go @@ -76,6 +76,7 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage tmplLinkUsersDone: "link_users_done.html", tmplExternalNotFoundOption: "external_not_found_option.html", tmplLoginSuccess: "login_success.html", + tmplLDAPLogin: "ldap_login.html", } funcs := map[string]interface{}{ "resourceUrl": func(file string) string { @@ -219,6 +220,9 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage "idpProviderClass": func(idpType domain.IDPType) string { return idpType.GetCSSClass() }, + "ldapUrl": func() string { + return path.Join(r.pathPrefix, EndpointLDAPCallback) + }, } var err error r.Renderer, err = renderer.NewRenderer( diff --git a/internal/api/ui/login/router.go b/internal/api/ui/login/router.go index e0f776f241..e723cad1ac 100644 --- a/internal/api/ui/login/router.go +++ b/internal/api/ui/login/router.go @@ -15,6 +15,8 @@ const ( EndpointExternalLoginCallback = "/login/externalidp/callback" EndpointJWTAuthorize = "/login/jwt/authorize" EndpointJWTCallback = "/login/jwt/callback" + EndpointLDAPLogin = "/login/ldap" + EndpointLDAPCallback = "/login/ldap/callback" EndpointPasswordlessLogin = "/login/passwordless" EndpointPasswordlessRegistration = "/login/passwordless/init" EndpointPasswordlessPrompt = "/login/passwordless/prompt" @@ -102,6 +104,8 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrg).Methods(http.MethodGet) router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrgCheck).Methods(http.MethodPost) router.HandleFunc(EndpointLoginSuccess, login.handleLoginSuccess).Methods(http.MethodGet) + router.HandleFunc(EndpointLDAPLogin, login.handleLDAP).Methods(http.MethodGet) + router.HandleFunc(EndpointLDAPCallback, login.handleLDAPCallback).Methods(http.MethodPost) router.SkipClean(true).Handle("", http.RedirectHandler(HandlerPrefix+"/", http.StatusMovedPermanently)) return router } diff --git a/internal/api/ui/login/static/i18n/de.yaml b/internal/api/ui/login/static/i18n/de.yaml index bb463266fb..be2f1e4c95 100644 --- a/internal/api/ui/login/static/i18n/de.yaml +++ b/internal/api/ui/login/static/i18n/de.yaml @@ -11,6 +11,13 @@ Login: RegisterButtonText: registrieren NextButtonText: weiter +LDAP: + Title: Anmeldung + Description: Mit Konto anmelden. + LoginNameLabel: Loginname + PasswordLabel: Passwort + NextButtonText: weiter + SelectAccount: Title: Account auswählen Description: Wähle deinen Account aus. diff --git a/internal/api/ui/login/static/i18n/en.yaml b/internal/api/ui/login/static/i18n/en.yaml index bc980dc97b..2ca0b37d26 100644 --- a/internal/api/ui/login/static/i18n/en.yaml +++ b/internal/api/ui/login/static/i18n/en.yaml @@ -11,6 +11,13 @@ Login: RegisterButtonText: register NextButtonText: next +LDAP: + Title: Login + Description: Enter your login data. + LoginNameLabel: Loginname + PasswordLabel: Password + NextButtonText: next + SelectAccount: Title: Select account Description: Use your ZITADEL-Account diff --git a/internal/api/ui/login/static/i18n/fr.yaml b/internal/api/ui/login/static/i18n/fr.yaml index 75a08ae808..f19a3e0b79 100644 --- a/internal/api/ui/login/static/i18n/fr.yaml +++ b/internal/api/ui/login/static/i18n/fr.yaml @@ -11,6 +11,13 @@ Login: RegisterButtonText: s'inscrire NextButtonText: suivant +LDAP: + Title: Connexion + Description: Entrez vos données de connexion. + LoginNameLabel: Identifiant + PasswordLabel: Mot de passe + NextButtonText: suivant + SelectAccount: Title: Sélectionner un compte Description: Utilisez votre compte ZITADEL diff --git a/internal/api/ui/login/static/i18n/it.yaml b/internal/api/ui/login/static/i18n/it.yaml index 547c4fc8e2..0e72249d9b 100644 --- a/internal/api/ui/login/static/i18n/it.yaml +++ b/internal/api/ui/login/static/i18n/it.yaml @@ -11,6 +11,13 @@ Login: RegisterButtonText: registrare NextButtonText: Avanti +LDAP: + Title: Accesso + Description: Inserisci i tuoi dati di accesso. + LoginNameLabel: Nome di accesso + PasswordLabel: Password + NextButtonText: Avanti + SelectAccount: Title: Seleziona l'account Description: Usa il tuo account ZITADEL diff --git a/internal/api/ui/login/static/i18n/pl.yaml b/internal/api/ui/login/static/i18n/pl.yaml index f6d6577028..e8910efe5f 100644 --- a/internal/api/ui/login/static/i18n/pl.yaml +++ b/internal/api/ui/login/static/i18n/pl.yaml @@ -11,6 +11,13 @@ Login: RegisterButtonText: zarejestruj NextButtonText: dalej +LDAP: + Title: Rejestracja + Description: Wprowadź swoje dane logowania. + LoginNameLabel: Nazwa użytkownika + PasswordLabel: Hasło + NextButtonText: dalej + SelectAccount: Title: Wybierz konto Description: Użyj swojego konta ZITADEL diff --git a/internal/api/ui/login/static/i18n/zh.yaml b/internal/api/ui/login/static/i18n/zh.yaml index b395899127..112c99c2e8 100644 --- a/internal/api/ui/login/static/i18n/zh.yaml +++ b/internal/api/ui/login/static/i18n/zh.yaml @@ -11,6 +11,13 @@ Login: RegisterButtonText: 注册 NextButtonText: 继续 +LDAP: + Title: 注册 + Description: 输入您的登录数据。 + LoginNameLabel: 登录名 + PasswordLabel: 密码 + NextButtonText: 继续 + SelectAccount: Title: 选择账户 Description: 使用您的 ZITADEL 帐户 diff --git a/internal/api/ui/login/static/templates/ldap_login.html b/internal/api/ui/login/static/templates/ldap_login.html new file mode 100644 index 0000000000..850bf27e57 --- /dev/null +++ b/internal/api/ui/login/static/templates/ldap_login.html @@ -0,0 +1,40 @@ +{{template "main-top" .}} + +
+

{{t "LDAP.Title"}}

+

{{t "LDAP.Description"}}

+
+ + +
+ + {{ .CSRF }} + + + +
+ + +
+
+ + +
+ + {{template "error-message" .}} + +
+ + + +
+
+ + + + +{{template "main-bottom" .}} \ No newline at end of file diff --git a/internal/auth/repository/auth_request.go b/internal/auth/repository/auth_request.go index 028feb9973..17f8893655 100644 --- a/internal/auth/repository/auth_request.go +++ b/internal/auth/repository/auth_request.go @@ -34,4 +34,5 @@ type AuthRequestRepository interface { LinkExternalUsers(ctx context.Context, authReqID, userAgentID string, info *domain.BrowserInfo) error AutoRegisterExternalUser(ctx context.Context, user *domain.Human, externalIDP *domain.UserIDPLink, orgMemberRoles []string, authReqID, userAgentID, resourceOwner string, metadatas []*domain.Metadata, info *domain.BrowserInfo) error ResetLinkingUsers(ctx context.Context, authReqID, userAgentID string) error + ResetSelectedIDP(ctx context.Context, authReqID, userAgentID string) error } diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 6d806ef2ab..7c77b61639 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -461,6 +461,15 @@ func (repo *AuthRequestRepo) ResetLinkingUsers(ctx context.Context, authReqID, u return repo.AuthRequests.UpdateAuthRequest(ctx, request) } +func (repo *AuthRequestRepo) ResetSelectedIDP(ctx context.Context, authReqID, userAgentID string) error { + request, err := repo.getAuthRequest(ctx, authReqID, userAgentID) + if err != nil { + return err + } + request.SelectedIDPConfigID = "" + return repo.AuthRequests.UpdateAuthRequest(ctx, request) +} + func (repo *AuthRequestRepo) AutoRegisterExternalUser(ctx context.Context, registerUser *domain.Human, externalIDP *domain.UserIDPLink, orgMemberRoles []string, authReqID, userAgentID, resourceOwner string, metadatas []*domain.Metadata, info *domain.BrowserInfo) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/command/idp.go b/internal/command/idp.go index 7ae7cfd68a..a5605e8a3a 100644 --- a/internal/command/idp.go +++ b/internal/command/idp.go @@ -2,6 +2,7 @@ package command import ( "context" + "time" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" @@ -94,17 +95,18 @@ type GoogleProvider struct { } type LDAPProvider struct { - Name string - Host string - Port string - TLS bool - BaseDN string - UserObjectClass string - UserUniqueAttribute string - Admin string - Password string - LDAPAttributes idp.LDAPAttributes - IDPOptions idp.Options + Name string + Servers []string + StartTLS bool + BaseDN string + BindDN string + BindPassword string + UserBase string + UserObjectClasses []string + UserFilters []string + Timeout time.Duration + LDAPAttributes idp.LDAPAttributes + IDPOptions idp.Options } func ExistsIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id, orgID string) (exists bool, err error) { diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go index 452de29198..2e098fecfd 100644 --- a/internal/command/idp_model.go +++ b/internal/command/idp_model.go @@ -2,6 +2,7 @@ package command import ( "reflect" + "time" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -999,16 +1000,17 @@ func (wm *GoogleIDPWriteModel) NewChanges( type LDAPIDPWriteModel struct { eventstore.WriteModel - ID string - Name string - Host string - Port string - TLS bool - BaseDN string - UserObjectClass string - UserUniqueAttribute string - Admin string - Password *crypto.CryptoValue + ID string + Name string + Servers []string + StartTLS bool + BaseDN string + BindDN string + BindPassword *crypto.CryptoValue + UserBase string + UserObjectClasses []string + UserFilters []string + Timeout time.Duration idp.LDAPAttributes idp.Options @@ -1040,14 +1042,15 @@ func (wm *LDAPIDPWriteModel) Reduce() error { func (wm *LDAPIDPWriteModel) reduceAddedEvent(e *idp.LDAPIDPAddedEvent) { wm.Name = e.Name - wm.Host = e.Host - wm.Port = e.Port - wm.TLS = e.TLS + wm.Servers = e.Servers + wm.StartTLS = e.StartTLS wm.BaseDN = e.BaseDN - wm.UserObjectClass = e.UserObjectClass - wm.UserUniqueAttribute = e.UserUniqueAttribute - wm.Admin = e.Admin - wm.Password = e.Password + wm.BindDN = e.BindDN + wm.BindPassword = e.BindPassword + wm.UserBase = e.UserBase + wm.UserObjectClasses = e.UserObjectClasses + wm.UserFilters = e.UserFilters + wm.Timeout = e.Timeout wm.LDAPAttributes = e.LDAPAttributes wm.Options = e.Options wm.State = domain.IDPStateActive @@ -1060,44 +1063,48 @@ func (wm *LDAPIDPWriteModel) reduceChangedEvent(e *idp.LDAPIDPChangedEvent) { if e.Name != nil { wm.Name = *e.Name } - if e.Host != nil { - wm.Host = *e.Host + if e.Servers != nil { + wm.Servers = e.Servers } - if e.Port != nil { - wm.Port = *e.Port - } - if e.TLS != nil { - wm.TLS = *e.TLS + if e.StartTLS != nil { + wm.StartTLS = *e.StartTLS } if e.BaseDN != nil { wm.BaseDN = *e.BaseDN } - if e.UserObjectClass != nil { - wm.UserObjectClass = *e.UserObjectClass + if e.BindDN != nil { + wm.BindDN = *e.BindDN } - if e.UserUniqueAttribute != nil { - wm.UserUniqueAttribute = *e.UserUniqueAttribute + if e.BindPassword != nil { + wm.BindPassword = e.BindPassword } - if e.Admin != nil { - wm.Admin = *e.Admin + if e.UserBase != nil { + wm.UserBase = *e.UserBase } - if e.Password != nil { - wm.Password = e.Password + if e.UserObjectClasses != nil { + wm.UserObjectClasses = e.UserObjectClasses + } + if e.UserFilters != nil { + wm.UserFilters = e.UserFilters + } + if e.Timeout != nil { + wm.Timeout = *e.Timeout } wm.LDAPAttributes.ReduceChanges(e.LDAPAttributeChanges) wm.Options.ReduceChanges(e.OptionChanges) } func (wm *LDAPIDPWriteModel) NewChanges( - name, - host, - port string, - tls bool, - baseDN, - userObjectClass, - userUniqueAttribute, - admin string, - password string, + name string, + servers []string, + startTLS bool, + baseDN string, + bindDN string, + bindPassword string, + userBase string, + userObjectClasses []string, + userFilters []string, + timeout time.Duration, secretCrypto crypto.Crypto, attributes idp.LDAPAttributes, options idp.Options, @@ -1105,36 +1112,39 @@ func (wm *LDAPIDPWriteModel) NewChanges( changes := make([]idp.LDAPIDPChanges, 0) var cryptedPassword *crypto.CryptoValue var err error - if password != "" { - cryptedPassword, err = crypto.Crypt([]byte(password), secretCrypto) + if bindPassword != "" { + cryptedPassword, err = crypto.Crypt([]byte(bindPassword), secretCrypto) if err != nil { return nil, err } - changes = append(changes, idp.ChangeLDAPPassword(cryptedPassword)) + changes = append(changes, idp.ChangeLDAPBindPassword(cryptedPassword)) } if wm.Name != name { changes = append(changes, idp.ChangeLDAPName(name)) } - if wm.Host != host { - changes = append(changes, idp.ChangeLDAPHost(host)) + if !reflect.DeepEqual(wm.Servers, servers) { + changes = append(changes, idp.ChangeLDAPServers(servers)) } - if wm.Port != port { - changes = append(changes, idp.ChangeLDAPPort(port)) - } - if wm.TLS != tls { - changes = append(changes, idp.ChangeLDAPTLS(tls)) + if wm.StartTLS != startTLS { + changes = append(changes, idp.ChangeLDAPStartTLS(startTLS)) } if wm.BaseDN != baseDN { changes = append(changes, idp.ChangeLDAPBaseDN(baseDN)) } - if wm.UserObjectClass != userObjectClass { - changes = append(changes, idp.ChangeLDAPUserObjectClass(userObjectClass)) + if wm.BindDN != bindDN { + changes = append(changes, idp.ChangeLDAPBindDN(bindDN)) } - if wm.UserUniqueAttribute != userUniqueAttribute { - changes = append(changes, idp.ChangeLDAPUserUniqueAttribute(userUniqueAttribute)) + if wm.UserBase != userBase { + changes = append(changes, idp.ChangeLDAPUserBase(userBase)) } - if wm.Admin != admin { - changes = append(changes, idp.ChangeLDAPAdmin(admin)) + if !reflect.DeepEqual(wm.UserObjectClasses, userObjectClasses) { + changes = append(changes, idp.ChangeLDAPUserObjectClasses(userObjectClasses)) + } + if !reflect.DeepEqual(wm.UserFilters, userFilters) { + changes = append(changes, idp.ChangeLDAPUserFilters(userFilters)) + } + if wm.Timeout != timeout { + changes = append(changes, idp.ChangeLDAPTimeout(timeout)) } attrs := wm.LDAPAttributes.Changes(attributes) if !attrs.IsZero() { diff --git a/internal/command/instance_idp.go b/internal/command/instance_idp.go index e96f172bee..9d2533a12f 100644 --- a/internal/command/instance_idp.go +++ b/internal/command/instance_idp.go @@ -1278,23 +1278,26 @@ func (c *Commands) prepareAddInstanceLDAPProvider(a *instance.Aggregate, writeMo if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SAfdd", "Errors.Invalid.Argument") } - if provider.Host = strings.TrimSpace(provider.Host); provider.Host == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SDVg2", "Errors.Invalid.Argument") - } if provider.BaseDN = strings.TrimSpace(provider.BaseDN); provider.BaseDN == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INST-sv31s", "Errors.Invalid.Argument") } - if provider.UserObjectClass = strings.TrimSpace(provider.UserObjectClass); provider.UserObjectClass == "" { + if provider.BindDN = strings.TrimSpace(provider.BindDN); provider.BindDN == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INST-sdgf4", "Errors.Invalid.Argument") } - if provider.UserUniqueAttribute = strings.TrimSpace(provider.UserUniqueAttribute); provider.UserUniqueAttribute == "" { + if provider.BindPassword = strings.TrimSpace(provider.BindPassword); provider.BindPassword == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INST-AEG2w", "Errors.Invalid.Argument") } - if provider.Admin = strings.TrimSpace(provider.Admin); provider.Admin == "" { + if provider.UserBase = strings.TrimSpace(provider.UserBase); provider.UserBase == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SAD5n", "Errors.Invalid.Argument") } - if provider.Password = strings.TrimSpace(provider.Password); provider.Password == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "INST-sdf5h", "Errors.Invalid.Argument") + if len(provider.Servers) == 0 { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SAx905n", "Errors.Invalid.Argument") + } + if len(provider.UserObjectClasses) == 0 { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-S1x905n", "Errors.Invalid.Argument") + } + if len(provider.UserFilters) == 0 { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-aAx905n", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { events, err := filter(ctx, writeModel.Query()) @@ -1305,7 +1308,7 @@ func (c *Commands) prepareAddInstanceLDAPProvider(a *instance.Aggregate, writeMo if err = writeModel.Reduce(); err != nil { return nil, err } - secret, err := crypto.Encrypt([]byte(provider.Password), c.idpConfigEncryption) + secret, err := crypto.Encrypt([]byte(provider.BindPassword), c.idpConfigEncryption) if err != nil { return nil, err } @@ -1315,14 +1318,15 @@ func (c *Commands) prepareAddInstanceLDAPProvider(a *instance.Aggregate, writeMo &a.Aggregate, writeModel.ID, provider.Name, - provider.Host, - provider.Port, - provider.TLS, + provider.Servers, + provider.StartTLS, provider.BaseDN, - provider.UserObjectClass, - provider.UserUniqueAttribute, - provider.Admin, + provider.BindDN, secret, + provider.UserBase, + provider.UserObjectClasses, + provider.UserFilters, + provider.Timeout, provider.LDAPAttributes, provider.IDPOptions, ), @@ -1339,21 +1343,24 @@ func (c *Commands) prepareUpdateInstanceLDAPProvider(a *instance.Aggregate, writ if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Sffgd", "Errors.Invalid.Argument") } - if provider.Host = strings.TrimSpace(provider.Host); provider.Host == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Dz62d", "Errors.Invalid.Argument") - } if provider.BaseDN = strings.TrimSpace(provider.BaseDN); provider.BaseDN == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INST-vb3ss", "Errors.Invalid.Argument") } - if provider.UserObjectClass = strings.TrimSpace(provider.UserObjectClass); provider.UserObjectClass == "" { + if provider.BindDN = strings.TrimSpace(provider.BindDN); provider.BindDN == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INST-hbere", "Errors.Invalid.Argument") } - if provider.UserUniqueAttribute = strings.TrimSpace(provider.UserUniqueAttribute); provider.UserUniqueAttribute == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "INST-ASFt6", "Errors.Invalid.Argument") - } - if provider.Admin = strings.TrimSpace(provider.Admin); provider.Admin == "" { + if provider.UserBase = strings.TrimSpace(provider.UserBase); provider.UserBase == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "INST-DG45z", "Errors.Invalid.Argument") } + if len(provider.Servers) == 0 { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SAx945n", "Errors.Invalid.Argument") + } + if len(provider.UserObjectClasses) == 0 { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-S1x605n", "Errors.Invalid.Argument") + } + if len(provider.UserFilters) == 0 { + return nil, caos_errs.ThrowInvalidArgument(nil, "INST-aAx901n", "Errors.Invalid.Argument") + } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { events, err := filter(ctx, writeModel.Query()) if err != nil { @@ -1370,16 +1377,16 @@ func (c *Commands) prepareUpdateInstanceLDAPProvider(a *instance.Aggregate, writ ctx, &a.Aggregate, writeModel.ID, - writeModel.Name, provider.Name, - provider.Host, - provider.Port, - provider.TLS, + provider.Servers, + provider.StartTLS, provider.BaseDN, - provider.UserObjectClass, - provider.UserUniqueAttribute, - provider.Admin, - provider.Password, + provider.BindDN, + provider.BindPassword, + provider.UserBase, + provider.UserObjectClasses, + provider.UserFilters, + provider.Timeout, c.idpConfigEncryption, provider.LDAPAttributes, provider.IDPOptions, diff --git a/internal/command/instance_idp_model.go b/internal/command/instance_idp_model.go index f2363e54f1..41c971cba3 100644 --- a/internal/command/instance_idp_model.go +++ b/internal/command/instance_idp_model.go @@ -2,6 +2,7 @@ package command import ( "context" + "time" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" @@ -744,16 +745,16 @@ func (wm *InstanceLDAPIDPWriteModel) NewChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, id, - oldName, - name, - host, - port string, - tls bool, - baseDN, - userObjectClass, - userUniqueAttribute, - admin string, - password string, + name string, + servers []string, + startTLS bool, + baseDN string, + bindDN string, + bindPassword string, + userBase string, + userObjectClasses []string, + userFilters []string, + timeout time.Duration, secretCrypto crypto.Crypto, attributes idp.LDAPAttributes, options idp.Options, @@ -761,14 +762,15 @@ func (wm *InstanceLDAPIDPWriteModel) NewChangedEvent( changes, err := wm.LDAPIDPWriteModel.NewChanges( name, - host, - port, - tls, + servers, + startTLS, baseDN, - userObjectClass, - userUniqueAttribute, - admin, - password, + bindDN, + bindPassword, + userBase, + userObjectClasses, + userFilters, + timeout, secretCrypto, attributes, options, @@ -776,7 +778,7 @@ func (wm *InstanceLDAPIDPWriteModel) NewChangedEvent( if err != nil || len(changes) == 0 { return nil, err } - return instance.NewLDAPIDPChangedEvent(ctx, aggregate, id, oldName, changes) + return instance.NewLDAPIDPChangedEvent(ctx, aggregate, id, changes) } type InstanceIDPRemoveWriteModel struct { diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index e4adfee60e..ed2b5fcaf8 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" @@ -18,7 +19,6 @@ import ( "github.com/zitadel/zitadel/internal/id" id_mock "github.com/zitadel/zitadel/internal/id/mock" "github.com/zitadel/zitadel/internal/repository/idp" - "github.com/zitadel/zitadel/internal/repository/idpconfig" "github.com/zitadel/zitadel/internal/repository/instance" ) @@ -3677,24 +3677,6 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { }, }, }, - { - "invalid host", - fields{ - eventstore: eventstoreExpect(t), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), - }, - args{ - ctx: authz.WithInstanceID(context.Background(), "instance1"), - provider: LDAPProvider{ - Name: "name", - }, - }, - res{ - err: func(err error) bool { - return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-SDVg2", "")) - }, - }, - }, { "invalid baseDN", fields{ @@ -3705,7 +3687,6 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { ctx: authz.WithInstanceID(context.Background(), "instance1"), provider: LDAPProvider{ Name: "name", - Host: "host", }, }, res{ @@ -3715,7 +3696,7 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { }, }, { - "invalid userObjectClass", + "invalid bindDN", fields{ eventstore: eventstoreExpect(t), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), @@ -3724,7 +3705,6 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { ctx: authz.WithInstanceID(context.Background(), "instance1"), provider: LDAPProvider{ Name: "name", - Host: "host", BaseDN: "baseDN", }, }, @@ -3735,7 +3715,7 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { }, }, { - "invalid userUniqueAttribute", + "invalid bindPassword", fields{ eventstore: eventstoreExpect(t), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), @@ -3743,10 +3723,9 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { args{ ctx: authz.WithInstanceID(context.Background(), "instance1"), provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", + Name: "name", + BindDN: "binddn", + BaseDN: "baseDN", }, }, res{ @@ -3756,7 +3735,7 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { }, }, { - "invalid admin", + "invalid userBase", fields{ eventstore: eventstoreExpect(t), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), @@ -3764,11 +3743,10 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { args{ ctx: authz.WithInstanceID(context.Background(), "instance1"), provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - UserUniqueAttribute: "userUniqueAttribute", + Name: "name", + BindDN: "binddn", + BaseDN: "baseDN", + BindPassword: "password", }, }, res{ @@ -3778,7 +3756,7 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { }, }, { - "invalid password", + "invalid servers", fields{ eventstore: eventstoreExpect(t), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), @@ -3786,17 +3764,63 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { args{ ctx: authz.WithInstanceID(context.Background(), "instance1"), provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - UserUniqueAttribute: "userUniqueAttribute", - Admin: "admin", + Name: "name", + BindDN: "binddn", + BaseDN: "baseDN", + BindPassword: "password", + UserBase: "user", }, }, res{ err: func(err error) bool { - return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-sdf5h", "")) + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-SAx905n", "")) + }, + }, + }, + { + "invalid userObjectClasses", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: LDAPProvider{ + Name: "name", + Servers: []string{"server"}, + BindDN: "binddn", + BaseDN: "baseDN", + BindPassword: "password", + UserBase: "user", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-S1x905n", "")) + }, + }, + }, + { + "invalid userFilters", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + provider: LDAPProvider{ + Name: "name", + Servers: []string{"server"}, + BindDN: "binddn", + BaseDN: "baseDN", + BindPassword: "password", + UserBase: "user", + UserObjectClasses: []string{"object"}, + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-aAx905n", "")) }, }, }, @@ -3812,24 +3836,24 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { instance.NewLDAPIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, "id1", "name", - "host", - "", + []string{"server"}, false, "baseDN", - "userObjectClass", - "userUniqueAttribute", - "admin", + "dn", &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("password"), }, + "user", + []string{"object"}, + []string{"filter"}, + time.Second*30, idp.LDAPAttributes{}, idp.Options{}, )), }, - uniqueConstraintsFromEventConstraintWithInstanceID("instance1", idpconfig.NewAddIDPConfigNameUniqueConstraint("name", "instance1")), ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), @@ -3838,13 +3862,16 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { args: args{ ctx: authz.WithInstanceID(context.Background(), "instance1"), provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - UserUniqueAttribute: "userUniqueAttribute", - Admin: "admin", - Password: "password", + Name: "name", + Servers: []string{"server"}, + StartTLS: false, + BaseDN: "baseDN", + BindDN: "dn", + BindPassword: "password", + UserBase: "user", + UserObjectClasses: []string{"object"}, + UserFilters: []string{"filter"}, + Timeout: time.Second * 30, }, }, res: res{ @@ -3864,19 +3891,20 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { instance.NewLDAPIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, "id1", "name", - "host", - "port", - true, + []string{"server"}, + false, "baseDN", - "userObjectClass", - "userUniqueAttribute", - "admin", + "dn", &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("password"), }, + "user", + []string{"object"}, + []string{"filter"}, + time.Second*30, idp.LDAPAttributes{ IDAttribute: "id", FirstNameAttribute: "firstName", @@ -3900,7 +3928,6 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { }, )), }, - uniqueConstraintsFromEventConstraintWithInstanceID("instance1", idpconfig.NewAddIDPConfigNameUniqueConstraint("name", "instance1")), ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), @@ -3909,15 +3936,16 @@ func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) { args: args{ ctx: authz.WithInstanceID(context.Background(), "instance1"), provider: LDAPProvider{ - Name: "name", - Host: "host", - Port: "port", - TLS: true, - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - UserUniqueAttribute: "userUniqueAttribute", - Admin: "admin", - Password: "password", + Name: "name", + Servers: []string{"server"}, + StartTLS: false, + BaseDN: "baseDN", + BindDN: "dn", + BindPassword: "password", + UserBase: "user", + UserObjectClasses: []string{"object"}, + UserFilters: []string{"filter"}, + Timeout: time.Second * 30, LDAPAttributes: idp.LDAPAttributes{ IDAttribute: "id", FirstNameAttribute: "firstName", @@ -4020,24 +4048,6 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { }, }, }, - { - "invalid host", - fields{ - eventstore: eventstoreExpect(t), - }, - args{ - ctx: authz.WithInstanceID(context.Background(), "instance1"), - id: "id1", - provider: LDAPProvider{ - Name: "name", - }, - }, - res{ - err: func(err error) bool { - return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-Dz62d", "")) - }, - }, - }, { "invalid baseDN", fields{ @@ -4048,7 +4058,6 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { id: "id1", provider: LDAPProvider{ Name: "name", - Host: "host", }, }, res{ @@ -4058,7 +4067,7 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { }, }, { - "invalid userObjectClass", + "invalid bindDN", fields{ eventstore: eventstoreExpect(t), }, @@ -4067,7 +4076,6 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { id: "id1", provider: LDAPProvider{ Name: "name", - Host: "host", BaseDN: "baseDN", }, }, @@ -4078,7 +4086,7 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { }, }, { - "invalid userUniqueAttribute", + "invalid userbase", fields{ eventstore: eventstoreExpect(t), }, @@ -4086,32 +4094,9 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { ctx: authz.WithInstanceID(context.Background(), "instance1"), id: "id1", provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - }, - }, - res{ - err: func(err error) bool { - return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-ASFt6", "")) - }, - }, - }, - { - "invalid admin", - fields{ - eventstore: eventstoreExpect(t), - }, - args{ - ctx: authz.WithInstanceID(context.Background(), "instance1"), - id: "id1", - provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - UserUniqueAttribute: "userUniqueAttribute", + Name: "name", + BaseDN: "baseDN", + BindDN: "bindDN", }, }, res{ @@ -4120,6 +4105,72 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { }, }, }, + { + "invalid servers", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: LDAPProvider{ + Name: "name", + BaseDN: "baseDN", + BindDN: "bindDN", + UserBase: "user", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-SAx945n", "")) + }, + }, + }, + { + "invalid userObjectClasses", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: LDAPProvider{ + Name: "name", + Servers: []string{"server"}, + BaseDN: "baseDN", + BindDN: "bindDN", + UserBase: "user", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-S1x605n", "")) + }, + }, + }, + { + "invalid userFilters", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + id: "id1", + provider: LDAPProvider{ + Name: "name", + Servers: []string{"server"}, + BaseDN: "baseDN", + BindDN: "bindDN", + UserBase: "user", + UserObjectClasses: []string{"object"}, + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "INST-aAx901n", "")) + }, + }, + }, { name: "not found", fields: fields{ @@ -4131,16 +4182,20 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { ctx: authz.WithInstanceID(context.Background(), "instance1"), id: "id1", provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - UserUniqueAttribute: "userUniqueAttribute", - Admin: "admin", + Name: "name", + Servers: []string{"server"}, + BaseDN: "baseDN", + BindDN: "binddn", + BindPassword: "password", + UserBase: "user", + UserObjectClasses: []string{"object"}, + UserFilters: []string{"filter"}, }, }, res: res{ - err: caos_errors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowNotFound(nil, "INST-ASF3F", "")) + }, }, }, { @@ -4152,19 +4207,20 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { instance.NewLDAPIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, "id1", "name", - "host", - "", + []string{"server"}, false, - "baseDN", - "userObjectClass", - "userUniqueAttribute", - "admin", + "basedn", + "binddn", &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("password"), }, + "user", + []string{"object"}, + []string{"filter"}, + time.Second*30, idp.LDAPAttributes{}, idp.Options{}, )), @@ -4175,12 +4231,15 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { ctx: authz.WithInstanceID(context.Background(), "instance1"), id: "id1", provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - UserUniqueAttribute: "userUniqueAttribute", - Admin: "admin", + Name: "name", + Servers: []string{"server"}, + StartTLS: false, + BaseDN: "basedn", + BindDN: "binddn", + UserBase: "user", + UserObjectClasses: []string{"object"}, + UserFilters: []string{"filter"}, + Timeout: time.Second * 30, }, }, res: res{ @@ -4196,19 +4255,20 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { instance.NewLDAPIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, "id1", "name", - "host", - "port", + []string{"server"}, false, - "baseDN", - "userObjectClass", - "userUniqueAttribute", - "admin", + "basedn", + "binddn", &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("password"), }, + "user", + []string{"object"}, + []string{"filter"}, + time.Second*30, idp.LDAPAttributes{}, idp.Options{}, )), @@ -4221,22 +4281,22 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { t := true event, _ := instance.NewLDAPIDPChangedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate, "id1", - "name", []idp.LDAPIDPChanges{ idp.ChangeLDAPName("new name"), - idp.ChangeLDAPHost("new host"), - idp.ChangeLDAPPort("new port"), - idp.ChangeLDAPTLS(true), - idp.ChangeLDAPBaseDN("new baseDN"), - idp.ChangeLDAPUserObjectClass("new userObjectClass"), - idp.ChangeLDAPUserUniqueAttribute("new userUniqueAttribute"), - idp.ChangeLDAPAdmin("new admin"), - idp.ChangeLDAPPassword(&crypto.CryptoValue{ + idp.ChangeLDAPServers([]string{"new server"}), + idp.ChangeLDAPStartTLS(true), + idp.ChangeLDAPBaseDN("new basedn"), + idp.ChangeLDAPBindDN("new binddn"), + idp.ChangeLDAPBindPassword(&crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("new password"), }), + idp.ChangeLDAPUserBase("new user"), + idp.ChangeLDAPUserObjectClasses([]string{"new object"}), + idp.ChangeLDAPUserFilters([]string{"new filter"}), + idp.ChangeLDAPTimeout(time.Second * 20), idp.ChangeLDAPAttributes(idp.LDAPAttributeChanges{ IDAttribute: stringPointer("new id"), FirstNameAttribute: stringPointer("new firstName"), @@ -4264,8 +4324,6 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { }(), ), }, - uniqueConstraintsFromEventConstraintWithInstanceID("instance1", idpconfig.NewRemoveIDPConfigNameUniqueConstraint("name", "instance1")), - uniqueConstraintsFromEventConstraintWithInstanceID("instance1", idpconfig.NewAddIDPConfigNameUniqueConstraint("new name", "instance1")), ), ), secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), @@ -4274,15 +4332,16 @@ func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) { ctx: authz.WithInstanceID(context.Background(), "instance1"), id: "id1", provider: LDAPProvider{ - Name: "new name", - Host: "new host", - Port: "new port", - TLS: true, - BaseDN: "new baseDN", - UserObjectClass: "new userObjectClass", - UserUniqueAttribute: "new userUniqueAttribute", - Admin: "new admin", - Password: "new password", + Name: "new name", + Servers: []string{"new server"}, + StartTLS: true, + BaseDN: "new basedn", + BindDN: "new binddn", + BindPassword: "new password", + UserBase: "new user", + UserObjectClasses: []string{"new object"}, + UserFilters: []string{"new filter"}, + Timeout: time.Second * 20, LDAPAttributes: idp.LDAPAttributes{ IDAttribute: "new id", FirstNameAttribute: "new firstName", diff --git a/internal/command/org_idp.go b/internal/command/org_idp.go index d9872be198..e6e22c4473 100644 --- a/internal/command/org_idp.go +++ b/internal/command/org_idp.go @@ -1268,23 +1268,26 @@ func (c *Commands) prepareAddOrgLDAPProvider(a *org.Aggregate, writeModel *OrgLD if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SAfdd", "Errors.Invalid.Argument") } - if provider.Host = strings.TrimSpace(provider.Host); provider.Host == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SDVg2", "Errors.Invalid.Argument") - } if provider.BaseDN = strings.TrimSpace(provider.BaseDN); provider.BaseDN == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-sv31s", "Errors.Invalid.Argument") } - if provider.UserObjectClass = strings.TrimSpace(provider.UserObjectClass); provider.UserObjectClass == "" { + if provider.BindDN = strings.TrimSpace(provider.BindDN); provider.BindDN == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-sdgf4", "Errors.Invalid.Argument") } - if provider.UserUniqueAttribute = strings.TrimSpace(provider.UserUniqueAttribute); provider.UserUniqueAttribute == "" { + if provider.BindPassword = strings.TrimSpace(provider.BindPassword); provider.BindPassword == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-AEG2w", "Errors.Invalid.Argument") } - if provider.Admin = strings.TrimSpace(provider.Admin); provider.Admin == "" { + if provider.UserBase = strings.TrimSpace(provider.UserBase); provider.UserBase == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SAD5n", "Errors.Invalid.Argument") } - if provider.Password = strings.TrimSpace(provider.Password); provider.Password == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-sdf5h", "Errors.Invalid.Argument") + if len(provider.Servers) == 0 { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SAy945n", "Errors.Invalid.Argument") + } + if len(provider.UserObjectClasses) == 0 { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-S1x705n", "Errors.Invalid.Argument") + } + if len(provider.UserFilters) == 0 { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-aAx9x1n", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { events, err := filter(ctx, writeModel.Query()) @@ -1295,7 +1298,7 @@ func (c *Commands) prepareAddOrgLDAPProvider(a *org.Aggregate, writeModel *OrgLD if err = writeModel.Reduce(); err != nil { return nil, err } - secret, err := crypto.Encrypt([]byte(provider.Password), c.idpConfigEncryption) + secret, err := crypto.Encrypt([]byte(provider.BindPassword), c.idpConfigEncryption) if err != nil { return nil, err } @@ -1305,14 +1308,15 @@ func (c *Commands) prepareAddOrgLDAPProvider(a *org.Aggregate, writeModel *OrgLD &a.Aggregate, writeModel.ID, provider.Name, - provider.Host, - provider.Port, - provider.TLS, + provider.Servers, + provider.StartTLS, provider.BaseDN, - provider.UserObjectClass, - provider.UserUniqueAttribute, - provider.Admin, + provider.BindDN, secret, + provider.UserBase, + provider.UserObjectClasses, + provider.UserFilters, + provider.Timeout, provider.LDAPAttributes, provider.IDPOptions, ), @@ -1329,21 +1333,24 @@ func (c *Commands) prepareUpdateOrgLDAPProvider(a *org.Aggregate, writeModel *Or if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Sffgd", "Errors.Invalid.Argument") } - if provider.Host = strings.TrimSpace(provider.Host); provider.Host == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Dz62d", "Errors.Invalid.Argument") - } if provider.BaseDN = strings.TrimSpace(provider.BaseDN); provider.BaseDN == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-vb3ss", "Errors.Invalid.Argument") } - if provider.UserObjectClass = strings.TrimSpace(provider.UserObjectClass); provider.UserObjectClass == "" { + if provider.BindDN = strings.TrimSpace(provider.BindDN); provider.BindDN == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-hbere", "Errors.Invalid.Argument") } - if provider.UserUniqueAttribute = strings.TrimSpace(provider.UserUniqueAttribute); provider.UserUniqueAttribute == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-ASFt6", "Errors.Invalid.Argument") - } - if provider.Admin = strings.TrimSpace(provider.Admin); provider.Admin == "" { + if provider.UserBase = strings.TrimSpace(provider.UserBase); provider.UserBase == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-DG45z", "Errors.Invalid.Argument") } + if len(provider.Servers) == 0 { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Sxx945n", "Errors.Invalid.Argument") + } + if len(provider.UserObjectClasses) == 0 { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-S1p605n", "Errors.Invalid.Argument") + } + if len(provider.UserFilters) == 0 { + return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-aBx901n", "Errors.Invalid.Argument") + } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { events, err := filter(ctx, writeModel.Query()) if err != nil { @@ -1360,16 +1367,16 @@ func (c *Commands) prepareUpdateOrgLDAPProvider(a *org.Aggregate, writeModel *Or ctx, &a.Aggregate, writeModel.ID, - writeModel.Name, provider.Name, - provider.Host, - provider.Port, - provider.TLS, + provider.Servers, + provider.StartTLS, provider.BaseDN, - provider.UserObjectClass, - provider.UserUniqueAttribute, - provider.Admin, - provider.Password, + provider.BindDN, + provider.BindPassword, + provider.UserBase, + provider.UserObjectClasses, + provider.UserFilters, + provider.Timeout, c.idpConfigEncryption, provider.LDAPAttributes, provider.IDPOptions, diff --git a/internal/command/org_idp_model.go b/internal/command/org_idp_model.go index 379a7eac66..ca8011121c 100644 --- a/internal/command/org_idp_model.go +++ b/internal/command/org_idp_model.go @@ -2,6 +2,7 @@ package command import ( "context" + "time" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" @@ -754,16 +755,16 @@ func (wm *OrgLDAPIDPWriteModel) NewChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, id, - oldName, - name, - host, - port string, - tls bool, - baseDN, - userObjectClass, - userUniqueAttribute, - admin string, - password string, + name string, + servers []string, + startTLS bool, + baseDN string, + bindDN string, + bindPassword string, + userBase string, + userObjectClasses []string, + userFilters []string, + timeout time.Duration, secretCrypto crypto.Crypto, attributes idp.LDAPAttributes, options idp.Options, @@ -771,14 +772,15 @@ func (wm *OrgLDAPIDPWriteModel) NewChangedEvent( changes, err := wm.LDAPIDPWriteModel.NewChanges( name, - host, - port, - tls, + servers, + startTLS, baseDN, - userObjectClass, - userUniqueAttribute, - admin, - password, + bindDN, + bindPassword, + userBase, + userObjectClasses, + userFilters, + timeout, secretCrypto, attributes, options, @@ -786,7 +788,7 @@ func (wm *OrgLDAPIDPWriteModel) NewChangedEvent( if err != nil || len(changes) == 0 { return nil, err } - return org.NewLDAPIDPChangedEvent(ctx, aggregate, id, oldName, changes) + return org.NewLDAPIDPChangedEvent(ctx, aggregate, id, changes) } type OrgIDPRemoveWriteModel struct { diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index 83d95025a4..7110cdd1dd 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" @@ -17,7 +18,6 @@ import ( "github.com/zitadel/zitadel/internal/id" id_mock "github.com/zitadel/zitadel/internal/id/mock" "github.com/zitadel/zitadel/internal/repository/idp" - "github.com/zitadel/zitadel/internal/repository/idpconfig" "github.com/zitadel/zitadel/internal/repository/org" ) @@ -3734,25 +3734,6 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { }, }, }, - { - "invalid host", - fields{ - eventstore: eventstoreExpect(t), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), - }, - args{ - ctx: context.Background(), - resourceOwner: "org1", - provider: LDAPProvider{ - Name: "name", - }, - }, - res{ - err: func(err error) bool { - return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-SDVg2", "")) - }, - }, - }, { "invalid baseDN", fields{ @@ -3764,7 +3745,6 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { resourceOwner: "org1", provider: LDAPProvider{ Name: "name", - Host: "host", }, }, res{ @@ -3774,7 +3754,7 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { }, }, { - "invalid userObjectClass", + "invalid binddn", fields{ eventstore: eventstoreExpect(t), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), @@ -3784,7 +3764,6 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { resourceOwner: "org1", provider: LDAPProvider{ Name: "name", - Host: "host", BaseDN: "baseDN", }, }, @@ -3794,51 +3773,6 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { }, }, }, - { - "invalid userUniqueAttribute", - fields{ - eventstore: eventstoreExpect(t), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), - }, - args{ - ctx: context.Background(), - resourceOwner: "org1", - provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - }, - }, - res{ - err: func(err error) bool { - return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-AEG2w", "")) - }, - }, - }, - { - "invalid admin", - fields{ - eventstore: eventstoreExpect(t), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), - }, - args{ - ctx: context.Background(), - resourceOwner: "org1", - provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - UserUniqueAttribute: "userUniqueAttribute", - }, - }, - res{ - err: func(err error) bool { - return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-SAD5n", "")) - }, - }, - }, { "invalid password", fields{ @@ -3849,17 +3783,108 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { ctx: context.Background(), resourceOwner: "org1", provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - UserUniqueAttribute: "userUniqueAttribute", - Admin: "admin", + Name: "name", + BindDN: "binddn", + BaseDN: "baseDN", }, }, res{ err: func(err error) bool { - return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-sdf5h", "")) + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-AEG2w", "")) + }, + }, + }, + { + "invalid userbase", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: LDAPProvider{ + Name: "name", + BindDN: "binddn", + BaseDN: "baseDN", + BindPassword: "password", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-SAD5n", "")) + }, + }, + }, + { + "invalid servers", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: LDAPProvider{ + Name: "name", + BindDN: "binddn", + BaseDN: "baseDN", + BindPassword: "password", + UserBase: "user", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-SAy945n", "")) + }, + }, + }, + { + "invalid userObjectClasses", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: LDAPProvider{ + Name: "name", + Servers: []string{"server"}, + BindDN: "binddn", + BaseDN: "baseDN", + BindPassword: "password", + UserBase: "user", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-S1x705n", "")) + }, + }, + }, + { + "invalid userFilters", + fields{ + eventstore: eventstoreExpect(t), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + provider: LDAPProvider{ + Name: "name", + Servers: []string{"server"}, + BindDN: "binddn", + BaseDN: "baseDN", + BindPassword: "password", + UserBase: "user", + UserObjectClasses: []string{"object"}, + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-aAx9x1n", "")) }, }, }, @@ -3873,23 +3898,23 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { org.NewLDAPIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, "id1", "name", - "host", - "", + []string{"server"}, false, "baseDN", - "userObjectClass", - "userUniqueAttribute", - "admin", + "dn", &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("password"), }, + "user", + []string{"object"}, + []string{"filter"}, + time.Second*30, idp.LDAPAttributes{}, idp.Options{}, )), - uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("name", "org1")), ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), @@ -3899,13 +3924,16 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { ctx: context.Background(), resourceOwner: "org1", provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - UserUniqueAttribute: "userUniqueAttribute", - Admin: "admin", - Password: "password", + Name: "name", + Servers: []string{"server"}, + StartTLS: false, + BaseDN: "baseDN", + BindDN: "dn", + BindPassword: "password", + UserBase: "user", + UserObjectClasses: []string{"object"}, + UserFilters: []string{"filter"}, + Timeout: time.Second * 30, }, }, res: res{ @@ -3923,19 +3951,20 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { org.NewLDAPIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, "id1", "name", - "host", - "port", - true, + []string{"server"}, + false, "baseDN", - "userObjectClass", - "userUniqueAttribute", - "admin", + "dn", &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("password"), }, + "user", + []string{"object"}, + []string{"filter"}, + time.Second*30, idp.LDAPAttributes{ IDAttribute: "id", FirstNameAttribute: "firstName", @@ -3958,7 +3987,6 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { IsAutoUpdate: true, }, )), - uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("name", "org1")), ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"), @@ -3968,15 +3996,16 @@ func TestCommandSide_AddOrgLDAPIDP(t *testing.T) { ctx: context.Background(), resourceOwner: "org1", provider: LDAPProvider{ - Name: "name", - Host: "host", - Port: "port", - TLS: true, - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - UserUniqueAttribute: "userUniqueAttribute", - Admin: "admin", - Password: "password", + Name: "name", + Servers: []string{"server"}, + StartTLS: false, + BaseDN: "baseDN", + BindDN: "dn", + BindPassword: "password", + UserBase: "user", + UserObjectClasses: []string{"object"}, + UserFilters: []string{"filter"}, + Timeout: time.Second * 30, LDAPAttributes: idp.LDAPAttributes{ IDAttribute: "id", FirstNameAttribute: "firstName", @@ -4082,25 +4111,6 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { }, }, }, - { - "invalid host", - fields{ - eventstore: eventstoreExpect(t), - }, - args{ - ctx: context.Background(), - resourceOwner: "org1", - id: "id1", - provider: LDAPProvider{ - Name: "name", - }, - }, - res{ - err: func(err error) bool { - return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-Dz62d", "")) - }, - }, - }, { "invalid baseDN", fields{ @@ -4112,7 +4122,6 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { id: "id1", provider: LDAPProvider{ Name: "name", - Host: "host", }, }, res{ @@ -4122,7 +4131,7 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { }, }, { - "invalid userObjectClass", + "invalid binddn", fields{ eventstore: eventstoreExpect(t), }, @@ -4132,7 +4141,6 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { id: "id1", provider: LDAPProvider{ Name: "name", - Host: "host", BaseDN: "baseDN", }, }, @@ -4143,7 +4151,7 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { }, }, { - "invalid userUniqueAttribute", + "invalid userbase", fields{ eventstore: eventstoreExpect(t), }, @@ -4152,33 +4160,9 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { resourceOwner: "org1", id: "id1", provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - }, - }, - res{ - err: func(err error) bool { - return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-ASFt6", "")) - }, - }, - }, - { - "invalid admin", - fields{ - eventstore: eventstoreExpect(t), - }, - args{ - ctx: context.Background(), - resourceOwner: "org1", - id: "id1", - provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - UserUniqueAttribute: "userUniqueAttribute", + Name: "name", + BaseDN: "baseDN", + BindDN: "bindDN", }, }, res{ @@ -4187,6 +4171,75 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { }, }, }, + { + "invalid servers", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: LDAPProvider{ + Name: "name", + BaseDN: "baseDN", + BindDN: "bindDN", + UserBase: "user", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-Sxx945n", "")) + }, + }, + }, + { + "invalid userObjectClasses", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: LDAPProvider{ + Name: "name", + Servers: []string{"server"}, + BaseDN: "baseDN", + BindDN: "bindDN", + UserBase: "user", + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-S1p605n", "")) + }, + }, + }, + { + "invalid userFilters", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + resourceOwner: "org1", + id: "id1", + provider: LDAPProvider{ + Name: "name", + Servers: []string{"server"}, + BaseDN: "baseDN", + BindDN: "bindDN", + UserBase: "user", + UserObjectClasses: []string{"object"}, + }, + }, + res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "ORG-aBx901n", "")) + }, + }, + }, { name: "not found", fields: fields{ @@ -4199,16 +4252,20 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { resourceOwner: "org1", id: "id1", provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - UserUniqueAttribute: "userUniqueAttribute", - Admin: "admin", + Name: "name", + Servers: []string{"server"}, + BaseDN: "baseDN", + BindDN: "binddn", + BindPassword: "password", + UserBase: "user", + UserObjectClasses: []string{"object"}, + UserFilters: []string{"filter"}, }, }, res: res{ - err: caos_errors.IsNotFound, + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowNotFound(nil, "ORG-ASF3F", "")) + }, }, }, { @@ -4220,19 +4277,20 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { org.NewLDAPIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, "id1", "name", - "host", - "", + []string{"server"}, false, - "baseDN", - "userObjectClass", - "userUniqueAttribute", - "admin", + "basedn", + "binddn", &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("password"), }, + "user", + []string{"object"}, + []string{"filter"}, + time.Second*30, idp.LDAPAttributes{}, idp.Options{}, )), @@ -4244,12 +4302,14 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { resourceOwner: "org1", id: "id1", provider: LDAPProvider{ - Name: "name", - Host: "host", - BaseDN: "baseDN", - UserObjectClass: "userObjectClass", - UserUniqueAttribute: "userUniqueAttribute", - Admin: "admin", + Name: "name", + Servers: []string{"server"}, + BaseDN: "basedn", + BindDN: "binddn", + UserObjectClasses: []string{"object"}, + UserFilters: []string{"filter"}, + UserBase: "user", + Timeout: time.Second * 30, }, }, res: res{ @@ -4265,19 +4325,20 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { org.NewLDAPIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, "id1", "name", - "host", - "port", + []string{"server"}, false, - "baseDN", - "userObjectClass", - "userUniqueAttribute", - "admin", + "basedn", + "binddn", &crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("password"), }, + "user", + []string{"object"}, + []string{"filter"}, + time.Second*30, idp.LDAPAttributes{}, idp.Options{}, )), @@ -4288,22 +4349,22 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { t := true event, _ := org.NewLDAPIDPChangedEvent(context.Background(), &org.NewAggregate("org1").Aggregate, "id1", - "name", []idp.LDAPIDPChanges{ idp.ChangeLDAPName("new name"), - idp.ChangeLDAPHost("new host"), - idp.ChangeLDAPPort("new port"), - idp.ChangeLDAPTLS(true), - idp.ChangeLDAPBaseDN("new baseDN"), - idp.ChangeLDAPUserObjectClass("new userObjectClass"), - idp.ChangeLDAPUserUniqueAttribute("new userUniqueAttribute"), - idp.ChangeLDAPAdmin("new admin"), - idp.ChangeLDAPPassword(&crypto.CryptoValue{ + idp.ChangeLDAPServers([]string{"new server"}), + idp.ChangeLDAPStartTLS(true), + idp.ChangeLDAPBaseDN("new basedn"), + idp.ChangeLDAPBindDN("new binddn"), + idp.ChangeLDAPBindPassword(&crypto.CryptoValue{ CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "id", Crypted: []byte("new password"), }), + idp.ChangeLDAPUserBase("new user"), + idp.ChangeLDAPUserObjectClasses([]string{"new object"}), + idp.ChangeLDAPUserFilters([]string{"new filter"}), + idp.ChangeLDAPTimeout(time.Second * 20), idp.ChangeLDAPAttributes(idp.LDAPAttributeChanges{ IDAttribute: stringPointer("new id"), FirstNameAttribute: stringPointer("new firstName"), @@ -4330,8 +4391,6 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { return event }(), ), - uniqueConstraintsFromEventConstraint(idpconfig.NewRemoveIDPConfigNameUniqueConstraint("name", "org1")), - uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("new name", "org1")), ), ), secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), @@ -4341,15 +4400,16 @@ func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) { resourceOwner: "org1", id: "id1", provider: LDAPProvider{ - Name: "new name", - Host: "new host", - Port: "new port", - TLS: true, - BaseDN: "new baseDN", - UserObjectClass: "new userObjectClass", - UserUniqueAttribute: "new userUniqueAttribute", - Admin: "new admin", - Password: "new password", + Name: "new name", + Servers: []string{"new server"}, + StartTLS: true, + BaseDN: "new basedn", + BindDN: "new binddn", + BindPassword: "new password", + UserBase: "new user", + UserObjectClasses: []string{"new object"}, + UserFilters: []string{"new filter"}, + Timeout: time.Second * 20, LDAPAttributes: idp.LDAPAttributes{ IDAttribute: "new id", FirstNameAttribute: "new firstName", diff --git a/internal/idp/providers/ldap/ldap.go b/internal/idp/providers/ldap/ldap.go index 5fc14f60d3..d2c950b29b 100644 --- a/internal/idp/providers/ldap/ldap.go +++ b/internal/idp/providers/ldap/ldap.go @@ -2,6 +2,7 @@ package ldap import ( "context" + "time" "github.com/zitadel/zitadel/internal/idp" ) @@ -12,16 +13,18 @@ var _ idp.Provider = (*Provider)(nil) // Provider is the [idp.Provider] implementation for a generic LDAP provider type Provider struct { - name string - host string - port string - tls bool - baseDN string - userObjectClass string - userUniqueAttribute string - admin string - password string - loginUrl string + name string + servers []string + startTLS bool + baseDN string + bindDN string + bindPassword string + userBase string + userObjectClasses []string + userFilters []string + timeout time.Duration + + loginUrl string isLinkingAllowed bool isCreationAllowed bool @@ -74,17 +77,10 @@ func WithAutoUpdate() ProviderOpts { } } -// WithCustomPort configures a custom port used for the communication instead of :389 as per default -func WithCustomPort(port string) ProviderOpts { +// WithoutStartTLS configures to communication insecure with the LDAP server without startTLS +func WithoutStartTLS() ProviderOpts { return func(p *Provider) { - p.port = port - } -} - -// Insecure configures to communication insecure with the LDAP server without TLS -func Insecure() ProviderOpts { - return func(p *Provider) { - p.tls = false + p.startTLS = false } } @@ -181,27 +177,29 @@ func WithProfileAttribute(name string) ProviderOpts { func New( name string, - host string, + servers []string, baseDN string, - userObjectClass string, - userUniqueAttribute string, - admin string, - password string, + bindDN string, + bindPassword string, + userBase string, + userObjectClasses []string, + userFilters []string, + timeout time.Duration, loginUrl string, options ...ProviderOpts, ) *Provider { provider := &Provider{ - name: name, - host: host, - port: DefaultPort, - tls: true, - baseDN: baseDN, - userObjectClass: userObjectClass, - userUniqueAttribute: userUniqueAttribute, - admin: admin, - password: password, - loginUrl: loginUrl, - idAttribute: userUniqueAttribute, + name: name, + servers: servers, + startTLS: true, + baseDN: baseDN, + bindDN: bindDN, + bindPassword: bindPassword, + userBase: userBase, + userObjectClasses: userObjectClasses, + userFilters: userFilters, + timeout: timeout, + loginUrl: loginUrl, } for _, option := range options { option(provider) @@ -216,7 +214,7 @@ func (p *Provider) Name() string { func (p *Provider) BeginAuth(ctx context.Context, state string, params ...any) (idp.Session, error) { return &Session{ Provider: p, - loginUrl: p.loginUrl + "?state=" + state, + loginUrl: p.loginUrl + state, }, nil } @@ -235,3 +233,47 @@ func (p *Provider) IsAutoCreation() bool { func (p *Provider) IsAutoUpdate() bool { return p.isAutoUpdate } + +func (p *Provider) getNecessaryAttributes() []string { + attributes := []string{p.userBase} + if p.idAttribute != "" { + attributes = append(attributes, p.idAttribute) + } + if p.firstNameAttribute != "" { + attributes = append(attributes, p.firstNameAttribute) + } + if p.lastNameAttribute != "" { + attributes = append(attributes, p.lastNameAttribute) + } + if p.displayNameAttribute != "" { + attributes = append(attributes, p.displayNameAttribute) + } + if p.nickNameAttribute != "" { + attributes = append(attributes, p.nickNameAttribute) + } + if p.preferredUsernameAttribute != "" { + attributes = append(attributes, p.preferredUsernameAttribute) + } + if p.emailAttribute != "" { + attributes = append(attributes, p.emailAttribute) + } + if p.emailVerifiedAttribute != "" { + attributes = append(attributes, p.emailVerifiedAttribute) + } + if p.phoneAttribute != "" { + attributes = append(attributes, p.phoneAttribute) + } + if p.phoneVerifiedAttribute != "" { + attributes = append(attributes, p.phoneVerifiedAttribute) + } + if p.preferredLanguageAttribute != "" { + attributes = append(attributes, p.preferredLanguageAttribute) + } + if p.avatarURLAttribute != "" { + attributes = append(attributes, p.avatarURLAttribute) + } + if p.profileAttribute != "" { + attributes = append(attributes, p.profileAttribute) + } + return attributes +} diff --git a/internal/idp/providers/ldap/ldap_test.go b/internal/idp/providers/ldap/ldap_test.go index 6ec2790c96..d00680da1a 100644 --- a/internal/idp/providers/ldap/ldap_test.go +++ b/internal/idp/providers/ldap/ldap_test.go @@ -2,26 +2,28 @@ package ldap import ( "testing" + "time" "github.com/stretchr/testify/assert" ) func TestProvider_Options(t *testing.T) { type fields struct { - name string - host string - baseDN string - userObjectClass string - userUniqueAttribute string - admin string - password string - loginUrl string - opts []ProviderOpts + name string + servers []string + baseDN string + bindDN string + bindPassword string + userBase string + userObjectClasses []string + userFilters []string + timeout time.Duration + loginUrl string + opts []ProviderOpts } type want struct { name string - port string - tls bool + startTls bool linkingAllowed bool creationAllowed bool autoCreation bool @@ -48,39 +50,43 @@ func TestProvider_Options(t *testing.T) { { name: "default", fields: fields{ - name: "ldap", - host: "host", - baseDN: "base", - userObjectClass: "class", - userUniqueAttribute: "attr", - admin: "admin", - password: "password", - loginUrl: "url", - opts: nil, + name: "ldap", + servers: []string{"server"}, + baseDN: "base", + bindDN: "binddn", + bindPassword: "password", + userBase: "user", + userObjectClasses: []string{"object"}, + userFilters: []string{"filter"}, + timeout: 30 * time.Second, + loginUrl: "url", + opts: nil, }, want: want{ name: "ldap", - port: DefaultPort, - tls: true, + startTls: true, linkingAllowed: false, creationAllowed: false, autoCreation: false, autoUpdate: false, - idAttribute: "attr", + idAttribute: "", }, }, { name: "all true", fields: fields{ - name: "ldap", - host: "host", - baseDN: "base", - userObjectClass: "class", - userUniqueAttribute: "attr", - admin: "admin", - password: "password", - loginUrl: "url", + name: "ldap", + servers: []string{"server"}, + baseDN: "base", + bindDN: "binddn", + bindPassword: "password", + userBase: "user", + userObjectClasses: []string{"object"}, + userFilters: []string{"filter"}, + timeout: 30 * time.Second, + loginUrl: "url", opts: []ProviderOpts{ + WithoutStartTLS(), WithLinkingAllowed(), WithCreationAllowed(), WithAutoCreation(), @@ -89,28 +95,28 @@ func TestProvider_Options(t *testing.T) { }, want: want{ name: "ldap", - port: DefaultPort, - tls: true, + startTls: false, linkingAllowed: true, creationAllowed: true, autoCreation: true, autoUpdate: true, - idAttribute: "attr", + idAttribute: "", }, }, { name: "all true, attributes set", fields: fields{ - name: "ldap", - host: "host", - baseDN: "base", - userObjectClass: "class", - userUniqueAttribute: "attr", - admin: "admin", - password: "password", - loginUrl: "url", + name: "ldap", + servers: []string{"server"}, + baseDN: "base", + bindDN: "binddn", + bindPassword: "password", + userBase: "user", + userObjectClasses: []string{"object"}, + userFilters: []string{"filter"}, + timeout: 30 * time.Second, + loginUrl: "url", opts: []ProviderOpts{ - Insecure(), - WithCustomPort("port"), + WithoutStartTLS(), WithLinkingAllowed(), WithCreationAllowed(), WithAutoCreation(), @@ -132,8 +138,7 @@ func TestProvider_Options(t *testing.T) { }, want: want{ name: "ldap", - port: "port", - tls: false, + startTls: false, linkingAllowed: true, creationAllowed: true, autoCreation: true, @@ -157,11 +162,22 @@ func TestProvider_Options(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := assert.New(t) - provider := New(tt.fields.name, tt.fields.host, tt.fields.baseDN, tt.fields.userObjectClass, tt.fields.userUniqueAttribute, tt.fields.admin, tt.fields.password, tt.fields.loginUrl, tt.fields.opts...) + provider := New( + tt.fields.name, + tt.fields.servers, + tt.fields.baseDN, + tt.fields.bindDN, + tt.fields.bindPassword, + tt.fields.userBase, + tt.fields.userObjectClasses, + tt.fields.userFilters, + tt.fields.timeout, + tt.fields.loginUrl, + tt.fields.opts..., + ) a.Equal(tt.want.name, provider.Name()) - a.Equal(tt.want.port, provider.port) - a.Equal(tt.want.tls, provider.tls) + a.Equal(tt.want.startTls, provider.startTLS) a.Equal(tt.want.linkingAllowed, provider.IsLinkingAllowed()) a.Equal(tt.want.creationAllowed, provider.IsCreationAllowed()) a.Equal(tt.want.autoCreation, provider.IsAutoCreation()) diff --git a/internal/idp/providers/ldap/session.go b/internal/idp/providers/ldap/session.go index 638f4f46e4..46bc06573e 100644 --- a/internal/idp/providers/ldap/session.go +++ b/internal/idp/providers/ldap/session.go @@ -4,8 +4,10 @@ import ( "context" "crypto/tls" "errors" - "fmt" + "net" + "net/url" "strconv" + "time" "github.com/go-ldap/ldap/v3" "golang.org/x/text/language" @@ -15,49 +17,154 @@ import ( ) var ErrNoSingleUser = errors.New("user does not exist or too many entries returned") +var ErrFailedLogin = errors.New("user failed to login") var _ idp.Session = (*Session)(nil) type Session struct { Provider *Provider loginUrl string - user string - password string + User string + Password string } func (s *Session) GetAuthURL() string { return s.loginUrl } -func (s *Session) FetchUser(_ context.Context) (idp.User, error) { - l, err := ldap.DialURL("ldap://" + s.Provider.host + ":" + s.Provider.port) + +func (s *Session) FetchUser(_ context.Context) (_ idp.User, err error) { + var user *ldap.Entry + for _, server := range s.Provider.servers { + user, err = tryBind(server, + s.Provider.startTLS, + s.Provider.bindDN, + s.Provider.bindPassword, + s.Provider.baseDN, + s.Provider.getNecessaryAttributes(), + s.Provider.userObjectClasses, + s.Provider.userFilters, + s.User, + s.Password, s.Provider.timeout) + // If there were invalid credentials or multiple users with the credentials cancel process + if err != nil && (errors.Is(err, ErrFailedLogin) || errors.Is(err, ErrNoSingleUser)) { + return nil, err + } + // If a user bind was successful and user is filled continue with login, otherwise try next server + if err == nil && user != nil { + break + } + } if err != nil { return nil, err } - defer l.Close() - if s.Provider.tls { - err = l.StartTLS(&tls.Config{ServerName: s.Provider.host}) + return mapLDAPEntryToUser( + user, + s.Provider.idAttribute, + s.Provider.firstNameAttribute, + s.Provider.lastNameAttribute, + s.Provider.displayNameAttribute, + s.Provider.nickNameAttribute, + s.Provider.preferredUsernameAttribute, + s.Provider.emailAttribute, + s.Provider.emailVerifiedAttribute, + s.Provider.phoneAttribute, + s.Provider.phoneVerifiedAttribute, + s.Provider.preferredLanguageAttribute, + s.Provider.avatarURLAttribute, + s.Provider.profileAttribute, + ) +} + +func tryBind( + server string, + startTLS bool, + bindDN string, + bindPassword string, + baseDN string, + attributes []string, + objectClasses []string, + userFilters []string, + username string, + password string, + timeout time.Duration, +) (*ldap.Entry, error) { + conn, err := getConnection(server, startTLS, timeout) + if err != nil { + return nil, err + } + defer conn.Close() + + if err := conn.Bind(bindDN, bindPassword); err != nil { + return nil, err + } + + return trySearchAndUserBind( + conn, + baseDN, + attributes, + objectClasses, + userFilters, + username, + password, + timeout, + ) +} + +func getConnection( + server string, + startTLS bool, + timeout time.Duration, +) (*ldap.Conn, error) { + if timeout == 0 { + timeout = ldap.DefaultTimeout + } + + conn, err := ldap.DialURL(server, ldap.DialWithDialer(&net.Dialer{Timeout: timeout})) + if err != nil { + return nil, err + } + + u, err := url.Parse(server) + if err != nil { + return nil, err + } + if u.Scheme == "ldaps" && startTLS { + err = conn.StartTLS(&tls.Config{ServerName: u.Host}) if err != nil { return nil, err } } + return conn, nil +} - // Bind as the admin to search for user - err = l.Bind("cn="+s.Provider.admin+","+s.Provider.baseDN, s.Provider.password) - if err != nil { - return nil, err - } +func trySearchAndUserBind( + conn *ldap.Conn, + baseDN string, + attributes []string, + objectClasses []string, + userFilters []string, + username string, + password string, + timeout time.Duration, +) (*ldap.Entry, error) { + searchQuery := queriesAndToSearchQuery( + objectClassesToSearchQuery(objectClasses), + queriesOrToSearchQuery( + userFiltersToSearchQuery(userFilters, username), + ), + ) // Search for user with the unique attribute for the userDN searchRequest := ldap.NewSearchRequest( - s.Provider.baseDN, - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - fmt.Sprintf("(&(objectClass="+s.Provider.userObjectClass+")("+s.Provider.userUniqueAttribute+"=%s))", ldap.EscapeFilter(s.user)), - []string{"dn"}, + baseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, int(timeout.Seconds()), false, + searchQuery, + attributes, nil, ) - sr, err := l.Search(searchRequest) + sr, err := conn.Search(searchRequest) if err != nil { return nil, err } @@ -67,33 +174,100 @@ func (s *Session) FetchUser(_ context.Context) (idp.User, error) { user := sr.Entries[0] // Bind as the user to verify their password - err = l.Bind(user.DN, s.password) - if err != nil { - return nil, err + if err = conn.Bind(user.DN, password); err != nil { + return nil, ErrFailedLogin } + return user, nil +} - emailVerified, err := strconv.ParseBool(user.GetAttributeValue(s.Provider.emailVerifiedAttribute)) - if err != nil { - return nil, err +func queriesAndToSearchQuery(queries ...string) string { + if len(queries) == 0 { + return "" } - phoneVerified, err := strconv.ParseBool(user.GetAttributeValue(s.Provider.phoneVerifiedAttribute)) - if err != nil { - return nil, err + if len(queries) == 1 { + return queries[0] + } + joinQueries := "(&" + for _, s := range queries { + joinQueries += s + } + return joinQueries + ")" +} + +func queriesOrToSearchQuery(queries ...string) string { + if len(queries) == 0 { + return "" + } + if len(queries) == 1 { + return queries[0] + } + joinQueries := "(|" + for _, s := range queries { + joinQueries += s + } + return joinQueries + ")" +} + +func objectClassesToSearchQuery(classes []string) string { + searchQuery := "" + for _, class := range classes { + searchQuery += "(objectClass=" + class + ")" + } + return searchQuery +} + +func userFiltersToSearchQuery(filters []string, username string) string { + searchQuery := "" + for _, filter := range filters { + searchQuery += "(" + filter + "=" + ldap.EscapeFilter(username) + ")" + } + return searchQuery +} + +func mapLDAPEntryToUser( + user *ldap.Entry, + idAttribute, + firstNameAttribute, + lastNameAttribute, + displayNameAttribute, + nickNameAttribute, + preferredUsernameAttribute, + emailAttribute, + emailVerifiedAttribute, + phoneAttribute, + phoneVerifiedAttribute, + preferredLanguageAttribute, + avatarURLAttribute, + profileAttribute string, +) (_ *User, err error) { + var emailVerified bool + if v := user.GetAttributeValue(emailVerifiedAttribute); v != "" { + emailVerified, err = strconv.ParseBool(v) + if err != nil { + return nil, err + } + } + var phoneVerified bool + if v := user.GetAttributeValue(phoneVerifiedAttribute); v != "" { + phoneVerified, err = strconv.ParseBool(v) + if err != nil { + return nil, err + } } return NewUser( - user.GetAttributeValue(s.Provider.idAttribute), - user.GetAttributeValue(s.Provider.firstNameAttribute), - user.GetAttributeValue(s.Provider.lastNameAttribute), - user.GetAttributeValue(s.Provider.displayNameAttribute), - user.GetAttributeValue(s.Provider.nickNameAttribute), - user.GetAttributeValue(s.Provider.preferredUsernameAttribute), - domain.EmailAddress(user.GetAttributeValue(s.Provider.emailAttribute)), + user.GetAttributeValue(idAttribute), + user.GetAttributeValue(firstNameAttribute), + user.GetAttributeValue(lastNameAttribute), + user.GetAttributeValue(displayNameAttribute), + user.GetAttributeValue(nickNameAttribute), + user.GetAttributeValue(preferredUsernameAttribute), + domain.EmailAddress(user.GetAttributeValue(emailAttribute)), emailVerified, - domain.PhoneNumber(user.GetAttributeValue(s.Provider.phoneAttribute)), + domain.PhoneNumber(user.GetAttributeValue(phoneAttribute)), phoneVerified, - language.Make(user.GetAttributeValue(s.Provider.preferredLanguageAttribute)), - user.GetAttributeValue(s.Provider.avatarURLAttribute), - user.GetAttributeValue(s.Provider.profileAttribute), + language.Make(user.GetAttributeValue(preferredLanguageAttribute)), + user.GetAttributeValue(avatarURLAttribute), + user.GetAttributeValue(profileAttribute), ), nil } diff --git a/internal/idp/providers/ldap/session_test.go b/internal/idp/providers/ldap/session_test.go new file mode 100644 index 0000000000..f6bcee6544 --- /dev/null +++ b/internal/idp/providers/ldap/session_test.go @@ -0,0 +1,400 @@ +package ldap + +import ( + "testing" + + "github.com/go-ldap/ldap/v3" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" +) + +func TestProvider_objectClassesToSearchQuery(t *testing.T) { + tests := []struct { + name string + fields []string + want string + }{ + { + name: "zero", + fields: []string{}, + want: "", + }, + { + name: "one", + fields: []string{"test"}, + want: "(objectClass=test)", + }, + { + name: "three", + fields: []string{"test1", "test2", "test3"}, + want: "(objectClass=test1)(objectClass=test2)(objectClass=test3)", + }, + { + name: "five", + fields: []string{"test1", "test2", "test3", "test4", "test5"}, + want: "(objectClass=test1)(objectClass=test2)(objectClass=test3)(objectClass=test4)(objectClass=test5)", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := assert.New(t) + + a.Equal(tt.want, objectClassesToSearchQuery(tt.fields)) + }) + } +} + +func TestProvider_userFiltersToSearchQuery(t *testing.T) { + tests := []struct { + name string + fields []string + username string + want string + }{ + { + name: "zero", + fields: []string{}, + username: "user", + want: "", + }, + { + name: "one", + fields: []string{"test"}, + username: "user", + want: "(test=user)", + }, + { + name: "three", + fields: []string{"test1", "test2", "test3"}, + username: "user", + want: "(test1=user)(test2=user)(test3=user)", + }, + { + name: "five", + fields: []string{"test1", "test2", "test3", "test4", "test5"}, + username: "user", + want: "(test1=user)(test2=user)(test3=user)(test4=user)(test5=user)", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := assert.New(t) + + a.Equal(tt.want, userFiltersToSearchQuery(tt.fields, tt.username)) + }) + } +} + +func TestProvider_queriesAndToSearchQuery(t *testing.T) { + tests := []struct { + name string + fields []string + want string + }{ + { + name: "zero", + fields: []string{}, + want: "", + }, + { + name: "one", + fields: []string{"(test)"}, + want: "(test)", + }, + { + name: "three", + fields: []string{"(test1)", "(test2)", "(test3)"}, + want: "(&(test1)(test2)(test3))", + }, + { + name: "five", + fields: []string{"(test1)", "(test2)", "(test3)", "(test4)", "(test5)"}, + want: "(&(test1)(test2)(test3)(test4)(test5))", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := assert.New(t) + + a.Equal(tt.want, queriesAndToSearchQuery(tt.fields...)) + }) + } +} + +func TestProvider_queriesOrToSearchQuery(t *testing.T) { + tests := []struct { + name string + fields []string + want string + }{ + { + name: "zero", + fields: []string{}, + want: "", + }, + { + name: "one", + fields: []string{"(test)"}, + want: "(test)", + }, + { + name: "three", + fields: []string{"(test1)", "(test2)", "(test3)"}, + want: "(|(test1)(test2)(test3))", + }, + { + name: "five", + fields: []string{"(test1)", "(test2)", "(test3)", "(test4)", "(test5)"}, + want: "(|(test1)(test2)(test3)(test4)(test5))", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := assert.New(t) + + a.Equal(tt.want, queriesOrToSearchQuery(tt.fields...)) + }) + } +} + +func TestProvider_mapLDAPEntryToUser(t *testing.T) { + type fields struct { + user *ldap.Entry + idAttribute string + firstNameAttribute string + lastNameAttribute string + displayNameAttribute string + nickNameAttribute string + preferredUsernameAttribute string + emailAttribute string + emailVerifiedAttribute string + phoneAttribute string + phoneVerifiedAttribute string + preferredLanguageAttribute string + avatarURLAttribute string + profileAttribute string + } + type want struct { + user *User + err func(error) bool + } + tests := []struct { + name string + fields fields + want want + }{ + { + name: "empty", + fields: fields{ + user: &ldap.Entry{ + Attributes: []*ldap.EntryAttribute{ + {Name: "id", Values: []string{"id"}}, + {Name: "first", Values: []string{"first"}}, + {Name: "last", Values: []string{"last"}}, + {Name: "display", Values: []string{"display"}}, + {Name: "nick", Values: []string{"nick"}}, + {Name: "preferred", Values: []string{"preferred"}}, + {Name: "email", Values: []string{"email"}}, + {Name: "emailVerified", Values: []string{"false"}}, + {Name: "phone", Values: []string{"phone"}}, + {Name: "phoneVerified", Values: []string{"false"}}, + {Name: "lang", Values: []string{"und"}}, + {Name: "avatar", Values: []string{"avatar"}}, + {Name: "profile", Values: []string{"profile"}}, + }, + }, + idAttribute: "", + firstNameAttribute: "", + lastNameAttribute: "", + displayNameAttribute: "", + nickNameAttribute: "", + preferredUsernameAttribute: "", + emailAttribute: "", + emailVerifiedAttribute: "", + phoneAttribute: "", + phoneVerifiedAttribute: "", + preferredLanguageAttribute: "", + avatarURLAttribute: "", + profileAttribute: "", + }, + want: want{ + user: &User{ + id: "", + firstName: "", + lastName: "", + displayName: "", + nickName: "", + preferredUsername: "", + email: "", + emailVerified: false, + phone: "", + phoneVerified: false, + preferredLanguage: language.Tag{}, + avatarURL: "", + profile: "", + }, + }, + }, + { + name: "failed parse emailVerified", + fields: fields{ + user: &ldap.Entry{ + Attributes: []*ldap.EntryAttribute{ + {Name: "id", Values: []string{"id"}}, + {Name: "first", Values: []string{"first"}}, + {Name: "last", Values: []string{"last"}}, + {Name: "display", Values: []string{"display"}}, + {Name: "nick", Values: []string{"nick"}}, + {Name: "preferred", Values: []string{"preferred"}}, + {Name: "email", Values: []string{"email"}}, + {Name: "emailVerified", Values: []string{"failure"}}, + {Name: "phone", Values: []string{"phone"}}, + {Name: "phoneVerified", Values: []string{"false"}}, + {Name: "lang", Values: []string{"und"}}, + {Name: "avatar", Values: []string{"avatar"}}, + {Name: "profile", Values: []string{"profile"}}, + }, + }, + idAttribute: "id", + firstNameAttribute: "first", + lastNameAttribute: "last", + displayNameAttribute: "display", + nickNameAttribute: "nick", + preferredUsernameAttribute: "preferred", + emailAttribute: "email", + emailVerifiedAttribute: "emailVerified", + phoneAttribute: "phone", + phoneVerifiedAttribute: "phoneVerified", + preferredLanguageAttribute: "lang", + avatarURLAttribute: "avatar", + profileAttribute: "profile", + }, + want: want{ + err: func(err error) bool { + return err != nil + }, + }, + }, + { + name: "failed parse phoneVerified", + fields: fields{ + user: &ldap.Entry{ + Attributes: []*ldap.EntryAttribute{ + {Name: "id", Values: []string{"id"}}, + {Name: "first", Values: []string{"first"}}, + {Name: "last", Values: []string{"last"}}, + {Name: "display", Values: []string{"display"}}, + {Name: "nick", Values: []string{"nick"}}, + {Name: "preferred", Values: []string{"preferred"}}, + {Name: "email", Values: []string{"email"}}, + {Name: "emailVerified", Values: []string{"false"}}, + {Name: "phone", Values: []string{"phone"}}, + {Name: "phoneVerified", Values: []string{"failure"}}, + {Name: "lang", Values: []string{"und"}}, + {Name: "avatar", Values: []string{"avatar"}}, + {Name: "profile", Values: []string{"profile"}}, + }, + }, + idAttribute: "id", + firstNameAttribute: "first", + lastNameAttribute: "last", + displayNameAttribute: "display", + nickNameAttribute: "nick", + preferredUsernameAttribute: "preferred", + emailAttribute: "email", + emailVerifiedAttribute: "emailVerified", + phoneAttribute: "phone", + phoneVerifiedAttribute: "phoneVerified", + preferredLanguageAttribute: "lang", + avatarURLAttribute: "avatar", + profileAttribute: "profile", + }, + want: want{ + err: func(err error) bool { + return err != nil + }, + }, + }, + { + name: "full user", + fields: fields{ + user: &ldap.Entry{ + Attributes: []*ldap.EntryAttribute{ + {Name: "id", Values: []string{"id"}}, + {Name: "first", Values: []string{"first"}}, + {Name: "last", Values: []string{"last"}}, + {Name: "display", Values: []string{"display"}}, + {Name: "nick", Values: []string{"nick"}}, + {Name: "preferred", Values: []string{"preferred"}}, + {Name: "email", Values: []string{"email"}}, + {Name: "emailVerified", Values: []string{"false"}}, + {Name: "phone", Values: []string{"phone"}}, + {Name: "phoneVerified", Values: []string{"false"}}, + {Name: "lang", Values: []string{"und"}}, + {Name: "avatar", Values: []string{"avatar"}}, + {Name: "profile", Values: []string{"profile"}}, + }, + }, + idAttribute: "id", + firstNameAttribute: "first", + lastNameAttribute: "last", + displayNameAttribute: "display", + nickNameAttribute: "nick", + preferredUsernameAttribute: "preferred", + emailAttribute: "email", + emailVerifiedAttribute: "emailVerified", + phoneAttribute: "phone", + phoneVerifiedAttribute: "phoneVerified", + preferredLanguageAttribute: "lang", + avatarURLAttribute: "avatar", + profileAttribute: "profile", + }, + want: want{ + user: &User{ + id: "id", + firstName: "first", + lastName: "last", + displayName: "display", + nickName: "nick", + preferredUsername: "preferred", + email: "email", + emailVerified: false, + phone: "phone", + phoneVerified: false, + preferredLanguage: language.Make("und"), + avatarURL: "avatar", + profile: "profile", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := mapLDAPEntryToUser( + tt.fields.user, + tt.fields.idAttribute, + tt.fields.firstNameAttribute, + tt.fields.lastNameAttribute, + tt.fields.displayNameAttribute, + tt.fields.nickNameAttribute, + tt.fields.preferredUsernameAttribute, + tt.fields.emailAttribute, + tt.fields.emailVerifiedAttribute, + tt.fields.phoneAttribute, + tt.fields.phoneVerifiedAttribute, + tt.fields.preferredLanguageAttribute, + tt.fields.avatarURLAttribute, + tt.fields.profileAttribute, + ) + if tt.want.err == nil { + assert.NoError(t, err) + } + if tt.want.err != nil && !tt.want.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.want.err == nil { + assert.Equal(t, tt.want.user, got) + } + }) + } +} diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go index 53190297f1..2639691a74 100644 --- a/internal/query/idp_template.go +++ b/internal/query/idp_template.go @@ -128,15 +128,16 @@ type GoogleIDPTemplate struct { } type LDAPIDPTemplate struct { - IDPID string - Host string - Port string - TLS bool - BaseDN string - UserObjectClass string - UserUniqueAttribute string - Admin string - Password *crypto.CryptoValue + IDPID string + Servers []string + StartTLS bool + BaseDN string + BindDN string + BindPassword *crypto.CryptoValue + UserBase string + UserObjectClasses []string + UserFilters []string + Timeout time.Duration idp.LDAPAttributes } @@ -515,36 +516,40 @@ var ( name: projection.LDAPInstanceIDCol, table: ldapIdpTemplateTable, } - LDAPHostCol = Column{ - name: projection.LDAPHostCol, + LDAPServersCol = Column{ + name: projection.LDAPServersCol, table: ldapIdpTemplateTable, } - LDAPPortCol = Column{ - name: projection.LDAPPortCol, - table: ldapIdpTemplateTable, - } - LDAPTlsCol = Column{ - name: projection.LDAPTlsCol, + LDAPStartTLSCol = Column{ + name: projection.LDAPStartTLSCol, table: ldapIdpTemplateTable, } LDAPBaseDNCol = Column{ name: projection.LDAPBaseDNCol, table: ldapIdpTemplateTable, } - LDAPUserObjectClassCol = Column{ - name: projection.LDAPUserObjectClassCol, + LDAPBindDNCol = Column{ + name: projection.LDAPBindDNCol, table: ldapIdpTemplateTable, } - LDAPUserUniqueAttributeCol = Column{ - name: projection.LDAPUserUniqueAttributeCol, + LDAPBindPasswordCol = Column{ + name: projection.LDAPBindPasswordCol, table: ldapIdpTemplateTable, } - LDAPAdminCol = Column{ - name: projection.LDAPAdminCol, + LDAPUserBaseCol = Column{ + name: projection.LDAPUserBaseCol, table: ldapIdpTemplateTable, } - LDAPPasswordCol = Column{ - name: projection.LDAPPasswordCol, + LDAPUserObjectClassesCol = Column{ + name: projection.LDAPUserObjectClassesCol, + table: ldapIdpTemplateTable, + } + LDAPUserFiltersCol = Column{ + name: projection.LDAPUserFiltersCol, + table: ldapIdpTemplateTable, + } + LDAPTimeoutCol = Column{ + name: projection.LDAPTimeoutCol, table: ldapIdpTemplateTable, } LDAPIDAttributeCol = Column{ @@ -772,14 +777,15 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se GoogleScopesCol.identifier(), // ldap LDAPIDCol.identifier(), - LDAPHostCol.identifier(), - LDAPPortCol.identifier(), - LDAPTlsCol.identifier(), + LDAPServersCol.identifier(), + LDAPStartTLSCol.identifier(), LDAPBaseDNCol.identifier(), - LDAPUserObjectClassCol.identifier(), - LDAPUserUniqueAttributeCol.identifier(), - LDAPAdminCol.identifier(), - LDAPPasswordCol.identifier(), + LDAPBindDNCol.identifier(), + LDAPBindPasswordCol.identifier(), + LDAPUserBaseCol.identifier(), + LDAPUserObjectClassesCol.identifier(), + LDAPUserFiltersCol.identifier(), + LDAPTimeoutCol.identifier(), LDAPIDAttributeCol.identifier(), LDAPFirstNameAttributeCol.identifier(), LDAPLastNameAttributeCol.identifier(), @@ -869,14 +875,15 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se googleScopes := database.StringArray{} ldapID := sql.NullString{} - ldapHost := sql.NullString{} - ldapPort := sql.NullString{} - ldapTls := sql.NullBool{} + ldapServers := database.StringArray{} + ldapStartTls := sql.NullBool{} ldapBaseDN := sql.NullString{} - ldapUserObjectClass := sql.NullString{} - ldapUserUniqueAttribute := sql.NullString{} - ldapAdmin := sql.NullString{} - ldapPassword := new(crypto.CryptoValue) + ldapBindDN := sql.NullString{} + ldapBindPassword := new(crypto.CryptoValue) + ldapUserBase := sql.NullString{} + ldapUserObjectClasses := database.StringArray{} + ldapUserFilters := database.StringArray{} + ldapTimeout := sql.NullInt64{} ldapIDAttribute := sql.NullString{} ldapFirstNameAttribute := sql.NullString{} ldapLastNameAttribute := sql.NullString{} @@ -965,14 +972,15 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se &googleScopes, // ldap &ldapID, - &ldapHost, - &ldapPort, - &ldapTls, + &ldapServers, + &ldapStartTls, &ldapBaseDN, - &ldapUserObjectClass, - &ldapUserUniqueAttribute, - &ldapAdmin, - &ldapPassword, + &ldapBindDN, + &ldapBindPassword, + &ldapUserBase, + &ldapUserObjectClasses, + &ldapUserFilters, + &ldapTimeout, &ldapIDAttribute, &ldapFirstNameAttribute, &ldapLastNameAttribute, @@ -1083,15 +1091,16 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se } if ldapID.Valid { idpTemplate.LDAPIDPTemplate = &LDAPIDPTemplate{ - IDPID: ldapID.String, - Host: ldapHost.String, - Port: ldapPort.String, - TLS: ldapTls.Bool, - BaseDN: ldapBaseDN.String, - UserObjectClass: ldapUserObjectClass.String, - UserUniqueAttribute: ldapUserUniqueAttribute.String, - Admin: ldapAdmin.String, - Password: ldapPassword, + IDPID: ldapID.String, + Servers: ldapServers, + StartTLS: ldapStartTls.Bool, + BaseDN: ldapBaseDN.String, + BindDN: ldapBindDN.String, + BindPassword: ldapBindPassword, + UserBase: ldapUserBase.String, + UserObjectClasses: ldapUserObjectClasses, + UserFilters: ldapUserFilters, + Timeout: time.Duration(ldapTimeout.Int64), LDAPAttributes: idp.LDAPAttributes{ IDAttribute: ldapIDAttribute.String, FirstNameAttribute: ldapFirstNameAttribute.String, @@ -1189,14 +1198,15 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec GoogleScopesCol.identifier(), // ldap LDAPIDCol.identifier(), - LDAPHostCol.identifier(), - LDAPPortCol.identifier(), - LDAPTlsCol.identifier(), + LDAPServersCol.identifier(), + LDAPStartTLSCol.identifier(), LDAPBaseDNCol.identifier(), - LDAPUserObjectClassCol.identifier(), - LDAPUserUniqueAttributeCol.identifier(), - LDAPAdminCol.identifier(), - LDAPPasswordCol.identifier(), + LDAPBindDNCol.identifier(), + LDAPBindPasswordCol.identifier(), + LDAPUserBaseCol.identifier(), + LDAPUserObjectClassesCol.identifier(), + LDAPUserFiltersCol.identifier(), + LDAPTimeoutCol.identifier(), LDAPIDAttributeCol.identifier(), LDAPFirstNameAttributeCol.identifier(), LDAPLastNameAttributeCol.identifier(), @@ -1290,14 +1300,15 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec googleScopes := database.StringArray{} ldapID := sql.NullString{} - ldapHost := sql.NullString{} - ldapPort := sql.NullString{} - ldapTls := sql.NullBool{} + ldapServers := database.StringArray{} + ldapStartTls := sql.NullBool{} ldapBaseDN := sql.NullString{} - ldapUserObjectClass := sql.NullString{} - ldapUserUniqueAttribute := sql.NullString{} - ldapAdmin := sql.NullString{} - ldapPassword := new(crypto.CryptoValue) + ldapBindDN := sql.NullString{} + ldapBindPassword := new(crypto.CryptoValue) + ldapUserBase := sql.NullString{} + ldapUserObjectClasses := database.StringArray{} + ldapUserFilters := database.StringArray{} + ldapTimeout := sql.NullInt64{} ldapIDAttribute := sql.NullString{} ldapFirstNameAttribute := sql.NullString{} ldapLastNameAttribute := sql.NullString{} @@ -1386,14 +1397,15 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec &googleScopes, // ldap &ldapID, - &ldapHost, - &ldapPort, - &ldapTls, + &ldapServers, + &ldapStartTls, &ldapBaseDN, - &ldapUserObjectClass, - &ldapUserUniqueAttribute, - &ldapAdmin, - &ldapPassword, + &ldapBindDN, + &ldapBindPassword, + &ldapUserBase, + &ldapUserObjectClasses, + &ldapUserFilters, + &ldapTimeout, &ldapIDAttribute, &ldapFirstNameAttribute, &ldapLastNameAttribute, @@ -1503,15 +1515,16 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec } if ldapID.Valid { idpTemplate.LDAPIDPTemplate = &LDAPIDPTemplate{ - IDPID: ldapID.String, - Host: ldapHost.String, - Port: ldapPort.String, - TLS: ldapTls.Bool, - BaseDN: ldapBaseDN.String, - UserObjectClass: ldapUserObjectClass.String, - UserUniqueAttribute: ldapUserUniqueAttribute.String, - Admin: ldapAdmin.String, - Password: ldapPassword, + IDPID: ldapID.String, + Servers: ldapServers, + StartTLS: ldapStartTls.Bool, + BaseDN: ldapBaseDN.String, + BindDN: ldapBindDN.String, + BindPassword: ldapBindPassword, + UserBase: ldapUserBase.String, + UserObjectClasses: ldapUserObjectClasses, + UserFilters: ldapUserFilters, + Timeout: time.Duration(ldapTimeout.Int64), LDAPAttributes: idp.LDAPAttributes{ IDAttribute: ldapIDAttribute.String, FirstNameAttribute: ldapFirstNameAttribute.String, diff --git a/internal/query/idp_template_test.go b/internal/query/idp_template_test.go index e3147116fe..3d39eacc21 100644 --- a/internal/query/idp_template_test.go +++ b/internal/query/idp_template_test.go @@ -7,6 +7,7 @@ import ( "fmt" "regexp" "testing" + "time" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" @@ -87,28 +88,29 @@ var ( ` projections.idp_templates4_google.client_secret,` + ` projections.idp_templates4_google.scopes,` + // ldap - ` projections.idp_templates4_ldap.idp_id,` + - ` projections.idp_templates4_ldap.host,` + - ` projections.idp_templates4_ldap.port,` + - ` projections.idp_templates4_ldap.tls,` + - ` projections.idp_templates4_ldap.base_dn,` + - ` projections.idp_templates4_ldap.user_object_class,` + - ` projections.idp_templates4_ldap.user_unique_attribute,` + - ` projections.idp_templates4_ldap.admin,` + - ` projections.idp_templates4_ldap.password,` + - ` projections.idp_templates4_ldap.id_attribute,` + - ` projections.idp_templates4_ldap.first_name_attribute,` + - ` projections.idp_templates4_ldap.last_name_attribute,` + - ` projections.idp_templates4_ldap.display_name_attribute,` + - ` projections.idp_templates4_ldap.nick_name_attribute,` + - ` projections.idp_templates4_ldap.preferred_username_attribute,` + - ` projections.idp_templates4_ldap.email_attribute,` + - ` projections.idp_templates4_ldap.email_verified,` + - ` projections.idp_templates4_ldap.phone_attribute,` + - ` projections.idp_templates4_ldap.phone_verified_attribute,` + - ` projections.idp_templates4_ldap.preferred_language_attribute,` + - ` projections.idp_templates4_ldap.avatar_url_attribute,` + - ` projections.idp_templates4_ldap.profile_attribute` + + ` projections.idp_templates4_ldap2.idp_id,` + + ` projections.idp_templates4_ldap2.servers,` + + ` projections.idp_templates4_ldap2.start_tls,` + + ` projections.idp_templates4_ldap2.base_dn,` + + ` projections.idp_templates4_ldap2.bind_dn,` + + ` projections.idp_templates4_ldap2.bind_password,` + + ` projections.idp_templates4_ldap2.user_base,` + + ` projections.idp_templates4_ldap2.user_object_classes,` + + ` projections.idp_templates4_ldap2.user_filters,` + + ` projections.idp_templates4_ldap2.timeout,` + + ` projections.idp_templates4_ldap2.id_attribute,` + + ` projections.idp_templates4_ldap2.first_name_attribute,` + + ` projections.idp_templates4_ldap2.last_name_attribute,` + + ` projections.idp_templates4_ldap2.display_name_attribute,` + + ` projections.idp_templates4_ldap2.nick_name_attribute,` + + ` projections.idp_templates4_ldap2.preferred_username_attribute,` + + ` projections.idp_templates4_ldap2.email_attribute,` + + ` projections.idp_templates4_ldap2.email_verified,` + + ` projections.idp_templates4_ldap2.phone_attribute,` + + ` projections.idp_templates4_ldap2.phone_verified_attribute,` + + ` projections.idp_templates4_ldap2.preferred_language_attribute,` + + ` projections.idp_templates4_ldap2.avatar_url_attribute,` + + ` projections.idp_templates4_ldap2.profile_attribute` + ` FROM projections.idp_templates4` + ` LEFT JOIN projections.idp_templates4_oauth2 ON projections.idp_templates4.id = projections.idp_templates4_oauth2.idp_id AND projections.idp_templates4.instance_id = projections.idp_templates4_oauth2.instance_id` + ` LEFT JOIN projections.idp_templates4_oidc ON projections.idp_templates4.id = projections.idp_templates4_oidc.idp_id AND projections.idp_templates4.instance_id = projections.idp_templates4_oidc.instance_id` + @@ -119,7 +121,7 @@ var ( ` LEFT JOIN projections.idp_templates4_gitlab ON projections.idp_templates4.id = projections.idp_templates4_gitlab.idp_id AND projections.idp_templates4.instance_id = projections.idp_templates4_gitlab.instance_id` + ` LEFT JOIN projections.idp_templates4_gitlab_self_hosted ON projections.idp_templates4.id = projections.idp_templates4_gitlab_self_hosted.idp_id AND projections.idp_templates4.instance_id = projections.idp_templates4_gitlab_self_hosted.instance_id` + ` LEFT JOIN projections.idp_templates4_google ON projections.idp_templates4.id = projections.idp_templates4_google.idp_id AND projections.idp_templates4.instance_id = projections.idp_templates4_google.instance_id` + - ` LEFT JOIN projections.idp_templates4_ldap ON projections.idp_templates4.id = projections.idp_templates4_ldap.idp_id AND projections.idp_templates4.instance_id = projections.idp_templates4_ldap.instance_id` + + ` LEFT JOIN projections.idp_templates4_ldap2 ON projections.idp_templates4.id = projections.idp_templates4_ldap2.idp_id AND projections.idp_templates4.instance_id = projections.idp_templates4_ldap2.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` idpTemplateCols = []string{ "id", @@ -195,14 +197,15 @@ var ( "scopes", // ldap config "idp_id", - "host", - "port", - "tls", + "servers", + "start_tls", "base_dn", - "user_object_class", - "user_unique_attribute", - "admin", - "password", + "bind_dn", + "bind_password", + "user_base", + "user_object_classes", + "user_filters", + "timeout", "id_attribute", "first_name_attribute", "last_name_attribute", @@ -289,28 +292,29 @@ var ( ` projections.idp_templates4_google.client_secret,` + ` projections.idp_templates4_google.scopes,` + // ldap - ` projections.idp_templates4_ldap.idp_id,` + - ` projections.idp_templates4_ldap.host,` + - ` projections.idp_templates4_ldap.port,` + - ` projections.idp_templates4_ldap.tls,` + - ` projections.idp_templates4_ldap.base_dn,` + - ` projections.idp_templates4_ldap.user_object_class,` + - ` projections.idp_templates4_ldap.user_unique_attribute,` + - ` projections.idp_templates4_ldap.admin,` + - ` projections.idp_templates4_ldap.password,` + - ` projections.idp_templates4_ldap.id_attribute,` + - ` projections.idp_templates4_ldap.first_name_attribute,` + - ` projections.idp_templates4_ldap.last_name_attribute,` + - ` projections.idp_templates4_ldap.display_name_attribute,` + - ` projections.idp_templates4_ldap.nick_name_attribute,` + - ` projections.idp_templates4_ldap.preferred_username_attribute,` + - ` projections.idp_templates4_ldap.email_attribute,` + - ` projections.idp_templates4_ldap.email_verified,` + - ` projections.idp_templates4_ldap.phone_attribute,` + - ` projections.idp_templates4_ldap.phone_verified_attribute,` + - ` projections.idp_templates4_ldap.preferred_language_attribute,` + - ` projections.idp_templates4_ldap.avatar_url_attribute,` + - ` projections.idp_templates4_ldap.profile_attribute,` + + ` projections.idp_templates4_ldap2.idp_id,` + + ` projections.idp_templates4_ldap2.servers,` + + ` projections.idp_templates4_ldap2.start_tls,` + + ` projections.idp_templates4_ldap2.base_dn,` + + ` projections.idp_templates4_ldap2.bind_dn,` + + ` projections.idp_templates4_ldap2.bind_password,` + + ` projections.idp_templates4_ldap2.user_base,` + + ` projections.idp_templates4_ldap2.user_object_classes,` + + ` projections.idp_templates4_ldap2.user_filters,` + + ` projections.idp_templates4_ldap2.timeout,` + + ` projections.idp_templates4_ldap2.id_attribute,` + + ` projections.idp_templates4_ldap2.first_name_attribute,` + + ` projections.idp_templates4_ldap2.last_name_attribute,` + + ` projections.idp_templates4_ldap2.display_name_attribute,` + + ` projections.idp_templates4_ldap2.nick_name_attribute,` + + ` projections.idp_templates4_ldap2.preferred_username_attribute,` + + ` projections.idp_templates4_ldap2.email_attribute,` + + ` projections.idp_templates4_ldap2.email_verified,` + + ` projections.idp_templates4_ldap2.phone_attribute,` + + ` projections.idp_templates4_ldap2.phone_verified_attribute,` + + ` projections.idp_templates4_ldap2.preferred_language_attribute,` + + ` projections.idp_templates4_ldap2.avatar_url_attribute,` + + ` projections.idp_templates4_ldap2.profile_attribute,` + ` COUNT(*) OVER ()` + ` FROM projections.idp_templates4` + ` LEFT JOIN projections.idp_templates4_oauth2 ON projections.idp_templates4.id = projections.idp_templates4_oauth2.idp_id AND projections.idp_templates4.instance_id = projections.idp_templates4_oauth2.instance_id` + @@ -322,7 +326,7 @@ var ( ` LEFT JOIN projections.idp_templates4_gitlab ON projections.idp_templates4.id = projections.idp_templates4_gitlab.idp_id AND projections.idp_templates4.instance_id = projections.idp_templates4_gitlab.instance_id` + ` LEFT JOIN projections.idp_templates4_gitlab_self_hosted ON projections.idp_templates4.id = projections.idp_templates4_gitlab_self_hosted.idp_id AND projections.idp_templates4.instance_id = projections.idp_templates4_gitlab_self_hosted.instance_id` + ` LEFT JOIN projections.idp_templates4_google ON projections.idp_templates4.id = projections.idp_templates4_google.idp_id AND projections.idp_templates4.instance_id = projections.idp_templates4_google.instance_id` + - ` LEFT JOIN projections.idp_templates4_ldap ON projections.idp_templates4.id = projections.idp_templates4_ldap.idp_id AND projections.idp_templates4.instance_id = projections.idp_templates4_ldap.instance_id` + + ` LEFT JOIN projections.idp_templates4_ldap2 ON projections.idp_templates4.id = projections.idp_templates4_ldap2.idp_id AND projections.idp_templates4.instance_id = projections.idp_templates4_ldap2.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` idpTemplatesCols = []string{ "id", @@ -398,14 +402,15 @@ var ( "scopes", // ldap config "idp_id", - "host", - "port", - "tls", + "servers", + "start_tls", "base_dn", - "user_object_class", - "user_unique_attribute", - "admin", - "password", + "bind_dn", + "bind_password", + "user_base", + "user_object_classes", + "user_filters", + "timeout", "id_attribute", "first_name_attribute", "last_name_attribute", @@ -554,6 +559,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, }, ), }, @@ -685,6 +691,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, }, ), }, @@ -814,6 +821,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, }, ), }, @@ -942,6 +950,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, }, ), }, @@ -1069,6 +1078,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, }, ), }, @@ -1196,6 +1206,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, }, ), }, @@ -1324,6 +1335,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, }, ), }, @@ -1430,14 +1442,15 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, // ldap config "idp-id", - "host", - "port", + database.StringArray{"server"}, true, "base", - "user", - "uid", - "admin", + "dn", nil, + "user", + database.StringArray{"object"}, + database.StringArray{"filter"}, + time.Duration(30000000000), "id", "first", "last", @@ -1469,14 +1482,15 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { IsAutoCreation: true, IsAutoUpdate: true, LDAPIDPTemplate: &LDAPIDPTemplate{ - IDPID: "idp-id", - Host: "host", - Port: "port", - TLS: true, - BaseDN: "base", - UserObjectClass: "user", - UserUniqueAttribute: "uid", - Admin: "admin", + IDPID: "idp-id", + Servers: []string{"server"}, + StartTLS: true, + BaseDN: "base", + BindDN: "dn", + UserBase: "user", + UserObjectClasses: []string{"object"}, + UserFilters: []string{"filter"}, + Timeout: time.Duration(30000000000), LDAPAttributes: idp.LDAPAttributes{ IDAttribute: "id", FirstNameAttribute: "first", @@ -1597,6 +1611,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, }, ), }, @@ -1733,14 +1748,15 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, // ldap config "idp-id", - "host", - "port", + database.StringArray{"server"}, true, "base", - "user", - "uid", - "admin", + "dn", nil, + "user", + database.StringArray{"object"}, + database.StringArray{"filter"}, + time.Duration(30000000000), "id", "first", "last", @@ -1778,14 +1794,15 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { IsAutoCreation: true, IsAutoUpdate: true, LDAPIDPTemplate: &LDAPIDPTemplate{ - IDPID: "idp-id", - Host: "host", - Port: "port", - TLS: true, - BaseDN: "base", - UserObjectClass: "user", - UserUniqueAttribute: "uid", - Admin: "admin", + IDPID: "idp-id", + Servers: []string{"server"}, + StartTLS: true, + BaseDN: "base", + BindDN: "dn", + UserBase: "user", + UserObjectClasses: []string{"object"}, + UserFilters: []string{"filter"}, + Timeout: time.Duration(30000000000), LDAPAttributes: idp.LDAPAttributes{ IDAttribute: "id", FirstNameAttribute: "first", @@ -1909,6 +1926,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, }, }, ), @@ -2018,14 +2036,15 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, // ldap config "idp-id-ldap", - "host", - "port", + database.StringArray{"server"}, true, "base", - "user", - "uid", - "admin", + "dn", nil, + "user", + database.StringArray{"object"}, + database.StringArray{"filter"}, + time.Duration(30000000000), "id", "first", "last", @@ -2135,6 +2154,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, }, { "idp-id-oauth", @@ -2231,6 +2251,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, }, { "idp-id-oidc", @@ -2327,6 +2348,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, }, { "idp-id-jwt", @@ -2423,6 +2445,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { nil, nil, nil, + nil, }, }, ), @@ -2447,14 +2470,15 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) { IsAutoCreation: true, IsAutoUpdate: true, LDAPIDPTemplate: &LDAPIDPTemplate{ - IDPID: "idp-id-ldap", - Host: "host", - Port: "port", - TLS: true, - BaseDN: "base", - UserObjectClass: "user", - UserUniqueAttribute: "uid", - Admin: "admin", + IDPID: "idp-id-ldap", + Servers: []string{"server"}, + StartTLS: true, + BaseDN: "base", + BindDN: "dn", + UserBase: "user", + UserObjectClasses: []string{"object"}, + UserFilters: []string{"filter"}, + Timeout: time.Duration(30000000000), LDAPAttributes: idp.LDAPAttributes{ IDAttribute: "id", FirstNameAttribute: "first", diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go index 53afb832ac..2a1f9a9cc0 100644 --- a/internal/query/projection/idp_template.go +++ b/internal/query/projection/idp_template.go @@ -38,7 +38,7 @@ const ( IDPTemplateGitLabSuffix = "gitlab" IDPTemplateGitLabSelfHostedSuffix = "gitlab_self_hosted" IDPTemplateGoogleSuffix = "google" - IDPTemplateLDAPSuffix = "ldap" + IDPTemplateLDAPSuffix = "ldap2" IDPTemplateIDCol = "id" IDPTemplateCreationDateCol = "creation_date" @@ -125,14 +125,15 @@ const ( LDAPIDCol = "idp_id" LDAPInstanceIDCol = "instance_id" - LDAPHostCol = "host" - LDAPPortCol = "port" - LDAPTlsCol = "tls" + LDAPServersCol = "servers" + LDAPStartTLSCol = "start_tls" LDAPBaseDNCol = "base_dn" - LDAPUserObjectClassCol = "user_object_class" - LDAPUserUniqueAttributeCol = "user_unique_attribute" - LDAPAdminCol = "admin" - LDAPPasswordCol = "password" + LDAPBindDNCol = "bind_dn" + LDAPBindPasswordCol = "bind_password" + LDAPUserBaseCol = "user_base" + LDAPUserObjectClassesCol = "user_object_classes" + LDAPUserFiltersCol = "user_filters" + LDAPTimeoutCol = "timeout" LDAPIDAttributeCol = "id_attribute" LDAPFirstNameAttributeCol = "first_name_attribute" LDAPLastNameAttributeCol = "last_name_attribute" @@ -293,14 +294,15 @@ func newIDPTemplateProjection(ctx context.Context, config crdb.StatementHandlerC crdb.NewSuffixedTable([]*crdb.Column{ crdb.NewColumn(LDAPIDCol, crdb.ColumnTypeText), crdb.NewColumn(LDAPInstanceIDCol, crdb.ColumnTypeText), - crdb.NewColumn(LDAPHostCol, crdb.ColumnTypeText, crdb.Nullable()), - crdb.NewColumn(LDAPPortCol, crdb.ColumnTypeText, crdb.Nullable()), - crdb.NewColumn(LDAPTlsCol, crdb.ColumnTypeBool, crdb.Nullable()), - crdb.NewColumn(LDAPBaseDNCol, crdb.ColumnTypeText, crdb.Nullable()), - crdb.NewColumn(LDAPUserObjectClassCol, crdb.ColumnTypeText, crdb.Nullable()), - crdb.NewColumn(LDAPUserUniqueAttributeCol, crdb.ColumnTypeText, crdb.Nullable()), - crdb.NewColumn(LDAPAdminCol, crdb.ColumnTypeText, crdb.Nullable()), - crdb.NewColumn(LDAPPasswordCol, crdb.ColumnTypeJSONB, crdb.Nullable()), + crdb.NewColumn(LDAPServersCol, crdb.ColumnTypeTextArray), + crdb.NewColumn(LDAPStartTLSCol, crdb.ColumnTypeBool), + crdb.NewColumn(LDAPBaseDNCol, crdb.ColumnTypeText), + crdb.NewColumn(LDAPBindDNCol, crdb.ColumnTypeText), + crdb.NewColumn(LDAPBindPasswordCol, crdb.ColumnTypeJSONB), + crdb.NewColumn(LDAPUserBaseCol, crdb.ColumnTypeText), + crdb.NewColumn(LDAPUserObjectClassesCol, crdb.ColumnTypeTextArray), + crdb.NewColumn(LDAPUserFiltersCol, crdb.ColumnTypeTextArray), + crdb.NewColumn(LDAPTimeoutCol, crdb.ColumnTypeInt64), crdb.NewColumn(LDAPIDAttributeCol, crdb.ColumnTypeText, crdb.Nullable()), crdb.NewColumn(LDAPFirstNameAttributeCol, crdb.ColumnTypeText, crdb.Nullable()), crdb.NewColumn(LDAPLastNameAttributeCol, crdb.ColumnTypeText, crdb.Nullable()), @@ -1663,14 +1665,15 @@ func (p *idpTemplateProjection) reduceLDAPIDPAdded(event eventstore.Event) (*han []handler.Column{ handler.NewCol(LDAPIDCol, idpEvent.ID), handler.NewCol(LDAPInstanceIDCol, idpEvent.Aggregate().InstanceID), - handler.NewCol(LDAPHostCol, idpEvent.Host), - handler.NewCol(LDAPPortCol, idpEvent.Port), - handler.NewCol(LDAPTlsCol, idpEvent.TLS), + handler.NewCol(LDAPServersCol, database.StringArray(idpEvent.Servers)), + handler.NewCol(LDAPStartTLSCol, idpEvent.StartTLS), handler.NewCol(LDAPBaseDNCol, idpEvent.BaseDN), - handler.NewCol(LDAPUserObjectClassCol, idpEvent.UserObjectClass), - handler.NewCol(LDAPUserUniqueAttributeCol, idpEvent.UserUniqueAttribute), - handler.NewCol(LDAPAdminCol, idpEvent.Admin), - handler.NewCol(LDAPPasswordCol, idpEvent.Password), + handler.NewCol(LDAPBindDNCol, idpEvent.BindDN), + handler.NewCol(LDAPBindPasswordCol, idpEvent.BindPassword), + handler.NewCol(LDAPUserBaseCol, idpEvent.UserBase), + handler.NewCol(LDAPUserObjectClassesCol, database.StringArray(idpEvent.UserObjectClasses)), + handler.NewCol(LDAPUserFiltersCol, database.StringArray(idpEvent.UserFilters)), + handler.NewCol(LDAPTimeoutCol, idpEvent.Timeout), handler.NewCol(LDAPIDAttributeCol, idpEvent.IDAttribute), handler.NewCol(LDAPFirstNameAttributeCol, idpEvent.FirstNameAttribute), handler.NewCol(LDAPLastNameAttributeCol, idpEvent.LastNameAttribute), @@ -1962,29 +1965,32 @@ func reduceGoogleIDPChangedColumns(idpEvent idp.GoogleIDPChangedEvent) []handler func reduceLDAPIDPChangedColumns(idpEvent idp.LDAPIDPChangedEvent) []handler.Column { ldapCols := make([]handler.Column, 0, 4) - if idpEvent.Host != nil { - ldapCols = append(ldapCols, handler.NewCol(LDAPHostCol, *idpEvent.Host)) + if idpEvent.Servers != nil { + ldapCols = append(ldapCols, handler.NewCol(LDAPServersCol, database.StringArray(idpEvent.Servers))) } - if idpEvent.Port != nil { - ldapCols = append(ldapCols, handler.NewCol(LDAPPortCol, *idpEvent.Port)) - } - if idpEvent.TLS != nil { - ldapCols = append(ldapCols, handler.NewCol(LDAPTlsCol, *idpEvent.TLS)) + if idpEvent.StartTLS != nil { + ldapCols = append(ldapCols, handler.NewCol(LDAPStartTLSCol, *idpEvent.StartTLS)) } if idpEvent.BaseDN != nil { ldapCols = append(ldapCols, handler.NewCol(LDAPBaseDNCol, *idpEvent.BaseDN)) } - if idpEvent.UserObjectClass != nil { - ldapCols = append(ldapCols, handler.NewCol(LDAPUserObjectClassCol, *idpEvent.UserObjectClass)) + if idpEvent.BindDN != nil { + ldapCols = append(ldapCols, handler.NewCol(LDAPBindDNCol, *idpEvent.BindDN)) } - if idpEvent.UserUniqueAttribute != nil { - ldapCols = append(ldapCols, handler.NewCol(LDAPUserUniqueAttributeCol, *idpEvent.UserUniqueAttribute)) + if idpEvent.BindPassword != nil { + ldapCols = append(ldapCols, handler.NewCol(LDAPBindPasswordCol, idpEvent.BindPassword)) } - if idpEvent.Admin != nil { - ldapCols = append(ldapCols, handler.NewCol(LDAPAdminCol, *idpEvent.Admin)) + if idpEvent.UserBase != nil { + ldapCols = append(ldapCols, handler.NewCol(LDAPUserBaseCol, *idpEvent.UserBase)) } - if idpEvent.Password != nil { - ldapCols = append(ldapCols, handler.NewCol(LDAPPasswordCol, *idpEvent.Password)) + if idpEvent.UserObjectClasses != nil { + ldapCols = append(ldapCols, handler.NewCol(LDAPUserObjectClassesCol, database.StringArray(idpEvent.UserObjectClasses))) + } + if idpEvent.UserFilters != nil { + ldapCols = append(ldapCols, handler.NewCol(LDAPUserFiltersCol, database.StringArray(idpEvent.UserFilters))) + } + if idpEvent.Timeout != nil { + ldapCols = append(ldapCols, handler.NewCol(LDAPTimeoutCol, *idpEvent.Timeout)) } if idpEvent.IDAttribute != nil { ldapCols = append(ldapCols, handler.NewCol(LDAPIDAttributeCol, *idpEvent.IDAttribute)) diff --git a/internal/query/projection/idp_template_test.go b/internal/query/projection/idp_template_test.go index 6e58a76edc..0b26770a8a 100644 --- a/internal/query/projection/idp_template_test.go +++ b/internal/query/projection/idp_template_test.go @@ -2,6 +2,7 @@ package projection import ( "testing" + "time" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" @@ -2033,18 +2034,19 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { []byte(`{ "id": "idp-id", "name": "custom-zitadel-instance", - "host": "host", - "port": "port", - "tls": true, - "baseDN": "base", - "userObjectClass": "user", - "userUniqueAttribute": "uid", - "admin": "admin", - "password": { + "servers": ["server"], + "startTls": false, + "baseDN": "basedn", + "bindDN": "binddn", + "bindPassword": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }, + "userBase": "user", + "userObjectClasses": ["object"], + "userFilters": ["filter"], + "timeout": 30000000000, "idAttribute": "id", "firstNameAttribute": "first", "lastNameAttribute": "last", @@ -2092,18 +2094,19 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.idp_templates4_ldap (idp_id, instance_id, host, port, tls, base_dn, user_object_class, user_unique_attribute, admin, password, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)", + expectedStmt: "INSERT INTO projections.idp_templates4_ldap2 (idp_id, instance_id, servers, start_tls, base_dn, bind_dn, bind_password, user_base, user_object_classes, user_filters, timeout, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)", expectedArgs: []interface{}{ "idp-id", "instance-id", - "host", - "port", - true, - "base", - "user", - "uid", - "admin", + database.StringArray{"server"}, + false, + "basedn", + "binddn", anyArg{}, + "user", + database.StringArray{"object"}, + database.StringArray{"filter"}, + time.Duration(30000000000), "id", "first", "last", @@ -2132,18 +2135,19 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { []byte(`{ "id": "idp-id", "name": "custom-zitadel-instance", - "host": "host", - "port": "port", - "tls": true, - "baseDN": "base", - "userObjectClass": "user", - "userUniqueAttribute": "uid", - "admin": "admin", - "password": { + "servers": ["server"], + "startTls": false, + "baseDN": "basedn", + "bindDN": "binddn", + "bindPassword": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }, + "userBase": "user", + "userObjectClasses": ["object"], + "userFilters": ["filter"], + "timeout": 30000000000, "idAttribute": "id", "firstNameAttribute": "first", "lastNameAttribute": "last", @@ -2191,18 +2195,19 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.idp_templates4_ldap (idp_id, instance_id, host, port, tls, base_dn, user_object_class, user_unique_attribute, admin, password, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)", + expectedStmt: "INSERT INTO projections.idp_templates4_ldap2 (idp_id, instance_id, servers, start_tls, base_dn, bind_dn, bind_password, user_base, user_object_classes, user_filters, timeout, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)", expectedArgs: []interface{}{ "idp-id", "instance-id", - "host", - "port", - true, - "base", - "user", - "uid", - "admin", + database.StringArray{"server"}, + false, + "basedn", + "binddn", anyArg{}, + "user", + database.StringArray{"object"}, + database.StringArray{"filter"}, + time.Duration(30000000000), "id", "first", "last", @@ -2231,7 +2236,7 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { []byte(`{ "id": "idp-id", "name": "custom-zitadel-instance", - "host": "host" + "baseDN": "basedn" }`), ), instance.LDAPIDPChangedEventMapper), }, @@ -2253,9 +2258,9 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.idp_templates4_ldap SET host = $1 WHERE (idp_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.idp_templates4_ldap2 SET base_dn = $1 WHERE (idp_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ - "host", + "basedn", "idp-id", "instance-id", }, @@ -2273,18 +2278,19 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { []byte(`{ "id": "idp-id", "name": "custom-zitadel-instance", - "host": "host", - "port": "port", - "tls": true, - "baseDN": "base", - "userObjectClass": "user", - "userUniqueAttribute": "uid", - "admin": "admin", - "password": { + "servers": ["server"], + "startTls": false, + "baseDN": "basedn", + "bindDN": "binddn", + "bindPassword": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }, + "userBase": "user", + "userObjectClasses": ["object"], + "userFilters": ["filter"], + "timeout": 30000000000, "idAttribute": "id", "firstNameAttribute": "first", "lastNameAttribute": "last", @@ -2327,16 +2333,17 @@ func TestIDPTemplateProjection_reducesLDAP(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.idp_templates4_ldap SET (host, port, tls, base_dn, user_object_class, user_unique_attribute, admin, password, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) WHERE (idp_id = $22) AND (instance_id = $23)", + expectedStmt: "UPDATE projections.idp_templates4_ldap2 SET (servers, start_tls, base_dn, bind_dn, bind_password, user_base, user_object_classes, user_filters, timeout, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22) WHERE (idp_id = $23) AND (instance_id = $24)", expectedArgs: []interface{}{ - "host", - "port", - true, - "base", - "user", - "uid", - "admin", + database.StringArray{"server"}, + false, + "basedn", + "binddn", anyArg{}, + "user", + database.StringArray{"object"}, + database.StringArray{"filter"}, + time.Duration(30000000000), "id", "first", "last", diff --git a/internal/repository/idp/ldap.go b/internal/repository/idp/ldap.go index 99e8bd1ff2..5115bc46ae 100644 --- a/internal/repository/idp/ldap.go +++ b/internal/repository/idp/ldap.go @@ -2,27 +2,28 @@ package idp import ( "encoding/json" + "time" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" - "github.com/zitadel/zitadel/internal/repository/idpconfig" ) type LDAPIDPAddedEvent struct { eventstore.BaseEvent `json:"-"` - ID string `json:"id"` - Name string `json:"name"` - Host string `json:"host"` - Port string `json:"port,omitempty"` - TLS bool `json:"tls"` - BaseDN string `json:"baseDN"` - UserObjectClass string `json:"userObjectClass"` - UserUniqueAttribute string `json:"userUniqueAttribute"` - Admin string `json:"admin"` - Password *crypto.CryptoValue `json:"password"` + ID string `json:"id"` + Name string `json:"name"` + Servers []string `json:"servers"` + StartTLS bool `json:"startTLS"` + BaseDN string `json:"baseDN"` + BindDN string `json:"bindDN"` + BindPassword *crypto.CryptoValue `json:"bindPassword"` + UserBase string `json:"userBase"` + UserObjectClasses []string `json:"userObjectClasses"` + UserFilters []string `json:"userFilters"` + Timeout time.Duration `json:"timeout"` LDAPAttributes Options @@ -132,33 +133,35 @@ func (o *LDAPAttributes) ReduceChanges(changes LDAPAttributeChanges) { func NewLDAPIDPAddedEvent( base *eventstore.BaseEvent, - id, - name, - host, - port string, - tls bool, - baseDN, - userObjectClass, - userUniqueAttribute, - admin string, - password *crypto.CryptoValue, + id string, + name string, + servers []string, + startTLS bool, + baseDN string, + bindDN string, + bindPassword *crypto.CryptoValue, + userBase string, + userObjectClasses []string, + userFilters []string, + timeout time.Duration, attributes LDAPAttributes, options Options, ) *LDAPIDPAddedEvent { return &LDAPIDPAddedEvent{ - BaseEvent: *base, - ID: id, - Name: name, - Host: host, - Port: port, - TLS: tls, - BaseDN: baseDN, - UserObjectClass: userObjectClass, - UserUniqueAttribute: userUniqueAttribute, - Admin: admin, - Password: password, - LDAPAttributes: attributes, - Options: options, + BaseEvent: *base, + ID: id, + Name: name, + Servers: servers, + StartTLS: startTLS, + BaseDN: baseDN, + BindDN: bindDN, + BindPassword: bindPassword, + UserBase: userBase, + UserObjectClasses: userObjectClasses, + UserFilters: userFilters, + Timeout: timeout, + LDAPAttributes: attributes, + Options: options, } } @@ -167,7 +170,7 @@ func (e *LDAPIDPAddedEvent) Data() interface{} { } func (e *LDAPIDPAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { - return []*eventstore.EventUniqueConstraint{idpconfig.NewAddIDPConfigNameUniqueConstraint(e.Name, e.Aggregate().ResourceOwner)} + return nil } func LDAPIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) { @@ -186,18 +189,17 @@ func LDAPIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) type LDAPIDPChangedEvent struct { eventstore.BaseEvent `json:"-"` - oldName string - - ID string `json:"id"` - Name *string `json:"name,omitempty"` - Host *string `json:"host,omitempty"` - Port *string `json:"port,omitempty"` - TLS *bool `json:"tls,omitempty"` - BaseDN *string `json:"baseDN,omitempty"` - UserObjectClass *string `json:"userObjectClass,omitempty"` - UserUniqueAttribute *string `json:"userUniqueAttribute,omitempty"` - Admin *string `json:"admin,omitempty"` - Password *crypto.CryptoValue `json:"password,omitempty"` + ID string `json:"id"` + Name *string `json:"name,omitempty"` + Servers []string `json:"servers,omitempty"` + StartTLS *bool `json:"startTLS,omitempty"` + BaseDN *string `json:"baseDN,omitempty"` + BindDN *string `json:"bindDN,omitempty"` + BindPassword *crypto.CryptoValue `json:"bindPassword,omitempty"` + UserBase *string `json:"userBase,omitempty"` + UserObjectClasses []string `json:"userObjectClasses,omitempty"` + UserFilters []string `json:"userFilters,omitempty"` + Timeout *time.Duration `json:"timeout,omitempty"` LDAPAttributeChanges OptionChanges @@ -238,7 +240,6 @@ func (o LDAPAttributeChanges) IsZero() bool { func NewLDAPIDPChangedEvent( base *eventstore.BaseEvent, id string, - oldName string, changes []LDAPIDPChanges, ) (*LDAPIDPChangedEvent, error) { if len(changes) == 0 { @@ -247,7 +248,6 @@ func NewLDAPIDPChangedEvent( changedEvent := &LDAPIDPChangedEvent{ BaseEvent: *base, ID: id, - oldName: oldName, } for _, change := range changes { change(changedEvent) @@ -263,51 +263,57 @@ func ChangeLDAPName(name string) func(*LDAPIDPChangedEvent) { } } -func ChangeLDAPHost(host string) func(*LDAPIDPChangedEvent) { +func ChangeLDAPServers(servers []string) func(*LDAPIDPChangedEvent) { return func(e *LDAPIDPChangedEvent) { - e.Host = &host + e.Servers = servers } } -func ChangeLDAPPort(port string) func(*LDAPIDPChangedEvent) { +func ChangeLDAPStartTLS(startTls bool) func(*LDAPIDPChangedEvent) { return func(e *LDAPIDPChangedEvent) { - e.Port = &port + e.StartTLS = &startTls } } -func ChangeLDAPTLS(tls bool) func(*LDAPIDPChangedEvent) { +func ChangeLDAPBaseDN(baseDN string) func(*LDAPIDPChangedEvent) { return func(e *LDAPIDPChangedEvent) { - e.TLS = &tls + e.BaseDN = &baseDN } } -func ChangeLDAPBaseDN(basDN string) func(*LDAPIDPChangedEvent) { +func ChangeLDAPBindDN(bindDN string) func(*LDAPIDPChangedEvent) { return func(e *LDAPIDPChangedEvent) { - e.BaseDN = &basDN + e.BindDN = &bindDN } } -func ChangeLDAPUserObjectClass(userObjectClass string) func(*LDAPIDPChangedEvent) { +func ChangeLDAPBindPassword(password *crypto.CryptoValue) func(*LDAPIDPChangedEvent) { return func(e *LDAPIDPChangedEvent) { - e.UserObjectClass = &userObjectClass + e.BindPassword = password } } -func ChangeLDAPUserUniqueAttribute(userUniqueAttribute string) func(*LDAPIDPChangedEvent) { +func ChangeLDAPUserBase(userBase string) func(*LDAPIDPChangedEvent) { return func(e *LDAPIDPChangedEvent) { - e.UserUniqueAttribute = &userUniqueAttribute + e.UserBase = &userBase } } -func ChangeLDAPAdmin(admin string) func(*LDAPIDPChangedEvent) { +func ChangeLDAPUserObjectClasses(objectClasses []string) func(*LDAPIDPChangedEvent) { return func(e *LDAPIDPChangedEvent) { - e.Admin = &admin + e.UserObjectClasses = objectClasses } } -func ChangeLDAPPassword(password *crypto.CryptoValue) func(*LDAPIDPChangedEvent) { +func ChangeLDAPUserFilters(userFilters []string) func(*LDAPIDPChangedEvent) { return func(e *LDAPIDPChangedEvent) { - e.Password = password + e.UserFilters = userFilters + } +} + +func ChangeLDAPTimeout(timeout time.Duration) func(*LDAPIDPChangedEvent) { + return func(e *LDAPIDPChangedEvent) { + e.Timeout = &timeout } } @@ -328,13 +334,7 @@ func (e *LDAPIDPChangedEvent) Data() interface{} { } func (e *LDAPIDPChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { - if e.Name == nil || e.oldName == *e.Name { // TODO: nil check should be enough? - return nil - } - return []*eventstore.EventUniqueConstraint{ - idpconfig.NewRemoveIDPConfigNameUniqueConstraint(e.oldName, e.Aggregate().ResourceOwner), - idpconfig.NewAddIDPConfigNameUniqueConstraint(*e.Name, e.Aggregate().ResourceOwner), - } + return nil } func LDAPIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) { diff --git a/internal/repository/instance/idp.go b/internal/repository/instance/idp.go index bf34360e7b..612933f0ee 100644 --- a/internal/repository/instance/idp.go +++ b/internal/repository/instance/idp.go @@ -2,6 +2,7 @@ package instance import ( "context" + "time" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" @@ -28,8 +29,8 @@ const ( GitLabSelfHostedIDPChangedEventType eventstore.EventType = "instance.idp.gitlab_self_hosted.changed" GoogleIDPAddedEventType eventstore.EventType = "instance.idp.google.added" GoogleIDPChangedEventType eventstore.EventType = "instance.idp.google.changed" - LDAPIDPAddedEventType eventstore.EventType = "instance.idp.ldap.added" - LDAPIDPChangedEventType eventstore.EventType = "instance.idp.ldap.changed" + LDAPIDPAddedEventType eventstore.EventType = "instance.idp.ldap.v2.added" + LDAPIDPChangedEventType eventstore.EventType = "instance.idp.ldap.v2.changed" IDPRemovedEventType eventstore.EventType = "instance.idp.removed" ) @@ -751,15 +752,16 @@ func NewLDAPIDPAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate, id, - name, - host, - port string, - tls bool, - baseDN, - userObjectClass, - userUniqueAttribute, - admin string, - password *crypto.CryptoValue, + name string, + servers []string, + startTLS bool, + baseDN string, + bindDN string, + bindPassword *crypto.CryptoValue, + userBase string, + userObjectClasses []string, + userFilters []string, + timeout time.Duration, attributes idp.LDAPAttributes, options idp.Options, ) *LDAPIDPAddedEvent { @@ -773,14 +775,15 @@ func NewLDAPIDPAddedEvent( ), id, name, - host, - port, - tls, + servers, + startTLS, baseDN, - userObjectClass, - userUniqueAttribute, - admin, - password, + bindDN, + bindPassword, + userBase, + userObjectClasses, + userFilters, + timeout, attributes, options, ), @@ -803,8 +806,7 @@ type LDAPIDPChangedEvent struct { func NewLDAPIDPChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, - id, - oldName string, + id string, changes []idp.LDAPIDPChanges, ) (*LDAPIDPChangedEvent, error) { @@ -815,7 +817,6 @@ func NewLDAPIDPChangedEvent( LDAPIDPChangedEventType, ), id, - oldName, changes, ) if err != nil { diff --git a/internal/repository/org/idp.go b/internal/repository/org/idp.go index a5a0204b1a..97b0ebffc1 100644 --- a/internal/repository/org/idp.go +++ b/internal/repository/org/idp.go @@ -2,6 +2,7 @@ package org import ( "context" + "time" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" @@ -751,15 +752,16 @@ func NewLDAPIDPAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate, id, - name, - host, - port string, - tls bool, - baseDN, - userObjectClass, - userUniqueAttribute, - admin string, - password *crypto.CryptoValue, + name string, + servers []string, + startTLS bool, + baseDN string, + bindDN string, + bindPassword *crypto.CryptoValue, + userBase string, + userObjectClasses []string, + userFilters []string, + timeout time.Duration, attributes idp.LDAPAttributes, options idp.Options, ) *LDAPIDPAddedEvent { @@ -773,14 +775,15 @@ func NewLDAPIDPAddedEvent( ), id, name, - host, - port, - tls, + servers, + startTLS, baseDN, - userObjectClass, - userUniqueAttribute, - admin, - password, + bindDN, + bindPassword, + userBase, + userObjectClasses, + userFilters, + timeout, attributes, options, ), @@ -803,8 +806,7 @@ type LDAPIDPChangedEvent struct { func NewLDAPIDPChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, - id, - oldName string, + id string, changes []idp.LDAPIDPChanges, ) (*LDAPIDPChangedEvent, error) { @@ -815,7 +817,6 @@ func NewLDAPIDPChangedEvent( LDAPIDPChangedEventType, ), id, - oldName, changes, ) if err != nil { diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index fd0cab5ee7..beae4750ec 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -4732,16 +4732,17 @@ message UpdateGoogleProviderResponse { message AddLDAPProviderRequest { string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string host = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string port = 3 [(validate.rules).string = {max_len: 5}]; - bool tls = 4; - string base_dn = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string user_object_class = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string user_unique_attribute = 7 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string admin = 8 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string password = 9 [(validate.rules).string = {min_len: 1, max_len: 200}]; - zitadel.idp.v1.LDAPAttributes attributes = 10; - zitadel.idp.v1.Options provider_options = 11; + repeated string servers = 2 [(validate.rules).repeated = {min_items: 1, max_items: 20, items: {string: {min_len: 1, max_len: 200}}}]; + bool start_tls = 3; + string base_dn = 4 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string bind_dn = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string bind_password = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string user_base = 7 [(validate.rules).string = {min_len: 1, max_len: 200}]; + repeated string user_object_classes = 8 [(validate.rules).repeated = {min_items: 1, max_items: 20, items: {string: {min_len: 1, max_len: 200}}}]; + repeated string user_filters = 9 [(validate.rules).repeated = {min_items: 1, max_items: 20, items: {string: {min_len: 1, max_len: 200}}}]; + google.protobuf.Duration timeout = 10; + zitadel.idp.v1.LDAPAttributes attributes = 11; + zitadel.idp.v1.Options provider_options = 12; } message AddLDAPProviderResponse { @@ -4752,16 +4753,17 @@ message AddLDAPProviderResponse { message UpdateLDAPProviderRequest { string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; string name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string host = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string port = 4 [(validate.rules).string = {max_len: 5}]; - bool tls = 5; - string base_dn = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string user_object_class = 7 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string user_unique_attribute = 8 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string admin = 9 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string password = 10 [(validate.rules).string = {max_len: 200}]; - zitadel.idp.v1.LDAPAttributes attributes = 11; - zitadel.idp.v1.Options provider_options = 12; + repeated string servers = 3 [(validate.rules).repeated = {min_items: 1, max_items: 20, items: {string: {min_len: 1, max_len: 200}}}]; + bool start_tls = 4; + string base_dn = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string bind_dn = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string bind_password = 7 [(validate.rules).string = {max_len: 200}]; + string user_base = 8 [(validate.rules).string = {min_len: 1, max_len: 200}]; + repeated string user_object_classes = 9 [(validate.rules).repeated = {min_items: 1, max_items: 20, items: {string: {min_len: 1, max_len: 200}}}]; + repeated string user_filters = 10 [(validate.rules).repeated = {min_items: 1, max_items: 20, items: {string: {min_len: 1, max_len: 200}}}]; + google.protobuf.Duration timeout = 11; + zitadel.idp.v1.LDAPAttributes attributes = 12; + zitadel.idp.v1.Options provider_options = 13; } message UpdateLDAPProviderResponse { diff --git a/proto/zitadel/idp.proto b/proto/zitadel/idp.proto index 9cf248c7c2..fb9c154391 100644 --- a/proto/zitadel/idp.proto +++ b/proto/zitadel/idp.proto @@ -3,6 +3,7 @@ syntax = "proto3"; import "zitadel/object.proto"; import "validate/validate.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/duration.proto"; package zitadel.idp.v1; @@ -321,15 +322,15 @@ message GitLabSelfHostedConfig { } message LDAPConfig { - string host = 1; - string port = 2; - bool tls = 3; - string base_dn = 4; - string user_object_class = 5; - string user_unique_attribute = 6; - string admin = 7; - LDAPAttributes attributes = 8; - Options provider_options = 9; + repeated string servers = 1; + bool start_tls = 2; + string base_dn = 3; + string bind_dn = 4; + string user_base = 5; + repeated string user_object_classes = 6; + repeated string user_filters = 7; + google.protobuf.Duration timeout = 8; + LDAPAttributes attributes = 9; } message AzureADConfig { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 788340a69f..73fe75eb4a 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -11406,16 +11406,17 @@ message UpdateGoogleProviderResponse { message AddLDAPProviderRequest { string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string host = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string port = 3 [(validate.rules).string = {max_len: 5}]; - bool tls = 4; - string base_dn = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string user_object_class = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string user_unique_attribute = 7 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string admin = 8 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string password = 9 [(validate.rules).string = {min_len: 1, max_len: 200}]; - zitadel.idp.v1.LDAPAttributes attributes = 10; - zitadel.idp.v1.Options provider_options = 11; + repeated string servers = 2 [(validate.rules).repeated = {min_items: 1, max_items: 20, items: {string: {min_len: 1, max_len: 200}}}]; + bool start_tls = 3; + string base_dn = 4 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string bind_dn = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string bind_password = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string user_base = 7 [(validate.rules).string = {min_len: 1, max_len: 200}]; + repeated string user_object_classes = 8 [(validate.rules).repeated = {min_items: 1, max_items: 20, items: {string: {min_len: 1, max_len: 200}}}]; + repeated string user_filters = 9 [(validate.rules).repeated = {min_items: 1, max_items: 20, items: {string: {min_len: 1, max_len: 200}}}]; + google.protobuf.Duration timeout = 10; + zitadel.idp.v1.LDAPAttributes attributes = 11; + zitadel.idp.v1.Options provider_options = 12; } message AddLDAPProviderResponse { @@ -11426,16 +11427,17 @@ message AddLDAPProviderResponse { message UpdateLDAPProviderRequest { string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; string name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string host = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string port = 4 [(validate.rules).string = {max_len: 5}]; - bool tls = 5; - string base_dn = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string user_object_class = 7 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string user_unique_attribute = 8 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string admin = 9 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string password = 10 [(validate.rules).string = {max_len: 200}]; - zitadel.idp.v1.LDAPAttributes attributes = 11; - zitadel.idp.v1.Options provider_options = 12; + repeated string servers = 3 [(validate.rules).repeated = {min_items: 1, max_items: 20, items: {string: {min_len: 1, max_len: 200}}}]; + bool start_tls = 4; + string base_dn = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string bind_dn = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string bind_password = 7 [(validate.rules).string = {max_len: 200}]; + string user_base = 8 [(validate.rules).string = {min_len: 1, max_len: 200}]; + repeated string user_object_classes = 9 [(validate.rules).repeated = {min_items: 1, max_items: 20, items: {string: {min_len: 1, max_len: 200}}}]; + repeated string user_filters = 10 [(validate.rules).repeated = {min_items: 1, max_items: 20, items: {string: {min_len: 1, max_len: 200}}}]; + google.protobuf.Duration timeout = 11; + zitadel.idp.v1.LDAPAttributes attributes = 12; + zitadel.idp.v1.Options provider_options = 13; } message UpdateLDAPProviderResponse {