├── .githooks └── pre-push ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── cifuzz.yml │ ├── lint.yml │ └── pr.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md └── v3 ├── add.go ├── bind.go ├── client.go ├── compare.go ├── conn.go ├── conn_test.go ├── control.go ├── control_test.go ├── debug.go ├── del.go ├── dn.go ├── dn_test.go ├── doc.go ├── error.go ├── error_test.go ├── examples_moddn_test.go ├── examples_test.go ├── examples_windows_test.go ├── extended.go ├── extended_test.go ├── filter.go ├── filter_test.go ├── go.mod ├── go.sum ├── gssapi ├── client.go └── sspi.go ├── ldap.go ├── ldap_test.go ├── moddn.go ├── modify.go ├── passwdmodify.go ├── request.go ├── response.go ├── search.go ├── search_test.go ├── unbind.go └── whoami.go /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # install from the root of the repo with: 4 | # ln -s ../../.githooks/pre-push .git/hooks/pre-push 5 | 6 | make vet fmt lint -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug, triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Code snippets** 20 | If applicable, add code snippets to help explain your problem. 21 | 22 | **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE] Feature request" 5 | labels: enhancement, triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/cifuzz.yml: -------------------------------------------------------------------------------- 1 | name: CIFuzz 2 | on: [pull_request] 3 | permissions: {} 4 | jobs: 5 | Fuzzing: 6 | if: false # Until go-fuzz project is updated to use v3 directory 7 | runs-on: ubuntu-latest 8 | permissions: 9 | security-events: write 10 | steps: 11 | - name: Build Fuzzers 12 | id: build 13 | uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master 14 | with: 15 | oss-fuzz-project-name: "go-ldap" 16 | language: go 17 | - name: Run Fuzzers 18 | uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master 19 | with: 20 | oss-fuzz-project-name: "go-ldap" 21 | language: go 22 | fuzz-seconds: 300 23 | output-sarif: true 24 | - name: Upload Crash 25 | uses: actions/upload-artifact@v3 26 | if: failure() && steps.build.outcome == 'success' 27 | with: 28 | name: artifacts 29 | path: ./out/artifacts 30 | - name: Upload Sarif 31 | if: always() && steps.build.outcome == 'success' 32 | uses: github/codeql-action/upload-sarif@v2 33 | with: 34 | # Path to SARIF file relative to the root of the repository 35 | sarif_file: cifuzz-sarif/results.sarif 36 | checkout_path: cifuzz-sarif 37 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | pull_request: 4 | branches: [master] 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | golangci: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/setup-go@v5 15 | with: 16 | go-version: "1.24" 17 | cache: false 18 | - uses: actions/checkout@v4 19 | - name: golangci-lint 20 | uses: golangci/golangci-lint-action@v6 21 | with: 22 | version: latest 23 | only-new-issues: true 24 | working-directory: v3 25 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | build-and-test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | go: ["1.24", "1.23"] 13 | directory: ["./v3"] 14 | name: Go ${{ matrix.go }}.x PR Validate ${{ matrix.directory }} (Modules) 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: ${{ matrix.go }} 23 | 24 | - name: Version 25 | run: go version 26 | 27 | - name: Build, Validate, and Test 28 | run: | 29 | cd ${{ matrix.directory }} 30 | go vet . 31 | go test . 32 | go test -cover -race -cpu 1,2,4 . 33 | go build . 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-ldap/ldap/bab0be501db45d56829a9465cfd8135041a39bee/.gitignore -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | We welcome contribution and improvements. 4 | 5 | ## Guiding Principles 6 | 7 | To begin with here is a draft from an email exchange: 8 | 9 | * take compatibility seriously (our semvers, compatibility with older go versions, etc) 10 | * don't tag untested code for release 11 | * beware of baking in implicit behavior based on other libraries/tools choices 12 | * be as high-fidelity as possible in plumbing through LDAP data (don't mask errors or reduce power of someone using the library) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com) 4 | Portions copyright (c) 2015-2024 go-ldap Authors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default install build test quicktest fmt vet lint 2 | 3 | # List of all release tags "supported" by our current Go version 4 | # E.g. ":go1.1:go1.2:go1.3:go1.4:go1.5:go1.6:go1.7:go1.8:go1.9:go1.10:go1.11:go1.12:" 5 | GO_RELEASE_TAGS := $(shell go list -f ':{{join (context.ReleaseTags) ":"}}:' runtime) 6 | 7 | # Only use the `-race` flag on newer versions of Go (version 1.3 and newer) 8 | ifeq (,$(findstring :go1.3:,$(GO_RELEASE_TAGS))) 9 | RACE_FLAG := 10 | else 11 | RACE_FLAG := -race -cpu 1,2,4 12 | endif 13 | 14 | # Run `go vet` on Go 1.12 and newer. For Go 1.5-1.11, use `go tool vet` 15 | ifneq (,$(findstring :go1.12:,$(GO_RELEASE_TAGS))) 16 | GO_VET := go vet \ 17 | -atomic \ 18 | -bool \ 19 | -copylocks \ 20 | -nilfunc \ 21 | -printf \ 22 | -rangeloops \ 23 | -unreachable \ 24 | -unsafeptr \ 25 | -unusedresult \ 26 | . 27 | else ifneq (,$(findstring :go1.5:,$(GO_RELEASE_TAGS))) 28 | GO_VET := go tool vet \ 29 | -atomic \ 30 | -bool \ 31 | -copylocks \ 32 | -nilfunc \ 33 | -printf \ 34 | -shadow \ 35 | -rangeloops \ 36 | -unreachable \ 37 | -unsafeptr \ 38 | -unusedresult \ 39 | . 40 | else 41 | GO_VET := @echo "go vet skipped -- not supported on this version of Go" 42 | endif 43 | 44 | default: fmt vet lint build quicktest 45 | 46 | install: 47 | go get -t -v ./... 48 | 49 | build: 50 | go build -v ./... 51 | 52 | test: 53 | go test -v $(RACE_FLAG) -cover ./... 54 | 55 | quicktest: 56 | go test ./... 57 | 58 | fuzz: 59 | go test -fuzz=FuzzParseDN -fuzztime=600s . 60 | go test -fuzz=FuzzDecodeEscapedSymbols -fuzztime=600s . 61 | go test -fuzz=FuzzEscapeDN -fuzztime=600s . 62 | 63 | # Capture output and force failure when there is non-empty output 64 | fmt: 65 | @echo gofmt -l . 66 | @OUTPUT=`gofmt -l . 2>&1`; \ 67 | if [ "$$OUTPUT" ]; then \ 68 | echo "gofmt must be run on the following files:"; \ 69 | echo "$$OUTPUT"; \ 70 | exit 1; \ 71 | fi 72 | 73 | vet: 74 | $(GO_VET) 75 | 76 | # https://github.com/golang/lint 77 | # go get github.com/golang/lint/golint 78 | # Capture output and force failure when there is non-empty output 79 | # Only run on go1.5+ 80 | lint: 81 | @echo golint ./... 82 | @OUTPUT=`command -v golint >/dev/null 2>&1 && golint ./... 2>&1`; \ 83 | if [ "$$OUTPUT" ]; then \ 84 | echo "golint errors:"; \ 85 | echo "$$OUTPUT"; \ 86 | exit 1; \ 87 | fi 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/go-ldap/ldap?status.svg)](https://godoc.org/github.com/go-ldap/ldap) 2 | 3 | # Basic LDAP v3 functionality for the GO programming language. 4 | 5 | The library implements the following specifications: 6 | 7 | - https://datatracker.ietf.org/doc/html/rfc4511 for basic operations 8 | - https://datatracker.ietf.org/doc/html/rfc3062 for password modify operation 9 | - https://datatracker.ietf.org/doc/html/rfc4514 for distinguished names parsing 10 | - https://datatracker.ietf.org/doc/html/rfc4533 for Content Synchronization Operation 11 | - https://datatracker.ietf.org/doc/html/draft-armijo-ldap-treedelete-02 for Tree Delete Control 12 | - https://datatracker.ietf.org/doc/html/rfc2891 for Server Side Sorting of Search Results 13 | - https://datatracker.ietf.org/doc/html/rfc4532 for WhoAmI requests 14 | 15 | ## Features: 16 | 17 | - Connecting to LDAP server (non-TLS, TLS, STARTTLS, through a custom dialer) 18 | - Binding to LDAP server (Simple Bind, GSSAPI, SASL) 19 | - "Who Am I" Requests / Responses 20 | - Searching for entries (normal and asynchronous) 21 | - Filter Compile / Decompile 22 | - Paging Search Results 23 | - Modify Requests / Responses 24 | - Add Requests / Responses 25 | - Delete Requests / Responses 26 | - Modify DN Requests / Responses 27 | 28 | ## Go Modules: 29 | 30 | `go get github.com/go-ldap/ldap/v3` 31 | 32 | ## Contributing: 33 | 34 | Bug reports and pull requests are welcome! 35 | 36 | Before submitting a pull request, please make sure tests and verification scripts pass: 37 | 38 | ``` 39 | make all 40 | ``` 41 | 42 | To set up a pre-push hook to run the tests and verify scripts before pushing: 43 | 44 | ``` 45 | ln -s ../../.githooks/pre-push .git/hooks/pre-push 46 | ``` 47 | 48 | --- 49 | 50 | The Go gopher was designed by Renee French. (http://reneefrench.blogspot.com/) 51 | The design is licensed under the Creative Commons 3.0 Attributions license. 52 | Read this article for more details: http://blog.golang.org/gopher 53 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | go-ldap values responsible disclosure to protect the security and privacy of all users. We actively encourage respectful and non-disruptive testing and reporting of detected vulnerabilities, within the following guidelines: 4 | 5 | * DO submit reports as soon as you are aware of an issue 6 | * DO allow time to assess and respond to the submission 7 | * DO respect the availability, confidentiality and privacy of our services, users and any 3rd party systems 8 | * DO NOT attack or otherwise interfere with any account you are not the owner of 9 | * DO NOT violate any applicable laws or regulations 10 | * DO NOT disclose issues publicly before we've had time to assess and respond appropriately 11 | 12 | Please submit individual reports including a full description of the finding, how to reproduce the behavior and any supporting information. These reports should be sent to: 13 | - [John Weldon](https://github.com/johnweldon) 14 | - [Christopher Puschmann](https://github.com/cpuschma) 15 | 16 | -------------------------------------------------------------------------------- /v3/add.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | ber "github.com/go-asn1-ber/asn1-ber" 6 | ) 7 | 8 | // Attribute represents an LDAP attribute 9 | type Attribute struct { 10 | // Type is the name of the LDAP attribute 11 | Type string 12 | // Vals are the LDAP attribute values 13 | Vals []string 14 | } 15 | 16 | func (a *Attribute) encode() *ber.Packet { 17 | seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attribute") 18 | seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, a.Type, "Type")) 19 | set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") 20 | for _, value := range a.Vals { 21 | set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) 22 | } 23 | seq.AppendChild(set) 24 | return seq 25 | } 26 | 27 | // AddRequest represents an LDAP AddRequest operation 28 | type AddRequest struct { 29 | // DN identifies the entry being added 30 | DN string 31 | // Attributes list the attributes of the new entry 32 | Attributes []Attribute 33 | // Controls hold optional controls to send with the request 34 | Controls []Control 35 | } 36 | 37 | func (req *AddRequest) appendTo(envelope *ber.Packet) error { 38 | pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationAddRequest, nil, "Add Request") 39 | pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.DN, "DN")) 40 | attributes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes") 41 | for _, attribute := range req.Attributes { 42 | attributes.AppendChild(attribute.encode()) 43 | } 44 | pkt.AppendChild(attributes) 45 | 46 | envelope.AppendChild(pkt) 47 | if len(req.Controls) > 0 { 48 | envelope.AppendChild(encodeControls(req.Controls)) 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // Attribute adds an attribute with the given type and values 55 | func (req *AddRequest) Attribute(attrType string, attrVals []string) { 56 | req.Attributes = append(req.Attributes, Attribute{Type: attrType, Vals: attrVals}) 57 | } 58 | 59 | // NewAddRequest returns an AddRequest for the given DN, with no attributes 60 | func NewAddRequest(dn string, controls []Control) *AddRequest { 61 | return &AddRequest{ 62 | DN: dn, 63 | Controls: controls, 64 | } 65 | } 66 | 67 | // Add performs the given AddRequest 68 | func (l *Conn) Add(addRequest *AddRequest) error { 69 | msgCtx, err := l.doRequest(addRequest) 70 | if err != nil { 71 | return err 72 | } 73 | defer l.finishMessage(msgCtx) 74 | 75 | packet, err := l.readPacket(msgCtx) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | if packet.Children[1].Tag == ApplicationAddResponse { 81 | err := GetLDAPError(packet) 82 | if err != nil { 83 | return err 84 | } 85 | } else { 86 | return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /v3/client.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "time" 7 | ) 8 | 9 | // Client knows how to interact with an LDAP server 10 | type Client interface { 11 | Start() 12 | StartTLS(*tls.Config) error 13 | Close() error 14 | GetLastError() error 15 | IsClosing() bool 16 | SetTimeout(time.Duration) 17 | TLSConnectionState() (tls.ConnectionState, bool) 18 | 19 | Bind(username, password string) error 20 | UnauthenticatedBind(username string) error 21 | SimpleBind(*SimpleBindRequest) (*SimpleBindResult, error) 22 | ExternalBind() error 23 | NTLMUnauthenticatedBind(domain, username string) error 24 | Unbind() error 25 | 26 | Add(*AddRequest) error 27 | Del(*DelRequest) error 28 | Modify(*ModifyRequest) error 29 | ModifyDN(*ModifyDNRequest) error 30 | ModifyWithResult(*ModifyRequest) (*ModifyResult, error) 31 | Extended(*ExtendedRequest) (*ExtendedResponse, error) 32 | 33 | Compare(dn, attribute, value string) (bool, error) 34 | PasswordModify(*PasswordModifyRequest) (*PasswordModifyResult, error) 35 | 36 | Search(*SearchRequest) (*SearchResult, error) 37 | SearchAsync(ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response 38 | SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) 39 | DirSync(searchRequest *SearchRequest, flags, maxAttrCount int64, cookie []byte) (*SearchResult, error) 40 | DirSyncAsync(ctx context.Context, searchRequest *SearchRequest, bufferSize int, flags, maxAttrCount int64, cookie []byte) Response 41 | Syncrepl(ctx context.Context, searchRequest *SearchRequest, bufferSize int, mode ControlSyncRequestMode, cookie []byte, reloadHint bool) Response 42 | } 43 | -------------------------------------------------------------------------------- /v3/compare.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | 6 | ber "github.com/go-asn1-ber/asn1-ber" 7 | ) 8 | 9 | // CompareRequest represents an LDAP CompareRequest operation. 10 | type CompareRequest struct { 11 | DN string 12 | Attribute string 13 | Value string 14 | } 15 | 16 | func (req *CompareRequest) appendTo(envelope *ber.Packet) error { 17 | pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationCompareRequest, nil, "Compare Request") 18 | pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.DN, "DN")) 19 | 20 | ava := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "AttributeValueAssertion") 21 | ava.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.Attribute, "AttributeDesc")) 22 | ava.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.Value, "AssertionValue")) 23 | 24 | pkt.AppendChild(ava) 25 | 26 | envelope.AppendChild(pkt) 27 | 28 | return nil 29 | } 30 | 31 | // Compare checks to see if the attribute of the dn matches value. Returns true if it does otherwise 32 | // false with any error that occurs if any. 33 | func (l *Conn) Compare(dn, attribute, value string) (bool, error) { 34 | msgCtx, err := l.doRequest(&CompareRequest{ 35 | DN: dn, 36 | Attribute: attribute, 37 | Value: value, 38 | }) 39 | if err != nil { 40 | return false, err 41 | } 42 | defer l.finishMessage(msgCtx) 43 | 44 | packet, err := l.readPacket(msgCtx) 45 | if err != nil { 46 | return false, err 47 | } 48 | 49 | if packet.Children[1].Tag == ApplicationCompareResponse { 50 | err := GetLDAPError(packet) 51 | 52 | switch { 53 | case IsErrorWithCode(err, LDAPResultCompareTrue): 54 | return true, nil 55 | case IsErrorWithCode(err, LDAPResultCompareFalse): 56 | return false, nil 57 | default: 58 | return false, err 59 | } 60 | } 61 | return false, fmt.Errorf("unexpected Response: %d", packet.Children[1].Tag) 62 | } 63 | -------------------------------------------------------------------------------- /v3/conn_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "net" 8 | "net/http" 9 | "net/http/httptest" 10 | "runtime" 11 | "sync" 12 | "testing" 13 | "time" 14 | 15 | ber "github.com/go-asn1-ber/asn1-ber" 16 | ) 17 | 18 | func TestUnresponsiveConnection(t *testing.T) { 19 | // The do-nothing server that accepts requests and does nothing 20 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 21 | })) 22 | defer ts.Close() 23 | c, err := net.Dial(ts.Listener.Addr().Network(), ts.Listener.Addr().String()) 24 | if err != nil { 25 | t.Fatalf("error connecting to localhost tcp: %v", err) 26 | } 27 | 28 | // Create an Ldap connection 29 | conn := NewConn(c, false) 30 | conn.SetTimeout(time.Millisecond) 31 | conn.Start() 32 | defer conn.Close() 33 | 34 | // Mock a packet 35 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 36 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, conn.nextMessageID(), "MessageID")) 37 | bindRequest := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") 38 | bindRequest.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) 39 | packet.AppendChild(bindRequest) 40 | 41 | // Send packet and test response 42 | msgCtx, err := conn.sendMessage(packet) 43 | if err != nil { 44 | t.Fatalf("error sending message: %v", err) 45 | } 46 | defer conn.finishMessage(msgCtx) 47 | 48 | packetResponse, ok := <-msgCtx.responses 49 | if !ok { 50 | t.Fatalf("no PacketResponse in response channel") 51 | } 52 | _, err = packetResponse.ReadPacket() 53 | if err == nil { 54 | t.Fatalf("expected timeout error") 55 | } 56 | if !IsErrorWithCode(err, ErrorNetwork) || err.(*Error).Err.Error() != "ldap: connection timed out" { 57 | t.Fatalf("unexpected error: %v", err) 58 | } 59 | } 60 | 61 | func TestRequestTimeoutDeadlock(t *testing.T) { 62 | // The do-nothing server that accepts requests and does nothing 63 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 64 | })) 65 | defer ts.Close() 66 | c, err := net.Dial(ts.Listener.Addr().Network(), ts.Listener.Addr().String()) 67 | if err != nil { 68 | t.Fatalf("error connecting to localhost tcp: %v", err) 69 | } 70 | 71 | // Create an Ldap connection 72 | conn := NewConn(c, false) 73 | conn.Start() 74 | // trigger a race condition on accessing request timeout 75 | n := 3 76 | for i := 0; i < n; i++ { 77 | go func() { 78 | conn.SetTimeout(time.Millisecond) 79 | }() 80 | } 81 | 82 | // Attempt to close the connection when the message handler is 83 | // blocked or inactive 84 | conn.Close() 85 | } 86 | 87 | // TestInvalidStateCloseDeadlock tests that we do not enter deadlock when the 88 | // message handler is blocked or inactive. 89 | func TestInvalidStateCloseDeadlock(t *testing.T) { 90 | // The do-nothing server that accepts requests and does nothing 91 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 92 | })) 93 | defer ts.Close() 94 | c, err := net.Dial(ts.Listener.Addr().Network(), ts.Listener.Addr().String()) 95 | if err != nil { 96 | t.Fatalf("error connecting to localhost tcp: %v", err) 97 | } 98 | 99 | // Create an Ldap connection 100 | conn := NewConn(c, false) 101 | conn.SetTimeout(time.Millisecond) 102 | 103 | // Attempt to close the connection when the message handler is 104 | // blocked or inactive 105 | conn.Close() 106 | } 107 | 108 | // TestInvalidStateSendResponseDeadlock tests that we do not enter deadlock when the 109 | // message handler is blocked or inactive. 110 | func TestInvalidStateSendResponseDeadlock(t *testing.T) { 111 | // Attempt to send a response packet when the message handler is blocked or inactive 112 | msgCtx := &messageContext{ 113 | id: 0, 114 | done: make(chan struct{}), 115 | responses: make(chan *PacketResponse), 116 | } 117 | msgCtx.sendResponse(&PacketResponse{}, time.Millisecond) 118 | } 119 | 120 | // TestFinishMessage tests that we do not enter deadlock when a goroutine makes 121 | // a request but does not handle all responses from the server. 122 | func TestFinishMessage(t *testing.T) { 123 | ptc := newPacketTranslatorConn() 124 | defer ptc.Close() 125 | 126 | conn := NewConn(ptc, false) 127 | conn.Start() 128 | 129 | // Test sending 5 different requests in series. Ensure that we can 130 | // get a response packet from the underlying connection and also 131 | // ensure that we can gracefully ignore unhandled responses. 132 | for i := 0; i < 5; i++ { 133 | t.Logf("serial request %d", i) 134 | // Create a message and make sure we can receive responses. 135 | msgCtx := testSendRequest(t, ptc, conn) 136 | testReceiveResponse(t, ptc, msgCtx) 137 | 138 | // Send a few unhandled responses and finish the message. 139 | testSendUnhandledResponsesAndFinish(t, ptc, conn, msgCtx, 5) 140 | t.Logf("serial request %d done", i) 141 | } 142 | 143 | // Test sending 5 different requests in parallel. 144 | var wg sync.WaitGroup 145 | for i := 0; i < 5; i++ { 146 | wg.Add(1) 147 | go func(i int) { 148 | defer wg.Done() 149 | t.Logf("parallel request %d", i) 150 | // Create a message and make sure we can receive responses. 151 | msgCtx := testSendRequest(t, ptc, conn) 152 | testReceiveResponse(t, ptc, msgCtx) 153 | 154 | // Send a few unhandled responses and finish the message. 155 | testSendUnhandledResponsesAndFinish(t, ptc, conn, msgCtx, 5) 156 | t.Logf("parallel request %d done", i) 157 | }(i) 158 | } 159 | wg.Wait() 160 | 161 | // We cannot run Close() in a defer because t.FailNow() will run it and 162 | // it will block if the processMessage Loop is in a deadlock. 163 | conn.Close() 164 | } 165 | 166 | // See: https://github.com/go-ldap/ldap/issues/332 167 | func TestNilConnection(t *testing.T) { 168 | var conn *Conn 169 | _, err := conn.Search(&SearchRequest{}) 170 | if err != ErrNilConnection { 171 | t.Fatalf("expected error to be ErrNilConnection, got %v", err) 172 | } 173 | } 174 | 175 | func testSendRequest(t *testing.T, ptc *packetTranslatorConn, conn *Conn) (msgCtx *messageContext) { 176 | var msgID int64 177 | runWithTimeout(t, time.Second, func() { 178 | msgID = conn.nextMessageID() 179 | }) 180 | 181 | requestPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 182 | requestPacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, msgID, "MessageID")) 183 | 184 | var err error 185 | 186 | runWithTimeout(t, time.Second, func() { 187 | msgCtx, err = conn.sendMessage(requestPacket) 188 | if err != nil { 189 | t.Fatalf("unable to send request message: %s", err) 190 | } 191 | }) 192 | 193 | // We should now be able to get this request packet out from the other 194 | // side. 195 | runWithTimeout(t, time.Second, func() { 196 | if _, err = ptc.ReceiveRequest(); err != nil { 197 | t.Fatalf("unable to receive request packet: %s", err) 198 | } 199 | }) 200 | 201 | return msgCtx 202 | } 203 | 204 | func testReceiveResponse(t *testing.T, ptc *packetTranslatorConn, msgCtx *messageContext) { 205 | // Send a mock response packet. 206 | responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response") 207 | responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, msgCtx.id, "MessageID")) 208 | 209 | runWithTimeout(t, time.Second, func() { 210 | if err := ptc.SendResponse(responsePacket); err != nil { 211 | t.Fatalf("unable to send response packet: %s", err) 212 | } 213 | }) 214 | 215 | // We should be able to receive the packet from the connection. 216 | runWithTimeout(t, time.Second, func() { 217 | if _, ok := <-msgCtx.responses; !ok { 218 | t.Fatal("response channel closed") 219 | } 220 | }) 221 | } 222 | 223 | func testSendUnhandledResponsesAndFinish(t *testing.T, ptc *packetTranslatorConn, conn *Conn, msgCtx *messageContext, numResponses int) { 224 | // Send a mock response packet. 225 | responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response") 226 | responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, msgCtx.id, "MessageID")) 227 | 228 | // Send extra responses but do not attempt to receive them on the 229 | // client side. 230 | for i := 0; i < numResponses; i++ { 231 | runWithTimeout(t, time.Second, func() { 232 | if err := ptc.SendResponse(responsePacket); err != nil { 233 | t.Fatalf("unable to send response packet: %s", err) 234 | } 235 | }) 236 | } 237 | 238 | // Finally, attempt to finish this message. 239 | runWithTimeout(t, time.Second, func() { 240 | conn.finishMessage(msgCtx) 241 | }) 242 | } 243 | 244 | func runWithTimeout(t *testing.T, timeout time.Duration, f func()) { 245 | done := make(chan struct{}) 246 | go func() { 247 | f() 248 | close(done) 249 | }() 250 | 251 | select { 252 | case <-done: // Success! 253 | case <-time.After(timeout): 254 | _, file, line, _ := runtime.Caller(1) 255 | t.Fatalf("%s:%d timed out", file, line) 256 | } 257 | } 258 | 259 | // packetTranslatorConn is a helpful type which can be used with various tests 260 | // in this package. It implements the net.Conn interface to be used as an 261 | // underlying connection for a *ldap.Conn. Most methods are no-ops but the 262 | // Read() and Write() methods are able to translate ber-encoded packets for 263 | // testing LDAP requests and responses. 264 | // 265 | // Test cases can simulate an LDAP server sending a response by calling the 266 | // SendResponse() method with a ber-encoded LDAP response packet. Test cases 267 | // can simulate an LDAP server receiving a request from a client by calling the 268 | // ReceiveRequest() method which returns a ber-encoded LDAP request packet. 269 | type packetTranslatorConn struct { 270 | lock sync.Mutex 271 | isClosed bool 272 | 273 | responseCond sync.Cond 274 | requestCond sync.Cond 275 | 276 | responseBuf bytes.Buffer 277 | requestBuf bytes.Buffer 278 | } 279 | 280 | var errPacketTranslatorConnClosed = errors.New("connection closed") 281 | 282 | func newPacketTranslatorConn() *packetTranslatorConn { 283 | conn := &packetTranslatorConn{} 284 | conn.responseCond = sync.Cond{L: &conn.lock} 285 | conn.requestCond = sync.Cond{L: &conn.lock} 286 | 287 | return conn 288 | } 289 | 290 | // Read is called by the reader() loop to receive response packets. It will 291 | // block until there are more packet bytes available or this connection is 292 | // closed. 293 | func (c *packetTranslatorConn) Read(b []byte) (n int, err error) { 294 | c.lock.Lock() 295 | defer c.lock.Unlock() 296 | 297 | for !c.isClosed { 298 | // Attempt to read data from the response buffer. If it fails 299 | // with an EOF, wait and try again. 300 | n, err = c.responseBuf.Read(b) 301 | if err != io.EOF { 302 | return n, err 303 | } 304 | 305 | c.responseCond.Wait() 306 | } 307 | 308 | return 0, errPacketTranslatorConnClosed 309 | } 310 | 311 | // SendResponse writes the given response packet to the response buffer for 312 | // this connection, signalling any goroutine waiting to read a response. 313 | func (c *packetTranslatorConn) SendResponse(packet *ber.Packet) error { 314 | c.lock.Lock() 315 | defer c.lock.Unlock() 316 | 317 | if c.isClosed { 318 | return errPacketTranslatorConnClosed 319 | } 320 | 321 | // Signal any goroutine waiting to read a response. 322 | defer c.responseCond.Broadcast() 323 | 324 | // Writes to the buffer should always succeed. 325 | c.responseBuf.Write(packet.Bytes()) 326 | 327 | return nil 328 | } 329 | 330 | // Write is called by the processMessages() loop to send request packets. 331 | func (c *packetTranslatorConn) Write(b []byte) (n int, err error) { 332 | c.lock.Lock() 333 | defer c.lock.Unlock() 334 | 335 | if c.isClosed { 336 | return 0, errPacketTranslatorConnClosed 337 | } 338 | 339 | // Signal any goroutine waiting to read a request. 340 | defer c.requestCond.Broadcast() 341 | 342 | // Writes to the buffer should always succeed. 343 | return c.requestBuf.Write(b) 344 | } 345 | 346 | // ReceiveRequest attempts to read a request packet from this connection. It 347 | // will block until it is able to read a full request packet or until this 348 | // connection is closed. 349 | func (c *packetTranslatorConn) ReceiveRequest() (*ber.Packet, error) { 350 | c.lock.Lock() 351 | defer c.lock.Unlock() 352 | 353 | for !c.isClosed { 354 | // Attempt to parse a request packet from the request buffer. 355 | // If it fails with an unexpected EOF, wait and try again. 356 | requestReader := bytes.NewReader(c.requestBuf.Bytes()) 357 | packet, err := ber.ReadPacket(requestReader) 358 | switch err { 359 | case io.EOF, io.ErrUnexpectedEOF: 360 | c.requestCond.Wait() 361 | case nil: 362 | // Advance the request buffer by the number of bytes 363 | // read to decode the request packet. 364 | c.requestBuf.Next(c.requestBuf.Len() - requestReader.Len()) 365 | return packet, nil 366 | default: 367 | return nil, err 368 | } 369 | } 370 | 371 | return nil, errPacketTranslatorConnClosed 372 | } 373 | 374 | // Close closes this connection causing Read() and Write() calls to fail. 375 | func (c *packetTranslatorConn) Close() error { 376 | c.lock.Lock() 377 | defer c.lock.Unlock() 378 | 379 | c.isClosed = true 380 | c.responseCond.Broadcast() 381 | c.requestCond.Broadcast() 382 | 383 | return nil 384 | } 385 | 386 | func (c *packetTranslatorConn) LocalAddr() net.Addr { 387 | return (*net.TCPAddr)(nil) 388 | } 389 | 390 | func (c *packetTranslatorConn) RemoteAddr() net.Addr { 391 | return (*net.TCPAddr)(nil) 392 | } 393 | 394 | func (c *packetTranslatorConn) SetDeadline(t time.Time) error { 395 | return nil 396 | } 397 | 398 | func (c *packetTranslatorConn) SetReadDeadline(t time.Time) error { 399 | return nil 400 | } 401 | 402 | func (c *packetTranslatorConn) SetWriteDeadline(t time.Time) error { 403 | return nil 404 | } 405 | -------------------------------------------------------------------------------- /v3/control_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "reflect" 7 | "runtime" 8 | "testing" 9 | 10 | ber "github.com/go-asn1-ber/asn1-ber" 11 | ) 12 | 13 | func TestControlPaging(t *testing.T) { 14 | runControlTest(t, NewControlPaging(0)) 15 | runControlTest(t, NewControlPaging(100)) 16 | } 17 | 18 | func TestControlManageDsaIT(t *testing.T) { 19 | runControlTest(t, NewControlManageDsaIT(true)) 20 | runControlTest(t, NewControlManageDsaIT(false)) 21 | } 22 | 23 | func TestControlMicrosoftNotification(t *testing.T) { 24 | runControlTest(t, NewControlMicrosoftNotification()) 25 | } 26 | 27 | func TestControlMicrosoftShowDeleted(t *testing.T) { 28 | runControlTest(t, NewControlMicrosoftShowDeleted()) 29 | } 30 | 31 | func TestControlMicrosoftServerLinkTTL(t *testing.T) { 32 | runControlTest(t, NewControlMicrosoftServerLinkTTL()) 33 | } 34 | 35 | func TestControlSubtreeDelete(t *testing.T) { 36 | runControlTest(t, NewControlSubtreeDelete()) 37 | } 38 | 39 | func TestControlString(t *testing.T) { 40 | runControlTest(t, NewControlString("x", true, "y")) 41 | runControlTest(t, NewControlString("x", true, "")) 42 | runControlTest(t, NewControlString("x", false, "y")) 43 | runControlTest(t, NewControlString("x", false, "")) 44 | } 45 | 46 | func TestControlDirSync(t *testing.T) { 47 | runControlTest(t, NewRequestControlDirSync(DirSyncObjectSecurity, 1000, nil)) 48 | runControlTest(t, NewRequestControlDirSync(DirSyncObjectSecurity, 1000, []byte("I'm a cookie!"))) 49 | } 50 | 51 | func runControlTest(t *testing.T, originalControl Control) { 52 | header := "" 53 | if callerpc, _, line, ok := runtime.Caller(1); ok { 54 | if caller := runtime.FuncForPC(callerpc); caller != nil { 55 | header = fmt.Sprintf("%s:%d: ", caller.Name(), line) 56 | } 57 | } 58 | 59 | encodedPacket := originalControl.Encode() 60 | encodedBytes := encodedPacket.Bytes() 61 | 62 | // Decode directly from the encoded packet (ensures Value is correct) 63 | fromPacket, err := DecodeControl(encodedPacket) 64 | if err != nil { 65 | t.Errorf("%sdecoding encoded bytes control failed: %s", header, err) 66 | } 67 | if !bytes.Equal(encodedBytes, fromPacket.Encode().Bytes()) { 68 | t.Errorf("%sround-trip from encoded packet failed", header) 69 | } 70 | if reflect.TypeOf(originalControl) != reflect.TypeOf(fromPacket) { 71 | t.Errorf("%sgot different type decoding from encoded packet: %T vs %T", header, fromPacket, originalControl) 72 | } 73 | 74 | // Decode from the wire bytes (ensures ber-encoding is correct) 75 | pkt, err := ber.DecodePacketErr(encodedBytes) 76 | if err != nil { 77 | t.Errorf("%sdecoding encoded bytes failed: %s", header, err) 78 | } 79 | fromBytes, err := DecodeControl(pkt) 80 | if err != nil { 81 | t.Errorf("%sdecoding control failed: %s", header, err) 82 | } 83 | if !bytes.Equal(encodedBytes, fromBytes.Encode().Bytes()) { 84 | t.Errorf("%sround-trip from encoded bytes failed", header) 85 | } 86 | if reflect.TypeOf(originalControl) != reflect.TypeOf(fromPacket) { 87 | t.Errorf("%sgot different type decoding from encoded bytes: %T vs %T", header, fromBytes, originalControl) 88 | } 89 | } 90 | 91 | func TestDescribeControlManageDsaIT(t *testing.T) { 92 | runAddControlDescriptions(t, NewControlManageDsaIT(false), "Control Type (Manage DSA IT)") 93 | runAddControlDescriptions(t, NewControlManageDsaIT(true), "Control Type (Manage DSA IT)", "Criticality") 94 | } 95 | 96 | func TestDescribeControlPaging(t *testing.T) { 97 | runAddControlDescriptions(t, NewControlPaging(100), "Control Type (Paging)", "Control Value (Paging)") 98 | runAddControlDescriptions(t, NewControlPaging(0), "Control Type (Paging)", "Control Value (Paging)") 99 | } 100 | 101 | func TestDescribeControlSubtreeDelete(t *testing.T) { 102 | runAddControlDescriptions(t, NewControlSubtreeDelete(), "Control Type (Subtree Delete Control)") 103 | } 104 | 105 | func TestDescribeControlMicrosoftNotification(t *testing.T) { 106 | runAddControlDescriptions(t, NewControlMicrosoftNotification(), "Control Type (Change Notification - Microsoft)") 107 | } 108 | 109 | func TestDescribeControlMicrosoftShowDeleted(t *testing.T) { 110 | runAddControlDescriptions(t, NewControlMicrosoftShowDeleted(), "Control Type (Show Deleted Objects - Microsoft)") 111 | } 112 | 113 | func TestDescribeControlMicrosoftServerLinkTTL(t *testing.T) { 114 | runAddControlDescriptions(t, NewControlMicrosoftServerLinkTTL(), "Control Type (Return TTL-DNs for link values with associated expiry times - Microsoft)") 115 | } 116 | 117 | func TestDescribeControlString(t *testing.T) { 118 | runAddControlDescriptions(t, NewControlString("x", true, "y"), "Control Type ()", "Criticality", "Control Value") 119 | runAddControlDescriptions(t, NewControlString("x", true, ""), "Control Type ()", "Criticality") 120 | runAddControlDescriptions(t, NewControlString("x", false, "y"), "Control Type ()", "Control Value") 121 | runAddControlDescriptions(t, NewControlString("x", false, ""), "Control Type ()") 122 | } 123 | 124 | func TestDescribeControlDirSync(t *testing.T) { 125 | runAddControlDescriptions(t, NewRequestControlDirSync(DirSyncObjectSecurity, 1000, nil), "Control Type (DirSync)", "Criticality", "Control Value") 126 | } 127 | 128 | func runAddControlDescriptions(t *testing.T, originalControl Control, childDescriptions ...string) { 129 | header := "" 130 | if callerpc, _, line, ok := runtime.Caller(1); ok { 131 | if caller := runtime.FuncForPC(callerpc); caller != nil { 132 | header = fmt.Sprintf("%s:%d: ", caller.Name(), line) 133 | } 134 | } 135 | 136 | encodedControls := encodeControls([]Control{originalControl}) 137 | _ = addControlDescriptions(encodedControls) 138 | encodedPacket := encodedControls.Children[0] 139 | if len(encodedPacket.Children) != len(childDescriptions) { 140 | t.Errorf("%sinvalid number of children: %d != %d", header, len(encodedPacket.Children), len(childDescriptions)) 141 | } 142 | for i, desc := range childDescriptions { 143 | if encodedPacket.Children[i].Description != desc { 144 | t.Errorf("%sdescription not as expected: %s != %s", header, encodedPacket.Children[i].Description, desc) 145 | } 146 | } 147 | } 148 | 149 | func TestDecodeControl(t *testing.T) { 150 | type args struct { 151 | packet *ber.Packet 152 | } 153 | 154 | tests := []struct { 155 | name string 156 | args args 157 | want Control 158 | wantErr bool 159 | }{ 160 | { 161 | 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})}, 162 | want: &ControlBeheraPasswordPolicy{Expire: 2147481180, Grace: -1, Error: -1, ErrorString: ""}, wantErr: false, 163 | }, 164 | { 165 | 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})}, 166 | want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: 17, Error: -1, ErrorString: ""}, wantErr: false, 167 | }, 168 | { 169 | 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})}, 170 | want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 0, ErrorString: "Password expired"}, wantErr: false, 171 | }, 172 | { 173 | 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})}, 174 | want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 1, ErrorString: "Account locked"}, wantErr: false, 175 | }, 176 | { 177 | 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})}, 178 | want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 3, ErrorString: "Policy prevents password modification"}, wantErr: false, 179 | }, 180 | { 181 | 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})}, 182 | want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 4, ErrorString: "Policy requires old password in order to change password"}, wantErr: false, 183 | }, 184 | { 185 | 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})}, 186 | want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 5, ErrorString: "Password fails quality checks"}, wantErr: false, 187 | }, 188 | { 189 | 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})}, 190 | want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 6, ErrorString: "Password is too short for policy"}, wantErr: false, 191 | }, 192 | { 193 | 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})}, 194 | want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 7, ErrorString: "Password has been changed too recently"}, wantErr: false, 195 | }, 196 | { 197 | 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})}, 198 | want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 8, ErrorString: "New password is in list of old passwords"}, wantErr: false, 199 | }, 200 | } 201 | for i := range tests { 202 | err := addControlDescriptions(tests[i].args.packet) 203 | if err != nil { 204 | t.Fatal(err) 205 | } 206 | tests[i].args.packet = tests[i].args.packet.Children[0] 207 | } 208 | 209 | for _, tt := range tests { 210 | t.Run(tt.name, func(t *testing.T) { 211 | got, err := DecodeControl(tt.args.packet) 212 | if (err != nil) != tt.wantErr { 213 | t.Errorf("DecodeControl() error = %v, wantErr %v", err, tt.wantErr) 214 | return 215 | } 216 | if !reflect.DeepEqual(got, tt.want) { 217 | t.Errorf("DecodeControl() got = %v, want %v", got, tt.want) 218 | } 219 | }) 220 | } 221 | } 222 | 223 | func TestControlServerSideSortingDecoding(t *testing.T) { 224 | control := NewControlServerSideSortingWithSortKeys([]*SortKey{{ 225 | MatchingRule: "foo", 226 | AttributeType: "foobar", 227 | Reverse: true, 228 | }, { 229 | MatchingRule: "foo", 230 | AttributeType: "foobar", 231 | Reverse: false, 232 | }, { 233 | MatchingRule: "", 234 | AttributeType: "", 235 | Reverse: false, 236 | }, { 237 | MatchingRule: "totoRule", 238 | AttributeType: "", 239 | Reverse: false, 240 | }, { 241 | MatchingRule: "", 242 | AttributeType: "totoType", 243 | Reverse: false, 244 | }}) 245 | 246 | controlDecoded, err := NewControlServerSideSorting(control.Encode()) 247 | if err != nil { 248 | t.Fatal(err) 249 | } 250 | 251 | if control.GetControlType() != controlDecoded.GetControlType() { 252 | t.Fatalf("control type mismatch: control:%s - decoded:%s", control.GetControlType(), controlDecoded.GetControlType()) 253 | } 254 | 255 | if len(control.SortKeys) != len(controlDecoded.SortKeys) { 256 | t.Fatalf("sort keys length mismatch (control: %d - decoded: %d)", len(control.SortKeys), len(controlDecoded.SortKeys)) 257 | } 258 | 259 | for i, sk := range control.SortKeys { 260 | dsk := controlDecoded.SortKeys[i] 261 | 262 | if sk.AttributeType != dsk.AttributeType { 263 | t.Fatalf("attribute type mismatch for sortkey %d", i) 264 | } 265 | 266 | if sk.MatchingRule != dsk.MatchingRule { 267 | t.Fatalf("matching rule mismatch for sortkey %d", i) 268 | } 269 | 270 | if sk.Reverse != dsk.Reverse { 271 | t.Fatalf("reverse mismtach for sortkey %d", i) 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /v3/debug.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | ber "github.com/go-asn1-ber/asn1-ber" 5 | ) 6 | 7 | // debugging type 8 | // - has a Printf method to write the debug output 9 | type debugging bool 10 | 11 | // Enable controls debugging mode. 12 | func (debug *debugging) Enable(b bool) { 13 | *debug = debugging(b) 14 | } 15 | 16 | // Printf writes debug output. 17 | func (debug debugging) Printf(format string, args ...interface{}) { 18 | if debug { 19 | logger.Printf(format, args...) 20 | } 21 | } 22 | 23 | // PrintPacket dumps a packet. 24 | func (debug debugging) PrintPacket(packet *ber.Packet) { 25 | if debug { 26 | ber.WritePacket(logger.Writer(), packet) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /v3/del.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | ber "github.com/go-asn1-ber/asn1-ber" 6 | ) 7 | 8 | // DelRequest implements an LDAP deletion request 9 | type DelRequest struct { 10 | // DN is the name of the directory entry to delete 11 | DN string 12 | // Controls hold optional controls to send with the request 13 | Controls []Control 14 | } 15 | 16 | func (req *DelRequest) appendTo(envelope *ber.Packet) error { 17 | pkt := ber.Encode(ber.ClassApplication, ber.TypePrimitive, ApplicationDelRequest, req.DN, "Del Request") 18 | pkt.Data.Write([]byte(req.DN)) 19 | 20 | envelope.AppendChild(pkt) 21 | if len(req.Controls) > 0 { 22 | envelope.AppendChild(encodeControls(req.Controls)) 23 | } 24 | 25 | return nil 26 | } 27 | 28 | // NewDelRequest creates a delete request for the given DN and controls 29 | func NewDelRequest(DN string, Controls []Control) *DelRequest { 30 | return &DelRequest{ 31 | DN: DN, 32 | Controls: Controls, 33 | } 34 | } 35 | 36 | // Del executes the given delete request 37 | func (l *Conn) Del(delRequest *DelRequest) error { 38 | msgCtx, err := l.doRequest(delRequest) 39 | if err != nil { 40 | return err 41 | } 42 | defer l.finishMessage(msgCtx) 43 | 44 | packet, err := l.readPacket(msgCtx) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | if packet.Children[1].Tag == ApplicationDelResponse { 50 | err := GetLDAPError(packet) 51 | if err != nil { 52 | return err 53 | } 54 | } else { 55 | return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) 56 | } 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /v3/dn.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "encoding/hex" 5 | "errors" 6 | "fmt" 7 | ber "github.com/go-asn1-ber/asn1-ber" 8 | "sort" 9 | "strings" 10 | "unicode" 11 | "unicode/utf8" 12 | ) 13 | 14 | // AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514 15 | type AttributeTypeAndValue struct { 16 | // Type is the attribute type 17 | Type string 18 | // Value is the attribute value 19 | Value string 20 | } 21 | 22 | func (a *AttributeTypeAndValue) setType(str string) error { 23 | result, err := decodeString(str) 24 | if err != nil { 25 | return err 26 | } 27 | a.Type = result 28 | 29 | return nil 30 | } 31 | 32 | func (a *AttributeTypeAndValue) setValue(s string) error { 33 | // https://www.ietf.org/rfc/rfc4514.html#section-2.4 34 | // If the AttributeType is of the dotted-decimal form, the 35 | // AttributeValue is represented by an number sign ('#' U+0023) 36 | // character followed by the hexadecimal encoding of each of the octets 37 | // of the BER encoding of the X.500 AttributeValue. 38 | if len(s) > 0 && s[0] == '#' { 39 | decodedString, err := decodeEncodedString(s[1:]) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | a.Value = decodedString 45 | return nil 46 | } else { 47 | decodedString, err := decodeString(s) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | a.Value = decodedString 53 | return nil 54 | } 55 | } 56 | 57 | // String returns a normalized string representation of this attribute type and 58 | // value pair which is the lowercase join of the Type and Value with a "=". 59 | func (a *AttributeTypeAndValue) String() string { 60 | return encodeString(foldString(a.Type), false) + "=" + encodeString(a.Value, true) 61 | } 62 | 63 | // RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514 64 | type RelativeDN struct { 65 | Attributes []*AttributeTypeAndValue 66 | } 67 | 68 | // String returns a normalized string representation of this relative DN which 69 | // is the a join of all attributes (sorted in increasing order) with a "+". 70 | func (r *RelativeDN) String() string { 71 | attrs := make([]string, len(r.Attributes)) 72 | for i := range r.Attributes { 73 | attrs[i] = r.Attributes[i].String() 74 | } 75 | sort.Strings(attrs) 76 | return strings.Join(attrs, "+") 77 | } 78 | 79 | // DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514 80 | type DN struct { 81 | RDNs []*RelativeDN 82 | } 83 | 84 | // String returns a normalized string representation of this DN which is the 85 | // join of all relative DNs with a ",". 86 | func (d *DN) String() string { 87 | rdns := make([]string, len(d.RDNs)) 88 | for i := range d.RDNs { 89 | rdns[i] = d.RDNs[i].String() 90 | } 91 | return strings.Join(rdns, ",") 92 | } 93 | 94 | func stripLeadingAndTrailingSpaces(inVal string) string { 95 | noSpaces := strings.Trim(inVal, " ") 96 | 97 | // Re-add the trailing space if it was an escaped space 98 | if len(noSpaces) > 0 && noSpaces[len(noSpaces)-1] == '\\' && inVal[len(inVal)-1] == ' ' { 99 | noSpaces = noSpaces + " " 100 | } 101 | 102 | return noSpaces 103 | } 104 | 105 | // Remove leading and trailing spaces from the attribute type and value 106 | // and unescape any escaped characters in these fields 107 | // 108 | // decodeString is based on https://github.com/inteon/cert-manager/blob/ed280d28cd02b262c5db46054d88e70ab518299c/pkg/util/pki/internal/dn.go#L170 109 | func decodeString(str string) (string, error) { 110 | s := []rune(stripLeadingAndTrailingSpaces(str)) 111 | 112 | builder := strings.Builder{} 113 | for i := 0; i < len(s); i++ { 114 | char := s[i] 115 | 116 | // If the character is not an escape character, just add it to the 117 | // builder and continue 118 | if char != '\\' { 119 | builder.WriteRune(char) 120 | continue 121 | } 122 | 123 | // If the escape character is the last character, it's a corrupted 124 | // escaped character 125 | if i+1 >= len(s) { 126 | return "", fmt.Errorf("got corrupted escaped character: '%s'", string(s)) 127 | } 128 | 129 | // If the escaped character is a special character, just add it to 130 | // the builder and continue 131 | switch s[i+1] { 132 | case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\': 133 | builder.WriteRune(s[i+1]) 134 | i++ 135 | continue 136 | } 137 | 138 | // If the escaped character is not a special character, it should 139 | // be a hex-encoded character of the form \XX if it's not at least 140 | // two characters long, it's a corrupted escaped character 141 | if i+2 >= len(s) { 142 | return "", errors.New("failed to decode escaped character: encoding/hex: invalid byte: " + string(s[i+1])) 143 | } 144 | 145 | // Get the runes for the two characters after the escape character 146 | // and convert them to a byte slice 147 | xx := []byte(string(s[i+1 : i+3])) 148 | 149 | // If the two runes are not hex characters and result in more than 150 | // two bytes when converted to a byte slice, it's a corrupted 151 | // escaped character 152 | if len(xx) != 2 { 153 | return "", errors.New("failed to decode escaped character: invalid byte: " + string(xx)) 154 | } 155 | 156 | // Decode the hex-encoded character and add it to the builder 157 | dst := []byte{0} 158 | if n, err := hex.Decode(dst, xx); err != nil { 159 | return "", errors.New("failed to decode escaped character: " + err.Error()) 160 | } else if n != 1 { 161 | return "", fmt.Errorf("failed to decode escaped character: encoding/hex: expected 1 byte when un-escaping, got %d", n) 162 | } 163 | 164 | builder.WriteByte(dst[0]) 165 | i += 2 166 | } 167 | 168 | return builder.String(), nil 169 | } 170 | 171 | // Escape a string according to RFC 4514 172 | func encodeString(value string, isValue bool) string { 173 | builder := strings.Builder{} 174 | 175 | escapeChar := func(c byte) { 176 | builder.WriteByte('\\') 177 | builder.WriteByte(c) 178 | } 179 | 180 | escapeHex := func(c byte) { 181 | builder.WriteByte('\\') 182 | builder.WriteString(hex.EncodeToString([]byte{c})) 183 | } 184 | 185 | // Loop through each byte and escape as necessary. 186 | // Runes that take up more than one byte are escaped 187 | // byte by byte (since both bytes are non-ASCII). 188 | for i := 0; i < len(value); i++ { 189 | char := value[i] 190 | if i == 0 && (char == ' ' || char == '#') { 191 | // Special case leading space or number sign. 192 | escapeChar(char) 193 | continue 194 | } 195 | if i == len(value)-1 && char == ' ' { 196 | // Special case trailing space. 197 | escapeChar(char) 198 | continue 199 | } 200 | 201 | switch char { 202 | case '"', '+', ',', ';', '<', '>', '\\': 203 | // Each of these special characters must be escaped. 204 | escapeChar(char) 205 | continue 206 | } 207 | 208 | if !isValue && char == '=' { 209 | // Equal signs have to be escaped only in the type part of 210 | // the attribute type and value pair. 211 | escapeChar(char) 212 | continue 213 | } 214 | 215 | if char < ' ' || char > '~' { 216 | // All special character escapes are handled first 217 | // above. All bytes less than ASCII SPACE and all bytes 218 | // greater than ASCII TILDE must be hex-escaped. 219 | escapeHex(char) 220 | continue 221 | } 222 | 223 | // Any other character does not require escaping. 224 | builder.WriteByte(char) 225 | } 226 | 227 | return builder.String() 228 | } 229 | 230 | func decodeEncodedString(str string) (string, error) { 231 | decoded, err := hex.DecodeString(str) 232 | if err != nil { 233 | return "", fmt.Errorf("failed to decode BER encoding: %w", err) 234 | } 235 | 236 | packet, err := ber.DecodePacketErr(decoded) 237 | if err != nil { 238 | return "", fmt.Errorf("failed to decode BER encoding: %w", err) 239 | } 240 | 241 | return packet.Data.String(), nil 242 | } 243 | 244 | // ParseDN returns a distinguishedName or an error. 245 | // The function respects https://tools.ietf.org/html/rfc4514 246 | func ParseDN(str string) (*DN, error) { 247 | var dn = &DN{RDNs: make([]*RelativeDN, 0)} 248 | if strings.TrimSpace(str) == "" { 249 | return dn, nil 250 | } 251 | 252 | var ( 253 | rdn = &RelativeDN{} 254 | attr = &AttributeTypeAndValue{} 255 | escaping bool 256 | startPos int 257 | appendAttributesToRDN = func(end bool) { 258 | rdn.Attributes = append(rdn.Attributes, attr) 259 | attr = &AttributeTypeAndValue{} 260 | if end { 261 | dn.RDNs = append(dn.RDNs, rdn) 262 | rdn = &RelativeDN{} 263 | } 264 | } 265 | ) 266 | 267 | // Loop through each character in the string and 268 | // build up the attribute type and value pairs. 269 | // We only check for ascii characters here, which 270 | // allows us to iterate over the string byte by byte. 271 | for i := 0; i < len(str); i++ { 272 | char := str[i] 273 | switch { 274 | case escaping: 275 | escaping = false 276 | case char == '\\': 277 | escaping = true 278 | case char == '=' && len(attr.Type) == 0: 279 | if err := attr.setType(str[startPos:i]); err != nil { 280 | return nil, err 281 | } 282 | startPos = i + 1 283 | case char == ',' || char == '+' || char == ';': 284 | if len(attr.Type) == 0 { 285 | return dn, errors.New("incomplete type, value pair") 286 | } 287 | if err := attr.setValue(str[startPos:i]); err != nil { 288 | return nil, err 289 | } 290 | 291 | startPos = i + 1 292 | last := char == ',' || char == ';' 293 | appendAttributesToRDN(last) 294 | } 295 | } 296 | 297 | if len(attr.Type) == 0 { 298 | return dn, errors.New("DN ended with incomplete type, value pair") 299 | } 300 | 301 | if err := attr.setValue(str[startPos:]); err != nil { 302 | return dn, err 303 | } 304 | appendAttributesToRDN(true) 305 | 306 | return dn, nil 307 | } 308 | 309 | // Equal returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). 310 | // Returns true if they have the same number of relative distinguished names 311 | // and corresponding relative distinguished names (by position) are the same. 312 | func (d *DN) Equal(other *DN) bool { 313 | if len(d.RDNs) != len(other.RDNs) { 314 | return false 315 | } 316 | for i := range d.RDNs { 317 | if !d.RDNs[i].Equal(other.RDNs[i]) { 318 | return false 319 | } 320 | } 321 | return true 322 | } 323 | 324 | // AncestorOf returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN. 325 | // "ou=widgets,o=acme.com" is an ancestor of "ou=sprockets,ou=widgets,o=acme.com" 326 | // "ou=widgets,o=acme.com" is not an ancestor of "ou=sprockets,ou=widgets,o=foo.com" 327 | // "ou=widgets,o=acme.com" is not an ancestor of "ou=widgets,o=acme.com" 328 | func (d *DN) AncestorOf(other *DN) bool { 329 | if len(d.RDNs) >= len(other.RDNs) { 330 | return false 331 | } 332 | // Take the last `len(d.RDNs)` RDNs from the other DN to compare against 333 | otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):] 334 | for i := range d.RDNs { 335 | if !d.RDNs[i].Equal(otherRDNs[i]) { 336 | return false 337 | } 338 | } 339 | return true 340 | } 341 | 342 | // Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). 343 | // Relative distinguished names are the same if and only if they have the same number of AttributeTypeAndValues 344 | // and each attribute of the first RDN is the same as the attribute of the second RDN with the same attribute type. 345 | // The order of attributes is not significant. 346 | // Case of attribute types is not significant. 347 | func (r *RelativeDN) Equal(other *RelativeDN) bool { 348 | if len(r.Attributes) != len(other.Attributes) { 349 | return false 350 | } 351 | return r.hasAllAttributes(other.Attributes) && other.hasAllAttributes(r.Attributes) 352 | } 353 | 354 | func (r *RelativeDN) hasAllAttributes(attrs []*AttributeTypeAndValue) bool { 355 | for _, attr := range attrs { 356 | found := false 357 | for _, myattr := range r.Attributes { 358 | if myattr.Equal(attr) { 359 | found = true 360 | break 361 | } 362 | } 363 | if !found { 364 | return false 365 | } 366 | } 367 | return true 368 | } 369 | 370 | // Equal returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue 371 | // Case of the attribute type is not significant 372 | func (a *AttributeTypeAndValue) Equal(other *AttributeTypeAndValue) bool { 373 | return strings.EqualFold(a.Type, other.Type) && a.Value == other.Value 374 | } 375 | 376 | // EqualFold returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). 377 | // Returns true if they have the same number of relative distinguished names 378 | // and corresponding relative distinguished names (by position) are the same. 379 | // Case of the attribute type and value is not significant 380 | func (d *DN) EqualFold(other *DN) bool { 381 | if len(d.RDNs) != len(other.RDNs) { 382 | return false 383 | } 384 | for i := range d.RDNs { 385 | if !d.RDNs[i].EqualFold(other.RDNs[i]) { 386 | return false 387 | } 388 | } 389 | return true 390 | } 391 | 392 | // AncestorOfFold returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN. 393 | // Case of the attribute type and value is not significant 394 | func (d *DN) AncestorOfFold(other *DN) bool { 395 | if len(d.RDNs) >= len(other.RDNs) { 396 | return false 397 | } 398 | // Take the last `len(d.RDNs)` RDNs from the other DN to compare against 399 | otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):] 400 | for i := range d.RDNs { 401 | if !d.RDNs[i].EqualFold(otherRDNs[i]) { 402 | return false 403 | } 404 | } 405 | return true 406 | } 407 | 408 | // EqualFold returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). 409 | // Case of the attribute type is not significant 410 | func (r *RelativeDN) EqualFold(other *RelativeDN) bool { 411 | if len(r.Attributes) != len(other.Attributes) { 412 | return false 413 | } 414 | return r.hasAllAttributesFold(other.Attributes) && other.hasAllAttributesFold(r.Attributes) 415 | } 416 | 417 | func (r *RelativeDN) hasAllAttributesFold(attrs []*AttributeTypeAndValue) bool { 418 | for _, attr := range attrs { 419 | found := false 420 | for _, myattr := range r.Attributes { 421 | if myattr.EqualFold(attr) { 422 | found = true 423 | break 424 | } 425 | } 426 | if !found { 427 | return false 428 | } 429 | } 430 | return true 431 | } 432 | 433 | // EqualFold returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue 434 | // Case of the attribute type and value is not significant 435 | func (a *AttributeTypeAndValue) EqualFold(other *AttributeTypeAndValue) bool { 436 | return strings.EqualFold(a.Type, other.Type) && strings.EqualFold(a.Value, other.Value) 437 | } 438 | 439 | // foldString returns a folded string such that foldString(x) == foldString(y) 440 | // is identical to bytes.EqualFold(x, y). 441 | // based on https://go.dev/src/encoding/json/fold.go 442 | func foldString(s string) string { 443 | builder := strings.Builder{} 444 | for _, char := range s { 445 | // Handle single-byte ASCII. 446 | if char < utf8.RuneSelf { 447 | if 'A' <= char && char <= 'Z' { 448 | char += 'a' - 'A' 449 | } 450 | builder.WriteRune(char) 451 | continue 452 | } 453 | 454 | builder.WriteRune(foldRune(char)) 455 | } 456 | return builder.String() 457 | } 458 | 459 | // foldRune is returns the smallest rune for all runes in the same fold set. 460 | func foldRune(r rune) rune { 461 | for { 462 | r2 := unicode.SimpleFold(r) 463 | if r2 <= r { 464 | return r 465 | } 466 | r = r2 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /v3/dn_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSuccessfulDNParsing(t *testing.T) { 11 | testcases := map[string]DN{ 12 | "": {[]*RelativeDN{}}, 13 | "cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": {[]*RelativeDN{ 14 | {[]*AttributeTypeAndValue{{"cn", "Jim, \"Hasse Hö\" Hansson!"}}}, 15 | {[]*AttributeTypeAndValue{{"dc", "dummy"}}}, 16 | {[]*AttributeTypeAndValue{{"dc", "com"}}}, 17 | }}, 18 | "UID=jsmith,DC=example,DC=net": {[]*RelativeDN{ 19 | {[]*AttributeTypeAndValue{{"UID", "jsmith"}}}, 20 | {[]*AttributeTypeAndValue{{"DC", "example"}}}, 21 | {[]*AttributeTypeAndValue{{"DC", "net"}}}, 22 | }}, 23 | "OU=Sales+CN=J. Smith,DC=example,DC=net": {[]*RelativeDN{ 24 | {[]*AttributeTypeAndValue{ 25 | {"OU", "Sales"}, 26 | {"CN", "J. Smith"}, 27 | }}, 28 | {[]*AttributeTypeAndValue{{"DC", "example"}}}, 29 | {[]*AttributeTypeAndValue{{"DC", "net"}}}, 30 | }}, 31 | "1.3.6.1.4.1.1466.0=#04024869": {[]*RelativeDN{ 32 | {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}}, 33 | }}, 34 | "1.3.6.1.4.1.1466.0=#04024869,DC=net": {[]*RelativeDN{ 35 | {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}}, 36 | {[]*AttributeTypeAndValue{{"DC", "net"}}}, 37 | }}, 38 | "CN=Lu\\C4\\8Di\\C4\\87": {[]*RelativeDN{ 39 | {[]*AttributeTypeAndValue{{"CN", "Lučić"}}}, 40 | }}, 41 | " CN = Lu\\C4\\8Di\\C4\\87 ": {[]*RelativeDN{ 42 | {[]*AttributeTypeAndValue{{"CN", "Lučić"}}}, 43 | }}, 44 | ` A = 1 , B = 2 `: {[]*RelativeDN{ 45 | {[]*AttributeTypeAndValue{{"A", "1"}}}, 46 | {[]*AttributeTypeAndValue{{"B", "2"}}}, 47 | }}, 48 | ` A = 1 + B = 2 `: {[]*RelativeDN{ 49 | {[]*AttributeTypeAndValue{ 50 | {"A", "1"}, 51 | {"B", "2"}, 52 | }}, 53 | }}, 54 | ` \ \ A\ \ = \ \ 1\ \ , \ \ B\ \ = \ \ 2\ \ `: {[]*RelativeDN{ 55 | {[]*AttributeTypeAndValue{{" A ", " 1 "}}}, 56 | {[]*AttributeTypeAndValue{{" B ", " 2 "}}}, 57 | }}, 58 | ` \ \ A\ \ = \ \ 1\ \ + \ \ B\ \ = \ \ 2\ \ `: {[]*RelativeDN{ 59 | {[]*AttributeTypeAndValue{ 60 | {" A ", " 1 "}, 61 | {" B ", " 2 "}, 62 | }}, 63 | }}, 64 | "A = 88 \t": {[]*RelativeDN{ 65 | {[]*AttributeTypeAndValue{ 66 | {"A", "88 \t"}, 67 | }}, 68 | }}, 69 | "A = 88 \n": {[]*RelativeDN{ 70 | {[]*AttributeTypeAndValue{ 71 | {"A", "88 \n"}, 72 | }}, 73 | }}, 74 | `cn=john.doe;dc=example,dc=net`: {[]*RelativeDN{ 75 | {[]*AttributeTypeAndValue{{"cn", "john.doe"}}}, 76 | {[]*AttributeTypeAndValue{{"dc", "example"}}}, 77 | {[]*AttributeTypeAndValue{{"dc", "net"}}}, 78 | }}, 79 | `cn=⭐;dc=❤️=\==,dc=❤️\\`: {[]*RelativeDN{ 80 | {[]*AttributeTypeAndValue{{"cn", "⭐"}}}, 81 | {[]*AttributeTypeAndValue{{"dc", "❤️==="}}}, 82 | {[]*AttributeTypeAndValue{{"dc", "❤️\\"}}}, 83 | }}, 84 | 85 | // Escaped `;` should not be treated as RDN 86 | `cn=john.doe\;weird name,dc=example,dc=net`: {[]*RelativeDN{ 87 | {[]*AttributeTypeAndValue{{"cn", "john.doe;weird name"}}}, 88 | {[]*AttributeTypeAndValue{{"dc", "example"}}}, 89 | {[]*AttributeTypeAndValue{{"dc", "net"}}}, 90 | }}, 91 | `cn=ZXhhbXBsZVRleHQ=,dc=dummy,dc=com`: {[]*RelativeDN{ 92 | {[]*AttributeTypeAndValue{{"cn", "ZXhhbXBsZVRleHQ="}}}, 93 | {[]*AttributeTypeAndValue{{"dc", "dummy"}}}, 94 | {[]*AttributeTypeAndValue{{"dc", "com"}}}, 95 | }}, 96 | `1.3.6.1.4.1.1466.0=test`: {[]*RelativeDN{ 97 | {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "test"}}}, 98 | }}, 99 | `1=#04024869`: {[]*RelativeDN{ 100 | {[]*AttributeTypeAndValue{{"1", "Hi"}}}, 101 | }}, 102 | `CN=James \"Jim\" Smith\, III,DC=example,DC=net`: {[]*RelativeDN{ 103 | {[]*AttributeTypeAndValue{{"CN", "James \"Jim\" Smith, III"}}}, 104 | {[]*AttributeTypeAndValue{{"DC", "example"}}}, 105 | {[]*AttributeTypeAndValue{{"DC", "net"}}}, 106 | }}, 107 | `CN=Before\0dAfter,DC=example,DC=net`: {[]*RelativeDN{ 108 | {[]*AttributeTypeAndValue{{"CN", "Before\x0dAfter"}}}, 109 | {[]*AttributeTypeAndValue{{"DC", "example"}}}, 110 | {[]*AttributeTypeAndValue{{"DC", "net"}}}, 111 | }}, 112 | `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{ 113 | {[]*AttributeTypeAndValue{{"cn", "foo-lon❤️,g.com"}}}, 114 | {[]*AttributeTypeAndValue{{"OU", "Foo===Long"}}}, 115 | {[]*AttributeTypeAndValue{{"ou", "Ba # rq"}}}, 116 | {[]*AttributeTypeAndValue{{"ou", "Baz"}}}, 117 | {[]*AttributeTypeAndValue{{"o", "C; orp."}, {"c", "US"}}}, 118 | }}, 119 | } 120 | 121 | for test, answer := range testcases { 122 | dn, err := ParseDN(test) 123 | if err != nil { 124 | t.Errorf("ParseDN failed for DN test '%s': %s", test, err) 125 | continue 126 | } 127 | if !reflect.DeepEqual(dn, &answer) { 128 | t.Errorf("Parsed DN '%s' is not equal to the expected structure", test) 129 | t.Logf("Expected:") 130 | for _, rdn := range answer.RDNs { 131 | for _, attribute := range rdn.Attributes { 132 | t.Logf(" #%v\n", attribute) 133 | } 134 | } 135 | t.Logf("Actual:") 136 | for _, rdn := range dn.RDNs { 137 | for _, attribute := range rdn.Attributes { 138 | t.Logf(" #%v\n", attribute) 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | func TestErrorDNParsing(t *testing.T) { 146 | testcases := map[string]string{ 147 | "*": "DN ended with incomplete type, value pair", 148 | "cn=Jim\\0Test": "failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'", 149 | "cn=Jim\\0": "failed to decode escaped character: encoding/hex: invalid byte: 0", 150 | "DC=example,=net": "DN ended with incomplete type, value pair", 151 | "1=#0402486": "failed to decode BER encoding: encoding/hex: odd length hex string", 152 | "test,DC=example,DC=com": "incomplete type, value pair", 153 | "=test,DC=example,DC=com": "incomplete type, value pair", 154 | "1.3.6.1.4.1.1466.0=test+": "DN ended with incomplete type, value pair", 155 | `1.3.6.1.4.1.1466.0=test;`: "DN ended with incomplete type, value pair", 156 | "1.3.6.1.4.1.1466.0=test+,": "incomplete type, value pair", 157 | "DF=#6666666666665006838820013100000746939546349182108463491821809FBFFFFFFFFF": "failed to decode BER encoding: unexpected EOF", 158 | } 159 | 160 | for test, answer := range testcases { 161 | _, err := ParseDN(test) 162 | if err == nil { 163 | t.Errorf("Expected '%s' to fail parsing but succeeded\n", test) 164 | } else if err.Error() != answer { 165 | t.Errorf("Unexpected error on: '%s':\nExpected: %s\nGot: %s\n", test, answer, err.Error()) 166 | } 167 | } 168 | } 169 | 170 | func TestDNEqual(t *testing.T) { 171 | testcases := []struct { 172 | A string 173 | B string 174 | Equal bool 175 | }{ 176 | // Exact match 177 | {"", "", true}, 178 | {"o=A", "o=A", true}, 179 | {"o=A", "o=B", false}, 180 | 181 | {"o=A,o=B", "o=A,o=B", true}, 182 | {"o=A,o=B", "o=A,o=C", false}, 183 | 184 | {"o=A+o=B", "o=A+o=B", true}, 185 | {"o=A+o=B", "o=A+o=C", false}, 186 | 187 | // Case mismatch in type is ignored 188 | {"o=A", "O=A", true}, 189 | {"o=A,o=B", "o=A,O=B", true}, 190 | {"o=A+o=B", "o=A+O=B", true}, 191 | 192 | // Case mismatch in value is significant 193 | {"o=a", "O=A", false}, 194 | {"o=a,o=B", "o=A,O=B", false}, 195 | {"o=a+o=B", "o=A+O=B", false}, 196 | 197 | // Multi-valued RDN order mismatch is ignored 198 | {"o=A+o=B", "O=B+o=A", true}, 199 | // Number of RDN attributes is significant 200 | {"o=A+o=B", "O=B+o=A+O=B", false}, 201 | 202 | // Missing values are significant 203 | {"o=A+o=B", "O=B+o=A+O=C", false}, // missing values matter 204 | {"o=A+o=B+o=C", "O=B+o=A", false}, // missing values matter 205 | 206 | // Whitespace tests 207 | // Matching 208 | { 209 | "cn=John Doe, ou=People, dc=sun.com", 210 | "cn=John Doe, ou=People, dc=sun.com", 211 | true, 212 | }, 213 | // Difference in leading/trailing chars is ignored 214 | { 215 | "cn=\\ John\\20Doe, ou=People, dc=sun.com", 216 | "cn= \\ John Doe,ou=People,dc=sun.com", 217 | true, 218 | }, 219 | // Difference in values is significant 220 | { 221 | "cn=John Doe, ou=People, dc=sun.com", 222 | "cn=John Doe, ou=People, dc=sun.com", 223 | false, 224 | }, 225 | // Test parsing of `;` for separating RDNs 226 | {"cn=john;dc=example,dc=com", "cn=john,dc=example,dc=com", true}, // missing values matter 227 | } 228 | 229 | for i, tc := range testcases { 230 | a, err := ParseDN(tc.A) 231 | if err != nil { 232 | t.Errorf("%d: %v", i, err) 233 | continue 234 | } 235 | b, err := ParseDN(tc.B) 236 | if err != nil { 237 | t.Errorf("%d: %v", i, err) 238 | continue 239 | } 240 | if expected, actual := tc.Equal, a.Equal(b); expected != actual { 241 | t.Errorf("%d: when comparing %q and %q expected %v, got %v", i, a, b, expected, actual) 242 | continue 243 | } 244 | if expected, actual := tc.Equal, b.Equal(a); expected != actual { 245 | t.Errorf("%d: when comparing %q and %q expected %v, got %v", i, a, b, expected, actual) 246 | continue 247 | } 248 | 249 | if expected, actual := a.Equal(b), a.String() == b.String(); expected != actual { 250 | t.Errorf("%d: when asserting string comparison of %q and %q expected equal %v, got %v", i, a, b, expected, actual) 251 | continue 252 | } 253 | } 254 | } 255 | 256 | func TestDNEqualFold(t *testing.T) { 257 | testcases := []struct { 258 | A string 259 | B string 260 | Equal bool 261 | }{ 262 | // Match on case insensitive 263 | {"o=A", "o=a", true}, 264 | {"o=A,o=b", "o=a,o=B", true}, 265 | {"o=a+o=B", "o=A+o=b", true}, 266 | { 267 | "cn=users,ou=example,dc=com", 268 | "cn=Users,ou=example,dc=com", 269 | true, 270 | }, 271 | 272 | // Match on case insensitive and case mismatch in type 273 | {"o=A", "O=a", true}, 274 | {"o=A,o=b", "o=a,O=B", true}, 275 | {"o=a+o=B", "o=A+O=b", true}, 276 | } 277 | 278 | for i, tc := range testcases { 279 | a, err := ParseDN(tc.A) 280 | if err != nil { 281 | t.Errorf("%d: %v", i, err) 282 | continue 283 | } 284 | b, err := ParseDN(tc.B) 285 | if err != nil { 286 | t.Errorf("%d: %v", i, err) 287 | continue 288 | } 289 | if expected, actual := tc.Equal, a.EqualFold(b); expected != actual { 290 | t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) 291 | continue 292 | } 293 | if expected, actual := tc.Equal, b.EqualFold(a); expected != actual { 294 | t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) 295 | continue 296 | } 297 | } 298 | } 299 | 300 | func TestDNAncestor(t *testing.T) { 301 | testcases := []struct { 302 | A string 303 | B string 304 | Ancestor bool 305 | }{ 306 | // Exact match returns false 307 | {"", "", false}, 308 | {"o=A", "o=A", false}, 309 | {"o=A,o=B", "o=A,o=B", false}, 310 | {"o=A+o=B", "o=A+o=B", false}, 311 | 312 | // Mismatch 313 | {"ou=C,ou=B,o=A", "ou=E,ou=D,ou=B,o=A", false}, 314 | 315 | // Descendant 316 | {"ou=C,ou=B,o=A", "ou=E,ou=C,ou=B,o=A", true}, 317 | } 318 | 319 | for i, tc := range testcases { 320 | a, err := ParseDN(tc.A) 321 | if err != nil { 322 | t.Errorf("%d: %v", i, err) 323 | continue 324 | } 325 | b, err := ParseDN(tc.B) 326 | if err != nil { 327 | t.Errorf("%d: %v", i, err) 328 | continue 329 | } 330 | if expected, actual := tc.Ancestor, a.AncestorOf(b); expected != actual { 331 | t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) 332 | continue 333 | } 334 | } 335 | } 336 | 337 | func BenchmarkParseSubject(b *testing.B) { 338 | for n := 0; n < b.N; n++ { 339 | _, err := ParseDN("DF=#6666666666665006838820013100000746939546349182108463491821809FBFFFFFFFFF") 340 | if err == nil { 341 | b.Fatal("expected error, but got none") 342 | } 343 | } 344 | } 345 | 346 | func TestMustKeepOrderInRawDerBytes(t *testing.T) { 347 | subject := "cn=foo-long.com,ou=FooLong,ou=Barq,ou=Baz,ou=Dept.,o=Corp.,c=US" 348 | rdnSeq, err := ParseDN(subject) 349 | if err != nil { 350 | t.Fatal(err) 351 | } 352 | 353 | expectedRdnSeq := &DN{ 354 | []*RelativeDN{ 355 | {[]*AttributeTypeAndValue{{Type: "cn", Value: "foo-long.com"}}}, 356 | {[]*AttributeTypeAndValue{{Type: "ou", Value: "FooLong"}}}, 357 | {[]*AttributeTypeAndValue{{Type: "ou", Value: "Barq"}}}, 358 | {[]*AttributeTypeAndValue{{Type: "ou", Value: "Baz"}}}, 359 | {[]*AttributeTypeAndValue{{Type: "ou", Value: "Dept."}}}, 360 | {[]*AttributeTypeAndValue{{Type: "o", Value: "Corp."}}}, 361 | {[]*AttributeTypeAndValue{{Type: "c", Value: "US"}}}, 362 | }, 363 | } 364 | 365 | assert.Equal(t, expectedRdnSeq, rdnSeq) 366 | assert.Equal(t, subject, rdnSeq.String()) 367 | } 368 | 369 | func TestRoundTripLiteralSubject(t *testing.T) { 370 | rdnSequences := map[string]string{ 371 | "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", 372 | "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", 373 | "cn=fo\x00o-long.com,ou=\x04FooLong": "cn=fo\\00o-long.com,ou=\\04FooLong", 374 | } 375 | 376 | for subjIn, subjOut := range rdnSequences { 377 | t.Logf("Testing subject: %s", subjIn) 378 | 379 | newRDNSeq, err := ParseDN(subjIn) 380 | if err != nil { 381 | t.Fatal(err) 382 | } 383 | 384 | assert.Equal(t, subjOut, newRDNSeq.String()) 385 | } 386 | } 387 | 388 | func TestDecodeString(t *testing.T) { 389 | successTestcases := map[string]string{ 390 | "foo-long.com": "foo-long.com", 391 | "foo-lon❤️\\,g.com": "foo-lon❤️,g.com", 392 | "fo\x00o-long.com": "fo\x00o-long.com", 393 | "fo\\00o-long.com": "fo\x00o-long.com", 394 | } 395 | 396 | for encoded, decoded := range successTestcases { 397 | t.Logf("Testing encoded string: %s", encoded) 398 | decodedString, err := decodeString(encoded) 399 | if err != nil { 400 | t.Fatal(err) 401 | } 402 | 403 | assert.Equal(t, decoded, decodedString) 404 | } 405 | 406 | errorTestcases := map[string]string{ 407 | "fo\\": "got corrupted escaped character: 'fo\\'", 408 | "fo\\0": "failed to decode escaped character: encoding/hex: invalid byte: 0", 409 | "fo\\UU️o-long.com": "failed to decode escaped character: encoding/hex: invalid byte: U+0055 'U'", 410 | "fo\\0❤️o-long.com": "failed to decode escaped character: invalid byte: 0❤", 411 | } 412 | 413 | for encoded, expectedError := range errorTestcases { 414 | t.Logf("Testing encoded string: %s", encoded) 415 | _, err := decodeString(encoded) 416 | assert.EqualError(t, err, expectedError) 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /v3/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package ldap provides basic LDAP v3 functionality. 3 | */ 4 | package ldap 5 | -------------------------------------------------------------------------------- /v3/error.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | ber "github.com/go-asn1-ber/asn1-ber" 8 | ) 9 | 10 | // LDAP Result Codes 11 | const ( 12 | LDAPResultSuccess = 0 13 | LDAPResultOperationsError = 1 14 | LDAPResultProtocolError = 2 15 | LDAPResultTimeLimitExceeded = 3 16 | LDAPResultSizeLimitExceeded = 4 17 | LDAPResultCompareFalse = 5 18 | LDAPResultCompareTrue = 6 19 | LDAPResultAuthMethodNotSupported = 7 20 | LDAPResultStrongAuthRequired = 8 21 | LDAPResultReferral = 10 22 | LDAPResultAdminLimitExceeded = 11 23 | LDAPResultUnavailableCriticalExtension = 12 24 | LDAPResultConfidentialityRequired = 13 25 | LDAPResultSaslBindInProgress = 14 26 | LDAPResultNoSuchAttribute = 16 27 | LDAPResultUndefinedAttributeType = 17 28 | LDAPResultInappropriateMatching = 18 29 | LDAPResultConstraintViolation = 19 30 | LDAPResultAttributeOrValueExists = 20 31 | LDAPResultInvalidAttributeSyntax = 21 32 | LDAPResultNoSuchObject = 32 33 | LDAPResultAliasProblem = 33 34 | LDAPResultInvalidDNSyntax = 34 35 | LDAPResultIsLeaf = 35 36 | LDAPResultAliasDereferencingProblem = 36 37 | LDAPResultInappropriateAuthentication = 48 38 | LDAPResultInvalidCredentials = 49 39 | LDAPResultInsufficientAccessRights = 50 40 | LDAPResultBusy = 51 41 | LDAPResultUnavailable = 52 42 | LDAPResultUnwillingToPerform = 53 43 | LDAPResultLoopDetect = 54 44 | LDAPResultSortControlMissing = 60 45 | LDAPResultOffsetRangeError = 61 46 | LDAPResultNamingViolation = 64 47 | LDAPResultObjectClassViolation = 65 48 | LDAPResultNotAllowedOnNonLeaf = 66 49 | LDAPResultNotAllowedOnRDN = 67 50 | LDAPResultEntryAlreadyExists = 68 51 | LDAPResultObjectClassModsProhibited = 69 52 | LDAPResultResultsTooLarge = 70 53 | LDAPResultAffectsMultipleDSAs = 71 54 | LDAPResultVirtualListViewErrorOrControlError = 76 55 | LDAPResultOther = 80 56 | LDAPResultServerDown = 81 57 | LDAPResultLocalError = 82 58 | LDAPResultEncodingError = 83 59 | LDAPResultDecodingError = 84 60 | LDAPResultTimeout = 85 61 | LDAPResultAuthUnknown = 86 62 | LDAPResultFilterError = 87 63 | LDAPResultUserCanceled = 88 64 | LDAPResultParamError = 89 65 | LDAPResultNoMemory = 90 66 | LDAPResultConnectError = 91 67 | LDAPResultNotSupported = 92 68 | LDAPResultControlNotFound = 93 69 | LDAPResultNoResultsReturned = 94 70 | LDAPResultMoreResultsToReturn = 95 71 | LDAPResultClientLoop = 96 72 | LDAPResultReferralLimitExceeded = 97 73 | LDAPResultInvalidResponse = 100 74 | LDAPResultAmbiguousResponse = 101 75 | LDAPResultTLSNotSupported = 112 76 | LDAPResultIntermediateResponse = 113 77 | LDAPResultUnknownType = 114 78 | LDAPResultCanceled = 118 79 | LDAPResultNoSuchOperation = 119 80 | LDAPResultTooLate = 120 81 | LDAPResultCannotCancel = 121 82 | LDAPResultAssertionFailed = 122 83 | LDAPResultAuthorizationDenied = 123 84 | LDAPResultSyncRefreshRequired = 4096 85 | 86 | ErrorNetwork = 200 87 | ErrorFilterCompile = 201 88 | ErrorFilterDecompile = 202 89 | ErrorDebugging = 203 90 | ErrorUnexpectedMessage = 204 91 | ErrorUnexpectedResponse = 205 92 | ErrorEmptyPassword = 206 93 | ) 94 | 95 | // LDAPResultCodeMap contains string descriptions for LDAP error codes 96 | var LDAPResultCodeMap = map[uint16]string{ 97 | LDAPResultSuccess: "Success", 98 | LDAPResultOperationsError: "Operations Error", 99 | LDAPResultProtocolError: "Protocol Error", 100 | LDAPResultTimeLimitExceeded: "Time Limit Exceeded", 101 | LDAPResultSizeLimitExceeded: "Size Limit Exceeded", 102 | LDAPResultCompareFalse: "Compare False", 103 | LDAPResultCompareTrue: "Compare True", 104 | LDAPResultAuthMethodNotSupported: "Auth Method Not Supported", 105 | LDAPResultStrongAuthRequired: "Strong Auth Required", 106 | LDAPResultReferral: "Referral", 107 | LDAPResultAdminLimitExceeded: "Admin Limit Exceeded", 108 | LDAPResultUnavailableCriticalExtension: "Unavailable Critical Extension", 109 | LDAPResultConfidentialityRequired: "Confidentiality Required", 110 | LDAPResultSaslBindInProgress: "Sasl Bind In Progress", 111 | LDAPResultNoSuchAttribute: "No Such Attribute", 112 | LDAPResultUndefinedAttributeType: "Undefined Attribute Type", 113 | LDAPResultInappropriateMatching: "Inappropriate Matching", 114 | LDAPResultConstraintViolation: "Constraint Violation", 115 | LDAPResultAttributeOrValueExists: "Attribute Or Value Exists", 116 | LDAPResultInvalidAttributeSyntax: "Invalid Attribute Syntax", 117 | LDAPResultNoSuchObject: "No Such Object", 118 | LDAPResultAliasProblem: "Alias Problem", 119 | LDAPResultInvalidDNSyntax: "Invalid DN Syntax", 120 | LDAPResultIsLeaf: "Is Leaf", 121 | LDAPResultAliasDereferencingProblem: "Alias Dereferencing Problem", 122 | LDAPResultInappropriateAuthentication: "Inappropriate Authentication", 123 | LDAPResultInvalidCredentials: "Invalid Credentials", 124 | LDAPResultInsufficientAccessRights: "Insufficient Access Rights", 125 | LDAPResultBusy: "Busy", 126 | LDAPResultUnavailable: "Unavailable", 127 | LDAPResultUnwillingToPerform: "Unwilling To Perform", 128 | LDAPResultLoopDetect: "Loop Detect", 129 | LDAPResultSortControlMissing: "Sort Control Missing", 130 | LDAPResultOffsetRangeError: "Result Offset Range Error", 131 | LDAPResultNamingViolation: "Naming Violation", 132 | LDAPResultObjectClassViolation: "Object Class Violation", 133 | LDAPResultResultsTooLarge: "Results Too Large", 134 | LDAPResultNotAllowedOnNonLeaf: "Not Allowed On Non Leaf", 135 | LDAPResultNotAllowedOnRDN: "Not Allowed On RDN", 136 | LDAPResultEntryAlreadyExists: "Entry Already Exists", 137 | LDAPResultObjectClassModsProhibited: "Object Class Mods Prohibited", 138 | LDAPResultAffectsMultipleDSAs: "Affects Multiple DSAs", 139 | LDAPResultVirtualListViewErrorOrControlError: "Failed because of a problem related to the virtual list view", 140 | LDAPResultOther: "Other", 141 | LDAPResultServerDown: "Cannot establish a connection", 142 | LDAPResultLocalError: "An error occurred", 143 | LDAPResultEncodingError: "LDAP encountered an error while encoding", 144 | LDAPResultDecodingError: "LDAP encountered an error while decoding", 145 | LDAPResultTimeout: "LDAP timeout while waiting for a response from the server", 146 | LDAPResultAuthUnknown: "The auth method requested in a bind request is unknown", 147 | LDAPResultFilterError: "An error occurred while encoding the given search filter", 148 | LDAPResultUserCanceled: "The user canceled the operation", 149 | LDAPResultParamError: "An invalid parameter was specified", 150 | LDAPResultNoMemory: "Out of memory error", 151 | LDAPResultConnectError: "A connection to the server could not be established", 152 | LDAPResultNotSupported: "An attempt has been made to use a feature not supported LDAP", 153 | LDAPResultControlNotFound: "The controls required to perform the requested operation were not found", 154 | LDAPResultNoResultsReturned: "No results were returned from the server", 155 | LDAPResultMoreResultsToReturn: "There are more results in the chain of results", 156 | LDAPResultClientLoop: "A loop has been detected. For example when following referrals", 157 | LDAPResultReferralLimitExceeded: "The referral hop limit has been exceeded", 158 | LDAPResultCanceled: "Operation was canceled", 159 | LDAPResultNoSuchOperation: "Server has no knowledge of the operation requested for cancellation", 160 | LDAPResultTooLate: "Too late to cancel the outstanding operation", 161 | LDAPResultCannotCancel: "The identified operation does not support cancellation or the cancel operation cannot be performed", 162 | LDAPResultAssertionFailed: "An assertion control given in the LDAP operation evaluated to false causing the operation to not be performed", 163 | LDAPResultSyncRefreshRequired: "Refresh Required", 164 | LDAPResultInvalidResponse: "Invalid Response", 165 | LDAPResultAmbiguousResponse: "Ambiguous Response", 166 | LDAPResultTLSNotSupported: "Tls Not Supported", 167 | LDAPResultIntermediateResponse: "Intermediate Response", 168 | LDAPResultUnknownType: "Unknown Type", 169 | LDAPResultAuthorizationDenied: "Authorization Denied", 170 | 171 | ErrorNetwork: "Network Error", 172 | ErrorFilterCompile: "Filter Compile Error", 173 | ErrorFilterDecompile: "Filter Decompile Error", 174 | ErrorDebugging: "Debugging Error", 175 | ErrorUnexpectedMessage: "Unexpected Message", 176 | ErrorUnexpectedResponse: "Unexpected Response", 177 | ErrorEmptyPassword: "Empty password not allowed by the client", 178 | } 179 | 180 | // Error holds LDAP error information 181 | type Error struct { 182 | // Err is the underlying error 183 | Err error 184 | // ResultCode is the LDAP error code 185 | ResultCode uint16 186 | // MatchedDN is the matchedDN returned if any 187 | MatchedDN string 188 | // Packet is the returned packet if any 189 | Packet *ber.Packet 190 | } 191 | 192 | func (e *Error) Error() string { 193 | return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error()) 194 | } 195 | 196 | func (e *Error) Unwrap() error { return e.Err } 197 | 198 | // GetLDAPError creates an Error out of a BER packet representing a LDAPResult 199 | // The return is an error object. It can be casted to a Error structure. 200 | // This function returns nil if resultCode in the LDAPResult sequence is success(0). 201 | func GetLDAPError(packet *ber.Packet) error { 202 | if packet == nil { 203 | return &Error{ResultCode: ErrorUnexpectedResponse, Err: fmt.Errorf("Empty packet")} 204 | } 205 | 206 | if len(packet.Children) >= 2 { 207 | response := packet.Children[1] 208 | if response == nil { 209 | return &Error{ResultCode: ErrorUnexpectedResponse, Err: fmt.Errorf("Empty response in packet"), Packet: packet} 210 | } 211 | if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 { 212 | if ber.Type(response.Children[0].Tag) == ber.Type(ber.TagInteger) || ber.Type(response.Children[0].Tag) == ber.Type(ber.TagEnumerated) { 213 | resultCode := uint16(response.Children[0].Value.(int64)) 214 | if resultCode == 0 { // No error 215 | return nil 216 | } 217 | 218 | if ber.Type(response.Children[1].Tag) == ber.Type(ber.TagOctetString) && 219 | ber.Type(response.Children[2].Tag) == ber.Type(ber.TagOctetString) { 220 | return &Error{ 221 | ResultCode: resultCode, 222 | MatchedDN: response.Children[1].Value.(string), 223 | Err: fmt.Errorf("%v", response.Children[2].Value), 224 | Packet: packet, 225 | } 226 | } 227 | } 228 | } 229 | } 230 | 231 | return &Error{ResultCode: ErrorNetwork, Err: fmt.Errorf("Invalid packet format"), Packet: packet} 232 | } 233 | 234 | // NewError creates an LDAP error with the given code and underlying error 235 | func NewError(resultCode uint16, err error) error { 236 | return &Error{ResultCode: resultCode, Err: err} 237 | } 238 | 239 | // IsErrorAnyOf returns true if the given error is an LDAP error with any one of the given result codes 240 | func IsErrorAnyOf(err error, codes ...uint16) bool { 241 | if err == nil { 242 | return false 243 | } 244 | 245 | var serverError *Error 246 | if !errors.As(err, &serverError) { 247 | return false 248 | } 249 | 250 | for _, code := range codes { 251 | if serverError.ResultCode == code { 252 | return true 253 | } 254 | } 255 | 256 | return false 257 | } 258 | 259 | // IsErrorWithCode returns true if the given error is an LDAP error with the given result code 260 | func IsErrorWithCode(err error, desiredResultCode uint16) bool { 261 | return IsErrorAnyOf(err, desiredResultCode) 262 | } 263 | -------------------------------------------------------------------------------- /v3/error_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | ber "github.com/go-asn1-ber/asn1-ber" 13 | ) 14 | 15 | // TestWrappedError tests that match the result code when an error is wrapped. 16 | func TestWrappedError(t *testing.T) { 17 | resultCodes := []uint16{ 18 | LDAPResultProtocolError, 19 | LDAPResultBusy, 20 | ErrorNetwork, 21 | } 22 | 23 | tests := []struct { 24 | name string 25 | err error 26 | codes []uint16 27 | expected bool 28 | }{ 29 | // success 30 | { 31 | name: "a normal error", 32 | err: &Error{ 33 | ResultCode: ErrorNetwork, 34 | }, 35 | codes: resultCodes, 36 | expected: true, 37 | }, 38 | 39 | { 40 | name: "a wrapped error", 41 | err: fmt.Errorf("wrap: %w", &Error{ 42 | ResultCode: LDAPResultBusy, 43 | }), 44 | codes: resultCodes, 45 | expected: true, 46 | }, 47 | 48 | { 49 | name: "multiple wrapped error", 50 | err: fmt.Errorf("second: %w", 51 | fmt.Errorf("first: %w", 52 | &Error{ 53 | ResultCode: LDAPResultProtocolError, 54 | }, 55 | ), 56 | ), 57 | codes: resultCodes, 58 | expected: true, 59 | }, 60 | 61 | // failure 62 | { 63 | name: "not match a normal error", 64 | err: &Error{ 65 | ResultCode: LDAPResultSuccess, 66 | }, 67 | codes: resultCodes, 68 | expected: false, 69 | }, 70 | 71 | { 72 | name: "not match a wrapped error", 73 | err: fmt.Errorf("wrap: %w", &Error{ 74 | ResultCode: LDAPResultNoSuchObject, 75 | }), 76 | codes: resultCodes, 77 | expected: false, 78 | }, 79 | } 80 | 81 | for _, tt := range tests { 82 | tt := tt 83 | t.Run(tt.name, func(t *testing.T) { 84 | t.Parallel() 85 | actual := IsErrorAnyOf(tt.err, tt.codes...) 86 | if tt.expected != actual { 87 | t.Errorf("expected %t, but got %t", tt.expected, actual) 88 | } 89 | }) 90 | } 91 | } 92 | 93 | // TestNilPacket tests that nil packets don't cause a panic. 94 | func TestNilPacket(t *testing.T) { 95 | // Test for nil packet 96 | err := GetLDAPError(nil) 97 | if !IsErrorWithCode(err, ErrorUnexpectedResponse) { 98 | t.Errorf("Should have an 'ErrorUnexpectedResponse' error in nil packets, got: %v", err) 99 | } 100 | 101 | // Test for nil result 102 | kids := []*ber.Packet{ 103 | {}, // Unused 104 | nil, // Can't be nil 105 | } 106 | pack := &ber.Packet{Children: kids} 107 | err = GetLDAPError(pack) 108 | 109 | if !IsErrorWithCode(err, ErrorUnexpectedResponse) { 110 | t.Errorf("Should have an 'ErrorUnexpectedResponse' error in nil packets, got: %v", err) 111 | } 112 | } 113 | 114 | // TestConnReadErr tests that an unexpected error reading from underlying 115 | // connection bubbles up to the goroutine which makes a request. 116 | func TestConnReadErr(t *testing.T) { 117 | conn := &signalErrConn{ 118 | signals: make(chan error), 119 | } 120 | 121 | ldapConn := NewConn(conn, false) 122 | ldapConn.Start() 123 | 124 | // Make a dummy search request. 125 | searchReq := NewSearchRequest("dc=example,dc=com", ScopeWholeSubtree, DerefAlways, 0, 0, false, "(objectClass=*)", nil, nil) 126 | 127 | expectedError := errors.New("this is the error you are looking for") 128 | 129 | // Send the signal after a short amount of time. 130 | time.AfterFunc(10*time.Millisecond, func() { conn.signals <- expectedError }) 131 | 132 | // This should block until the underlying conn gets the error signal 133 | // which should bubble up through the reader() goroutine, close the 134 | // connection, and 135 | _, err := ldapConn.Search(searchReq) 136 | if err == nil || !strings.Contains(err.Error(), expectedError.Error()) { 137 | t.Errorf("not the expected error: %s", err) 138 | } 139 | } 140 | 141 | // TestGetLDAPError tests parsing of result with a error response. 142 | func TestGetLDAPError(t *testing.T) { 143 | diagnosticMessage := "Detailed error message" 144 | bindResponse := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindResponse, nil, "Bind Response") 145 | bindResponse.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(LDAPResultInvalidCredentials), "resultCode")) 146 | bindResponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "dc=example,dc=org", "matchedDN")) 147 | bindResponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, diagnosticMessage, "diagnosticMessage")) 148 | packet := ber.NewSequence("LDAPMessage") 149 | packet.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(0), "messageID")) 150 | packet.AppendChild(bindResponse) 151 | err := GetLDAPError(packet) 152 | if err == nil { 153 | t.Errorf("Did not get error response") 154 | } 155 | 156 | ldapError := err.(*Error) 157 | if ldapError.ResultCode != LDAPResultInvalidCredentials { 158 | t.Errorf("Got incorrect error code in LDAP error; got %v, expected %v", ldapError.ResultCode, LDAPResultInvalidCredentials) 159 | } 160 | if ldapError.Err.Error() != diagnosticMessage { 161 | t.Errorf("Got incorrect error message in LDAP error; got %v, expected %v", ldapError.Err.Error(), diagnosticMessage) 162 | } 163 | } 164 | 165 | // TestGetLDAPErrorInvalidResponse tests that responses with an unexpected ordering or combination of children 166 | // don't cause a panic. 167 | func TestGetLDAPErrorInvalidResponse(t *testing.T) { 168 | bindResponse := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindResponse, nil, "Bind Response") 169 | bindResponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "dc=example,dc=org", "matchedDN")) 170 | bindResponse.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(LDAPResultInvalidCredentials), "resultCode")) 171 | bindResponse.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(LDAPResultInvalidCredentials), "resultCode")) 172 | packet := ber.NewSequence("LDAPMessage") 173 | packet.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(0), "messageID")) 174 | packet.AppendChild(bindResponse) 175 | err := GetLDAPError(packet) 176 | if err == nil { 177 | t.Errorf("Did not get error response") 178 | } 179 | 180 | ldapError := err.(*Error) 181 | if ldapError.ResultCode != ErrorNetwork { 182 | t.Errorf("Got incorrect error code in LDAP error; got %v, expected %v", ldapError.ResultCode, ErrorNetwork) 183 | } 184 | } 185 | 186 | func TestErrorIs(t *testing.T) { 187 | err := NewError(ErrorNetwork, io.EOF) 188 | if !errors.Is(err, io.EOF) { 189 | t.Errorf("Expected an io.EOF error: %v", err) 190 | } 191 | } 192 | 193 | func TestErrorAs(t *testing.T) { 194 | var netErr net.InvalidAddrError = "invalid addr" 195 | err := NewError(ErrorNetwork, netErr) 196 | 197 | var target net.InvalidAddrError 198 | ok := errors.As(err, &target) 199 | if !ok { 200 | t.Error("Expected an InvalidAddrError") 201 | } 202 | } 203 | 204 | // TestGetLDAPErrorSuccess tests parsing of a result with no error (resultCode == 0). 205 | func TestGetLDAPErrorSuccess(t *testing.T) { 206 | bindResponse := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindResponse, nil, "Bind Response") 207 | bindResponse.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(0), "resultCode")) 208 | bindResponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN")) 209 | bindResponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "diagnosticMessage")) 210 | packet := ber.NewSequence("LDAPMessage") 211 | packet.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(0), "messageID")) 212 | packet.AppendChild(bindResponse) 213 | err := GetLDAPError(packet) 214 | if err != nil { 215 | t.Errorf("Successful responses should not produce an error, but got: %v", err) 216 | } 217 | } 218 | 219 | // signalErrConn is a helpful type used with TestConnReadErr. It implements the 220 | // net.Conn interface to be used as a connection for the test. Most methods are 221 | // no-ops but the Read() method blocks until it receives a signal which it 222 | // returns as an error. 223 | type signalErrConn struct { 224 | signals chan error 225 | } 226 | 227 | // Read blocks until an error is sent on the internal signals channel. That 228 | // error is returned. 229 | func (c *signalErrConn) Read(b []byte) (n int, err error) { 230 | return 0, <-c.signals 231 | } 232 | 233 | func (c *signalErrConn) Write(b []byte) (n int, err error) { 234 | return len(b), nil 235 | } 236 | 237 | func (c *signalErrConn) Close() error { 238 | close(c.signals) 239 | return nil 240 | } 241 | 242 | func (c *signalErrConn) LocalAddr() net.Addr { 243 | return (*net.TCPAddr)(nil) 244 | } 245 | 246 | func (c *signalErrConn) RemoteAddr() net.Addr { 247 | return (*net.TCPAddr)(nil) 248 | } 249 | 250 | func (c *signalErrConn) SetDeadline(t time.Time) error { 251 | return nil 252 | } 253 | 254 | func (c *signalErrConn) SetReadDeadline(t time.Time) error { 255 | return nil 256 | } 257 | 258 | func (c *signalErrConn) SetWriteDeadline(t time.Time) error { 259 | return nil 260 | } 261 | -------------------------------------------------------------------------------- /v3/examples_moddn_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | // This example shows how to rename an entry without moving it 8 | func ExampleConn_ModifyDN_renameNoMove() { 9 | conn, err := DialURL("ldap://ldap.example.org:389") 10 | if err != nil { 11 | log.Fatalf("Failed to connect: %s\n", err) 12 | } 13 | defer conn.Close() 14 | 15 | _, err = conn.SimpleBind(&SimpleBindRequest{ 16 | Username: "uid=someone,ou=people,dc=example,dc=org", 17 | Password: "MySecretPass", 18 | }) 19 | if err != nil { 20 | log.Fatalf("Failed to bind: %s\n", err) 21 | } 22 | // just rename to uid=new,ou=people,dc=example,dc=org: 23 | req := NewModifyDNRequest("uid=user,ou=people,dc=example,dc=org", "uid=new", true, "") 24 | if err = conn.ModifyDN(req); err != nil { 25 | log.Fatalf("Failed to call ModifyDN(): %s\n", err) 26 | } 27 | } 28 | 29 | // This example shows how to rename an entry and moving it to a new base 30 | func ExampleConn_ModifyDN_renameAndMove() { 31 | conn, err := DialURL("ldap://ldap.example.org:389") 32 | if err != nil { 33 | log.Fatalf("Failed to connect: %s\n", err) 34 | } 35 | defer conn.Close() 36 | 37 | _, err = conn.SimpleBind(&SimpleBindRequest{ 38 | Username: "uid=someone,ou=people,dc=example,dc=org", 39 | Password: "MySecretPass", 40 | }) 41 | if err != nil { 42 | log.Fatalf("Failed to bind: %s\n", err) 43 | } 44 | // rename to uid=new,ou=people,dc=example,dc=org and move to ou=users,dc=example,dc=org -> 45 | // uid=new,ou=users,dc=example,dc=org 46 | req := NewModifyDNRequest("uid=user,ou=people,dc=example,dc=org", "uid=new", true, "ou=users,dc=example,dc=org") 47 | 48 | if err = conn.ModifyDN(req); err != nil { 49 | log.Fatalf("Failed to call ModifyDN(): %s\n", err) 50 | } 51 | } 52 | 53 | // This example shows how to move an entry to a new base without renaming the RDN 54 | func ExampleConn_ModifyDN_moveOnly() { 55 | conn, err := DialURL("ldap://ldap.example.org:389") 56 | if err != nil { 57 | log.Fatalf("Failed to connect: %s\n", err) 58 | } 59 | defer conn.Close() 60 | 61 | _, err = conn.SimpleBind(&SimpleBindRequest{ 62 | Username: "uid=someone,ou=people,dc=example,dc=org", 63 | Password: "MySecretPass", 64 | }) 65 | if err != nil { 66 | log.Fatalf("Failed to bind: %s\n", err) 67 | } 68 | // move to ou=users,dc=example,dc=org -> uid=user,ou=users,dc=example,dc=org 69 | req := NewModifyDNRequest("uid=user,ou=people,dc=example,dc=org", "uid=user", true, "ou=users,dc=example,dc=org") 70 | if err = conn.ModifyDN(req); err != nil { 71 | log.Fatalf("Failed to call ModifyDN(): %s\n", err) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /v3/examples_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "time" 11 | ) 12 | 13 | // This example demonstrates how to bind a connection to an ldap user 14 | // allowing access to restricted attributes that user has access to 15 | func ExampleConn_Bind() { 16 | l, err := DialURL("ldap://ldap.example.com:389") 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | defer l.Close() 21 | 22 | err = l.Bind("cn=read-only-admin,dc=example,dc=com", "password") 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | } 27 | 28 | // This example demonstrates how to use the search interface 29 | func ExampleConn_Search() { 30 | l, err := DialURL("ldap://ldap.example.com:389") 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | defer l.Close() 35 | 36 | searchRequest := NewSearchRequest( 37 | "dc=example,dc=com", // The base dn to search 38 | ScopeWholeSubtree, NeverDerefAliases, 0, 0, false, 39 | "(&(objectClass=organizationalPerson))", // The filter to apply 40 | []string{"dn", "cn"}, // A list attributes to retrieve 41 | nil, 42 | ) 43 | 44 | sr, err := l.Search(searchRequest) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | for _, entry := range sr.Entries { 50 | fmt.Printf("%s: %v\n", entry.DN, entry.GetAttributeValue("cn")) 51 | } 52 | } 53 | 54 | // This example demonstrates how to search asynchronously 55 | func ExampleConn_SearchAsync() { 56 | l, err := DialURL(fmt.Sprintf("%s:%d", "ldap.example.com", 389)) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | defer l.Close() 61 | 62 | searchRequest := NewSearchRequest( 63 | "dc=example,dc=com", // The base dn to search 64 | ScopeWholeSubtree, NeverDerefAliases, 0, 0, false, 65 | "(&(objectClass=organizationalPerson))", // The filter to apply 66 | []string{"dn", "cn"}, // A list attributes to retrieve 67 | nil, 68 | ) 69 | 70 | ctx, cancel := context.WithCancel(context.Background()) 71 | defer cancel() 72 | 73 | r := l.SearchAsync(ctx, searchRequest, 64) 74 | for r.Next() { 75 | entry := r.Entry() 76 | fmt.Printf("%s has DN %s\n", entry.GetAttributeValue("cn"), entry.DN) 77 | } 78 | if err := r.Err(); err != nil { 79 | log.Fatal(err) 80 | } 81 | } 82 | 83 | // This example demonstrates how to do syncrepl (persistent search) 84 | func ExampleConn_Syncrepl() { 85 | l, err := DialURL(fmt.Sprintf("%s:%d", "ldap.example.com", 389)) 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | defer l.Close() 90 | 91 | searchRequest := NewSearchRequest( 92 | "dc=example,dc=com", // The base dn to search 93 | ScopeWholeSubtree, NeverDerefAliases, 0, 0, false, 94 | "(&(objectClass=organizationalPerson))", // The filter to apply 95 | []string{"dn", "cn"}, // A list attributes to retrieve 96 | nil, 97 | ) 98 | 99 | ctx, cancel := context.WithCancel(context.Background()) 100 | defer cancel() 101 | 102 | mode := SyncRequestModeRefreshAndPersist 103 | var cookie []byte = nil 104 | r := l.Syncrepl(ctx, searchRequest, 64, mode, cookie, false) 105 | for r.Next() { 106 | entry := r.Entry() 107 | if entry != nil { 108 | fmt.Printf("%s has DN %s\n", entry.GetAttributeValue("cn"), entry.DN) 109 | } 110 | controls := r.Controls() 111 | if len(controls) != 0 { 112 | fmt.Printf("%s", controls) 113 | } 114 | } 115 | if err := r.Err(); err != nil { 116 | log.Fatal(err) 117 | } 118 | } 119 | 120 | // This example demonstrates how to start a TLS connection 121 | func ExampleConn_StartTLS() { 122 | l, err := DialURL("ldap://ldap.example.com:389") 123 | if err != nil { 124 | log.Fatal(err) 125 | } 126 | defer l.Close() 127 | 128 | // Reconnect with TLS 129 | err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) 130 | if err != nil { 131 | log.Fatal(err) 132 | } 133 | 134 | // Operations via l are now encrypted 135 | } 136 | 137 | // This example demonstrates how to compare an attribute with a value 138 | func ExampleConn_Compare() { 139 | l, err := DialURL("ldap://ldap.example.com:389") 140 | if err != nil { 141 | log.Fatal(err) 142 | } 143 | defer l.Close() 144 | 145 | matched, err := l.Compare("cn=user,dc=example,dc=com", "uid", "someuserid") 146 | if err != nil { 147 | log.Fatal(err) 148 | } 149 | 150 | fmt.Println(matched) 151 | } 152 | 153 | func ExampleConn_PasswordModify_admin() { 154 | l, err := DialURL("ldap://ldap.example.com:389") 155 | if err != nil { 156 | log.Fatal(err) 157 | } 158 | defer l.Close() 159 | 160 | err = l.Bind("cn=admin,dc=example,dc=com", "password") 161 | if err != nil { 162 | log.Fatal(err) 163 | } 164 | 165 | passwordModifyRequest := NewPasswordModifyRequest("cn=user,dc=example,dc=com", "", "NewPassword") 166 | _, err = l.PasswordModify(passwordModifyRequest) 167 | 168 | if err != nil { 169 | log.Fatalf("Password could not be changed: %s", err.Error()) 170 | } 171 | } 172 | 173 | func ExampleConn_PasswordModify_generatedPassword() { 174 | l, err := DialURL("ldap://ldap.example.com:389") 175 | if err != nil { 176 | log.Fatal(err) 177 | } 178 | defer l.Close() 179 | 180 | err = l.Bind("cn=user,dc=example,dc=com", "password") 181 | if err != nil { 182 | log.Fatal(err) 183 | } 184 | 185 | passwordModifyRequest := NewPasswordModifyRequest("", "OldPassword", "") 186 | passwordModifyResponse, err := l.PasswordModify(passwordModifyRequest) 187 | if err != nil { 188 | log.Fatalf("Password could not be changed: %s", err.Error()) 189 | } 190 | 191 | generatedPassword := passwordModifyResponse.GeneratedPassword 192 | log.Printf("Generated password: %s\n", generatedPassword) 193 | } 194 | 195 | func ExampleConn_PasswordModify_setNewPassword() { 196 | l, err := DialURL("ldap://ldap.example.com:389") 197 | if err != nil { 198 | log.Fatal(err) 199 | } 200 | defer l.Close() 201 | 202 | err = l.Bind("cn=user,dc=example,dc=com", "password") 203 | if err != nil { 204 | log.Fatal(err) 205 | } 206 | 207 | passwordModifyRequest := NewPasswordModifyRequest("", "OldPassword", "NewPassword") 208 | _, err = l.PasswordModify(passwordModifyRequest) 209 | 210 | if err != nil { 211 | log.Fatalf("Password could not be changed: %s", err.Error()) 212 | } 213 | } 214 | 215 | func ExampleConn_Modify() { 216 | l, err := DialURL("ldap://ldap.example.com:389") 217 | if err != nil { 218 | log.Fatal(err) 219 | } 220 | defer l.Close() 221 | 222 | // Add a description, and replace the mail attributes 223 | modify := NewModifyRequest("cn=user,dc=example,dc=com", nil) 224 | modify.Add("description", []string{"An example user"}) 225 | modify.Replace("mail", []string{"user@example.org"}) 226 | 227 | err = l.Modify(modify) 228 | if err != nil { 229 | log.Fatal(err) 230 | } 231 | } 232 | 233 | // Example_userAuthentication shows how a typical application can verify a login attempt 234 | // Refer to https://github.com/go-ldap/ldap/issues/93 for issues revolving around unauthenticated binds, with zero length passwords 235 | func Example_userAuthentication() { 236 | // The username and password we want to check 237 | username := "someuser" 238 | password := "userpassword" 239 | 240 | bindusername := "readonly" 241 | bindpassword := "password" 242 | 243 | l, err := DialURL("ldap://ldap.example.com:389") 244 | if err != nil { 245 | log.Fatal(err) 246 | } 247 | defer l.Close() 248 | 249 | // Reconnect with TLS 250 | err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) 251 | if err != nil { 252 | log.Fatal(err) 253 | } 254 | 255 | // First bind with a read only user 256 | err = l.Bind(bindusername, bindpassword) 257 | if err != nil { 258 | log.Fatal(err) 259 | } 260 | 261 | // Search for the given username 262 | searchRequest := NewSearchRequest( 263 | "dc=example,dc=com", 264 | ScopeWholeSubtree, NeverDerefAliases, 0, 0, false, 265 | fmt.Sprintf("(&(objectClass=organizationalPerson)(uid=%s))", EscapeFilter(username)), 266 | []string{"dn"}, 267 | nil, 268 | ) 269 | 270 | sr, err := l.Search(searchRequest) 271 | if err != nil { 272 | log.Fatal(err) 273 | } 274 | 275 | if len(sr.Entries) != 1 { 276 | log.Fatal("User does not exist or too many entries returned") 277 | } 278 | 279 | userdn := sr.Entries[0].DN 280 | 281 | // Bind as the user to verify their password 282 | err = l.Bind(userdn, password) 283 | if err != nil { 284 | log.Fatal(err) 285 | } 286 | 287 | // Rebind as the read only user for any further queries 288 | err = l.Bind(bindusername, bindpassword) 289 | if err != nil { 290 | log.Fatal(err) 291 | } 292 | } 293 | 294 | func Example_beherappolicy() { 295 | l, err := DialURL("ldap://ldap.example.com:389") 296 | if err != nil { 297 | log.Fatal(err) 298 | } 299 | defer l.Close() 300 | 301 | controls := []Control{} 302 | controls = append(controls, NewControlBeheraPasswordPolicy()) 303 | bindRequest := NewSimpleBindRequest("cn=admin,dc=example,dc=com", "password", controls) 304 | 305 | r, err := l.SimpleBind(bindRequest) 306 | ppolicyControl := FindControl(r.Controls, ControlTypeBeheraPasswordPolicy) 307 | 308 | var ppolicy *ControlBeheraPasswordPolicy 309 | if ppolicyControl != nil { 310 | ppolicy = ppolicyControl.(*ControlBeheraPasswordPolicy) 311 | } else { 312 | log.Printf("ppolicyControl response not available.\n") 313 | } 314 | if err != nil { 315 | errStr := "ERROR: Cannot bind: " + err.Error() 316 | if ppolicy != nil && ppolicy.Error >= 0 { 317 | errStr += ":" + ppolicy.ErrorString 318 | } 319 | log.Print(errStr) 320 | } else { 321 | logStr := "Login Ok" 322 | if ppolicy != nil { 323 | if ppolicy.Expire >= 0 { 324 | logStr += fmt.Sprintf(". Password expires in %d seconds\n", ppolicy.Expire) 325 | } else if ppolicy.Grace >= 0 { 326 | logStr += fmt.Sprintf(". Password expired, %d grace logins remain\n", ppolicy.Grace) 327 | } 328 | } 329 | log.Print(logStr) 330 | } 331 | } 332 | 333 | func Example_vchuppolicy() { 334 | l, err := DialURL("ldap://ldap.example.com:389") 335 | if err != nil { 336 | log.Fatal(err) 337 | } 338 | defer l.Close() 339 | l.Debug = true 340 | 341 | bindRequest := NewSimpleBindRequest("cn=admin,dc=example,dc=com", "password", nil) 342 | 343 | r, err := l.SimpleBind(bindRequest) 344 | 345 | passwordMustChangeControl := FindControl(r.Controls, ControlTypeVChuPasswordMustChange) 346 | var passwordMustChange *ControlVChuPasswordMustChange 347 | if passwordMustChangeControl != nil { 348 | passwordMustChange = passwordMustChangeControl.(*ControlVChuPasswordMustChange) 349 | } 350 | 351 | if passwordMustChange != nil && passwordMustChange.MustChange { 352 | log.Printf("Password Must be changed.\n") 353 | } 354 | 355 | passwordWarningControl := FindControl(r.Controls, ControlTypeVChuPasswordWarning) 356 | 357 | var passwordWarning *ControlVChuPasswordWarning 358 | if passwordWarningControl != nil { 359 | passwordWarning = passwordWarningControl.(*ControlVChuPasswordWarning) 360 | } else { 361 | log.Printf("ppolicyControl response not available.\n") 362 | } 363 | if err != nil { 364 | log.Print("ERROR: Cannot bind: " + err.Error()) 365 | } else { 366 | logStr := "Login Ok" 367 | if passwordWarning != nil { 368 | if passwordWarning.Expire >= 0 { 369 | logStr += fmt.Sprintf(". Password expires in %d seconds\n", passwordWarning.Expire) 370 | } 371 | } 372 | log.Print(logStr) 373 | } 374 | } 375 | 376 | // This example demonstrates how to use ControlPaging to manually execute a 377 | // paginated search request instead of using SearchWithPaging. 378 | func ExampleControlPaging_manualPaging() { 379 | conn, err := DialURL("ldap://ldap.example.com:389") 380 | if err != nil { 381 | log.Fatal(err) 382 | } 383 | defer conn.Close() 384 | 385 | var pageSize uint32 = 32 386 | searchBase := "dc=example,dc=com" 387 | filter := "(objectClass=group)" 388 | pagingControl := NewControlPaging(pageSize) 389 | attributes := []string{} 390 | controls := []Control{pagingControl} 391 | 392 | for { 393 | request := NewSearchRequest(searchBase, ScopeWholeSubtree, DerefAlways, 0, 0, false, filter, attributes, controls) 394 | response, err := conn.Search(request) 395 | if err != nil { 396 | log.Fatalf("Failed to execute search request: %s", err.Error()) 397 | } 398 | 399 | // [do something with the response entries] 400 | 401 | // In order to prepare the next request, we check if the response 402 | // contains another ControlPaging object and a not-empty cookie and 403 | // copy that cookie into our pagingControl object: 404 | updatedControl := FindControl(response.Controls, ControlTypePaging) 405 | if ctrl, ok := updatedControl.(*ControlPaging); ctrl != nil && ok && len(ctrl.Cookie) != 0 { 406 | pagingControl.SetCookie(ctrl.Cookie) 407 | continue 408 | } 409 | // If no new paging information is available or the cookie is empty, we 410 | // are done with the pagination. 411 | break 412 | } 413 | } 414 | 415 | // This example demonstrates how to use DirSync to manually execute a 416 | // DirSync search request 417 | func ExampleConn_DirSync() { 418 | conn, err := DialURL("ldap://ad.example.org:389") 419 | if err != nil { 420 | log.Fatalf("Failed to connect: %s\n", err) 421 | } 422 | defer conn.Close() 423 | 424 | _, err = conn.SimpleBind(&SimpleBindRequest{ 425 | Username: "cn=Some User,ou=people,dc=example,dc=org", 426 | Password: "MySecretPass", 427 | }) 428 | if err != nil { 429 | log.Fatalf("failed to bind: %s", err) 430 | } 431 | 432 | req := &SearchRequest{ 433 | BaseDN: `DC=example,DC=org`, 434 | Filter: `(&(objectClass=person)(!(objectClass=computer)))`, 435 | Attributes: []string{"*"}, 436 | Scope: ScopeWholeSubtree, 437 | } 438 | // This is the initial sync with all entries matching the filter 439 | doMore := true 440 | var cookie []byte 441 | for doMore { 442 | res, err := conn.DirSync(req, DirSyncObjectSecurity, 1000, cookie) 443 | if err != nil { 444 | log.Fatalf("failed to search: %s", err) 445 | } 446 | for _, entry := range res.Entries { 447 | entry.Print() 448 | } 449 | ctrl := FindControl(res.Controls, ControlTypeDirSync) 450 | if ctrl == nil || ctrl.(*ControlDirSync).Flags == 0 { 451 | doMore = false 452 | } 453 | cookie = ctrl.(*ControlDirSync).Cookie 454 | } 455 | // We're done with the initial sync. Now pull every 15 seconds for the 456 | // updated entries - note that you get just the changes, not a full entry. 457 | for { 458 | res, err := conn.DirSync(req, DirSyncObjectSecurity, 1000, cookie) 459 | if err != nil { 460 | log.Fatalf("failed to search: %s", err) 461 | } 462 | for _, entry := range res.Entries { 463 | entry.Print() 464 | } 465 | time.Sleep(15 * time.Second) 466 | } 467 | } 468 | 469 | // This example demonstrates how to use DirSync search asynchronously 470 | func ExampleConn_DirSyncAsync() { 471 | conn, err := DialURL("ldap://ad.example.org:389") 472 | if err != nil { 473 | log.Fatalf("Failed to connect: %s\n", err) 474 | } 475 | defer conn.Close() 476 | 477 | _, err = conn.SimpleBind(&SimpleBindRequest{ 478 | Username: "cn=Some User,ou=people,dc=example,dc=org", 479 | Password: "MySecretPass", 480 | }) 481 | if err != nil { 482 | log.Fatalf("failed to bind: %s", err) 483 | } 484 | 485 | req := &SearchRequest{ 486 | BaseDN: `DC=example,DC=org`, 487 | Filter: `(&(objectClass=person)(!(objectClass=computer)))`, 488 | Attributes: []string{"*"}, 489 | Scope: ScopeWholeSubtree, 490 | } 491 | 492 | ctx, cancel := context.WithCancel(context.Background()) 493 | defer cancel() 494 | 495 | var cookie []byte = nil 496 | r := conn.DirSyncAsync(ctx, req, 64, DirSyncObjectSecurity, 1000, cookie) 497 | for r.Next() { 498 | entry := r.Entry() 499 | if entry != nil { 500 | entry.Print() 501 | } 502 | controls := r.Controls() 503 | if len(controls) != 0 { 504 | fmt.Printf("%s", controls) 505 | } 506 | } 507 | if err := r.Err(); err != nil { 508 | log.Fatal(err) 509 | } 510 | } 511 | 512 | // This example demonstrates how to use EXTERNAL SASL with TLS client certificates. 513 | func ExampleConn_ExternalBind() { 514 | ldapCert := "/path/to/cert.pem" 515 | ldapKey := "/path/to/key.pem" 516 | ldapCAchain := "/path/to/ca_chain.pem" 517 | 518 | // Load client cert and key 519 | cert, err := tls.LoadX509KeyPair(ldapCert, ldapKey) 520 | if err != nil { 521 | log.Fatal(err) 522 | } 523 | 524 | // Load CA chain 525 | caCert, err := ioutil.ReadFile(ldapCAchain) 526 | if err != nil { 527 | log.Fatal(err) 528 | } 529 | caCertPool := x509.NewCertPool() 530 | caCertPool.AppendCertsFromPEM(caCert) 531 | 532 | // Setup TLS with ldap client cert 533 | tlsConfig := &tls.Config{ 534 | Certificates: []tls.Certificate{cert}, 535 | RootCAs: caCertPool, 536 | InsecureSkipVerify: true, 537 | } 538 | 539 | // connect to ldap server 540 | l, err := DialURL("ldap://ldap.example.com:389") 541 | if err != nil { 542 | log.Fatal(err) 543 | } 544 | defer l.Close() 545 | 546 | // reconnect using tls 547 | err = l.StartTLS(tlsConfig) 548 | if err != nil { 549 | log.Fatal(err) 550 | } 551 | 552 | // sasl external bind 553 | err = l.ExternalBind() 554 | if err != nil { 555 | log.Fatal(err) 556 | } 557 | 558 | // Conduct ldap queries 559 | } 560 | 561 | // ExampleConn_WhoAmI demonstrates how to run a whoami request according to https://tools.ietf.org/html/rfc4532 562 | func ExampleConn_WhoAmI() { 563 | conn, err := DialURL("ldap.example.org:389") 564 | if err != nil { 565 | log.Fatalf("Failed to connect: %s\n", err) 566 | } 567 | 568 | _, err = conn.SimpleBind(&SimpleBindRequest{ 569 | Username: "uid=someone,ou=people,dc=example,dc=org", 570 | Password: "MySecretPass", 571 | }) 572 | if err != nil { 573 | log.Fatalf("Failed to bind: %s\n", err) 574 | } 575 | 576 | res, err := conn.WhoAmI(nil) 577 | if err != nil { 578 | log.Fatalf("Failed to call WhoAmI(): %s\n", err) 579 | } 580 | fmt.Printf("I am: %s\n", res.AuthzID) 581 | } 582 | -------------------------------------------------------------------------------- /v3/examples_windows_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package ldap 5 | 6 | import ( 7 | "log" 8 | 9 | "github.com/go-ldap/ldap/v3/gssapi" 10 | ) 11 | 12 | // This example demonstrates passwordless bind using the current process' user 13 | // credentials on Windows (SASL GSSAPI mechanism bind with SSPI client). 14 | func ExampleConn_GSSAPIBind() { 15 | // Windows only: Create a GSSAPIClient using Windows built-in SSPI lib 16 | // (secur32.dll). 17 | // This will use the credentials of the current process' user. 18 | sspiClient, err := gssapi.NewSSPIClient() 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | defer sspiClient.Close() 23 | 24 | l, err := DialURL("ldap://ldap.example.com:389") 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | defer l.Close() 29 | 30 | // Bind using supplied GSSAPIClient implementation 31 | err = l.GSSAPIBind(sspiClient, "ldap/ldap.example.com", "") 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /v3/extended.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | ber "github.com/go-asn1-ber/asn1-ber" 6 | ) 7 | 8 | // ExtendedRequest represents an extended request to send to the server 9 | // See: https://www.rfc-editor.org/rfc/rfc4511#section-4.12 10 | type ExtendedRequest struct { 11 | // ExtendedRequest ::= [APPLICATION 23] SEQUENCE { 12 | // requestName [0] LDAPOID, 13 | // requestValue [1] OCTET STRING OPTIONAL } 14 | 15 | Name string 16 | Value *ber.Packet 17 | Controls []Control 18 | } 19 | 20 | // NewExtendedRequest returns a new ExtendedRequest. The value can be 21 | // nil depending on the type of request 22 | func NewExtendedRequest(name string, value *ber.Packet) *ExtendedRequest { 23 | return &ExtendedRequest{ 24 | Name: name, 25 | Value: value, 26 | } 27 | } 28 | 29 | func (er ExtendedRequest) appendTo(envelope *ber.Packet) error { 30 | pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Extended Request") 31 | pkt.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, ber.TagEOC, er.Name, "Extended Request Name")) 32 | if er.Value != nil { 33 | pkt.AppendChild(er.Value) 34 | } 35 | envelope.AppendChild(pkt) 36 | if len(er.Controls) > 0 { 37 | envelope.AppendChild(encodeControls(er.Controls)) 38 | } 39 | return nil 40 | } 41 | 42 | // ExtendedResponse represents the response from the directory server 43 | // after sending an extended request 44 | // See: https://www.rfc-editor.org/rfc/rfc4511#section-4.12 45 | type ExtendedResponse struct { 46 | // ExtendedResponse ::= [APPLICATION 24] SEQUENCE { 47 | // COMPONENTS OF LDAPResult, 48 | // responseName [10] LDAPOID OPTIONAL, 49 | // responseValue [11] OCTET STRING OPTIONAL } 50 | 51 | Name string 52 | Value *ber.Packet 53 | Controls []Control 54 | } 55 | 56 | // Extended performs an extended request. The resulting 57 | // ExtendedResponse may return a value in the form of a *ber.Packet 58 | func (l *Conn) Extended(er *ExtendedRequest) (*ExtendedResponse, error) { 59 | msgCtx, err := l.doRequest(er) 60 | if err != nil { 61 | return nil, err 62 | } 63 | defer l.finishMessage(msgCtx) 64 | 65 | packet, err := l.readPacket(msgCtx) 66 | if err != nil { 67 | return nil, err 68 | } 69 | if err = GetLDAPError(packet); err != nil { 70 | return nil, err 71 | } 72 | 73 | if len(packet.Children[1].Children) < 4 { 74 | return nil, fmt.Errorf( 75 | "ldap: malformed extended response: expected 4 children, got %d", 76 | len(packet.Children), 77 | ) 78 | } 79 | 80 | response := &ExtendedResponse{ 81 | Name: packet.Children[1].Children[3].Data.String(), 82 | Controls: make([]Control, 0), 83 | } 84 | 85 | if len(packet.Children) == 3 { 86 | for _, child := range packet.Children[2].Children { 87 | decodedChild, decodeErr := DecodeControl(child) 88 | if decodeErr != nil { 89 | return nil, fmt.Errorf("failed to decode child control: %s", decodeErr) 90 | } 91 | response.Controls = append(response.Controls, decodedChild) 92 | } 93 | } 94 | 95 | if len(packet.Children[1].Children) == 5 { 96 | response.Value = packet.Children[1].Children[4] 97 | } 98 | 99 | return response, nil 100 | } 101 | -------------------------------------------------------------------------------- /v3/extended_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestExtendedRequest_WhoAmI(t *testing.T) { 8 | l, err := DialURL(ldapServer) 9 | if err != nil { 10 | t.Errorf("%s failed: %v", t.Name(), err) 11 | return 12 | } 13 | defer l.Close() 14 | 15 | l.Bind("", "") // anonymous 16 | defer l.Unbind() 17 | 18 | rfc4532req := NewExtendedRequest("1.3.6.1.4.1.4203.1.11.3", nil) // request value is 19 | 20 | var rfc4532resp *ExtendedResponse 21 | if rfc4532resp, err = l.Extended(rfc4532req); err != nil { 22 | t.Errorf("%s failed: %v", t.Name(), err) 23 | return 24 | } 25 | t.Logf("%#v\n", rfc4532resp) 26 | } 27 | 28 | func TestExtendedRequest_FastBind(t *testing.T) { 29 | conn, err := DialURL(ldapServer) 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | defer conn.Close() 34 | 35 | request := NewExtendedRequest("1.3.6.1.4.1.4203.1.11.3", nil) 36 | _, err = conn.Extended(request) 37 | if err != nil { 38 | t.Errorf("%s failed: %v", t.Name(), err) 39 | return 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /v3/filter_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | ber "github.com/go-asn1-ber/asn1-ber" 8 | ) 9 | 10 | type compileTest struct { 11 | filterStr string 12 | 13 | expectedFilter string 14 | expectedType int 15 | expectedErr string 16 | } 17 | 18 | var testFilters = []compileTest{ 19 | { 20 | filterStr: "(&(sn=Miller)(givenName=Bob))", 21 | expectedFilter: "(&(sn=Miller)(givenName=Bob))", 22 | expectedType: FilterAnd, 23 | }, 24 | { 25 | filterStr: "(|(sn=Miller)(givenName=Bob))", 26 | expectedFilter: "(|(sn=Miller)(givenName=Bob))", 27 | expectedType: FilterOr, 28 | }, 29 | { 30 | filterStr: "(!(sn=Miller))", 31 | expectedFilter: "(!(sn=Miller))", 32 | expectedType: FilterNot, 33 | }, 34 | { 35 | filterStr: "(sn=Miller)", 36 | expectedFilter: "(sn=Miller)", 37 | expectedType: FilterEqualityMatch, 38 | }, 39 | { 40 | filterStr: "(sn=Mill*)", 41 | expectedFilter: "(sn=Mill*)", 42 | expectedType: FilterSubstrings, 43 | }, 44 | { 45 | filterStr: "(sn=*Mill)", 46 | expectedFilter: "(sn=*Mill)", 47 | expectedType: FilterSubstrings, 48 | }, 49 | { 50 | filterStr: "(sn=*Mill*)", 51 | expectedFilter: "(sn=*Mill*)", 52 | expectedType: FilterSubstrings, 53 | }, 54 | { 55 | filterStr: "(sn=*i*le*)", 56 | expectedFilter: "(sn=*i*le*)", 57 | expectedType: FilterSubstrings, 58 | }, 59 | { 60 | filterStr: "(sn=Mi*l*r)", 61 | expectedFilter: "(sn=Mi*l*r)", 62 | expectedType: FilterSubstrings, 63 | }, 64 | // substring filters escape properly 65 | { 66 | filterStr: `(sn=Mi*함*r)`, 67 | expectedFilter: `(sn=Mi*\ed\95\a8*r)`, 68 | expectedType: FilterSubstrings, 69 | }, 70 | // already escaped substring filters don't get double-escaped 71 | { 72 | filterStr: `(sn=Mi*\ed\95\a8*r)`, 73 | expectedFilter: `(sn=Mi*\ed\95\a8*r)`, 74 | expectedType: FilterSubstrings, 75 | }, 76 | { 77 | filterStr: "(sn=Mi*le*)", 78 | expectedFilter: "(sn=Mi*le*)", 79 | expectedType: FilterSubstrings, 80 | }, 81 | { 82 | filterStr: "(sn=*i*ler)", 83 | expectedFilter: "(sn=*i*ler)", 84 | expectedType: FilterSubstrings, 85 | }, 86 | { 87 | filterStr: "(sn>=Miller)", 88 | expectedFilter: "(sn>=Miller)", 89 | expectedType: FilterGreaterOrEqual, 90 | }, 91 | { 92 | filterStr: "(sn<=Miller)", 93 | expectedFilter: "(sn<=Miller)", 94 | expectedType: FilterLessOrEqual, 95 | }, 96 | { 97 | filterStr: "(sn=*)", 98 | expectedFilter: "(sn=*)", 99 | expectedType: FilterPresent, 100 | }, 101 | { 102 | filterStr: "(sn~=Miller)", 103 | expectedFilter: "(sn~=Miller)", 104 | expectedType: FilterApproxMatch, 105 | }, 106 | { 107 | filterStr: `(objectGUID='\fc\fe\a3\ab\f9\90N\aaGm\d5I~\d12)`, 108 | expectedFilter: `(objectGUID='\fc\fe\a3\ab\f9\90N\aaGm\d5I~\d12)`, 109 | expectedType: FilterEqualityMatch, 110 | }, 111 | { 112 | filterStr: `(objectGUID=абвгдеёжзийклмнопрстуфхцчшщъыьэюя)`, 113 | 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)`, 114 | expectedType: FilterEqualityMatch, 115 | }, 116 | { 117 | filterStr: `(objectGUID=함수목록)`, 118 | expectedFilter: `(objectGUID=\ed\95\a8\ec\88\98\eb\aa\a9\eb\a1\9d)`, 119 | expectedType: FilterEqualityMatch, 120 | }, 121 | { 122 | filterStr: `(objectGUID=`, 123 | expectedFilter: ``, 124 | expectedType: 0, 125 | expectedErr: "unexpected end of filter", 126 | }, 127 | { 128 | filterStr: `(objectGUID=함수목록`, 129 | expectedFilter: ``, 130 | expectedType: 0, 131 | expectedErr: "unexpected end of filter", 132 | }, 133 | { 134 | filterStr: `((cn=)`, 135 | expectedFilter: ``, 136 | expectedType: 0, 137 | expectedErr: "unexpected end of filter", 138 | }, 139 | { 140 | filterStr: `(&(objectclass=inetorgperson)(cn=中文))`, 141 | expectedFilter: `(&(objectclass=inetorgperson)(cn=\e4\b8\ad\e6\96\87))`, 142 | expectedType: 0, 143 | }, 144 | // attr extension 145 | { 146 | filterStr: `(memberOf:=foo)`, 147 | expectedFilter: `(memberOf:=foo)`, 148 | expectedType: FilterExtensibleMatch, 149 | }, 150 | // attr+named matching rule extension 151 | { 152 | filterStr: `(memberOf:test:=foo)`, 153 | expectedFilter: `(memberOf:test:=foo)`, 154 | expectedType: FilterExtensibleMatch, 155 | }, 156 | // attr+oid matching rule extension 157 | { 158 | filterStr: `(cn:1.2.3.4.5:=Fred Flintstone)`, 159 | expectedFilter: `(cn:1.2.3.4.5:=Fred Flintstone)`, 160 | expectedType: FilterExtensibleMatch, 161 | }, 162 | // attr+dn+oid matching rule extension 163 | { 164 | filterStr: `(sn:dn:2.4.6.8.10:=Barney Rubble)`, 165 | expectedFilter: `(sn:dn:2.4.6.8.10:=Barney Rubble)`, 166 | expectedType: FilterExtensibleMatch, 167 | }, 168 | // attr+dn extension 169 | { 170 | filterStr: `(o:dn:=Ace Industry)`, 171 | expectedFilter: `(o:dn:=Ace Industry)`, 172 | expectedType: FilterExtensibleMatch, 173 | }, 174 | // dn extension 175 | { 176 | filterStr: `(:dn:2.4.6.8.10:=Dino)`, 177 | expectedFilter: `(:dn:2.4.6.8.10:=Dino)`, 178 | expectedType: FilterExtensibleMatch, 179 | }, 180 | { 181 | filterStr: `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`, 182 | expectedFilter: `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`, 183 | expectedType: FilterExtensibleMatch, 184 | }, 185 | 186 | // compileTest{ filterStr: "()", filterType: FilterExtensibleMatch }, 187 | } 188 | 189 | var testInvalidFilters = []string{ 190 | `(objectGUID=\zz)`, 191 | `(objectGUID=\a)`, 192 | } 193 | 194 | func TestFilter(t *testing.T) { 195 | // Test Compiler and Decompiler 196 | for _, i := range testFilters { 197 | filter, err := CompileFilter(i.filterStr) 198 | switch { 199 | case err != nil: 200 | if i.expectedErr == "" || !strings.Contains(err.Error(), i.expectedErr) { 201 | t.Errorf("Problem compiling '%s' - '%v' (expected error to contain '%v')", i.filterStr, err, i.expectedErr) 202 | } 203 | case filter.Tag != ber.Tag(i.expectedType): 204 | t.Errorf("%q Expected %q got %q", i.filterStr, FilterMap[uint64(i.expectedType)], FilterMap[uint64(filter.Tag)]) 205 | default: 206 | o, err := DecompileFilter(filter) 207 | if err != nil { 208 | t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error()) 209 | } else if i.expectedFilter != o { 210 | t.Errorf("%q expected, got %q", i.expectedFilter, o) 211 | } 212 | } 213 | } 214 | } 215 | 216 | func TestDecodeEscapedSymbols(t *testing.T) { 217 | for _, testInfo := range []struct { 218 | Src string 219 | Err string 220 | }{ 221 | { 222 | Src: "a\u0100\x80", 223 | Err: `LDAP Result Code 201 "Filter Compile Error": ldap: error reading rune at position 3`, 224 | }, 225 | { 226 | Src: `start\d`, 227 | Err: `LDAP Result Code 201 "Filter Compile Error": ldap: missing characters for escape in filter`, 228 | }, 229 | { 230 | Src: `\`, 231 | Err: `LDAP Result Code 201 "Filter Compile Error": ldap: invalid characters for escape in filter: EOF`, 232 | }, 233 | { 234 | Src: `start\--end`, 235 | Err: `LDAP Result Code 201 "Filter Compile Error": ldap: invalid characters for escape in filter: encoding/hex: invalid byte: U+002D '-'`, 236 | }, 237 | { 238 | Src: `start\d0\hh`, 239 | Err: `LDAP Result Code 201 "Filter Compile Error": ldap: invalid characters for escape in filter: encoding/hex: invalid byte: U+0068 'h'`, 240 | }, 241 | } { 242 | 243 | res, err := decodeEscapedSymbols([]byte(testInfo.Src)) 244 | if err == nil || err.Error() != testInfo.Err { 245 | t.Fatal(testInfo.Src, "=> ", err, "!=", testInfo.Err) 246 | } 247 | if res != "" { 248 | t.Fatal(testInfo.Src, "=> ", "invalid result", res) 249 | } 250 | } 251 | } 252 | 253 | func TestInvalidFilter(t *testing.T) { 254 | for _, filterStr := range testInvalidFilters { 255 | if _, err := CompileFilter(filterStr); err == nil { 256 | t.Errorf("Problem compiling %s - expected err", filterStr) 257 | } 258 | } 259 | } 260 | 261 | func BenchmarkFilterCompile(b *testing.B) { 262 | b.StopTimer() 263 | filters := make([]string, len(testFilters)) 264 | 265 | // Test Compiler and Decompiler 266 | for idx, i := range testFilters { 267 | filters[idx] = i.filterStr 268 | } 269 | 270 | maxIdx := len(filters) 271 | b.StartTimer() 272 | for i := 0; i < b.N; i++ { 273 | _, _ = CompileFilter(filters[i%maxIdx]) 274 | } 275 | } 276 | 277 | func BenchmarkFilterDecompile(b *testing.B) { 278 | b.StopTimer() 279 | filters := make([]*ber.Packet, len(testFilters)) 280 | 281 | // Test Compiler and Decompiler 282 | for idx, i := range testFilters { 283 | filters[idx], _ = CompileFilter(i.filterStr) 284 | } 285 | 286 | maxIdx := len(filters) 287 | b.StartTimer() 288 | for i := 0; i < b.N; i++ { 289 | _, _ = DecompileFilter(filters[i%maxIdx]) 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /v3/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-ldap/ldap/v3 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 7 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa 8 | github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 9 | github.com/google/uuid v1.6.0 10 | github.com/jcmturner/gokrb5/v8 v8.4.4 11 | github.com/stretchr/testify v1.8.1 12 | golang.org/x/crypto v0.36.0 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/hashicorp/go-uuid v1.0.3 // indirect 18 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 19 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 20 | github.com/jcmturner/gofork v1.7.6 // indirect 21 | github.com/jcmturner/goidentity/v6 v6.0.1 // indirect 22 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | golang.org/x/net v0.38.0 // indirect 25 | gopkg.in/yaml.v3 v3.0.1 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /v3/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= 2 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= 3 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= 4 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= 9 | github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= 10 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 11 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 12 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 13 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 14 | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= 15 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 16 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 17 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 18 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 19 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 20 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 21 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 22 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 23 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 24 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 25 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 26 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 27 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 28 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 29 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 30 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 31 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 34 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 35 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 36 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 37 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 38 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 39 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 40 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 41 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 42 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 43 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 44 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 45 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 46 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 47 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 48 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 49 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 50 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 51 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 52 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 53 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 54 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 55 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 56 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 57 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 58 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 59 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 65 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 66 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 67 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 68 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 69 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 70 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 71 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 72 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 73 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 74 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 75 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 76 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 77 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 78 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 79 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 80 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 81 | -------------------------------------------------------------------------------- /v3/gssapi/client.go: -------------------------------------------------------------------------------- 1 | package gssapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | 10 | "github.com/jcmturner/gokrb5/v8/client" 11 | "github.com/jcmturner/gokrb5/v8/config" 12 | "github.com/jcmturner/gokrb5/v8/keytab" 13 | "github.com/jcmturner/gokrb5/v8/types" 14 | 15 | "github.com/jcmturner/gokrb5/v8/gssapi" 16 | "github.com/jcmturner/gokrb5/v8/spnego" 17 | 18 | "github.com/jcmturner/gokrb5/v8/crypto" 19 | "github.com/jcmturner/gokrb5/v8/iana/keyusage" 20 | "github.com/jcmturner/gokrb5/v8/messages" 21 | 22 | "github.com/jcmturner/gokrb5/v8/credentials" 23 | ) 24 | 25 | // Client implements ldap.GSSAPIClient interface. 26 | type Client struct { 27 | *client.Client 28 | 29 | ekey types.EncryptionKey 30 | Subkey types.EncryptionKey 31 | } 32 | 33 | // NewClientWithKeytab creates a new client from a keytab credential. 34 | // Set the realm to empty string to use the default realm from config. 35 | func NewClientWithKeytab(username, realm, keytabPath, krb5confPath string, settings ...func(*client.Settings)) (*Client, error) { 36 | krb5conf, err := config.Load(krb5confPath) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | keytab, err := keytab.Load(keytabPath) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | client := client.NewWithKeytab(username, realm, keytab, krb5conf, settings...) 47 | 48 | return &Client{ 49 | Client: client, 50 | }, nil 51 | } 52 | 53 | // NewClientWithPassword creates a new client from a password credential. 54 | // Set the realm to empty string to use the default realm from config. 55 | func NewClientWithPassword(username, realm, password string, krb5confPath string, settings ...func(*client.Settings)) (*Client, error) { 56 | krb5conf, err := config.Load(krb5confPath) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | client := client.NewWithPassword(username, realm, password, krb5conf, settings...) 62 | 63 | return &Client{ 64 | Client: client, 65 | }, nil 66 | } 67 | 68 | // NewClientFromCCache creates a new client from a populated client cache. 69 | func NewClientFromCCache(ccachePath, krb5confPath string, settings ...func(*client.Settings)) (*Client, error) { 70 | krb5conf, err := config.Load(krb5confPath) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | ccache, err := credentials.LoadCCache(ccachePath) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | client, err := client.NewFromCCache(ccache, krb5conf, settings...) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return &Client{ 86 | Client: client, 87 | }, nil 88 | } 89 | 90 | // Close deletes any established secure context and closes the client. 91 | func (client *Client) Close() error { 92 | client.Client.Destroy() 93 | return nil 94 | } 95 | 96 | // DeleteSecContext destroys any established secure context. 97 | func (client *Client) DeleteSecContext() error { 98 | client.ekey = types.EncryptionKey{} 99 | client.Subkey = types.EncryptionKey{} 100 | return nil 101 | } 102 | 103 | // InitSecContext initiates the establishment of a security context for 104 | // GSS-API between the client and server. 105 | // See RFC 4752 section 3.1. 106 | func (client *Client) InitSecContext(target string, input []byte) ([]byte, bool, error) { 107 | return client.InitSecContextWithOptions(target, input, []int{}) 108 | } 109 | 110 | // InitSecContextWithOptions initiates the establishment of a security context for 111 | // GSS-API between the client and server. 112 | // See RFC 4752 section 3.1. 113 | func (client *Client) InitSecContextWithOptions(target string, input []byte, APOptions []int) ([]byte, bool, error) { 114 | gssapiFlags := []int{gssapi.ContextFlagInteg, gssapi.ContextFlagConf, gssapi.ContextFlagMutual} 115 | 116 | switch input { 117 | case nil: 118 | tkt, ekey, err := client.Client.GetServiceTicket(target) 119 | if err != nil { 120 | return nil, false, err 121 | } 122 | client.ekey = ekey 123 | 124 | token, err := spnego.NewKRB5TokenAPREQ(client.Client, tkt, ekey, gssapiFlags, APOptions) 125 | if err != nil { 126 | return nil, false, err 127 | } 128 | 129 | output, err := token.Marshal() 130 | if err != nil { 131 | return nil, false, err 132 | } 133 | 134 | return output, true, nil 135 | 136 | default: 137 | var token spnego.KRB5Token 138 | 139 | err := token.Unmarshal(input) 140 | if err != nil { 141 | return nil, false, err 142 | } 143 | 144 | var completed bool 145 | 146 | if token.IsAPRep() { 147 | completed = true 148 | 149 | encpart, err := crypto.DecryptEncPart(token.APRep.EncPart, client.ekey, keyusage.AP_REP_ENCPART) 150 | if err != nil { 151 | return nil, false, err 152 | } 153 | 154 | part := &messages.EncAPRepPart{} 155 | 156 | if err = part.Unmarshal(encpart); err != nil { 157 | return nil, false, err 158 | } 159 | client.Subkey = part.Subkey 160 | } 161 | 162 | if token.IsKRBError() { 163 | return nil, !false, token.KRBError 164 | } 165 | 166 | return make([]byte, 0), !completed, nil 167 | } 168 | } 169 | 170 | // NegotiateSaslAuth performs the last step of the SASL handshake. 171 | // See RFC 4752 section 3.1. 172 | func (client *Client) NegotiateSaslAuth(input []byte, authzid string) ([]byte, error) { 173 | token := &gssapi.WrapToken{} 174 | err := UnmarshalWrapToken(token, input, true) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | if (token.Flags & 0b1) == 0 { 180 | return nil, fmt.Errorf("got a Wrapped token that's not from the server") 181 | } 182 | 183 | key := client.ekey 184 | if (token.Flags & 0b100) != 0 { 185 | key = client.Subkey 186 | } 187 | 188 | _, err = token.Verify(key, keyusage.GSSAPI_ACCEPTOR_SEAL) 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | pl := token.Payload 194 | if len(pl) != 4 { 195 | return nil, fmt.Errorf("server send bad final token for SASL GSSAPI Handshake") 196 | } 197 | 198 | // We never want a security layer 199 | b := [4]byte{0, 0, 0, 0} 200 | payload := append(b[:], []byte(authzid)...) 201 | 202 | encType, err := crypto.GetEtype(key.KeyType) 203 | if err != nil { 204 | return nil, err 205 | } 206 | 207 | token = &gssapi.WrapToken{ 208 | Flags: 0b100, 209 | EC: uint16(encType.GetHMACBitLength() / 8), 210 | RRC: 0, 211 | SndSeqNum: 1, 212 | Payload: payload, 213 | } 214 | 215 | if err := token.SetCheckSum(key, keyusage.GSSAPI_INITIATOR_SEAL); err != nil { 216 | return nil, err 217 | } 218 | 219 | output, err := token.Marshal() 220 | if err != nil { 221 | return nil, err 222 | } 223 | 224 | return output, nil 225 | } 226 | 227 | func getGssWrapTokenId() *[2]byte { 228 | return &[2]byte{0x05, 0x04} 229 | } 230 | 231 | func UnmarshalWrapToken(wt *gssapi.WrapToken, b []byte, expectFromAcceptor bool) error { 232 | // Check if we can read a whole header 233 | if len(b) < 16 { 234 | return errors.New("bytes shorter than header length") 235 | } 236 | // Is the Token ID correct? 237 | if !bytes.Equal(getGssWrapTokenId()[:], b[0:2]) { 238 | return fmt.Errorf("wrong Token ID. Expected %s, was %s", 239 | hex.EncodeToString(getGssWrapTokenId()[:]), 240 | hex.EncodeToString(b[0:2])) 241 | } 242 | // Check the acceptor flag 243 | flags := b[2] 244 | isFromAcceptor := flags&0x01 == 1 245 | if isFromAcceptor && !expectFromAcceptor { 246 | return errors.New("unexpected acceptor flag is set: not expecting a token from the acceptor") 247 | } 248 | if !isFromAcceptor && expectFromAcceptor { 249 | return errors.New("expected acceptor flag is not set: expecting a token from the acceptor, not the initiator") 250 | } 251 | // Check the filler byte 252 | if b[3] != gssapi.FillerByte { 253 | return fmt.Errorf("unexpected filler byte: expecting 0xFF, was %s ", hex.EncodeToString(b[3:4])) 254 | } 255 | checksumL := binary.BigEndian.Uint16(b[4:6]) 256 | // Sanity check on the checksum length 257 | if int(checksumL) > len(b)-gssapi.HdrLen { 258 | return fmt.Errorf("inconsistent checksum length: %d bytes to parse, checksum length is %d", len(b), checksumL) 259 | } 260 | 261 | payloadStart := 16 + checksumL 262 | 263 | wt.Flags = flags 264 | wt.EC = checksumL 265 | wt.RRC = binary.BigEndian.Uint16(b[6:8]) 266 | wt.SndSeqNum = binary.BigEndian.Uint64(b[8:16]) 267 | wt.CheckSum = b[16:payloadStart] 268 | wt.Payload = b[payloadStart:] 269 | 270 | return nil 271 | } 272 | -------------------------------------------------------------------------------- /v3/gssapi/sspi.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package gssapi 5 | 6 | import ( 7 | "bytes" 8 | "encoding/binary" 9 | "fmt" 10 | 11 | "github.com/alexbrainman/sspi" 12 | "github.com/alexbrainman/sspi/kerberos" 13 | ) 14 | 15 | // SSPIClient implements ldap.GSSAPIClient interface. 16 | // Depends on secur32.dll. 17 | type SSPIClient struct { 18 | creds *sspi.Credentials 19 | ctx *kerberos.ClientContext 20 | } 21 | 22 | // NewSSPIClient returns a client with credentials of the current user. 23 | func NewSSPIClient() (*SSPIClient, error) { 24 | creds, err := kerberos.AcquireCurrentUserCredentials() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return NewSSPIClientWithCredentials(creds), nil 30 | } 31 | 32 | // NewSSPIClientWithCredentials returns a client with the provided credentials. 33 | func NewSSPIClientWithCredentials(creds *sspi.Credentials) *SSPIClient { 34 | return &SSPIClient{ 35 | creds: creds, 36 | } 37 | } 38 | 39 | // NewSSPIClientWithUserCredentials returns a client using the provided user's 40 | // credentials. 41 | func NewSSPIClientWithUserCredentials(domain, username, password string) (*SSPIClient, error) { 42 | creds, err := kerberos.AcquireUserCredentials(domain, username, password) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return &SSPIClient{ 48 | creds: creds, 49 | }, nil 50 | } 51 | 52 | // Close deletes any established secure context and closes the client. 53 | func (c *SSPIClient) Close() error { 54 | err1 := c.DeleteSecContext() 55 | err2 := c.creds.Release() 56 | if err1 != nil { 57 | return err1 58 | } 59 | if err2 != nil { 60 | return err2 61 | } 62 | return nil 63 | } 64 | 65 | // DeleteSecContext destroys any established secure context. 66 | func (c *SSPIClient) DeleteSecContext() error { 67 | return c.ctx.Release() 68 | } 69 | 70 | // InitSecContext initiates the establishment of a security context for 71 | // GSS-API between the client and server. 72 | // See RFC 4752 section 3.1. 73 | func (c *SSPIClient) InitSecContext(target string, token []byte) ([]byte, bool, error) { 74 | return c.InitSecContextWithOptions(target, token, []int{}) 75 | } 76 | 77 | // InitSecContextWithOptions initiates the establishment of a security context for 78 | // GSS-API between the client and server. 79 | // See RFC 4752 section 3.1. 80 | func (c *SSPIClient) InitSecContextWithOptions(target string, token []byte, APOptions []int) ([]byte, bool, error) { 81 | sspiFlags := uint32(sspi.ISC_REQ_INTEGRITY | sspi.ISC_REQ_CONFIDENTIALITY | sspi.ISC_REQ_MUTUAL_AUTH) 82 | 83 | switch token { 84 | case nil: 85 | ctx, completed, output, err := kerberos.NewClientContextWithFlags(c.creds, target, sspiFlags) 86 | if err != nil { 87 | return nil, false, err 88 | } 89 | c.ctx = ctx 90 | 91 | return output, !completed, nil 92 | default: 93 | 94 | completed, output, err := c.ctx.Update(token) 95 | if err != nil { 96 | return nil, false, err 97 | } 98 | if err := c.ctx.VerifyFlags(); err != nil { 99 | return nil, false, fmt.Errorf("error verifying flags: %v", err) 100 | } 101 | return output, !completed, nil 102 | 103 | } 104 | } 105 | 106 | // NegotiateSaslAuth performs the last step of the SASL handshake. 107 | // See RFC 4752 section 3.1. 108 | func (c *SSPIClient) NegotiateSaslAuth(token []byte, authzid string) ([]byte, error) { 109 | // Using SSPI rather than of GSSAPI, relevant documentation of differences here: 110 | // https://learn.microsoft.com/en-us/windows/win32/secauthn/sspi-kerberos-interoperability-with-gssapi 111 | 112 | // KERB_WRAP_NO_ENCRYPT (SECQOP_WRAP_NO_ENCRYPT) flag indicates Wrap and Unwrap 113 | // should only sign & verify (not encrypt & decrypt). 114 | const KERB_WRAP_NO_ENCRYPT = 0x80000001 115 | 116 | // https://learn.microsoft.com/en-us/windows/win32/api/sspi/nf-sspi-decryptmessage 117 | flags, inputPayload, err := c.ctx.DecryptMessage(token, 0) 118 | if err != nil { 119 | return nil, fmt.Errorf("error decrypting message: %w", err) 120 | } 121 | if flags&KERB_WRAP_NO_ENCRYPT == 0 { 122 | // Encrypted message, this is unexpected. 123 | return nil, fmt.Errorf("message encrypted") 124 | } 125 | 126 | // `bytes` describes available security context: 127 | // "the first octet of resulting cleartext as a 128 | // bit-mask specifying the security layers supported by the server and 129 | // the second through fourth octets as the maximum size output_message 130 | // the server is able to receive (in network byte order). If the 131 | // resulting cleartext is not 4 octets long, the client fails the 132 | // negotiation. The client verifies that the server maximum buffer is 0 133 | // if the server does not advertise support for any security layer." 134 | // From https://www.rfc-editor.org/rfc/rfc4752#section-3.1 135 | if len(inputPayload) != 4 { 136 | return nil, fmt.Errorf("bad server token") 137 | } 138 | if inputPayload[0] == 0x0 && !bytes.Equal(inputPayload, []byte{0x0, 0x0, 0x0, 0x0}) { 139 | return nil, fmt.Errorf("bad server token") 140 | } 141 | 142 | // Security layers https://www.rfc-editor.org/rfc/rfc4422#section-3.7 143 | // https://www.rfc-editor.org/rfc/rfc4752#section-3.3 144 | // supportNoSecurity := input[0] & 0b00000001 145 | // supportIntegrity := input[0] & 0b00000010 146 | // supportPrivacy := input[0] & 0b00000100 147 | selectedSec := 0 // Disabled 148 | var maxSecMsgSize uint32 149 | if selectedSec != 0 { 150 | maxSecMsgSize, _, _, _, err = c.ctx.Sizes() 151 | if err != nil { 152 | return nil, fmt.Errorf("error getting security context max message size: %w", err) 153 | } 154 | } 155 | 156 | // https://learn.microsoft.com/en-us/windows/win32/api/sspi/nf-sspi-encryptmessage 157 | inputPayload, err = c.ctx.EncryptMessage(handshakePayload(byte(selectedSec), maxSecMsgSize, []byte(authzid)), KERB_WRAP_NO_ENCRYPT, 0) 158 | if err != nil { 159 | return nil, fmt.Errorf("error encrypting message: %w", err) 160 | } 161 | 162 | return inputPayload, nil 163 | } 164 | 165 | func handshakePayload(secLayer byte, maxSize uint32, authzid []byte) []byte { 166 | // construct payload and send unencrypted: 167 | // "The client then constructs data, with the first octet containing the 168 | // bit-mask specifying the selected security layer, the second through 169 | // fourth octets containing in network byte order the maximum size 170 | // output_message the client is able to receive (which MUST be 0 if the 171 | // client does not support any security layer), and the remaining octets 172 | // containing the UTF-8 [UTF8] encoded authorization identity. 173 | // (Implementation note: The authorization identity is not terminated 174 | // with the zero-valued (%x00) octet (e.g., the UTF-8 encoding of the 175 | // NUL (U+0000) character)). The client passes the data to GSS_Wrap 176 | // with conf_flag set to FALSE and responds with the generated 177 | // output_message. The client can then consider the server 178 | // authenticated." 179 | // From https://www.rfc-editor.org/rfc/rfc4752#section-3.1 180 | 181 | // Client picks security layer to use, 0 is disabled. 182 | var selectedSecurity byte = secLayer 183 | var truncatedSize uint32 // must be 0 if secLayer is 0 184 | if selectedSecurity != 0 { 185 | // Only 3 bytes to describe the max size, set the maximum. 186 | truncatedSize = 0b00000000_11111111_11111111_11111111 187 | if truncatedSize > maxSize { 188 | truncatedSize = maxSize 189 | } 190 | } 191 | 192 | payload := make([]byte, 4, 4+len(authzid)) 193 | binary.BigEndian.PutUint32(payload, truncatedSize) 194 | payload[0] = selectedSecurity // Overwrites most significant byte of `maxSize` 195 | payload = append(payload, []byte(authzid)...) 196 | 197 | return payload 198 | } 199 | -------------------------------------------------------------------------------- /v3/ldap.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "strings" 9 | 10 | ber "github.com/go-asn1-ber/asn1-ber" 11 | ) 12 | 13 | // LDAP Application Codes 14 | const ( 15 | ApplicationBindRequest = 0 16 | ApplicationBindResponse = 1 17 | ApplicationUnbindRequest = 2 18 | ApplicationSearchRequest = 3 19 | ApplicationSearchResultEntry = 4 20 | ApplicationSearchResultDone = 5 21 | ApplicationModifyRequest = 6 22 | ApplicationModifyResponse = 7 23 | ApplicationAddRequest = 8 24 | ApplicationAddResponse = 9 25 | ApplicationDelRequest = 10 26 | ApplicationDelResponse = 11 27 | ApplicationModifyDNRequest = 12 28 | ApplicationModifyDNResponse = 13 29 | ApplicationCompareRequest = 14 30 | ApplicationCompareResponse = 15 31 | ApplicationAbandonRequest = 16 32 | ApplicationSearchResultReference = 19 33 | ApplicationExtendedRequest = 23 34 | ApplicationExtendedResponse = 24 35 | ApplicationIntermediateResponse = 25 36 | ) 37 | 38 | // ApplicationMap contains human readable descriptions of LDAP Application Codes 39 | var ApplicationMap = map[uint8]string{ 40 | ApplicationBindRequest: "Bind Request", 41 | ApplicationBindResponse: "Bind Response", 42 | ApplicationUnbindRequest: "Unbind Request", 43 | ApplicationSearchRequest: "Search Request", 44 | ApplicationSearchResultEntry: "Search Result Entry", 45 | ApplicationSearchResultDone: "Search Result Done", 46 | ApplicationModifyRequest: "Modify Request", 47 | ApplicationModifyResponse: "Modify Response", 48 | ApplicationAddRequest: "Add Request", 49 | ApplicationAddResponse: "Add Response", 50 | ApplicationDelRequest: "Del Request", 51 | ApplicationDelResponse: "Del Response", 52 | ApplicationModifyDNRequest: "Modify DN Request", 53 | ApplicationModifyDNResponse: "Modify DN Response", 54 | ApplicationCompareRequest: "Compare Request", 55 | ApplicationCompareResponse: "Compare Response", 56 | ApplicationAbandonRequest: "Abandon Request", 57 | ApplicationSearchResultReference: "Search Result Reference", 58 | ApplicationExtendedRequest: "Extended Request", 59 | ApplicationExtendedResponse: "Extended Response", 60 | ApplicationIntermediateResponse: "Intermediate Response", 61 | } 62 | 63 | // Ldap Behera Password Policy Draft 10 (https://tools.ietf.org/html/draft-behera-ldap-password-policy-10) 64 | const ( 65 | BeheraPasswordExpired = 0 66 | BeheraAccountLocked = 1 67 | BeheraChangeAfterReset = 2 68 | BeheraPasswordModNotAllowed = 3 69 | BeheraMustSupplyOldPassword = 4 70 | BeheraInsufficientPasswordQuality = 5 71 | BeheraPasswordTooShort = 6 72 | BeheraPasswordTooYoung = 7 73 | BeheraPasswordInHistory = 8 74 | ) 75 | 76 | // BeheraPasswordPolicyErrorMap contains human readable descriptions of Behera Password Policy error codes 77 | var BeheraPasswordPolicyErrorMap = map[int8]string{ 78 | BeheraPasswordExpired: "Password expired", 79 | BeheraAccountLocked: "Account locked", 80 | BeheraChangeAfterReset: "Password must be changed", 81 | BeheraPasswordModNotAllowed: "Policy prevents password modification", 82 | BeheraMustSupplyOldPassword: "Policy requires old password in order to change password", 83 | BeheraInsufficientPasswordQuality: "Password fails quality checks", 84 | BeheraPasswordTooShort: "Password is too short for policy", 85 | BeheraPasswordTooYoung: "Password has been changed too recently", 86 | BeheraPasswordInHistory: "New password is in list of old passwords", 87 | } 88 | 89 | var logger = log.New(os.Stderr, "", log.LstdFlags) 90 | 91 | // Logger allows clients to override the default logger 92 | func Logger(l *log.Logger) { 93 | logger = l 94 | } 95 | 96 | // Adds descriptions to an LDAP Response packet for debugging 97 | func addLDAPDescriptions(packet *ber.Packet) (err error) { 98 | defer func() { 99 | if r := recover(); r != nil { 100 | err = NewError(ErrorDebugging, fmt.Errorf("ldap: cannot process packet to add descriptions: %s", r)) 101 | } 102 | }() 103 | packet.Description = "LDAP Response" 104 | packet.Children[0].Description = "Message ID" 105 | 106 | application := uint8(packet.Children[1].Tag) 107 | packet.Children[1].Description = ApplicationMap[application] 108 | 109 | switch application { 110 | case ApplicationBindRequest: 111 | err = addRequestDescriptions(packet) 112 | case ApplicationBindResponse: 113 | err = addDefaultLDAPResponseDescriptions(packet) 114 | case ApplicationUnbindRequest: 115 | err = addRequestDescriptions(packet) 116 | case ApplicationSearchRequest: 117 | err = addRequestDescriptions(packet) 118 | case ApplicationSearchResultEntry: 119 | packet.Children[1].Children[0].Description = "Object Name" 120 | packet.Children[1].Children[1].Description = "Attributes" 121 | for _, child := range packet.Children[1].Children[1].Children { 122 | child.Description = "Attribute" 123 | child.Children[0].Description = "Attribute Name" 124 | child.Children[1].Description = "Attribute Values" 125 | for _, grandchild := range child.Children[1].Children { 126 | grandchild.Description = "Attribute Value" 127 | } 128 | } 129 | if len(packet.Children) == 3 { 130 | err = addControlDescriptions(packet.Children[2]) 131 | } 132 | case ApplicationSearchResultDone: 133 | err = addDefaultLDAPResponseDescriptions(packet) 134 | case ApplicationModifyRequest: 135 | err = addRequestDescriptions(packet) 136 | case ApplicationModifyResponse: 137 | case ApplicationAddRequest: 138 | err = addRequestDescriptions(packet) 139 | case ApplicationAddResponse: 140 | case ApplicationDelRequest: 141 | err = addRequestDescriptions(packet) 142 | case ApplicationDelResponse: 143 | case ApplicationModifyDNRequest: 144 | err = addRequestDescriptions(packet) 145 | case ApplicationModifyDNResponse: 146 | case ApplicationCompareRequest: 147 | err = addRequestDescriptions(packet) 148 | case ApplicationCompareResponse: 149 | case ApplicationAbandonRequest: 150 | err = addRequestDescriptions(packet) 151 | case ApplicationSearchResultReference: 152 | case ApplicationExtendedRequest: 153 | err = addRequestDescriptions(packet) 154 | case ApplicationExtendedResponse: 155 | } 156 | 157 | return err 158 | } 159 | 160 | func addControlDescriptions(packet *ber.Packet) error { 161 | packet.Description = "Controls" 162 | for _, child := range packet.Children { 163 | var value *ber.Packet 164 | controlType := "" 165 | child.Description = "Control" 166 | switch len(child.Children) { 167 | case 0: 168 | // at least one child is required for control type 169 | return fmt.Errorf("at least one child is required for control type") 170 | 171 | case 1: 172 | // just type, no criticality or value 173 | controlType = child.Children[0].Value.(string) 174 | child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" 175 | 176 | case 2: 177 | controlType = child.Children[0].Value.(string) 178 | child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" 179 | // Children[1] could be criticality or value (both are optional) 180 | // duck-type on whether this is a boolean 181 | if _, ok := child.Children[1].Value.(bool); ok { 182 | child.Children[1].Description = "Criticality" 183 | } else { 184 | child.Children[1].Description = "Control Value" 185 | value = child.Children[1] 186 | } 187 | 188 | case 3: 189 | // criticality and value present 190 | controlType = child.Children[0].Value.(string) 191 | child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" 192 | child.Children[1].Description = "Criticality" 193 | child.Children[2].Description = "Control Value" 194 | value = child.Children[2] 195 | 196 | default: 197 | // more than 3 children is invalid 198 | return fmt.Errorf("more than 3 children for control packet found") 199 | } 200 | 201 | if value == nil { 202 | continue 203 | } 204 | switch controlType { 205 | case ControlTypePaging: 206 | value.Description += " (Paging)" 207 | if value.Value != nil { 208 | valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) 209 | if err != nil { 210 | return fmt.Errorf("failed to decode data bytes: %s", err) 211 | } 212 | value.Data.Truncate(0) 213 | value.Value = nil 214 | valueChildren.Children[1].Value = valueChildren.Children[1].Data.Bytes() 215 | value.AppendChild(valueChildren) 216 | } 217 | value.Children[0].Description = "Real Search Control Value" 218 | value.Children[0].Children[0].Description = "Paging Size" 219 | value.Children[0].Children[1].Description = "Cookie" 220 | 221 | case ControlTypeBeheraPasswordPolicy: 222 | value.Description += " (Password Policy - Behera Draft)" 223 | if value.Value != nil { 224 | valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) 225 | if err != nil { 226 | return fmt.Errorf("failed to decode data bytes: %s", err) 227 | } 228 | value.Data.Truncate(0) 229 | value.Value = nil 230 | value.AppendChild(valueChildren) 231 | } 232 | sequence := value.Children[0] 233 | for _, child := range sequence.Children { 234 | if child.Tag == 0 { 235 | // Warning 236 | warningPacket := child.Children[0] 237 | val, err := ber.ParseInt64(warningPacket.Data.Bytes()) 238 | if err != nil { 239 | return fmt.Errorf("failed to decode data bytes: %s", err) 240 | } 241 | if warningPacket.Tag == 0 { 242 | // timeBeforeExpiration 243 | value.Description += " (TimeBeforeExpiration)" 244 | warningPacket.Value = val 245 | } else if warningPacket.Tag == 1 { 246 | // graceAuthNsRemaining 247 | value.Description += " (GraceAuthNsRemaining)" 248 | warningPacket.Value = val 249 | } 250 | } else if child.Tag == 1 { 251 | // Error 252 | bs := child.Data.Bytes() 253 | if len(bs) != 1 || bs[0] > 8 { 254 | return fmt.Errorf("failed to decode data bytes: %s", "invalid PasswordPolicyResponse enum value") 255 | } 256 | val := int8(bs[0]) 257 | child.Description = "Error" 258 | child.Value = val 259 | } 260 | } 261 | } 262 | } 263 | return nil 264 | } 265 | 266 | func addRequestDescriptions(packet *ber.Packet) error { 267 | packet.Description = "LDAP Request" 268 | packet.Children[0].Description = "Message ID" 269 | packet.Children[1].Description = ApplicationMap[uint8(packet.Children[1].Tag)] 270 | if len(packet.Children) == 3 { 271 | return addControlDescriptions(packet.Children[2]) 272 | } 273 | return nil 274 | } 275 | 276 | func addDefaultLDAPResponseDescriptions(packet *ber.Packet) error { 277 | resultCode := uint16(LDAPResultSuccess) 278 | matchedDN := "" 279 | description := "Success" 280 | if err := GetLDAPError(packet); err != nil { 281 | resultCode = err.(*Error).ResultCode 282 | matchedDN = err.(*Error).MatchedDN 283 | description = "Error Message" 284 | } 285 | 286 | packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[resultCode] + ")" 287 | packet.Children[1].Children[1].Description = "Matched DN (" + matchedDN + ")" 288 | packet.Children[1].Children[2].Description = description 289 | if len(packet.Children[1].Children) > 3 { 290 | packet.Children[1].Children[3].Description = "Referral" 291 | } 292 | if len(packet.Children) == 3 { 293 | return addControlDescriptions(packet.Children[2]) 294 | } 295 | return nil 296 | } 297 | 298 | // DebugBinaryFile reads and prints packets from the given filename 299 | func DebugBinaryFile(fileName string) error { 300 | file, err := ioutil.ReadFile(fileName) 301 | if err != nil { 302 | return NewError(ErrorDebugging, err) 303 | } 304 | ber.PrintBytes(os.Stdout, file, "") 305 | packet, err := ber.DecodePacketErr(file) 306 | if err != nil { 307 | return fmt.Errorf("failed to decode packet: %s", err) 308 | } 309 | if err := addLDAPDescriptions(packet); err != nil { 310 | return err 311 | } 312 | ber.PrintPacket(packet) 313 | 314 | return nil 315 | } 316 | 317 | func mustEscape(c byte) bool { 318 | return c > 0x7f || c == '(' || c == ')' || c == '\\' || c == '*' || c == 0 319 | } 320 | 321 | // EscapeFilter escapes from the provided LDAP filter string the special 322 | // characters in the set `()*\` and those out of the range 0 < c < 0x80, 323 | // as defined in RFC4515. 324 | func EscapeFilter(filter string) string { 325 | const hexValues = "0123456789abcdef" 326 | escape := 0 327 | for i := 0; i < len(filter); i++ { 328 | if mustEscape(filter[i]) { 329 | escape++ 330 | } 331 | } 332 | if escape == 0 { 333 | return filter 334 | } 335 | buf := make([]byte, len(filter)+escape*2) 336 | for i, j := 0, 0; i < len(filter); i++ { 337 | c := filter[i] 338 | if mustEscape(c) { 339 | buf[j+0] = '\\' 340 | buf[j+1] = hexValues[c>>4] 341 | buf[j+2] = hexValues[c&0xf] 342 | j += 3 343 | } else { 344 | buf[j] = c 345 | j++ 346 | } 347 | } 348 | return string(buf) 349 | } 350 | 351 | // EscapeDN escapes distinguished names as described in RFC4514. Characters in the 352 | // set `"+,;<>\` are escaped by prepending a backslash, which is also done for trailing 353 | // spaces or a leading `#`. Null bytes are replaced with `\00`. 354 | func EscapeDN(dn string) string { 355 | if dn == "" { 356 | return "" 357 | } 358 | 359 | builder := strings.Builder{} 360 | 361 | for i, r := range dn { 362 | // Escape leading and trailing spaces 363 | if (i == 0 || i == len(dn)-1) && r == ' ' { 364 | builder.WriteRune('\\') 365 | builder.WriteRune(r) 366 | continue 367 | } 368 | 369 | // Escape leading '#' 370 | if i == 0 && r == '#' { 371 | builder.WriteRune('\\') 372 | builder.WriteRune(r) 373 | continue 374 | } 375 | 376 | // Escape characters as defined in RFC4514 377 | switch r { 378 | case '"', '+', ',', ';', '<', '>', '\\': 379 | builder.WriteRune('\\') 380 | builder.WriteRune(r) 381 | case '\x00': // Null byte may not be escaped by a leading backslash 382 | builder.WriteString("\\00") 383 | default: 384 | builder.WriteRune(r) 385 | } 386 | } 387 | 388 | return builder.String() 389 | } 390 | -------------------------------------------------------------------------------- /v3/ldap_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "log" 7 | "testing" 8 | 9 | ber "github.com/go-asn1-ber/asn1-ber" 10 | ) 11 | 12 | const ( 13 | ldapServer = "ldap://ldap.itd.umich.edu:389" 14 | ldapsServer = "ldaps://ldap.itd.umich.edu:636" 15 | baseDN = "dc=umich,dc=edu" 16 | ) 17 | 18 | var filter = []string{ 19 | "(cn=cis-fac)", 20 | "(&(owner=*)(cn=cis-fac))", 21 | "(&(objectclass=rfc822mailgroup)(cn=*Computer*))", 22 | "(&(objectclass=rfc822mailgroup)(cn=*Mathematics*))", 23 | } 24 | 25 | var attributes = []string{ 26 | "cn", 27 | "description", 28 | } 29 | 30 | func TestUnsecureDialURL(t *testing.T) { 31 | l, err := DialURL(ldapServer) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | defer l.Close() 36 | } 37 | 38 | func TestSecureDialURL(t *testing.T) { 39 | l, err := DialURL(ldapsServer, DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true})) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | defer l.Close() 44 | } 45 | 46 | func TestStartTLS(t *testing.T) { 47 | l, err := DialURL(ldapServer) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | defer l.Close() 52 | err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | } 57 | 58 | func TestTLSConnectionState(t *testing.T) { 59 | l, err := DialURL(ldapServer) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | defer l.Close() 64 | err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | cs, ok := l.TLSConnectionState() 70 | if !ok { 71 | t.Errorf("TLSConnectionState returned ok == false; want true") 72 | } 73 | if cs.Version == 0 || !cs.HandshakeComplete { 74 | t.Errorf("ConnectionState = %#v; expected Version != 0 and HandshakeComplete = true", cs) 75 | } 76 | } 77 | 78 | func TestSearch(t *testing.T) { 79 | l, err := DialURL(ldapServer) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | defer l.Close() 84 | 85 | searchRequest := NewSearchRequest( 86 | baseDN, 87 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 88 | filter[0], 89 | attributes, 90 | nil) 91 | 92 | sr, err := l.Search(searchRequest) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | t.Logf("TestSearch: %s -> num of entries = %d", searchRequest.Filter, len(sr.Entries)) 97 | } 98 | 99 | func TestSearchStartTLS(t *testing.T) { 100 | l, err := DialURL(ldapServer) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | defer l.Close() 105 | 106 | searchRequest := NewSearchRequest( 107 | baseDN, 108 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 109 | filter[0], 110 | attributes, 111 | nil) 112 | 113 | sr, err := l.Search(searchRequest) 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | 118 | t.Logf("TestSearchStartTLS: %s -> num of entries = %d", searchRequest.Filter, len(sr.Entries)) 119 | 120 | t.Log("TestSearchStartTLS: upgrading with startTLS") 121 | err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | 126 | sr, err = l.Search(searchRequest) 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | 131 | t.Logf("TestSearchStartTLS: %s -> num of entries = %d", searchRequest.Filter, len(sr.Entries)) 132 | } 133 | 134 | func TestSearchWithPaging(t *testing.T) { 135 | l, err := DialURL(ldapServer) 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | defer l.Close() 140 | 141 | err = l.UnauthenticatedBind("") 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | 146 | searchRequest := NewSearchRequest( 147 | baseDN, 148 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 149 | filter[2], 150 | attributes, 151 | nil) 152 | sr, err := l.SearchWithPaging(searchRequest, 5) 153 | if err != nil { 154 | t.Fatal(err) 155 | } 156 | 157 | t.Logf("TestSearchWithPaging: %s -> num of entries = %d", searchRequest.Filter, len(sr.Entries)) 158 | 159 | searchRequest = NewSearchRequest( 160 | baseDN, 161 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 162 | filter[2], 163 | attributes, 164 | []Control{NewControlPaging(5)}) 165 | sr, err = l.SearchWithPaging(searchRequest, 5) 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | 170 | t.Logf("TestSearchWithPaging: %s -> num of entries = %d", searchRequest.Filter, len(sr.Entries)) 171 | 172 | searchRequest = NewSearchRequest( 173 | baseDN, 174 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 175 | filter[2], 176 | attributes, 177 | []Control{NewControlPaging(500)}) 178 | _, err = l.SearchWithPaging(searchRequest, 5) 179 | if err == nil { 180 | t.Fatal("expected an error when paging size in control in search request doesn't match size given in call, got none") 181 | } 182 | } 183 | 184 | func searchGoroutine(t *testing.T, l *Conn, results chan *SearchResult, i int) { 185 | searchRequest := NewSearchRequest( 186 | baseDN, 187 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 188 | filter[i], 189 | attributes, 190 | nil) 191 | sr, err := l.Search(searchRequest) 192 | if err != nil { 193 | t.Error(err) 194 | results <- nil 195 | return 196 | } 197 | results <- sr 198 | } 199 | 200 | func testMultiGoroutineSearch(t *testing.T, TLS bool, startTLS bool) { 201 | var l *Conn 202 | var err error 203 | if TLS { 204 | l, err = DialURL(ldapsServer, DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true})) 205 | if err != nil { 206 | t.Fatal(err) 207 | } 208 | defer l.Close() 209 | } else { 210 | l, err = DialURL(ldapServer) 211 | if err != nil { 212 | t.Fatal(err) 213 | } 214 | defer l.Close() 215 | if startTLS { 216 | t.Log("TestMultiGoroutineSearch: using StartTLS...") 217 | err := l.StartTLS(&tls.Config{InsecureSkipVerify: true}) 218 | if err != nil { 219 | t.Fatal(err) 220 | } 221 | } 222 | } 223 | 224 | results := make([]chan *SearchResult, len(filter)) 225 | for i := range filter { 226 | results[i] = make(chan *SearchResult) 227 | go searchGoroutine(t, l, results[i], i) 228 | } 229 | for i := range filter { 230 | sr := <-results[i] 231 | if sr == nil { 232 | t.Errorf("Did not receive results from goroutine for %q", filter[i]) 233 | } else { 234 | t.Logf("TestMultiGoroutineSearch(%d): %s -> num of entries = %d", i, filter[i], len(sr.Entries)) 235 | } 236 | } 237 | } 238 | 239 | func TestMultiGoroutineSearch(t *testing.T) { 240 | testMultiGoroutineSearch(t, false, false) 241 | testMultiGoroutineSearch(t, true, true) 242 | testMultiGoroutineSearch(t, false, true) 243 | } 244 | 245 | func TestEscapeFilter(t *testing.T) { 246 | if got, want := EscapeFilter("a\x00b(c)d*e\\f"), `a\00b\28c\29d\2ae\5cf`; got != want { 247 | t.Errorf("Got %s, expected %s", want, got) 248 | } 249 | if got, want := EscapeFilter("Lučić"), `Lu\c4\8di\c4\87`; got != want { 250 | t.Errorf("Got %s, expected %s", want, got) 251 | } 252 | } 253 | 254 | func TestCompare(t *testing.T) { 255 | l, err := DialURL(ldapServer) 256 | if err != nil { 257 | t.Fatal(err) 258 | } 259 | defer l.Close() 260 | 261 | const dn = "cn=math mich,ou=User Groups,ou=Groups,dc=umich,dc=edu" 262 | const attribute = "cn" 263 | const value = "math mich" 264 | 265 | sr, err := l.Compare(dn, attribute, value) 266 | if err != nil { 267 | t.Fatal(err) 268 | } 269 | 270 | t.Log("Compare result:", sr) 271 | } 272 | 273 | func TestMatchDNError(t *testing.T) { 274 | l, err := DialURL(ldapServer) 275 | if err != nil { 276 | t.Fatal(err) 277 | } 278 | defer l.Close() 279 | 280 | const wrongBase = "ou=roups,dc=umich,dc=edu" 281 | 282 | searchRequest := NewSearchRequest( 283 | wrongBase, 284 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 285 | filter[0], 286 | attributes, 287 | nil) 288 | 289 | _, err = l.Search(searchRequest) 290 | if err == nil { 291 | t.Fatal("Expected Error, got nil") 292 | } 293 | 294 | t.Log("TestMatchDNError:", err) 295 | } 296 | 297 | func Test_addControlDescriptions(t *testing.T) { 298 | type args struct { 299 | packet *ber.Packet 300 | } 301 | tests := []struct { 302 | name string 303 | args args 304 | wantErr bool 305 | }{ 306 | {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}, 307 | {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}, 308 | {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}, 309 | {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}, 310 | {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}, 311 | {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}, 312 | {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}, 313 | {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}, 314 | {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}, 315 | {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}, 316 | } 317 | for _, tt := range tests { 318 | t.Run(tt.name, func(t *testing.T) { 319 | if err := addControlDescriptions(tt.args.packet); (err != nil) != tt.wantErr { 320 | t.Errorf("addControlDescriptions() error = %v, wantErr %v", err, tt.wantErr) 321 | } 322 | }) 323 | } 324 | } 325 | 326 | func TestEscapeDN(t *testing.T) { 327 | tests := []struct { 328 | name string 329 | dn string 330 | want string 331 | }{ 332 | {name: "emptyString", dn: "", want: ""}, 333 | {name: "comma", dn: "test,user", want: "test\\,user"}, 334 | {name: "numberSign", dn: "#test#user#", want: "\\#test#user#"}, 335 | {name: "backslash", dn: "\\test\\user\\", want: "\\\\test\\\\user\\\\"}, 336 | {name: "whitespaces", dn: " test user ", want: "\\ test user \\ "}, 337 | {name: "nullByte", dn: "\u0000te\x00st\x00user" + string(rune(0)), want: "\\00te\\00st\\00user\\00"}, 338 | {name: "variousCharacters", dn: "test\"+,;<>\\-_user", want: "test\\\"\\+\\,\\;\\<\\>\\\\-_user"}, 339 | {name: "multiByteRunes", dn: "test\u0391user ", want: "test\u0391user\\ "}, 340 | } 341 | for _, tt := range tests { 342 | t.Run(tt.name, func(t *testing.T) { 343 | if got := EscapeDN(tt.dn); got != tt.want { 344 | t.Errorf("EscapeDN(%s) = %s, expected %s", tt.dn, got, tt.want) 345 | } 346 | }) 347 | } 348 | } 349 | 350 | func TestSearchAsync(t *testing.T) { 351 | l, err := DialURL(ldapServer) 352 | if err != nil { 353 | t.Fatal(err) 354 | } 355 | defer l.Close() 356 | 357 | searchRequest := NewSearchRequest( 358 | baseDN, 359 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 360 | filter[2], 361 | attributes, 362 | nil) 363 | 364 | srs := make([]*Entry, 0) 365 | ctx := context.Background() 366 | r := l.SearchAsync(ctx, searchRequest, 64) 367 | for r.Next() { 368 | srs = append(srs, r.Entry()) 369 | } 370 | if err := r.Err(); err != nil { 371 | log.Fatal(err) 372 | } 373 | 374 | t.Logf("TestSearcAsync: %s -> num of entries = %d", searchRequest.Filter, len(srs)) 375 | } 376 | 377 | func TestSearchAsyncAndCancel(t *testing.T) { 378 | l, err := DialURL(ldapServer) 379 | if err != nil { 380 | t.Fatal(err) 381 | } 382 | defer l.Close() 383 | 384 | searchRequest := NewSearchRequest( 385 | baseDN, 386 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 387 | filter[2], 388 | attributes, 389 | nil) 390 | 391 | cancelNum := 10 392 | srs := make([]*Entry, 0) 393 | ctx, cancel := context.WithCancel(context.Background()) 394 | defer cancel() 395 | r := l.SearchAsync(ctx, searchRequest, 0) 396 | for r.Next() { 397 | srs = append(srs, r.Entry()) 398 | if len(srs) == cancelNum { 399 | cancel() 400 | } 401 | } 402 | if err := r.Err(); err != nil { 403 | log.Fatal(err) 404 | } 405 | 406 | if len(srs) > cancelNum+3 { 407 | // the cancellation process is asynchronous, 408 | // so it might get some entries after calling cancel() 409 | t.Errorf("Got entries %d, expected < %d", len(srs), cancelNum+3) 410 | } 411 | t.Logf("TestSearchAsyncAndCancel: %s -> num of entries = %d", searchRequest.Filter, len(srs)) 412 | } 413 | -------------------------------------------------------------------------------- /v3/moddn.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | ber "github.com/go-asn1-ber/asn1-ber" 6 | ) 7 | 8 | // ModifyDNRequest holds the request to modify a DN 9 | type ModifyDNRequest struct { 10 | DN string 11 | NewRDN string 12 | DeleteOldRDN bool 13 | NewSuperior string 14 | // Controls hold optional controls to send with the request 15 | Controls []Control 16 | } 17 | 18 | // NewModifyDNRequest creates a new request which can be passed to ModifyDN(). 19 | // 20 | // To move an object in the tree, set the "newSup" to the new parent entry DN. Use an 21 | // empty string for just changing the object's RDN. 22 | // 23 | // For moving the object without renaming, the "rdn" must be the first 24 | // RDN of the given DN. 25 | // 26 | // A call like 27 | // 28 | // mdnReq := NewModifyDNRequest("uid=someone,dc=example,dc=org", "uid=newname", true, "") 29 | // 30 | // will setup the request to just rename uid=someone,dc=example,dc=org to 31 | // uid=newname,dc=example,dc=org. 32 | func NewModifyDNRequest(dn string, rdn string, delOld bool, newSup string) *ModifyDNRequest { 33 | return &ModifyDNRequest{ 34 | DN: dn, 35 | NewRDN: rdn, 36 | DeleteOldRDN: delOld, 37 | NewSuperior: newSup, 38 | } 39 | } 40 | 41 | // NewModifyDNWithControlsRequest creates a new request which can be passed to ModifyDN() 42 | // and also allows setting LDAP request controls. 43 | // 44 | // Refer NewModifyDNRequest for other parameters 45 | func NewModifyDNWithControlsRequest(dn string, rdn string, delOld bool, 46 | newSup string, controls []Control) *ModifyDNRequest { 47 | return &ModifyDNRequest{ 48 | DN: dn, 49 | NewRDN: rdn, 50 | DeleteOldRDN: delOld, 51 | NewSuperior: newSup, 52 | Controls: controls, 53 | } 54 | } 55 | 56 | func (req *ModifyDNRequest) appendTo(envelope *ber.Packet) error { 57 | pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationModifyDNRequest, nil, "Modify DN Request") 58 | pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.DN, "DN")) 59 | pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.NewRDN, "New RDN")) 60 | if req.DeleteOldRDN { 61 | buf := []byte{0xff} 62 | pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, string(buf), "Delete old RDN")) 63 | } else { 64 | pkt.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, req.DeleteOldRDN, "Delete old RDN")) 65 | } 66 | if req.NewSuperior != "" { 67 | pkt.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, req.NewSuperior, "New Superior")) 68 | } 69 | 70 | envelope.AppendChild(pkt) 71 | if len(req.Controls) > 0 { 72 | envelope.AppendChild(encodeControls(req.Controls)) 73 | } 74 | 75 | return nil 76 | } 77 | 78 | // ModifyDN renames the given DN and optionally move to another base (when the "newSup" argument 79 | // to NewModifyDNRequest() is not ""). 80 | func (l *Conn) ModifyDN(m *ModifyDNRequest) error { 81 | msgCtx, err := l.doRequest(m) 82 | if err != nil { 83 | return err 84 | } 85 | defer l.finishMessage(msgCtx) 86 | 87 | packet, err := l.readPacket(msgCtx) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | if packet.Children[1].Tag == ApplicationModifyDNResponse { 93 | err := GetLDAPError(packet) 94 | if err != nil { 95 | return err 96 | } 97 | } else { 98 | return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) 99 | } 100 | 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /v3/modify.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | ber "github.com/go-asn1-ber/asn1-ber" 8 | ) 9 | 10 | // Change operation choices 11 | const ( 12 | AddAttribute = 0 13 | DeleteAttribute = 1 14 | ReplaceAttribute = 2 15 | IncrementAttribute = 3 // (https://tools.ietf.org/html/rfc4525) 16 | ) 17 | 18 | // PartialAttribute for a ModifyRequest as defined in https://tools.ietf.org/html/rfc4511 19 | type PartialAttribute struct { 20 | // Type is the type of the partial attribute 21 | Type string 22 | // Vals are the values of the partial attribute 23 | Vals []string 24 | } 25 | 26 | func (p *PartialAttribute) encode() *ber.Packet { 27 | seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "PartialAttribute") 28 | seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, p.Type, "Type")) 29 | set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") 30 | for _, value := range p.Vals { 31 | set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) 32 | } 33 | seq.AppendChild(set) 34 | return seq 35 | } 36 | 37 | // Change for a ModifyRequest as defined in https://tools.ietf.org/html/rfc4511 38 | type Change struct { 39 | // Operation is the type of change to be made 40 | Operation uint 41 | // Modification is the attribute to be modified 42 | Modification PartialAttribute 43 | } 44 | 45 | func (c *Change) encode() *ber.Packet { 46 | change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") 47 | change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(c.Operation), "Operation")) 48 | change.AppendChild(c.Modification.encode()) 49 | return change 50 | } 51 | 52 | // ModifyRequest as defined in https://tools.ietf.org/html/rfc4511 53 | type ModifyRequest struct { 54 | // DN is the distinguishedName of the directory entry to modify 55 | DN string 56 | // Changes contain the attributes to modify 57 | Changes []Change 58 | // Controls hold optional controls to send with the request 59 | Controls []Control 60 | } 61 | 62 | // Add appends the given attribute to the list of changes to be made 63 | func (req *ModifyRequest) Add(attrType string, attrVals []string) { 64 | req.appendChange(AddAttribute, attrType, attrVals) 65 | } 66 | 67 | // Delete appends the given attribute to the list of changes to be made 68 | func (req *ModifyRequest) Delete(attrType string, attrVals []string) { 69 | req.appendChange(DeleteAttribute, attrType, attrVals) 70 | } 71 | 72 | // Replace appends the given attribute to the list of changes to be made 73 | func (req *ModifyRequest) Replace(attrType string, attrVals []string) { 74 | req.appendChange(ReplaceAttribute, attrType, attrVals) 75 | } 76 | 77 | // Increment appends the given attribute to the list of changes to be made 78 | func (req *ModifyRequest) Increment(attrType string, attrVal string) { 79 | req.appendChange(IncrementAttribute, attrType, []string{attrVal}) 80 | } 81 | 82 | func (req *ModifyRequest) appendChange(operation uint, attrType string, attrVals []string) { 83 | req.Changes = append(req.Changes, Change{operation, PartialAttribute{Type: attrType, Vals: attrVals}}) 84 | } 85 | 86 | func (req *ModifyRequest) appendTo(envelope *ber.Packet) error { 87 | pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationModifyRequest, nil, "Modify Request") 88 | pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, req.DN, "DN")) 89 | changes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Changes") 90 | for _, change := range req.Changes { 91 | changes.AppendChild(change.encode()) 92 | } 93 | pkt.AppendChild(changes) 94 | 95 | envelope.AppendChild(pkt) 96 | if len(req.Controls) > 0 { 97 | envelope.AppendChild(encodeControls(req.Controls)) 98 | } 99 | 100 | return nil 101 | } 102 | 103 | // NewModifyRequest creates a modify request for the given DN 104 | func NewModifyRequest(dn string, controls []Control) *ModifyRequest { 105 | return &ModifyRequest{ 106 | DN: dn, 107 | Controls: controls, 108 | } 109 | } 110 | 111 | // Modify performs the ModifyRequest 112 | func (l *Conn) Modify(modifyRequest *ModifyRequest) error { 113 | msgCtx, err := l.doRequest(modifyRequest) 114 | if err != nil { 115 | return err 116 | } 117 | defer l.finishMessage(msgCtx) 118 | 119 | packet, err := l.readPacket(msgCtx) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | if packet.Children[1].Tag == ApplicationModifyResponse { 125 | err := GetLDAPError(packet) 126 | if err != nil { 127 | return err 128 | } 129 | } else { 130 | return fmt.Errorf("ldap: unexpected response: %d", packet.Children[1].Tag) 131 | } 132 | 133 | return nil 134 | } 135 | 136 | // ModifyResult holds the server's response to a modify request 137 | type ModifyResult struct { 138 | // Controls are the returned controls 139 | Controls []Control 140 | // Referral is the returned referral 141 | Referral string 142 | } 143 | 144 | // ModifyWithResult performs the ModifyRequest and returns the result 145 | func (l *Conn) ModifyWithResult(modifyRequest *ModifyRequest) (*ModifyResult, error) { 146 | msgCtx, err := l.doRequest(modifyRequest) 147 | if err != nil { 148 | return nil, err 149 | } 150 | defer l.finishMessage(msgCtx) 151 | 152 | result := &ModifyResult{ 153 | Controls: make([]Control, 0), 154 | } 155 | 156 | l.Debug.Printf("%d: waiting for response", msgCtx.id) 157 | packet, err := l.readPacket(msgCtx) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | switch packet.Children[1].Tag { 163 | case ApplicationModifyResponse: 164 | if err = GetLDAPError(packet); err != nil { 165 | result.Referral = getReferral(err, packet) 166 | 167 | return result, err 168 | } 169 | if len(packet.Children) == 3 { 170 | for _, child := range packet.Children[2].Children { 171 | decodedChild, err := DecodeControl(child) 172 | if err != nil { 173 | return nil, errors.New("failed to decode child control: " + err.Error()) 174 | } 175 | result.Controls = append(result.Controls, decodedChild) 176 | } 177 | } 178 | } 179 | l.Debug.Printf("%d: returning", msgCtx.id) 180 | return result, nil 181 | } 182 | -------------------------------------------------------------------------------- /v3/passwdmodify.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | 6 | ber "github.com/go-asn1-ber/asn1-ber" 7 | ) 8 | 9 | const ( 10 | passwordModifyOID = "1.3.6.1.4.1.4203.1.11.1" 11 | ) 12 | 13 | // PasswordModifyRequest implements the Password Modify Extended Operation as defined in https://www.ietf.org/rfc/rfc3062.txt 14 | type PasswordModifyRequest struct { 15 | // UserIdentity is an optional string representation of the user associated with the request. 16 | // This string may or may not be an LDAPDN [RFC2253]. 17 | // If no UserIdentity field is present, the request acts up upon the password of the user currently associated with the LDAP session 18 | UserIdentity string 19 | // OldPassword, if present, contains the user's current password 20 | OldPassword string 21 | // NewPassword, if present, contains the desired password for this user 22 | NewPassword string 23 | } 24 | 25 | // PasswordModifyResult holds the server response to a PasswordModifyRequest 26 | type PasswordModifyResult struct { 27 | // GeneratedPassword holds a password generated by the server, if present 28 | GeneratedPassword string 29 | // Referral are the returned referral 30 | Referral string 31 | } 32 | 33 | func (req *PasswordModifyRequest) appendTo(envelope *ber.Packet) error { 34 | pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Password Modify Extended Operation") 35 | pkt.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, passwordModifyOID, "Extended Request Name: Password Modify OID")) 36 | 37 | extendedRequestValue := ber.Encode(ber.ClassContext, ber.TypePrimitive, 1, nil, "Extended Request Value: Password Modify Request") 38 | passwordModifyRequestValue := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Password Modify Request") 39 | if req.UserIdentity != "" { 40 | passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, req.UserIdentity, "User Identity")) 41 | } 42 | if req.OldPassword != "" { 43 | passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 1, req.OldPassword, "Old Password")) 44 | } 45 | if req.NewPassword != "" { 46 | passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 2, req.NewPassword, "New Password")) 47 | } 48 | extendedRequestValue.AppendChild(passwordModifyRequestValue) 49 | 50 | pkt.AppendChild(extendedRequestValue) 51 | 52 | envelope.AppendChild(pkt) 53 | 54 | return nil 55 | } 56 | 57 | // NewPasswordModifyRequest creates a new PasswordModifyRequest 58 | // 59 | // According to the RFC 3602 (https://tools.ietf.org/html/rfc3062): 60 | // userIdentity is a string representing the user associated with the request. 61 | // This string may or may not be an LDAPDN (RFC 2253). 62 | // If userIdentity is empty then the operation will act on the user associated 63 | // with the session. 64 | // 65 | // oldPassword is the current user's password, it can be empty or it can be 66 | // needed depending on the session user access rights (usually an administrator 67 | // can change a user's password without knowing the current one) and the 68 | // password policy (see pwdSafeModify password policy's attribute) 69 | // 70 | // newPassword is the desired user's password. If empty the server can return 71 | // an error or generate a new password that will be available in the 72 | // PasswordModifyResult.GeneratedPassword 73 | func NewPasswordModifyRequest(userIdentity string, oldPassword string, newPassword string) *PasswordModifyRequest { 74 | return &PasswordModifyRequest{ 75 | UserIdentity: userIdentity, 76 | OldPassword: oldPassword, 77 | NewPassword: newPassword, 78 | } 79 | } 80 | 81 | // PasswordModify performs the modification request 82 | func (l *Conn) PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*PasswordModifyResult, error) { 83 | msgCtx, err := l.doRequest(passwordModifyRequest) 84 | if err != nil { 85 | return nil, err 86 | } 87 | defer l.finishMessage(msgCtx) 88 | 89 | packet, err := l.readPacket(msgCtx) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | result := &PasswordModifyResult{} 95 | 96 | if packet.Children[1].Tag == ApplicationExtendedResponse { 97 | if err = GetLDAPError(packet); err != nil { 98 | result.Referral = getReferral(err, packet) 99 | 100 | return result, err 101 | } 102 | } else { 103 | return nil, NewError(ErrorUnexpectedResponse, fmt.Errorf("unexpected Response: %d", packet.Children[1].Tag)) 104 | } 105 | 106 | extendedResponse := packet.Children[1] 107 | for _, child := range extendedResponse.Children { 108 | if child.Tag == ber.TagEmbeddedPDV { 109 | passwordModifyResponseValue := ber.DecodePacket(child.Data.Bytes()) 110 | if len(passwordModifyResponseValue.Children) == 1 { 111 | if passwordModifyResponseValue.Children[0].Tag == ber.TagEOC { 112 | result.GeneratedPassword = ber.DecodeString(passwordModifyResponseValue.Children[0].Data.Bytes()) 113 | } 114 | } 115 | } 116 | } 117 | 118 | return result, nil 119 | } 120 | -------------------------------------------------------------------------------- /v3/request.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "errors" 5 | 6 | ber "github.com/go-asn1-ber/asn1-ber" 7 | ) 8 | 9 | var ( 10 | errRespChanClosed = errors.New("ldap: response channel closed") 11 | errCouldNotRetMsg = errors.New("ldap: could not retrieve message") 12 | // ErrNilConnection is returned if doRequest is called with a nil connection. 13 | ErrNilConnection = errors.New("ldap: conn is nil, expected net.Conn") 14 | ) 15 | 16 | type request interface { 17 | appendTo(*ber.Packet) error 18 | } 19 | 20 | type requestFunc func(*ber.Packet) error 21 | 22 | func (f requestFunc) appendTo(p *ber.Packet) error { 23 | return f(p) 24 | } 25 | 26 | func (l *Conn) doRequest(req request) (*messageContext, error) { 27 | if l == nil || l.conn == nil { 28 | return nil, ErrNilConnection 29 | } 30 | 31 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 32 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) 33 | if err := req.appendTo(packet); err != nil { 34 | return nil, err 35 | } 36 | 37 | if l.Debug { 38 | l.Debug.PrintPacket(packet) 39 | } 40 | 41 | msgCtx, err := l.sendMessage(packet) 42 | if err != nil { 43 | return nil, err 44 | } 45 | l.Debug.Printf("%d: returning", msgCtx.id) 46 | return msgCtx, nil 47 | } 48 | 49 | func (l *Conn) readPacket(msgCtx *messageContext) (*ber.Packet, error) { 50 | l.Debug.Printf("%d: waiting for response", msgCtx.id) 51 | packetResponse, ok := <-msgCtx.responses 52 | if !ok { 53 | return nil, NewError(ErrorNetwork, errRespChanClosed) 54 | } 55 | packet, err := packetResponse.ReadPacket() 56 | l.Debug.Printf("%d: got response %p", msgCtx.id, packet) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | if packet == nil { 62 | return nil, NewError(ErrorNetwork, errCouldNotRetMsg) 63 | } 64 | 65 | if l.Debug { 66 | if err = addLDAPDescriptions(packet); err != nil { 67 | return nil, err 68 | } 69 | l.Debug.PrintPacket(packet) 70 | } 71 | return packet, nil 72 | } 73 | 74 | func getReferral(err error, packet *ber.Packet) (referral string) { 75 | if !IsErrorWithCode(err, LDAPResultReferral) { 76 | return "" 77 | } 78 | 79 | if len(packet.Children) < 2 { 80 | return "" 81 | } 82 | 83 | // The packet Tag itself (of child 2) is generally a ber.TagObjectDescriptor with referrals however OpenLDAP 84 | // seemingly returns a ber.Tag.GeneralizedTime. Every currently tested LDAP server which returns referrals returns 85 | // an ASN.1 BER packet with the Type of ber.TypeConstructed and Class of ber.ClassApplication however. Thus this 86 | // check expressly checks these fields instead. 87 | // 88 | // Related Issues: 89 | // - https://github.com/authelia/authelia/issues/4199 (downstream) 90 | if len(packet.Children[1].Children) == 0 || (packet.Children[1].TagType != ber.TypeConstructed || packet.Children[1].ClassType != ber.ClassApplication) { 91 | return "" 92 | } 93 | 94 | var ok bool 95 | 96 | for _, child := range packet.Children[1].Children { 97 | // The referral URI itself should be contained within a child which has a Tag of ber.BitString or 98 | // ber.TagPrintableString, and the Type of ber.TypeConstructed and the Class of ClassContext. As soon as any of 99 | // these conditions is not true we can skip this child. 100 | if (child.Tag != ber.TagBitString && child.Tag != ber.TagPrintableString) || child.TagType != ber.TypeConstructed || child.ClassType != ber.ClassContext { 101 | continue 102 | } 103 | 104 | if referral, ok = child.Children[0].Value.(string); ok { 105 | return referral 106 | } 107 | } 108 | 109 | return "" 110 | } 111 | -------------------------------------------------------------------------------- /v3/response.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | ber "github.com/go-asn1-ber/asn1-ber" 9 | ) 10 | 11 | // Response defines an interface to get data from an LDAP server 12 | type Response interface { 13 | Entry() *Entry 14 | Referral() string 15 | Controls() []Control 16 | Err() error 17 | Next() bool 18 | } 19 | 20 | type searchResponse struct { 21 | conn *Conn 22 | ch chan *SearchSingleResult 23 | 24 | entry *Entry 25 | referral string 26 | controls []Control 27 | err error 28 | } 29 | 30 | // Entry returns an entry from the given search request 31 | func (r *searchResponse) Entry() *Entry { 32 | return r.entry 33 | } 34 | 35 | // Referral returns a referral from the given search request 36 | func (r *searchResponse) Referral() string { 37 | return r.referral 38 | } 39 | 40 | // Controls returns controls from the given search request 41 | func (r *searchResponse) Controls() []Control { 42 | return r.controls 43 | } 44 | 45 | // Err returns an error when the given search request was failed 46 | func (r *searchResponse) Err() error { 47 | return r.err 48 | } 49 | 50 | // Next returns whether next data exist or not 51 | func (r *searchResponse) Next() bool { 52 | res, ok := <-r.ch 53 | if !ok { 54 | return false 55 | } 56 | if res == nil { 57 | return false 58 | } 59 | r.err = res.Error 60 | if r.err != nil { 61 | return false 62 | } 63 | r.entry = res.Entry 64 | r.referral = res.Referral 65 | r.controls = res.Controls 66 | return true 67 | } 68 | 69 | func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest) { 70 | go func() { 71 | defer func() { 72 | close(r.ch) 73 | if err := recover(); err != nil { 74 | r.conn.err = fmt.Errorf("ldap: recovered panic in searchResponse: %v", err) 75 | } 76 | }() 77 | 78 | if r.conn.IsClosing() { 79 | return 80 | } 81 | 82 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 83 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, r.conn.nextMessageID(), "MessageID")) 84 | // encode search request 85 | err := searchRequest.appendTo(packet) 86 | if err != nil { 87 | r.ch <- &SearchSingleResult{Error: err} 88 | return 89 | } 90 | r.conn.Debug.PrintPacket(packet) 91 | 92 | msgCtx, err := r.conn.sendMessage(packet) 93 | if err != nil { 94 | r.ch <- &SearchSingleResult{Error: err} 95 | return 96 | } 97 | defer r.conn.finishMessage(msgCtx) 98 | 99 | foundSearchSingleResultDone := false 100 | for !foundSearchSingleResultDone { 101 | r.conn.Debug.Printf("%d: waiting for response", msgCtx.id) 102 | select { 103 | case <-ctx.Done(): 104 | r.conn.Debug.Printf("%d: %s", msgCtx.id, ctx.Err().Error()) 105 | return 106 | case packetResponse, ok := <-msgCtx.responses: 107 | if !ok { 108 | err := NewError(ErrorNetwork, errors.New("ldap: response channel closed")) 109 | r.ch <- &SearchSingleResult{Error: err} 110 | return 111 | } 112 | packet, err = packetResponse.ReadPacket() 113 | r.conn.Debug.Printf("%d: got response %p", msgCtx.id, packet) 114 | if err != nil { 115 | r.ch <- &SearchSingleResult{Error: err} 116 | return 117 | } 118 | 119 | if r.conn.Debug { 120 | if err := addLDAPDescriptions(packet); err != nil { 121 | r.ch <- &SearchSingleResult{Error: err} 122 | return 123 | } 124 | ber.PrintPacket(packet) 125 | } 126 | 127 | switch packet.Children[1].Tag { 128 | case ApplicationSearchResultEntry: 129 | result := &SearchSingleResult{ 130 | Entry: &Entry{ 131 | DN: packet.Children[1].Children[0].Value.(string), 132 | Attributes: unpackAttributes(packet.Children[1].Children[1].Children), 133 | }, 134 | } 135 | if len(packet.Children) != 3 { 136 | r.ch <- result 137 | continue 138 | } 139 | decoded, err := DecodeControl(packet.Children[2].Children[0]) 140 | if err != nil { 141 | werr := fmt.Errorf("failed to decode search result entry: %w", err) 142 | result.Error = werr 143 | r.ch <- result 144 | return 145 | } 146 | result.Controls = append(result.Controls, decoded) 147 | r.ch <- result 148 | 149 | case ApplicationSearchResultDone: 150 | if err := GetLDAPError(packet); err != nil { 151 | r.ch <- &SearchSingleResult{Error: err} 152 | return 153 | } 154 | if len(packet.Children) == 3 { 155 | result := &SearchSingleResult{} 156 | for _, child := range packet.Children[2].Children { 157 | decodedChild, err := DecodeControl(child) 158 | if err != nil { 159 | werr := fmt.Errorf("failed to decode child control: %w", err) 160 | r.ch <- &SearchSingleResult{Error: werr} 161 | return 162 | } 163 | result.Controls = append(result.Controls, decodedChild) 164 | } 165 | r.ch <- result 166 | } 167 | foundSearchSingleResultDone = true 168 | 169 | case ApplicationSearchResultReference: 170 | ref := packet.Children[1].Children[0].Value.(string) 171 | r.ch <- &SearchSingleResult{Referral: ref} 172 | 173 | case ApplicationIntermediateResponse: 174 | decoded, err := DecodeControl(packet.Children[1]) 175 | if err != nil { 176 | werr := fmt.Errorf("failed to decode intermediate response: %w", err) 177 | r.ch <- &SearchSingleResult{Error: werr} 178 | return 179 | } 180 | result := &SearchSingleResult{} 181 | result.Controls = append(result.Controls, decoded) 182 | r.ch <- result 183 | 184 | default: 185 | err := fmt.Errorf("unknown tag: %d", packet.Children[1].Tag) 186 | r.ch <- &SearchSingleResult{Error: err} 187 | return 188 | } 189 | } 190 | } 191 | r.conn.Debug.Printf("%d: returning", msgCtx.id) 192 | }() 193 | } 194 | 195 | func newSearchResponse(conn *Conn, bufferSize int) *searchResponse { 196 | var ch chan *SearchSingleResult 197 | if bufferSize > 0 { 198 | ch = make(chan *SearchSingleResult, bufferSize) 199 | } else { 200 | ch = make(chan *SearchSingleResult) 201 | } 202 | return &searchResponse{ 203 | conn: conn, 204 | ch: ch, 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /v3/search_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | // TestNewEntry tests that repeated calls to NewEntry return the same value with the same input 13 | func TestNewEntry(t *testing.T) { 14 | dn := "testDN" 15 | attributes := map[string][]string{ 16 | "alpha": {"value"}, 17 | "beta": {"value"}, 18 | "gamma": {"value"}, 19 | "delta": {"value"}, 20 | "epsilon": {"value"}, 21 | } 22 | executedEntry := NewEntry(dn, attributes) 23 | 24 | iteration := 0 25 | for { 26 | if iteration == 100 { 27 | break 28 | } 29 | testEntry := NewEntry(dn, attributes) 30 | if !reflect.DeepEqual(executedEntry, testEntry) { 31 | 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) 32 | } 33 | iteration = iteration + 1 34 | } 35 | } 36 | 37 | func TestGetAttributeValue(t *testing.T) { 38 | dn := "testDN" 39 | attributes := map[string][]string{ 40 | "Alpha": {"value"}, 41 | "bEta": {"value"}, 42 | "gaMma": {"value"}, 43 | "delTa": {"value"}, 44 | "epsiLon": {"value"}, 45 | } 46 | entry := NewEntry(dn, attributes) 47 | if entry.GetAttributeValue("Alpha") != "value" { 48 | t.Errorf("failed to get attribute in original case") 49 | } 50 | 51 | if entry.GetEqualFoldAttributeValue("alpha") != "value" { 52 | t.Errorf("failed to get attribute in changed case") 53 | } 54 | } 55 | 56 | func TestEntry_Unmarshal(t *testing.T) { 57 | t.Run("passing a struct should fail", func(t *testing.T) { 58 | entry := &Entry{} 59 | 60 | type toStruct struct{} 61 | 62 | result := toStruct{} 63 | err := entry.Unmarshal(result) 64 | 65 | assert.NotNil(t, err) 66 | }) 67 | 68 | t.Run("passing a ptr to string should fail", func(t *testing.T) { 69 | entry := &Entry{} 70 | 71 | str := "foo" 72 | err := entry.Unmarshal(&str) 73 | 74 | assert.NotNil(t, err) 75 | }) 76 | 77 | t.Run("user struct be decoded", func(t *testing.T) { 78 | entry := &Entry{ 79 | DN: "cn=mario,ou=Users,dc=go-ldap,dc=github,dc=com", 80 | Attributes: []*EntryAttribute{ 81 | { 82 | Name: "cn", 83 | Values: []string{"mario"}, 84 | ByteValues: nil, 85 | }, 86 | { 87 | Name: "mail", 88 | Values: []string{"mario@go-ldap.com"}, 89 | ByteValues: nil, 90 | }, 91 | { 92 | Name: "upn", 93 | Values: []string{"mario@go-ldap.com.domain"}, 94 | ByteValues: nil, 95 | }, 96 | // Tests int value. 97 | { 98 | Name: "id", 99 | Values: []string{"2147483647"}, 100 | ByteValues: nil, 101 | }, 102 | // Tests int64 value. 103 | { 104 | Name: "longId", 105 | Values: []string{"9223372036854775807"}, 106 | ByteValues: nil, 107 | }, 108 | // Tests []byte value. 109 | { 110 | Name: "data", 111 | Values: []string{"data"}, 112 | ByteValues: [][]byte{ 113 | []byte("data"), 114 | }, 115 | }, 116 | // Tests time.Time value. 117 | { 118 | Name: "createdTimestamp", 119 | Values: []string{"202305041930Z"}, 120 | ByteValues: nil, 121 | }, 122 | // Tests *DN value 123 | { 124 | Name: "owner", 125 | Values: []string{"uid=foo,dc=example,dc=org"}, 126 | ByteValues: nil, 127 | }, 128 | // Tests []*DN value 129 | { 130 | Name: "children", 131 | Values: []string{"uid=bar,dc=example,dc=org", "uid=baz,dc=example,dc=org"}, 132 | ByteValues: nil, 133 | }, 134 | }, 135 | } 136 | 137 | type User struct { 138 | Dn string `ldap:"dn"` 139 | Cn string `ldap:"cn"` 140 | Mail string `ldap:"mail"` 141 | UPN *string `ldap:"upn"` 142 | ID int `ldap:"id"` 143 | LongID int64 `ldap:"longId"` 144 | Data []byte `ldap:"data"` 145 | Created time.Time `ldap:"createdTimestamp"` 146 | Owner *DN `ldap:"owner"` 147 | Children []*DN `ldap:"children"` 148 | } 149 | 150 | created, err := time.Parse("200601021504Z", "202305041930Z") 151 | if err != nil { 152 | t.Errorf("failed to parse ref time: %s", err) 153 | } 154 | owner, err := ParseDN("uid=foo,dc=example,dc=org") 155 | if err != nil { 156 | t.Errorf("failed to parse ref DN: %s", err) 157 | } 158 | var children []*DN 159 | for _, child := range []string{"uid=bar,dc=example,dc=org", "uid=baz,dc=example,dc=org"} { 160 | dn, err := ParseDN(child) 161 | if err != nil { 162 | t.Errorf("failed to parse child ref DN: %s", err) 163 | } 164 | children = append(children, dn) 165 | } 166 | 167 | UPN := "mario@go-ldap.com.domain" 168 | expect := &User{ 169 | Dn: "cn=mario,ou=Users,dc=go-ldap,dc=github,dc=com", 170 | Cn: "mario", 171 | Mail: "mario@go-ldap.com", 172 | UPN: &UPN, 173 | ID: 2147483647, 174 | LongID: 9223372036854775807, 175 | Data: []byte("data"), 176 | Created: created, 177 | Owner: owner, 178 | Children: children, 179 | } 180 | result := &User{} 181 | err = entry.Unmarshal(result) 182 | 183 | assert.Nil(t, err) 184 | assert.Equal(t, expect, result) 185 | }) 186 | 187 | t.Run("group struct be decoded", func(t *testing.T) { 188 | entry := &Entry{ 189 | DN: "cn=DREAM_TEAM,ou=Groups,dc=go-ldap,dc=github,dc=com", 190 | Attributes: []*EntryAttribute{ 191 | { 192 | Name: "cn", 193 | Values: []string{"DREAM_TEAM"}, 194 | ByteValues: nil, 195 | }, 196 | { 197 | Name: "member", 198 | Values: []string{"mario", "luigi", "browser"}, 199 | ByteValues: nil, 200 | }, 201 | }, 202 | } 203 | 204 | type Group struct { 205 | DN string `ldap:"dn" yaml:"dn" json:"dn"` 206 | CN string `ldap:"cn" yaml:"cn" json:"cn"` 207 | Members []string `ldap:"member"` 208 | } 209 | 210 | expect := &Group{ 211 | DN: "cn=DREAM_TEAM,ou=Groups,dc=go-ldap,dc=github,dc=com", 212 | CN: "DREAM_TEAM", 213 | Members: []string{"mario", "luigi", "browser"}, 214 | } 215 | 216 | result := &Group{} 217 | err := entry.Unmarshal(result) 218 | 219 | assert.Nil(t, err) 220 | assert.Equal(t, expect, result) 221 | }) 222 | } 223 | 224 | func TestEntry_UnmarshalFunc(t *testing.T) { 225 | conn, err := DialURL(ldapServer) 226 | if err != nil { 227 | t.Fatalf("Failed to connect: %s\n", err) 228 | } 229 | defer conn.Close() 230 | 231 | searchResult, err := conn.Search(&SearchRequest{ 232 | BaseDN: baseDN, 233 | Scope: ScopeWholeSubtree, 234 | Filter: "(cn=cis-fac)", 235 | Attributes: []string{"cn", "objectClass"}, 236 | }) 237 | if err != nil { 238 | t.Fatalf("Failed to search: %s\n", err) 239 | } 240 | 241 | type user struct { 242 | ObjectClass string `custom_tag:"objectClass"` 243 | CN string `custom_tag:"cn"` 244 | } 245 | 246 | t.Run("expect custom unmarshal function to be successfull", func(t *testing.T) { 247 | for _, entry := range searchResult.Entries { 248 | var u user 249 | if err := entry.UnmarshalFunc(&u, func(entry *Entry, fieldType reflect.StructField, fieldValue reflect.Value) error { 250 | tagData, ok := fieldType.Tag.Lookup("custom_tag") 251 | if !ok { 252 | return nil 253 | } 254 | 255 | value := entry.GetAttributeValue(tagData) 256 | // log.Printf("Marshaling field %s with tag %s and value '%s'", fieldType.Name, tagData, value) 257 | fieldValue.SetString(value) 258 | return nil 259 | }); err != nil { 260 | t.Errorf("Failed to unmarshal entry: %s\n", err) 261 | } 262 | 263 | if u.CN != entry.GetAttributeValue("cn") { 264 | t.Errorf("UnmarshalFunc did not set the field correctly. Expected: %s, got: %s", entry.GetAttributeValue("cn"), u.CN) 265 | } 266 | } 267 | }) 268 | 269 | t.Run("expect an error within the custom unmarshal function", func(t *testing.T) { 270 | for _, entry := range searchResult.Entries { 271 | var u user 272 | err := entry.UnmarshalFunc(&u, func(entry *Entry, fieldType reflect.StructField, fieldValue reflect.Value) error { 273 | return fmt.Errorf("error from custom unmarshal func on field: %s", fieldType.Name) 274 | }) 275 | if err == nil { 276 | t.Errorf("UnmarshalFunc should have returned an error") 277 | } 278 | } 279 | }) 280 | } 281 | -------------------------------------------------------------------------------- /v3/unbind.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "errors" 5 | 6 | ber "github.com/go-asn1-ber/asn1-ber" 7 | ) 8 | 9 | // ErrConnUnbound is returned when Unbind is called on an already closing connection. 10 | var ErrConnUnbound = NewError(ErrorNetwork, errors.New("ldap: connection is closed")) 11 | 12 | type unbindRequest struct{} 13 | 14 | func (unbindRequest) appendTo(envelope *ber.Packet) error { 15 | envelope.AppendChild(ber.Encode(ber.ClassApplication, ber.TypePrimitive, ApplicationUnbindRequest, nil, ApplicationMap[ApplicationUnbindRequest])) 16 | return nil 17 | } 18 | 19 | // Unbind will perform an unbind request. The Unbind operation 20 | // should be thought of as the "quit" operation. 21 | // See https://datatracker.ietf.org/doc/html/rfc4511#section-4.3 22 | func (l *Conn) Unbind() error { 23 | if l.IsClosing() { 24 | return ErrConnUnbound 25 | } 26 | 27 | _, err := l.doRequest(unbindRequest{}) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | // Sending an unbindRequest will make the connection unusable. 33 | // Pending requests will fail with: 34 | // LDAP Result Code 200 "Network Error": ldap: response channel closed 35 | l.Close() 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /v3/whoami.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | // This file contains the "Who Am I?" extended operation as specified in rfc 4532 4 | // 5 | // https://tools.ietf.org/html/rfc4532 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | 11 | ber "github.com/go-asn1-ber/asn1-ber" 12 | ) 13 | 14 | type whoAmIRequest bool 15 | 16 | // WhoAmIResult is returned by the WhoAmI() call 17 | type WhoAmIResult struct { 18 | AuthzID string 19 | } 20 | 21 | func (r whoAmIRequest) encode() (*ber.Packet, error) { 22 | request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Who Am I? Extended Operation") 23 | request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, ControlTypeWhoAmI, "Extended Request Name: Who Am I? OID")) 24 | return request, nil 25 | } 26 | 27 | // WhoAmI returns the authzId the server thinks we are, you may pass controls 28 | // like a Proxied Authorization control 29 | func (l *Conn) WhoAmI(controls []Control) (*WhoAmIResult, error) { 30 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 31 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) 32 | req := whoAmIRequest(true) 33 | encodedWhoAmIRequest, err := req.encode() 34 | if err != nil { 35 | return nil, err 36 | } 37 | packet.AppendChild(encodedWhoAmIRequest) 38 | 39 | if len(controls) != 0 { 40 | packet.AppendChild(encodeControls(controls)) 41 | } 42 | 43 | l.Debug.PrintPacket(packet) 44 | 45 | msgCtx, err := l.sendMessage(packet) 46 | if err != nil { 47 | return nil, err 48 | } 49 | defer l.finishMessage(msgCtx) 50 | 51 | result := &WhoAmIResult{} 52 | 53 | l.Debug.Printf("%d: waiting for response", msgCtx.id) 54 | packetResponse, ok := <-msgCtx.responses 55 | if !ok { 56 | return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) 57 | } 58 | packet, err = packetResponse.ReadPacket() 59 | l.Debug.Printf("%d: got response %p", msgCtx.id, packet) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | if packet == nil { 65 | return nil, NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) 66 | } 67 | 68 | if l.Debug { 69 | if err := addLDAPDescriptions(packet); err != nil { 70 | return nil, err 71 | } 72 | ber.PrintPacket(packet) 73 | } 74 | 75 | if packet.Children[1].Tag == ApplicationExtendedResponse { 76 | if err := GetLDAPError(packet); err != nil { 77 | return nil, err 78 | } 79 | } else { 80 | return nil, NewError(ErrorUnexpectedResponse, fmt.Errorf("Unexpected Response: %d", packet.Children[1].Tag)) 81 | } 82 | 83 | extendedResponse := packet.Children[1] 84 | for _, child := range extendedResponse.Children { 85 | if child.Tag == 11 { 86 | result.AuthzID = ber.DecodeString(child.Data.Bytes()) 87 | } 88 | } 89 | 90 | return result, nil 91 | } 92 | --------------------------------------------------------------------------------