Skip to content

Commit

Permalink
First commit, simple ldap client
Browse files Browse the repository at this point in the history
  • Loading branch information
jtblin committed Dec 17, 2015
0 parents commit 58071cc
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 0 deletions.
27 changes: 27 additions & 0 deletions LICENSE
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.
56 changes: 56 additions & 0 deletions README.md
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).
50 changes: 50 additions & 0 deletions example_test.go
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)
}
146 changes: 146 additions & 0 deletions ldap-client.go
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
}

0 comments on commit 58071cc

Please sign in to comment.