Browse Source

additional LDAP support

tags/v2.0.0-rc1
Shivaram Lingamneni 4 years ago
parent
commit
c13597f807
10 changed files with 521 additions and 113 deletions
  1. 1
    0
      go.mod
  2. 38
    80
      irc/accounts.go
  3. 2
    23
      irc/config.go
  4. 1
    0
      irc/errors.go
  5. 63
    0
      irc/ldap/config.go
  6. 51
    0
      irc/ldap/helpers.go
  7. 323
    0
      irc/ldap/login.go
  8. 11
    9
      irc/nickserv.go
  9. 30
    0
      oragono.yaml
  10. 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

+ 38
- 80
irc/accounts.go View File

@@ -4,7 +4,6 @@
4 4
 package irc
5 5
 
6 6
 import (
7
-	"crypto/tls"
8 7
 	"encoding/json"
9 8
 	"fmt"
10 9
 	"net/smtp"
@@ -15,8 +14,8 @@ import (
15 14
 	"time"
16 15
 	"unicode"
17 16
 
18
-	"github.com/go-ldap/ldap"
19 17
 	"github.com/oragono/oragono/irc/caps"
18
+	"github.com/oragono/oragono/irc/ldap"
20 19
 	"github.com/oragono/oragono/irc/passwd"
21 20
 	"github.com/oragono/oragono/irc/utils"
22 21
 	"github.com/tidwall/buntdb"
@@ -448,6 +447,10 @@ func (am *AccountManager) setPassword(account string, password string, hasPrivs
448 447
 		return err
449 448
 	}
450 449
 
450
+	if !hasPrivs && creds.Empty() {
451
+		return errCredsExternallyManaged
452
+	}
453
+
451 454
 	err = creds.SetPassphrase(password, am.server.Config().Accounts.Registration.BcryptCost)
452 455
 	if err != nil {
453 456
 		return err
@@ -502,6 +505,10 @@ func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasP
502 505
 		return err
503 506
 	}
504 507
 
508
+	if !hasPrivs && creds.Empty() {
509
+		return errCredsExternallyManaged
510
+	}
511
+
505 512
 	if add {
506 513
 		err = creds.AddCertfp(certfp)
507 514
 	} else {
@@ -688,6 +695,15 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
688 695
 	return nil
689 696
 }
690 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
+
691 707
 func marshalReservedNicks(nicks []string) string {
692 708
 	return strings.Join(nicks, ",")
693 709
 }
@@ -830,92 +846,34 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou
830 846
 	return
831 847
 }
832 848
 
833
-func (am *AccountManager) checkLDAPPassphrase(accountName, passphrase string) (account ClientAccount, err error) {
834
-	var (
835
-		host, url string
836
-		port      int
837
-		sr		  *ldap.SearchResult
838
-		l		  *ldap.Conn
839
-	)
840
-
841
-	host = am.server.AccountConfig().LDAP.Servers.Host
842
-	port = am.server.AccountConfig().LDAP.Servers.Port
843
-
844
-	account, err = am.LoadAccount(accountName)
845
-	if err != nil {
846
-		return
847
-	}
848
-
849
-	if !account.Verified {
850
-		err = errAccountUnverified
851
-		return
852
-	}
853
-
854
-	if am.server.AccountConfig().LDAP.Servers.UseSSL {
855
-		url = fmt.Sprintf("ldaps://%s:%d", host, port)
856
-	} else {
857
-		url = fmt.Sprintf("ldap://%s:%d", host, port)
858
-	}
859
-
860
-	l, err = ldap.DialURL(url)
861
-	if err != nil {
862
-		return
863
-	}
864
-	defer l.Close()
865
-
866
-	if am.server.AccountConfig().LDAP.Servers.StartTLS {
867
-		err = l.StartTLS(&tls.Config{InsecureSkipVerify: am.server.AccountConfig().LDAP.Servers.SkipTLSVerify})
868
-		if err != nil {
869
-			return
870
-		}
871
-	}
872
-
873
-	err = l.Bind(am.server.AccountConfig().LDAP.BindDN, am.server.AccountConfig().LDAP.BindPwd)
874
-	if err != nil {
875
-		return
876
-	}
877
-
878
-	for _, baseDN := range am.server.AccountConfig().LDAP.SearchBaseDNs {
879
-		req := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, am.server.AccountConfig().LDAP.Timeout, false, fmt.Sprintf("(&(objectClass=organizationalPerson)(uid=%s))", accountName), []string{"dn"}, nil)
880
-		sr, err = l.Search(req)
881
-		if err != nil {
882
-			return
883
-		}
884
-
885
-		userdn := sr.Entries[0].DN
886
-
887
-		if len(sr.Entries) > 0 {
888
-			// verify the user passphrase
889
-			err = l.Bind(userdn, passphrase)
890
-			if err != nil {
891
-				continue
892
-			}
893
-			break
894
-		}
895
-	}
896
-
897
-	return
898
-}
899
-
900
-func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) error {
849
+func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) (err error) {
901 850
 	var account ClientAccount
902
-	var err error
903 851
 
904
-	if am.server.AccountConfig().LDAP.Enabled {
905
-		account, err = am.checkLDAPPassphrase(accountName, passphrase)
852
+	defer func() {
906 853
 		if err == nil {
907 854
 			am.Login(client, account)
908
-			return nil
909 855
 		}
910
-	}
856
+	}()
911 857
 
912
-	account, err = am.checkPassphrase(accountName, passphrase)
913
-	if err != nil {
914
-		return err
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
+		}
915 873
 	}
916 874
 
917
-	am.Login(client, account)
918
-	return nil
875
+	account, err = am.checkPassphrase(accountName, passphrase)
876
+	return err
919 877
 }
920 878
 
921 879
 func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount, err error) {

+ 2
- 23
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,7 +69,7 @@ type AccountConfig struct {
68 69
 		Exempted     []string
69 70
 		exemptedNets []net.IPNet
70 71
 	} `yaml:"require-sasl"`
71
-	LDAP            LDAPConfig
72
+	LDAP            ldap.LDAPConfig
72 73
 	LoginThrottling struct {
73 74
 		Enabled     bool
74 75
 		Duration    time.Duration
@@ -83,28 +84,6 @@ type AccountConfig struct {
83 84
 	VHosts VHostConfig
84 85
 }
85 86
 
86
-type LDAPConfig struct {
87
-	Timeout       int
88
-	Enabled       bool
89
-	AllowSignup   bool     `yaml:"allow-signup"`
90
-	BindDN        string   `yaml:"bind-dn"`
91
-	BindPwd       string   `yaml:"bind-password"`
92
-	SearchFilter  string   `yaml:"search-filter"`
93
-	SearchBaseDNs []string `yaml:"search-base-dns"`
94
-	Attributes    map[string]string
95
-	Servers       LDAPServerConfig
96
-}
97
-
98
-type LDAPServerConfig struct {
99
-	Host          string
100
-	Port          int
101
-	UseSSL        bool   `yaml:"use-ssl"`
102
-	StartTLS      bool   `yaml:"start-tls"`
103
-	SkipTLSVerify bool   `yaml:"skip-tls-verify"`
104
-	ClientCert    string `yaml:"client-cert"`
105
-	ClientKey     string `yaml:"client-key"`
106
-}
107
-
108 87
 // AccountRegistrationConfig controls account registration.
109 88
 type AccountRegistrationConfig struct {
110 89
 	Enabled                bool

+ 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

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

@@ -0,0 +1,63 @@
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
+package ldap
9
+
10
+import (
11
+	"fmt"
12
+	"strings"
13
+	"time"
14
+)
15
+
16
+type LDAPConfig struct {
17
+	Enabled    bool
18
+	Autocreate bool
19
+
20
+	Host          string
21
+	Port          int
22
+	Timeout       time.Duration
23
+	UseSSL        bool   `yaml:"use-ssl"`
24
+	StartTLS      bool   `yaml:"start-tls"`
25
+	SkipTLSVerify bool   `yaml:"skip-tls-verify"`
26
+	RootCACert    string `yaml:"root-ca-cert"`
27
+	ClientCert    string `yaml:"client-cert"`
28
+	ClientKey     string `yaml:"client-key"`
29
+
30
+	BindDN        string   `yaml:"bind-dn"`
31
+	BindPassword  string   `yaml:"bind-password"`
32
+	SearchFilter  string   `yaml:"search-filter"`
33
+	SearchBaseDNs []string `yaml:"search-base-dns"`
34
+
35
+	// user validation: require them to be in any one of these groups
36
+	RequireGroups []string `yaml:"require-groups"`
37
+
38
+	// two ways of testing group membership: either via an attribute
39
+	// of the user's DN, typically named 'memberOf', but customizable:
40
+	MemberOfAttribute string `yaml:"member-of-attribute"`
41
+	// or by searching for groups that match the user's DN
42
+	// and testing their names:
43
+	GroupSearchFilter              string   `yaml:"group-search-filter"`
44
+	GroupSearchFilterUserAttribute string   `yaml:"group-search-filter-user-attribute"`
45
+	GroupSearchBaseDNs             []string `yaml:"group-search-base-dns"`
46
+}
47
+
48
+// shouldAdminBind checks if we should use
49
+// admin username & password for LDAP bind
50
+func (config *LDAPConfig) shouldAdminBind() bool {
51
+	return config.BindPassword != ""
52
+}
53
+
54
+// shouldSingleBind checks if we can use "single bind" approach
55
+func (config *LDAPConfig) shouldSingleBind() bool {
56
+	return strings.Contains(config.BindDN, "%s")
57
+}
58
+
59
+// singleBindDN combines the bind with the username
60
+// in order to get the proper path
61
+func (config *LDAPConfig) singleBindDN(username string) string {
62
+	return fmt.Sprintf(config.BindDN, username)
63
+}

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

@@ -0,0 +1,51 @@
1
+// Copyright Grafana Labs and contributors
2
+// and 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
+}

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

@@ -0,0 +1,323 @@
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
+	"crypto/tls"
34
+	"crypto/x509"
35
+	"errors"
36
+	"fmt"
37
+	"io/ioutil"
38
+	"strings"
39
+
40
+	ldap "github.com/go-ldap/ldap/v3"
41
+
42
+	"github.com/oragono/oragono/irc/logger"
43
+)
44
+
45
+var (
46
+	ErrCouldNotFindUser       = errors.New("No such user")
47
+	ErrUserNotInRequiredGroup = errors.New("User is not a member of any required groups")
48
+	ErrInvalidCredentials     = errors.New("Invalid credentials")
49
+)
50
+
51
+func CheckLDAPPassphrase(config LDAPConfig, accountName, passphrase string, log *logger.Manager) (err error) {
52
+	defer func() {
53
+		if err != nil {
54
+			log.Debug("ldap", "failed passphrase check", err.Error())
55
+		}
56
+	}()
57
+
58
+	l, err := dial(&config)
59
+	if err != nil {
60
+		return
61
+	}
62
+	defer l.Close()
63
+
64
+	l.SetTimeout(config.Timeout)
65
+
66
+	passphraseChecked := false
67
+
68
+	if config.shouldSingleBind() {
69
+		log.Debug("ldap", "attempting single bind to", accountName)
70
+		err = l.Bind(config.singleBindDN(accountName), passphrase)
71
+		passphraseChecked = (err == nil)
72
+	} else if config.shouldAdminBind() {
73
+		log.Debug("ldap", "attempting admin bind to", config.BindDN)
74
+		err = l.Bind(config.BindDN, config.BindPassword)
75
+	} else {
76
+		log.Debug("ldap", "attempting unauthenticated bind")
77
+		err = l.UnauthenticatedBind(config.BindDN)
78
+	}
79
+
80
+	if err != nil {
81
+		return
82
+	}
83
+
84
+	if passphraseChecked && len(config.RequireGroups) == 0 {
85
+		return nil
86
+	}
87
+
88
+	users, err := lookupUsers(l, &config, accountName)
89
+	if err != nil {
90
+		log.Debug("ldap", "failed user lookup")
91
+		return err
92
+	}
93
+
94
+	if len(users) == 0 {
95
+		return ErrCouldNotFindUser
96
+	}
97
+
98
+	user := users[0]
99
+
100
+	log.Debug("ldap", "looked up user", user.DN)
101
+
102
+	err = validateGroupMembership(l, &config, user, log)
103
+	if err != nil {
104
+		return err
105
+	}
106
+
107
+	if !passphraseChecked {
108
+		// Authenticate user
109
+		log.Debug("ldap", "rebinding", user.DN)
110
+		err = l.Bind(user.DN, passphrase)
111
+		if err != nil {
112
+			log.Debug("ldap", "failed rebind", err.Error())
113
+			if ldapErr, ok := err.(*ldap.Error); ok {
114
+				if ldapErr.ResultCode == 49 {
115
+					return ErrInvalidCredentials
116
+				}
117
+			}
118
+		}
119
+		return err
120
+	}
121
+
122
+	return nil
123
+}
124
+
125
+func dial(config *LDAPConfig) (conn *ldap.Conn, err error) {
126
+	var certPool *x509.CertPool
127
+	if config.RootCACert != "" {
128
+		certPool = x509.NewCertPool()
129
+		for _, caCertFile := range strings.Split(config.RootCACert, " ") {
130
+			pem, err := ioutil.ReadFile(caCertFile)
131
+			if err != nil {
132
+				return nil, err
133
+			}
134
+			if !certPool.AppendCertsFromPEM(pem) {
135
+				return nil, errors.New("Failed to append CA certificate " + caCertFile)
136
+			}
137
+		}
138
+	}
139
+	var clientCert tls.Certificate
140
+	if config.ClientCert != "" && config.ClientKey != "" {
141
+		clientCert, err = tls.LoadX509KeyPair(config.ClientCert, config.ClientKey)
142
+		if err != nil {
143
+			return
144
+		}
145
+	}
146
+	for _, host := range strings.Split(config.Host, " ") {
147
+		address := fmt.Sprintf("%s:%d", host, config.Port)
148
+		if config.UseSSL {
149
+			tlsCfg := &tls.Config{
150
+				InsecureSkipVerify: config.SkipTLSVerify,
151
+				ServerName:         host,
152
+				RootCAs:            certPool,
153
+			}
154
+			if len(clientCert.Certificate) > 0 {
155
+				tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
156
+			}
157
+			if config.StartTLS {
158
+				conn, err = ldap.Dial("tcp", address)
159
+				if err == nil {
160
+					if err = conn.StartTLS(tlsCfg); err == nil {
161
+						return
162
+					}
163
+				}
164
+			} else {
165
+				conn, err = ldap.DialTLS("tcp", address, tlsCfg)
166
+			}
167
+		} else {
168
+			conn, err = ldap.Dial("tcp", address)
169
+		}
170
+
171
+		if err == nil {
172
+			return
173
+		}
174
+	}
175
+	return
176
+}
177
+
178
+func validateGroupMembership(conn *ldap.Conn, config *LDAPConfig, user *ldap.Entry, log *logger.Manager) (err error) {
179
+	if len(config.RequireGroups) != 0 {
180
+		var memberOf []string
181
+		memberOf, err = getMemberOf(conn, config, user)
182
+		if err != nil {
183
+			log.Debug("ldap", "could not retrieve group memberships", err.Error())
184
+			return
185
+		}
186
+		log.Debug("ldap", fmt.Sprintf("found group memberships: %v", memberOf))
187
+		foundGroup := false
188
+		for _, inGroup := range memberOf {
189
+			for _, acceptableGroup := range config.RequireGroups {
190
+				if inGroup == acceptableGroup {
191
+					foundGroup = true
192
+					break
193
+				}
194
+			}
195
+			if foundGroup {
196
+				break
197
+			}
198
+		}
199
+		if !foundGroup {
200
+			return ErrUserNotInRequiredGroup
201
+		}
202
+	}
203
+	return nil
204
+}
205
+
206
+func lookupUsers(conn *ldap.Conn, config *LDAPConfig, accountName string) (results []*ldap.Entry, err error) {
207
+	var result *ldap.SearchResult
208
+
209
+	for _, base := range config.SearchBaseDNs {
210
+		result, err = conn.Search(
211
+			getSearchRequest(config, base, accountName),
212
+		)
213
+		if err != nil {
214
+			return nil, err
215
+		} else if len(result.Entries) > 0 {
216
+			return result.Entries, nil
217
+		}
218
+	}
219
+
220
+	return nil, nil
221
+}
222
+
223
+// getSearchRequest returns LDAP search request for users
224
+func getSearchRequest(
225
+	config *LDAPConfig,
226
+	base string,
227
+	accountName string,
228
+) *ldap.SearchRequest {
229
+
230
+	var attributes []string
231
+	if config.MemberOfAttribute != "" {
232
+		attributes = []string{config.MemberOfAttribute}
233
+	}
234
+
235
+	query := strings.Replace(
236
+		config.SearchFilter,
237
+		"%s", ldap.EscapeFilter(accountName),
238
+		-1,
239
+	)
240
+
241
+	return &ldap.SearchRequest{
242
+		BaseDN:       base,
243
+		Scope:        ldap.ScopeWholeSubtree,
244
+		DerefAliases: ldap.NeverDerefAliases,
245
+		Attributes:   attributes,
246
+		Filter:       query,
247
+	}
248
+}
249
+
250
+// getMemberOf finds memberOf property or request it
251
+func getMemberOf(conn *ldap.Conn, config *LDAPConfig, result *ldap.Entry) (
252
+	[]string, error,
253
+) {
254
+	if config.GroupSearchFilter == "" {
255
+		memberOf := getArrayAttribute(config.MemberOfAttribute, result)
256
+
257
+		return memberOf, nil
258
+	}
259
+
260
+	memberOf, err := requestMemberOf(conn, config, result)
261
+	if err != nil {
262
+		return nil, err
263
+	}
264
+
265
+	return memberOf, nil
266
+}
267
+
268
+// requestMemberOf use this function when POSIX LDAP
269
+// schema does not support memberOf, so it manually search the groups
270
+func requestMemberOf(conn *ldap.Conn, config *LDAPConfig, entry *ldap.Entry) ([]string, error) {
271
+	var memberOf []string
272
+
273
+	for _, groupSearchBase := range config.GroupSearchBaseDNs {
274
+		var filterReplace string
275
+		if config.GroupSearchFilterUserAttribute == "" {
276
+			filterReplace = "cn"
277
+		} else {
278
+			filterReplace = getAttribute(
279
+				config.GroupSearchFilterUserAttribute,
280
+				entry,
281
+			)
282
+		}
283
+
284
+		filter := strings.Replace(
285
+			config.GroupSearchFilter, "%s",
286
+			ldap.EscapeFilter(filterReplace),
287
+			-1,
288
+		)
289
+
290
+		// support old way of reading settings
291
+		groupIDAttribute := config.MemberOfAttribute
292
+		// but prefer dn attribute if default settings are used
293
+		if groupIDAttribute == "" || groupIDAttribute == "memberOf" {
294
+			groupIDAttribute = "dn"
295
+		}
296
+
297
+		groupSearchReq := ldap.SearchRequest{
298
+			BaseDN:       groupSearchBase,
299
+			Scope:        ldap.ScopeWholeSubtree,
300
+			DerefAliases: ldap.NeverDerefAliases,
301
+			Attributes:   []string{groupIDAttribute},
302
+			Filter:       filter,
303
+		}
304
+
305
+		groupSearchResult, err := conn.Search(&groupSearchReq)
306
+		if err != nil {
307
+			return nil, err
308
+		}
309
+
310
+		if len(groupSearchResult.Entries) > 0 {
311
+			for _, group := range groupSearchResult.Entries {
312
+
313
+				memberOf = append(
314
+					memberOf,
315
+					getAttribute(groupIDAttribute, group),
316
+				)
317
+			}
318
+			break
319
+		}
320
+	}
321
+
322
+	return memberOf, nil
323
+}

+ 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:

+ 30
- 0
oragono.yaml View File

@@ -384,6 +384,36 @@ 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
+    # ldap:
391
+    #     enabled: true
392
+    #     # should we automatically create users if their LDAP login succeeds?
393
+    #     autocreate: true
394
+    #     host: "ldap.forumsys.com"
395
+    #     port: 389
396
+    #     timeout: 30s
397
+    #     # example "single-bind" configuration, where we bind directly to the user's entry:
398
+    #     bind-dn: "uid=%s,dc=example,dc=com"
399
+    #     # example "admin bind" configuration, where we bind to an initial admin user,
400
+    #     # then search for the user's entry with a search filter:
401
+    #     #search-base-dns:
402
+    #     #    - "dc=example,dc=com"
403
+    #     #bind-dn: "cn=read-only-admin,dc=example,dc=com"
404
+    #     #bind-password: "password"
405
+    #     #search-filter: "(uid=%s)"
406
+    #     # example of requiring that users be in a particular group:
407
+    #     #require-groups:
408
+    #     #    - "ou=mathematicians,dc=example,dc=com"
409
+    #     #group-search-filter-user-attribute: "dn"
410
+    #     #group-search-filter: "(uniqueMember=%s)"
411
+    #     #group-search-base-dns:
412
+    #     #    - "dc=example,dc=com"
413
+    #     # example of group membership testing via user attributes, as in AD
414
+    #     # or with OpenLDAP's "memberOf overlay" (overrides group-search-filter):
415
+    #     #member-of-attribute: "memberOf"
416
+
387 417
 # channel options
388 418
 channels:
389 419
     # 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