package ldap import ( "context" "fmt" "reflect" "sort" "testing" "time" "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/logical" logicaltest "github.com/hashicorp/vault/logical/testing" "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_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/teSlA", 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] != "teSlA" { t.Fatalf("bad: %s", keys[0]) } default: if keys[0] != "tesla" { 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/tesla", Storage: storage, } 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/tesla", Data: map[string]interface{}{ "password": "password", }, Storage: storage, } 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) } } configReq := &logical.Request{ Operation: logical.UpdateOperation, Path: "config", Data: map[string]interface{}{ // Online LDAP test server // http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ "url": "ldap://ldap.forumsys.com", "userattr": "uid", "userdn": "dc=example,dc=com", "groupdn": "dc=example,dc=com", "binddn": "cn=read-only-admin,dc=example,dc=com", }, 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) configReq := &logical.Request{ Operation: logical.UpdateOperation, Path: "config", Data: map[string]interface{}{ // Online LDAP test server // http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ "url": "ldap://ldap.forumsys.com", "userattr": "uid", "userdn": "dc=example,dc=com", "groupdn": "dc=example,dc=com", "binddn": "cn=read-only-admin,dc=example,dc=com", }, 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, } 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/tesla", Storage: storage, } 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/tesla", Data: map[string]interface{}{ "password": "password", }, Storage: storage, } 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 public LDAP server: * [http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/] * * ...as well as existence of a person object, `uid=tesla,dc=example,dc=com`, * which is a member of a group, `ou=scientists,dc=example,dc=com` * * Querying the server from the command line: * $ ldapsearch -x -H ldap://ldap.forumsys.com -b dc=example,dc=com -s sub \ * '(&(objectClass=groupOfUniqueNames)(uniqueMember=uid=tesla,dc=example,dc=com))' * * $ ldapsearch -x -H ldap://ldap.forumsys.com -b dc=example,dc=com -s sub uid=tesla */ func factory(t *testing.T) logical.Backend { defaultLeaseTTLVal := time.Hour * 24 maxLeaseTTLVal := time.Hour * 24 * 32 b, err := Factory(context.Background(), &logical.BackendConfig{ Logger: nil, 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) logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ testAccStepConfigUrl(t), // Map Scientists group (from LDAP server) with foo policy testAccStepGroup(t, "Scientists", "foo"), // Map engineers group (local) with bar policy testAccStepGroup(t, "engineers", "bar"), // Map tesla user with local engineers group testAccStepUser(t, "tesla", "engineers"), // Authenticate testAccStepLogin(t, "tesla", "password"), // Verify both groups mappings can be listed back testAccStepGroupList(t, []string{"engineers", "Scientists"}), // Verify user mapping can be listed back testAccStepUserList(t, []string{"tesla"}), }, }) } func TestBackend_basic_authbind(t *testing.T) { b := factory(t) logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ testAccStepConfigUrlWithAuthBind(t), testAccStepGroup(t, "Scientists", "foo"), testAccStepGroup(t, "engineers", "bar"), testAccStepUser(t, "tesla", "engineers"), testAccStepLogin(t, "tesla", "password"), }, }) } func TestBackend_basic_discover(t *testing.T) { b := factory(t) logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ testAccStepConfigUrlWithDiscover(t), testAccStepGroup(t, "Scientists", "foo"), testAccStepGroup(t, "engineers", "bar"), testAccStepUser(t, "tesla", "engineers"), testAccStepLogin(t, "tesla", "password"), }, }) } func TestBackend_basic_nogroupdn(t *testing.T) { b := factory(t) logicaltest.Test(t, logicaltest.TestCase{ Backend: b, Steps: []logicaltest.TestStep{ testAccStepConfigUrlNoGroupDN(t), testAccStepGroup(t, "Scientists", "foo"), testAccStepGroup(t, "engineers", "bar"), testAccStepUser(t, "tesla", "engineers"), testAccStepLoginNoGroupDN(t, "tesla", "password"), }, }) } func TestBackend_groupCrud(t *testing.T) { b := factory(t) logicaltest.Test(t, logicaltest.TestCase{ Backend: 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{ Backend: b, Steps: []logicaltest.TestStep{ logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "config", Data: map[string]interface{}{}, }, logicaltest.TestStep{ 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"]) } 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) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "config", Data: map[string]interface{}{ // Online LDAP test server // http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ "url": "ldap://ldap.forumsys.com", "userattr": "uid", "userdn": "dc=example,dc=com", "groupdn": "dc=example,dc=com", "case_sensitive_names": true, }, } } func testAccStepConfigUrlWithAuthBind(t *testing.T) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "config", Data: map[string]interface{}{ // Online LDAP test server // http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ // In this test we also exercise multiple URL support "url": "foobar://ldap.example.com,ldap://ldap.forumsys.com", "userattr": "uid", "userdn": "dc=example,dc=com", "groupdn": "dc=example,dc=com", "binddn": "cn=read-only-admin,dc=example,dc=com", "bindpass": "password", "case_sensitive_names": true, }, } } func testAccStepConfigUrlWithDiscover(t *testing.T) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "config", Data: map[string]interface{}{ // Online LDAP test server // http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ "url": "ldap://ldap.forumsys.com", "userattr": "uid", "userdn": "dc=example,dc=com", "groupdn": "dc=example,dc=com", "discoverdn": true, "case_sensitive_names": true, }, } } func testAccStepConfigUrlNoGroupDN(t *testing.T) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "config", Data: map[string]interface{}{ // Online LDAP test server // http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ "url": "ldap://ldap.forumsys.com", "userattr": "uid", "userdn": "dc=example,dc=com", "discoverdn": true, "case_sensitive_names": true, }, } } 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{ Backend: 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 tesla maps to groups via local group (engineers) as well as remote group (Scientists) Check: logicaltest.TestCheckAuth([]string{"bar", "default", "foo"}), } } 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 TestLDAPEscape(t *testing.T) { testcases := map[string]string{ "#test": "\\#test", "test,hello": "test\\,hello", "test,hel+lo": "test\\,hel\\+lo", "test\\hello": "test\\\\hello", " test ": "\\ test \\ ", } for test, answer := range testcases { res := EscapeLDAPValue(test) if res != answer { t.Errorf("Failed to escape %s: %s != %s\n", test, res, answer) } } } 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 }, } }