@@ -4,24 +4,32 @@ import (
4
4
"crypto/tls"
5
5
"flag"
6
6
"fmt"
7
+ "github.com/go-ldap/ldap/v3"
7
8
"github.com/grepplabs/kafka-proxy/plugin/local-auth/shared"
8
9
"github.com/hashicorp/go-multierror"
9
10
"github.com/hashicorp/go-plugin"
11
+ "github.com/pkg/errors"
10
12
"github.com/sirupsen/logrus"
11
- "gopkg.in/ldap.v2"
12
13
"net"
13
14
"net/url"
14
15
"os"
15
16
"strings"
16
17
)
17
18
18
- //TODO: connection pooling, credential caching (TTL, max number of entries), negative caching
19
+ const UsernamePlaceholder = "%u"
20
+
19
21
type LdapAuthenticator struct {
20
- Urls []string
21
- StartTLS bool
22
+ Urls []string
23
+ StartTLS bool
24
+
22
25
UPNDomain string
23
26
UserDN string
24
27
UserAttr string
28
+
29
+ BindDN string
30
+ BindPassword string
31
+ UserSearchBase string
32
+ UserFilter string
25
33
}
26
34
27
35
func (pa LdapAuthenticator ) Authenticate (username , password string ) (bool , int32 , error ) {
@@ -31,9 +39,18 @@ func (pa LdapAuthenticator) Authenticate(username, password string) (bool, int32
31
39
logrus .Errorf ("user %s ldap dial error %v" , username , err )
32
40
return false , 1 , nil
33
41
}
42
+ if l == nil {
43
+ logrus .Errorf ("ldap connection is nil" )
44
+ return false , 1 , nil
45
+ }
34
46
defer l .Close ()
35
47
36
- err = l .Bind (pa .getUserBindDN (username ), password )
48
+ bindDN , err := pa .getUserBindDN (l , username )
49
+ if err != nil {
50
+ logrus .Errorf ("user %s ldap get user bindDN error %v" , username , err )
51
+ return false , 1 , nil
52
+ }
53
+ err = l .Bind (bindDN , password )
37
54
if err != nil {
38
55
if ldapErr , ok := err .(* ldap.Error ); ok && ldapErr .ResultCode == ldap .LDAPResultInvalidCredentials {
39
56
logrus .Errorf ("user %s credentials are invalid" , username )
@@ -45,12 +62,48 @@ func (pa LdapAuthenticator) Authenticate(username, password string) (bool, int32
45
62
return true , 0 , nil
46
63
}
47
64
48
- func (pa LdapAuthenticator ) getUserBindDN (username string ) string {
49
-
50
- if pa .UPNDomain != "" {
51
- return fmt .Sprintf ("%s@%s" , escapeLDAPValue (username ), pa .UPNDomain )
65
+ func (pa LdapAuthenticator ) getUserBindDN (conn * ldap.Conn , username string ) (string , error ) {
66
+ bindDN := ""
67
+ if pa .BindDN != "" {
68
+ var err error
69
+ if pa .BindPassword != "" {
70
+ err = conn .Bind (pa .BindDN , pa .BindPassword )
71
+ } else {
72
+ err = conn .UnauthenticatedBind (pa .BindDN )
73
+ }
74
+ if err != nil {
75
+ return "" , errors .Wrapf (err , "LDAP bind (service) failed" )
76
+ }
77
+ searchRequest := ldap .NewSearchRequest (
78
+ pa .UserSearchBase ,
79
+ ldap .ScopeWholeSubtree ,
80
+ ldap .NeverDerefAliases ,
81
+ 0 ,
82
+ 0 ,
83
+ false ,
84
+ strings .ReplaceAll (pa .UserFilter , UsernamePlaceholder , username ),
85
+ []string {"dn" },
86
+ nil ,
87
+ )
88
+ sr , err := conn .Search (searchRequest )
89
+ if err != nil {
90
+ return "" , err
91
+ }
92
+ if len (sr .Entries ) < 1 {
93
+ return "" , errors .New ("LDAP user search empty result" )
94
+ }
95
+ if len (sr .Entries ) > 1 {
96
+ return "" , errors .New ("LDAP user search not unique result" )
97
+ }
98
+ bindDN = sr .Entries [0 ].DN
99
+ } else {
100
+ if pa .UPNDomain != "" {
101
+ bindDN = fmt .Sprintf ("%s@%s" , escapeLDAPValue (username ), pa .UPNDomain )
102
+ } else {
103
+ bindDN = fmt .Sprintf ("%s=%s,%s" , pa .UserAttr , escapeLDAPValue (username ), pa .UserDN )
104
+ }
52
105
}
53
- return fmt . Sprintf ( "%s=%s,%s" , pa . UserAttr , escapeLDAPValue ( username ), pa . UserDN )
106
+ return bindDN , nil
54
107
}
55
108
56
109
func escapeLDAPValue (input string ) string {
@@ -139,6 +192,11 @@ type pluginMeta struct {
139
192
upnDomain string
140
193
userDN string
141
194
userAttr string
195
+
196
+ bindDN string
197
+ bindPassword string
198
+ userSearchBase string
199
+ userFilter string
142
200
}
143
201
144
202
func (f * pluginMeta ) flagSet () * flag.FlagSet {
@@ -149,6 +207,12 @@ func (f *pluginMeta) flagSet() *flag.FlagSet {
149
207
fs .StringVar (& f .upnDomain , "upn-domain" , "" , "Enables userPrincipalDomain login with [username]@UPNDomain (optional)" )
150
208
fs .StringVar (& f .userDN , "user-dn" , "" , "LDAP domain to use for users (eg: cn=users,dc=example,dc=org)" )
151
209
fs .StringVar (& f .userAttr , "user-attr" , "uid" , " Attribute used for users" )
210
+
211
+ fs .StringVar (& f .bindDN , "bind-dn" , "" , "The Distinguished Name to bind to the LDAP directory to search a user. This can be a readonly or admin user" )
212
+ fs .StringVar (& f .bindPassword , "bind-passwd" , "" , "The password used with bindDN" )
213
+ fs .StringVar (& f .userSearchBase , "user-search-base" , "" , "The search base as the starting point for the user search e.g. ou=people,dc=example,dc=org" )
214
+ fs .StringVar (& f .userFilter , "user-filter" , "" , fmt .Sprintf ("The user search filter. It must contain '%s' placeholder for the username e.g. (&(objectClass=person)(uid=%s)(memberOf=cn=kafka-users,ou=realm-roles,dc=example,dc=org))" , UsernamePlaceholder , UsernamePlaceholder ))
215
+
152
216
return fs
153
217
}
154
218
@@ -188,20 +252,39 @@ func main() {
188
252
logrus .Error (err )
189
253
os .Exit (1 )
190
254
}
191
- if pluginMeta .upnDomain == "" && (pluginMeta .userDN == "" || pluginMeta .userAttr == "" ) {
192
- logrus .Errorf ("parameters user-dn and user-attr are required" )
255
+ if pluginMeta .bindDN != "" {
256
+ logrus .Infof ("user-search-base='%s',user-filter='%s'" , pluginMeta .userSearchBase ,pluginMeta .userFilter )
257
+
258
+ if pluginMeta .userSearchBase == "" {
259
+ logrus .Errorf ("user-search-base is required" )
260
+ }
261
+ if ! strings .Contains (pluginMeta .userFilter ,UsernamePlaceholder ) {
262
+ logrus .Errorf ("user-filter must contain '%s' as username placeholder" , UsernamePlaceholder )
263
+ }
264
+
265
+ } else if pluginMeta .upnDomain != "" || pluginMeta .userDN != "" {
266
+ if pluginMeta .userDN != "" && pluginMeta .userAttr == "" {
267
+ logrus .Errorf ("parameters user-dn and user-attr are required" )
268
+ os .Exit (1 )
269
+ }
270
+ } else {
271
+ logrus .Errorf ("parameters user-dn or bind-dn are required" )
193
272
os .Exit (1 )
194
273
}
195
274
196
275
plugin .Serve (& plugin.ServeConfig {
197
276
HandshakeConfig : shared .Handshake ,
198
277
Plugins : map [string ]plugin.Plugin {
199
278
"passwordAuthenticator" : & shared.PasswordAuthenticatorPlugin {Impl : & LdapAuthenticator {
200
- Urls : urls ,
201
- StartTLS : pluginMeta .startTLS ,
202
- UPNDomain : pluginMeta .upnDomain ,
203
- UserDN : pluginMeta .userDN ,
204
- UserAttr : pluginMeta .userAttr ,
279
+ Urls : urls ,
280
+ StartTLS : pluginMeta .startTLS ,
281
+ UPNDomain : pluginMeta .upnDomain ,
282
+ UserDN : pluginMeta .userDN ,
283
+ UserAttr : pluginMeta .userAttr ,
284
+ BindDN : pluginMeta .bindDN ,
285
+ BindPassword : pluginMeta .bindPassword ,
286
+ UserSearchBase : pluginMeta .userSearchBase ,
287
+ UserFilter : pluginMeta .userFilter ,
205
288
}},
206
289
},
207
290
// A non-nil value here enables gRPC serving for this plugin...
0 commit comments