diff --git a/api/v1/manager_types.go b/api/v1/manager_types.go index f338ce0d99..d5d887e282 100644 --- a/api/v1/manager_types.go +++ b/api/v1/manager_types.go @@ -30,6 +30,12 @@ type ManagerSpec struct { // RBACUI configures the RBAC management UI feature. // +optional RBACUI *RBACUI `json:"rbacUI,omitempty"` + + // WAFUI configures the WAF management UI feature. This controls visibility of + // the WAF management UI only; it does not enable WAF enforcement on traffic + // (that is configured separately via GatewayAPI.spec.extensions.waf). + // +optional + WAFUI *WAFUI `json:"wafUI,omitempty"` } // +kubebuilder:validation:Enum=Enabled;Disabled @@ -59,6 +65,34 @@ func (m *Manager) RBACManagementEnabled() bool { return *m.Spec.RBACUI.State == RBACUIEnabled } +// +kubebuilder:validation:Enum=Enabled;Disabled +type WAFUIStatusType string + +const ( + WAFUIDisabled WAFUIStatusType = "Disabled" + WAFUIEnabled WAFUIStatusType = "Enabled" +) + +// WAFUI configures the WAF management UI. This turns the WAF management surface +// in the Calico Enterprise manager on or off. It controls UI visibility only, +// and does not enable WAF enforcement on traffic, which is configured +// separately via GatewayAPI.spec.extensions.waf. +type WAFUI struct { + // State turns the WAF management UI on or off. Defaults to Disabled. + // +optional + State *WAFUIStatusType `json:"state,omitempty"` +} + +// WAFManagementEnabled returns true when the Manager CR enables the WAF +// management UI. Safe to call on a nil receiver; returns false for a nil +// Manager, an unset WAFUI, or any state other than Enabled. +func (m *Manager) WAFManagementEnabled() bool { + if m == nil || m.Spec.WAFUI == nil || m.Spec.WAFUI.State == nil { + return false + } + return *m.Spec.WAFUI.State == WAFUIEnabled +} + // ManagerDeployment is the configuration for the Manager Deployment. type ManagerDeployment struct { // Spec is the specification of the Manager Deployment. diff --git a/api/v1/manager_types_test.go b/api/v1/manager_types_test.go index 9a59442dff..0851252177 100644 --- a/api/v1/manager_types_test.go +++ b/api/v1/manager_types_test.go @@ -49,3 +49,37 @@ func TestRBACManagementEnabled(t *testing.T) { }) } } + +func TestWAFManagementEnabled(t *testing.T) { + state := func(s WAFUIStatusType) *WAFUIStatusType { return &s } + + for _, tc := range []struct { + name string + m *Manager + want bool + }{ + // Nil paths: WAFManagementEnabled is called as managerCR.WAFManagementEnabled() + // where managerCR is nil when no Manager CR exists, so a nil receiver (and each + // nil field below) must return false rather than panic. + {name: "nil Manager", m: nil, want: false}, + {name: "nil WAFUI", m: &Manager{}, want: false}, + {name: "nil State", m: &Manager{Spec: ManagerSpec{WAFUI: &WAFUI{}}}, want: false}, + + {name: "State Enabled", m: &Manager{Spec: ManagerSpec{WAFUI: &WAFUI{State: state(WAFUIEnabled)}}}, want: true}, + {name: "State Disabled", m: &Manager{Spec: ManagerSpec{WAFUI: &WAFUI{State: state(WAFUIDisabled)}}}, want: false}, + // Any value other than Enabled is off (the Enum marker rejects this at the + // apiserver, but the helper must not treat a non-empty value as enabled). + {name: "State unrecognized value", m: &Manager{Spec: ManagerSpec{WAFUI: &WAFUI{State: state("SomethingElse")}}}, want: false}, + } { + t.Run(tc.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("WAFManagementEnabled panicked on %s: %v", tc.name, r) + } + }() + if got := tc.m.WAFManagementEnabled(); got != tc.want { + t.Errorf("WAFManagementEnabled() = %v, want %v", got, tc.want) + } + }) + } +} diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 30e6ba5f6c..2715e88203 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -7932,6 +7932,11 @@ func (in *ManagerSpec) DeepCopyInto(out *ManagerSpec) { *out = new(RBACUI) (*in).DeepCopyInto(*out) } + if in.WAFUI != nil { + in, out := &in.WAFUI, &out.WAFUI + *out = new(WAFUI) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagerSpec. @@ -9983,6 +9988,26 @@ func (in *WAFExtensionSpec) DeepCopy() *WAFExtensionSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WAFUI) DeepCopyInto(out *WAFUI) { + *out = *in + if in.State != nil { + in, out := &in.State, &out.State + *out = new(WAFUIStatusType) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WAFUI. +func (in *WAFUI) DeepCopy() *WAFUI { + if in == nil { + return nil + } + out := new(WAFUI) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Whisker) DeepCopyInto(out *Whisker) { *out = *in diff --git a/pkg/imports/crds/operator/operator.tigera.io_managers.yaml b/pkg/imports/crds/operator/operator.tigera.io_managers.yaml index 66c1345bf4..d78d7f0e41 100644 --- a/pkg/imports/crds/operator/operator.tigera.io_managers.yaml +++ b/pkg/imports/crds/operator/operator.tigera.io_managers.yaml @@ -310,6 +310,21 @@ spec: - Disabled type: string type: object + wafUI: + description: |- + WAFUI configures the WAF management UI feature. This controls visibility of + the WAF management UI only; it does not enable WAF enforcement on traffic + (that is configured separately via GatewayAPI.spec.extensions.waf). + properties: + state: + description: + State turns the WAF management UI on or off. Defaults + to Disabled. + enum: + - Enabled + - Disabled + type: string + type: object type: object status: description: Most recently observed state for the Calico Enterprise manager. diff --git a/pkg/render/manager.go b/pkg/render/manager.go index 06512c4f16..b84f3f8c7d 100644 --- a/pkg/render/manager.go +++ b/pkg/render/manager.go @@ -778,6 +778,7 @@ func (c *managerComponent) managerUIAPIsContainer() corev1.Container { {Name: "ELASTIC_KIBANA_DISABLED", Value: strconv.FormatBool(c.cfg.Tenant.MultiTenant())}, {Name: "VOLTRON_URL", Value: ManagerService(c.cfg.Tenant)}, {Name: "RBAC_UI_ENABLED", Value: strconv.FormatBool(c.cfg.Manager.RBACManagementEnabled() && !c.cfg.Tenant.MultiTenant())}, + {Name: "WAF_UI_ENABLED", Value: strconv.FormatBool(c.cfg.Manager.WAFManagementEnabled() && !c.cfg.Tenant.MultiTenant())}, } // Determine the Linseed location. Use code default unless in multi-tenant mode, diff --git a/pkg/render/manager_test.go b/pkg/render/manager_test.go index 5579011ec4..161babc09e 100644 --- a/pkg/render/manager_test.go +++ b/pkg/render/manager_test.go @@ -163,6 +163,7 @@ var _ = Describe("Tigera Secure Manager rendering tests", func() { {Name: "ELASTIC_KIBANA_DISABLED", Value: "false"}, {Name: "VOLTRON_URL", Value: render.ManagerService(nil)}, {Name: "RBAC_UI_ENABLED", Value: "false"}, + {Name: "WAF_UI_ENABLED", Value: "false"}, } Expect(uiAPIs.Env).To(Equal(uiAPIsExpectedEnvVars)) @@ -1962,6 +1963,100 @@ var _ = Describe("Tigera Secure Manager rendering tests", func() { }) }) }) + + Context("WAF management UI", func() { + var installation *operatorv1.InstallationSpec + BeforeEach(func() { + replicas := int32(1) + installation = &operatorv1.InstallationSpec{ + ControlPlaneReplicas: &replicas, + Variant: operatorv1.CalicoEnterprise, + Registry: "testregistry.com/", + } + }) + + wafUIEnabledEnv := func(d *appsv1.Deployment) corev1.EnvVar { + for _, c := range d.Spec.Template.Spec.Containers { + if c.Name == render.UIAPIsName { + for _, e := range c.Env { + if e.Name == "WAF_UI_ENABLED" { + return e + } + } + } + } + return corev1.EnvVar{} + } + + It("renders WAF_UI_ENABLED=false when the Manager is unset", func() { + resources, _ := renderObjects(renderConfig{ + installation: installation, + ns: render.ManagerNamespace, + }) + d := rtest.GetResource(resources, render.ManagerDeploymentName, render.ManagerNamespace, appsv1.GroupName, "v1", "Deployment").(*appsv1.Deployment) + Expect(wafUIEnabledEnv(d)).To(Equal(corev1.EnvVar{Name: "WAF_UI_ENABLED", Value: "false"})) + }) + + It("renders WAF_UI_ENABLED=false when the Manager exists but wafUI is unset", func() { + resources, _ := renderObjects(renderConfig{ + installation: installation, + ns: render.ManagerNamespace, + manager: &operatorv1.Manager{Spec: operatorv1.ManagerSpec{}}, + }) + d := rtest.GetResource(resources, render.ManagerDeploymentName, render.ManagerNamespace, appsv1.GroupName, "v1", "Deployment").(*appsv1.Deployment) + Expect(wafUIEnabledEnv(d)).To(Equal(corev1.EnvVar{Name: "WAF_UI_ENABLED", Value: "false"})) + }) + + It("renders WAF_UI_ENABLED=true when wafUI.state is Enabled", func() { + resources, _ := renderObjects(renderConfig{ + installation: installation, + ns: render.ManagerNamespace, + manager: &operatorv1.Manager{ + Spec: operatorv1.ManagerSpec{ + WAFUI: &operatorv1.WAFUI{State: ptr.To(operatorv1.WAFUIEnabled)}, + }, + }, + }) + d := rtest.GetResource(resources, render.ManagerDeploymentName, render.ManagerNamespace, appsv1.GroupName, "v1", "Deployment").(*appsv1.Deployment) + Expect(wafUIEnabledEnv(d)).To(Equal(corev1.EnvVar{Name: "WAF_UI_ENABLED", Value: "true"})) + }) + + It("renders WAF_UI_ENABLED=false when wafUI.state is Disabled", func() { + resources, _ := renderObjects(renderConfig{ + installation: installation, + ns: render.ManagerNamespace, + manager: &operatorv1.Manager{ + Spec: operatorv1.ManagerSpec{ + WAFUI: &operatorv1.WAFUI{State: ptr.To(operatorv1.WAFUIDisabled)}, + }, + }, + }) + d := rtest.GetResource(resources, render.ManagerDeploymentName, render.ManagerNamespace, appsv1.GroupName, "v1", "Deployment").(*appsv1.Deployment) + Expect(wafUIEnabledEnv(d)).To(Equal(corev1.EnvVar{Name: "WAF_UI_ENABLED", Value: "false"})) + }) + + It("renders WAF_UI_ENABLED=false in multi-tenant mode even when wafUI.state is Enabled", func() { + resources, _ := renderObjects(renderConfig{ + installation: installation, + ns: "tenant-a", + bindingNamespaces: []string{"tenant-a"}, + tenant: &operatorv1.Tenant{ + ObjectMeta: metav1.ObjectMeta{Name: "tenantA", Namespace: "tenant-a"}, + Spec: operatorv1.TenantSpec{ + ID: "tenant-a", + ManagedClusterVariant: &operatorv1.Calico, + }, + }, + manager: &operatorv1.Manager{ + Spec: operatorv1.ManagerSpec{ + WAFUI: &operatorv1.WAFUI{State: ptr.To(operatorv1.WAFUIEnabled)}, + }, + }, + }) + d := rtest.GetResource(resources, render.ManagerDeploymentName, "tenant-a", appsv1.GroupName, "v1", "Deployment").(*appsv1.Deployment) + Expect(wafUIEnabledEnv(d)).To(Equal(corev1.EnvVar{Name: "WAF_UI_ENABLED", Value: "false"})) + }) + }) }) type renderConfig struct {