Skip to content

Commit

Permalink
support ad rotate-root with userattr="userPrincipalName" (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
raymonstah authored Oct 16, 2023
1 parent aeb8323 commit 86195e1
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 7 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Unreleased

### IMPROVEMENTS:
* add rotate-root support when using userattr=userPrincipalName

## v0.11.1

### IMPROVEMENTS:
Expand Down
25 changes: 23 additions & 2 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"github.com/go-ldap/ldap/v3"
"github.com/go-ldap/ldif"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/helper/ldaputil"

"github.com/hashicorp/vault-plugin-secrets-openldap/client"
)

Expand All @@ -32,14 +34,33 @@ type Client struct {

// UpdateDNPassword updates the password for the object with the given DN.
func (c *Client) UpdateDNPassword(conf *client.Config, dn string, newPassword string) error {
filters := map[*client.Field][]string{client.FieldRegistry.ObjectClass: {"*"}}
scope := ldap.ScopeBaseObject
filters := map[*client.Field][]string{
client.FieldRegistry.ObjectClass: {"*"},
}

userAttr := conf.UserAttr
if userAttr == "" {
userAttr = defaultUserAttr(conf.Schema)
}
field := client.FieldRegistry.Parse(userAttr)
if field == nil {
return fmt.Errorf("unsupported userattr %q", userAttr)
}

if field == client.FieldRegistry.UserPrincipalName {
scope = ldap.ScopeWholeSubtree
bindUser := fmt.Sprintf("%s@%s", ldaputil.EscapeLDAPValue(dn), conf.UPNDomain)
filters[field] = []string{bindUser}
dn = conf.UserDN
}

newValues, err := client.GetSchemaFieldRegistry(conf.Schema, newPassword)
if err != nil {
return fmt.Errorf("error updating password: %s", err)
}

return c.ldap.UpdatePassword(conf, dn, ldap.ScopeBaseObject, newValues, filters)
return c.ldap.UpdatePassword(conf, dn, scope, newValues, filters)
}

// UpdateUserPassword updates the password for the object with the given username.
Expand Down
34 changes: 30 additions & 4 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"math"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -37,6 +38,15 @@ func New(logger hclog.Logger) Client {
}
}

func NewWithClient(logger hclog.Logger, ldap ldaputil.LDAP) Client {
return Client{
ldap: &ldaputil.Client{
Logger: logger,
LDAP: ldap,
},
}
}

type Client struct {
ldap *ldaputil.Client
}
Expand Down Expand Up @@ -110,15 +120,31 @@ func (c *Client) UpdatePassword(cfg *Config, baseDN string, scope int, newValues

// toString turns the following map of filters into LDAP search filter strings
// For example: "(cn=Ellen Jones)"
// when multiple filters are applied, they get AND'ed together.
// example: (&(x=1)(y=2))
// for test assertions, this sorts the filters alphabetically.
func toString(filters map[*Field][]string) string {
var fieldEquals []string
for f, values := range filters {
sortedFilters := make([]*Field, 0, len(filters))
for filter := range filters {
sortedFilters = append(sortedFilters, filter)
}
sort.Slice(sortedFilters, func(i, j int) bool {
return sortedFilters[i].String() < sortedFilters[j].String()
})
for _, filter := range sortedFilters {
values := filters[filter]
// make values deterministic
sort.Strings(values)
for _, v := range values {
fieldEquals = append(fieldEquals, fmt.Sprintf("%s=%s", f, v))
fieldEquals = append(fieldEquals, fmt.Sprintf("(%s=%s)", filter, v))
}
}
result := strings.Join(fieldEquals, ",")
return "(" + result + ")"
if len(fieldEquals) <= 1 {
return strings.Join(fieldEquals, "")
}

return "(&" + strings.Join(fieldEquals, "") + ")"
}

func bind(cfg *Config, conn ldaputil.Connection) error {
Expand Down
34 changes: 33 additions & 1 deletion client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (

"github.com/go-ldap/ldap/v3"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault-plugin-secrets-openldap/ldapifc"
"github.com/hashicorp/vault/sdk/helper/ldaputil"
"github.com/stretchr/testify/assert"

"github.com/hashicorp/vault-plugin-secrets-openldap/ldapifc"
)

func TestSearch(t *testing.T) {
Expand Down Expand Up @@ -304,3 +306,33 @@ func testSearchResult() *ldap.SearchResult {
},
}
}

func TestToString(t *testing.T) {
tcs := map[string]struct {
filters map[*Field][]string
expectedFilterString string
}{
"no-filters": {
filters: nil,
expectedFilterString: "",
},
"single-filter": {
filters: map[*Field][]string{FieldRegistry.DomainName: {"bob"}},
expectedFilterString: "(dn=bob)",
},
"two-filters": {
filters: map[*Field][]string{
FieldRegistry.DomainName: {"bob"},
FieldRegistry.UserPrincipalName: {"[email protected]"},
},
expectedFilterString: "(&(dn=bob)([email protected]))",
},
}

for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
got := toString(tc.filters)
assert.Equal(t, tc.expectedFilterString, got)
})
}
}
110 changes: 110 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package openldap

import (
"testing"

"github.com/go-ldap/ldap/v3"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/helper/ldaputil"
"github.com/stretchr/testify/assert"

"github.com/hashicorp/vault-plugin-secrets-openldap/client"
"github.com/hashicorp/vault-plugin-secrets-openldap/ldapifc"
)

func GetTestClient(fake *ldapifc.FakeLDAPConnection) *Client {
ldapClient := client.NewWithClient(hclog.NewNullLogger(), &ldapifc.FakeLDAPClient{
ConnToReturn: fake,
})

return &Client{ldap: ldapClient}
}

// UpdateDNPassword when the UserAttr is "userPrincipalName"
func Test_UpdateDNPassword_AD_UserPrincipalName(t *testing.T) {
newPassword := "newpassword"
conn := &ldapifc.FakeLDAPConnection{
ModifyRequestToExpect: &ldap.ModifyRequest{
DN: "CN=Bob,CN=Users,DC=example,DC=net",
},
SearchRequestToExpect: &ldap.SearchRequest{
BaseDN: "cn=users",
Scope: ldap.ScopeWholeSubtree,
Filter: "(&(objectClass=*)([email protected]))",
},
SearchResultToReturn: &ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: "CN=Bob,CN=Users,DC=example,DC=net",
},
},
},
}

c := GetTestClient(conn)
config := &client.Config{
ConfigEntry: &ldaputil.ConfigEntry{
Url: "ldaps://ldap:386",
UserDN: "cn=users",
UPNDomain: "example.net",
UserAttr: "userPrincipalName",
BindDN: "username",
BindPassword: "password",
},
Schema: client.SchemaAD,
}

// depending on the schema, the password may be formatted, so we leverage this helper function
fields, err := client.GetSchemaFieldRegistry(config.Schema, newPassword)
assert.NoError(t, err)
for k, v := range fields {
conn.ModifyRequestToExpect.Replace(k.String(), v)
}

err = c.UpdateDNPassword(config, "bob", newPassword)
assert.NoError(t, err)
}

// UpdateDNPassword when the UserAttr is "dn"
func Test_UpdateDNPassword_AD_DN(t *testing.T) {
newPassword := "newpassword"
conn := &ldapifc.FakeLDAPConnection{
ModifyRequestToExpect: &ldap.ModifyRequest{
DN: "CN=Bob,CN=Users,DC=example,DC=net",
},
SearchRequestToExpect: &ldap.SearchRequest{
BaseDN: "CN=Bob,CN=Users,DC=example,DC=net",
Scope: ldap.ScopeBaseObject,
Filter: "(objectClass=*)",
},
SearchResultToReturn: &ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: "CN=Bob,CN=Users,DC=example,DC=net",
},
},
},
}

c := GetTestClient(conn)
config := &client.Config{
ConfigEntry: &ldaputil.ConfigEntry{
Url: "ldaps://ldap:386",
UserAttr: "dn",
BindDN: "username",
BindPassword: "password",
},
Schema: client.SchemaAD,
}

// depending on the schema, the password may be formatted, so we leverage this helper function
fields, err := client.GetSchemaFieldRegistry(config.Schema, newPassword)
assert.NoError(t, err)
for k, v := range fields {
conn.ModifyRequestToExpect.Replace(k.String(), v)
}

err = c.UpdateDNPassword(config, "CN=Bob,CN=Users,DC=example,DC=net", newPassword)
assert.NoError(t, err)

}

0 comments on commit 86195e1

Please sign in to comment.