-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 58071cc
Showing
4 changed files
with
279 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
Copyright (c) Jerome Touffe-Blin ("Author") | ||
All rights reserved. | ||
|
||
The BSD License | ||
|
||
Redistribution and use in source and binary forms, with or without | ||
modification, are permitted provided that the following conditions | ||
are met: | ||
|
||
1. Redistributions of source code must retain the above copyright | ||
notice, this list of conditions and the following disclaimer. | ||
|
||
2. Redistributions in binary form must reproduce the above copyright | ||
notice, this list of conditions and the following disclaimer in the | ||
documentation and/or other materials provided with the distribution. | ||
|
||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND | ||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | ||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS | ||
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR | ||
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, | ||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE | ||
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN | ||
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# go-ldap-client | ||
|
||
Simple ldap client to authenticate, retrieve basic information and groups for a user. | ||
|
||
# Usage | ||
|
||
[Go Doc](https://godoc.org/github.com/jtblin/go-ldap-client) | ||
|
||
See [example](example_test.go). The only external dependency is [gopkg.in/ldap.v2](http://gopkg.in/ldap.v2). | ||
|
||
```golang | ||
package main | ||
|
||
import ( | ||
"log" | ||
|
||
"github.com/jtblin/go-ldap-client" | ||
) | ||
|
||
func main() { | ||
client := &ldap.LDAPClient{ | ||
Base: "dc=example,dc=com", | ||
Host: "ldap.example.com", | ||
Port: 389, | ||
UseSSL: false, | ||
BindDN: "uid=readonlysuer,ou=People,dc=example,dc=com", | ||
BindPassword: "readonlypassword", | ||
UserFilter: "(uid=%s)", | ||
GroupFilter: "(memberUid=%s)", | ||
Attributes: []string{"givenName", "sn", "mail", "uid"}, | ||
} | ||
# It is the responsibility of the caller to close the connection | ||
defer client.Close() | ||
|
||
ok, user, err := client.Authenticate("username", "password") | ||
if err != nil { | ||
log.Fatalf("Error authenticating user %s: %+v", "username", err) | ||
} | ||
if !ok { | ||
log.Fatalf("Authenticating failed for user %s", "username") | ||
} | ||
log.Printf("User: %+v", user) | ||
|
||
groups, err := client.GetGroupsOfUser("username") | ||
if err != nil { | ||
log.Fatalf("Error getting groups for user %s: %+v", "username", err) | ||
} | ||
log.Printf("Groups: %+v", groups) | ||
} | ||
``` | ||
|
||
# Why? | ||
|
||
There are already [tons](https://godoc.org/?q=ldap) of ldap libraries for `golang` but most of them | ||
are just forks of another one, most of them are too low level or too limited (e.g. do not return errors | ||
which make it hard to troubleshoot issues). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package ldap_test | ||
|
||
import ( | ||
"log" | ||
|
||
"github.com/jtblin/go-ldap-client" | ||
) | ||
|
||
// ExampleLDAPClient_Authenticate shows how a typical application can verify a login attempt | ||
func ExampleLDAPClient_Authenticate() { | ||
client := &ldap.LDAPClient{ | ||
Base: "dc=example,dc=com", | ||
Host: "ldap.example.com", | ||
Port: 389, | ||
UseSSL: false, | ||
BindDN: "uid=readonlysuer,ou=People,dc=example,dc=com", | ||
BindPassword: "readonlypassword", | ||
UserFilter: "(uid=%s)", | ||
GroupFilter: "(memberUid=%s)", | ||
Attributes: []string{"givenName", "sn", "mail", "uid"}, | ||
} | ||
defer client.Close() | ||
|
||
ok, user, err := client.Authenticate("username", "password") | ||
if err != nil { | ||
log.Fatalf("Error authenticating user %s: %+v", "username", err) | ||
} | ||
if !ok { | ||
log.Fatalf("Authenticating failed for user %s", "username") | ||
} | ||
log.Printf("User: %+v", user) | ||
|
||
|
||
} | ||
|
||
// ExampleLDAPClient_GetGroupsOfUser shows how to retrieve user groups | ||
func ExampleLDAPClient_GetGroupsOfUser() { | ||
client := &ldap.LDAPClient{ | ||
Base: "dc=example,dc=com", | ||
Host: "ldap.example.com", | ||
Port: 389, | ||
GroupFilter: "(memberUid=%s)", | ||
} | ||
defer client.Close() | ||
groups, err := client.GetGroupsOfUser("username") | ||
if err != nil { | ||
log.Fatalf("Error getting groups for user %s: %+v", "username", err) | ||
} | ||
log.Printf("Groups: %+v", groups) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
// Package ldap provides a simple ldap client to authenticate, | ||
// retrieve basic information and groups for a user. | ||
package ldap | ||
|
||
import ( | ||
"crypto/tls" | ||
"errors" | ||
"fmt" | ||
|
||
"gopkg.in/ldap.v2" | ||
) | ||
|
||
type LDAPClient struct { | ||
Conn *ldap.Conn | ||
Host string | ||
Port int | ||
UseSSL bool | ||
BindDN string | ||
BindPassword string | ||
GroupFilter string // e.g. "(memberUid=%s)" | ||
UserFilter string // e.g. "(uid=%s)" | ||
Base string | ||
Attributes []string | ||
} | ||
|
||
// Connect connects to the ldap backend | ||
func (lc *LDAPClient) Connect() error { | ||
if lc.Conn == nil { | ||
var l *ldap.Conn | ||
var err error | ||
address := fmt.Sprintf("%s:%d", lc.Host, lc.Port) | ||
if !lc.UseSSL { | ||
l, err = ldap.Dial("tcp", address) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Reconnect with TLS | ||
err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) | ||
if err != nil { | ||
return err | ||
} | ||
} else { | ||
l, err = ldap.DialTLS("tcp", address, &tls.Config{InsecureSkipVerify: false}) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
lc.Conn = l | ||
} | ||
return nil | ||
} | ||
|
||
// Close closes the ldap backend connection | ||
func (lc *LDAPClient) Close() { | ||
if lc.Conn != nil { | ||
lc.Conn.Close() | ||
} | ||
} | ||
|
||
// Authenticate authenticates the user against the ldap backend | ||
func (lc *LDAPClient) Authenticate(username, password string) (bool, map[string]string, error) { | ||
err := lc.Connect() | ||
if err != nil { | ||
return false, nil, err | ||
} | ||
|
||
// First bind with a read only user | ||
if lc.BindDN != "" && lc.BindPassword != "" { | ||
err := lc.Conn.Bind(lc.BindDN, lc.BindPassword) | ||
if err != nil { | ||
return false, nil, err | ||
} | ||
} | ||
|
||
attributes := append(lc.Attributes, "dn") | ||
// Search for the given username | ||
searchRequest := ldap.NewSearchRequest( | ||
lc.Base, | ||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, | ||
fmt.Sprintf(lc.UserFilter, username), | ||
attributes, | ||
nil, | ||
) | ||
|
||
sr, err := lc.Conn.Search(searchRequest) | ||
if err != nil { | ||
return false, nil, err | ||
} | ||
|
||
if len(sr.Entries) < 1 { | ||
return false, nil, errors.New("User does not exist") | ||
} | ||
|
||
if len(sr.Entries) > 1 { | ||
return false, nil, errors.New("Too many entries returned") | ||
} | ||
|
||
userDN := sr.Entries[0].DN | ||
user := map[string]string{} | ||
for _, attr := range lc.Attributes { | ||
user[attr] = sr.Entries[0].GetAttributeValue(attr) | ||
} | ||
|
||
// Bind as the user to verify their password | ||
err = lc.Conn.Bind(userDN, password) | ||
if err != nil { | ||
return false, user, err | ||
} | ||
|
||
// Rebind as the read only user for any further queries | ||
if lc.BindDN != "" && lc.BindPassword != "" { | ||
err = lc.Conn.Bind(lc.BindDN, lc.BindPassword) | ||
if err != nil { | ||
return true, user, err | ||
} | ||
} | ||
|
||
return true, user, nil | ||
} | ||
|
||
// GetGroupsOfUser returns the group for a user | ||
func (lc *LDAPClient) GetGroupsOfUser(username string) ([]string, error) { | ||
err := lc.Connect() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
searchRequest := ldap.NewSearchRequest( | ||
lc.Base, | ||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, | ||
fmt.Sprintf(lc.GroupFilter, username), | ||
[]string{"cn"}, // can it be something else than "cn"? | ||
nil, | ||
) | ||
sr, err := lc.Conn.Search(searchRequest) | ||
if err != nil { | ||
return nil, err | ||
} | ||
groups := []string{} | ||
for _, entry := range sr.Entries { | ||
groups = append(groups, entry.GetAttributeValue("cn")) | ||
} | ||
return groups, nil | ||
} |