// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package api

import (
	"context"

	slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1"
	"github.com/cilium/cilium/pkg/slices"
)

// IngressCommonRule is a rule that shares some of its fields across the
// IngressRule and IngressDenyRule. It's publicly exported so the code
// generators can generate code for this structure.
//
// +deepequal-gen:private-method=true
type IngressCommonRule struct {
	// FromEndpoints is a list of endpoints identified by an
	// EndpointSelector which are allowed to communicate with the endpoint
	// subject to the rule.
	//
	// Example:
	// Any endpoint with the label "role=backend" can be consumed by any
	// endpoint carrying the label "role=frontend".
	//
	// +kubebuilder:validation:Optional
	FromEndpoints []EndpointSelector `json:"fromEndpoints,omitempty"`

	// FromRequires is a list of additional constraints which must be met
	// in order for the selected endpoints to be reachable. These
	// additional constraints do no by itself grant access privileges and
	// must always be accompanied with at least one matching FromEndpoints.
	//
	// Example:
	// Any Endpoint with the label "team=A" requires consuming endpoint
	// to also carry the label "team=A".
	//
	// +kubebuilder:validation:Optional
	FromRequires []EndpointSelector `json:"fromRequires,omitempty"`

	// FromCIDR is a list of IP blocks which the endpoint subject to the
	// rule is allowed to receive connections from. Only connections which
	// do *not* originate from the cluster or from the local host are subject
	// to CIDR rules. In order to allow in-cluster connectivity, use the
	// FromEndpoints field.  This will match on the source IP address of
	// incoming connections. Adding  a prefix into FromCIDR or into
	// FromCIDRSet with no ExcludeCIDRs is  equivalent.  Overlaps are
	// allowed between FromCIDR and FromCIDRSet.
	//
	// Example:
	// Any endpoint with the label "app=my-legacy-pet" is allowed to receive
	// connections from 10.3.9.1
	//
	// +kubebuilder:validation:Optional
	FromCIDR CIDRSlice `json:"fromCIDR,omitempty"`

	// FromCIDRSet is a list of IP blocks which the endpoint subject to the
	// rule is allowed to receive connections from in addition to FromEndpoints,
	// along with a list of subnets contained within their corresponding IP block
	// from which traffic should not be allowed.
	// This will match on the source IP address of incoming connections. Adding
	// a prefix into FromCIDR or into FromCIDRSet with no ExcludeCIDRs is
	// equivalent. Overlaps are allowed between FromCIDR and FromCIDRSet.
	//
	// Example:
	// Any endpoint with the label "app=my-legacy-pet" is allowed to receive
	// connections from 10.0.0.0/8 except from IPs in subnet 10.96.0.0/12.
	//
	// +kubebuilder:validation:Optional
	FromCIDRSet CIDRRuleSlice `json:"fromCIDRSet,omitempty"`

	// FromEntities is a list of special entities which the endpoint subject
	// to the rule is allowed to receive connections from. Supported entities are
	// `world`, `cluster`, `host`, `remote-node`, `kube-apiserver`, `ingress`, `init`,
	// `health`, `unmanaged`, `none` and `all`.
	//
	// +kubebuilder:validation:Optional
	FromEntities EntitySlice `json:"fromEntities,omitempty"`

	// FromGroups is a directive that allows the integration with multiple outside
	// providers. Currently, only AWS is supported, and the rule can select by
	// multiple sub directives:
	//
	// Example:
	// FromGroups:
	// - aws:
	//     securityGroupsIds:
	//     - 'sg-XXXXXXXXXXXXX'
	//
	// +kubebuilder:validation:Optional
	FromGroups []Groups `json:"fromGroups,omitempty"`

	// FromNodes is a list of nodes identified by an
	// EndpointSelector which are allowed to communicate with the endpoint
	// subject to the rule.
	//
	// +kubebuilder:validation:Optional
	FromNodes []EndpointSelector `json:"fromNodes,omitempty"`

	// TODO: Move this to the policy package
	// (https://github.com/cilium/cilium/issues/8353)
	aggregatedSelectors EndpointSelectorSlice `json:"-"`
}

// DeepEqual returns true if both IngressCommonRule are deep equal.
// The semantic of a nil slice in one of its fields is different from the semantic
// of an empty non-nil slice, thus it explicitly checks for that case before calling
// the autogenerated method.
func (in *IngressCommonRule) DeepEqual(other *IngressCommonRule) bool {
	if slices.XorNil(in.FromEndpoints, other.FromEndpoints) {
		return false
	}
	if slices.XorNil(in.FromCIDR, other.FromCIDR) {
		return false
	}
	if slices.XorNil(in.FromCIDRSet, other.FromCIDRSet) {
		return false
	}
	if slices.XorNil(in.FromEntities, other.FromEntities) {
		return false
	}

	return in.deepEqual(other)
}

// IngressRule contains all rule types which can be applied at ingress,
// i.e. network traffic that originates outside of the endpoint and
// is entering the endpoint selected by the endpointSelector.
//
//   - All members of this structure are optional. If omitted or empty, the
//     member will have no effect on the rule.
//
//   - If multiple members are set, all of them need to match in order for
//     the rule to take effect. The exception to this rule is FromRequires field;
//     the effects of any Requires field in any rule will apply to all other
//     rules as well.
//
//   - FromEndpoints, FromCIDR, FromCIDRSet and FromEntities are mutually
//     exclusive. Only one of these members may be present within an individual
//     rule.
type IngressRule struct {
	IngressCommonRule `json:",inline"`

	// ToPorts is a list of destination ports identified by port number and
	// protocol which the endpoint subject to the rule is allowed to
	// receive connections on.
	//
	// Example:
	// Any endpoint with the label "app=httpd" can only accept incoming
	// connections on port 80/tcp.
	//
	// +kubebuilder:validation:Optional
	ToPorts PortRules `json:"toPorts,omitempty"`

	// ICMPs is a list of ICMP rule identified by type number
	// which the endpoint subject to the rule is allowed to
	// receive connections on.
	//
	// Example:
	// Any endpoint with the label "app=httpd" can only accept incoming
	// type 8 ICMP connections.
	//
	// +kubebuilder:validation:Optional
	ICMPs ICMPRules `json:"icmps,omitempty"`

	// Authentication is the required authentication type for the allowed traffic, if any.
	//
	// +kubebuilder:validation:Optional
	Authentication *Authentication `json:"authentication,omitempty"`
}

// IngressDenyRule contains all rule types which can be applied at ingress,
// i.e. network traffic that originates outside of the endpoint and
// is entering the endpoint selected by the endpointSelector.
//
//   - All members of this structure are optional. If omitted or empty, the
//     member will have no effect on the rule.
//
//   - If multiple members are set, all of them need to match in order for
//     the rule to take effect. The exception to this rule is FromRequires field;
//     the effects of any Requires field in any rule will apply to all other
//     rules as well.
//
//   - FromEndpoints, FromCIDR, FromCIDRSet, FromGroups and FromEntities are mutually
//     exclusive. Only one of these members may be present within an individual
//     rule.
type IngressDenyRule struct {
	IngressCommonRule `json:",inline"`

	// ToPorts is a list of destination ports identified by port number and
	// protocol which the endpoint subject to the rule is not allowed to
	// receive connections on.
	//
	// Example:
	// Any endpoint with the label "app=httpd" can not accept incoming
	// connections on port 80/tcp.
	//
	// +kubebuilder:validation:Optional
	ToPorts PortDenyRules `json:"toPorts,omitempty"`

	// ICMPs is a list of ICMP rule identified by type number
	// which the endpoint subject to the rule is not allowed to
	// receive connections on.
	//
	// Example:
	// Any endpoint with the label "app=httpd" can not accept incoming
	// type 8 ICMP connections.
	//
	// +kubebuilder:validation:Optional
	ICMPs ICMPRules `json:"icmps,omitempty"`
}

// SetAggregatedSelectors creates a single slice containing all of the following
// fields within the IngressRule, converted to EndpointSelector, to be stored
// within the IngressRule for easy lookup while performing policy evaluation
// for the rule:
// * FromEntities
// * FromCIDR
// * FromCIDRSet
//
// FromEndpoints is not aggregated due to requirement folding in
// GetSourceEndpointSelectorsWithRequirements()
func (i *IngressCommonRule) SetAggregatedSelectors() {
	// Goroutines can race setting i.aggregatedSelectors, but they will all compute the same result, so it does not matter.

	// explicitly check for empty non-nil slices, it should not result in any identity being selected.
	if (i.FromCIDR != nil && len(i.FromCIDR) == 0) ||
		(i.FromCIDRSet != nil && len(i.FromCIDRSet) == 0) ||
		(i.FromEntities != nil && len(i.FromEntities) == 0) {
		i.aggregatedSelectors = nil
		return
	}

	res := make(EndpointSelectorSlice, 0, len(i.FromEntities)+len(i.FromCIDR)+len(i.FromCIDRSet))
	res = append(res, i.FromEntities.GetAsEndpointSelectors()...)
	res = append(res, i.FromCIDR.GetAsEndpointSelectors()...)
	res = append(res, i.FromCIDRSet.GetAsEndpointSelectors()...)

	i.aggregatedSelectors = res
}

// GetSourceEndpointSelectorsWithRequirements returns a slice of endpoints selectors covering
// all L3 source selectors of the ingress rule
func (i *IngressCommonRule) GetSourceEndpointSelectorsWithRequirements(requirements []slim_metav1.LabelSelectorRequirement) EndpointSelectorSlice {
	if i.aggregatedSelectors == nil {
		i.SetAggregatedSelectors()
	}

	// explicitly check for empty non-nil slices, it should not result in any identity being selected.
	if i.aggregatedSelectors == nil || (i.FromEndpoints != nil && len(i.FromEndpoints) == 0) ||
		(i.FromNodes != nil && len(i.FromNodes) == 0) {
		return nil
	}

	res := make(EndpointSelectorSlice, 0, len(i.FromEndpoints)+len(i.aggregatedSelectors)+len(i.FromNodes))
	if len(requirements) > 0 && len(i.FromEndpoints) > 0 {
		for idx := range i.FromEndpoints {
			sel := *i.FromEndpoints[idx].DeepCopy()
			sel.MatchExpressions = append(sel.MatchExpressions, requirements...)
			sel.SyncRequirementsWithLabelSelector()
			// Even though this string is deep copied, we need to override it
			// because we are updating the contents of the MatchExpressions.
			sel.cachedLabelSelectorString = sel.LabelSelector.String()
			res = append(res, sel)
		}
	} else {
		res = append(res, i.FromEndpoints...)
		res = append(res, i.FromNodes...)
	}

	return append(res, i.aggregatedSelectors...)
}

// AllowsWildcarding returns true if wildcarding should be performed upon
// policy evaluation for the given rule.
func (i *IngressCommonRule) AllowsWildcarding() bool {
	return len(i.FromRequires) == 0
}

// RequiresDerivative returns true when the EgressCommonRule contains sections
// that need a derivative policy created in order to be enforced
// (e.g. FromGroups).
func (e *IngressCommonRule) RequiresDerivative() bool {
	return len(e.FromGroups) > 0
}

// IsL3 returns true if the IngressCommonRule contains at least a rule that
// affects L3 policy enforcement.
func (in *IngressCommonRule) IsL3() bool {
	if in == nil {
		return false
	}
	return len(in.FromEndpoints) > 0 ||
		len(in.FromRequires) > 0 ||
		len(in.FromCIDR) > 0 ||
		len(in.FromCIDRSet) > 0 ||
		len(in.FromEntities) > 0 ||
		len(in.FromGroups) > 0 ||
		len(in.FromNodes) > 0
}

// CreateDerivative will return a new rule based on the data gathered by the
// rules that creates a new derivative policy.
// In the case of FromGroups will call outside using the groups callback and this
// function can take a bit of time.
func (e *IngressRule) CreateDerivative(ctx context.Context) (*IngressRule, error) {
	newRule := e.DeepCopy()
	if !e.RequiresDerivative() {
		return newRule, nil
	}
	newRule.FromCIDRSet = make(CIDRRuleSlice, 0, len(e.FromGroups))
	cidrSet, err := ExtractCidrSet(ctx, e.FromGroups)
	if err != nil {
		return &IngressRule{}, err
	}
	newRule.FromCIDRSet = append(e.FromCIDRSet, cidrSet...)
	newRule.FromGroups = nil
	e.SetAggregatedSelectors()
	return newRule, nil
}

// CreateDerivative will return a new rule based on the data gathered by the
// rules that creates a new derivative policy.
// In the case of FromGroups will call outside using the groups callback and this
// function can take a bit of time.
func (e *IngressDenyRule) CreateDerivative(ctx context.Context) (*IngressDenyRule, error) {
	newRule := e.DeepCopy()
	if !e.RequiresDerivative() {
		return newRule, nil
	}
	newRule.FromCIDRSet = make(CIDRRuleSlice, 0, len(e.FromGroups))
	cidrSet, err := ExtractCidrSet(ctx, e.FromGroups)
	if err != nil {
		return &IngressDenyRule{}, err
	}
	newRule.FromCIDRSet = append(e.FromCIDRSet, cidrSet...)
	newRule.FromGroups = nil
	e.SetAggregatedSelectors()
	return newRule, nil
}
