cluster/admitomatic: finish up ingress admission logic

This gives us nearly everything required to run the admission
controller. In addition to checking for allowed domains, we also do some
nginx-inress-controller security checks.

Change-Id: Ib187de6d2c06c58bd8c320503d4f850df2ec8abd
diff --git a/cluster/admitomatic/ingress_test.go b/cluster/admitomatic/ingress_test.go
index 91cf2b9..15a6049 100644
--- a/cluster/admitomatic/ingress_test.go
+++ b/cluster/admitomatic/ingress_test.go
@@ -1,6 +1,15 @@
 package main
 
-import "testing"
+import (
+	"encoding/json"
+	"strings"
+	"testing"
+
+	admission "k8s.io/api/admission/v1beta1"
+	networking "k8s.io/api/networking/v1beta1"
+	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
 
 func TestPatterns(t *testing.T) {
 	f := ingressFilter{}
@@ -76,3 +85,121 @@
 		}
 	}
 }
+
+func TestIngressPermitted(t *testing.T) {
+	f := ingressFilter{}
+	// Errors discarded, tested in TestPatterns.
+	f.allow("matrix", "matrix.hackerspace.pl")
+	f.allow("ceph-waw3", "*.hackerspace.pl")
+	f.allow("personal-q3k", "*.k0.q3k.org")
+	f.allow("personal-vuko", "shells.vuko.pl")
+	f.allow("minecraft", "*.k0.q3k.org")
+
+	mkReq := func(ns string, annotations map[string]string, is *networking.IngressSpec) *admission.AdmissionRequest {
+		i := &networking.Ingress{
+			Spec: *is,
+		}
+		i.Annotations = annotations
+		raw, err := json.Marshal(i)
+		if err != nil {
+			t.Fatalf("marshaling test ingress: %v", err)
+		}
+		return &admission.AdmissionRequest{
+			UID: "test",
+			Kind: meta.GroupVersionKind{
+				Group:   "networking.k8s.io",
+				Version: "v1beta1",
+				Kind:    "Ingress",
+			},
+			Namespace: ns,
+			Operation: "CREATE",
+			Object: runtime.RawExtension{
+				Raw: raw,
+			},
+		}
+	}
+
+	for i, el := range []struct {
+		req *admission.AdmissionRequest
+		err string
+	}{
+		// 0: unrelated domain, should be allowed
+		{mkReq("default", nil, &networking.IngressSpec{
+			Rules: []networking.IngressRule{
+				{Host: "example.com"},
+			},
+			TLS: []networking.IngressTLS{
+				{
+					Hosts: []string{"example.com"},
+				},
+			},
+		}), ""},
+		// 1: permitted restricted domain, should be allowed
+		{mkReq("matrix", nil, &networking.IngressSpec{
+			Rules: []networking.IngressRule{
+				{Host: "matrix.hackerspace.pl"},
+			},
+			TLS: []networking.IngressTLS{
+				{
+					Hosts: []string{"matrix.hackerspace.pl"},
+				},
+			},
+		}), ""},
+		// 2: forbidden restricted domain, should be rejected
+		{mkReq("personal-hacker", nil, &networking.IngressSpec{
+			Rules: []networking.IngressRule{
+				{Host: "matrix.hackerspace.pl"},
+			},
+			TLS: []networking.IngressTLS{
+				{
+					Hosts: []string{"matrix.hackerspace.pl"},
+				},
+			},
+		}), "not allowed in namespace"},
+		// 3: weird ingress but okay
+		{mkReq("personal-hacker", nil, &networking.IngressSpec{}), ""},
+		// 4: janky annotations, should be rejected
+		{mkReq("matrix", map[string]string{
+			"nginx.ingress.kubernetes.io/configuration-snippet": "omghax",
+		}, &networking.IngressSpec{
+			Rules: []networking.IngressRule{
+				{Host: "matrix.hackerspace.pl"},
+			},
+			TLS: []networking.IngressTLS{
+				{
+					Hosts: []string{"matrix.hackerspace.pl"},
+				},
+			},
+		}), "forbidden annotation"},
+		// 5: accepted annotations, should be allowed
+		{mkReq("matrix", map[string]string{
+			"nginx.ingress.kubernetes.io/proxy-body-size": "2137",
+			"foo.q3k.org/bar": "baz",
+		}, &networking.IngressSpec{
+			Rules: []networking.IngressRule{
+				{Host: "matrix.hackerspace.pl"},
+			},
+			TLS: []networking.IngressTLS{
+				{
+					Hosts: []string{"matrix.hackerspace.pl"},
+				},
+			},
+		}), ""},
+	} {
+		res, err := f.admit(el.req)
+		if err != nil {
+			t.Errorf("test %d: admit: %v", i, err)
+		}
+		if el.err == "" {
+			if !res.Allowed {
+				t.Errorf("test %d: wanted allow, got %q", i, res.Result.Message)
+			}
+		} else {
+			if res.Allowed {
+				t.Errorf("test %d: wanted %q, got allowed", i, el.err)
+			} else if !strings.Contains(res.Result.Message, el.err) {
+				t.Errorf("test %d: wanted %q, got %q", i, el.err, res.Result.Message)
+			}
+		}
+	}
+}