Просмотр исходного кода

Merge pull request #4461 from dfwarden/ldap_nested_groups

LDAP Nested Groups
Torkel Ödegaard 9 лет назад
Родитель
Сommit
f27ce7e1e4

+ 2 - 2
Godeps/Godeps.json

@@ -160,8 +160,8 @@
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/go-ldap/ldap",
 			"ImportPath": "github.com/go-ldap/ldap",
-			"Comment": "v1-19-g83e6542",
-			"Rev": "83e65426fd1c06626e88aa8a085e5bfed0208e29"
+			"Comment": "v2.2.1",
+			"Rev": "07a7330929b9ee80495c88a4439657d89c7dbd87"
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/go-macaron/binding",
 			"ImportPath": "github.com/go-macaron/binding",

+ 4 - 1
Godeps/_workspace/src/github.com/go-ldap/ldap/.travis.yml

@@ -2,10 +2,13 @@ language: go
 go:
 go:
     - 1.2
     - 1.2
     - 1.3
     - 1.3
+    - 1.4
+    - 1.5
     - tip
     - tip
+go_import_path: gopkg.in/ldap.v2
 install:
 install:
     - go get gopkg.in/asn1-ber.v1
     - go get gopkg.in/asn1-ber.v1
-    - go get gopkg.in/ldap.v1
+    - go get gopkg.in/ldap.v2
     - go get code.google.com/p/go.tools/cmd/cover || go get golang.org/x/tools/cmd/cover
     - go get code.google.com/p/go.tools/cmd/cover || go get golang.org/x/tools/cmd/cover
     - go build -v ./...
     - go build -v ./...
 script:
 script:

+ 22 - 15
Godeps/_workspace/src/github.com/go-ldap/ldap/README.md

@@ -1,8 +1,20 @@
-[![GoDoc](https://godoc.org/gopkg.in/ldap.v1?status.svg)](https://godoc.org/gopkg.in/ldap.v1) [![Build Status](https://travis-ci.org/go-ldap/ldap.svg)](https://travis-ci.org/go-ldap/ldap)
+[![GoDoc](https://godoc.org/gopkg.in/ldap.v2?status.svg)](https://godoc.org/gopkg.in/ldap.v2)
+[![Build Status](https://travis-ci.org/go-ldap/ldap.svg)](https://travis-ci.org/go-ldap/ldap)
 
 
 # Basic LDAP v3 functionality for the GO programming language.
 # Basic LDAP v3 functionality for the GO programming language.
 
 
-## Required Librarys: 
+## Install
+
+For the latest version use:
+
+    go get gopkg.in/ldap.v2
+
+Import the latest version with:
+
+    import "gopkg.in/ldap.v2"
+
+
+## Required Libraries:
 
 
  - gopkg.in/asn1-ber.v1
  - gopkg.in/asn1-ber.v1
 
 
@@ -14,6 +26,9 @@
  - Compiling string filters to LDAP filters
  - Compiling string filters to LDAP filters
  - Paging Search Results
  - Paging Search Results
  - Modify Requests / Responses
  - Modify Requests / Responses
+ - Add Requests / Responses
+ - Delete Requests / Responses
+ - Better Unicode support
 
 
 ## Examples:
 ## Examples:
 
 
@@ -26,23 +41,15 @@
 
 
 ## TODO:
 ## TODO:
 
 
- - Add Requests / Responses
- - Delete Requests / Responses
- - Modify DN Requests / Responses
- - Compare Requests / Responses
- - Implement Tests / Benchmarks
+ - [x] Add Requests / Responses
+ - [x] Delete Requests / Responses
+ - [x] Modify DN Requests / Responses
+ - [ ] Compare Requests / Responses
+ - [ ] Implement Tests / Benchmarks
 
 
----
-This feature is disabled at the moment, because in some cases the "Search Request Done" packet will be handled before the last "Search Request Entry":
 
 
- - Mulitple internal goroutines to handle network traffic
-        Makes library goroutine safe
-        Can perform multiple search requests at the same time and return
-        the results to the proper goroutine. All requests are blocking requests,
-        so the goroutine does not need special handling
 
 
 ---
 ---
-
 The Go gopher was designed by Renee French. (http://reneefrench.blogspot.com/)
 The Go gopher was designed by Renee French. (http://reneefrench.blogspot.com/)
 The design is licensed under the Creative Commons 3.0 Attributions license.
 The design is licensed under the Creative Commons 3.0 Attributions license.
 Read this article for more details: http://blog.golang.org/gopher
 Read this article for more details: http://blog.golang.org/gopher

+ 104 - 0
Godeps/_workspace/src/github.com/go-ldap/ldap/add.go

@@ -0,0 +1,104 @@
+//
+// https://tools.ietf.org/html/rfc4511
+//
+// AddRequest ::= [APPLICATION 8] SEQUENCE {
+//      entry           LDAPDN,
+//      attributes      AttributeList }
+//
+// AttributeList ::= SEQUENCE OF attribute Attribute
+
+package ldap
+
+import (
+	"errors"
+	"log"
+
+	"gopkg.in/asn1-ber.v1"
+)
+
+type Attribute struct {
+	attrType string
+	attrVals []string
+}
+
+func (a *Attribute) encode() *ber.Packet {
+	seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attribute")
+	seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, a.attrType, "Type"))
+	set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue")
+	for _, value := range a.attrVals {
+		set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals"))
+	}
+	seq.AppendChild(set)
+	return seq
+}
+
+type AddRequest struct {
+	dn         string
+	attributes []Attribute
+}
+
+func (a AddRequest) encode() *ber.Packet {
+	request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationAddRequest, nil, "Add Request")
+	request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, a.dn, "DN"))
+	attributes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes")
+	for _, attribute := range a.attributes {
+		attributes.AppendChild(attribute.encode())
+	}
+	request.AppendChild(attributes)
+	return request
+}
+
+func (a *AddRequest) Attribute(attrType string, attrVals []string) {
+	a.attributes = append(a.attributes, Attribute{attrType: attrType, attrVals: attrVals})
+}
+
+func NewAddRequest(dn string) *AddRequest {
+	return &AddRequest{
+		dn: dn,
+	}
+
+}
+
+func (l *Conn) Add(addRequest *AddRequest) error {
+	messageID := l.nextMessageID()
+	packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request")
+	packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID"))
+	packet.AppendChild(addRequest.encode())
+
+	l.Debug.PrintPacket(packet)
+
+	channel, err := l.sendMessage(packet)
+	if err != nil {
+		return err
+	}
+	if channel == nil {
+		return NewError(ErrorNetwork, errors.New("ldap: could not send message"))
+	}
+	defer l.finishMessage(messageID)
+
+	l.Debug.Printf("%d: waiting for response", messageID)
+	packet = <-channel
+	l.Debug.Printf("%d: got response %p", messageID, packet)
+	if packet == nil {
+		return NewError(ErrorNetwork, errors.New("ldap: could not retrieve message"))
+	}
+
+	if l.Debug {
+		if err := addLDAPDescriptions(packet); err != nil {
+			return err
+		}
+		ber.PrintPacket(packet)
+	}
+
+	if packet.Children[1].Tag == ApplicationAddResponse {
+		resultCode, resultDescription := getLDAPResultCode(packet)
+		if resultCode != 0 {
+			return NewError(resultCode, errors.New(resultDescription))
+		}
+	} else {
+		log.Printf("Unexpected Response: %d", packet.Children[1].Tag)
+	}
+
+	l.Debug.Printf("%d: returning", messageID)
+	return nil
+}

+ 23 - 0
Godeps/_workspace/src/github.com/go-ldap/ldap/client.go

@@ -0,0 +1,23 @@
+package ldap
+
+import "crypto/tls"
+
+// Client knows how to interact with an LDAP server
+type Client interface {
+	Start()
+	StartTLS(config *tls.Config) error
+	Close()
+
+	Bind(username, password string) error
+	SimpleBind(simpleBindRequest *SimpleBindRequest) (*SimpleBindResult, error)
+
+	Add(addRequest *AddRequest) error
+	Del(delRequest *DelRequest) error
+	Modify(modifyRequest *ModifyRequest) error
+
+	Compare(dn, attribute, value string) (bool, error)
+	PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*PasswordModifyResult, error)
+
+	Search(searchRequest *SearchRequest) (*SearchResult, error)
+	SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error)
+}

+ 7 - 2
Godeps/_workspace/src/github.com/go-ldap/ldap/conn.go

@@ -8,11 +8,12 @@ import (
 	"crypto/tls"
 	"crypto/tls"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
-	"gopkg.in/asn1-ber.v1"
 	"log"
 	"log"
 	"net"
 	"net"
 	"sync"
 	"sync"
 	"time"
 	"time"
+
+	"gopkg.in/asn1-ber.v1"
 )
 )
 
 
 const (
 const (
@@ -53,6 +54,8 @@ type Conn struct {
 	messageMutex        sync.Mutex
 	messageMutex        sync.Mutex
 }
 }
 
 
+var _ Client = &Conn{}
+
 // DefaultTimeout is a package-level variable that sets the timeout value
 // DefaultTimeout is a package-level variable that sets the timeout value
 // used for the Dial and DialTLS methods.
 // used for the Dial and DialTLS methods.
 //
 //
@@ -176,7 +179,7 @@ func (l *Conn) StartTLS(config *tls.Config) error {
 		ber.PrintPacket(packet)
 		ber.PrintPacket(packet)
 	}
 	}
 
 
-	if packet.Children[1].Children[0].Value.(int64) == 0 {
+	if resultCode, message := getLDAPResultCode(packet); resultCode == LDAPResultSuccess {
 		conn := tls.Client(l.conn, config)
 		conn := tls.Client(l.conn, config)
 
 
 		if err := conn.Handshake(); err != nil {
 		if err := conn.Handshake(); err != nil {
@@ -186,6 +189,8 @@ func (l *Conn) StartTLS(config *tls.Config) error {
 
 
 		l.isTLS = true
 		l.isTLS = true
 		l.conn = conn
 		l.conn = conn
+	} else {
+		return NewError(resultCode, fmt.Errorf("ldap: cannot StartTLS (%s)", message))
 	}
 	}
 	go l.reader()
 	go l.reader()
 
 

+ 32 - 0
Godeps/_workspace/src/github.com/go-ldap/ldap/control.go

@@ -16,11 +16,13 @@ const (
 	ControlTypeBeheraPasswordPolicy   = "1.3.6.1.4.1.42.2.27.8.5.1"
 	ControlTypeBeheraPasswordPolicy   = "1.3.6.1.4.1.42.2.27.8.5.1"
 	ControlTypeVChuPasswordMustChange = "2.16.840.1.113730.3.4.4"
 	ControlTypeVChuPasswordMustChange = "2.16.840.1.113730.3.4.4"
 	ControlTypeVChuPasswordWarning    = "2.16.840.1.113730.3.4.5"
 	ControlTypeVChuPasswordWarning    = "2.16.840.1.113730.3.4.5"
+	ControlTypeManageDsaIT            = "2.16.840.1.113730.3.4.2"
 )
 )
 
 
 var ControlTypeMap = map[string]string{
 var ControlTypeMap = map[string]string{
 	ControlTypePaging:               "Paging",
 	ControlTypePaging:               "Paging",
 	ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft",
 	ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft",
+	ControlTypeManageDsaIT:          "Manage DSA IT",
 }
 }
 
 
 type Control interface {
 type Control interface {
@@ -165,6 +167,36 @@ func (c *ControlVChuPasswordWarning) String() string {
 		c.Expire)
 		c.Expire)
 }
 }
 
 
+type ControlManageDsaIT struct {
+	Criticality bool
+}
+
+func (c *ControlManageDsaIT) GetControlType() string {
+	return ControlTypeManageDsaIT
+}
+
+func (c *ControlManageDsaIT) Encode() *ber.Packet {
+	//FIXME
+	packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control")
+	packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeManageDsaIT, "Control Type ("+ControlTypeMap[ControlTypeManageDsaIT]+")"))
+	if c.Criticality {
+		packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality"))
+	}
+	return packet
+}
+
+func (c *ControlManageDsaIT) String() string {
+	return fmt.Sprintf(
+		"Control Type: %s (%q)  Criticality: %t",
+		ControlTypeMap[ControlTypeManageDsaIT],
+		ControlTypeManageDsaIT,
+		c.Criticality)
+}
+
+func NewControlManageDsaIT(Criticality bool) *ControlManageDsaIT {
+	return &ControlManageDsaIT{Criticality: Criticality}
+}
+
 func FindControl(controls []Control, controlType string) Control {
 func FindControl(controls []Control, controlType string) Control {
 	for _, c := range controls {
 	for _, c := range controls {
 		if c.GetControlType() == controlType {
 		if c.GetControlType() == controlType {

+ 79 - 0
Godeps/_workspace/src/github.com/go-ldap/ldap/del.go

@@ -0,0 +1,79 @@
+//
+// https://tools.ietf.org/html/rfc4511
+//
+// DelRequest ::= [APPLICATION 10] LDAPDN
+
+package ldap
+
+import (
+	"errors"
+	"log"
+
+	"gopkg.in/asn1-ber.v1"
+)
+
+type DelRequest struct {
+	DN       string
+	Controls []Control
+}
+
+func (d DelRequest) encode() *ber.Packet {
+	request := ber.Encode(ber.ClassApplication, ber.TypePrimitive, ApplicationDelRequest, d.DN, "Del Request")
+	request.Data.Write([]byte(d.DN))
+	return request
+}
+
+func NewDelRequest(DN string,
+	Controls []Control) *DelRequest {
+	return &DelRequest{
+		DN:       DN,
+		Controls: Controls,
+	}
+}
+
+func (l *Conn) Del(delRequest *DelRequest) error {
+	messageID := l.nextMessageID()
+	packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request")
+	packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID"))
+	packet.AppendChild(delRequest.encode())
+	if delRequest.Controls != nil {
+		packet.AppendChild(encodeControls(delRequest.Controls))
+	}
+
+	l.Debug.PrintPacket(packet)
+
+	channel, err := l.sendMessage(packet)
+	if err != nil {
+		return err
+	}
+	if channel == nil {
+		return NewError(ErrorNetwork, errors.New("ldap: could not send message"))
+	}
+	defer l.finishMessage(messageID)
+
+	l.Debug.Printf("%d: waiting for response", messageID)
+	packet = <-channel
+	l.Debug.Printf("%d: got response %p", messageID, packet)
+	if packet == nil {
+		return NewError(ErrorNetwork, errors.New("ldap: could not retrieve message"))
+	}
+
+	if l.Debug {
+		if err := addLDAPDescriptions(packet); err != nil {
+			return err
+		}
+		ber.PrintPacket(packet)
+	}
+
+	if packet.Children[1].Tag == ApplicationDelResponse {
+		resultCode, resultDescription := getLDAPResultCode(packet)
+		if resultCode != 0 {
+			return NewError(resultCode, errors.New(resultDescription))
+		}
+	} else {
+		log.Printf("Unexpected Response: %d", packet.Children[1].Tag)
+	}
+
+	l.Debug.Printf("%d: returning", messageID)
+	return nil
+}

+ 6 - 6
Godeps/_workspace/src/github.com/go-ldap/ldap/dn.go

@@ -47,17 +47,17 @@ package ldap
 
 
 import (
 import (
 	"bytes"
 	"bytes"
+	enchex "encoding/hex"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"strings"
 	"strings"
-	enchex "encoding/hex"
 
 
 	ber "gopkg.in/asn1-ber.v1"
 	ber "gopkg.in/asn1-ber.v1"
 )
 )
 
 
 type AttributeTypeAndValue struct {
 type AttributeTypeAndValue struct {
-	Type       string
-	Value      string
+	Type  string
+	Value string
 }
 }
 
 
 type RelativeDN struct {
 type RelativeDN struct {
@@ -71,7 +71,7 @@ type DN struct {
 func ParseDN(str string) (*DN, error) {
 func ParseDN(str string) (*DN, error) {
 	dn := new(DN)
 	dn := new(DN)
 	dn.RDNs = make([]*RelativeDN, 0)
 	dn.RDNs = make([]*RelativeDN, 0)
-	rdn := new (RelativeDN)
+	rdn := new(RelativeDN)
 	rdn.Attributes = make([]*AttributeTypeAndValue, 0)
 	rdn.Attributes = make([]*AttributeTypeAndValue, 0)
 	buffer := bytes.Buffer{}
 	buffer := bytes.Buffer{}
 	attribute := new(AttributeTypeAndValue)
 	attribute := new(AttributeTypeAndValue)
@@ -115,7 +115,7 @@ func ParseDN(str string) (*DN, error) {
 				index := strings.IndexAny(str[i:], ",+")
 				index := strings.IndexAny(str[i:], ",+")
 				data := str
 				data := str
 				if index > 0 {
 				if index > 0 {
-				  data = str[i:i+index]
+					data = str[i : i+index]
 				} else {
 				} else {
 					data = str[i:]
 					data = str[i:]
 				}
 				}
@@ -126,7 +126,7 @@ func ParseDN(str string) (*DN, error) {
 				}
 				}
 				packet := ber.DecodePacket(raw_ber)
 				packet := ber.DecodePacket(raw_ber)
 				buffer.WriteString(packet.Data.String())
 				buffer.WriteString(packet.Data.String())
-				i += len(data)-1
+				i += len(data) - 1
 			}
 			}
 		} else if char == ',' || char == '+' {
 		} else if char == ',' || char == '+' {
 			// We're done with this RDN or value, push it
 			// We're done with this RDN or value, push it

+ 33 - 33
Godeps/_workspace/src/github.com/go-ldap/ldap/dn_test.go

@@ -1,38 +1,40 @@
-package ldap
+package ldap_test
 
 
 import (
 import (
 	"reflect"
 	"reflect"
 	"testing"
 	"testing"
+
+	"gopkg.in/ldap.v2"
 )
 )
 
 
 func TestSuccessfulDNParsing(t *testing.T) {
 func TestSuccessfulDNParsing(t *testing.T) {
-	testcases := map[string]DN {
-		"": DN{[]*RelativeDN{}},
-		"cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": DN{[]*RelativeDN{
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"cn", "Jim, \"Hasse Hö\" Hansson!"},}},
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"dc", "dummy"},}},
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"dc", "com"}, }},}},
-		"UID=jsmith,DC=example,DC=net": DN{[]*RelativeDN{
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"UID", "jsmith"},}},
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "example"},}},
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}, }},}},
-		"OU=Sales+CN=J. Smith,DC=example,DC=net": DN{[]*RelativeDN{
-			&RelativeDN{[]*AttributeTypeAndValue{
-				&AttributeTypeAndValue{"OU", "Sales"},
-				&AttributeTypeAndValue{"CN", "J. Smith"},}},
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "example"},}},
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}, }},}},
-		"1.3.6.1.4.1.1466.0=#04024869": DN{[]*RelativeDN{
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"},}},}},
-		"1.3.6.1.4.1.1466.0=#04024869,DC=net": DN{[]*RelativeDN{
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"},}},
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}, }},}},
-		"CN=Lu\\C4\\8Di\\C4\\87": DN{[]*RelativeDN{
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"CN", "Lučić"},}},}},
+	testcases := map[string]ldap.DN{
+		"": ldap.DN{[]*ldap.RelativeDN{}},
+		"cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": ldap.DN{[]*ldap.RelativeDN{
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"cn", "Jim, \"Hasse Hö\" Hansson!"}}},
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"dc", "dummy"}}},
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"dc", "com"}}}}},
+		"UID=jsmith,DC=example,DC=net": ldap.DN{[]*ldap.RelativeDN{
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"UID", "jsmith"}}},
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "example"}}},
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}},
+		"OU=Sales+CN=J. Smith,DC=example,DC=net": ldap.DN{[]*ldap.RelativeDN{
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{
+				&ldap.AttributeTypeAndValue{"OU", "Sales"},
+				&ldap.AttributeTypeAndValue{"CN", "J. Smith"}}},
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "example"}}},
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}},
+		"1.3.6.1.4.1.1466.0=#04024869": ldap.DN{[]*ldap.RelativeDN{
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"}}}}},
+		"1.3.6.1.4.1.1466.0=#04024869,DC=net": ldap.DN{[]*ldap.RelativeDN{
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"}}},
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}},
+		"CN=Lu\\C4\\8Di\\C4\\87": ldap.DN{[]*ldap.RelativeDN{
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"CN", "Lučić"}}}}},
 	}
 	}
 
 
 	for test, answer := range testcases {
 	for test, answer := range testcases {
-		dn, err := ParseDN(test)
+		dn, err := ldap.ParseDN(test)
 		if err != nil {
 		if err != nil {
 			t.Errorf(err.Error())
 			t.Errorf(err.Error())
 			continue
 			continue
@@ -49,16 +51,16 @@ func TestSuccessfulDNParsing(t *testing.T) {
 }
 }
 
 
 func TestErrorDNParsing(t *testing.T) {
 func TestErrorDNParsing(t *testing.T) {
-	testcases := map[string]string {
-		"*": "DN ended with incomplete type, value pair",
-		"cn=Jim\\0Test": "Failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'",
-		"cn=Jim\\0": "Got corrupted escaped character",
+	testcases := map[string]string{
+		"*":               "DN ended with incomplete type, value pair",
+		"cn=Jim\\0Test":   "Failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'",
+		"cn=Jim\\0":       "Got corrupted escaped character",
 		"DC=example,=net": "DN ended with incomplete type, value pair",
 		"DC=example,=net": "DN ended with incomplete type, value pair",
-		"1=#0402486": "Failed to decode BER encoding: encoding/hex: odd length hex string",
+		"1=#0402486":      "Failed to decode BER encoding: encoding/hex: odd length hex string",
 	}
 	}
 
 
 	for test, answer := range testcases {
 	for test, answer := range testcases {
-		_, err := ParseDN(test)
+		_, err := ldap.ParseDN(test)
 		if err == nil {
 		if err == nil {
 			t.Errorf("Expected %s to fail parsing but succeeded\n", test)
 			t.Errorf("Expected %s to fail parsing but succeeded\n", test)
 		} else if err.Error() != answer {
 		} else if err.Error() != answer {
@@ -66,5 +68,3 @@ func TestErrorDNParsing(t *testing.T) {
 		}
 		}
 	}
 	}
 }
 }
-
-

+ 137 - 0
Godeps/_workspace/src/github.com/go-ldap/ldap/error.go

@@ -0,0 +1,137 @@
+package ldap
+
+import (
+	"fmt"
+
+	"gopkg.in/asn1-ber.v1"
+)
+
+// LDAP Result Codes
+const (
+	LDAPResultSuccess                      = 0
+	LDAPResultOperationsError              = 1
+	LDAPResultProtocolError                = 2
+	LDAPResultTimeLimitExceeded            = 3
+	LDAPResultSizeLimitExceeded            = 4
+	LDAPResultCompareFalse                 = 5
+	LDAPResultCompareTrue                  = 6
+	LDAPResultAuthMethodNotSupported       = 7
+	LDAPResultStrongAuthRequired           = 8
+	LDAPResultReferral                     = 10
+	LDAPResultAdminLimitExceeded           = 11
+	LDAPResultUnavailableCriticalExtension = 12
+	LDAPResultConfidentialityRequired      = 13
+	LDAPResultSaslBindInProgress           = 14
+	LDAPResultNoSuchAttribute              = 16
+	LDAPResultUndefinedAttributeType       = 17
+	LDAPResultInappropriateMatching        = 18
+	LDAPResultConstraintViolation          = 19
+	LDAPResultAttributeOrValueExists       = 20
+	LDAPResultInvalidAttributeSyntax       = 21
+	LDAPResultNoSuchObject                 = 32
+	LDAPResultAliasProblem                 = 33
+	LDAPResultInvalidDNSyntax              = 34
+	LDAPResultAliasDereferencingProblem    = 36
+	LDAPResultInappropriateAuthentication  = 48
+	LDAPResultInvalidCredentials           = 49
+	LDAPResultInsufficientAccessRights     = 50
+	LDAPResultBusy                         = 51
+	LDAPResultUnavailable                  = 52
+	LDAPResultUnwillingToPerform           = 53
+	LDAPResultLoopDetect                   = 54
+	LDAPResultNamingViolation              = 64
+	LDAPResultObjectClassViolation         = 65
+	LDAPResultNotAllowedOnNonLeaf          = 66
+	LDAPResultNotAllowedOnRDN              = 67
+	LDAPResultEntryAlreadyExists           = 68
+	LDAPResultObjectClassModsProhibited    = 69
+	LDAPResultAffectsMultipleDSAs          = 71
+	LDAPResultOther                        = 80
+
+	ErrorNetwork            = 200
+	ErrorFilterCompile      = 201
+	ErrorFilterDecompile    = 202
+	ErrorDebugging          = 203
+	ErrorUnexpectedMessage  = 204
+	ErrorUnexpectedResponse = 205
+)
+
+var LDAPResultCodeMap = map[uint8]string{
+	LDAPResultSuccess:                      "Success",
+	LDAPResultOperationsError:              "Operations Error",
+	LDAPResultProtocolError:                "Protocol Error",
+	LDAPResultTimeLimitExceeded:            "Time Limit Exceeded",
+	LDAPResultSizeLimitExceeded:            "Size Limit Exceeded",
+	LDAPResultCompareFalse:                 "Compare False",
+	LDAPResultCompareTrue:                  "Compare True",
+	LDAPResultAuthMethodNotSupported:       "Auth Method Not Supported",
+	LDAPResultStrongAuthRequired:           "Strong Auth Required",
+	LDAPResultReferral:                     "Referral",
+	LDAPResultAdminLimitExceeded:           "Admin Limit Exceeded",
+	LDAPResultUnavailableCriticalExtension: "Unavailable Critical Extension",
+	LDAPResultConfidentialityRequired:      "Confidentiality Required",
+	LDAPResultSaslBindInProgress:           "Sasl Bind In Progress",
+	LDAPResultNoSuchAttribute:              "No Such Attribute",
+	LDAPResultUndefinedAttributeType:       "Undefined Attribute Type",
+	LDAPResultInappropriateMatching:        "Inappropriate Matching",
+	LDAPResultConstraintViolation:          "Constraint Violation",
+	LDAPResultAttributeOrValueExists:       "Attribute Or Value Exists",
+	LDAPResultInvalidAttributeSyntax:       "Invalid Attribute Syntax",
+	LDAPResultNoSuchObject:                 "No Such Object",
+	LDAPResultAliasProblem:                 "Alias Problem",
+	LDAPResultInvalidDNSyntax:              "Invalid DN Syntax",
+	LDAPResultAliasDereferencingProblem:    "Alias Dereferencing Problem",
+	LDAPResultInappropriateAuthentication:  "Inappropriate Authentication",
+	LDAPResultInvalidCredentials:           "Invalid Credentials",
+	LDAPResultInsufficientAccessRights:     "Insufficient Access Rights",
+	LDAPResultBusy:                         "Busy",
+	LDAPResultUnavailable:                  "Unavailable",
+	LDAPResultUnwillingToPerform:           "Unwilling To Perform",
+	LDAPResultLoopDetect:                   "Loop Detect",
+	LDAPResultNamingViolation:              "Naming Violation",
+	LDAPResultObjectClassViolation:         "Object Class Violation",
+	LDAPResultNotAllowedOnNonLeaf:          "Not Allowed On Non Leaf",
+	LDAPResultNotAllowedOnRDN:              "Not Allowed On RDN",
+	LDAPResultEntryAlreadyExists:           "Entry Already Exists",
+	LDAPResultObjectClassModsProhibited:    "Object Class Mods Prohibited",
+	LDAPResultAffectsMultipleDSAs:          "Affects Multiple DSAs",
+	LDAPResultOther:                        "Other",
+}
+
+func getLDAPResultCode(packet *ber.Packet) (code uint8, description string) {
+	if len(packet.Children) >= 2 {
+		response := packet.Children[1]
+		if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 {
+			// Children[1].Children[2] is the diagnosticMessage which is guaranteed to exist as seen here: https://tools.ietf.org/html/rfc4511#section-4.1.9
+			return uint8(response.Children[0].Value.(int64)), response.Children[2].Value.(string)
+		}
+	}
+
+	return ErrorNetwork, "Invalid packet format"
+}
+
+type Error struct {
+	Err        error
+	ResultCode uint8
+}
+
+func (e *Error) Error() string {
+	return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error())
+}
+
+func NewError(resultCode uint8, err error) error {
+	return &Error{ResultCode: resultCode, Err: err}
+}
+
+func IsErrorWithCode(err error, desiredResultCode uint8) bool {
+	if err == nil {
+		return false
+	}
+
+	serverError, ok := err.(*Error)
+	if !ok {
+		return false
+	}
+
+	return serverError.ResultCode == desiredResultCode
+}

+ 2 - 2
Godeps/_workspace/src/github.com/go-ldap/ldap/example_test.go

@@ -5,10 +5,10 @@ import (
 	"fmt"
 	"fmt"
 	"log"
 	"log"
 
 
-	"github.com/go-ldap/ldap"
+	"gopkg.in/ldap.v2"
 )
 )
 
 
-// ExampleConn_Bind demonstrats how to bind a connection to an ldap user
+// ExampleConn_Bind demonstrates how to bind a connection to an ldap user
 // allowing access to restricted attrabutes that user has access to
 // allowing access to restricted attrabutes that user has access to
 func ExampleConn_Bind() {
 func ExampleConn_Bind() {
 	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389))
 	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389))

+ 239 - 35
Godeps/_workspace/src/github.com/go-ldap/ldap/filter.go

@@ -5,9 +5,12 @@
 package ldap
 package ldap
 
 
 import (
 import (
+	"bytes"
+	hexpac "encoding/hex"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"strings"
 	"strings"
+	"unicode/utf8"
 
 
 	"gopkg.in/asn1-ber.v1"
 	"gopkg.in/asn1-ber.v1"
 )
 )
@@ -50,6 +53,20 @@ var FilterSubstringsMap = map[uint64]string{
 	FilterSubstringsFinal:   "Substrings Final",
 	FilterSubstringsFinal:   "Substrings Final",
 }
 }
 
 
+const (
+	MatchingRuleAssertionMatchingRule = 1
+	MatchingRuleAssertionType         = 2
+	MatchingRuleAssertionMatchValue   = 3
+	MatchingRuleAssertionDNAttributes = 4
+)
+
+var MatchingRuleAssertionMap = map[uint64]string{
+	MatchingRuleAssertionMatchingRule: "Matching Rule Assertion Matching Rule",
+	MatchingRuleAssertionType:         "Matching Rule Assertion Type",
+	MatchingRuleAssertionMatchValue:   "Matching Rule Assertion Match Value",
+	MatchingRuleAssertionDNAttributes: "Matching Rule Assertion DN Attributes",
+}
+
 func CompileFilter(filter string) (*ber.Packet, error) {
 func CompileFilter(filter string) (*ber.Packet, error) {
 	if len(filter) == 0 || filter[0] != '(' {
 	if len(filter) == 0 || filter[0] != '(' {
 		return nil, NewError(ErrorFilterCompile, errors.New("ldap: filter does not start with an '('"))
 		return nil, NewError(ErrorFilterCompile, errors.New("ldap: filter does not start with an '('"))
@@ -108,7 +125,7 @@ func DecompileFilter(packet *ber.Packet) (ret string, err error) {
 			if i == 0 && child.Tag != FilterSubstringsInitial {
 			if i == 0 && child.Tag != FilterSubstringsInitial {
 				ret += "*"
 				ret += "*"
 			}
 			}
-			ret += ber.DecodeString(child.Data.Bytes())
+			ret += EscapeFilter(ber.DecodeString(child.Data.Bytes()))
 			if child.Tag != FilterSubstringsFinal {
 			if child.Tag != FilterSubstringsFinal {
 				ret += "*"
 				ret += "*"
 			}
 			}
@@ -116,22 +133,53 @@ func DecompileFilter(packet *ber.Packet) (ret string, err error) {
 	case FilterEqualityMatch:
 	case FilterEqualityMatch:
 		ret += ber.DecodeString(packet.Children[0].Data.Bytes())
 		ret += ber.DecodeString(packet.Children[0].Data.Bytes())
 		ret += "="
 		ret += "="
-		ret += ber.DecodeString(packet.Children[1].Data.Bytes())
+		ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))
 	case FilterGreaterOrEqual:
 	case FilterGreaterOrEqual:
 		ret += ber.DecodeString(packet.Children[0].Data.Bytes())
 		ret += ber.DecodeString(packet.Children[0].Data.Bytes())
 		ret += ">="
 		ret += ">="
-		ret += ber.DecodeString(packet.Children[1].Data.Bytes())
+		ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))
 	case FilterLessOrEqual:
 	case FilterLessOrEqual:
 		ret += ber.DecodeString(packet.Children[0].Data.Bytes())
 		ret += ber.DecodeString(packet.Children[0].Data.Bytes())
 		ret += "<="
 		ret += "<="
-		ret += ber.DecodeString(packet.Children[1].Data.Bytes())
+		ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))
 	case FilterPresent:
 	case FilterPresent:
 		ret += ber.DecodeString(packet.Data.Bytes())
 		ret += ber.DecodeString(packet.Data.Bytes())
 		ret += "=*"
 		ret += "=*"
 	case FilterApproxMatch:
 	case FilterApproxMatch:
 		ret += ber.DecodeString(packet.Children[0].Data.Bytes())
 		ret += ber.DecodeString(packet.Children[0].Data.Bytes())
 		ret += "~="
 		ret += "~="
-		ret += ber.DecodeString(packet.Children[1].Data.Bytes())
+		ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))
+	case FilterExtensibleMatch:
+		attr := ""
+		dnAttributes := false
+		matchingRule := ""
+		value := ""
+
+		for _, child := range packet.Children {
+			switch child.Tag {
+			case MatchingRuleAssertionMatchingRule:
+				matchingRule = ber.DecodeString(child.Data.Bytes())
+			case MatchingRuleAssertionType:
+				attr = ber.DecodeString(child.Data.Bytes())
+			case MatchingRuleAssertionMatchValue:
+				value = ber.DecodeString(child.Data.Bytes())
+			case MatchingRuleAssertionDNAttributes:
+				dnAttributes = child.Value.(bool)
+			}
+		}
+
+		if len(attr) > 0 {
+			ret += attr
+		}
+		if dnAttributes {
+			ret += ":dn"
+		}
+		if len(matchingRule) > 0 {
+			ret += ":"
+			ret += matchingRule
+		}
+		ret += ":="
+		ret += EscapeFilter(value)
 	}
 	}
 
 
 	ret += ")"
 	ret += ")"
@@ -155,58 +203,143 @@ func compileFilterSet(filter string, pos int, parent *ber.Packet) (int, error) {
 }
 }
 
 
 func compileFilter(filter string, pos int) (*ber.Packet, int, error) {
 func compileFilter(filter string, pos int) (*ber.Packet, int, error) {
-	var packet *ber.Packet
-	var err error
+	var (
+		packet *ber.Packet
+		err    error
+	)
 
 
 	defer func() {
 	defer func() {
 		if r := recover(); r != nil {
 		if r := recover(); r != nil {
 			err = NewError(ErrorFilterCompile, errors.New("ldap: error compiling filter"))
 			err = NewError(ErrorFilterCompile, errors.New("ldap: error compiling filter"))
 		}
 		}
 	}()
 	}()
-
 	newPos := pos
 	newPos := pos
-	switch filter[pos] {
+
+	currentRune, currentWidth := utf8.DecodeRuneInString(filter[newPos:])
+
+	switch currentRune {
+	case utf8.RuneError:
+		return nil, 0, NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", newPos))
 	case '(':
 	case '(':
-		packet, newPos, err = compileFilter(filter, pos+1)
+		packet, newPos, err = compileFilter(filter, pos+currentWidth)
 		newPos++
 		newPos++
 		return packet, newPos, err
 		return packet, newPos, err
 	case '&':
 	case '&':
 		packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterAnd, nil, FilterMap[FilterAnd])
 		packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterAnd, nil, FilterMap[FilterAnd])
-		newPos, err = compileFilterSet(filter, pos+1, packet)
+		newPos, err = compileFilterSet(filter, pos+currentWidth, packet)
 		return packet, newPos, err
 		return packet, newPos, err
 	case '|':
 	case '|':
 		packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterOr, nil, FilterMap[FilterOr])
 		packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterOr, nil, FilterMap[FilterOr])
-		newPos, err = compileFilterSet(filter, pos+1, packet)
+		newPos, err = compileFilterSet(filter, pos+currentWidth, packet)
 		return packet, newPos, err
 		return packet, newPos, err
 	case '!':
 	case '!':
 		packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterNot, nil, FilterMap[FilterNot])
 		packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterNot, nil, FilterMap[FilterNot])
 		var child *ber.Packet
 		var child *ber.Packet
-		child, newPos, err = compileFilter(filter, pos+1)
+		child, newPos, err = compileFilter(filter, pos+currentWidth)
 		packet.AppendChild(child)
 		packet.AppendChild(child)
 		return packet, newPos, err
 		return packet, newPos, err
 	default:
 	default:
+		READING_ATTR := 0
+		READING_EXTENSIBLE_MATCHING_RULE := 1
+		READING_CONDITION := 2
+
+		state := READING_ATTR
+
 		attribute := ""
 		attribute := ""
+		extensibleDNAttributes := false
+		extensibleMatchingRule := ""
 		condition := ""
 		condition := ""
-		for newPos < len(filter) && filter[newPos] != ')' {
-			switch {
-			case packet != nil:
-				condition += fmt.Sprintf("%c", filter[newPos])
-			case filter[newPos] == '=':
-				packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch])
-			case filter[newPos] == '>' && filter[newPos+1] == '=':
-				packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual])
-				newPos++
-			case filter[newPos] == '<' && filter[newPos+1] == '=':
-				packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual])
-				newPos++
-			case filter[newPos] == '~' && filter[newPos+1] == '=':
-				packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterLessOrEqual])
-				newPos++
-			case packet == nil:
-				attribute += fmt.Sprintf("%c", filter[newPos])
-			}
-			newPos++
+
+		for newPos < len(filter) {
+			remainingFilter := filter[newPos:]
+			currentRune, currentWidth = utf8.DecodeRuneInString(remainingFilter)
+			if currentRune == ')' {
+				break
+			}
+			if currentRune == utf8.RuneError {
+				return packet, newPos, NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", newPos))
+			}
+
+			switch state {
+			case READING_ATTR:
+				switch {
+				// Extensible rule, with only DN-matching
+				case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:="):
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch])
+					extensibleDNAttributes = true
+					state = READING_CONDITION
+					newPos += 5
+
+				// Extensible rule, with DN-matching and a matching OID
+				case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:"):
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch])
+					extensibleDNAttributes = true
+					state = READING_EXTENSIBLE_MATCHING_RULE
+					newPos += 4
+
+				// Extensible rule, with attr only
+				case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="):
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch])
+					state = READING_CONDITION
+					newPos += 2
+
+				// Extensible rule, with no DN attribute matching
+				case currentRune == ':':
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch])
+					state = READING_EXTENSIBLE_MATCHING_RULE
+					newPos += 1
+
+				// Equality condition
+				case currentRune == '=':
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch])
+					state = READING_CONDITION
+					newPos += 1
+
+				// Greater-than or equal
+				case currentRune == '>' && strings.HasPrefix(remainingFilter, ">="):
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual])
+					state = READING_CONDITION
+					newPos += 2
+
+				// Less-than or equal
+				case currentRune == '<' && strings.HasPrefix(remainingFilter, "<="):
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual])
+					state = READING_CONDITION
+					newPos += 2
+
+				// Approx
+				case currentRune == '~' && strings.HasPrefix(remainingFilter, "~="):
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterApproxMatch])
+					state = READING_CONDITION
+					newPos += 2
+
+				// Still reading the attribute name
+				default:
+					attribute += fmt.Sprintf("%c", currentRune)
+					newPos += currentWidth
+				}
+
+			case READING_EXTENSIBLE_MATCHING_RULE:
+				switch {
+
+				// Matching rule OID is done
+				case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="):
+					state = READING_CONDITION
+					newPos += 2
+
+				// Still reading the matching rule oid
+				default:
+					extensibleMatchingRule += fmt.Sprintf("%c", currentRune)
+					newPos += currentWidth
+				}
+
+			case READING_CONDITION:
+				// append to the condition
+				condition += fmt.Sprintf("%c", currentRune)
+				newPos += currentWidth
+			}
 		}
 		}
+
 		if newPos == len(filter) {
 		if newPos == len(filter) {
 			err = NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter"))
 			err = NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter"))
 			return packet, newPos, err
 			return packet, newPos, err
@@ -217,6 +350,36 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) {
 		}
 		}
 
 
 		switch {
 		switch {
+		case packet.Tag == FilterExtensibleMatch:
+			// MatchingRuleAssertion ::= SEQUENCE {
+			//         matchingRule    [1] MatchingRuleID OPTIONAL,
+			//         type            [2] AttributeDescription OPTIONAL,
+			//         matchValue      [3] AssertionValue,
+			//         dnAttributes    [4] BOOLEAN DEFAULT FALSE
+			// }
+
+			// Include the matching rule oid, if specified
+			if len(extensibleMatchingRule) > 0 {
+				packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchingRule, extensibleMatchingRule, MatchingRuleAssertionMap[MatchingRuleAssertionMatchingRule]))
+			}
+
+			// Include the attribute, if specified
+			if len(attribute) > 0 {
+				packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionType, attribute, MatchingRuleAssertionMap[MatchingRuleAssertionType]))
+			}
+
+			// Add the value (only required child)
+			encodedString, err := escapedStringToEncodedBytes(condition)
+			if err != nil {
+				return packet, newPos, err
+			}
+			packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchValue, encodedString, MatchingRuleAssertionMap[MatchingRuleAssertionMatchValue]))
+
+			// Defaults to false, so only include in the sequence if true
+			if extensibleDNAttributes {
+				packet.AppendChild(ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionDNAttributes, extensibleDNAttributes, MatchingRuleAssertionMap[MatchingRuleAssertionDNAttributes]))
+			}
+
 		case packet.Tag == FilterEqualityMatch && condition == "*":
 		case packet.Tag == FilterEqualityMatch && condition == "*":
 			packet = ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterPresent, attribute, FilterMap[FilterPresent])
 			packet = ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterPresent, attribute, FilterMap[FilterPresent])
 		case packet.Tag == FilterEqualityMatch && strings.Contains(condition, "*"):
 		case packet.Tag == FilterEqualityMatch && strings.Contains(condition, "*"):
@@ -238,15 +401,56 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) {
 				default:
 				default:
 					tag = FilterSubstringsAny
 					tag = FilterSubstringsAny
 				}
 				}
-				seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, part, FilterSubstringsMap[uint64(tag)]))
+				encodedString, err := escapedStringToEncodedBytes(part)
+				if err != nil {
+					return packet, newPos, err
+				}
+				seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, encodedString, FilterSubstringsMap[uint64(tag)]))
 			}
 			}
 			packet.AppendChild(seq)
 			packet.AppendChild(seq)
 		default:
 		default:
+			encodedString, err := escapedStringToEncodedBytes(condition)
+			if err != nil {
+				return packet, newPos, err
+			}
 			packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute"))
 			packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute"))
-			packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, condition, "Condition"))
+			packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, encodedString, "Condition"))
 		}
 		}
 
 
-		newPos++
+		newPos += currentWidth
 		return packet, newPos, err
 		return packet, newPos, err
 	}
 	}
 }
 }
+
+// Convert from "ABC\xx\xx\xx" form to literal bytes for transport
+func escapedStringToEncodedBytes(escapedString string) (string, error) {
+	var buffer bytes.Buffer
+	i := 0
+	for i < len(escapedString) {
+		currentRune, currentWidth := utf8.DecodeRuneInString(escapedString[i:])
+		if currentRune == utf8.RuneError {
+			return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", i))
+		}
+
+		// Check for escaped hex characters and convert them to their literal value for transport.
+		if currentRune == '\\' {
+			// http://tools.ietf.org/search/rfc4515
+			// \ (%x5C) is not a valid character unless it is followed by two HEX characters due to not
+			// being a member of UTF1SUBSET.
+			if i+2 > len(escapedString) {
+				return "", NewError(ErrorFilterCompile, errors.New("ldap: missing characters for escape in filter"))
+			}
+			if escByte, decodeErr := hexpac.DecodeString(escapedString[i+1 : i+3]); decodeErr != nil {
+				return "", NewError(ErrorFilterCompile, errors.New("ldap: invalid characters for escape in filter"))
+			} else {
+				buffer.WriteByte(escByte[0])
+				i += 2 // +1 from end of loop, so 3 total for \xx.
+			}
+		} else {
+			buffer.WriteRune(currentRune)
+		}
+
+		i += currentWidth
+	}
+	return buffer.String(), nil
+}

+ 194 - 28
Godeps/_workspace/src/github.com/go-ldap/ldap/filter_test.go

@@ -1,54 +1,220 @@
-package ldap
+package ldap_test
 
 
 import (
 import (
+	"strings"
 	"testing"
 	"testing"
 
 
 	"gopkg.in/asn1-ber.v1"
 	"gopkg.in/asn1-ber.v1"
+	"gopkg.in/ldap.v2"
 )
 )
 
 
 type compileTest struct {
 type compileTest struct {
-	filterStr  string
-	filterType int
+	filterStr string
+
+	expectedFilter string
+	expectedType   int
+	expectedErr    string
 }
 }
 
 
 var testFilters = []compileTest{
 var testFilters = []compileTest{
-	compileTest{filterStr: "(&(sn=Miller)(givenName=Bob))", filterType: FilterAnd},
-	compileTest{filterStr: "(|(sn=Miller)(givenName=Bob))", filterType: FilterOr},
-	compileTest{filterStr: "(!(sn=Miller))", filterType: FilterNot},
-	compileTest{filterStr: "(sn=Miller)", filterType: FilterEqualityMatch},
-	compileTest{filterStr: "(sn=Mill*)", filterType: FilterSubstrings},
-	compileTest{filterStr: "(sn=*Mill)", filterType: FilterSubstrings},
-	compileTest{filterStr: "(sn=*Mill*)", filterType: FilterSubstrings},
-	compileTest{filterStr: "(sn=*i*le*)", filterType: FilterSubstrings},
-	compileTest{filterStr: "(sn=Mi*l*r)", filterType: FilterSubstrings},
-	compileTest{filterStr: "(sn=Mi*le*)", filterType: FilterSubstrings},
-	compileTest{filterStr: "(sn=*i*ler)", filterType: FilterSubstrings},
-	compileTest{filterStr: "(sn>=Miller)", filterType: FilterGreaterOrEqual},
-	compileTest{filterStr: "(sn<=Miller)", filterType: FilterLessOrEqual},
-	compileTest{filterStr: "(sn=*)", filterType: FilterPresent},
-	compileTest{filterStr: "(sn~=Miller)", filterType: FilterApproxMatch},
+	compileTest{
+		filterStr:      "(&(sn=Miller)(givenName=Bob))",
+		expectedFilter: "(&(sn=Miller)(givenName=Bob))",
+		expectedType:   ldap.FilterAnd,
+	},
+	compileTest{
+		filterStr:      "(|(sn=Miller)(givenName=Bob))",
+		expectedFilter: "(|(sn=Miller)(givenName=Bob))",
+		expectedType:   ldap.FilterOr,
+	},
+	compileTest{
+		filterStr:      "(!(sn=Miller))",
+		expectedFilter: "(!(sn=Miller))",
+		expectedType:   ldap.FilterNot,
+	},
+	compileTest{
+		filterStr:      "(sn=Miller)",
+		expectedFilter: "(sn=Miller)",
+		expectedType:   ldap.FilterEqualityMatch,
+	},
+	compileTest{
+		filterStr:      "(sn=Mill*)",
+		expectedFilter: "(sn=Mill*)",
+		expectedType:   ldap.FilterSubstrings,
+	},
+	compileTest{
+		filterStr:      "(sn=*Mill)",
+		expectedFilter: "(sn=*Mill)",
+		expectedType:   ldap.FilterSubstrings,
+	},
+	compileTest{
+		filterStr:      "(sn=*Mill*)",
+		expectedFilter: "(sn=*Mill*)",
+		expectedType:   ldap.FilterSubstrings,
+	},
+	compileTest{
+		filterStr:      "(sn=*i*le*)",
+		expectedFilter: "(sn=*i*le*)",
+		expectedType:   ldap.FilterSubstrings,
+	},
+	compileTest{
+		filterStr:      "(sn=Mi*l*r)",
+		expectedFilter: "(sn=Mi*l*r)",
+		expectedType:   ldap.FilterSubstrings,
+	},
+	// substring filters escape properly
+	compileTest{
+		filterStr:      `(sn=Mi*함*r)`,
+		expectedFilter: `(sn=Mi*\ed\95\a8*r)`,
+		expectedType:   ldap.FilterSubstrings,
+	},
+	// already escaped substring filters don't get double-escaped
+	compileTest{
+		filterStr:      `(sn=Mi*\ed\95\a8*r)`,
+		expectedFilter: `(sn=Mi*\ed\95\a8*r)`,
+		expectedType:   ldap.FilterSubstrings,
+	},
+	compileTest{
+		filterStr:      "(sn=Mi*le*)",
+		expectedFilter: "(sn=Mi*le*)",
+		expectedType:   ldap.FilterSubstrings,
+	},
+	compileTest{
+		filterStr:      "(sn=*i*ler)",
+		expectedFilter: "(sn=*i*ler)",
+		expectedType:   ldap.FilterSubstrings,
+	},
+	compileTest{
+		filterStr:      "(sn>=Miller)",
+		expectedFilter: "(sn>=Miller)",
+		expectedType:   ldap.FilterGreaterOrEqual,
+	},
+	compileTest{
+		filterStr:      "(sn<=Miller)",
+		expectedFilter: "(sn<=Miller)",
+		expectedType:   ldap.FilterLessOrEqual,
+	},
+	compileTest{
+		filterStr:      "(sn=*)",
+		expectedFilter: "(sn=*)",
+		expectedType:   ldap.FilterPresent,
+	},
+	compileTest{
+		filterStr:      "(sn~=Miller)",
+		expectedFilter: "(sn~=Miller)",
+		expectedType:   ldap.FilterApproxMatch,
+	},
+	compileTest{
+		filterStr:      `(objectGUID='\fc\fe\a3\ab\f9\90N\aaGm\d5I~\d12)`,
+		expectedFilter: `(objectGUID='\fc\fe\a3\ab\f9\90N\aaGm\d5I~\d12)`,
+		expectedType:   ldap.FilterEqualityMatch,
+	},
+	compileTest{
+		filterStr:      `(objectGUID=абвгдеёжзийклмнопрстуфхцчшщъыьэюя)`,
+		expectedFilter: `(objectGUID=\d0\b0\d0\b1\d0\b2\d0\b3\d0\b4\d0\b5\d1\91\d0\b6\d0\b7\d0\b8\d0\b9\d0\ba\d0\bb\d0\bc\d0\bd\d0\be\d0\bf\d1\80\d1\81\d1\82\d1\83\d1\84\d1\85\d1\86\d1\87\d1\88\d1\89\d1\8a\d1\8b\d1\8c\d1\8d\d1\8e\d1\8f)`,
+		expectedType:   ldap.FilterEqualityMatch,
+	},
+	compileTest{
+		filterStr:      `(objectGUID=함수목록)`,
+		expectedFilter: `(objectGUID=\ed\95\a8\ec\88\98\eb\aa\a9\eb\a1\9d)`,
+		expectedType:   ldap.FilterEqualityMatch,
+	},
+	compileTest{
+		filterStr:      `(objectGUID=`,
+		expectedFilter: ``,
+		expectedType:   0,
+		expectedErr:    "unexpected end of filter",
+	},
+	compileTest{
+		filterStr:      `(objectGUID=함수목록`,
+		expectedFilter: ``,
+		expectedType:   0,
+		expectedErr:    "unexpected end of filter",
+	},
+	compileTest{
+		filterStr:      `(&(objectclass=inetorgperson)(cn=中文))`,
+		expectedFilter: `(&(objectclass=inetorgperson)(cn=\e4\b8\ad\e6\96\87))`,
+		expectedType:   0,
+	},
+	// attr extension
+	compileTest{
+		filterStr:      `(memberOf:=foo)`,
+		expectedFilter: `(memberOf:=foo)`,
+		expectedType:   ldap.FilterExtensibleMatch,
+	},
+	// attr+named matching rule extension
+	compileTest{
+		filterStr:      `(memberOf:test:=foo)`,
+		expectedFilter: `(memberOf:test:=foo)`,
+		expectedType:   ldap.FilterExtensibleMatch,
+	},
+	// attr+oid matching rule extension
+	compileTest{
+		filterStr:      `(cn:1.2.3.4.5:=Fred Flintstone)`,
+		expectedFilter: `(cn:1.2.3.4.5:=Fred Flintstone)`,
+		expectedType:   ldap.FilterExtensibleMatch,
+	},
+	// attr+dn+oid matching rule extension
+	compileTest{
+		filterStr:      `(sn:dn:2.4.6.8.10:=Barney Rubble)`,
+		expectedFilter: `(sn:dn:2.4.6.8.10:=Barney Rubble)`,
+		expectedType:   ldap.FilterExtensibleMatch,
+	},
+	// attr+dn extension
+	compileTest{
+		filterStr:      `(o:dn:=Ace Industry)`,
+		expectedFilter: `(o:dn:=Ace Industry)`,
+		expectedType:   ldap.FilterExtensibleMatch,
+	},
+	// dn extension
+	compileTest{
+		filterStr:      `(:dn:2.4.6.8.10:=Dino)`,
+		expectedFilter: `(:dn:2.4.6.8.10:=Dino)`,
+		expectedType:   ldap.FilterExtensibleMatch,
+	},
+	compileTest{
+		filterStr:      `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`,
+		expectedFilter: `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`,
+		expectedType:   ldap.FilterExtensibleMatch,
+	},
+
 	// compileTest{ filterStr: "()", filterType: FilterExtensibleMatch },
 	// compileTest{ filterStr: "()", filterType: FilterExtensibleMatch },
 }
 }
 
 
+var testInvalidFilters = []string{
+	`(objectGUID=\zz)`,
+	`(objectGUID=\a)`,
+}
+
 func TestFilter(t *testing.T) {
 func TestFilter(t *testing.T) {
 	// Test Compiler and Decompiler
 	// Test Compiler and Decompiler
 	for _, i := range testFilters {
 	for _, i := range testFilters {
-		filter, err := CompileFilter(i.filterStr)
+		filter, err := ldap.CompileFilter(i.filterStr)
 		if err != nil {
 		if err != nil {
-			t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error())
-		} else if filter.Tag != ber.Tag(i.filterType) {
-			t.Errorf("%q Expected %q got %q", i.filterStr, FilterMap[uint64(i.filterType)], FilterMap[uint64(filter.Tag)])
+			if i.expectedErr == "" || !strings.Contains(err.Error(), i.expectedErr) {
+				t.Errorf("Problem compiling '%s' - '%v' (expected error to contain '%v')", i.filterStr, err, i.expectedErr)
+			}
+		} else if filter.Tag != ber.Tag(i.expectedType) {
+			t.Errorf("%q Expected %q got %q", i.filterStr, ldap.FilterMap[uint64(i.expectedType)], ldap.FilterMap[uint64(filter.Tag)])
 		} else {
 		} else {
-			o, err := DecompileFilter(filter)
+			o, err := ldap.DecompileFilter(filter)
 			if err != nil {
 			if err != nil {
 				t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error())
 				t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error())
-			} else if i.filterStr != o {
-				t.Errorf("%q expected, got %q", i.filterStr, o)
+			} else if i.expectedFilter != o {
+				t.Errorf("%q expected, got %q", i.expectedFilter, o)
 			}
 			}
 		}
 		}
 	}
 	}
 }
 }
 
 
+func TestInvalidFilter(t *testing.T) {
+	for _, filterStr := range testInvalidFilters {
+		if _, err := ldap.CompileFilter(filterStr); err == nil {
+			t.Errorf("Problem compiling %s - expected err", filterStr)
+		}
+	}
+}
+
 func BenchmarkFilterCompile(b *testing.B) {
 func BenchmarkFilterCompile(b *testing.B) {
 	b.StopTimer()
 	b.StopTimer()
 	filters := make([]string, len(testFilters))
 	filters := make([]string, len(testFilters))
@@ -61,7 +227,7 @@ func BenchmarkFilterCompile(b *testing.B) {
 	maxIdx := len(filters)
 	maxIdx := len(filters)
 	b.StartTimer()
 	b.StartTimer()
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
-		CompileFilter(filters[i%maxIdx])
+		ldap.CompileFilter(filters[i%maxIdx])
 	}
 	}
 }
 }
 
 
@@ -71,12 +237,12 @@ func BenchmarkFilterDecompile(b *testing.B) {
 
 
 	// Test Compiler and Decompiler
 	// Test Compiler and Decompiler
 	for idx, i := range testFilters {
 	for idx, i := range testFilters {
-		filters[idx], _ = CompileFilter(i.filterStr)
+		filters[idx], _ = ldap.CompileFilter(i.filterStr)
 	}
 	}
 
 
 	maxIdx := len(filters)
 	maxIdx := len(filters)
 	b.StartTimer()
 	b.StartTimer()
 	for i := 0; i < b.N; i++ {
 	for i := 0; i < b.N; i++ {
-		DecompileFilter(filters[i%maxIdx])
+		ldap.DecompileFilter(filters[i%maxIdx])
 	}
 	}
 }
 }

+ 2 - 119
Godeps/_workspace/src/github.com/go-ldap/ldap/ldap.go

@@ -6,7 +6,6 @@ package ldap
 
 
 import (
 import (
 	"errors"
 	"errors"
-	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"os"
 	"os"
 
 
@@ -60,98 +59,6 @@ var ApplicationMap = map[uint8]string{
 	ApplicationExtendedResponse:      "Extended Response",
 	ApplicationExtendedResponse:      "Extended Response",
 }
 }
 
 
-// LDAP Result Codes
-const (
-	LDAPResultSuccess                      = 0
-	LDAPResultOperationsError              = 1
-	LDAPResultProtocolError                = 2
-	LDAPResultTimeLimitExceeded            = 3
-	LDAPResultSizeLimitExceeded            = 4
-	LDAPResultCompareFalse                 = 5
-	LDAPResultCompareTrue                  = 6
-	LDAPResultAuthMethodNotSupported       = 7
-	LDAPResultStrongAuthRequired           = 8
-	LDAPResultReferral                     = 10
-	LDAPResultAdminLimitExceeded           = 11
-	LDAPResultUnavailableCriticalExtension = 12
-	LDAPResultConfidentialityRequired      = 13
-	LDAPResultSaslBindInProgress           = 14
-	LDAPResultNoSuchAttribute              = 16
-	LDAPResultUndefinedAttributeType       = 17
-	LDAPResultInappropriateMatching        = 18
-	LDAPResultConstraintViolation          = 19
-	LDAPResultAttributeOrValueExists       = 20
-	LDAPResultInvalidAttributeSyntax       = 21
-	LDAPResultNoSuchObject                 = 32
-	LDAPResultAliasProblem                 = 33
-	LDAPResultInvalidDNSyntax              = 34
-	LDAPResultAliasDereferencingProblem    = 36
-	LDAPResultInappropriateAuthentication  = 48
-	LDAPResultInvalidCredentials           = 49
-	LDAPResultInsufficientAccessRights     = 50
-	LDAPResultBusy                         = 51
-	LDAPResultUnavailable                  = 52
-	LDAPResultUnwillingToPerform           = 53
-	LDAPResultLoopDetect                   = 54
-	LDAPResultNamingViolation              = 64
-	LDAPResultObjectClassViolation         = 65
-	LDAPResultNotAllowedOnNonLeaf          = 66
-	LDAPResultNotAllowedOnRDN              = 67
-	LDAPResultEntryAlreadyExists           = 68
-	LDAPResultObjectClassModsProhibited    = 69
-	LDAPResultAffectsMultipleDSAs          = 71
-	LDAPResultOther                        = 80
-
-	ErrorNetwork            = 200
-	ErrorFilterCompile      = 201
-	ErrorFilterDecompile    = 202
-	ErrorDebugging          = 203
-	ErrorUnexpectedMessage  = 204
-	ErrorUnexpectedResponse = 205
-)
-
-var LDAPResultCodeMap = map[uint8]string{
-	LDAPResultSuccess:                      "Success",
-	LDAPResultOperationsError:              "Operations Error",
-	LDAPResultProtocolError:                "Protocol Error",
-	LDAPResultTimeLimitExceeded:            "Time Limit Exceeded",
-	LDAPResultSizeLimitExceeded:            "Size Limit Exceeded",
-	LDAPResultCompareFalse:                 "Compare False",
-	LDAPResultCompareTrue:                  "Compare True",
-	LDAPResultAuthMethodNotSupported:       "Auth Method Not Supported",
-	LDAPResultStrongAuthRequired:           "Strong Auth Required",
-	LDAPResultReferral:                     "Referral",
-	LDAPResultAdminLimitExceeded:           "Admin Limit Exceeded",
-	LDAPResultUnavailableCriticalExtension: "Unavailable Critical Extension",
-	LDAPResultConfidentialityRequired:      "Confidentiality Required",
-	LDAPResultSaslBindInProgress:           "Sasl Bind In Progress",
-	LDAPResultNoSuchAttribute:              "No Such Attribute",
-	LDAPResultUndefinedAttributeType:       "Undefined Attribute Type",
-	LDAPResultInappropriateMatching:        "Inappropriate Matching",
-	LDAPResultConstraintViolation:          "Constraint Violation",
-	LDAPResultAttributeOrValueExists:       "Attribute Or Value Exists",
-	LDAPResultInvalidAttributeSyntax:       "Invalid Attribute Syntax",
-	LDAPResultNoSuchObject:                 "No Such Object",
-	LDAPResultAliasProblem:                 "Alias Problem",
-	LDAPResultInvalidDNSyntax:              "Invalid DN Syntax",
-	LDAPResultAliasDereferencingProblem:    "Alias Dereferencing Problem",
-	LDAPResultInappropriateAuthentication:  "Inappropriate Authentication",
-	LDAPResultInvalidCredentials:           "Invalid Credentials",
-	LDAPResultInsufficientAccessRights:     "Insufficient Access Rights",
-	LDAPResultBusy:                         "Busy",
-	LDAPResultUnavailable:                  "Unavailable",
-	LDAPResultUnwillingToPerform:           "Unwilling To Perform",
-	LDAPResultLoopDetect:                   "Loop Detect",
-	LDAPResultNamingViolation:              "Naming Violation",
-	LDAPResultObjectClassViolation:         "Object Class Violation",
-	LDAPResultNotAllowedOnNonLeaf:          "Not Allowed On Non Leaf",
-	LDAPResultNotAllowedOnRDN:              "Not Allowed On RDN",
-	LDAPResultEntryAlreadyExists:           "Entry Already Exists",
-	LDAPResultObjectClassModsProhibited:    "Object Class Mods Prohibited",
-	LDAPResultAffectsMultipleDSAs:          "Affects Multiple DSAs",
-	LDAPResultOther:                        "Other",
-}
-
 // Ldap Behera Password Policy Draft 10 (https://tools.ietf.org/html/draft-behera-ldap-password-policy-10)
 // Ldap Behera Password Policy Draft 10 (https://tools.ietf.org/html/draft-behera-ldap-password-policy-10)
 const (
 const (
 	BeheraPasswordExpired             = 0
 	BeheraPasswordExpired             = 0
@@ -318,8 +225,8 @@ func addRequestDescriptions(packet *ber.Packet) {
 }
 }
 
 
 func addDefaultLDAPResponseDescriptions(packet *ber.Packet) {
 func addDefaultLDAPResponseDescriptions(packet *ber.Packet) {
-	resultCode := packet.Children[1].Children[0].Value.(int64)
-	packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[uint8(resultCode)] + ")"
+	resultCode, _ := getLDAPResultCode(packet)
+	packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[resultCode] + ")"
 	packet.Children[1].Children[1].Description = "Matched DN"
 	packet.Children[1].Children[1].Description = "Matched DN"
 	packet.Children[1].Children[2].Description = "Error Message"
 	packet.Children[1].Children[2].Description = "Error Message"
 	if len(packet.Children[1].Children) > 3 {
 	if len(packet.Children[1].Children) > 3 {
@@ -343,30 +250,6 @@ func DebugBinaryFile(fileName string) error {
 	return nil
 	return nil
 }
 }
 
 
-type Error struct {
-	Err        error
-	ResultCode uint8
-}
-
-func (e *Error) Error() string {
-	return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error())
-}
-
-func NewError(resultCode uint8, err error) error {
-	return &Error{ResultCode: resultCode, Err: err}
-}
-
-func getLDAPResultCode(packet *ber.Packet) (code uint8, description string) {
-	if len(packet.Children) >= 2 {
-		response := packet.Children[1]
-		if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 {
-			return uint8(response.Children[0].Value.(int64)), response.Children[2].Value.(string)
-		}
-	}
-
-	return ErrorNetwork, "Invalid packet format"
-}
-
 var hex = "0123456789abcdef"
 var hex = "0123456789abcdef"
 
 
 func mustEscape(c byte) bool {
 func mustEscape(c byte) bool {

+ 53 - 25
Godeps/_workspace/src/github.com/go-ldap/ldap/ldap_test.go

@@ -1,9 +1,11 @@
-package ldap
+package ldap_test
 
 
 import (
 import (
 	"crypto/tls"
 	"crypto/tls"
 	"fmt"
 	"fmt"
 	"testing"
 	"testing"
+
+	"gopkg.in/ldap.v2"
 )
 )
 
 
 var ldapServer = "ldap.itd.umich.edu"
 var ldapServer = "ldap.itd.umich.edu"
@@ -21,7 +23,7 @@ var attributes = []string{
 
 
 func TestDial(t *testing.T) {
 func TestDial(t *testing.T) {
 	fmt.Printf("TestDial: starting...\n")
 	fmt.Printf("TestDial: starting...\n")
-	l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
+	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
 	if err != nil {
 	if err != nil {
 		t.Errorf(err.Error())
 		t.Errorf(err.Error())
 		return
 		return
@@ -32,7 +34,7 @@ func TestDial(t *testing.T) {
 
 
 func TestDialTLS(t *testing.T) {
 func TestDialTLS(t *testing.T) {
 	fmt.Printf("TestDialTLS: starting...\n")
 	fmt.Printf("TestDialTLS: starting...\n")
-	l, err := DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true})
+	l, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true})
 	if err != nil {
 	if err != nil {
 		t.Errorf(err.Error())
 		t.Errorf(err.Error())
 		return
 		return
@@ -43,7 +45,7 @@ func TestDialTLS(t *testing.T) {
 
 
 func TestStartTLS(t *testing.T) {
 func TestStartTLS(t *testing.T) {
 	fmt.Printf("TestStartTLS: starting...\n")
 	fmt.Printf("TestStartTLS: starting...\n")
-	l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
+	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
 	if err != nil {
 	if err != nil {
 		t.Errorf(err.Error())
 		t.Errorf(err.Error())
 		return
 		return
@@ -58,16 +60,16 @@ func TestStartTLS(t *testing.T) {
 
 
 func TestSearch(t *testing.T) {
 func TestSearch(t *testing.T) {
 	fmt.Printf("TestSearch: starting...\n")
 	fmt.Printf("TestSearch: starting...\n")
-	l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
+	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
 	if err != nil {
 	if err != nil {
 		t.Errorf(err.Error())
 		t.Errorf(err.Error())
 		return
 		return
 	}
 	}
 	defer l.Close()
 	defer l.Close()
 
 
-	searchRequest := NewSearchRequest(
+	searchRequest := ldap.NewSearchRequest(
 		baseDN,
 		baseDN,
-		ScopeWholeSubtree, DerefAlways, 0, 0, false,
+		ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
 		filter[0],
 		filter[0],
 		attributes,
 		attributes,
 		nil)
 		nil)
@@ -83,16 +85,16 @@ func TestSearch(t *testing.T) {
 
 
 func TestSearchStartTLS(t *testing.T) {
 func TestSearchStartTLS(t *testing.T) {
 	fmt.Printf("TestSearchStartTLS: starting...\n")
 	fmt.Printf("TestSearchStartTLS: starting...\n")
-	l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
+	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
 	if err != nil {
 	if err != nil {
 		t.Errorf(err.Error())
 		t.Errorf(err.Error())
 		return
 		return
 	}
 	}
 	defer l.Close()
 	defer l.Close()
 
 
-	searchRequest := NewSearchRequest(
+	searchRequest := ldap.NewSearchRequest(
 		baseDN,
 		baseDN,
-		ScopeWholeSubtree, DerefAlways, 0, 0, false,
+		ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
 		filter[0],
 		filter[0],
 		attributes,
 		attributes,
 		nil)
 		nil)
@@ -123,7 +125,7 @@ func TestSearchStartTLS(t *testing.T) {
 
 
 func TestSearchWithPaging(t *testing.T) {
 func TestSearchWithPaging(t *testing.T) {
 	fmt.Printf("TestSearchWithPaging: starting...\n")
 	fmt.Printf("TestSearchWithPaging: starting...\n")
-	l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
+	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
 	if err != nil {
 	if err != nil {
 		t.Errorf(err.Error())
 		t.Errorf(err.Error())
 		return
 		return
@@ -136,9 +138,9 @@ func TestSearchWithPaging(t *testing.T) {
 		return
 		return
 	}
 	}
 
 
-	searchRequest := NewSearchRequest(
+	searchRequest := ldap.NewSearchRequest(
 		baseDN,
 		baseDN,
-		ScopeWholeSubtree, DerefAlways, 0, 0, false,
+		ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
 		filter[2],
 		filter[2],
 		attributes,
 		attributes,
 		nil)
 		nil)
@@ -149,12 +151,38 @@ func TestSearchWithPaging(t *testing.T) {
 	}
 	}
 
 
 	fmt.Printf("TestSearchWithPaging: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries))
 	fmt.Printf("TestSearchWithPaging: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries))
+
+	searchRequest = ldap.NewSearchRequest(
+		baseDN,
+		ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
+		filter[2],
+		attributes,
+		[]ldap.Control{ldap.NewControlPaging(5)})
+	sr, err = l.SearchWithPaging(searchRequest, 5)
+	if err != nil {
+		t.Errorf(err.Error())
+		return
+	}
+
+	fmt.Printf("TestSearchWithPaging: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries))
+
+	searchRequest = ldap.NewSearchRequest(
+		baseDN,
+		ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
+		filter[2],
+		attributes,
+		[]ldap.Control{ldap.NewControlPaging(500)})
+	sr, err = l.SearchWithPaging(searchRequest, 5)
+	if err == nil {
+		t.Errorf("expected an error when paging size in control in search request doesn't match size given in call, got none")
+		return
+	}
 }
 }
 
 
-func searchGoroutine(t *testing.T, l *Conn, results chan *SearchResult, i int) {
-	searchRequest := NewSearchRequest(
+func searchGoroutine(t *testing.T, l *ldap.Conn, results chan *ldap.SearchResult, i int) {
+	searchRequest := ldap.NewSearchRequest(
 		baseDN,
 		baseDN,
-		ScopeWholeSubtree, DerefAlways, 0, 0, false,
+		ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
 		filter[i],
 		filter[i],
 		attributes,
 		attributes,
 		nil)
 		nil)
@@ -169,17 +197,17 @@ func searchGoroutine(t *testing.T, l *Conn, results chan *SearchResult, i int) {
 
 
 func testMultiGoroutineSearch(t *testing.T, TLS bool, startTLS bool) {
 func testMultiGoroutineSearch(t *testing.T, TLS bool, startTLS bool) {
 	fmt.Printf("TestMultiGoroutineSearch: starting...\n")
 	fmt.Printf("TestMultiGoroutineSearch: starting...\n")
-	var l *Conn
+	var l *ldap.Conn
 	var err error
 	var err error
 	if TLS {
 	if TLS {
-		l, err = DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true})
+		l, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true})
 		if err != nil {
 		if err != nil {
 			t.Errorf(err.Error())
 			t.Errorf(err.Error())
 			return
 			return
 		}
 		}
 		defer l.Close()
 		defer l.Close()
 	} else {
 	} else {
-		l, err = Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
+		l, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
 		if err != nil {
 		if err != nil {
 			t.Errorf(err.Error())
 			t.Errorf(err.Error())
 			return
 			return
@@ -195,9 +223,9 @@ func testMultiGoroutineSearch(t *testing.T, TLS bool, startTLS bool) {
 		}
 		}
 	}
 	}
 
 
-	results := make([]chan *SearchResult, len(filter))
+	results := make([]chan *ldap.SearchResult, len(filter))
 	for i := range filter {
 	for i := range filter {
-		results[i] = make(chan *SearchResult)
+		results[i] = make(chan *ldap.SearchResult)
 		go searchGoroutine(t, l, results[i], i)
 		go searchGoroutine(t, l, results[i], i)
 	}
 	}
 	for i := range filter {
 	for i := range filter {
@@ -217,17 +245,17 @@ func TestMultiGoroutineSearch(t *testing.T) {
 }
 }
 
 
 func TestEscapeFilter(t *testing.T) {
 func TestEscapeFilter(t *testing.T) {
-	if got, want := EscapeFilter("a\x00b(c)d*e\\f"), `a\00b\28c\29d\2ae\5cf`; got != want {
+	if got, want := ldap.EscapeFilter("a\x00b(c)d*e\\f"), `a\00b\28c\29d\2ae\5cf`; got != want {
 		t.Errorf("Got %s, expected %s", want, got)
 		t.Errorf("Got %s, expected %s", want, got)
 	}
 	}
-	if got, want := EscapeFilter("Lučić"), `Lu\c4\8di\c4\87`; got != want {
+	if got, want := ldap.EscapeFilter("Lučić"), `Lu\c4\8di\c4\87`; got != want {
 		t.Errorf("Got %s, expected %s", want, got)
 		t.Errorf("Got %s, expected %s", want, got)
 	}
 	}
 }
 }
 
 
 func TestCompare(t *testing.T) {
 func TestCompare(t *testing.T) {
 	fmt.Printf("TestCompare: starting...\n")
 	fmt.Printf("TestCompare: starting...\n")
-	l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
+	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
 	if err != nil {
 	if err != nil {
 		t.Fatal(err.Error())
 		t.Fatal(err.Error())
 	}
 	}
@@ -243,5 +271,5 @@ func TestCompare(t *testing.T) {
 		return
 		return
 	}
 	}
 
 
-	fmt.Printf("TestCompare: -> num of entries = %d\n", sr)
+	fmt.Printf("TestCompare: -> %v\n", sr)
 }
 }

+ 57 - 4
Godeps/_workspace/src/github.com/go-ldap/ldap/search.go

@@ -62,6 +62,7 @@ package ldap
 import (
 import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"sort"
 	"strings"
 	"strings"
 
 
 	"gopkg.in/asn1-ber.v1"
 	"gopkg.in/asn1-ber.v1"
@@ -93,6 +94,26 @@ var DerefMap = map[int]string{
 	DerefAlways:         "DerefAlways",
 	DerefAlways:         "DerefAlways",
 }
 }
 
 
+// NewEntry returns an Entry object with the specified distinguished name and attribute key-value pairs.
+// The map of attributes is accessed in alphabetical order of the keys in order to ensure that, for the
+// same input map of attributes, the output entry will contain the same order of attributes
+func NewEntry(dn string, attributes map[string][]string) *Entry {
+	var attributeNames []string
+	for attributeName := range attributes {
+		attributeNames = append(attributeNames, attributeName)
+	}
+	sort.Strings(attributeNames)
+
+	var encodedAttributes []*EntryAttribute
+	for _, attributeName := range attributeNames {
+		encodedAttributes = append(encodedAttributes, NewEntryAttribute(attributeName, attributes[attributeName]))
+	}
+	return &Entry{
+		DN:         dn,
+		Attributes: encodedAttributes,
+	}
+}
+
 type Entry struct {
 type Entry struct {
 	DN         string
 	DN         string
 	Attributes []*EntryAttribute
 	Attributes []*EntryAttribute
@@ -146,6 +167,19 @@ func (e *Entry) PrettyPrint(indent int) {
 	}
 	}
 }
 }
 
 
+// NewEntryAttribute returns a new EntryAttribute with the desired key-value pair
+func NewEntryAttribute(name string, values []string) *EntryAttribute {
+	var bytes [][]byte
+	for _, value := range values {
+		bytes = append(bytes, []byte(value))
+	}
+	return &EntryAttribute{
+		Name:       name,
+		Values:     values,
+		ByteValues: bytes,
+	}
+}
+
 type EntryAttribute struct {
 type EntryAttribute struct {
 	Name       string
 	Name       string
 	Values     []string
 	Values     []string
@@ -234,13 +268,32 @@ func NewSearchRequest(
 	}
 	}
 }
 }
 
 
+// SearchWithPaging accepts a search request and desired page size in order to execute LDAP queries to fulfill the
+// search request. All paged LDAP query responses will be buffered and the final result will be returned atomically.
+// The following four cases are possible given the arguments:
+//  - given SearchRequest missing a control of type ControlTypePaging: we will add one with the desired paging size
+//  - given SearchRequest contains a control of type ControlTypePaging that isn't actually a ControlPaging: fail without issuing any queries
+//  - given SearchRequest contains a control of type ControlTypePaging with pagingSize equal to the size requested: no change to the search request
+//  - given SearchRequest contains a control of type ControlTypePaging with pagingSize not equal to the size requested: fail without issuing any queries
+// A requested pagingSize of 0 is interpreted as no limit by LDAP servers.
 func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) {
 func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) {
-	if searchRequest.Controls == nil {
-		searchRequest.Controls = make([]Control, 0)
+	var pagingControl *ControlPaging
+
+	control := FindControl(searchRequest.Controls, ControlTypePaging)
+	if control == nil {
+		pagingControl = NewControlPaging(pagingSize)
+		searchRequest.Controls = append(searchRequest.Controls, pagingControl)
+	} else {
+		castControl, ok := control.(*ControlPaging)
+		if !ok {
+			return nil, fmt.Errorf("Expected paging control to be of type *ControlPaging, got %v", control)
+		}
+		if castControl.PagingSize != pagingSize {
+			return nil, fmt.Errorf("Paging size given in search request (%d) conflicts with size given in search call (%d)", castControl.PagingSize, pagingSize)
+		}
+		pagingControl = castControl
 	}
 	}
 
 
-	pagingControl := NewControlPaging(pagingSize)
-	searchRequest.Controls = append(searchRequest.Controls, pagingControl)
 	searchResult := new(SearchResult)
 	searchResult := new(SearchResult)
 	for {
 	for {
 		result, err := l.Search(searchRequest)
 		result, err := l.Search(searchRequest)

+ 31 - 0
Godeps/_workspace/src/github.com/go-ldap/ldap/search_test.go

@@ -0,0 +1,31 @@
+package ldap
+
+import (
+	"reflect"
+	"testing"
+)
+
+// TestNewEntry tests that repeated calls to NewEntry return the same value with the same input
+func TestNewEntry(t *testing.T) {
+	dn := "testDN"
+	attributes := map[string][]string{
+		"alpha":   {"value"},
+		"beta":    {"value"},
+		"gamma":   {"value"},
+		"delta":   {"value"},
+		"epsilon": {"value"},
+	}
+	exectedEntry := NewEntry(dn, attributes)
+
+	iteration := 0
+	for {
+		if iteration == 100 {
+			break
+		}
+		testEntry := NewEntry(dn, attributes)
+		if !reflect.DeepEqual(exectedEntry, testEntry) {
+			t.Fatalf("consequent calls to NewEntry did not yield the same result:\n\texpected:\n\t%s\n\tgot:\n\t%s\n", exectedEntry, testEntry)
+		}
+		iteration = iteration + 1
+	}
+}

+ 23 - 0
conf/ldap.toml

@@ -28,8 +28,31 @@ search_base_dns = ["dc=grafana,dc=org"]
 # This is done by enabling group_search_filter below. You must also set member_of= "cn"
 # This is done by enabling group_search_filter below. You must also set member_of= "cn"
 # in [servers.attributes] below.
 # in [servers.attributes] below.
 
 
+# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN
+# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of
+# below in such a way that the user's recursive group membership is considered.
+#
+# Nested Groups + Active Directory (AD) Example:
+#
+#   AD groups store the Distinguished Names (DNs) of members, so your filter must
+#   recursively search your groups for the authenticating user's DN. For example:
+#
+#     group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)"
+#     group_search_filter_user_attribute = "distinguishedName"
+#     group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
+#
+#     [servers.attributes]
+#     ...
+#     member_of = "distinguishedName"
+
 ## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
 ## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
 # group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
 # group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
+## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter.
+## Defaults to the value of username in [server.attributes]
+## Valid options are any of your values in [servers.attributes]
+## If you are using nested groups you probably want to set this and member_of in
+## [servers.attributes] to "distinguishedName"
+# group_search_filter_user_attribute = "distinguishedName"
 ## An array of the base DNs to search through for groups. Typically uses ou=groups
 ## An array of the base DNs to search through for groups. Typically uses ou=groups
 # group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
 # group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
 
 

+ 6 - 1
pkg/login/ldap.go

@@ -318,7 +318,12 @@ func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) {
 		// If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups
 		// If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups
 		var groupSearchResult *ldap.SearchResult
 		var groupSearchResult *ldap.SearchResult
 		for _, groupSearchBase := range a.server.GroupSearchBaseDNs {
 		for _, groupSearchBase := range a.server.GroupSearchBaseDNs {
-			filter := strings.Replace(a.server.GroupSearchFilter, "%s", username, -1)
+			var filter_replace string
+			filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
+			if a.server.GroupSearchFilterUserAttribute == "" {
+				filter_replace = getLdapAttr(a.server.Attr.Username, searchResult)
+			}
+			filter := strings.Replace(a.server.GroupSearchFilter, "%s", filter_replace, -1)
 
 
 			if ldapCfg.VerboseLogging {
 			if ldapCfg.VerboseLogging {
 				log.Info("LDAP: Searching for user's groups: %s", filter)
 				log.Info("LDAP: Searching for user's groups: %s", filter)

+ 3 - 2
pkg/login/settings.go

@@ -27,8 +27,9 @@ type LdapServerConf struct {
 	SearchFilter  string   `toml:"search_filter"`
 	SearchFilter  string   `toml:"search_filter"`
 	SearchBaseDNs []string `toml:"search_base_dns"`
 	SearchBaseDNs []string `toml:"search_base_dns"`
 
 
-	GroupSearchFilter  string   `toml:"group_search_filter"`
-	GroupSearchBaseDNs []string `toml:"group_search_base_dns"`
+	GroupSearchFilter              string   `toml:"group_search_filter"`
+	GroupSearchFilterUserAttribute string   `toml:"group_search_filter_user_attribute"`
+	GroupSearchBaseDNs             []string `toml:"group_search_base_dns"`
 
 
 	LdapGroups []*LdapGroupToOrgRole `toml:"group_mappings"`
 	LdapGroups []*LdapGroupToOrgRole `toml:"group_mappings"`
 }
 }