Skip to content

Commit

Permalink
Allow user group membership check in ldap plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
everesio committed Aug 23, 2020
1 parent c0288b7 commit 228229b
Show file tree
Hide file tree
Showing 69 changed files with 3,676 additions and 1,514 deletions.
136 changes: 136 additions & 0 deletions cmd/plugin-auth-ldap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# auth-ldap plugin config

## user search and group membership check

```
make clean build plugin.auth-ldap
build/kafka-proxy server \
--bootstrap-server-mapping "localhost:19092,0.0.0.0:30001" \
--bootstrap-server-mapping "localhost:29092,0.0.0.0:30002" \
--bootstrap-server-mapping "localhost:39092,0.0.0.0:30003" \
--debug-enable \
--auth-local-enable \
--auth-local-command=build/auth-ldap \
--auth-local-param=--url=ldap://localhost:389 \
--auth-local-param=--start-tls=false \
--auth-local-param=--bind-dn=cn=admin,dc=example,dc=org \
--auth-local-param=--bind-passwd=admin \
--auth-local-param=--user-search-base=ou=people,dc=example,dc=org \
--auth-local-param=--user-filter="(&(objectClass=person)(uid=%u)(memberOf=cn=kafka-users,ou=realm-roles,dc=example,dc=org))"
```

## simple user bind

```
make clean build plugin.auth-ldap
build/kafka-proxy server \
--bootstrap-server-mapping "localhost:19092,0.0.0.0:30001" \
--bootstrap-server-mapping "localhost:29092,0.0.0.0:30002" \
--bootstrap-server-mapping "localhost:39092,0.0.0.0:30003" \
--debug-enable \
--auth-local-enable \
--auth-local-command=build/auth-ldap \
--auth-local-param=--url=ldap://localhost:389 \
--auth-local-param=--start-tls=false \
--auth-local-param=--user-dn=ou=people,dc=example,dc=org \
--auth-local-param=--user-attr=uid
```

## openldap example
### openldap setup

docker-compose.yml
```
---
version: '2'
services:
openldap:
ports:
- 389:389
image: osixia/openldap
container_name: openldap
volumes:
- .:/.ldif
environment:
- LDAP_SEED_INTERNAL_LDIF_PATH=/.ldif
- LDAP_TLS=false
- LDAP_LOG_LEVEL=256
```

openldap-entries.ldif
```
dn: ou=people,dc=example,dc=org
objectClass: organizationalUnit
ou: People
dn: ou=realm-roles,dc=example,dc=org
objectclass: top
objectclass: organizationalUnit
ou: realm-roles
dn: ou=admin-roles,dc=example,dc=org
objectclass: top
objectclass: organizationalUnit
ou: admin-roles
dn: uid=jbrown,ou=people,dc=example,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
uid: jbrown
cn: James
sn: Brown
userPassword: password1
dn: uid=bwilson,ou=people,dc=example,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
uid: bwilson
cn: Bruce
sn: Wilson
userPassword: password2
dn: cn=lynch,ou=people,dc=example,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
uid: lynch
cn: Lynch
sn: Peter
userPassword: password3
dn: cn=superadmin,ou=admin-roles,dc=example,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: accountant
uniqueMember: uid=bwilson,ou=people,dc=example,dc=org
dn: cn=kafka-users,ou=realm-roles,dc=example,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: kafka-users
uniqueMember: uid=jbrown,ou=people,dc=example,dc=org
uniqueMember: cn=lynch,ou=people,dc=example,dc=org
dn: cn=ldap-users,ou=realm-roles,dc=example,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: ldap-users
uniqueMember: uid=jbrown,ou=people,dc=example,dc=org
uniqueMember: cn=lynch,ou=people,dc=example,dc=org
uniqueMember: uid=bwilson,ou=people,dc=example,dc=org
```

### openldap queries

```
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)"
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
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))"
```
117 changes: 100 additions & 17 deletions cmd/plugin-auth-ldap/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,32 @@ import (
"crypto/tls"
"flag"
"fmt"
"github.com/go-ldap/ldap/v3"
"github.com/grepplabs/kafka-proxy/plugin/local-auth/shared"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-plugin"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"gopkg.in/ldap.v2"
"net"
"net/url"
"os"
"strings"
)

//TODO: connection pooling, credential caching (TTL, max number of entries), negative caching
const UsernamePlaceholder = "%u"

type LdapAuthenticator struct {
Urls []string
StartTLS bool
Urls []string
StartTLS bool

UPNDomain string
UserDN string
UserAttr string

BindDN string
BindPassword string
UserSearchBase string
UserFilter string
}

func (pa LdapAuthenticator) Authenticate(username, password string) (bool, int32, error) {
Expand All @@ -31,9 +39,18 @@ func (pa LdapAuthenticator) Authenticate(username, password string) (bool, int32
logrus.Errorf("user %s ldap dial error %v", username, err)
return false, 1, nil
}
if l == nil {
logrus.Errorf("ldap connection is nil")
return false, 1, nil
}
defer l.Close()

err = l.Bind(pa.getUserBindDN(username), password)
bindDN, err := pa.getUserBindDN(l, username)
if err != nil {
logrus.Errorf("user %s ldap get user bindDN error %v", username, err)
return false, 1, nil
}
err = l.Bind(bindDN, password)
if err != nil {
if ldapErr, ok := err.(*ldap.Error); ok && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials {
logrus.Errorf("user %s credentials are invalid", username)
Expand All @@ -45,12 +62,48 @@ func (pa LdapAuthenticator) Authenticate(username, password string) (bool, int32
return true, 0, nil
}

func (pa LdapAuthenticator) getUserBindDN(username string) string {

if pa.UPNDomain != "" {
return fmt.Sprintf("%s@%s", escapeLDAPValue(username), pa.UPNDomain)
func (pa LdapAuthenticator) getUserBindDN(conn *ldap.Conn, username string) (string, error) {
bindDN := ""
if pa.BindDN != "" {
var err error
if pa.BindPassword != "" {
err = conn.Bind(pa.BindDN, pa.BindPassword)
} else {
err = conn.UnauthenticatedBind(pa.BindDN)
}
if err != nil {
return "", errors.Wrapf(err, "LDAP bind (service) failed")
}
searchRequest := ldap.NewSearchRequest(
pa.UserSearchBase,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0,
0,
false,
strings.ReplaceAll(pa.UserFilter, UsernamePlaceholder, username),
[]string{"dn"},
nil,
)
sr, err := conn.Search(searchRequest)
if err != nil {
return "", err
}
if len(sr.Entries) < 1 {
return "", errors.New("LDAP user search empty result")
}
if len(sr.Entries) > 1 {
return "", errors.New("LDAP user search not unique result")
}
bindDN = sr.Entries[0].DN
} else {
if pa.UPNDomain != "" {
bindDN = fmt.Sprintf("%s@%s", escapeLDAPValue(username), pa.UPNDomain)
} else {
bindDN = fmt.Sprintf("%s=%s,%s", pa.UserAttr, escapeLDAPValue(username), pa.UserDN)
}
}
return fmt.Sprintf("%s=%s,%s", pa.UserAttr, escapeLDAPValue(username), pa.UserDN)
return bindDN, nil
}

func escapeLDAPValue(input string) string {
Expand Down Expand Up @@ -139,6 +192,11 @@ type pluginMeta struct {
upnDomain string
userDN string
userAttr string

bindDN string
bindPassword string
userSearchBase string
userFilter string
}

func (f *pluginMeta) flagSet() *flag.FlagSet {
Expand All @@ -149,6 +207,12 @@ func (f *pluginMeta) flagSet() *flag.FlagSet {
fs.StringVar(&f.upnDomain, "upn-domain", "", "Enables userPrincipalDomain login with [username]@UPNDomain (optional)")
fs.StringVar(&f.userDN, "user-dn", "", "LDAP domain to use for users (eg: cn=users,dc=example,dc=org)")
fs.StringVar(&f.userAttr, "user-attr", "uid", " Attribute used for users")

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")
fs.StringVar(&f.bindPassword, "bind-passwd", "", "The password used with bindDN")
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")
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))

return fs
}

Expand Down Expand Up @@ -188,20 +252,39 @@ func main() {
logrus.Error(err)
os.Exit(1)
}
if pluginMeta.upnDomain == "" && (pluginMeta.userDN == "" || pluginMeta.userAttr == "") {
logrus.Errorf("parameters user-dn and user-attr are required")
if pluginMeta.bindDN != "" {
logrus.Infof("user-search-base='%s',user-filter='%s'", pluginMeta.userSearchBase,pluginMeta.userFilter)

if pluginMeta.userSearchBase == "" {
logrus.Errorf("user-search-base is required")
}
if !strings.Contains(pluginMeta.userFilter,UsernamePlaceholder) {
logrus.Errorf("user-filter must contain '%s' as username placeholder", UsernamePlaceholder)
}

} else if pluginMeta.upnDomain != "" || pluginMeta.userDN != "" {
if pluginMeta.userDN != "" && pluginMeta.userAttr == "" {
logrus.Errorf("parameters user-dn and user-attr are required")
os.Exit(1)
}
} else {
logrus.Errorf("parameters user-dn or bind-dn are required")
os.Exit(1)
}

plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: shared.Handshake,
Plugins: map[string]plugin.Plugin{
"passwordAuthenticator": &shared.PasswordAuthenticatorPlugin{Impl: &LdapAuthenticator{
Urls: urls,
StartTLS: pluginMeta.startTLS,
UPNDomain: pluginMeta.upnDomain,
UserDN: pluginMeta.userDN,
UserAttr: pluginMeta.userAttr,
Urls: urls,
StartTLS: pluginMeta.startTLS,
UPNDomain: pluginMeta.upnDomain,
UserDN: pluginMeta.userDN,
UserAttr: pluginMeta.userAttr,
BindDN: pluginMeta.bindDN,
BindPassword: pluginMeta.bindPassword,
UserSearchBase: pluginMeta.userSearchBase,
UserFilter: pluginMeta.userFilter,
}},
},
// A non-nil value here enables gRPC serving for this plugin...
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/cenkalti/backoff v1.1.0
github.com/elazarl/goproxy v0.0.0-20171101143503-a96fa3a31826
github.com/fsnotify/fsnotify v1.4.9
github.com/go-ldap/ldap/v3 v3.2.3
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/protobuf v1.4.2
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce // indirect
Expand Down Expand Up @@ -36,6 +37,7 @@ require (
github.com/stretchr/testify v1.4.0
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c
github.com/xdg/stringprep v1.0.0 // indirect
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7
golang.org/x/oauth2 v0.0.0-20180314180239-fdc9e635145a
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
Expand All @@ -45,6 +47,5 @@ require (
google.golang.org/genproto v0.0.0-20180316064809-f8c870359523 // indirect
google.golang.org/grpc v1.10.0
gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225 // indirect
gopkg.in/ldap.v2 v2.5.1
gopkg.in/yaml.v2 v2.3.0 // indirect
)
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
cloud.google.com/go v0.19.0 h1:lsRUy6VQM3ZNma0fk7uhGxEQW3pcc9x1aHI2tVcGsYA=
cloud.google.com/go v0.19.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
Expand All @@ -24,8 +26,13 @@ github.com/elazarl/goproxy v0.0.0-20171101143503-a96fa3a31826 h1:C0fzkSk9AgMlLF2
github.com/elazarl/goproxy v0.0.0-20171101143503-a96fa3a31826/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHjsvuZyatzwk=
github.com/go-ldap/ldap/v3 v3.2.3 h1:FBt+5w3q/vPVPb4eYMQSn+pOiz4zewPamYhlGMmc7yM=
github.com/go-ldap/ldap/v3 v3.2.3/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
Expand Down Expand Up @@ -146,7 +153,12 @@ github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
Expand All @@ -158,6 +170,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down

0 comments on commit 228229b

Please sign in to comment.