Skip to content

Commit 58071cc

Browse files
committed
First commit, simple ldap client
0 parents  commit 58071cc

File tree

4 files changed

+279
-0
lines changed

4 files changed

+279
-0
lines changed

LICENSE

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Copyright (c) Jerome Touffe-Blin ("Author")
2+
All rights reserved.
3+
4+
The BSD License
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions
8+
are met:
9+
10+
1. Redistributions of source code must retain the above copyright
11+
notice, this list of conditions and the following disclaimer.
12+
13+
2. Redistributions in binary form must reproduce the above copyright
14+
notice, this list of conditions and the following disclaimer in the
15+
documentation and/or other materials provided with the distribution.
16+
17+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20+
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
21+
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
24+
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
25+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
26+
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
27+
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# go-ldap-client
2+
3+
Simple ldap client to authenticate, retrieve basic information and groups for a user.
4+
5+
# Usage
6+
7+
[Go Doc](https://godoc.org/github.com/jtblin/go-ldap-client)
8+
9+
See [example](example_test.go). The only external dependency is [gopkg.in/ldap.v2](http://gopkg.in/ldap.v2).
10+
11+
```golang
12+
package main
13+
14+
import (
15+
"log"
16+
17+
"github.com/jtblin/go-ldap-client"
18+
)
19+
20+
func main() {
21+
client := &ldap.LDAPClient{
22+
Base: "dc=example,dc=com",
23+
Host: "ldap.example.com",
24+
Port: 389,
25+
UseSSL: false,
26+
BindDN: "uid=readonlysuer,ou=People,dc=example,dc=com",
27+
BindPassword: "readonlypassword",
28+
UserFilter: "(uid=%s)",
29+
GroupFilter: "(memberUid=%s)",
30+
Attributes: []string{"givenName", "sn", "mail", "uid"},
31+
}
32+
# It is the responsibility of the caller to close the connection
33+
defer client.Close()
34+
35+
ok, user, err := client.Authenticate("username", "password")
36+
if err != nil {
37+
log.Fatalf("Error authenticating user %s: %+v", "username", err)
38+
}
39+
if !ok {
40+
log.Fatalf("Authenticating failed for user %s", "username")
41+
}
42+
log.Printf("User: %+v", user)
43+
44+
groups, err := client.GetGroupsOfUser("username")
45+
if err != nil {
46+
log.Fatalf("Error getting groups for user %s: %+v", "username", err)
47+
}
48+
log.Printf("Groups: %+v", groups)
49+
}
50+
```
51+
52+
# Why?
53+
54+
There are already [tons](https://godoc.org/?q=ldap) of ldap libraries for `golang` but most of them
55+
are just forks of another one, most of them are too low level or too limited (e.g. do not return errors
56+
which make it hard to troubleshoot issues).

example_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package ldap_test
2+
3+
import (
4+
"log"
5+
6+
"github.com/jtblin/go-ldap-client"
7+
)
8+
9+
// ExampleLDAPClient_Authenticate shows how a typical application can verify a login attempt
10+
func ExampleLDAPClient_Authenticate() {
11+
client := &ldap.LDAPClient{
12+
Base: "dc=example,dc=com",
13+
Host: "ldap.example.com",
14+
Port: 389,
15+
UseSSL: false,
16+
BindDN: "uid=readonlysuer,ou=People,dc=example,dc=com",
17+
BindPassword: "readonlypassword",
18+
UserFilter: "(uid=%s)",
19+
GroupFilter: "(memberUid=%s)",
20+
Attributes: []string{"givenName", "sn", "mail", "uid"},
21+
}
22+
defer client.Close()
23+
24+
ok, user, err := client.Authenticate("username", "password")
25+
if err != nil {
26+
log.Fatalf("Error authenticating user %s: %+v", "username", err)
27+
}
28+
if !ok {
29+
log.Fatalf("Authenticating failed for user %s", "username")
30+
}
31+
log.Printf("User: %+v", user)
32+
33+
34+
}
35+
36+
// ExampleLDAPClient_GetGroupsOfUser shows how to retrieve user groups
37+
func ExampleLDAPClient_GetGroupsOfUser() {
38+
client := &ldap.LDAPClient{
39+
Base: "dc=example,dc=com",
40+
Host: "ldap.example.com",
41+
Port: 389,
42+
GroupFilter: "(memberUid=%s)",
43+
}
44+
defer client.Close()
45+
groups, err := client.GetGroupsOfUser("username")
46+
if err != nil {
47+
log.Fatalf("Error getting groups for user %s: %+v", "username", err)
48+
}
49+
log.Printf("Groups: %+v", groups)
50+
}

ldap-client.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Package ldap provides a simple ldap client to authenticate,
2+
// retrieve basic information and groups for a user.
3+
package ldap
4+
5+
import (
6+
"crypto/tls"
7+
"errors"
8+
"fmt"
9+
10+
"gopkg.in/ldap.v2"
11+
)
12+
13+
type LDAPClient struct {
14+
Conn *ldap.Conn
15+
Host string
16+
Port int
17+
UseSSL bool
18+
BindDN string
19+
BindPassword string
20+
GroupFilter string // e.g. "(memberUid=%s)"
21+
UserFilter string // e.g. "(uid=%s)"
22+
Base string
23+
Attributes []string
24+
}
25+
26+
// Connect connects to the ldap backend
27+
func (lc *LDAPClient) Connect() error {
28+
if lc.Conn == nil {
29+
var l *ldap.Conn
30+
var err error
31+
address := fmt.Sprintf("%s:%d", lc.Host, lc.Port)
32+
if !lc.UseSSL {
33+
l, err = ldap.Dial("tcp", address)
34+
if err != nil {
35+
return err
36+
}
37+
38+
// Reconnect with TLS
39+
err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
40+
if err != nil {
41+
return err
42+
}
43+
} else {
44+
l, err = ldap.DialTLS("tcp", address, &tls.Config{InsecureSkipVerify: false})
45+
if err != nil {
46+
return err
47+
}
48+
}
49+
50+
lc.Conn = l
51+
}
52+
return nil
53+
}
54+
55+
// Close closes the ldap backend connection
56+
func (lc *LDAPClient) Close() {
57+
if lc.Conn != nil {
58+
lc.Conn.Close()
59+
}
60+
}
61+
62+
// Authenticate authenticates the user against the ldap backend
63+
func (lc *LDAPClient) Authenticate(username, password string) (bool, map[string]string, error) {
64+
err := lc.Connect()
65+
if err != nil {
66+
return false, nil, err
67+
}
68+
69+
// First bind with a read only user
70+
if lc.BindDN != "" && lc.BindPassword != "" {
71+
err := lc.Conn.Bind(lc.BindDN, lc.BindPassword)
72+
if err != nil {
73+
return false, nil, err
74+
}
75+
}
76+
77+
attributes := append(lc.Attributes, "dn")
78+
// Search for the given username
79+
searchRequest := ldap.NewSearchRequest(
80+
lc.Base,
81+
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
82+
fmt.Sprintf(lc.UserFilter, username),
83+
attributes,
84+
nil,
85+
)
86+
87+
sr, err := lc.Conn.Search(searchRequest)
88+
if err != nil {
89+
return false, nil, err
90+
}
91+
92+
if len(sr.Entries) < 1 {
93+
return false, nil, errors.New("User does not exist")
94+
}
95+
96+
if len(sr.Entries) > 1 {
97+
return false, nil, errors.New("Too many entries returned")
98+
}
99+
100+
userDN := sr.Entries[0].DN
101+
user := map[string]string{}
102+
for _, attr := range lc.Attributes {
103+
user[attr] = sr.Entries[0].GetAttributeValue(attr)
104+
}
105+
106+
// Bind as the user to verify their password
107+
err = lc.Conn.Bind(userDN, password)
108+
if err != nil {
109+
return false, user, err
110+
}
111+
112+
// Rebind as the read only user for any further queries
113+
if lc.BindDN != "" && lc.BindPassword != "" {
114+
err = lc.Conn.Bind(lc.BindDN, lc.BindPassword)
115+
if err != nil {
116+
return true, user, err
117+
}
118+
}
119+
120+
return true, user, nil
121+
}
122+
123+
// GetGroupsOfUser returns the group for a user
124+
func (lc *LDAPClient) GetGroupsOfUser(username string) ([]string, error) {
125+
err := lc.Connect()
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
searchRequest := ldap.NewSearchRequest(
131+
lc.Base,
132+
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
133+
fmt.Sprintf(lc.GroupFilter, username),
134+
[]string{"cn"}, // can it be something else than "cn"?
135+
nil,
136+
)
137+
sr, err := lc.Conn.Search(searchRequest)
138+
if err != nil {
139+
return nil, err
140+
}
141+
groups := []string{}
142+
for _, entry := range sr.Entries {
143+
groups = append(groups, entry.GetAttributeValue("cn"))
144+
}
145+
return groups, nil
146+
}

0 commit comments

Comments
 (0)