Skip to content

Commit 88b2f57

Browse files
authored
Merge pull request #59 from grepplabs/ldap-filter
Allow user group membership check in ldap plugin
2 parents c0288b7 + 228229b commit 88b2f57

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+3676
-1514
lines changed

cmd/plugin-auth-ldap/README.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# auth-ldap plugin config
2+
3+
## user search and group membership check
4+
5+
```
6+
make clean build plugin.auth-ldap
7+
8+
build/kafka-proxy server \
9+
--bootstrap-server-mapping "localhost:19092,0.0.0.0:30001" \
10+
--bootstrap-server-mapping "localhost:29092,0.0.0.0:30002" \
11+
--bootstrap-server-mapping "localhost:39092,0.0.0.0:30003" \
12+
--debug-enable \
13+
--auth-local-enable \
14+
--auth-local-command=build/auth-ldap \
15+
--auth-local-param=--url=ldap://localhost:389 \
16+
--auth-local-param=--start-tls=false \
17+
--auth-local-param=--bind-dn=cn=admin,dc=example,dc=org \
18+
--auth-local-param=--bind-passwd=admin \
19+
--auth-local-param=--user-search-base=ou=people,dc=example,dc=org \
20+
--auth-local-param=--user-filter="(&(objectClass=person)(uid=%u)(memberOf=cn=kafka-users,ou=realm-roles,dc=example,dc=org))"
21+
```
22+
23+
## simple user bind
24+
25+
```
26+
make clean build plugin.auth-ldap
27+
28+
build/kafka-proxy server \
29+
--bootstrap-server-mapping "localhost:19092,0.0.0.0:30001" \
30+
--bootstrap-server-mapping "localhost:29092,0.0.0.0:30002" \
31+
--bootstrap-server-mapping "localhost:39092,0.0.0.0:30003" \
32+
--debug-enable \
33+
--auth-local-enable \
34+
--auth-local-command=build/auth-ldap \
35+
--auth-local-param=--url=ldap://localhost:389 \
36+
--auth-local-param=--start-tls=false \
37+
--auth-local-param=--user-dn=ou=people,dc=example,dc=org \
38+
--auth-local-param=--user-attr=uid
39+
```
40+
41+
## openldap example
42+
### openldap setup
43+
44+
docker-compose.yml
45+
```
46+
---
47+
version: '2'
48+
services:
49+
openldap:
50+
ports:
51+
- 389:389
52+
image: osixia/openldap
53+
container_name: openldap
54+
volumes:
55+
- .:/.ldif
56+
environment:
57+
- LDAP_SEED_INTERNAL_LDIF_PATH=/.ldif
58+
- LDAP_TLS=false
59+
- LDAP_LOG_LEVEL=256
60+
```
61+
62+
openldap-entries.ldif
63+
```
64+
dn: ou=people,dc=example,dc=org
65+
objectClass: organizationalUnit
66+
ou: People
67+
68+
dn: ou=realm-roles,dc=example,dc=org
69+
objectclass: top
70+
objectclass: organizationalUnit
71+
ou: realm-roles
72+
73+
dn: ou=admin-roles,dc=example,dc=org
74+
objectclass: top
75+
objectclass: organizationalUnit
76+
ou: admin-roles
77+
78+
dn: uid=jbrown,ou=people,dc=example,dc=org
79+
objectclass: top
80+
objectclass: person
81+
objectclass: organizationalPerson
82+
objectclass: inetOrgPerson
83+
uid: jbrown
84+
cn: James
85+
sn: Brown
86+
userPassword: password1
87+
88+
dn: uid=bwilson,ou=people,dc=example,dc=org
89+
objectclass: top
90+
objectclass: person
91+
objectclass: organizationalPerson
92+
objectclass: inetOrgPerson
93+
uid: bwilson
94+
cn: Bruce
95+
sn: Wilson
96+
userPassword: password2
97+
98+
dn: cn=lynch,ou=people,dc=example,dc=org
99+
objectclass: top
100+
objectclass: person
101+
objectclass: organizationalPerson
102+
objectclass: inetOrgPerson
103+
uid: lynch
104+
cn: Lynch
105+
sn: Peter
106+
userPassword: password3
107+
108+
dn: cn=superadmin,ou=admin-roles,dc=example,dc=org
109+
objectclass: top
110+
objectclass: groupOfUniqueNames
111+
cn: accountant
112+
uniqueMember: uid=bwilson,ou=people,dc=example,dc=org
113+
114+
dn: cn=kafka-users,ou=realm-roles,dc=example,dc=org
115+
objectclass: top
116+
objectclass: groupOfUniqueNames
117+
cn: kafka-users
118+
uniqueMember: uid=jbrown,ou=people,dc=example,dc=org
119+
uniqueMember: cn=lynch,ou=people,dc=example,dc=org
120+
121+
dn: cn=ldap-users,ou=realm-roles,dc=example,dc=org
122+
objectclass: top
123+
objectclass: groupOfUniqueNames
124+
cn: ldap-users
125+
uniqueMember: uid=jbrown,ou=people,dc=example,dc=org
126+
uniqueMember: cn=lynch,ou=people,dc=example,dc=org
127+
uniqueMember: uid=bwilson,ou=people,dc=example,dc=org
128+
```
129+
130+
### openldap queries
131+
132+
```
133+
ldapsearch -x -LLL -H ldap://localhost:389 -D "cn=admin,dc=example,dc=org" -w admin -b "ou=people,dc=example,dc=org" "(objectClass=person)"
134+
ldapsearch -x -LLL -H ldap://localhost:389 -D "cn=admin,dc=example,dc=org" -w admin -b "ou=people,dc=example,dc=org" "(objectClass=person)" memberOf
135+
ldapsearch -x -H ldap://localhost:389 -D "cn=admin,dc=example,dc=org" -w admin -b "ou=people,dc=example,dc=org" "(&(objectClass=person)(uid=jbrown)(memberOf=cn=kafka-users,ou=realm-roles,dc=example,dc=org))"
136+
```

cmd/plugin-auth-ldap/main.go

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,32 @@ import (
44
"crypto/tls"
55
"flag"
66
"fmt"
7+
"github.com/go-ldap/ldap/v3"
78
"github.com/grepplabs/kafka-proxy/plugin/local-auth/shared"
89
"github.com/hashicorp/go-multierror"
910
"github.com/hashicorp/go-plugin"
11+
"github.com/pkg/errors"
1012
"github.com/sirupsen/logrus"
11-
"gopkg.in/ldap.v2"
1213
"net"
1314
"net/url"
1415
"os"
1516
"strings"
1617
)
1718

18-
//TODO: connection pooling, credential caching (TTL, max number of entries), negative caching
19+
const UsernamePlaceholder = "%u"
20+
1921
type LdapAuthenticator struct {
20-
Urls []string
21-
StartTLS bool
22+
Urls []string
23+
StartTLS bool
24+
2225
UPNDomain string
2326
UserDN string
2427
UserAttr string
28+
29+
BindDN string
30+
BindPassword string
31+
UserSearchBase string
32+
UserFilter string
2533
}
2634

2735
func (pa LdapAuthenticator) Authenticate(username, password string) (bool, int32, error) {
@@ -31,9 +39,18 @@ func (pa LdapAuthenticator) Authenticate(username, password string) (bool, int32
3139
logrus.Errorf("user %s ldap dial error %v", username, err)
3240
return false, 1, nil
3341
}
42+
if l == nil {
43+
logrus.Errorf("ldap connection is nil")
44+
return false, 1, nil
45+
}
3446
defer l.Close()
3547

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)
3754
if err != nil {
3855
if ldapErr, ok := err.(*ldap.Error); ok && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials {
3956
logrus.Errorf("user %s credentials are invalid", username)
@@ -45,12 +62,48 @@ func (pa LdapAuthenticator) Authenticate(username, password string) (bool, int32
4562
return true, 0, nil
4663
}
4764

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+
}
52105
}
53-
return fmt.Sprintf("%s=%s,%s", pa.UserAttr, escapeLDAPValue(username), pa.UserDN)
106+
return bindDN, nil
54107
}
55108

56109
func escapeLDAPValue(input string) string {
@@ -139,6 +192,11 @@ type pluginMeta struct {
139192
upnDomain string
140193
userDN string
141194
userAttr string
195+
196+
bindDN string
197+
bindPassword string
198+
userSearchBase string
199+
userFilter string
142200
}
143201

144202
func (f *pluginMeta) flagSet() *flag.FlagSet {
@@ -149,6 +207,12 @@ func (f *pluginMeta) flagSet() *flag.FlagSet {
149207
fs.StringVar(&f.upnDomain, "upn-domain", "", "Enables userPrincipalDomain login with [username]@UPNDomain (optional)")
150208
fs.StringVar(&f.userDN, "user-dn", "", "LDAP domain to use for users (eg: cn=users,dc=example,dc=org)")
151209
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+
152216
return fs
153217
}
154218

@@ -188,20 +252,39 @@ func main() {
188252
logrus.Error(err)
189253
os.Exit(1)
190254
}
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")
193272
os.Exit(1)
194273
}
195274

196275
plugin.Serve(&plugin.ServeConfig{
197276
HandshakeConfig: shared.Handshake,
198277
Plugins: map[string]plugin.Plugin{
199278
"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,
205288
}},
206289
},
207290
// A non-nil value here enables gRPC serving for this plugin...

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/cenkalti/backoff v1.1.0
1010
github.com/elazarl/goproxy v0.0.0-20171101143503-a96fa3a31826
1111
github.com/fsnotify/fsnotify v1.4.9
12+
github.com/go-ldap/ldap/v3 v3.2.3
1213
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
1314
github.com/golang/protobuf v1.4.2
1415
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce // indirect
@@ -36,6 +37,7 @@ require (
3637
github.com/stretchr/testify v1.4.0
3738
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c
3839
github.com/xdg/stringprep v1.0.0 // indirect
40+
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
3941
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7
4042
golang.org/x/oauth2 v0.0.0-20180314180239-fdc9e635145a
4143
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
@@ -45,6 +47,5 @@ require (
4547
google.golang.org/genproto v0.0.0-20180316064809-f8c870359523 // indirect
4648
google.golang.org/grpc v1.10.0
4749
gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225 // indirect
48-
gopkg.in/ldap.v2 v2.5.1
4950
gopkg.in/yaml.v2 v2.3.0 // indirect
5051
)

go.sum

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
cloud.google.com/go v0.19.0 h1:lsRUy6VQM3ZNma0fk7uhGxEQW3pcc9x1aHI2tVcGsYA=
22
cloud.google.com/go v0.19.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3+
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
4+
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
35
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
46
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
57
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -24,8 +26,13 @@ github.com/elazarl/goproxy v0.0.0-20171101143503-a96fa3a31826 h1:C0fzkSk9AgMlLF2
2426
github.com/elazarl/goproxy v0.0.0-20171101143503-a96fa3a31826/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
2527
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
2628
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
29+
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
30+
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
2731
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
2832
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
33+
github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHjsvuZyatzwk=
34+
github.com/go-ldap/ldap/v3 v3.2.3 h1:FBt+5w3q/vPVPb4eYMQSn+pOiz4zewPamYhlGMmc7yM=
35+
github.com/go-ldap/ldap/v3 v3.2.3/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
2936
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
3037
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
3138
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -146,7 +153,12 @@ github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0
146153
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
147154
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
148155
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
156+
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
157+
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
158+
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
159+
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
149160
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
161+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
150162
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
151163
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0=
152164
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
@@ -158,6 +170,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
158170
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
159171
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
160172
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
173+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
161174
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
162175
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
163176
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

0 commit comments

Comments
 (0)