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

package nomad

import (
	"context"
	"fmt"
	"strings"

	"github.com/hashicorp/nomad/helper"
	"github.com/hashicorp/nomad/nomad/structs"
	vapi "github.com/hashicorp/vault/api"
	"golang.org/x/exp/slices"
)

// jobVaultHook is an job registration admission controller for Vault blocks.
type jobVaultHook struct {
	srv *Server
}

func (jobVaultHook) Name() string {
	return "vault"
}

func (h jobVaultHook) Validate(job *structs.Job) ([]error, error) {
	vaultBlocks := job.Vault()
	if len(vaultBlocks) == 0 {
		return nil, nil
	}

	vconf := h.srv.config.VaultConfig
	if !vconf.IsEnabled() {
		return nil, fmt.Errorf("Vault not enabled but used in the job")
	}

	// Return early if Vault configuration doesn't require authentication.
	if vconf.AllowsUnauthenticated() {
		return nil, nil
	}

	// At this point the job has a vault block and the server requires
	// authentication, so check if the user has the right permissions.
	if job.VaultToken == "" {
		return nil, fmt.Errorf("Vault used in the job but missing Vault token")
	}

	tokenSecret, err := h.srv.vault.LookupToken(context.Background(), job.VaultToken)
	if err != nil {
		return nil, fmt.Errorf("failed to lookup Vault token: %v", err)
	}

	// Check namespaces.
	err = h.validateNamespaces(vaultBlocks, tokenSecret)
	if err != nil {
		return nil, err
	}

	// Check policies.
	err = h.validatePolicies(vaultBlocks, tokenSecret)
	if err != nil {
		return nil, err
	}

	return nil, nil
}

// validatePolicies returns an error if the job contains Vault blocks that
// require policies that the request token is not allowed to access.
func (jobVaultHook) validatePolicies(
	blocks map[string]map[string]*structs.Vault,
	token *vapi.Secret,
) error {

	jobPolicies := structs.VaultPoliciesSet(blocks)
	if len(jobPolicies) == 0 {
		return nil
	}

	allowedPolicies, err := token.TokenPolicies()
	if err != nil {
		return fmt.Errorf("failed to lookup Vault token policies: %v", err)
	}

	// If we are given a root token it can access all policies
	if slices.Contains(allowedPolicies, "root") {
		return nil
	}

	subset, offending := helper.IsSubset(allowedPolicies, jobPolicies)
	if !subset {
		return fmt.Errorf("Vault token doesn't allow access to the following policies: %s",
			strings.Join(offending, ", "))
	}

	return nil
}
