package awsauth

import (
	"context"
	"os"
	"reflect"
	"strings"
	"testing"

	"github.com/go-test/deep"
	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/go-secure-stdlib/awsutil"
	"github.com/hashicorp/go-secure-stdlib/strutil"
	vlttesting "github.com/hashicorp/vault/helper/testhelpers/logical"
	"github.com/hashicorp/vault/sdk/helper/logging"
	"github.com/hashicorp/vault/sdk/helper/policyutil"
	"github.com/hashicorp/vault/sdk/logical"
)

func TestBackend_pathRoleEc2(t *testing.T) {
	config := logical.TestBackendConfig()
	storage := &logical.InmemStorage{}
	config.StorageView = storage

	b, err := Backend(config)
	if err != nil {
		t.Fatal(err)
	}

	err = b.Setup(context.Background(), config)
	if err != nil {
		t.Fatal(err)
	}

	data := map[string]interface{}{
		"auth_type":    "ec2",
		"policies":     "p,q,r,s",
		"max_ttl":      "2h",
		"bound_ami_id": "ami-abcd123",
	}
	resp, err := b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.CreateOperation,
		Path:      "role/ami-abcd123",
		Data:      data,
		Storage:   storage,
	})
	if resp != nil && resp.IsError() {
		t.Fatalf("failed to create role")
	}
	if err != nil {
		t.Fatal(err)
	}

	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.ReadOperation,
		Path:      "role/ami-abcd123",
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}
	if resp == nil || resp.IsError() {
		t.Fatal("failed to read the role entry")
	}
	if !policyutil.EquivalentPolicies(strings.Split(data["policies"].(string), ","), resp.Data["policies"].([]string)) {
		t.Fatalf("bad: policies: expected: %#v\ngot: %#v\n", data, resp.Data)
	}

	data["allow_instance_migration"] = true
	data["disallow_reauthentication"] = true
	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.UpdateOperation,
		Path:      "role/ami-abcd123",
		Data:      data,
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}
	if resp == nil || !resp.IsError() {
		t.Fatalf("expected failure to create role with both allow_instance_migration true and disallow_reauthentication true")
	}
	data["disallow_reauthentication"] = false
	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.UpdateOperation,
		Path:      "role/ami-abcd123",
		Data:      data,
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}
	if resp != nil && resp.IsError() {
		t.Fatalf("failure to update role: %v", resp.Data["error"])
	}
	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.ReadOperation,
		Path:      "role/ami-abcd123",
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}
	if !resp.Data["allow_instance_migration"].(bool) {
		t.Fatal("bad: expected allow_instance_migration:true got:false\n")
	}

	if resp.Data["disallow_reauthentication"].(bool) {
		t.Fatal("bad: expected disallow_reauthentication: false got:true\n")
	}

	// add another entry, to test listing of role entries
	data["bound_ami_id"] = "ami-abcd456"
	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.CreateOperation,
		Path:      "role/ami-abcd456",
		Data:      data,
		Storage:   storage,
	})
	if resp != nil && resp.IsError() {
		t.Fatalf("failed to create role: %s", resp.Data["error"])
	}
	if err != nil {
		t.Fatal(err)
	}

	data["bound_iam_principal_arn"] = ""
	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.UpdateOperation,
		Path:      "role/ami-abcd456",
		Data:      data,
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}
	if resp != nil && resp.IsError() {
		t.Fatalf("failed to update role with empty bound_iam_principal_arn: %s", resp.Data["error"])
	}

	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.ListOperation,
		Path:      "roles",
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}
	if resp == nil || resp.Data == nil || resp.IsError() {
		t.Fatalf("failed to list the role entries")
	}
	keys := resp.Data["keys"].([]string)
	if len(keys) != 2 {
		t.Fatalf("bad: keys: %#v\n", keys)
	}

	_, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.DeleteOperation,
		Path:      "role/ami-abcd123",
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}

	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.ReadOperation,
		Path:      "role/ami-abcd123",
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}
	if resp != nil {
		t.Fatalf("bad: response: expected:nil actual:%#v\n", resp)
	}
}

func Test_enableIamIDResolution(t *testing.T) {
	config := logical.TestBackendConfig()
	storage := &logical.InmemStorage{}
	config.StorageView = storage

	b, err := Backend(config)
	if err != nil {
		t.Fatal(err)
	}
	err = b.Setup(context.Background(), config)
	if err != nil {
		t.Fatal(err)
	}
	roleName := "upgradable_role"

	b.resolveArnToUniqueIDFunc = resolveArnToFakeUniqueId

	boundIamRoleARNs := []string{"arn:aws:iam::123456789012:role/MyRole", "arn:aws:iam::123456789012:role/path/*"}
	data := map[string]interface{}{
		"auth_type":               iamAuthType,
		"policies":                "p,q",
		"bound_iam_principal_arn": boundIamRoleARNs,
		"resolve_aws_unique_ids":  false,
	}

	submitRequest := func(roleName string, op logical.Operation) (*logical.Response, error) {
		return b.HandleRequest(context.Background(), &logical.Request{
			Operation: op,
			Path:      "role/" + roleName,
			Data:      data,
			Storage:   storage,
		})
	}

	resp, err := submitRequest(roleName, logical.CreateOperation)
	if err != nil {
		t.Fatal(err)
	}
	if resp != nil && resp.IsError() {
		t.Fatalf("failed to create role: %#v", resp)
	}

	resp, err = submitRequest(roleName, logical.ReadOperation)
	if err != nil {
		t.Fatal(err)
	}
	if resp == nil || resp.IsError() {
		t.Fatalf("failed to read role: resp:%#v,\nerr:%#v", resp, err)
	}
	if resp.Data["bound_iam_principal_id"] != nil && len(resp.Data["bound_iam_principal_id"].([]string)) > 0 {
		t.Fatalf("expected to get no unique ID in role, but got %q", resp.Data["bound_iam_principal_id"])
	}

	data = map[string]interface{}{
		"resolve_aws_unique_ids": true,
	}
	resp, err = submitRequest(roleName, logical.UpdateOperation)
	if err != nil {
		t.Fatal(err)
	}
	if resp != nil && resp.IsError() {
		t.Fatalf("unable to upgrade role to resolve internal IDs: resp:%#v", resp)
	}

	resp, err = submitRequest(roleName, logical.ReadOperation)
	if err != nil {
		t.Fatal(err)
	}
	if resp == nil || resp.IsError() {
		t.Fatalf("failed to read role: resp:%#v,\nerr:%#v", resp, err)
	}
	principalIDs := resp.Data["bound_iam_principal_id"].([]string)
	if len(principalIDs) != 1 || principalIDs[0] != "FakeUniqueId1" {
		t.Fatalf("bad: expected upgrade of role resolve principal ID to %q, but got %q instead", "FakeUniqueId1", resp.Data["bound_iam_principal_id"])
	}
	returnedARNs := resp.Data["bound_iam_principal_arn"].([]string)
	if !strutil.EquivalentSlices(returnedARNs, boundIamRoleARNs) {
		t.Fatalf("bad: expected to return bound_iam_principal_arn of %q, but got %q instead", boundIamRoleARNs, returnedARNs)
	}
}

func TestBackend_pathIam(t *testing.T) {
	config := logical.TestBackendConfig()
	storage := &logical.InmemStorage{}
	config.StorageView = storage

	b, err := Backend(config)
	if err != nil {
		t.Fatal(err)
	}
	err = b.Setup(context.Background(), config)
	if err != nil {
		t.Fatal(err)
	}

	// make sure we start with empty roles, which gives us confidence that the read later
	// actually is the two roles we created
	resp, err := b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.ListOperation,
		Path:      "roles",
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}
	if resp == nil || resp.Data == nil || resp.IsError() {
		t.Fatalf("failed to list role entries")
	}
	if resp.Data["keys"] != nil {
		t.Fatalf("Received roles when expected none")
	}

	data := map[string]interface{}{
		"auth_type":               iamAuthType,
		"policies":                "p,q,r,s",
		"max_ttl":                 "2h",
		"bound_iam_principal_arn": "n:aws:iam::123456789012:user/MyUserName",
		"resolve_aws_unique_ids":  false,
	}
	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.CreateOperation,
		Path:      "role/MyRoleName",
		Data:      data,
		Storage:   storage,
	})

	if err != nil {
		t.Fatal(err)
	}
	if resp != nil && resp.IsError() {
		t.Fatalf("failed to create the role entry; resp: %#v", resp)
	}

	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.ReadOperation,
		Path:      "role/MyRoleName",
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}
	if resp == nil || resp.IsError() {
		t.Fatal("failed to read the role entry")
	}
	if !policyutil.EquivalentPolicies(strings.Split(data["policies"].(string), ","), resp.Data["policies"].([]string)) {
		t.Fatalf("bad: policies: expected %#v\ngot: %#v\n", data, resp.Data)
	}

	data["inferred_entity_type"] = "invalid"
	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.CreateOperation,
		Path:      "role/ShouldNeverExist",
		Data:      data,
		Storage:   storage,
	})
	if resp == nil || !resp.IsError() {
		t.Fatalf("Created role with invalid inferred_entity_type")
	}
	if err != nil {
		t.Fatal(err)
	}

	data["inferred_entity_type"] = ec2EntityType
	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.CreateOperation,
		Path:      "role/ShouldNeverExist",
		Data:      data,
		Storage:   storage,
	})
	if resp == nil || !resp.IsError() {
		t.Fatalf("Created role without necessary inferred_aws_region")
	}
	if err != nil {
		t.Fatal(err)
	}

	delete(data, "bound_iam_principal_arn")
	data["inferred_aws_region"] = "us-east-1"
	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.CreateOperation,
		Path:      "role/ShouldNeverExist",
		Data:      data,
		Storage:   storage,
	})
	if resp == nil || !resp.IsError() {
		t.Fatalf("Created role without anything bound")
	}
	if err != nil {
		t.Fatal(err)
	}

	// generate a second role, ensure we're able to list both
	data["bound_ami_id"] = "ami-abcd123"
	secondRole := &logical.Request{
		Operation: logical.CreateOperation,
		Path:      "role/MyOtherRoleName",
		Data:      data,
		Storage:   storage,
	}
	resp, err = b.HandleRequest(context.Background(), secondRole)
	if err != nil {
		t.Fatal(err)
	}
	if resp != nil && resp.IsError() {
		t.Fatalf("failed to create additional role: %v", *secondRole)
	}

	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.ListOperation,
		Path:      "roles",
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}
	if resp == nil || resp.Data == nil || resp.IsError() {
		t.Fatalf("failed to list role entries")
	}
	keys := resp.Data["keys"].([]string)
	if len(keys) != 2 {
		t.Fatalf("bad: keys %#v\n", keys)
	}

	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.DeleteOperation,
		Path:      "role/MyOtherRoleName",
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}

	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.ReadOperation,
		Path:      "role/MyOtherRoleName",
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}
	if resp != nil {
		t.Fatalf("bad: response: expected: nil actual:%3v\n", resp)
	}
}

func TestBackend_pathRoleMixedTypes(t *testing.T) {
	config := logical.TestBackendConfig()
	storage := &logical.InmemStorage{}
	config.StorageView = storage

	b, err := Backend(config)
	if err != nil {
		t.Fatal(err)
	}
	err = b.Setup(context.Background(), config)
	if err != nil {
		t.Fatal(err)
	}

	data := map[string]interface{}{
		"policies":     "p,q,r,s",
		"bound_ami_id": "ami-abc1234",
		"auth_type":    "ec2,invalid",
	}

	submitRequest := func(roleName string, op logical.Operation) (*logical.Response, error) {
		return b.HandleRequest(context.Background(), &logical.Request{
			Operation: op,
			Path:      "role/" + roleName,
			Data:      data,
			Storage:   storage,
		})
	}

	resp, err := submitRequest("shouldNeverExist", logical.CreateOperation)
	if resp == nil || !resp.IsError() {
		t.Fatalf("created role with invalid auth_type; resp: %#v", resp)
	}
	if err != nil {
		t.Fatal(err)
	}

	data["auth_type"] = "ec2,,iam"
	resp, err = submitRequest("shouldNeverExist", logical.CreateOperation)
	if resp == nil || !resp.IsError() {
		t.Fatalf("created role mixed auth types")
	}
	if err != nil {
		t.Fatal(err)
	}

	data["auth_type"] = ec2AuthType
	resp, err = submitRequest("ec2_to_iam", logical.CreateOperation)
	if resp != nil && resp.IsError() {
		t.Fatalf("failed to create valid role; resp: %#v", resp)
	}
	if err != nil {
		t.Fatal(err)
	}

	data["auth_type"] = iamAuthType
	delete(data, "bound_ami_id")
	boundIamPrincipalARNs := []string{"arn:aws:iam::123456789012:role/MyRole", "arn:aws:iam::123456789012:role/path/*"}
	data["bound_iam_principal_arn"] = boundIamPrincipalARNs
	resp, err = submitRequest("ec2_to_iam", logical.UpdateOperation)
	if resp == nil || !resp.IsError() {
		t.Fatalf("changed auth type on the role")
	}
	if err != nil {
		t.Fatal(err)
	}

	data["inferred_entity_type"] = ec2EntityType
	data["inferred_aws_region"] = "us-east-1"
	data["resolve_aws_unique_ids"] = false
	resp, err = submitRequest("multipleTypesInferred", logical.CreateOperation)
	if err != nil {
		t.Fatal(err)
	}
	if resp.IsError() {
		t.Fatalf("didn't allow creation of roles with only inferred bindings")
	}

	b.resolveArnToUniqueIDFunc = resolveArnToFakeUniqueId
	data["resolve_aws_unique_ids"] = true
	resp, err = submitRequest("withInternalIdResolution", logical.CreateOperation)
	if err != nil {
		t.Fatal(err)
	}
	if resp.IsError() {
		t.Fatalf("didn't allow creation of role resolving unique IDs")
	}
	resp, err = submitRequest("withInternalIdResolution", logical.ReadOperation)
	if err != nil {
		t.Fatal(err)
	}
	principalIDs := resp.Data["bound_iam_principal_id"].([]string)
	if len(principalIDs) != 1 || principalIDs[0] != "FakeUniqueId1" {
		t.Fatalf("expected fake unique ID of FakeUniqueId1, got %q", resp.Data["bound_iam_principal_id"])
	}
	returnedARNs := resp.Data["bound_iam_principal_arn"].([]string)
	if !strutil.EquivalentSlices(returnedARNs, boundIamPrincipalARNs) {
		t.Fatalf("bad: expected to return bound_iam_principal_arn of %q, but got %q instead", boundIamPrincipalARNs, returnedARNs)
	}
	data["resolve_aws_unique_ids"] = false
	resp, err = submitRequest("withInternalIdResolution", logical.UpdateOperation)
	if err != nil {
		t.Fatal(err)
	}
	if !resp.IsError() {
		t.Fatalf("allowed changing resolve_aws_unique_ids from true to false")
	}
}

func TestAwsEc2_RoleCrud(t *testing.T) {
	var err error
	var resp *logical.Response
	config := logical.TestBackendConfig()
	storage := &logical.InmemStorage{}
	config.StorageView = storage

	b, err := Backend(config)
	if err != nil {
		t.Fatal(err)
	}

	err = b.Setup(context.Background(), config)
	if err != nil {
		t.Fatal(err)
	}

	role1Data := map[string]interface{}{
		"auth_type":                "ec2",
		"bound_vpc_id":             "testvpcid",
		"allow_instance_migration": true,
		"policies":                 "testpolicy1,testpolicy2",
	}
	roleReq := &logical.Request{
		Operation: logical.UpdateOperation,
		Storage:   storage,
		Path:      "role/role1",
		Data:      role1Data,
	}

	resp, err = b.HandleRequest(context.Background(), roleReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("resp: %#v, err: %v", resp, err)
	}

	roleData := map[string]interface{}{
		"auth_type":                      "ec2",
		"bound_ami_id":                   "testamiid",
		"bound_account_id":               "testaccountid",
		"bound_region":                   "testregion",
		"bound_iam_role_arn":             "arn:aws:iam::123456789012:role/MyRole",
		"bound_iam_instance_profile_arn": "arn:aws:iam::123456789012:instance-profile/MyInstancePro*",
		"bound_subnet_id":                "testsubnetid",
		"bound_vpc_id":                   "testvpcid",
		"bound_ec2_instance_id":          "i-12345678901234567,i-76543210987654321",
		"role_tag":                       "testtag",
		"resolve_aws_unique_ids":         false,
		"allow_instance_migration":       true,
		"ttl":                            "10m",
		"max_ttl":                        "20m",
		"policies":                       "testpolicy1,testpolicy2",
		"disallow_reauthentication":      false,
		"hmac_key":                       "testhmackey",
		"period":                         "1m",
	}

	roleReq.Path = "role/testrole"
	roleReq.Data = roleData
	resp, err = b.HandleRequest(context.Background(), roleReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("resp: %#v, err: %v", resp, err)
	}

	roleReq.Operation = logical.ReadOperation
	resp, err = b.HandleRequest(context.Background(), roleReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("resp: %#v, err: %v", resp, err)
	}

	expected := map[string]interface{}{
		"auth_type":                      ec2AuthType,
		"bound_ami_id":                   []string{"testamiid"},
		"bound_account_id":               []string{"testaccountid"},
		"bound_region":                   []string{"testregion"},
		"bound_ec2_instance_id":          []string{"i-12345678901234567", "i-76543210987654321"},
		"bound_iam_principal_arn":        []string{},
		"bound_iam_principal_id":         []string{},
		"bound_iam_role_arn":             []string{"arn:aws:iam::123456789012:role/MyRole"},
		"bound_iam_instance_profile_arn": []string{"arn:aws:iam::123456789012:instance-profile/MyInstancePro*"},
		"bound_subnet_id":                []string{"testsubnetid"},
		"bound_vpc_id":                   []string{"testvpcid"},
		"inferred_entity_type":           "",
		"inferred_aws_region":            "",
		"resolve_aws_unique_ids":         false,
		"role_tag":                       "testtag",
		"allow_instance_migration":       true,
		"ttl":                            int64(600),
		"token_ttl":                      int64(600),
		"max_ttl":                        int64(1200),
		"token_max_ttl":                  int64(1200),
		"token_explicit_max_ttl":         int64(0),
		"policies":                       []string{"testpolicy1", "testpolicy2"},
		"token_policies":                 []string{"testpolicy1", "testpolicy2"},
		"disallow_reauthentication":      false,
		"period":                         int64(60),
		"token_period":                   int64(60),
		"token_bound_cidrs":              []string{},
		"token_no_default_policy":        false,
		"token_num_uses":                 0,
		"token_type":                     "default",
	}

	if resp.Data["role_id"] == nil {
		t.Fatal("role_id not found in repsonse")
	}
	expected["role_id"] = resp.Data["role_id"]
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}

	roleData["bound_vpc_id"] = "newvpcid"
	roleReq.Operation = logical.UpdateOperation
	resp, err = b.HandleRequest(context.Background(), roleReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("resp: %#v, err: %v", resp, err)
	}

	roleReq.Operation = logical.ReadOperation
	resp, err = b.HandleRequest(context.Background(), roleReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("resp: %#v, err: %v", resp, err)
	}
	expected["bound_vpc_id"] = []string{"newvpcid"}
	if !reflect.DeepEqual(expected, resp.Data) {
		t.Fatalf("bad: role data: expected: %#v\n actual: %#v", expected, resp.Data)
	}

	// Create a new backend so we have a new cache (thus populating from disk).
	// Then test reading (reading from disk + lock), writing, reading,
	// deleting, reading.
	b, err = Backend(config)
	if err != nil {
		t.Fatal(err)
	}

	err = b.Setup(context.Background(), config)
	if err != nil {
		t.Fatal(err)
	}

	// Read again, make sure things are what we expect
	resp, err = b.HandleRequest(context.Background(), roleReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("resp: %#v, err: %v", resp, err)
	}
	if !reflect.DeepEqual(expected, resp.Data) {
		t.Fatalf("bad: role data: expected: %#v\n actual: %#v", expected, resp.Data)
	}

	roleReq.Operation = logical.UpdateOperation
	roleData["bound_ami_id"] = "testamiid2"
	resp, err = b.HandleRequest(context.Background(), roleReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("resp: %#v, err: %v", resp, err)
	}

	roleReq.Operation = logical.ReadOperation
	resp, err = b.HandleRequest(context.Background(), roleReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("resp: %#v, err: %v", resp, err)
	}

	expected["bound_ami_id"] = []string{"testamiid2"}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}

	// Delete which should remove from disk and also cache
	roleReq.Operation = logical.DeleteOperation
	resp, err = b.HandleRequest(context.Background(), roleReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("resp: %#v, err: %v", resp, err)
	}
	if resp != nil {
		t.Fatalf("failed to delete role entry")
	}

	// Verify it was deleted, e.g. it isn't found in the role cache
	roleReq.Operation = logical.ReadOperation
	resp, err = b.HandleRequest(context.Background(), roleReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("resp: %#v, err: %v", resp, err)
	}
	if resp != nil {
		t.Fatal("expected nil")
	}
}

func TestAwsEc2_RoleDurationSeconds(t *testing.T) {
	config := logical.TestBackendConfig()
	storage := &logical.InmemStorage{}
	config.StorageView = storage

	b, err := Backend(config)
	if err != nil {
		t.Fatal(err)
	}

	err = b.Setup(context.Background(), config)
	if err != nil {
		t.Fatal(err)
	}

	roleData := map[string]interface{}{
		"auth_type":                      "ec2",
		"bound_iam_instance_profile_arn": "arn:aws:iam::123456789012:instance-profile/test-profile-name",
		"resolve_aws_unique_ids":         false,
		"ttl":                            "10s",
		"max_ttl":                        "20s",
		"period":                         "30s",
	}

	roleReq := &logical.Request{
		Operation: logical.CreateOperation,
		Storage:   storage,
		Path:      "role/testrole",
		Data:      roleData,
	}

	resp, err := b.HandleRequest(context.Background(), roleReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("resp: %#v, err: %v", resp, err)
	}

	roleReq.Operation = logical.ReadOperation

	resp, err = b.HandleRequest(context.Background(), roleReq)
	if err != nil || (resp != nil && resp.IsError()) {
		t.Fatalf("resp: %#v, err: %v", resp, err)
	}

	if resp.Data["ttl"].(int64) != 10 {
		t.Fatalf("bad: ttl; expected: 10, actual: %d", resp.Data["ttl"])
	}
	if resp.Data["max_ttl"].(int64) != 20 {
		t.Fatalf("bad: max_ttl; expected: 20, actual: %d", resp.Data["max_ttl"])
	}
	if resp.Data["period"].(int64) != 30 {
		t.Fatalf("bad: period; expected: 30, actual: %d", resp.Data["period"])
	}
}

func TestRoleEntryUpgradeV(t *testing.T) {
	config := logical.TestBackendConfig()
	storage := &logical.InmemStorage{}
	config.StorageView = storage
	b, err := Backend(config)
	if err != nil {
		t.Fatal(err)
	}

	err = b.Setup(context.Background(), config)
	if err != nil {
		t.Fatal(err)
	}

	roleEntryToUpgrade := &awsRoleEntry{
		BoundIamRoleARNs:            []string{"arn:aws:iam::123456789012:role/my_role_prefix"},
		BoundIamInstanceProfileARNs: []string{"arn:aws:iam::123456789012:instance-profile/my_profile-prefix"},
		Version:                     1,
	}
	expected := &awsRoleEntry{
		BoundIamRoleARNs:            []string{"arn:aws:iam::123456789012:role/my_role_prefix*"},
		BoundIamInstanceProfileARNs: []string{"arn:aws:iam::123456789012:instance-profile/my_profile-prefix*"},
		Version:                     currentRoleStorageVersion,
	}

	upgraded, err := b.upgradeRole(context.Background(), storage, roleEntryToUpgrade)
	if err != nil {
		t.Fatalf("error upgrading role entry: %#v", err)
	}
	if !upgraded {
		t.Fatalf("expected to upgrade role entry %#v but got no upgrade", roleEntryToUpgrade)
	}
	if roleEntryToUpgrade.RoleID == "" {
		t.Fatal("expected role ID to be populated")
	}
	expected.RoleID = roleEntryToUpgrade.RoleID
	if diff := deep.Equal(*roleEntryToUpgrade, *expected); diff != nil {
		t.Fatal(diff)
	}
}

func TestRoleInitialize(t *testing.T) {
	config := logical.TestBackendConfig()
	storage := &logical.InmemStorage{}
	config.StorageView = storage
	b, err := Backend(config)
	if err != nil {
		t.Fatal(err)
	}

	ctx := context.Background()
	err = b.Setup(ctx, config)
	if err != nil {
		t.Fatal(err)
	}

	// create some role entries, some of which will need to be upgraded
	type testData struct {
		name  string
		entry *awsRoleEntry
	}

	before := []testData{
		{
			name: "role1",
			entry: &awsRoleEntry{
				BoundIamRoleARNs:            []string{"arn:aws:iam::000000000001:role/my_role_prefix"},
				BoundIamInstanceProfileARNs: []string{"arn:aws:iam::000000000001:instance-profile/my_profile-prefix"},
				Version:                     1,
			},
		},
		{
			name: "role2",
			entry: &awsRoleEntry{
				BoundIamRoleARNs:            []string{"arn:aws:iam::000000000002:role/my_role_prefix"},
				BoundIamInstanceProfileARNs: []string{"arn:aws:iam::000000000002:instance-profile/my_profile-prefix"},
				Version:                     2,
			},
		},
		{
			name: "role3",
			entry: &awsRoleEntry{
				BoundIamRoleARNs:            []string{"arn:aws:iam::000000000003:role/my_role_prefix"},
				BoundIamInstanceProfileARNs: []string{"arn:aws:iam::000000000003:instance-profile/my_profile-prefix"},
				Version:                     currentRoleStorageVersion,
			},
		},
	}

	// put the entries in storage
	for _, role := range before {
		err = b.setRole(ctx, storage, role.name, role.entry)
		if err != nil {
			t.Fatal(err)
		}
	}

	// upgrade all the entries
	upgraded, err := b.upgrade(ctx, storage)
	if err != nil {
		t.Fatal(err)
	}
	if !upgraded {
		t.Fatalf("expected upgrade")
	}

	// read the entries from storage
	after := make([]testData, 0)
	names, err := storage.List(ctx, "role/")
	if err != nil {
		t.Fatal(err)
	}
	for _, name := range names {
		entry, err := b.role(ctx, storage, name)
		if err != nil {
			t.Fatal(err)
		}
		after = append(after, testData{name: name, entry: entry})
	}

	// make sure each entry is at the current version
	expected := []testData{
		{
			name: "role1",
			entry: &awsRoleEntry{
				BoundIamRoleARNs:            []string{"arn:aws:iam::000000000001:role/my_role_prefix"},
				BoundIamInstanceProfileARNs: []string{"arn:aws:iam::000000000001:instance-profile/my_profile-prefix"},
				Version:                     currentRoleStorageVersion,
			},
		},
		{
			name: "role2",
			entry: &awsRoleEntry{
				BoundIamRoleARNs:            []string{"arn:aws:iam::000000000002:role/my_role_prefix"},
				BoundIamInstanceProfileARNs: []string{"arn:aws:iam::000000000002:instance-profile/my_profile-prefix"},
				Version:                     currentRoleStorageVersion,
			},
		},
		{
			name: "role3",
			entry: &awsRoleEntry{
				BoundIamRoleARNs:            []string{"arn:aws:iam::000000000003:role/my_role_prefix"},
				BoundIamInstanceProfileARNs: []string{"arn:aws:iam::000000000003:instance-profile/my_profile-prefix"},
				Version:                     currentRoleStorageVersion,
			},
		},
	}
	if diff := deep.Equal(expected, after); diff != nil {
		t.Fatal(diff)
	}

	// run it again -- nothing will happen
	upgraded, err = b.upgrade(ctx, storage)
	if err != nil {
		t.Fatal(err)
	}
	if upgraded {
		t.Fatalf("expected no upgrade")
	}

	// make sure saved role version is correct
	entry, err := storage.Get(ctx, "config/version")
	if err != nil {
		t.Fatal(err)
	}
	var version awsVersion
	err = entry.DecodeJSON(&version)
	if err != nil {
		t.Fatal(err)
	}
	if version.Version != currentAwsVersion {
		t.Fatalf("expected version %d, got  %d", currentAwsVersion, version.Version)
	}

	// stomp on the saved version
	version.Version = 0
	e2, err := logical.StorageEntryJSON("config/version", version)
	if err != nil {
		t.Fatal(err)
	}
	err = storage.Put(ctx, e2)
	if err != nil {
		t.Fatal(err)
	}

	// run it again -- now an upgrade will happen
	upgraded, err = b.upgrade(ctx, storage)
	if err != nil {
		t.Fatal(err)
	}
	if !upgraded {
		t.Fatalf("expected upgrade")
	}
}

func TestAwsVersion(t *testing.T) {
	before := awsVersion{
		Version: 42,
	}

	entry, err := logical.StorageEntryJSON("config/version", &before)
	if err != nil {
		t.Fatal(err)
	}

	var after awsVersion
	err = entry.DecodeJSON(&after)
	if err != nil {
		t.Fatal(err)
	}

	if diff := deep.Equal(before, after); diff != nil {
		t.Fatal(diff)
	}
}

// This test was used to reproduce https://github.com/hashicorp/vault/issues/7418
// and verify its fix.
// Please run it at least 3 times to ensure that passing tests are due to actually
// passing, rather than the region being randomly chosen tying to the one in the
// test through luck.
func TestRoleResolutionWithSTSEndpointConfigured(t *testing.T) {
	if enabled := os.Getenv(vlttesting.TestEnvVar); enabled == "" {
		t.Skip()
	}

	/* ARN of an AWS role that Vault can query during testing.
	   This role should exist in your current AWS account and your credentials
	   should have iam:GetRole permissions to query it.
	*/
	assumableRoleArn := os.Getenv("AWS_ASSUMABLE_ROLE_ARN")
	if assumableRoleArn == "" {
		t.Skip("skipping because AWS_ASSUMABLE_ROLE_ARN is unset")
	}

	// Ensure aws credentials are available locally for testing.
	logger := logging.NewVaultLogger(hclog.Debug)
	credsConfig := &awsutil.CredentialsConfig{Logger: logger}
	credsChain, err := credsConfig.GenerateCredentialChain()
	if err != nil {
		t.Fatal(err)
	}
	_, err = credsChain.Get()
	if err != nil {
		t.SkipNow()
	}

	config := logical.TestBackendConfig()
	storage := &logical.InmemStorage{}
	config.StorageView = storage

	b, err := Backend(config)
	if err != nil {
		t.Fatal(err)
	}

	err = b.Setup(context.Background(), config)
	if err != nil {
		t.Fatal(err)
	}

	// configure the client with an sts endpoint that should be used in creating the role
	data := map[string]interface{}{
		"sts_endpoint": "https://sts.eu-west-1.amazonaws.com",
		// Note - if you comment this out, you can reproduce the error shown
		// in the linked GH issue above. This essentially reproduces the problem
		// we had when we didn't have an sts_region field.
		"sts_region": "eu-west-1",
	}
	resp, err := b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.CreateOperation,
		Path:      "config/client",
		Data:      data,
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}
	if resp != nil && resp.IsError() {
		t.Fatalf("failed to create the role entry; resp: %#v", resp)
	}

	data = map[string]interface{}{
		"auth_type":               iamAuthType,
		"bound_iam_principal_arn": assumableRoleArn,
		"resolve_aws_unique_ids":  true,
	}
	resp, err = b.HandleRequest(context.Background(), &logical.Request{
		Operation: logical.CreateOperation,
		Path:      "role/MyRoleName",
		Data:      data,
		Storage:   storage,
	})
	if err != nil {
		t.Fatal(err)
	}
	if resp != nil && resp.IsError() {
		t.Fatalf("failed to create the role entry; resp: %#v", resp)
	}
}

func resolveArnToFakeUniqueId(_ context.Context, _ logical.Storage, _ string) (string, error) {
	return "FakeUniqueId1", nil
}
