// Copyright IBM Corp. 2015, 2025
// SPDX-License-Identifier: BUSL-1.1

package structs

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"maps"
	"path"
	"regexp"
	"slices"
	"strconv"
	"time"

	"github.com/hashicorp/go-bexpr"
	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/go-multierror"
	"github.com/hashicorp/go-set/v3"
	lru "github.com/hashicorp/golang-lru/v2"
	"github.com/hashicorp/nomad/acl"
	"github.com/hashicorp/nomad/helper"
	"github.com/hashicorp/nomad/helper/pointer"
	"github.com/hashicorp/nomad/helper/uuid"
	"github.com/hashicorp/nomad/lib/lang"
	"golang.org/x/crypto/blake2b"
	"oss.indeed.com/go/libtime"
)

const (
	// ACLUpsertPoliciesRPCMethod is the RPC method for batch creating or
	// modifying ACL policies.
	//
	// Args: ACLPolicyUpsertRequest
	// Reply: GenericResponse
	ACLUpsertPoliciesRPCMethod = "ACL.UpsertPolicies"

	// ACLUpsertTokensRPCMethod is the RPC method for batch creating or
	// modifying ACL tokens.
	//
	// Args: ACLTokenUpsertRequest
	// Reply: ACLTokenUpsertResponse
	ACLUpsertTokensRPCMethod = "ACL.UpsertTokens"

	// ACLDeleteTokensRPCMethod is the RPC method for batch deleting ACL
	// tokens.
	//
	// Args: ACLTokenDeleteRequest
	// Reply: GenericResponse
	ACLDeleteTokensRPCMethod = "ACL.DeleteTokens"

	// ACLUpsertRolesRPCMethod is the RPC method for batch creating or
	// modifying ACL roles.
	//
	// Args: ACLRolesUpsertRequest
	// Reply: ACLRolesUpsertResponse
	ACLUpsertRolesRPCMethod = "ACL.UpsertRoles"

	// ACLDeleteRolesByIDRPCMethod the RPC method for batch deleting ACL
	// roles by their ID.
	//
	// Args: ACLRolesDeleteByIDRequest
	// Reply: ACLRolesDeleteByIDResponse
	ACLDeleteRolesByIDRPCMethod = "ACL.DeleteRolesByID"

	// ACLListRolesRPCMethod is the RPC method for listing ACL roles.
	//
	// Args: ACLRolesListRequest
	// Reply: ACLRolesListResponse
	ACLListRolesRPCMethod = "ACL.ListRoles"

	// ACLGetRolesByIDRPCMethod is the RPC method for detailing a number of ACL
	// roles using their ID. This is an internal only RPC endpoint and used by
	// the ACL Role replication process.
	//
	// Args: ACLRolesByIDRequest
	// Reply: ACLRolesByIDResponse
	ACLGetRolesByIDRPCMethod = "ACL.GetRolesByID"

	// ACLGetRoleByIDRPCMethod is the RPC method for detailing an individual
	// ACL role using its ID.
	//
	// Args: ACLRoleByIDRequest
	// Reply: ACLRoleByIDResponse
	ACLGetRoleByIDRPCMethod = "ACL.GetRoleByID"

	// ACLGetRoleByNameRPCMethod is the RPC method for detailing an individual
	// ACL role using its name.
	//
	// Args: ACLRoleByNameRequest
	// Reply: ACLRoleByNameResponse
	ACLGetRoleByNameRPCMethod = "ACL.GetRoleByName"

	// ACLUpsertAuthMethodsRPCMethod is the RPC method for batch creating or
	// modifying auth methods.
	//
	// Args: ACLAuthMethodsUpsertRequest
	// Reply: ACLAuthMethodUpsertResponse
	ACLUpsertAuthMethodsRPCMethod = "ACL.UpsertAuthMethods"

	// ACLDeleteAuthMethodsRPCMethod is the RPC method for batch deleting auth
	// methods.
	//
	// Args: ACLAuthMethodDeleteRequest
	// Reply: ACLAuthMethodDeleteResponse
	ACLDeleteAuthMethodsRPCMethod = "ACL.DeleteAuthMethods"

	// ACLListAuthMethodsRPCMethod is the RPC method for listing auth methods.
	//
	// Args: ACLAuthMethodListRequest
	// Reply: ACLAuthMethodListResponse
	ACLListAuthMethodsRPCMethod = "ACL.ListAuthMethods"

	// ACLGetAuthMethodRPCMethod is the RPC method for detailing an individual
	// auth method using its name.
	//
	// Args: ACLAuthMethodGetRequest
	// Reply: ACLAuthMethodGetResponse
	ACLGetAuthMethodRPCMethod = "ACL.GetAuthMethod"

	// ACLGetAuthMethodsRPCMethod is the RPC method for getting multiple auth
	// methods using their names.
	//
	// Args: ACLAuthMethodsGetRequest
	// Reply: ACLAuthMethodsGetResponse
	ACLGetAuthMethodsRPCMethod = "ACL.GetAuthMethods"

	// ACLUpsertBindingRulesRPCMethod is the RPC method for batch creating or
	// modifying binding rules.
	//
	// Args: ACLBindingRulesUpsertRequest
	// Reply: ACLBindingRulesUpsertResponse
	ACLUpsertBindingRulesRPCMethod = "ACL.UpsertBindingRules"

	// ACLDeleteBindingRulesRPCMethod is the RPC method for batch deleting
	// binding rules.
	//
	// Args: ACLBindingRulesDeleteRequest
	// Reply: ACLBindingRulesDeleteResponse
	ACLDeleteBindingRulesRPCMethod = "ACL.DeleteBindingRules"

	// ACLListBindingRulesRPCMethod is the RPC method listing binding rules.
	//
	// Args: ACLBindingRulesListRequest
	// Reply: ACLBindingRulesListResponse
	ACLListBindingRulesRPCMethod = "ACL.ListBindingRules"

	// ACLGetBindingRulesRPCMethod is the RPC method for getting multiple
	// binding rules using their IDs.
	//
	// Args: ACLBindingRulesRequest
	// Reply: ACLBindingRulesResponse
	ACLGetBindingRulesRPCMethod = "ACL.GetBindingRules"

	// ACLGetBindingRuleRPCMethod is the RPC method for detailing an individual
	// binding rule using its ID.
	//
	// Args: ACLBindingRuleRequest
	// Reply: ACLBindingRuleResponse
	ACLGetBindingRuleRPCMethod = "ACL.GetBindingRule"

	// ACLOIDCAuthURLRPCMethod is the RPC method for starting the OIDC login
	// workflow. It generates the OIDC provider URL which will be used for user
	// authentication.
	//
	// Args: ACLOIDCAuthURLRequest
	// Reply: ACLOIDCAuthURLResponse
	ACLOIDCAuthURLRPCMethod = "ACL.OIDCAuthURL"

	// ACLOIDCCompleteAuthRPCMethod is the RPC method for completing the OIDC
	// login workflow. It exchanges the OIDC provider token for a Nomad ACL
	// token with roles as defined within the remote provider.
	//
	// Args: ACLOIDCCompleteAuthRequest
	// Reply: ACLOIDCCompleteAuthResponse
	ACLOIDCCompleteAuthRPCMethod = "ACL.OIDCCompleteAuth"

	// ACLLoginRPCMethod is the RPC method for performing a non-OIDC login
	// workflow. It exchanges the provided token for a Nomad ACL token with
	// roles as defined within the remote provider.
	//
	// Args: ACLLoginRequest
	// Reply: ACLLoginResponse
	ACLLoginRPCMethod = "ACL.Login"

	// ACLCreateClientIntroductionTokenRPCMethod is the RPC method for
	// generating a client introduction token. This token is used by Nomad
	// clients as an authentication token when first registering with the
	// cluster.
	//
	// Args: ACLCreateClientIntroductionTokenRequest
	// Reply: ACLCreateClientIntroductionTokenResponse
	ACLCreateClientIntroductionTokenRPCMethod = "ACL.CreateClientIntroductionToken"
)

const (
	// ACLMaxExpiredBatchSize is the maximum number of expired ACL tokens that
	// will be garbage collected in a single trigger. This number helps limit
	// the replication pressure due to expired token deletion. If there are a
	// large number of expired tokens pending garbage collection, this value is
	// a potential limiting factor.
	ACLMaxExpiredBatchSize = 4096

	// maxACLRoleDescriptionLength limits an ACL roles description length.
	maxACLRoleDescriptionLength = 256

	// maxACLBindingRuleDescriptionLength limits an ACL binding rules
	// description length and should be used to validate the object.
	maxACLBindingRuleDescriptionLength = 256

	// ACLAuthMethodTokenLocalityLocal is the ACLAuthMethod.TokenLocality that
	// will generate ACL tokens which can only be used on the local cluster the
	// request was made.
	ACLAuthMethodTokenLocalityLocal = "local"

	// ACLAuthMethodTokenLocalityGlobal is the ACLAuthMethod.TokenLocality that
	// will generate ACL tokens which can be used on all federated clusters.
	ACLAuthMethodTokenLocalityGlobal = "global"

	// ACLAuthMethodTypeOIDC the ACLAuthMethod.Type and represents an
	// auth-method which uses the OIDC protocol.
	ACLAuthMethodTypeOIDC = "OIDC"

	// ACLAuthMethodTypeJWT the ACLAuthMethod.Type and represents an auth-method
	// which uses the JWT type.
	ACLAuthMethodTypeJWT = "JWT"

	DefaultACLAuthMethodTokenNameFormat = "${auth_method_type}-${auth_method_name}"
)

var (
	// ValidACLRoleName is used to validate an ACL role name.
	ValidACLRoleName = regexp.MustCompile("^[a-zA-Z0-9-]{1,128}$")

	// ValidACLAuthMethod is used to validate an ACL auth method name.
	ValidACLAuthMethod = regexp.MustCompile("^[a-zA-Z0-9-]{1,128}$")

	// ValidACLAuthMethodTypes lists supported auth method types.
	ValidACLAuthMethodTypes = []string{ACLAuthMethodTypeOIDC, ACLAuthMethodTypeJWT}

	// AnonymousACLToken is used when no SecretID is provided, and the request
	// is made anonymously.
	AnonymousACLToken = &ACLToken{
		AccessorID: "anonymous",
		Name:       "Anonymous Token",
		Type:       ACLClientToken,
		Policies:   []string{"anonymous"},
		Global:     false,
	}

	// LeaderACLToken is used to represent a leader's own token; this object
	// never gets used except on the leader
	LeaderACLToken = &ACLToken{
		AccessorID: "leader",
		Name:       "Leader Token",
		Type:       ACLManagementToken,
	}

	// ACLsDisabledToken is used when ACLs are disabled.
	ACLsDisabledToken = &ACLToken{
		AccessorID: "acls-disabled",
		Name:       "ACLs disabled token",
		Type:       ACLClientToken,
		Global:     false,
	}
)

type ACLCacheEntry[T any] lang.Pair[T, time.Time]

func (e ACLCacheEntry[T]) Age() time.Duration {
	return time.Since(e.Second)
}

func (e ACLCacheEntry[T]) Get() T {
	return e.First
}

// An ACLCache caches ACL tokens by their policy content.
type ACLCache[T any] struct {
	*lru.TwoQueueCache[string, ACLCacheEntry[T]]
	clock libtime.Clock
}

func (c *ACLCache[T]) Add(key string, item T) {
	c.AddAtTime(key, item, c.clock.Now())
}

func (c *ACLCache[T]) AddAtTime(key string, item T, now time.Time) {
	c.TwoQueueCache.Add(key, ACLCacheEntry[T]{
		First:  item,
		Second: now,
	})
}

func NewACLCache[T any](size int) *ACLCache[T] {
	c, err := lru.New2Q[string, ACLCacheEntry[T]](size)
	if err != nil {
		panic(err) // not possible
	}
	return &ACLCache[T]{
		TwoQueueCache: c,
		clock:         libtime.SystemClock(),
	}
}

// ACLPolicy is used to represent an ACL policy
type ACLPolicy struct {
	Name        string      // Unique name
	Description string      // Human readable
	Rules       string      // HCL or JSON format
	RulesJSON   *acl.Policy // Generated from Rules on read
	JobACL      *JobACL
	Hash        []byte

	CreateIndex uint64
	ModifyIndex uint64
}

// JobACL represents an ACL policy's attachment to a job, group, or task.
type JobACL struct {
	Namespace string // namespace of the job
	JobID     string // ID of the job
	Group     string // ID of the group
	Task      string // ID of the task
}

// SetHash is used to compute and set the hash of the ACL policy
func (a *ACLPolicy) SetHash() []byte {
	// Initialize a 256bit Blake2 hash (32 bytes)
	hash, err := blake2b.New256(nil)
	if err != nil {
		panic(err)
	}

	// Write all the user set fields
	_, _ = hash.Write([]byte(a.Name))
	_, _ = hash.Write([]byte(a.Description))
	_, _ = hash.Write([]byte(a.Rules))

	if a.JobACL != nil {
		_, _ = hash.Write([]byte(a.JobACL.Namespace))
		_, _ = hash.Write([]byte(a.JobACL.JobID))
		_, _ = hash.Write([]byte(a.JobACL.Group))
		_, _ = hash.Write([]byte(a.JobACL.Task))
	}

	// Finalize the hash
	hashVal := hash.Sum(nil)

	// Set and return the hash
	a.Hash = hashVal
	return hashVal
}

func (a *ACLPolicy) Stub() *ACLPolicyListStub {
	return &ACLPolicyListStub{
		Name:        a.Name,
		Description: a.Description,
		JobACL:      a.JobACL,
		Hash:        a.Hash,
		CreateIndex: a.CreateIndex,
		ModifyIndex: a.ModifyIndex,
	}
}

func (a *ACLPolicy) Validate() error {
	var mErr multierror.Error
	if !ValidPolicyName.MatchString(a.Name) {
		err := fmt.Errorf("invalid name '%s'", a.Name)
		mErr.Errors = append(mErr.Errors, err)
	}
	if _, err := acl.Parse(a.Rules, acl.PolicyParseStrict); err != nil {
		err = fmt.Errorf("failed to parse rules: %v", err)
		mErr.Errors = append(mErr.Errors, err)
	}
	if len(a.Description) > maxPolicyDescriptionLength {
		err := fmt.Errorf("description longer than %d", maxPolicyDescriptionLength)
		mErr.Errors = append(mErr.Errors, err)
	}
	if a.JobACL != nil {
		if a.JobACL.JobID != "" && a.JobACL.Namespace == "" {
			err := fmt.Errorf("namespace must be set to set job ID")
			mErr.Errors = append(mErr.Errors, err)
		}
		if a.JobACL.Group != "" && a.JobACL.JobID == "" {
			err := fmt.Errorf("job ID must be set to set group")
			mErr.Errors = append(mErr.Errors, err)
		}
		if a.JobACL.Task != "" && a.JobACL.Group == "" {
			err := fmt.Errorf("group must be set to set task")
			mErr.Errors = append(mErr.Errors, err)
		}
	}

	return mErr.ErrorOrNil()
}

// ACLPolicyListStub is used to for listing ACL policies
type ACLPolicyListStub struct {
	Name        string
	Description string
	JobACL      *JobACL
	Hash        []byte
	CreateIndex uint64
	ModifyIndex uint64
}

// ACLPolicyListRequest is used to request a list of policies
type ACLPolicyListRequest struct {
	QueryOptions
}

// ACLPolicySpecificRequest is used to query a specific policy
type ACLPolicySpecificRequest struct {
	Name string
	QueryOptions
}

// ACLPolicySetRequest is used to query a set of policies
type ACLPolicySetRequest struct {
	Names []string
	QueryOptions
}

// ACLPolicyListResponse is used for a list request
type ACLPolicyListResponse struct {
	Policies []*ACLPolicyListStub
	QueryMeta
}

// SingleACLPolicyResponse is used to return a single policy
type SingleACLPolicyResponse struct {
	Policy *ACLPolicy
	QueryMeta
}

// ACLPolicySetResponse is used to return a set of policies
type ACLPolicySetResponse struct {
	Policies map[string]*ACLPolicy
	QueryMeta
}

// ACLPolicyDeleteRequest is used to delete a set of policies
type ACLPolicyDeleteRequest struct {
	Names []string
	WriteRequest
}

// ACLPolicyUpsertRequest is used to upsert a set of policies
type ACLPolicyUpsertRequest struct {
	Policies []*ACLPolicy
	WriteRequest
}

// ACLToken represents a client token which is used to Authenticate
type ACLToken struct {
	AccessorID string   // Public Accessor ID (UUID)
	SecretID   string   // Secret ID, private (UUID)
	Name       string   // Human friendly name
	Type       string   // Client or Management
	Policies   []string // Policies this token ties to

	// Roles represents the ACL roles that this token is tied to. The token
	// will inherit the permissions of all policies detailed within the role.
	Roles []*ACLTokenRoleLink

	Global     bool // Global or Region local
	Hash       []byte
	CreateTime time.Time // Time of creation

	// ExpirationTime represents the point after which a token should be
	// considered revoked and is eligible for destruction. This time should
	// always use UTC to account for multi-region global tokens. It is a
	// pointer, so we can store nil, rather than the zero value of time.Time.
	ExpirationTime *time.Time

	// ExpirationTTL is a convenience field for helping set ExpirationTime to a
	// value of CreateTime+ExpirationTTL. This can only be set during token
	// creation. This is a string version of a time.Duration like "2m".
	ExpirationTTL time.Duration

	CreateIndex uint64
	ModifyIndex uint64
}

// GetID implements the IDGetter interface, required for pagination.
func (a *ACLToken) GetID() string {
	if a == nil {
		return ""
	}
	return a.AccessorID
}

// GetCreateIndex implements the CreateIndexGetter interface, required for
// pagination.
func (a *ACLToken) GetCreateIndex() uint64 {
	if a == nil {
		return 0
	}
	return a.CreateIndex
}

func (a *ACLToken) Copy() *ACLToken {
	c := new(ACLToken)
	*c = *a

	c.Policies = make([]string, len(a.Policies))
	copy(c.Policies, a.Policies)

	c.Hash = make([]byte, len(a.Hash))
	copy(c.Hash, a.Hash)

	c.Roles = make([]*ACLTokenRoleLink, len(a.Roles))
	copy(c.Roles, a.Roles)

	return c
}

type ACLTokenListStub struct {
	AccessorID     string
	Name           string
	Type           string
	Policies       []string
	Roles          []*ACLTokenRoleLink
	Global         bool
	Hash           []byte
	CreateTime     time.Time
	ExpirationTime *time.Time
	CreateIndex    uint64
	ModifyIndex    uint64
}

// SetHash is used to compute and set the hash of the ACL token. It only hashes
// fields which can be updated, and as such, does not hash fields such as
// ExpirationTime.
func (a *ACLToken) SetHash() []byte {
	// Initialize a 256bit Blake2 hash (32 bytes)
	hash, err := blake2b.New256(nil)
	if err != nil {
		panic(err)
	}

	// Write all the user set fields
	_, _ = hash.Write([]byte(a.Name))
	_, _ = hash.Write([]byte(a.Type))
	for _, policyName := range a.Policies {
		_, _ = hash.Write([]byte(policyName))
	}
	if a.Global {
		_, _ = hash.Write([]byte("global"))
	} else {
		_, _ = hash.Write([]byte("local"))
	}

	// Iterate the ACL role links and hash the ID. The ID is immutable and the
	// canonical way to reference a role. The name can be modified by
	// operators, but won't impact the ACL token resolution.
	for _, roleLink := range a.Roles {
		_, _ = hash.Write([]byte(roleLink.ID))
	}

	// Finalize the hash
	hashVal := hash.Sum(nil)

	// Set and return the hash
	a.Hash = hashVal
	return hashVal
}

func (a *ACLToken) Stub() (*ACLTokenListStub, error) {
	return &ACLTokenListStub{
		AccessorID:     a.AccessorID,
		Name:           a.Name,
		Type:           a.Type,
		Policies:       a.Policies,
		Roles:          a.Roles,
		Global:         a.Global,
		Hash:           a.Hash,
		CreateTime:     a.CreateTime,
		ExpirationTime: a.ExpirationTime,
		CreateIndex:    a.CreateIndex,
		ModifyIndex:    a.ModifyIndex,
	}, nil
}

// ACLTokenListRequest is used to request a list of tokens
type ACLTokenListRequest struct {
	GlobalOnly bool
	QueryOptions
}

// ACLTokenSpecificRequest is used to query a specific token
type ACLTokenSpecificRequest struct {
	AccessorID string
	QueryOptions
}

// ACLTokenSetRequest is used to query a set of tokens
type ACLTokenSetRequest struct {
	AccessorIDS []string
	QueryOptions
}

// ACLTokenListResponse is used for a list request
type ACLTokenListResponse struct {
	Tokens []*ACLTokenListStub
	QueryMeta
}

// SingleACLTokenResponse is used to return a single token
type SingleACLTokenResponse struct {
	Token *ACLToken
	QueryMeta
}

// ACLTokenSetResponse is used to return a set of token
type ACLTokenSetResponse struct {
	Tokens map[string]*ACLToken // Keyed by Accessor ID
	QueryMeta
}

// ResolveACLTokenRequest is used to resolve a specific token
type ResolveACLTokenRequest struct {
	SecretID string
	QueryOptions
}

// ResolveACLTokenResponse is used to resolve a single token
type ResolveACLTokenResponse struct {
	Token *ACLToken
	QueryMeta
}

// ACLTokenDeleteRequest is used to delete a set of tokens
type ACLTokenDeleteRequest struct {
	AccessorIDs []string
	WriteRequest
}

// ACLTokenBootstrapRequest is used to bootstrap ACLs
type ACLTokenBootstrapRequest struct {
	Token           *ACLToken // Not client specifiable
	ResetIndex      uint64    // Reset index is used to clear the bootstrap token
	BootstrapSecret string
	WriteRequest
}

// ACLTokenUpsertRequest is used to upsert a set of tokens
type ACLTokenUpsertRequest struct {
	Tokens []*ACLToken
	WriteRequest
}

// ACLTokenUpsertResponse is used to return from an ACLTokenUpsertRequest
type ACLTokenUpsertResponse struct {
	Tokens []*ACLToken
	WriteMeta
}

// OneTimeToken is used to log into the web UI using a token provided by the
// command line.
type OneTimeToken struct {
	OneTimeSecretID string
	AccessorID      string
	ExpiresAt       time.Time
	CreateIndex     uint64
	ModifyIndex     uint64
}

// OneTimeTokenUpsertRequest is the request for a UpsertOneTimeToken RPC
type OneTimeTokenUpsertRequest struct {
	WriteRequest
}

// OneTimeTokenUpsertResponse is the response to a UpsertOneTimeToken RPC.
type OneTimeTokenUpsertResponse struct {
	OneTimeToken *OneTimeToken
	WriteMeta
}

// OneTimeTokenExchangeRequest is a request to swap the one-time token with
// the backing ACL token
type OneTimeTokenExchangeRequest struct {
	OneTimeSecretID string
	WriteRequest
}

// OneTimeTokenExchangeResponse is the response to swapping the one-time token
// with the backing ACL token
type OneTimeTokenExchangeResponse struct {
	Token *ACLToken
	WriteMeta
}

// OneTimeTokenDeleteRequest is a request to delete a group of one-time tokens
type OneTimeTokenDeleteRequest struct {
	AccessorIDs []string
	WriteRequest
}

// OneTimeTokenExpireRequest is a request to delete all expired one-time tokens
type OneTimeTokenExpireRequest struct {
	Timestamp time.Time
	WriteRequest
}

// ACLTokenRoleLink is used to link an ACL token to an ACL role. The ACL token
// can therefore inherit all the ACL policy permissions that the ACL role
// contains.
type ACLTokenRoleLink struct {

	// ID is the ACLRole.ID UUID. This field is immutable and represents the
	// absolute truth for the link.
	ID string

	// Name is the human friendly identifier for the ACL role and is a
	// convenience field for operators. This field is always resolved to the
	// ID and discarded before the token is stored in state. This is because
	// operators can change the name of an ACL role.
	Name string
}

// Canonicalize performs basic canonicalization on the ACL token object. It is
// important for callers to understand certain fields such as AccessorID are
// set if it is empty, so copies should be taken if needed before calling this
// function.
func (a *ACLToken) Canonicalize() {

	// If the accessor ID is empty, it means this is creation of a new token,
	// therefore we need to generate base information.
	if a.AccessorID == "" {

		a.AccessorID = uuid.Generate()
		a.SecretID = uuid.Generate()
		a.CreateTime = time.Now().UTC()

		// If the user has not set the expiration time, but has provided a TTL, we
		// calculate and populate the former filed.
		if a.ExpirationTime == nil && a.ExpirationTTL != 0 {
			a.ExpirationTime = pointer.Of(a.CreateTime.Add(a.ExpirationTTL))
		}
	}
}

// Validate is used to check a token for reasonableness
func (a *ACLToken) Validate(minTTL, maxTTL time.Duration, existing *ACLToken) error {
	var mErr multierror.Error

	// The human friendly name of an ACL token cannot exceed 256 characters.
	if len(a.Name) > maxTokenNameLength {
		mErr.Errors = append(mErr.Errors, errors.New("token name too long"))
	}

	// The type of an ACL token must be set. An ACL token of type client must
	// have associated policies or roles, whereas a management token cannot be
	// associated with policies.
	switch a.Type {
	case ACLClientToken:
		if len(a.Policies) == 0 && len(a.Roles) == 0 {
			mErr.Errors = append(mErr.Errors, errors.New("client token missing policies or roles"))
		}
	case ACLManagementToken:
		if len(a.Policies) != 0 || len(a.Roles) != 0 {
			mErr.Errors = append(mErr.Errors, errors.New("management token cannot be associated with policies or roles"))
		}
	default:
		mErr.Errors = append(mErr.Errors, errors.New("token type must be client or management"))
	}

	// There are different validation rules depending on whether the ACL token
	// is being created or updated.
	switch existing {
	case nil:
		if a.ExpirationTTL < 0 {
			mErr.Errors = append(mErr.Errors,
				fmt.Errorf("token expiration TTL '%s' should not be negative", a.ExpirationTTL))
		}

		if a.ExpirationTime != nil && !a.ExpirationTime.IsZero() {

			if a.CreateTime.After(*a.ExpirationTime) {
				mErr.Errors = append(mErr.Errors, errors.New("expiration time cannot be before create time"))
			}

			// Create a time duration which details the time-til-expiry, so we can
			// check this against the regions max and min values.
			expiresIn := a.ExpirationTime.Sub(a.CreateTime)
			if expiresIn > maxTTL {
				mErr.Errors = append(mErr.Errors,
					fmt.Errorf("expiration time cannot be more than %s in the future (was %s)",
						maxTTL, expiresIn))

			} else if expiresIn < minTTL {
				mErr.Errors = append(mErr.Errors,
					fmt.Errorf("expiration time cannot be less than %s in the future (was %s)",
						minTTL, expiresIn))
			}
		}
	default:
		if existing.Global != a.Global {
			mErr.Errors = append(mErr.Errors, errors.New("cannot toggle global mode"))
		}
		if existing.ExpirationTTL != a.ExpirationTTL {
			mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration TTL"))
		}
		if a.ExpirationTime != nil {
			if !existing.ExpirationTime.Equal(*a.ExpirationTime) {
				mErr.Errors = append(mErr.Errors, errors.New("cannot update expiration time"))
			}
		}

	}

	return mErr.ErrorOrNil()
}

// HasExpirationTime checks whether the ACL token has an expiration time value
// set.
func (a *ACLToken) HasExpirationTime() bool {
	if a == nil || a.ExpirationTime == nil {
		return false
	}
	return !a.ExpirationTime.IsZero()
}

// IsExpired compares the ACLToken.ExpirationTime against the passed t to
// identify whether the token is considered expired. The function can be called
// without checking whether the ACL token has an expiry time.
func (a *ACLToken) IsExpired(t time.Time) bool {

	// Check the token has an expiration time before potentially modifying the
	// supplied time. This allows us to avoid extra work, if it isn't needed.
	if !a.HasExpirationTime() {
		return false
	}

	// Check and ensure the time location is set to UTC. This is vital for
	// consistency with multi-region global tokens.
	if t.Location() != time.UTC {
		t = t.UTC()
	}

	return a.ExpirationTime.Before(t) || t.IsZero()
}

// HasRoles checks if a given set of role IDs are assigned to the ACL token. It
// does not account for management tokens, therefore it is the responsibility
// of the caller to perform this check, if required.
func (a *ACLToken) HasRoles(roleIDs []string) bool {

	// Generate a set of role IDs that the token is assigned.
	roleSet := set.FromFunc(a.Roles, func(roleLink *ACLTokenRoleLink) string { return roleLink.ID })

	// Iterate the role IDs within the request and check whether these are
	// present within the token assignment.
	for _, roleID := range roleIDs {
		if !roleSet.Contains(roleID) {
			return false
		}
	}
	return true
}

// MarshalJSON implements the json.Marshaler interface and allows
// ACLToken.ExpirationTTL to be marshaled correctly.
func (a *ACLToken) MarshalJSON() ([]byte, error) {
	type Alias ACLToken
	exported := &struct {
		ExpirationTTL string
		*Alias
	}{
		ExpirationTTL: a.ExpirationTTL.String(),
		Alias:         (*Alias)(a),
	}
	if a.ExpirationTTL == 0 {
		exported.ExpirationTTL = ""
	}
	return json.Marshal(exported)
}

// UnmarshalJSON implements the json.Unmarshaler interface and allows
// ACLToken.ExpirationTTL to be unmarshalled correctly.
func (a *ACLToken) UnmarshalJSON(data []byte) (err error) {
	type Alias ACLToken
	aux := &struct {
		ExpirationTTL interface{}
		Hash          string
		*Alias
	}{
		Alias: (*Alias)(a),
	}

	if err = json.Unmarshal(data, &aux); err != nil {
		return err
	}
	if aux.ExpirationTTL != nil {
		switch v := aux.ExpirationTTL.(type) {
		case string:
			if v != "" {
				if a.ExpirationTTL, err = time.ParseDuration(v); err != nil {
					return err
				}
			}
		case float64:
			a.ExpirationTTL = time.Duration(v)
		}

	}
	if aux.Hash != "" {
		a.Hash = []byte(aux.Hash)
	}
	return nil
}

func (a *ACLToken) Sanitize() *ACLToken {
	if a == nil {
		return nil
	}

	out := a.Copy()
	out.SecretID = ""
	return out
}

// ACLRole is an abstraction for the ACL system which allows the grouping of
// ACL policies into a single object. ACL tokens can be created and linked to
// a role; the token then inherits all the permissions granted by the policies.
type ACLRole struct {

	// ID is an internally generated UUID for this role and is controlled by
	// Nomad.
	ID string

	// Name is unique across the entire set of federated clusters and is
	// supplied by the operator on role creation. The name can be modified by
	// updating the role and including the Nomad generated ID. This update will
	// not affect tokens created and linked to this role. This is a required
	// field.
	Name string

	// Description is a human-readable, operator set description that can
	// provide additional context about the role. This is an operational field.
	Description string

	// Policies is an array of ACL policy links. Although currently policies
	// can only be linked using their name, in the future we will want to add
	// IDs also and thus allow operators to specify either a name, an ID, or
	// both.
	Policies []*ACLRolePolicyLink

	// Hash is the hashed value of the role and is generated using all fields
	// above this point.
	Hash []byte

	CreateIndex uint64
	ModifyIndex uint64
}

// ACLRolePolicyLink is used to link a policy to an ACL role. We use a struct
// rather than a list of strings as in the future we will want to add IDs to
// policies and then link via these.
type ACLRolePolicyLink struct {

	// Name is the ACLPolicy.Name value which will be linked to the ACL role.
	Name string
}

// SetHash is used to compute and set the hash of the ACL role. This should be
// called every and each time a user specified field on the role is changed
// before updating the Nomad state store.
func (a *ACLRole) SetHash() []byte {

	// Initialize a 256bit Blake2 hash (32 bytes).
	hash, err := blake2b.New256(nil)
	if err != nil {
		panic(err)
	}

	// Write all the user set fields.
	_, _ = hash.Write([]byte(a.Name))
	_, _ = hash.Write([]byte(a.Description))

	for _, policyLink := range a.Policies {
		_, _ = hash.Write([]byte(policyLink.Name))
	}

	// Finalize the hash.
	hashVal := hash.Sum(nil)

	// Set and return the hash.
	a.Hash = hashVal
	return hashVal
}

// Validate ensure the ACL role contains valid information which meets Nomad's
// internal requirements. This does not include any state calls, such as
// ensuring the linked policies exist.
func (a *ACLRole) Validate() error {

	var mErr multierror.Error

	if !ValidACLRoleName.MatchString(a.Name) {
		mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid name '%s'", a.Name))
	}

	if len(a.Description) > maxACLRoleDescriptionLength {
		mErr.Errors = append(mErr.Errors, fmt.Errorf("description longer than %d", maxACLRoleDescriptionLength))
	}

	if len(a.Policies) < 1 {
		mErr.Errors = append(mErr.Errors, errors.New("at least one policy should be specified"))
	}

	return mErr.ErrorOrNil()
}

// Canonicalize performs basic canonicalization on the ACL role object. It is
// important for callers to understand certain fields such as ID are set if it
// is empty, so copies should be taken if needed before calling this function.
func (a *ACLRole) Canonicalize() {
	if a.ID == "" {
		a.ID = uuid.Generate()
	}
}

// Equal performs an equality check on the two service registrations. It
// handles nil objects.
func (a *ACLRole) Equal(o *ACLRole) bool {
	if a == nil || o == nil {
		return a == o
	}
	if len(a.Hash) == 0 {
		a.SetHash()
	}
	if len(o.Hash) == 0 {
		o.SetHash()
	}
	return bytes.Equal(a.Hash, o.Hash)
}

// Copy creates a deep copy of the ACL role. This copy can then be safely
// modified. It handles nil objects.
func (a *ACLRole) Copy() *ACLRole {
	if a == nil {
		return nil
	}

	c := new(ACLRole)
	*c = *a

	c.Policies = slices.Clone(a.Policies)
	c.Hash = slices.Clone(a.Hash)

	return c
}

// Stub converts the ACLRole object into a ACLRoleListStub object.
func (a *ACLRole) Stub() *ACLRoleListStub {
	return &ACLRoleListStub{
		ID:          a.ID,
		Name:        a.Name,
		Description: a.Description,
		Policies:    a.Policies,
		Hash:        a.Hash,
		CreateIndex: a.CreateIndex,
		ModifyIndex: a.ModifyIndex,
	}
}

// ACLRoleListStub is the stub object returned when performing a listing of ACL
// roles. While it might not currently be different to the full response
// object, it allows us to future-proof the RPC in the event the ACLRole object
// grows over time.
type ACLRoleListStub struct {

	// ID is an internally generated UUID for this role and is controlled by
	// Nomad.
	ID string

	// Name is unique across the entire set of federated clusters and is
	// supplied by the operator on role creation. The name can be modified by
	// updating the role and including the Nomad generated ID. This update will
	// not affect tokens created and linked to this role. This is a required
	// field.
	Name string

	// Description is a human-readable, operator set description that can
	// provide additional context about the role. This is an operational field.
	Description string

	// Policies is an array of ACL policy links. Although currently policies
	// can only be linked using their name, in the future we will want to add
	// IDs also and thus allow operators to specify either a name, an ID, or
	// both.
	Policies []*ACLRolePolicyLink

	// Hash is the hashed value of the role and is generated using all fields
	// above this point.
	Hash []byte

	CreateIndex uint64
	ModifyIndex uint64
}

// ACLRolesUpsertRequest is the request object used to upsert one or more ACL
// roles.
type ACLRolesUpsertRequest struct {
	ACLRoles []*ACLRole

	// AllowMissingPolicies skips the ACL Role policy link verification and is
	// used by the replication process. The replication cannot ensure policies
	// are present before ACL Roles are replicated.
	AllowMissingPolicies bool

	WriteRequest
}

// ACLRolesUpsertResponse is the response object when one or more ACL roles
// have been successfully upserted into state.
type ACLRolesUpsertResponse struct {
	ACLRoles []*ACLRole
	WriteMeta
}

// ACLRolesDeleteByIDRequest is the request object to delete one or more ACL
// roles using the role ID.
type ACLRolesDeleteByIDRequest struct {
	ACLRoleIDs []string
	WriteRequest
}

// ACLRolesDeleteByIDResponse is the response object when performing a deletion
// of one or more ACL roles using the role ID.
type ACLRolesDeleteByIDResponse struct {
	WriteMeta
}

// ACLRolesListRequest is the request object when performing ACL role listings.
type ACLRolesListRequest struct {
	QueryOptions
}

// ACLRolesListResponse is the response object when performing ACL role
// listings.
type ACLRolesListResponse struct {
	ACLRoles []*ACLRoleListStub
	QueryMeta
}

// ACLRolesByIDRequest is the request object when performing a lookup of
// multiple roles by the ID.
type ACLRolesByIDRequest struct {
	ACLRoleIDs []string
	QueryOptions
}

// ACLRolesByIDResponse is the response object when performing a lookup of
// multiple roles by their IDs.
type ACLRolesByIDResponse struct {
	ACLRoles map[string]*ACLRole
	QueryMeta
}

// ACLRoleByIDRequest is the request object to perform a lookup of an ACL
// role using a specific ID.
type ACLRoleByIDRequest struct {
	RoleID string
	QueryOptions
}

// ACLRoleByIDResponse is the response object when performing a lookup of an
// ACL role matching a specific ID.
type ACLRoleByIDResponse struct {
	ACLRole *ACLRole
	QueryMeta
}

// ACLRoleByNameRequest is the request object to perform a lookup of an ACL
// role using a specific name.
type ACLRoleByNameRequest struct {
	RoleName string
	QueryOptions
}

// ACLRoleByNameResponse is the response object when performing a lookup of an
// ACL role matching a specific name.
type ACLRoleByNameResponse struct {
	ACLRole *ACLRole
	QueryMeta
}

// ACLAuthMethod is used to capture the properties of an authentication method
// used for single sing-on
type ACLAuthMethod struct {
	Name            string
	Type            string
	TokenLocality   string // is the token valid locally or globally?
	TokenNameFormat string
	MaxTokenTTL     time.Duration
	Default         bool
	Config          *ACLAuthMethodConfig

	Hash []byte

	CreateTime  time.Time
	ModifyTime  time.Time
	CreateIndex uint64
	ModifyIndex uint64
}

// SetHash is used to compute and set the hash of the ACL auth method. This
// should be called every and each time a user specified field on the method is
// changed before updating the Nomad state store.
func (a *ACLAuthMethod) SetHash() []byte {

	// Initialize a 256bit Blake2 hash (32 bytes).
	hash, err := blake2b.New256(nil)
	if err != nil {
		panic(err)
	}

	_, _ = hash.Write([]byte(a.Name))
	_, _ = hash.Write([]byte(a.Type))
	_, _ = hash.Write([]byte(a.TokenLocality))
	_, _ = hash.Write([]byte(a.TokenNameFormat))
	_, _ = hash.Write([]byte(a.MaxTokenTTL.String()))
	_, _ = hash.Write([]byte(strconv.FormatBool(a.Default)))

	if a.Config != nil {
		_, _ = hash.Write([]byte(a.Config.JWKSURL))
		_, _ = hash.Write([]byte(a.Config.JWKSCACert))
		_, _ = hash.Write([]byte(a.Config.OIDCDiscoveryURL))
		_, _ = hash.Write([]byte(a.Config.OIDCClientID))
		_, _ = hash.Write([]byte(a.Config.OIDCClientSecret))
		_, _ = hash.Write([]byte(strconv.FormatBool(a.Config.OIDCEnablePKCE)))
		_, _ = hash.Write([]byte(strconv.FormatBool(a.Config.OIDCDisableUserInfo)))
		_, _ = hash.Write([]byte(strconv.FormatBool(a.Config.VerboseLogging)))
		_, _ = hash.Write([]byte(a.Config.ExpirationLeeway.String()))
		_, _ = hash.Write([]byte(a.Config.NotBeforeLeeway.String()))
		_, _ = hash.Write([]byte(a.Config.ClockSkewLeeway.String()))
		for _, ba := range a.Config.BoundAudiences {
			_, _ = hash.Write([]byte(ba))
		}
		for _, bi := range a.Config.BoundIssuer {
			_, _ = hash.Write([]byte(bi))
		}
		for _, uri := range a.Config.AllowedRedirectURIs {
			_, _ = hash.Write([]byte(uri))
		}
		for _, pem := range a.Config.DiscoveryCaPem {
			_, _ = hash.Write([]byte(pem))
		}
		for _, scope := range a.Config.OIDCScopes {
			_, _ = hash.Write([]byte(scope))
		}
		for _, sa := range a.Config.SigningAlgs {
			_, _ = hash.Write([]byte(sa))
		}
		for _, key := range a.Config.JWTValidationPubKeys {
			_, _ = hash.Write([]byte(key))
		}
		for k, v := range a.Config.ClaimMappings {
			_, _ = hash.Write([]byte(k))
			_, _ = hash.Write([]byte(v))
		}
		for k, v := range a.Config.ListClaimMappings {
			_, _ = hash.Write([]byte(k))
			_, _ = hash.Write([]byte(v))
		}
		if a.Config.OIDCClientAssertion != nil {
			_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.KeySource))
			_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.KeyAlgorithm))
			for _, aud := range a.Config.OIDCClientAssertion.Audience {
				_, _ = hash.Write([]byte(aud))
			}
			for k, v := range a.Config.OIDCClientAssertion.ExtraHeaders {
				_, _ = hash.Write([]byte(k))
				_, _ = hash.Write([]byte(v))
			}
			if a.Config.OIDCClientAssertion.PrivateKey != nil {
				_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.PrivateKey.KeyIDHeader))
				_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.PrivateKey.KeyID))
				_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.PrivateKey.PemKey))
				_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.PrivateKey.PemKeyFile))
				_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.PrivateKey.PemCert))
				_, _ = hash.Write([]byte(a.Config.OIDCClientAssertion.PrivateKey.PemCertFile))
			}
		}
	}

	// Finalize the hash.
	hashVal := hash.Sum(nil)

	// Set and return the hash.
	a.Hash = hashVal
	return hashVal
}

// MarshalJSON implements the json.Marshaler interface and allows
// ACLAuthMethod.MaxTokenTTL to be marshaled correctly.
func (a *ACLAuthMethod) MarshalJSON() ([]byte, error) {
	type Alias ACLAuthMethod
	exported := &struct {
		MaxTokenTTL string
		*Alias
	}{
		MaxTokenTTL: a.MaxTokenTTL.String(),
		Alias:       (*Alias)(a),
	}
	if a.MaxTokenTTL == 0 {
		exported.MaxTokenTTL = ""
	}
	return json.Marshal(exported)
}

// UnmarshalJSON implements the json.Unmarshaler interface and allows
// ACLAuthMethod.MaxTokenTTL to be unmarshalled correctly.
func (a *ACLAuthMethod) UnmarshalJSON(data []byte) (err error) {
	type Alias ACLAuthMethod
	aux := &struct {
		MaxTokenTTL interface{}
		*Alias
	}{
		Alias: (*Alias)(a),
	}
	if err = json.Unmarshal(data, &aux); err != nil {
		return err
	}
	if aux.MaxTokenTTL != nil {
		switch v := aux.MaxTokenTTL.(type) {
		case string:
			if a.MaxTokenTTL, err = time.ParseDuration(v); err != nil {
				return err
			}
		case float64:
			a.MaxTokenTTL = time.Duration(v)
		}
	}
	return nil
}

func (a *ACLAuthMethod) Stub() *ACLAuthMethodStub {
	return &ACLAuthMethodStub{
		Name:        a.Name,
		Type:        a.Type,
		Default:     a.Default,
		Hash:        a.Hash,
		CreateIndex: a.CreateIndex,
		ModifyIndex: a.ModifyIndex,
	}
}

func (a *ACLAuthMethod) Equal(other *ACLAuthMethod) bool {
	if a == nil || other == nil {
		return a == other
	}
	if len(a.Hash) == 0 {
		a.SetHash()
	}
	if len(other.Hash) == 0 {
		other.SetHash()
	}
	return bytes.Equal(a.Hash, other.Hash)

}

// Copy creates a deep copy of the ACL auth method. This copy can then be safely
// modified. It handles nil objects.
func (a *ACLAuthMethod) Copy() *ACLAuthMethod {
	if a == nil {
		return nil
	}

	c := new(ACLAuthMethod)
	*c = *a

	c.Hash = slices.Clone(a.Hash)
	c.Config = a.Config.Copy()

	return c
}

// Canonicalize performs basic canonicalization on the ACL auth method object.
func (a *ACLAuthMethod) Canonicalize() {
	t := time.Now().UTC()

	if a.CreateTime.IsZero() {
		a.CreateTime = t
	}
	a.ModifyTime = t

	if a.TokenNameFormat == "" {
		a.TokenNameFormat = DefaultACLAuthMethodTokenNameFormat
	}

	a.Config.Canonicalize()
}

// Merge merges auth method a with method b. It sets all required empty fields
// of method a to corresponding values of method b, except for "default" and
// "name."
func (a *ACLAuthMethod) Merge(b *ACLAuthMethod) {
	if b != nil {
		a.Type = helper.Merge(a.Type, b.Type)
		a.TokenLocality = helper.Merge(a.TokenLocality, b.TokenLocality)
		a.TokenNameFormat = helper.Merge(a.TokenNameFormat, b.TokenNameFormat)
		a.MaxTokenTTL = helper.Merge(a.MaxTokenTTL, b.MaxTokenTTL)
		a.Config = helper.Merge(a.Config, b.Config)
	}
}

// Validate returns an error is the ACLAuthMethod is invalid.
//
// TODO revisit possible other validity conditions in the future
func (a *ACLAuthMethod) Validate(minTTL, maxTTL time.Duration) error {
	var mErr multierror.Error

	if !ValidACLAuthMethod.MatchString(a.Name) {
		mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid name '%s'", a.Name))
	}

	if !slices.Contains([]string{ACLAuthMethodTokenLocalityLocal, ACLAuthMethodTokenLocalityGlobal}, a.TokenLocality) {
		mErr.Errors = append(
			mErr.Errors, fmt.Errorf("invalid token locality '%s'", a.TokenLocality))
	}

	if !slices.Contains(ValidACLAuthMethodTypes, a.Type) {
		mErr.Errors = append(
			mErr.Errors, fmt.Errorf("invalid token type '%s'", a.Type))
	}

	if err := a.Config.Validate(a.Type); err != nil {
		mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid config: %w", err))
	}

	if minTTL > a.MaxTokenTTL || a.MaxTokenTTL > maxTTL {
		mErr.Errors = append(mErr.Errors, fmt.Errorf(
			"invalid MaxTokenTTL value '%s' (should be between %s and %s)",
			a.MaxTokenTTL.String(), minTTL.String(), maxTTL.String()))
	}

	return mErr.ErrorOrNil()
}

// Sanitize returns a copy of the ACLAuthMethod with any secrets redacted
func (a *ACLAuthMethod) Sanitize() *ACLAuthMethod {
	if a == nil || a.Config == nil {
		return a
	}
	// copy to ensure we do not mutate a pointer pulled directly out of state.
	clean := a.Copy()
	// clean nested structs here, so it's obvious what all is being cleaned
	// in one spot, rather than follow a stack of sanitization calls.
	if clean.Config.OIDCClientSecret != "" {
		clean.Config.OIDCClientSecret = "redacted"
	}
	if clean.Config.OIDCClientAssertion != nil {
		// this ClientSecret gets inherited by the above one
		if clean.Config.OIDCClientAssertion.ClientSecret != "" {
			clean.Config.OIDCClientAssertion.ClientSecret = "redacted"
		}
		if clean.Config.OIDCClientAssertion.PrivateKey != nil &&
			clean.Config.OIDCClientAssertion.PrivateKey.PemKey != "" {
			clean.Config.OIDCClientAssertion.PrivateKey.PemKey = "redacted"
		}
	}

	return clean
}

// TokenLocalityIsGlobal returns whether the auth method creates global ACL
// tokens or not.
func (a *ACLAuthMethod) TokenLocalityIsGlobal() bool {
	return a.TokenLocality == ACLAuthMethodTokenLocalityGlobal
}

// ACLAuthMethodConfig is used to store configuration of an auth method
type ACLAuthMethodConfig struct {
	// A list of PEM-encoded public keys to use to authenticate signatures
	// locally
	JWTValidationPubKeys []string

	// JSON Web Key Sets url for authenticating signatures
	JWKSURL string

	// The OIDC Discovery URL, without any .well-known component (base path)
	OIDCDiscoveryURL string

	// The OAuth Client ID configured with the OIDC provider
	OIDCClientID string

	// The OAuth Client Secret configured with the OIDC provider
	OIDCClientSecret string

	// Optional client assertion ("private key jwt") config
	OIDCClientAssertion *OIDCClientAssertion

	// Enable PKCE challenge verification
	OIDCEnablePKCE bool

	// Disable claims from the OIDC UserInfo endpoint
	OIDCDisableUserInfo bool

	// List of OIDC scopes
	OIDCScopes []string

	// List of auth claims that are valid for login
	BoundAudiences []string

	// The value against which to match the iss claim in a JWT
	BoundIssuer []string

	// A list of allowed values for redirect_uri
	AllowedRedirectURIs []string

	// PEM encoded CA certs for use by the TLS client used to talk with the
	// OIDC Discovery URL.
	DiscoveryCaPem []string

	// PEM encoded CA cert for use by the TLS client used to talk with the JWKS
	// URL
	JWKSCACert string

	// A list of supported signing algorithms
	SigningAlgs []string

	// Duration in seconds of leeway when validating expiration of a token to
	// account for clock skew
	ExpirationLeeway time.Duration

	// Duration in seconds of leeway when validating not before values of a
	// token to account for clock skew.
	NotBeforeLeeway time.Duration

	// Duration in seconds of leeway when validating all claims to account for
	// clock skew.
	ClockSkewLeeway time.Duration

	// Mappings of claims (key) that will be copied to a metadata field
	// (value).
	ClaimMappings     map[string]string
	ListClaimMappings map[string]string

	// Enables logging of claims and binding-rule evaluations when
	// debug level logging is enabled.
	VerboseLogging bool
}

func (a *ACLAuthMethodConfig) Canonicalize() {
	if a == nil {
		return
	}
	if a.OIDCClientAssertion != nil {
		// client assertions inherit certain values from auth method
		if len(a.OIDCClientAssertion.Audience) == 0 {
			a.OIDCClientAssertion.Audience = []string{a.OIDCDiscoveryURL}
		}
		// the client assertion inherits the client secret,
		// in case KeySource = "client_secret"
		a.OIDCClientAssertion.ClientSecret = a.OIDCClientSecret
		a.OIDCClientAssertion.Canonicalize()
	}
}

func (a *ACLAuthMethodConfig) Validate(methodType string) error {
	if a == nil {
		return errors.New("missing auth method Config")
	}
	mErr := &multierror.Error{}

	switch methodType {
	case ACLAuthMethodTypeOIDC:
		if a.OIDCDiscoveryURL == "" {
			mErr = multierror.Append(mErr, errors.New("missing OIDCDiscoveryURL"))
		}
		if a.OIDCClientID == "" {
			mErr = multierror.Append(mErr, errors.New("missing OIDCClientID"))
		}
		if err := a.OIDCClientAssertion.Validate(); err != nil {
			mErr = multierror.Append(mErr, fmt.Errorf("invalid client assertion config: %w", err))
		}

	case ACLAuthMethodTypeJWT:
		if a.OIDCDiscoveryURL == "" && a.JWKSURL == "" && len(a.JWTValidationPubKeys) == 0 {
			mErr = multierror.Append(mErr, errors.New(
				"JWT auth method requires either OIDCDiscoveryURL, or JWKS URL, or JWTValidationPubKeys set"),
			)
		}
	}

	return helper.FlattenMultierror(mErr)
}

func (a *ACLAuthMethodConfig) Copy() *ACLAuthMethodConfig {
	if a == nil {
		return nil
	}

	c := new(ACLAuthMethodConfig)
	*c = *a

	c.JWTValidationPubKeys = slices.Clone(a.JWTValidationPubKeys)
	c.OIDCScopes = slices.Clone(a.OIDCScopes)
	c.BoundAudiences = slices.Clone(a.BoundAudiences)
	c.BoundIssuer = slices.Clone(a.BoundIssuer)
	c.AllowedRedirectURIs = slices.Clone(a.AllowedRedirectURIs)
	c.DiscoveryCaPem = slices.Clone(a.DiscoveryCaPem)
	c.SigningAlgs = slices.Clone(a.SigningAlgs)
	c.OIDCClientAssertion = a.OIDCClientAssertion.Copy()

	return c
}

// MarshalJSON implements the json.Marshaler interface and allows
// time.Diration fields to be marshaled correctly.
func (a *ACLAuthMethodConfig) MarshalJSON() ([]byte, error) {
	type Alias ACLAuthMethodConfig
	exported := &struct {
		ExpirationLeeway string
		NotBeforeLeeway  string
		ClockSkewLeeway  string
		*Alias
	}{
		ExpirationLeeway: a.ExpirationLeeway.String(),
		NotBeforeLeeway:  a.NotBeforeLeeway.String(),
		ClockSkewLeeway:  a.ClockSkewLeeway.String(),
		Alias:            (*Alias)(a),
	}
	if a.ExpirationLeeway == 0 {
		exported.ExpirationLeeway = ""
	}
	if a.NotBeforeLeeway == 0 {
		exported.NotBeforeLeeway = ""
	}
	if a.ClockSkewLeeway == 0 {
		exported.ClockSkewLeeway = ""
	}
	return json.Marshal(exported)
}

// UnmarshalJSON implements the json.Unmarshaler interface and allows
// time.Duration fields to be unmarshalled correctly.
func (a *ACLAuthMethodConfig) UnmarshalJSON(data []byte) (err error) {
	type Alias ACLAuthMethodConfig
	aux := &struct {
		ExpirationLeeway any
		NotBeforeLeeway  any
		ClockSkewLeeway  any
		*Alias
	}{
		Alias: (*Alias)(a),
	}
	if err = json.Unmarshal(data, &aux); err != nil {
		return err
	}
	if aux.ExpirationLeeway != nil {
		switch v := aux.ExpirationLeeway.(type) {
		case string:
			if v != "" {
				if a.ExpirationLeeway, err = time.ParseDuration(v); err != nil {
					return err
				}
			}
		case float64:
			a.ExpirationLeeway = time.Duration(v)
		default:
			return fmt.Errorf("unexpected ExpirationLeeway type: %v", v)
		}
	}
	if aux.NotBeforeLeeway != nil {
		switch v := aux.NotBeforeLeeway.(type) {
		case string:
			if v != "" {
				if a.NotBeforeLeeway, err = time.ParseDuration(v); err != nil {
					return err
				}
			}
		case float64:
			a.NotBeforeLeeway = time.Duration(v)
		default:
			return fmt.Errorf("unexpected NotBeforeLeeway type: %v", v)
		}
	}
	if aux.ClockSkewLeeway != nil {
		switch v := aux.ClockSkewLeeway.(type) {
		case string:
			if v != "" {
				if a.ClockSkewLeeway, err = time.ParseDuration(v); err != nil {
					return err
				}
			}
		case float64:
			a.ClockSkewLeeway = time.Duration(v)
		default:
			return fmt.Errorf("unexpected ClockSkewLeeway type: %v", v)
		}
	}
	return nil
}

type OIDCClientAssertionKeySource string

const (
	OIDCKeySourceNomad        OIDCClientAssertionKeySource = "nomad"
	OIDCKeySourceClientSecret OIDCClientAssertionKeySource = "client_secret"
	OIDCKeySourcePrivateKey   OIDCClientAssertionKeySource = "private_key"
)

// OIDCClientAssertion (a.k.a private_key_jwt) is used to send
// a client_assertion along with an OIDC token request.
// See api.OIDCClientAssertion for full field descriptions.
type OIDCClientAssertion struct {
	KeySource    OIDCClientAssertionKeySource
	Audience     []string
	PrivateKey   *OIDCClientAssertionKey
	ExtraHeaders map[string]string
	KeyAlgorithm string
	// ClientSecret here is not part of the public api; it's inherited from the
	// parent ACLAuthMethodConfig struct via ACLAuthMethodConfig.Canonicalize.
	// It's exported mainly so that it gets saved across msgpack in raft state.
	ClientSecret string
}

func (c *OIDCClientAssertion) Copy() *OIDCClientAssertion {
	if c == nil {
		return nil
	}
	n := new(OIDCClientAssertion)
	*n = *c
	n.Audience = slices.Clone(c.Audience)
	n.PrivateKey = c.PrivateKey.Copy()
	n.ExtraHeaders = maps.Clone(c.ExtraHeaders)
	return n
}

func (c *OIDCClientAssertion) Canonicalize() {
	if c == nil {
		return
	}
	// default KeyAlgorithm to "RS256" for nomad and user keys, "HS256" for client_secret
	if c.KeyAlgorithm == "" {
		switch c.KeySource {
		case OIDCKeySourceClientSecret:
			c.KeyAlgorithm = "HS256"
		case OIDCKeySourceNomad, OIDCKeySourcePrivateKey:
			c.KeyAlgorithm = "RS256"
		}
	}
	c.PrivateKey.Canonicalize()
}

func (c *OIDCClientAssertion) IsSet() bool {
	return c != nil && c.KeySource != ""
}

func (c *OIDCClientAssertion) Validate() error {
	if c == nil {
		return nil
	}
	if len(c.Audience) == 0 || c.Audience[0] == "" {
		return errors.New("missing Audience")
	}
	switch c.KeySource {
	case OIDCKeySourceNomad:
	case OIDCKeySourcePrivateKey:
		if c.PrivateKey == nil {
			return errors.New("PrivateKey is required for `private_key` KeySource")
		}
		if err := c.PrivateKey.Validate(); err != nil {
			return fmt.Errorf("invalid PrivateKey: %w", err)
		}
	case OIDCKeySourceClientSecret:
		if c.ClientSecret == "" {
			return errors.New("OIDCClientSecret is required for `client_secret` KeySource")
		}
	default:
		return fmt.Errorf("invalid KeySource %q", c.KeySource)
	}
	return nil
}

type OIDCClientAssertionKeyIDHeader string

const (
	OIDCClientAssertionHeaderKid     OIDCClientAssertionKeyIDHeader = "kid"
	OIDCClientAssertionHeaderX5t     OIDCClientAssertionKeyIDHeader = "x5t"
	OIDCClientAssertionHeaderX5tS256 OIDCClientAssertionKeyIDHeader = "x5t#S256"
)

// OIDCClientAssertionKey contains key material provided by users for Nomad
// to use to sign the private key JWT.
// See api.OIDCClientAssertionKey for full field descriptions.
type OIDCClientAssertionKey struct {
	PemKey     string
	PemKeyFile string

	KeyIDHeader OIDCClientAssertionKeyIDHeader
	PemCert     string
	PemCertFile string
	KeyID       string
}

func (k *OIDCClientAssertionKey) Copy() *OIDCClientAssertionKey {
	if k == nil {
		return nil
	}
	n := new(OIDCClientAssertionKey)
	*n = *k
	return n
}

func (k *OIDCClientAssertionKey) Canonicalize() {
	if k == nil {
		return
	}
	if k.KeyIDHeader == "" {
		if k.KeyID != "" {
			k.KeyIDHeader = OIDCClientAssertionHeaderKid
		}
		if k.PemCert != "" || k.PemCertFile != "" {
			k.KeyIDHeader = OIDCClientAssertionHeaderX5tS256
		}
	}
}

var (
	ErrMissingClientAssertionKey      = errors.New("missing PemKey or PemKeyFile")
	ErrAmbiguousClientAssertionKey    = errors.New("require only one of PemKey or PemKeyFile")
	ErrMissingClientAssertionKeyID    = errors.New("missing PemCert, PemCertFile, or KeyID")
	ErrAmbiguousClientAssertionKeyID  = errors.New("require only one of PemCert, PemCertFile, or KeyID")
	ErrInvalidClientAssertionKeyPath  = errors.New("invalid PemKeyFile")
	ErrInvalidClientAssertionCertPath = errors.New("invalid PemCertFile")
	ErrInvalidKeyIDHeader             = errors.New("invalid KeyIDHeader")
)

// Validate ensures that one Key and one Cert or KeyID are provided,
// and that the key ID header is valid for the provided KeyID or cert.
func (k *OIDCClientAssertionKey) Validate() error {
	if k == nil {
		return nil
	}

	// mutually exclusive key fields
	// must have key file or base64, but not both
	if k.PemKey == "" && k.PemKeyFile == "" {
		return ErrMissingClientAssertionKey
	}
	if k.PemKey != "" && k.PemKeyFile != "" {
		return ErrAmbiguousClientAssertionKey
	}
	if k.PemKeyFile != "" {
		if !path.IsAbs(k.PemKeyFile) {
			return fmt.Errorf("%w: must be absolute; got: %s", ErrInvalidClientAssertionKeyPath, k.PemKeyFile)
		}
	}

	// mutually exclusive cert fields
	// must have exactly one of: cert file or base64, or keyid
	if k.PemCert == "" && k.PemCertFile == "" && k.KeyID == "" {
		return ErrMissingClientAssertionKeyID
	}
	if k.PemCert != "" && (k.PemCertFile != "" || k.KeyID != "") {
		return ErrAmbiguousClientAssertionKeyID
	}
	if k.PemCertFile != "" && (k.PemCert != "" || k.KeyID != "") {
		return ErrAmbiguousClientAssertionKeyID
	}
	if k.KeyID != "" && (k.PemCert != "" || k.PemCertFile != "") {
		return ErrAmbiguousClientAssertionKeyID
	}
	if k.PemCertFile != "" {
		if !path.IsAbs(k.PemCertFile) {
			return fmt.Errorf("%w: must be absolute; got: %s", ErrInvalidClientAssertionCertPath, k.PemCertFile)
		}
	}

	// only allow certain key id headers
	// only "kid" for KeyID
	if k.KeyID != "" && k.KeyIDHeader != OIDCClientAssertionHeaderKid {
		return fmt.Errorf("%w; key header for key ID must be %q",
			ErrInvalidKeyIDHeader, OIDCClientAssertionHeaderKid)
	}
	// only "x5t*" for certs
	if k.PemCert != "" || k.PemCertFile != "" {
		if k.KeyIDHeader != OIDCClientAssertionHeaderX5t && k.KeyIDHeader != OIDCClientAssertionHeaderX5tS256 {
			return fmt.Errorf("%w; certificate-derived key header must be one of: %q, %q",
				ErrInvalidKeyIDHeader, OIDCClientAssertionHeaderX5tS256, OIDCClientAssertionHeaderX5t)
		}
	}

	return nil
}

// ACLAuthClaims is the claim mapping of the OIDC auth method in a format that
// can be used with go-bexpr. This structure is used during rule binding
// evaluation.
type ACLAuthClaims struct {
	Value map[string]string   `bexpr:"value"`
	List  map[string][]string `bexpr:"list"`
}

// ACLAuthMethodStub is used for listing ACL auth methods
type ACLAuthMethodStub struct {
	Name    string
	Type    string
	Default bool

	// Hash is the hashed value of the auth-method and is generated using all
	// fields from the full object except the create and modify times and
	// indexes.
	Hash []byte

	CreateIndex uint64
	ModifyIndex uint64
}

// ACLAuthMethodListRequest is used to list auth methods
type ACLAuthMethodListRequest struct {
	QueryOptions
}

// ACLAuthMethodListResponse is used to list auth methods
type ACLAuthMethodListResponse struct {
	AuthMethods []*ACLAuthMethodStub
	QueryMeta
}

// ACLAuthMethodGetRequest is used to query a specific auth method
type ACLAuthMethodGetRequest struct {
	MethodName string
	QueryOptions
}

// ACLAuthMethodGetResponse is used to return a single auth method
type ACLAuthMethodGetResponse struct {
	AuthMethod *ACLAuthMethod
	QueryMeta
}

// ACLAuthMethodsGetRequest is used to query a set of auth methods
type ACLAuthMethodsGetRequest struct {
	Names []string
	QueryOptions
}

// ACLAuthMethodsGetResponse is used to return a set of auth methods
type ACLAuthMethodsGetResponse struct {
	AuthMethods map[string]*ACLAuthMethod
	QueryMeta
}

// ACLAuthMethodUpsertRequest is used to upsert a set of auth methods
type ACLAuthMethodUpsertRequest struct {
	AuthMethods []*ACLAuthMethod
	WriteRequest
}

// ACLAuthMethodUpsertResponse is a response of the upsert ACL auth methods
// operation
type ACLAuthMethodUpsertResponse struct {
	AuthMethods []*ACLAuthMethod
	WriteMeta
}

// ACLAuthMethodDeleteRequest is used to delete a set of auth methods by their
// name
type ACLAuthMethodDeleteRequest struct {
	Names []string
	WriteRequest
}

// ACLAuthMethodDeleteResponse is a response of the delete ACL auth methods
// operation
type ACLAuthMethodDeleteResponse struct {
	WriteMeta
}

type ACLWhoAmIResponse struct {
	Identity *AuthenticatedIdentity
	QueryMeta
}

// ACLBindingRule contains a direct relation to an ACLAuthMethod and represents
// a rule to apply when logging in via the named AuthMethod. This allows the
// transformation of OIDC provider claims, to Nomad based ACL concepts such as
// ACL Roles and Policies.
type ACLBindingRule struct {

	// ID is an internally generated UUID for this rule and is controlled by
	// Nomad.
	ID string

	// Description is a human-readable, operator set description that can
	// provide additional context about the binding role. This is an
	// operational field.
	Description string

	// AuthMethod is the name of the auth method for which this rule applies
	// to. This is required and the method must exist within state before the
	// cluster administrator can create the rule.
	AuthMethod string

	// Selector is an expression that matches against verified identity
	// attributes returned from the auth method during login. This is optional
	// and when not set, provides a catch-all rule.
	Selector string

	// BindType adjusts how this binding rule is applied at login time. The
	// valid values are ACLBindingRuleBindTypeRole,
	// ACLBindingRuleBindTypePolicy, and ACLBindingRuleBindTypeManagement.
	BindType string

	// BindName is the target of the binding. Can be lightly templated using
	// HIL ${foo} syntax from available field names. How it is used depends
	// upon the BindType.
	BindName string

	// Hash is the hashed value of the binding rule and is generated using all
	// fields from the full object except the create and modify times and
	// indexes.
	Hash []byte

	CreateTime  time.Time
	ModifyTime  time.Time
	CreateIndex uint64
	ModifyIndex uint64
}

const (
	// ACLBindingRuleBindTypeRole is the ACL binding rule bind type that only
	// allows the binding rule to function if a role exists at login-time. The
	// role will be specified within the ACLBindingRule.BindName parameter, and
	// will identify whether this is an ID or Name.
	ACLBindingRuleBindTypeRole = "role"

	// ACLBindingRuleBindTypePolicy is the ACL binding rule bind type that
	// assigns a policy to the generate ACL token. The role will be specified
	// within the ACLBindingRule.BindName parameter, and will be the policy
	// name.
	ACLBindingRuleBindTypePolicy = "policy"

	// ACLBindingRuleBindTypeManagement is the ACL binding rule bind type that
	// will generate management ACL tokens when matched.
	ACLBindingRuleBindTypeManagement = "management"
)

// Canonicalize performs basic canonicalization on the ACL token object. It is
// important for callers to understand certain fields such as ID are set if it
// is empty, so copies should be taken if needed before calling this function.
func (a *ACLBindingRule) Canonicalize() {

	now := time.Now().UTC()

	// If the ID is empty, it means this is creation of a new binding rule,
	// therefore we need to generate base information.
	if a.ID == "" {
		a.ID = uuid.Generate()
		a.CreateTime = now
	}

	// The fact this function is being called indicates we are attempting an
	// upsert into state. Therefore, update the modify time.
	a.ModifyTime = now
}

// Validate ensures the ACL binding rule contains valid information which meets
// Nomad's internal requirements.
func (a *ACLBindingRule) Validate() error {

	var mErr multierror.Error

	if a.AuthMethod == "" {
		mErr.Errors = append(mErr.Errors, errors.New("auth method is missing"))
	}
	if len(a.Description) > maxACLBindingRuleDescriptionLength {
		mErr.Errors = append(mErr.Errors, fmt.Errorf("description longer than %d", maxACLRoleDescriptionLength))
	}

	// Depending on the bind type, we have some specific validation. Catching
	// the empty string also provides easier to understand feedback to the
	// user.
	switch a.BindType {
	case "":
		mErr.Errors = append(mErr.Errors, errors.New("bind type is missing"))
	case ACLBindingRuleBindTypeRole, ACLBindingRuleBindTypePolicy:
		if a.BindName == "" {
			mErr.Errors = append(mErr.Errors, errors.New("bind name is missing"))
		}
	case ACLBindingRuleBindTypeManagement:
		if a.BindName != "" {
			mErr.Errors = append(mErr.Errors, errors.New("bind name should be empty"))
		}
	default:
		mErr.Errors = append(mErr.Errors, fmt.Errorf("unsupported bind type: %q", a.BindType))
	}

	// If there is a selector configured, ensure that go-bexpr can parse this.
	// Otherwise, the user will get an ambiguous failure when attempting to
	// login.
	if a.Selector != "" {
		if _, err := bexpr.CreateEvaluator(a.Selector, nil); err != nil {
			mErr.Errors = append(mErr.Errors, fmt.Errorf("selector is invalid: %v", err))
		}
	}

	return mErr.ErrorOrNil()
}

// Merge merges binding rule a with b. It sets all required empty fields of rule
// a to corresponding values of rule b, except for "ID" which must be provided.
func (a *ACLBindingRule) Merge(b *ACLBindingRule) {
	a.BindName = helper.Merge(a.BindName, b.BindName)
	a.BindType = helper.Merge(a.BindType, b.BindType)
	a.AuthMethod = helper.Merge(a.AuthMethod, b.AuthMethod)
}

// SetHash is used to compute and set the hash of the ACL binding rule. This
// should be called every and each time a user specified field on the method is
// changed before updating the Nomad state store.
func (a *ACLBindingRule) SetHash() []byte {

	// Initialize a 256bit Blake2 hash (32 bytes).
	hash, err := blake2b.New256(nil)
	if err != nil {
		panic(err)
	}

	_, _ = hash.Write([]byte(a.ID))
	_, _ = hash.Write([]byte(a.Description))
	_, _ = hash.Write([]byte(a.AuthMethod))
	_, _ = hash.Write([]byte(a.Selector))
	_, _ = hash.Write([]byte(a.BindType))
	_, _ = hash.Write([]byte(a.BindName))

	// Finalize the hash.
	hashVal := hash.Sum(nil)

	// Set and return the hash.
	a.Hash = hashVal
	return hashVal
}

// Equal performs an equality check on the two ACL binding rules. It handles
// nil objects.
func (a *ACLBindingRule) Equal(other *ACLBindingRule) bool {
	if a == nil || other == nil {
		return a == other
	}
	if len(a.Hash) == 0 {
		a.SetHash()
	}
	if len(other.Hash) == 0 {
		other.SetHash()
	}
	return bytes.Equal(a.Hash, other.Hash)
}

// Copy creates a deep copy of the ACL binding rule. This copy can then be
// safely modified. It handles nil objects.
func (a *ACLBindingRule) Copy() *ACLBindingRule {
	if a == nil {
		return nil
	}

	c := new(ACLBindingRule)
	*c = *a
	c.Hash = slices.Clone(a.Hash)

	return c
}

// Stub converts the ACLBindingRule object into a ACLBindingRuleListStub
// object.
func (a *ACLBindingRule) Stub() *ACLBindingRuleListStub {
	return &ACLBindingRuleListStub{
		ID:          a.ID,
		Description: a.Description,
		AuthMethod:  a.AuthMethod,
		Hash:        a.Hash,
		CreateIndex: a.CreateIndex,
		ModifyIndex: a.ModifyIndex,
	}
}

// ACLBindingRuleListStub is the stub object returned when performing a listing
// of ACL binding rules.
type ACLBindingRuleListStub struct {

	// ID is an internally generated UUID for this role and is controlled by
	// Nomad.
	ID string

	// Description is a human-readable, operator set description that can
	// provide additional context about the binding role. This is an
	// operational field.
	Description string

	// AuthMethod is the name of the auth method for which this rule applies
	// to. This is required and the method must exist within state before the
	// cluster administrator can create the rule.
	AuthMethod string

	// Hash is the hashed value of the binding rule and is generated using all
	// fields from the full object except the create and modify times and
	// indexes.
	Hash []byte

	CreateIndex uint64
	ModifyIndex uint64
}

// ACLBindingRulesUpsertRequest is used to upsert a set of ACL binding rules.
type ACLBindingRulesUpsertRequest struct {
	ACLBindingRules []*ACLBindingRule

	// AllowMissingAuthMethods skips the ACL binding rule auth method link
	// verification and is used by the replication process. The replication
	// cannot ensure auth methods are present before ACL binding rules are
	// replicated.
	AllowMissingAuthMethods bool

	WriteRequest
}

// ACLBindingRulesUpsertResponse is a response of the upsert ACL binding rules
// operation.
type ACLBindingRulesUpsertResponse struct {
	ACLBindingRules []*ACLBindingRule
	WriteMeta
}

// ACLBindingRulesDeleteRequest is used to delete a set of ACL binding rules by
// their IDs.
type ACLBindingRulesDeleteRequest struct {
	ACLBindingRuleIDs []string
	WriteRequest
}

// ACLBindingRulesDeleteResponse is a response of the delete ACL binding rules
// operation.
type ACLBindingRulesDeleteResponse struct {
	WriteMeta
}

// ACLBindingRulesListRequest  is the request object when performing ACL
// binding rules listings.
type ACLBindingRulesListRequest struct {
	QueryOptions
}

// ACLBindingRulesListResponse is the response object when performing ACL
// binding rule listings.
type ACLBindingRulesListResponse struct {
	ACLBindingRules []*ACLBindingRuleListStub
	QueryMeta
}

// ACLBindingRulesRequest is the request object when performing a lookup of
// multiple binding rules by the ID.
type ACLBindingRulesRequest struct {
	ACLBindingRuleIDs []string
	QueryOptions
}

// ACLBindingRulesResponse is the response object when performing a lookup of
// multiple binding rules by their IDs.
type ACLBindingRulesResponse struct {
	ACLBindingRules map[string]*ACLBindingRule
	QueryMeta
}

// ACLBindingRuleRequest is the request object to perform a lookup of an ACL
// binding rule using a specific ID.
type ACLBindingRuleRequest struct {
	ACLBindingRuleID string
	QueryOptions
}

// ACLBindingRuleResponse is the response object when performing a lookup of an
// ACL binding rule matching a specific ID.
type ACLBindingRuleResponse struct {
	ACLBindingRule *ACLBindingRule
	QueryMeta
}

// ACLOIDCAuthURLRequest is the request to make when starting the OIDC
// authentication login flow.
type ACLOIDCAuthURLRequest struct {

	// AuthMethodName is the OIDC auth-method to use. This is a required
	// parameter.
	AuthMethodName string

	// RedirectURI is the URL that authorization should redirect to. This is a
	// required parameter.
	RedirectURI string

	// ClientNonce is a randomly generated string to prevent replay attacks. It
	// is up to the client to generate this and Go integrations should use the
	// oidc.NewID function within the hashicorp/cap library. This must then be
	// passed back to ACLOIDCCompleteAuthRequest. This is a required parameter.
	ClientNonce string

	// WriteRequest is used due to the requirement by the RPC forwarding
	// mechanism. This request doesn't write anything to Nomad's internal
	// state.
	WriteRequest
}

// Validate ensures the request object contains all the required fields in
// order to start the OIDC authentication flow.
func (a *ACLOIDCAuthURLRequest) Validate() error {

	var mErr multierror.Error

	if a.AuthMethodName == "" {
		mErr.Errors = append(mErr.Errors, errors.New("missing auth method name"))
	}
	if a.ClientNonce == "" {
		mErr.Errors = append(mErr.Errors, errors.New("missing client nonce"))
	}
	if a.RedirectURI == "" {
		mErr.Errors = append(mErr.Errors, errors.New("missing redirect URI"))
	}
	return mErr.ErrorOrNil()
}

// ACLOIDCAuthURLResponse is the response when starting the OIDC authentication
// login flow.
type ACLOIDCAuthURLResponse struct {

	// AuthURL is URL to begin authorization and is where the user logging in
	// should go.
	AuthURL string
}

// ACLOIDCCompleteAuthRequest is the request object to begin completing the
// OIDC auth cycle after receiving the callback from the OIDC provider.
type ACLOIDCCompleteAuthRequest struct {

	// AuthMethodName is the name of the auth method being used to login via
	// OIDC. This will match ACLOIDCAuthURLRequest.AuthMethodName. This is a
	// required parameter.
	AuthMethodName string

	// ClientNonce, State, and Code are provided from the parameters given to
	// the redirect URL. These are all required parameters.
	ClientNonce string
	State       string
	Code        string
	Iss         string

	// RedirectURI is the URL that authorization should redirect to. This is a
	// required parameter.
	RedirectURI string

	WriteRequest
}

// Validate ensures the request object contains all the required fields in
// order to complete the OIDC authentication flow.
func (a *ACLOIDCCompleteAuthRequest) Validate() error {

	var mErr multierror.Error

	if a.AuthMethodName == "" {
		mErr.Errors = append(mErr.Errors, errors.New("missing auth method name"))
	}
	if a.ClientNonce == "" {
		mErr.Errors = append(mErr.Errors, errors.New("missing client nonce"))
	}
	if a.State == "" {
		mErr.Errors = append(mErr.Errors, errors.New("missing state"))
	}
	if a.Code == "" {
		mErr.Errors = append(mErr.Errors, errors.New("missing code"))
	}
	if a.RedirectURI == "" {
		mErr.Errors = append(mErr.Errors, errors.New("missing redirect URI"))
	}
	return mErr.ErrorOrNil()
}

// ACLLoginResponse is the response when the auth flow has been
// completed successfully.
type ACLLoginResponse struct {
	ACLToken *ACLToken
	WriteMeta
}

// ACLLoginRequest is the request object to begin auth with an external
// token provider.
type ACLLoginRequest struct {

	// AuthMethodName is the name of the auth method being used to login. This
	// is a required parameter.
	AuthMethodName string

	// LoginToken is the 3rd party token that we use to exchange for Nomad ACL
	// Token in order to authenticate. This is a required parameter.
	LoginToken string

	WriteRequest
}

// Validate ensures the request object contains all the required fields in
// order to complete the authentication flow.
func (a *ACLLoginRequest) Validate() error {

	var mErr multierror.Error

	if a.AuthMethodName == "" {
		mErr.Errors = append(mErr.Errors, errors.New("missing auth method name"))
	}
	if a.LoginToken == "" {
		mErr.Errors = append(mErr.Errors, errors.New("missing login token"))
	}
	return mErr.ErrorOrNil()
}

// ACLCreateClientIntroductionTokenRequest is the request object used within the ACL
// client introduction RPC handler. This is used to generate a JWT token that
// can be used to register a new client node into the cluster.
type ACLCreateClientIntroductionTokenRequest struct {

	// TTL is the requested TTL for the identity token. This is an optional
	// parameter and if not set, defaults to the server defined default TTL.
	TTL time.Duration

	// NodeName is the name of the node that is being introduced. This is added
	// to the token as a claim when present, but is optional.
	NodeName string

	// NodePool is the name of the node pool that this node belongs to. This is
	// an optional parameter, and if not set, defaults to "default".
	NodePool string

	WriteRequest
}

// Canonicalize performs basic canonicalization on the ACL client introduction
// request object. This should be called within the RPC handler, to ensure a
// consistent experience for the user across CLI and HTTP API calls.
func (a *ACLCreateClientIntroductionTokenRequest) Canonicalize() {
	if a.NodePool == "" {
		a.NodePool = NodePoolDefault
	}
}

// IdentityTTL returns the TTL that should be used for the identity token based
// on the request and server defaults.
func (a *ACLCreateClientIntroductionTokenRequest) IdentityTTL(
	logger hclog.Logger,
	serverDefault, serverMax time.Duration) time.Duration {

	// If the user has not provided a TTL, we use the server default.
	if a.TTL == 0 {
		return serverDefault
	}

	// If the user has requested a TTL that is greater than the server defined
	// maximum, we log a warning and use the server maximum instead. It is
	// possible to return an error here, but providing a ceiling provides a
	// smoother UX.
	if a.TTL > serverMax {
		logger.Warn(
			"node introduction identity TTL request exceeds server maximum, using server maximum",
			"requested_ttl", a.TTL, "server_max_ttl", serverMax,
		)
		return serverMax
	}
	return a.TTL
}

// MarshalJSON implements the json.Marshaler interface and allows
// ACLCreateClientIntroductionTokenRequest.TTL to be marshaled correctly.
func (a *ACLCreateClientIntroductionTokenRequest) MarshalJSON() ([]byte, error) {
	type Alias ACLCreateClientIntroductionTokenRequest
	exported := &struct {
		TTL string
		*Alias
	}{
		TTL:   a.TTL.String(),
		Alias: (*Alias)(a),
	}
	if a.TTL == 0 {
		exported.TTL = ""
	}
	return json.Marshal(exported)
}

// UnmarshalJSON implements the json.Unmarshaler interface and allows
// ACLCreateClientIntroductionTokenRequest.TTL to be unmarshalled correctly.
func (a *ACLCreateClientIntroductionTokenRequest) UnmarshalJSON(data []byte) (err error) {
	type Alias ACLCreateClientIntroductionTokenRequest
	aux := &struct {
		TTL interface{}
		*Alias
	}{
		Alias: (*Alias)(a),
	}
	if err = json.Unmarshal(data, &aux); err != nil {
		return err
	}
	if aux.TTL != nil {
		switch v := aux.TTL.(type) {
		case string:
			if v != "" {
				if a.TTL, err = time.ParseDuration(v); err != nil {
					return err
				}
			}
		case float64:
			a.TTL = time.Duration(v)
		default:
			return fmt.Errorf("unexpected TTL type: %v", v)
		}
	}
	return nil
}

// ACLCreateClientIntroductionTokenResponse is the response object used within the ACL
// client introduction RPC handler.
type ACLCreateClientIntroductionTokenResponse struct {

	// JWT is the signed identity token that can be used as an introduction
	// token for a new client node to register with the Nomad cluster.
	JWT string
}
