mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 20:46:42 +00:00
195 lines
5.7 KiB
Go
195 lines
5.7 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package ingress
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
|
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
|
"github.com/projectcapsule/capsule/pkg/api"
|
|
)
|
|
|
|
// tenantWithAllowedHostnames builds a minimal Tenant carrying the given exact
|
|
// allow-list and regex under spec.ingressOptions.allowedHostnames.
|
|
func tenantWithAllowedHostnames(exact []string, regex string) capsulev1beta2.Tenant {
|
|
return capsulev1beta2.Tenant{
|
|
Spec: capsulev1beta2.TenantSpec{
|
|
IngressOptions: capsulev1beta2.IngressOptions{
|
|
AllowedHostnames: &api.AllowedListSpec{
|
|
Exact: exact,
|
|
Regex: regex,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestValidateHostnames(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
appsRegex = `^[a-z0-9-]{3,40}\.apps\.example\.com$`
|
|
)
|
|
|
|
tests := []struct {
|
|
name string
|
|
// tenant carries the allowed hostnames configuration under test.
|
|
tenant capsulev1beta2.Tenant
|
|
// hostnames are the Ingress hostnames being validated.
|
|
hostnames []string
|
|
// wantErr is true when the hostnames must be denied.
|
|
wantErr bool
|
|
// wantDenied lists hostnames that must appear in the denial message.
|
|
wantDenied []string
|
|
// wantAbsent lists hostnames that must NOT appear as denied (e.g. valid
|
|
// via regex but outside the exact list).
|
|
wantAbsent []string
|
|
}{
|
|
{
|
|
name: "no allowed hostnames configured allows everything",
|
|
tenant: capsulev1beta2.Tenant{},
|
|
hostnames: []string{"anything.example.com"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty hostname set is allowed",
|
|
tenant: tenantWithAllowedHostnames([]string{"a.example.com"}, ""),
|
|
hostnames: nil,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "all hostnames in exact list are allowed",
|
|
tenant: tenantWithAllowedHostnames([]string{"a.example.com", "b.example.com"}, ""),
|
|
hostnames: []string{"a.example.com", "b.example.com"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "hostname outside exact list without regex is denied",
|
|
tenant: tenantWithAllowedHostnames([]string{"a.example.com"}, ""),
|
|
hostnames: []string{"a.example.com", "c.example.com"},
|
|
wantErr: true,
|
|
wantDenied: []string{"c.example.com"},
|
|
},
|
|
{
|
|
name: "hostnames matching regex are allowed",
|
|
tenant: tenantWithAllowedHostnames(nil, `.*\.clastix\.io`),
|
|
hostnames: []string{"foo.clastix.io", "bar.clastix.io"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "mixed exact and regex hostnames are allowed",
|
|
tenant: tenantWithAllowedHostnames([]string{"a.example.com"}, `.*\.clastix\.io`),
|
|
hostnames: []string{"a.example.com", "foo.clastix.io"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "hostname not matching regex is denied",
|
|
tenant: tenantWithAllowedHostnames(nil, `.*\.clastix\.io`),
|
|
hostnames: []string{"foo.example.com"},
|
|
wantErr: true,
|
|
wantDenied: []string{"foo.example.com"},
|
|
},
|
|
{
|
|
name: "denies only the hostname that is neither in the exact list nor matches the regex",
|
|
tenant: tenantWithAllowedHostnames([]string{"allowed.example.com"}, appsRegex),
|
|
hostnames: []string{
|
|
"allowed.example.com", // allowed via exact
|
|
"web.apps.example.com", // allowed via regex
|
|
"denied.example.com", // denied: neither exact nor regex
|
|
},
|
|
wantErr: true,
|
|
wantDenied: []string{"denied.example.com"},
|
|
wantAbsent: []string{"web.apps.example.com"},
|
|
},
|
|
{
|
|
name: "invalid regex denies hostnames outside the exact list",
|
|
tenant: tenantWithAllowedHostnames([]string{"a.example.com"}, "("),
|
|
hostnames: []string{"a.example.com", "b.example.com"},
|
|
wantErr: true,
|
|
wantDenied: []string{"b.example.com"},
|
|
},
|
|
{
|
|
name: "invalid regex is ignored when every hostname is in the exact list",
|
|
tenant: tenantWithAllowedHostnames([]string{"a.example.com"}, "("),
|
|
hostnames: []string{"a.example.com"},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
h := &hostnames{}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := h.validateHostnames(tt.tenant, sets.New[string](tt.hostnames...))
|
|
|
|
if tt.wantErr && err == nil {
|
|
t.Fatalf("expected hostnames %v to be denied, got no error", tt.hostnames)
|
|
}
|
|
|
|
if !tt.wantErr {
|
|
if err != nil {
|
|
t.Fatalf("expected hostnames %v to be allowed, got error: %v", tt.hostnames, err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
msg := err.Error()
|
|
|
|
for _, denied := range tt.wantDenied {
|
|
if !strings.Contains(msg, denied) {
|
|
t.Errorf("expected denial message to mention %q, got: %s", denied, msg)
|
|
}
|
|
}
|
|
|
|
for _, absent := range tt.wantAbsent {
|
|
if strings.Contains(msg, absent) {
|
|
t.Errorf("did not expect denial message to mention allowed hostname %q, got: %s", absent, msg)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateHostnamesDeterministic guards against the historical
|
|
// non-deterministic bug: because the hostname set has a randomized iteration
|
|
// order, validation of the same input must always produce the same result.
|
|
func TestValidateHostnamesDeterministic(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tenant := tenantWithAllowedHostnames(
|
|
[]string{"allowed.example.com"},
|
|
`^[a-z0-9-]{3,40}\.apps\.example\.com$`,
|
|
)
|
|
|
|
hostnameSet := sets.New[string](
|
|
"allowed.example.com",
|
|
"web.apps.example.com",
|
|
"denied.example.com",
|
|
)
|
|
|
|
h := &hostnames{}
|
|
|
|
for i := 0; i < 50; i++ {
|
|
err := h.validateHostnames(tenant, hostnameSet)
|
|
if err == nil {
|
|
t.Fatalf("iteration %d: expected denial, got no error", i)
|
|
}
|
|
|
|
msg := err.Error()
|
|
if !strings.Contains(msg, "denied.example.com") {
|
|
t.Fatalf("iteration %d: expected message to mention denied.example.com, got: %s", i, msg)
|
|
}
|
|
|
|
if strings.Contains(msg, "web.apps.example.com") {
|
|
t.Fatalf("iteration %d: message wrongly mentions regex-allowed hostname: %s", i, msg)
|
|
}
|
|
}
|
|
}
|