Skip to content

Commit

Permalink
Add Paging Interface for LDAP Connection (hashicorp#17640)
Browse files Browse the repository at this point in the history
  • Loading branch information
ltcarbonell authored and jayant07-yb committed Mar 15, 2023
1 parent cf995e6 commit b04fdca
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 18 deletions.
3 changes: 3 additions & 0 deletions changelog/17640.txt
@@ -0,0 +1,3 @@
```release-note:improvement
sdk/ldap: Added support for paging when searching for groups using group filters
```
97 changes: 79 additions & 18 deletions sdk/helper/ldaputil/client.go
Expand Up @@ -14,7 +14,6 @@ import (
"time"

"github.com/go-ldap/ldap/v3"
"github.com/hashicorp/errwrap"
hclog "github.com/hashicorp/go-hclog"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-secure-stdlib/tlsutil"
Expand All @@ -32,7 +31,7 @@ func (c *Client) DialLDAP(cfg *ConfigEntry) (Connection, error) {
for _, uut := range urls {
u, err := url.Parse(uut)
if err != nil {
retErr = multierror.Append(retErr, errwrap.Wrapf(fmt.Sprintf("error parsing url %q: {{err}}", uut), err))
retErr = multierror.Append(retErr, fmt.Errorf(fmt.Sprintf("error parsing url %q: {{err}}", uut), err))
continue
}
host, port, err := net.SplitHostPort(u.Host)
Expand Down Expand Up @@ -83,7 +82,7 @@ func (c *Client) DialLDAP(cfg *ConfigEntry) (Connection, error) {
retErr = nil
break
}
retErr = multierror.Append(retErr, errwrap.Wrapf(fmt.Sprintf("error connecting to host %q: {{err}}", uut), err))
retErr = multierror.Append(retErr, fmt.Errorf(fmt.Sprintf("error connecting to host %q: {{err}}", uut), err))
}
if retErr != nil {
return nil, retErr
Expand Down Expand Up @@ -158,7 +157,7 @@ func (c *Client) GetUserBindDN(cfg *ConfigEntry, conn Connection, username strin

result, err := c.makeLdapSearchRequest(cfg, conn, username)
if err != nil {
return bindDN, errwrap.Wrapf("LDAP search for binddn failed: {{err}}", err)
return bindDN, fmt.Errorf("LDAP search for binddn failed %w", err)
}
if len(result.Entries) != 1 {
return bindDN, fmt.Errorf("LDAP search for binddn 0 or not unique")
Expand Down Expand Up @@ -194,7 +193,7 @@ func (c *Client) RenderUserSearchFilter(cfg *ConfigEntry, username string) (stri
// Example template "({{.UserAttr}}={{.Username}})"
t, err := template.New("queryTemplate").Parse(cfg.UserFilter)
if err != nil {
return "", errwrap.Wrapf("LDAP search failed due to template compilation error: {{err}}", err)
return "", fmt.Errorf("LDAP search failed due to template compilation error: %w", err)
}

// Build context to pass to template - we will be exposing UserDn and Username.
Expand All @@ -212,7 +211,7 @@ func (c *Client) RenderUserSearchFilter(cfg *ConfigEntry, username string) (stri

var renderedFilter bytes.Buffer
if err := t.Execute(&renderedFilter, context); err != nil {
return "", errwrap.Wrapf("LDAP search failed due to template parsing error: {{err}}", err)
return "", fmt.Errorf("LDAP search failed due to template parsing error: %w", err)
}

return renderedFilter.String(), nil
Expand All @@ -237,7 +236,7 @@ func (c *Client) GetUserAliasAttributeValue(cfg *ConfigEntry, conn Connection, u

result, err := c.makeLdapSearchRequest(cfg, conn, username)
if err != nil {
return aliasAttributeValue, errwrap.Wrapf("LDAP search for entity alias attribute failed: {{err}}", err)
return aliasAttributeValue, fmt.Errorf("LDAP search for entity alias attribute failed: %w", err)
}
if len(result.Entries) != 1 {
return aliasAttributeValue, fmt.Errorf("LDAP search for entity alias attribute 0 or not unique")
Expand Down Expand Up @@ -281,7 +280,7 @@ func (c *Client) GetUserDN(cfg *ConfigEntry, conn Connection, bindDN, username s
SizeLimit: math.MaxInt32,
})
if err != nil {
return userDN, errwrap.Wrapf("LDAP search failed for detecting user: {{err}}", err)
return userDN, fmt.Errorf("LDAP search failed for detecting user: %w", err)
}
for _, e := range result.Entries {
userDN = e.DN
Expand Down Expand Up @@ -314,7 +313,7 @@ func (c *Client) performLdapFilterGroupsSearch(cfg *ConfigEntry, conn Connection
// Example template "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}}))"
t, err := template.New("queryTemplate").Parse(cfg.GroupFilter)
if err != nil {
return nil, errwrap.Wrapf("LDAP search failed due to template compilation error: {{err}}", err)
return nil, fmt.Errorf("LDAP search failed due to template compilation error: %w", err)
}

// Build context to pass to template - we will be exposing UserDn and Username.
Expand All @@ -328,7 +327,7 @@ func (c *Client) performLdapFilterGroupsSearch(cfg *ConfigEntry, conn Connection

var renderedQuery bytes.Buffer
if err := t.Execute(&renderedQuery, context); err != nil {
return nil, errwrap.Wrapf("LDAP search failed due to template parsing error: {{err}}", err)
return nil, fmt.Errorf("LDAP search failed due to template parsing error: %w", err)
}

if c.Logger.IsDebug() {
Expand All @@ -345,7 +344,65 @@ func (c *Client) performLdapFilterGroupsSearch(cfg *ConfigEntry, conn Connection
SizeLimit: math.MaxInt32,
})
if err != nil {
return nil, errwrap.Wrapf("LDAP search failed: {{err}}", err)
return nil, fmt.Errorf("LDAP search failed: %w", err)
}

return result.Entries, nil
}

func (c *Client) performLdapFilterGroupsSearchPaging(cfg *ConfigEntry, conn PagingConnection, userDN string, username string) ([]*ldap.Entry, error) {
if cfg.GroupFilter == "" {
c.Logger.Warn("groupfilter is empty, will not query server")
return make([]*ldap.Entry, 0), nil
}

if cfg.GroupDN == "" {
c.Logger.Warn("groupdn is empty, will not query server")
return make([]*ldap.Entry, 0), nil
}

// If groupfilter was defined, resolve it as a Go template and use the query for
// returning the user's groups
if c.Logger.IsDebug() {
c.Logger.Debug("compiling group filter", "group_filter", cfg.GroupFilter)
}

// Parse the configuration as a template.
// Example template "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}}))"
t, err := template.New("queryTemplate").Parse(cfg.GroupFilter)
if err != nil {
return nil, fmt.Errorf("LDAP search failed due to template compilation error: %w", err)
}

// Build context to pass to template - we will be exposing UserDn and Username.
context := struct {
UserDN string
Username string
}{
ldap.EscapeFilter(userDN),
ldap.EscapeFilter(username),
}

var renderedQuery bytes.Buffer
if err := t.Execute(&renderedQuery, context); err != nil {
return nil, fmt.Errorf("LDAP search failed due to template parsing error: %w", err)
}

if c.Logger.IsDebug() {
c.Logger.Debug("searching", "groupdn", cfg.GroupDN, "rendered_query", renderedQuery.String())
}

result, err := conn.SearchWithPaging(&ldap.SearchRequest{
BaseDN: cfg.GroupDN,
Scope: ldap.ScopeWholeSubtree,
Filter: renderedQuery.String(),
Attributes: []string{
cfg.GroupAttr,
},
SizeLimit: math.MaxInt32,
}, math.MaxInt32)
if err != nil {
return nil, fmt.Errorf("LDAP search failed: %w", err)
}

return result.Entries, nil
Expand All @@ -358,21 +415,21 @@ func sidBytesToString(b []byte) (string, error) {
var identifierAuthorityParts [3]uint16

if err := binary.Read(reader, binary.LittleEndian, &revision); err != nil {
return "", errwrap.Wrapf(fmt.Sprintf("SID %#v convert failed reading Revision: {{err}}", b), err)
return "", fmt.Errorf(fmt.Sprintf("SID %#v convert failed reading Revision: {{err}}", b), err)
}

if err := binary.Read(reader, binary.LittleEndian, &subAuthorityCount); err != nil {
return "", errwrap.Wrapf(fmt.Sprintf("SID %#v convert failed reading SubAuthorityCount: {{err}}", b), err)
return "", fmt.Errorf(fmt.Sprintf("SID %#v convert failed reading SubAuthorityCount: {{err}}", b), err)
}

if err := binary.Read(reader, binary.BigEndian, &identifierAuthorityParts); err != nil {
return "", errwrap.Wrapf(fmt.Sprintf("SID %#v convert failed reading IdentifierAuthority: {{err}}", b), err)
return "", fmt.Errorf(fmt.Sprintf("SID %#v convert failed reading IdentifierAuthority: {{err}}", b), err)
}
identifierAuthority := (uint64(identifierAuthorityParts[0]) << 32) + (uint64(identifierAuthorityParts[1]) << 16) + uint64(identifierAuthorityParts[2])

subAuthority := make([]uint32, subAuthorityCount)
if err := binary.Read(reader, binary.LittleEndian, &subAuthority); err != nil {
return "", errwrap.Wrapf(fmt.Sprintf("SID %#v convert failed reading SubAuthority: {{err}}", b), err)
return "", fmt.Errorf(fmt.Sprintf("SID %#v convert failed reading SubAuthority: {{err}}", b), err)
}

result := fmt.Sprintf("S-%d-%d", revision, identifierAuthority)
Expand All @@ -394,7 +451,7 @@ func (c *Client) performLdapTokenGroupsSearch(cfg *ConfigEntry, conn Connection,
SizeLimit: 1,
})
if err != nil {
return nil, errwrap.Wrapf("LDAP search failed: {{err}}", err)
return nil, fmt.Errorf("LDAP search failed: %w", err)
}
if len(result.Entries) == 0 {
c.Logger.Warn("unable to read object for group attributes", "userdn", userDN, "groupattr", cfg.GroupAttr)
Expand Down Expand Up @@ -462,7 +519,11 @@ func (c *Client) GetLdapGroups(cfg *ConfigEntry, conn Connection, userDN string,
if cfg.UseTokenGroups {
entries, err = c.performLdapTokenGroupsSearch(cfg, conn, userDN)
} else {
entries, err = c.performLdapFilterGroupsSearch(cfg, conn, userDN, username)
if paging, ok := conn.(PagingConnection); ok {
entries, err = c.performLdapFilterGroupsSearchPaging(cfg, paging, userDN, username)
} else {
entries, err = c.performLdapFilterGroupsSearch(cfg, conn, userDN, username)
}
}
if err != nil {
return nil, err
Expand Down Expand Up @@ -603,7 +664,7 @@ func getTLSConfig(cfg *ConfigEntry, host string) (*tls.Config, error) {
if cfg.ClientTLSCert != "" && cfg.ClientTLSKey != "" {
certificate, err := tls.X509KeyPair([]byte(cfg.ClientTLSCert), []byte(cfg.ClientTLSKey))
if err != nil {
return nil, errwrap.Wrapf("failed to parse client X509 key pair: {{err}}", err)
return nil, fmt.Errorf("failed to parse client X509 key pair: %w", err)
}
tlsConfig.Certificates = append(tlsConfig.Certificates, certificate)
} else if cfg.ClientTLSCert != "" || cfg.ClientTLSKey != "" {
Expand Down
5 changes: 5 additions & 0 deletions sdk/helper/ldaputil/connection.go
Expand Up @@ -20,3 +20,8 @@ type Connection interface {
SetTimeout(timeout time.Duration)
UnauthenticatedBind(username string) error
}

type PagingConnection interface {
Connection
SearchWithPaging(searchRequest *ldap.SearchRequest, pagingSize uint32) (*ldap.SearchResult, error)
}

0 comments on commit b04fdca

Please sign in to comment.