Selaa lähdekoodia

Merge pull request #783 from oragono/ldap

LDAP support
tags/v2.0.0-rc1
Shivaram Lingamneni 4 vuotta sitten
vanhempi
commit
ed3a43861f
No account linked to committer's email address
12 muutettua tiedostoa jossa 838 lisäystä ja 16 poistoa
  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 Näytä tiedosto

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

+ 44
- 6
irc/accounts.go Näytä tiedosto

15
 	"unicode"
15
 	"unicode"
16
 
16
 
17
 	"github.com/oragono/oragono/irc/caps"
17
 	"github.com/oragono/oragono/irc/caps"
18
+	"github.com/oragono/oragono/irc/ldap"
18
 	"github.com/oragono/oragono/irc/passwd"
19
 	"github.com/oragono/oragono/irc/passwd"
19
 	"github.com/oragono/oragono/irc/utils"
20
 	"github.com/oragono/oragono/irc/utils"
20
 	"github.com/tidwall/buntdb"
21
 	"github.com/tidwall/buntdb"
446
 		return err
447
 		return err
447
 	}
448
 	}
448
 
449
 
450
+	if !hasPrivs && creds.Empty() {
451
+		return errCredsExternallyManaged
452
+	}
453
+
449
 	err = creds.SetPassphrase(password, am.server.Config().Accounts.Registration.BcryptCost)
454
 	err = creds.SetPassphrase(password, am.server.Config().Accounts.Registration.BcryptCost)
450
 	if err != nil {
455
 	if err != nil {
451
 		return err
456
 		return err
500
 		return err
505
 		return err
501
 	}
506
 	}
502
 
507
 
508
+	if !hasPrivs && creds.Empty() {
509
+		return errCredsExternallyManaged
510
+	}
511
+
503
 	if add {
512
 	if add {
504
 		err = creds.AddCertfp(certfp)
513
 		err = creds.AddCertfp(certfp)
505
 	} else {
514
 	} else {
686
 	return nil
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
 func marshalReservedNicks(nicks []string) string {
707
 func marshalReservedNicks(nicks []string) string {
690
 	return strings.Join(nicks, ",")
708
 	return strings.Join(nicks, ",")
691
 }
709
 }
828
 	return
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
 func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount, err error) {
879
 func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount, err error) {

+ 2
- 0
irc/config.go Näytä tiedosto

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

+ 1
- 0
irc/errors.go Näytä tiedosto

55
 	errNoop                           = errors.New("Action was a no-op")
55
 	errNoop                           = errors.New("Action was a no-op")
56
 	errCASFailed                      = errors.New("Compare-and-swap update of database value failed")
56
 	errCASFailed                      = errors.New("Compare-and-swap update of database value failed")
57
 	errEmptyCredentials               = errors.New("No more credentials are approved")
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
 // Socket Errors
61
 // Socket Errors

+ 202
- 0
irc/ldap/LICENSE Näytä tiedosto

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 Näytä tiedosto

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 Näytä tiedosto

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 Näytä tiedosto

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 Näytä tiedosto

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 Näytä tiedosto

132
 		},
132
 		},
133
 		"saregister": {
133
 		"saregister": {
134
 			handler: nsSaregisterHandler,
134
 			handler: nsSaregisterHandler,
135
-			help: `Syntax: $bSAREGISTER <username> <password>$b
135
+			help: `Syntax: $bSAREGISTER <username> [password]$b
136
 
136
 
137
 SAREGISTER registers an account on someone else's behalf.
137
 SAREGISTER registers an account on someone else's behalf.
138
 This is for use in configurations that require SASL for all connections;
138
 This is for use in configurations that require SASL for all connections;
140
 			helpShort: `$bSAREGISTER$b registers an account on someone else's behalf.`,
140
 			helpShort: `$bSAREGISTER$b registers an account on someone else's behalf.`,
141
 			enabled:   servCmdRequiresAuthEnabled,
141
 			enabled:   servCmdRequiresAuthEnabled,
142
 			capabs:    []string{"accreg"},
142
 			capabs:    []string{"accreg"},
143
-			minParams: 2,
143
+			minParams: 1,
144
 		},
144
 		},
145
 		"sessions": {
145
 		"sessions": {
146
 			handler: nsSessionsHandler,
146
 			handler: nsSessionsHandler,
681
 }
681
 }
682
 
682
 
683
 func nsSaregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
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
 	if err != nil {
691
 	if err != nil {
694
 		var errMsg string
692
 		var errMsg string
830
 		nsNotice(rb, client.t("Password changed"))
828
 		nsNotice(rb, client.t("Password changed"))
831
 	case errEmptyCredentials:
829
 	case errEmptyCredentials:
832
 		nsNotice(rb, client.t("You can't delete your password unless you add a certificate fingerprint"))
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
 	case errCASFailed:
833
 	case errCASFailed:
834
 		nsNotice(rb, client.t("Try again later"))
834
 		nsNotice(rb, client.t("Try again later"))
835
 	default:
835
 	default:
961
 		nsNotice(rb, client.t("That certificate fingerprint is already associated with another account"))
961
 		nsNotice(rb, client.t("That certificate fingerprint is already associated with another account"))
962
 	case errEmptyCredentials:
962
 	case errEmptyCredentials:
963
 		nsNotice(rb, client.t("You can't remove all your certificate fingerprints unless you add a password"))
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
 	case errCASFailed:
966
 	case errCASFailed:
965
 		nsNotice(rb, client.t("Try again later"))
967
 		nsNotice(rb, client.t("Try again later"))
966
 	default:
968
 	default:

+ 35
- 0
oragono.yaml Näytä tiedosto

384
         offer-list:
384
         offer-list:
385
             #- "oragono.test"
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
 # channel options
422
 # channel options
388
 channels:
423
 channels:
389
     # modes that are set when new channels are created
424
     # modes that are set when new channels are created

+ 1
- 1
vendor

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

Loading…
Peruuta
Tallenna