From 73c82a012ea8b34fe46101813e8e16ed9ea11f17 Mon Sep 17 00:00:00 2001 From: broccoli Date: Mon, 12 May 2025 14:30:59 +0200 Subject: [PATCH 01/10] feat: init commit --- connector-ldap/i18n/en_US.yaml | 28 ++++++++++++++++++++++++++++ connector-ldap/i18n/translation.go | 26 ++++++++++++++++++++++++++ connector-ldap/info.yaml | 22 ++++++++++++++++++++++ connector-ldap/ldap.go | 17 +++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 connector-ldap/i18n/en_US.yaml create mode 100644 connector-ldap/i18n/translation.go create mode 100644 connector-ldap/info.yaml create mode 100644 connector-ldap/ldap.go diff --git a/connector-ldap/i18n/en_US.yaml b/connector-ldap/i18n/en_US.yaml new file mode 100644 index 000000000..57618f56c --- /dev/null +++ b/connector-ldap/i18n/en_US.yaml @@ -0,0 +1,28 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +plugin: + ldap_connector: + backend: + name: + other: LDAP + info: + name: + other: LDAP Connector + description: + other: Connect to LDAP for third-party login + \ No newline at end of file diff --git a/connector-ldap/i18n/translation.go b/connector-ldap/i18n/translation.go new file mode 100644 index 000000000..8fbed9c05 --- /dev/null +++ b/connector-ldap/i18n/translation.go @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package i18n + +const ( + ConnectorName = "plugin.github_connector.backend.name" + InfoName = "plugin.github_connector.backend.info.name" + InfoDescription = "plugin.github_connector.backend.info.description" +) diff --git a/connector-ldap/info.yaml b/connector-ldap/info.yaml new file mode 100644 index 000000000..621212709 --- /dev/null +++ b/connector-ldap/info.yaml @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +slug_name: ldap_connector +type: connector +version: 0.1.0 +author: DanielAuerX +link: https://github.com/apache/answer-plugins/tree/main/connector-ldap diff --git a/connector-ldap/ldap.go b/connector-ldap/ldap.go new file mode 100644 index 000000000..0a37f6047 --- /dev/null +++ b/connector-ldap/ldap.go @@ -0,0 +1,17 @@ +package ldap + +import "github.com/apache/answer-plugins/connector-ldap/i18n" + +func (g *Connector) Info() plugin.Info { + info := &util.Info{} + info.GetInfo(Info) + + return plugin.Info{ + Name: plugin.MakeTranslator(i18n.InfoName), + SlugName: info.SlugName, + Description: plugin.MakeTranslator(i18n.InfoDescription), + Author: info.Author, + Version: info.Version, + Link: info.Link, + } +} From 670aeda84c498802139587e3ae3780608dc62d86 Mon Sep 17 00:00:00 2001 From: broccoli Date: Mon, 12 May 2025 22:24:14 +0200 Subject: [PATCH 02/10] feat: basic implementation --- connector-ldap/i18n/translation.go | 6 +- connector-ldap/ldap.go | 129 ++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/connector-ldap/i18n/translation.go b/connector-ldap/i18n/translation.go index 8fbed9c05..98cf62f46 100644 --- a/connector-ldap/i18n/translation.go +++ b/connector-ldap/i18n/translation.go @@ -20,7 +20,7 @@ package i18n const ( - ConnectorName = "plugin.github_connector.backend.name" - InfoName = "plugin.github_connector.backend.info.name" - InfoDescription = "plugin.github_connector.backend.info.description" + ConnectorName = "plugin.ldap_connector.backend.name" + InfoName = "plugin.ldap_connector.backend.info.name" + InfoDescription = "plugin.ldap_connector.backend.info.description" ) diff --git a/connector-ldap/ldap.go b/connector-ldap/ldap.go index 0a37f6047..07fd596fd 100644 --- a/connector-ldap/ldap.go +++ b/connector-ldap/ldap.go @@ -1,6 +1,38 @@ package ldap -import "github.com/apache/answer-plugins/connector-ldap/i18n" +import ( + "embed" + "encoding/json" + "fmt" + + "github.com/apache/answer-plugins/connector-ldap/i18n" + "github.com/apache/answer-plugins/util" + "github.com/apache/answer/plugin" + "github.com/go-ldap/ldap/v3" +) + +// TODO: sanitization (e.g. username) +// TODO: email and display name lookup from ldap? +var Info embed.FS + +type Connector struct { + Config *ConnectorConfig +} + +type ConnectorConfig struct { + Name string `json:"name"` + Server string `json:"server"` + BaseDN string `json:"base_dn"` + BindPrefix string `json:"bind_prefix"` +} + +var _ plugin.Connector = &Connector{} + +func init() { + plugin.Register(&Connector{ + Config: &ConnectorConfig{}, + }) +} func (g *Connector) Info() plugin.Info { info := &util.Info{} @@ -15,3 +47,98 @@ func (g *Connector) Info() plugin.Info { Link: info.Link, } } + +func (g *Connector) ConnectorName() plugin.Translator { + if g.Config.Name != "" { + return plugin.MakeTranslator(g.Config.Name) + } + return plugin.MakeTranslator(i18n.ConnectorName) +} + +// get from info.yaml? != ldap +func (g *Connector) ConnectorSlugName() string { + return "ldap" +} + +// TODO: SVG support? +func (g *Connector) ConnectorLogoSVG() string { + return "" +} + +// TODO get from translator +func (g *Connector) ConfigFields() []plugin.ConfigField { + return []plugin.ConfigField{ + createTextInput("name", "LDAP", "LDAP connector name", g.Config.Name, true), + createTextInput("server", "LDAP Server", "e.g. ldap.example.com:389", g.Config.Server, true), + createTextInput("base_dn", "Base DN", "e.g. dc=example,dc=com", g.Config.BaseDN, true), + createTextInput("bind_prefix", "Bind Prefix", "e.g. CN= or uid=", g.Config.BindPrefix, false), + } +} + +func (g *Connector) ConfigReceiver(config []byte) error { + c := &ConnectorConfig{} + if err := json.Unmarshal(config, c); err != nil { + return err + } + g.Config = c + return nil +} + +func (g *Connector) ConnectorReceiver(ctx *plugin.GinContext, receiverURL string) (plugin.ExternalLoginUserInfo, error) { + var userInfo plugin.ExternalLoginUserInfo + + username := ctx.Request.FormValue("username") + password := ctx.Request.FormValue("password") + if username == "" || password == "" { + return userInfo, fmt.Errorf("missing username or password") + } + + bindDN := fmt.Sprintf("%s%s,%s", g.Config.BindPrefix, username, g.Config.BaseDN) + + err := ldapAuthenticate(g.Config.Server, g.Config.BaseDN, bindDN, password) + if err != nil { + return userInfo, fmt.Errorf("LDAP auth failed: %s", err) + } + + // returning to answer core + userInfo = plugin.ExternalLoginUserInfo{ + ExternalID: bindDN, + DisplayName: username, + Username: username, + Email: fmt.Sprintf("%s@example.com", username), // optional, needed? + MetaInfo: fmt.Sprintf("LDAP user %s", username), + } + return userInfo, nil +} + +func ldapAuthenticate(server, baseDN, bindDN, password string) error { + l, err := ldap.Dial("tcp", server) + if err != nil { + return err + } + defer l.Close() + + // bind with user credentials + err = l.Bind(bindDN, password) + if err != nil { + return err + } + + // search user info? + + return nil +} + +func createTextInput(name, title, desc, value string, require bool) plugin.ConfigField { + return plugin.ConfigField{ + Name: name, + Type: plugin.ConfigTypeInput, + Title: plugin.MakeTranslator(title), + Description: plugin.MakeTranslator(desc), + Required: require, + UIOptions: plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + }, + Value: value, + } +} From 25c3e13cf955c70276d779e8c3c56f173e8d7e81 Mon Sep 17 00:00:00 2001 From: broccoli Date: Sat, 17 May 2025 13:01:01 +0200 Subject: [PATCH 03/10] feat: working prototype --- connector-ldap/go.mod | 9 ++ connector-ldap/ldap.go | 183 ++++++++++++++++++++++++++++---------- connector-ldap/login.html | 14 +++ 3 files changed, 161 insertions(+), 45 deletions(-) create mode 100644 connector-ldap/go.mod create mode 100644 connector-ldap/login.html diff --git a/connector-ldap/go.mod b/connector-ldap/go.mod new file mode 100644 index 000000000..7bd749042 --- /dev/null +++ b/connector-ldap/go.mod @@ -0,0 +1,9 @@ +module github.com/DanielAuerX/answer-plugins/connector-ldap + +go 1.22 + +require ( + github.com/apache/answer v1.4.2-RC1.0.20250107023923-061894735091 + github.com/apache/answer-plugins/util v1.0.3-0.20250107030257-cf94ebc70954 +) + diff --git a/connector-ldap/ldap.go b/connector-ldap/ldap.go index 07fd596fd..db1a50d99 100644 --- a/connector-ldap/ldap.go +++ b/connector-ldap/ldap.go @@ -4,26 +4,43 @@ import ( "embed" "encoding/json" "fmt" + "net/http" + + "github.com/DanielAuerX/answer-plugins/connector-ldap/i18n" + "github.com/segmentfault/pacman/log" - "github.com/apache/answer-plugins/connector-ldap/i18n" "github.com/apache/answer-plugins/util" "github.com/apache/answer/plugin" "github.com/go-ldap/ldap/v3" ) -// TODO: sanitization (e.g. username) -// TODO: email and display name lookup from ldap? +//go:embed info.yaml var Info embed.FS +//go:embed login.html +var loginHTML embed.FS + +const ( + LdapAttributeDn = "dn" + LdapAttributeUid = "uid" + LdapAttributeCn = "cn" + LdapAttributeMail = "mail" + LdapAttributeDisplayName = "displayName" + LdapAttributeSamAccountName = "sAMAccountName" +) + type Connector struct { Config *ConnectorConfig } type ConnectorConfig struct { - Name string `json:"name"` - Server string `json:"server"` - BaseDN string `json:"base_dn"` - BindPrefix string `json:"bind_prefix"` + Name string `json:"name"` + Server string `json:"server"` + BaseDN string `json:"base_dn"` + BindPrefix string `json:"bind_prefix"` // e.g., uid= + BindDN string `json:"bind_dn"` // service account DN + BindPassword string `json:"bind_password"` // service account password + UserAttr string `json:"user_attr"` // e.g., uid, sAMAccountName } var _ plugin.Connector = &Connector{} @@ -55,23 +72,42 @@ func (g *Connector) ConnectorName() plugin.Translator { return plugin.MakeTranslator(i18n.ConnectorName) } -// get from info.yaml? != ldap func (g *Connector) ConnectorSlugName() string { return "ldap" } -// TODO: SVG support? func (g *Connector) ConnectorLogoSVG() string { return "" } +func (g *Connector) ConnectorSender(ctx *plugin.GinContext, receiverURL string) string { + log.Info("LDAP connector ConnectorSender...") + + htmlContent, err := loginHTML.ReadFile("login.html") + if err != nil { + log.Errorf("failed to read embedded html file: %v", err) + ctx.Writer.WriteHeader(500) + ctx.Writer.Write([]byte("Internal Server Error")) + return "" + } + + ctx.Writer.WriteHeader(200) + ctx.Writer.Header().Set("Content-Type", "text/html") + _, _ = ctx.Writer.Write([]byte(fmt.Sprintf(string(htmlContent), receiverURL))) + + return ctx.Request.Host +} + // TODO get from translator func (g *Connector) ConfigFields() []plugin.ConfigField { return []plugin.ConfigField{ - createTextInput("name", "LDAP", "LDAP connector name", g.Config.Name, true), - createTextInput("server", "LDAP Server", "e.g. ldap.example.com:389", g.Config.Server, true), - createTextInput("base_dn", "Base DN", "e.g. dc=example,dc=com", g.Config.BaseDN, true), - createTextInput("bind_prefix", "Bind Prefix", "e.g. CN= or uid=", g.Config.BindPrefix, false), + createTextInput("name", "LDAP", "LDAP connector name", g.Config.Name, true, false), + createTextInput("server", "LDAP Server", "e.g. ldap.example.com:389", g.Config.Server, true, false), + createTextInput("base_dn", "Base DN", "e.g. dc=example,dc=com", g.Config.BaseDN, true, false), + createTextInput("bind_prefix", "Bind Prefix", "e.g. CN= or uid=", g.Config.BindPrefix, false, false), //TODO NOT USED YET + createTextInput("bind_dn", "Bind DN", "DN of LDAP bind user", g.Config.BindDN, true, false), + createTextInput("bind_password", "Bind Password", "Password for bind DN", g.Config.BindPassword, true, true), + createTextInput("user_attr", "User Attribute", "LDAP attribute for username (e.g., uid or sAMAccountName)", g.Config.UserAttr, true, false), } } @@ -84,61 +120,118 @@ func (g *Connector) ConfigReceiver(config []byte) error { return nil } -func (g *Connector) ConnectorReceiver(ctx *plugin.GinContext, receiverURL string) (plugin.ExternalLoginUserInfo, error) { - var userInfo plugin.ExternalLoginUserInfo +func (c *Connector) ConnectorReceiver(ctx *plugin.GinContext, receiverURL string) (userInfo plugin.ExternalLoginUserInfo, err error) { + log.Info("ConnectorReceiver called!") - username := ctx.Request.FormValue("username") - password := ctx.Request.FormValue("password") - if username == "" || password == "" { - return userInfo, fmt.Errorf("missing username or password") + username, password, err := extractCredentials(ctx.Request) + if err != nil { + return userInfo, err } - bindDN := fmt.Sprintf("%s%s,%s", g.Config.BindPrefix, username, g.Config.BaseDN) + l, err := ldap.DialURL(c.Config.Server) + if err != nil { + return userInfo, fmt.Errorf("failed to connect to LDAP server: %w", err) + } + defer l.Close() - err := ldapAuthenticate(g.Config.Server, g.Config.BaseDN, bindDN, password) + err = l.Bind(c.Config.BindDN, c.Config.BindPassword) if err != nil { - return userInfo, fmt.Errorf("LDAP auth failed: %s", err) + return userInfo, fmt.Errorf("bind failed: %w", err) } - // returning to answer core - userInfo = plugin.ExternalLoginUserInfo{ - ExternalID: bindDN, - DisplayName: username, - Username: username, - Email: fmt.Sprintf("%s@example.com", username), // optional, needed? - MetaInfo: fmt.Sprintf("LDAP user %s", username), + searchRequest := ldap.NewSearchRequest( + c.Config.BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, + fmt.Sprintf("(%s=%s)", c.Config.UserAttr, ldap.EscapeFilter(username)), + []string{LdapAttributeDn, LdapAttributeUid, LdapAttributeCn, LdapAttributeMail, LdapAttributeDisplayName, LdapAttributeSamAccountName}, + nil, + ) + + sr, err := l.Search(searchRequest) + if err != nil || len(sr.Entries) == 0 { + return userInfo, fmt.Errorf("user not found: %w", err) } + + entry := sr.Entries[0] + + err = l.Bind(entry.DN, password) + if err != nil { + return userInfo, fmt.Errorf("invalid username or password") + } + + userInfo = extractUserInfo(entry) + + log.Infof("userInfo %s", &userInfo) + return userInfo, nil } -func ldapAuthenticate(server, baseDN, bindDN, password string) error { - l, err := ldap.Dial("tcp", server) - if err != nil { - return err +func extractCredentials(request *http.Request) (username string, password string, err error) { + queryParams := request.URL.Query() + + username = queryParams.Get("username") + password = queryParams.Get("password") + + if username == "" || password == "" { + log.Errorf("missing username or password") + err = fmt.Errorf("missing username or password") } - defer l.Close() + return +} - // bind with user credentials - err = l.Bind(bindDN, password) - if err != nil { - return err +func extractUserInfo(entry *ldap.Entry) plugin.ExternalLoginUserInfo { + + displayName := entry.GetAttributeValue(LdapAttributeDisplayName) + log.Infof("displayName %s", displayName) + + if displayName == "" { + displayName = entry.GetAttributeValue(LdapAttributeCn) } - // search user info? + username := entry.GetAttributeValue(LdapAttributeUid) + if username == "" { + username = entry.GetAttributeValue(LdapAttributeSamAccountName) + } + log.Infof("username %s", &username) - return nil + externalID := username + if externalID == "" { + externalID = entry.DN // fallback + } + + /* + email is used to login, therefore required. + wether the email is correct, is not important for our use case + */ + email := entry.GetAttributeValue(LdapAttributeMail) + if email == "" { + email = username + "@dummymail.xyz" + } + + return plugin.ExternalLoginUserInfo{ + ExternalID: externalID, + DisplayName: displayName, + Username: username, + Email: email, + } } -func createTextInput(name, title, desc, value string, require bool) plugin.ConfigField { +func createTextInput(name, title, desc, value string, require bool, password bool) plugin.ConfigField { + uiOptions := plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypeText, + } + if password { + uiOptions = plugin.ConfigFieldUIOptions{ + InputType: plugin.InputTypePassword, + } + } return plugin.ConfigField{ Name: name, Type: plugin.ConfigTypeInput, Title: plugin.MakeTranslator(title), Description: plugin.MakeTranslator(desc), Required: require, - UIOptions: plugin.ConfigFieldUIOptions{ - InputType: plugin.InputTypeText, - }, - Value: value, + UIOptions: uiOptions, + Value: value, } } diff --git a/connector-ldap/login.html b/connector-ldap/login.html new file mode 100644 index 000000000..f1f331fe5 --- /dev/null +++ b/connector-ldap/login.html @@ -0,0 +1,14 @@ + +LDAP Login + +

LDAP Login

+
+
+
+
+
+
+ +
+ + \ No newline at end of file From d8d4beef1062ea80b0ac9ad679567e55f2fc7f0f Mon Sep 17 00:00:00 2001 From: broccoli Date: Sat, 17 May 2025 13:01:01 +0200 Subject: [PATCH 04/10] feat: refacorting and ldaps + removed logs + using login.html + implemented ldaps: user can set a cert file for private ca --- connector-ldap/ldap.go | 184 +++++++++++++++++++++++++++----------- connector-ldap/login.html | 29 +++--- 2 files changed, 151 insertions(+), 62 deletions(-) diff --git a/connector-ldap/ldap.go b/connector-ldap/ldap.go index db1a50d99..e5e58c6b1 100644 --- a/connector-ldap/ldap.go +++ b/connector-ldap/ldap.go @@ -1,10 +1,14 @@ package ldap import ( + "crypto/tls" + "crypto/x509" "embed" "encoding/json" "fmt" "net/http" + "os" + "strings" "github.com/DanielAuerX/answer-plugins/connector-ldap/i18n" "github.com/segmentfault/pacman/log" @@ -34,21 +38,32 @@ type Connector struct { } type ConnectorConfig struct { - Name string `json:"name"` - Server string `json:"server"` - BaseDN string `json:"base_dn"` - BindPrefix string `json:"bind_prefix"` // e.g., uid= - BindDN string `json:"bind_dn"` // service account DN - BindPassword string `json:"bind_password"` // service account password - UserAttr string `json:"user_attr"` // e.g., uid, sAMAccountName + Name string `json:"name"` + Server string `json:"server"` + BaseDN string `json:"base_dn"` + BindDN string `json:"bind_dn"` + BindPassword string `json:"bind_password"` + UserAttr string `json:"user_attr"` + TLSCACertPath string `json:"tls_ca_cert_path"` } var _ plugin.Connector = &Connector{} +var loginHTMLContent string + func init() { plugin.Register(&Connector{ Config: &ConnectorConfig{}, }) + + htmlContent, err := loginHTML.ReadFile("login.html") + if err != nil { + log.Errorf("failed to read embedded html file: %v", err) + } + loginHTMLContent = string(htmlContent) + if "" == loginHTMLContent { + log.Error("html file is empty") + } } func (g *Connector) Info() plugin.Info { @@ -74,6 +89,7 @@ func (g *Connector) ConnectorName() plugin.Translator { func (g *Connector) ConnectorSlugName() string { return "ldap" + } func (g *Connector) ConnectorLogoSVG() string { @@ -81,108 +97,123 @@ func (g *Connector) ConnectorLogoSVG() string { } func (g *Connector) ConnectorSender(ctx *plugin.GinContext, receiverURL string) string { - log.Info("LDAP connector ConnectorSender...") - htmlContent, err := loginHTML.ReadFile("login.html") + htmlContent := strings.Replace(loginHTMLContent, "RECEIVER_URL_PLACEHOLDER", receiverURL, -1) + ctx.Writer.WriteHeader(200) + ctx.Writer.Header().Set("Content-Type", "text/html") + err := writeHtmlContent(ctx, htmlContent) if err != nil { - log.Errorf("failed to read embedded html file: %v", err) - ctx.Writer.WriteHeader(500) - ctx.Writer.Write([]byte("Internal Server Error")) - return "" + log.Errorf("failed to write HTML response: %v", err) } + return "" +} + +func writeHtmlContent(ctx *plugin.GinContext, htmlContent string) error { ctx.Writer.WriteHeader(200) ctx.Writer.Header().Set("Content-Type", "text/html") - _, _ = ctx.Writer.Write([]byte(fmt.Sprintf(string(htmlContent), receiverURL))) - - return ctx.Request.Host + _, err := ctx.Writer.Write([]byte(htmlContent)) + return err } // TODO get from translator func (g *Connector) ConfigFields() []plugin.ConfigField { return []plugin.ConfigField{ createTextInput("name", "LDAP", "LDAP connector name", g.Config.Name, true, false), - createTextInput("server", "LDAP Server", "e.g. ldap.example.com:389", g.Config.Server, true, false), + createTextInput("server", "LDAP Server", "e.g. ldaps://ldap.example.com:636", g.Config.Server, true, false), createTextInput("base_dn", "Base DN", "e.g. dc=example,dc=com", g.Config.BaseDN, true, false), - createTextInput("bind_prefix", "Bind Prefix", "e.g. CN= or uid=", g.Config.BindPrefix, false, false), //TODO NOT USED YET createTextInput("bind_dn", "Bind DN", "DN of LDAP bind user", g.Config.BindDN, true, false), createTextInput("bind_password", "Bind Password", "Password for bind DN", g.Config.BindPassword, true, true), createTextInput("user_attr", "User Attribute", "LDAP attribute for username (e.g., uid or sAMAccountName)", g.Config.UserAttr, true, false), + createTextInput("tls_ca_cert_path", "TLS CA Certificate Path", "Path to custom CA certificate file (optional)", g.Config.TLSCACertPath, false, false), } } func (g *Connector) ConfigReceiver(config []byte) error { c := &ConnectorConfig{} if err := json.Unmarshal(config, c); err != nil { - return err + return fmt.Errorf("invalid config json: %w", err) } g.Config = c return nil } func (c *Connector) ConnectorReceiver(ctx *plugin.GinContext, receiverURL string) (userInfo plugin.ExternalLoginUserInfo, err error) { - log.Info("ConnectorReceiver called!") username, password, err := extractCredentials(ctx.Request) if err != nil { return userInfo, err } - l, err := ldap.DialURL(c.Config.Server) + l, err := dialWithTLS(c.Config.Server, c.Config.TLSCACertPath) if err != nil { return userInfo, fmt.Errorf("failed to connect to LDAP server: %w", err) } defer l.Close() - err = l.Bind(c.Config.BindDN, c.Config.BindPassword) + if err := bindServiceAccount(l, c.Config.BindDN, c.Config.BindPassword); err != nil { + return userInfo, fmt.Errorf("service account bind failed: %w", err) + } + + entry, err := searchUser(l, c.Config.BaseDN, c.Config.UserAttr, username) if err != nil { - return userInfo, fmt.Errorf("bind failed: %w", err) + return userInfo, err } + err = l.Bind(entry.DN, password) + if err != nil { + return userInfo, fmt.Errorf("invalid username or password") + } + + userInfo, err = extractUserInfo(entry) + if err != nil { + return userInfo, err + } + + return userInfo, nil +} + +func bindServiceAccount(l *ldap.Conn, bindDN, bindPassword string) error { + return l.Bind(bindDN, bindPassword) +} + +func searchUser(l *ldap.Conn, baseDN, userAttr, username string) (*ldap.Entry, error) { searchRequest := ldap.NewSearchRequest( - c.Config.BaseDN, + baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, - fmt.Sprintf("(%s=%s)", c.Config.UserAttr, ldap.EscapeFilter(username)), + fmt.Sprintf("(%s=%s)", userAttr, ldap.EscapeFilter(username)), []string{LdapAttributeDn, LdapAttributeUid, LdapAttributeCn, LdapAttributeMail, LdapAttributeDisplayName, LdapAttributeSamAccountName}, nil, ) sr, err := l.Search(searchRequest) if err != nil || len(sr.Entries) == 0 { - return userInfo, fmt.Errorf("user not found: %w", err) - } - - entry := sr.Entries[0] - - err = l.Bind(entry.DN, password) - if err != nil { - return userInfo, fmt.Errorf("invalid username or password") + return nil, fmt.Errorf("user not found: %w", err) } - userInfo = extractUserInfo(entry) - - log.Infof("userInfo %s", &userInfo) - - return userInfo, nil + return sr.Entries[0], nil } func extractCredentials(request *http.Request) (username string, password string, err error) { - queryParams := request.URL.Query() + err = request.ParseForm() + if err != nil { + log.Errorf("failed to parse form: %v", err) + return "", "", err + } - username = queryParams.Get("username") - password = queryParams.Get("password") + username = request.FormValue("username") + password = request.FormValue("password") if username == "" || password == "" { - log.Errorf("missing username or password") + log.Errorf("missing username and/or password") err = fmt.Errorf("missing username or password") } return } -func extractUserInfo(entry *ldap.Entry) plugin.ExternalLoginUserInfo { +func extractUserInfo(entry *ldap.Entry) (plugin.ExternalLoginUserInfo, error) { displayName := entry.GetAttributeValue(LdapAttributeDisplayName) - log.Infof("displayName %s", displayName) if displayName == "" { displayName = entry.GetAttributeValue(LdapAttributeCn) @@ -192,20 +223,16 @@ func extractUserInfo(entry *ldap.Entry) plugin.ExternalLoginUserInfo { if username == "" { username = entry.GetAttributeValue(LdapAttributeSamAccountName) } - log.Infof("username %s", &username) externalID := username if externalID == "" { externalID = entry.DN // fallback } - /* - email is used to login, therefore required. - wether the email is correct, is not important for our use case - */ + //email is used to login, therefore required email := entry.GetAttributeValue(LdapAttributeMail) if email == "" { - email = username + "@dummymail.xyz" + return nil, fmt.Errorf("email is required") } return plugin.ExternalLoginUserInfo{ @@ -213,7 +240,7 @@ func extractUserInfo(entry *ldap.Entry) plugin.ExternalLoginUserInfo { DisplayName: displayName, Username: username, Email: email, - } + }, nil } func createTextInput(name, title, desc, value string, require bool, password bool) plugin.ConfigField { @@ -235,3 +262,58 @@ func createTextInput(name, title, desc, value string, require bool, password boo Value: value, } } + +func createBoolInput(name, title, desc string, value bool, require bool) plugin.ConfigField { + return plugin.ConfigField{ + + Name: name, + Type: plugin.ConfigTypeCheckbox, + Title: plugin.MakeTranslator(title), + Description: plugin.MakeTranslator(desc), + Required: require, + UIOptions: plugin.ConfigFieldUIOptions{}, + Value: value, + } + +} + +func dialWithTLS(server string, certPath string) (*ldap.Conn, error) { + + tlsConfig := &tls.Config{ + InsecureSkipVerify: false, + } + + if certPath != "" { + certPool := x509.NewCertPool() + certData, err := os.ReadFile(certPath) + if err != nil { + log.Errorf("failed to read cert file: %v", err) + return nil, fmt.Errorf("failed to read LDAP cert: %w", err) + } + + if !certPool.AppendCertsFromPEM(certData) { + log.Errorf("failed to append cert from %s", certPath) + return nil, fmt.Errorf("failed to append cert") + } + + tlsConfig.RootCAs = certPool + } + + if strings.HasPrefix(server, "ldaps://") { + return ldap.DialURL(server, ldap.DialWithTLSConfig(tlsConfig)) + } + + conn, err := ldap.DialURL(server) + if err != nil { + log.Errorf("initial plain connection failed: %v", err) + return nil, err + } + + if err := conn.StartTLS(tlsConfig); err != nil { + log.Errorf("startTLS failed: %v", err) + conn.Close() + return nil, err + } + + return conn, nil +} diff --git a/connector-ldap/login.html b/connector-ldap/login.html index f1f331fe5..a5774f40d 100644 --- a/connector-ldap/login.html +++ b/connector-ldap/login.html @@ -1,14 +1,21 @@ - -LDAP Login + + + + + + LDAP Login + -

LDAP Login

-
-
-
-
-
-
- -
+
+

LDAP Login

+
+ + + + +
+ +
+
\ No newline at end of file From 6133ea103137f17126011d8096f7e080996f9487 Mon Sep 17 00:00:00 2001 From: broccoli Date: Sun, 13 Sep 2026 12:49:44 +0200 Subject: [PATCH 05/10] fix: repair StartTLS handshake and build, add README + set tls.Config.ServerName in dialWithTLS so StartTLS connections (non-ldaps:// servers) actually verify. Without it every StartTLS attempt failed with "ServerName or InsecureSkipVerify must be specified". + fix extractUserInfo returning nil for the non-pointer plugin.ExternalLoginUserInfo struct, which failed to compile. + added missing go.sum and go-ldap/pacman + added readme --- connector-ldap/README.md | 22 +++ connector-ldap/go.mod | 46 +++++- connector-ldap/go.sum | 328 +++++++++++++++++++++++++++++++++++++++ connector-ldap/ldap.go | 9 +- 4 files changed, 403 insertions(+), 2 deletions(-) create mode 100644 connector-ldap/README.md create mode 100644 connector-ldap/go.sum diff --git a/connector-ldap/README.md b/connector-ldap/README.md new file mode 100644 index 000000000..93973de45 --- /dev/null +++ b/connector-ldap/README.md @@ -0,0 +1,22 @@ +# LDAP connector +> LDAP connector is a plug-in designed to support login via LDAP or Active Directory, including LDAPS and StartTLS. + +## How to use + +### Build +```bash +./answer build --with github.com/apache/answer-plugins/connector-ldap +``` + +### Configuration +- `Name` - Display name for the connector shown on the login page +- `Server` - LDAP server URL, e.g. `ldaps://ldap.example.com:636` or `ldap://ldap.example.com:389` +- `Base DN` - Base DN to search for users, e.g. `dc=example,dc=com` +- `Bind DN` - DN of the service account used to bind and search the directory +- `Bind Password` - Password for the bind DN +- `User Attribute` - LDAP attribute holding the username, e.g. `uid` for OpenLDAP or `sAMAccountName` for Active Directory +- `TLS CA Certificate Path` - Optional path to a custom CA certificate, used to verify the LDAP server's certificate (e.g. for a private/internal CA) + +If the server URL starts with `ldaps://`, the connection is established over TLS directly. Otherwise, a plain connection is opened and upgraded via StartTLS. + +Users must have a `mail` attribute set in LDAP, since it is required to create/match the Answer account on login. diff --git a/connector-ldap/go.mod b/connector-ldap/go.mod index 7bd749042..30d477d8a 100644 --- a/connector-ldap/go.mod +++ b/connector-ldap/go.mod @@ -1,9 +1,53 @@ module github.com/DanielAuerX/answer-plugins/connector-ldap -go 1.22 +go 1.22.0 + +toolchain go1.24.2 require ( github.com/apache/answer v1.4.2-RC1.0.20250107023923-061894735091 github.com/apache/answer-plugins/util v1.0.3-0.20250107030257-cf94ebc70954 + github.com/go-ldap/ldap/v3 v3.4.10 + github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f ) +require ( + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bytedance/sonic v1.12.2 // indirect + github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.5 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.1 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/google/wire v0.5.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.10.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/connector-ldap/go.sum b/connector-ldap/go.sum new file mode 100644 index 000000000..2485d459e --- /dev/null +++ b/connector-ldap/go.sum @@ -0,0 +1,328 @@ +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= +github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= +github.com/Machiel/slugify v1.0.1/go.mod h1:fTFGn5uWEynW4CUMG7sWkYXOf1UgDxyTM3DbR6Qfg3k= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267/go.mod h1:Yj3yPP/vi87JjwylUTCMyd6FrOfGqP1AHk0305hDm2o= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/apache/answer v1.4.2-RC1.0.20250107023923-061894735091 h1:TmUPU0tX3VzbUO7rCBW3hJDUGO/WOI343zcbymIWQSM= +github.com/apache/answer v1.4.2-RC1.0.20250107023923-061894735091/go.mod h1:ehAJmrP4X9kBJKlYzTtRrwRbKeRqjnHxyYlhf11yzfw= +github.com/apache/answer-plugins/util v1.0.3-0.20250107030257-cf94ebc70954 h1:jVqxzyeHvYxAT30vGfHXtZLNoi9qAWnvTtuMSj6pFys= +github.com/apache/answer-plugins/util v1.0.3-0.20250107030257-cf94ebc70954/go.mod h1:wQEKNXVa/BKKq5yro9qo5bFiO3/SW1noORabxEndk3o= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= +github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg= +github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= +github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/docker/cli v27.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs= +github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc= +github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= +github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102/go.mod h1:6+tQXZ+I62x13UZ+hemLVoZIuq/usVzvau7bqwUo9P0= +github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= +github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= +github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d/go.mod h1:yTR3tKgyk20phAFg6IE9ulMA5NjEDD2wyx+okRFLVtw= +github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= +github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= +github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= +github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= +github.com/lestrrat-go/strftime v1.1.0/go.mod h1:uzeIB52CeUJenCo1syghlugshMysrqUT51HlxphXVeI= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mozillazg/go-pinyin v0.20.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= +github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405/go.mod h1:rIxVzVLKlBwLxO+lC+k/I4HJfRQcemg/f/76Xmmzsec= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= +github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822083413-c0075a2d401f/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs= +github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20230822083413-c0075a2d401f/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f h1:xia6AXJor4UV4T6htmHlfN7CGXZ04vlWwybVtFKJ/mA= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E= +github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20230822083413-c0075a2d401f/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= +github.com/segmentfault/pacman/contrib/server/http v0.0.0-20230822083413-c0075a2d401f/go.mod h1:UjNiOFYv1uGCq1ZCcONaKq4eE7MW3nbgpLqgl8f9N40= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= +golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= +xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw= diff --git a/connector-ldap/ldap.go b/connector-ldap/ldap.go index e5e58c6b1..fc2a6ddb3 100644 --- a/connector-ldap/ldap.go +++ b/connector-ldap/ldap.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "os" "strings" @@ -232,7 +233,7 @@ func extractUserInfo(entry *ldap.Entry) (plugin.ExternalLoginUserInfo, error) { //email is used to login, therefore required email := entry.GetAttributeValue(LdapAttributeMail) if email == "" { - return nil, fmt.Errorf("email is required") + return plugin.ExternalLoginUserInfo{}, fmt.Errorf("email is required") } return plugin.ExternalLoginUserInfo{ @@ -279,8 +280,14 @@ func createBoolInput(name, title, desc string, value bool, require bool) plugin. func dialWithTLS(server string, certPath string) (*ldap.Conn, error) { + serverURL, err := url.Parse(server) + if err != nil { + return nil, fmt.Errorf("invalid LDAP server URL: %w", err) + } + tlsConfig := &tls.Config{ InsecureSkipVerify: false, + ServerName: serverURL.Hostname(), } if certPath != "" { From 7fc3fb31ab92ca589374d24744e0e8ac802346f9 Mon Sep 17 00:00:00 2001 From: broccoli Date: Sun, 13 Sep 2026 15:02:55 +0200 Subject: [PATCH 06/10] feat: style login page and pull site branding + replace the bare unstyled form with a centered card layout matching common auth-page conventions + fetch /answer/api/v1/siteinfo client-side to set the page title, favicon, logo, and accent color from the site's configured branding/theme, falling back to plain defaults if the request fails. + keep the RECEIVER_URL_PLACEHOLDER form action and username/password field names unchanged so ConnectorSender/ConnectorReceiver still work. --- connector-ldap/login.html | 136 +++++++++++++++++++++++++++++++++++--- 1 file changed, 126 insertions(+), 10 deletions(-) diff --git a/connector-ldap/login.html b/connector-ldap/login.html index a5774f40d..3f00c1ca0 100644 --- a/connector-ldap/login.html +++ b/connector-ldap/login.html @@ -3,19 +3,135 @@ - LDAP Login + Login + + -
-

LDAP Login

+
+
+ +

Login

+

Sign in with your LDAP account

+
- - - - -
- + + + + +
+ - \ No newline at end of file + From 476ceaa2beeda5b62d050e76a1ae194e64987f7a Mon Sep 17 00:00:00 2001 From: broccoli Date: Thu, 17 Sep 2026 15:45:53 +0200 Subject: [PATCH 07/10] feat: require and round-trip state, add stable ExternalID + ConnectorSender now reads state via ctx.Request.URL.Query() instead of ctx.Query(), which returns a stale cached value here since core injects state into the raw query after already having called ctx.Query() once. appends it to the receiver url so it survives the login forms post. + implemented ConnectorRequireState() to opt into cores strict state validation (cf pr apache/answer#1612). missing or invalid state is now rejected instead of treated as a normal login. + ExternalID now comes from a configurable, stable attribute (default entryUUID; objectGUID supported for Active Directory, including proper binary/mixed-endian decoding) instead of the mutable uid or sAMAccountName. + added tests (mocked dependencies) + dialWithTLS now returns the ldap.Client interface instead of the concrete *ldap.Conn enabling a mock ldap client in tests --- connector-ldap/ldap.go | 91 +++++++--- connector-ldap/ldap_test.go | 329 ++++++++++++++++++++++++++++++++++++ 2 files changed, 401 insertions(+), 19 deletions(-) create mode 100644 connector-ldap/ldap_test.go diff --git a/connector-ldap/ldap.go b/connector-ldap/ldap.go index fc2a6ddb3..f1b8c778a 100644 --- a/connector-ldap/ldap.go +++ b/connector-ldap/ldap.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "crypto/x509" "embed" + "encoding/binary" "encoding/json" "fmt" "net/http" @@ -32,6 +33,9 @@ const ( LdapAttributeMail = "mail" LdapAttributeDisplayName = "displayName" LdapAttributeSamAccountName = "sAMAccountName" + LdapAttributeObjectGUID = "objectGUID" + + DefaultExternalIDAttr = "entryUUID" ) type Connector struct { @@ -39,16 +43,18 @@ type Connector struct { } type ConnectorConfig struct { - Name string `json:"name"` - Server string `json:"server"` - BaseDN string `json:"base_dn"` - BindDN string `json:"bind_dn"` - BindPassword string `json:"bind_password"` - UserAttr string `json:"user_attr"` - TLSCACertPath string `json:"tls_ca_cert_path"` + Name string `json:"name"` + Server string `json:"server"` + BaseDN string `json:"base_dn"` + BindDN string `json:"bind_dn"` + BindPassword string `json:"bind_password"` + UserAttr string `json:"user_attr"` + ExternalIDAttr string `json:"external_id_attr"` + TLSCACertPath string `json:"tls_ca_cert_path"` } var _ plugin.Connector = &Connector{} +var _ plugin.ConnectorStateRequired = &Connector{} var loginHTMLContent string @@ -97,8 +103,17 @@ func (g *Connector) ConnectorLogoSVG() string { return "" } +func (g *Connector) ConnectorRequireState() bool { + return true +} + func (g *Connector) ConnectorSender(ctx *plugin.GinContext, receiverURL string) string { + state := ctx.Request.URL.Query().Get("state") + if state != "" { + receiverURL = receiverURL + "?state=" + url.QueryEscape(state) + } + htmlContent := strings.Replace(loginHTMLContent, "RECEIVER_URL_PLACEHOLDER", receiverURL, -1) ctx.Writer.WriteHeader(200) ctx.Writer.Header().Set("Content-Type", "text/html") @@ -126,6 +141,7 @@ func (g *Connector) ConfigFields() []plugin.ConfigField { createTextInput("bind_dn", "Bind DN", "DN of LDAP bind user", g.Config.BindDN, true, false), createTextInput("bind_password", "Bind Password", "Password for bind DN", g.Config.BindPassword, true, true), createTextInput("user_attr", "User Attribute", "LDAP attribute for username (e.g., uid or sAMAccountName)", g.Config.UserAttr, true, false), + createTextInput("external_id_attr", "External ID Attribute", "Stable LDAP attribute used to identify the user across logins, e.g. entryUUID (OpenLDAP) or objectGUID (Active Directory). Do not use a mutable attribute like uid.", externalIDAttrOrDefault(g.Config.ExternalIDAttr), true, false), createTextInput("tls_ca_cert_path", "TLS CA Certificate Path", "Path to custom CA certificate file (optional)", g.Config.TLSCACertPath, false, false), } } @@ -146,7 +162,7 @@ func (c *Connector) ConnectorReceiver(ctx *plugin.GinContext, receiverURL string return userInfo, err } - l, err := dialWithTLS(c.Config.Server, c.Config.TLSCACertPath) + l, err := connectLDAP(c.Config.Server, c.Config.TLSCACertPath) if err != nil { return userInfo, fmt.Errorf("failed to connect to LDAP server: %w", err) } @@ -156,7 +172,9 @@ func (c *Connector) ConnectorReceiver(ctx *plugin.GinContext, receiverURL string return userInfo, fmt.Errorf("service account bind failed: %w", err) } - entry, err := searchUser(l, c.Config.BaseDN, c.Config.UserAttr, username) + externalIDAttr := externalIDAttrOrDefault(c.Config.ExternalIDAttr) + + entry, err := searchUser(l, c.Config.BaseDN, c.Config.UserAttr, externalIDAttr, username) if err != nil { return userInfo, err } @@ -166,7 +184,7 @@ func (c *Connector) ConnectorReceiver(ctx *plugin.GinContext, receiverURL string return userInfo, fmt.Errorf("invalid username or password") } - userInfo, err = extractUserInfo(entry) + userInfo, err = extractUserInfo(entry, externalIDAttr) if err != nil { return userInfo, err } @@ -174,16 +192,19 @@ func (c *Connector) ConnectorReceiver(ctx *plugin.GinContext, receiverURL string return userInfo, nil } -func bindServiceAccount(l *ldap.Conn, bindDN, bindPassword string) error { +var connectLDAP = dialWithTLS + +func bindServiceAccount(l ldap.Client, bindDN, bindPassword string) error { return l.Bind(bindDN, bindPassword) } -func searchUser(l *ldap.Conn, baseDN, userAttr, username string) (*ldap.Entry, error) { +func searchUser(l ldap.Client, baseDN, userAttr, externalIDAttr, username string) (*ldap.Entry, error) { + attributes := []string{LdapAttributeDn, LdapAttributeUid, LdapAttributeCn, LdapAttributeMail, LdapAttributeDisplayName, LdapAttributeSamAccountName, externalIDAttr} searchRequest := ldap.NewSearchRequest( baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, fmt.Sprintf("(%s=%s)", userAttr, ldap.EscapeFilter(username)), - []string{LdapAttributeDn, LdapAttributeUid, LdapAttributeCn, LdapAttributeMail, LdapAttributeDisplayName, LdapAttributeSamAccountName}, + attributes, nil, ) @@ -212,7 +233,7 @@ func extractCredentials(request *http.Request) (username string, password string return } -func extractUserInfo(entry *ldap.Entry) (plugin.ExternalLoginUserInfo, error) { +func extractUserInfo(entry *ldap.Entry, externalIDAttr string) (plugin.ExternalLoginUserInfo, error) { displayName := entry.GetAttributeValue(LdapAttributeDisplayName) @@ -225,12 +246,11 @@ func extractUserInfo(entry *ldap.Entry) (plugin.ExternalLoginUserInfo, error) { username = entry.GetAttributeValue(LdapAttributeSamAccountName) } - externalID := username - if externalID == "" { - externalID = entry.DN // fallback + externalID, err := extractExternalID(entry, externalIDAttr) + if err != nil { + return plugin.ExternalLoginUserInfo{}, err } - //email is used to login, therefore required email := entry.GetAttributeValue(LdapAttributeMail) if email == "" { return plugin.ExternalLoginUserInfo{}, fmt.Errorf("email is required") @@ -244,6 +264,39 @@ func extractUserInfo(entry *ldap.Entry) (plugin.ExternalLoginUserInfo, error) { }, nil } +func externalIDAttrOrDefault(externalIDAttr string) string { + if externalIDAttr == "" { + return DefaultExternalIDAttr + } + return externalIDAttr +} + +func extractExternalID(entry *ldap.Entry, externalIDAttr string) (string, error) { + if externalIDAttr == LdapAttributeObjectGUID { + raw := entry.GetRawAttributeValue(externalIDAttr) + if len(raw) != 16 { + return "", fmt.Errorf("missing or invalid %s attribute", externalIDAttr) + } + return formatObjectGUID(raw), nil + } + + externalID := entry.GetAttributeValue(externalIDAttr) + if externalID == "" { + return "", fmt.Errorf("missing %s attribute", externalIDAttr) + } + return externalID, nil +} + +func formatObjectGUID(guid []byte) string { + return fmt.Sprintf("%08x-%04x-%04x-%x-%x", + binary.LittleEndian.Uint32(guid[0:4]), + binary.LittleEndian.Uint16(guid[4:6]), + binary.LittleEndian.Uint16(guid[6:8]), + guid[8:10], + guid[10:16], + ) +} + func createTextInput(name, title, desc, value string, require bool, password bool) plugin.ConfigField { uiOptions := plugin.ConfigFieldUIOptions{ InputType: plugin.InputTypeText, @@ -278,7 +331,7 @@ func createBoolInput(name, title, desc string, value bool, require bool) plugin. } -func dialWithTLS(server string, certPath string) (*ldap.Conn, error) { +func dialWithTLS(server string, certPath string) (ldap.Client, error) { serverURL, err := url.Parse(server) if err != nil { diff --git a/connector-ldap/ldap_test.go b/connector-ldap/ldap_test.go new file mode 100644 index 000000000..d42d0fd9f --- /dev/null +++ b/connector-ldap/ldap_test.go @@ -0,0 +1,329 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package ldap + +import ( + "context" + "crypto/tls" + "fmt" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/gin-gonic/gin" + goldap "github.com/go-ldap/ldap/v3" +) + +type mockLDAPClient struct { + bindFunc func(username, password string) error + searchFunc func(req *goldap.SearchRequest) (*goldap.SearchResult, error) +} + +var _ goldap.Client = &mockLDAPClient{} + +const ( + testUsername = "mickeyMouse" + testPassword = "iLoveMinnieMouse" + testEmail = "mickey@test.com" + testExternalID = "01234567-89ab-cdef-0123-456789abcdef" + testServer = "ldap://localhost:389" + testBaseDN = "dc=example,dc=com" + testBindDN = "cn=admin,dc=example,dc=com" + testBindPassword = "admin-password" +) + +func (m *mockLDAPClient) Start() {} +func (m *mockLDAPClient) StartTLS(*tls.Config) error { return nil } +func (m *mockLDAPClient) Close() error { return nil } +func (m *mockLDAPClient) GetLastError() error { return nil } +func (m *mockLDAPClient) IsClosing() bool { return false } +func (m *mockLDAPClient) SetTimeout(time.Duration) {} +func (m *mockLDAPClient) TLSConnectionState() (tls.ConnectionState, bool) { + return tls.ConnectionState{}, false +} + +func (m *mockLDAPClient) Bind(username, password string) error { + return m.bindFunc(username, password) +} +func (m *mockLDAPClient) UnauthenticatedBind(username string) error { return nil } +func (m *mockLDAPClient) SimpleBind(*goldap.SimpleBindRequest) (*goldap.SimpleBindResult, error) { + return nil, nil +} +func (m *mockLDAPClient) ExternalBind() error { return nil } +func (m *mockLDAPClient) NTLMUnauthenticatedBind(domain, username string) error { return nil } +func (m *mockLDAPClient) Unbind() error { return nil } + +func (m *mockLDAPClient) Add(*goldap.AddRequest) error { return nil } +func (m *mockLDAPClient) Del(*goldap.DelRequest) error { return nil } +func (m *mockLDAPClient) Modify(*goldap.ModifyRequest) error { return nil } +func (m *mockLDAPClient) ModifyDN(*goldap.ModifyDNRequest) error { + return nil +} +func (m *mockLDAPClient) ModifyWithResult(*goldap.ModifyRequest) (*goldap.ModifyResult, error) { + return nil, nil +} +func (m *mockLDAPClient) Extended(*goldap.ExtendedRequest) (*goldap.ExtendedResponse, error) { + return nil, nil +} + +func (m *mockLDAPClient) Compare(dn, attribute, value string) (bool, error) { + return false, nil +} +func (m *mockLDAPClient) PasswordModify(*goldap.PasswordModifyRequest) (*goldap.PasswordModifyResult, error) { + return nil, nil +} + +func (m *mockLDAPClient) Search(req *goldap.SearchRequest) (*goldap.SearchResult, error) { + return m.searchFunc(req) +} +func (m *mockLDAPClient) SearchAsync(ctx context.Context, req *goldap.SearchRequest, bufferSize int) goldap.Response { + return nil +} +func (m *mockLDAPClient) SearchWithPaging(*goldap.SearchRequest, uint32) (*goldap.SearchResult, error) { + return nil, nil +} +func (m *mockLDAPClient) DirSync(req *goldap.SearchRequest, flags, maxAttrCount int64, cookie []byte) (*goldap.SearchResult, error) { + return nil, nil +} +func (m *mockLDAPClient) DirSyncAsync(ctx context.Context, req *goldap.SearchRequest, bufferSize int, flags, maxAttrCount int64, cookie []byte) goldap.Response { + return nil +} +func (m *mockLDAPClient) Syncrepl(ctx context.Context, req *goldap.SearchRequest, bufferSize int, mode goldap.ControlSyncRequestMode, cookie []byte, reloadHint bool) goldap.Response { + return nil +} + +func withMockLDAP(t *testing.T, mock *mockLDAPClient) { + original := connectLDAP + connectLDAP = func(server, certPath string) (goldap.Client, error) { + return mock, nil + } + t.Cleanup(func() { connectLDAP = original }) +} + +func testEntry(username, email, externalIDAttr, externalIDValue string) *goldap.Entry { + return &goldap.Entry{ + DN: "uid=" + username + "," + testBaseDN, + Attributes: []*goldap.EntryAttribute{ + {Name: LdapAttributeUid, Values: []string{username}}, + {Name: LdapAttributeCn, Values: []string{username}}, + {Name: LdapAttributeMail, Values: []string{email}}, + {Name: externalIDAttr, Values: []string{externalIDValue}}, + }, + } +} + +func loginRequest(username, password string) *gin.Context { + form := url.Values{"username": {username}, "password": {password}} + req := httptest.NewRequest("POST", "/answer/api/v1/connector/redirect/ldap", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + ctx.Request = req + return ctx +} + +func TestConnector_SenderIncludesStateDespiteGinQueryCache(t *testing.T) { + c := &Connector{Config: &ConnectorConfig{}} + + req := httptest.NewRequest("GET", "/answer/api/v1/connector/login/ldap", nil) + rec := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(rec) + ctx.Request = req + + ctx.Query("state") + ctx.Request.URL.RawQuery = "state=injected-after-cache" + + c.ConnectorSender(ctx, "https://example.com/answer/api/v1/connector/redirect/ldap") + + if !strings.Contains(rec.Body.String(), "state=injected-after-cache") { + t.Fatalf("expected rendered form action to include the state injected after gin's query cache was primed, got: %s", rec.Body.String()) + } +} + +func TestConnector_SuccessfulLogin(t *testing.T) { + entry := testEntry(testUsername, testEmail, DefaultExternalIDAttr, testExternalID) + withMockLDAP(t, &mockLDAPClient{ + bindFunc: func(username, password string) error { + if username == testBindDN { + return nil + } + if username == entry.DN && password == testPassword { + return nil + } + return fmt.Errorf("invalid credentials") + }, + searchFunc: func(req *goldap.SearchRequest) (*goldap.SearchResult, error) { + return &goldap.SearchResult{Entries: []*goldap.Entry{entry}}, nil + }, + }) + + c := &Connector{Config: &ConnectorConfig{ + Server: testServer, + BaseDN: testBaseDN, + BindDN: testBindDN, + BindPassword: testBindPassword, + UserAttr: LdapAttributeUid, + ExternalIDAttr: DefaultExternalIDAttr, + }} + + userInfo, err := c.ConnectorReceiver(loginRequest(testUsername, testPassword), "") + if err != nil { + t.Fatal(err) + } + if userInfo.Email != testEmail { + t.Fatalf("expected email %q, got %q", testEmail, userInfo.Email) + } + if userInfo.ExternalID != testExternalID { + t.Fatalf("expected external ID from entryUUID, got %q", userInfo.ExternalID) + } +} + +func TestConnector_ServiceAccountBindFailure(t *testing.T) { + withMockLDAP(t, &mockLDAPClient{ + bindFunc: func(username, password string) error { + return fmt.Errorf("invalid credentials") + }, + searchFunc: func(req *goldap.SearchRequest) (*goldap.SearchResult, error) { + t.Fatal("search should not be called when the service account bind fails") + return nil, nil + }, + }) + + c := &Connector{Config: &ConnectorConfig{ + Server: testServer, + BaseDN: testBaseDN, + BindDN: testBindDN, + BindPassword: "wrongPassword", + UserAttr: LdapAttributeUid, + ExternalIDAttr: DefaultExternalIDAttr, + }} + + _, err := c.ConnectorReceiver(loginRequest(testUsername, "correctPassword"), "") + if err == nil { + t.Fatal("expected service account bind failure to produce an error") + } +} + +func TestConnector_UserNotFound(t *testing.T) { + withMockLDAP(t, &mockLDAPClient{ + bindFunc: func(username, password string) error { return nil }, + searchFunc: func(req *goldap.SearchRequest) (*goldap.SearchResult, error) { + return &goldap.SearchResult{Entries: nil}, nil + }, + }) + + c := &Connector{Config: &ConnectorConfig{ + Server: testServer, + BaseDN: testBaseDN, + BindDN: testBindDN, + BindPassword: testBindPassword, + UserAttr: LdapAttributeUid, + ExternalIDAttr: DefaultExternalIDAttr, + }} + + _, err := c.ConnectorReceiver(loginRequest("foo", "bar"), "") + if err == nil { + t.Fatal("expected an error when the user search returns no entries") + } +} + +func TestConnector_WrongPassword(t *testing.T) { + entry := testEntry(testUsername, testEmail, DefaultExternalIDAttr, testExternalID) + withMockLDAP(t, &mockLDAPClient{ + bindFunc: func(username, password string) error { + if username == testBindDN { + return nil + } + return fmt.Errorf("invalid credentials") + }, + searchFunc: func(req *goldap.SearchRequest) (*goldap.SearchResult, error) { + return &goldap.SearchResult{Entries: []*goldap.Entry{entry}}, nil + }, + }) + + c := &Connector{Config: &ConnectorConfig{ + Server: testServer, + BaseDN: testBaseDN, + BindDN: testBindDN, + BindPassword: testBindPassword, + UserAttr: LdapAttributeUid, + ExternalIDAttr: DefaultExternalIDAttr, + }} + + _, err := c.ConnectorReceiver(loginRequest(testUsername, "iAmAWrongPassword"), "") + if err == nil { + t.Fatal("expected wrong password to produce an error") + } +} + +func TestExtractCredentials(t *testing.T) { + req := loginRequest(testUsername, testPassword).Request + username, password, err := extractCredentials(req) + if err != nil { + t.Fatal(err) + } + if username != testUsername || password != testPassword { + t.Fatalf("expected %q/%q, got %q/%q", testUsername, testPassword, username, password) + } +} + +func TestExtractCredentials_Missing(t *testing.T) { + req := loginRequest(testUsername, "").Request + if _, _, err := extractCredentials(req); err == nil { + t.Fatal("expected an error when the password is missing") + } +} + +func TestExtractUserInfo_MissingEmail(t *testing.T) { + entry := testEntry(testUsername, "", DefaultExternalIDAttr, testExternalID) + if _, err := extractUserInfo(entry, DefaultExternalIDAttr); err == nil { + t.Fatal("expected an error when the mail attribute is missing") + } +} + +func TestExtractExternalID_MissingAttribute(t *testing.T) { + entry := testEntry(testUsername, testEmail, DefaultExternalIDAttr, "") + entry.Attributes = entry.Attributes[:len(entry.Attributes)-1] + if _, err := extractExternalID(entry, DefaultExternalIDAttr); err == nil { + t.Fatal("expected an error when the configured external ID attribute is missing") + } +} + +func TestExtractExternalID_ObjectGUID(t *testing.T) { + entry := &goldap.Entry{ + DN: "cn=" + testUsername + "," + testBaseDN, + Attributes: []*goldap.EntryAttribute{ + { + Name: LdapAttributeObjectGUID, + ByteValues: [][]byte{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}}, + }, + }, + } + + externalID, err := extractExternalID(entry, LdapAttributeObjectGUID) + if err != nil { + t.Fatal(err) + } + const expected = "04030201-0605-0807-090a-0b0c0d0e0f10" + if externalID != expected { + t.Fatalf("expected %q, got %q", expected, externalID) + } +} From f86e06755199f848f454f093f982ce81c3139197 Mon Sep 17 00:00:00 2001 From: broccoli Date: Thu, 17 Sep 2026 17:26:14 +0200 Subject: [PATCH 08/10] feat: added same-origin check added same-origin check to reject cross-site credential submissions --- connector-ldap/ldap.go | 29 +++++++++++++++++++++++++ connector-ldap/ldap_test.go | 43 +++++++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/connector-ldap/ldap.go b/connector-ldap/ldap.go index f1b8c778a..993a4fc4b 100644 --- a/connector-ldap/ldap.go +++ b/connector-ldap/ldap.go @@ -157,6 +157,10 @@ func (g *Connector) ConfigReceiver(config []byte) error { func (c *Connector) ConnectorReceiver(ctx *plugin.GinContext, receiverURL string) (userInfo plugin.ExternalLoginUserInfo, err error) { + if err := checkSameOrigin(ctx.Request, receiverURL); err != nil { + return userInfo, fmt.Errorf("csrf check failed: %w", err) + } + username, password, err := extractCredentials(ctx.Request) if err != nil { return userInfo, err @@ -216,6 +220,31 @@ func searchUser(l ldap.Client, baseDN, userAttr, externalIDAttr, username string return sr.Entries[0], nil } +func checkSameOrigin(request *http.Request, receiverURL string) error { + expected, err := url.Parse(receiverURL) + if err != nil { + return fmt.Errorf("invalid receiver URL: %w", err) + } + + if origin := request.Header.Get("Origin"); origin != "" { + originURL, err := url.Parse(origin) + if err != nil || originURL.Scheme != expected.Scheme || originURL.Host != expected.Host { + return fmt.Errorf("request origin %q does not match site origin", origin) + } + return nil + } + + if referer := request.Header.Get("Referer"); referer != "" { + refererURL, err := url.Parse(referer) + if err != nil || refererURL.Scheme != expected.Scheme || refererURL.Host != expected.Host { + return fmt.Errorf("request referer %q does not match site origin", referer) + } + return nil + } + + return fmt.Errorf("missing Origin and Referer headers") +} + func extractCredentials(request *http.Request) (username string, password string, err error) { err = request.ParseForm() if err != nil { diff --git a/connector-ldap/ldap_test.go b/connector-ldap/ldap_test.go index d42d0fd9f..378eb7a06 100644 --- a/connector-ldap/ldap_test.go +++ b/connector-ldap/ldap_test.go @@ -49,6 +49,8 @@ const ( testBaseDN = "dc=example,dc=com" testBindDN = "cn=admin,dc=example,dc=com" testBindPassword = "admin-password" + testReceiverURL = "http://localhost:8080/answer/api/v1/connector/redirect/ldap" + testOrigin = "http://localhost:8080" ) func (m *mockLDAPClient) Start() {} @@ -135,6 +137,7 @@ func loginRequest(username, password string) *gin.Context { form := url.Values{"username": {username}, "password": {password}} req := httptest.NewRequest("POST", "/answer/api/v1/connector/redirect/ldap", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Origin", testOrigin) ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) ctx.Request = req return ctx @@ -184,7 +187,7 @@ func TestConnector_SuccessfulLogin(t *testing.T) { ExternalIDAttr: DefaultExternalIDAttr, }} - userInfo, err := c.ConnectorReceiver(loginRequest(testUsername, testPassword), "") + userInfo, err := c.ConnectorReceiver(loginRequest(testUsername, testPassword), testReceiverURL) if err != nil { t.Fatal(err) } @@ -216,7 +219,7 @@ func TestConnector_ServiceAccountBindFailure(t *testing.T) { ExternalIDAttr: DefaultExternalIDAttr, }} - _, err := c.ConnectorReceiver(loginRequest(testUsername, "correctPassword"), "") + _, err := c.ConnectorReceiver(loginRequest(testUsername, "correctPassword"), testReceiverURL) if err == nil { t.Fatal("expected service account bind failure to produce an error") } @@ -239,7 +242,7 @@ func TestConnector_UserNotFound(t *testing.T) { ExternalIDAttr: DefaultExternalIDAttr, }} - _, err := c.ConnectorReceiver(loginRequest("foo", "bar"), "") + _, err := c.ConnectorReceiver(loginRequest("foo", "bar"), testReceiverURL) if err == nil { t.Fatal("expected an error when the user search returns no entries") } @@ -268,12 +271,44 @@ func TestConnector_WrongPassword(t *testing.T) { ExternalIDAttr: DefaultExternalIDAttr, }} - _, err := c.ConnectorReceiver(loginRequest(testUsername, "iAmAWrongPassword"), "") + _, err := c.ConnectorReceiver(loginRequest(testUsername, "iAmAWrongPassword"), testReceiverURL) if err == nil { t.Fatal("expected wrong password to produce an error") } } +func TestCheckSameOrigin_MatchingOrigin(t *testing.T) { + req := loginRequest(testUsername, testPassword).Request + if err := checkSameOrigin(req, testReceiverURL); err != nil { + t.Fatal(err) + } +} + +func TestCheckSameOrigin_MatchingReferer(t *testing.T) { + req := loginRequest(testUsername, testPassword).Request + req.Header.Del("Origin") + req.Header.Set("Referer", testReceiverURL+"?state=foo") + if err := checkSameOrigin(req, testReceiverURL); err != nil { + t.Fatal(err) + } +} + +func TestCheckSameOrigin_MismatchedOrigin(t *testing.T) { + req := loginRequest(testUsername, testPassword).Request + req.Header.Set("Origin", "https://definitely.not.evil.com") + if err := checkSameOrigin(req, testReceiverURL); err == nil { + t.Fatal("expected a mismatched Origin header to be rejected") + } +} + +func TestCheckSameOrigin_MissingHeaders(t *testing.T) { + req := loginRequest(testUsername, testPassword).Request + req.Header.Del("Origin") + if err := checkSameOrigin(req, testReceiverURL); err == nil { + t.Fatal("expected a request with no Origin or Referer header to be rejected") + } +} + func TestExtractCredentials(t *testing.T) { req := loginRequest(testUsername, testPassword).Request username, password, err := extractCredentials(req) From 47cb774ade320034c7b0806ba47661b9c5913916 Mon Sep 17 00:00:00 2001 From: broccoli Date: Sat, 19 Sep 2026 12:13:08 +0200 Subject: [PATCH 09/10] feat: renamed module and added integration tests + module path is now github.com/apache/answer-plugins/connector-ldap + added ldap_integration_test.go: StartTLS, LDAPS, and private CA rejection tests using github.com/jimlambrt/gldap/testdirectory. gldap is an in-process fake ldap server with real tls support. runs with 'go test ./....' --- connector-ldap/go.mod | 13 ++- connector-ldap/go.sum | 135 +++++------------------- connector-ldap/ldap.go | 2 +- connector-ldap/ldap_integration_test.go | 114 ++++++++++++++++++++ 4 files changed, 150 insertions(+), 114 deletions(-) create mode 100644 connector-ldap/ldap_integration_test.go diff --git a/connector-ldap/go.mod b/connector-ldap/go.mod index 30d477d8a..a80cc115d 100644 --- a/connector-ldap/go.mod +++ b/connector-ldap/go.mod @@ -1,4 +1,4 @@ -module github.com/DanielAuerX/answer-plugins/connector-ldap +module github.com/apache/answer-plugins/connector-ldap go 1.22.0 @@ -7,7 +7,9 @@ toolchain go1.24.2 require ( github.com/apache/answer v1.4.2-RC1.0.20250107023923-061894735091 github.com/apache/answer-plugins/util v1.0.3-0.20250107030257-cf94ebc70954 + github.com/gin-gonic/gin v1.10.0 github.com/go-ldap/ldap/v3 v3.4.10 + github.com/jimlambrt/gldap v0.1.14 github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f ) @@ -17,11 +19,13 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/bytedance/sonic v1.12.2 // indirect github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fatih/color v1.17.0 // indirect github.com/gabriel-vasile/mimetype v1.4.5 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.10.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -30,20 +34,25 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/google/wire v0.5.0 // indirect github.com/gorilla/css v1.0.1 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f // indirect + github.com/stretchr/testify v1.9.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.10.0 // indirect golang.org/x/crypto v0.31.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/connector-ldap/go.sum b/connector-ldap/go.sum index 2485d459e..635e0565c 100644 --- a/connector-ldap/go.sum +++ b/connector-ldap/go.sum @@ -1,60 +1,36 @@ -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= -github.com/Machiel/slugify v1.0.1/go.mod h1:fTFGn5uWEynW4CUMG7sWkYXOf1UgDxyTM3DbR6Qfg3k= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267/go.mod h1:Yj3yPP/vi87JjwylUTCMyd6FrOfGqP1AHk0305hDm2o= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/apache/answer v1.4.2-RC1.0.20250107023923-061894735091 h1:TmUPU0tX3VzbUO7rCBW3hJDUGO/WOI343zcbymIWQSM= github.com/apache/answer v1.4.2-RC1.0.20250107023923-061894735091/go.mod h1:ehAJmrP4X9kBJKlYzTtRrwRbKeRqjnHxyYlhf11yzfw= github.com/apache/answer-plugins/util v1.0.3-0.20250107030257-cf94ebc70954 h1:jVqxzyeHvYxAT30vGfHXtZLNoi9qAWnvTtuMSj6pFys= github.com/apache/answer-plugins/util v1.0.3-0.20250107030257-cf94ebc70954/go.mod h1:wQEKNXVa/BKKq5yro9qo5bFiO3/SW1noORabxEndk3o= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg= github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= -github.com/docker/cli v27.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs= -github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc= -github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= -github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102/go.mod h1:6+tQXZ+I62x13UZ+hemLVoZIuq/usVzvau7bqwUo9P0= -github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= -github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= -github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d/go.mod h1:yTR3tKgyk20phAFg6IE9ulMA5NjEDD2wyx+okRFLVtw= -github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -63,13 +39,8 @@ github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -78,21 +49,13 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -102,13 +65,11 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -121,9 +82,8 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jimlambrt/gldap v0.1.14 h1:InG9kldhIu6OoQK0hvfkW1Lqpc5eLJhxiiDTNmRnrDM= +github.com/jimlambrt/gldap v0.1.14/go.mod h1:yobW9JIAmqe23dVNOaMWewPaff6jGaHgYjspPIIgYmg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -136,58 +96,33 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= -github.com/lestrrat-go/strftime v1.1.0/go.mod h1:uzeIB52CeUJenCo1syghlugshMysrqUT51HlxphXVeI= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mozillazg/go-pinyin v0.20.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= -github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= -github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405/go.mod h1:rIxVzVLKlBwLxO+lC+k/I4HJfRQcemg/f/76Xmmzsec= github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0= github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= -github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822083413-c0075a2d401f/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs= -github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20230822083413-c0075a2d401f/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f h1:xia6AXJor4UV4T6htmHlfN7CGXZ04vlWwybVtFKJ/mA= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E= -github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20230822083413-c0075a2d401f/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= -github.com/segmentfault/pacman/contrib/server/http v0.0.0-20230822083413-c0075a2d401f/go.mod h1:UjNiOFYv1uGCq1ZCcONaKq4eE7MW3nbgpLqgl8f9N40= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -195,30 +130,16 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= -github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= -github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= -github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= -github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -229,8 +150,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= -golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -257,10 +178,16 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -296,17 +223,12 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -314,15 +236,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= -modernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= -xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw= diff --git a/connector-ldap/ldap.go b/connector-ldap/ldap.go index 993a4fc4b..6bbe7a602 100644 --- a/connector-ldap/ldap.go +++ b/connector-ldap/ldap.go @@ -12,7 +12,7 @@ import ( "os" "strings" - "github.com/DanielAuerX/answer-plugins/connector-ldap/i18n" + "github.com/apache/answer-plugins/connector-ldap/i18n" "github.com/segmentfault/pacman/log" "github.com/apache/answer-plugins/util" diff --git a/connector-ldap/ldap_integration_test.go b/connector-ldap/ldap_integration_test.go new file mode 100644 index 000000000..67ba06eae --- /dev/null +++ b/connector-ldap/ldap_integration_test.go @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package ldap + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/jimlambrt/gldap" + "github.com/jimlambrt/gldap/testdirectory" +) + +func newTestDirectoryEntry() *gldap.Entry { + dn := "uid=" + testUsername + "," + testBaseDN + return gldap.NewEntry(dn, map[string][]string{ + LdapAttributeUid: {testUsername}, + LdapAttributeCn: {testUsername}, + LdapAttributeMail: {testEmail}, + DefaultExternalIDAttr: {testExternalID}, + "password": {testPassword}, + }) +} + +func caCertFile(t *testing.T, pemCert string) string { + path := filepath.Join(t.TempDir(), "ca.crt") + if err := os.WriteFile(path, []byte(pemCert), 0o600); err != nil { + t.Fatal(err) + } + return path +} + +func TestIntegration_StartTLS(t *testing.T) { + td := testdirectory.Start(t, testdirectory.WithNoTLS(t)) + td.SetUsers(newTestDirectoryEntry()) + + c := &Connector{Config: &ConnectorConfig{ + Server: fmt.Sprintf("ldap://%s:%d", td.Host(), td.Port()), + BaseDN: testBaseDN, + BindDN: "uid=" + testUsername + "," + testBaseDN, + BindPassword: testPassword, + UserAttr: LdapAttributeUid, + ExternalIDAttr: DefaultExternalIDAttr, + TLSCACertPath: caCertFile(t, td.Cert()), + }} + + userInfo, err := c.ConnectorReceiver(loginRequest(testUsername, testPassword), testReceiverURL) + if err != nil { + t.Fatal(err) + } + if userInfo.ExternalID != testExternalID { + t.Fatalf("expected external ID %q, got %q", testExternalID, userInfo.ExternalID) + } +} + +func TestIntegration_LDAPS(t *testing.T) { + td := testdirectory.Start(t) + td.SetUsers(newTestDirectoryEntry()) + + c := &Connector{Config: &ConnectorConfig{ + Server: fmt.Sprintf("ldaps://%s:%d", td.Host(), td.Port()), + BaseDN: testBaseDN, + BindDN: "uid=" + testUsername + "," + testBaseDN, + BindPassword: testPassword, + UserAttr: LdapAttributeUid, + ExternalIDAttr: DefaultExternalIDAttr, + TLSCACertPath: caCertFile(t, td.Cert()), + }} + + userInfo, err := c.ConnectorReceiver(loginRequest(testUsername, testPassword), testReceiverURL) + if err != nil { + t.Fatal(err) + } + if userInfo.ExternalID != testExternalID { + t.Fatalf("expected external ID %q, got %q", testExternalID, userInfo.ExternalID) + } +} + +func TestIntegration_PrivateCA_RejectedWithoutIt(t *testing.T) { + td := testdirectory.Start(t) + td.SetUsers(newTestDirectoryEntry()) + + c := &Connector{Config: &ConnectorConfig{ + Server: fmt.Sprintf("ldaps://%s:%d", td.Host(), td.Port()), + BaseDN: testBaseDN, + BindDN: "uid=" + testUsername + "," + testBaseDN, + BindPassword: testPassword, + UserAttr: LdapAttributeUid, + ExternalIDAttr: DefaultExternalIDAttr, + }} + + _, err := c.ConnectorReceiver(loginRequest(testUsername, testPassword), testReceiverURL) + if err == nil { + t.Fatal("expected the connection to fail without the private CA configured") + } +} From 8b9a915c6df14dc1a65c9665a8d975ccbfdc9129 Mon Sep 17 00:00:00 2001 From: broccoli Date: Sat, 19 Sep 2026 13:14:37 +0200 Subject: [PATCH 10/10] feat: moved and added translations accessing translations via translation.go not hard coded. Added some German translations for login.html. --- connector-ldap/i18n/de_DE.yaml | 31 +++++++++++++++++ connector-ldap/i18n/en_US.yaml | 53 +++++++++++++++++++++++++++++- connector-ldap/i18n/translation.go | 23 +++++++++++++ connector-ldap/ldap.go | 22 ++++++++----- connector-ldap/login.html | 16 +++++---- 5 files changed, 128 insertions(+), 17 deletions(-) create mode 100644 connector-ldap/i18n/de_DE.yaml diff --git a/connector-ldap/i18n/de_DE.yaml b/connector-ldap/i18n/de_DE.yaml new file mode 100644 index 000000000..e50e7db92 --- /dev/null +++ b/connector-ldap/i18n/de_DE.yaml @@ -0,0 +1,31 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +plugin: + ldap_connector: + backend: + login: + title: + other: Anmeldung + subtitle: + other: Mit LDAP-Konto anmelden + username: + other: Benutzername + password: + other: Passwort + submit: + other: Anmelden diff --git a/connector-ldap/i18n/en_US.yaml b/connector-ldap/i18n/en_US.yaml index 57618f56c..ad71ff860 100644 --- a/connector-ldap/i18n/en_US.yaml +++ b/connector-ldap/i18n/en_US.yaml @@ -25,4 +25,55 @@ plugin: other: LDAP Connector description: other: Connect to LDAP for third-party login - \ No newline at end of file + config: + name: + title: + other: LDAP + description: + other: LDAP connector name + server: + title: + other: LDAP Server + description: + other: e.g. ldaps://ldap.example.com:636 + base_dn: + title: + other: Base DN + description: + other: e.g. dc=example,dc=com + bind_dn: + title: + other: Bind DN + description: + other: DN of LDAP bind user + bind_password: + title: + other: Bind Password + description: + other: Password for bind DN + user_attr: + title: + other: User Attribute + description: + other: LDAP attribute for username (e.g., uid or sAMAccountName) + external_id_attr: + title: + other: External ID Attribute + description: + other: Stable LDAP attribute used to identify the user across logins, e.g. entryUUID (OpenLDAP) or objectGUID (Active Directory). Do not use a mutable attribute like uid. + tls_ca_cert_path: + title: + other: TLS CA Certificate Path + description: + other: Path to custom CA certificate file (optional) + login: + title: + other: Login + subtitle: + other: Sign in with your LDAP account + username: + other: Username + password: + other: Password + submit: + other: Login diff --git a/connector-ldap/i18n/translation.go b/connector-ldap/i18n/translation.go index 98cf62f46..923c020c5 100644 --- a/connector-ldap/i18n/translation.go +++ b/connector-ldap/i18n/translation.go @@ -23,4 +23,27 @@ const ( ConnectorName = "plugin.ldap_connector.backend.name" InfoName = "plugin.ldap_connector.backend.info.name" InfoDescription = "plugin.ldap_connector.backend.info.description" + + ConfigNameTitle = "plugin.ldap_connector.backend.config.name.title" + ConfigNameDescription = "plugin.ldap_connector.backend.config.name.description" + ConfigServerTitle = "plugin.ldap_connector.backend.config.server.title" + ConfigServerDescription = "plugin.ldap_connector.backend.config.server.description" + ConfigBaseDNTitle = "plugin.ldap_connector.backend.config.base_dn.title" + ConfigBaseDNDescription = "plugin.ldap_connector.backend.config.base_dn.description" + ConfigBindDNTitle = "plugin.ldap_connector.backend.config.bind_dn.title" + ConfigBindDNDescription = "plugin.ldap_connector.backend.config.bind_dn.description" + ConfigBindPasswordTitle = "plugin.ldap_connector.backend.config.bind_password.title" + ConfigBindPasswordDescription = "plugin.ldap_connector.backend.config.bind_password.description" + ConfigUserAttrTitle = "plugin.ldap_connector.backend.config.user_attr.title" + ConfigUserAttrDescription = "plugin.ldap_connector.backend.config.user_attr.description" + ConfigExternalIDAttrTitle = "plugin.ldap_connector.backend.config.external_id_attr.title" + ConfigExternalIDAttrDescription = "plugin.ldap_connector.backend.config.external_id_attr.description" + ConfigTLSCACertPathTitle = "plugin.ldap_connector.backend.config.tls_ca_cert_path.title" + ConfigTLSCACertPathDescription = "plugin.ldap_connector.backend.config.tls_ca_cert_path.description" + + LoginTitle = "plugin.ldap_connector.backend.login.title" + LoginSubtitle = "plugin.ldap_connector.backend.login.subtitle" + LoginUsername = "plugin.ldap_connector.backend.login.username" + LoginPassword = "plugin.ldap_connector.backend.login.password" + LoginSubmit = "plugin.ldap_connector.backend.login.submit" ) diff --git a/connector-ldap/ldap.go b/connector-ldap/ldap.go index 6bbe7a602..2cd321a9d 100644 --- a/connector-ldap/ldap.go +++ b/connector-ldap/ldap.go @@ -115,6 +115,11 @@ func (g *Connector) ConnectorSender(ctx *plugin.GinContext, receiverURL string) } htmlContent := strings.Replace(loginHTMLContent, "RECEIVER_URL_PLACEHOLDER", receiverURL, -1) + htmlContent = strings.Replace(htmlContent, "LOGIN_TITLE_PLACEHOLDER", plugin.Translate(ctx, i18n.LoginTitle), -1) + htmlContent = strings.Replace(htmlContent, "LOGIN_SUBTITLE_PLACEHOLDER", plugin.Translate(ctx, i18n.LoginSubtitle), -1) + htmlContent = strings.Replace(htmlContent, "LOGIN_USERNAME_PLACEHOLDER", plugin.Translate(ctx, i18n.LoginUsername), -1) + htmlContent = strings.Replace(htmlContent, "LOGIN_PASSWORD_PLACEHOLDER", plugin.Translate(ctx, i18n.LoginPassword), -1) + htmlContent = strings.Replace(htmlContent, "LOGIN_SUBMIT_PLACEHOLDER", plugin.Translate(ctx, i18n.LoginSubmit), -1) ctx.Writer.WriteHeader(200) ctx.Writer.Header().Set("Content-Type", "text/html") err := writeHtmlContent(ctx, htmlContent) @@ -132,17 +137,16 @@ func writeHtmlContent(ctx *plugin.GinContext, htmlContent string) error { return err } -// TODO get from translator func (g *Connector) ConfigFields() []plugin.ConfigField { return []plugin.ConfigField{ - createTextInput("name", "LDAP", "LDAP connector name", g.Config.Name, true, false), - createTextInput("server", "LDAP Server", "e.g. ldaps://ldap.example.com:636", g.Config.Server, true, false), - createTextInput("base_dn", "Base DN", "e.g. dc=example,dc=com", g.Config.BaseDN, true, false), - createTextInput("bind_dn", "Bind DN", "DN of LDAP bind user", g.Config.BindDN, true, false), - createTextInput("bind_password", "Bind Password", "Password for bind DN", g.Config.BindPassword, true, true), - createTextInput("user_attr", "User Attribute", "LDAP attribute for username (e.g., uid or sAMAccountName)", g.Config.UserAttr, true, false), - createTextInput("external_id_attr", "External ID Attribute", "Stable LDAP attribute used to identify the user across logins, e.g. entryUUID (OpenLDAP) or objectGUID (Active Directory). Do not use a mutable attribute like uid.", externalIDAttrOrDefault(g.Config.ExternalIDAttr), true, false), - createTextInput("tls_ca_cert_path", "TLS CA Certificate Path", "Path to custom CA certificate file (optional)", g.Config.TLSCACertPath, false, false), + createTextInput("name", i18n.ConfigNameTitle, i18n.ConfigNameDescription, g.Config.Name, true, false), + createTextInput("server", i18n.ConfigServerTitle, i18n.ConfigServerDescription, g.Config.Server, true, false), + createTextInput("base_dn", i18n.ConfigBaseDNTitle, i18n.ConfigBaseDNDescription, g.Config.BaseDN, true, false), + createTextInput("bind_dn", i18n.ConfigBindDNTitle, i18n.ConfigBindDNDescription, g.Config.BindDN, true, false), + createTextInput("bind_password", i18n.ConfigBindPasswordTitle, i18n.ConfigBindPasswordDescription, g.Config.BindPassword, true, true), + createTextInput("user_attr", i18n.ConfigUserAttrTitle, i18n.ConfigUserAttrDescription, g.Config.UserAttr, true, false), + createTextInput("external_id_attr", i18n.ConfigExternalIDAttrTitle, i18n.ConfigExternalIDAttrDescription, externalIDAttrOrDefault(g.Config.ExternalIDAttr), true, false), + createTextInput("tls_ca_cert_path", i18n.ConfigTLSCACertPathTitle, i18n.ConfigTLSCACertPathDescription, g.Config.TLSCACertPath, false, false), } } diff --git a/connector-ldap/login.html b/connector-ldap/login.html index 3f00c1ca0..9605eb780 100644 --- a/connector-ldap/login.html +++ b/connector-ldap/login.html @@ -3,7 +3,7 @@ - Login + LOGIN_TITLE_PLACEHOLDER