diff --git a/client.go b/client.go index c7f41f6f..facd7093 100644 --- a/client.go +++ b/client.go @@ -25,4 +25,5 @@ type Client interface { Search(searchRequest *SearchRequest) (*SearchResult, error) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) + DirSync(searchRequest *SearchRequest, flags, maxAttrCount int64) (*SearchResult, error) } diff --git a/control.go b/control.go index bc946fae..58466bdc 100644 --- a/control.go +++ b/control.go @@ -18,6 +18,16 @@ const ( ControlTypeVChuPasswordWarning = "2.16.840.1.113730.3.4.5" // ControlTypeManageDsaIT - https://tools.ietf.org/html/rfc3296 ControlTypeManageDsaIT = "2.16.840.1.113730.3.4.2" + // ControlTypeDirSync - Active Directory DirSync - https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx + ControlTypeDirSync = "1.2.840.113556.1.4.841" +) + +// Flags for DirSync control +const ( + DirSyncIncrementalValues int64 = 2147483648 + DirSyncPublicDataOnly int64 = 8192 + DirSyncAncestorsFirstOrder int64 = 2048 + DirSyncObjectSecurity int64 = 1 ) // ControlTypeMap maps controls to text descriptions @@ -25,6 +35,7 @@ var ControlTypeMap = map[string]string{ ControlTypePaging: "Paging", ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft", ControlTypeManageDsaIT: "Manage DSA IT", + ControlTypeDirSync: "DirSync", } // Control defines an interface controls provide to encode and describe themselves @@ -385,6 +396,32 @@ func DecodeControl(packet *ber.Packet) (Control, error) { value.Value = c.Expire return c, nil + case ControlTypeDirSync: + value.Description += " (DirSync)" + c := new(ControlDirSync) + if value.Value != nil { + valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) + if err != nil { + return nil, err + } + value.Data.Truncate(0) + value.Value = nil + value.AppendChild(valueChildren) + } + value = value.Children[0] + if len(value.Children) != 3 { // also on initial creation, Cookie is an empty string + return nil, fmt.Errorf("invalid number of children in dirSync control") + } + value.Description = "DirSync Control Value" + value.Children[0].Description = "Flags" + value.Children[1].Description = "MaxAttrCnt" + value.Children[2].Description = "Cookie" + c.Flags = value.Children[0].Value.(int64) + c.MaxAttrCnt = value.Children[1].Value.(int64) + c.Cookie = value.Children[2].Data.Bytes() + value.Children[2].Value = c.Cookie + return c, nil + default: c := new(ControlString) c.ControlType = ControlType @@ -426,3 +463,59 @@ func encodeControls(controls []Control) *ber.Packet { } return packet } + +// ControlDirSync implements the control described in https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx +type ControlDirSync struct { + Flags int64 + MaxAttrCnt int64 + Cookie []byte +} + +// NewControlDirSync returns a dir sync control +func NewControlDirSync(flags int64, maxAttrCount int64, cookie []byte) *ControlDirSync { + return &ControlDirSync{ + Flags: flags, + MaxAttrCnt: maxAttrCount, + Cookie: cookie, + } +} + +// GetControlType returns the OID +func (c *ControlDirSync) GetControlType() string { + return ControlTypeDirSync +} + +// String returns a human-readable description +func (c *ControlDirSync) String() string { + return fmt.Sprintf("ControlType: %s (%q), Criticality: true, ControlValue: Flags: %d, MaxAttrCnt: %d", ControlTypeMap[ControlTypeDirSync], ControlTypeDirSync, c.Flags, c.MaxAttrCnt) +} + +// Encode returns the ber packet representation +func (c *ControlDirSync) Encode() *ber.Packet { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeDirSync, "Control Type ("+ControlTypeMap[ControlTypeDirSync]+")")) + packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, true, "Criticality")) // must be true always + + val := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (DirSync)") + + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "DirSync Control Value") + seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.Flags), "Flags")) + seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.MaxAttrCnt), "MaxAttrCount")) + + cookie := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "Cookie") + if len(c.Cookie) != 0 { + cookie.Value = c.Cookie + cookie.Data.Write(c.Cookie) + } + seq.AppendChild(cookie) + + val.AppendChild(seq) + + packet.AppendChild(val) + return packet +} + +// SetCookie stores the given cookie in the dirSync control +func (c *ControlDirSync) SetCookie(cookie []byte) { + c.Cookie = cookie +} diff --git a/control_test.go b/control_test.go index 4c7637d1..1046fdfd 100644 --- a/control_test.go +++ b/control_test.go @@ -27,6 +27,11 @@ func TestControlString(t *testing.T) { runControlTest(t, NewControlString("x", false, "")) } +func TestControlDirSync(t *testing.T) { + runControlTest(t, NewControlDirSync(DirSyncObjectSecurity, 1000, nil)) + runControlTest(t, NewControlDirSync(DirSyncObjectSecurity, 1000, []byte("I'm a cookie!"))) +} + func runControlTest(t *testing.T, originalControl Control) { header := "" if callerpc, _, line, ok := runtime.Caller(1); ok { @@ -84,6 +89,10 @@ func TestDescribeControlString(t *testing.T) { runAddControlDescriptions(t, NewControlString("x", false, ""), "Control Type ()", "Control Value") } +func TestDescribeControlDirSync(t *testing.T) { + runAddControlDescriptions(t, NewControlDirSync(DirSyncObjectSecurity, 1000, nil), "Control Type (DirSync)", "Criticality", "Control Value") +} + func runAddControlDescriptions(t *testing.T, originalControl Control, childDescriptions ...string) { header := "" if callerpc, _, line, ok := runtime.Caller(1); ok { diff --git a/dn.go b/dn.go index e10a21b7..b13741a5 100644 --- a/dn.go +++ b/dn.go @@ -1,3 +1,7 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// // File contains DN parsing functionality // // https://tools.ietf.org/html/rfc4514 diff --git a/search.go b/search.go index 8d3967da..1d28a774 100644 --- a/search.go +++ b/search.go @@ -448,3 +448,64 @@ func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) { l.Debug.Printf("%d: returning", msgCtx.id) return result, nil } + +// DirSync does a Search with dirSync Control. +func (l *Conn) DirSync(searchRequest *SearchRequest, flags int64, maxAttrCount int64) (*SearchResult, error) { + var dirSyncControl *ControlDirSync + + control := FindControl(searchRequest.Controls, ControlTypeDirSync) + if control == nil { + dirSyncControl = NewControlDirSync(flags, maxAttrCount, nil) + searchRequest.Controls = append(searchRequest.Controls, dirSyncControl) + } else { + castControl, ok := control.(*ControlDirSync) + if !ok { + return nil, fmt.Errorf("Expected DirSync control to be of type *ControlDirSync, got %v", control) + } + if castControl.Flags != flags { + return nil, fmt.Errorf("flags given in search request (%d) conflicts with flags given in search call (%d)", castControl.Flags, flags) + } + if castControl.MaxAttrCnt != maxAttrCount { + return nil, fmt.Errorf("MaxAttrCnt given in search request (%d) conflicts with maxAttrCount given in search call (%d)", castControl.MaxAttrCnt, maxAttrCount) + } + dirSyncControl = castControl + } + + searchResult := new(SearchResult) + result, err := l.Search(searchRequest) + l.Debug.Printf("Looking for result...") + if err != nil { + return searchResult, err + } + if result == nil { + return searchResult, NewError(ErrorNetwork, errors.New("ldap: packet not received")) + } + + for _, entry := range result.Entries { + searchResult.Entries = append(searchResult.Entries, entry) + } + for _, referral := range result.Referrals { + searchResult.Referrals = append(searchResult.Referrals, referral) + } + for _, control := range result.Controls { + searchResult.Controls = append(searchResult.Controls, control) + } + + l.Debug.Printf("Looking for DirSync Control...") + dirSyncResult := FindControl(result.Controls, ControlTypeDirSync) + if dirSyncResult == nil { + dirSyncControl = nil + l.Debug.Printf("Could not find dirSyncControl control. Breaking...") + return searchResult, nil + } + + cookie := dirSyncResult.(*ControlDirSync).Cookie + if len(cookie) == 0 { + dirSyncControl = nil + l.Debug.Printf("Could not find cookie. Breaking...") + return searchResult, nil + } + dirSyncControl.SetCookie(cookie) + + return searchResult, nil +} diff --git a/search_test.go b/search_test.go index 5f77b22e..100b7242 100644 --- a/search_test.go +++ b/search_test.go @@ -1,8 +1,10 @@ package ldap import ( + "log" "reflect" "testing" + "time" ) // TestNewEntry tests that repeated calls to NewEntry return the same value with the same input @@ -29,3 +31,50 @@ func TestNewEntry(t *testing.T) { iteration = iteration + 1 } } + +func ExampleDirSync() { + conn, err := Dial("tcp", "ad.example.org:389") + if err != nil { + log.Fatalf("Failed to connect: %s\n", err) + } + defer conn.Close() + + _, err = conn.SimpleBind(&SimpleBindRequest{ + Username: "cn=Some User,ou=people,dc=example,dc=org", + Password: "MySecretPass", + }) + + req := &SearchRequest{ + BaseDN: `DC=example,DC=org`, + Filter: `(&(objectClass=person)(!(objectClass=computer)))`, + Attributes: []string{"*"}, + Scope: ScopeWholeSubtree, + } + // This is the initial sync with all entries matching the filter + doMore := true + for doMore { + res, err := conn.DirSync(req, DirSyncObjectSecurity, 1000) + if err != nil { + log.Fatalf("failed to search: %s", err) + } + for _, entry := range res.Entries { + entry.Print() + } + ctrl := FindControl(res.Controls, ControlTypeDirSync) + if ctrl == nil || ctrl.(*ControlDirSync).Flags == 0 { + doMore = false + } + } + // We're done with the initial sync. Now pull every 15 seconds for the + // updated entries - note that you get just the changes, not a full entry. + for { + res, err := conn.DirSync(req, DirSyncObjectSecurity, 1000) + if err != nil { + log.Fatalf("failed to search: %s", err) + } + for _, entry := range res.Entries { + entry.Print() + } + time.Sleep(15 * time.Second) + } +}