Skip to content

Commit

Permalink
feat: enable DirSync control in search operation (#436)
Browse files Browse the repository at this point in the history
* feat: enable DirSync control in search operation

* fix: fixed ineffectual assignment to err

* fix: fixed should replace loop with error

* fix: not include Cookie in SearchResult struct

---------

Co-authored-by: shimokubo <shimokubo@osstech.co.jp>
  • Loading branch information
masato-sso and shimokubo committed May 21, 2023
1 parent 039466e commit cdb0754
Show file tree
Hide file tree
Showing 10 changed files with 422 additions and 0 deletions.
1 change: 1 addition & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ type Client interface {

Search(*SearchRequest) (*SearchResult, error)
SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error)
DirSync(searchRequest *SearchRequest, flags, maxAttrCount int64, cookie []byte) (*SearchResult, error)
}
92 changes: 92 additions & 0 deletions control.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ const (
ControlTypeMicrosoftShowDeleted = "1.2.840.113556.1.4.417"
// ControlTypeMicrosoftServerLinkTTL - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/f4f523a8-abc0-4b3a-a471-6b2fef135481?redirectedfrom=MSDN
ControlTypeMicrosoftServerLinkTTL = "1.2.840.113556.1.4.2309"
// ControlTypeDirSync - Active Directory DirSync - https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx
ControlTypeDirSync = "1.2.840.113556.1.4.841"
)

// Flags for DirSync control
const (
DirSyncIncrementalValues int64 = 2147483648
DirSyncPublicDataOnly int64 = 8192
DirSyncAncestorsFirstOrder int64 = 2048
DirSyncObjectSecurity int64 = 1
)

// ControlTypeMap maps controls to text descriptions
Expand All @@ -40,6 +50,7 @@ var ControlTypeMap = map[string]string{
ControlTypeMicrosoftNotification: "Change Notification - Microsoft",
ControlTypeMicrosoftShowDeleted: "Show Deleted Objects - Microsoft",
ControlTypeMicrosoftServerLinkTTL: "Return TTL-DNs for link values with associated expiry times - Microsoft",
ControlTypeDirSync: "DirSync",
}

// Control defines an interface controls provide to encode and describe themselves
Expand Down Expand Up @@ -490,6 +501,31 @@ func DecodeControl(packet *ber.Packet) (Control, error) {
return NewControlMicrosoftServerLinkTTL(), nil
case ControlTypeSubtreeDelete:
return NewControlSubtreeDelete(), 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
Expand Down Expand Up @@ -560,3 +596,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
}
9 changes: 9 additions & 0 deletions control_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,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 {
Expand Down Expand Up @@ -116,6 +121,10 @@ func TestDescribeControlString(t *testing.T) {
runAddControlDescriptions(t, NewControlString("x", false, ""), "Control Type ()")
}

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 {
Expand Down
55 changes: 55 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io/ioutil"
"log"
"time"
)

// This example demonstrates how to bind a connection to an ldap user
Expand Down Expand Up @@ -344,6 +345,60 @@ func ExampleControlPaging_manualPaging() {
}
}

// This example demonstrates how to use DirSync to manually execute a
// DirSync search request
func ExampleConn_DirSync() {
conn, err := Dial("tcp", "ad.example.org:389")
if err != nil {
log.Fatalf("Failed to connect: %s\n", err)
}
defer conn.Close()

_, err = conn.SimpleBind(&SimpleBindRequest{
Username: "cn=Some User,ou=people,dc=example,dc=org",
Password: "MySecretPass",
})
if err != nil {
log.Fatalf("failed to bind: %s", err)
}

req := &SearchRequest{
BaseDN: `DC=example,DC=org`,
Filter: `(&(objectClass=person)(!(objectClass=computer)))`,
Attributes: []string{"*"},
Scope: ScopeWholeSubtree,
}
// This is the initial sync with all entries matching the filter
doMore := true
var cookie []byte
for doMore {
res, err := conn.DirSync(req, DirSyncObjectSecurity, 1000, cookie)
if err != nil {
log.Fatalf("failed to search: %s", err)
}
for _, entry := range res.Entries {
entry.Print()
}
ctrl := FindControl(res.Controls, ControlTypeDirSync)
if ctrl == nil || ctrl.(*ControlDirSync).Flags == 0 {
doMore = false
}
cookie = ctrl.(*ControlDirSync).Cookie
}
// We're done with the initial sync. Now pull every 15 seconds for the
// updated entries - note that you get just the changes, not a full entry.
for {
res, err := conn.DirSync(req, DirSyncObjectSecurity, 1000, cookie)
if err != nil {
log.Fatalf("failed to search: %s", err)
}
for _, entry := range res.Entries {
entry.Print()
}
time.Sleep(15 * time.Second)
}
}

// This example demonstrates how to use EXTERNAL SASL with TLS client certificates.
func ExampleConn_ExternalBind() {
ldapCert := "/path/to/cert.pem"
Expand Down
54 changes: 54 additions & 0 deletions search.go
Original file line number Diff line number Diff line change
Expand Up @@ -582,3 +582,57 @@ func unpackAttributes(children []*ber.Packet) []*EntryAttribute {

return entries
}

// DirSync does a Search with dirSync Control.
func (l *Conn) DirSync(searchRequest *SearchRequest, flags int64, maxAttrCount int64, cookie []byte) (*SearchResult, error) {
var dirSyncControl *ControlDirSync

control := FindControl(searchRequest.Controls, ControlTypeDirSync)
if control == nil {
dirSyncControl = NewControlDirSync(flags, maxAttrCount, cookie)
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"))
}

searchResult.Entries = append(searchResult.Entries, result.Entries...)
searchResult.Referrals = append(searchResult.Referrals, result.Referrals...)
searchResult.Controls = append(searchResult.Controls, result.Controls...)

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
}
1 change: 1 addition & 0 deletions v3/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ type Client interface {

Search(*SearchRequest) (*SearchResult, error)
SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error)
DirSync(searchRequest *SearchRequest, flags, maxAttrCount int64, cookie []byte) (*SearchResult, error)
}
92 changes: 92 additions & 0 deletions v3/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ const (
ControlTypeMicrosoftShowDeleted = "1.2.840.113556.1.4.417"
// ControlTypeMicrosoftServerLinkTTL - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/f4f523a8-abc0-4b3a-a471-6b2fef135481?redirectedfrom=MSDN
ControlTypeMicrosoftServerLinkTTL = "1.2.840.113556.1.4.2309"
// ControlTypeDirSync - Active Directory DirSync - https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx
ControlTypeDirSync = "1.2.840.113556.1.4.841"
)

// Flags for DirSync control
const (
DirSyncIncrementalValues int64 = 2147483648
DirSyncPublicDataOnly int64 = 8192
DirSyncAncestorsFirstOrder int64 = 2048
DirSyncObjectSecurity int64 = 1
)

// ControlTypeMap maps controls to text descriptions
Expand All @@ -47,6 +57,7 @@ var ControlTypeMap = map[string]string{
ControlTypeMicrosoftServerLinkTTL: "Return TTL-DNs for link values with associated expiry times - Microsoft",
ControlTypeServerSideSorting: "Server Side Sorting Request - LDAP Control Extension for Server Side Sorting of Search Results (RFC2891)",
ControlTypeServerSideSortingResult: "Server Side Sorting Results - LDAP Control Extension for Server Side Sorting of Search Results (RFC2891)",
ControlTypeDirSync: "DirSync",
}

// Control defines an interface controls provide to encode and describe themselves
Expand Down Expand Up @@ -501,6 +512,31 @@ func DecodeControl(packet *ber.Packet) (Control, error) {
return NewControlServerSideSorting(value)
case ControlTypeServerSideSortingResult:
return NewControlServerSideSortingResult(value)
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
Expand Down Expand Up @@ -572,6 +608,62 @@ 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
}

// ControlServerSideSorting

type SortKey struct {
Expand Down

0 comments on commit cdb0754

Please sign in to comment.