From 19da90d6396ce9471f612d6e9a31f1b1c8d605b1 Mon Sep 17 00:00:00 2001 From: Anish Ramasekar Date: Thu, 25 Jan 2024 22:35:16 +0000 Subject: [PATCH] Add AudienceMatchPolicy to AuthenticationConfiguration Signed-off-by: Anish Ramasekar --- .../apiserver/pkg/apis/apiserver/types.go | 9 +++++++ .../pkg/apis/apiserver/v1alpha1/types.go | 24 +++++++++++++++++ .../v1alpha1/zz_generated.conversion.go | 2 ++ .../apis/apiserver/validation/validation.go | 8 ++++-- .../apiserver/validation/validation_test.go | 27 ++++++++++++------- 5 files changed, 59 insertions(+), 11 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/types.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/types.go index f3b4ae321ef..9153dfaf79a 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/types.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/types.go @@ -180,8 +180,17 @@ type Issuer struct { URL string CertificateAuthority string Audiences []string + AudienceMatchPolicy AudienceMatchPolicyType } +// AudienceMatchPolicyType is a set of valid values for Issuer.AudienceMatchPolicy +type AudienceMatchPolicyType string + +// Valid types for AudienceMatchPolicyType +const ( + AudienceMatchPolicyMatchAny AudienceMatchPolicyType = "MatchAny" +) + // ClaimValidationRule provides the configuration for a single claim validation rule. type ClaimValidationRule struct { Claim string diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/types.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/types.go index b557d906489..9a859103cf9 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/types.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/types.go @@ -225,8 +225,32 @@ type Issuer struct { // Required to be non-empty. // +required Audiences []string `json:"audiences"` + + // audienceMatchPolicy defines how the "audiences" field is used to match the "aud" claim in the presented JWT. + // Allowed values are: + // 1. "MatchAny" when multiple audiences are specified and + // 2. empty (or unset) or "MatchAny" when a single audience is specified. + // + // - MatchAny: the "aud" claim in the presented JWT must match at least one of the entries in the "audiences" field. + // For example, if "audiences" is ["foo", "bar"], the "aud" claim in the presented JWT must contain either "foo" or "bar" (and may contain both). + // + // - "": The match policy can be empty (or unset) when a single audience is specified in the "audiences" field. The "aud" claim in the presented JWT must contain the single audience (and may contain others). + // + // For more nuanced audience validation, use claimValidationRules. + // example: claimValidationRule[].expression: 'sets.equivalent(claims.aud, ["bar", "foo", "baz"])' to require an exact match. + // +optional + AudienceMatchPolicy AudienceMatchPolicyType `json:"audienceMatchPolicy,omitempty"` } +// AudienceMatchPolicyType is a set of valid values for Issuer.AudienceMatchPolicy +type AudienceMatchPolicyType string + +// Valid types for AudienceMatchPolicyType +const ( + // MatchAny means the "aud" claim in the presented JWT must match at least one of the entries in the "audiences" field. + AudienceMatchPolicyMatchAny AudienceMatchPolicyType = "MatchAny" +) + // ClaimValidationRule provides the configuration for a single claim validation rule. type ClaimValidationRule struct { // claim is the name of a required claim. diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/zz_generated.conversion.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/zz_generated.conversion.go index 92060206840..815df06aa9f 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/zz_generated.conversion.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/zz_generated.conversion.go @@ -582,6 +582,7 @@ func autoConvert_v1alpha1_Issuer_To_apiserver_Issuer(in *Issuer, out *apiserver. out.URL = in.URL out.CertificateAuthority = in.CertificateAuthority out.Audiences = *(*[]string)(unsafe.Pointer(&in.Audiences)) + out.AudienceMatchPolicy = apiserver.AudienceMatchPolicyType(in.AudienceMatchPolicy) return nil } @@ -594,6 +595,7 @@ func autoConvert_apiserver_Issuer_To_v1alpha1_Issuer(in *apiserver.Issuer, out * out.URL = in.URL out.CertificateAuthority = in.CertificateAuthority out.Audiences = *(*[]string)(unsafe.Pointer(&in.Audiences)) + out.AudienceMatchPolicy = AudienceMatchPolicyType(in.AudienceMatchPolicy) return nil } diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go index ad43ddda176..b4d03fd51dd 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go @@ -101,7 +101,7 @@ func validateIssuer(issuer api.Issuer, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateURL(issuer.URL, fldPath.Child("url"))...) - allErrs = append(allErrs, validateAudiences(issuer.Audiences, fldPath.Child("audiences"))...) + allErrs = append(allErrs, validateAudiences(issuer.Audiences, issuer.AudienceMatchPolicy, fldPath.Child("audiences"), fldPath.Child("audienceMatchPolicy"))...) allErrs = append(allErrs, validateCertificateAuthority(issuer.CertificateAuthority, fldPath.Child("certificateAuthority"))...) return allErrs @@ -136,7 +136,7 @@ func validateURL(issuerURL string, fldPath *field.Path) field.ErrorList { return allErrs } -func validateAudiences(audiences []string, fldPath *field.Path) field.ErrorList { +func validateAudiences(audiences []string, audienceMatchPolicy api.AudienceMatchPolicyType, fldPath, audienceMatchPolicyFldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if len(audiences) == 0 { @@ -157,6 +157,10 @@ func validateAudiences(audiences []string, fldPath *field.Path) field.ErrorList } } + if len(audienceMatchPolicy) > 0 && audienceMatchPolicy != api.AudienceMatchPolicyMatchAny { + allErrs = append(allErrs, field.Invalid(audienceMatchPolicyFldPath, audienceMatchPolicy, "audienceMatchPolicy must be empty or MatchAny for single audience")) + } + return allErrs } diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation_test.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation_test.go index 4be785d6a2d..e3ea3a46684 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation_test.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation_test.go @@ -269,37 +269,46 @@ func TestValidateURL(t *testing.T) { func TestValidateAudiences(t *testing.T) { fldPath := field.NewPath("issuer", "audiences") + audienceMatchPolicyFldPath := field.NewPath("issuer", "audienceMatchPolicy") testCases := []struct { - name string - in []string - want string + name string + in []string + matchPolicy string + want string }{ { name: "audiences is empty", in: []string{}, want: "issuer.audiences: Required value: at least one issuer.audiences is required", }, - { - name: "at most one audiences is allowed", - in: []string{"audience1", "audience2"}, - want: "issuer.audiences: Too many: 2: must have at most 1 items", - }, { name: "audience is empty", in: []string{""}, want: "issuer.audiences[0]: Required value: audience can't be empty", }, + { + name: "invalid match policy with single audience", + in: []string{"audience"}, + matchPolicy: "MatchExact", + want: `issuer.audienceMatchPolicy: Invalid value: "MatchExact": audienceMatchPolicy must be empty or MatchAny for single audience`, + }, { name: "valid audience", in: []string{"audience"}, want: "", }, + { + name: "valid audience with MatchAny policy", + in: []string{"audience"}, + matchPolicy: "MatchAny", + want: "", + }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - got := validateAudiences(tt.in, fldPath).ToAggregate() + got := validateAudiences(tt.in, api.AudienceMatchPolicyType(tt.matchPolicy), fldPath, audienceMatchPolicyFldPath).ToAggregate() if d := cmp.Diff(tt.want, errString(got)); d != "" { t.Fatalf("Audiences validation mismatch (-want +got):\n%s", d) }