2582 lines
93 KiB
Go
2582 lines
93 KiB
Go
/*
|
|
Copyright 2023 The Kubernetes 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 (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/util/errors"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
|
api "k8s.io/apiserver/pkg/apis/apiserver"
|
|
authenticationcel "k8s.io/apiserver/pkg/authentication/cel"
|
|
"k8s.io/apiserver/pkg/cel/environment"
|
|
"k8s.io/apiserver/pkg/features"
|
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
|
certutil "k8s.io/client-go/util/cert"
|
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
|
"k8s.io/utils/pointer"
|
|
)
|
|
|
|
var (
|
|
compiler = authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
|
|
)
|
|
|
|
func TestValidateAuthenticationConfiguration(t *testing.T) {
|
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
in *api.AuthenticationConfiguration
|
|
disallowedIssuers []string
|
|
want string
|
|
}{
|
|
{
|
|
name: "jwt authenticator is empty",
|
|
in: &api.AuthenticationConfiguration{},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "duplicate issuer across jwt authenticators",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "sub",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "sub",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: `jwt[1].issuer.url: Duplicate value: "https://issuer-url"`,
|
|
},
|
|
{
|
|
name: "duplicate discoveryURL across jwt authenticators",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
DiscoveryURL: "https://discovery-url/.well-known/openid-configuration",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "sub",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://different-issuer-url",
|
|
DiscoveryURL: "https://discovery-url/.well-known/openid-configuration",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "sub",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: `jwt[1].issuer.discoveryURL: Duplicate value: "https://discovery-url/.well-known/openid-configuration"`,
|
|
},
|
|
{
|
|
name: "failed issuer validation",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "invalid-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: `jwt[0].issuer.url: Invalid value: "invalid-url": URL scheme must be https`,
|
|
},
|
|
{
|
|
name: "failed claimValidationRule validation",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "baz",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: `jwt[0].claimValidationRules[1].claim: Duplicate value: "foo"`,
|
|
},
|
|
{
|
|
name: "failed claimMapping validation",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: "jwt[0].claimMappings.username: Required value: claim or expression is required",
|
|
},
|
|
{
|
|
name: "failed userValidationRule validation",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "sub",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
},
|
|
UserValidationRules: []api.UserValidationRule{
|
|
{Expression: "user.username == 'foo'"},
|
|
{Expression: "user.username == 'foo'"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: `jwt[0].userValidationRules[1].expression: Duplicate value: "user.username == 'foo'"`,
|
|
},
|
|
{
|
|
name: "valid authentication configuration with disallowed issuer",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "sub",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
disallowedIssuers: []string{"a", "b", "https://issuer-url", "c"},
|
|
want: `jwt[0].issuer.url: Invalid value: "https://issuer-url": URL must not overlap with disallowed issuers: [a b c https://issuer-url]`,
|
|
},
|
|
{
|
|
name: "valid authentication configuration that uses unverified email",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: "claims.email",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: `jwt[0].claimMappings.username.expression: Invalid value: "claims.email": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
|
|
},
|
|
{
|
|
name: "valid authentication configuration that almost uses unverified email",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: "claims.email_",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "valid authentication configuration that uses unverified email join",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: `['yay', string(claims.email), 'panda'].join(' ')`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: `jwt[0].claimMappings.username.expression: Invalid value: "['yay', string(claims.email), 'panda'].join(' ')": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
|
|
},
|
|
{
|
|
name: "valid authentication configuration that uses unverified optional email",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: `claims.?email`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: `jwt[0].claimMappings.username.expression: Invalid value: "claims.?email": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
|
|
},
|
|
{
|
|
name: "valid authentication configuration that uses unverified optional map email key",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: `{claims.?email: "panda"}`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: `jwt[0].claimMappings.username.expression: Invalid value: "{claims.?email: \"panda\"}": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
|
|
},
|
|
{
|
|
name: "valid authentication configuration that uses unverified optional map email value",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: `{"fancy": claims.?email}`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: `jwt[0].claimMappings.username.expression: Invalid value: "{\"fancy\": claims.?email}": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
|
|
},
|
|
{
|
|
name: "valid authentication configuration that uses unverified email value in list iteration",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: `["a"].map(i, i + claims.email)`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: `jwt[0].claimMappings.username.expression: Invalid value: "[\"a\"].map(i, i + claims.email)": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
|
|
},
|
|
{
|
|
name: "valid authentication configuration that uses verified email join via rule",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Expression: `string(claims.email_verified) == "panda"`,
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: `['yay', string(claims.email), 'panda'].join(' ')`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "valid authentication configuration that uses verified email join via extra",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: `['yay', string(claims.email), 'panda'].join(' ')`,
|
|
},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "panda.io/foo", ValueExpression: "claims.email_verified.upperAscii()"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "valid authentication configuration that uses verified email join via extra optional",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: `['yay', string(claims.email), 'panda'].join(' ')`,
|
|
},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "panda.io/foo", ValueExpression: "claims.?email_verified"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "valid authentication configuration that uses email and email_verified || true via username",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
// allow email claim when email_verified is true or absent
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: `claims.?email_verified.orValue(true) ? claims.email : claims.sub`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "valid authentication configuration that uses email and email_verified || false via username",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
// allow email claim only when email_verified is present and true
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: `claims.?email_verified.orValue(false) ? claims.email : claims.sub`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "valid authentication configuration",
|
|
in: &api.AuthenticationConfiguration{
|
|
JWT: []api.JWTAuthenticator{
|
|
{
|
|
Issuer: api.Issuer{
|
|
URL: "https://issuer-url",
|
|
Audiences: []string{"audience"},
|
|
},
|
|
ClaimValidationRules: []api.ClaimValidationRule{
|
|
{
|
|
Claim: "foo",
|
|
RequiredValue: "bar",
|
|
},
|
|
},
|
|
ClaimMappings: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "sub",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := ValidateAuthenticationConfiguration(tt.in, tt.disallowedIssuers).ToAggregate()
|
|
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
|
t.Fatalf("AuthenticationConfiguration validation mismatch (-want +got):\n%s", d)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateIssuerURL(t *testing.T) {
|
|
fldPath := field.NewPath("issuer", "url")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
in string
|
|
disallowedIssuers sets.Set[string]
|
|
want string
|
|
}{
|
|
{
|
|
name: "url is empty",
|
|
in: "",
|
|
want: "issuer.url: Required value: URL is required",
|
|
},
|
|
{
|
|
name: "url parse error",
|
|
in: "https://issuer-url:invalid-port",
|
|
want: `issuer.url: Invalid value: "https://issuer-url:invalid-port": parse "https://issuer-url:invalid-port": invalid port ":invalid-port" after host`,
|
|
},
|
|
{
|
|
name: "url is not https",
|
|
in: "http://issuer-url",
|
|
want: `issuer.url: Invalid value: "http://issuer-url": URL scheme must be https`,
|
|
},
|
|
{
|
|
name: "url user info is not allowed",
|
|
in: "https://user:pass@issuer-url",
|
|
want: `issuer.url: Invalid value: "https://user:pass@issuer-url": URL must not contain a username or password`,
|
|
},
|
|
{
|
|
name: "url raw query is not allowed",
|
|
in: "https://issuer-url?query",
|
|
want: `issuer.url: Invalid value: "https://issuer-url?query": URL must not contain a query`,
|
|
},
|
|
{
|
|
name: "url fragment is not allowed",
|
|
in: "https://issuer-url#fragment",
|
|
want: `issuer.url: Invalid value: "https://issuer-url#fragment": URL must not contain a fragment`,
|
|
},
|
|
{
|
|
name: "valid url that is disallowed",
|
|
in: "https://issuer-url",
|
|
disallowedIssuers: sets.New("https://issuer-url"),
|
|
want: `issuer.url: Invalid value: "https://issuer-url": URL must not overlap with disallowed issuers: [https://issuer-url]`,
|
|
},
|
|
{
|
|
name: "valid url",
|
|
in: "https://issuer-url",
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := validateIssuerURL(tt.in, tt.disallowedIssuers, fldPath).ToAggregate()
|
|
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
|
t.Fatalf("URL validation mismatch (-want +got):\n%s", d)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateIssuerDiscoveryURL(t *testing.T) {
|
|
fldPath := field.NewPath("issuer", "discoveryURL")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
in string
|
|
issuerURL string
|
|
want string
|
|
structuredAuthnFeatureEnabled bool
|
|
}{
|
|
{
|
|
name: "url is empty",
|
|
in: "",
|
|
want: "",
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "url parse error",
|
|
in: "https://oidc.oidc-namespace.svc:invalid-port",
|
|
want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc:invalid-port": parse "https://oidc.oidc-namespace.svc:invalid-port": invalid port ":invalid-port" after host`,
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "url is not https",
|
|
in: "http://oidc.oidc-namespace.svc",
|
|
want: `issuer.discoveryURL: Invalid value: "http://oidc.oidc-namespace.svc": URL scheme must be https`,
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "url user info is not allowed",
|
|
in: "https://user:pass@oidc.oidc-namespace.svc",
|
|
want: `issuer.discoveryURL: Invalid value: "https://user:pass@oidc.oidc-namespace.svc": URL must not contain a username or password`,
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "url raw query is not allowed",
|
|
in: "https://oidc.oidc-namespace.svc?query",
|
|
want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc?query": URL must not contain a query`,
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "url fragment is not allowed",
|
|
in: "https://oidc.oidc-namespace.svc#fragment",
|
|
want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc#fragment": URL must not contain a fragment`,
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "valid url",
|
|
in: "https://oidc.oidc-namespace.svc",
|
|
want: "",
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "valid url with path",
|
|
in: "https://oidc.oidc-namespace.svc/path",
|
|
want: "",
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "discovery url same as issuer url",
|
|
issuerURL: "https://issuer-url",
|
|
in: "https://issuer-url",
|
|
want: `issuer.discoveryURL: Invalid value: "https://issuer-url": discoveryURL must be different from URL`,
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "discovery url same as issuer url, with trailing slash",
|
|
issuerURL: "https://issuer-url",
|
|
in: "https://issuer-url/",
|
|
want: `issuer.discoveryURL: Invalid value: "https://issuer-url/": discoveryURL must be different from URL`,
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "discovery url same as issuer url, with multiple trailing slashes",
|
|
issuerURL: "https://issuer-url",
|
|
in: "https://issuer-url///",
|
|
want: `issuer.discoveryURL: Invalid value: "https://issuer-url///": discoveryURL must be different from URL`,
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "discovery url same as issuer url, issuer url with trailing slash",
|
|
issuerURL: "https://issuer-url/",
|
|
in: "https://issuer-url",
|
|
want: `issuer.discoveryURL: Invalid value: "https://issuer-url": discoveryURL must be different from URL`,
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "discovery url set but structured authn feature disabled",
|
|
in: "https://oidc.oidc-namespace.svc",
|
|
want: `issuer.discoveryURL: Invalid value: "https://oidc.oidc-namespace.svc": discoveryURL is not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := validateIssuerDiscoveryURL(tt.issuerURL, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
|
|
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
|
t.Fatalf("URL validation mismatch (-want +got):\n%s", d)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateAudiences(t *testing.T) {
|
|
fldPath := field.NewPath("issuer", "audiences")
|
|
audienceMatchPolicyFldPath := field.NewPath("issuer", "audienceMatchPolicy")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
in []string
|
|
matchPolicy string
|
|
want string
|
|
structuredAuthnFeatureEnabled bool
|
|
}{
|
|
{
|
|
name: "audiences is empty",
|
|
in: []string{},
|
|
want: "issuer.audiences: Required value: at least one issuer.audiences is required",
|
|
},
|
|
{
|
|
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: "",
|
|
},
|
|
{
|
|
name: "duplicate audience",
|
|
in: []string{"audience", "audience"},
|
|
matchPolicy: "MatchAny",
|
|
want: `issuer.audiences[1]: Duplicate value: "audience"`,
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "match policy not set with multiple audiences",
|
|
in: []string{"audience1", "audience2"},
|
|
want: `issuer.audienceMatchPolicy: Invalid value: "": audienceMatchPolicy must be MatchAny for multiple audiences`,
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "valid multiple audiences",
|
|
in: []string{"audience1", "audience2"},
|
|
matchPolicy: "MatchAny",
|
|
want: "",
|
|
structuredAuthnFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "multiple audiences set when structured authn feature is disabled",
|
|
in: []string{"audience1", "audience2"},
|
|
matchPolicy: "MatchAny",
|
|
want: `issuer.audiences: Invalid value: []string{"audience1", "audience2"}: multiple audiences are not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := validateAudiences(tt.in, api.AudienceMatchPolicyType(tt.matchPolicy), fldPath, audienceMatchPolicyFldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
|
|
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
|
t.Fatalf("Audiences validation mismatch (-want +got):\n%s", d)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateCertificateAuthority(t *testing.T) {
|
|
fldPath := field.NewPath("issuer", "certificateAuthority")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
in func() string
|
|
want string
|
|
}{
|
|
{
|
|
name: "invalid certificate authority",
|
|
in: func() string { return "invalid" },
|
|
want: `issuer.certificateAuthority: Invalid value: "<omitted>": data does not contain any valid RSA or ECDSA certificates`,
|
|
},
|
|
{
|
|
name: "certificate authority is empty",
|
|
in: func() string { return "" },
|
|
want: "",
|
|
},
|
|
{
|
|
name: "valid certificate authority",
|
|
in: func() string {
|
|
caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: "test-ca"}, caPrivateKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw}))
|
|
},
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := validateCertificateAuthority(tt.in(), fldPath).ToAggregate()
|
|
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
|
t.Fatalf("CertificateAuthority validation mismatch (-want +got):\n%s", d)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateClaimValidationRules(t *testing.T) {
|
|
fldPath := field.NewPath("issuer", "claimValidationRules")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
in []api.ClaimValidationRule
|
|
structuredAuthnFeatureEnabled bool
|
|
want string
|
|
wantCELMapper bool
|
|
wantUsesEmailVerifiedClaim bool
|
|
}{
|
|
{
|
|
name: "claim and expression are empty, structured authn feature enabled",
|
|
in: []api.ClaimValidationRule{{}},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: "issuer.claimValidationRules[0]: Required value: claim or expression is required",
|
|
},
|
|
{
|
|
name: "claim and expression are set",
|
|
in: []api.ClaimValidationRule{
|
|
{Claim: "claim", Expression: "expression"},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimValidationRules[0]: Invalid value: "claim": claim and expression can't both be set`,
|
|
},
|
|
{
|
|
name: "message set when claim is set",
|
|
in: []api.ClaimValidationRule{
|
|
{Claim: "claim", Message: "message"},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimValidationRules[0].message: Invalid value: "message": message can't be set when claim is set`,
|
|
},
|
|
{
|
|
name: "requiredValue set when expression is set",
|
|
in: []api.ClaimValidationRule{
|
|
{Expression: "claims.foo == 'bar'", RequiredValue: "value"},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimValidationRules[0].requiredValue: Invalid value: "value": requiredValue can't be set when expression is set`,
|
|
},
|
|
{
|
|
name: "duplicate claim",
|
|
in: []api.ClaimValidationRule{
|
|
{Claim: "claim"},
|
|
{Claim: "claim"},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimValidationRules[1].claim: Duplicate value: "claim"`,
|
|
},
|
|
{
|
|
name: "duplicate expression",
|
|
in: []api.ClaimValidationRule{
|
|
{Expression: "claims.foo == 'bar'"},
|
|
{Expression: "claims.foo == 'bar'"},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimValidationRules[1].expression: Duplicate value: "claims.foo == 'bar'"`,
|
|
},
|
|
{
|
|
name: "expression set when structured authn feature is disabled",
|
|
in: []api.ClaimValidationRule{
|
|
{Expression: "claims.foo == 'bar'"},
|
|
},
|
|
structuredAuthnFeatureEnabled: false,
|
|
want: `issuer.claimValidationRules[0].expression: Invalid value: "claims.foo == 'bar'": expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
|
|
},
|
|
{
|
|
name: "CEL expression compilation error",
|
|
in: []api.ClaimValidationRule{
|
|
{Expression: "foo.bar"},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimValidationRules[0].expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
|
| foo.bar
|
|
| ^`,
|
|
},
|
|
{
|
|
name: "expression does not evaluate to bool",
|
|
in: []api.ClaimValidationRule{
|
|
{Expression: "claims.foo"},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimValidationRules[0].expression: Invalid value: "claims.foo": must evaluate to bool`,
|
|
},
|
|
{
|
|
name: "valid claim validation rule with expression",
|
|
in: []api.ClaimValidationRule{
|
|
{Expression: "claims.foo == 'bar'"},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: "",
|
|
wantCELMapper: true,
|
|
},
|
|
{
|
|
name: "valid claim validation rule with multiple rules and email_verified check",
|
|
in: []api.ClaimValidationRule{
|
|
{Claim: "claim1", RequiredValue: "value1"},
|
|
{Claim: "claim2", RequiredValue: "value2"},
|
|
{Expression: "has(claims.email_verified)"},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: "",
|
|
wantUsesEmailVerifiedClaim: true,
|
|
},
|
|
{
|
|
name: "valid claim validation rule with multiple rules and almost email_verified check",
|
|
in: []api.ClaimValidationRule{
|
|
{Claim: "claim1", RequiredValue: "value1"},
|
|
{Claim: "claim2", RequiredValue: "value2"},
|
|
{Expression: "has(claims.email_verified_)"},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: "",
|
|
wantUsesEmailVerifiedClaim: false,
|
|
},
|
|
{
|
|
name: "valid claim validation rule with multiple rules",
|
|
in: []api.ClaimValidationRule{
|
|
{Claim: "claim1", RequiredValue: "value1"},
|
|
{Claim: "claim2", RequiredValue: "claims.email_verified"}, // not a CEL expression
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: "",
|
|
wantUsesEmailVerifiedClaim: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
state := &validationState{}
|
|
got := validateClaimValidationRules(compiler, state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
|
|
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
|
t.Fatalf("ClaimValidationRules validation mismatch (-want +got):\n%s", d)
|
|
}
|
|
if tt.wantCELMapper && state.mapper.ClaimValidationRules == nil {
|
|
t.Fatalf("ClaimValidationRules validation mismatch: CELMapper.ClaimValidationRules is nil")
|
|
}
|
|
if tt.wantUsesEmailVerifiedClaim != state.usesEmailVerifiedClaim {
|
|
t.Fatalf("ClaimValidationRules state.usesEmailVerifiedClaim mismatch: want %v, got %v", tt.wantUsesEmailVerifiedClaim, state.usesEmailVerifiedClaim)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateClaimMappings(t *testing.T) {
|
|
fldPath := field.NewPath("issuer", "claimMappings")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
in api.ClaimMappings
|
|
usesEmailVerifiedClaim bool
|
|
structuredAuthnFeatureEnabled bool
|
|
want string
|
|
wantCELMapper bool
|
|
}{
|
|
{
|
|
name: "username expression and claim are set",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Expression: "claims.username",
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.username: Invalid value: "": claim and expression can't both be set`,
|
|
},
|
|
{
|
|
name: "username expression and claim are empty",
|
|
in: api.ClaimMappings{Username: api.PrefixedClaimOrExpression{}},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: "issuer.claimMappings.username: Required value: claim or expression is required",
|
|
},
|
|
{
|
|
name: "username prefix set when expression is set",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: "claims.username",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.username.prefix: Invalid value: "prefix": prefix can't be set when expression is set`,
|
|
},
|
|
{
|
|
name: "username prefix is nil when claim is set",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.username.prefix: Required value: prefix is required when claim is set. It can be set to an empty string to disable prefixing`,
|
|
},
|
|
{
|
|
name: "username expression is invalid",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: "foo.bar",
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.username.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
|
| foo.bar
|
|
| ^`,
|
|
},
|
|
{
|
|
name: "groups expression and claim are set",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
Groups: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Expression: "claims.groups",
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.groups: Invalid value: "": claim and expression can't both be set`,
|
|
},
|
|
{
|
|
name: "groups prefix set when expression is set",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
Groups: api.PrefixedClaimOrExpression{
|
|
Expression: "claims.groups",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.groups.prefix: Invalid value: "prefix": prefix can't be set when expression is set`,
|
|
},
|
|
{
|
|
name: "groups prefix is nil when claim is set",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
Groups: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.groups.prefix: Required value: prefix is required when claim is set. It can be set to an empty string to disable prefixing`,
|
|
},
|
|
{
|
|
name: "groups expression is invalid",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
Groups: api.PrefixedClaimOrExpression{
|
|
Expression: "foo.bar",
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.groups.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
|
| foo.bar
|
|
| ^`,
|
|
},
|
|
{
|
|
name: "uid claim and expression are set",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
UID: api.ClaimOrExpression{
|
|
Claim: "claim",
|
|
Expression: "claims.uid",
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.uid: Invalid value: "": claim and expression can't both be set`,
|
|
},
|
|
{
|
|
name: "uid expression is invalid",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
UID: api.ClaimOrExpression{
|
|
Expression: "foo.bar",
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.uid.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
|
| foo.bar
|
|
| ^`,
|
|
},
|
|
{
|
|
name: "extra mapping key is empty",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.extra[0].key: Required value`,
|
|
},
|
|
{
|
|
name: "extra mapping value expression is empty",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/foo", ValueExpression: ""},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.extra[0].valueExpression: Required value: valueExpression is required`,
|
|
},
|
|
{
|
|
name: "extra mapping value expression is invalid",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/foo", ValueExpression: "foo.bar"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.extra[0].valueExpression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
|
| foo.bar
|
|
| ^`,
|
|
},
|
|
{
|
|
name: "username expression is invalid when structured authn feature is disabled",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Expression: "foo.bar",
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: false,
|
|
want: `[issuer.claimMappings.username.expression: Invalid value: "foo.bar": expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.username.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
|
| foo.bar
|
|
| ^]`,
|
|
},
|
|
{
|
|
name: "groups expression is invalid when structured authn feature is disabled",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
Groups: api.PrefixedClaimOrExpression{
|
|
Expression: "foo.bar",
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: false,
|
|
want: `[issuer.claimMappings.groups.expression: Invalid value: "foo.bar": expression is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.groups.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
|
| foo.bar
|
|
| ^]`,
|
|
},
|
|
{
|
|
name: "uid expression is invalid when structured authn feature is disabled",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
UID: api.ClaimOrExpression{
|
|
Expression: "foo.bar",
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: false,
|
|
want: `[issuer.claimMappings.uid: Invalid value: "": uid claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled, issuer.claimMappings.uid.expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
|
| foo.bar
|
|
| ^]`,
|
|
},
|
|
{
|
|
name: "uid claim is invalid when structured authn feature is disabled",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
UID: api.ClaimOrExpression{
|
|
Claim: "claim",
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: false,
|
|
want: `issuer.claimMappings.uid: Invalid value: "": uid claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
|
|
},
|
|
{
|
|
name: "extra mapping is invalid when structured authn feature is disabled",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{
|
|
Claim: "claim",
|
|
Prefix: pointer.String("prefix"),
|
|
},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: false,
|
|
want: `issuer.claimMappings.extra: Invalid value: "": extra claim mapping is not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
|
|
},
|
|
{
|
|
name: "duplicate extra mapping key",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
|
{Key: "example.org/foo", ValueExpression: "claims.extras"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.extra[1].key: Duplicate value: "example.org/foo"`,
|
|
},
|
|
{
|
|
name: "extra mapping key is not domain prefix path",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.extra[0].key: Invalid value: "foo": must be a domain-prefixed path (such as "acme.io/foo")`,
|
|
},
|
|
{
|
|
name: "extra mapping key is not lower case",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/Foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.extra[0].key: Invalid value: "example.org/Foo": key must be lowercase`,
|
|
},
|
|
{
|
|
name: "extra mapping key prefix is k8.io",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "k8s.io/foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.extra[0].key: Invalid value: "k8s.io/foo": k8s.io, kubernetes.io and their subdomains are reserved for Kubernetes use`,
|
|
},
|
|
{
|
|
name: "extra mapping key prefix contains k8.io",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.k8s.io/foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.extra[0].key: Invalid value: "example.k8s.io/foo": k8s.io, kubernetes.io and their subdomains are reserved for Kubernetes use`,
|
|
},
|
|
{
|
|
name: "extra mapping key prefix is kubernetes.io",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "kubernetes.io/foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.extra[0].key: Invalid value: "kubernetes.io/foo": k8s.io, kubernetes.io and their subdomains are reserved for Kubernetes use`,
|
|
},
|
|
{
|
|
name: "extra mapping key prefix contains kubernetes.io",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.kubernetes.io/foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.claimMappings.extra[0].key: Invalid value: "example.kubernetes.io/foo": k8s.io, kubernetes.io and their subdomains are reserved for Kubernetes use`,
|
|
},
|
|
{
|
|
name: "extra mapping key prefix with ak8s.io, *.ak8s.io, bkubernetes.io, *.bkubernetes.io are still valid",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "ak8s.io/foo", ValueExpression: "claims.extra"},
|
|
{Key: "example.ak8s.io/foo", ValueExpression: "claims.extra"},
|
|
{Key: "bkubernetes.io/foo", ValueExpression: "claims.extra"},
|
|
{Key: "example.bkubernetes.io/foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: "",
|
|
},
|
|
{
|
|
name: "valid claim mappings but uses email without verification",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.email"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
UID: api.ClaimOrExpression{Expression: "claims.uid"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
wantCELMapper: true,
|
|
want: `issuer.claimMappings.username.expression: Invalid value: "claims.email": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
|
|
},
|
|
{
|
|
name: "valid claim mappings but uses email in complex CEL expression without verification",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "has(claims.email) ? claims.email : claims.sub"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
UID: api.ClaimOrExpression{Expression: "claims.uid"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
wantCELMapper: true,
|
|
want: `issuer.claimMappings.username.expression: Invalid value: "has(claims.email) ? claims.email : claims.sub": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
|
|
},
|
|
{
|
|
name: "valid claim mappings but uses email in CEL expression function without verification",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.email.trim()"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
UID: api.ClaimOrExpression{Expression: "claims.uid"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
wantCELMapper: true,
|
|
want: `issuer.claimMappings.username.expression: Invalid value: "claims.email.trim()": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
|
|
},
|
|
{
|
|
name: "valid claim mappings and uses email with verification via extra",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.email"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
UID: api.ClaimOrExpression{Expression: "claims.uid"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/foo", ValueExpression: "claims.email_verified"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
wantCELMapper: true,
|
|
want: "",
|
|
},
|
|
{
|
|
name: "valid claim mappings and uses email with verification via extra optional",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.email"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
UID: api.ClaimOrExpression{Expression: "claims.uid"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/foo", ValueExpression: `has(claims.email_verified) ? string(claims.email_verified) : "false"`},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
wantCELMapper: true,
|
|
want: "",
|
|
},
|
|
{
|
|
name: "valid claim mappings and almost uses email with verification via extra optional",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.email"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
UID: api.ClaimOrExpression{Expression: "claims.uid"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/foo", ValueExpression: `has(claims.email_verified_) ? string(claims.email_verified_) : "false"`},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
wantCELMapper: true,
|
|
want: `issuer.claimMappings.username.expression: Invalid value: "claims.email": claims.email_verified must be used in claimMappings.username.expression or claimMappings.extra[*].valueExpression or claimValidationRules[*].expression when claims.email is used in claimMappings.username.expression`,
|
|
},
|
|
{
|
|
name: "valid claim mappings and uses email with verification via hasVerifiedEmail",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.email"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
UID: api.ClaimOrExpression{Expression: "claims.uid"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
usesEmailVerifiedClaim: true,
|
|
structuredAuthnFeatureEnabled: true,
|
|
wantCELMapper: true,
|
|
want: "",
|
|
},
|
|
{
|
|
name: "valid claim mappings that almost use claims.email",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.email_"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
UID: api.ClaimOrExpression{Expression: "claims.uid"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
wantCELMapper: true,
|
|
want: "",
|
|
},
|
|
{
|
|
name: "valid claim mappings that almost use claims.email via nesting",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.other.claims.email"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
UID: api.ClaimOrExpression{Expression: "claims.uid"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
wantCELMapper: true,
|
|
want: "",
|
|
},
|
|
{
|
|
name: "valid claim mappings",
|
|
in: api.ClaimMappings{
|
|
Username: api.PrefixedClaimOrExpression{Expression: "claims.username"},
|
|
Groups: api.PrefixedClaimOrExpression{Expression: "claims.groups"},
|
|
UID: api.ClaimOrExpression{Expression: "claims.uid"},
|
|
Extra: []api.ExtraMapping{
|
|
{Key: "example.org/foo", ValueExpression: "claims.extra"},
|
|
},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
wantCELMapper: true,
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
state := &validationState{usesEmailVerifiedClaim: tt.usesEmailVerifiedClaim}
|
|
got := validateClaimMappings(compiler, state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
|
|
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
|
fmt.Println(errString(got))
|
|
t.Fatalf("ClaimMappings validation mismatch (-want +got):\n%s", d)
|
|
}
|
|
if tt.wantCELMapper {
|
|
if len(tt.in.Username.Expression) > 0 && state.mapper.Username == nil {
|
|
t.Fatalf("ClaimMappings validation mismatch: CELMapper.Username is nil")
|
|
}
|
|
if len(tt.in.Groups.Expression) > 0 && state.mapper.Groups == nil {
|
|
t.Fatalf("ClaimMappings validation mismatch: CELMapper.Groups is nil")
|
|
}
|
|
if len(tt.in.UID.Expression) > 0 && state.mapper.UID == nil {
|
|
t.Fatalf("ClaimMappings validation mismatch: CELMapper.UID is nil")
|
|
}
|
|
if len(tt.in.Extra) > 0 && state.mapper.Extra == nil {
|
|
t.Fatalf("ClaimMappings validation mismatch: CELMapper.Extra is nil")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateUserValidationRules(t *testing.T) {
|
|
fldPath := field.NewPath("issuer", "userValidationRules")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
in []api.UserValidationRule
|
|
structuredAuthnFeatureEnabled bool
|
|
want string
|
|
wantCELMapper bool
|
|
}{
|
|
{
|
|
name: "user info validation rule, expression is empty",
|
|
in: []api.UserValidationRule{{}},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: "issuer.userValidationRules[0].expression: Required value: expression is required",
|
|
},
|
|
{
|
|
name: "duplicate expression",
|
|
in: []api.UserValidationRule{
|
|
{Expression: "user.username == 'foo'"},
|
|
{Expression: "user.username == 'foo'"},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.userValidationRules[1].expression: Duplicate value: "user.username == 'foo'"`,
|
|
},
|
|
{
|
|
name: "user validation rule is invalid when structured authn feature is disabled",
|
|
in: []api.UserValidationRule{
|
|
{Expression: "user.username == 'foo'"},
|
|
},
|
|
structuredAuthnFeatureEnabled: false,
|
|
want: `issuer.userValidationRules: Invalid value: "": user validation rules are not supported when StructuredAuthenticationConfiguration feature gate is disabled`,
|
|
},
|
|
{
|
|
name: "expression is invalid",
|
|
in: []api.UserValidationRule{
|
|
{Expression: "foo.bar"},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.userValidationRules[0].expression: Invalid value: "foo.bar": compilation failed: ERROR: <input>:1:1: undeclared reference to 'foo' (in container '')
|
|
| foo.bar
|
|
| ^`,
|
|
},
|
|
{
|
|
name: "expression does not return bool",
|
|
in: []api.UserValidationRule{
|
|
{Expression: "user.username"},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: `issuer.userValidationRules[0].expression: Invalid value: "user.username": must evaluate to bool`,
|
|
},
|
|
{
|
|
name: "valid user info validation rule",
|
|
in: []api.UserValidationRule{
|
|
{Expression: "user.username == 'foo'"},
|
|
{Expression: "!user.username.startsWith('system:')", Message: "username cannot used reserved system: prefix"},
|
|
},
|
|
structuredAuthnFeatureEnabled: true,
|
|
want: "",
|
|
wantCELMapper: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
state := &validationState{}
|
|
got := validateUserValidationRules(compiler, state, tt.in, fldPath, tt.structuredAuthnFeatureEnabled).ToAggregate()
|
|
if d := cmp.Diff(tt.want, errString(got)); d != "" {
|
|
t.Fatalf("UserValidationRules validation mismatch (-want +got):\n%s", d)
|
|
}
|
|
if tt.wantCELMapper && state.mapper.UserValidationRules == nil {
|
|
t.Fatalf("UserValidationRules validation mismatch: CELMapper.UserValidationRules is nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func errString(errs errors.Aggregate) string {
|
|
if errs != nil {
|
|
return errs.Error()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type (
|
|
test struct {
|
|
name string
|
|
configuration api.AuthorizationConfiguration
|
|
expectedErrList field.ErrorList
|
|
knownTypes sets.String
|
|
repeatableTypes sets.String
|
|
}
|
|
)
|
|
|
|
func TestValidateAuthorizationConfiguration(t *testing.T) {
|
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true)
|
|
|
|
badKubeConfigFile := "../some/relative/path/kubeconfig"
|
|
|
|
tempKubeConfigFile, err := os.CreateTemp("/tmp", "kubeconfig")
|
|
if err != nil {
|
|
t.Fatalf("failed to set up temp file: %v", err)
|
|
}
|
|
tempKubeConfigFilePath := tempKubeConfigFile.Name()
|
|
defer os.Remove(tempKubeConfigFilePath)
|
|
|
|
tests := []test{
|
|
{
|
|
name: "atleast one authorizer should be defined",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Required(field.NewPath("authorizers"), "at least one authorization mode must be defined")},
|
|
knownTypes: sets.NewString(),
|
|
repeatableTypes: sets.NewString(),
|
|
},
|
|
{
|
|
name: "type and name are required if an authorizer is defined",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Required(field.NewPath("type"), "")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "authorizer names should be of non-zero length",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Foo",
|
|
Name: "",
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Required(field.NewPath("name"), "")},
|
|
knownTypes: sets.NewString(string("Foo")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "authorizer names should be unique",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Foo",
|
|
Name: "foo",
|
|
},
|
|
{
|
|
Type: "Bar",
|
|
Name: "foo",
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Duplicate(field.NewPath("name"), "foo")},
|
|
knownTypes: sets.NewString(string("Foo"), string("Bar")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "authorizer names should be DNS1123 labels",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Foo",
|
|
Name: "myauthorizer",
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{},
|
|
knownTypes: sets.NewString(string("Foo")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "authorizer names should be DNS1123 subdomains",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Foo",
|
|
Name: "foo.example.domain",
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{},
|
|
knownTypes: sets.NewString(string("Foo")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "authorizer names should not be invalid DNS1123 labels or subdomains",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Foo",
|
|
Name: "FOO.example.domain",
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Invalid(field.NewPath("name"), "FOO.example.domain", "")},
|
|
knownTypes: sets.NewString(string("Foo")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "bare minimum configuration with Webhook",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
FailurePolicy: "NoOpinion",
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "bare minimum configuration with Webhook and MatchConditions",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
FailurePolicy: "NoOpinion",
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
MatchConditions: []api.WebhookMatchCondition{
|
|
{
|
|
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'",
|
|
},
|
|
{
|
|
Expression: "request.user == 'admin'",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "bare minimum configuration with multiple webhooks",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
FailurePolicy: "NoOpinion",
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: "Webhook",
|
|
Name: "second-webhook",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
FailurePolicy: "NoOpinion",
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "configuration with unknown types",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Foo",
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.NotSupported(field.NewPath("type"), "Foo", []string{"..."})},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "configuration with not repeatable types",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Foo",
|
|
Name: "foo-1",
|
|
},
|
|
{
|
|
Type: "Foo",
|
|
Name: "foo-2",
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Duplicate(field.NewPath("type"), "Foo")},
|
|
knownTypes: sets.NewString(string("Foo")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "when type=Webhook, webhook needs to be defined",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Required(field.NewPath("webhook"), "required when type=Webhook")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "when type!=Webhook, webhooks needs to be nil",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Foo",
|
|
Name: "foo",
|
|
Webhook: &api.WebhookConfiguration{},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Invalid(field.NewPath("webhook"), "non-null", "may only be specified when type=Webhook")},
|
|
knownTypes: sets.NewString(string("Foo")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "timeout should be specified",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
FailurePolicy: "NoOpinion",
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Required(field.NewPath("timeout"), "")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
//
|
|
{
|
|
name: "timeout shouldn't be zero",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
FailurePolicy: "NoOpinion",
|
|
Timeout: metav1.Duration{Duration: 0 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Required(field.NewPath("timeout"), "")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "timeout shouldn't be negative",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
FailurePolicy: "NoOpinion",
|
|
Timeout: metav1.Duration{Duration: -30 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Invalid(field.NewPath("timeout"), time.Duration(-30*time.Second).String(), "must be > 0s and <= 30s")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "timeout shouldn't be greater than 30seconds",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
FailurePolicy: "NoOpinion",
|
|
Timeout: metav1.Duration{Duration: 60 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Invalid(field.NewPath("timeout"), time.Duration(60*time.Second).String(), "must be > 0s and <= 30s")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "authorizedTTL should be defined ",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
FailurePolicy: "NoOpinion",
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Required(field.NewPath("authorizedTTL"), "")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "authorizedTTL shouldn't be negative",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
FailurePolicy: "NoOpinion",
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: -30 * time.Second},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Invalid(field.NewPath("authorizedTTL"), time.Duration(-30*time.Second).String(), "must be > 0s")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "unauthorizedTTL should be defined ",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
FailurePolicy: "NoOpinion",
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Required(field.NewPath("unauthorizedTTL"), "")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "unauthorizedTTL shouldn't be negative",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
FailurePolicy: "NoOpinion",
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: -30 * time.Second},
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Invalid(field.NewPath("unauthorizedTTL"), time.Duration(-30*time.Second).String(), "must be > 0s")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "SAR should be defined",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
FailurePolicy: "NoOpinion",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Required(field.NewPath("subjectAccessReviewVersion"), "")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "SAR should be one of v1 and v1beta1",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
FailurePolicy: "NoOpinion",
|
|
SubjectAccessReviewVersion: "v2beta1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.NotSupported(field.NewPath("subjectAccessReviewVersion"), "v2beta1", []string{"v1", "v1beta1"})},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "MatchConditionSAR should be defined",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
FailurePolicy: "NoOpinion",
|
|
SubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
MatchConditions: []api.WebhookMatchCondition{{Expression: "true"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Required(field.NewPath("matchConditionSubjectAccessReviewVersion"), "")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "MatchConditionSAR must not be anything other than v1",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
FailurePolicy: "NoOpinion",
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1beta1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.NotSupported(field.NewPath("matchConditionSubjectAccessReviewVersion"), "v1beta1", []string{"v1"})},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "failurePolicy should be defined",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Required(field.NewPath("failurePolicy"), "")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "failurePolicy should be one of \"NoOpinion\" or \"Deny\"",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
FailurePolicy: "AlwaysAllow",
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.NotSupported(field.NewPath("failurePolicy"), "AlwaysAllow", []string{"NoOpinion", "Deny"})},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "connectionInfo should be defined",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
FailurePolicy: "NoOpinion",
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Required(field.NewPath("connectionInfo"), "")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "connectionInfo should be one of InClusterConfig or KubeConfigFile",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
FailurePolicy: "NoOpinion",
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "ExternalClusterConfig",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{
|
|
field.NotSupported(field.NewPath("connectionInfo"), api.WebhookConnectionInfo{Type: "ExternalClusterConfig"}, []string{"InClusterConfig", "KubeConfigFile"}),
|
|
},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "if connectionInfo=InClusterConfig, then kubeConfigFile should be nil",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
FailurePolicy: "NoOpinion",
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "InClusterConfig",
|
|
KubeConfigFile: new(string),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{
|
|
field.Invalid(field.NewPath("connectionInfo", "kubeConfigFile"), "", "can only be set when type=KubeConfigFile"),
|
|
},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "if connectionInfo=KubeConfigFile, then KubeConfigFile should be defined",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
FailurePolicy: "NoOpinion",
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "KubeConfigFile",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Required(field.NewPath("kubeConfigFile"), "")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "if connectionInfo=KubeConfigFile, then KubeConfigFile should be defined, must be an absolute path, should exist, shouldn't be a symlink",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
FailurePolicy: "NoOpinion",
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "KubeConfigFile",
|
|
KubeConfigFile: &badKubeConfigFile,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{field.Invalid(field.NewPath("kubeConfigFile"), badKubeConfigFile, "must be an absolute path")},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
{
|
|
name: "if connectionInfo=KubeConfigFile, an existent file needs to be passed",
|
|
configuration: api.AuthorizationConfiguration{
|
|
Authorizers: []api.AuthorizerConfiguration{
|
|
{
|
|
Type: "Webhook",
|
|
Name: "default",
|
|
Webhook: &api.WebhookConfiguration{
|
|
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
|
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
|
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
|
FailurePolicy: "NoOpinion",
|
|
SubjectAccessReviewVersion: "v1",
|
|
MatchConditionSubjectAccessReviewVersion: "v1",
|
|
ConnectionInfo: api.WebhookConnectionInfo{
|
|
Type: "KubeConfigFile",
|
|
KubeConfigFile: &tempKubeConfigFilePath,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErrList: field.ErrorList{},
|
|
knownTypes: sets.NewString(string("Webhook")),
|
|
repeatableTypes: sets.NewString(string("Webhook")),
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
errList := ValidateAuthorizationConfiguration(nil, &test.configuration, test.knownTypes, test.repeatableTypes)
|
|
if len(errList) != len(test.expectedErrList) {
|
|
t.Errorf("expected %d errs, got %d, errors %v", len(test.expectedErrList), len(errList), errList)
|
|
}
|
|
if len(errList) == len(test.expectedErrList) {
|
|
for i, expected := range test.expectedErrList {
|
|
if expected.Type.String() != errList[i].Type.String() {
|
|
t.Errorf("expected err type %s, got %s",
|
|
expected.Type.String(),
|
|
errList[i].Type.String())
|
|
}
|
|
if expected.BadValue != errList[i].BadValue {
|
|
t.Errorf("expected bad value '%s', got '%s'",
|
|
expected.BadValue,
|
|
errList[i].BadValue)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
func TestValidateAndCompileMatchConditions(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
matchConditions []api.WebhookMatchCondition
|
|
featureEnabled bool
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "match conditions are used With feature enabled",
|
|
matchConditions: []api.WebhookMatchCondition{
|
|
{
|
|
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'",
|
|
},
|
|
{
|
|
Expression: "request.user == 'admin'",
|
|
},
|
|
},
|
|
featureEnabled: true,
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
name: "should fail when match conditions are used without feature enabled",
|
|
matchConditions: []api.WebhookMatchCondition{
|
|
{
|
|
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'",
|
|
},
|
|
{
|
|
Expression: "request.user == 'admin'",
|
|
},
|
|
},
|
|
featureEnabled: false,
|
|
expectedErr: `matchConditions: Invalid value: "": matchConditions are not supported when StructuredAuthorizationConfiguration feature gate is disabled`,
|
|
},
|
|
{
|
|
name: "no matchConditions should not require feature enablement",
|
|
matchConditions: []api.WebhookMatchCondition{},
|
|
featureEnabled: false,
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
name: "match conditions with invalid expressions",
|
|
matchConditions: []api.WebhookMatchCondition{
|
|
{
|
|
Expression: " ",
|
|
},
|
|
},
|
|
featureEnabled: true,
|
|
expectedErr: "matchConditions[0].expression: Required value",
|
|
},
|
|
{
|
|
name: "match conditions with duplicate expressions",
|
|
matchConditions: []api.WebhookMatchCondition{
|
|
{
|
|
Expression: "request.user == 'admin'",
|
|
},
|
|
{
|
|
Expression: "request.user == 'admin'",
|
|
},
|
|
},
|
|
featureEnabled: true,
|
|
expectedErr: `matchConditions[1].expression: Duplicate value: "request.user == 'admin'"`,
|
|
},
|
|
{
|
|
name: "match conditions with undeclared reference",
|
|
matchConditions: []api.WebhookMatchCondition{
|
|
{
|
|
Expression: "test",
|
|
},
|
|
},
|
|
featureEnabled: true,
|
|
expectedErr: "matchConditions[0].expression: Invalid value: \"test\": compilation failed: ERROR: <input>:1:1: undeclared reference to 'test' (in container '')\n | test\n | ^",
|
|
},
|
|
{
|
|
name: "match conditions with bad return type",
|
|
matchConditions: []api.WebhookMatchCondition{
|
|
{
|
|
Expression: "request.user = 'test'",
|
|
},
|
|
},
|
|
featureEnabled: true,
|
|
expectedErr: "matchConditions[0].expression: Invalid value: \"request.user = 'test'\": compilation failed: ERROR: <input>:1:14: Syntax error: token recognition error at: '= '\n | request.user = 'test'\n | .............^\nERROR: <input>:1:16: Syntax error: extraneous input ''test'' expecting <EOF>\n | request.user = 'test'\n | ...............^",
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, tt.featureEnabled)
|
|
celMatcher, errList := ValidateAndCompileMatchConditions(tt.matchConditions)
|
|
if len(tt.expectedErr) == 0 && len(tt.matchConditions) > 0 && len(errList) == 0 && celMatcher == nil {
|
|
t.Errorf("celMatcher should not be nil when there are matchCondition and no error returned")
|
|
}
|
|
got := errList.ToAggregate()
|
|
if d := cmp.Diff(tt.expectedErr, errString(got)); d != "" {
|
|
t.Fatalf("ValidateAndCompileMatchConditions validation mismatch (-want +got):\n%s", d)
|
|
}
|
|
})
|
|
}
|
|
}
|