/*
Copyright 2020 The cert-manager Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package validation

import (
	"fmt"
	"net"
	"net/mail"
	"slices"
	"strings"
	"time"
	"unicode/utf8"

	admissionv1 "k8s.io/api/admission/v1"
	apivalidation "k8s.io/apimachinery/pkg/api/validation"
	metavalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/apimachinery/pkg/util/validation/field"

	internalcmapi "github.com/cert-manager/cert-manager/internal/apis/certmanager"
	cmmeta "github.com/cert-manager/cert-manager/internal/apis/meta"
	"github.com/cert-manager/cert-manager/internal/webhook/feature"
	"github.com/cert-manager/cert-manager/pkg/api/util"
	cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	utilfeature "github.com/cert-manager/cert-manager/pkg/util/feature"
	"github.com/cert-manager/cert-manager/pkg/util/pki"
)

// mapping for key algorithm to allowed signature algorithms
var keyAlgToAllowedSigAlgs = map[internalcmapi.PrivateKeyAlgorithm][]internalcmapi.SignatureAlgorithm{
	internalcmapi.RSAKeyAlgorithm: {
		internalcmapi.SHA256WithRSA,
		internalcmapi.SHA384WithRSA,
		internalcmapi.SHA512WithRSA,
	},
	internalcmapi.ECDSAKeyAlgorithm: {
		internalcmapi.ECDSAWithSHA256,
		internalcmapi.ECDSAWithSHA384,
		internalcmapi.ECDSAWithSHA512,
	},
	internalcmapi.Ed25519KeyAlgorithm: {
		internalcmapi.PureEd25519,
	},
}

// Validation functions for cert-manager Certificate types

func ValidateCertificateSpec(crt *internalcmapi.CertificateSpec, fldPath *field.Path) field.ErrorList {
	el := field.ErrorList{}
	if crt.SecretName == "" {
		el = append(el, field.Required(fldPath.Child("secretName"), "must be specified"))
	} else {
		for _, msg := range apivalidation.NameIsDNSSubdomain(crt.SecretName, false) {
			el = append(el, field.Invalid(fldPath.Child("secretName"), crt.SecretName, msg))
		}
	}

	el = append(el, validateIssuerRef(crt.IssuerRef, fldPath)...)

	var commonName = crt.CommonName
	if crt.LiteralSubject != "" {
		if !utilfeature.DefaultFeatureGate.Enabled(feature.LiteralCertificateSubject) {
			el = append(el, field.Forbidden(fldPath.Child("literalSubject"), "Feature gate LiteralCertificateSubject must be enabled on both webhook and controller to use the alpha `literalSubject` field"))
		}

		if len(commonName) != 0 {
			el = append(el, field.Invalid(fldPath.Child("commonName"), commonName, "When providing a `LiteralSubject` no `commonName` may be provided."))
		}

		if crt.Subject != nil && (len(crt.Subject.Organizations) > 0 ||
			len(crt.Subject.Countries) > 0 ||
			len(crt.Subject.OrganizationalUnits) > 0 ||
			len(crt.Subject.Localities) > 0 ||
			len(crt.Subject.Provinces) > 0 ||
			len(crt.Subject.StreetAddresses) > 0 ||
			len(crt.Subject.PostalCodes) > 0 ||
			len(crt.Subject.SerialNumber) > 0) {
			el = append(el, field.Invalid(fldPath.Child("subject"), crt.Subject, "When providing a `LiteralSubject` no `Subject` properties may be provided."))
		}

		sequence, err := pki.UnmarshalSubjectStringToRDNSequence(crt.LiteralSubject)
		if err != nil {
			el = append(el, field.Invalid(fldPath.Child("literalSubject"), crt.LiteralSubject, err.Error()))
		}

		// Must contain a CN
		for _, rdns := range sequence {
			for _, atv := range rdns {
				if atv.Type.Equal(pki.OIDConstants.CommonName) {
					if str, ok := atv.Value.(string); ok {
						commonName = str
					} else {
						el = append(el, field.Invalid(fldPath.Child("literalSubject"), atv.Value, "Field with type CN should be a string"))
					}
				}
			}
		}

		// Should not contain unrecognized OIDs
		for _, rdns := range sequence {
			for _, atv := range rdns {
				if atv.Type.Equal(nil) {
					el = append(el, field.Invalid(fldPath.Child("literalSubject"), crt.LiteralSubject, fmt.Sprintf("Literal subject contains unrecognized key with value [%s]", atv.Value)))
				}
			}
		}
	}

	if len(commonName) == 0 &&
		len(crt.DNSNames) == 0 &&
		len(crt.URIs) == 0 &&
		len(crt.EmailAddresses) == 0 &&
		len(crt.IPAddresses) == 0 &&
		len(crt.OtherNames) == 0 {
		el = append(el, field.Invalid(fldPath, "", "at least one of commonName (from the commonName field or from a literalSubject), dnsNames, uriSANs, ipAddresses, emailSANs or otherNames must be set"))
	}

	// if a common name has been specified, ensure it is no longer than 64 chars
	if len(commonName) > 64 {
		el = append(el, field.TooLong(fldPath.Child("commonName"), commonName, 64))
	}

	if len(crt.IPAddresses) > 0 {
		el = append(el, validateIPAddresses(crt, fldPath)...)
	}

	if len(crt.EmailAddresses) > 0 {
		el = append(el, validateEmailAddresses(crt, fldPath)...)
	}

	if len(crt.OtherNames) > 0 {
		if !utilfeature.DefaultFeatureGate.Enabled(feature.OtherNames) {
			el = append(el, field.Forbidden(fldPath.Child("OtherNames"), "Feature gate OtherNames must be enabled on both webhook and controller to use the alpha `otherNames` field"))
		} else {
			for i, otherName := range crt.OtherNames {
				if otherName.OID == "" {
					el = append(el, field.Required(fldPath.Child("otherNames").Index(i).Child("oid"), "must be specified"))
				}

				if _, err := pki.ParseObjectIdentifier(otherName.OID); err != nil {
					el = append(el, field.Invalid(fldPath.Child("otherNames").Index(i).Child("oid"), otherName.OID, "oid syntax invalid"))
				}

				if otherName.UTF8Value == "" || !utf8.ValidString(otherName.UTF8Value) {
					el = append(el, field.Required(fldPath.Child("otherNames").Index(i).Child("utf8Value"), "must be set to a valid non-empty UTF8 string"))
				}
			}
		}
	}

	if crt.PrivateKey != nil {
		switch crt.PrivateKey.Algorithm {
		case "", internalcmapi.RSAKeyAlgorithm:
			if crt.PrivateKey.Size > 0 && (crt.PrivateKey.Size < pki.MinRSAKeySize || crt.PrivateKey.Size > pki.MaxRSAKeySize) {
				el = append(el, field.Invalid(fldPath.Child("privateKey", "size"), crt.PrivateKey.Size, fmt.Sprintf("must be between %d and %d for rsa keyAlgorithm", pki.MinRSAKeySize, pki.MaxRSAKeySize)))
			}
		case internalcmapi.ECDSAKeyAlgorithm:
			if crt.PrivateKey.Size > 0 && crt.PrivateKey.Size != 256 && crt.PrivateKey.Size != 384 && crt.PrivateKey.Size != 521 {
				el = append(el, field.NotSupported(fldPath.Child("privateKey", "size"), crt.PrivateKey.Size, []string{"256", "384", "521"}))
			}
		case internalcmapi.Ed25519KeyAlgorithm:
			break
		default:
			el = append(el, field.Invalid(fldPath.Child("privateKey", "algorithm"), crt.PrivateKey.Algorithm, "must be either empty or one of rsa, ecdsa or ed25519"))
		}
	}

	if crt.SignatureAlgorithm != "" {
		actualKeyAlg := internalcmapi.RSAKeyAlgorithm
		if crt.PrivateKey != nil && crt.PrivateKey.Algorithm != "" {
			actualKeyAlg = crt.PrivateKey.Algorithm
		}
		allowed, ok := keyAlgToAllowedSigAlgs[actualKeyAlg]
		if ok && !slices.Contains(allowed, crt.SignatureAlgorithm) {
			el = append(el, field.Invalid(fldPath.Child("signatureAlgorithm"), crt.SignatureAlgorithm,
				fmt.Sprintf("for key algorithm %s the allowed signature algorithms are %v", actualKeyAlg, allowed)))
		}
	}

	if crt.Duration != nil || crt.RenewBefore != nil {
		el = append(el, ValidateDuration(crt, fldPath)...)
	}
	if len(crt.Usages) > 0 {
		el = append(el, validateUsages(crt, fldPath)...)
	}
	if crt.RevisionHistoryLimit != nil && *crt.RevisionHistoryLimit < 1 {
		el = append(el, field.Invalid(fldPath.Child("revisionHistoryLimit"), *crt.RevisionHistoryLimit, "must not be less than 1"))
	}

	if crt.SecretTemplate != nil {
		if len(crt.SecretTemplate.Labels) > 0 {
			el = append(el, validateSecretTemplateLabels(crt, fldPath)...)
		}
		if len(crt.SecretTemplate.Annotations) > 0 {
			el = append(el, validateSecretTemplateAnnotations(crt, fldPath)...)
		}
	}

	if crt.NameConstraints != nil {
		if !utilfeature.DefaultFeatureGate.Enabled(feature.NameConstraints) {
			el = append(el, field.Forbidden(fldPath.Child("nameConstraints"), "feature gate NameConstraints must be enabled"))
		} else {
			if !crt.IsCA {
				el = append(el, field.Invalid(fldPath.Child("nameConstraints"), crt.NameConstraints, "isCa should be true when nameConstraints is set"))
			}

			if crt.NameConstraints.Permitted == nil && crt.NameConstraints.Excluded == nil {
				el = append(el, field.Invalid(fldPath.Child("nameConstraints"), crt.NameConstraints, "either permitted or excluded must be set"))
			}
		}
	}

	el = append(el, validateAdditionalOutputFormats(crt, fldPath)...)

	if crt.Keystores != nil {
		el = append(el, validateKeystores(crt, fldPath)...)
	}

	return el
}

func ValidateCertificate(a *admissionv1.AdmissionRequest, obj runtime.Object) (allErrs field.ErrorList, warnings []string) {
	crt := obj.(*internalcmapi.Certificate)
	allErrs = ValidateCertificateSpec(&crt.Spec, field.NewPath("spec"))
	if crt.Spec.PrivateKey == nil || crt.Spec.PrivateKey.RotationPolicy == "" {
		warnings = append(warnings, newDefaultPrivateKeyRotationPolicy)
	}
	return allErrs, warnings
}

func ValidateUpdateCertificate(a *admissionv1.AdmissionRequest, oldObj, obj runtime.Object) (field.ErrorList, []string) {
	crt := obj.(*internalcmapi.Certificate)
	allErrs := ValidateCertificateSpec(&crt.Spec, field.NewPath("spec"))
	return allErrs, nil
}

func validateIssuerRef(issuerRef cmmeta.IssuerReference, fldPath *field.Path) field.ErrorList {
	el := field.ErrorList{}

	issuerRefPath := fldPath.Child("issuerRef")
	if issuerRef.Name == "" {
		// all issuerRefs must specify a name
		el = append(el, field.Required(issuerRefPath.Child("name"), "must be specified"))
	}

	if issuerRef.Group == "" || issuerRef.Group == internalcmapi.SchemeGroupVersion.Group {
		// if the user leaves the group blank, it's effectively defaulted to the built-in issuers (i.e. cert-manager.io)
		// if the cert-manager.io group is used, we can do extra validation on the Kind
		// if an external group is used, we don't have a mechanism currently to determine which Kinds are valid for those groups
		// so we don't check
		switch issuerRef.Kind {
		case "":
			// do nothing

		case "Issuer", "ClusterIssuer":
			// do nothing

		default:
			kindPath := issuerRefPath.Child("kind")
			errMsg := "must be one of Issuer or ClusterIssuer"

			if issuerRef.Group == "" {
				// Sometimes the user sets a kind for an external issuer (e.g., "AWSPCAClusterIssuer" or "VenafiIssuer") but forgets
				// to set the group (an easy mistake to make - see https://github.com/cert-manager/csi-driver/issues/197).
				// If the users forgets the group but otherwise has a correct Kind set for an external issuer, we can give a hint
				// as to what they need to do to fix.

				// If the user explicitly set the group to the cert-manager group though, we don't give the hint
				errMsg += fmt.Sprintf(" (did you forget to set %s?)", kindPath.Child("group").String())
			}

			el = append(el, field.Invalid(kindPath, issuerRef.Kind, errMsg))
		}
	}

	return el
}

func validateIPAddresses(a *internalcmapi.CertificateSpec, fldPath *field.Path) field.ErrorList {
	if len(a.IPAddresses) == 0 {
		return nil
	}
	el := field.ErrorList{}
	for i, d := range a.IPAddresses {
		ip := net.ParseIP(d)
		if ip == nil {
			el = append(el, field.Invalid(fldPath.Child("ipAddresses").Index(i), d, "invalid IP address"))
		}
	}
	return el
}

func validateEmailAddresses(a *internalcmapi.CertificateSpec, fldPath *field.Path) field.ErrorList {
	if len(a.EmailAddresses) == 0 {
		return nil
	}
	el := field.ErrorList{}
	for i, d := range a.EmailAddresses {
		e, err := mail.ParseAddress(d)
		if err != nil {
			el = append(el, field.Invalid(fldPath.Child("emailAddresses").Index(i), d, fmt.Sprintf("invalid email address: %s", err)))
		} else if e.Address != d {
			// Go accepts email names as per RFC 5322 (name <email>)
			// This checks if the supplied value only contains the email address and nothing else
			el = append(el, field.Invalid(fldPath.Child("emailAddresses").Index(i), d, "invalid email address: make sure the supplied value only contains the email address itself"))
		}
	}
	return el
}

func validateUsages(a *internalcmapi.CertificateSpec, fldPath *field.Path) field.ErrorList {
	el := field.ErrorList{}
	for i, u := range a.Usages {
		_, kok := util.KeyUsageType(cmapi.KeyUsage(u))
		_, ekok := util.ExtKeyUsageType(cmapi.KeyUsage(u))
		if !kok && !ekok {
			el = append(el, field.Invalid(fldPath.Child("usages").Index(i), u, "unknown keyusage"))
		}
	}
	return el
}

func validateSecretTemplateLabels(crt *internalcmapi.CertificateSpec, fldPath *field.Path) field.ErrorList {
	return metavalidation.ValidateLabels(crt.SecretTemplate.Labels, fldPath.Child("secretTemplate", "labels"))
}

func validateSecretTemplateAnnotations(crt *internalcmapi.CertificateSpec, fldPath *field.Path) field.ErrorList {
	el := field.ErrorList{}

	secretTemplateAnnotationsPath := fldPath.Child("secretTemplate", "annotations")
	for a := range crt.SecretTemplate.Annotations {
		if strings.HasPrefix(a, "cert-manager.io/") && a != "cert-manager.io/allow-direct-injection" {
			el = append(el, field.Invalid(secretTemplateAnnotationsPath, a, "cert-manager.io/* annotations are not allowed"))
		}
	}

	el = append(el, apivalidation.ValidateAnnotations(crt.SecretTemplate.Annotations, secretTemplateAnnotationsPath)...)
	return el
}

func ValidateDuration(crt *internalcmapi.CertificateSpec, fldPath *field.Path) field.ErrorList {
	el := field.ErrorList{}

	duration := util.DefaultCertDuration(crt.Duration)
	if duration < cmapi.MinimumCertificateDuration {
		el = append(el, field.Invalid(fldPath.Child("duration"), duration, fmt.Sprintf("certificate duration must be greater than %s", cmapi.MinimumCertificateDuration)))
	}

	// Must set at most one of spec.renewBefore or spec.renewBeforePercentage.
	if crt.RenewBefore != nil && crt.RenewBeforePercentage != nil {
		el = append(el, field.Invalid(fldPath.Child("renewBefore"), crt.RenewBefore.Duration, "renewBefore and renewBeforePercentage are mutually exclusive and cannot both be set"))
		el = append(el, field.Invalid(fldPath.Child("renewBeforePercentage"), *crt.RenewBeforePercentage, "renewBefore and renewBeforePercentage are mutually exclusive and cannot both be set"))
	}

	// If spec.renewBefore is set, check that it is not less than the minimum.
	if crt.RenewBefore != nil && crt.RenewBefore.Duration < cmapi.MinimumRenewBefore {
		el = append(el, field.Invalid(fldPath.Child("renewBefore"), crt.RenewBefore.Duration, fmt.Sprintf("certificate renewBefore must be greater than %s", cmapi.MinimumRenewBefore)))
	}
	// If spec.renewBefore is set, it must be less than the duration.
	if crt.RenewBefore != nil && crt.RenewBefore.Duration >= duration {
		el = append(el, field.Invalid(fldPath.Child("renewBefore"), crt.RenewBefore.Duration, fmt.Sprintf("certificate duration %s must be greater than renewBefore %s", duration, crt.RenewBefore.Duration)))
	}

	// If spec.renewBeforePercentage is set, check that it's within the allowed
	// range.
	if crt.RenewBeforePercentage != nil {
		renewBefore := duration * time.Duration(100-*crt.RenewBeforePercentage) / 100
		if renewBefore < cmapi.MinimumRenewBefore {
			el = append(el, field.Invalid(fldPath.Child("renewBeforePercentage"), *crt.RenewBeforePercentage, fmt.Sprintf("certificate renewBeforePercentage must result in a renewBefore greater than %s", cmapi.MinimumRenewBefore)))
		}
		if renewBefore >= duration {
			el = append(el, field.Invalid(fldPath.Child("renewBeforePercentage"), *crt.RenewBeforePercentage, "certificate renewBeforePercentage must result in a renewBefore less than duration"))
		}
	}

	return el
}

func validateAdditionalOutputFormats(crt *internalcmapi.CertificateSpec, fldPath *field.Path) field.ErrorList {
	var el field.ErrorList

	// Ensure the set of output formats is unique, keyed on "Type".
	aofSet := sets.NewString()
	for _, val := range crt.AdditionalOutputFormats {
		if aofSet.Has(string(val.Type)) {
			el = append(el, field.Duplicate(fldPath.Child("additionalOutputFormats").Key("type"), string(val.Type)))
			continue
		}
		aofSet.Insert(string(val.Type))
	}

	return el
}

const (
	keystoresMutuallyExclusivePasswordsFmt = "exactly one of passwordSecretRef and password must be provided for %s keystores; cannot set both"

	keystoresPasswordRequiredFmt = "must set exactly one of passwordSecretRef and password must for %s keystores"

	keystoresLiteralPasswordMustNotBeEmptyFmt = "literal password cannot be empty if set on %s keystores"
)

func validateKeystores(crt *internalcmapi.CertificateSpec, fldPath *field.Path) field.ErrorList {
	var el field.ErrorList

	if crt.Keystores.JKS != nil {
		if crt.Keystores.JKS.Password != nil && crt.Keystores.JKS.PasswordSecretRef.Name != "" {
			el = append(el, field.Forbidden(fldPath.Child("keystores", "jks"), fmt.Sprintf(keystoresMutuallyExclusivePasswordsFmt, "JKS")))
		}

		if crt.Keystores.JKS.Password == nil && crt.Keystores.JKS.PasswordSecretRef.Name == "" {
			el = append(el, field.Forbidden(fldPath.Child("keystores", "jks"), fmt.Sprintf(keystoresPasswordRequiredFmt, "JKS")))
		}

		if crt.Keystores.JKS.Password != nil && len(*crt.Keystores.JKS.Password) == 0 {
			el = append(el, field.Forbidden(fldPath.Child("keystores", "jks", "password"), fmt.Sprintf(keystoresLiteralPasswordMustNotBeEmptyFmt, "JKS")))
		}
	}

	if crt.Keystores.PKCS12 != nil {
		if crt.Keystores.PKCS12.Password != nil && crt.Keystores.PKCS12.PasswordSecretRef.Name != "" {
			el = append(el, field.Forbidden(fldPath.Child("keystores", "pkcs12"), fmt.Sprintf(keystoresMutuallyExclusivePasswordsFmt, "PKCS#12")))
		}

		if crt.Keystores.PKCS12.Password == nil && crt.Keystores.PKCS12.PasswordSecretRef.Name == "" {
			el = append(el, field.Forbidden(fldPath.Child("keystores", "pkcs12"), fmt.Sprintf(keystoresPasswordRequiredFmt, "PKCS#12")))
		}

		if crt.Keystores.PKCS12.Password != nil && len(*crt.Keystores.PKCS12.Password) == 0 {
			el = append(el, field.Forbidden(fldPath.Child("keystores", "pkcs12", "password"), fmt.Sprintf(keystoresLiteralPasswordMustNotBeEmptyFmt, "PKCS#12")))
		}
	}

	return el
}
