From 0c2d8adeacd2a84a0186bc161a52e47f029a0f6c Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 11 Feb 2020 16:09:43 -0500 Subject: [PATCH] improve maintainability and license compliance 0. Maximum parity with upstream code 1. Added Apache-required modification notices 2. Added Apache license --- irc/config.go | 2 +- irc/ldap/LICENSE | 202 +++++++++++++++++++++++++++++++++ irc/ldap/config.go | 51 ++++----- irc/ldap/grafana.go | 266 ++++++++++++++++++++++++++++++++++++++++++++ irc/ldap/helpers.go | 13 ++- irc/ldap/login.go | 263 ++++++++----------------------------------- oragono.yaml | 4 +- 7 files changed, 551 insertions(+), 250 deletions(-) create mode 100644 irc/ldap/LICENSE create mode 100644 irc/ldap/grafana.go diff --git a/irc/config.go b/irc/config.go index d64fb93b..1944f383 100644 --- a/irc/config.go +++ b/irc/config.go @@ -69,7 +69,7 @@ type AccountConfig struct { Exempted []string exemptedNets []net.IPNet } `yaml:"require-sasl"` - LDAP ldap.LDAPConfig + LDAP ldap.ServerConfig LoginThrottling struct { Enabled bool Duration time.Duration diff --git a/irc/ldap/LICENSE b/irc/ldap/LICENSE new file mode 100644 index 00000000..373dde57 --- /dev/null +++ b/irc/ldap/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2015 Grafana Labs + + Licensed 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. diff --git a/irc/ldap/config.go b/irc/ldap/config.go index 6eaf4ee3..cfd981e1 100644 --- a/irc/ldap/config.go +++ b/irc/ldap/config.go @@ -1,19 +1,21 @@ -// Copyright (c) 2020 Matt Ouille -// Copyright (c) 2020 Shivaram Lingamneni -// released under the MIT license +// Copyright 2014-2018 Grafana Labs +// Released under the Apache 2.0 license -// Portions of this code copyright Grafana Labs and contributors -// and released under the Apache 2.0 license +// Modification notice: +// 1. All field names were changed from toml and snake case to yaml and kebab case, +// matching the Oragono project conventions +// 2. Two fields were added: `Autocreate` and `Timeout` + +// XXX: none of AttributeMap does anything in oragono, except MemberOf, +// which can be used to retrieve group memberships package ldap import ( - "fmt" - "strings" "time" ) -type LDAPConfig struct { +type ServerConfig struct { Enabled bool Autocreate bool @@ -22,7 +24,7 @@ type LDAPConfig struct { Timeout time.Duration UseSSL bool `yaml:"use-ssl"` StartTLS bool `yaml:"start-tls"` - SkipTLSVerify bool `yaml:"skip-tls-verify"` + SkipVerifySSL bool `yaml:"ssl-skip-verify"` RootCACert string `yaml:"root-ca-cert"` ClientCert string `yaml:"client-cert"` ClientKey string `yaml:"client-key"` @@ -35,29 +37,22 @@ type LDAPConfig struct { // user validation: require them to be in any one of these groups RequireGroups []string `yaml:"require-groups"` - // two ways of testing group membership: either via an attribute - // of the user's DN, typically named 'memberOf', but customizable: - MemberOfAttribute string `yaml:"member-of-attribute"` - // or by searching for groups that match the user's DN + // two ways of testing group membership: + // either by searching for groups that match the user's DN // and testing their names: GroupSearchFilter string `yaml:"group-search-filter"` GroupSearchFilterUserAttribute string `yaml:"group-search-filter-user-attribute"` GroupSearchBaseDNs []string `yaml:"group-search-base-dns"` + + // or by an attribute on the user's DN, typically named 'memberOf', but customizable: + Attr AttributeMap `yaml:"attributes"` } -// shouldAdminBind checks if we should use -// admin username & password for LDAP bind -func (config *LDAPConfig) shouldAdminBind() bool { - return config.BindPassword != "" -} - -// shouldSingleBind checks if we can use "single bind" approach -func (config *LDAPConfig) shouldSingleBind() bool { - return strings.Contains(config.BindDN, "%s") -} - -// singleBindDN combines the bind with the username -// in order to get the proper path -func (config *LDAPConfig) singleBindDN(username string) string { - return fmt.Sprintf(config.BindDN, username) +// AttributeMap is a struct representation for LDAP "attributes" setting +type AttributeMap struct { + Username string + Name string + Surname string + Email string + MemberOf string `yaml:"member-of"` } diff --git a/irc/ldap/grafana.go b/irc/ldap/grafana.go new file mode 100644 index 00000000..d846e671 --- /dev/null +++ b/irc/ldap/grafana.go @@ -0,0 +1,266 @@ +// Copyright 2014-2018 Grafana Labs +// Released under the Apache 2.0 license + +// Modification notice: these functions were altered by substituting +// `serverConn` for `Server`. + +package ldap + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/ioutil" + "strings" + + ldap "github.com/go-ldap/ldap/v3" +) + +var ( + // ErrInvalidCredentials is returned if username and password do not match + ErrInvalidCredentials = errors.New("Invalid Username or Password") + + // ErrCouldNotFindUser is returned when username hasn't been found (not username+password) + ErrCouldNotFindUser = errors.New("Can't find user in LDAP") +) + +// shouldAdminBind checks if we should use +// admin username & password for LDAP bind +func (server *serverConn) shouldAdminBind() bool { + return server.Config.BindPassword != "" +} + +// singleBindDN combines the bind with the username +// in order to get the proper path +func (server *serverConn) singleBindDN(username string) string { + return fmt.Sprintf(server.Config.BindDN, username) +} + +// shouldSingleBind checks if we can use "single bind" approach +func (server *serverConn) shouldSingleBind() bool { + return strings.Contains(server.Config.BindDN, "%s") +} + +// Dial dials in the LDAP +// TODO: decrease cyclomatic complexity +func (server *serverConn) Dial() error { + var err error + var certPool *x509.CertPool + if server.Config.RootCACert != "" { + certPool = x509.NewCertPool() + for _, caCertFile := range strings.Split(server.Config.RootCACert, " ") { + pem, err := ioutil.ReadFile(caCertFile) + if err != nil { + return err + } + if !certPool.AppendCertsFromPEM(pem) { + return errors.New("Failed to append CA certificate " + caCertFile) + } + } + } + var clientCert tls.Certificate + if server.Config.ClientCert != "" && server.Config.ClientKey != "" { + clientCert, err = tls.LoadX509KeyPair(server.Config.ClientCert, server.Config.ClientKey) + if err != nil { + return err + } + } + for _, host := range strings.Split(server.Config.Host, " ") { + address := fmt.Sprintf("%s:%d", host, server.Config.Port) + if server.Config.UseSSL { + tlsCfg := &tls.Config{ + InsecureSkipVerify: server.Config.SkipVerifySSL, + ServerName: host, + RootCAs: certPool, + } + if len(clientCert.Certificate) > 0 { + tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert) + } + if server.Config.StartTLS { + server.Connection, err = ldap.Dial("tcp", address) + if err == nil { + if err = server.Connection.StartTLS(tlsCfg); err == nil { + return nil + } + } + } else { + server.Connection, err = ldap.DialTLS("tcp", address, tlsCfg) + } + } else { + server.Connection, err = ldap.Dial("tcp", address) + } + + if err == nil { + return nil + } + } + return err +} + +// Close closes the LDAP connection +// Dial() sets the connection with the server for this Struct. Therefore, we require a +// call to Dial() before being able to execute this function. +func (server *serverConn) Close() { + server.Connection.Close() +} + +// userBind binds the user with the LDAP server +func (server *serverConn) userBind(path, password string) error { + err := server.Connection.Bind(path, password) + if err != nil { + if ldapErr, ok := err.(*ldap.Error); ok { + if ldapErr.ResultCode == 49 { + return ErrInvalidCredentials + } + } + return err + } + + return nil +} + +// users is helper method for the Users() +func (server *serverConn) users(logins []string) ( + []*ldap.Entry, + error, +) { + var result *ldap.SearchResult + var Config = server.Config + var err error + + for _, base := range Config.SearchBaseDNs { + result, err = server.Connection.Search( + server.getSearchRequest(base, logins), + ) + if err != nil { + return nil, err + } + + if len(result.Entries) > 0 { + break + } + } + + return result.Entries, nil +} + +// getSearchRequest returns LDAP search request for users +func (server *serverConn) getSearchRequest( + base string, + logins []string, +) *ldap.SearchRequest { + attributes := []string{} + + inputs := server.Config.Attr + attributes = appendIfNotEmpty( + attributes, + inputs.Username, + inputs.Surname, + inputs.Email, + inputs.Name, + inputs.MemberOf, + + // In case for the POSIX LDAP schema server + server.Config.GroupSearchFilterUserAttribute, + ) + + search := "" + for _, login := range logins { + query := strings.Replace( + server.Config.SearchFilter, + "%s", ldap.EscapeFilter(login), + -1, + ) + + search = search + query + } + + filter := fmt.Sprintf("(|%s)", search) + + return &ldap.SearchRequest{ + BaseDN: base, + Scope: ldap.ScopeWholeSubtree, + DerefAliases: ldap.NeverDerefAliases, + Attributes: attributes, + Filter: filter, + } +} + +// requestMemberOf use this function when POSIX LDAP +// schema does not support memberOf, so it manually search the groups +func (server *serverConn) requestMemberOf(entry *ldap.Entry) ([]string, error) { + var memberOf []string + var config = server.Config + + for _, groupSearchBase := range config.GroupSearchBaseDNs { + var filterReplace string + if config.GroupSearchFilterUserAttribute == "" { + filterReplace = getAttribute(config.Attr.Username, entry) + } else { + filterReplace = getAttribute( + config.GroupSearchFilterUserAttribute, + entry, + ) + } + + filter := strings.Replace( + config.GroupSearchFilter, "%s", + ldap.EscapeFilter(filterReplace), + -1, + ) + + server.log.Info("Searching for user's groups", "filter", filter) + + // support old way of reading settings + groupIDAttribute := config.Attr.MemberOf + // but prefer dn attribute if default settings are used + if groupIDAttribute == "" || groupIDAttribute == "memberOf" { + groupIDAttribute = "dn" + } + + groupSearchReq := ldap.SearchRequest{ + BaseDN: groupSearchBase, + Scope: ldap.ScopeWholeSubtree, + DerefAliases: ldap.NeverDerefAliases, + Attributes: []string{groupIDAttribute}, + Filter: filter, + } + + groupSearchResult, err := server.Connection.Search(&groupSearchReq) + if err != nil { + return nil, err + } + + if len(groupSearchResult.Entries) > 0 { + for _, group := range groupSearchResult.Entries { + + memberOf = append( + memberOf, + getAttribute(groupIDAttribute, group), + ) + } + break + } + } + + return memberOf, nil +} + +// getMemberOf finds memberOf property or request it +func (server *serverConn) getMemberOf(result *ldap.Entry) ( + []string, error, +) { + if server.Config.GroupSearchFilter == "" { + memberOf := getArrayAttribute(server.Config.Attr.MemberOf, result) + + return memberOf, nil + } + + memberOf, err := server.requestMemberOf(result) + if err != nil { + return nil, err + } + + return memberOf, nil +} diff --git a/irc/ldap/helpers.go b/irc/ldap/helpers.go index 12cee6ad..cadb822c 100644 --- a/irc/ldap/helpers.go +++ b/irc/ldap/helpers.go @@ -1,5 +1,5 @@ -// Copyright Grafana Labs and contributors -// and released under the Apache 2.0 license +// Copyright 2014-2018 Grafana Labs +// Released under the Apache 2.0 license package ldap @@ -49,3 +49,12 @@ func getAttribute(name string, entry *ldap.Entry) string { } return "" } + +func appendIfNotEmpty(slice []string, values ...string) []string { + for _, v := range values { + if v != "" { + slice = append(slice, v) + } + } + return slice +} diff --git a/irc/ldap/login.go b/irc/ldap/login.go index cc6938fc..a28bf8bd 100644 --- a/irc/ldap/login.go +++ b/irc/ldap/login.go @@ -30,12 +30,8 @@ package ldap import ( - "crypto/tls" - "crypto/x509" "errors" "fmt" - "io/ioutil" - "strings" ldap "github.com/go-ldap/ldap/v3" @@ -43,38 +39,48 @@ import ( ) var ( - ErrCouldNotFindUser = errors.New("No such user") ErrUserNotInRequiredGroup = errors.New("User is not a member of any required groups") - ErrInvalidCredentials = errors.New("Invalid credentials") ) -func CheckLDAPPassphrase(config LDAPConfig, accountName, passphrase string, log *logger.Manager) (err error) { +// equivalent of Grafana's `Server`, but unexported +type serverConn struct { + Config *ServerConfig + Connection *ldap.Conn + log *logger.Manager +} + +func CheckLDAPPassphrase(config ServerConfig, accountName, passphrase string, log *logger.Manager) (err error) { defer func() { if err != nil { log.Debug("ldap", "failed passphrase check", err.Error()) } }() - l, err := dial(&config) + server := serverConn{ + Config: &config, + log: log, + } + + err = server.Dial() if err != nil { return } - defer l.Close() + defer server.Close() - l.SetTimeout(config.Timeout) + server.Connection.SetTimeout(config.Timeout) passphraseChecked := false - if config.shouldSingleBind() { + if server.shouldSingleBind() { log.Debug("ldap", "attempting single bind to", accountName) - err = l.Bind(config.singleBindDN(accountName), passphrase) + err = server.userBind(server.singleBindDN(accountName), passphrase) passphraseChecked = (err == nil) - } else if config.shouldAdminBind() { + } else if server.shouldAdminBind() { log.Debug("ldap", "attempting admin bind to", config.BindDN) - err = l.Bind(config.BindDN, config.BindPassword) + err = server.userBind(config.BindDN, config.BindPassword) } else { log.Debug("ldap", "attempting unauthenticated bind") - err = l.UnauthenticatedBind(config.BindDN) + err = server.Connection.UnauthenticatedBind(config.BindDN) } if err != nil { @@ -85,7 +91,7 @@ func CheckLDAPPassphrase(config LDAPConfig, accountName, passphrase string, log return nil } - users, err := lookupUsers(l, &config, accountName) + users, err := server.users([]string{accountName}) if err != nil { log.Debug("ldap", "failed user lookup") return err @@ -99,225 +105,46 @@ func CheckLDAPPassphrase(config LDAPConfig, accountName, passphrase string, log log.Debug("ldap", "looked up user", user.DN) - err = validateGroupMembership(l, &config, user, log) + err = server.validateGroupMembership(user) if err != nil { return err } if !passphraseChecked { - // Authenticate user log.Debug("ldap", "rebinding", user.DN) - err = l.Bind(user.DN, passphrase) - if err != nil { - log.Debug("ldap", "failed rebind", err.Error()) - if ldapErr, ok := err.(*ldap.Error); ok { - if ldapErr.ResultCode == 49 { - return ErrInvalidCredentials - } - } - } - return err + err = server.userBind(user.DN, passphrase) } - return nil + return err } -func dial(config *LDAPConfig) (conn *ldap.Conn, err error) { - var certPool *x509.CertPool - if config.RootCACert != "" { - certPool = x509.NewCertPool() - for _, caCertFile := range strings.Split(config.RootCACert, " ") { - pem, err := ioutil.ReadFile(caCertFile) - if err != nil { - return nil, err - } - if !certPool.AppendCertsFromPEM(pem) { - return nil, errors.New("Failed to append CA certificate " + caCertFile) - } - } +func (server *serverConn) validateGroupMembership(user *ldap.Entry) (err error) { + if len(server.Config.RequireGroups) == 0 { + return } - var clientCert tls.Certificate - if config.ClientCert != "" && config.ClientKey != "" { - clientCert, err = tls.LoadX509KeyPair(config.ClientCert, config.ClientKey) - if err != nil { - return - } - } - for _, host := range strings.Split(config.Host, " ") { - address := fmt.Sprintf("%s:%d", host, config.Port) - if config.UseSSL { - tlsCfg := &tls.Config{ - InsecureSkipVerify: config.SkipTLSVerify, - ServerName: host, - RootCAs: certPool, - } - if len(clientCert.Certificate) > 0 { - tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert) - } - if config.StartTLS { - conn, err = ldap.Dial("tcp", address) - if err == nil { - if err = conn.StartTLS(tlsCfg); err == nil { - return - } - } - } else { - conn, err = ldap.DialTLS("tcp", address, tlsCfg) - } - } else { - conn, err = ldap.Dial("tcp", address) - } - if err == nil { - return - } + var memberOf []string + memberOf, err = server.getMemberOf(user) + if err != nil { + server.log.Debug("ldap", "could not retrieve group memberships", err.Error()) + return } - return -} - -func validateGroupMembership(conn *ldap.Conn, config *LDAPConfig, user *ldap.Entry, log *logger.Manager) (err error) { - if len(config.RequireGroups) != 0 { - var memberOf []string - memberOf, err = getMemberOf(conn, config, user) - if err != nil { - log.Debug("ldap", "could not retrieve group memberships", err.Error()) - return - } - log.Debug("ldap", fmt.Sprintf("found group memberships: %v", memberOf)) - foundGroup := false - for _, inGroup := range memberOf { - for _, acceptableGroup := range config.RequireGroups { - if inGroup == acceptableGroup { - foundGroup = true - break - } - } - if foundGroup { + server.log.Debug("ldap", fmt.Sprintf("found group memberships: %v", memberOf)) + foundGroup := false + for _, inGroup := range memberOf { + for _, acceptableGroup := range server.Config.RequireGroups { + if inGroup == acceptableGroup { + foundGroup = true break } } - if !foundGroup { - return ErrUserNotInRequiredGroup - } - } - return nil -} - -func lookupUsers(conn *ldap.Conn, config *LDAPConfig, accountName string) (results []*ldap.Entry, err error) { - var result *ldap.SearchResult - - for _, base := range config.SearchBaseDNs { - result, err = conn.Search( - getSearchRequest(config, base, accountName), - ) - if err != nil { - return nil, err - } else if len(result.Entries) > 0 { - return result.Entries, nil - } - } - - return nil, nil -} - -// getSearchRequest returns LDAP search request for users -func getSearchRequest( - config *LDAPConfig, - base string, - accountName string, -) *ldap.SearchRequest { - - var attributes []string - if config.MemberOfAttribute != "" { - attributes = []string{config.MemberOfAttribute} - } - - query := strings.Replace( - config.SearchFilter, - "%s", ldap.EscapeFilter(accountName), - -1, - ) - - return &ldap.SearchRequest{ - BaseDN: base, - Scope: ldap.ScopeWholeSubtree, - DerefAliases: ldap.NeverDerefAliases, - Attributes: attributes, - Filter: query, - } -} - -// getMemberOf finds memberOf property or request it -func getMemberOf(conn *ldap.Conn, config *LDAPConfig, result *ldap.Entry) ( - []string, error, -) { - if config.GroupSearchFilter == "" { - memberOf := getArrayAttribute(config.MemberOfAttribute, result) - - return memberOf, nil - } - - memberOf, err := requestMemberOf(conn, config, result) - if err != nil { - return nil, err - } - - return memberOf, nil -} - -// requestMemberOf use this function when POSIX LDAP -// schema does not support memberOf, so it manually search the groups -func requestMemberOf(conn *ldap.Conn, config *LDAPConfig, entry *ldap.Entry) ([]string, error) { - var memberOf []string - - for _, groupSearchBase := range config.GroupSearchBaseDNs { - var filterReplace string - if config.GroupSearchFilterUserAttribute == "" { - filterReplace = "cn" - } else { - filterReplace = getAttribute( - config.GroupSearchFilterUserAttribute, - entry, - ) - } - - filter := strings.Replace( - config.GroupSearchFilter, "%s", - ldap.EscapeFilter(filterReplace), - -1, - ) - - // support old way of reading settings - groupIDAttribute := config.MemberOfAttribute - // but prefer dn attribute if default settings are used - if groupIDAttribute == "" || groupIDAttribute == "memberOf" { - groupIDAttribute = "dn" - } - - groupSearchReq := ldap.SearchRequest{ - BaseDN: groupSearchBase, - Scope: ldap.ScopeWholeSubtree, - DerefAliases: ldap.NeverDerefAliases, - Attributes: []string{groupIDAttribute}, - Filter: filter, - } - - groupSearchResult, err := conn.Search(&groupSearchReq) - if err != nil { - return nil, err - } - - if len(groupSearchResult.Entries) > 0 { - for _, group := range groupSearchResult.Entries { - - memberOf = append( - memberOf, - getAttribute(groupIDAttribute, group), - ) - } + if foundGroup { break } } - - return memberOf, nil + if foundGroup { + return nil + } else { + return ErrUserNotInRequiredGroup + } } diff --git a/oragono.yaml b/oragono.yaml index d1e661e8..9e2f3e97 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -387,6 +387,7 @@ accounts: # support for deferring password checking to an external LDAP server # you should probably ignore this section! consult the grafana docs for details: # https://grafana.com/docs/grafana/latest/auth/ldap/ + # you will probably want to set require-sasl and disable accounts.registration.enabled # ldap: # enabled: true # # should we automatically create users if their LDAP login succeeds? @@ -412,7 +413,8 @@ accounts: # # - "dc=example,dc=com" # # example of group membership testing via user attributes, as in AD # # or with OpenLDAP's "memberOf overlay" (overrides group-search-filter): - # #member-of-attribute: "memberOf" + # attributes: + # member-of: "memberOf" # channel options channels: