// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package token

import (
	"encoding/base64"
	"reflect"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/go-test/deep"
	"github.com/hashicorp/vault/api"
	credLdap "github.com/hashicorp/vault/builtin/credential/ldap"
	credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
	"github.com/hashicorp/vault/helper/testhelpers/ldap"
	vaulthttp "github.com/hashicorp/vault/http"
	"github.com/hashicorp/vault/sdk/helper/jsonutil"
	"github.com/hashicorp/vault/sdk/logical"
	"github.com/hashicorp/vault/vault"
)

func TestTokenStore_CreateOrphanResponse(t *testing.T) {
	cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
		HandlerFunc: vaulthttp.Handler,
	})
	cluster.Start()
	defer cluster.Cleanup()

	core := cluster.Cores[0].Core
	vault.TestWaitActive(t, core)
	client := cluster.Cores[0].Client

	secret, err := client.Auth().Token().CreateOrphan(&api.TokenCreateRequest{
		Policies: []string{"default"},
	})
	if err != nil {
		t.Fatal(err)
	}
	if !secret.Auth.Orphan {
		t.Fatalf("failed to set orphan as true, got: %#v", secret.Auth)
	}
}

func TestTokenStore_TokenInvalidEntityID(t *testing.T) {
	coreConfig := &vault.CoreConfig{
		CredentialBackends: map[string]logical.Factory{
			"userpass": credUserpass.Factory,
		},
	}
	cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
		HandlerFunc: vaulthttp.Handler,
	})
	cluster.Start()
	defer cluster.Cleanup()

	core := cluster.Cores[0].Core
	vault.TestWaitActive(t, core)
	client := cluster.Cores[0].Client

	// Enable userpass auth
	err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
		Type: "userpass",
	})
	if err != nil {
		t.Fatal(err)
	}

	// Add a user to userpass backend
	_, err = client.Logical().Write("auth/userpass/users/testuser", map[string]interface{}{
		"password": "testpassword",
	})
	if err != nil {
		t.Fatal(err)
	}

	secret, err := client.Logical().Write("auth/userpass/login/testuser", map[string]interface{}{
		"password": "testpassword",
	})
	if err != nil {
		t.Fatal(err)
	}
	clientToken := secret.Auth.ClientToken

	secret, err = client.Logical().Write("auth/token/lookup", map[string]interface{}{
		"token": clientToken,
	})
	if err != nil {
		t.Fatal(err)
	}

	entityID := secret.Data["entity_id"].(string)

	_, err = client.Logical().Delete("identity/entity/id/" + entityID)
	if err != nil {
		t.Fatal(err)
	}

	client.SetToken(clientToken)

	secret, err = client.Logical().Write("auth/token/lookup-self", nil)
	if err == nil {
		t.Fatalf("expected error due to token being invalid when its entity is invalid")
	}
}

func TestTokenStore_IdentityPolicies(t *testing.T) {
	coreConfig := &vault.CoreConfig{
		CredentialBackends: map[string]logical.Factory{
			"ldap": credLdap.Factory,
		},
		EnableRaw: true,
	}
	cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
		HandlerFunc: vaulthttp.Handler,
	})
	cluster.Start()
	defer cluster.Cleanup()

	core := cluster.Cores[0].Core
	vault.TestWaitActive(t, core)
	client := cluster.Cores[0].Client

	// Enable LDAP auth
	err := client.Sys().EnableAuthWithOptions("ldap", &api.EnableAuthOptions{
		Type: "ldap",
	})
	if err != nil {
		t.Fatal(err)
	}

	cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
	defer cleanup()

	// Configure LDAP auth
	_, err = client.Logical().Write("auth/ldap/config", map[string]interface{}{
		"url":       cfg.Url,
		"userattr":  cfg.UserAttr,
		"userdn":    cfg.UserDN,
		"groupdn":   cfg.GroupDN,
		"groupattr": cfg.GroupAttr,
		"binddn":    cfg.BindDN,
		"bindpass":  cfg.BindPassword,
	})
	if err != nil {
		t.Fatal(err)
	}

	// Create group in LDAP auth
	_, err = client.Logical().Write("auth/ldap/groups/testgroup1", map[string]interface{}{
		"policies": "testgroup1-policy",
	})
	if err != nil {
		t.Fatal(err)
	}

	// Create user in LDAP auth. We add two groups, but we should filter out
	// the ones that don't match aliases later (we will check for this)
	_, err = client.Logical().Write("auth/ldap/users/hermes conrad", map[string]interface{}{
		"policies": "default",
		"groups":   "testgroup1,testgroup2",
	})
	if err != nil {
		t.Fatal(err)
	}

	// Login using LDAP
	secret, err := client.Logical().Write("auth/ldap/login/hermes conrad", map[string]interface{}{
		"password": "hermes",
	})
	if err != nil {
		t.Fatal(err)
	}
	ldapClientToken := secret.Auth.ClientToken

	expectedPolicies := []string{
		"default",
		"testgroup1-policy",
	}
	if !reflect.DeepEqual(expectedPolicies, secret.Auth.Policies) {
		t.Fatalf("bad: identity policies; expected: %#v\nactual: %#v", expectedPolicies, secret.Auth.Policies)
	}

	// At this point there shouldn't be any identity policy on the token
	secret, err = client.Logical().Write("auth/token/lookup", map[string]interface{}{
		"token": ldapClientToken,
	})
	if err != nil {
		t.Fatal(err)
	}
	_, ok := secret.Data["identity_policies"]
	if ok {
		t.Fatalf("identity_policies should not have been set")
	}

	// Extract the entity ID of the token and set some policies on the entity
	entityID := secret.Data["entity_id"].(string)
	_, err = client.Logical().Write("identity/entity/id/"+entityID, map[string]interface{}{
		"policies": []string{
			"entity_policy_1",
			"entity_policy_2",
		},
	})
	if err != nil {
		t.Fatal(err)
	}

	// Lookup the token and expect entity policies on the token
	secret, err = client.Logical().Write("auth/token/lookup", map[string]interface{}{
		"token": ldapClientToken,
	})
	if err != nil {
		t.Fatal(err)
	}
	identityPolicies := secret.Data["identity_policies"].([]interface{})
	var actualPolicies []string
	for _, item := range identityPolicies {
		actualPolicies = append(actualPolicies, item.(string))
	}
	sort.Strings(actualPolicies)

	expectedPolicies = []string{
		"entity_policy_1",
		"entity_policy_2",
	}
	sort.Strings(expectedPolicies)
	if !reflect.DeepEqual(expectedPolicies, actualPolicies) {
		t.Fatalf("bad: identity policies; expected: %#v\nactual: %#v", expectedPolicies, actualPolicies)
	}

	// Create identity group and add entity as its member
	secret, err = client.Logical().Write("identity/group", map[string]interface{}{
		"policies": []string{
			"group_policy_1",
			"group_policy_2",
		},
		"member_entity_ids": []string{
			entityID,
		},
	})
	if err != nil {
		t.Fatal(err)
	}

	// Lookup token and expect both entity and group policies on the token
	secret, err = client.Logical().Write("auth/token/lookup", map[string]interface{}{
		"token": ldapClientToken,
	})
	if err != nil {
		t.Fatal(err)
	}
	identityPolicies = secret.Data["identity_policies"].([]interface{})
	actualPolicies = nil
	for _, item := range identityPolicies {
		actualPolicies = append(actualPolicies, item.(string))
	}
	sort.Strings(actualPolicies)

	expectedPolicies = []string{
		"entity_policy_1",
		"entity_policy_2",
		"group_policy_1",
		"group_policy_2",
	}
	sort.Strings(expectedPolicies)
	if !reflect.DeepEqual(expectedPolicies, actualPolicies) {
		t.Fatalf("bad: identity policies; expected: %#v\nactual: %#v", expectedPolicies, actualPolicies)
	}

	// Create an external group and renew the token. This should add external
	// group policies to the token.
	auths, err := client.Sys().ListAuth()
	if err != nil {
		t.Fatal(err)
	}
	ldapMountAccessor1 := auths["ldap/"].Accessor

	// Create an external group
	secret, err = client.Logical().Write("identity/group", map[string]interface{}{
		"type": "external",
		"policies": []string{
			"external_group_policy_1",
			"external_group_policy_2",
		},
	})
	if err != nil {
		t.Fatal(err)
	}
	ldapExtGroupID1 := secret.Data["id"].(string)

	// Associate a group from LDAP auth as a group-alias in the external group
	_, err = client.Logical().Write("identity/group-alias", map[string]interface{}{
		"name":           "testgroup1",
		"mount_accessor": ldapMountAccessor1,
		"canonical_id":   ldapExtGroupID1,
	})
	if err != nil {
		t.Fatal(err)
	}

	// Renew token to refresh external group memberships
	secret, err = client.Auth().Token().Renew(ldapClientToken, 10)
	if err != nil {
		t.Fatal(err)
	}

	// Lookup token and expect entity, group and external group policies on the
	// token
	secret, err = client.Logical().Write("auth/token/lookup", map[string]interface{}{
		"token": ldapClientToken,
	})
	if err != nil {
		t.Fatal(err)
	}
	identityPolicies = secret.Data["identity_policies"].([]interface{})
	actualPolicies = nil
	for _, item := range identityPolicies {
		actualPolicies = append(actualPolicies, item.(string))
	}
	sort.Strings(actualPolicies)

	expectedPolicies = []string{
		"entity_policy_1",
		"entity_policy_2",
		"group_policy_1",
		"group_policy_2",
		"external_group_policy_1",
		"external_group_policy_2",
	}
	sort.Strings(expectedPolicies)
	if !reflect.DeepEqual(expectedPolicies, actualPolicies) {
		t.Fatalf("bad: identity policies; expected: %#v\nactual: %#v", expectedPolicies, actualPolicies)
	}

	// Log in and get a new token, then renew it. See issue #4829. The logic is
	// continued after the next block.
	secret, err = client.Logical().Write("auth/ldap/login/hermes conrad", map[string]interface{}{
		"password": "hermes",
	})
	if err != nil {
		t.Fatal(err)
	}
	token4829 := secret.Auth.ClientToken

	// Check that the lease for the token contains only the single group; this
	// should be true for both as one was fresh and the other was a renew
	// (which is why we do the renew check on the 4839 token after this block)
	secret, err = client.Logical().List("sys/raw/sys/expire/id/auth/ldap/login/hermes conrad/")
	if err != nil {
		t.Fatal(err)
	}
	for _, key := range secret.Data["keys"].([]interface{}) {
		secret, err := client.Logical().Read("sys/raw/sys/expire/id/auth/ldap/login/hermes conrad/" + key.(string))
		if err != nil {
			t.Fatal(err)
		}
		// t.Logf("%#v", *secret)
		var resp logical.Response
		if err := jsonutil.DecodeJSON([]byte(secret.Data["value"].(string)), &resp); err != nil {
			t.Fatal(err)
		}
		if len(resp.Auth.GroupAliases) != 1 || resp.Auth.GroupAliases[0].Name != "testgroup1" {
			t.Fatalf("bad: %#v", resp.Auth.GroupAliases)
		}
	}

	secret, err = client.Auth().Token().Renew(token4829, 10)
	if err != nil {
		t.Fatal(err)
	}
}

func TestTokenStore_CIDRBlocks(t *testing.T) {
	testPolicy := `
path "auth/token/create" {
	capabilities = ["update"]
}
`

	cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
		HandlerFunc: vaulthttp.Handler,
	})
	cluster.Start()
	defer cluster.Cleanup()

	core := cluster.Cores[0].Core
	vault.TestWaitActive(t, core)
	client := cluster.Cores[0].Client
	rootToken := client.Token()

	var err error
	var secret *api.Secret

	_, err = client.Logical().Write("sys/policies/acl/test", map[string]interface{}{
		"policy": testPolicy,
	})
	if err != nil {
		t.Fatal(err)
	}

	// Test normally
	_, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{
		"bound_cidrs": []string{},
	})
	if err != nil {
		t.Fatal(err)
	}
	secret, err = client.Auth().Token().CreateWithRole(&api.TokenCreateRequest{
		Policies: []string{"default"},
	}, "testrole")
	if err != nil {
		t.Fatal(err)
	}
	client.SetToken(secret.Auth.ClientToken)
	_, err = client.Auth().Token().LookupSelf()
	if err != nil {
		t.Fatal(err)
	}

	// CIDR blocks, containing localhost
	client.SetToken(rootToken)
	_, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{
		"bound_cidrs":      []string{"127.0.0.1/32", "1.2.3.4/8", "5.6.7.8/24"},
		"allowed_policies": "test",
	})
	if err != nil {
		t.Fatal(err)
	}
	secret, err = client.Auth().Token().CreateWithRole(&api.TokenCreateRequest{
		Policies: []string{"test", "default"},
	}, "testrole")
	if err != nil {
		t.Fatal(err)
	}
	client.SetToken(secret.Auth.ClientToken)
	_, err = client.Auth().Token().LookupSelf()
	if err != nil {
		t.Fatal(err)
	}

	// Before moving on, validate that a child token created from this token
	// inherits the bound cidr blocks
	client.SetToken(secret.Auth.ClientToken)
	childSecret, err := client.Auth().Token().Create(&api.TokenCreateRequest{
		Policies: []string{"default"},
	})
	if err != nil {
		t.Fatal(err)
	}
	if err != nil {
		t.Fatal(err)
	}
	client.SetToken(childSecret.Auth.ClientToken)
	childInfo, err := client.Auth().Token().LookupSelf()
	if err != nil {
		t.Fatal(err)
	}
	if diff := deep.Equal(childInfo.Data["bound_cidrs"], []interface{}{"127.0.0.1", "1.2.3.4/8", "5.6.7.8/24"}); diff != nil {
		t.Fatal(diff)
	}

	// CIDR blocks, not containing localhost (should fail)
	client.SetToken(rootToken)
	_, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{
		"bound_cidrs": []string{"1.2.3.4/8", "5.6.7.8/24"},
	})
	if err != nil {
		t.Fatal(err)
	}
	secret, err = client.Auth().Token().CreateWithRole(&api.TokenCreateRequest{
		Policies: []string{"default"},
	}, "testrole")
	if err != nil {
		t.Fatal(err)
	}
	client.SetToken(secret.Auth.ClientToken)
	_, err = client.Auth().Token().LookupSelf()
	if err == nil {
		t.Fatal("expected error")
	}
	if !strings.Contains(err.Error(), "permission denied") {
		t.Fatalf("unexpected error: %v", err)
	}

	// Root token, no ttl, should work
	client.SetToken(rootToken)
	_, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{
		"bound_cidrs":      []string{"1.2.3.4/8", "5.6.7.8/24"},
		"allowed_policies": "",
	})
	if err != nil {
		t.Fatal(err)
	}
	secret, err = client.Auth().Token().CreateWithRole(&api.TokenCreateRequest{}, "testrole")
	if err != nil {
		t.Fatal(err)
	}
	client.SetToken(secret.Auth.ClientToken)
	_, err = client.Auth().Token().LookupSelf()
	if err != nil {
		t.Fatal(err)
	}

	// Root token, ttl, should not work
	client.SetToken(rootToken)
	_, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{
		"bound_cidrs": []string{"1.2.3.4/8", "5.6.7.8/24"},
		"period":      3600,
	})
	if err != nil {
		t.Fatal(err)
	}
	secret, err = client.Auth().Token().CreateWithRole(&api.TokenCreateRequest{}, "testrole")
	if err != nil {
		t.Fatal(err)
	}
	client.SetToken(secret.Auth.ClientToken)
	_, err = client.Auth().Token().LookupSelf()
	if err == nil {
		t.Fatal("expected error")
	}
	if !strings.Contains(err.Error(), "permission denied") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestTokenStore_RevocationOnStartup(t *testing.T) {
	cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
		HandlerFunc: vaulthttp.Handler,
		NumCores:    1,
	})
	cluster.Start()
	defer cluster.Cleanup()

	core := cluster.Cores[0].Core
	vault.TestWaitActive(t, core)
	client := cluster.Cores[0].Client
	rootToken := client.Token()

	type leaseEntry struct {
		LeaseID         string                 `json:"lease_id"`
		ClientToken     string                 `json:"client_token"`
		Path            string                 `json:"path"`
		Data            map[string]interface{} `json:"data"`
		Secret          *logical.Secret        `json:"secret"`
		Auth            *logical.Auth          `json:"auth"`
		IssueTime       time.Time              `json:"issue_time"`
		ExpireTime      time.Time              `json:"expire_time"`
		LastRenewalTime time.Time              `json:"last_renewal_time"`
	}

	var secret *api.Secret
	var err error
	var tokens []string
	// Create tokens
	for i := 0; i < 500; i++ {
		secret, err = client.Auth().Token().Create(&api.TokenCreateRequest{
			Policies: []string{"default"},
		})
		if err != nil {
			t.Fatal(err)
		}
		tokens = append(tokens, secret.Auth.ClientToken)
	}

	const tokenPath string = "sys/raw/sys/token/id/"
	secret, err = client.Logical().List(tokenPath)
	if err != nil {
		t.Fatal(err)
	}
	totalTokens := len(secret.Data["keys"].([]interface{}))

	// Get the list of leases
	const leasePath string = "sys/raw/sys/expire/id/auth/token/create/"
	secret, err = client.Logical().List(leasePath)
	if err != nil {
		t.Fatal(err)
	}
	leases := secret.Data["keys"].([]interface{})
	if len(leases) != 500 {
		t.Fatalf("unexpected number of leases: %d", len(leases))
	}

	// Holds non-root leases
	var validLeases []string
	// Fake times in the past
	for _, lease := range leases {
		secret, err = client.Logical().Read(leasePath + lease.(string))
		if err != nil {
			t.Fatal(err)
		}
		var entry leaseEntry
		if err := jsonutil.DecodeJSON([]byte(secret.Data["value"].(string)), &entry); err != nil {
			t.Fatal(err)
		}
		if entry.ExpireTime.IsZero() {
			continue
		}
		validLeases = append(validLeases, lease.(string))
		entry.IssueTime = entry.IssueTime.Add(-1 * time.Hour * 24 * 365)
		entry.ExpireTime = entry.ExpireTime.Add(-1 * time.Hour * 24 * 365)
		jsonEntry, err := jsonutil.EncodeJSON(&entry)
		if err != nil {
			t.Fatal(err)
		}
		if _, err := client.Logical().Write(leasePath+lease.(string), map[string]interface{}{
			"value": string(jsonEntry),
		}); err != nil {
			t.Fatal(err)
		}
	}

	if err := client.Sys().Seal(); err != nil {
		t.Fatal(err)
	}

	var status *api.SealStatusResponse
	for i := 0; i < len(cluster.BarrierKeys); i++ {
		status, err = client.Sys().Unseal(string(base64.StdEncoding.EncodeToString(cluster.BarrierKeys[i])))
		if err != nil {
			t.Fatal(err)
		}
		if !status.Sealed {
			break
		}
	}
	if status.Sealed {
		t.Fatal("did not unseal properly")
	}

	// Give lease loading some time to process
	time.Sleep(5 * time.Second)

	for i, token := range tokens {
		client.SetToken(token)
		_, err := client.Logical().Write("cubbyhole/foo", map[string]interface{}{
			"value": "bar",
		})
		if err == nil {
			t.Errorf("expected error but did not get one, token num %d", i)
		}
	}

	expectedLeases := len(leases) - len(validLeases)

	client.SetToken(rootToken)
	secret, err = client.Logical().List(leasePath)
	if err != nil {
		t.Fatal(err)
	}

	switch {
	case secret == nil:
		if expectedLeases != 0 {
			t.Fatalf("nil secret back but expected %d leases", expectedLeases)
		}

	case secret.Data == nil:
		if expectedLeases != 0 {
			t.Fatalf("nil secret data back but expected %d leases, secret is %#v", expectedLeases, *secret)
		}

	default:
		leasesLeft := len(secret.Data["keys"].([]interface{}))
		if leasesLeft != expectedLeases {
			t.Fatalf("found %d leases left, expected %d", leasesLeft, expectedLeases)
		}
	}

	expectedTokens := totalTokens - len(validLeases)
	secret, err = client.Logical().List(tokenPath)
	if err != nil {
		t.Fatal(err)
	}
	tokensLeft := len(secret.Data["keys"].([]interface{}))
	if tokensLeft != expectedTokens {
		t.Fatalf("found %d tokens left, expected %d", tokensLeft, expectedTokens)
	}
}
