Browse Source

Merge pull request #783 from oragono/ldap

LDAP support
tags/v2.0.0-rc1
Shivaram Lingamneni 4 years ago
parent
commit
ed3a43861f
No account linked to committer's email address
12 changed files with 838 additions and 16 deletions
  1. 1
    0
      go.mod
  2. 44
    6
      irc/accounts.go
  3. 2
    0
      irc/config.go
  4. 1
    0
      irc/errors.go
  5. 202
    0
      irc/ldap/LICENSE
  6. 62
    0
      irc/ldap/config.go
  7. 267
    0
      irc/ldap/grafana.go
  8. 60
    0
      irc/ldap/helpers.go
  9. 152
    0
      irc/ldap/login.go
  10. 11
    9
      irc/nickserv.go
  11. 35
    0
      oragono.yaml
  12. 1
    1
      vendor

+ 1
- 0
go.mod View File

@@ -5,6 +5,7 @@ go 1.14
5 5
 require (
6 6
 	code.cloudfoundry.org/bytefmt v0.0.0-20190819182555-854d396b647c
7 7
 	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
8
+	github.com/go-ldap/ldap/v3 v3.1.6
8 9
 	github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 // indirect
9 10
 	github.com/goshuirc/irc-go v0.0.0-20190713001546-05ecc95249a0
10 11
 	github.com/mattn/go-colorable v0.1.4

+ 44
- 6
irc/accounts.go View File

@@ -15,6 +15,7 @@ import (
15 15
 	"unicode"
16 16
 
17 17
 	"github.com/oragono/oragono/irc/caps"
18
+	"github.com/oragono/oragono/irc/ldap"
18 19
 	"github.com/oragono/oragono/irc/passwd"
19 20
 	"github.com/oragono/oragono/irc/utils"
20 21
 	"github.com/tidwall/buntdb"
@@ -446,6 +447,10 @@ func (am *AccountManager) setPassword(account string, password string, hasPrivs
446 447
 		return err
447 448
 	}
448 449
 
450
+	if !hasPrivs && creds.Empty() {
451
+		return errCredsExternallyManaged
452
+	}
453
+
449 454
 	err = creds.SetPassphrase(password, am.server.Config().Accounts.Registration.BcryptCost)
450 455
 	if err != nil {
451 456
 		return err
@@ -500,6 +505,10 @@ func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasP
500 505
 		return err
501 506
 	}
502 507
 
508
+	if !hasPrivs && creds.Empty() {
509
+		return errCredsExternallyManaged
510
+	}
511
+
503 512
 	if add {
504 513
 		err = creds.AddCertfp(certfp)
505 514
 	} else {
@@ -686,6 +695,15 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
686 695
 	return nil
687 696
 }
688 697
 
698
+// register and verify an account, for internal use
699
+func (am *AccountManager) SARegister(account, passphrase string) (err error) {
700
+	err = am.Register(nil, account, "admin", "", passphrase, "")
701
+	if err == nil {
702
+		err = am.Verify(nil, account, "")
703
+	}
704
+	return
705
+}
706
+
689 707
 func marshalReservedNicks(nicks []string) string {
690 708
 	return strings.Join(nicks, ",")
691 709
 }
@@ -828,14 +846,34 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou
828 846
 	return
829 847
 }
830 848
 
831
-func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) error {
832
-	account, err := am.checkPassphrase(accountName, passphrase)
833
-	if err != nil {
834
-		return err
849
+func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) (err error) {
850
+	var account ClientAccount
851
+
852
+	defer func() {
853
+		if err == nil {
854
+			am.Login(client, account)
855
+		}
856
+	}()
857
+
858
+	ldapConf := am.server.Config().Accounts.LDAP
859
+	if ldapConf.Enabled {
860
+		err = ldap.CheckLDAPPassphrase(ldapConf, accountName, passphrase, am.server.logger)
861
+		if err == nil {
862
+			account, err = am.LoadAccount(accountName)
863
+			// autocreate if necessary:
864
+			if err == errAccountDoesNotExist && ldapConf.Autocreate {
865
+				err = am.SARegister(accountName, "")
866
+				if err != nil {
867
+					return
868
+				}
869
+				account, err = am.LoadAccount(accountName)
870
+			}
871
+			return
872
+		}
835 873
 	}
836 874
 
837
-	am.Login(client, account)
838
-	return nil
875
+	account, err = am.checkPassphrase(accountName, passphrase)
876
+	return err
839 877
 }
840 878
 
841 879
 func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount, err error) {

+ 2
- 0
irc/config.go View File

@@ -25,6 +25,7 @@ import (
25 25
 	"github.com/oragono/oragono/irc/custime"
26 26
 	"github.com/oragono/oragono/irc/isupport"
27 27
 	"github.com/oragono/oragono/irc/languages"
28
+	"github.com/oragono/oragono/irc/ldap"
28 29
 	"github.com/oragono/oragono/irc/logger"
29 30
 	"github.com/oragono/oragono/irc/modes"
30 31
 	"github.com/oragono/oragono/irc/passwd"
@@ -68,6 +69,7 @@ type AccountConfig struct {
68 69
 		Exempted     []string
69 70
 		exemptedNets []net.IPNet
70 71
 	} `yaml:"require-sasl"`
72
+	LDAP            ldap.ServerConfig
71 73
 	LoginThrottling struct {
72 74
 		Enabled     bool
73 75
 		Duration    time.Duration

+ 1
- 0
irc/errors.go View File

@@ -55,6 +55,7 @@ var (
55 55
 	errNoop                           = errors.New("Action was a no-op")
56 56
 	errCASFailed                      = errors.New("Compare-and-swap update of database value failed")
57 57
 	errEmptyCredentials               = errors.New("No more credentials are approved")
58
+	errCredsExternallyManaged         = errors.New("Credentials are externally managed and cannot be changed here")
58 59
 )
59 60
 
60 61
 // Socket Errors

+ 202
- 0
irc/ldap/LICENSE View File

@@ -0,0 +1,202 @@
1
+
2
+                                 Apache License
3
+                           Version 2.0, January 2004
4
+                        http://www.apache.org/licenses/
5
+
6
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+   1. Definitions.
9
+
10
+      "License" shall mean the terms and conditions for use, reproduction,
11
+      and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+      "Licensor" shall mean the copyright owner or entity authorized by
14
+      the copyright owner that is granting the License.
15
+
16
+      "Legal Entity" shall mean the union of the acting entity and all
17
+      other entities that control, are controlled by, or are under common
18
+      control with that entity. For the purposes of this definition,
19
+      "control" means (i) the power, direct or indirect, to cause the
20
+      direction or management of such entity, whether by contract or
21
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+      outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+      "You" (or "Your") shall mean an individual or Legal Entity
25
+      exercising permissions granted by this License.
26
+
27
+      "Source" form shall mean the preferred form for making modifications,
28
+      including but not limited to software source code, documentation
29
+      source, and configuration files.
30
+
31
+      "Object" form shall mean any form resulting from mechanical
32
+      transformation or translation of a Source form, including but
33
+      not limited to compiled object code, generated documentation,
34
+      and conversions to other media types.
35
+
36
+      "Work" shall mean the work of authorship, whether in Source or
37
+      Object form, made available under the License, as indicated by a
38
+      copyright notice that is included in or attached to the work
39
+      (an example is provided in the Appendix below).
40
+
41
+      "Derivative Works" shall mean any work, whether in Source or Object
42
+      form, that is based on (or derived from) the Work and for which the
43
+      editorial revisions, annotations, elaborations, or other modifications
44
+      represent, as a whole, an original work of authorship. For the purposes
45
+      of this License, Derivative Works shall not include works that remain
46
+      separable from, or merely link (or bind by name) to the interfaces of,
47
+      the Work and Derivative Works thereof.
48
+
49
+      "Contribution" shall mean any work of authorship, including
50
+      the original version of the Work and any modifications or additions
51
+      to that Work or Derivative Works thereof, that is intentionally
52
+      submitted to Licensor for inclusion in the Work by the copyright owner
53
+      or by an individual or Legal Entity authorized to submit on behalf of
54
+      the copyright owner. For the purposes of this definition, "submitted"
55
+      means any form of electronic, verbal, or written communication sent
56
+      to the Licensor or its representatives, including but not limited to
57
+      communication on electronic mailing lists, source code control systems,
58
+      and issue tracking systems that are managed by, or on behalf of, the
59
+      Licensor for the purpose of discussing and improving the Work, but
60
+      excluding communication that is conspicuously marked or otherwise
61
+      designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+      "Contributor" shall mean Licensor and any individual or Legal Entity
64
+      on behalf of whom a Contribution has been received by Licensor and
65
+      subsequently incorporated within the Work.
66
+
67
+   2. Grant of Copyright License. Subject to the terms and conditions of
68
+      this License, each Contributor hereby grants to You a perpetual,
69
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+      copyright license to reproduce, prepare Derivative Works of,
71
+      publicly display, publicly perform, sublicense, and distribute the
72
+      Work and such Derivative Works in Source or Object form.
73
+
74
+   3. Grant of Patent License. Subject to the terms and conditions of
75
+      this License, each Contributor hereby grants to You a perpetual,
76
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+      (except as stated in this section) patent license to make, have made,
78
+      use, offer to sell, sell, import, and otherwise transfer the Work,
79
+      where such license applies only to those patent claims licensable
80
+      by such Contributor that are necessarily infringed by their
81
+      Contribution(s) alone or by combination of their Contribution(s)
82
+      with the Work to which such Contribution(s) was submitted. If You
83
+      institute patent litigation against any entity (including a
84
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+      or a Contribution incorporated within the Work constitutes direct
86
+      or contributory patent infringement, then any patent licenses
87
+      granted to You under this License for that Work shall terminate
88
+      as of the date such litigation is filed.
89
+
90
+   4. Redistribution. You may reproduce and distribute copies of the
91
+      Work or Derivative Works thereof in any medium, with or without
92
+      modifications, and in Source or Object form, provided that You
93
+      meet the following conditions:
94
+
95
+      (a) You must give any other recipients of the Work or
96
+          Derivative Works a copy of this License; and
97
+
98
+      (b) You must cause any modified files to carry prominent notices
99
+          stating that You changed the files; and
100
+
101
+      (c) You must retain, in the Source form of any Derivative Works
102
+          that You distribute, all copyright, patent, trademark, and
103
+          attribution notices from the Source form of the Work,
104
+          excluding those notices that do not pertain to any part of
105
+          the Derivative Works; and
106
+
107
+      (d) If the Work includes a "NOTICE" text file as part of its
108
+          distribution, then any Derivative Works that You distribute must
109
+          include a readable copy of the attribution notices contained
110
+          within such NOTICE file, excluding those notices that do not
111
+          pertain to any part of the Derivative Works, in at least one
112
+          of the following places: within a NOTICE text file distributed
113
+          as part of the Derivative Works; within the Source form or
114
+          documentation, if provided along with the Derivative Works; or,
115
+          within a display generated by the Derivative Works, if and
116
+          wherever such third-party notices normally appear. The contents
117
+          of the NOTICE file are for informational purposes only and
118
+          do not modify the License. You may add Your own attribution
119
+          notices within Derivative Works that You distribute, alongside
120
+          or as an addendum to the NOTICE text from the Work, provided
121
+          that such additional attribution notices cannot be construed
122
+          as modifying the License.
123
+
124
+      You may add Your own copyright statement to Your modifications and
125
+      may provide additional or different license terms and conditions
126
+      for use, reproduction, or distribution of Your modifications, or
127
+      for any such Derivative Works as a whole, provided Your use,
128
+      reproduction, and distribution of the Work otherwise complies with
129
+      the conditions stated in this License.
130
+
131
+   5. Submission of Contributions. Unless You explicitly state otherwise,
132
+      any Contribution intentionally submitted for inclusion in the Work
133
+      by You to the Licensor shall be under the terms and conditions of
134
+      this License, without any additional terms or conditions.
135
+      Notwithstanding the above, nothing herein shall supersede or modify
136
+      the terms of any separate license agreement you may have executed
137
+      with Licensor regarding such Contributions.
138
+
139
+   6. Trademarks. This License does not grant permission to use the trade
140
+      names, trademarks, service marks, or product names of the Licensor,
141
+      except as required for reasonable and customary use in describing the
142
+      origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+   7. Disclaimer of Warranty. Unless required by applicable law or
145
+      agreed to in writing, Licensor provides the Work (and each
146
+      Contributor provides its Contributions) on an "AS IS" BASIS,
147
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+      implied, including, without limitation, any warranties or conditions
149
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+      PARTICULAR PURPOSE. You are solely responsible for determining the
151
+      appropriateness of using or redistributing the Work and assume any
152
+      risks associated with Your exercise of permissions under this License.
153
+
154
+   8. Limitation of Liability. In no event and under no legal theory,
155
+      whether in tort (including negligence), contract, or otherwise,
156
+      unless required by applicable law (such as deliberate and grossly
157
+      negligent acts) or agreed to in writing, shall any Contributor be
158
+      liable to You for damages, including any direct, indirect, special,
159
+      incidental, or consequential damages of any character arising as a
160
+      result of this License or out of the use or inability to use the
161
+      Work (including but not limited to damages for loss of goodwill,
162
+      work stoppage, computer failure or malfunction, or any and all
163
+      other commercial damages or losses), even if such Contributor
164
+      has been advised of the possibility of such damages.
165
+
166
+   9. Accepting Warranty or Additional Liability. While redistributing
167
+      the Work or Derivative Works thereof, You may choose to offer,
168
+      and charge a fee for, acceptance of support, warranty, indemnity,
169
+      or other liability obligations and/or rights consistent with this
170
+      License. However, in accepting such obligations, You may act only
171
+      on Your own behalf and on Your sole responsibility, not on behalf
172
+      of any other Contributor, and only if You agree to indemnify,
173
+      defend, and hold each Contributor harmless for any liability
174
+      incurred by, or claims asserted against, such Contributor by reason
175
+      of your accepting any such warranty or additional liability.
176
+
177
+   END OF TERMS AND CONDITIONS
178
+
179
+   APPENDIX: How to apply the Apache License to your work.
180
+
181
+      To apply the Apache License to your work, attach the following
182
+      boilerplate notice, with the fields enclosed by brackets "[]"
183
+      replaced with your own identifying information. (Don't include
184
+      the brackets!)  The text should be enclosed in the appropriate
185
+      comment syntax for the file format. We also recommend that a
186
+      file or class name and description of purpose be included on the
187
+      same "printed page" as the copyright notice for easier
188
+      identification within third-party archives.
189
+
190
+   Copyright 2015 Grafana Labs
191
+
192
+   Licensed under the Apache License, Version 2.0 (the "License");
193
+   you may not use this file except in compliance with the License.
194
+   You may obtain a copy of the License at
195
+
196
+       http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+   Unless required by applicable law or agreed to in writing, software
199
+   distributed under the License is distributed on an "AS IS" BASIS,
200
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+   See the License for the specific language governing permissions and
202
+   limitations under the License.

+ 62
- 0
irc/ldap/config.go View File

@@ -0,0 +1,62 @@
1
+// Copyright 2014-2018 Grafana Labs
2
+// Released under the Apache 2.0 license
3
+
4
+// Modification notice:
5
+// 1. All field names were changed from toml and snake case to yaml and kebab case,
6
+//    matching the Oragono project conventions
7
+// 2. Four fields were added:
8
+//    2.1 `Enabled`
9
+//    2.2 `Autocreate`
10
+//    2.3 `Timeout`
11
+//    2.4 `RequireGroups`
12
+
13
+// XXX: none of AttributeMap does anything in oragono, except MemberOf,
14
+// which can be used to retrieve group memberships
15
+
16
+package ldap
17
+
18
+import (
19
+	"time"
20
+)
21
+
22
+type ServerConfig struct {
23
+	Enabled    bool
24
+	Autocreate bool
25
+
26
+	Host          string
27
+	Port          int
28
+	Timeout       time.Duration
29
+	UseSSL        bool   `yaml:"use-ssl"`
30
+	StartTLS      bool   `yaml:"start-tls"`
31
+	SkipVerifySSL bool   `yaml:"ssl-skip-verify"`
32
+	RootCACert    string `yaml:"root-ca-cert"`
33
+	ClientCert    string `yaml:"client-cert"`
34
+	ClientKey     string `yaml:"client-key"`
35
+
36
+	BindDN        string   `yaml:"bind-dn"`
37
+	BindPassword  string   `yaml:"bind-password"`
38
+	SearchFilter  string   `yaml:"search-filter"`
39
+	SearchBaseDNs []string `yaml:"search-base-dns"`
40
+
41
+	// user validation: require them to be in any one of these groups
42
+	RequireGroups []string `yaml:"require-groups"`
43
+
44
+	// two ways of testing group membership:
45
+	// either by searching for groups that match the user's DN
46
+	// and testing their names:
47
+	GroupSearchFilter              string   `yaml:"group-search-filter"`
48
+	GroupSearchFilterUserAttribute string   `yaml:"group-search-filter-user-attribute"`
49
+	GroupSearchBaseDNs             []string `yaml:"group-search-base-dns"`
50
+
51
+	// or by an attribute on the user's DN, typically named 'memberOf', but customizable:
52
+	Attr AttributeMap `yaml:"attributes"`
53
+}
54
+
55
+// AttributeMap is a struct representation for LDAP "attributes" setting
56
+type AttributeMap struct {
57
+	Username string
58
+	Name     string
59
+	Surname  string
60
+	Email    string
61
+	MemberOf string `yaml:"member-of"`
62
+}

+ 267
- 0
irc/ldap/grafana.go View File

@@ -0,0 +1,267 @@
1
+// Copyright 2014-2018 Grafana Labs
2
+// Released under the Apache 2.0 license
3
+
4
+// Modification notice:
5
+// 1. `serverConn` was substituted for `Server` as the type of the server object
6
+// 2. Debug loglines were altered to work with Oragono's logging system
7
+
8
+package ldap
9
+
10
+import (
11
+	"crypto/tls"
12
+	"crypto/x509"
13
+	"errors"
14
+	"fmt"
15
+	"io/ioutil"
16
+	"strings"
17
+
18
+	ldap "github.com/go-ldap/ldap/v3"
19
+)
20
+
21
+var (
22
+	// ErrInvalidCredentials is returned if username and password do not match
23
+	ErrInvalidCredentials = errors.New("Invalid Username or Password")
24
+
25
+	// ErrCouldNotFindUser is returned when username hasn't been found (not username+password)
26
+	ErrCouldNotFindUser = errors.New("Can't find user in LDAP")
27
+)
28
+
29
+// shouldAdminBind checks if we should use
30
+// admin username & password for LDAP bind
31
+func (server *serverConn) shouldAdminBind() bool {
32
+	return server.Config.BindPassword != ""
33
+}
34
+
35
+// singleBindDN combines the bind with the username
36
+// in order to get the proper path
37
+func (server *serverConn) singleBindDN(username string) string {
38
+	return fmt.Sprintf(server.Config.BindDN, username)
39
+}
40
+
41
+// shouldSingleBind checks if we can use "single bind" approach
42
+func (server *serverConn) shouldSingleBind() bool {
43
+	return strings.Contains(server.Config.BindDN, "%s")
44
+}
45
+
46
+// Dial dials in the LDAP
47
+// TODO: decrease cyclomatic complexity
48
+func (server *serverConn) Dial() error {
49
+	var err error
50
+	var certPool *x509.CertPool
51
+	if server.Config.RootCACert != "" {
52
+		certPool = x509.NewCertPool()
53
+		for _, caCertFile := range strings.Split(server.Config.RootCACert, " ") {
54
+			pem, err := ioutil.ReadFile(caCertFile)
55
+			if err != nil {
56
+				return err
57
+			}
58
+			if !certPool.AppendCertsFromPEM(pem) {
59
+				return errors.New("Failed to append CA certificate " + caCertFile)
60
+			}
61
+		}
62
+	}
63
+	var clientCert tls.Certificate
64
+	if server.Config.ClientCert != "" && server.Config.ClientKey != "" {
65
+		clientCert, err = tls.LoadX509KeyPair(server.Config.ClientCert, server.Config.ClientKey)
66
+		if err != nil {
67
+			return err
68
+		}
69
+	}
70
+	for _, host := range strings.Split(server.Config.Host, " ") {
71
+		address := fmt.Sprintf("%s:%d", host, server.Config.Port)
72
+		if server.Config.UseSSL {
73
+			tlsCfg := &tls.Config{
74
+				InsecureSkipVerify: server.Config.SkipVerifySSL,
75
+				ServerName:         host,
76
+				RootCAs:            certPool,
77
+			}
78
+			if len(clientCert.Certificate) > 0 {
79
+				tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
80
+			}
81
+			if server.Config.StartTLS {
82
+				server.Connection, err = ldap.Dial("tcp", address)
83
+				if err == nil {
84
+					if err = server.Connection.StartTLS(tlsCfg); err == nil {
85
+						return nil
86
+					}
87
+				}
88
+			} else {
89
+				server.Connection, err = ldap.DialTLS("tcp", address, tlsCfg)
90
+			}
91
+		} else {
92
+			server.Connection, err = ldap.Dial("tcp", address)
93
+		}
94
+
95
+		if err == nil {
96
+			return nil
97
+		}
98
+	}
99
+	return err
100
+}
101
+
102
+// Close closes the LDAP connection
103
+// Dial() sets the connection with the server for this Struct. Therefore, we require a
104
+// call to Dial() before being able to execute this function.
105
+func (server *serverConn) Close() {
106
+	server.Connection.Close()
107
+}
108
+
109
+// userBind binds the user with the LDAP server
110
+func (server *serverConn) userBind(path, password string) error {
111
+	err := server.Connection.Bind(path, password)
112
+	if err != nil {
113
+		if ldapErr, ok := err.(*ldap.Error); ok {
114
+			if ldapErr.ResultCode == 49 {
115
+				return ErrInvalidCredentials
116
+			}
117
+		}
118
+		return err
119
+	}
120
+
121
+	return nil
122
+}
123
+
124
+// users is helper method for the Users()
125
+func (server *serverConn) users(logins []string) (
126
+	[]*ldap.Entry,
127
+	error,
128
+) {
129
+	var result *ldap.SearchResult
130
+	var Config = server.Config
131
+	var err error
132
+
133
+	for _, base := range Config.SearchBaseDNs {
134
+		result, err = server.Connection.Search(
135
+			server.getSearchRequest(base, logins),
136
+		)
137
+		if err != nil {
138
+			return nil, err
139
+		}
140
+
141
+		if len(result.Entries) > 0 {
142
+			break
143
+		}
144
+	}
145
+
146
+	return result.Entries, nil
147
+}
148
+
149
+// getSearchRequest returns LDAP search request for users
150
+func (server *serverConn) getSearchRequest(
151
+	base string,
152
+	logins []string,
153
+) *ldap.SearchRequest {
154
+	attributes := []string{}
155
+
156
+	inputs := server.Config.Attr
157
+	attributes = appendIfNotEmpty(
158
+		attributes,
159
+		inputs.Username,
160
+		inputs.Surname,
161
+		inputs.Email,
162
+		inputs.Name,
163
+		inputs.MemberOf,
164
+
165
+		// In case for the POSIX LDAP schema server
166
+		server.Config.GroupSearchFilterUserAttribute,
167
+	)
168
+
169
+	search := ""
170
+	for _, login := range logins {
171
+		query := strings.Replace(
172
+			server.Config.SearchFilter,
173
+			"%s", ldap.EscapeFilter(login),
174
+			-1,
175
+		)
176
+
177
+		search = search + query
178
+	}
179
+
180
+	filter := fmt.Sprintf("(|%s)", search)
181
+
182
+	return &ldap.SearchRequest{
183
+		BaseDN:       base,
184
+		Scope:        ldap.ScopeWholeSubtree,
185
+		DerefAliases: ldap.NeverDerefAliases,
186
+		Attributes:   attributes,
187
+		Filter:       filter,
188
+	}
189
+}
190
+
191
+// requestMemberOf use this function when POSIX LDAP
192
+// schema does not support memberOf, so it manually search the groups
193
+func (server *serverConn) requestMemberOf(entry *ldap.Entry) ([]string, error) {
194
+	var memberOf []string
195
+	var config = server.Config
196
+
197
+	for _, groupSearchBase := range config.GroupSearchBaseDNs {
198
+		var filterReplace string
199
+		if config.GroupSearchFilterUserAttribute == "" {
200
+			filterReplace = getAttribute(config.Attr.Username, entry)
201
+		} else {
202
+			filterReplace = getAttribute(
203
+				config.GroupSearchFilterUserAttribute,
204
+				entry,
205
+			)
206
+		}
207
+
208
+		filter := strings.Replace(
209
+			config.GroupSearchFilter, "%s",
210
+			ldap.EscapeFilter(filterReplace),
211
+			-1,
212
+		)
213
+
214
+		server.logger.Debug("ldap", "Searching for groups with filter", filter)
215
+
216
+		// support old way of reading settings
217
+		groupIDAttribute := config.Attr.MemberOf
218
+		// but prefer dn attribute if default settings are used
219
+		if groupIDAttribute == "" || groupIDAttribute == "memberOf" {
220
+			groupIDAttribute = "dn"
221
+		}
222
+
223
+		groupSearchReq := ldap.SearchRequest{
224
+			BaseDN:       groupSearchBase,
225
+			Scope:        ldap.ScopeWholeSubtree,
226
+			DerefAliases: ldap.NeverDerefAliases,
227
+			Attributes:   []string{groupIDAttribute},
228
+			Filter:       filter,
229
+		}
230
+
231
+		groupSearchResult, err := server.Connection.Search(&groupSearchReq)
232
+		if err != nil {
233
+			return nil, err
234
+		}
235
+
236
+		if len(groupSearchResult.Entries) > 0 {
237
+			for _, group := range groupSearchResult.Entries {
238
+
239
+				memberOf = append(
240
+					memberOf,
241
+					getAttribute(groupIDAttribute, group),
242
+				)
243
+			}
244
+			break
245
+		}
246
+	}
247
+
248
+	return memberOf, nil
249
+}
250
+
251
+// getMemberOf finds memberOf property or request it
252
+func (server *serverConn) getMemberOf(result *ldap.Entry) (
253
+	[]string, error,
254
+) {
255
+	if server.Config.GroupSearchFilter == "" {
256
+		memberOf := getArrayAttribute(server.Config.Attr.MemberOf, result)
257
+
258
+		return memberOf, nil
259
+	}
260
+
261
+	memberOf, err := server.requestMemberOf(result)
262
+	if err != nil {
263
+		return nil, err
264
+	}
265
+
266
+	return memberOf, nil
267
+}

+ 60
- 0
irc/ldap/helpers.go View File

@@ -0,0 +1,60 @@
1
+// Copyright 2014-2018 Grafana Labs
2
+// Released under the Apache 2.0 license
3
+
4
+package ldap
5
+
6
+import (
7
+	"strings"
8
+
9
+	ldap "github.com/go-ldap/ldap/v3"
10
+)
11
+
12
+func isMemberOf(memberOf []string, group string) bool {
13
+	if group == "*" {
14
+		return true
15
+	}
16
+
17
+	for _, member := range memberOf {
18
+		if strings.EqualFold(member, group) {
19
+			return true
20
+		}
21
+	}
22
+	return false
23
+}
24
+
25
+func getArrayAttribute(name string, entry *ldap.Entry) []string {
26
+	if strings.ToLower(name) == "dn" {
27
+		return []string{entry.DN}
28
+	}
29
+
30
+	for _, attr := range entry.Attributes {
31
+		if attr.Name == name && len(attr.Values) > 0 {
32
+			return attr.Values
33
+		}
34
+	}
35
+	return []string{}
36
+}
37
+
38
+func getAttribute(name string, entry *ldap.Entry) string {
39
+	if strings.ToLower(name) == "dn" {
40
+		return entry.DN
41
+	}
42
+
43
+	for _, attr := range entry.Attributes {
44
+		if attr.Name == name {
45
+			if len(attr.Values) > 0 {
46
+				return attr.Values[0]
47
+			}
48
+		}
49
+	}
50
+	return ""
51
+}
52
+
53
+func appendIfNotEmpty(slice []string, values ...string) []string {
54
+	for _, v := range values {
55
+		if v != "" {
56
+			slice = append(slice, v)
57
+		}
58
+	}
59
+	return slice
60
+}

+ 152
- 0
irc/ldap/login.go View File

@@ -0,0 +1,152 @@
1
+// Copyright (c) 2020 Matt Ouille
2
+// Copyright (c) 2020 Shivaram Lingamneni
3
+// released under the MIT license
4
+
5
+// Portions of this code copyright Grafana Labs and contributors
6
+// and released under the Apache 2.0 license
7
+
8
+// Copying Grafana's original comment on the different cases for LDAP:
9
+// There are several cases -
10
+// 1. "admin" user
11
+// Bind the "admin" user (defined in Grafana config file) which has the search privileges
12
+// in LDAP server, then we search the targeted user through that bind, then the second
13
+// perform the bind via passed login/password.
14
+// 2. Single bind
15
+// // If all the users meant to be used with Grafana have the ability to search in LDAP server
16
+// then we bind with LDAP server with targeted login/password
17
+// and then search for the said user in order to retrive all the information about them
18
+// 3. Unauthenticated bind
19
+// For some LDAP configurations it is allowed to search the
20
+// user without login/password binding with LDAP server, in such case
21
+// we will perform "unauthenticated bind", then search for the
22
+// targeted user and then perform the bind with passed login/password.
23
+
24
+// Note: the only validation we do on users is to check RequiredGroups.
25
+// If RequiredGroups is not set and we can do a single bind, we don't
26
+// even need to search. So our case 2 is not restricted
27
+// to setups where all the users have search privileges: we only need to
28
+// be able to do DN resolution via pure string substitution.
29
+
30
+package ldap
31
+
32
+import (
33
+	"errors"
34
+	"fmt"
35
+
36
+	ldap "github.com/go-ldap/ldap/v3"
37
+
38
+	"github.com/oragono/oragono/irc/logger"
39
+)
40
+
41
+var (
42
+	ErrUserNotInRequiredGroup = errors.New("User is not a member of any required groups")
43
+)
44
+
45
+// equivalent of Grafana's `Server`, but unexported
46
+// also, `log` was renamed to `logger`, since the APIs are slightly different
47
+// and this way the compiler will catch any unchanged references to Grafana's `Server.log`
48
+type serverConn struct {
49
+	Config     *ServerConfig
50
+	Connection *ldap.Conn
51
+	logger     *logger.Manager
52
+}
53
+
54
+func CheckLDAPPassphrase(config ServerConfig, accountName, passphrase string, log *logger.Manager) (err error) {
55
+	defer func() {
56
+		if err != nil {
57
+			log.Debug("ldap", "failed passphrase check", err.Error())
58
+		}
59
+	}()
60
+
61
+	server := serverConn{
62
+		Config: &config,
63
+		logger: log,
64
+	}
65
+
66
+	err = server.Dial()
67
+	if err != nil {
68
+		return
69
+	}
70
+	defer server.Close()
71
+
72
+	server.Connection.SetTimeout(config.Timeout)
73
+
74
+	passphraseChecked := false
75
+
76
+	if server.shouldSingleBind() {
77
+		log.Debug("ldap", "attempting single bind to", accountName)
78
+		err = server.userBind(server.singleBindDN(accountName), passphrase)
79
+		passphraseChecked = (err == nil)
80
+	} else if server.shouldAdminBind() {
81
+		log.Debug("ldap", "attempting admin bind to", config.BindDN)
82
+		err = server.userBind(config.BindDN, config.BindPassword)
83
+	} else {
84
+		log.Debug("ldap", "attempting unauthenticated bind")
85
+		err = server.Connection.UnauthenticatedBind(config.BindDN)
86
+	}
87
+
88
+	if err != nil {
89
+		return
90
+	}
91
+
92
+	if passphraseChecked && len(config.RequireGroups) == 0 {
93
+		return nil
94
+	}
95
+
96
+	users, err := server.users([]string{accountName})
97
+	if err != nil {
98
+		log.Debug("ldap", "failed user lookup")
99
+		return err
100
+	}
101
+
102
+	if len(users) == 0 {
103
+		return ErrCouldNotFindUser
104
+	}
105
+
106
+	user := users[0]
107
+
108
+	log.Debug("ldap", "looked up user", user.DN)
109
+
110
+	err = server.validateGroupMembership(user)
111
+	if err != nil {
112
+		return err
113
+	}
114
+
115
+	if !passphraseChecked {
116
+		log.Debug("ldap", "rebinding", user.DN)
117
+		err = server.userBind(user.DN, passphrase)
118
+	}
119
+
120
+	return err
121
+}
122
+
123
+func (server *serverConn) validateGroupMembership(user *ldap.Entry) (err error) {
124
+	if len(server.Config.RequireGroups) == 0 {
125
+		return
126
+	}
127
+
128
+	var memberOf []string
129
+	memberOf, err = server.getMemberOf(user)
130
+	if err != nil {
131
+		server.logger.Debug("ldap", "could not retrieve group memberships", err.Error())
132
+		return
133
+	}
134
+	server.logger.Debug("ldap", fmt.Sprintf("found group memberships: %v", memberOf))
135
+	foundGroup := false
136
+	for _, inGroup := range memberOf {
137
+		for _, acceptableGroup := range server.Config.RequireGroups {
138
+			if inGroup == acceptableGroup {
139
+				foundGroup = true
140
+				break
141
+			}
142
+		}
143
+		if foundGroup {
144
+			break
145
+		}
146
+	}
147
+	if foundGroup {
148
+		return nil
149
+	} else {
150
+		return ErrUserNotInRequiredGroup
151
+	}
152
+}

+ 11
- 9
irc/nickserv.go View File

@@ -132,7 +132,7 @@ SADROP forcibly de-links the given nickname from the attached user account.`,
132 132
 		},
133 133
 		"saregister": {
134 134
 			handler: nsSaregisterHandler,
135
-			help: `Syntax: $bSAREGISTER <username> <password>$b
135
+			help: `Syntax: $bSAREGISTER <username> [password]$b
136 136
 
137 137
 SAREGISTER registers an account on someone else's behalf.
138 138
 This is for use in configurations that require SASL for all connections;
@@ -140,7 +140,7 @@ an administrator can set use this command to set up user accounts.`,
140 140
 			helpShort: `$bSAREGISTER$b registers an account on someone else's behalf.`,
141 141
 			enabled:   servCmdRequiresAuthEnabled,
142 142
 			capabs:    []string{"accreg"},
143
-			minParams: 2,
143
+			minParams: 1,
144 144
 		},
145 145
 		"sessions": {
146 146
 			handler: nsSessionsHandler,
@@ -681,14 +681,12 @@ func nsRegisterHandler(server *Server, client *Client, command string, params []
681 681
 }
682 682
 
683 683
 func nsSaregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
684
-	account, passphrase := params[0], params[1]
685
-	if passphrase == "*" {
686
-		passphrase = ""
687
-	}
688
-	err := server.accounts.Register(nil, account, "admin", "", passphrase, "")
689
-	if err == nil {
690
-		err = server.accounts.Verify(nil, account, "")
684
+	var account, passphrase string
685
+	account = params[0]
686
+	if 1 < len(params) && params[1] != "*" {
687
+		passphrase = params[1]
691 688
 	}
689
+	err := server.accounts.SARegister(account, passphrase)
692 690
 
693 691
 	if err != nil {
694 692
 		var errMsg string
@@ -830,6 +828,8 @@ func nsPasswdHandler(server *Server, client *Client, command string, params []st
830 828
 		nsNotice(rb, client.t("Password changed"))
831 829
 	case errEmptyCredentials:
832 830
 		nsNotice(rb, client.t("You can't delete your password unless you add a certificate fingerprint"))
831
+	case errCredsExternallyManaged:
832
+		nsNotice(rb, client.t("Your account credentials are managed externally and cannot be changed here"))
833 833
 	case errCASFailed:
834 834
 		nsNotice(rb, client.t("Try again later"))
835 835
 	default:
@@ -961,6 +961,8 @@ func nsCertHandler(server *Server, client *Client, command string, params []stri
961 961
 		nsNotice(rb, client.t("That certificate fingerprint is already associated with another account"))
962 962
 	case errEmptyCredentials:
963 963
 		nsNotice(rb, client.t("You can't remove all your certificate fingerprints unless you add a password"))
964
+	case errCredsExternallyManaged:
965
+		nsNotice(rb, client.t("Your account credentials are managed externally and cannot be changed here"))
964 966
 	case errCASFailed:
965 967
 		nsNotice(rb, client.t("Try again later"))
966 968
 	default:

+ 35
- 0
oragono.yaml View File

@@ -384,6 +384,41 @@ accounts:
384 384
         offer-list:
385 385
             #- "oragono.test"
386 386
 
387
+    # support for deferring password checking to an external LDAP server
388
+    # you should probably ignore this section! consult the grafana docs for details:
389
+    # https://grafana.com/docs/grafana/latest/auth/ldap/
390
+    # you will probably want to set require-sasl and disable accounts.registration.enabled
391
+    # ldap:
392
+    #     enabled: true
393
+    #     # should we automatically create users if their LDAP login succeeds?
394
+    #     autocreate: true
395
+    #     # example configuration that works with Forum Systems's testing server:
396
+    #     # https://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/
397
+    #     host: "ldap.forumsys.com"
398
+    #     port: 389
399
+    #     timeout: 30s
400
+    #     # example "single-bind" configuration, where we bind directly to the user's entry:
401
+    #     bind-dn: "uid=%s,dc=example,dc=com"
402
+    #     # example "admin bind" configuration, where we bind to an initial admin user,
403
+    #     # then search for the user's entry with a search filter:
404
+    #     #search-base-dns:
405
+    #     #    - "dc=example,dc=com"
406
+    #     #bind-dn: "cn=read-only-admin,dc=example,dc=com"
407
+    #     #bind-password: "password"
408
+    #     #search-filter: "(uid=%s)"
409
+    #     # example of requiring that users be in a particular group
410
+    #     # (note that this is an OR over the listed groups, not an AND):
411
+    #     #require-groups:
412
+    #     #    - "ou=mathematicians,dc=example,dc=com"
413
+    #     #group-search-filter-user-attribute: "dn"
414
+    #     #group-search-filter: "(uniqueMember=%s)"
415
+    #     #group-search-base-dns:
416
+    #     #    - "dc=example,dc=com"
417
+    #     # example of group membership testing via user attributes, as in AD
418
+    #     # or with OpenLDAP's "memberOf overlay" (overrides group-search-filter):
419
+    #     attributes:
420
+    #         member-of: "memberOf"
421
+
387 422
 # channel options
388 423
 channels:
389 424
     # modes that are set when new channels are created

+ 1
- 1
vendor

@@ -1 +1 @@
1
-Subproject commit 269a9c041579d103a1cab3ca989174e63040a7c9
1
+Subproject commit 6e49b8a260f1ba3351c17876c2e2d0044c315078

Loading…
Cancel
Save