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

package vault

import (
	"errors"
	"fmt"
	"strings"
	"time"

	multierror "github.com/hashicorp/go-multierror"
	"github.com/hashicorp/go-secure-stdlib/parseutil"
	"github.com/hashicorp/hcl"
	"github.com/hashicorp/hcl/hcl/ast"
	"github.com/hashicorp/vault/helper/identity"
	"github.com/hashicorp/vault/helper/namespace"
	"github.com/hashicorp/vault/sdk/helper/hclutil"
	"github.com/hashicorp/vault/sdk/helper/identitytpl"
	"github.com/hashicorp/vault/sdk/logical"
	"github.com/mitchellh/copystructure"
)

const (
	DenyCapability   = "deny"
	CreateCapability = "create"
	ReadCapability   = "read"
	UpdateCapability = "update"
	DeleteCapability = "delete"
	ListCapability   = "list"
	SudoCapability   = "sudo"
	RootCapability   = "root"
	PatchCapability  = "patch"

	// Backwards compatibility
	OldDenyPathPolicy  = "deny"
	OldReadPathPolicy  = "read"
	OldWritePathPolicy = "write"
	OldSudoPathPolicy  = "sudo"
)

const (
	DenyCapabilityInt uint32 = 1 << iota
	CreateCapabilityInt
	ReadCapabilityInt
	UpdateCapabilityInt
	DeleteCapabilityInt
	ListCapabilityInt
	SudoCapabilityInt
	PatchCapabilityInt
)

// Error constants for testing
const (
	// ControlledCapabilityPolicySubsetError is thrown when a control group's controlled capabilities
	// are not a subset of the policy's capabilities.
	ControlledCapabilityPolicySubsetError = "control group factor capabilities must be a subset of the policy's capabilities"
)

type PolicyType uint32

const (
	PolicyTypeACL PolicyType = iota
	PolicyTypeRGP
	PolicyTypeEGP

	// Triggers a lookup in the map to figure out if ACL or RGP
	PolicyTypeToken
)

func (p PolicyType) String() string {
	switch p {
	case PolicyTypeACL:
		return "acl"
	case PolicyTypeRGP:
		return "rgp"
	case PolicyTypeEGP:
		return "egp"
	}

	return ""
}

var cap2Int = map[string]uint32{
	DenyCapability:   DenyCapabilityInt,
	CreateCapability: CreateCapabilityInt,
	ReadCapability:   ReadCapabilityInt,
	UpdateCapability: UpdateCapabilityInt,
	DeleteCapability: DeleteCapabilityInt,
	ListCapability:   ListCapabilityInt,
	SudoCapability:   SudoCapabilityInt,
	PatchCapability:  PatchCapabilityInt,
}

type egpPath struct {
	Path string `json:"path"`
	Glob bool   `json:"glob"`
}

// Policy is used to represent the policy specified by an ACL configuration.
type Policy struct {
	sentinelPolicy
	Name      string       `hcl:"name"`
	Paths     []*PathRules `hcl:"-"`
	Raw       string
	Type      PolicyType
	Templated bool
	namespace *namespace.Namespace
}

// ShallowClone returns a shallow clone of the policy. This should not be used
// if any of the reference-typed fields are going to be modified
func (p *Policy) ShallowClone() *Policy {
	return &Policy{
		sentinelPolicy: p.sentinelPolicy,
		Name:           p.Name,
		Paths:          p.Paths,
		Raw:            p.Raw,
		Type:           p.Type,
		Templated:      p.Templated,
		namespace:      p.namespace,
	}
}

// PathRules represents a policy for a path in the namespace.
type PathRules struct {
	Path                string
	Policy              string
	Permissions         *ACLPermissions
	IsPrefix            bool
	HasSegmentWildcards bool
	Capabilities        []string

	// These keys are used at the top level to make the HCL nicer; we store in
	// the ACLPermissions object though
	MinWrappingTTLHCL     interface{}              `hcl:"min_wrapping_ttl"`
	MaxWrappingTTLHCL     interface{}              `hcl:"max_wrapping_ttl"`
	AllowedParametersHCL  map[string][]interface{} `hcl:"allowed_parameters"`
	DeniedParametersHCL   map[string][]interface{} `hcl:"denied_parameters"`
	RequiredParametersHCL []string                 `hcl:"required_parameters"`
	MFAMethodsHCL         []string                 `hcl:"mfa_methods"`
	ControlGroupHCL       *ControlGroupHCL         `hcl:"control_group"`
}

type ControlGroupHCL struct {
	TTL     interface{}                    `hcl:"ttl"`
	Factors map[string]*ControlGroupFactor `hcl:"factor"`
}

type ControlGroup struct {
	TTL     time.Duration
	Factors []*ControlGroupFactor
}

func (c *ControlGroup) Clone() (*ControlGroup, error) {
	clonedControlGroup, err := copystructure.Copy(c)
	if err != nil {
		return nil, err
	}

	cg := clonedControlGroup.(*ControlGroup)

	return cg, nil
}

type ControlGroupFactor struct {
	Name                   string
	Identity               *IdentityFactor `hcl:"identity"`
	ControlledCapabilities []string        `hcl:"controlled_capabilities"`
}

type IdentityFactor struct {
	GroupIDs          []string `hcl:"group_ids"`
	GroupNames        []string `hcl:"group_names"`
	ApprovalsRequired int      `hcl:"approvals"`
}

type ACLPermissions struct {
	CapabilitiesBitmap  uint32
	MinWrappingTTL      time.Duration
	MaxWrappingTTL      time.Duration
	AllowedParameters   map[string][]interface{}
	DeniedParameters    map[string][]interface{}
	RequiredParameters  []string
	MFAMethods          []string
	ControlGroup        *ControlGroup
	GrantingPoliciesMap map[uint32][]logical.PolicyInfo
}

func (p *ACLPermissions) Clone() (*ACLPermissions, error) {
	ret := &ACLPermissions{
		CapabilitiesBitmap: p.CapabilitiesBitmap,
		MinWrappingTTL:     p.MinWrappingTTL,
		MaxWrappingTTL:     p.MaxWrappingTTL,
		RequiredParameters: p.RequiredParameters[:],
	}

	switch {
	case p.AllowedParameters == nil:
	case len(p.AllowedParameters) == 0:
		ret.AllowedParameters = make(map[string][]interface{})
	default:
		clonedAllowed, err := copystructure.Copy(p.AllowedParameters)
		if err != nil {
			return nil, err
		}
		ret.AllowedParameters = clonedAllowed.(map[string][]interface{})
	}

	switch {
	case p.DeniedParameters == nil:
	case len(p.DeniedParameters) == 0:
		ret.DeniedParameters = make(map[string][]interface{})
	default:
		clonedDenied, err := copystructure.Copy(p.DeniedParameters)
		if err != nil {
			return nil, err
		}
		ret.DeniedParameters = clonedDenied.(map[string][]interface{})
	}

	switch {
	case p.MFAMethods == nil:
	case len(p.MFAMethods) == 0:
		ret.MFAMethods = []string{}
	default:
		clonedMFAMethods, err := copystructure.Copy(p.MFAMethods)
		if err != nil {
			return nil, err
		}
		ret.MFAMethods = clonedMFAMethods.([]string)
	}

	switch {
	case p.ControlGroup == nil:
	default:
		clonedControlGroup, err := copystructure.Copy(p.ControlGroup)
		if err != nil {
			return nil, err
		}
		ret.ControlGroup = clonedControlGroup.(*ControlGroup)
	}

	switch {
	case p.GrantingPoliciesMap == nil:
	case len(p.GrantingPoliciesMap) == 0:
		ret.GrantingPoliciesMap = make(map[uint32][]logical.PolicyInfo)
	default:
		clonedGrantingPoliciesMap, err := copystructure.Copy(p.GrantingPoliciesMap)
		if err != nil {
			return nil, err
		}
		ret.GrantingPoliciesMap = clonedGrantingPoliciesMap.(map[uint32][]logical.PolicyInfo)
	}

	return ret, nil
}

func addGrantingPoliciesToMap(m map[uint32][]logical.PolicyInfo, policy *Policy, capabilitiesBitmap uint32) map[uint32][]logical.PolicyInfo {
	if m == nil {
		m = make(map[uint32][]logical.PolicyInfo)
	}

	// For all possible policies, check if the provided capabilities include
	// them
	for _, capability := range cap2Int {
		if capabilitiesBitmap&capability == 0 {
			continue
		}

		m[capability] = append(m[capability], logical.PolicyInfo{
			Name:          policy.Name,
			NamespaceId:   policy.namespace.ID,
			NamespacePath: policy.namespace.Path,
			Type:          "acl",
		})
	}

	return m
}

// ParseACLPolicy is used to parse the specified ACL rules into an
// intermediary set of policies, before being compiled into
// the ACL
func ParseACLPolicy(ns *namespace.Namespace, rules string) (*Policy, error) {
	return parseACLPolicyWithTemplating(ns, rules, false, nil, nil)
}

// parseACLPolicyWithTemplating performs the actual work and checks whether we
// should perform substitutions. If performTemplating is true we know that it
// is templated so we don't check again, otherwise we check to see if it's a
// templated policy.
func parseACLPolicyWithTemplating(ns *namespace.Namespace, rules string, performTemplating bool, entity *identity.Entity, groups []*identity.Group) (*Policy, error) {
	// Parse the rules
	root, err := hcl.Parse(rules)
	if err != nil {
		return nil, fmt.Errorf("failed to parse policy: %w", err)
	}

	// Top-level item should be the object list
	list, ok := root.Node.(*ast.ObjectList)
	if !ok {
		return nil, fmt.Errorf("failed to parse policy: does not contain a root object")
	}

	// Check for invalid top-level keys
	valid := []string{
		"name",
		"path",
	}
	if err := hclutil.CheckHCLKeys(list, valid); err != nil {
		return nil, fmt.Errorf("failed to parse policy: %w", err)
	}

	// Create the initial policy and store the raw text of the rules
	p := Policy{
		Raw:       rules,
		Type:      PolicyTypeACL,
		namespace: ns,
	}
	if err := hcl.DecodeObject(&p, list); err != nil {
		return nil, fmt.Errorf("failed to parse policy: %w", err)
	}

	if o := list.Filter("path"); len(o.Items) > 0 {
		if err := parsePaths(&p, o, performTemplating, entity, groups); err != nil {
			return nil, fmt.Errorf("failed to parse policy: %w", err)
		}
	}

	return &p, nil
}

func parsePaths(result *Policy, list *ast.ObjectList, performTemplating bool, entity *identity.Entity, groups []*identity.Group) error {
	paths := make([]*PathRules, 0, len(list.Items))
	for _, item := range list.Items {
		key := "path"
		if len(item.Keys) > 0 {
			key = item.Keys[0].Token.Value().(string)
		}

		// Check the path
		if performTemplating {
			_, templated, err := identitytpl.PopulateString(identitytpl.PopulateStringInput{
				Mode:        identitytpl.ACLTemplating,
				String:      key,
				Entity:      identity.ToSDKEntity(entity),
				Groups:      identity.ToSDKGroups(groups),
				NamespaceID: result.namespace.ID,
			})
			if err != nil {
				continue
			}
			key = templated
		} else {
			hasTemplating, _, err := identitytpl.PopulateString(identitytpl.PopulateStringInput{
				Mode:              identitytpl.ACLTemplating,
				ValidityCheckOnly: true,
				String:            key,
			})
			if err != nil {
				return fmt.Errorf("failed to validate policy templating: %w", err)
			}
			if hasTemplating {
				result.Templated = true
			}
		}

		valid := []string{
			"comment",
			"policy",
			"capabilities",
			"allowed_parameters",
			"denied_parameters",
			"required_parameters",
			"min_wrapping_ttl",
			"max_wrapping_ttl",
			"mfa_methods",
			"control_group",
		}
		if err := hclutil.CheckHCLKeys(item.Val, valid); err != nil {
			return multierror.Prefix(err, fmt.Sprintf("path %q:", key))
		}

		var pc PathRules

		// allocate memory so that DecodeObject can initialize the ACLPermissions struct
		pc.Permissions = new(ACLPermissions)

		pc.Path = key

		if err := hcl.DecodeObject(&pc, item.Val); err != nil {
			return multierror.Prefix(err, fmt.Sprintf("path %q:", key))
		}

		// Strip a leading '/' as paths in Vault start after the / in the API path
		if len(pc.Path) > 0 && pc.Path[0] == '/' {
			pc.Path = pc.Path[1:]
		}

		// Ensure we are using the full request path internally
		pc.Path = result.namespace.Path + pc.Path

		if strings.Contains(pc.Path, "+*") {
			return fmt.Errorf("path %q: invalid use of wildcards ('+*' is forbidden)", pc.Path)
		}

		if pc.Path == "+" || strings.Count(pc.Path, "/+") > 0 || strings.HasPrefix(pc.Path, "+/") {
			pc.HasSegmentWildcards = true
		}

		if strings.HasSuffix(pc.Path, "*") {
			// If there are segment wildcards, don't actually strip the
			// trailing asterisk, but don't want to hit the default case
			if !pc.HasSegmentWildcards {
				// Strip the glob character if found
				pc.Path = strings.TrimSuffix(pc.Path, "*")
				pc.IsPrefix = true
			}
		}

		// Map old-style policies into capabilities
		if len(pc.Policy) > 0 {
			switch pc.Policy {
			case OldDenyPathPolicy:
				pc.Capabilities = []string{DenyCapability}
			case OldReadPathPolicy:
				pc.Capabilities = append(pc.Capabilities, []string{ReadCapability, ListCapability}...)
			case OldWritePathPolicy:
				pc.Capabilities = append(pc.Capabilities, []string{CreateCapability, ReadCapability, UpdateCapability, DeleteCapability, ListCapability}...)
			case OldSudoPathPolicy:
				pc.Capabilities = append(pc.Capabilities, []string{CreateCapability, ReadCapability, UpdateCapability, DeleteCapability, ListCapability, SudoCapability}...)
			default:
				return fmt.Errorf("path %q: invalid policy %q", key, pc.Policy)
			}
		}

		// Initialize the map
		pc.Permissions.CapabilitiesBitmap = 0
		for _, cap := range pc.Capabilities {
			switch cap {
			// If it's deny, don't include any other capability
			case DenyCapability:
				pc.Capabilities = []string{DenyCapability}
				pc.Permissions.CapabilitiesBitmap = DenyCapabilityInt
				goto PathFinished
			case CreateCapability, ReadCapability, UpdateCapability, DeleteCapability, ListCapability, SudoCapability, PatchCapability:
				pc.Permissions.CapabilitiesBitmap |= cap2Int[cap]
			default:
				return fmt.Errorf("path %q: invalid capability %q", key, cap)
			}
		}

		if pc.AllowedParametersHCL != nil {
			pc.Permissions.AllowedParameters = make(map[string][]interface{}, len(pc.AllowedParametersHCL))
			for k, v := range pc.AllowedParametersHCL {
				pc.Permissions.AllowedParameters[strings.ToLower(k)] = v
			}
		}
		if pc.DeniedParametersHCL != nil {
			pc.Permissions.DeniedParameters = make(map[string][]interface{}, len(pc.DeniedParametersHCL))

			for k, v := range pc.DeniedParametersHCL {
				pc.Permissions.DeniedParameters[strings.ToLower(k)] = v
			}
		}
		if pc.MinWrappingTTLHCL != nil {
			dur, err := parseutil.ParseDurationSecond(pc.MinWrappingTTLHCL)
			if err != nil {
				return fmt.Errorf("error parsing min_wrapping_ttl: %w", err)
			}
			pc.Permissions.MinWrappingTTL = dur
		}
		if pc.MaxWrappingTTLHCL != nil {
			dur, err := parseutil.ParseDurationSecond(pc.MaxWrappingTTLHCL)
			if err != nil {
				return fmt.Errorf("error parsing max_wrapping_ttl: %w", err)
			}
			pc.Permissions.MaxWrappingTTL = dur
		}
		if pc.MFAMethodsHCL != nil {
			pc.Permissions.MFAMethods = make([]string, len(pc.MFAMethodsHCL))
			copy(pc.Permissions.MFAMethods, pc.MFAMethodsHCL)
		}
		if pc.ControlGroupHCL != nil {
			pc.Permissions.ControlGroup = new(ControlGroup)
			if pc.ControlGroupHCL.TTL != nil {
				dur, err := parseutil.ParseDurationSecond(pc.ControlGroupHCL.TTL)
				if err != nil {
					return fmt.Errorf("error parsing control group max ttl: %w", err)
				}
				pc.Permissions.ControlGroup.TTL = dur
			}
			var factors []*ControlGroupFactor
			if pc.ControlGroupHCL.Factors != nil {
				for key, factor := range pc.ControlGroupHCL.Factors {
					// Although we only have one factor here, we need to check to make sure there is at least
					// one factor defined in this factor block.
					if factor.Identity == nil {
						return errors.New("no control_group factor provided")
					}

					if factor.Identity.ApprovalsRequired <= 0 ||
						(len(factor.Identity.GroupIDs) == 0 && len(factor.Identity.GroupNames) == 0) {
						return errors.New("must provide more than one identity group and approvals > 0")
					}

					// Ensure that configured ControlledCapabilities for factor are a subset of the
					// Capabilities of the policy.
					if len(factor.ControlledCapabilities) > 0 {
						var found bool
						for _, controlledCapability := range factor.ControlledCapabilities {
							found = false
							for _, policyCap := range pc.Capabilities {
								if controlledCapability == policyCap {
									found = true
								}
							}
							if !found {
								return errors.New(ControlledCapabilityPolicySubsetError)
							}
						}
					}

					factors = append(factors, &ControlGroupFactor{
						Name:                   key,
						Identity:               factor.Identity,
						ControlledCapabilities: factor.ControlledCapabilities,
					})
				}
			}
			if len(factors) == 0 {
				return errors.New("no control group factors provided")
			}
			pc.Permissions.ControlGroup.Factors = factors
		}
		if pc.Permissions.MinWrappingTTL != 0 &&
			pc.Permissions.MaxWrappingTTL != 0 &&
			pc.Permissions.MaxWrappingTTL < pc.Permissions.MinWrappingTTL {
			return errors.New("max_wrapping_ttl cannot be less than min_wrapping_ttl")
		}
		if len(pc.RequiredParametersHCL) > 0 {
			pc.Permissions.RequiredParameters = pc.RequiredParametersHCL[:]
		}

	PathFinished:
		paths = append(paths, &pc)
	}

	result.Paths = paths
	return nil
}
