Skip to content

Commit 017f788

Browse files
authored
Merge pull request #258 from datum-cloud/fix/257-paranoia-default
2 parents f3a0132 + b5561bd commit 017f788

3 files changed

Lines changed: 143 additions & 2 deletions

File tree

api/v1alpha/trafficprotectionpolicy_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ type OWASPCRS struct {
9191
// ParanoiaLevels specifies the OWASP ModSecurity Core Rule Set (CRS)
9292
// paranoia levels to use.
9393
//
94-
// +kubebuilder:default={}
94+
// +kubebuilder:default={blocking:1,detection:1}
9595
ParanoiaLevels ParanoiaLevels `json:"paranoiaLevels,omitempty"`
9696

9797
// ScoreThresholds specifies the OWASP ModSecurity Core Rule Set (CRS)

config/crd/bases/networking.datumapis.com_trafficprotectionpolicies.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ spec:
6767
Core Rule Set (CRS).
6868
properties:
6969
paranoiaLevels:
70-
default: {}
70+
default:
71+
blocking: 1
72+
detection: 1
7173
description: |-
7274
ParanoiaLevels specifies the OWASP ModSecurity Core Rule Set (CRS)
7375
paranoia levels to use.

test/crd/crd_defaults_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
3+
// Package crd runs the generated CRDs against a real apiserver (envtest) to
4+
// guard invariants the fake client cannot enforce. The apiserver validates a
5+
// schema node's structural default against that node's own CEL rules at CRD
6+
// registration, so an invalid default is rejected here — the fake client used
7+
// by the rest of the suite never sees schema, defaults, or CEL.
8+
//
9+
// Regression guard for #257: a paranoiaLevels default of {} violated the
10+
// detection>=blocking rule added in #251, so the apiserver rejected the whole
11+
// CRD ("no such key: detection") and every deploy that applied it failed.
12+
package crd
13+
14+
import (
15+
"context"
16+
"fmt"
17+
"os"
18+
"path/filepath"
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
apierrors "k8s.io/apimachinery/pkg/api/errors"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/apimachinery/pkg/runtime"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
"sigs.k8s.io/controller-runtime/pkg/envtest"
28+
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
29+
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
30+
31+
networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha"
32+
)
33+
34+
// testClient is nil when KUBEBUILDER_ASSETS is unset (plain `go test` without
35+
// envtest binaries); tests then skip rather than fail. `make test` provides the
36+
// assets, so CI exercises them.
37+
var testClient client.Client
38+
39+
func TestMain(m *testing.M) {
40+
if os.Getenv("KUBEBUILDER_ASSETS") == "" {
41+
os.Exit(m.Run())
42+
}
43+
44+
env := &envtest.Environment{
45+
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
46+
ErrorIfCRDPathMissing: true,
47+
}
48+
cfg, err := env.Start()
49+
if err != nil {
50+
fmt.Fprintf(os.Stderr, "envtest start (installs generated CRDs): %v\n", err)
51+
os.Exit(1)
52+
}
53+
54+
scheme := runtime.NewScheme()
55+
if err := networkingv1alpha.AddToScheme(scheme); err != nil {
56+
fmt.Fprintf(os.Stderr, "add scheme: %v\n", err)
57+
os.Exit(1)
58+
}
59+
testClient, err = client.New(cfg, client.Options{Scheme: scheme})
60+
if err != nil {
61+
fmt.Fprintf(os.Stderr, "build client: %v\n", err)
62+
os.Exit(1)
63+
}
64+
65+
code := m.Run()
66+
_ = env.Stop()
67+
os.Exit(code)
68+
}
69+
70+
func requireEnv(t *testing.T) client.Client {
71+
t.Helper()
72+
if testClient == nil {
73+
t.Skip("KUBEBUILDER_ASSETS unset; run via `make test` to exercise envtest")
74+
}
75+
return testClient
76+
}
77+
78+
func gatewayTargetRef(name string) gatewayv1alpha2.LocalPolicyTargetReferenceWithSectionName {
79+
return gatewayv1alpha2.LocalPolicyTargetReferenceWithSectionName{
80+
LocalPolicyTargetReference: gatewayv1.LocalPolicyTargetReference{
81+
Group: gatewayv1.GroupName,
82+
Kind: gatewayv1.Kind("Gateway"),
83+
Name: gatewayv1.ObjectName(name),
84+
},
85+
}
86+
}
87+
88+
// TestTPPCRDInstallsAndDefaults asserts the generated CRD registers against a
89+
// real apiserver — an invalid structural default (the #257 regression) would
90+
// reject it during envtest start — and that a TPP created with only its
91+
// required targetRef defaults paranoiaLevels to a pair satisfying
92+
// detection>=blocking.
93+
func TestTPPCRDInstallsAndDefaults(t *testing.T) {
94+
cl := requireEnv(t)
95+
ctx := context.Background()
96+
97+
tpp := &networkingv1alpha.TrafficProtectionPolicy{
98+
ObjectMeta: metav1.ObjectMeta{Name: "defaults", Namespace: "default"},
99+
Spec: networkingv1alpha.TrafficProtectionPolicySpec{
100+
TargetRefs: []gatewayv1alpha2.LocalPolicyTargetReferenceWithSectionName{gatewayTargetRef("gw")},
101+
},
102+
}
103+
require.NoError(t, cl.Create(ctx, tpp))
104+
t.Cleanup(func() { _ = cl.Delete(ctx, tpp) })
105+
106+
var got networkingv1alpha.TrafficProtectionPolicy
107+
require.NoError(t, cl.Get(ctx, client.ObjectKeyFromObject(tpp), &got))
108+
require.Len(t, got.Spec.RuleSets, 1)
109+
110+
pl := got.Spec.RuleSets[0].OWASPCoreRuleSet.ParanoiaLevels
111+
assert.Equal(t, 1, pl.Blocking, "paranoiaLevels.blocking must default")
112+
assert.Equal(t, 1, pl.Detection, "paranoiaLevels.detection must default")
113+
assert.GreaterOrEqual(t, pl.Detection, pl.Blocking,
114+
"the defaulted pair must satisfy the detection>=blocking rule")
115+
}
116+
117+
// TestTPPRejectsInvertedParanoia asserts the detection>=blocking CEL rule from
118+
// #251 still rejects a user-supplied inverted pair, so the #257 default fix did
119+
// not loosen the validation it depends on.
120+
func TestTPPRejectsInvertedParanoia(t *testing.T) {
121+
cl := requireEnv(t)
122+
ctx := context.Background()
123+
124+
tpp := &networkingv1alpha.TrafficProtectionPolicy{
125+
ObjectMeta: metav1.ObjectMeta{Name: "inverted", Namespace: "default"},
126+
Spec: networkingv1alpha.TrafficProtectionPolicySpec{
127+
TargetRefs: []gatewayv1alpha2.LocalPolicyTargetReferenceWithSectionName{gatewayTargetRef("gw")},
128+
RuleSets: []networkingv1alpha.TrafficProtectionPolicyRuleSet{{
129+
Type: networkingv1alpha.TrafficProtectionPolicyOWASPCoreRuleSet,
130+
OWASPCoreRuleSet: networkingv1alpha.OWASPCRS{
131+
ParanoiaLevels: networkingv1alpha.ParanoiaLevels{Blocking: 3, Detection: 1},
132+
},
133+
}},
134+
},
135+
}
136+
err := cl.Create(ctx, tpp)
137+
require.Error(t, err, "detection<blocking must be rejected")
138+
assert.Truef(t, apierrors.IsInvalid(err), "expected an Invalid error, got %v", err)
139+
}

0 commit comments

Comments
 (0)