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

package ca

import (
	"bytes"
	"context"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/go-viper/mapstructure/v2"
	"github.com/hashicorp/go-hclog"
	vaultapi "github.com/hashicorp/vault/api"

	"github.com/hashicorp/consul/agent/connect"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/lib"
	"github.com/hashicorp/consul/lib/decode"
	"github.com/hashicorp/consul/lib/retry"
)

const (
	VaultCALeafCertRole = "leaf-cert"

	VaultAuthMethodTypeAliCloud     = "alicloud"
	VaultAuthMethodTypeAppRole      = "approle"
	VaultAuthMethodTypeAWS          = "aws"
	VaultAuthMethodTypeAzure        = "azure"
	VaultAuthMethodTypeCloudFoundry = "cf"
	VaultAuthMethodTypeGitHub       = "github"
	VaultAuthMethodTypeGCP          = "gcp"
	VaultAuthMethodTypeJWT          = "jwt"
	VaultAuthMethodTypeKerberos     = "kerberos"
	VaultAuthMethodTypeKubernetes   = "kubernetes"
	VaultAuthMethodTypeLDAP         = "ldap"
	VaultAuthMethodTypeOCI          = "oci"
	VaultAuthMethodTypeOkta         = "okta"
	VaultAuthMethodTypeRadius       = "radius"
	VaultAuthMethodTypeTLS          = "cert"
	VaultAuthMethodTypeToken        = "token"
	VaultAuthMethodTypeUserpass     = "userpass"

	defaultK8SServiceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
)

var (
	ErrBackendNotMounted     = fmt.Errorf("backend not mounted")
	ErrBackendNotInitialized = fmt.Errorf("backend not initialized")
)

type VaultProvider struct {
	config *structs.VaultCAProviderConfig

	client *vaultapi.Client

	baseNamespace string

	stopWatcher func()

	isPrimary bool
	clusterID string
	spiffeID  *connect.SpiffeIDSigning
	logger    hclog.Logger

	// isConsulMountedIntermediate is used to determine if we should tune the
	// mount if the VaultProvider is ever reconfigured. This is at most a
	// "best guess" to determine whether this instance of Consul created the
	// intermediate mount but will not be able to tell if an existing mount
	// was created by Consul (in a previous running instance) or was external.
	isConsulMountedIntermediate bool
}

var _ Provider = (*VaultProvider)(nil)

func NewVaultProvider(logger hclog.Logger) *VaultProvider {
	return &VaultProvider{
		stopWatcher: func() {},
		logger:      logger,
	}
}

func vaultTLSConfig(config *structs.VaultCAProviderConfig) *vaultapi.TLSConfig {
	return &vaultapi.TLSConfig{
		CACert:        config.CAFile,
		CAPath:        config.CAPath,
		ClientCert:    config.CertFile,
		ClientKey:     config.KeyFile,
		Insecure:      config.TLSSkipVerify,
		TLSServerName: config.TLSServerName,
	}
}

// Configure sets up the provider using the given configuration.
// Configure supports being called multiple times to re-configure the provider.
func (v *VaultProvider) Configure(cfg ProviderConfig) error {
	config, err := ParseVaultCAConfig(cfg.RawConfig, v.isPrimary)
	if err != nil {
		return err
	}

	clientConf := &vaultapi.Config{
		Address: config.Address,
	}
	err = clientConf.ConfigureTLS(vaultTLSConfig(config))
	if err != nil {
		return err
	}
	client, err := vaultapi.NewClient(clientConf)
	if err != nil {
		return err
	}

	// We don't want to set the namespace if it's empty to prevent potential
	// unknown behavior (what does Vault do with an empty namespace). The Vault
	// client also makes sure the inputs are not empty strings so let's do the
	// same.
	if config.Namespace != "" {
		client.SetNamespace(config.Namespace)
		v.baseNamespace = config.Namespace
	}

	if config.AuthMethod != nil {
		loginResp, err := vaultLogin(client, config.AuthMethod)
		if err != nil {
			return err
		}
		config.Token = loginResp.Auth.ClientToken
	}
	client.SetToken(config.Token)

	v.config = config
	v.client = client
	v.isPrimary = cfg.IsPrimary
	v.clusterID = cfg.ClusterID
	v.spiffeID = connect.SpiffeIDSigningForCluster(v.clusterID)

	// Look up the token to see if we can auto-renew its lease.
	secret, err := client.Auth().Token().LookupSelf()
	if err != nil {
		return err
	} else if secret == nil {
		return fmt.Errorf("could not look up Vault provider token: not found")
	}
	var token struct {
		Renewable bool
		TTL       int
	}
	if err := mapstructure.Decode(secret.Data, &token); err != nil {
		return err
	}

	// Set up a renewer to renew the token automatically, if supported.
	if token.Renewable || config.AuthMethod != nil {
		lifetimeWatcher, err := client.NewLifetimeWatcher(&vaultapi.LifetimeWatcherInput{
			Secret: &vaultapi.Secret{
				Auth: &vaultapi.SecretAuth{
					ClientToken:   config.Token,
					Renewable:     token.Renewable,
					LeaseDuration: secret.LeaseDuration,
				},
			},
			Increment:     token.TTL,
			RenewBehavior: vaultapi.RenewBehaviorIgnoreErrors,
		})
		if err != nil {
			return fmt.Errorf("error beginning Vault provider token renewal: %v", err)
		}

		ctx, cancel := context.WithCancel(context.Background())
		if v.stopWatcher != nil {
			// stop the running watcher loop if we are re-configuring
			v.stopWatcher()
		}
		v.stopWatcher = cancel
		// NOTE: Any codepaths after v.renewToken(...) which return an error
		// _must_ call v.stopWatcher() to prevent the renewal goroutine from
		// leaking when the CA initialization fails and gets retried later.
		go v.renewToken(ctx, lifetimeWatcher)
	}

	// Update the intermediate (managed) PKI mount and role
	if err := v.setupIntermediatePKIPath(); err != nil {
		if v.stopWatcher != nil {
			v.stopWatcher()
		}
		return err
	}

	return nil
}

func (v *VaultProvider) ValidateConfigUpdate(prevRaw, nextRaw map[string]interface{}) error {
	prev, err := ParseVaultCAConfig(prevRaw, v.isPrimary)
	if err != nil {
		return fmt.Errorf("failed to parse existing CA config: %w", err)
	}
	next, err := ParseVaultCAConfig(nextRaw, v.isPrimary)
	if err != nil {
		return fmt.Errorf("failed to parse new CA config: %w", err)
	}

	if prev.RootPKIPath != next.RootPKIPath {
		return nil
	}

	if prev.PrivateKeyType != "" && prev.PrivateKeyType != connect.DefaultPrivateKeyType {
		if prev.PrivateKeyType != next.PrivateKeyType {
			return fmt.Errorf("cannot update the PrivateKeyType field without changing RootPKIPath")
		}
	}

	if prev.PrivateKeyBits != 0 && prev.PrivateKeyBits != connect.DefaultPrivateKeyBits {
		if prev.PrivateKeyBits != next.PrivateKeyBits {
			return fmt.Errorf("cannot update the PrivateKeyBits field without changing RootPKIPath")
		}
	}
	return nil
}

// renewToken uses a vaultapi.LifetimeWatcher to repeatedly renew our token's lease.
// If the token can no longer be renewed and auth method is set,
// it will re-authenticate to Vault using the auth method and restart the renewer with the new token.
func (v *VaultProvider) renewToken(ctx context.Context, watcher *vaultapi.LifetimeWatcher) {
	go watcher.Start()
	defer watcher.Stop()

	// These values are chosen to start the exponential backoff
	// immediately. Since the Vault client implements its own
	// retries, this retry is mostly to avoid resource contention
	// and log spam.
	retrier := retry.Waiter{
		MinFailures: 1,
		MinWait:     1 * time.Second,
		Jitter:      retry.NewJitter(20),
	}

	for {
		select {
		case <-ctx.Done():
			return

		case err := <-watcher.DoneCh():
			// Watcher has stopped
			if err != nil {
				v.logger.Error("Error renewing token for Vault provider", "error", err, "retries", retrier.Failures())
			}

			// Although the vault watcher has its own retry logic, we have encountered
			// issues when passing an invalid Vault token which would send an error to
			// watcher.DoneCh() immediately, causing us to start the watcher over and
			// over again in a very tight loop.
			if err := retrier.Wait(ctx); err != nil {
				// only possible error is when context is cancelled
				return
			}

			// If the watcher has exited and auth method is enabled,
			// re-authenticate using the auth method and set up a new watcher.
			if v.config.AuthMethod != nil {
				// Login to Vault using the auth method.
				loginResp, err := vaultLogin(v.client, v.config.AuthMethod)
				if err != nil {
					v.logger.Error("Error login in to Vault with %q auth method", v.config.AuthMethod.Type)

					go watcher.Start()
					continue
				}

				// Set the new token for the vault client.
				v.client.SetToken(loginResp.Auth.ClientToken)
				v.logger.Info("Successfully re-authenticated with Vault using auth method")

				// Start the new watcher for the new token.
				watcher, err = v.client.NewLifetimeWatcher(&vaultapi.LifetimeWatcherInput{
					Secret:        loginResp,
					RenewBehavior: vaultapi.RenewBehaviorIgnoreErrors,
				})
				if err != nil {
					v.logger.Error("Error starting token renewal process")
					go watcher.Start()
					continue
				}
			}

			go watcher.Start()

		case <-watcher.RenewCh():
			retrier.Reset()
			v.logger.Info("Successfully renewed token for Vault provider")
		}
	}
}

// State implements Provider. Vault provider needs no state other than the
// user-provided config currently.
func (v *VaultProvider) State() (map[string]string, error) {
	return nil, nil
}

// GenerateCAChain mounts and initializes a new root PKI backend if needed.
func (v *VaultProvider) GenerateCAChain() (string, error) {
	if !v.isPrimary {
		return "", fmt.Errorf("provider is not the root certificate authority")
	}

	// Set up the root PKI backend if necessary.
	rootPEM, err := v.getCA(v.config.RootPKINamespace, v.config.RootPKIPath)
	switch err {
	case ErrBackendNotMounted:

		err := v.mountNamespaced(v.config.RootPKINamespace, v.config.RootPKIPath, &vaultapi.MountInput{
			Type:        "pki",
			Description: "root CA backend for Consul Connect",
			Config: vaultapi.MountConfigInput{
				// the max lease ttl denotes the maximum ttl that secrets are created from the engine
				// the default lease ttl is the kind of ttl that will *reliably* set the ttl to v.config.RootCertTTL
				// https://www.vaultproject.io/docs/secrets/pki#configure-a-ca-certificate
				MaxLeaseTTL:     v.config.RootCertTTL.String(),
				DefaultLeaseTTL: v.config.RootCertTTL.String(),
			},
		})
		if err != nil {
			return "", fmt.Errorf("failed to mount root CA backend: %w", err)
		}

		// We want to initialize afterwards
		fallthrough
	case ErrBackendNotInitialized:
		uid, err := connect.CompactUID()
		if err != nil {
			return "", err
		}
		resp, err := v.writeNamespaced(v.config.RootPKINamespace, v.config.RootPKIPath+"root/generate/internal", map[string]interface{}{
			"common_name": connect.CACN("vault", uid, v.clusterID, v.isPrimary),
			"uri_sans":    v.spiffeID.URI().String(),
			"key_type":    v.config.PrivateKeyType,
			"key_bits":    v.config.PrivateKeyBits,
		})
		if err != nil {
			return "", fmt.Errorf("failed to initialize root CA: %w", err)
		}
		var ok bool
		rootPEM, ok = resp.Data["certificate"].(string)
		if !ok {
			return "", fmt.Errorf("unexpected response from Vault: %v", resp.Data["certificate"])
		}

	default:
		if err != nil {
			return "", fmt.Errorf("unexpected error while setting root PKI backend: %w", err)
		}
	}

	rootChain, err := v.getCAChain(v.config.RootPKINamespace, v.config.RootPKIPath)
	if err != nil {
		return "", err
	}

	// Workaround for a bug in the Vault PKI API.
	// See https://github.com/hashicorp/vault/issues/13489
	if rootChain == "" {
		rootChain = rootPEM
	}

	return rootChain, nil
}

// GenerateIntermediateCSR creates a private key and generates a CSR
// for another datacenter's root to sign, overwriting the intermediate backend
// in the process.
func (v *VaultProvider) GenerateIntermediateCSR() (string, string, error) {
	if v.isPrimary {
		return "", "", fmt.Errorf("provider is the root certificate authority, " +
			"cannot generate an intermediate CSR")
	}

	return v.generateIntermediateCSR()
}

func (v *VaultProvider) setupIntermediatePKIPath() error {
	mountConfig := vaultapi.MountConfigInput{
		MaxLeaseTTL: v.config.IntermediateCertTTL.String(),
	}

	_, err := v.getCA(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath)
	if err != nil {
		switch err {
		case ErrBackendNotMounted:
			err := v.mountNamespaced(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath, &vaultapi.MountInput{
				Type:        "pki",
				Description: "intermediate CA backend for Consul Connect",
				Config:      mountConfig,
			})
			if err != nil {
				return fmt.Errorf("failed to mount intermediate PKI backend: %w", err)
			}
			// Required to determine if we should tune the mount
			// if the VaultProvider is ever reconfigured.
			v.isConsulMountedIntermediate = true

		case ErrBackendNotInitialized:
			// If this is the first time calling setupIntermediatePKIPath, the backend
			// will not have been initialized. Since the mount is ready we can suppress
			// this error.
		default:
			return fmt.Errorf("unexpected error while fetching intermediate CA: %w", err)
		}
	} else {
		v.logger.Info("Found existing Intermediate PKI path mount",
			"namespace", v.config.IntermediatePKINamespace,
			"path", v.config.IntermediatePKIPath,
		)

		// This codepath requires the Vault policy:
		//
		//   path "/sys/mounts/<intermediate_pki_path>/tune" {
		//     capabilities = [ "update" ]
		//   }
		//
		err := v.tuneMountNamespaced(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath, &mountConfig)
		if err != nil {
			if v.isConsulMountedIntermediate {
				v.logger.Warn("Intermediate PKI path was mounted by Consul but could not be tuned",
					"namespace", v.config.IntermediatePKINamespace,
					"path", v.config.IntermediatePKIPath,
					"error", err,
				)
			} else {
				v.logger.Debug("Failed to tune Intermediate PKI mount. 403 Forbidden is expected if Consul does not have tune capabilities for the Intermediate PKI mount (i.e. using Vault-managed policies)",
					"namespace", v.config.IntermediatePKINamespace,
					"path", v.config.IntermediatePKIPath,
					"error", err,
				)
			}
		}
	}

	// Create the role for issuing leaf certs
	rolePath := v.config.IntermediatePKIPath + "roles/" + VaultCALeafCertRole
	_, err = v.writeNamespaced(v.config.IntermediatePKINamespace, rolePath, map[string]interface{}{
		"allow_any_name":   true,
		"allowed_uri_sans": "spiffe://*",
		"key_type":         "any",
		"max_ttl":          v.config.LeafCertTTL.String(),
		"no_store":         true,
		"require_cn":       false,
	})

	// enable auto-tidy with tidy_expired_issuers
	v.autotidyIssuers(v.config.IntermediatePKIPath)

	return err
}

// generateIntermediateCSR returns the CSR and key_id (only present in
// Vault 1.11+) or any errors encountered.
func (v *VaultProvider) generateIntermediateCSR() (string, string, error) {
	// Generate a new intermediate CSR for the root to sign.
	uid, err := connect.CompactUID()
	if err != nil {
		return "", "", err
	}
	data, err := v.writeNamespaced(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath+"intermediate/generate/internal", map[string]interface{}{
		"common_name": connect.CACN("vault", uid, v.clusterID, v.isPrimary),
		"key_type":    v.config.PrivateKeyType,
		"key_bits":    v.config.PrivateKeyBits,
		"uri_sans":    v.spiffeID.URI().String(),
	})
	if err != nil {
		return "", "", err
	}
	if data == nil || data.Data["csr"] == "" {
		return "", "", fmt.Errorf("got empty value when generating intermediate CSR")
	}
	csr, ok := data.Data["csr"].(string)
	if !ok {
		return "", "", fmt.Errorf("csr result is not a string")
	}
	// Vault 1.11+ will return a "key_id" field which helps
	// identify the correct issuer to set as default.
	// https://github.com/hashicorp/vault/blob/e445c8b4f58dc20a0316a7fd1b5725b401c3b17a/builtin/logical/pki/path_intermediate.go#L154
	if rawkeyId, ok := data.Data["key_id"]; ok {
		keyId, ok := rawkeyId.(string)
		if !ok {
			return "", "", fmt.Errorf("key_id is not a string")
		}
		return csr, keyId, nil
	}
	return csr, "", nil
}

// SetIntermediate writes the incoming intermediate and root certificates to the
// intermediate backend (as a chain).
func (v *VaultProvider) SetIntermediate(intermediatePEM, rootPEM, keyId string) error {
	if v.isPrimary {
		return fmt.Errorf("cannot set an intermediate using another root in the primary datacenter")
	}

	err := validateSetIntermediate(intermediatePEM, rootPEM, v.spiffeID)
	if err != nil {
		return err
	}

	importResp, err := v.writeNamespaced(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath+"intermediate/set-signed", map[string]interface{}{
		"certificate": intermediatePEM,
	})
	if err != nil {
		return err
	}

	// Vault 1.11+ will return a non-nil response from intermediate/set-signed
	if importResp != nil {
		err := v.setDefaultIntermediateIssuer(importResp, keyId)
		if err != nil {
			return fmt.Errorf("failed to update default intermediate issuer: %w", err)
		}
	}

	return nil
}

// ActiveIntermediate returns the current intermediate certificate.
func (v *VaultProvider) ActiveLeafSigningCert() (string, error) {
	cert, err := v.getCA(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath)

	// This error is expected when calling initializeSecondaryCA for the
	// first time. It means that the backend is mounted and ready, but
	// there is no intermediate.
	// This error is swallowed because there is nothing the caller can do
	// about it. The caller needs to handle the empty cert though and
	// create an intermediate CA.
	if err == ErrBackendNotInitialized {
		return "", nil
	}
	return cert, err
}

// getCA returns the raw CA cert for the given endpoint if there is one.
// We have to use the raw NewRequest call here instead of Logical().Read
// because the endpoint only returns the raw PEM contents of the CA cert
// and not the typical format of the secrets endpoints.
func (v *VaultProvider) getCA(namespace, path string) (string, error) {
	resp, err := v.client.WithNamespace(v.getNamespace(namespace)).Logical().ReadRaw(path + "/ca/pem")
	if resp != nil {
		defer resp.Body.Close()
	}
	if resp != nil && resp.StatusCode == http.StatusNotFound {
		return "", ErrBackendNotMounted
	}
	if err != nil {
		return "", err
	}

	bytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	root := lib.EnsureTrailingNewline(string(bytes))
	if root == "" {
		return "", ErrBackendNotInitialized
	}

	return root, nil
}

// TODO: refactor to remove duplication with getCA
func (v *VaultProvider) getCAChain(namespace, path string) (string, error) {
	resp, err := v.client.WithNamespace(v.getNamespace(namespace)).Logical().ReadRaw(path + "/ca_chain")
	if resp != nil {
		defer resp.Body.Close()
	}
	if resp != nil && resp.StatusCode == http.StatusNotFound {
		return "", ErrBackendNotMounted
	}
	if err != nil {
		return "", err
	}

	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	root := lib.EnsureTrailingNewline(string(raw))
	return root, nil
}

// GenerateLeafSigningCert mounts the configured intermediate PKI backend if
// necessary, then generates and signs a new CA CSR using the root PKI backend
// and updates the intermediate backend to use that new certificate.
func (v *VaultProvider) GenerateLeafSigningCert() (string, error) {
	csr, keyId, err := v.generateIntermediateCSR()
	if err != nil {
		return "", err
	}

	// Sign the CSR with the root backend.
	intermediate, err := v.writeNamespaced(v.config.RootPKINamespace, v.config.RootPKIPath+"root/sign-intermediate", map[string]interface{}{
		"csr":            csr,
		"use_csr_values": true,
		"format":         "pem_bundle",
		"ttl":            v.config.IntermediateCertTTL.String(),
	})
	if err != nil {
		return "", err
	}
	if intermediate == nil || intermediate.Data["certificate"] == "" {
		return "", fmt.Errorf("got empty value when generating intermediate certificate")
	}

	// Set the intermediate backend to use the new certificate.
	importResp, err := v.writeNamespaced(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath+"intermediate/set-signed", map[string]interface{}{
		"certificate": intermediate.Data["certificate"],
	})
	if err != nil {
		return "", err
	}

	// Vault 1.11+ will return a non-nil response from intermediate/set-signed
	if importResp != nil {
		err := v.setDefaultIntermediateIssuer(importResp, keyId)
		if err != nil {
			return "", fmt.Errorf("failed to update default intermediate issuer: %w", err)
		}
	}

	return v.ActiveLeafSigningCert()
}

// setDefaultIntermediateIssuer updates the default issuer for
// intermediate CA since Vault, as part of its 1.11+ support for
// multiple issuers, no longer overwrites the default issuer when
// generateIntermediateCSR (intermediate/generate/internal) is called.
//
// The response we get from calling [/intermediate/set-signed]
// should contain a "mapping" data field we can use to cross-reference
// with the keyId returned when calling [/intermediate/generate/internal].
//
// After a new default issuer is written, this function also cleans up
// the previous default issuer along with its associated key.
//
// [/intermediate/set-signed]: https://developer.hashicorp.com/vault/api-docs/secret/pki#import-ca-certificates-and-keys
// [/intermediate/generate/internal]: https://developer.hashicorp.com/vault/api-docs/secret/pki#generate-intermediate-csr
func (v *VaultProvider) setDefaultIntermediateIssuer(vaultResp *vaultapi.Secret, keyId string) error {
	if vaultResp.Data["mapping"] == nil {
		return fmt.Errorf("expected Vault response data to have a 'mapping' key")
	}
	if keyId == "" {
		return fmt.Errorf("expected non-empty keyId")
	}

	mapping, ok := vaultResp.Data["mapping"].(map[string]any)
	if !ok {
		return fmt.Errorf("unexpected type for 'mapping' value in Vault response")
	}

	var intermediateId string
	// The value in this KV pair is called "key"
	for issuer, key := range mapping {
		if key == keyId {
			// Expect to find the key_id we got from Vault when we
			// generated the intermediate CSR.
			intermediateId = issuer
			break
		}
	}
	if intermediateId == "" {
		return fmt.Errorf("could not find key_id %q in response from vault", keyId)
	}

	// For Vault 1.11+ it is important to GET then POST to avoid clobbering fields
	// like `default_follows_latest_issuer`.
	// https://developer.hashicorp.com/vault/api-docs/secret/pki#default_follows_latest_issuer
	resp, err := v.readNamespaced(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath+"config/issuers")
	if err != nil {
		return fmt.Errorf("could not read from /config/issuers: %w", err)
	}
	issuersConf := resp.Data
	prevIssuer, ok := issuersConf["default"].(string)
	if !ok {
		return fmt.Errorf("unexpected type for 'default' value in Vault response from /pki/config/issuers")
	}

	if prevIssuer == intermediateId {
		return nil
	}

	// Overwrite the default issuer
	issuersConf["default"] = intermediateId
	_, err = v.writeNamespaced(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath+"config/issuers", issuersConf)
	if err != nil {
		return fmt.Errorf("could not write default issuer to /config/issuers: %w", err)
	}

	// Find the key_id of the previous issuer. In Consul, issuers have 1:1 relationship with
	// keys so we can delete issuer first then the key.
	resp, err = v.readNamespaced(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath+"issuer/"+prevIssuer)
	if err != nil {
		return fmt.Errorf("could not read issuer %q: %w", prevIssuer, err)
	}
	prevKeyId, ok := resp.Data["key_id"].(string)
	if !ok {
		return fmt.Errorf("unexpected type for 'key_id' value in Vault response")
	}

	// Delete the previously known default issuer to prevent the number of unused
	// issuers from increasing too much.
	_, err = v.deleteNamespaced(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath+"issuer/"+prevIssuer)
	if err != nil {
		v.logger.Warn("Could not delete previous issuer. Manually delete from Vault to prevent the list of issuers from growing too large.",
			"prev_issuer_id", prevIssuer,
			"error", err)
	}

	// Keys can only be deleted if there are no more issuers referencing them.
	_, err = v.deleteNamespaced(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath+"key/"+prevKeyId)
	if err != nil {
		v.logger.Warn("Could not delete previous key. Manually delete from Vault to prevent the list of keys from growing too large.",
			"prev_key_id", prevKeyId,
			"error", err)
	}

	return nil
}

// Sign calls the configured role in the intermediate PKI backend to issue
// a new leaf certificate based on the provided CSR, with the issuing
// intermediate CA cert attached.
func (v *VaultProvider) Sign(csr *x509.CertificateRequest) (string, error) {
	connect.HackSANExtensionForCSR(csr)

	var pemBuf bytes.Buffer
	if err := pem.Encode(&pemBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr.Raw}); err != nil {
		return "", err
	}

	// Use the leaf cert role to sign a new cert for this CSR.
	response, err := v.writeNamespaced(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath+"sign/"+VaultCALeafCertRole, map[string]interface{}{
		"csr": pemBuf.String(),
		"ttl": v.config.LeafCertTTL.String(),
	})
	if err != nil {
		return "", fmt.Errorf("error issuing cert: %v", err)
	}
	if response == nil || response.Data["certificate"] == "" || response.Data["issuing_ca"] == "" {
		return "", fmt.Errorf("certificate info returned from Vault was blank")
	}

	cert, ok := response.Data["certificate"].(string)
	if !ok {
		return "", fmt.Errorf("certificate was not a string")
	}
	return lib.EnsureTrailingNewline(cert), nil
}

// SignIntermediate returns a signed CA certificate with a path length constraint
// of 0 to ensure that the certificate cannot be used to generate further CA certs.
func (v *VaultProvider) SignIntermediate(csr *x509.CertificateRequest) (string, error) {
	err := validateSignIntermediate(csr, v.spiffeID)
	if err != nil {
		return "", err
	}

	var pemBuf bytes.Buffer
	err = pem.Encode(&pemBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr.Raw})
	if err != nil {
		return "", err
	}

	// Sign the CSR with the root backend.
	data, err := v.writeNamespaced(v.config.RootPKINamespace, v.config.RootPKIPath+"root/sign-intermediate", map[string]interface{}{
		"csr":             pemBuf.String(),
		"use_csr_values":  true,
		"format":          "pem_bundle",
		"max_path_length": 0,
		"ttl":             v.config.IntermediateCertTTL.String(),
	})
	if err != nil {
		return "", err
	}
	if data == nil || data.Data["certificate"] == "" {
		return "", fmt.Errorf("got empty value when generating intermediate certificate")
	}

	intermediate, ok := data.Data["certificate"].(string)
	if !ok {
		return "", fmt.Errorf("signed intermediate result is not a string")
	}

	return lib.EnsureTrailingNewline(intermediate), nil
}

// CrossSignCA takes a CA certificate and cross-signs it to form a trust chain
// back to our active root.
func (v *VaultProvider) CrossSignCA(cert *x509.Certificate) (string, error) {
	rootPEM, err := v.getCA(v.config.RootPKINamespace, v.config.RootPKIPath)
	if err != nil {
		return "", fmt.Errorf("failed to get root CA: %w", err)
	}
	rootCert, err := connect.ParseCert(rootPEM)
	if err != nil {
		return "", fmt.Errorf("error parsing root cert: %v", err)
	}
	if rootCert.NotAfter.Before(time.Now()) {
		return "", fmt.Errorf("root certificate is expired")
	}

	var pemBuf bytes.Buffer
	err = pem.Encode(&pemBuf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
	if err != nil {
		return "", err
	}

	// Have the root PKI backend sign this cert.
	response, err := v.writeNamespaced(v.config.RootPKINamespace, v.config.RootPKIPath+"root/sign-self-issued", map[string]interface{}{
		"certificate": pemBuf.String(),
	})
	if err != nil {
		return "", fmt.Errorf("error having Vault cross-sign cert: %v", err)
	}
	if response == nil || response.Data["certificate"] == "" {
		return "", fmt.Errorf("certificate info returned from Vault was blank")
	}

	xcCert, ok := response.Data["certificate"].(string)
	if !ok {
		return "", fmt.Errorf("certificate was not a string")
	}

	return lib.EnsureTrailingNewline(xcCert), nil
}

// SupportsCrossSigning implements Provider
func (v *VaultProvider) SupportsCrossSigning() (bool, error) {
	return true, nil
}

// Cleanup unmounts the configured intermediate PKI backend. It's fine to tear
// this down and recreate it on small config changes because the intermediate
// certs get bundled with the leaf certs, so there's no cost to the CA changing.
func (v *VaultProvider) Cleanup(providerTypeChange bool, otherConfig map[string]interface{}) error {
	v.Stop()

	if !providerTypeChange {
		newConfig, err := ParseVaultCAConfig(otherConfig, v.isPrimary)
		if err != nil {
			return err
		}

		// if the intermeidate PKI path isn't changing we don't want to delete it as
		// Cleanup is called after initializing the new provider
		if newConfig.IntermediatePKIPath == v.config.IntermediatePKIPath {
			return nil
		}
	}

	err := v.unmountNamespaced(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath)

	switch err {
	case ErrBackendNotMounted, ErrBackendNotInitialized:
		// suppress these errors if we didn't finish initialization before
		return nil
	default:
		return err
	}
}

// Stop shuts down the token renew goroutine.
func (v *VaultProvider) Stop() {
	v.stopWatcher()
}

// We use raw path here
func (v *VaultProvider) mountNamespaced(namespace, path string, mountInfo *vaultapi.MountInput) error {
	return v.client.WithNamespace(v.getNamespace(namespace)).Sys().Mount(path, mountInfo)
}

func (v *VaultProvider) tuneMountNamespaced(namespace, path string, mountConfig *vaultapi.MountConfigInput) error {
	return v.client.WithNamespace(v.getNamespace(namespace)).Sys().TuneMount(path, *mountConfig)
}

func (v *VaultProvider) unmountNamespaced(namespace, path string) error {
	return v.client.WithNamespace(v.getNamespace(namespace)).Sys().Unmount(path)
}

func (v *VaultProvider) readNamespaced(namespace string, resource string) (*vaultapi.Secret, error) {
	return v.client.WithNamespace(v.getNamespace(namespace)).Logical().Read(resource)
}

func (v *VaultProvider) writeNamespaced(namespace string, resource string, data map[string]interface{}) (*vaultapi.Secret, error) {
	return v.client.WithNamespace(v.getNamespace(namespace)).Logical().Write(resource, data)
}

func (v *VaultProvider) deleteNamespaced(namespace string, resource string) (*vaultapi.Secret, error) {
	return v.client.WithNamespace(v.getNamespace(namespace)).Logical().Delete(resource)
}

func (v *VaultProvider) getNamespace(namespace string) string {
	if namespace != "" {
		return namespace
	}
	return v.baseNamespace
}

// autotidyIssuers sets Vault's auto-tidy to remove expired issuers
// Returns a boolean on success for testing (as there is no post-facto way of
// checking if it is set). Logs at info level on failure to set and why,
// returning the log message for test purposes as well.
func (v *VaultProvider) autotidyIssuers(path string) (bool, string) {
	s, err := v.client.Logical().Write(path+"/config/auto-tidy",
		map[string]interface{}{
			"enabled":              true,
			"tidy_expired_issuers": true,
		})
	var errStr string
	if err != nil {
		errStr = err.Error()
		switch {
		case strings.Contains(errStr, "404"):
			errStr = "vault versions < 1.12 don't support auto-tidy"
		case strings.Contains(errStr, "400"):
			errStr = "vault versions < 1.13 don't support the tidy_expired_issuers field"
		case strings.Contains(errStr, "403"):
			errStr = "permission denied on auto-tidy path in vault"
		}
		v.logger.Info("Unable to enable Vault's auto-tidy feature for expired issuers", "reason", errStr, "path", path)
	}
	// return values for tests
	tidySet := false
	if s != nil {
		if tei, ok := s.Data["tidy_expired_issuers"]; ok {
			tidySet, _ = tei.(bool)
		}
	}
	return tidySet, errStr
}

func ParseVaultCAConfig(raw map[string]interface{}, isPrimary bool) (*structs.VaultCAProviderConfig, error) {
	config := structs.VaultCAProviderConfig{
		CommonCAProviderConfig: defaultCommonConfig(),
	}

	decodeConf := &mapstructure.DecoderConfig{
		DecodeHook: mapstructure.ComposeDecodeHookFunc(
			structs.ParseDurationFunc(),
			decode.HookTranslateKeys,
		),
		Result:           &config,
		WeaklyTypedInput: true,
	}

	decoder, err := mapstructure.NewDecoder(decodeConf)
	if err != nil {
		return nil, err
	}

	if err := decoder.Decode(raw); err != nil {
		return nil, fmt.Errorf("error decoding config: %s", err)
	}

	if config.Token == "" && config.AuthMethod == nil {
		return nil, fmt.Errorf("must provide a Vault token or configure a Vault auth method")
	}

	if config.Token != "" && config.AuthMethod != nil {
		return nil, fmt.Errorf("only one of Vault token or Vault auth method can be provided, but not both")
	}

	if isPrimary && config.RootPKIPath == "" {
		return nil, fmt.Errorf("must provide a valid path to a root PKI backend")
	}
	if config.RootPKIPath != "" && !strings.HasSuffix(config.RootPKIPath, "/") {
		config.RootPKIPath += "/"
	}

	if config.IntermediatePKIPath == "" {
		return nil, fmt.Errorf("must provide a valid path for the intermediate PKI backend")
	}
	if !strings.HasSuffix(config.IntermediatePKIPath, "/") {
		config.IntermediatePKIPath += "/"
	}

	if err := config.Validate(); err != nil {
		return nil, err
	}

	return &config, nil
}

func vaultLogin(client *vaultapi.Client, authMethod *structs.VaultAuthMethod) (*vaultapi.Secret, error) {
	vaultAuth, err := configureVaultAuthMethod(authMethod)
	if err != nil {
		return nil, err
	}

	resp, err := vaultAuth.Login(context.Background(), client)
	if err != nil {
		return nil, err
	}
	if resp == nil || resp.Auth == nil || resp.Auth.ClientToken == "" {
		return nil, fmt.Errorf("login response did not return client token")
	}

	return resp, nil
}

// Note the authMethod's parameters (Params) is populated from a freeform map
// in the configuration where they could hardcode values to be passed directly
// to the `auth/*/login` endpoint. Each auth method's authentication code
// needs to handle two cases:
// - The legacy case (which should be deprecated) where the user has
// hardcoded login values directly (eg. a `jwt` string)
// - The case where they use the configuration option used in the
// vault agent's auth methods.
func configureVaultAuthMethod(authMethod *structs.VaultAuthMethod) (VaultAuthenticator, error) {
	if authMethod.MountPath == "" {
		authMethod.MountPath = authMethod.Type
	}

	loginPath := ""
	switch authMethod.Type {
	case VaultAuthMethodTypeAWS:
		return NewAWSAuthClient(authMethod), nil
	case VaultAuthMethodTypeAzure:
		return NewAzureAuthClient(authMethod)
	case VaultAuthMethodTypeGCP:
		return NewGCPAuthClient(authMethod)
	case VaultAuthMethodTypeJWT:
		return NewJwtAuthClient(authMethod)
	case VaultAuthMethodTypeAppRole:
		return NewAppRoleAuthClient(authMethod)
	case VaultAuthMethodTypeAliCloud:
		return NewAliCloudAuthClient(authMethod)
	case VaultAuthMethodTypeKubernetes:
		return NewK8sAuthClient(authMethod)
	// These auth methods require a username for the login API path.
	case VaultAuthMethodTypeLDAP, VaultAuthMethodTypeUserpass, VaultAuthMethodTypeOkta, VaultAuthMethodTypeRadius:
		// Get username from the params.
		if username, ok := authMethod.Params["username"]; ok {
			loginPath = fmt.Sprintf("auth/%s/login/%s", authMethod.MountPath, username)
		} else {
			return nil, fmt.Errorf("failed to get 'username' from auth method params")
		}
		return NewVaultAPIAuthClient(authMethod, loginPath), nil
	// This auth method requires a role for the login API path.
	case VaultAuthMethodTypeOCI:
		if role, ok := authMethod.Params["role"]; ok {
			loginPath = fmt.Sprintf("auth/%s/login/%s", authMethod.MountPath, role)
		} else {
			return nil, fmt.Errorf("failed to get 'role' from auth method params")
		}
		return NewVaultAPIAuthClient(authMethod, loginPath), nil
	case VaultAuthMethodTypeToken:
		return nil, fmt.Errorf("'token' auth method is not supported via auth method configuration; " +
			"please provide the token with the 'token' parameter in the CA configuration")
	// The rest of the auth methods use auth/<auth method path> login API path.
	case VaultAuthMethodTypeCloudFoundry,
		VaultAuthMethodTypeGitHub,
		VaultAuthMethodTypeKerberos,
		VaultAuthMethodTypeTLS:
		return NewVaultAPIAuthClient(authMethod, loginPath), nil
	default:
		return nil, fmt.Errorf("auth method %q is not supported", authMethod.Type)
	}
}
