open-vault/builtin/credential/ldap/backend_test.go
Rémi Lapeyre d66333f7ac
Fix handling of username_as_alias during LDAP authentication (#15525)
* Fix handling of username_as_alias during LDAP authentication

There is a bug that was introduced in the LDAP authentication method by https://github.com/hashicorp/vault/pull/11000.
It was thought to be backward compatible but has broken a number of users. Later
a new parameter `username_as_alias` was introduced in https://github.com/hashicorp/vault/pull/14324
to make it possible for operators to restore the previous behavior.
The way it is currently working is not completely backward compatible thought
because when username_as_alias is set, a call to GetUserAliasAttributeValue() will
first be made, then this value is completely discarded in pathLogin() and replaced
by the username as expected.

This is an issue because it makes useless calls to the LDAP server and will break
backward compatibility if one of the constraints in GetUserAliasAttributeValue()
is not respected, even though the resulting value will be discarded anyway.

In order to maintain backward compatibility here we have to only call
GetUserAliasAttributeValue() if necessary.

Since this change of behavior was introduced in 1.9, this fix will need to be
backported to the 1.9, 1.10 and 1.11 branches.

* Add changelog

* Add tests

* Format code

* Update builtin/credential/ldap/backend.go

Co-authored-by: Calvin Leung Huang <1883212+calvn@users.noreply.github.com>

* Format and fix declaration

* Reword changelog

Co-authored-by: Calvin Leung Huang <1883212+calvn@users.noreply.github.com>
2022-05-20 14:17:26 -07:00

1265 lines
37 KiB
Go

package ldap
import (
"context"
"fmt"
"reflect"
"sort"
"testing"
"time"
goldap "github.com/go-ldap/ldap/v3"
"github.com/go-test/deep"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/testhelpers/ldap"
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
"github.com/hashicorp/vault/sdk/helper/ldaputil"
"github.com/hashicorp/vault/sdk/helper/policyutil"
"github.com/hashicorp/vault/sdk/helper/tokenutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/mitchellh/mapstructure"
)
func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b := Backend()
if b == nil {
t.Fatalf("failed to create backend")
}
err := b.Backend.Setup(context.Background(), config)
if err != nil {
t.Fatal(err)
}
return b, config.StorageView
}
func TestLdapAuthBackend_Listing(t *testing.T) {
b, storage := createBackendWithStorage(t)
// Create group "testgroup"
resp, err := b.HandleRequest(namespace.RootContext(nil), &logical.Request{
Path: "groups/testgroup",
Operation: logical.UpdateOperation,
Storage: storage,
Data: map[string]interface{}{
"policies": []string{"default"},
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
// Create group "nested/testgroup"
resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{
Path: "groups/nested/testgroup",
Operation: logical.UpdateOperation,
Storage: storage,
Data: map[string]interface{}{
"policies": []string{"default"},
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
// Create user "testuser"
resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{
Path: "users/testuser",
Operation: logical.UpdateOperation,
Storage: storage,
Data: map[string]interface{}{
"policies": []string{"default"},
"groups": "testgroup,nested/testgroup",
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
// Create user "nested/testuser"
resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{
Path: "users/nested/testuser",
Operation: logical.UpdateOperation,
Storage: storage,
Data: map[string]interface{}{
"policies": []string{"default"},
"groups": "testgroup,nested/testgroup",
},
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
// List users
resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{
Path: "users/",
Operation: logical.ListOperation,
Storage: storage,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
expected := []string{"testuser", "nested/testuser"}
if !reflect.DeepEqual(expected, resp.Data["keys"].([]string)) {
t.Fatalf("bad: listed users; expected: %#v actual: %#v", expected, resp.Data["keys"].([]string))
}
// List groups
resp, err = b.HandleRequest(namespace.RootContext(nil), &logical.Request{
Path: "groups/",
Operation: logical.ListOperation,
Storage: storage,
})
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
}
expected = []string{"testgroup", "nested/testgroup"}
if !reflect.DeepEqual(expected, resp.Data["keys"].([]string)) {
t.Fatalf("bad: listed groups; expected: %#v actual: %#v", expected, resp.Data["keys"].([]string))
}
}
func TestLdapAuthBackend_CaseSensitivity(t *testing.T) {
var resp *logical.Response
var err error
b, storage := createBackendWithStorage(t)
ctx := context.Background()
testVals := func(caseSensitive bool) {
// Clear storage
userList, err := storage.List(ctx, "user/")
if err != nil {
t.Fatal(err)
}
for _, user := range userList {
err = storage.Delete(ctx, "user/"+user)
if err != nil {
t.Fatal(err)
}
}
groupList, err := storage.List(ctx, "group/")
if err != nil {
t.Fatal(err)
}
for _, group := range groupList {
err = storage.Delete(ctx, "group/"+group)
if err != nil {
t.Fatal(err)
}
}
configReq := &logical.Request{
Path: "config",
Operation: logical.ReadOperation,
Storage: storage,
}
resp, err = b.HandleRequest(ctx, configReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
if resp == nil {
t.Fatal("nil response")
}
if resp.Data["case_sensitive_names"].(bool) != caseSensitive {
t.Fatalf("expected case sensitivity %t, got %t", caseSensitive, resp.Data["case_sensitive_names"].(bool))
}
groupReq := &logical.Request{
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"policies": "grouppolicy",
},
Path: "groups/EngineerS",
Storage: storage,
}
resp, err = b.HandleRequest(ctx, groupReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
keys, err := storage.List(ctx, "group/")
if err != nil {
t.Fatal(err)
}
switch caseSensitive {
case true:
if keys[0] != "EngineerS" {
t.Fatalf("bad: %s", keys[0])
}
default:
if keys[0] != "engineers" {
t.Fatalf("bad: %s", keys[0])
}
}
userReq := &logical.Request{
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"groups": "EngineerS",
"policies": "userpolicy",
},
Path: "users/hermeS conRad",
Storage: storage,
}
resp, err = b.HandleRequest(ctx, userReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
keys, err = storage.List(ctx, "user/")
if err != nil {
t.Fatal(err)
}
switch caseSensitive {
case true:
if keys[0] != "hermeS conRad" {
t.Fatalf("bad: %s", keys[0])
}
default:
if keys[0] != "hermes conrad" {
t.Fatalf("bad: %s", keys[0])
}
}
if caseSensitive {
// The online test server is actually case sensitive so we need to
// write again so it works
userReq = &logical.Request{
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"groups": "EngineerS",
"policies": "userpolicy",
},
Path: "users/Hermes Conrad",
Storage: storage,
Connection: &logical.Connection{},
}
resp, err = b.HandleRequest(ctx, userReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
}
loginReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "login/Hermes Conrad",
Data: map[string]interface{}{
"password": "hermes",
},
Storage: storage,
Connection: &logical.Connection{},
}
resp, err = b.HandleRequest(ctx, loginReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
expected := []string{"grouppolicy", "userpolicy"}
if !reflect.DeepEqual(expected, resp.Auth.Policies) {
t.Fatalf("bad: policies: expected: %q, actual: %q", expected, resp.Auth.Policies)
}
}
cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
defer cleanup()
configReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config",
Data: map[string]interface{}{
"url": cfg.Url,
"userattr": cfg.UserAttr,
"userdn": cfg.UserDN,
"groupdn": cfg.GroupDN,
"groupattr": cfg.GroupAttr,
"binddn": cfg.BindDN,
"bindpass": cfg.BindPassword,
},
Storage: storage,
}
resp, err = b.HandleRequest(ctx, configReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
testVals(false)
// Check that if the value is nil, on read it is case sensitive
configEntry, err := b.Config(ctx, configReq)
if err != nil {
t.Fatal(err)
}
configEntry.CaseSensitiveNames = nil
entry, err := logical.StorageEntryJSON("config", configEntry)
if err != nil {
t.Fatal(err)
}
err = configReq.Storage.Put(ctx, entry)
if err != nil {
t.Fatal(err)
}
testVals(true)
}
func TestLdapAuthBackend_UserPolicies(t *testing.T) {
var resp *logical.Response
var err error
b, storage := createBackendWithStorage(t)
cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
defer cleanup()
configReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config",
Data: map[string]interface{}{
"url": cfg.Url,
"userattr": cfg.UserAttr,
"userdn": cfg.UserDN,
"groupdn": cfg.GroupDN,
"groupattr": cfg.GroupAttr,
"binddn": cfg.BindDN,
"bindpassword": cfg.BindPassword,
},
Storage: storage,
}
resp, err = b.HandleRequest(context.Background(), configReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
groupReq := &logical.Request{
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"policies": "grouppolicy",
},
Path: "groups/engineers",
Storage: storage,
Connection: &logical.Connection{},
}
resp, err = b.HandleRequest(context.Background(), groupReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
userReq := &logical.Request{
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"groups": "engineers",
"policies": "userpolicy",
},
Path: "users/hermes conrad",
Storage: storage,
Connection: &logical.Connection{},
}
resp, err = b.HandleRequest(context.Background(), userReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
loginReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "login/hermes conrad",
Data: map[string]interface{}{
"password": "hermes",
},
Storage: storage,
Connection: &logical.Connection{},
}
resp, err = b.HandleRequest(context.Background(), loginReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
expected := []string{"grouppolicy", "userpolicy"}
if !reflect.DeepEqual(expected, resp.Auth.Policies) {
t.Fatalf("bad: policies: expected: %q, actual: %q", expected, resp.Auth.Policies)
}
}
/*
* Acceptance test for LDAP Auth Method
*
* The tests here rely on a docker LDAP server:
* [https://github.com/rroemhild/docker-test-openldap]
*
* ...as well as existence of a person object, `cn=Hermes Conrad,dc=example,dc=com`,
* which is a member of a group, `cn=admin_staff,ou=people,dc=example,dc=com`
*
* Querying the server from the command line:
* $ docker run --privileged -d -p 389:389 --name ldap --rm rroemhild/test-openldap
* $ ldapsearch -x -H ldap://localhost -b dc=planetexpress,dc=com -s sub uid=hermes
* $ ldapsearch -x -H ldap://localhost -b dc=planetexpress,dc=com -s sub \
'member=cn=Hermes Conrad,ou=people,dc=planetexpress,dc=com'
*/
func factory(t *testing.T) logical.Backend {
defaultLeaseTTLVal := time.Hour * 24
maxLeaseTTLVal := time.Hour * 24 * 32
b, err := Factory(context.Background(), &logical.BackendConfig{
Logger: hclog.New(&hclog.LoggerOptions{
Name: "FactoryLogger",
Level: hclog.Debug,
}),
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: defaultLeaseTTLVal,
MaxLeaseTTLVal: maxLeaseTTLVal,
},
})
if err != nil {
t.Fatalf("Unable to create backend: %s", err)
}
return b
}
func TestBackend_basic(t *testing.T) {
b := factory(t)
cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
defer cleanup()
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrl(t, cfg),
// Map Admin_staff group (from LDAP server) with foo policy
testAccStepGroup(t, "admin_staff", "foo"),
// Map engineers group (local) with bar policy
testAccStepGroup(t, "engineers", "bar"),
// Map hermes conrad user with local engineers group
testAccStepUser(t, "hermes conrad", "engineers"),
// Authenticate
testAccStepLogin(t, "hermes conrad", "hermes"),
// Verify both groups mappings can be listed back
testAccStepGroupList(t, []string{"engineers", "admin_staff"}),
// Verify user mapping can be listed back
testAccStepUserList(t, []string{"hermes conrad"}),
},
})
}
func TestBackend_basic_noPolicies(t *testing.T) {
b := factory(t)
cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
defer cleanup()
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrl(t, cfg),
// Create LDAP user
testAccStepUser(t, "hermes conrad", ""),
// Authenticate
testAccStepLoginNoAttachedPolicies(t, "hermes conrad", "hermes"),
testAccStepUserList(t, []string{"hermes conrad"}),
},
})
}
func TestBackend_basic_group_noPolicies(t *testing.T) {
b := factory(t)
cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
defer cleanup()
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrl(t, cfg),
// Create engineers group with no policies
testAccStepGroup(t, "engineers", ""),
// Map hermes conrad user with local engineers group
testAccStepUser(t, "hermes conrad", "engineers"),
// Authenticate
testAccStepLoginNoAttachedPolicies(t, "hermes conrad", "hermes"),
// Verify group mapping can be listed back
testAccStepGroupList(t, []string{"engineers"}),
},
})
}
func TestBackend_basic_authbind(t *testing.T) {
b := factory(t)
cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
defer cleanup()
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrlWithAuthBind(t, cfg),
testAccStepGroup(t, "admin_staff", "foo"),
testAccStepGroup(t, "engineers", "bar"),
testAccStepUser(t, "hermes conrad", "engineers"),
testAccStepLogin(t, "hermes conrad", "hermes"),
},
})
}
func TestBackend_basic_authbind_userfilter(t *testing.T) {
b := factory(t)
cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
defer cleanup()
// userattr not used in the userfilter should result in a warning in the response
cfg.UserFilter = "((mail={{.Username}}))"
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrlWarningCheck(t, cfg, logical.UpdateOperation, []string{userFilterWarning}),
testAccStepConfigUrlWarningCheck(t, cfg, logical.ReadOperation, []string{userFilterWarning}),
},
})
// If both upndomain and userfilter is set, ensure that a warning is still
// returned if userattr is not considered
cfg.UPNDomain = "planetexpress.com"
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrlWarningCheck(t, cfg, logical.UpdateOperation, []string{userFilterWarning}),
testAccStepConfigUrlWarningCheck(t, cfg, logical.ReadOperation, []string{userFilterWarning}),
},
})
cfg.UPNDomain = ""
// Add a liberal user filter, allowing to log in with either cn or email
cfg.UserFilter = "(|({{.UserAttr}}={{.Username}})(mail={{.Username}}))"
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrl(t, cfg),
// Create engineers group with no policies
testAccStepGroup(t, "engineers", ""),
// Map hermes conrad user with local engineers group
testAccStepUser(t, "hermes conrad", "engineers"),
// Authenticate with cn attribute
testAccStepLoginNoAttachedPolicies(t, "hermes conrad", "hermes"),
// Authenticate with mail attribute
testAccStepLoginNoAttachedPolicies(t, "hermes@planetexpress.com", "hermes"),
},
})
// A filter giving the same DN makes the entity_id the same
entity_id := ""
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrl(t, cfg),
// Create engineers group with no policies
testAccStepGroup(t, "engineers", ""),
// Map hermes conrad user with local engineers group
testAccStepUser(t, "hermes conrad", "engineers"),
// Authenticate with cn attribute
testAccStepLoginReturnsSameEntity(t, "hermes conrad", "hermes", &entity_id),
// Authenticate with mail attribute
testAccStepLoginReturnsSameEntity(t, "hermes@planetexpress.com", "hermes", &entity_id),
},
})
// Missing entity alias attribute means access denied
cfg.UserAttr = "inexistent"
cfg.UserFilter = "(|({{.UserAttr}}={{.Username}})(mail={{.Username}}))"
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrl(t, cfg),
// Authenticate with mail attribute will find DN but missing attribute means access denied
testAccStepLoginFailure(t, "hermes@planetexpress.com", "hermes"),
},
})
cfg.UserAttr = "cn"
// UPNDomain has precedence over userfilter, for backward compatibility
cfg.UPNDomain = "planetexpress.com"
addUPNAttributeToLDAPSchemaAndUser(t, cfg, "cn=Hubert J. Farnsworth,ou=people,dc=planetexpress,dc=com", "professor@planetexpress.com")
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrlWithAuthBind(t, cfg),
testAccStepLoginNoAttachedPolicies(t, "professor", "professor"),
},
})
cfg.UPNDomain = ""
// Add a strict user filter, rejecting login of bureaucrats
cfg.UserFilter = "(&({{.UserAttr}}={{.Username}})(!(employeeType=Bureaucrat)))"
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrl(t, cfg),
// Authenticate with cn attribute
testAccStepLoginFailure(t, "hermes conrad", "hermes"),
},
})
// Login fails when multiple user match search filter (using an incorrect filter on purporse)
cfg.UserFilter = "(objectClass=*)"
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
// testAccStepConfigUrl(t, cfg),
testAccStepConfigUrlWithAuthBind(t, cfg),
// Authenticate with cn attribute
testAccStepLoginFailure(t, "hermes conrad", "hermes"),
},
})
// If UserAttr returns multiple attributes that can be used as alias then
// we return an error...
cfg.UserAttr = "employeeType"
cfg.UserFilter = "(cn={{.Username}})"
cfg.UsernameAsAlias = false
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrl(t, cfg),
testAccStepLoginFailure(t, "hermes conrad", "hermes"),
},
})
// ...unless username_as_alias has been set in which case we don't care
// about the alias returned by the LDAP server and always use the username
cfg.UsernameAsAlias = true
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrl(t, cfg),
testAccStepLoginNoAttachedPolicies(t, "hermes conrad", "hermes"),
},
})
}
func TestBackend_basic_authbind_metadata_name(t *testing.T) {
b := factory(t)
cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
defer cleanup()
cfg.UserAttr = "cn"
cfg.UPNDomain = "planetexpress.com"
addUPNAttributeToLDAPSchemaAndUser(t, cfg, "cn=Hubert J. Farnsworth,ou=people,dc=planetexpress,dc=com", "professor@planetexpress.com")
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrlWithAuthBind(t, cfg),
testAccStepLoginAliasMetadataName(t, "professor", "professor"),
},
})
}
func addUPNAttributeToLDAPSchemaAndUser(t *testing.T, cfg *ldaputil.ConfigEntry, testUserDN string, testUserUPN string) {
// Setup connection
client := &ldaputil.Client{
Logger: hclog.New(&hclog.LoggerOptions{
Name: "LDAPAuthTest",
Level: hclog.Debug,
}),
LDAP: ldaputil.NewLDAP(),
}
conn, err := client.DialLDAP(cfg)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
if err := conn.Bind("cn=admin,cn=config", cfg.BindPassword); err != nil {
t.Fatal(err)
}
// Add userPrincipalName attribute type
userPrincipleNameTypeReq := goldap.NewModifyRequest("cn={0}core,cn=schema,cn=config", nil)
userPrincipleNameTypeReq.Add("olcAttributetypes", []string{"( 2.25.247072656268950430024439664556757516066 NAME ( 'userPrincipalName' ) SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 EQUALITY caseIgnoreMatch SINGLE-VALUE )"})
if err := conn.Modify(userPrincipleNameTypeReq); err != nil {
t.Fatal(err)
}
// Add new object class
userPrincipleNameObjClassReq := goldap.NewModifyRequest("cn={0}core,cn=schema,cn=config", nil)
userPrincipleNameObjClassReq.Add("olcObjectClasses", []string{"( 1.2.840.113556.6.2.6 NAME 'PrincipalNameClass' AUXILIARY MAY ( userPrincipalName ) )"})
if err := conn.Modify(userPrincipleNameObjClassReq); err != nil {
t.Fatal(err)
}
// Re-authenticate with the binddn user
if err := conn.Bind(cfg.BindDN, cfg.BindPassword); err != nil {
t.Fatal(err)
}
// Modify professor user and add userPrincipalName attribute
modifyUserReq := goldap.NewModifyRequest(testUserDN, nil)
modifyUserReq.Add("objectClass", []string{"PrincipalNameClass"})
modifyUserReq.Add("userPrincipalName", []string{testUserUPN})
if err := conn.Modify(modifyUserReq); err != nil {
t.Fatal(err)
}
}
func TestBackend_basic_discover(t *testing.T) {
b := factory(t)
cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
defer cleanup()
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrlWithDiscover(t, cfg),
testAccStepGroup(t, "admin_staff", "foo"),
testAccStepGroup(t, "engineers", "bar"),
testAccStepUser(t, "hermes conrad", "engineers"),
testAccStepLogin(t, "hermes conrad", "hermes"),
},
})
}
func TestBackend_basic_nogroupdn(t *testing.T) {
b := factory(t)
cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
defer cleanup()
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepConfigUrlNoGroupDN(t, cfg),
testAccStepGroup(t, "admin_staff", "foo"),
testAccStepGroup(t, "engineers", "bar"),
testAccStepUser(t, "hermes conrad", "engineers"),
testAccStepLoginNoGroupDN(t, "hermes conrad", "hermes"),
},
})
}
func TestBackend_groupCrud(t *testing.T) {
b := factory(t)
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepGroup(t, "g1", "foo"),
testAccStepReadGroup(t, "g1", "foo"),
testAccStepDeleteGroup(t, "g1"),
testAccStepReadGroup(t, "g1", ""),
},
})
}
/*
* Test backend configuration defaults are successfully read.
*/
func TestBackend_configDefaultsAfterUpdate(t *testing.T) {
b := factory(t)
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
{
Operation: logical.UpdateOperation,
Path: "config",
Data: map[string]interface{}{},
},
{
Operation: logical.ReadOperation,
Path: "config",
Check: func(resp *logical.Response) error {
if resp == nil {
return fmt.Errorf("bad: %#v", resp)
}
// Test well-known defaults
cfg := resp.Data
defaultGroupFilter := "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))"
if cfg["groupfilter"] != defaultGroupFilter {
t.Errorf("Default mismatch: groupfilter. Expected: '%s', received :'%s'", defaultGroupFilter, cfg["groupfilter"])
}
defaultGroupAttr := "cn"
if cfg["groupattr"] != defaultGroupAttr {
t.Errorf("Default mismatch: groupattr. Expected: '%s', received :'%s'", defaultGroupAttr, cfg["groupattr"])
}
defaultUserAttr := "cn"
if cfg["userattr"] != defaultUserAttr {
t.Errorf("Default mismatch: userattr. Expected: '%s', received :'%s'", defaultUserAttr, cfg["userattr"])
}
defaultUserFilter := "({{.UserAttr}}={{.Username}})"
if cfg["userfilter"] != defaultUserFilter {
t.Errorf("Default mismatch: userfilter. Expected: '%s', received :'%s'", defaultUserFilter, cfg["userfilter"])
}
defaultDenyNullBind := true
if cfg["deny_null_bind"] != defaultDenyNullBind {
t.Errorf("Default mismatch: deny_null_bind. Expected: '%t', received :'%s'", defaultDenyNullBind, cfg["deny_null_bind"])
}
return nil
},
},
},
})
}
func testAccStepConfigUrl(t *testing.T, cfg *ldaputil.ConfigEntry) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config",
Data: map[string]interface{}{
"url": cfg.Url,
"userattr": cfg.UserAttr,
"userdn": cfg.UserDN,
"userfilter": cfg.UserFilter,
"groupdn": cfg.GroupDN,
"groupattr": cfg.GroupAttr,
"binddn": cfg.BindDN,
"bindpass": cfg.BindPassword,
"case_sensitive_names": true,
"token_policies": "abc,xyz",
"request_timeout": cfg.RequestTimeout,
"username_as_alias": cfg.UsernameAsAlias,
},
}
}
func testAccStepConfigUrlWithAuthBind(t *testing.T, cfg *ldaputil.ConfigEntry) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config",
Data: map[string]interface{}{
// In this test we also exercise multiple URL support
"url": "foobar://ldap.example.com," + cfg.Url,
"userattr": cfg.UserAttr,
"userdn": cfg.UserDN,
"groupdn": cfg.GroupDN,
"groupattr": cfg.GroupAttr,
"binddn": cfg.BindDN,
"bindpass": cfg.BindPassword,
"upndomain": cfg.UPNDomain,
"case_sensitive_names": true,
"token_policies": "abc,xyz",
"request_timeout": cfg.RequestTimeout,
},
}
}
func testAccStepConfigUrlWithDiscover(t *testing.T, cfg *ldaputil.ConfigEntry) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config",
Data: map[string]interface{}{
"url": cfg.Url,
"userattr": cfg.UserAttr,
"userdn": cfg.UserDN,
"groupdn": cfg.GroupDN,
"groupattr": cfg.GroupAttr,
"binddn": cfg.BindDN,
"bindpass": cfg.BindPassword,
"discoverdn": true,
"case_sensitive_names": true,
"token_policies": "abc,xyz",
"request_timeout": cfg.RequestTimeout,
},
}
}
func testAccStepConfigUrlNoGroupDN(t *testing.T, cfg *ldaputil.ConfigEntry) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config",
Data: map[string]interface{}{
"url": cfg.Url,
"userattr": cfg.UserAttr,
"userdn": cfg.UserDN,
"binddn": cfg.BindDN,
"bindpass": cfg.BindPassword,
"discoverdn": true,
"case_sensitive_names": true,
"request_timeout": cfg.RequestTimeout,
},
}
}
func testAccStepConfigUrlWarningCheck(t *testing.T, cfg *ldaputil.ConfigEntry, operation logical.Operation, warnings []string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: operation,
Path: "config",
Data: map[string]interface{}{
"url": cfg.Url,
"userattr": cfg.UserAttr,
"userdn": cfg.UserDN,
"userfilter": cfg.UserFilter,
"groupdn": cfg.GroupDN,
"groupattr": cfg.GroupAttr,
"binddn": cfg.BindDN,
"bindpass": cfg.BindPassword,
"case_sensitive_names": true,
"token_policies": "abc,xyz",
"request_timeout": cfg.RequestTimeout,
},
Check: func(response *logical.Response) error {
if len(response.Warnings) == 0 {
return fmt.Errorf("expected warnings, got none")
}
if !strutil.StrListSubset(response.Warnings, warnings) {
return fmt.Errorf("expected response to contain the following warnings:\n%s\ngot:\n%s", warnings, response.Warnings)
}
return nil
},
}
}
func testAccStepGroup(t *testing.T, group string, policies string) logicaltest.TestStep {
t.Logf("[testAccStepGroup] - Registering group %s, policy %s", group, policies)
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "groups/" + group,
Data: map[string]interface{}{
"policies": policies,
},
}
}
func testAccStepReadGroup(t *testing.T, group string, policies string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "groups/" + group,
Check: func(resp *logical.Response) error {
if resp == nil {
if policies == "" {
return nil
}
return fmt.Errorf("bad: %#v", resp)
}
var d struct {
Policies []string `mapstructure:"policies"`
}
if err := mapstructure.Decode(resp.Data, &d); err != nil {
return err
}
if !reflect.DeepEqual(d.Policies, policyutil.ParsePolicies(policies)) {
return fmt.Errorf("bad: %#v", resp)
}
return nil
},
}
}
func testAccStepDeleteGroup(t *testing.T, group string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.DeleteOperation,
Path: "groups/" + group,
}
}
func TestBackend_userCrud(t *testing.T) {
b := Backend()
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testAccStepUser(t, "g1", "bar"),
testAccStepReadUser(t, "g1", "bar"),
testAccStepDeleteUser(t, "g1"),
testAccStepReadUser(t, "g1", ""),
},
})
}
func testAccStepUser(t *testing.T, user string, groups string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "users/" + user,
Data: map[string]interface{}{
"groups": groups,
},
}
}
func testAccStepReadUser(t *testing.T, user string, groups string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "users/" + user,
Check: func(resp *logical.Response) error {
if resp == nil {
if groups == "" {
return nil
}
return fmt.Errorf("bad: %#v", resp)
}
var d struct {
Groups string `mapstructure:"groups"`
}
if err := mapstructure.Decode(resp.Data, &d); err != nil {
return err
}
if d.Groups != groups {
return fmt.Errorf("bad: %#v", resp)
}
return nil
},
}
}
func testAccStepDeleteUser(t *testing.T, user string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.DeleteOperation,
Path: "users/" + user,
}
}
func testAccStepLogin(t *testing.T, user string, pass string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login/" + user,
Data: map[string]interface{}{
"password": pass,
},
Unauthenticated: true,
// Verifies user hermes conrad maps to groups via local group (engineers) as well as remote group (Scientists)
Check: logicaltest.TestCheckAuth([]string{"abc", "bar", "default", "foo", "xyz"}),
}
}
func testAccStepLoginReturnsSameEntity(t *testing.T, user string, pass string, entity_id *string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login/" + user,
Data: map[string]interface{}{
"password": pass,
},
Unauthenticated: true,
// Verifies user hermes conrad maps to groups via local group (engineers) as well as remote group (Scientists)
Check: logicaltest.TestCheckAuthEntityId(entity_id),
}
}
func testAccStepLoginNoAttachedPolicies(t *testing.T, user string, pass string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login/" + user,
Data: map[string]interface{}{
"password": pass,
},
Unauthenticated: true,
// Verifies user hermes conrad maps to groups via local group (engineers) as well as remote group (Scientists)
Check: logicaltest.TestCheckAuth([]string{"abc", "default", "xyz"}),
}
}
func testAccStepLoginAliasMetadataName(t *testing.T, user string, pass string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login/" + user,
Data: map[string]interface{}{
"password": pass,
},
Unauthenticated: true,
Check: logicaltest.TestCheckAuthEntityAliasMetadataName("name", user),
}
}
func testAccStepLoginFailure(t *testing.T, user string, pass string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login/" + user,
Data: map[string]interface{}{
"password": pass,
},
Unauthenticated: true,
ErrorOk: true,
}
}
func testAccStepLoginNoGroupDN(t *testing.T, user string, pass string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login/" + user,
Data: map[string]interface{}{
"password": pass,
},
Unauthenticated: true,
// Verifies a search without defined GroupDN returns a warning rather than failing
Check: func(resp *logical.Response) error {
if len(resp.Warnings) != 1 {
return fmt.Errorf("expected a warning due to no group dn, got: %#v", resp.Warnings)
}
return logicaltest.TestCheckAuth([]string{"bar", "default"})(resp)
},
}
}
func testAccStepGroupList(t *testing.T, groups []string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ListOperation,
Path: "groups",
Check: func(resp *logical.Response) error {
if resp.IsError() {
return fmt.Errorf("got error response: %#v", *resp)
}
expected := make([]string, len(groups))
copy(expected, groups)
sort.Strings(expected)
sortedResponse := make([]string, len(resp.Data["keys"].([]string)))
copy(sortedResponse, resp.Data["keys"].([]string))
sort.Strings(sortedResponse)
if !reflect.DeepEqual(expected, sortedResponse) {
return fmt.Errorf("expected:\n%#v\ngot:\n%#v\n", expected, sortedResponse)
}
return nil
},
}
}
func testAccStepUserList(t *testing.T, users []string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ListOperation,
Path: "users",
Check: func(resp *logical.Response) error {
if resp.IsError() {
return fmt.Errorf("got error response: %#v", *resp)
}
expected := make([]string, len(users))
copy(expected, users)
sort.Strings(expected)
sortedResponse := make([]string, len(resp.Data["keys"].([]string)))
copy(sortedResponse, resp.Data["keys"].([]string))
sort.Strings(sortedResponse)
if !reflect.DeepEqual(expected, sortedResponse) {
return fmt.Errorf("expected:\n%#v\ngot:\n%#v\n", expected, sortedResponse)
}
return nil
},
}
}
func TestLdapAuthBackend_ConfigUpgrade(t *testing.T) {
var resp *logical.Response
var err error
b, storage := createBackendWithStorage(t)
ctx := context.Background()
cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
defer cleanup()
configReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config",
Data: map[string]interface{}{
"url": cfg.Url,
"userattr": cfg.UserAttr,
"userdn": cfg.UserDN,
"userfilter": cfg.UserFilter,
"groupdn": cfg.GroupDN,
"groupattr": cfg.GroupAttr,
"binddn": cfg.BindDN,
"bindpass": cfg.BindPassword,
"token_period": "5m",
"token_explicit_max_ttl": "24h",
"request_timeout": cfg.RequestTimeout,
},
Storage: storage,
Connection: &logical.Connection{},
}
resp, err = b.HandleRequest(ctx, configReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
fd, err := b.getConfigFieldData()
if err != nil {
t.Fatal(err)
}
defParams, err := ldaputil.NewConfigEntry(nil, fd)
if err != nil {
t.Fatal(err)
}
falseBool := new(bool)
*falseBool = false
exp := &ldapConfigEntry{
TokenParams: tokenutil.TokenParams{
TokenPeriod: 5 * time.Minute,
TokenExplicitMaxTTL: 24 * time.Hour,
},
ConfigEntry: &ldaputil.ConfigEntry{
Url: cfg.Url,
UserAttr: cfg.UserAttr,
UserFilter: cfg.UserFilter,
UserDN: cfg.UserDN,
GroupDN: cfg.GroupDN,
GroupAttr: cfg.GroupAttr,
BindDN: cfg.BindDN,
BindPassword: cfg.BindPassword,
GroupFilter: defParams.GroupFilter,
DenyNullBind: defParams.DenyNullBind,
TLSMinVersion: defParams.TLSMinVersion,
TLSMaxVersion: defParams.TLSMaxVersion,
CaseSensitiveNames: falseBool,
UsePre111GroupCNBehavior: new(bool),
RequestTimeout: cfg.RequestTimeout,
UsernameAsAlias: false,
},
}
configEntry, err := b.Config(ctx, configReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(exp, configEntry); diff != nil {
t.Fatal(diff)
}
// Store just the config entry portion, for upgrade testing
entry, err := logical.StorageEntryJSON("config", configEntry.ConfigEntry)
if err != nil {
t.Fatal(err)
}
err = configReq.Storage.Put(ctx, entry)
if err != nil {
t.Fatal(err)
}
configEntry, err = b.Config(ctx, configReq)
if err != nil {
t.Fatal(err)
}
// We won't have token params anymore so nil those out
exp.TokenParams = tokenutil.TokenParams{}
if diff := deep.Equal(exp, configEntry); diff != nil {
t.Fatal(diff)
}
}