diff --git a/LICENSE b/LICENSE index 6c0ed4b..ef07293 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ The MIT License (MIT) Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com) -Portions copyright (c) 2015-2016 go-ldap Authors +Portions copyright (c) 2015-2024 go-ldap Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 06a9a17..5d7f2fb 100644 --- a/README.md +++ b/README.md @@ -3,63 +3,50 @@ # Basic LDAP v3 functionality for the GO programming language. The library implements the following specifications: - - https://datatracker.ietf.org/doc/html/rfc4511 for basic operations - - https://datatracker.ietf.org/doc/html/rfc3062 for password modify operation - - https://datatracker.ietf.org/doc/html/rfc4514 for distinguished names parsing - - https://datatracker.ietf.org/doc/html/rfc4533 for Content Synchronization Operation - - https://datatracker.ietf.org/doc/html/draft-armijo-ldap-treedelete-02 for Tree Delete Control - - https://datatracker.ietf.org/doc/html/rfc2891 for Server Side Sorting of Search Results - - https://datatracker.ietf.org/doc/html/rfc4532 for WhoAmI requests + +- https://datatracker.ietf.org/doc/html/rfc4511 for basic operations +- https://datatracker.ietf.org/doc/html/rfc3062 for password modify operation +- https://datatracker.ietf.org/doc/html/rfc4514 for distinguished names parsing +- https://datatracker.ietf.org/doc/html/rfc4533 for Content Synchronization Operation +- https://datatracker.ietf.org/doc/html/draft-armijo-ldap-treedelete-02 for Tree Delete Control +- https://datatracker.ietf.org/doc/html/rfc2891 for Server Side Sorting of Search Results +- https://datatracker.ietf.org/doc/html/rfc4532 for WhoAmI requests ## Features: - - Connecting to LDAP server (non-TLS, TLS, STARTTLS, through a custom dialer) - - Binding to LDAP server (Simple Bind, GSSAPI, SASL) - - "Who Am I" Requests / Responses - - Searching for entries (normal and asynchronous) - - Filter Compile / Decompile - - Paging Search Results - - Modify Requests / Responses - - Add Requests / Responses - - Delete Requests / Responses - - Modify DN Requests / Responses +- Connecting to LDAP server (non-TLS, TLS, STARTTLS, through a custom dialer) +- Binding to LDAP server (Simple Bind, GSSAPI, SASL) +- "Who Am I" Requests / Responses +- Searching for entries (normal and asynchronous) +- Filter Compile / Decompile +- Paging Search Results +- Modify Requests / Responses +- Add Requests / Responses +- Delete Requests / Responses +- Modify DN Requests / Responses ## Go Modules: `go get github.com/go-ldap/ldap/v3` -As go-ldap was v2+ when Go Modules came out, updating to Go Modules would be considered a breaking change. - -To maintain backwards compatability, we ultimately decided to use subfolders (as v3 was already a branch). -Whilst this duplicates the code, we can move toward implementing a backwards-compatible versioning system that allows for code reuse. -The alternative would be to increment the version number, however we believe that this would confuse users as v3 is in line with LDAPv3 (RFC-4511) -https://tools.ietf.org/html/rfc4511 - - -For more info, please visit the pull request that updated to modules. -https://github.com/go-ldap/ldap/pull/247 - -To install with `GOMODULE111=off`, use `go get github.com/go-ldap/ldap` -https://golang.org/cmd/go/#hdr-Legacy_GOPATH_go_get - -As always, we are looking for contributors with great ideas on how to best move forward. - - ## Contributing: Bug reports and pull requests are welcome! Before submitting a pull request, please make sure tests and verification scripts pass: + ``` make all ``` To set up a pre-push hook to run the tests and verify scripts before pushing: + ``` ln -s ../../.githooks/pre-push .git/hooks/pre-push ``` --- + The Go gopher was designed by Renee French. (http://reneefrench.blogspot.com/) The design is licensed under the Creative Commons 3.0 Attributions license. Read this article for more details: http://blog.golang.org/gopher diff --git a/add.go b/add.go deleted file mode 100644 index ab32b0b..0000000 --- a/add.go +++ /dev/null @@ -1,89 +0,0 @@ -package ldap - -import ( - "fmt" - ber "github.com/go-asn1-ber/asn1-ber" -) - -// Attribute represents an LDAP attribute -type Attribute struct { - // Type is the name of the LDAP attribute - Type string - // Vals are the LDAP attribute values - Vals []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.Type, "Type")) - set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") - for _, value := range a.Vals { - set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) - } - seq.AppendChild(set) - return seq -} - -// AddRequest represents an LDAP AddRequest operation -type AddRequest struct { - // DN identifies the entry being added - DN string - // Attributes list the attributes of the new entry - Attributes []Attribute - // Controls hold optional controls to send with the request - Controls []Control -} - -func (req *AddRequest) appendTo(envelope *ber.Packet) error { - pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationAddRequest, nil, "Add Request") - pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.DN, "DN")) - attributes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes") - for _, attribute := range req.Attributes { - attributes.AppendChild(attribute.encode()) - } - pkt.AppendChild(attributes) - - envelope.AppendChild(pkt) - if len(req.Controls) > 0 { - envelope.AppendChild(encodeControls(req.Controls)) - } - - return nil -} - -// Attribute adds an attribute with the given type and values -func (req *AddRequest) Attribute(attrType string, attrVals []string) { - req.Attributes = append(req.Attributes, Attribute{Type: attrType, Vals: attrVals}) -} - -// NewAddRequest returns an AddRequest for the given DN, with no attributes -func NewAddRequest(dn string, controls []Control) *AddRequest { - return &AddRequest{ - DN: dn, - Controls: controls, - } -} - -// Add performs the given AddRequest -func (l *Conn) Add(addRequest *AddRequest) error { - msgCtx, err := l.doRequest(addRequest) - if err != nil { - return err - } - defer l.finishMessage(msgCtx) - - packet, err := l.readPacket(msgCtx) - if err != nil { - return err - } - - if packet.Children[1].Tag == ApplicationAddResponse { - err := GetLDAPError(packet) - if err != nil { - return err - } - } else { - return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) - } - return nil -} diff --git a/bind.go b/bind.go deleted file mode 100644 index a37f8e2..0000000 --- a/bind.go +++ /dev/null @@ -1,735 +0,0 @@ -package ldap - -import ( - "bytes" - "crypto/md5" - enchex "encoding/hex" - "errors" - "fmt" - "io/ioutil" - "math/rand" - "strings" - - "github.com/Azure/go-ntlmssp" - ber "github.com/go-asn1-ber/asn1-ber" -) - -// SimpleBindRequest represents a username/password bind operation -type SimpleBindRequest struct { - // Username is the name of the Directory object that the client wishes to bind as - Username string - // Password is the credentials to bind with - Password string - // Controls are optional controls to send with the bind request - Controls []Control - // AllowEmptyPassword sets whether the client allows binding with an empty password - // (normally used for unauthenticated bind). - AllowEmptyPassword bool -} - -// SimpleBindResult contains the response from the server -type SimpleBindResult struct { - Controls []Control -} - -// NewSimpleBindRequest returns a bind request -func NewSimpleBindRequest(username string, password string, controls []Control) *SimpleBindRequest { - return &SimpleBindRequest{ - Username: username, - Password: password, - Controls: controls, - AllowEmptyPassword: false, - } -} - -func (req *SimpleBindRequest) appendTo(envelope *ber.Packet) error { - pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") - pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) - pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.Username, "User Name")) - pkt.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, req.Password, "Password")) - - envelope.AppendChild(pkt) - if len(req.Controls) > 0 { - envelope.AppendChild(encodeControls(req.Controls)) - } - - return nil -} - -// SimpleBind performs the simple bind operation defined in the given request -func (l *Conn) SimpleBind(simpleBindRequest *SimpleBindRequest) (*SimpleBindResult, error) { - if simpleBindRequest.Password == "" && !simpleBindRequest.AllowEmptyPassword { - return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client")) - } - - msgCtx, err := l.doRequest(simpleBindRequest) - if err != nil { - return nil, err - } - defer l.finishMessage(msgCtx) - - packet, err := l.readPacket(msgCtx) - if err != nil { - return nil, err - } - - result := &SimpleBindResult{ - Controls: make([]Control, 0), - } - - if len(packet.Children) == 3 { - for _, child := range packet.Children[2].Children { - decodedChild, decodeErr := DecodeControl(child) - if decodeErr != nil { - return nil, fmt.Errorf("failed to decode child control: %s", decodeErr) - } - result.Controls = append(result.Controls, decodedChild) - } - } - - err = GetLDAPError(packet) - return result, err -} - -// Bind performs a bind with the given username and password. -// -// It does not allow unauthenticated bind (i.e. empty password). Use the UnauthenticatedBind method -// for that. -func (l *Conn) Bind(username, password string) error { - req := &SimpleBindRequest{ - Username: username, - Password: password, - AllowEmptyPassword: false, - } - _, err := l.SimpleBind(req) - return err -} - -// UnauthenticatedBind performs an unauthenticated bind. -// -// A username may be provided for trace (e.g. logging) purpose only, but it is normally not -// authenticated or otherwise validated by the LDAP server. -// -// See https://tools.ietf.org/html/rfc4513#section-5.1.2 . -// See https://tools.ietf.org/html/rfc4513#section-6.3.1 . -func (l *Conn) UnauthenticatedBind(username string) error { - req := &SimpleBindRequest{ - Username: username, - Password: "", - AllowEmptyPassword: true, - } - _, err := l.SimpleBind(req) - return err -} - -// DigestMD5BindRequest represents a digest-md5 bind operation -type DigestMD5BindRequest struct { - Host string - // Username is the name of the Directory object that the client wishes to bind as - Username string - // Password is the credentials to bind with - Password string - // Controls are optional controls to send with the bind request - Controls []Control -} - -func (req *DigestMD5BindRequest) appendTo(envelope *ber.Packet) error { - request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") - request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) - request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) - - auth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication") - auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "DIGEST-MD5", "SASL Mech")) - request.AppendChild(auth) - envelope.AppendChild(request) - if len(req.Controls) > 0 { - envelope.AppendChild(encodeControls(req.Controls)) - } - return nil -} - -// DigestMD5BindResult contains the response from the server -type DigestMD5BindResult struct { - Controls []Control -} - -// MD5Bind performs a digest-md5 bind with the given host, username and password. -func (l *Conn) MD5Bind(host, username, password string) error { - req := &DigestMD5BindRequest{ - Host: host, - Username: username, - Password: password, - } - _, err := l.DigestMD5Bind(req) - return err -} - -// DigestMD5Bind performs the digest-md5 bind operation defined in the given request -func (l *Conn) DigestMD5Bind(digestMD5BindRequest *DigestMD5BindRequest) (*DigestMD5BindResult, error) { - if digestMD5BindRequest.Password == "" { - return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client")) - } - - msgCtx, err := l.doRequest(digestMD5BindRequest) - if err != nil { - return nil, err - } - defer l.finishMessage(msgCtx) - - packet, err := l.readPacket(msgCtx) - if err != nil { - return nil, err - } - l.Debug.Printf("%d: got response %p", msgCtx.id, packet) - if l.Debug { - if err = addLDAPDescriptions(packet); err != nil { - return nil, err - } - ber.PrintPacket(packet) - } - - result := &DigestMD5BindResult{ - Controls: make([]Control, 0), - } - var params map[string]string - if len(packet.Children) == 2 { - if len(packet.Children[1].Children) == 4 { - child := packet.Children[1].Children[0] - if child.Tag != ber.TagEnumerated { - return result, GetLDAPError(packet) - } - if child.Value.(int64) != 14 { - return result, GetLDAPError(packet) - } - child = packet.Children[1].Children[3] - if child.Tag != ber.TagObjectDescriptor { - return result, GetLDAPError(packet) - } - if child.Data == nil { - return result, GetLDAPError(packet) - } - data, _ := ioutil.ReadAll(child.Data) - params, err = parseParams(string(data)) - if err != nil { - return result, fmt.Errorf("parsing digest-challenge: %s", err) - } - } - } - - if params != nil { - resp := computeResponse( - params, - "ldap/"+strings.ToLower(digestMD5BindRequest.Host), - digestMD5BindRequest.Username, - digestMD5BindRequest.Password, - ) - packet = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") - packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) - - request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") - request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) - request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) - - auth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication") - auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "DIGEST-MD5", "SASL Mech")) - auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, resp, "Credentials")) - request.AppendChild(auth) - packet.AppendChild(request) - msgCtx, err = l.sendMessage(packet) - if err != nil { - return nil, fmt.Errorf("send message: %s", err) - } - defer l.finishMessage(msgCtx) - packetResponse, ok := <-msgCtx.responses - if !ok { - return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) - } - packet, err = packetResponse.ReadPacket() - l.Debug.Printf("%d: got response %p", msgCtx.id, packet) - if err != nil { - return nil, fmt.Errorf("read packet: %s", err) - } - } - - err = GetLDAPError(packet) - return result, err -} - -func parseParams(str string) (map[string]string, error) { - m := make(map[string]string) - var key, value string - var state int - for i := 0; i <= len(str); i++ { - switch state { - case 0: // reading key - if i == len(str) { - return nil, fmt.Errorf("syntax error on %d", i) - } - if str[i] != '=' { - key += string(str[i]) - continue - } - state = 1 - case 1: // reading value - if i == len(str) { - m[key] = value - break - } - switch str[i] { - case ',': - m[key] = value - state = 0 - key = "" - value = "" - case '"': - if value != "" { - return nil, fmt.Errorf("syntax error on %d", i) - } - state = 2 - default: - value += string(str[i]) - } - case 2: // inside quotes - if i == len(str) { - return nil, fmt.Errorf("syntax error on %d", i) - } - if str[i] != '"' { - value += string(str[i]) - } else { - state = 1 - } - } - } - return m, nil -} - -func computeResponse(params map[string]string, uri, username, password string) string { - nc := "00000001" - qop := "auth" - cnonce := enchex.EncodeToString(randomBytes(16)) - x := username + ":" + params["realm"] + ":" + password - y := md5Hash([]byte(x)) - - a1 := bytes.NewBuffer(y) - a1.WriteString(":" + params["nonce"] + ":" + cnonce) - if len(params["authzid"]) > 0 { - a1.WriteString(":" + params["authzid"]) - } - a2 := bytes.NewBuffer([]byte("AUTHENTICATE")) - a2.WriteString(":" + uri) - ha1 := enchex.EncodeToString(md5Hash(a1.Bytes())) - ha2 := enchex.EncodeToString(md5Hash(a2.Bytes())) - - kd := ha1 - kd += ":" + params["nonce"] - kd += ":" + nc - kd += ":" + cnonce - kd += ":" + qop - kd += ":" + ha2 - resp := enchex.EncodeToString(md5Hash([]byte(kd))) - return fmt.Sprintf( - `username="%s",realm="%s",nonce="%s",cnonce="%s",nc=00000001,qop=%s,digest-uri="%s",response=%s`, - username, - params["realm"], - params["nonce"], - cnonce, - qop, - uri, - resp, - ) -} - -func md5Hash(b []byte) []byte { - hasher := md5.New() - hasher.Write(b) - return hasher.Sum(nil) -} - -func randomBytes(len int) []byte { - b := make([]byte, len) - for i := 0; i < len; i++ { - b[i] = byte(rand.Intn(256)) - } - return b -} - -var externalBindRequest = requestFunc(func(envelope *ber.Packet) error { - pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") - pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) - pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) - - saslAuth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication") - saslAuth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "EXTERNAL", "SASL Mech")) - saslAuth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "SASL Cred")) - - pkt.AppendChild(saslAuth) - - envelope.AppendChild(pkt) - - return nil -}) - -// ExternalBind performs SASL/EXTERNAL authentication. -// -// Use ldap.DialURL("ldapi://") to connect to the Unix socket before ExternalBind. -// -// See https://tools.ietf.org/html/rfc4422#appendix-A -func (l *Conn) ExternalBind() error { - msgCtx, err := l.doRequest(externalBindRequest) - if err != nil { - return err - } - defer l.finishMessage(msgCtx) - - packet, err := l.readPacket(msgCtx) - if err != nil { - return err - } - - return GetLDAPError(packet) -} - -// NTLMBind performs an NTLMSSP bind leveraging https://github.com/Azure/go-ntlmssp - -// NTLMBindRequest represents an NTLMSSP bind operation -type NTLMBindRequest struct { - // Domain is the AD Domain to authenticate too. If not specified, it will be grabbed from the NTLMSSP Challenge - Domain string - // Username is the name of the Directory object that the client wishes to bind as - Username string - // Password is the credentials to bind with - Password string - // AllowEmptyPassword sets whether the client allows binding with an empty password - // (normally used for unauthenticated bind). - AllowEmptyPassword bool - // Hash is the hex NTLM hash to bind with. Password or hash must be provided - Hash string - // Controls are optional controls to send with the bind request - Controls []Control -} - -func (req *NTLMBindRequest) appendTo(envelope *ber.Packet) error { - request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") - request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) - request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) - - // generate an NTLMSSP Negotiation message for the specified domain (it can be blank) - negMessage, err := ntlmssp.NewNegotiateMessage(req.Domain, "") - if err != nil { - return fmt.Errorf("err creating negmessage: %s", err) - } - - // append the generated NTLMSSP message as a TagEnumerated BER value - auth := ber.Encode(ber.ClassContext, ber.TypePrimitive, ber.TagEnumerated, negMessage, "authentication") - request.AppendChild(auth) - envelope.AppendChild(request) - if len(req.Controls) > 0 { - envelope.AppendChild(encodeControls(req.Controls)) - } - return nil -} - -// NTLMBindResult contains the response from the server -type NTLMBindResult struct { - Controls []Control -} - -// NTLMBind performs an NTLMSSP Bind with the given domain, username and password -func (l *Conn) NTLMBind(domain, username, password string) error { - req := &NTLMBindRequest{ - Domain: domain, - Username: username, - Password: password, - } - _, err := l.NTLMChallengeBind(req) - return err -} - -// NTLMUnauthenticatedBind performs an bind with an empty password. -// -// A username is required. The anonymous bind is not (yet) supported by the go-ntlmssp library (https://github.com/Azure/go-ntlmssp/blob/819c794454d067543bc61d29f61fef4b3c3df62c/authenticate_message.go#L87) -// -// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 part 3.2.5.1.2 -func (l *Conn) NTLMUnauthenticatedBind(domain, username string) error { - req := &NTLMBindRequest{ - Domain: domain, - Username: username, - Password: "", - AllowEmptyPassword: true, - } - _, err := l.NTLMChallengeBind(req) - return err -} - -// NTLMBindWithHash performs an NTLM Bind with an NTLM hash instead of plaintext password (pass-the-hash) -func (l *Conn) NTLMBindWithHash(domain, username, hash string) error { - req := &NTLMBindRequest{ - Domain: domain, - Username: username, - Hash: hash, - } - _, err := l.NTLMChallengeBind(req) - return err -} - -// NTLMChallengeBind performs the NTLMSSP bind operation defined in the given request -func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindResult, error) { - if !ntlmBindRequest.AllowEmptyPassword && ntlmBindRequest.Password == "" && ntlmBindRequest.Hash == "" { - return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client")) - } - - msgCtx, err := l.doRequest(ntlmBindRequest) - if err != nil { - return nil, err - } - defer l.finishMessage(msgCtx) - packet, err := l.readPacket(msgCtx) - if err != nil { - return nil, err - } - l.Debug.Printf("%d: got response %p", msgCtx.id, packet) - if l.Debug { - if err = addLDAPDescriptions(packet); err != nil { - return nil, err - } - ber.PrintPacket(packet) - } - result := &NTLMBindResult{ - Controls: make([]Control, 0), - } - var ntlmsspChallenge []byte - - // now find the NTLM Response Message - if len(packet.Children) == 2 { - if len(packet.Children[1].Children) == 3 { - child := packet.Children[1].Children[1] - ntlmsspChallenge = child.ByteValue - // Check to make sure we got the right message. It will always start with NTLMSSP - if len(ntlmsspChallenge) < 7 || !bytes.Equal(ntlmsspChallenge[:7], []byte("NTLMSSP")) { - return result, GetLDAPError(packet) - } - l.Debug.Printf("%d: found ntlmssp challenge", msgCtx.id) - } - } - if ntlmsspChallenge != nil { - var err error - var responseMessage []byte - // generate a response message to the challenge with the given Username/Password if password is provided - if ntlmBindRequest.Hash != "" { - responseMessage, err = ntlmssp.ProcessChallengeWithHash(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Hash) - } else if ntlmBindRequest.Password != "" || ntlmBindRequest.AllowEmptyPassword { - _, _, domainNeeded := ntlmssp.GetDomain(ntlmBindRequest.Username) - responseMessage, err = ntlmssp.ProcessChallenge(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Password, domainNeeded) - } else { - err = fmt.Errorf("need a password or hash to generate reply") - } - if err != nil { - return result, fmt.Errorf("parsing ntlm-challenge: %s", err) - } - packet = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") - packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) - - request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") - request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) - request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) - - // append the challenge response message as a TagEmbeddedPDV BER value - auth := ber.Encode(ber.ClassContext, ber.TypePrimitive, ber.TagEmbeddedPDV, responseMessage, "authentication") - - request.AppendChild(auth) - packet.AppendChild(request) - msgCtx, err = l.sendMessage(packet) - if err != nil { - return nil, fmt.Errorf("send message: %s", err) - } - defer l.finishMessage(msgCtx) - packetResponse, ok := <-msgCtx.responses - if !ok { - return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) - } - packet, err = packetResponse.ReadPacket() - l.Debug.Printf("%d: got response %p", msgCtx.id, packet) - if err != nil { - return nil, fmt.Errorf("read packet: %s", err) - } - - } - - err = GetLDAPError(packet) - return result, err -} - -// GSSAPIClient interface is used as the client-side implementation for the -// GSSAPI SASL mechanism. -// Interface inspired by GSSAPIClient from golang.org/x/crypto/ssh -type GSSAPIClient interface { - // InitSecContext initiates the establishment of a security context for - // GSS-API between the client and server. - // Initially the token parameter should be specified as nil. - // The routine may return a outputToken which should be transferred to - // the server, where the server will present it to AcceptSecContext. - // If no token need be sent, InitSecContext will indicate this by setting - // needContinue to false. To complete the context - // establishment, one or more reply tokens may be required from the server; - // if so, InitSecContext will return a needContinue which is true. - // In this case, InitSecContext should be called again when the - // reply token is received from the server, passing the reply token - // to InitSecContext via the token parameters. - // See RFC 4752 section 3.1. - InitSecContext(target string, token []byte) (outputToken []byte, needContinue bool, err error) - // NegotiateSaslAuth performs the last step of the Sasl handshake. - // It takes a token, which, when unwrapped, describes the servers supported - // security layers (first octet) and maximum receive buffer (remaining - // three octets). - // If the received token is unacceptable an error must be returned to abort - // the handshake. - // Outputs a signed token describing the client's selected security layer - // and receive buffer size and optionally an authorization identity. - // The returned token will be sent to the server and the handshake considered - // completed successfully and the server authenticated. - // See RFC 4752 section 3.1. - NegotiateSaslAuth(token []byte, authzid string) ([]byte, error) - // DeleteSecContext destroys any established secure context. - DeleteSecContext() error -} - -// GSSAPIBindRequest represents a GSSAPI SASL mechanism bind request. -// See rfc4752 and rfc4513 section 5.2.1.2. -type GSSAPIBindRequest struct { - // Service Principal Name user for the service ticket. Eg. "ldap/" - ServicePrincipalName string - // (Optional) Authorization entity - AuthZID string - // (Optional) Controls to send with the bind request - Controls []Control -} - -// GSSAPIBind performs the GSSAPI SASL bind using the provided GSSAPI client. -func (l *Conn) GSSAPIBind(client GSSAPIClient, servicePrincipal, authzid string) error { - return l.GSSAPIBindRequest(client, &GSSAPIBindRequest{ - ServicePrincipalName: servicePrincipal, - AuthZID: authzid, - }) -} - -// GSSAPIBindRequest performs the GSSAPI SASL bind using the provided GSSAPI client. -func (l *Conn) GSSAPIBindRequest(client GSSAPIClient, req *GSSAPIBindRequest) error { - //nolint:errcheck - defer client.DeleteSecContext() - - var err error - var reqToken []byte - var recvToken []byte - needInit := true - for { - if needInit { - // Establish secure context between client and server. - reqToken, needInit, err = client.InitSecContext(req.ServicePrincipalName, recvToken) - if err != nil { - return err - } - } else { - // Secure context is set up, perform the last step of SASL handshake. - reqToken, err = client.NegotiateSaslAuth(recvToken, req.AuthZID) - if err != nil { - return err - } - } - // Send Bind request containing the current token and extract the - // token sent by server. - recvToken, err = l.saslBindTokenExchange(req.Controls, reqToken) - if err != nil { - return err - } - - if !needInit && len(recvToken) == 0 { - break - } - } - - return nil -} - -func (l *Conn) saslBindTokenExchange(reqControls []Control, reqToken []byte) ([]byte, error) { - // Construct LDAP Bind request with GSSAPI SASL mechanism. - envelope := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") - envelope.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) - - request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") - request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) - request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name")) - - auth := ber.Encode(ber.ClassContext, ber.TypeConstructed, 3, "", "authentication") - auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "GSSAPI", "SASL Mech")) - if len(reqToken) > 0 { - auth.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, string(reqToken), "Credentials")) - } - request.AppendChild(auth) - envelope.AppendChild(request) - if len(reqControls) > 0 { - envelope.AppendChild(encodeControls(reqControls)) - } - - msgCtx, err := l.sendMessage(envelope) - if err != nil { - return nil, err - } - defer l.finishMessage(msgCtx) - - packet, err := l.readPacket(msgCtx) - if err != nil { - return nil, err - } - l.Debug.Printf("%d: got response %p", msgCtx.id, packet) - if l.Debug { - if err = addLDAPDescriptions(packet); err != nil { - return nil, err - } - ber.PrintPacket(packet) - } - - // https://www.rfc-editor.org/rfc/rfc4511#section-4.1.1 - // packet is an envelope - // child 0 is message id - // child 1 is protocolOp - if len(packet.Children) != 2 { - return nil, fmt.Errorf("bad bind response") - } - - protocolOp := packet.Children[1] -RESP: - switch protocolOp.Description { - case "Bind Response": // Bind Response - // Bind Reponse is an LDAP Response (https://www.rfc-editor.org/rfc/rfc4511#section-4.1.9) - // with an additional optional serverSaslCreds string (https://www.rfc-editor.org/rfc/rfc4511#section-4.2.2) - // child 0 is resultCode - resultCode := protocolOp.Children[0] - if resultCode.Tag != ber.TagEnumerated { - break RESP - } - switch resultCode.Value.(int64) { - case 14: // Sasl bind in progress - if len(protocolOp.Children) < 3 { - break RESP - } - referral := protocolOp.Children[3] - switch referral.Description { - case "Referral": - if referral.ClassType != ber.ClassContext || referral.Tag != ber.TagObjectDescriptor { - break RESP - } - return ioutil.ReadAll(referral.Data) - } - // Optional: - //if len(protocolOp.Children) == 4 { - // serverSaslCreds := protocolOp.Children[4] - //} - case 0: // Success - Bind OK. - // SASL layer in effect (if any) (See https://www.rfc-editor.org/rfc/rfc4513#section-5.2.1.4) - // NOTE: SASL security layers are not supported currently. - return nil, nil - } - } - - return nil, GetLDAPError(packet) -} diff --git a/client.go b/client.go deleted file mode 100644 index ee473fc..0000000 --- a/client.go +++ /dev/null @@ -1,42 +0,0 @@ -package ldap - -import ( - "context" - "crypto/tls" - "time" -) - -// Client knows how to interact with an LDAP server -type Client interface { - Start() - StartTLS(*tls.Config) error - Close() error - GetLastError() error - IsClosing() bool - SetTimeout(time.Duration) - TLSConnectionState() (tls.ConnectionState, bool) - - Bind(username, password string) error - UnauthenticatedBind(username string) error - SimpleBind(*SimpleBindRequest) (*SimpleBindResult, error) - ExternalBind() error - NTLMUnauthenticatedBind(domain, username string) error - Unbind() error - - Add(*AddRequest) error - Del(*DelRequest) error - Modify(*ModifyRequest) error - ModifyDN(*ModifyDNRequest) error - ModifyWithResult(*ModifyRequest) (*ModifyResult, error) - Extended(*ExtendedRequest) (*ExtendedResponse, error) - - Compare(dn, attribute, value string) (bool, error) - PasswordModify(*PasswordModifyRequest) (*PasswordModifyResult, error) - - Search(*SearchRequest) (*SearchResult, error) - SearchAsync(ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response - SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) - DirSync(searchRequest *SearchRequest, flags, maxAttrCount int64, cookie []byte) (*SearchResult, error) - DirSyncAsync(ctx context.Context, searchRequest *SearchRequest, bufferSize int, flags, maxAttrCount int64, cookie []byte) Response - Syncrepl(ctx context.Context, searchRequest *SearchRequest, bufferSize int, mode ControlSyncRequestMode, cookie []byte, reloadHint bool) Response -} diff --git a/compare.go b/compare.go deleted file mode 100644 index a1cd760..0000000 --- a/compare.go +++ /dev/null @@ -1,62 +0,0 @@ -package ldap - -import ( - "fmt" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -// CompareRequest represents an LDAP CompareRequest operation. -type CompareRequest struct { - DN string - Attribute string - Value string -} - -func (req *CompareRequest) appendTo(envelope *ber.Packet) error { - pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationCompareRequest, nil, "Compare Request") - pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.DN, "DN")) - - ava := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "AttributeValueAssertion") - ava.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.Attribute, "AttributeDesc")) - ava.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.Value, "AssertionValue")) - - pkt.AppendChild(ava) - - envelope.AppendChild(pkt) - - return nil -} - -// Compare checks to see if the attribute of the dn matches value. Returns true if it does otherwise -// false with any error that occurs if any. -func (l *Conn) Compare(dn, attribute, value string) (bool, error) { - msgCtx, err := l.doRequest(&CompareRequest{ - DN: dn, - Attribute: attribute, - Value: value, - }) - if err != nil { - return false, err - } - defer l.finishMessage(msgCtx) - - packet, err := l.readPacket(msgCtx) - if err != nil { - return false, err - } - - if packet.Children[1].Tag == ApplicationCompareResponse { - err := GetLDAPError(packet) - - switch { - case IsErrorWithCode(err, LDAPResultCompareTrue): - return true, nil - case IsErrorWithCode(err, LDAPResultCompareFalse): - return false, nil - default: - return false, err - } - } - return false, fmt.Errorf("unexpected Response: %d", packet.Children[1].Tag) -} diff --git a/conn.go b/conn.go deleted file mode 100644 index 05febbc..0000000 --- a/conn.go +++ /dev/null @@ -1,636 +0,0 @@ -package ldap - -import ( - "bufio" - "context" - "crypto/tls" - "errors" - "fmt" - "net" - "net/url" - "sync" - "sync/atomic" - "time" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -const ( - // MessageQuit causes the processMessages loop to exit - MessageQuit = 0 - // MessageRequest sends a request to the server - MessageRequest = 1 - // MessageResponse receives a response from the server - MessageResponse = 2 - // MessageFinish indicates the client considers a particular message ID to be finished - MessageFinish = 3 - // MessageTimeout indicates the client-specified timeout for a particular message ID has been reached - MessageTimeout = 4 -) - -const ( - // DefaultLdapPort default ldap port for pure TCP connection - DefaultLdapPort = "389" - // DefaultLdapsPort default ldap port for SSL connection - DefaultLdapsPort = "636" -) - -// PacketResponse contains the packet or error encountered reading a response -type PacketResponse struct { - // Packet is the packet read from the server - Packet *ber.Packet - // Error is an error encountered while reading - Error error -} - -// ReadPacket returns the packet or an error -func (pr *PacketResponse) ReadPacket() (*ber.Packet, error) { - if (pr == nil) || (pr.Packet == nil && pr.Error == nil) { - return nil, NewError(ErrorNetwork, errors.New("ldap: could not retrieve response")) - } - return pr.Packet, pr.Error -} - -type messageContext struct { - id int64 - // close(done) should only be called from finishMessage() - done chan struct{} - // close(responses) should only be called from processMessages(), and only sent to from sendResponse() - responses chan *PacketResponse -} - -// sendResponse should only be called within the processMessages() loop which -// is also responsible for closing the responses channel. -func (msgCtx *messageContext) sendResponse(packet *PacketResponse, timeout time.Duration) { - timeoutCtx := context.Background() - if timeout > 0 { - var cancelFunc context.CancelFunc - timeoutCtx, cancelFunc = context.WithTimeout(context.Background(), timeout) - defer cancelFunc() - } - select { - case msgCtx.responses <- packet: - // Successfully sent packet to message handler. - case <-msgCtx.done: - // The request handler is done and will not receive more - // packets. - case <-timeoutCtx.Done(): - // The timeout was reached before the packet was sent. - } -} - -type messagePacket struct { - Op int - MessageID int64 - Packet *ber.Packet - Context *messageContext -} - -type sendMessageFlags uint - -const ( - startTLS sendMessageFlags = 1 << iota -) - -// Conn represents an LDAP Connection -type Conn struct { - // requestTimeout is loaded atomically - // so we need to ensure 64-bit alignment on 32-bit platforms. - // https://github.com/go-ldap/ldap/pull/199 - requestTimeout int64 - conn net.Conn - isTLS bool - closing uint32 - closeErr atomic.Value - isStartingTLS bool - Debug debugging - chanConfirm chan struct{} - messageContexts map[int64]*messageContext - chanMessage chan *messagePacket - chanMessageID chan int64 - wgClose sync.WaitGroup - outstandingRequests uint - messageMutex sync.Mutex - - err error -} - -var _ Client = &Conn{} - -// DefaultTimeout is a package-level variable that sets the timeout value -// used for the Dial and DialTLS methods. -// -// WARNING: since this is a package-level variable, setting this value from -// multiple places will probably result in undesired behaviour. -var DefaultTimeout = 60 * time.Second - -// DialOpt configures DialContext. -type DialOpt func(*DialContext) - -// DialWithDialer updates net.Dialer in DialContext. -func DialWithDialer(d *net.Dialer) DialOpt { - return func(dc *DialContext) { - dc.dialer = d - } -} - -// DialWithTLSConfig updates tls.Config in DialContext. -func DialWithTLSConfig(tc *tls.Config) DialOpt { - return func(dc *DialContext) { - dc.tlsConfig = tc - } -} - -// DialWithTLSDialer is a wrapper for DialWithTLSConfig with the option to -// specify a net.Dialer to for example define a timeout or a custom resolver. -// -// Deprecated: Use DialWithDialer and DialWithTLSConfig instead -func DialWithTLSDialer(tlsConfig *tls.Config, dialer *net.Dialer) DialOpt { - return func(dc *DialContext) { - dc.tlsConfig = tlsConfig - dc.dialer = dialer - } -} - -// DialContext contains necessary parameters to dial the given ldap URL. -type DialContext struct { - dialer *net.Dialer - tlsConfig *tls.Config -} - -func (dc *DialContext) dial(u *url.URL) (net.Conn, error) { - if u.Scheme == "ldapi" { - if u.Path == "" || u.Path == "/" { - u.Path = "/var/run/slapd/ldapi" - } - return dc.dialer.Dial("unix", u.Path) - } - - host, port, err := net.SplitHostPort(u.Host) - if err != nil { - // we assume that error is due to missing port - host = u.Host - port = "" - } - - switch u.Scheme { - case "cldap": - if port == "" { - port = DefaultLdapPort - } - return dc.dialer.Dial("udp", net.JoinHostPort(host, port)) - case "ldap": - if port == "" { - port = DefaultLdapPort - } - return dc.dialer.Dial("tcp", net.JoinHostPort(host, port)) - case "ldaps": - if port == "" { - port = DefaultLdapsPort - } - return tls.DialWithDialer(dc.dialer, "tcp", net.JoinHostPort(host, port), dc.tlsConfig) - } - - return nil, fmt.Errorf("Unknown scheme '%s'", u.Scheme) -} - -// Dial connects to the given address on the given network using net.Dial -// and then returns a new Conn for the connection. -// -// Deprecated: Use DialURL instead. -func Dial(network, addr string) (*Conn, error) { - c, err := net.DialTimeout(network, addr, DefaultTimeout) - if err != nil { - return nil, NewError(ErrorNetwork, err) - } - conn := NewConn(c, false) - conn.Start() - return conn, nil -} - -// DialTLS connects to the given address on the given network using tls.Dial -// and then returns a new Conn for the connection. -// -// Deprecated: Use DialURL instead. -func DialTLS(network, addr string, config *tls.Config) (*Conn, error) { - c, err := tls.DialWithDialer(&net.Dialer{Timeout: DefaultTimeout}, network, addr, config) - if err != nil { - return nil, NewError(ErrorNetwork, err) - } - conn := NewConn(c, true) - conn.Start() - return conn, nil -} - -// DialURL connects to the given ldap URL. -// The following schemas are supported: ldap://, ldaps://, ldapi://, -// and cldap:// (RFC1798, deprecated but used by Active Directory). -// On success a new Conn for the connection is returned. -func DialURL(addr string, opts ...DialOpt) (*Conn, error) { - u, err := url.Parse(addr) - if err != nil { - return nil, NewError(ErrorNetwork, err) - } - - var dc DialContext - for _, opt := range opts { - opt(&dc) - } - if dc.dialer == nil { - dc.dialer = &net.Dialer{Timeout: DefaultTimeout} - } - - c, err := dc.dial(u) - if err != nil { - return nil, NewError(ErrorNetwork, err) - } - - conn := NewConn(c, u.Scheme == "ldaps") - conn.Start() - return conn, nil -} - -// NewConn returns a new Conn using conn for network I/O. -func NewConn(conn net.Conn, isTLS bool) *Conn { - l := &Conn{ - conn: conn, - chanConfirm: make(chan struct{}), - chanMessageID: make(chan int64), - chanMessage: make(chan *messagePacket, 10), - messageContexts: map[int64]*messageContext{}, - requestTimeout: 0, - isTLS: isTLS, - } - l.wgClose.Add(1) - return l -} - -// Start initialises goroutines to read replies and process messages. -// Warning: Calling this function in addition to Dial or DialURL -// may cause race conditions. -// -// See: https://github.com/go-ldap/ldap/issues/356 -func (l *Conn) Start() { - go l.reader() - go l.processMessages() -} - -// IsClosing returns whether or not we're currently closing. -func (l *Conn) IsClosing() bool { - return atomic.LoadUint32(&l.closing) == 1 -} - -// setClosing sets the closing value to true -func (l *Conn) setClosing() bool { - return atomic.CompareAndSwapUint32(&l.closing, 0, 1) -} - -// Close closes the connection. -func (l *Conn) Close() (err error) { - l.messageMutex.Lock() - defer l.messageMutex.Unlock() - - if l.setClosing() { - l.Debug.Printf("Sending quit message and waiting for confirmation") - l.chanMessage <- &messagePacket{Op: MessageQuit} - - timeoutCtx := context.Background() - if l.getTimeout() > 0 { - var cancelFunc context.CancelFunc - timeoutCtx, cancelFunc = context.WithTimeout(timeoutCtx, time.Duration(l.getTimeout())) - defer cancelFunc() - } - select { - case <-l.chanConfirm: - // Confirmation was received. - case <-timeoutCtx.Done(): - // The timeout was reached before confirmation was received. - } - - close(l.chanMessage) - - l.Debug.Printf("Closing network connection") - err = l.conn.Close() - l.wgClose.Done() - } - l.wgClose.Wait() - - return err -} - -// SetTimeout sets the time after a request is sent that a MessageTimeout triggers -func (l *Conn) SetTimeout(timeout time.Duration) { - atomic.StoreInt64(&l.requestTimeout, int64(timeout)) -} - -func (l *Conn) getTimeout() int64 { - return atomic.LoadInt64(&l.requestTimeout) -} - -// Returns the next available messageID -func (l *Conn) nextMessageID() int64 { - if messageID, ok := <-l.chanMessageID; ok { - return messageID - } - return 0 -} - -// GetLastError returns the last recorded error from goroutines like processMessages and reader. -// Only the last recorded error will be returned. -func (l *Conn) GetLastError() error { - l.messageMutex.Lock() - defer l.messageMutex.Unlock() - return l.err -} - -// StartTLS sends the command to start a TLS session and then creates a new TLS Client -func (l *Conn) StartTLS(config *tls.Config) error { - if l.isTLS { - return NewError(ErrorNetwork, errors.New("ldap: already encrypted")) - } - - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") - packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) - request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Start TLS") - request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, "1.3.6.1.4.1.1466.20037", "TLS Extended Command")) - packet.AppendChild(request) - l.Debug.PrintPacket(packet) - - msgCtx, err := l.sendMessageWithFlags(packet, startTLS) - if err != nil { - return err - } - defer l.finishMessage(msgCtx) - - l.Debug.Printf("%d: waiting for response", msgCtx.id) - - packetResponse, ok := <-msgCtx.responses - if !ok { - return NewError(ErrorNetwork, errors.New("ldap: response channel closed")) - } - packet, err = packetResponse.ReadPacket() - l.Debug.Printf("%d: got response %p", msgCtx.id, packet) - if err != nil { - return err - } - - if l.Debug { - if err := addLDAPDescriptions(packet); err != nil { - l.Close() - return err - } - l.Debug.PrintPacket(packet) - } - - if err := GetLDAPError(packet); err == nil { - conn := tls.Client(l.conn, config) - - if connErr := conn.Handshake(); connErr != nil { - l.Close() - return NewError(ErrorNetwork, fmt.Errorf("TLS handshake failed (%v)", connErr)) - } - - l.isTLS = true - l.conn = conn - } else { - return err - } - go l.reader() - - return nil -} - -// TLSConnectionState returns the client's TLS connection state. -// The return values are their zero values if StartTLS did -// not succeed. -func (l *Conn) TLSConnectionState() (state tls.ConnectionState, ok bool) { - tc, ok := l.conn.(*tls.Conn) - if !ok { - return - } - return tc.ConnectionState(), true -} - -func (l *Conn) sendMessage(packet *ber.Packet) (*messageContext, error) { - return l.sendMessageWithFlags(packet, 0) -} - -func (l *Conn) sendMessageWithFlags(packet *ber.Packet, flags sendMessageFlags) (*messageContext, error) { - if l.IsClosing() { - return nil, NewError(ErrorNetwork, errors.New("ldap: connection closed")) - } - l.messageMutex.Lock() - l.Debug.Printf("flags&startTLS = %d", flags&startTLS) - if l.isStartingTLS { - l.messageMutex.Unlock() - return nil, NewError(ErrorNetwork, errors.New("ldap: connection is in startls phase")) - } - if flags&startTLS != 0 { - if l.outstandingRequests != 0 { - l.messageMutex.Unlock() - return nil, NewError(ErrorNetwork, errors.New("ldap: cannot StartTLS with outstanding requests")) - } - l.isStartingTLS = true - } - l.outstandingRequests++ - - l.messageMutex.Unlock() - - responses := make(chan *PacketResponse) - messageID := packet.Children[0].Value.(int64) - message := &messagePacket{ - Op: MessageRequest, - MessageID: messageID, - Packet: packet, - Context: &messageContext{ - id: messageID, - done: make(chan struct{}), - responses: responses, - }, - } - if !l.sendProcessMessage(message) { - if l.IsClosing() { - return nil, NewError(ErrorNetwork, errors.New("ldap: connection closed")) - } - return nil, NewError(ErrorNetwork, errors.New("ldap: could not send message for unknown reason")) - } - return message.Context, nil -} - -func (l *Conn) finishMessage(msgCtx *messageContext) { - close(msgCtx.done) - - if l.IsClosing() { - return - } - - l.messageMutex.Lock() - l.outstandingRequests-- - if l.isStartingTLS { - l.isStartingTLS = false - } - l.messageMutex.Unlock() - - message := &messagePacket{ - Op: MessageFinish, - MessageID: msgCtx.id, - } - l.sendProcessMessage(message) -} - -func (l *Conn) sendProcessMessage(message *messagePacket) bool { - l.messageMutex.Lock() - defer l.messageMutex.Unlock() - if l.IsClosing() { - return false - } - l.chanMessage <- message - return true -} - -func (l *Conn) processMessages() { - defer func() { - if err := recover(); err != nil { - l.err = fmt.Errorf("ldap: recovered panic in processMessages: %v", err) - } - for messageID, msgCtx := range l.messageContexts { - // If we are closing due to an error, inform anyone who - // is waiting about the error. - if l.IsClosing() && l.closeErr.Load() != nil { - msgCtx.sendResponse(&PacketResponse{Error: l.closeErr.Load().(error)}, time.Duration(l.getTimeout())) - } - l.Debug.Printf("Closing channel for MessageID %d", messageID) - close(msgCtx.responses) - delete(l.messageContexts, messageID) - } - close(l.chanMessageID) - close(l.chanConfirm) - }() - - var messageID int64 = 1 - for { - select { - case l.chanMessageID <- messageID: - messageID++ - case message := <-l.chanMessage: - switch message.Op { - case MessageQuit: - l.Debug.Printf("Shutting down - quit message received") - return - case MessageRequest: - // Add to message list and write to network - l.Debug.Printf("Sending message %d", message.MessageID) - - buf := message.Packet.Bytes() - _, err := l.conn.Write(buf) - if err != nil { - l.Debug.Printf("Error Sending Message: %s", err.Error()) - message.Context.sendResponse(&PacketResponse{Error: fmt.Errorf("unable to send request: %s", err)}, time.Duration(l.getTimeout())) - close(message.Context.responses) - break - } - - // Only add to messageContexts if we were able to - // successfully write the message. - l.messageContexts[message.MessageID] = message.Context - - // Add timeout if defined - requestTimeout := l.getTimeout() - if requestTimeout > 0 { - go func() { - timer := time.NewTimer(time.Duration(requestTimeout)) - defer func() { - if err := recover(); err != nil { - l.err = fmt.Errorf("ldap: recovered panic in RequestTimeout: %v", err) - } - - timer.Stop() - }() - - select { - case <-timer.C: - timeoutMessage := &messagePacket{ - Op: MessageTimeout, - MessageID: message.MessageID, - } - l.sendProcessMessage(timeoutMessage) - case <-message.Context.done: - } - }() - } - case MessageResponse: - l.Debug.Printf("Receiving message %d", message.MessageID) - if msgCtx, ok := l.messageContexts[message.MessageID]; ok { - msgCtx.sendResponse(&PacketResponse{message.Packet, nil}, time.Duration(l.getTimeout())) - } else { - l.err = fmt.Errorf("ldap: received unexpected message %d, %v", message.MessageID, l.IsClosing()) - l.Debug.PrintPacket(message.Packet) - } - case MessageTimeout: - // Handle the timeout by closing the channel - // All reads will return immediately - if msgCtx, ok := l.messageContexts[message.MessageID]; ok { - l.Debug.Printf("Receiving message timeout for %d", message.MessageID) - msgCtx.sendResponse(&PacketResponse{message.Packet, NewError(ErrorNetwork, errors.New("ldap: connection timed out"))}, time.Duration(l.getTimeout())) - delete(l.messageContexts, message.MessageID) - close(msgCtx.responses) - } - case MessageFinish: - l.Debug.Printf("Finished message %d", message.MessageID) - if msgCtx, ok := l.messageContexts[message.MessageID]; ok { - delete(l.messageContexts, message.MessageID) - close(msgCtx.responses) - } - } - } - } -} - -func (l *Conn) reader() { - cleanstop := false - defer func() { - if err := recover(); err != nil { - l.err = fmt.Errorf("ldap: recovered panic in reader: %v", err) - } - if !cleanstop { - l.Close() - } - }() - - bufConn := bufio.NewReader(l.conn) - for { - if cleanstop { - l.Debug.Printf("reader clean stopping (without closing the connection)") - return - } - packet, err := ber.ReadPacket(bufConn) - if err != nil { - // A read error is expected here if we are closing the connection... - if !l.IsClosing() { - l.closeErr.Store(fmt.Errorf("unable to read LDAP response packet: %s", err)) - l.Debug.Printf("reader error: %s", err) - } - return - } - if err := addLDAPDescriptions(packet); err != nil { - l.Debug.Printf("descriptions error: %s", err) - } - if len(packet.Children) == 0 { - l.Debug.Printf("Received bad ldap packet") - continue - } - l.messageMutex.Lock() - if l.isStartingTLS { - cleanstop = true - } - l.messageMutex.Unlock() - message := &messagePacket{ - Op: MessageResponse, - MessageID: packet.Children[0].Value.(int64), - Packet: packet, - } - if !l.sendProcessMessage(message) { - return - } - } -} diff --git a/conn_test.go b/conn_test.go deleted file mode 100644 index d0bfc0c..0000000 --- a/conn_test.go +++ /dev/null @@ -1,404 +0,0 @@ -package ldap - -import ( - "bytes" - "errors" - "io" - "net" - "net/http" - "net/http/httptest" - "runtime" - "sync" - "testing" - "time" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -func TestUnresponsiveConnection(t *testing.T) { - // The do-nothing server that accepts requests and does nothing - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - })) - defer ts.Close() - c, err := net.Dial(ts.Listener.Addr().Network(), ts.Listener.Addr().String()) - if err != nil { - t.Fatalf("error connecting to localhost tcp: %v", err) - } - - // Create an Ldap connection - conn := NewConn(c, false) - conn.SetTimeout(time.Millisecond) - conn.Start() - defer conn.Close() - - // Mock a packet - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") - packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, conn.nextMessageID(), "MessageID")) - bindRequest := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") - bindRequest.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) - packet.AppendChild(bindRequest) - - // Send packet and test response - msgCtx, err := conn.sendMessage(packet) - if err != nil { - t.Fatalf("error sending message: %v", err) - } - defer conn.finishMessage(msgCtx) - - packetResponse, ok := <-msgCtx.responses - if !ok { - t.Fatalf("no PacketResponse in response channel") - } - _, err = packetResponse.ReadPacket() - if err == nil { - t.Fatalf("expected timeout error") - } - if !IsErrorWithCode(err, ErrorNetwork) || err.(*Error).Err.Error() != "ldap: connection timed out" { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestRequestTimeoutDeadlock(t *testing.T) { - // The do-nothing server that accepts requests and does nothing - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - })) - defer ts.Close() - c, err := net.Dial(ts.Listener.Addr().Network(), ts.Listener.Addr().String()) - if err != nil { - t.Fatalf("error connecting to localhost tcp: %v", err) - } - - // Create an Ldap connection - conn := NewConn(c, false) - conn.Start() - // trigger a race condition on accessing request timeout - n := 3 - for i := 0; i < n; i++ { - go func() { - conn.SetTimeout(time.Millisecond) - }() - } - - // Attempt to close the connection when the message handler is - // blocked or inactive - conn.Close() -} - -// TestInvalidStateCloseDeadlock tests that we do not enter deadlock when the -// message handler is blocked or inactive. -func TestInvalidStateCloseDeadlock(t *testing.T) { - // The do-nothing server that accepts requests and does nothing - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - })) - defer ts.Close() - c, err := net.Dial(ts.Listener.Addr().Network(), ts.Listener.Addr().String()) - if err != nil { - t.Fatalf("error connecting to localhost tcp: %v", err) - } - - // Create an Ldap connection - conn := NewConn(c, false) - conn.SetTimeout(time.Millisecond) - - // Attempt to close the connection when the message handler is - // blocked or inactive - conn.Close() -} - -// TestInvalidStateSendResponseDeadlock tests that we do not enter deadlock when the -// message handler is blocked or inactive. -func TestInvalidStateSendResponseDeadlock(t *testing.T) { - // Attempt to send a response packet when the message handler is blocked or inactive - msgCtx := &messageContext{ - id: 0, - done: make(chan struct{}), - responses: make(chan *PacketResponse), - } - msgCtx.sendResponse(&PacketResponse{}, time.Millisecond) -} - -// TestFinishMessage tests that we do not enter deadlock when a goroutine makes -// a request but does not handle all responses from the server. -func TestFinishMessage(t *testing.T) { - ptc := newPacketTranslatorConn() - defer ptc.Close() - - conn := NewConn(ptc, false) - conn.Start() - - // Test sending 5 different requests in series. Ensure that we can - // get a response packet from the underlying connection and also - // ensure that we can gracefully ignore unhandled responses. - for i := 0; i < 5; i++ { - t.Logf("serial request %d", i) - // Create a message and make sure we can receive responses. - msgCtx := testSendRequest(t, ptc, conn) - testReceiveResponse(t, ptc, msgCtx) - - // Send a few unhandled responses and finish the message. - testSendUnhandledResponsesAndFinish(t, ptc, conn, msgCtx, 5) - t.Logf("serial request %d done", i) - } - - // Test sending 5 different requests in parallel. - var wg sync.WaitGroup - for i := 0; i < 5; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - t.Logf("parallel request %d", i) - // Create a message and make sure we can receive responses. - msgCtx := testSendRequest(t, ptc, conn) - testReceiveResponse(t, ptc, msgCtx) - - // Send a few unhandled responses and finish the message. - testSendUnhandledResponsesAndFinish(t, ptc, conn, msgCtx, 5) - t.Logf("parallel request %d done", i) - }(i) - } - wg.Wait() - - // We cannot run Close() in a defer because t.FailNow() will run it and - // it will block if the processMessage Loop is in a deadlock. - conn.Close() -} - -// See: https://github.com/go-ldap/ldap/issues/332 -func TestNilConnection(t *testing.T) { - var conn *Conn - _, err := conn.Search(&SearchRequest{}) - if err != ErrNilConnection { - t.Fatalf("expected error to be ErrNilConnection, got %v", err) - } -} - -func testSendRequest(t *testing.T, ptc *packetTranslatorConn, conn *Conn) (msgCtx *messageContext) { - var msgID int64 - runWithTimeout(t, time.Second, func() { - msgID = conn.nextMessageID() - }) - - requestPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") - requestPacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, msgID, "MessageID")) - - var err error - - runWithTimeout(t, time.Second, func() { - msgCtx, err = conn.sendMessage(requestPacket) - if err != nil { - t.Fatalf("unable to send request message: %s", err) - } - }) - - // We should now be able to get this request packet out from the other - // side. - runWithTimeout(t, time.Second, func() { - if _, err = ptc.ReceiveRequest(); err != nil { - t.Fatalf("unable to receive request packet: %s", err) - } - }) - - return msgCtx -} - -func testReceiveResponse(t *testing.T, ptc *packetTranslatorConn, msgCtx *messageContext) { - // Send a mock response packet. - responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response") - responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, msgCtx.id, "MessageID")) - - runWithTimeout(t, time.Second, func() { - if err := ptc.SendResponse(responsePacket); err != nil { - t.Fatalf("unable to send response packet: %s", err) - } - }) - - // We should be able to receive the packet from the connection. - runWithTimeout(t, time.Second, func() { - if _, ok := <-msgCtx.responses; !ok { - t.Fatal("response channel closed") - } - }) -} - -func testSendUnhandledResponsesAndFinish(t *testing.T, ptc *packetTranslatorConn, conn *Conn, msgCtx *messageContext, numResponses int) { - // Send a mock response packet. - responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response") - responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, msgCtx.id, "MessageID")) - - // Send extra responses but do not attempt to receive them on the - // client side. - for i := 0; i < numResponses; i++ { - runWithTimeout(t, time.Second, func() { - if err := ptc.SendResponse(responsePacket); err != nil { - t.Fatalf("unable to send response packet: %s", err) - } - }) - } - - // Finally, attempt to finish this message. - runWithTimeout(t, time.Second, func() { - conn.finishMessage(msgCtx) - }) -} - -func runWithTimeout(t *testing.T, timeout time.Duration, f func()) { - done := make(chan struct{}) - go func() { - f() - close(done) - }() - - select { - case <-done: // Success! - case <-time.After(timeout): - _, file, line, _ := runtime.Caller(1) - t.Fatalf("%s:%d timed out", file, line) - } -} - -// packetTranslatorConn is a helpful type which can be used with various tests -// in this package. It implements the net.Conn interface to be used as an -// underlying connection for a *ldap.Conn. Most methods are no-ops but the -// Read() and Write() methods are able to translate ber-encoded packets for -// testing LDAP requests and responses. -// -// Test cases can simulate an LDAP server sending a response by calling the -// SendResponse() method with a ber-encoded LDAP response packet. Test cases -// can simulate an LDAP server receiving a request from a client by calling the -// ReceiveRequest() method which returns a ber-encoded LDAP request packet. -type packetTranslatorConn struct { - lock sync.Mutex - isClosed bool - - responseCond sync.Cond - requestCond sync.Cond - - responseBuf bytes.Buffer - requestBuf bytes.Buffer -} - -var errPacketTranslatorConnClosed = errors.New("connection closed") - -func newPacketTranslatorConn() *packetTranslatorConn { - conn := &packetTranslatorConn{} - conn.responseCond = sync.Cond{L: &conn.lock} - conn.requestCond = sync.Cond{L: &conn.lock} - - return conn -} - -// Read is called by the reader() loop to receive response packets. It will -// block until there are more packet bytes available or this connection is -// closed. -func (c *packetTranslatorConn) Read(b []byte) (n int, err error) { - c.lock.Lock() - defer c.lock.Unlock() - - for !c.isClosed { - // Attempt to read data from the response buffer. If it fails - // with an EOF, wait and try again. - n, err = c.responseBuf.Read(b) - if err != io.EOF { - return n, err - } - - c.responseCond.Wait() - } - - return 0, errPacketTranslatorConnClosed -} - -// SendResponse writes the given response packet to the response buffer for -// this connection, signalling any goroutine waiting to read a response. -func (c *packetTranslatorConn) SendResponse(packet *ber.Packet) error { - c.lock.Lock() - defer c.lock.Unlock() - - if c.isClosed { - return errPacketTranslatorConnClosed - } - - // Signal any goroutine waiting to read a response. - defer c.responseCond.Broadcast() - - // Writes to the buffer should always succeed. - c.responseBuf.Write(packet.Bytes()) - - return nil -} - -// Write is called by the processMessages() loop to send request packets. -func (c *packetTranslatorConn) Write(b []byte) (n int, err error) { - c.lock.Lock() - defer c.lock.Unlock() - - if c.isClosed { - return 0, errPacketTranslatorConnClosed - } - - // Signal any goroutine waiting to read a request. - defer c.requestCond.Broadcast() - - // Writes to the buffer should always succeed. - return c.requestBuf.Write(b) -} - -// ReceiveRequest attempts to read a request packet from this connection. It -// will block until it is able to read a full request packet or until this -// connection is closed. -func (c *packetTranslatorConn) ReceiveRequest() (*ber.Packet, error) { - c.lock.Lock() - defer c.lock.Unlock() - - for !c.isClosed { - // Attempt to parse a request packet from the request buffer. - // If it fails with an unexpected EOF, wait and try again. - requestReader := bytes.NewReader(c.requestBuf.Bytes()) - packet, err := ber.ReadPacket(requestReader) - switch err { - case io.EOF, io.ErrUnexpectedEOF: - c.requestCond.Wait() - case nil: - // Advance the request buffer by the number of bytes - // read to decode the request packet. - c.requestBuf.Next(c.requestBuf.Len() - requestReader.Len()) - return packet, nil - default: - return nil, err - } - } - - return nil, errPacketTranslatorConnClosed -} - -// Close closes this connection causing Read() and Write() calls to fail. -func (c *packetTranslatorConn) Close() error { - c.lock.Lock() - defer c.lock.Unlock() - - c.isClosed = true - c.responseCond.Broadcast() - c.requestCond.Broadcast() - - return nil -} - -func (c *packetTranslatorConn) LocalAddr() net.Addr { - return (*net.TCPAddr)(nil) -} - -func (c *packetTranslatorConn) RemoteAddr() net.Addr { - return (*net.TCPAddr)(nil) -} - -func (c *packetTranslatorConn) SetDeadline(t time.Time) error { - return nil -} - -func (c *packetTranslatorConn) SetReadDeadline(t time.Time) error { - return nil -} - -func (c *packetTranslatorConn) SetWriteDeadline(t time.Time) error { - return nil -} diff --git a/control.go b/control.go deleted file mode 100644 index ab75c34..0000000 --- a/control.go +++ /dev/null @@ -1,1294 +0,0 @@ -package ldap - -import ( - "fmt" - "strconv" - - ber "github.com/go-asn1-ber/asn1-ber" - "github.com/google/uuid" -) - -const ( - // ControlTypePaging - https://www.ietf.org/rfc/rfc2696.txt - ControlTypePaging = "1.2.840.113556.1.4.319" - // ControlTypeBeheraPasswordPolicy - https://tools.ietf.org/html/draft-behera-ldap-password-policy-10 - ControlTypeBeheraPasswordPolicy = "1.3.6.1.4.1.42.2.27.8.5.1" - // ControlTypeVChuPasswordMustChange - https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 - ControlTypeVChuPasswordMustChange = "2.16.840.1.113730.3.4.4" - // ControlTypeVChuPasswordWarning - https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 - ControlTypeVChuPasswordWarning = "2.16.840.1.113730.3.4.5" - // ControlTypeManageDsaIT - https://tools.ietf.org/html/rfc3296 - ControlTypeManageDsaIT = "2.16.840.1.113730.3.4.2" - // ControlTypeWhoAmI - https://tools.ietf.org/html/rfc4532 - ControlTypeWhoAmI = "1.3.6.1.4.1.4203.1.11.3" - // ControlTypeSubtreeDelete - https://datatracker.ietf.org/doc/html/draft-armijo-ldap-treedelete-02 - ControlTypeSubtreeDelete = "1.2.840.113556.1.4.805" - - // ControlTypeServerSideSorting - https://www.ietf.org/rfc/rfc2891.txt - ControlTypeServerSideSorting = "1.2.840.113556.1.4.473" - // ControlTypeServerSideSorting - https://www.ietf.org/rfc/rfc2891.txt - ControlTypeServerSideSortingResult = "1.2.840.113556.1.4.474" - - // ControlTypeMicrosoftNotification - https://msdn.microsoft.com/en-us/library/aa366983(v=vs.85).aspx - ControlTypeMicrosoftNotification = "1.2.840.113556.1.4.528" - // ControlTypeMicrosoftShowDeleted - https://msdn.microsoft.com/en-us/library/aa366989(v=vs.85).aspx - ControlTypeMicrosoftShowDeleted = "1.2.840.113556.1.4.417" - // ControlTypeMicrosoftServerLinkTTL - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/f4f523a8-abc0-4b3a-a471-6b2fef135481?redirectedfrom=MSDN - ControlTypeMicrosoftServerLinkTTL = "1.2.840.113556.1.4.2309" - // ControlTypeDirSync - Active Directory DirSync - https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx - ControlTypeDirSync = "1.2.840.113556.1.4.841" - - // ControlTypeSyncRequest - https://www.ietf.org/rfc/rfc4533.txt - ControlTypeSyncRequest = "1.3.6.1.4.1.4203.1.9.1.1" - // ControlTypeSyncState - https://www.ietf.org/rfc/rfc4533.txt - ControlTypeSyncState = "1.3.6.1.4.1.4203.1.9.1.2" - // ControlTypeSyncDone - https://www.ietf.org/rfc/rfc4533.txt - ControlTypeSyncDone = "1.3.6.1.4.1.4203.1.9.1.3" - // ControlTypeSyncInfo - https://www.ietf.org/rfc/rfc4533.txt - ControlTypeSyncInfo = "1.3.6.1.4.1.4203.1.9.1.4" -) - -// Flags for DirSync control -const ( - DirSyncIncrementalValues int64 = 2147483648 - DirSyncPublicDataOnly int64 = 8192 - DirSyncAncestorsFirstOrder int64 = 2048 - DirSyncObjectSecurity int64 = 1 -) - -// ControlTypeMap maps controls to text descriptions -var ControlTypeMap = map[string]string{ - ControlTypePaging: "Paging", - ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft", - ControlTypeManageDsaIT: "Manage DSA IT", - ControlTypeSubtreeDelete: "Subtree Delete Control", - ControlTypeMicrosoftNotification: "Change Notification - Microsoft", - ControlTypeMicrosoftShowDeleted: "Show Deleted Objects - Microsoft", - ControlTypeMicrosoftServerLinkTTL: "Return TTL-DNs for link values with associated expiry times - Microsoft", - ControlTypeServerSideSorting: "Server Side Sorting Request - LDAP Control Extension for Server Side Sorting of Search Results (RFC2891)", - ControlTypeServerSideSortingResult: "Server Side Sorting Results - LDAP Control Extension for Server Side Sorting of Search Results (RFC2891)", - ControlTypeDirSync: "DirSync", - ControlTypeSyncRequest: "Sync Request", - ControlTypeSyncState: "Sync State", - ControlTypeSyncDone: "Sync Done", - ControlTypeSyncInfo: "Sync Info", -} - -// Control defines an interface controls provide to encode and describe themselves -type Control interface { - // GetControlType returns the OID - GetControlType() string - // Encode returns the ber packet representation - Encode() *ber.Packet - // String returns a human-readable description - String() string -} - -// ControlString implements the Control interface for simple controls -type ControlString struct { - ControlType string - Criticality bool - ControlValue string -} - -// GetControlType returns the OID -func (c *ControlString) GetControlType() string { - return c.ControlType -} - -// Encode returns the ber packet representation -func (c *ControlString) Encode() *ber.Packet { - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.ControlType, "Control Type ("+ControlTypeMap[c.ControlType]+")")) - if c.Criticality { - packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) - } - if c.ControlValue != "" { - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, string(c.ControlValue), "Control Value")) - } - return packet -} - -// String returns a human-readable description -func (c *ControlString) String() string { - return fmt.Sprintf("Control Type: %s (%q) Criticality: %t Control Value: %s", ControlTypeMap[c.ControlType], c.ControlType, c.Criticality, c.ControlValue) -} - -// ControlPaging implements the paging control described in https://www.ietf.org/rfc/rfc2696.txt -type ControlPaging struct { - // PagingSize indicates the page size - PagingSize uint32 - // Cookie is an opaque value returned by the server to track a paging cursor - Cookie []byte -} - -// GetControlType returns the OID -func (c *ControlPaging) GetControlType() string { - return ControlTypePaging -} - -// Encode returns the ber packet representation -func (c *ControlPaging) Encode() *ber.Packet { - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypePaging, "Control Type ("+ControlTypeMap[ControlTypePaging]+")")) - - p2 := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (Paging)") - seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Search Control Value") - seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.PagingSize), "Paging Size")) - cookie := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Cookie") - cookie.Value = c.Cookie - cookie.Data.Write(c.Cookie) - seq.AppendChild(cookie) - p2.AppendChild(seq) - - packet.AppendChild(p2) - return packet -} - -// String returns a human-readable description -func (c *ControlPaging) String() string { - return fmt.Sprintf( - "Control Type: %s (%q) Criticality: %t PagingSize: %d Cookie: %q", - ControlTypeMap[ControlTypePaging], - ControlTypePaging, - false, - c.PagingSize, - c.Cookie) -} - -// SetCookie stores the given cookie in the paging control -func (c *ControlPaging) SetCookie(cookie []byte) { - c.Cookie = cookie -} - -// ControlBeheraPasswordPolicy implements the control described in https://tools.ietf.org/html/draft-behera-ldap-password-policy-10 -type ControlBeheraPasswordPolicy struct { - // Expire contains the number of seconds before a password will expire - Expire int64 - // Grace indicates the remaining number of times a user will be allowed to authenticate with an expired password - Grace int64 - // Error indicates the error code - Error int8 - // ErrorString is a human readable error - ErrorString string -} - -// GetControlType returns the OID -func (c *ControlBeheraPasswordPolicy) GetControlType() string { - return ControlTypeBeheraPasswordPolicy -} - -// Encode returns the ber packet representation -func (c *ControlBeheraPasswordPolicy) Encode() *ber.Packet { - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeBeheraPasswordPolicy, "Control Type ("+ControlTypeMap[ControlTypeBeheraPasswordPolicy]+")")) - - return packet -} - -// String returns a human-readable description -func (c *ControlBeheraPasswordPolicy) String() string { - return fmt.Sprintf( - "Control Type: %s (%q) Criticality: %t Expire: %d Grace: %d Error: %d, ErrorString: %s", - ControlTypeMap[ControlTypeBeheraPasswordPolicy], - ControlTypeBeheraPasswordPolicy, - false, - c.Expire, - c.Grace, - c.Error, - c.ErrorString) -} - -// ControlVChuPasswordMustChange implements the control described in https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 -type ControlVChuPasswordMustChange struct { - // MustChange indicates if the password is required to be changed - MustChange bool -} - -// GetControlType returns the OID -func (c *ControlVChuPasswordMustChange) GetControlType() string { - return ControlTypeVChuPasswordMustChange -} - -// Encode returns the ber packet representation -func (c *ControlVChuPasswordMustChange) Encode() *ber.Packet { - return nil -} - -// String returns a human-readable description -func (c *ControlVChuPasswordMustChange) String() string { - return fmt.Sprintf( - "Control Type: %s (%q) Criticality: %t MustChange: %v", - ControlTypeMap[ControlTypeVChuPasswordMustChange], - ControlTypeVChuPasswordMustChange, - false, - c.MustChange) -} - -// ControlVChuPasswordWarning implements the control described in https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 -type ControlVChuPasswordWarning struct { - // Expire indicates the time in seconds until the password expires - Expire int64 -} - -// GetControlType returns the OID -func (c *ControlVChuPasswordWarning) GetControlType() string { - return ControlTypeVChuPasswordWarning -} - -// Encode returns the ber packet representation -func (c *ControlVChuPasswordWarning) Encode() *ber.Packet { - return nil -} - -// String returns a human-readable description -func (c *ControlVChuPasswordWarning) String() string { - return fmt.Sprintf( - "Control Type: %s (%q) Criticality: %t Expire: %b", - ControlTypeMap[ControlTypeVChuPasswordWarning], - ControlTypeVChuPasswordWarning, - false, - c.Expire) -} - -// ControlManageDsaIT implements the control described in https://tools.ietf.org/html/rfc3296 -type ControlManageDsaIT struct { - // Criticality indicates if this control is required - Criticality bool -} - -// GetControlType returns the OID -func (c *ControlManageDsaIT) GetControlType() string { - return ControlTypeManageDsaIT -} - -// Encode returns the ber packet representation -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 -} - -// String returns a human-readable description -func (c *ControlManageDsaIT) String() string { - return fmt.Sprintf( - "Control Type: %s (%q) Criticality: %t", - ControlTypeMap[ControlTypeManageDsaIT], - ControlTypeManageDsaIT, - c.Criticality) -} - -// NewControlManageDsaIT returns a ControlManageDsaIT control -func NewControlManageDsaIT(Criticality bool) *ControlManageDsaIT { - return &ControlManageDsaIT{Criticality: Criticality} -} - -// ControlMicrosoftNotification implements the control described in https://msdn.microsoft.com/en-us/library/aa366983(v=vs.85).aspx -type ControlMicrosoftNotification struct{} - -// GetControlType returns the OID -func (c *ControlMicrosoftNotification) GetControlType() string { - return ControlTypeMicrosoftNotification -} - -// Encode returns the ber packet representation -func (c *ControlMicrosoftNotification) Encode() *ber.Packet { - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeMicrosoftNotification, "Control Type ("+ControlTypeMap[ControlTypeMicrosoftNotification]+")")) - - return packet -} - -// String returns a human-readable description -func (c *ControlMicrosoftNotification) String() string { - return fmt.Sprintf( - "Control Type: %s (%q)", - ControlTypeMap[ControlTypeMicrosoftNotification], - ControlTypeMicrosoftNotification) -} - -// NewControlMicrosoftNotification returns a ControlMicrosoftNotification control -func NewControlMicrosoftNotification() *ControlMicrosoftNotification { - return &ControlMicrosoftNotification{} -} - -// ControlMicrosoftShowDeleted implements the control described in https://msdn.microsoft.com/en-us/library/aa366989(v=vs.85).aspx -type ControlMicrosoftShowDeleted struct{} - -// GetControlType returns the OID -func (c *ControlMicrosoftShowDeleted) GetControlType() string { - return ControlTypeMicrosoftShowDeleted -} - -// Encode returns the ber packet representation -func (c *ControlMicrosoftShowDeleted) Encode() *ber.Packet { - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeMicrosoftShowDeleted, "Control Type ("+ControlTypeMap[ControlTypeMicrosoftShowDeleted]+")")) - - return packet -} - -// String returns a human-readable description -func (c *ControlMicrosoftShowDeleted) String() string { - return fmt.Sprintf( - "Control Type: %s (%q)", - ControlTypeMap[ControlTypeMicrosoftShowDeleted], - ControlTypeMicrosoftShowDeleted) -} - -// NewControlMicrosoftShowDeleted returns a ControlMicrosoftShowDeleted control -func NewControlMicrosoftShowDeleted() *ControlMicrosoftShowDeleted { - return &ControlMicrosoftShowDeleted{} -} - -// ControlMicrosoftServerLinkTTL implements the control described in https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/f4f523a8-abc0-4b3a-a471-6b2fef135481?redirectedfrom=MSDN -type ControlMicrosoftServerLinkTTL struct{} - -// GetControlType returns the OID -func (c *ControlMicrosoftServerLinkTTL) GetControlType() string { - return ControlTypeMicrosoftServerLinkTTL -} - -// Encode returns the ber packet representation -func (c *ControlMicrosoftServerLinkTTL) Encode() *ber.Packet { - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeMicrosoftServerLinkTTL, "Control Type ("+ControlTypeMap[ControlTypeMicrosoftServerLinkTTL]+")")) - - return packet -} - -// String returns a human-readable description -func (c *ControlMicrosoftServerLinkTTL) String() string { - return fmt.Sprintf( - "Control Type: %s (%q)", - ControlTypeMap[ControlTypeMicrosoftServerLinkTTL], - ControlTypeMicrosoftServerLinkTTL) -} - -// NewControlMicrosoftServerLinkTTL returns a ControlMicrosoftServerLinkTTL control -func NewControlMicrosoftServerLinkTTL() *ControlMicrosoftServerLinkTTL { - return &ControlMicrosoftServerLinkTTL{} -} - -// FindControl returns the first control of the given type in the list, or nil -func FindControl(controls []Control, controlType string) Control { - for _, c := range controls { - if c.GetControlType() == controlType { - return c - } - } - return nil -} - -// DecodeControl returns a control read from the given packet, or nil if no recognized control can be made -func DecodeControl(packet *ber.Packet) (Control, error) { - var ( - ControlType = "" - Criticality = false - value *ber.Packet - ) - - switch len(packet.Children) { - case 0: - // at least one child is required for control type - return nil, fmt.Errorf("at least one child is required for control type") - - case 1: - // just type, no criticality or value - packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" - ControlType = packet.Children[0].Value.(string) - - case 2: - packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" - if packet.Children[0].Value != nil { - ControlType = packet.Children[0].Value.(string) - } else if packet.Children[0].Data != nil { - ControlType = packet.Children[0].Data.String() - } else { - return nil, fmt.Errorf("not found where to get the control type") - } - - // Children[1] could be criticality or value (both are optional) - // duck-type on whether this is a boolean - if _, ok := packet.Children[1].Value.(bool); ok { - packet.Children[1].Description = "Criticality" - Criticality = packet.Children[1].Value.(bool) - } else { - packet.Children[1].Description = "Control Value" - value = packet.Children[1] - } - - case 3: - packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" - ControlType = packet.Children[0].Value.(string) - - packet.Children[1].Description = "Criticality" - Criticality = packet.Children[1].Value.(bool) - - packet.Children[2].Description = "Control Value" - value = packet.Children[2] - - default: - // more than 3 children is invalid - return nil, fmt.Errorf("more than 3 children is invalid for controls") - } - - switch ControlType { - case ControlTypeManageDsaIT: - return NewControlManageDsaIT(Criticality), nil - case ControlTypePaging: - value.Description += " (Paging)" - c := new(ControlPaging) - if value.Value != nil { - valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) - if err != nil { - return nil, fmt.Errorf("failed to decode data bytes: %s", err) - } - value.Data.Truncate(0) - value.Value = nil - value.AppendChild(valueChildren) - } - value = value.Children[0] - value.Description = "Search Control Value" - value.Children[0].Description = "Paging Size" - value.Children[1].Description = "Cookie" - c.PagingSize = uint32(value.Children[0].Value.(int64)) - c.Cookie = value.Children[1].Data.Bytes() - value.Children[1].Value = c.Cookie - return c, nil - case ControlTypeBeheraPasswordPolicy: - value.Description += " (Password Policy - Behera)" - c := NewControlBeheraPasswordPolicy() - if value.Value != nil { - valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) - if err != nil { - return nil, fmt.Errorf("failed to decode data bytes: %s", err) - } - value.Data.Truncate(0) - value.Value = nil - value.AppendChild(valueChildren) - } - - sequence := value.Children[0] - - for _, child := range sequence.Children { - if child.Tag == 0 { - // Warning - warningPacket := child.Children[0] - val, err := ber.ParseInt64(warningPacket.Data.Bytes()) - if err != nil { - return nil, fmt.Errorf("failed to decode data bytes: %s", err) - } - if warningPacket.Tag == 0 { - // timeBeforeExpiration - c.Expire = val - warningPacket.Value = c.Expire - } else if warningPacket.Tag == 1 { - // graceAuthNsRemaining - c.Grace = val - warningPacket.Value = c.Grace - } - } else if child.Tag == 1 { - // Error - bs := child.Data.Bytes() - if len(bs) != 1 || bs[0] > 8 { - return nil, fmt.Errorf("failed to decode data bytes: %s", "invalid PasswordPolicyResponse enum value") - } - val := int8(bs[0]) - c.Error = val - child.Value = c.Error - c.ErrorString = BeheraPasswordPolicyErrorMap[c.Error] - } - } - return c, nil - case ControlTypeVChuPasswordMustChange: - c := &ControlVChuPasswordMustChange{MustChange: true} - return c, nil - case ControlTypeVChuPasswordWarning: - c := &ControlVChuPasswordWarning{Expire: -1} - expireStr := ber.DecodeString(value.Data.Bytes()) - - expire, err := strconv.ParseInt(expireStr, 10, 64) - if err != nil { - return nil, fmt.Errorf("failed to parse value as int: %s", err) - } - c.Expire = expire - value.Value = c.Expire - - return c, nil - case ControlTypeMicrosoftNotification: - return NewControlMicrosoftNotification(), nil - case ControlTypeMicrosoftShowDeleted: - return NewControlMicrosoftShowDeleted(), nil - case ControlTypeMicrosoftServerLinkTTL: - return NewControlMicrosoftServerLinkTTL(), nil - case ControlTypeSubtreeDelete: - return NewControlSubtreeDelete(), nil - case ControlTypeServerSideSorting: - return NewControlServerSideSorting(value) - case ControlTypeServerSideSortingResult: - return NewControlServerSideSortingResult(value) - case ControlTypeDirSync: - value.Description += " (DirSync)" - return NewResponseControlDirSync(value) - case ControlTypeSyncState: - value.Description += " (Sync State)" - valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) - if err != nil { - return nil, fmt.Errorf("failed to decode data bytes: %s", err) - } - return NewControlSyncState(valueChildren) - case ControlTypeSyncDone: - value.Description += " (Sync Done)" - valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) - if err != nil { - return nil, fmt.Errorf("failed to decode data bytes: %s", err) - } - return NewControlSyncDone(valueChildren) - case ControlTypeSyncInfo: - value.Description += " (Sync Info)" - valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) - if err != nil { - return nil, fmt.Errorf("failed to decode data bytes: %s", err) - } - return NewControlSyncInfo(valueChildren) - default: - c := new(ControlString) - c.ControlType = ControlType - c.Criticality = Criticality - if value != nil { - c.ControlValue = value.Value.(string) - } - return c, nil - } -} - -// NewControlString returns a generic control -func NewControlString(controlType string, criticality bool, controlValue string) *ControlString { - return &ControlString{ - ControlType: controlType, - Criticality: criticality, - ControlValue: controlValue, - } -} - -// NewControlPaging returns a paging control -func NewControlPaging(pagingSize uint32) *ControlPaging { - return &ControlPaging{PagingSize: pagingSize} -} - -// NewControlBeheraPasswordPolicy returns a ControlBeheraPasswordPolicy -func NewControlBeheraPasswordPolicy() *ControlBeheraPasswordPolicy { - return &ControlBeheraPasswordPolicy{ - Expire: -1, - Grace: -1, - Error: -1, - } -} - -// ControlSubtreeDelete implements the subtree delete control described in -// https://datatracker.ietf.org/doc/html/draft-armijo-ldap-treedelete-02 -type ControlSubtreeDelete struct{} - -// GetControlType returns the OID -func (c *ControlSubtreeDelete) GetControlType() string { - return ControlTypeSubtreeDelete -} - -// NewControlSubtreeDelete returns a ControlSubtreeDelete control. -func NewControlSubtreeDelete() *ControlSubtreeDelete { - return &ControlSubtreeDelete{} -} - -// Encode returns the ber packet representation -func (c *ControlSubtreeDelete) Encode() *ber.Packet { - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeSubtreeDelete, "Control Type ("+ControlTypeMap[ControlTypeSubtreeDelete]+")")) - - return packet -} - -func (c *ControlSubtreeDelete) String() string { - return fmt.Sprintf( - "Control Type: %s (%q)", - ControlTypeMap[ControlTypeSubtreeDelete], - ControlTypeSubtreeDelete) -} - -func encodeControls(controls []Control) *ber.Packet { - packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0, nil, "Controls") - for _, control := range controls { - packet.AppendChild(control.Encode()) - } - return packet -} - -// ControlDirSync implements the control described in https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx -type ControlDirSync struct { - Criticality bool - Flags int64 - MaxAttrCount int64 - Cookie []byte -} - -// Deprecated: Use NewRequestControlDirSync instead -func NewControlDirSync(flags int64, maxAttrCount int64, cookie []byte) *ControlDirSync { - return NewRequestControlDirSync(flags, maxAttrCount, cookie) -} - -// NewRequestControlDirSync returns a dir sync control -func NewRequestControlDirSync( - flags int64, maxAttrCount int64, cookie []byte, -) *ControlDirSync { - return &ControlDirSync{ - Criticality: true, - Flags: flags, - MaxAttrCount: maxAttrCount, - Cookie: cookie, - } -} - -// NewResponseControlDirSync returns a dir sync control -func NewResponseControlDirSync(value *ber.Packet) (*ControlDirSync, error) { - if value.Value != nil { - valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) - if err != nil { - return nil, fmt.Errorf("failed to decode data bytes: %s", err) - } - value.Data.Truncate(0) - value.Value = nil - value.AppendChild(valueChildren) - } - child := value.Children[0] - if len(child.Children) != 3 { // also on initial creation, Cookie is an empty string - return nil, fmt.Errorf("invalid number of children in dirSync control") - } - child.Description = "DirSync Control Value" - child.Children[0].Description = "Flags" - child.Children[1].Description = "MaxAttrCount" - child.Children[2].Description = "Cookie" - - cookie := child.Children[2].Data.Bytes() - child.Children[2].Value = cookie - return &ControlDirSync{ - Criticality: true, - Flags: child.Children[0].Value.(int64), - MaxAttrCount: child.Children[1].Value.(int64), - Cookie: cookie, - }, nil -} - -// GetControlType returns the OID -func (c *ControlDirSync) GetControlType() string { - return ControlTypeDirSync -} - -// String returns a human-readable description -func (c *ControlDirSync) String() string { - return fmt.Sprintf( - "ControlType: %s (%q) Criticality: %t ControlValue: Flags: %d MaxAttrCount: %d", - ControlTypeMap[ControlTypeDirSync], - ControlTypeDirSync, - c.Criticality, - c.Flags, - c.MaxAttrCount, - ) -} - -// Encode returns the ber packet representation -func (c *ControlDirSync) Encode() *ber.Packet { - cookie := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "Cookie") - if len(c.Cookie) != 0 { - cookie.Value = c.Cookie - cookie.Data.Write(c.Cookie) - } - - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeDirSync, "Control Type ("+ControlTypeMap[ControlTypeDirSync]+")")) - packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) // must be true always - - val := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (DirSync)") - seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "DirSync Control Value") - seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.Flags), "Flags")) - seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.MaxAttrCount), "MaxAttrCount")) - seq.AppendChild(cookie) - val.AppendChild(seq) - - packet.AppendChild(val) - return packet -} - -// SetCookie stores the given cookie in the dirSync control -func (c *ControlDirSync) SetCookie(cookie []byte) { - c.Cookie = cookie -} - -// ControlServerSideSorting - -type SortKey struct { - Reverse bool - AttributeType string - MatchingRule string -} - -type ControlServerSideSorting struct { - SortKeys []*SortKey -} - -func (c *ControlServerSideSorting) GetControlType() string { - return ControlTypeServerSideSorting -} - -func NewControlServerSideSorting(value *ber.Packet) (*ControlServerSideSorting, error) { - sortKeys := []*SortKey{} - - val := value.Children[1].Children - - if len(val) != 1 { - return nil, fmt.Errorf("no sequence value in packet") - } - - sequences := val[0].Children - - for i, sequence := range sequences { - sortKey := &SortKey{} - - if len(sequence.Children) < 2 { - return nil, fmt.Errorf("attributeType or matchingRule is missing from sequence %d", i) - } - - sortKey.AttributeType = sequence.Children[0].Value.(string) - sortKey.MatchingRule = sequence.Children[1].Value.(string) - - if len(sequence.Children) == 3 { - sortKey.Reverse = sequence.Children[2].Value.(bool) - } - - sortKeys = append(sortKeys, sortKey) - } - - return &ControlServerSideSorting{SortKeys: sortKeys}, nil -} - -func NewControlServerSideSortingWithSortKeys(sortKeys []*SortKey) *ControlServerSideSorting { - return &ControlServerSideSorting{SortKeys: sortKeys} -} - -func (c *ControlServerSideSorting) Encode() *ber.Packet { - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") - control := ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.GetControlType(), "Control Type") - - value := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value") - seqs := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "SortKeyList") - - for _, f := range c.SortKeys { - seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "") - - seq.AppendChild( - ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, f.AttributeType, "attributeType"), - ) - seq.AppendChild( - ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, f.MatchingRule, "orderingRule"), - ) - if f.Reverse { - seq.AppendChild( - ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, 1, f.Reverse, "reverseOrder"), - ) - } - - seqs.AppendChild(seq) - } - - value.AppendChild(seqs) - - packet.AppendChild(control) - packet.AppendChild(value) - - return packet -} - -func (c *ControlServerSideSorting) String() string { - return fmt.Sprintf( - "Control Type: %s (%q) Criticality:%t %+v", - "Server Side Sorting", - c.GetControlType(), - false, - c.SortKeys, - ) -} - -// ControlServerSideSortingResponse - -const ( - ControlServerSideSortingCodeSuccess ControlServerSideSortingCode = 0 - ControlServerSideSortingCodeOperationsError ControlServerSideSortingCode = 1 - ControlServerSideSortingCodeTimeLimitExceeded ControlServerSideSortingCode = 2 - ControlServerSideSortingCodeStrongAuthRequired ControlServerSideSortingCode = 8 - ControlServerSideSortingCodeAdminLimitExceeded ControlServerSideSortingCode = 11 - ControlServerSideSortingCodeNoSuchAttribute ControlServerSideSortingCode = 16 - ControlServerSideSortingCodeInappropriateMatching ControlServerSideSortingCode = 18 - ControlServerSideSortingCodeInsufficientAccessRights ControlServerSideSortingCode = 50 - ControlServerSideSortingCodeBusy ControlServerSideSortingCode = 51 - ControlServerSideSortingCodeUnwillingToPerform ControlServerSideSortingCode = 53 - ControlServerSideSortingCodeOther ControlServerSideSortingCode = 80 -) - -var ControlServerSideSortingCodes = []ControlServerSideSortingCode{ - ControlServerSideSortingCodeSuccess, - ControlServerSideSortingCodeOperationsError, - ControlServerSideSortingCodeTimeLimitExceeded, - ControlServerSideSortingCodeStrongAuthRequired, - ControlServerSideSortingCodeAdminLimitExceeded, - ControlServerSideSortingCodeNoSuchAttribute, - ControlServerSideSortingCodeInappropriateMatching, - ControlServerSideSortingCodeInsufficientAccessRights, - ControlServerSideSortingCodeBusy, - ControlServerSideSortingCodeUnwillingToPerform, - ControlServerSideSortingCodeOther, -} - -type ControlServerSideSortingCode int64 - -// Valid test the code contained in the control against the ControlServerSideSortingCodes slice and return an error if the code is unknown. -func (c ControlServerSideSortingCode) Valid() error { - for _, validRet := range ControlServerSideSortingCodes { - if c == validRet { - return nil - } - } - return fmt.Errorf("unknown return code : %d", c) -} - -func NewControlServerSideSortingResult(pkt *ber.Packet) (*ControlServerSideSortingResult, error) { - control := &ControlServerSideSortingResult{} - - if pkt == nil || len(pkt.Children) == 0 { - return nil, fmt.Errorf("bad packet") - } - - codeInt, err := ber.ParseInt64(pkt.Children[0].Data.Bytes()) - if err != nil { - return nil, err - } - - code := ControlServerSideSortingCode(codeInt) - if err := code.Valid(); err != nil { - return nil, err - } - - return control, nil -} - -type ControlServerSideSortingResult struct { - Criticality bool - - Result ControlServerSideSortingCode - - // Not populated for now. I can't get openldap to send me this value, so I think this is specific to other directory server - // AttributeType string -} - -func (control *ControlServerSideSortingResult) GetControlType() string { - return ControlTypeServerSideSortingResult -} - -func (c *ControlServerSideSortingResult) Encode() *ber.Packet { - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "SortResult sequence") - sortResult := ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, int64(c.Result), "SortResult") - packet.AppendChild(sortResult) - - return packet -} - -func (c *ControlServerSideSortingResult) String() string { - return fmt.Sprintf( - "Control Type: %s (%q) Criticality:%t ResultCode:%+v", - "Server Side Sorting Result", - c.GetControlType(), - c.Criticality, - c.Result, - ) -} - -// Mode for ControlTypeSyncRequest -type ControlSyncRequestMode int64 - -const ( - SyncRequestModeRefreshOnly ControlSyncRequestMode = 1 - SyncRequestModeRefreshAndPersist ControlSyncRequestMode = 3 -) - -// ControlSyncRequest implements the Sync Request Control described in https://www.ietf.org/rfc/rfc4533.txt -type ControlSyncRequest struct { - Criticality bool - Mode ControlSyncRequestMode - Cookie []byte - ReloadHint bool -} - -func NewControlSyncRequest( - mode ControlSyncRequestMode, cookie []byte, reloadHint bool, -) *ControlSyncRequest { - return &ControlSyncRequest{ - Criticality: true, - Mode: mode, - Cookie: cookie, - ReloadHint: reloadHint, - } -} - -// GetControlType returns the OID -func (c *ControlSyncRequest) GetControlType() string { - return ControlTypeSyncRequest -} - -// Encode encodes the control -func (c *ControlSyncRequest) Encode() *ber.Packet { - _mode := int64(c.Mode) - mode := ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, _mode, "Mode") - var cookie *ber.Packet - if len(c.Cookie) > 0 { - cookie = ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Cookie") - cookie.Value = c.Cookie - cookie.Data.Write(c.Cookie) - } - reloadHint := ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.ReloadHint, "Reload Hint") - - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeSyncRequest, "Control Type ("+ControlTypeMap[ControlTypeSyncRequest]+")")) - packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) - - val := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (Sync Request)") - seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Sync Request Value") - seq.AppendChild(mode) - if cookie != nil { - seq.AppendChild(cookie) - } - seq.AppendChild(reloadHint) - val.AppendChild(seq) - - packet.AppendChild(val) - return packet -} - -// String returns a human-readable description -func (c *ControlSyncRequest) String() string { - return fmt.Sprintf( - "Control Type: %s (%q) Criticality: %t Mode: %d Cookie: %s ReloadHint: %t", - ControlTypeMap[ControlTypeSyncRequest], - ControlTypeSyncRequest, - c.Criticality, - c.Mode, - string(c.Cookie), - c.ReloadHint, - ) -} - -// State for ControlSyncState -type ControlSyncStateState int64 - -const ( - SyncStatePresent ControlSyncStateState = 0 - SyncStateAdd ControlSyncStateState = 1 - SyncStateModify ControlSyncStateState = 2 - SyncStateDelete ControlSyncStateState = 3 -) - -// ControlSyncState implements the Sync State Control described in https://www.ietf.org/rfc/rfc4533.txt -type ControlSyncState struct { - Criticality bool - State ControlSyncStateState - EntryUUID uuid.UUID - Cookie []byte -} - -func NewControlSyncState(pkt *ber.Packet) (*ControlSyncState, error) { - var ( - state ControlSyncStateState - entryUUID uuid.UUID - cookie []byte - err error - ) - switch len(pkt.Children) { - case 0, 1: - return nil, fmt.Errorf("at least two children are required: %d", len(pkt.Children)) - case 2: - state = ControlSyncStateState(pkt.Children[0].Value.(int64)) - entryUUID, err = uuid.FromBytes(pkt.Children[1].ByteValue) - if err != nil { - return nil, fmt.Errorf("failed to decode uuid: %w", err) - } - case 3: - state = ControlSyncStateState(pkt.Children[0].Value.(int64)) - entryUUID, err = uuid.FromBytes(pkt.Children[1].ByteValue) - if err != nil { - return nil, fmt.Errorf("failed to decode uuid: %w", err) - } - cookie = pkt.Children[2].ByteValue - } - return &ControlSyncState{ - Criticality: false, - State: state, - EntryUUID: entryUUID, - Cookie: cookie, - }, nil -} - -// GetControlType returns the OID -func (c *ControlSyncState) GetControlType() string { - return ControlTypeSyncState -} - -// Encode encodes the control -func (c *ControlSyncState) Encode() *ber.Packet { - return nil -} - -// String returns a human-readable description -func (c *ControlSyncState) String() string { - return fmt.Sprintf( - "Control Type: %s (%q) Criticality: %t State: %d EntryUUID: %s Cookie: %s", - ControlTypeMap[ControlTypeSyncState], - ControlTypeSyncState, - c.Criticality, - c.State, - c.EntryUUID.String(), - string(c.Cookie), - ) -} - -// ControlSyncDone implements the Sync Done Control described in https://www.ietf.org/rfc/rfc4533.txt -type ControlSyncDone struct { - Criticality bool - Cookie []byte - RefreshDeletes bool -} - -func NewControlSyncDone(pkt *ber.Packet) (*ControlSyncDone, error) { - var ( - cookie []byte - refreshDeletes bool - ) - switch len(pkt.Children) { - case 0: - // have nothing to do - case 1: - cookie = pkt.Children[0].ByteValue - case 2: - cookie = pkt.Children[0].ByteValue - refreshDeletes = pkt.Children[1].Value.(bool) - } - return &ControlSyncDone{ - Criticality: false, - Cookie: cookie, - RefreshDeletes: refreshDeletes, - }, nil -} - -// GetControlType returns the OID -func (c *ControlSyncDone) GetControlType() string { - return ControlTypeSyncDone -} - -// Encode encodes the control -func (c *ControlSyncDone) Encode() *ber.Packet { - return nil -} - -// String returns a human-readable description -func (c *ControlSyncDone) String() string { - return fmt.Sprintf( - "Control Type: %s (%q) Criticality: %t Cookie: %s RefreshDeletes: %t", - ControlTypeMap[ControlTypeSyncDone], - ControlTypeSyncDone, - c.Criticality, - string(c.Cookie), - c.RefreshDeletes, - ) -} - -// Tag For ControlSyncInfo -type ControlSyncInfoValue uint64 - -const ( - SyncInfoNewcookie ControlSyncInfoValue = 0 - SyncInfoRefreshDelete ControlSyncInfoValue = 1 - SyncInfoRefreshPresent ControlSyncInfoValue = 2 - SyncInfoSyncIdSet ControlSyncInfoValue = 3 -) - -// ControlSyncInfoNewCookie implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt -type ControlSyncInfoNewCookie struct { - Cookie []byte -} - -// String returns a human-readable description -func (c *ControlSyncInfoNewCookie) String() string { - return fmt.Sprintf( - "NewCookie[Cookie: %s]", - string(c.Cookie), - ) -} - -// ControlSyncInfoRefreshDelete implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt -type ControlSyncInfoRefreshDelete struct { - Cookie []byte - RefreshDone bool -} - -// String returns a human-readable description -func (c *ControlSyncInfoRefreshDelete) String() string { - return fmt.Sprintf( - "RefreshDelete[Cookie: %s RefreshDone: %t]", - string(c.Cookie), - c.RefreshDone, - ) -} - -// ControlSyncInfoRefreshPresent implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt -type ControlSyncInfoRefreshPresent struct { - Cookie []byte - RefreshDone bool -} - -// String returns a human-readable description -func (c *ControlSyncInfoRefreshPresent) String() string { - return fmt.Sprintf( - "RefreshPresent[Cookie: %s RefreshDone: %t]", - string(c.Cookie), - c.RefreshDone, - ) -} - -// ControlSyncInfoSyncIdSet implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt -type ControlSyncInfoSyncIdSet struct { - Cookie []byte - RefreshDeletes bool - SyncUUIDs []uuid.UUID -} - -// String returns a human-readable description -func (c *ControlSyncInfoSyncIdSet) String() string { - return fmt.Sprintf( - "SyncIdSet[Cookie: %s RefreshDeletes: %t SyncUUIDs: %v]", - string(c.Cookie), - c.RefreshDeletes, - c.SyncUUIDs, - ) -} - -// ControlSyncInfo implements the Sync Info Control described in https://www.ietf.org/rfc/rfc4533.txt -type ControlSyncInfo struct { - Criticality bool - Value ControlSyncInfoValue - NewCookie *ControlSyncInfoNewCookie - RefreshDelete *ControlSyncInfoRefreshDelete - RefreshPresent *ControlSyncInfoRefreshPresent - SyncIdSet *ControlSyncInfoSyncIdSet -} - -func NewControlSyncInfo(pkt *ber.Packet) (*ControlSyncInfo, error) { - var ( - cookie []byte - refreshDone = true - refreshDeletes bool - syncUUIDs []uuid.UUID - ) - c := &ControlSyncInfo{Criticality: false} - switch ControlSyncInfoValue(pkt.Identifier.Tag) { - case SyncInfoNewcookie: - c.Value = SyncInfoNewcookie - c.NewCookie = &ControlSyncInfoNewCookie{ - Cookie: pkt.ByteValue, - } - case SyncInfoRefreshDelete: - c.Value = SyncInfoRefreshDelete - switch len(pkt.Children) { - case 0: - // have nothing to do - case 1: - cookie = pkt.Children[0].ByteValue - case 2: - cookie = pkt.Children[0].ByteValue - refreshDone = pkt.Children[1].Value.(bool) - } - c.RefreshDelete = &ControlSyncInfoRefreshDelete{ - Cookie: cookie, - RefreshDone: refreshDone, - } - case SyncInfoRefreshPresent: - c.Value = SyncInfoRefreshPresent - switch len(pkt.Children) { - case 0: - // have nothing to do - case 1: - cookie = pkt.Children[0].ByteValue - case 2: - cookie = pkt.Children[0].ByteValue - refreshDone = pkt.Children[1].Value.(bool) - } - c.RefreshPresent = &ControlSyncInfoRefreshPresent{ - Cookie: cookie, - RefreshDone: refreshDone, - } - case SyncInfoSyncIdSet: - c.Value = SyncInfoSyncIdSet - switch len(pkt.Children) { - case 0: - // have nothing to do - case 1: - cookie = pkt.Children[0].ByteValue - case 2: - cookie = pkt.Children[0].ByteValue - refreshDeletes = pkt.Children[1].Value.(bool) - case 3: - cookie = pkt.Children[0].ByteValue - refreshDeletes = pkt.Children[1].Value.(bool) - syncUUIDs = make([]uuid.UUID, 0, len(pkt.Children[2].Children)) - for _, child := range pkt.Children[2].Children { - u, err := uuid.FromBytes(child.ByteValue) - if err != nil { - return nil, fmt.Errorf("failed to decode uuid: %w", err) - } - syncUUIDs = append(syncUUIDs, u) - } - } - c.SyncIdSet = &ControlSyncInfoSyncIdSet{ - Cookie: cookie, - RefreshDeletes: refreshDeletes, - SyncUUIDs: syncUUIDs, - } - default: - return nil, fmt.Errorf("unknown sync info value: %d", pkt.Identifier.Tag) - } - return c, nil -} - -// GetControlType returns the OID -func (c *ControlSyncInfo) GetControlType() string { - return ControlTypeSyncInfo -} - -// Encode encodes the control -func (c *ControlSyncInfo) Encode() *ber.Packet { - return nil -} - -// String returns a human-readable description -func (c *ControlSyncInfo) String() string { - return fmt.Sprintf( - "Control Type: %s (%q) Criticality: %t Value: %d %s %s %s %s", - ControlTypeMap[ControlTypeSyncInfo], - ControlTypeSyncInfo, - c.Criticality, - c.Value, - c.NewCookie, - c.RefreshDelete, - c.RefreshPresent, - c.SyncIdSet, - ) -} diff --git a/control_test.go b/control_test.go deleted file mode 100644 index 5f43ccb..0000000 --- a/control_test.go +++ /dev/null @@ -1,274 +0,0 @@ -package ldap - -import ( - "bytes" - "fmt" - "reflect" - "runtime" - "testing" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -func TestControlPaging(t *testing.T) { - runControlTest(t, NewControlPaging(0)) - runControlTest(t, NewControlPaging(100)) -} - -func TestControlManageDsaIT(t *testing.T) { - runControlTest(t, NewControlManageDsaIT(true)) - runControlTest(t, NewControlManageDsaIT(false)) -} - -func TestControlMicrosoftNotification(t *testing.T) { - runControlTest(t, NewControlMicrosoftNotification()) -} - -func TestControlMicrosoftShowDeleted(t *testing.T) { - runControlTest(t, NewControlMicrosoftShowDeleted()) -} - -func TestControlMicrosoftServerLinkTTL(t *testing.T) { - runControlTest(t, NewControlMicrosoftServerLinkTTL()) -} - -func TestControlSubtreeDelete(t *testing.T) { - runControlTest(t, NewControlSubtreeDelete()) -} - -func TestControlString(t *testing.T) { - runControlTest(t, NewControlString("x", true, "y")) - runControlTest(t, NewControlString("x", true, "")) - runControlTest(t, NewControlString("x", false, "y")) - runControlTest(t, NewControlString("x", false, "")) -} - -func TestControlDirSync(t *testing.T) { - runControlTest(t, NewRequestControlDirSync(DirSyncObjectSecurity, 1000, nil)) - runControlTest(t, NewRequestControlDirSync(DirSyncObjectSecurity, 1000, []byte("I'm a cookie!"))) -} - -func runControlTest(t *testing.T, originalControl Control) { - header := "" - if callerpc, _, line, ok := runtime.Caller(1); ok { - if caller := runtime.FuncForPC(callerpc); caller != nil { - header = fmt.Sprintf("%s:%d: ", caller.Name(), line) - } - } - - encodedPacket := originalControl.Encode() - encodedBytes := encodedPacket.Bytes() - - // Decode directly from the encoded packet (ensures Value is correct) - fromPacket, err := DecodeControl(encodedPacket) - if err != nil { - t.Errorf("%sdecoding encoded bytes control failed: %s", header, err) - } - if !bytes.Equal(encodedBytes, fromPacket.Encode().Bytes()) { - t.Errorf("%sround-trip from encoded packet failed", header) - } - if reflect.TypeOf(originalControl) != reflect.TypeOf(fromPacket) { - t.Errorf("%sgot different type decoding from encoded packet: %T vs %T", header, fromPacket, originalControl) - } - - // Decode from the wire bytes (ensures ber-encoding is correct) - pkt, err := ber.DecodePacketErr(encodedBytes) - if err != nil { - t.Errorf("%sdecoding encoded bytes failed: %s", header, err) - } - fromBytes, err := DecodeControl(pkt) - if err != nil { - t.Errorf("%sdecoding control failed: %s", header, err) - } - if !bytes.Equal(encodedBytes, fromBytes.Encode().Bytes()) { - t.Errorf("%sround-trip from encoded bytes failed", header) - } - if reflect.TypeOf(originalControl) != reflect.TypeOf(fromPacket) { - t.Errorf("%sgot different type decoding from encoded bytes: %T vs %T", header, fromBytes, originalControl) - } -} - -func TestDescribeControlManageDsaIT(t *testing.T) { - runAddControlDescriptions(t, NewControlManageDsaIT(false), "Control Type (Manage DSA IT)") - runAddControlDescriptions(t, NewControlManageDsaIT(true), "Control Type (Manage DSA IT)", "Criticality") -} - -func TestDescribeControlPaging(t *testing.T) { - runAddControlDescriptions(t, NewControlPaging(100), "Control Type (Paging)", "Control Value (Paging)") - runAddControlDescriptions(t, NewControlPaging(0), "Control Type (Paging)", "Control Value (Paging)") -} - -func TestDescribeControlSubtreeDelete(t *testing.T) { - runAddControlDescriptions(t, NewControlSubtreeDelete(), "Control Type (Subtree Delete Control)") -} - -func TestDescribeControlMicrosoftNotification(t *testing.T) { - runAddControlDescriptions(t, NewControlMicrosoftNotification(), "Control Type (Change Notification - Microsoft)") -} - -func TestDescribeControlMicrosoftShowDeleted(t *testing.T) { - runAddControlDescriptions(t, NewControlMicrosoftShowDeleted(), "Control Type (Show Deleted Objects - Microsoft)") -} - -func TestDescribeControlMicrosoftServerLinkTTL(t *testing.T) { - runAddControlDescriptions(t, NewControlMicrosoftServerLinkTTL(), "Control Type (Return TTL-DNs for link values with associated expiry times - Microsoft)") -} - -func TestDescribeControlString(t *testing.T) { - runAddControlDescriptions(t, NewControlString("x", true, "y"), "Control Type ()", "Criticality", "Control Value") - runAddControlDescriptions(t, NewControlString("x", true, ""), "Control Type ()", "Criticality") - runAddControlDescriptions(t, NewControlString("x", false, "y"), "Control Type ()", "Control Value") - runAddControlDescriptions(t, NewControlString("x", false, ""), "Control Type ()") -} - -func TestDescribeControlDirSync(t *testing.T) { - runAddControlDescriptions(t, NewRequestControlDirSync(DirSyncObjectSecurity, 1000, nil), "Control Type (DirSync)", "Criticality", "Control Value") -} - -func runAddControlDescriptions(t *testing.T, originalControl Control, childDescriptions ...string) { - header := "" - if callerpc, _, line, ok := runtime.Caller(1); ok { - if caller := runtime.FuncForPC(callerpc); caller != nil { - header = fmt.Sprintf("%s:%d: ", caller.Name(), line) - } - } - - encodedControls := encodeControls([]Control{originalControl}) - _ = addControlDescriptions(encodedControls) - encodedPacket := encodedControls.Children[0] - if len(encodedPacket.Children) != len(childDescriptions) { - t.Errorf("%sinvalid number of children: %d != %d", header, len(encodedPacket.Children), len(childDescriptions)) - } - for i, desc := range childDescriptions { - if encodedPacket.Children[i].Description != desc { - t.Errorf("%sdescription not as expected: %s != %s", header, encodedPacket.Children[i].Description, desc) - } - } -} - -func TestDecodeControl(t *testing.T) { - type args struct { - packet *ber.Packet - } - - tests := []struct { - name string - args args - want Control - wantErr bool - }{ - { - name: "timeBeforeExpiration", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x29, 0x30, 0x27, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0xa, 0x30, 0x8, 0xa0, 0x6, 0x80, 0x4, 0x7f, 0xff, 0xf6, 0x5c})}, - want: &ControlBeheraPasswordPolicy{Expire: 2147481180, Grace: -1, Error: -1, ErrorString: ""}, wantErr: false, - }, - { - name: "graceAuthNsRemaining", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x26, 0x30, 0x24, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x7, 0x30, 0x5, 0xa0, 0x3, 0x81, 0x1, 0x11})}, - want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: 17, Error: -1, ErrorString: ""}, wantErr: false, - }, - { - name: "passwordExpired", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x0})}, - want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 0, ErrorString: "Password expired"}, wantErr: false, - }, - { - name: "accountLocked", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x1})}, - want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 1, ErrorString: "Account locked"}, wantErr: false, - }, - { - name: "passwordModNotAllowed", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x3})}, - want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 3, ErrorString: "Policy prevents password modification"}, wantErr: false, - }, - { - name: "mustSupplyOldPassword", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x4})}, - want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 4, ErrorString: "Policy requires old password in order to change password"}, wantErr: false, - }, - { - name: "insufficientPasswordQuality", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x5})}, - want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 5, ErrorString: "Password fails quality checks"}, wantErr: false, - }, - { - name: "passwordTooShort", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x6})}, - want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 6, ErrorString: "Password is too short for policy"}, wantErr: false, - }, - { - name: "passwordTooYoung", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x7})}, - want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 7, ErrorString: "Password has been changed too recently"}, wantErr: false, - }, - { - name: "passwordInHistory", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x8})}, - want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 8, ErrorString: "New password is in list of old passwords"}, wantErr: false, - }, - } - for i := range tests { - err := addControlDescriptions(tests[i].args.packet) - if err != nil { - t.Fatal(err) - } - tests[i].args.packet = tests[i].args.packet.Children[0] - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := DecodeControl(tt.args.packet) - if (err != nil) != tt.wantErr { - t.Errorf("DecodeControl() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("DecodeControl() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestControlServerSideSortingDecoding(t *testing.T) { - control := NewControlServerSideSortingWithSortKeys([]*SortKey{{ - MatchingRule: "foo", - AttributeType: "foobar", - Reverse: true, - }, { - MatchingRule: "foo", - AttributeType: "foobar", - Reverse: false, - }, { - MatchingRule: "", - AttributeType: "", - Reverse: false, - }, { - MatchingRule: "totoRule", - AttributeType: "", - Reverse: false, - }, { - MatchingRule: "", - AttributeType: "totoType", - Reverse: false, - }}) - - controlDecoded, err := NewControlServerSideSorting(control.Encode()) - if err != nil { - t.Fatal(err) - } - - if control.GetControlType() != controlDecoded.GetControlType() { - t.Fatalf("control type mismatch: control:%s - decoded:%s", control.GetControlType(), controlDecoded.GetControlType()) - } - - if len(control.SortKeys) != len(controlDecoded.SortKeys) { - t.Fatalf("sort keys length mismatch (control: %d - decoded: %d)", len(control.SortKeys), len(controlDecoded.SortKeys)) - } - - for i, sk := range control.SortKeys { - dsk := controlDecoded.SortKeys[i] - - if sk.AttributeType != dsk.AttributeType { - t.Fatalf("attribute type mismatch for sortkey %d", i) - } - - if sk.MatchingRule != dsk.MatchingRule { - t.Fatalf("matching rule mismatch for sortkey %d", i) - } - - if sk.Reverse != dsk.Reverse { - t.Fatalf("reverse mismtach for sortkey %d", i) - } - } -} diff --git a/debug.go b/debug.go deleted file mode 100644 index 6f89b4a..0000000 --- a/debug.go +++ /dev/null @@ -1,28 +0,0 @@ -package ldap - -import ( - ber "github.com/go-asn1-ber/asn1-ber" -) - -// debugging type -// - has a Printf method to write the debug output -type debugging bool - -// Enable controls debugging mode. -func (debug *debugging) Enable(b bool) { - *debug = debugging(b) -} - -// Printf writes debug output. -func (debug debugging) Printf(format string, args ...interface{}) { - if debug { - logger.Printf(format, args...) - } -} - -// PrintPacket dumps a packet. -func (debug debugging) PrintPacket(packet *ber.Packet) { - if debug { - ber.WritePacket(logger.Writer(), packet) - } -} diff --git a/del.go b/del.go deleted file mode 100644 index 6230695..0000000 --- a/del.go +++ /dev/null @@ -1,59 +0,0 @@ -package ldap - -import ( - "fmt" - ber "github.com/go-asn1-ber/asn1-ber" -) - -// DelRequest implements an LDAP deletion request -type DelRequest struct { - // DN is the name of the directory entry to delete - DN string - // Controls hold optional controls to send with the request - Controls []Control -} - -func (req *DelRequest) appendTo(envelope *ber.Packet) error { - pkt := ber.Encode(ber.ClassApplication, ber.TypePrimitive, ApplicationDelRequest, req.DN, "Del Request") - pkt.Data.Write([]byte(req.DN)) - - envelope.AppendChild(pkt) - if len(req.Controls) > 0 { - envelope.AppendChild(encodeControls(req.Controls)) - } - - return nil -} - -// NewDelRequest creates a delete request for the given DN and controls -func NewDelRequest(DN string, Controls []Control) *DelRequest { - return &DelRequest{ - DN: DN, - Controls: Controls, - } -} - -// Del executes the given delete request -func (l *Conn) Del(delRequest *DelRequest) error { - msgCtx, err := l.doRequest(delRequest) - if err != nil { - return err - } - defer l.finishMessage(msgCtx) - - packet, err := l.readPacket(msgCtx) - if err != nil { - return err - } - - if packet.Children[1].Tag == ApplicationDelResponse { - err := GetLDAPError(packet) - if err != nil { - return err - } - } else { - return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) - } - - return nil -} diff --git a/dn.go b/dn.go deleted file mode 100644 index 6520b8e..0000000 --- a/dn.go +++ /dev/null @@ -1,468 +0,0 @@ -package ldap - -import ( - "encoding/hex" - "errors" - "fmt" - ber "github.com/go-asn1-ber/asn1-ber" - "sort" - "strings" - "unicode" - "unicode/utf8" -) - -// AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514 -type AttributeTypeAndValue struct { - // Type is the attribute type - Type string - // Value is the attribute value - Value string -} - -func (a *AttributeTypeAndValue) setType(str string) error { - result, err := decodeString(str) - if err != nil { - return err - } - a.Type = result - - return nil -} - -func (a *AttributeTypeAndValue) setValue(s string) error { - // https://www.ietf.org/rfc/rfc4514.html#section-2.4 - // If the AttributeType is of the dotted-decimal form, the - // AttributeValue is represented by an number sign ('#' U+0023) - // character followed by the hexadecimal encoding of each of the octets - // of the BER encoding of the X.500 AttributeValue. - if len(s) > 0 && s[0] == '#' { - decodedString, err := decodeEncodedString(s[1:]) - if err != nil { - return err - } - - a.Value = decodedString - return nil - } else { - decodedString, err := decodeString(s) - if err != nil { - return err - } - - a.Value = decodedString - return nil - } -} - -// String returns a normalized string representation of this attribute type and -// value pair which is the lowercase join of the Type and Value with a "=". -func (a *AttributeTypeAndValue) String() string { - return encodeString(foldString(a.Type), false) + "=" + encodeString(a.Value, true) -} - -// RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514 -type RelativeDN struct { - Attributes []*AttributeTypeAndValue -} - -// String returns a normalized string representation of this relative DN which -// is the a join of all attributes (sorted in increasing order) with a "+". -func (r *RelativeDN) String() string { - attrs := make([]string, len(r.Attributes)) - for i := range r.Attributes { - attrs[i] = r.Attributes[i].String() - } - sort.Strings(attrs) - return strings.Join(attrs, "+") -} - -// DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514 -type DN struct { - RDNs []*RelativeDN -} - -// String returns a normalized string representation of this DN which is the -// join of all relative DNs with a ",". -func (d *DN) String() string { - rdns := make([]string, len(d.RDNs)) - for i := range d.RDNs { - rdns[i] = d.RDNs[i].String() - } - return strings.Join(rdns, ",") -} - -func stripLeadingAndTrailingSpaces(inVal string) string { - noSpaces := strings.Trim(inVal, " ") - - // Re-add the trailing space if it was an escaped space - if len(noSpaces) > 0 && noSpaces[len(noSpaces)-1] == '\\' && inVal[len(inVal)-1] == ' ' { - noSpaces = noSpaces + " " - } - - return noSpaces -} - -// Remove leading and trailing spaces from the attribute type and value -// and unescape any escaped characters in these fields -// -// decodeString is based on https://github.com/inteon/cert-manager/blob/ed280d28cd02b262c5db46054d88e70ab518299c/pkg/util/pki/internal/dn.go#L170 -func decodeString(str string) (string, error) { - s := []rune(stripLeadingAndTrailingSpaces(str)) - - builder := strings.Builder{} - for i := 0; i < len(s); i++ { - char := s[i] - - // If the character is not an escape character, just add it to the - // builder and continue - if char != '\\' { - builder.WriteRune(char) - continue - } - - // If the escape character is the last character, it's a corrupted - // escaped character - if i+1 >= len(s) { - return "", fmt.Errorf("got corrupted escaped character: '%s'", string(s)) - } - - // If the escaped character is a special character, just add it to - // the builder and continue - switch s[i+1] { - case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\': - builder.WriteRune(s[i+1]) - i++ - continue - } - - // If the escaped character is not a special character, it should - // be a hex-encoded character of the form \XX if it's not at least - // two characters long, it's a corrupted escaped character - if i+2 >= len(s) { - return "", errors.New("failed to decode escaped character: encoding/hex: invalid byte: " + string(s[i+1])) - } - - // Get the runes for the two characters after the escape character - // and convert them to a byte slice - xx := []byte(string(s[i+1 : i+3])) - - // If the two runes are not hex characters and result in more than - // two bytes when converted to a byte slice, it's a corrupted - // escaped character - if len(xx) != 2 { - return "", errors.New("failed to decode escaped character: invalid byte: " + string(xx)) - } - - // Decode the hex-encoded character and add it to the builder - dst := []byte{0} - if n, err := hex.Decode(dst, xx); err != nil { - return "", errors.New("failed to decode escaped character: " + err.Error()) - } else if n != 1 { - return "", fmt.Errorf("failed to decode escaped character: encoding/hex: expected 1 byte when un-escaping, got %d", n) - } - - builder.WriteByte(dst[0]) - i += 2 - } - - return builder.String(), nil -} - -// Escape a string according to RFC 4514 -func encodeString(value string, isValue bool) string { - builder := strings.Builder{} - - escapeChar := func(c byte) { - builder.WriteByte('\\') - builder.WriteByte(c) - } - - escapeHex := func(c byte) { - builder.WriteByte('\\') - builder.WriteString(hex.EncodeToString([]byte{c})) - } - - // Loop through each byte and escape as necessary. - // Runes that take up more than one byte are escaped - // byte by byte (since both bytes are non-ASCII). - for i := 0; i < len(value); i++ { - char := value[i] - if i == 0 && (char == ' ' || char == '#') { - // Special case leading space or number sign. - escapeChar(char) - continue - } - if i == len(value)-1 && char == ' ' { - // Special case trailing space. - escapeChar(char) - continue - } - - switch char { - case '"', '+', ',', ';', '<', '>', '\\': - // Each of these special characters must be escaped. - escapeChar(char) - continue - } - - if !isValue && char == '=' { - // Equal signs have to be escaped only in the type part of - // the attribute type and value pair. - escapeChar(char) - continue - } - - if char < ' ' || char > '~' { - // All special character escapes are handled first - // above. All bytes less than ASCII SPACE and all bytes - // greater than ASCII TILDE must be hex-escaped. - escapeHex(char) - continue - } - - // Any other character does not require escaping. - builder.WriteByte(char) - } - - return builder.String() -} - -func decodeEncodedString(str string) (string, error) { - decoded, err := hex.DecodeString(str) - if err != nil { - return "", fmt.Errorf("failed to decode BER encoding: %w", err) - } - - packet, err := ber.DecodePacketErr(decoded) - if err != nil { - return "", fmt.Errorf("failed to decode BER encoding: %w", err) - } - - return packet.Data.String(), nil -} - -// ParseDN returns a distinguishedName or an error. -// The function respects https://tools.ietf.org/html/rfc4514 -func ParseDN(str string) (*DN, error) { - var dn = &DN{RDNs: make([]*RelativeDN, 0)} - if strings.TrimSpace(str) == "" { - return dn, nil - } - - var ( - rdn = &RelativeDN{} - attr = &AttributeTypeAndValue{} - escaping bool - startPos int - appendAttributesToRDN = func(end bool) { - rdn.Attributes = append(rdn.Attributes, attr) - attr = &AttributeTypeAndValue{} - if end { - dn.RDNs = append(dn.RDNs, rdn) - rdn = &RelativeDN{} - } - } - ) - - // Loop through each character in the string and - // build up the attribute type and value pairs. - // We only check for ascii characters here, which - // allows us to iterate over the string byte by byte. - for i := 0; i < len(str); i++ { - char := str[i] - switch { - case escaping: - escaping = false - case char == '\\': - escaping = true - case char == '=' && len(attr.Type) == 0: - if err := attr.setType(str[startPos:i]); err != nil { - return nil, err - } - startPos = i + 1 - case char == ',' || char == '+' || char == ';': - if len(attr.Type) == 0 { - return dn, errors.New("incomplete type, value pair") - } - if err := attr.setValue(str[startPos:i]); err != nil { - return nil, err - } - - startPos = i + 1 - last := char == ',' || char == ';' - appendAttributesToRDN(last) - } - } - - if len(attr.Type) == 0 { - return dn, errors.New("DN ended with incomplete type, value pair") - } - - if err := attr.setValue(str[startPos:]); err != nil { - return dn, err - } - appendAttributesToRDN(true) - - return dn, nil -} - -// Equal returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). -// Returns true if they have the same number of relative distinguished names -// and corresponding relative distinguished names (by position) are the same. -func (d *DN) Equal(other *DN) bool { - if len(d.RDNs) != len(other.RDNs) { - return false - } - for i := range d.RDNs { - if !d.RDNs[i].Equal(other.RDNs[i]) { - return false - } - } - return true -} - -// AncestorOf returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN. -// "ou=widgets,o=acme.com" is an ancestor of "ou=sprockets,ou=widgets,o=acme.com" -// "ou=widgets,o=acme.com" is not an ancestor of "ou=sprockets,ou=widgets,o=foo.com" -// "ou=widgets,o=acme.com" is not an ancestor of "ou=widgets,o=acme.com" -func (d *DN) AncestorOf(other *DN) bool { - if len(d.RDNs) >= len(other.RDNs) { - return false - } - // Take the last `len(d.RDNs)` RDNs from the other DN to compare against - otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):] - for i := range d.RDNs { - if !d.RDNs[i].Equal(otherRDNs[i]) { - return false - } - } - return true -} - -// Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). -// Relative distinguished names are the same if and only if they have the same number of AttributeTypeAndValues -// and each attribute of the first RDN is the same as the attribute of the second RDN with the same attribute type. -// The order of attributes is not significant. -// Case of attribute types is not significant. -func (r *RelativeDN) Equal(other *RelativeDN) bool { - if len(r.Attributes) != len(other.Attributes) { - return false - } - return r.hasAllAttributes(other.Attributes) && other.hasAllAttributes(r.Attributes) -} - -func (r *RelativeDN) hasAllAttributes(attrs []*AttributeTypeAndValue) bool { - for _, attr := range attrs { - found := false - for _, myattr := range r.Attributes { - if myattr.Equal(attr) { - found = true - break - } - } - if !found { - return false - } - } - return true -} - -// Equal returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue -// Case of the attribute type is not significant -func (a *AttributeTypeAndValue) Equal(other *AttributeTypeAndValue) bool { - return strings.EqualFold(a.Type, other.Type) && a.Value == other.Value -} - -// EqualFold returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). -// Returns true if they have the same number of relative distinguished names -// and corresponding relative distinguished names (by position) are the same. -// Case of the attribute type and value is not significant -func (d *DN) EqualFold(other *DN) bool { - if len(d.RDNs) != len(other.RDNs) { - return false - } - for i := range d.RDNs { - if !d.RDNs[i].EqualFold(other.RDNs[i]) { - return false - } - } - return true -} - -// AncestorOfFold returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN. -// Case of the attribute type and value is not significant -func (d *DN) AncestorOfFold(other *DN) bool { - if len(d.RDNs) >= len(other.RDNs) { - return false - } - // Take the last `len(d.RDNs)` RDNs from the other DN to compare against - otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):] - for i := range d.RDNs { - if !d.RDNs[i].EqualFold(otherRDNs[i]) { - return false - } - } - return true -} - -// EqualFold returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). -// Case of the attribute type is not significant -func (r *RelativeDN) EqualFold(other *RelativeDN) bool { - if len(r.Attributes) != len(other.Attributes) { - return false - } - return r.hasAllAttributesFold(other.Attributes) && other.hasAllAttributesFold(r.Attributes) -} - -func (r *RelativeDN) hasAllAttributesFold(attrs []*AttributeTypeAndValue) bool { - for _, attr := range attrs { - found := false - for _, myattr := range r.Attributes { - if myattr.EqualFold(attr) { - found = true - break - } - } - if !found { - return false - } - } - return true -} - -// EqualFold returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue -// Case of the attribute type and value is not significant -func (a *AttributeTypeAndValue) EqualFold(other *AttributeTypeAndValue) bool { - return strings.EqualFold(a.Type, other.Type) && strings.EqualFold(a.Value, other.Value) -} - -// foldString returns a folded string such that foldString(x) == foldString(y) -// is identical to bytes.EqualFold(x, y). -// based on https://go.dev/src/encoding/json/fold.go -func foldString(s string) string { - builder := strings.Builder{} - for _, char := range s { - // Handle single-byte ASCII. - if char < utf8.RuneSelf { - if 'A' <= char && char <= 'Z' { - char += 'a' - 'A' - } - builder.WriteRune(char) - continue - } - - builder.WriteRune(foldRune(char)) - } - return builder.String() -} - -// foldRune is returns the smallest rune for all runes in the same fold set. -func foldRune(r rune) rune { - for { - r2 := unicode.SimpleFold(r) - if r2 <= r { - return r - } - r = r2 - } -} diff --git a/dn_test.go b/dn_test.go deleted file mode 100644 index da367d8..0000000 --- a/dn_test.go +++ /dev/null @@ -1,418 +0,0 @@ -package ldap - -import ( - "reflect" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSuccessfulDNParsing(t *testing.T) { - testcases := map[string]DN{ - "": {[]*RelativeDN{}}, - "cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"cn", "Jim, \"Hasse Hö\" Hansson!"}}}, - {[]*AttributeTypeAndValue{{"dc", "dummy"}}}, - {[]*AttributeTypeAndValue{{"dc", "com"}}}, - }}, - "UID=jsmith,DC=example,DC=net": {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"UID", "jsmith"}}}, - {[]*AttributeTypeAndValue{{"DC", "example"}}}, - {[]*AttributeTypeAndValue{{"DC", "net"}}}, - }}, - "OU=Sales+CN=J. Smith,DC=example,DC=net": {[]*RelativeDN{ - {[]*AttributeTypeAndValue{ - {"OU", "Sales"}, - {"CN", "J. Smith"}, - }}, - {[]*AttributeTypeAndValue{{"DC", "example"}}}, - {[]*AttributeTypeAndValue{{"DC", "net"}}}, - }}, - "1.3.6.1.4.1.1466.0=#04024869": {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}}, - }}, - "1.3.6.1.4.1.1466.0=#04024869,DC=net": {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}}, - {[]*AttributeTypeAndValue{{"DC", "net"}}}, - }}, - "CN=Lu\\C4\\8Di\\C4\\87": {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"CN", "Lučić"}}}, - }}, - " CN = Lu\\C4\\8Di\\C4\\87 ": {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"CN", "Lučić"}}}, - }}, - ` A = 1 , B = 2 `: {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"A", "1"}}}, - {[]*AttributeTypeAndValue{{"B", "2"}}}, - }}, - ` A = 1 + B = 2 `: {[]*RelativeDN{ - {[]*AttributeTypeAndValue{ - {"A", "1"}, - {"B", "2"}, - }}, - }}, - ` \ \ A\ \ = \ \ 1\ \ , \ \ B\ \ = \ \ 2\ \ `: {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{" A ", " 1 "}}}, - {[]*AttributeTypeAndValue{{" B ", " 2 "}}}, - }}, - ` \ \ A\ \ = \ \ 1\ \ + \ \ B\ \ = \ \ 2\ \ `: {[]*RelativeDN{ - {[]*AttributeTypeAndValue{ - {" A ", " 1 "}, - {" B ", " 2 "}, - }}, - }}, - "A = 88 \t": {[]*RelativeDN{ - {[]*AttributeTypeAndValue{ - {"A", "88 \t"}, - }}, - }}, - "A = 88 \n": {[]*RelativeDN{ - {[]*AttributeTypeAndValue{ - {"A", "88 \n"}, - }}, - }}, - `cn=john.doe;dc=example,dc=net`: {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"cn", "john.doe"}}}, - {[]*AttributeTypeAndValue{{"dc", "example"}}}, - {[]*AttributeTypeAndValue{{"dc", "net"}}}, - }}, - `cn=⭐;dc=❤️=\==,dc=❤️\\`: {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"cn", "⭐"}}}, - {[]*AttributeTypeAndValue{{"dc", "❤️==="}}}, - {[]*AttributeTypeAndValue{{"dc", "❤️\\"}}}, - }}, - - // Escaped `;` should not be treated as RDN - `cn=john.doe\;weird name,dc=example,dc=net`: {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"cn", "john.doe;weird name"}}}, - {[]*AttributeTypeAndValue{{"dc", "example"}}}, - {[]*AttributeTypeAndValue{{"dc", "net"}}}, - }}, - `cn=ZXhhbXBsZVRleHQ=,dc=dummy,dc=com`: {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"cn", "ZXhhbXBsZVRleHQ="}}}, - {[]*AttributeTypeAndValue{{"dc", "dummy"}}}, - {[]*AttributeTypeAndValue{{"dc", "com"}}}, - }}, - `1.3.6.1.4.1.1466.0=test`: {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "test"}}}, - }}, - `1=#04024869`: {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"1", "Hi"}}}, - }}, - `CN=James \"Jim\" Smith\, III,DC=example,DC=net`: {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"CN", "James \"Jim\" Smith, III"}}}, - {[]*AttributeTypeAndValue{{"DC", "example"}}}, - {[]*AttributeTypeAndValue{{"DC", "net"}}}, - }}, - `CN=Before\0dAfter,DC=example,DC=net`: {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"CN", "Before\x0dAfter"}}}, - {[]*AttributeTypeAndValue{{"DC", "example"}}}, - {[]*AttributeTypeAndValue{{"DC", "net"}}}, - }}, - `cn=foo-lon\e2\9d\a4\ef\b8\8f\,g.com,OU=Foo===Long;ou=Ba # rq,ou=Baz,o=C\; orp.+c=US`: {[]*RelativeDN{ - {[]*AttributeTypeAndValue{{"cn", "foo-lon❤️,g.com"}}}, - {[]*AttributeTypeAndValue{{"OU", "Foo===Long"}}}, - {[]*AttributeTypeAndValue{{"ou", "Ba # rq"}}}, - {[]*AttributeTypeAndValue{{"ou", "Baz"}}}, - {[]*AttributeTypeAndValue{{"o", "C; orp."}, {"c", "US"}}}, - }}, - } - - for test, answer := range testcases { - dn, err := ParseDN(test) - if err != nil { - t.Errorf("ParseDN failed for DN test '%s': %s", test, err) - continue - } - if !reflect.DeepEqual(dn, &answer) { - t.Errorf("Parsed DN '%s' is not equal to the expected structure", test) - t.Logf("Expected:") - for _, rdn := range answer.RDNs { - for _, attribute := range rdn.Attributes { - t.Logf(" #%v\n", attribute) - } - } - t.Logf("Actual:") - for _, rdn := range dn.RDNs { - for _, attribute := range rdn.Attributes { - t.Logf(" #%v\n", attribute) - } - } - } - } -} - -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": "failed to decode escaped character: encoding/hex: invalid byte: 0", - "DC=example,=net": "DN ended with incomplete type, value pair", - "1=#0402486": "failed to decode BER encoding: encoding/hex: odd length hex string", - "test,DC=example,DC=com": "incomplete type, value pair", - "=test,DC=example,DC=com": "incomplete type, value pair", - "1.3.6.1.4.1.1466.0=test+": "DN ended with incomplete type, value pair", - `1.3.6.1.4.1.1466.0=test;`: "DN ended with incomplete type, value pair", - "1.3.6.1.4.1.1466.0=test+,": "incomplete type, value pair", - "DF=#6666666666665006838820013100000746939546349182108463491821809FBFFFFFFFFF": "failed to decode BER encoding: unexpected EOF", - } - - for test, answer := range testcases { - _, err := ParseDN(test) - if err == nil { - t.Errorf("Expected '%s' to fail parsing but succeeded\n", test) - } else if err.Error() != answer { - t.Errorf("Unexpected error on: '%s':\nExpected: %s\nGot: %s\n", test, answer, err.Error()) - } - } -} - -func TestDNEqual(t *testing.T) { - testcases := []struct { - A string - B string - Equal bool - }{ - // Exact match - {"", "", true}, - {"o=A", "o=A", true}, - {"o=A", "o=B", false}, - - {"o=A,o=B", "o=A,o=B", true}, - {"o=A,o=B", "o=A,o=C", false}, - - {"o=A+o=B", "o=A+o=B", true}, - {"o=A+o=B", "o=A+o=C", false}, - - // Case mismatch in type is ignored - {"o=A", "O=A", true}, - {"o=A,o=B", "o=A,O=B", true}, - {"o=A+o=B", "o=A+O=B", true}, - - // Case mismatch in value is significant - {"o=a", "O=A", false}, - {"o=a,o=B", "o=A,O=B", false}, - {"o=a+o=B", "o=A+O=B", false}, - - // Multi-valued RDN order mismatch is ignored - {"o=A+o=B", "O=B+o=A", true}, - // Number of RDN attributes is significant - {"o=A+o=B", "O=B+o=A+O=B", false}, - - // Missing values are significant - {"o=A+o=B", "O=B+o=A+O=C", false}, // missing values matter - {"o=A+o=B+o=C", "O=B+o=A", false}, // missing values matter - - // Whitespace tests - // Matching - { - "cn=John Doe, ou=People, dc=sun.com", - "cn=John Doe, ou=People, dc=sun.com", - true, - }, - // Difference in leading/trailing chars is ignored - { - "cn=\\ John\\20Doe, ou=People, dc=sun.com", - "cn= \\ John Doe,ou=People,dc=sun.com", - true, - }, - // Difference in values is significant - { - "cn=John Doe, ou=People, dc=sun.com", - "cn=John Doe, ou=People, dc=sun.com", - false, - }, - // Test parsing of `;` for separating RDNs - {"cn=john;dc=example,dc=com", "cn=john,dc=example,dc=com", true}, // missing values matter - } - - for i, tc := range testcases { - a, err := ParseDN(tc.A) - if err != nil { - t.Errorf("%d: %v", i, err) - continue - } - b, err := ParseDN(tc.B) - if err != nil { - t.Errorf("%d: %v", i, err) - continue - } - if expected, actual := tc.Equal, a.Equal(b); expected != actual { - t.Errorf("%d: when comparing %q and %q expected %v, got %v", i, a, b, expected, actual) - continue - } - if expected, actual := tc.Equal, b.Equal(a); expected != actual { - t.Errorf("%d: when comparing %q and %q expected %v, got %v", i, a, b, expected, actual) - continue - } - - if expected, actual := a.Equal(b), a.String() == b.String(); expected != actual { - t.Errorf("%d: when asserting string comparison of %q and %q expected equal %v, got %v", i, a, b, expected, actual) - continue - } - } -} - -func TestDNEqualFold(t *testing.T) { - testcases := []struct { - A string - B string - Equal bool - }{ - // Match on case insensitive - {"o=A", "o=a", true}, - {"o=A,o=b", "o=a,o=B", true}, - {"o=a+o=B", "o=A+o=b", true}, - { - "cn=users,ou=example,dc=com", - "cn=Users,ou=example,dc=com", - true, - }, - - // Match on case insensitive and case mismatch in type - {"o=A", "O=a", true}, - {"o=A,o=b", "o=a,O=B", true}, - {"o=a+o=B", "o=A+O=b", true}, - } - - for i, tc := range testcases { - a, err := ParseDN(tc.A) - if err != nil { - t.Errorf("%d: %v", i, err) - continue - } - b, err := ParseDN(tc.B) - if err != nil { - t.Errorf("%d: %v", i, err) - continue - } - if expected, actual := tc.Equal, a.EqualFold(b); expected != actual { - t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) - continue - } - if expected, actual := tc.Equal, b.EqualFold(a); expected != actual { - t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) - continue - } - } -} - -func TestDNAncestor(t *testing.T) { - testcases := []struct { - A string - B string - Ancestor bool - }{ - // Exact match returns false - {"", "", false}, - {"o=A", "o=A", false}, - {"o=A,o=B", "o=A,o=B", false}, - {"o=A+o=B", "o=A+o=B", false}, - - // Mismatch - {"ou=C,ou=B,o=A", "ou=E,ou=D,ou=B,o=A", false}, - - // Descendant - {"ou=C,ou=B,o=A", "ou=E,ou=C,ou=B,o=A", true}, - } - - for i, tc := range testcases { - a, err := ParseDN(tc.A) - if err != nil { - t.Errorf("%d: %v", i, err) - continue - } - b, err := ParseDN(tc.B) - if err != nil { - t.Errorf("%d: %v", i, err) - continue - } - if expected, actual := tc.Ancestor, a.AncestorOf(b); expected != actual { - t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) - continue - } - } -} - -func BenchmarkParseSubject(b *testing.B) { - for n := 0; n < b.N; n++ { - _, err := ParseDN("DF=#6666666666665006838820013100000746939546349182108463491821809FBFFFFFFFFF") - if err == nil { - b.Fatal("expected error, but got none") - } - } -} - -func TestMustKeepOrderInRawDerBytes(t *testing.T) { - subject := "cn=foo-long.com,ou=FooLong,ou=Barq,ou=Baz,ou=Dept.,o=Corp.,c=US" - rdnSeq, err := ParseDN(subject) - if err != nil { - t.Fatal(err) - } - - expectedRdnSeq := &DN{ - []*RelativeDN{ - {[]*AttributeTypeAndValue{{Type: "cn", Value: "foo-long.com"}}}, - {[]*AttributeTypeAndValue{{Type: "ou", Value: "FooLong"}}}, - {[]*AttributeTypeAndValue{{Type: "ou", Value: "Barq"}}}, - {[]*AttributeTypeAndValue{{Type: "ou", Value: "Baz"}}}, - {[]*AttributeTypeAndValue{{Type: "ou", Value: "Dept."}}}, - {[]*AttributeTypeAndValue{{Type: "o", Value: "Corp."}}}, - {[]*AttributeTypeAndValue{{Type: "c", Value: "US"}}}, - }, - } - - assert.Equal(t, expectedRdnSeq, rdnSeq) - assert.Equal(t, subject, rdnSeq.String()) -} - -func TestRoundTripLiteralSubject(t *testing.T) { - rdnSequences := map[string]string{ - "cn=foo-long.com,ou=FooLong,ou=Barq,ou=Baz,ou=Dept.,o=Corp.,c=US": "cn=foo-long.com,ou=FooLong,ou=Barq,ou=Baz,ou=Dept.,o=Corp.,c=US", - "cn=foo-lon❤️\\,g.com,ou=Foo===Long,ou=Ba # rq,ou=Baz,o=C\\; orp.,c=US": "cn=foo-lon\\e2\\9d\\a4\\ef\\b8\\8f\\,g.com,ou=Foo===Long,ou=Ba # rq,ou=Baz,o=C\\; orp.,c=US", - "cn=fo\x00o-long.com,ou=\x04FooLong": "cn=fo\\00o-long.com,ou=\\04FooLong", - } - - for subjIn, subjOut := range rdnSequences { - t.Logf("Testing subject: %s", subjIn) - - newRDNSeq, err := ParseDN(subjIn) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, subjOut, newRDNSeq.String()) - } -} - -func TestDecodeString(t *testing.T) { - successTestcases := map[string]string{ - "foo-long.com": "foo-long.com", - "foo-lon❤️\\,g.com": "foo-lon❤️,g.com", - "fo\x00o-long.com": "fo\x00o-long.com", - "fo\\00o-long.com": "fo\x00o-long.com", - } - - for encoded, decoded := range successTestcases { - t.Logf("Testing encoded string: %s", encoded) - decodedString, err := decodeString(encoded) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, decoded, decodedString) - } - - errorTestcases := map[string]string{ - "fo\\": "got corrupted escaped character: 'fo\\'", - "fo\\0": "failed to decode escaped character: encoding/hex: invalid byte: 0", - "fo\\UU️o-long.com": "failed to decode escaped character: encoding/hex: invalid byte: U+0055 'U'", - "fo\\0❤️o-long.com": "failed to decode escaped character: invalid byte: 0❤", - } - - for encoded, expectedError := range errorTestcases { - t.Logf("Testing encoded string: %s", encoded) - _, err := decodeString(encoded) - assert.EqualError(t, err, expectedError) - } -} diff --git a/doc.go b/doc.go deleted file mode 100644 index f20d39b..0000000 --- a/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Package ldap provides basic LDAP v3 functionality. -*/ -package ldap diff --git a/error.go b/error.go deleted file mode 100644 index 0014ffe..0000000 --- a/error.go +++ /dev/null @@ -1,262 +0,0 @@ -package ldap - -import ( - "errors" - "fmt" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -// 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 - LDAPResultIsLeaf = 35 - LDAPResultAliasDereferencingProblem = 36 - LDAPResultInappropriateAuthentication = 48 - LDAPResultInvalidCredentials = 49 - LDAPResultInsufficientAccessRights = 50 - LDAPResultBusy = 51 - LDAPResultUnavailable = 52 - LDAPResultUnwillingToPerform = 53 - LDAPResultLoopDetect = 54 - LDAPResultSortControlMissing = 60 - LDAPResultOffsetRangeError = 61 - LDAPResultNamingViolation = 64 - LDAPResultObjectClassViolation = 65 - LDAPResultNotAllowedOnNonLeaf = 66 - LDAPResultNotAllowedOnRDN = 67 - LDAPResultEntryAlreadyExists = 68 - LDAPResultObjectClassModsProhibited = 69 - LDAPResultResultsTooLarge = 70 - LDAPResultAffectsMultipleDSAs = 71 - LDAPResultVirtualListViewErrorOrControlError = 76 - LDAPResultOther = 80 - LDAPResultServerDown = 81 - LDAPResultLocalError = 82 - LDAPResultEncodingError = 83 - LDAPResultDecodingError = 84 - LDAPResultTimeout = 85 - LDAPResultAuthUnknown = 86 - LDAPResultFilterError = 87 - LDAPResultUserCanceled = 88 - LDAPResultParamError = 89 - LDAPResultNoMemory = 90 - LDAPResultConnectError = 91 - LDAPResultNotSupported = 92 - LDAPResultControlNotFound = 93 - LDAPResultNoResultsReturned = 94 - LDAPResultMoreResultsToReturn = 95 - LDAPResultClientLoop = 96 - LDAPResultReferralLimitExceeded = 97 - LDAPResultInvalidResponse = 100 - LDAPResultAmbiguousResponse = 101 - LDAPResultTLSNotSupported = 112 - LDAPResultIntermediateResponse = 113 - LDAPResultUnknownType = 114 - LDAPResultCanceled = 118 - LDAPResultNoSuchOperation = 119 - LDAPResultTooLate = 120 - LDAPResultCannotCancel = 121 - LDAPResultAssertionFailed = 122 - LDAPResultAuthorizationDenied = 123 - LDAPResultSyncRefreshRequired = 4096 - - ErrorNetwork = 200 - ErrorFilterCompile = 201 - ErrorFilterDecompile = 202 - ErrorDebugging = 203 - ErrorUnexpectedMessage = 204 - ErrorUnexpectedResponse = 205 - ErrorEmptyPassword = 206 -) - -// LDAPResultCodeMap contains string descriptions for LDAP error codes -var LDAPResultCodeMap = map[uint16]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", - LDAPResultIsLeaf: "Is Leaf", - 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", - LDAPResultSortControlMissing: "Sort Control Missing", - LDAPResultOffsetRangeError: "Result Offset Range Error", - LDAPResultNamingViolation: "Naming Violation", - LDAPResultObjectClassViolation: "Object Class Violation", - LDAPResultResultsTooLarge: "Results Too Large", - LDAPResultNotAllowedOnNonLeaf: "Not Allowed On Non Leaf", - LDAPResultNotAllowedOnRDN: "Not Allowed On RDN", - LDAPResultEntryAlreadyExists: "Entry Already Exists", - LDAPResultObjectClassModsProhibited: "Object Class Mods Prohibited", - LDAPResultAffectsMultipleDSAs: "Affects Multiple DSAs", - LDAPResultVirtualListViewErrorOrControlError: "Failed because of a problem related to the virtual list view", - LDAPResultOther: "Other", - LDAPResultServerDown: "Cannot establish a connection", - LDAPResultLocalError: "An error occurred", - LDAPResultEncodingError: "LDAP encountered an error while encoding", - LDAPResultDecodingError: "LDAP encountered an error while decoding", - LDAPResultTimeout: "LDAP timeout while waiting for a response from the server", - LDAPResultAuthUnknown: "The auth method requested in a bind request is unknown", - LDAPResultFilterError: "An error occurred while encoding the given search filter", - LDAPResultUserCanceled: "The user canceled the operation", - LDAPResultParamError: "An invalid parameter was specified", - LDAPResultNoMemory: "Out of memory error", - LDAPResultConnectError: "A connection to the server could not be established", - LDAPResultNotSupported: "An attempt has been made to use a feature not supported LDAP", - LDAPResultControlNotFound: "The controls required to perform the requested operation were not found", - LDAPResultNoResultsReturned: "No results were returned from the server", - LDAPResultMoreResultsToReturn: "There are more results in the chain of results", - LDAPResultClientLoop: "A loop has been detected. For example when following referrals", - LDAPResultReferralLimitExceeded: "The referral hop limit has been exceeded", - LDAPResultCanceled: "Operation was canceled", - LDAPResultNoSuchOperation: "Server has no knowledge of the operation requested for cancellation", - LDAPResultTooLate: "Too late to cancel the outstanding operation", - LDAPResultCannotCancel: "The identified operation does not support cancellation or the cancel operation cannot be performed", - LDAPResultAssertionFailed: "An assertion control given in the LDAP operation evaluated to false causing the operation to not be performed", - LDAPResultSyncRefreshRequired: "Refresh Required", - LDAPResultInvalidResponse: "Invalid Response", - LDAPResultAmbiguousResponse: "Ambiguous Response", - LDAPResultTLSNotSupported: "Tls Not Supported", - LDAPResultIntermediateResponse: "Intermediate Response", - LDAPResultUnknownType: "Unknown Type", - LDAPResultAuthorizationDenied: "Authorization Denied", - - ErrorNetwork: "Network Error", - ErrorFilterCompile: "Filter Compile Error", - ErrorFilterDecompile: "Filter Decompile Error", - ErrorDebugging: "Debugging Error", - ErrorUnexpectedMessage: "Unexpected Message", - ErrorUnexpectedResponse: "Unexpected Response", - ErrorEmptyPassword: "Empty password not allowed by the client", -} - -// Error holds LDAP error information -type Error struct { - // Err is the underlying error - Err error - // ResultCode is the LDAP error code - ResultCode uint16 - // MatchedDN is the matchedDN returned if any - MatchedDN string - // Packet is the returned packet if any - Packet *ber.Packet -} - -func (e *Error) Error() string { - return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error()) -} - -func (e *Error) Unwrap() error { return e.Err } - -// GetLDAPError creates an Error out of a BER packet representing a LDAPResult -// The return is an error object. It can be casted to a Error structure. -// This function returns nil if resultCode in the LDAPResult sequence is success(0). -func GetLDAPError(packet *ber.Packet) error { - if packet == nil { - return &Error{ResultCode: ErrorUnexpectedResponse, Err: fmt.Errorf("Empty packet")} - } - - if len(packet.Children) >= 2 { - response := packet.Children[1] - if response == nil { - return &Error{ResultCode: ErrorUnexpectedResponse, Err: fmt.Errorf("Empty response in packet"), Packet: packet} - } - if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 { - if ber.Type(response.Children[0].Tag) == ber.Type(ber.TagInteger) || ber.Type(response.Children[0].Tag) == ber.Type(ber.TagEnumerated) { - resultCode := uint16(response.Children[0].Value.(int64)) - if resultCode == 0 { // No error - return nil - } - - if ber.Type(response.Children[1].Tag) == ber.Type(ber.TagOctetString) && - ber.Type(response.Children[2].Tag) == ber.Type(ber.TagOctetString) { - return &Error{ - ResultCode: resultCode, - MatchedDN: response.Children[1].Value.(string), - Err: fmt.Errorf("%v", response.Children[2].Value), - Packet: packet, - } - } - } - } - } - - return &Error{ResultCode: ErrorNetwork, Err: fmt.Errorf("Invalid packet format"), Packet: packet} -} - -// NewError creates an LDAP error with the given code and underlying error -func NewError(resultCode uint16, err error) error { - return &Error{ResultCode: resultCode, Err: err} -} - -// IsErrorAnyOf returns true if the given error is an LDAP error with any one of the given result codes -func IsErrorAnyOf(err error, codes ...uint16) bool { - if err == nil { - return false - } - - var serverError *Error - if !errors.As(err, &serverError) { - return false - } - - for _, code := range codes { - if serverError.ResultCode == code { - return true - } - } - - return false -} - -// IsErrorWithCode returns true if the given error is an LDAP error with the given result code -func IsErrorWithCode(err error, desiredResultCode uint16) bool { - return IsErrorAnyOf(err, desiredResultCode) -} diff --git a/error_test.go b/error_test.go deleted file mode 100644 index 323f766..0000000 --- a/error_test.go +++ /dev/null @@ -1,260 +0,0 @@ -package ldap - -import ( - "errors" - "fmt" - "io" - "net" - "strings" - "testing" - "time" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -// TestWrappedError tests that match the result code when an error is wrapped. -func TestWrappedError(t *testing.T) { - resultCodes := []uint16{ - LDAPResultProtocolError, - LDAPResultBusy, - ErrorNetwork, - } - - tests := []struct { - name string - err error - codes []uint16 - expected bool - }{ - // success - { - name: "a normal error", - err: &Error{ - ResultCode: ErrorNetwork, - }, - codes: resultCodes, - expected: true, - }, - - { - name: "a wrapped error", - err: fmt.Errorf("wrap: %w", &Error{ - ResultCode: LDAPResultBusy, - }), - codes: resultCodes, - expected: true, - }, - - { - name: "multiple wrapped error", - err: fmt.Errorf("second: %w", - fmt.Errorf("first: %w", - &Error{ - ResultCode: LDAPResultProtocolError, - }, - ), - ), - codes: resultCodes, - expected: true, - }, - - // failure - { - name: "not match a normal error", - err: &Error{ - ResultCode: LDAPResultSuccess, - }, - codes: resultCodes, - expected: false, - }, - - { - name: "not match a wrapped error", - err: fmt.Errorf("wrap: %w", &Error{ - ResultCode: LDAPResultNoSuchObject, - }), - codes: resultCodes, - expected: false, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - actual := IsErrorAnyOf(tt.err, tt.codes...) - if tt.expected != actual { - t.Errorf("expected %t, but got %t", tt.expected, actual) - } - }) - } -} - -// TestNilPacket tests that nil packets don't cause a panic. -func TestNilPacket(t *testing.T) { - // Test for nil packet - err := GetLDAPError(nil) - if !IsErrorWithCode(err, ErrorUnexpectedResponse) { - t.Errorf("Should have an 'ErrorUnexpectedResponse' error in nil packets, got: %v", err) - } - - // Test for nil result - kids := []*ber.Packet{ - {}, // Unused - nil, // Can't be nil - } - pack := &ber.Packet{Children: kids} - err = GetLDAPError(pack) - - if !IsErrorWithCode(err, ErrorUnexpectedResponse) { - t.Errorf("Should have an 'ErrorUnexpectedResponse' error in nil packets, got: %v", err) - } -} - -// TestConnReadErr tests that an unexpected error reading from underlying -// connection bubbles up to the goroutine which makes a request. -func TestConnReadErr(t *testing.T) { - conn := &signalErrConn{ - signals: make(chan error), - } - - ldapConn := NewConn(conn, false) - ldapConn.Start() - - // Make a dummy search request. - searchReq := NewSearchRequest("dc=example,dc=com", ScopeWholeSubtree, DerefAlways, 0, 0, false, "(objectClass=*)", nil, nil) - - expectedError := errors.New("this is the error you are looking for") - - // Send the signal after a short amount of time. - time.AfterFunc(10*time.Millisecond, func() { conn.signals <- expectedError }) - - // This should block until the underlying conn gets the error signal - // which should bubble up through the reader() goroutine, close the - // connection, and - _, err := ldapConn.Search(searchReq) - if err == nil || !strings.Contains(err.Error(), expectedError.Error()) { - t.Errorf("not the expected error: %s", err) - } -} - -// TestGetLDAPError tests parsing of result with a error response. -func TestGetLDAPError(t *testing.T) { - diagnosticMessage := "Detailed error message" - bindResponse := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindResponse, nil, "Bind Response") - bindResponse.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(LDAPResultInvalidCredentials), "resultCode")) - bindResponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "dc=example,dc=org", "matchedDN")) - bindResponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, diagnosticMessage, "diagnosticMessage")) - packet := ber.NewSequence("LDAPMessage") - packet.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(0), "messageID")) - packet.AppendChild(bindResponse) - err := GetLDAPError(packet) - if err == nil { - t.Errorf("Did not get error response") - } - - ldapError := err.(*Error) - if ldapError.ResultCode != LDAPResultInvalidCredentials { - t.Errorf("Got incorrect error code in LDAP error; got %v, expected %v", ldapError.ResultCode, LDAPResultInvalidCredentials) - } - if ldapError.Err.Error() != diagnosticMessage { - t.Errorf("Got incorrect error message in LDAP error; got %v, expected %v", ldapError.Err.Error(), diagnosticMessage) - } -} - -// TestGetLDAPErrorInvalidResponse tests that responses with an unexpected ordering or combination of children -// don't cause a panic. -func TestGetLDAPErrorInvalidResponse(t *testing.T) { - bindResponse := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindResponse, nil, "Bind Response") - bindResponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "dc=example,dc=org", "matchedDN")) - bindResponse.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(LDAPResultInvalidCredentials), "resultCode")) - bindResponse.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(LDAPResultInvalidCredentials), "resultCode")) - packet := ber.NewSequence("LDAPMessage") - packet.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(0), "messageID")) - packet.AppendChild(bindResponse) - err := GetLDAPError(packet) - if err == nil { - t.Errorf("Did not get error response") - } - - ldapError := err.(*Error) - if ldapError.ResultCode != ErrorNetwork { - t.Errorf("Got incorrect error code in LDAP error; got %v, expected %v", ldapError.ResultCode, ErrorNetwork) - } -} - -func TestErrorIs(t *testing.T) { - err := NewError(ErrorNetwork, io.EOF) - if !errors.Is(err, io.EOF) { - t.Errorf("Expected an io.EOF error: %v", err) - } -} - -func TestErrorAs(t *testing.T) { - var netErr net.InvalidAddrError = "invalid addr" - err := NewError(ErrorNetwork, netErr) - - var target net.InvalidAddrError - ok := errors.As(err, &target) - if !ok { - t.Error("Expected an InvalidAddrError") - } -} - -// TestGetLDAPErrorSuccess tests parsing of a result with no error (resultCode == 0). -func TestGetLDAPErrorSuccess(t *testing.T) { - bindResponse := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindResponse, nil, "Bind Response") - bindResponse.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(0), "resultCode")) - bindResponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN")) - bindResponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "diagnosticMessage")) - packet := ber.NewSequence("LDAPMessage") - packet.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(0), "messageID")) - packet.AppendChild(bindResponse) - err := GetLDAPError(packet) - if err != nil { - t.Errorf("Successful responses should not produce an error, but got: %v", err) - } -} - -// signalErrConn is a helpful type used with TestConnReadErr. It implements the -// net.Conn interface to be used as a connection for the test. Most methods are -// no-ops but the Read() method blocks until it receives a signal which it -// returns as an error. -type signalErrConn struct { - signals chan error -} - -// Read blocks until an error is sent on the internal signals channel. That -// error is returned. -func (c *signalErrConn) Read(b []byte) (n int, err error) { - return 0, <-c.signals -} - -func (c *signalErrConn) Write(b []byte) (n int, err error) { - return len(b), nil -} - -func (c *signalErrConn) Close() error { - close(c.signals) - return nil -} - -func (c *signalErrConn) LocalAddr() net.Addr { - return (*net.TCPAddr)(nil) -} - -func (c *signalErrConn) RemoteAddr() net.Addr { - return (*net.TCPAddr)(nil) -} - -func (c *signalErrConn) SetDeadline(t time.Time) error { - return nil -} - -func (c *signalErrConn) SetReadDeadline(t time.Time) error { - return nil -} - -func (c *signalErrConn) SetWriteDeadline(t time.Time) error { - return nil -} diff --git a/examples_moddn_test.go b/examples_moddn_test.go deleted file mode 100644 index bab907c..0000000 --- a/examples_moddn_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package ldap - -import ( - "log" -) - -// This example shows how to rename an entry without moving it -func ExampleConn_ModifyDN_renameNoMove() { - conn, err := DialURL("ldap://ldap.example.org:389") - if err != nil { - log.Fatalf("Failed to connect: %s\n", err) - } - defer conn.Close() - - _, err = conn.SimpleBind(&SimpleBindRequest{ - Username: "uid=someone,ou=people,dc=example,dc=org", - Password: "MySecretPass", - }) - if err != nil { - log.Fatalf("Failed to bind: %s\n", err) - } - // just rename to uid=new,ou=people,dc=example,dc=org: - req := NewModifyDNRequest("uid=user,ou=people,dc=example,dc=org", "uid=new", true, "") - if err = conn.ModifyDN(req); err != nil { - log.Fatalf("Failed to call ModifyDN(): %s\n", err) - } -} - -// This example shows how to rename an entry and moving it to a new base -func ExampleConn_ModifyDN_renameAndMove() { - conn, err := DialURL("ldap://ldap.example.org:389") - if err != nil { - log.Fatalf("Failed to connect: %s\n", err) - } - defer conn.Close() - - _, err = conn.SimpleBind(&SimpleBindRequest{ - Username: "uid=someone,ou=people,dc=example,dc=org", - Password: "MySecretPass", - }) - if err != nil { - log.Fatalf("Failed to bind: %s\n", err) - } - // rename to uid=new,ou=people,dc=example,dc=org and move to ou=users,dc=example,dc=org -> - // uid=new,ou=users,dc=example,dc=org - req := NewModifyDNRequest("uid=user,ou=people,dc=example,dc=org", "uid=new", true, "ou=users,dc=example,dc=org") - - if err = conn.ModifyDN(req); err != nil { - log.Fatalf("Failed to call ModifyDN(): %s\n", err) - } -} - -// This example shows how to move an entry to a new base without renaming the RDN -func ExampleConn_ModifyDN_moveOnly() { - conn, err := DialURL("ldap://ldap.example.org:389") - if err != nil { - log.Fatalf("Failed to connect: %s\n", err) - } - defer conn.Close() - - _, err = conn.SimpleBind(&SimpleBindRequest{ - Username: "uid=someone,ou=people,dc=example,dc=org", - Password: "MySecretPass", - }) - if err != nil { - log.Fatalf("Failed to bind: %s\n", err) - } - // move to ou=users,dc=example,dc=org -> uid=user,ou=users,dc=example,dc=org - req := NewModifyDNRequest("uid=user,ou=people,dc=example,dc=org", "uid=user", true, "ou=users,dc=example,dc=org") - if err = conn.ModifyDN(req); err != nil { - log.Fatalf("Failed to call ModifyDN(): %s\n", err) - } -} diff --git a/examples_test.go b/examples_test.go deleted file mode 100644 index 817016b..0000000 --- a/examples_test.go +++ /dev/null @@ -1,581 +0,0 @@ -package ldap - -import ( - "context" - "crypto/tls" - "crypto/x509" - "fmt" - "io/ioutil" - "log" - "time" -) - -// This example demonstrates how to bind a connection to an ldap user -// allowing access to restricted attributes that user has access to -func ExampleConn_Bind() { - l, err := DialURL("ldap://ldap.example.com:389") - if err != nil { - log.Fatal(err) - } - defer l.Close() - - err = l.Bind("cn=read-only-admin,dc=example,dc=com", "password") - if err != nil { - log.Fatal(err) - } -} - -// This example demonstrates how to use the search interface -func ExampleConn_Search() { - l, err := DialURL("ldap://ldap.example.com:389") - if err != nil { - log.Fatal(err) - } - defer l.Close() - - searchRequest := NewSearchRequest( - "dc=example,dc=com", // The base dn to search - ScopeWholeSubtree, NeverDerefAliases, 0, 0, false, - "(&(objectClass=organizationalPerson))", // The filter to apply - []string{"dn", "cn"}, // A list attributes to retrieve - nil, - ) - - sr, err := l.Search(searchRequest) - if err != nil { - log.Fatal(err) - } - - for _, entry := range sr.Entries { - fmt.Printf("%s: %v\n", entry.DN, entry.GetAttributeValue("cn")) - } -} - -// This example demonstrates how to search asynchronously -func ExampleConn_SearchAsync() { - l, err := DialURL(fmt.Sprintf("%s:%d", "ldap.example.com", 389)) - if err != nil { - log.Fatal(err) - } - defer l.Close() - - searchRequest := NewSearchRequest( - "dc=example,dc=com", // The base dn to search - ScopeWholeSubtree, NeverDerefAliases, 0, 0, false, - "(&(objectClass=organizationalPerson))", // The filter to apply - []string{"dn", "cn"}, // A list attributes to retrieve - nil, - ) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - r := l.SearchAsync(ctx, searchRequest, 64) - for r.Next() { - entry := r.Entry() - fmt.Printf("%s has DN %s\n", entry.GetAttributeValue("cn"), entry.DN) - } - if err := r.Err(); err != nil { - log.Fatal(err) - } -} - -// This example demonstrates how to do syncrepl (persistent search) -func ExampleConn_Syncrepl() { - l, err := DialURL(fmt.Sprintf("%s:%d", "ldap.example.com", 389)) - if err != nil { - log.Fatal(err) - } - defer l.Close() - - searchRequest := NewSearchRequest( - "dc=example,dc=com", // The base dn to search - ScopeWholeSubtree, NeverDerefAliases, 0, 0, false, - "(&(objectClass=organizationalPerson))", // The filter to apply - []string{"dn", "cn"}, // A list attributes to retrieve - nil, - ) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - mode := SyncRequestModeRefreshAndPersist - var cookie []byte = nil - r := l.Syncrepl(ctx, searchRequest, 64, mode, cookie, false) - for r.Next() { - entry := r.Entry() - if entry != nil { - fmt.Printf("%s has DN %s\n", entry.GetAttributeValue("cn"), entry.DN) - } - controls := r.Controls() - if len(controls) != 0 { - fmt.Printf("%s", controls) - } - } - if err := r.Err(); err != nil { - log.Fatal(err) - } -} - -// This example demonstrates how to start a TLS connection -func ExampleConn_StartTLS() { - l, err := DialURL("ldap://ldap.example.com:389") - if err != nil { - log.Fatal(err) - } - defer l.Close() - - // Reconnect with TLS - err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) - if err != nil { - log.Fatal(err) - } - - // Operations via l are now encrypted -} - -// This example demonstrates how to compare an attribute with a value -func ExampleConn_Compare() { - l, err := DialURL("ldap://ldap.example.com:389") - if err != nil { - log.Fatal(err) - } - defer l.Close() - - matched, err := l.Compare("cn=user,dc=example,dc=com", "uid", "someuserid") - if err != nil { - log.Fatal(err) - } - - fmt.Println(matched) -} - -func ExampleConn_PasswordModify_admin() { - l, err := DialURL("ldap://ldap.example.com:389") - if err != nil { - log.Fatal(err) - } - defer l.Close() - - err = l.Bind("cn=admin,dc=example,dc=com", "password") - if err != nil { - log.Fatal(err) - } - - passwordModifyRequest := NewPasswordModifyRequest("cn=user,dc=example,dc=com", "", "NewPassword") - _, err = l.PasswordModify(passwordModifyRequest) - - if err != nil { - log.Fatalf("Password could not be changed: %s", err.Error()) - } -} - -func ExampleConn_PasswordModify_generatedPassword() { - l, err := DialURL("ldap://ldap.example.com:389") - if err != nil { - log.Fatal(err) - } - defer l.Close() - - err = l.Bind("cn=user,dc=example,dc=com", "password") - if err != nil { - log.Fatal(err) - } - - passwordModifyRequest := NewPasswordModifyRequest("", "OldPassword", "") - passwordModifyResponse, err := l.PasswordModify(passwordModifyRequest) - if err != nil { - log.Fatalf("Password could not be changed: %s", err.Error()) - } - - generatedPassword := passwordModifyResponse.GeneratedPassword - log.Printf("Generated password: %s\n", generatedPassword) -} - -func ExampleConn_PasswordModify_setNewPassword() { - l, err := DialURL("ldap://ldap.example.com:389") - if err != nil { - log.Fatal(err) - } - defer l.Close() - - err = l.Bind("cn=user,dc=example,dc=com", "password") - if err != nil { - log.Fatal(err) - } - - passwordModifyRequest := NewPasswordModifyRequest("", "OldPassword", "NewPassword") - _, err = l.PasswordModify(passwordModifyRequest) - - if err != nil { - log.Fatalf("Password could not be changed: %s", err.Error()) - } -} - -func ExampleConn_Modify() { - l, err := DialURL("ldap://ldap.example.com:389") - if err != nil { - log.Fatal(err) - } - defer l.Close() - - // Add a description, and replace the mail attributes - modify := NewModifyRequest("cn=user,dc=example,dc=com", nil) - modify.Add("description", []string{"An example user"}) - modify.Replace("mail", []string{"user@example.org"}) - - err = l.Modify(modify) - if err != nil { - log.Fatal(err) - } -} - -// Example_userAuthentication shows how a typical application can verify a login attempt -// Refer to https://github.com/go-ldap/ldap/issues/93 for issues revolving around unauthenticated binds, with zero length passwords -func Example_userAuthentication() { - // The username and password we want to check - username := "someuser" - password := "userpassword" - - bindusername := "readonly" - bindpassword := "password" - - l, err := DialURL("ldap://ldap.example.com:389") - if err != nil { - log.Fatal(err) - } - defer l.Close() - - // Reconnect with TLS - err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) - if err != nil { - log.Fatal(err) - } - - // First bind with a read only user - err = l.Bind(bindusername, bindpassword) - if err != nil { - log.Fatal(err) - } - - // Search for the given username - searchRequest := NewSearchRequest( - "dc=example,dc=com", - ScopeWholeSubtree, NeverDerefAliases, 0, 0, false, - fmt.Sprintf("(&(objectClass=organizationalPerson)(uid=%s))", EscapeFilter(username)), - []string{"dn"}, - nil, - ) - - sr, err := l.Search(searchRequest) - if err != nil { - log.Fatal(err) - } - - if len(sr.Entries) != 1 { - log.Fatal("User does not exist or too many entries returned") - } - - userdn := sr.Entries[0].DN - - // Bind as the user to verify their password - err = l.Bind(userdn, password) - if err != nil { - log.Fatal(err) - } - - // Rebind as the read only user for any further queries - err = l.Bind(bindusername, bindpassword) - if err != nil { - log.Fatal(err) - } -} - -func Example_beherappolicy() { - l, err := DialURL("ldap://ldap.example.com:389") - if err != nil { - log.Fatal(err) - } - defer l.Close() - - controls := []Control{} - controls = append(controls, NewControlBeheraPasswordPolicy()) - bindRequest := NewSimpleBindRequest("cn=admin,dc=example,dc=com", "password", controls) - - r, err := l.SimpleBind(bindRequest) - ppolicyControl := FindControl(r.Controls, ControlTypeBeheraPasswordPolicy) - - var ppolicy *ControlBeheraPasswordPolicy - if ppolicyControl != nil { - ppolicy = ppolicyControl.(*ControlBeheraPasswordPolicy) - } else { - log.Printf("ppolicyControl response not available.\n") - } - if err != nil { - errStr := "ERROR: Cannot bind: " + err.Error() - if ppolicy != nil && ppolicy.Error >= 0 { - errStr += ":" + ppolicy.ErrorString - } - log.Print(errStr) - } else { - logStr := "Login Ok" - if ppolicy != nil { - if ppolicy.Expire >= 0 { - logStr += fmt.Sprintf(". Password expires in %d seconds\n", ppolicy.Expire) - } else if ppolicy.Grace >= 0 { - logStr += fmt.Sprintf(". Password expired, %d grace logins remain\n", ppolicy.Grace) - } - } - log.Print(logStr) - } -} - -func Example_vchuppolicy() { - l, err := DialURL("ldap://ldap.example.com:389") - if err != nil { - log.Fatal(err) - } - defer l.Close() - l.Debug = true - - bindRequest := NewSimpleBindRequest("cn=admin,dc=example,dc=com", "password", nil) - - r, err := l.SimpleBind(bindRequest) - - passwordMustChangeControl := FindControl(r.Controls, ControlTypeVChuPasswordMustChange) - var passwordMustChange *ControlVChuPasswordMustChange - if passwordMustChangeControl != nil { - passwordMustChange = passwordMustChangeControl.(*ControlVChuPasswordMustChange) - } - - if passwordMustChange != nil && passwordMustChange.MustChange { - log.Printf("Password Must be changed.\n") - } - - passwordWarningControl := FindControl(r.Controls, ControlTypeVChuPasswordWarning) - - var passwordWarning *ControlVChuPasswordWarning - if passwordWarningControl != nil { - passwordWarning = passwordWarningControl.(*ControlVChuPasswordWarning) - } else { - log.Printf("ppolicyControl response not available.\n") - } - if err != nil { - log.Print("ERROR: Cannot bind: " + err.Error()) - } else { - logStr := "Login Ok" - if passwordWarning != nil { - if passwordWarning.Expire >= 0 { - logStr += fmt.Sprintf(". Password expires in %d seconds\n", passwordWarning.Expire) - } - } - log.Print(logStr) - } -} - -// This example demonstrates how to use ControlPaging to manually execute a -// paginated search request instead of using SearchWithPaging. -func ExampleControlPaging_manualPaging() { - conn, err := DialURL("ldap://ldap.example.com:389") - if err != nil { - log.Fatal(err) - } - defer conn.Close() - - var pageSize uint32 = 32 - searchBase := "dc=example,dc=com" - filter := "(objectClass=group)" - pagingControl := NewControlPaging(pageSize) - attributes := []string{} - controls := []Control{pagingControl} - - for { - request := NewSearchRequest(searchBase, ScopeWholeSubtree, DerefAlways, 0, 0, false, filter, attributes, controls) - response, err := conn.Search(request) - if err != nil { - log.Fatalf("Failed to execute search request: %s", err.Error()) - } - - // [do something with the response entries] - - // In order to prepare the next request, we check if the response - // contains another ControlPaging object and a not-empty cookie and - // copy that cookie into our pagingControl object: - updatedControl := FindControl(response.Controls, ControlTypePaging) - if ctrl, ok := updatedControl.(*ControlPaging); ctrl != nil && ok && len(ctrl.Cookie) != 0 { - pagingControl.SetCookie(ctrl.Cookie) - continue - } - // If no new paging information is available or the cookie is empty, we - // are done with the pagination. - break - } -} - -// This example demonstrates how to use DirSync to manually execute a -// DirSync search request -func ExampleConn_DirSync() { - conn, err := Dial("tcp", "ad.example.org:389") - if err != nil { - log.Fatalf("Failed to connect: %s\n", err) - } - defer conn.Close() - - _, err = conn.SimpleBind(&SimpleBindRequest{ - Username: "cn=Some User,ou=people,dc=example,dc=org", - Password: "MySecretPass", - }) - if err != nil { - log.Fatalf("failed to bind: %s", err) - } - - req := &SearchRequest{ - BaseDN: `DC=example,DC=org`, - Filter: `(&(objectClass=person)(!(objectClass=computer)))`, - Attributes: []string{"*"}, - Scope: ScopeWholeSubtree, - } - // This is the initial sync with all entries matching the filter - doMore := true - var cookie []byte - for doMore { - res, err := conn.DirSync(req, DirSyncObjectSecurity, 1000, cookie) - if err != nil { - log.Fatalf("failed to search: %s", err) - } - for _, entry := range res.Entries { - entry.Print() - } - ctrl := FindControl(res.Controls, ControlTypeDirSync) - if ctrl == nil || ctrl.(*ControlDirSync).Flags == 0 { - doMore = false - } - cookie = ctrl.(*ControlDirSync).Cookie - } - // We're done with the initial sync. Now pull every 15 seconds for the - // updated entries - note that you get just the changes, not a full entry. - for { - res, err := conn.DirSync(req, DirSyncObjectSecurity, 1000, cookie) - if err != nil { - log.Fatalf("failed to search: %s", err) - } - for _, entry := range res.Entries { - entry.Print() - } - time.Sleep(15 * time.Second) - } -} - -// This example demonstrates how to use DirSync search asynchronously -func ExampleConn_DirSyncAsync() { - conn, err := Dial("tcp", "ad.example.org:389") - if err != nil { - log.Fatalf("Failed to connect: %s\n", err) - } - defer conn.Close() - - _, err = conn.SimpleBind(&SimpleBindRequest{ - Username: "cn=Some User,ou=people,dc=example,dc=org", - Password: "MySecretPass", - }) - if err != nil { - log.Fatalf("failed to bind: %s", err) - } - - req := &SearchRequest{ - BaseDN: `DC=example,DC=org`, - Filter: `(&(objectClass=person)(!(objectClass=computer)))`, - Attributes: []string{"*"}, - Scope: ScopeWholeSubtree, - } - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - var cookie []byte = nil - r := conn.DirSyncAsync(ctx, req, 64, DirSyncObjectSecurity, 1000, cookie) - for r.Next() { - entry := r.Entry() - if entry != nil { - entry.Print() - } - controls := r.Controls() - if len(controls) != 0 { - fmt.Printf("%s", controls) - } - } - if err := r.Err(); err != nil { - log.Fatal(err) - } -} - -// This example demonstrates how to use EXTERNAL SASL with TLS client certificates. -func ExampleConn_ExternalBind() { - ldapCert := "/path/to/cert.pem" - ldapKey := "/path/to/key.pem" - ldapCAchain := "/path/to/ca_chain.pem" - - // Load client cert and key - cert, err := tls.LoadX509KeyPair(ldapCert, ldapKey) - if err != nil { - log.Fatal(err) - } - - // Load CA chain - caCert, err := ioutil.ReadFile(ldapCAchain) - if err != nil { - log.Fatal(err) - } - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM(caCert) - - // Setup TLS with ldap client cert - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{cert}, - RootCAs: caCertPool, - InsecureSkipVerify: true, - } - - // connect to ldap server - l, err := DialURL("ldap://ldap.example.com:389") - if err != nil { - log.Fatal(err) - } - defer l.Close() - - // reconnect using tls - err = l.StartTLS(tlsConfig) - if err != nil { - log.Fatal(err) - } - - // sasl external bind - err = l.ExternalBind() - if err != nil { - log.Fatal(err) - } - - // Conduct ldap queries -} - -// ExampleConn_WhoAmI demonstrates how to run a whoami request according to https://tools.ietf.org/html/rfc4532 -func ExampleConn_WhoAmI() { - conn, err := DialURL("ldap.example.org:389") - if err != nil { - log.Fatalf("Failed to connect: %s\n", err) - } - - _, err = conn.SimpleBind(&SimpleBindRequest{ - Username: "uid=someone,ou=people,dc=example,dc=org", - Password: "MySecretPass", - }) - if err != nil { - log.Fatalf("Failed to bind: %s\n", err) - } - - res, err := conn.WhoAmI(nil) - if err != nil { - log.Fatalf("Failed to call WhoAmI(): %s\n", err) - } - fmt.Printf("I am: %s\n", res.AuthzID) -} diff --git a/examples_windows_test.go b/examples_windows_test.go deleted file mode 100644 index f24698b..0000000 --- a/examples_windows_test.go +++ /dev/null @@ -1,35 +0,0 @@ -//go:build windows -// +build windows - -package ldap - -import ( - "log" - - "github.com/go-ldap/ldap/gssapi" -) - -// This example demonstrates passwordless bind using the current process' user -// credentials on Windows (SASL GSSAPI mechanism bind with SSPI client). -func ExampleConn_SSPIClient_GSSAPIBind() { - // Windows only: Create a GSSAPIClient using Windows built-in SSPI lib - // (secur32.dll). - // This will use the credentials of the current process' user. - sspiClient, err := gssapi.NewSSPIClient() - if err != nil { - log.Fatal(err) - } - defer sspiClient.Close() - - l, err := DialURL("ldap://ldap.example.com:389") - if err != nil { - log.Fatal(err) - } - defer l.Close() - - // Bind using supplied GSSAPIClient implementation - err = l.GSSAPIBind(sspiClient, "ldap/ldap.example.com", "") - if err != nil { - log.Fatal(err) - } -} diff --git a/extended.go b/extended.go deleted file mode 100644 index e71d982..0000000 --- a/extended.go +++ /dev/null @@ -1,100 +0,0 @@ -package ldap - -import ( - "fmt" - ber "github.com/go-asn1-ber/asn1-ber" -) - -// ExtendedRequest represents an extended request to send to the server -// See: https://www.rfc-editor.org/rfc/rfc4511#section-4.12 -type ExtendedRequest struct { - // ExtendedRequest ::= [APPLICATION 23] SEQUENCE { - // requestName [0] LDAPOID, - // requestValue [1] OCTET STRING OPTIONAL } - - Name string - Value *ber.Packet - Controls []Control -} - -// NewExtendedRequest returns a new ExtendedRequest. The value can be -// nil depending on the type of request -func NewExtendedRequest(name string, value *ber.Packet) *ExtendedRequest { - return &ExtendedRequest{ - Name: name, - Value: value, - } -} - -func (er ExtendedRequest) appendTo(envelope *ber.Packet) error { - pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Extended Request") - pkt.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, ber.TagEOC, er.Name, "Extended Request Name")) - if er.Value != nil { - pkt.AppendChild(er.Value) - } - envelope.AppendChild(pkt) - if len(er.Controls) > 0 { - envelope.AppendChild(encodeControls(er.Controls)) - } - return nil -} - -// ExtendedResponse represents the response from the directory server -// after sending an extended request -// See: https://www.rfc-editor.org/rfc/rfc4511#section-4.12 -type ExtendedResponse struct { - // ExtendedResponse ::= [APPLICATION 24] SEQUENCE { - // COMPONENTS OF LDAPResult, - // responseName [10] LDAPOID OPTIONAL, - // responseValue [11] OCTET STRING OPTIONAL } - - Name string - Value *ber.Packet - Controls []Control -} - -// Extended performs an extended request. The resulting -// ExtendedResponse may return a value in the form of a *ber.Packet -func (l *Conn) Extended(er *ExtendedRequest) (*ExtendedResponse, error) { - msgCtx, err := l.doRequest(er) - if err != nil { - return nil, err - } - defer l.finishMessage(msgCtx) - - packet, err := l.readPacket(msgCtx) - if err != nil { - return nil, err - } - if err = GetLDAPError(packet); err != nil { - return nil, err - } - - if len(packet.Children[1].Children) < 4 { - return nil, fmt.Errorf( - "ldap: malformed extended response: expected 4 children, got %d", - len(packet.Children), - ) - } - - response := &ExtendedResponse{ - Name: packet.Children[1].Children[3].Data.String(), - Controls: make([]Control, 0), - } - - if len(packet.Children) == 3 { - for _, child := range packet.Children[2].Children { - decodedChild, decodeErr := DecodeControl(child) - if decodeErr != nil { - return nil, fmt.Errorf("failed to decode child control: %s", decodeErr) - } - response.Controls = append(response.Controls, decodedChild) - } - } - - if len(packet.Children[1].Children) == 5 { - response.Value = packet.Children[1].Children[4] - } - - return response, nil -} diff --git a/extended_test.go b/extended_test.go deleted file mode 100644 index 6bd83a1..0000000 --- a/extended_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package ldap - -import ( - "testing" -) - -func TestExtendedRequest_WhoAmI(t *testing.T) { - l, err := DialURL(ldapServer) - if err != nil { - t.Errorf("%s failed: %v", t.Name(), err) - return - } - defer l.Close() - - l.Bind("", "") // anonymous - defer l.Unbind() - - rfc4532req := NewExtendedRequest("1.3.6.1.4.1.4203.1.11.3", nil) // request value is - - var rfc4532resp *ExtendedResponse - if rfc4532resp, err = l.Extended(rfc4532req); err != nil { - t.Errorf("%s failed: %v", t.Name(), err) - return - } - t.Logf("%#v\n", rfc4532resp) -} - -func TestExtendedRequest_FastBind(t *testing.T) { - conn, err := DialURL(ldapServer) - if err != nil { - t.Error(err) - } - defer conn.Close() - - request := NewExtendedRequest("1.3.6.1.4.1.4203.1.11.3", nil) - _, err = conn.Extended(request) - if err != nil { - t.Errorf("%s failed: %v", t.Name(), err) - return - } -} diff --git a/filter.go b/filter.go deleted file mode 100644 index db76210..0000000 --- a/filter.go +++ /dev/null @@ -1,486 +0,0 @@ -package ldap - -import ( - "bytes" - hexpac "encoding/hex" - "errors" - "fmt" - "io" - "strings" - "unicode" - "unicode/utf8" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -// Filter choices -const ( - FilterAnd = 0 - FilterOr = 1 - FilterNot = 2 - FilterEqualityMatch = 3 - FilterSubstrings = 4 - FilterGreaterOrEqual = 5 - FilterLessOrEqual = 6 - FilterPresent = 7 - FilterApproxMatch = 8 - FilterExtensibleMatch = 9 -) - -// FilterMap contains human readable descriptions of Filter choices -var FilterMap = map[uint64]string{ - FilterAnd: "And", - FilterOr: "Or", - FilterNot: "Not", - FilterEqualityMatch: "Equality Match", - FilterSubstrings: "Substrings", - FilterGreaterOrEqual: "Greater Or Equal", - FilterLessOrEqual: "Less Or Equal", - FilterPresent: "Present", - FilterApproxMatch: "Approx Match", - FilterExtensibleMatch: "Extensible Match", -} - -// SubstringFilter options -const ( - FilterSubstringsInitial = 0 - FilterSubstringsAny = 1 - FilterSubstringsFinal = 2 -) - -// FilterSubstringsMap contains human readable descriptions of SubstringFilter choices -var FilterSubstringsMap = map[uint64]string{ - FilterSubstringsInitial: "Substrings Initial", - FilterSubstringsAny: "Substrings Any", - FilterSubstringsFinal: "Substrings Final", -} - -// MatchingRuleAssertion choices -const ( - MatchingRuleAssertionMatchingRule = 1 - MatchingRuleAssertionType = 2 - MatchingRuleAssertionMatchValue = 3 - MatchingRuleAssertionDNAttributes = 4 -) - -// MatchingRuleAssertionMap contains human readable descriptions of MatchingRuleAssertion choices -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", -} - -var _SymbolAny = []byte{'*'} - -// CompileFilter converts a string representation of a filter into a BER-encoded packet -func CompileFilter(filter string) (*ber.Packet, error) { - if len(filter) == 0 || filter[0] != '(' { - return nil, NewError(ErrorFilterCompile, errors.New("ldap: filter does not start with an '('")) - } - packet, pos, err := compileFilter(filter, 1) - if err != nil { - return nil, err - } - switch { - case pos > len(filter): - return nil, NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) - case pos < len(filter): - return nil, NewError(ErrorFilterCompile, errors.New("ldap: finished compiling filter with extra at end: "+fmt.Sprint(filter[pos:]))) - } - return packet, nil -} - -// DecompileFilter converts a packet representation of a filter into a string representation -func DecompileFilter(packet *ber.Packet) (_ string, err error) { - defer func() { - if r := recover(); r != nil { - err = NewError(ErrorFilterDecompile, errors.New("ldap: error decompiling filter")) - } - }() - - buf := bytes.NewBuffer(nil) - buf.WriteByte('(') - childStr := "" - - switch packet.Tag { - case FilterAnd: - buf.WriteByte('&') - for _, child := range packet.Children { - childStr, err = DecompileFilter(child) - if err != nil { - return - } - buf.WriteString(childStr) - } - case FilterOr: - buf.WriteByte('|') - for _, child := range packet.Children { - childStr, err = DecompileFilter(child) - if err != nil { - return - } - buf.WriteString(childStr) - } - case FilterNot: - buf.WriteByte('!') - childStr, err = DecompileFilter(packet.Children[0]) - if err != nil { - return - } - buf.WriteString(childStr) - - case FilterSubstrings: - buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes())) - buf.WriteByte('=') - for i, child := range packet.Children[1].Children { - if i == 0 && child.Tag != FilterSubstringsInitial { - buf.Write(_SymbolAny) - } - buf.WriteString(EscapeFilter(ber.DecodeString(child.Data.Bytes()))) - if child.Tag != FilterSubstringsFinal { - buf.Write(_SymbolAny) - } - } - case FilterEqualityMatch: - buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes())) - buf.WriteByte('=') - buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))) - case FilterGreaterOrEqual: - buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes())) - buf.WriteString(">=") - buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))) - case FilterLessOrEqual: - buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes())) - buf.WriteString("<=") - buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))) - case FilterPresent: - buf.WriteString(ber.DecodeString(packet.Data.Bytes())) - buf.WriteString("=*") - case FilterApproxMatch: - buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes())) - buf.WriteString("~=") - buf.WriteString(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 { - buf.WriteString(attr) - } - if dnAttributes { - buf.WriteString(":dn") - } - if len(matchingRule) > 0 { - buf.WriteString(":") - buf.WriteString(matchingRule) - } - buf.WriteString(":=") - buf.WriteString(EscapeFilter(value)) - } - - buf.WriteByte(')') - - return buf.String(), nil -} - -func compileFilterSet(filter string, pos int, parent *ber.Packet) (int, error) { - for pos < len(filter) && filter[pos] == '(' { - child, newPos, err := compileFilter(filter, pos+1) - if err != nil { - return pos, err - } - pos = newPos - parent.AppendChild(child) - } - if pos == len(filter) { - return pos, NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) - } - - return pos + 1, nil -} - -func compileFilter(filter string, pos int) (*ber.Packet, int, error) { - var ( - packet *ber.Packet - err error - ) - - defer func() { - if r := recover(); r != nil { - err = NewError(ErrorFilterCompile, errors.New("ldap: error compiling filter")) - } - }() - newPos := 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 '(': - packet, newPos, err = compileFilter(filter, pos+currentWidth) - newPos++ - return packet, newPos, err - case '&': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterAnd, nil, FilterMap[FilterAnd]) - newPos, err = compileFilterSet(filter, pos+currentWidth, packet) - return packet, newPos, err - case '|': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterOr, nil, FilterMap[FilterOr]) - newPos, err = compileFilterSet(filter, pos+currentWidth, packet) - return packet, newPos, err - case '!': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterNot, nil, FilterMap[FilterNot]) - var child *ber.Packet - child, newPos, err = compileFilter(filter, pos+currentWidth) - packet.AppendChild(child) - return packet, newPos, err - default: - const ( - stateReadingAttr = 0 - stateReadingExtensibleMatchingRule = 1 - stateReadingCondition = 2 - ) - - state := stateReadingAttr - attribute := bytes.NewBuffer(nil) - extensibleDNAttributes := false - extensibleMatchingRule := bytes.NewBuffer(nil) - condition := bytes.NewBuffer(nil) - - 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 stateReadingAttr: - 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 = stateReadingCondition - 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 = stateReadingExtensibleMatchingRule - newPos += 4 - - // Extensible rule, with attr only - case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) - state = stateReadingCondition - newPos += 2 - - // Extensible rule, with no DN attribute matching - case currentRune == ':': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) - state = stateReadingExtensibleMatchingRule - newPos++ - - // Equality condition - case currentRune == '=': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch]) - state = stateReadingCondition - newPos++ - - // Greater-than or equal - case currentRune == '>' && strings.HasPrefix(remainingFilter, ">="): - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual]) - state = stateReadingCondition - newPos += 2 - - // Less-than or equal - case currentRune == '<' && strings.HasPrefix(remainingFilter, "<="): - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual]) - state = stateReadingCondition - newPos += 2 - - // Approx - case currentRune == '~' && strings.HasPrefix(remainingFilter, "~="): - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterApproxMatch]) - state = stateReadingCondition - newPos += 2 - - // Still reading the attribute name - default: - attribute.WriteRune(currentRune) - newPos += currentWidth - } - - case stateReadingExtensibleMatchingRule: - switch { - - // Matching rule OID is done - case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): - state = stateReadingCondition - newPos += 2 - - // Still reading the matching rule oid - default: - extensibleMatchingRule.WriteRune(currentRune) - newPos += currentWidth - } - - case stateReadingCondition: - // append to the condition - condition.WriteRune(currentRune) - newPos += currentWidth - } - } - - if newPos == len(filter) { - err = NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) - return packet, newPos, err - } - if packet == nil { - err = NewError(ErrorFilterCompile, errors.New("ldap: error parsing filter")) - return packet, newPos, err - } - - 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 extensibleMatchingRule.Len() > 0 { - packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchingRule, extensibleMatchingRule.String(), MatchingRuleAssertionMap[MatchingRuleAssertionMatchingRule])) - } - - // Include the attribute, if specified - if attribute.Len() > 0 { - packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionType, attribute.String(), MatchingRuleAssertionMap[MatchingRuleAssertionType])) - } - - // Add the value (only required child) - encodedString, encodeErr := decodeEscapedSymbols(condition.Bytes()) - if encodeErr != nil { - return packet, newPos, encodeErr - } - 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 && bytes.Equal(condition.Bytes(), _SymbolAny): - packet = ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterPresent, attribute.String(), FilterMap[FilterPresent]) - case packet.Tag == FilterEqualityMatch && bytes.Contains(condition.Bytes(), _SymbolAny): - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute.String(), "Attribute")) - packet.Tag = FilterSubstrings - packet.Description = FilterMap[uint64(packet.Tag)] - seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings") - parts := bytes.Split(condition.Bytes(), _SymbolAny) - for i, part := range parts { - if len(part) == 0 { - continue - } - var tag ber.Tag - switch i { - case 0: - tag = FilterSubstringsInitial - case len(parts) - 1: - tag = FilterSubstringsFinal - default: - tag = FilterSubstringsAny - } - encodedString, encodeErr := decodeEscapedSymbols(part) - if encodeErr != nil { - return packet, newPos, encodeErr - } - seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, encodedString, FilterSubstringsMap[uint64(tag)])) - } - packet.AppendChild(seq) - default: - encodedString, encodeErr := decodeEscapedSymbols(condition.Bytes()) - if encodeErr != nil { - return packet, newPos, encodeErr - } - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute.String(), "Attribute")) - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, encodedString, "Condition")) - } - - newPos += currentWidth - return packet, newPos, err - } -} - -// Convert from "ABC\xx\xx\xx" form to literal bytes for transport -func decodeEscapedSymbols(src []byte) (string, error) { - var ( - buffer bytes.Buffer - offset int - reader = bytes.NewReader(src) - byteHex []byte - byteVal []byte - ) - - for { - runeVal, runeSize, err := reader.ReadRune() - if err == io.EOF { - return buffer.String(), nil - } else if err != nil { - return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: failed to read filter: %v", err)) - } else if runeVal == unicode.ReplacementChar { - return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", offset)) - } - - if runeVal == '\\' { - // 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 byteHex == nil { - byteHex = make([]byte, 2) - byteVal = make([]byte, 1) - } - - if _, err := io.ReadFull(reader, byteHex); err != nil { - if err == io.ErrUnexpectedEOF { - return "", NewError(ErrorFilterCompile, errors.New("ldap: missing characters for escape in filter")) - } - return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: invalid characters for escape in filter: %v", err)) - } - - if _, err := hexpac.Decode(byteVal, byteHex); err != nil { - return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: invalid characters for escape in filter: %v", err)) - } - - buffer.Write(byteVal) - } else { - buffer.WriteRune(runeVal) - } - - offset += runeSize - } -} diff --git a/filter_test.go b/filter_test.go deleted file mode 100644 index 4c88eb1..0000000 --- a/filter_test.go +++ /dev/null @@ -1,291 +0,0 @@ -package ldap - -import ( - "strings" - "testing" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -type compileTest struct { - filterStr string - - expectedFilter string - expectedType int - expectedErr string -} - -var testFilters = []compileTest{ - { - filterStr: "(&(sn=Miller)(givenName=Bob))", - expectedFilter: "(&(sn=Miller)(givenName=Bob))", - expectedType: FilterAnd, - }, - { - filterStr: "(|(sn=Miller)(givenName=Bob))", - expectedFilter: "(|(sn=Miller)(givenName=Bob))", - expectedType: FilterOr, - }, - { - filterStr: "(!(sn=Miller))", - expectedFilter: "(!(sn=Miller))", - expectedType: FilterNot, - }, - { - filterStr: "(sn=Miller)", - expectedFilter: "(sn=Miller)", - expectedType: FilterEqualityMatch, - }, - { - filterStr: "(sn=Mill*)", - expectedFilter: "(sn=Mill*)", - expectedType: FilterSubstrings, - }, - { - filterStr: "(sn=*Mill)", - expectedFilter: "(sn=*Mill)", - expectedType: FilterSubstrings, - }, - { - filterStr: "(sn=*Mill*)", - expectedFilter: "(sn=*Mill*)", - expectedType: FilterSubstrings, - }, - { - filterStr: "(sn=*i*le*)", - expectedFilter: "(sn=*i*le*)", - expectedType: FilterSubstrings, - }, - { - filterStr: "(sn=Mi*l*r)", - expectedFilter: "(sn=Mi*l*r)", - expectedType: FilterSubstrings, - }, - // substring filters escape properly - { - filterStr: `(sn=Mi*함*r)`, - expectedFilter: `(sn=Mi*\ed\95\a8*r)`, - expectedType: FilterSubstrings, - }, - // already escaped substring filters don't get double-escaped - { - filterStr: `(sn=Mi*\ed\95\a8*r)`, - expectedFilter: `(sn=Mi*\ed\95\a8*r)`, - expectedType: FilterSubstrings, - }, - { - filterStr: "(sn=Mi*le*)", - expectedFilter: "(sn=Mi*le*)", - expectedType: FilterSubstrings, - }, - { - filterStr: "(sn=*i*ler)", - expectedFilter: "(sn=*i*ler)", - expectedType: FilterSubstrings, - }, - { - filterStr: "(sn>=Miller)", - expectedFilter: "(sn>=Miller)", - expectedType: FilterGreaterOrEqual, - }, - { - filterStr: "(sn<=Miller)", - expectedFilter: "(sn<=Miller)", - expectedType: FilterLessOrEqual, - }, - { - filterStr: "(sn=*)", - expectedFilter: "(sn=*)", - expectedType: FilterPresent, - }, - { - filterStr: "(sn~=Miller)", - expectedFilter: "(sn~=Miller)", - expectedType: FilterApproxMatch, - }, - { - filterStr: `(objectGUID='\fc\fe\a3\ab\f9\90N\aaGm\d5I~\d12)`, - expectedFilter: `(objectGUID='\fc\fe\a3\ab\f9\90N\aaGm\d5I~\d12)`, - expectedType: FilterEqualityMatch, - }, - { - 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: FilterEqualityMatch, - }, - { - filterStr: `(objectGUID=함수목록)`, - expectedFilter: `(objectGUID=\ed\95\a8\ec\88\98\eb\aa\a9\eb\a1\9d)`, - expectedType: FilterEqualityMatch, - }, - { - filterStr: `(objectGUID=`, - expectedFilter: ``, - expectedType: 0, - expectedErr: "unexpected end of filter", - }, - { - filterStr: `(objectGUID=함수목록`, - expectedFilter: ``, - expectedType: 0, - expectedErr: "unexpected end of filter", - }, - { - filterStr: `((cn=)`, - expectedFilter: ``, - expectedType: 0, - expectedErr: "unexpected end of filter", - }, - { - filterStr: `(&(objectclass=inetorgperson)(cn=中文))`, - expectedFilter: `(&(objectclass=inetorgperson)(cn=\e4\b8\ad\e6\96\87))`, - expectedType: 0, - }, - // attr extension - { - filterStr: `(memberOf:=foo)`, - expectedFilter: `(memberOf:=foo)`, - expectedType: FilterExtensibleMatch, - }, - // attr+named matching rule extension - { - filterStr: `(memberOf:test:=foo)`, - expectedFilter: `(memberOf:test:=foo)`, - expectedType: FilterExtensibleMatch, - }, - // attr+oid matching rule extension - { - filterStr: `(cn:1.2.3.4.5:=Fred Flintstone)`, - expectedFilter: `(cn:1.2.3.4.5:=Fred Flintstone)`, - expectedType: FilterExtensibleMatch, - }, - // attr+dn+oid matching rule extension - { - filterStr: `(sn:dn:2.4.6.8.10:=Barney Rubble)`, - expectedFilter: `(sn:dn:2.4.6.8.10:=Barney Rubble)`, - expectedType: FilterExtensibleMatch, - }, - // attr+dn extension - { - filterStr: `(o:dn:=Ace Industry)`, - expectedFilter: `(o:dn:=Ace Industry)`, - expectedType: FilterExtensibleMatch, - }, - // dn extension - { - filterStr: `(:dn:2.4.6.8.10:=Dino)`, - expectedFilter: `(:dn:2.4.6.8.10:=Dino)`, - expectedType: FilterExtensibleMatch, - }, - { - 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: FilterExtensibleMatch, - }, - - // compileTest{ filterStr: "()", filterType: FilterExtensibleMatch }, -} - -var testInvalidFilters = []string{ - `(objectGUID=\zz)`, - `(objectGUID=\a)`, -} - -func TestFilter(t *testing.T) { - // Test Compiler and Decompiler - for _, i := range testFilters { - filter, err := CompileFilter(i.filterStr) - switch { - case err != nil: - 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) - } - case filter.Tag != ber.Tag(i.expectedType): - t.Errorf("%q Expected %q got %q", i.filterStr, FilterMap[uint64(i.expectedType)], FilterMap[uint64(filter.Tag)]) - default: - o, err := DecompileFilter(filter) - if err != nil { - t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error()) - } else if i.expectedFilter != o { - t.Errorf("%q expected, got %q", i.expectedFilter, o) - } - } - } -} - -func TestDecodeEscapedSymbols(t *testing.T) { - for _, testInfo := range []struct { - Src string - Err string - }{ - { - Src: "a\u0100\x80", - Err: `LDAP Result Code 201 "Filter Compile Error": ldap: error reading rune at position 3`, - }, - { - Src: `start\d`, - Err: `LDAP Result Code 201 "Filter Compile Error": ldap: missing characters for escape in filter`, - }, - { - Src: `\`, - Err: `LDAP Result Code 201 "Filter Compile Error": ldap: invalid characters for escape in filter: EOF`, - }, - { - Src: `start\--end`, - Err: `LDAP Result Code 201 "Filter Compile Error": ldap: invalid characters for escape in filter: encoding/hex: invalid byte: U+002D '-'`, - }, - { - Src: `start\d0\hh`, - Err: `LDAP Result Code 201 "Filter Compile Error": ldap: invalid characters for escape in filter: encoding/hex: invalid byte: U+0068 'h'`, - }, - } { - - res, err := decodeEscapedSymbols([]byte(testInfo.Src)) - if err == nil || err.Error() != testInfo.Err { - t.Fatal(testInfo.Src, "=> ", err, "!=", testInfo.Err) - } - if res != "" { - t.Fatal(testInfo.Src, "=> ", "invalid result", res) - } - } -} - -func TestInvalidFilter(t *testing.T) { - for _, filterStr := range testInvalidFilters { - if _, err := CompileFilter(filterStr); err == nil { - t.Errorf("Problem compiling %s - expected err", filterStr) - } - } -} - -func BenchmarkFilterCompile(b *testing.B) { - b.StopTimer() - filters := make([]string, len(testFilters)) - - // Test Compiler and Decompiler - for idx, i := range testFilters { - filters[idx] = i.filterStr - } - - maxIdx := len(filters) - b.StartTimer() - for i := 0; i < b.N; i++ { - _, _ = CompileFilter(filters[i%maxIdx]) - } -} - -func BenchmarkFilterDecompile(b *testing.B) { - b.StopTimer() - filters := make([]*ber.Packet, len(testFilters)) - - // Test Compiler and Decompiler - for idx, i := range testFilters { - filters[idx], _ = CompileFilter(i.filterStr) - } - - maxIdx := len(filters) - b.StartTimer() - for i := 0; i < b.N; i++ { - _, _ = DecompileFilter(filters[i%maxIdx]) - } -} diff --git a/fuzz_test.go b/fuzz_test.go deleted file mode 100644 index e2d3008..0000000 --- a/fuzz_test.go +++ /dev/null @@ -1,95 +0,0 @@ -//go:build go1.18 -// +build go1.18 - -package ldap - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func FuzzParseDN(f *testing.F) { - f.Add("*") - f.Add("cn=Jim\\0Test") - f.Add("cn=Jim\\0") - f.Add("DC=example,=net") - f.Add("o=a+o=B") - - f.Fuzz(func(t *testing.T, input_data string) { - _, _ = ParseDN(input_data) - }) -} - -func FuzzDecodeEscapedSymbols(f *testing.F) { - f.Add([]byte("a\u0100\x80")) - f.Add([]byte(`start\d`)) - f.Add([]byte(`\`)) - f.Add([]byte(`start\--end`)) - f.Add([]byte(`start\d0\hh`)) - - f.Fuzz(func(t *testing.T, input_data []byte) { - _, _ = decodeEscapedSymbols(input_data) - }) -} - -func FuzzEscapeDN(f *testing.F) { - f.Add("test,user") - f.Add("#test#user#") - f.Add("\\test\\user\\") - f.Add(" test user ") - f.Add("\u0000te\x00st\x00user" + string(rune(0))) - f.Add("test\"+,;<>\\-_user") - f.Add("test\u0391user ") - - f.Fuzz(func(t *testing.T, input_data string) { - _ = EscapeDN(input_data) - }) -} - -func FuzzRoundTripRDNSequence(f *testing.F) { - f.Add("CN=foo-long.com,OU=FooLong,OU=Barq,OU=Baz,OU=Dept.,O=Corp.,C=US") - f.Add("CN=foo-lon❤️\\,g.com,OU=Foo===Long,OU=Ba # rq,OU=Baz,O=C\\; orp.,C=US") - f.Add("CN=fo\x00o-long.com,OU=\x04FooLong") - f.Add("İ=") - - f.Fuzz(func(t *testing.T, subjectString string) { - t.Parallel() - rdnSeq, err := ParseDN(subjectString) - if err != nil { - t.Skip() - } - - newRDNSeq, err := ParseDN(rdnSeq.String()) - if err != nil { - t.Fatal(err) - } - - assert.True(t, rdnSeq.Equal(newRDNSeq)) - assert.True(t, rdnSeq.EqualFold(newRDNSeq)) - }) -} - -func FuzzRoundTripEncodeDecode(f *testing.F) { - f.Add("dffad=-fasdfsd") - f.Add("❤️\\,") - f.Add("aaa\x00o-long.c\x04FooLong") - f.Add("İ") - - f.Fuzz(func(t *testing.T, rawString string) { - t.Parallel() - keyEncoded := encodeString(rawString, true) - keyDecoded, err := decodeString(keyEncoded) - if err != nil { - t.Fatal(err) - } - assert.Equal(t, rawString, keyDecoded) - - valueEncoded := encodeString(rawString, false) - valueDecoded, err := decodeString(valueEncoded) - if err != nil { - t.Fatal(err) - } - assert.Equal(t, rawString, valueDecoded) - }) -} diff --git a/go.mod b/go.mod deleted file mode 100644 index a7c82bf..0000000 --- a/go.mod +++ /dev/null @@ -1,13 +0,0 @@ -module github.com/go-ldap/ldap - -go 1.14 - -require ( - github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 - github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa - github.com/go-asn1-ber/asn1-ber v1.5.7 - github.com/google/uuid v1.6.0 - github.com/jcmturner/gokrb5/v8 v8.4.4 - github.com/stretchr/testify v1.8.1 - golang.org/x/net v0.33.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index b49d11d..0000000 --- a/go.sum +++ /dev/null @@ -1,116 +0,0 @@ -github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= -github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= -github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= -github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= -github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= -github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= -github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= -github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gssapi/client.go b/gssapi/client.go deleted file mode 100644 index d6c6dbd..0000000 --- a/gssapi/client.go +++ /dev/null @@ -1,214 +0,0 @@ -package gssapi - -import ( - "fmt" - - "github.com/jcmturner/gokrb5/v8/client" - "github.com/jcmturner/gokrb5/v8/config" - "github.com/jcmturner/gokrb5/v8/keytab" - "github.com/jcmturner/gokrb5/v8/types" - - "github.com/jcmturner/gokrb5/v8/gssapi" - "github.com/jcmturner/gokrb5/v8/spnego" - - "github.com/jcmturner/gokrb5/v8/crypto" - "github.com/jcmturner/gokrb5/v8/iana/keyusage" - "github.com/jcmturner/gokrb5/v8/messages" - - "github.com/jcmturner/gokrb5/v8/credentials" -) - -// Client implements ldap.GSSAPIClient interface. -type Client struct { - *client.Client - - ekey types.EncryptionKey - Subkey types.EncryptionKey -} - -// NewClientWithKeytab creates a new client from a keytab credential. -// Set the realm to empty string to use the default realm from config. -func NewClientWithKeytab(username, realm, keytabPath, krb5confPath string, settings ...func(*client.Settings)) (*Client, error) { - krb5conf, err := config.Load(krb5confPath) - if err != nil { - return nil, err - } - - keytab, err := keytab.Load(keytabPath) - if err != nil { - return nil, err - } - - client := client.NewWithKeytab(username, realm, keytab, krb5conf, settings...) - - return &Client{ - Client: client, - }, nil -} - -// NewClientWithPassword creates a new client from a password credential. -// Set the realm to empty string to use the default realm from config. -func NewClientWithPassword(username, realm, password string, krb5confPath string, settings ...func(*client.Settings)) (*Client, error) { - krb5conf, err := config.Load(krb5confPath) - if err != nil { - return nil, err - } - - client := client.NewWithPassword(username, realm, password, krb5conf, settings...) - - return &Client{ - Client: client, - }, nil -} - -// NewClientFromCCache creates a new client from a populated client cache. -func NewClientFromCCache(ccachePath, krb5confPath string, settings ...func(*client.Settings)) (*Client, error) { - krb5conf, err := config.Load(krb5confPath) - if err != nil { - return nil, err - } - - ccache, err := credentials.LoadCCache(ccachePath) - if err != nil { - return nil, err - } - - client, err := client.NewFromCCache(ccache, krb5conf, settings...) - if err != nil { - return nil, err - } - - return &Client{ - Client: client, - }, nil -} - -// Close deletes any established secure context and closes the client. -func (client *Client) Close() error { - client.Client.Destroy() - return nil -} - -// DeleteSecContext destroys any established secure context. -func (client *Client) DeleteSecContext() error { - client.ekey = types.EncryptionKey{} - client.Subkey = types.EncryptionKey{} - return nil -} - -// InitSecContext initiates the establishment of a security context for -// GSS-API between the client and server. -// See RFC 4752 section 3.1. -func (client *Client) InitSecContext(target string, input []byte) ([]byte, bool, error) { - gssapiFlags := []int{gssapi.ContextFlagInteg, gssapi.ContextFlagConf, gssapi.ContextFlagMutual} - - switch input { - case nil: - tkt, ekey, err := client.Client.GetServiceTicket(target) - if err != nil { - return nil, false, err - } - client.ekey = ekey - - token, err := spnego.NewKRB5TokenAPREQ(client.Client, tkt, ekey, gssapiFlags, []int{}) - if err != nil { - return nil, false, err - } - - output, err := token.Marshal() - if err != nil { - return nil, false, err - } - - return output, true, nil - - default: - var token spnego.KRB5Token - - err := token.Unmarshal(input) - if err != nil { - return nil, false, err - } - - var completed bool - - if token.IsAPRep() { - completed = true - - encpart, err := crypto.DecryptEncPart(token.APRep.EncPart, client.ekey, keyusage.AP_REP_ENCPART) - if err != nil { - return nil, false, err - } - - part := &messages.EncAPRepPart{} - - if err = part.Unmarshal(encpart); err != nil { - return nil, false, err - } - client.Subkey = part.Subkey - } - - if token.IsKRBError() { - return nil, !false, token.KRBError - } - - return make([]byte, 0), !completed, nil - } -} - -// NegotiateSaslAuth performs the last step of the SASL handshake. -// See RFC 4752 section 3.1. -func (client *Client) NegotiateSaslAuth(input []byte, authzid string) ([]byte, error) { - token := &gssapi.WrapToken{} - err := token.Unmarshal(input, true) - if err != nil { - return nil, err - } - - if (token.Flags & 0b1) == 0 { - return nil, fmt.Errorf("got a Wrapped token that's not from the server") - } - - key := client.ekey - if (token.Flags & 0b100) != 0 { - key = client.Subkey - } - - _, err = token.Verify(key, keyusage.GSSAPI_ACCEPTOR_SEAL) - if err != nil { - return nil, err - } - - pl := token.Payload - if len(pl) != 4 { - return nil, fmt.Errorf("server send bad final token for SASL GSSAPI Handshake") - } - - // We never want a security layer - b := [4]byte{0, 0, 0, 0} - payload := append(b[:], []byte(authzid)...) - - encType, err := crypto.GetEtype(key.KeyType) - if err != nil { - return nil, err - } - - token = &gssapi.WrapToken{ - Flags: 0b100, - EC: uint16(encType.GetHMACBitLength() / 8), - RRC: 0, - SndSeqNum: 1, - Payload: payload, - } - - if err := token.SetCheckSum(key, keyusage.GSSAPI_INITIATOR_SEAL); err != nil { - return nil, err - } - - output, err := token.Marshal() - if err != nil { - return nil, err - } - - return output, nil -} diff --git a/gssapi/sspi.go b/gssapi/sspi.go deleted file mode 100644 index 3bad8b4..0000000 --- a/gssapi/sspi.go +++ /dev/null @@ -1,191 +0,0 @@ -//go:build windows -// +build windows - -package gssapi - -import ( - "bytes" - "encoding/binary" - "fmt" - - "github.com/alexbrainman/sspi" - "github.com/alexbrainman/sspi/kerberos" -) - -// SSPIClient implements ldap.GSSAPIClient interface. -// Depends on secur32.dll. -type SSPIClient struct { - creds *sspi.Credentials - ctx *kerberos.ClientContext -} - -// NewSSPIClient returns a client with credentials of the current user. -func NewSSPIClient() (*SSPIClient, error) { - creds, err := kerberos.AcquireCurrentUserCredentials() - if err != nil { - return nil, err - } - - return NewSSPIClientWithCredentials(creds), nil -} - -// NewSSPIClientWithCredentials returns a client with the provided credentials. -func NewSSPIClientWithCredentials(creds *sspi.Credentials) *SSPIClient { - return &SSPIClient{ - creds: creds, - } -} - -// NewSSPIClientWithUserCredentials returns a client using the provided user's -// credentials. -func NewSSPIClientWithUserCredentials(domain, username, password string) (*SSPIClient, error) { - creds, err := kerberos.AcquireUserCredentials(domain, username, password) - if err != nil { - return nil, err - } - - return &SSPIClient{ - creds: creds, - }, nil -} - -// Close deletes any established secure context and closes the client. -func (c *SSPIClient) Close() error { - err1 := c.DeleteSecContext() - err2 := c.creds.Release() - if err1 != nil { - return err1 - } - if err2 != nil { - return err2 - } - return nil -} - -// DeleteSecContext destroys any established secure context. -func (c *SSPIClient) DeleteSecContext() error { - return c.ctx.Release() -} - -// InitSecContext initiates the establishment of a security context for -// GSS-API between the client and server. -// See RFC 4752 section 3.1. -func (c *SSPIClient) InitSecContext(target string, token []byte) ([]byte, bool, error) { - sspiFlags := uint32(sspi.ISC_REQ_INTEGRITY | sspi.ISC_REQ_CONFIDENTIALITY | sspi.ISC_REQ_MUTUAL_AUTH) - - switch token { - case nil: - ctx, completed, output, err := kerberos.NewClientContextWithFlags(c.creds, target, sspiFlags) - if err != nil { - return nil, false, err - } - c.ctx = ctx - - return output, !completed, nil - default: - - completed, output, err := c.ctx.Update(token) - if err != nil { - return nil, false, err - } - if err := c.ctx.VerifyFlags(); err != nil { - return nil, false, fmt.Errorf("error verifying flags: %v", err) - } - return output, !completed, nil - - } -} - -// NegotiateSaslAuth performs the last step of the SASL handshake. -// See RFC 4752 section 3.1. -func (c *SSPIClient) NegotiateSaslAuth(token []byte, authzid string) ([]byte, error) { - // Using SSPI rather than of GSSAPI, relevant documentation of differences here: - // https://learn.microsoft.com/en-us/windows/win32/secauthn/sspi-kerberos-interoperability-with-gssapi - - // KERB_WRAP_NO_ENCRYPT (SECQOP_WRAP_NO_ENCRYPT) flag indicates Wrap and Unwrap - // should only sign & verify (not encrypt & decrypt). - const KERB_WRAP_NO_ENCRYPT = 0x80000001 - - // https://learn.microsoft.com/en-us/windows/win32/api/sspi/nf-sspi-decryptmessage - flags, inputPayload, err := c.ctx.DecryptMessage(token, 0) - if err != nil { - return nil, fmt.Errorf("error decrypting message: %w", err) - } - if flags&KERB_WRAP_NO_ENCRYPT == 0 { - // Encrypted message, this is unexpected. - return nil, fmt.Errorf("message encrypted") - } - - // `bytes` describes available security context: - // "the first octet of resulting cleartext as a - // bit-mask specifying the security layers supported by the server and - // the second through fourth octets as the maximum size output_message - // the server is able to receive (in network byte order). If the - // resulting cleartext is not 4 octets long, the client fails the - // negotiation. The client verifies that the server maximum buffer is 0 - // if the server does not advertise support for any security layer." - // From https://www.rfc-editor.org/rfc/rfc4752#section-3.1 - if len(inputPayload) != 4 { - return nil, fmt.Errorf("bad server token") - } - if inputPayload[0] == 0x0 && !bytes.Equal(inputPayload, []byte{0x0, 0x0, 0x0, 0x0}) { - return nil, fmt.Errorf("bad server token") - } - - // Security layers https://www.rfc-editor.org/rfc/rfc4422#section-3.7 - // https://www.rfc-editor.org/rfc/rfc4752#section-3.3 - // supportNoSecurity := input[0] & 0b00000001 - // supportIntegrity := input[0] & 0b00000010 - // supportPrivacy := input[0] & 0b00000100 - selectedSec := 0 // Disabled - var maxSecMsgSize uint32 - if selectedSec != 0 { - maxSecMsgSize, _, _, _, err = c.ctx.Sizes() - if err != nil { - return nil, fmt.Errorf("error getting security context max message size: %w", err) - } - } - - // https://learn.microsoft.com/en-us/windows/win32/api/sspi/nf-sspi-encryptmessage - inputPayload, err = c.ctx.EncryptMessage(handshakePayload(byte(selectedSec), maxSecMsgSize, []byte(authzid)), KERB_WRAP_NO_ENCRYPT, 0) - if err != nil { - return nil, fmt.Errorf("error encrypting message: %w", err) - } - - return inputPayload, nil -} - -func handshakePayload(secLayer byte, maxSize uint32, authzid []byte) []byte { - // construct payload and send unencrypted: - // "The client then constructs data, with the first octet containing the - // bit-mask specifying the selected security layer, the second through - // fourth octets containing in network byte order the maximum size - // output_message the client is able to receive (which MUST be 0 if the - // client does not support any security layer), and the remaining octets - // containing the UTF-8 [UTF8] encoded authorization identity. - // (Implementation note: The authorization identity is not terminated - // with the zero-valued (%x00) octet (e.g., the UTF-8 encoding of the - // NUL (U+0000) character)). The client passes the data to GSS_Wrap - // with conf_flag set to FALSE and responds with the generated - // output_message. The client can then consider the server - // authenticated." - // From https://www.rfc-editor.org/rfc/rfc4752#section-3.1 - - // Client picks security layer to use, 0 is disabled. - var selectedSecurity byte = secLayer - var truncatedSize uint32 // must be 0 if secLayer is 0 - if selectedSecurity != 0 { - // Only 3 bytes to describe the max size, set the maximum. - truncatedSize = 0b00000000_11111111_11111111_11111111 - if truncatedSize > maxSize { - truncatedSize = maxSize - } - } - - payload := make([]byte, 4, 4+len(authzid)) - binary.BigEndian.PutUint32(payload, truncatedSize) - payload[0] = selectedSecurity // Overwrites most significant byte of `maxSize` - payload = append(payload, []byte(authzid)...) - - return payload -} diff --git a/ldap.go b/ldap.go deleted file mode 100644 index 802a529..0000000 --- a/ldap.go +++ /dev/null @@ -1,389 +0,0 @@ -package ldap - -import ( - "fmt" - "io/ioutil" - "log" - "os" - "strings" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -// LDAP Application Codes -const ( - ApplicationBindRequest = 0 - ApplicationBindResponse = 1 - ApplicationUnbindRequest = 2 - ApplicationSearchRequest = 3 - ApplicationSearchResultEntry = 4 - ApplicationSearchResultDone = 5 - ApplicationModifyRequest = 6 - ApplicationModifyResponse = 7 - ApplicationAddRequest = 8 - ApplicationAddResponse = 9 - ApplicationDelRequest = 10 - ApplicationDelResponse = 11 - ApplicationModifyDNRequest = 12 - ApplicationModifyDNResponse = 13 - ApplicationCompareRequest = 14 - ApplicationCompareResponse = 15 - ApplicationAbandonRequest = 16 - ApplicationSearchResultReference = 19 - ApplicationExtendedRequest = 23 - ApplicationExtendedResponse = 24 - ApplicationIntermediateResponse = 25 -) - -// ApplicationMap contains human readable descriptions of LDAP Application Codes -var ApplicationMap = map[uint8]string{ - ApplicationBindRequest: "Bind Request", - ApplicationBindResponse: "Bind Response", - ApplicationUnbindRequest: "Unbind Request", - ApplicationSearchRequest: "Search Request", - ApplicationSearchResultEntry: "Search Result Entry", - ApplicationSearchResultDone: "Search Result Done", - ApplicationModifyRequest: "Modify Request", - ApplicationModifyResponse: "Modify Response", - ApplicationAddRequest: "Add Request", - ApplicationAddResponse: "Add Response", - ApplicationDelRequest: "Del Request", - ApplicationDelResponse: "Del Response", - ApplicationModifyDNRequest: "Modify DN Request", - ApplicationModifyDNResponse: "Modify DN Response", - ApplicationCompareRequest: "Compare Request", - ApplicationCompareResponse: "Compare Response", - ApplicationAbandonRequest: "Abandon Request", - ApplicationSearchResultReference: "Search Result Reference", - ApplicationExtendedRequest: "Extended Request", - ApplicationExtendedResponse: "Extended Response", - ApplicationIntermediateResponse: "Intermediate Response", -} - -// Ldap Behera Password Policy Draft 10 (https://tools.ietf.org/html/draft-behera-ldap-password-policy-10) -const ( - BeheraPasswordExpired = 0 - BeheraAccountLocked = 1 - BeheraChangeAfterReset = 2 - BeheraPasswordModNotAllowed = 3 - BeheraMustSupplyOldPassword = 4 - BeheraInsufficientPasswordQuality = 5 - BeheraPasswordTooShort = 6 - BeheraPasswordTooYoung = 7 - BeheraPasswordInHistory = 8 -) - -// BeheraPasswordPolicyErrorMap contains human readable descriptions of Behera Password Policy error codes -var BeheraPasswordPolicyErrorMap = map[int8]string{ - BeheraPasswordExpired: "Password expired", - BeheraAccountLocked: "Account locked", - BeheraChangeAfterReset: "Password must be changed", - BeheraPasswordModNotAllowed: "Policy prevents password modification", - BeheraMustSupplyOldPassword: "Policy requires old password in order to change password", - BeheraInsufficientPasswordQuality: "Password fails quality checks", - BeheraPasswordTooShort: "Password is too short for policy", - BeheraPasswordTooYoung: "Password has been changed too recently", - BeheraPasswordInHistory: "New password is in list of old passwords", -} - -var logger = log.New(os.Stderr, "", log.LstdFlags) - -// Logger allows clients to override the default logger -func Logger(l *log.Logger) { - logger = l -} - -// Adds descriptions to an LDAP Response packet for debugging -func addLDAPDescriptions(packet *ber.Packet) (err error) { - defer func() { - if r := recover(); r != nil { - err = NewError(ErrorDebugging, fmt.Errorf("ldap: cannot process packet to add descriptions: %s", r)) - } - }() - packet.Description = "LDAP Response" - packet.Children[0].Description = "Message ID" - - application := uint8(packet.Children[1].Tag) - packet.Children[1].Description = ApplicationMap[application] - - switch application { - case ApplicationBindRequest: - err = addRequestDescriptions(packet) - case ApplicationBindResponse: - err = addDefaultLDAPResponseDescriptions(packet) - case ApplicationUnbindRequest: - err = addRequestDescriptions(packet) - case ApplicationSearchRequest: - err = addRequestDescriptions(packet) - case ApplicationSearchResultEntry: - packet.Children[1].Children[0].Description = "Object Name" - packet.Children[1].Children[1].Description = "Attributes" - for _, child := range packet.Children[1].Children[1].Children { - child.Description = "Attribute" - child.Children[0].Description = "Attribute Name" - child.Children[1].Description = "Attribute Values" - for _, grandchild := range child.Children[1].Children { - grandchild.Description = "Attribute Value" - } - } - if len(packet.Children) == 3 { - err = addControlDescriptions(packet.Children[2]) - } - case ApplicationSearchResultDone: - err = addDefaultLDAPResponseDescriptions(packet) - case ApplicationModifyRequest: - err = addRequestDescriptions(packet) - case ApplicationModifyResponse: - case ApplicationAddRequest: - err = addRequestDescriptions(packet) - case ApplicationAddResponse: - case ApplicationDelRequest: - err = addRequestDescriptions(packet) - case ApplicationDelResponse: - case ApplicationModifyDNRequest: - err = addRequestDescriptions(packet) - case ApplicationModifyDNResponse: - case ApplicationCompareRequest: - err = addRequestDescriptions(packet) - case ApplicationCompareResponse: - case ApplicationAbandonRequest: - err = addRequestDescriptions(packet) - case ApplicationSearchResultReference: - case ApplicationExtendedRequest: - err = addRequestDescriptions(packet) - case ApplicationExtendedResponse: - } - - return err -} - -func addControlDescriptions(packet *ber.Packet) error { - packet.Description = "Controls" - for _, child := range packet.Children { - var value *ber.Packet - controlType := "" - child.Description = "Control" - switch len(child.Children) { - case 0: - // at least one child is required for control type - return fmt.Errorf("at least one child is required for control type") - - case 1: - // just type, no criticality or value - controlType = child.Children[0].Value.(string) - child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" - - case 2: - controlType = child.Children[0].Value.(string) - child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" - // Children[1] could be criticality or value (both are optional) - // duck-type on whether this is a boolean - if _, ok := child.Children[1].Value.(bool); ok { - child.Children[1].Description = "Criticality" - } else { - child.Children[1].Description = "Control Value" - value = child.Children[1] - } - - case 3: - // criticality and value present - controlType = child.Children[0].Value.(string) - child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" - child.Children[1].Description = "Criticality" - child.Children[2].Description = "Control Value" - value = child.Children[2] - - default: - // more than 3 children is invalid - return fmt.Errorf("more than 3 children for control packet found") - } - - if value == nil { - continue - } - switch controlType { - case ControlTypePaging: - value.Description += " (Paging)" - if value.Value != nil { - valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) - if err != nil { - return fmt.Errorf("failed to decode data bytes: %s", err) - } - value.Data.Truncate(0) - value.Value = nil - valueChildren.Children[1].Value = valueChildren.Children[1].Data.Bytes() - value.AppendChild(valueChildren) - } - value.Children[0].Description = "Real Search Control Value" - value.Children[0].Children[0].Description = "Paging Size" - value.Children[0].Children[1].Description = "Cookie" - - case ControlTypeBeheraPasswordPolicy: - value.Description += " (Password Policy - Behera Draft)" - if value.Value != nil { - valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) - if err != nil { - return fmt.Errorf("failed to decode data bytes: %s", err) - } - value.Data.Truncate(0) - value.Value = nil - value.AppendChild(valueChildren) - } - sequence := value.Children[0] - for _, child := range sequence.Children { - if child.Tag == 0 { - // Warning - warningPacket := child.Children[0] - val, err := ber.ParseInt64(warningPacket.Data.Bytes()) - if err != nil { - return fmt.Errorf("failed to decode data bytes: %s", err) - } - if warningPacket.Tag == 0 { - // timeBeforeExpiration - value.Description += " (TimeBeforeExpiration)" - warningPacket.Value = val - } else if warningPacket.Tag == 1 { - // graceAuthNsRemaining - value.Description += " (GraceAuthNsRemaining)" - warningPacket.Value = val - } - } else if child.Tag == 1 { - // Error - bs := child.Data.Bytes() - if len(bs) != 1 || bs[0] > 8 { - return fmt.Errorf("failed to decode data bytes: %s", "invalid PasswordPolicyResponse enum value") - } - val := int8(bs[0]) - child.Description = "Error" - child.Value = val - } - } - } - } - return nil -} - -func addRequestDescriptions(packet *ber.Packet) error { - packet.Description = "LDAP Request" - packet.Children[0].Description = "Message ID" - packet.Children[1].Description = ApplicationMap[uint8(packet.Children[1].Tag)] - if len(packet.Children) == 3 { - return addControlDescriptions(packet.Children[2]) - } - return nil -} - -func addDefaultLDAPResponseDescriptions(packet *ber.Packet) error { - resultCode := uint16(LDAPResultSuccess) - matchedDN := "" - description := "Success" - if err := GetLDAPError(packet); err != nil { - resultCode = err.(*Error).ResultCode - matchedDN = err.(*Error).MatchedDN - description = "Error Message" - } - - packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[resultCode] + ")" - packet.Children[1].Children[1].Description = "Matched DN (" + matchedDN + ")" - packet.Children[1].Children[2].Description = description - if len(packet.Children[1].Children) > 3 { - packet.Children[1].Children[3].Description = "Referral" - } - if len(packet.Children) == 3 { - return addControlDescriptions(packet.Children[2]) - } - return nil -} - -// DebugBinaryFile reads and prints packets from the given filename -func DebugBinaryFile(fileName string) error { - file, err := ioutil.ReadFile(fileName) - if err != nil { - return NewError(ErrorDebugging, err) - } - ber.PrintBytes(os.Stdout, file, "") - packet, err := ber.DecodePacketErr(file) - if err != nil { - return fmt.Errorf("failed to decode packet: %s", err) - } - if err := addLDAPDescriptions(packet); err != nil { - return err - } - ber.PrintPacket(packet) - - return nil -} - -func mustEscape(c byte) bool { - return c > 0x7f || c == '(' || c == ')' || c == '\\' || c == '*' || c == 0 -} - -// EscapeFilter escapes from the provided LDAP filter string the special -// characters in the set `()*\` and those out of the range 0 < c < 0x80, -// as defined in RFC4515. -func EscapeFilter(filter string) string { - const hexValues = "0123456789abcdef" - escape := 0 - for i := 0; i < len(filter); i++ { - if mustEscape(filter[i]) { - escape++ - } - } - if escape == 0 { - return filter - } - buf := make([]byte, len(filter)+escape*2) - for i, j := 0, 0; i < len(filter); i++ { - c := filter[i] - if mustEscape(c) { - buf[j+0] = '\\' - buf[j+1] = hexValues[c>>4] - buf[j+2] = hexValues[c&0xf] - j += 3 - } else { - buf[j] = c - j++ - } - } - return string(buf) -} - -// EscapeDN escapes distinguished names as described in RFC4514. Characters in the -// set `"+,;<>\` are escaped by prepending a backslash, which is also done for trailing -// spaces or a leading `#`. Null bytes are replaced with `\00`. -func EscapeDN(dn string) string { - if dn == "" { - return "" - } - - builder := strings.Builder{} - - for i, r := range dn { - // Escape leading and trailing spaces - if (i == 0 || i == len(dn)-1) && r == ' ' { - builder.WriteRune('\\') - builder.WriteRune(r) - continue - } - - // Escape leading '#' - if i == 0 && r == '#' { - builder.WriteRune('\\') - builder.WriteRune(r) - continue - } - - // Escape characters as defined in RFC4514 - switch r { - case '"', '+', ',', ';', '<', '>', '\\': - builder.WriteRune('\\') - builder.WriteRune(r) - case '\x00': // Null byte may not be escaped by a leading backslash - builder.WriteString("\\00") - default: - builder.WriteRune(r) - } - } - - return builder.String() -} diff --git a/ldap_test.go b/ldap_test.go deleted file mode 100644 index 5b96e03..0000000 --- a/ldap_test.go +++ /dev/null @@ -1,412 +0,0 @@ -package ldap - -import ( - "context" - "crypto/tls" - "log" - "testing" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -const ( - ldapServer = "ldap://ldap.itd.umich.edu:389" - ldapsServer = "ldaps://ldap.itd.umich.edu:636" - baseDN = "dc=umich,dc=edu" -) - -var filter = []string{ - "(cn=cis-fac)", - "(&(owner=*)(cn=cis-fac))", - "(&(objectclass=rfc822mailgroup)(cn=*Computer*))", - "(&(objectclass=rfc822mailgroup)(cn=*Mathematics*))", -} - -var attributes = []string{ - "cn", - "description", -} - -func TestUnsecureDialURL(t *testing.T) { - l, err := DialURL(ldapServer) - if err != nil { - t.Fatal(err) - } - defer l.Close() -} - -func TestSecureDialURL(t *testing.T) { - l, err := DialURL(ldapsServer, DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true})) - if err != nil { - t.Fatal(err) - } - defer l.Close() -} - -func TestStartTLS(t *testing.T) { - l, err := DialURL(ldapServer) - if err != nil { - t.Fatal(err) - } - defer l.Close() - err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) - if err != nil { - t.Fatal(err) - } -} - -func TestTLSConnectionState(t *testing.T) { - l, err := DialURL(ldapServer) - if err != nil { - t.Fatal(err) - } - defer l.Close() - err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) - if err != nil { - t.Fatal(err) - } - - cs, ok := l.TLSConnectionState() - if !ok { - t.Errorf("TLSConnectionState returned ok == false; want true") - } - if cs.Version == 0 || !cs.HandshakeComplete { - t.Errorf("ConnectionState = %#v; expected Version != 0 and HandshakeComplete = true", cs) - } -} - -func TestSearch(t *testing.T) { - l, err := DialURL(ldapServer) - if err != nil { - t.Fatal(err) - } - defer l.Close() - - searchRequest := NewSearchRequest( - baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, - filter[0], - attributes, - nil) - - sr, err := l.Search(searchRequest) - if err != nil { - t.Fatal(err) - } - t.Logf("TestSearch: %s -> num of entries = %d", searchRequest.Filter, len(sr.Entries)) -} - -func TestSearchStartTLS(t *testing.T) { - l, err := DialURL(ldapServer) - if err != nil { - t.Fatal(err) - } - defer l.Close() - - searchRequest := NewSearchRequest( - baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, - filter[0], - attributes, - nil) - - sr, err := l.Search(searchRequest) - if err != nil { - t.Fatal(err) - } - - t.Logf("TestSearchStartTLS: %s -> num of entries = %d", searchRequest.Filter, len(sr.Entries)) - - t.Log("TestSearchStartTLS: upgrading with startTLS") - err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) - if err != nil { - t.Fatal(err) - } - - sr, err = l.Search(searchRequest) - if err != nil { - t.Fatal(err) - } - - t.Logf("TestSearchStartTLS: %s -> num of entries = %d", searchRequest.Filter, len(sr.Entries)) -} - -func TestSearchWithPaging(t *testing.T) { - l, err := DialURL(ldapServer) - if err != nil { - t.Fatal(err) - } - defer l.Close() - - err = l.UnauthenticatedBind("") - if err != nil { - t.Fatal(err) - } - - searchRequest := NewSearchRequest( - baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, - filter[2], - attributes, - nil) - sr, err := l.SearchWithPaging(searchRequest, 5) - if err != nil { - t.Fatal(err) - } - - t.Logf("TestSearchWithPaging: %s -> num of entries = %d", searchRequest.Filter, len(sr.Entries)) - - searchRequest = NewSearchRequest( - baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, - filter[2], - attributes, - []Control{NewControlPaging(5)}) - sr, err = l.SearchWithPaging(searchRequest, 5) - if err != nil { - t.Fatal(err) - } - - t.Logf("TestSearchWithPaging: %s -> num of entries = %d", searchRequest.Filter, len(sr.Entries)) - - searchRequest = NewSearchRequest( - baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, - filter[2], - attributes, - []Control{NewControlPaging(500)}) - _, err = l.SearchWithPaging(searchRequest, 5) - if err == nil { - t.Fatal("expected an error when paging size in control in search request doesn't match size given in call, got none") - } -} - -func searchGoroutine(t *testing.T, l *Conn, results chan *SearchResult, i int) { - searchRequest := NewSearchRequest( - baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, - filter[i], - attributes, - nil) - sr, err := l.Search(searchRequest) - if err != nil { - t.Error(err) - results <- nil - return - } - results <- sr -} - -func testMultiGoroutineSearch(t *testing.T, TLS bool, startTLS bool) { - var l *Conn - var err error - if TLS { - l, err = DialURL(ldapsServer, DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true})) - if err != nil { - t.Fatal(err) - } - defer l.Close() - } else { - l, err = DialURL(ldapServer) - if err != nil { - t.Fatal(err) - } - defer l.Close() - if startTLS { - t.Log("TestMultiGoroutineSearch: using StartTLS...") - err := l.StartTLS(&tls.Config{InsecureSkipVerify: true}) - if err != nil { - t.Fatal(err) - } - } - } - - results := make([]chan *SearchResult, len(filter)) - for i := range filter { - results[i] = make(chan *SearchResult) - go searchGoroutine(t, l, results[i], i) - } - for i := range filter { - sr := <-results[i] - if sr == nil { - t.Errorf("Did not receive results from goroutine for %q", filter[i]) - } else { - t.Logf("TestMultiGoroutineSearch(%d): %s -> num of entries = %d", i, filter[i], len(sr.Entries)) - } - } -} - -func TestMultiGoroutineSearch(t *testing.T) { - testMultiGoroutineSearch(t, false, false) - testMultiGoroutineSearch(t, true, true) - testMultiGoroutineSearch(t, false, true) -} - -func TestEscapeFilter(t *testing.T) { - if got, want := EscapeFilter("a\x00b(c)d*e\\f"), `a\00b\28c\29d\2ae\5cf`; got != want { - t.Errorf("Got %s, expected %s", want, got) - } - if got, want := EscapeFilter("Lučić"), `Lu\c4\8di\c4\87`; got != want { - t.Errorf("Got %s, expected %s", want, got) - } -} - -func TestCompare(t *testing.T) { - l, err := DialURL(ldapServer) - if err != nil { - t.Fatal(err) - } - defer l.Close() - - const dn = "cn=math mich,ou=User Groups,ou=Groups,dc=umich,dc=edu" - const attribute = "cn" - const value = "math mich" - - sr, err := l.Compare(dn, attribute, value) - if err != nil { - t.Fatal(err) - } - - t.Log("Compare result:", sr) -} - -func TestMatchDNError(t *testing.T) { - l, err := DialURL(ldapServer) - if err != nil { - t.Fatal(err) - } - defer l.Close() - - const wrongBase = "ou=roups,dc=umich,dc=edu" - - searchRequest := NewSearchRequest( - wrongBase, - ScopeWholeSubtree, DerefAlways, 0, 0, false, - filter[0], - attributes, - nil) - - _, err = l.Search(searchRequest) - if err == nil { - t.Fatal("Expected Error, got nil") - } - - t.Log("TestMatchDNError:", err) -} - -func Test_addControlDescriptions(t *testing.T) { - type args struct { - packet *ber.Packet - } - tests := []struct { - name string - args args - wantErr bool - }{ - {name: "timeBeforeExpiration", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x29, 0x30, 0x27, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0xa, 0x30, 0x8, 0xa0, 0x6, 0x80, 0x4, 0x7f, 0xff, 0xf6, 0x5c})}, wantErr: false}, - {name: "graceAuthNsRemaining", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x26, 0x30, 0x24, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x7, 0x30, 0x5, 0xa0, 0x3, 0x81, 0x1, 0x11})}, wantErr: false}, - {name: "passwordExpired", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x0})}, wantErr: false}, - {name: "accountLocked", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x1})}, wantErr: false}, - {name: "passwordModNotAllowed", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x3})}, wantErr: false}, - {name: "mustSupplyOldPassword", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x4})}, wantErr: false}, - {name: "insufficientPasswordQuality", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x5})}, wantErr: false}, - {name: "passwordTooShort", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x6})}, wantErr: false}, - {name: "passwordTooYoung", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x7})}, wantErr: false}, - {name: "passwordInHistory", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x8})}, wantErr: false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := addControlDescriptions(tt.args.packet); (err != nil) != tt.wantErr { - t.Errorf("addControlDescriptions() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestEscapeDN(t *testing.T) { - tests := []struct { - name string - dn string - want string - }{ - {name: "emptyString", dn: "", want: ""}, - {name: "comma", dn: "test,user", want: "test\\,user"}, - {name: "numberSign", dn: "#test#user#", want: "\\#test#user#"}, - {name: "backslash", dn: "\\test\\user\\", want: "\\\\test\\\\user\\\\"}, - {name: "whitespaces", dn: " test user ", want: "\\ test user \\ "}, - {name: "nullByte", dn: "\u0000te\x00st\x00user" + string(rune(0)), want: "\\00te\\00st\\00user\\00"}, - {name: "variousCharacters", dn: "test\"+,;<>\\-_user", want: "test\\\"\\+\\,\\;\\<\\>\\\\-_user"}, - {name: "multiByteRunes", dn: "test\u0391user ", want: "test\u0391user\\ "}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := EscapeDN(tt.dn); got != tt.want { - t.Errorf("EscapeDN(%s) = %s, expected %s", tt.dn, got, tt.want) - } - }) - } -} - -func TestSearchAsync(t *testing.T) { - l, err := DialURL(ldapServer) - if err != nil { - t.Fatal(err) - } - defer l.Close() - - searchRequest := NewSearchRequest( - baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, - filter[2], - attributes, - nil) - - srs := make([]*Entry, 0) - ctx := context.Background() - r := l.SearchAsync(ctx, searchRequest, 64) - for r.Next() { - srs = append(srs, r.Entry()) - } - if err := r.Err(); err != nil { - log.Fatal(err) - } - - t.Logf("TestSearcAsync: %s -> num of entries = %d", searchRequest.Filter, len(srs)) -} - -func TestSearchAsyncAndCancel(t *testing.T) { - l, err := DialURL(ldapServer) - if err != nil { - t.Fatal(err) - } - defer l.Close() - - searchRequest := NewSearchRequest( - baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, - filter[2], - attributes, - nil) - - cancelNum := 10 - srs := make([]*Entry, 0) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - r := l.SearchAsync(ctx, searchRequest, 0) - for r.Next() { - srs = append(srs, r.Entry()) - if len(srs) == cancelNum { - cancel() - } - } - if err := r.Err(); err != nil { - log.Fatal(err) - } - - if len(srs) > cancelNum+3 { - // the cancellation process is asynchronous, - // so it might get some entries after calling cancel() - t.Errorf("Got entries %d, expected < %d", len(srs), cancelNum+3) - } - t.Logf("TestSearchAsyncAndCancel: %s -> num of entries = %d", searchRequest.Filter, len(srs)) -} diff --git a/moddn.go b/moddn.go deleted file mode 100644 index 84a6488..0000000 --- a/moddn.go +++ /dev/null @@ -1,102 +0,0 @@ -package ldap - -import ( - "fmt" - ber "github.com/go-asn1-ber/asn1-ber" -) - -// ModifyDNRequest holds the request to modify a DN -type ModifyDNRequest struct { - DN string - NewRDN string - DeleteOldRDN bool - NewSuperior string - // Controls hold optional controls to send with the request - Controls []Control -} - -// NewModifyDNRequest creates a new request which can be passed to ModifyDN(). -// -// To move an object in the tree, set the "newSup" to the new parent entry DN. Use an -// empty string for just changing the object's RDN. -// -// For moving the object without renaming, the "rdn" must be the first -// RDN of the given DN. -// -// A call like -// -// mdnReq := NewModifyDNRequest("uid=someone,dc=example,dc=org", "uid=newname", true, "") -// -// will setup the request to just rename uid=someone,dc=example,dc=org to -// uid=newname,dc=example,dc=org. -func NewModifyDNRequest(dn string, rdn string, delOld bool, newSup string) *ModifyDNRequest { - return &ModifyDNRequest{ - DN: dn, - NewRDN: rdn, - DeleteOldRDN: delOld, - NewSuperior: newSup, - } -} - -// NewModifyDNWithControlsRequest creates a new request which can be passed to ModifyDN() -// and also allows setting LDAP request controls. -// -// Refer NewModifyDNRequest for other parameters -func NewModifyDNWithControlsRequest(dn string, rdn string, delOld bool, - newSup string, controls []Control) *ModifyDNRequest { - return &ModifyDNRequest{ - DN: dn, - NewRDN: rdn, - DeleteOldRDN: delOld, - NewSuperior: newSup, - Controls: controls, - } -} - -func (req *ModifyDNRequest) appendTo(envelope *ber.Packet) error { - pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationModifyDNRequest, nil, "Modify DN Request") - pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.DN, "DN")) - pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.NewRDN, "New RDN")) - if req.DeleteOldRDN { - buf := []byte{0xff} - pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, string(buf), "Delete old RDN")) - } else { - pkt.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, req.DeleteOldRDN, "Delete old RDN")) - } - if req.NewSuperior != "" { - pkt.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, req.NewSuperior, "New Superior")) - } - - envelope.AppendChild(pkt) - if len(req.Controls) > 0 { - envelope.AppendChild(encodeControls(req.Controls)) - } - - return nil -} - -// ModifyDN renames the given DN and optionally move to another base (when the "newSup" argument -// to NewModifyDNRequest() is not ""). -func (l *Conn) ModifyDN(m *ModifyDNRequest) error { - msgCtx, err := l.doRequest(m) - if err != nil { - return err - } - defer l.finishMessage(msgCtx) - - packet, err := l.readPacket(msgCtx) - if err != nil { - return err - } - - if packet.Children[1].Tag == ApplicationModifyDNResponse { - err := GetLDAPError(packet) - if err != nil { - return err - } - } else { - return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) - } - - return nil -} diff --git a/modify.go b/modify.go deleted file mode 100644 index 0e50136..0000000 --- a/modify.go +++ /dev/null @@ -1,181 +0,0 @@ -package ldap - -import ( - "errors" - "fmt" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -// Change operation choices -const ( - AddAttribute = 0 - DeleteAttribute = 1 - ReplaceAttribute = 2 - IncrementAttribute = 3 // (https://tools.ietf.org/html/rfc4525) -) - -// PartialAttribute for a ModifyRequest as defined in https://tools.ietf.org/html/rfc4511 -type PartialAttribute struct { - // Type is the type of the partial attribute - Type string - // Vals are the values of the partial attribute - Vals []string -} - -func (p *PartialAttribute) encode() *ber.Packet { - seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "PartialAttribute") - seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, p.Type, "Type")) - set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") - for _, value := range p.Vals { - set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) - } - seq.AppendChild(set) - return seq -} - -// Change for a ModifyRequest as defined in https://tools.ietf.org/html/rfc4511 -type Change struct { - // Operation is the type of change to be made - Operation uint - // Modification is the attribute to be modified - Modification PartialAttribute -} - -func (c *Change) encode() *ber.Packet { - change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") - change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(c.Operation), "Operation")) - change.AppendChild(c.Modification.encode()) - return change -} - -// ModifyRequest as defined in https://tools.ietf.org/html/rfc4511 -type ModifyRequest struct { - // DN is the distinguishedName of the directory entry to modify - DN string - // Changes contain the attributes to modify - Changes []Change - // Controls hold optional controls to send with the request - Controls []Control -} - -// Add appends the given attribute to the list of changes to be made -func (req *ModifyRequest) Add(attrType string, attrVals []string) { - req.appendChange(AddAttribute, attrType, attrVals) -} - -// Delete appends the given attribute to the list of changes to be made -func (req *ModifyRequest) Delete(attrType string, attrVals []string) { - req.appendChange(DeleteAttribute, attrType, attrVals) -} - -// Replace appends the given attribute to the list of changes to be made -func (req *ModifyRequest) Replace(attrType string, attrVals []string) { - req.appendChange(ReplaceAttribute, attrType, attrVals) -} - -// Increment appends the given attribute to the list of changes to be made -func (req *ModifyRequest) Increment(attrType string, attrVal string) { - req.appendChange(IncrementAttribute, attrType, []string{attrVal}) -} - -func (req *ModifyRequest) appendChange(operation uint, attrType string, attrVals []string) { - req.Changes = append(req.Changes, Change{operation, PartialAttribute{Type: attrType, Vals: attrVals}}) -} - -func (req *ModifyRequest) appendTo(envelope *ber.Packet) error { - pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationModifyRequest, nil, "Modify Request") - pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.DN, "DN")) - changes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Changes") - for _, change := range req.Changes { - changes.AppendChild(change.encode()) - } - pkt.AppendChild(changes) - - envelope.AppendChild(pkt) - if len(req.Controls) > 0 { - envelope.AppendChild(encodeControls(req.Controls)) - } - - return nil -} - -// NewModifyRequest creates a modify request for the given DN -func NewModifyRequest(dn string, controls []Control) *ModifyRequest { - return &ModifyRequest{ - DN: dn, - Controls: controls, - } -} - -// Modify performs the ModifyRequest -func (l *Conn) Modify(modifyRequest *ModifyRequest) error { - msgCtx, err := l.doRequest(modifyRequest) - if err != nil { - return err - } - defer l.finishMessage(msgCtx) - - packet, err := l.readPacket(msgCtx) - if err != nil { - return err - } - - if packet.Children[1].Tag == ApplicationModifyResponse { - err := GetLDAPError(packet) - if err != nil { - return err - } - } else { - return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) - } - - return nil -} - -// ModifyResult holds the server's response to a modify request -type ModifyResult struct { - // Controls are the returned controls - Controls []Control - // Referral is the returned referral - Referral string -} - -// ModifyWithResult performs the ModifyRequest and returns the result -func (l *Conn) ModifyWithResult(modifyRequest *ModifyRequest) (*ModifyResult, error) { - msgCtx, err := l.doRequest(modifyRequest) - if err != nil { - return nil, err - } - defer l.finishMessage(msgCtx) - - result := &ModifyResult{ - Controls: make([]Control, 0), - } - - l.Debug.Printf("%d: waiting for response", msgCtx.id) - packet, err := l.readPacket(msgCtx) - if err != nil { - return nil, err - } - - switch packet.Children[1].Tag { - case ApplicationModifyResponse: - if err = GetLDAPError(packet); err != nil { - result.Referral = getReferral(err, packet) - - return result, err - } - if len(packet.Children) == 3 { - for _, child := range packet.Children[2].Children { - decodedChild, err := DecodeControl(child) - if err != nil { - return nil, errors.New("failed to decode child control: " + err.Error()) - } - result.Controls = append(result.Controls, decodedChild) - } - } - } - l.Debug.Printf("%d: returning", msgCtx.id) - return result, nil -} diff --git a/passwdmodify.go b/passwdmodify.go deleted file mode 100644 index 72a2351..0000000 --- a/passwdmodify.go +++ /dev/null @@ -1,119 +0,0 @@ -package ldap - -import ( - "fmt" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -const ( - passwordModifyOID = "1.3.6.1.4.1.4203.1.11.1" -) - -// PasswordModifyRequest implements the Password Modify Extended Operation as defined in https://www.ietf.org/rfc/rfc3062.txt -type PasswordModifyRequest struct { - // UserIdentity is an optional string representation of the user associated with the request. - // This string may or may not be an LDAPDN [RFC2253]. - // If no UserIdentity field is present, the request acts up upon the password of the user currently associated with the LDAP session - UserIdentity string - // OldPassword, if present, contains the user's current password - OldPassword string - // NewPassword, if present, contains the desired password for this user - NewPassword string -} - -// PasswordModifyResult holds the server response to a PasswordModifyRequest -type PasswordModifyResult struct { - // GeneratedPassword holds a password generated by the server, if present - GeneratedPassword string - // Referral are the returned referral - Referral string -} - -func (req *PasswordModifyRequest) appendTo(envelope *ber.Packet) error { - pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Password Modify Extended Operation") - pkt.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, passwordModifyOID, "Extended Request Name: Password Modify OID")) - - extendedRequestValue := ber.Encode(ber.ClassContext, ber.TypePrimitive, 1, nil, "Extended Request Value: Password Modify Request") - passwordModifyRequestValue := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Password Modify Request") - if req.UserIdentity != "" { - passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, req.UserIdentity, "User Identity")) - } - if req.OldPassword != "" { - passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 1, req.OldPassword, "Old Password")) - } - if req.NewPassword != "" { - passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 2, req.NewPassword, "New Password")) - } - extendedRequestValue.AppendChild(passwordModifyRequestValue) - - pkt.AppendChild(extendedRequestValue) - - envelope.AppendChild(pkt) - - return nil -} - -// NewPasswordModifyRequest creates a new PasswordModifyRequest -// -// According to the RFC 3602 (https://tools.ietf.org/html/rfc3062): -// userIdentity is a string representing the user associated with the request. -// This string may or may not be an LDAPDN (RFC 2253). -// If userIdentity is empty then the operation will act on the user associated -// with the session. -// -// oldPassword is the current user's password, it can be empty or it can be -// needed depending on the session user access rights (usually an administrator -// can change a user's password without knowing the current one) and the -// password policy (see pwdSafeModify password policy's attribute) -// -// newPassword is the desired user's password. If empty the server can return -// an error or generate a new password that will be available in the -// PasswordModifyResult.GeneratedPassword -func NewPasswordModifyRequest(userIdentity string, oldPassword string, newPassword string) *PasswordModifyRequest { - return &PasswordModifyRequest{ - UserIdentity: userIdentity, - OldPassword: oldPassword, - NewPassword: newPassword, - } -} - -// PasswordModify performs the modification request -func (l *Conn) PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*PasswordModifyResult, error) { - msgCtx, err := l.doRequest(passwordModifyRequest) - if err != nil { - return nil, err - } - defer l.finishMessage(msgCtx) - - packet, err := l.readPacket(msgCtx) - if err != nil { - return nil, err - } - - result := &PasswordModifyResult{} - - if packet.Children[1].Tag == ApplicationExtendedResponse { - if err = GetLDAPError(packet); err != nil { - result.Referral = getReferral(err, packet) - - return result, err - } - } else { - return nil, NewError(ErrorUnexpectedResponse, fmt.Errorf("unexpected Response: %d", packet.Children[1].Tag)) - } - - extendedResponse := packet.Children[1] - for _, child := range extendedResponse.Children { - if child.Tag == ber.TagEmbeddedPDV { - passwordModifyResponseValue := ber.DecodePacket(child.Data.Bytes()) - if len(passwordModifyResponseValue.Children) == 1 { - if passwordModifyResponseValue.Children[0].Tag == ber.TagEOC { - result.GeneratedPassword = ber.DecodeString(passwordModifyResponseValue.Children[0].Data.Bytes()) - } - } - } - } - - return result, nil -} diff --git a/request.go b/request.go deleted file mode 100644 index b64f232..0000000 --- a/request.go +++ /dev/null @@ -1,110 +0,0 @@ -package ldap - -import ( - "errors" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -var ( - errRespChanClosed = errors.New("ldap: response channel closed") - errCouldNotRetMsg = errors.New("ldap: could not retrieve message") - // ErrNilConnection is returned if doRequest is called with a nil connection. - ErrNilConnection = errors.New("ldap: conn is nil, expected net.Conn") -) - -type request interface { - appendTo(*ber.Packet) error -} - -type requestFunc func(*ber.Packet) error - -func (f requestFunc) appendTo(p *ber.Packet) error { - return f(p) -} - -func (l *Conn) doRequest(req request) (*messageContext, error) { - if l == nil || l.conn == nil { - return nil, ErrNilConnection - } - - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") - packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) - if err := req.appendTo(packet); err != nil { - return nil, err - } - - if l.Debug { - l.Debug.PrintPacket(packet) - } - - msgCtx, err := l.sendMessage(packet) - if err != nil { - return nil, err - } - l.Debug.Printf("%d: returning", msgCtx.id) - return msgCtx, nil -} - -func (l *Conn) readPacket(msgCtx *messageContext) (*ber.Packet, error) { - l.Debug.Printf("%d: waiting for response", msgCtx.id) - packetResponse, ok := <-msgCtx.responses - if !ok { - return nil, NewError(ErrorNetwork, errRespChanClosed) - } - packet, err := packetResponse.ReadPacket() - l.Debug.Printf("%d: got response %p", msgCtx.id, packet) - if err != nil { - return nil, err - } - - if packet == nil { - return nil, NewError(ErrorNetwork, errCouldNotRetMsg) - } - - if l.Debug { - if err = addLDAPDescriptions(packet); err != nil { - return nil, err - } - l.Debug.PrintPacket(packet) - } - return packet, nil -} - -func getReferral(err error, packet *ber.Packet) (referral string) { - if !IsErrorWithCode(err, LDAPResultReferral) { - return "" - } - - if len(packet.Children) < 2 { - return "" - } - - // The packet Tag itself (of child 2) is generally a ber.TagObjectDescriptor with referrals however OpenLDAP - // seemingly returns a ber.Tag.GeneralizedTime. Every currently tested LDAP server which returns referrals returns - // an ASN.1 BER packet with the Type of ber.TypeConstructed and Class of ber.ClassApplication however. Thus this - // check expressly checks these fields instead. - // - // Related Issues: - // - https://github.com/authelia/authelia/issues/4199 (downstream) - if len(packet.Children[1].Children) == 0 || (packet.Children[1].TagType != ber.TypeConstructed || packet.Children[1].ClassType != ber.ClassApplication) { - return "" - } - - var ok bool - - for _, child := range packet.Children[1].Children { - // The referral URI itself should be contained within a child which has a Tag of ber.BitString or - // ber.TagPrintableString, and the Type of ber.TypeConstructed and the Class of ClassContext. As soon as any of - // these conditions is not true we can skip this child. - if (child.Tag != ber.TagBitString && child.Tag != ber.TagPrintableString) || child.TagType != ber.TypeConstructed || child.ClassType != ber.ClassContext { - continue - } - - if referral, ok = child.Children[0].Value.(string); ok { - return referral - } - } - - return "" -} diff --git a/response.go b/response.go deleted file mode 100644 index 0eae100..0000000 --- a/response.go +++ /dev/null @@ -1,206 +0,0 @@ -package ldap - -import ( - "context" - "errors" - "fmt" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -// Response defines an interface to get data from an LDAP server -type Response interface { - Entry() *Entry - Referral() string - Controls() []Control - Err() error - Next() bool -} - -type searchResponse struct { - conn *Conn - ch chan *SearchSingleResult - - entry *Entry - referral string - controls []Control - err error -} - -// Entry returns an entry from the given search request -func (r *searchResponse) Entry() *Entry { - return r.entry -} - -// Referral returns a referral from the given search request -func (r *searchResponse) Referral() string { - return r.referral -} - -// Controls returns controls from the given search request -func (r *searchResponse) Controls() []Control { - return r.controls -} - -// Err returns an error when the given search request was failed -func (r *searchResponse) Err() error { - return r.err -} - -// Next returns whether next data exist or not -func (r *searchResponse) Next() bool { - res, ok := <-r.ch - if !ok { - return false - } - if res == nil { - return false - } - r.err = res.Error - if r.err != nil { - return false - } - r.entry = res.Entry - r.referral = res.Referral - r.controls = res.Controls - return true -} - -func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest) { - go func() { - defer func() { - close(r.ch) - if err := recover(); err != nil { - r.conn.err = fmt.Errorf("ldap: recovered panic in searchResponse: %v", err) - } - }() - - if r.conn.IsClosing() { - return - } - - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") - packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, r.conn.nextMessageID(), "MessageID")) - // encode search request - err := searchRequest.appendTo(packet) - if err != nil { - r.ch <- &SearchSingleResult{Error: err} - return - } - r.conn.Debug.PrintPacket(packet) - - msgCtx, err := r.conn.sendMessage(packet) - if err != nil { - r.ch <- &SearchSingleResult{Error: err} - return - } - defer r.conn.finishMessage(msgCtx) - - foundSearchSingleResultDone := false - for !foundSearchSingleResultDone { - r.conn.Debug.Printf("%d: waiting for response", msgCtx.id) - select { - case <-ctx.Done(): - r.conn.Debug.Printf("%d: %s", msgCtx.id, ctx.Err().Error()) - return - case packetResponse, ok := <-msgCtx.responses: - if !ok { - err := NewError(ErrorNetwork, errors.New("ldap: response channel closed")) - r.ch <- &SearchSingleResult{Error: err} - return - } - packet, err = packetResponse.ReadPacket() - r.conn.Debug.Printf("%d: got response %p", msgCtx.id, packet) - if err != nil { - r.ch <- &SearchSingleResult{Error: err} - return - } - - if r.conn.Debug { - if err := addLDAPDescriptions(packet); err != nil { - r.ch <- &SearchSingleResult{Error: err} - return - } - ber.PrintPacket(packet) - } - - switch packet.Children[1].Tag { - case ApplicationSearchResultEntry: - result := &SearchSingleResult{ - Entry: &Entry{ - DN: packet.Children[1].Children[0].Value.(string), - Attributes: unpackAttributes(packet.Children[1].Children[1].Children), - }, - } - if len(packet.Children) != 3 { - r.ch <- result - continue - } - decoded, err := DecodeControl(packet.Children[2].Children[0]) - if err != nil { - werr := fmt.Errorf("failed to decode search result entry: %w", err) - result.Error = werr - r.ch <- result - return - } - result.Controls = append(result.Controls, decoded) - r.ch <- result - - case ApplicationSearchResultDone: - if err := GetLDAPError(packet); err != nil { - r.ch <- &SearchSingleResult{Error: err} - return - } - if len(packet.Children) == 3 { - result := &SearchSingleResult{} - for _, child := range packet.Children[2].Children { - decodedChild, err := DecodeControl(child) - if err != nil { - werr := fmt.Errorf("failed to decode child control: %w", err) - r.ch <- &SearchSingleResult{Error: werr} - return - } - result.Controls = append(result.Controls, decodedChild) - } - r.ch <- result - } - foundSearchSingleResultDone = true - - case ApplicationSearchResultReference: - ref := packet.Children[1].Children[0].Value.(string) - r.ch <- &SearchSingleResult{Referral: ref} - - case ApplicationIntermediateResponse: - decoded, err := DecodeControl(packet.Children[1]) - if err != nil { - werr := fmt.Errorf("failed to decode intermediate response: %w", err) - r.ch <- &SearchSingleResult{Error: werr} - return - } - result := &SearchSingleResult{} - result.Controls = append(result.Controls, decoded) - r.ch <- result - - default: - err := fmt.Errorf("unknown tag: %d", packet.Children[1].Tag) - r.ch <- &SearchSingleResult{Error: err} - return - } - } - } - r.conn.Debug.Printf("%d: returning", msgCtx.id) - }() -} - -func newSearchResponse(conn *Conn, bufferSize int) *searchResponse { - var ch chan *SearchSingleResult - if bufferSize > 0 { - ch = make(chan *SearchSingleResult, bufferSize) - } else { - ch = make(chan *SearchSingleResult) - } - return &searchResponse{ - conn: conn, - ch: ch, - } -} diff --git a/search.go b/search.go deleted file mode 100644 index 62be105..0000000 --- a/search.go +++ /dev/null @@ -1,715 +0,0 @@ -package ldap - -import ( - "context" - "errors" - "fmt" - "reflect" - "sort" - "strconv" - "strings" - "time" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -// scope choices -const ( - ScopeBaseObject = 0 - ScopeSingleLevel = 1 - ScopeWholeSubtree = 2 - // ScopeChildren is an OpenLDAP extension that may not be supported by another directory server. - // See: https://github.com/openldap/openldap/blob/7c55484ee153047efd0e562fc1638c1a2525f320/include/ldap.h#L598 - ScopeChildren = 3 -) - -// ScopeMap contains human readable descriptions of scope choices -var ScopeMap = map[int]string{ - ScopeBaseObject: "Base Object", - ScopeSingleLevel: "Single Level", - ScopeWholeSubtree: "Whole Subtree", - ScopeChildren: "Children", -} - -// derefAliases -const ( - NeverDerefAliases = 0 - DerefInSearching = 1 - DerefFindingBaseObj = 2 - DerefAlways = 3 -) - -// DerefMap contains human readable descriptions of derefAliases choices -var DerefMap = map[int]string{ - NeverDerefAliases: "NeverDerefAliases", - DerefInSearching: "DerefInSearching", - DerefFindingBaseObj: "DerefFindingBaseObj", - DerefAlways: "DerefAlways", -} - -// ErrSizeLimitExceeded will be returned if the search result is exceeding the defined SizeLimit -// and enforcing the requested limit is enabled in the search request (EnforceSizeLimit) -var ErrSizeLimitExceeded = NewError(ErrorNetwork, errors.New("ldap: size limit exceeded")) - -// 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, - } -} - -// Entry represents a single search result entry -type Entry struct { - // DN is the distinguished name of the entry - DN string - // Attributes are the returned attributes for the entry - Attributes []*EntryAttribute -} - -// GetAttributeValues returns the values for the named attribute, or an empty list -func (e *Entry) GetAttributeValues(attribute string) []string { - for _, attr := range e.Attributes { - if attr.Name == attribute { - return attr.Values - } - } - return []string{} -} - -// GetEqualFoldAttributeValues returns the values for the named attribute, or an -// empty list. Attribute matching is done with strings.EqualFold. -func (e *Entry) GetEqualFoldAttributeValues(attribute string) []string { - for _, attr := range e.Attributes { - if strings.EqualFold(attribute, attr.Name) { - return attr.Values - } - } - return []string{} -} - -// GetRawAttributeValues returns the byte values for the named attribute, or an empty list -func (e *Entry) GetRawAttributeValues(attribute string) [][]byte { - for _, attr := range e.Attributes { - if attr.Name == attribute { - return attr.ByteValues - } - } - return [][]byte{} -} - -// GetEqualFoldRawAttributeValues returns the byte values for the named attribute, or an empty list -func (e *Entry) GetEqualFoldRawAttributeValues(attribute string) [][]byte { - for _, attr := range e.Attributes { - if strings.EqualFold(attr.Name, attribute) { - return attr.ByteValues - } - } - return [][]byte{} -} - -// GetAttributeValue returns the first value for the named attribute, or "" -func (e *Entry) GetAttributeValue(attribute string) string { - values := e.GetAttributeValues(attribute) - if len(values) == 0 { - return "" - } - return values[0] -} - -// GetEqualFoldAttributeValue returns the first value for the named attribute, or "". -// Attribute comparison is done with strings.EqualFold. -func (e *Entry) GetEqualFoldAttributeValue(attribute string) string { - values := e.GetEqualFoldAttributeValues(attribute) - if len(values) == 0 { - return "" - } - return values[0] -} - -// GetRawAttributeValue returns the first value for the named attribute, or an empty slice -func (e *Entry) GetRawAttributeValue(attribute string) []byte { - values := e.GetRawAttributeValues(attribute) - if len(values) == 0 { - return []byte{} - } - return values[0] -} - -// GetEqualFoldRawAttributeValue returns the first value for the named attribute, or an empty slice -func (e *Entry) GetEqualFoldRawAttributeValue(attribute string) []byte { - values := e.GetEqualFoldRawAttributeValues(attribute) - if len(values) == 0 { - return []byte{} - } - return values[0] -} - -// Print outputs a human-readable description -func (e *Entry) Print() { - fmt.Printf("DN: %s\n", e.DN) - for _, attr := range e.Attributes { - attr.Print() - } -} - -// PrettyPrint outputs a human-readable description indenting -func (e *Entry) PrettyPrint(indent int) { - fmt.Printf("%sDN: %s\n", strings.Repeat(" ", indent), e.DN) - for _, attr := range e.Attributes { - attr.PrettyPrint(indent + 2) - } -} - -// Describe the tag to use for struct field tags -const decoderTagName = "ldap" - -// readTag will read the reflect.StructField value for -// the key defined in decoderTagName. If omitempty is -// specified, the field may not be filled. -func readTag(f reflect.StructField) (string, bool) { - val, ok := f.Tag.Lookup(decoderTagName) - if !ok { - return f.Name, false - } - opts := strings.Split(val, ",") - omit := false - if len(opts) == 2 { - omit = opts[1] == "omitempty" - } - return opts[0], omit -} - -// Unmarshal parses the Entry in the value pointed to by i -// -// Currently, this methods only supports struct fields of type -// string, *string, []string, int, int64, []byte, *DN, []*DN or time.Time. -// Other field types will not be regarded. If the field type is a string or int but multiple -// attribute values are returned, the first value will be used to fill the field. -// -// Example: -// -// type UserEntry struct { -// // Fields with the tag key `dn` are automatically filled with the -// // objects distinguishedName. This can be used multiple times. -// DN string `ldap:"dn"` -// -// // This field will be filled with the attribute value for -// // userPrincipalName. An attribute can be read into a struct field -// // multiple times. Missing attributes will not result in an error. -// UserPrincipalName string `ldap:"userPrincipalName"` -// -// // memberOf may have multiple values. If you don't -// // know the amount of attribute values at runtime, use a string array. -// MemberOf []string `ldap:"memberOf"` -// -// // ID is an integer value, it will fail unmarshaling when the given -// // attribute value cannot be parsed into an integer. -// ID int `ldap:"id"` -// -// // LongID is similar to ID but uses an int64 instead. -// LongID int64 `ldap:"longId"` -// -// // Data is similar to MemberOf a slice containing all attribute -// // values. -// Data []byte `ldap:"data"` -// -// // Time is parsed with the generalizedTime spec into a time.Time -// Created time.Time `ldap:"createdTimestamp"` -// -// // *DN is parsed with the ParseDN -// Owner *ldap.DN `ldap:"owner"` -// -// // []*DN is parsed with the ParseDN -// Children []*ldap.DN `ldap:"children"` -// -// // This won't work, as the field is not of type string. For this -// // to work, you'll have to temporarily store the result in string -// // (or string array) and convert it to the desired type afterwards. -// UserAccountControl uint32 `ldap:"userPrincipalName"` -// } -// user := UserEntry{} -// -// if err := result.Unmarshal(&user); err != nil { -// // ... -// } -func (e *Entry) Unmarshal(i interface{}) (err error) { - // Make sure it's a ptr - if vo := reflect.ValueOf(i).Kind(); vo != reflect.Ptr { - return fmt.Errorf("ldap: cannot use %s, expected pointer to a struct", vo) - } - - sv, st := reflect.ValueOf(i).Elem(), reflect.TypeOf(i).Elem() - // Make sure it's pointing to a struct - if sv.Kind() != reflect.Struct { - return fmt.Errorf("ldap: expected pointer to a struct, got %s", sv.Kind()) - } - - for n := 0; n < st.NumField(); n++ { - // Holds struct field value and type - fv, ft := sv.Field(n), st.Field(n) - - // skip unexported fields - if ft.PkgPath != "" { - continue - } - - // omitempty can be safely discarded, as it's not needed when unmarshalling - fieldTag, _ := readTag(ft) - - // Fill the field with the distinguishedName if the tag key is `dn` - if fieldTag == "dn" { - fv.SetString(e.DN) - continue - } - - values := e.GetAttributeValues(fieldTag) - if len(values) == 0 { - continue - } - - switch fv.Interface().(type) { - case []string: - for _, item := range values { - fv.Set(reflect.Append(fv, reflect.ValueOf(item))) - } - case string: - fv.SetString(values[0]) - case *string: - fv.Set(reflect.ValueOf(&values[0])) - case []byte: - fv.SetBytes([]byte(values[0])) - case int, int64: - intVal, err := strconv.ParseInt(values[0], 10, 64) - if err != nil { - return fmt.Errorf("ldap: could not parse value '%s' into int field", values[0]) - } - fv.SetInt(intVal) - case time.Time: - t, err := ber.ParseGeneralizedTime([]byte(values[0])) - if err != nil { - return fmt.Errorf("ldap: could not parse value '%s' into time.Time field", values[0]) - } - fv.Set(reflect.ValueOf(t)) - case *DN: - dn, err := ParseDN(values[0]) - if err != nil { - return fmt.Errorf("ldap: could not parse value '%s' into *ldap.DN field", values[0]) - } - fv.Set(reflect.ValueOf(dn)) - case []*DN: - for _, item := range values { - dn, err := ParseDN(item) - if err != nil { - return fmt.Errorf("ldap: could not parse value '%s' into *ldap.DN field", item) - } - fv.Set(reflect.Append(fv, reflect.ValueOf(dn))) - } - default: - return fmt.Errorf("ldap: expected field to be of type string, *string, []string, int, int64, []byte, *DN, []*DN or time.Time, got %v", ft.Type) - } - } - return -} - -// 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, - } -} - -// EntryAttribute holds a single attribute -type EntryAttribute struct { - // Name is the name of the attribute - Name string - // Values contain the string values of the attribute - Values []string - // ByteValues contain the raw values of the attribute - ByteValues [][]byte -} - -// Print outputs a human-readable description -func (e *EntryAttribute) Print() { - fmt.Printf("%s: %s\n", e.Name, e.Values) -} - -// PrettyPrint outputs a human-readable description with indenting -func (e *EntryAttribute) PrettyPrint(indent int) { - fmt.Printf("%s%s: %s\n", strings.Repeat(" ", indent), e.Name, e.Values) -} - -// SearchResult holds the server's response to a search request -type SearchResult struct { - // Entries are the returned entries - Entries []*Entry - // Referrals are the returned referrals - Referrals []string - // Controls are the returned controls - Controls []Control -} - -// Print outputs a human-readable description -func (s *SearchResult) Print() { - for _, entry := range s.Entries { - entry.Print() - } -} - -// PrettyPrint outputs a human-readable description with indenting -func (s *SearchResult) PrettyPrint(indent int) { - for _, entry := range s.Entries { - entry.PrettyPrint(indent) - } -} - -// appendTo appends all entries of `s` to `r` -func (s *SearchResult) appendTo(r *SearchResult) { - r.Entries = append(r.Entries, s.Entries...) - r.Referrals = append(r.Referrals, s.Referrals...) - r.Controls = append(r.Controls, s.Controls...) -} - -// SearchSingleResult holds the server's single entry response to a search request -type SearchSingleResult struct { - // Entry is the returned entry - Entry *Entry - // Referral is the returned referral - Referral string - // Controls are the returned controls - Controls []Control - // Error is set when the search request was failed - Error error -} - -// Print outputs a human-readable description -func (s *SearchSingleResult) Print() { - s.Entry.Print() -} - -// PrettyPrint outputs a human-readable description with indenting -func (s *SearchSingleResult) PrettyPrint(indent int) { - s.Entry.PrettyPrint(indent) -} - -// SearchRequest represents a search request to send to the server -type SearchRequest struct { - BaseDN string - Scope int - DerefAliases int - SizeLimit int - TimeLimit int - TypesOnly bool - Filter string - Attributes []string - Controls []Control - - // EnforceSizeLimit will hard limit the maximum number of entries parsed, in case the directory - // server returns more results than requested. This setting is disabled by default and does not - // work in async search requests. - EnforceSizeLimit bool -} - -func (req *SearchRequest) appendTo(envelope *ber.Packet) error { - pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationSearchRequest, nil, "Search Request") - pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.BaseDN, "Base DN")) - pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(req.Scope), "Scope")) - pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(req.DerefAliases), "Deref Aliases")) - pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, uint64(req.SizeLimit), "Size Limit")) - pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, uint64(req.TimeLimit), "Time Limit")) - pkt.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, req.TypesOnly, "Types Only")) - // compile and encode filter - filterPacket, err := CompileFilter(req.Filter) - if err != nil { - return err - } - pkt.AppendChild(filterPacket) - // encode attributes - attributesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes") - for _, attribute := range req.Attributes { - attributesPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) - } - pkt.AppendChild(attributesPacket) - - envelope.AppendChild(pkt) - if len(req.Controls) > 0 { - envelope.AppendChild(encodeControls(req.Controls)) - } - - return nil -} - -// NewSearchRequest creates a new search request -func NewSearchRequest( - BaseDN string, - Scope, DerefAliases, SizeLimit, TimeLimit int, - TypesOnly bool, - Filter string, - Attributes []string, - Controls []Control, -) *SearchRequest { - return &SearchRequest{ - BaseDN: BaseDN, - Scope: Scope, - DerefAliases: DerefAliases, - SizeLimit: SizeLimit, - TimeLimit: TimeLimit, - TypesOnly: TypesOnly, - Filter: Filter, - Attributes: Attributes, - Controls: Controls, - } -} - -// 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) { - 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 - } - - searchResult := new(SearchResult) - for { - result, err := l.Search(searchRequest) - if result != nil { - result.appendTo(searchResult) - } else { - if err == nil { - // We have to do this beautifulness in case something absolutely strange happens, which - // should only occur in case there is no packet, but also no error. - return searchResult, NewError(ErrorNetwork, errors.New("ldap: packet not received")) - } - } - if err != nil { - // If an error occurred, all results that have been received so far will be returned - return searchResult, err - } - - l.Debug.Printf("Looking for Paging Control...") - pagingResult := FindControl(result.Controls, ControlTypePaging) - if pagingResult == nil { - pagingControl = nil - l.Debug.Printf("Could not find paging control. Breaking...") - break - } - - cookie := pagingResult.(*ControlPaging).Cookie - if len(cookie) == 0 { - pagingControl = nil - l.Debug.Printf("Could not find cookie. Breaking...") - break - } - pagingControl.SetCookie(cookie) - } - - if pagingControl != nil { - l.Debug.Printf("Abandoning Paging...") - pagingControl.PagingSize = 0 - if _, err := l.Search(searchRequest); err != nil { - return searchResult, err - } - } - - return searchResult, nil -} - -// Search performs the given search request -func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) { - msgCtx, err := l.doRequest(searchRequest) - if err != nil { - return nil, err - } - defer l.finishMessage(msgCtx) - - result := &SearchResult{ - Entries: make([]*Entry, 0), - Referrals: make([]string, 0), - Controls: make([]Control, 0), - } - - for { - packet, err := l.readPacket(msgCtx) - if err != nil { - return result, err - } - - switch packet.Children[1].Tag { - case 4: - if searchRequest.EnforceSizeLimit && - searchRequest.SizeLimit > 0 && - len(result.Entries) >= searchRequest.SizeLimit { - return result, ErrSizeLimitExceeded - } - - attr := make([]*ber.Packet, 0) - if len(packet.Children[1].Children) > 1 { - attr = packet.Children[1].Children[1].Children - } - entry := &Entry{ - DN: packet.Children[1].Children[0].Value.(string), - Attributes: unpackAttributes(attr), - } - result.Entries = append(result.Entries, entry) - case 5: - err := GetLDAPError(packet) - if err != nil { - return result, err - } - if len(packet.Children) == 3 { - for _, child := range packet.Children[2].Children { - decodedChild, err := DecodeControl(child) - if err != nil { - return result, fmt.Errorf("failed to decode child control: %s", err) - } - result.Controls = append(result.Controls, decodedChild) - } - } - return result, nil - case 19: - result.Referrals = append(result.Referrals, packet.Children[1].Children[0].Value.(string)) - } - } -} - -// SearchAsync performs a search request and returns all search results asynchronously. -// This means you get all results until an error happens (or the search successfully finished), -// e.g. for size / time limited requests all are recieved until the limit is reached. -// To stop the search, call cancel function of the context. -func (l *Conn) SearchAsync( - ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response { - r := newSearchResponse(l, bufferSize) - r.start(ctx, searchRequest) - return r -} - -// Syncrepl is a short name for LDAP Sync Replication engine that works on the -// consumer-side. This can perform a persistent search and returns an entry -// when the entry is updated on the server side. -// To stop the search, call cancel function of the context. -func (l *Conn) Syncrepl( - ctx context.Context, searchRequest *SearchRequest, bufferSize int, - mode ControlSyncRequestMode, cookie []byte, reloadHint bool, -) Response { - control := NewControlSyncRequest(mode, cookie, reloadHint) - searchRequest.Controls = append(searchRequest.Controls, control) - r := newSearchResponse(l, bufferSize) - r.start(ctx, searchRequest) - return r -} - -// unpackAttributes will extract all given LDAP attributes and it's values -// from the ber.Packet -func unpackAttributes(children []*ber.Packet) []*EntryAttribute { - entries := make([]*EntryAttribute, len(children)) - for i, child := range children { - length := len(child.Children[1].Children) - entry := &EntryAttribute{ - Name: child.Children[0].Value.(string), - // pre-allocate the slice since we can determine - // the number of attributes at this point - Values: make([]string, length), - ByteValues: make([][]byte, length), - } - - for i, value := range child.Children[1].Children { - entry.ByteValues[i] = value.ByteValue - entry.Values[i] = value.Value.(string) - } - entries[i] = entry - } - - return entries -} - -// DirSync does a Search with dirSync Control. -func (l *Conn) DirSync( - searchRequest *SearchRequest, flags int64, maxAttrCount int64, cookie []byte, -) (*SearchResult, error) { - control := FindControl(searchRequest.Controls, ControlTypeDirSync) - if control == nil { - c := NewRequestControlDirSync(flags, maxAttrCount, cookie) - searchRequest.Controls = append(searchRequest.Controls, c) - } else { - c := control.(*ControlDirSync) - if c.Flags != flags { - return nil, fmt.Errorf("flags given in search request (%d) conflicts with flags given in search call (%d)", c.Flags, flags) - } - if c.MaxAttrCount != maxAttrCount { - return nil, fmt.Errorf("MaxAttrCnt given in search request (%d) conflicts with maxAttrCount given in search call (%d)", c.MaxAttrCount, maxAttrCount) - } - } - searchResult, err := l.Search(searchRequest) - l.Debug.Printf("Looking for result...") - if err != nil { - return nil, err - } - if searchResult == nil { - return nil, NewError(ErrorNetwork, errors.New("ldap: packet not received")) - } - - l.Debug.Printf("Looking for DirSync Control...") - resultControl := FindControl(searchResult.Controls, ControlTypeDirSync) - if resultControl == nil { - l.Debug.Printf("Could not find dirSyncControl control. Breaking...") - return searchResult, nil - } - - cookie = resultControl.(*ControlDirSync).Cookie - if len(cookie) == 0 { - l.Debug.Printf("Could not find cookie. Breaking...") - return searchResult, nil - } - - return searchResult, nil -} - -// DirSyncDirSyncAsync performs a search request and returns all search results -// asynchronously. This is efficient when the server returns lots of entries. -func (l *Conn) DirSyncAsync( - ctx context.Context, searchRequest *SearchRequest, bufferSize int, - flags, maxAttrCount int64, cookie []byte, -) Response { - control := NewRequestControlDirSync(flags, maxAttrCount, cookie) - searchRequest.Controls = append(searchRequest.Controls, control) - r := newSearchResponse(l, bufferSize) - r.start(ctx, searchRequest) - return r -} diff --git a/search_test.go b/search_test.go deleted file mode 100644 index 029029e..0000000 --- a/search_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package ldap - -import ( - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -// 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"}, - } - executedEntry := NewEntry(dn, attributes) - - iteration := 0 - for { - if iteration == 100 { - break - } - testEntry := NewEntry(dn, attributes) - if !reflect.DeepEqual(executedEntry, testEntry) { - t.Fatalf("subsequent calls to NewEntry did not yield the same result:\n\texpected:\n\t%v\n\tgot:\n\t%v\n", executedEntry, testEntry) - } - iteration = iteration + 1 - } -} - -func TestGetAttributeValue(t *testing.T) { - dn := "testDN" - attributes := map[string][]string{ - "Alpha": {"value"}, - "bEta": {"value"}, - "gaMma": {"value"}, - "delTa": {"value"}, - "epsiLon": {"value"}, - } - entry := NewEntry(dn, attributes) - if entry.GetAttributeValue("Alpha") != "value" { - t.Errorf("failed to get attribute in original case") - } - - if entry.GetEqualFoldAttributeValue("alpha") != "value" { - t.Errorf("failed to get attribute in changed case") - } -} - -func TestEntry_Unmarshal(t *testing.T) { - t.Run("passing a struct should fail", func(t *testing.T) { - entry := &Entry{} - - type toStruct struct{} - - result := toStruct{} - err := entry.Unmarshal(result) - - assert.NotNil(t, err) - }) - - t.Run("passing a ptr to string should fail", func(t *testing.T) { - entry := &Entry{} - - str := "foo" - err := entry.Unmarshal(&str) - - assert.NotNil(t, err) - }) - - t.Run("user struct be decoded", func(t *testing.T) { - entry := &Entry{ - DN: "cn=mario,ou=Users,dc=go-ldap,dc=github,dc=com", - Attributes: []*EntryAttribute{ - { - Name: "cn", - Values: []string{"mario"}, - ByteValues: nil, - }, - { - Name: "mail", - Values: []string{"mario@go-ldap.com"}, - ByteValues: nil, - }, - { - Name: "upn", - Values: []string{"mario@go-ldap.com.domain"}, - ByteValues: nil, - }, - // Tests int value. - { - Name: "id", - Values: []string{"2147483647"}, - ByteValues: nil, - }, - // Tests int64 value. - { - Name: "longId", - Values: []string{"9223372036854775807"}, - ByteValues: nil, - }, - // Tests []byte value. - { - Name: "data", - Values: []string{"data"}, - ByteValues: [][]byte{ - []byte("data"), - }, - }, - // Tests time.Time value. - { - Name: "createdTimestamp", - Values: []string{"202305041930Z"}, - ByteValues: nil, - }, - // Tests *DN value - { - Name: "owner", - Values: []string{"uid=foo,dc=example,dc=org"}, - ByteValues: nil, - }, - // Tests []*DN value - { - Name: "children", - Values: []string{"uid=bar,dc=example,dc=org", "uid=baz,dc=example,dc=org"}, - ByteValues: nil, - }, - }, - } - - type User struct { - Dn string `ldap:"dn"` - Cn string `ldap:"cn"` - Mail string `ldap:"mail"` - UPN *string `ldap:"upn"` - ID int `ldap:"id"` - LongID int64 `ldap:"longId"` - Data []byte `ldap:"data"` - Created time.Time `ldap:"createdTimestamp"` - Owner *DN `ldap:"owner"` - Children []*DN `ldap:"children"` - } - - created, err := time.Parse("200601021504Z", "202305041930Z") - if err != nil { - t.Errorf("failed to parse ref time: %s", err) - } - owner, err := ParseDN("uid=foo,dc=example,dc=org") - if err != nil { - t.Errorf("failed to parse ref DN: %s", err) - } - var children []*DN - for _, child := range []string{"uid=bar,dc=example,dc=org", "uid=baz,dc=example,dc=org"} { - dn, err := ParseDN(child) - if err != nil { - t.Errorf("failed to parse child ref DN: %s", err) - } - children = append(children, dn) - } - - UPN := "mario@go-ldap.com.domain" - expect := &User{ - Dn: "cn=mario,ou=Users,dc=go-ldap,dc=github,dc=com", - Cn: "mario", - Mail: "mario@go-ldap.com", - UPN: &UPN, - ID: 2147483647, - LongID: 9223372036854775807, - Data: []byte("data"), - Created: created, - Owner: owner, - Children: children, - } - result := &User{} - err = entry.Unmarshal(result) - - assert.Nil(t, err) - assert.Equal(t, expect, result) - }) - - t.Run("group struct be decoded", func(t *testing.T) { - entry := &Entry{ - DN: "cn=DREAM_TEAM,ou=Groups,dc=go-ldap,dc=github,dc=com", - Attributes: []*EntryAttribute{ - { - Name: "cn", - Values: []string{"DREAM_TEAM"}, - ByteValues: nil, - }, - { - Name: "member", - Values: []string{"mario", "luigi", "browser"}, - ByteValues: nil, - }, - }, - } - - type Group struct { - DN string `ldap:"dn" yaml:"dn" json:"dn"` - CN string `ldap:"cn" yaml:"cn" json:"cn"` - Members []string `ldap:"member"` - } - - expect := &Group{ - DN: "cn=DREAM_TEAM,ou=Groups,dc=go-ldap,dc=github,dc=com", - CN: "DREAM_TEAM", - Members: []string{"mario", "luigi", "browser"}, - } - - result := &Group{} - err := entry.Unmarshal(result) - - assert.Nil(t, err) - assert.Equal(t, expect, result) - }) -} diff --git a/unbind.go b/unbind.go deleted file mode 100644 index 10cf75c..0000000 --- a/unbind.go +++ /dev/null @@ -1,38 +0,0 @@ -package ldap - -import ( - "errors" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -// ErrConnUnbound is returned when Unbind is called on an already closing connection. -var ErrConnUnbound = NewError(ErrorNetwork, errors.New("ldap: connection is closed")) - -type unbindRequest struct{} - -func (unbindRequest) appendTo(envelope *ber.Packet) error { - envelope.AppendChild(ber.Encode(ber.ClassApplication, ber.TypePrimitive, ApplicationUnbindRequest, nil, ApplicationMap[ApplicationUnbindRequest])) - return nil -} - -// Unbind will perform an unbind request. The Unbind operation -// should be thought of as the "quit" operation. -// See https://datatracker.ietf.org/doc/html/rfc4511#section-4.3 -func (l *Conn) Unbind() error { - if l.IsClosing() { - return ErrConnUnbound - } - - _, err := l.doRequest(unbindRequest{}) - if err != nil { - return err - } - - // Sending an unbindRequest will make the connection unusable. - // Pending requests will fail with: - // LDAP Result Code 200 "Network Error": ldap: response channel closed - l.Close() - - return nil -} diff --git a/whoami.go b/whoami.go deleted file mode 100644 index 10c523d..0000000 --- a/whoami.go +++ /dev/null @@ -1,91 +0,0 @@ -package ldap - -// This file contains the "Who Am I?" extended operation as specified in rfc 4532 -// -// https://tools.ietf.org/html/rfc4532 - -import ( - "errors" - "fmt" - - ber "github.com/go-asn1-ber/asn1-ber" -) - -type whoAmIRequest bool - -// WhoAmIResult is returned by the WhoAmI() call -type WhoAmIResult struct { - AuthzID string -} - -func (r whoAmIRequest) encode() (*ber.Packet, error) { - request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Who Am I? Extended Operation") - request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, ControlTypeWhoAmI, "Extended Request Name: Who Am I? OID")) - return request, nil -} - -// WhoAmI returns the authzId the server thinks we are, you may pass controls -// like a Proxied Authorization control -func (l *Conn) WhoAmI(controls []Control) (*WhoAmIResult, error) { - packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") - packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) - req := whoAmIRequest(true) - encodedWhoAmIRequest, err := req.encode() - if err != nil { - return nil, err - } - packet.AppendChild(encodedWhoAmIRequest) - - if len(controls) != 0 { - packet.AppendChild(encodeControls(controls)) - } - - l.Debug.PrintPacket(packet) - - msgCtx, err := l.sendMessage(packet) - if err != nil { - return nil, err - } - defer l.finishMessage(msgCtx) - - result := &WhoAmIResult{} - - l.Debug.Printf("%d: waiting for response", msgCtx.id) - packetResponse, ok := <-msgCtx.responses - if !ok { - return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) - } - packet, err = packetResponse.ReadPacket() - l.Debug.Printf("%d: got response %p", msgCtx.id, packet) - if err != nil { - return nil, err - } - - if packet == nil { - return nil, NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) - } - - if l.Debug { - if err := addLDAPDescriptions(packet); err != nil { - return nil, err - } - ber.PrintPacket(packet) - } - - if packet.Children[1].Tag == ApplicationExtendedResponse { - if err := GetLDAPError(packet); err != nil { - return nil, err - } - } else { - return nil, NewError(ErrorUnexpectedResponse, fmt.Errorf("Unexpected Response: %d", packet.Children[1].Tag)) - } - - extendedResponse := packet.Children[1] - for _, child := range extendedResponse.Children { - if child.Tag == 11 { - result.AuthzID = ber.DecodeString(child.Data.Bytes()) - } - } - - return result, nil -}