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

package token

import (
	"strings"
	"testing"
	"time"

	"github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/helper/testhelpers/minimal"
	"github.com/hashicorp/vault/sdk/helper/consts"
	"github.com/hashicorp/vault/sdk/logical"
	"github.com/hashicorp/vault/vault"
)

func TestBatchTokens(t *testing.T) {
	t.Parallel()
	cluster := minimal.NewTestSoloCluster(t, &vault.CoreConfig{
		LogicalBackends: map[string]logical.Factory{
			"kv": vault.LeasedPassthroughBackendFactory,
		},
	})
	client := cluster.Cores[0].Client
	rootToken := client.Token()
	var err error

	// Set up a KV path
	err = client.Sys().Mount("kv", &api.MountInput{
		Type: "kv",
	})
	if err != nil {
		t.Fatal(err)
	}

	_, err = client.Logical().Write("kv/foo", map[string]interface{}{
		"foo": "bar",
		"ttl": "5m",
	})
	if err != nil {
		t.Fatal(err)
	}

	// Write the test policy
	err = client.Sys().PutPolicy("test", `
path "kv/*" {
	capabilities = ["read"]
}`)
	if err != nil {
		t.Fatal(err)
	}

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

	// Tune the mount
	if err = client.Sys().TuneMount("auth/approle", api.MountConfigInput{
		DefaultLeaseTTL: "5s",
		MaxLeaseTTL:     "5s",
	}); err != nil {
		t.Fatal(err)
	}

	// Create role
	resp, err := client.Logical().Write("auth/approle/role/test", map[string]interface{}{
		"policies": "test",
	})
	if err != nil {
		t.Fatal(err)
	}

	// Get role_id
	resp, err = client.Logical().Read("auth/approle/role/test/role-id")
	if err != nil {
		t.Fatal(err)
	}
	if resp == nil {
		t.Fatal("expected a response for fetching the role-id")
	}
	roleID := resp.Data["role_id"]

	// Get secret_id
	resp, err = client.Logical().Write("auth/approle/role/test/secret-id", map[string]interface{}{})
	if err != nil {
		t.Fatal(err)
	}
	if resp == nil {
		t.Fatal("expected a response for fetching the secret-id")
	}
	secretID := resp.Data["secret_id"]

	// Login
	testLogin := func(mountTuneType, roleType string, batch bool) string {
		t.Helper()
		if err = client.Sys().TuneMount("auth/approle", api.MountConfigInput{
			TokenType: mountTuneType,
		}); err != nil {
			t.Fatal(err)
		}
		_, err = client.Logical().Write("auth/approle/role/test", map[string]interface{}{
			"token_type": roleType,
		})
		if err != nil {
			t.Fatal(err)
		}

		resp, err = client.Logical().Write("auth/approle/login", map[string]interface{}{
			"role_id":   roleID,
			"secret_id": secretID,
		})
		if err != nil {
			t.Fatal(err)
		}
		if resp == nil {
			t.Fatal("expected a response for login")
		}
		if resp.Auth == nil {
			t.Fatal("expected auth object from response")
		}
		if resp.Auth.ClientToken == "" {
			t.Fatal("expected a client token")
		}
		if batch && !strings.HasPrefix(resp.Auth.ClientToken, consts.BatchTokenPrefix) {
			t.Fatal("expected a batch token")
		}
		if !batch && strings.HasPrefix(resp.Auth.ClientToken, consts.BatchTokenPrefix) {
			t.Fatal("expected a non-batch token")
		}
		return resp.Auth.ClientToken
	}
	testLogin("service", "default", false)
	testLogin("service", "batch", false)
	testLogin("service", "service", false)
	testLogin("batch", "default", true)
	testLogin("batch", "batch", true)
	testLogin("batch", "service", true)
	testLogin("default-service", "default", false)
	testLogin("default-service", "batch", true)
	testLogin("default-service", "service", false)
	testLogin("default-batch", "default", true)
	testLogin("default-batch", "batch", true)
	testLogin("default-batch", "service", false)

	finalToken := testLogin("batch", "batch", true)

	client.SetToken(finalToken)
	resp, err = client.Logical().Read("kv/foo")
	if err != nil {
		t.Fatal(err)
	}
	if resp.Data["foo"].(string) != "bar" {
		t.Fatal("bad")
	}
	if resp.LeaseID == "" {
		t.Fatal("expected lease")
	}
	if !resp.Renewable {
		t.Fatal("expected renewable")
	}
	if resp.LeaseDuration > 5 {
		t.Fatalf("lease duration too big: %d", resp.LeaseDuration)
	}
	leaseID := resp.LeaseID

	lastDuration := resp.LeaseDuration
	for i := 0; i < 3; i++ {
		time.Sleep(time.Second)
		resp, err = client.Sys().Renew(leaseID, 0)
		if err != nil {
			t.Fatal(err)
		}
		if resp.LeaseDuration >= lastDuration {
			t.Fatal("expected duration to go down")
		}
		lastDuration = resp.LeaseDuration
	}

	client.SetToken(rootToken)
	time.Sleep(2 * time.Second)
	resp, err = client.Logical().Write("sys/leases/lookup", map[string]interface{}{
		"lease_id": leaseID,
	})
	if err == nil {
		t.Fatal("expected error")
	}
}

func TestBatchToken_ParentLeaseRevoke(t *testing.T) {
	t.Parallel()
	cluster := minimal.NewTestSoloCluster(t, &vault.CoreConfig{
		LogicalBackends: map[string]logical.Factory{
			"kv": vault.LeasedPassthroughBackendFactory,
		},
	})
	client := cluster.Cores[0].Client
	rootToken := client.Token()
	var err error

	// Set up a KV path
	err = client.Sys().Mount("kv", &api.MountInput{
		Type: "kv",
	})
	if err != nil {
		t.Fatal(err)
	}

	_, err = client.Logical().Write("kv/foo", map[string]interface{}{
		"foo": "bar",
		"ttl": "5m",
	})
	if err != nil {
		t.Fatal(err)
	}

	// Write the test policy
	err = client.Sys().PutPolicy("test", `
path "kv/*" {
	capabilities = ["read"]
}`)
	if err != nil {
		t.Fatal(err)
	}

	// Create a second root token
	secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{
		Policies: []string{"root"},
	})
	if err != nil {
		t.Fatal(err)
	}
	rootToken2 := secret.Auth.ClientToken

	// Use this new token to create a batch token
	client.SetToken(rootToken2)
	secret, err = client.Auth().Token().Create(&api.TokenCreateRequest{
		Policies: []string{"test"},
		Type:     "batch",
	})
	if err != nil {
		t.Fatal(err)
	}
	batchToken := secret.Auth.ClientToken
	client.SetToken(batchToken)
	_, err = client.Auth().Token().LookupSelf()
	if err != nil {
		t.Fatal(err)
	}
	if secret.Auth.ClientToken[0:vault.TokenPrefixLength] != consts.BatchTokenPrefix {
		t.Fatal(secret.Auth.ClientToken)
	}

	// Get a lease with the batch token
	resp, err := client.Logical().Read("kv/foo")
	if err != nil {
		t.Fatal(err)
	}
	if resp.Data["foo"].(string) != "bar" {
		t.Fatal("bad")
	}
	if resp.LeaseID == "" {
		t.Fatal("expected lease")
	}
	leaseID := resp.LeaseID

	// Check the lease
	resp, err = client.Logical().Write("sys/leases/lookup", map[string]interface{}{
		"lease_id": leaseID,
	})
	if err != nil {
		t.Fatal(err)
	}

	// Revoke the parent
	client.SetToken(rootToken2)
	err = client.Auth().Token().RevokeSelf("")
	if err != nil {
		t.Fatal(err)
	}

	time.Sleep(1 * time.Second)

	// Verify the batch token is not usable anymore
	client.SetToken(rootToken)
	_, err = client.Auth().Token().Lookup(batchToken)
	if err == nil {
		t.Fatal("expected error")
	}

	// Verify the lease has been revoked
	resp, err = client.Logical().Write("sys/leases/lookup", map[string]interface{}{
		"lease_id": leaseID,
	})
	if err == nil {
		t.Fatal("expected error")
	}
}

func TestTokenStore_Roles_Batch(t *testing.T) {
	t.Parallel()
	cluster := minimal.NewTestSoloCluster(t, nil)
	client := cluster.Cores[0].Client
	rootToken := client.Token()

	var err error
	var secret *api.Secret

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

	// Test batch
	{
		client.SetToken(rootToken)
		_, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{
			"token_type": "batch",
		})
		// Orphan not set so we should error
		if err == nil {
			t.Fatal("expected error")
		}
		_, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{
			"token_type": "batch",
			"orphan":     true,
		})
		// Renewable set so we should error
		if err == nil {
			t.Fatal("expected error")
		}
		_, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{
			"token_type": "batch",
			"orphan":     true,
			"renewable":  false,
		})
		if err != nil {
			t.Fatal(err)
		}
		secret, err = client.Auth().Token().CreateWithRole(&api.TokenCreateRequest{
			Policies: []string{"default"},
			Type:     "service",
		}, "testrole")
		if err != nil {
			t.Fatal(err)
		}
		client.SetToken(secret.Auth.ClientToken)
		_, err = client.Auth().Token().LookupSelf()
		if err != nil {
			t.Fatal(err)
		}
		if secret.Auth.ClientToken[0:vault.TokenPrefixLength] != consts.BatchTokenPrefix {
			t.Fatal(secret.Auth.ClientToken)
		}
	}

	// Test default-service
	{
		client.SetToken(rootToken)
		_, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{
			"token_type": "default-service",
		})
		if err != nil {
			t.Fatal(err)
		}
		// Client specifies batch
		secret, err = client.Auth().Token().CreateWithRole(&api.TokenCreateRequest{
			Policies: []string{"default"},
			Type:     "batch",
		}, "testrole")
		if err != nil {
			t.Fatal(err)
		}
		client.SetToken(secret.Auth.ClientToken)
		_, err = client.Auth().Token().LookupSelf()
		if err != nil {
			t.Fatal(err)
		}
		if secret.Auth.ClientToken[0:vault.TokenPrefixLength] != consts.BatchTokenPrefix {
			t.Fatal(secret.Auth.ClientToken)
		}
		// Client specifies service
		client.SetToken(rootToken)
		secret, err = client.Auth().Token().CreateWithRole(&api.TokenCreateRequest{
			Policies: []string{"default"},
			Type:     "service",
		}, "testrole")
		if err != nil {
			t.Fatal(err)
		}
		client.SetToken(secret.Auth.ClientToken)
		_, err = client.Auth().Token().LookupSelf()
		if err != nil {
			t.Fatal(err)
		}
		if secret.Auth.ClientToken[0:vault.TokenPrefixLength] != consts.ServiceTokenPrefix {
			t.Fatal(secret.Auth.ClientToken)
		}
		// Client doesn't specify
		client.SetToken(rootToken)
		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)
		}
		if secret.Auth.ClientToken[0:vault.TokenPrefixLength] != consts.ServiceTokenPrefix {
			t.Fatal(secret.Auth.ClientToken)
		}
	}

	// Test default-batch
	{
		client.SetToken(rootToken)
		_, err = client.Logical().Write("auth/token/roles/testrole", map[string]interface{}{
			"token_type": "default-batch",
		})
		if err != nil {
			t.Fatal(err)
		}
		// Client specifies batch
		secret, err = client.Auth().Token().CreateWithRole(&api.TokenCreateRequest{
			Policies: []string{"default"},
			Type:     "batch",
		}, "testrole")
		if err != nil {
			t.Fatal(err)
		}
		client.SetToken(secret.Auth.ClientToken)
		_, err = client.Auth().Token().LookupSelf()
		if err != nil {
			t.Fatal(err)
		}
		if secret.Auth.ClientToken[0:vault.TokenPrefixLength] != consts.BatchTokenPrefix {
			t.Fatal(secret.Auth.ClientToken)
		}
		// Client specifies service
		client.SetToken(rootToken)
		secret, err = client.Auth().Token().CreateWithRole(&api.TokenCreateRequest{
			Policies: []string{"default"},
			Type:     "service",
		}, "testrole")
		if err != nil {
			t.Fatal(err)
		}
		client.SetToken(secret.Auth.ClientToken)
		_, err = client.Auth().Token().LookupSelf()
		if err != nil {
			t.Fatal(err)
		}
		if secret.Auth.ClientToken[0:vault.TokenPrefixLength] != consts.ServiceTokenPrefix {
			t.Fatal(secret.Auth.ClientToken)
		}
		// Client doesn't specify
		client.SetToken(rootToken)
		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)
		}
		if secret.Auth.ClientToken[0:vault.TokenPrefixLength] != consts.BatchTokenPrefix {
			t.Fatal(secret.Auth.ClientToken)
		}
	}
}
