// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

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: %q, received :%q", defaultGroupFilter, cfg["groupfilter"])
					}

					defaultGroupAttr := "cn"
					if cfg["groupattr"] != defaultGroupAttr {
						t.Errorf("Default mismatch: groupattr. Expected: %q, received :%q", defaultGroupAttr, cfg["groupattr"])
					}

					defaultUserAttr := "cn"
					if cfg["userattr"] != defaultUserAttr {
						t.Errorf("Default mismatch: userattr. Expected: %q, received :%q", defaultUserAttr, cfg["userattr"])
					}

					defaultUserFilter := "({{.UserAttr}}={{.Username}})"
					if cfg["userfilter"] != defaultUserFilter {
						t.Errorf("Default mismatch: userfilter. Expected: %q, received :%q", defaultUserFilter, cfg["userfilter"])
					}

					defaultDenyNullBind := true
					if cfg["deny_null_bind"] != defaultDenyNullBind {
						t.Errorf("Default mismatch: deny_null_bind. Expected: '%t', received :%q", 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,
			"connection_timeout":   cfg.ConnectionTimeout,
			"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,
			"connection_timeout":   cfg.ConnectionTimeout,
		},
	}
}

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,
			"connection_timeout":   cfg.ConnectionTimeout,
		},
	}
}

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,
			"connection_timeout":   cfg.ConnectionTimeout,
		},
	}
}

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,
			"connection_timeout":   cfg.ConnectionTimeout,
		},
		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,
			"max_page_size":          cfg.MaximumPageSize,
			"connection_timeout":     cfg.ConnectionTimeout,
		},
		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,
			ConnectionTimeout:        cfg.ConnectionTimeout,
			UsernameAsAlias:          false,
			DerefAliases:             "never",
			MaximumPageSize:          1000,
		},
	}

	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)
	}
}
