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

package vault

import (
	"strings"
	"testing"

	credLdap "github.com/hashicorp/vault/builtin/credential/ldap"
	credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
	"github.com/hashicorp/vault/helper/identity"
	"github.com/hashicorp/vault/helper/namespace"
	"github.com/hashicorp/vault/sdk/logical"
	"github.com/kr/pretty"
)

func TestIdentityStore_CaseInsensitiveGroupAliasName(t *testing.T) {
	ctx := namespace.RootContext(nil)
	i, accessor, _ := testIdentityStoreWithGithubAuth(ctx, t)

	// Create a group
	resp, err := i.HandleRequest(ctx, &logical.Request{
		Path:      "group",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"type": "external",
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
	}
	groupID := resp.Data["id"].(string)

	testAliasName := "testAliasName"

	// Create a case sensitive alias name
	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"mount_accessor": accessor,
			"canonical_id":   groupID,
			"name":           testAliasName,
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
	}
	aliasID := resp.Data["id"].(string)

	// Ensure that reading the alias returns case sensitive alias name
	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias/id/" + aliasID,
		Operation: logical.ReadOperation,
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
	}
	aliasName := resp.Data["name"].(string)
	if aliasName != testAliasName {
		t.Fatalf("bad alias name; expected: %q, actual: %q", testAliasName, aliasName)
	}

	// Overwrite the alias using lower cased alias name. This shouldn't error.
	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias/id/" + aliasID,
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"mount_accessor": accessor,
			"canonical_id":   groupID,
			"name":           strings.ToLower(testAliasName),
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
	}

	// Ensure that reading the alias returns lower cased alias name
	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias/id/" + aliasID,
		Operation: logical.ReadOperation,
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
	}
	aliasName = resp.Data["name"].(string)
	if aliasName != strings.ToLower(testAliasName) {
		t.Fatalf("bad alias name; expected: %q, actual: %q", testAliasName, aliasName)
	}
}

func TestIdentityStore_EnsureNoDanglingGroupAlias(t *testing.T) {
	err := AddTestCredentialBackend("userpass", credUserpass.Factory)
	if err != nil {
		t.Fatal(err)
	}

	err = AddTestCredentialBackend("ldap", credLdap.Factory)
	if err != nil {
		t.Fatal(err)
	}

	c, _, _ := TestCoreUnsealed(t)

	ctx := namespace.RootContext(nil)

	userpassMe := &MountEntry{
		Table:       credentialTableType,
		Path:        "userpass/",
		Type:        "userpass",
		Description: "userpass",
	}
	err = c.enableCredential(ctx, userpassMe)
	if err != nil {
		t.Fatal(err)
	}

	ldapMe := &MountEntry{
		Table:       credentialTableType,
		Path:        "ldap/",
		Type:        "ldap",
		Description: "ldap",
	}
	err = c.enableCredential(ctx, ldapMe)
	if err != nil {
		t.Fatal(err)
	}

	// Create a group
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "group",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"type": "external",
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}
	groupID := resp.Data["id"].(string)

	// Add an alias to the group from the userpass auth method
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"name":           "testgroupalias",
			"mount_accessor": userpassMe.Accessor,
			"canonical_id":   groupID,
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}
	userpassGroupAliasID := resp.Data["id"].(string)

	// Ensure that the alias is readable
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias/id/" + userpassGroupAliasID,
		Operation: logical.ReadOperation,
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}
	if resp == nil || resp.Data["id"].(string) != userpassGroupAliasID {
		t.Fatalf("failed to read userpass group alias")
	}

	// Attach a different alias to the same group, overriding the previous one
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"name":           "testgroupalias",
			"mount_accessor": ldapMe.Accessor,
			"canonical_id":   groupID,
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}
	ldapGroupAliasID := resp.Data["id"].(string)

	// Ensure that the new alias is readable
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias/id/" + ldapGroupAliasID,
		Operation: logical.ReadOperation,
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}
	if resp == nil || resp.Data["id"].(string) != ldapGroupAliasID {
		t.Fatalf("failed to read ldap group alias")
	}

	// Ensure previous alias is gone
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias/id/" + userpassGroupAliasID,
		Operation: logical.ReadOperation,
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}
	if resp != nil {
		t.Fatalf("expected a nil response")
	}
}

func TestIdentityStore_GroupAliasDeletionOnGroupDeletion(t *testing.T) {
	var resp *logical.Response
	var err error

	ctx := namespace.RootContext(nil)
	i, accessor, _ := testIdentityStoreWithGithubAuth(ctx, t)

	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"type": "external",
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}
	groupID := resp.Data["id"].(string)

	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"name":           "testgroupalias",
			"mount_accessor": accessor,
			"canonical_id":   groupID,
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}
	groupAliasID := resp.Data["id"].(string)

	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group/id/" + groupID,
		Operation: logical.DeleteOperation,
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
	}

	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias/id/" + groupAliasID,
		Operation: logical.ReadOperation,
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v", resp, err)
	}
	if resp != nil {
		t.Fatalf("expected a nil response")
	}
}

func TestIdentityStore_GroupAliases_CRUD(t *testing.T) {
	var resp *logical.Response
	var err error
	ctx := namespace.RootContext(nil)
	i, accessor, _ := testIdentityStoreWithGithubAuth(ctx, t)

	groupReq := &logical.Request{
		Path:      "group",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"type": "external",
		},
	}
	resp, err = i.HandleRequest(ctx, groupReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}
	groupID := resp.Data["id"].(string)

	groupAliasReq := &logical.Request{
		Path:      "group-alias",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"name":           "testgroupalias",
			"mount_accessor": accessor,
			"canonical_id":   groupID,
			"mount_type":     "ldap",
		},
	}
	resp, err = i.HandleRequest(ctx, groupAliasReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}
	groupAliasID := resp.Data["id"].(string)

	groupAliasReq.Path = "group-alias/id/" + groupAliasID
	groupAliasReq.Operation = logical.ReadOperation
	resp, err = i.HandleRequest(ctx, groupAliasReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}

	if resp.Data["id"].(string) != groupAliasID {
		t.Fatalf("bad: group alias: %#v\n", resp.Data)
	}

	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias/id/" + groupAliasID,
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"name":           "testupdatedgroupaliasname",
			"mount_accessor": accessor,
			"canonical_id":   groupID,
			"mount_type":     "ldap",
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err: %v; resp: %#v", err, resp)
	}
	if resp.Data["id"].(string) != groupAliasID {
		t.Fatalf("bad: group alias: %#v\n", resp.Data)
	}

	groupAliasReq.Operation = logical.DeleteOperation
	resp, err = i.HandleRequest(ctx, groupAliasReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}

	groupAliasReq.Operation = logical.ReadOperation
	resp, err = i.HandleRequest(ctx, groupAliasReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v\nerr: %v\n", resp, err)
	}

	if resp != nil {
		t.Fatalf("failed to delete group alias")
	}
}

func TestIdentityStore_GroupAliases_MemDBIndexes(t *testing.T) {
	var err error
	ctx := namespace.RootContext(nil)
	i, accessor, _ := testIdentityStoreWithGithubAuth(ctx, t)

	group := &identity.Group{
		ID:   "testgroupid",
		Name: "testgroupname",
		Metadata: map[string]string{
			"testmetadatakey1": "testmetadatavalue1",
			"testmetadatakey2": "testmetadatavalue2",
		},
		Alias: &identity.Alias{
			ID:            "testgroupaliasid",
			Name:          "testalias",
			MountAccessor: accessor,
			CanonicalID:   "testgroupid",
			MountType:     "ldap",
		},
		ParentGroupIDs:  []string{"testparentgroupid1", "testparentgroupid2"},
		MemberEntityIDs: []string{"testentityid1", "testentityid2"},
		Policies:        []string{"testpolicy1", "testpolicy2"},
		BucketKey:       i.groupPacker.BucketKey("testgroupid"),
	}

	txn := i.db.Txn(true)
	defer txn.Abort()
	err = i.MemDBUpsertAliasInTxn(txn, group.Alias, true)
	if err != nil {
		t.Fatal(err)
	}
	err = i.MemDBUpsertGroupInTxn(txn, group)
	if err != nil {
		t.Fatal(err)
	}
	txn.Commit()

	alias, err := i.MemDBAliasByID("testgroupaliasid", false, true)
	if err != nil {
		t.Fatal(err)
	}
	if alias.ID != "testgroupaliasid" {
		t.Fatalf("bad: group alias: %#v\n", alias)
	}

	group, err = i.MemDBGroupByAliasID("testgroupaliasid", false)
	if err != nil {
		t.Fatal(err)
	}
	if group.ID != "testgroupid" {
		t.Fatalf("bad: group: %#v\n", group)
	}

	aliasByFactors, err := i.MemDBAliasByFactors(group.Alias.MountAccessor, group.Alias.Name, false, true)
	if err != nil {
		t.Fatal(err)
	}
	if aliasByFactors.ID != "testgroupaliasid" {
		t.Fatalf("bad: group alias: %#v\n", aliasByFactors)
	}
}

func TestIdentityStore_GroupAliases_AliasOnInternalGroup(t *testing.T) {
	var err error
	var resp *logical.Response

	ctx := namespace.RootContext(nil)
	i, accessor, _ := testIdentityStoreWithGithubAuth(ctx, t)

	groupReq := &logical.Request{
		Path:      "group",
		Operation: logical.UpdateOperation,
	}
	resp, err = i.HandleRequest(ctx, groupReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: resp: %#v; err: %v", resp, err)
	}
	groupID := resp.Data["id"].(string)

	aliasReq := &logical.Request{
		Path:      "group-alias",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"name":           "testname",
			"mount_accessor": accessor,
			"canonical_id":   groupID,
		},
	}
	resp, err = i.HandleRequest(ctx, aliasReq)
	if err != nil {
		t.Fatal(err)
	}
	if !resp.IsError() {
		t.Fatalf("expected an error")
	}
}

func TestIdentityStore_GroupAliasesUpdate(t *testing.T) {
	ctx := namespace.RootContext(nil)
	i, accessor1, c := testIdentityStoreWithGithubAuth(ctx, t)

	ghme2 := &MountEntry{
		Table:       credentialTableType,
		Path:        "github2/",
		Type:        "github",
		Description: "github auth",
	}

	err := c.enableCredential(ctx, ghme2)
	if err != nil {
		t.Fatal(err)
	}
	accessor2 := ghme2.Accessor

	// Create two groups
	resp, err := i.HandleRequest(ctx, &logical.Request{
		Path:      "group",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"type": "external",
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
	}
	groupID1 := resp.Data["id"].(string)

	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"type": "external",
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
	}
	groupID2 := resp.Data["id"].(string)

	// Create an alias
	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"mount_accessor": accessor1,
			"canonical_id":   groupID1,
			"name":           "testalias",
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
	}
	aliasID := resp.Data["id"].(string)

	// Ensure that reading the alias returns the right group
	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias/id/" + aliasID,
		Operation: logical.ReadOperation,
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
	}
	aliasName := resp.Data["name"].(string)
	if aliasName != "testalias" {
		t.Fatalf("bad alias name; expected: testalias, actual: %q", aliasName)
	}
	mountAccessor := resp.Data["mount_accessor"].(string)
	if mountAccessor != accessor1 {
		t.Fatalf("bad mount accessor; expected: %q, actual: %q", accessor1, mountAccessor)
	}
	canonicalID := resp.Data["canonical_id"].(string)
	if canonicalID != groupID1 {
		t.Fatalf("bad canonical name; expected: %q, actual: %q", groupID1, canonicalID)
	}
	// Ensure that reading the group returns the alias
	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group/id/" + groupID1,
		Operation: logical.ReadOperation,
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
	}
	aliasName = resp.Data["alias"].(map[string]interface{})["name"].(string)
	if aliasName != "testalias" {
		t.Fatalf("bad alias name; expected: testalias, actual: %q", aliasName)
	}

	// Overwrite the alias
	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias/id/" + aliasID,
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"mount_accessor": accessor2,
			"canonical_id":   groupID2,
			"name":           "testalias2",
		},
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
	}

	// Ensure that reading the alias returns the right group
	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group-alias/id/" + aliasID,
		Operation: logical.ReadOperation,
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
	}
	aliasName = resp.Data["name"].(string)
	if aliasName != "testalias2" {
		t.Fatalf("bad alias name; expected: testalias2, actual: %q", aliasName)
	}
	mountAccessor = resp.Data["mount_accessor"].(string)
	if mountAccessor != accessor2 {
		t.Fatalf("bad mount accessor; expected: %q, actual: %q", accessor2, mountAccessor)
	}
	canonicalID = resp.Data["canonical_id"].(string)
	if canonicalID != groupID2 {
		t.Fatalf("bad canonical name; expected: %q, actual: %q", groupID2, canonicalID)
	}
	// Ensure that reading the group returns the alias
	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group/id/" + groupID2,
		Operation: logical.ReadOperation,
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
	}
	aliasName = resp.Data["alias"].(map[string]interface{})["name"].(string)
	if aliasName != "testalias2" {
		t.Fatalf("bad alias name; expected: testalias, actual: %q", aliasName)
	}
	// Ensure the old group does not
	resp, err = i.HandleRequest(ctx, &logical.Request{
		Path:      "group/id/" + groupID1,
		Operation: logical.ReadOperation,
	})
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("bad: err:%v\nresp: %#v", err, resp)
	}
	aliasInfo := resp.Data["alias"].(map[string]interface{})
	if len(aliasInfo) > 0 {
		t.Fatalf("still found alias with old group: %s", pretty.Sprint(resp.Data))
	}
}
