Skip to content

Commit f144d32

Browse files
committed
Add CEL validation to WorkspaceAuthenticationConfig
Signed-off-by: Nelo-T. Wallus <[email protected]> Signed-off-by: Nelo-T. Wallus <[email protected]>
1 parent f49f89b commit f144d32

File tree

2 files changed

+160
-8
lines changed

2 files changed

+160
-8
lines changed

sdk/apis/tenancy/v1alpha1/types_workspaceauthentication.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,37 +102,59 @@ const (
102102
)
103103

104104
// ClaimValidationRule provides the configuration for a single claim validation rule.
105+
// +kubebuilder:validation:XValidation:rule="has(self.claim) || has(self.expression)",message="either claim or expression must be specified"
106+
// +kubebuilder:validation:XValidation:rule="!(has(self.claim) && has(self.expression))",message="claim and expression cannot both be specified"
107+
// +kubebuilder:validation:XValidation:rule="(has(self.expression) && !has(self.requiredValue)) || (has(self.claim) && has(self.requiredValue))",message="requiredValue can only be specified when claim is specified"
108+
// +kubebuilder:validation:XValidation:rule="(has(self.expression) && has(self.message)) || (has(self.claim) && !has(self.message))",message="message can only be specified when expression is specified"
105109
type ClaimValidationRule struct {
106-
Claim string `json:"claim"`
107-
RequiredValue string `json:"requiredValue"`
110+
// +optional
111+
// +kubebuilder:validation:MinLength=1
112+
Claim string `json:"claim,omitempty"`
113+
// +optional
114+
// +kubebuilder:validation:MinLength=1
115+
RequiredValue string `json:"requiredValue,omitempty"`
108116

109-
Expression string `json:"expression"`
110-
Message string `json:"message"`
117+
// +optional
118+
// +kubebuilder:validation:MinLength=1
119+
Expression string `json:"expression,omitempty"`
120+
// +optional
121+
// +kubebuilder:validation:MinLength=1
122+
Message string `json:"message,omitempty"`
111123
}
112124

113125
// ClaimMappings provides the configuration for claim mapping.
114126
type ClaimMappings struct {
115-
Username PrefixedClaimOrExpression `json:"username"`
116-
Groups PrefixedClaimOrExpression `json:"groups"`
127+
Username PrefixedClaimOrExpression `json:"username,omitempty"`
128+
Groups PrefixedClaimOrExpression `json:"groups,omitempty"`
117129
// +optional
118130
UID ClaimOrExpression `json:"uid,omitempty"`
119131
// +optional
120132
Extra []ExtraMapping `json:"extra,omitempty"`
121133
}
122134

123135
// PrefixedClaimOrExpression provides the configuration for a single prefixed claim or expression.
136+
// +kubebuilder:validation:XValidation:rule="has(self.claim) || has(self.expression)",message="either claim or expression must be specified"
137+
// +kubebuilder:validation:XValidation:rule="!(has(self.claim) && has(self.expression))",message="claim and expression cannot both be specified"
138+
// +kubebuilder:validation:XValidation:rule="!(has(self.prefix)) || has(self.claim)",message="prefix can only be specified when claim is specified"
124139
type PrefixedClaimOrExpression struct {
125-
Claim string `json:"claim"`
140+
// +optional
141+
// +kubebuilder:validation:MinLength=1
142+
Claim string `json:"claim,omitempty"`
126143
// +optional
127144
Prefix *string `json:"prefix,omitempty"`
128145
// +optional
146+
// +kubebuilder:validation:MinLength=1
129147
Expression string `json:"expression,omitempty"`
130148
}
131149

132150
// ClaimOrExpression provides the configuration for a single claim or expression.
151+
// +kubebuilder:validation:XValidation:rule="!(has(self.claim) && has(self.expression))",message="claim and expression cannot both be specified"
133152
type ClaimOrExpression struct {
134-
Claim string `json:"claim"`
135153
// +optional
154+
// +kubebuilder:validation:MinLength=1
155+
Claim string `json:"claim,omitempty"`
156+
// +optional
157+
// +kubebuilder:validation:MinLength=1
136158
Expression string `json:"expression,omitempty"`
137159
}
138160

test/e2e/authentication/workspace_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
rbacv1 "k8s.io/api/rbac/v1"
3131
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3232
"k8s.io/apimachinery/pkg/util/wait"
33+
"k8s.io/utils/ptr"
3334

3435
kcpkubernetesclientset "github.com/kcp-dev/client-go/kubernetes"
3536
"github.com/kcp-dev/logicalcluster/v3"
@@ -419,6 +420,135 @@ func TestForbiddenSystemAccess(t *testing.T) {
419420
}
420421
}
421422

423+
func TestAcceptableWorkspaceAuthenticationConfigurations(t *testing.T) {
424+
framework.Suite(t, "control-plane")
425+
426+
// start kcp and setup clients
427+
server := kcptesting.SharedKcpServer(t)
428+
429+
wsPath, _ := kcptesting.NewWorkspaceFixture(t, server, logicalcluster.NewPath("root"), kcptesting.WithNamePrefix("oidc-acceptable"))
430+
431+
kcpConfig := server.BaseConfig(t)
432+
kcpClusterClient, err := kcpclientset.NewForConfig(kcpConfig)
433+
require.NoError(t, err)
434+
435+
testcases := map[string]struct {
436+
authConfig *tenancyv1alpha1.WorkspaceAuthenticationConfiguration
437+
expectedError string
438+
}{
439+
"empty": {
440+
authConfig: &tenancyv1alpha1.WorkspaceAuthenticationConfiguration{},
441+
expectedError: "spec.jwt: Required value",
442+
},
443+
"minimal-claim": {
444+
authConfig: &tenancyv1alpha1.WorkspaceAuthenticationConfiguration{
445+
Spec: tenancyv1alpha1.WorkspaceAuthenticationConfigurationSpec{
446+
JWT: []tenancyv1alpha1.JWTAuthenticator{
447+
{
448+
Issuer: tenancyv1alpha1.Issuer{
449+
URL: "https://example.com",
450+
},
451+
ClaimMappings: tenancyv1alpha1.ClaimMappings{
452+
Username: tenancyv1alpha1.PrefixedClaimOrExpression{
453+
Claim: "email",
454+
},
455+
Groups: tenancyv1alpha1.PrefixedClaimOrExpression{
456+
Claim: "groups",
457+
},
458+
},
459+
},
460+
},
461+
},
462+
},
463+
},
464+
"minimal-expression": {
465+
authConfig: &tenancyv1alpha1.WorkspaceAuthenticationConfiguration{
466+
Spec: tenancyv1alpha1.WorkspaceAuthenticationConfigurationSpec{
467+
JWT: []tenancyv1alpha1.JWTAuthenticator{
468+
{
469+
Issuer: tenancyv1alpha1.Issuer{
470+
URL: "https://example.com",
471+
},
472+
ClaimMappings: tenancyv1alpha1.ClaimMappings{
473+
Username: tenancyv1alpha1.PrefixedClaimOrExpression{
474+
Expression: "concat('oidc:', email)",
475+
},
476+
Groups: tenancyv1alpha1.PrefixedClaimOrExpression{
477+
Expression: "groups",
478+
},
479+
},
480+
},
481+
},
482+
},
483+
},
484+
},
485+
"claim-and-expression": {
486+
authConfig: &tenancyv1alpha1.WorkspaceAuthenticationConfiguration{
487+
Spec: tenancyv1alpha1.WorkspaceAuthenticationConfigurationSpec{
488+
JWT: []tenancyv1alpha1.JWTAuthenticator{
489+
{
490+
Issuer: tenancyv1alpha1.Issuer{
491+
URL: "https://example.com",
492+
},
493+
ClaimMappings: tenancyv1alpha1.ClaimMappings{
494+
Username: tenancyv1alpha1.PrefixedClaimOrExpression{
495+
Claim: "email",
496+
Expression: "concat('oidc:', email)",
497+
},
498+
Groups: tenancyv1alpha1.PrefixedClaimOrExpression{
499+
Claim: "roles",
500+
Expression: "groups",
501+
},
502+
},
503+
},
504+
},
505+
},
506+
},
507+
expectedError: "claim and expression cannot both be specified",
508+
},
509+
"expression-and-prefix": {
510+
authConfig: &tenancyv1alpha1.WorkspaceAuthenticationConfiguration{
511+
Spec: tenancyv1alpha1.WorkspaceAuthenticationConfigurationSpec{
512+
JWT: []tenancyv1alpha1.JWTAuthenticator{
513+
{
514+
Issuer: tenancyv1alpha1.Issuer{
515+
URL: "https://example.com",
516+
},
517+
ClaimMappings: tenancyv1alpha1.ClaimMappings{
518+
Username: tenancyv1alpha1.PrefixedClaimOrExpression{
519+
Prefix: ptr.To("random-prefix:"),
520+
Expression: "concat('oidc:', email)",
521+
},
522+
Groups: tenancyv1alpha1.PrefixedClaimOrExpression{
523+
Prefix: ptr.To("group-random-prefix:"),
524+
Expression: "groups",
525+
},
526+
},
527+
},
528+
},
529+
},
530+
},
531+
expectedError: "prefix can only be specified when claim is specified",
532+
},
533+
}
534+
535+
for name, tc := range testcases {
536+
t.Run(name, func(t *testing.T) {
537+
t.Parallel()
538+
539+
t.Logf("Creating WorkspaceAuthenticationConfguration %s...", name)
540+
tc.authConfig.Name = name
541+
_, err := kcpClusterClient.Cluster(wsPath).TenancyV1alpha1().WorkspaceAuthenticationConfigurations().Create(t.Context(), tc.authConfig, metav1.CreateOptions{})
542+
if tc.expectedError != "" {
543+
require.Error(t, err)
544+
require.Contains(t, err.Error(), tc.expectedError)
545+
} else {
546+
require.NoError(t, err)
547+
}
548+
})
549+
}
550+
}
551+
422552
func createWorkspaceAuthentication(t *testing.T, ctx context.Context, client kcpclientset.ClusterInterface, workspace logicalcluster.Path, mock *mockoidc.MockOIDC, ca *crypto.CA) string {
423553
name := fmt.Sprintf("mockoidc-%d", rand.Int())
424554

0 commit comments

Comments
 (0)