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

+ 38
- 80
irc/accounts.go View File

4
 package irc
4
 package irc
5
 
5
 
6
 import (
6
 import (
7
-	"crypto/tls"
8
 	"encoding/json"
7
 	"encoding/json"
9
 	"fmt"
8
 	"fmt"
10
 	"net/smtp"
9
 	"net/smtp"
15
 	"time"
14
 	"time"
16
 	"unicode"
15
 	"unicode"
17
 
16
 
18
-	"github.com/go-ldap/ldap"
19
 	"github.com/oragono/oragono/irc/caps"
17
 	"github.com/oragono/oragono/irc/caps"
18
+	"github.com/oragono/oragono/irc/ldap"
20
 	"github.com/oragono/oragono/irc/passwd"
19
 	"github.com/oragono/oragono/irc/passwd"
21
 	"github.com/oragono/oragono/irc/utils"
20
 	"github.com/oragono/oragono/irc/utils"
22
 	"github.com/tidwall/buntdb"
21
 	"github.com/tidwall/buntdb"
448
 		return err
447
 		return err
449
 	}
448
 	}
450
 
449
 
450
+	if !hasPrivs && creds.Empty() {
451
+		return errCredsExternallyManaged
452
+	}
453
+
451
 	err = creds.SetPassphrase(password, am.server.Config().Accounts.Registration.BcryptCost)
454
 	err = creds.SetPassphrase(password, am.server.Config().Accounts.Registration.BcryptCost)
452
 	if err != nil {
455
 	if err != nil {
453
 		return err
456
 		return err
502
 		return err
505
 		return err
503
 	}
506
 	}
504
 
507
 
508
+	if !hasPrivs && creds.Empty() {
509
+		return errCredsExternallyManaged
510
+	}
511
+
505
 	if add {
512
 	if add {
506
 		err = creds.AddCertfp(certfp)
513
 		err = creds.AddCertfp(certfp)
507
 	} else {
514
 	} else {
688
 	return nil
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
 func marshalReservedNicks(nicks []string) string {
707
 func marshalReservedNicks(nicks []string) string {
692
 	return strings.Join(nicks, ",")
708
 	return strings.Join(nicks, ",")
693
 }
709
 }
830
 	return
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
 	var account ClientAccount
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
 		if err == nil {
853
 		if err == nil {
907
 			am.Login(client, account)
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
 func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount, err error) {
879
 func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount, err error) {

+ 2
- 23
irc/config.go View File

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"`
71
-	LDAP            LDAPConfig
72
+	LDAP            ldap.LDAPConfig
72
 	LoginThrottling struct {
73
 	LoginThrottling struct {
73
 		Enabled     bool
74
 		Enabled     bool
74
 		Duration    time.Duration
75
 		Duration    time.Duration
83
 	VHosts VHostConfig
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
 // AccountRegistrationConfig controls account registration.
87
 // AccountRegistrationConfig controls account registration.
109
 type AccountRegistrationConfig struct {
88
 type AccountRegistrationConfig struct {
110
 	Enabled                bool
89
 	Enabled                bool

+ 1
- 0
irc/errors.go View File

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

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

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

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

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
 		},
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:

+ 30
- 0
oragono.yaml View File

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
+    # 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
 # channel options
417
 # channel options
388
 channels:
418
 channels:
389
     # modes that are set when new channels are created
419
     # modes that are set when new channels are created

+ 1
- 1
vendor

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

Loading…
Cancel
Save