Add AudienceMatchPolicy to AuthenticationConfiguration

Signed-off-by: Anish Ramasekar <anish.ramasekar@gmail.com>

Kubernetes-commit: 19da90d6396ce9471f612d6e9a31f1b1c8d605b1
This commit is contained in:
Anish Ramasekar 2024-01-25 22:35:16 +00:00 committed by Kubernetes Publisher
parent f980dbe8f0
commit 26996e3679
5 changed files with 59 additions and 11 deletions

View File

@ -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

View File

@ -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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}