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.go b/cluster/admitomatic/ingress.go
index 42cab98..298318f 100644
--- a/cluster/admitomatic/ingress.go
+++ b/cluster/admitomatic/ingress.go
@@ -1,10 +1,15 @@
 package main
 
 import (
+	"encoding/json"
 	"fmt"
 	"strings"
 
+	"github.com/golang/glog"
+
 	admission "k8s.io/api/admission/v1beta1"
+	networking "k8s.io/api/networking/v1beta1"
+	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 // ingressFilter is a filter which allows or denies the creation of an ingress
@@ -125,6 +130,99 @@
 	if req.Kind.Group != "networking.k8s.io" || req.Kind.Kind != "Ingress" {
 		return nil, fmt.Errorf("not an ingress")
 	}
-	// TODO(q3k); implement
-	return nil, fmt.Errorf("unimplemented")
+
+	result := func(s string, args ...interface{}) (*admission.AdmissionResponse, error) {
+		res := &admission.AdmissionResponse{
+			UID: req.UID,
+		}
+		if s == "" {
+			res.Allowed = true
+		} else {
+			res.Allowed = false
+			res.Result = &meta.Status{
+				Code:    403,
+				Message: fmt.Sprintf("admitomatic: %s", fmt.Sprintf(s, args...)),
+			}
+		}
+		return res, nil
+	}
+
+	// Permit any actions on critical system namespaes. See:
+	// https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
+	// “Avoiding operating on the kube-system namespace”
+	if req.Namespace == "kube-system" {
+		return result("")
+	}
+
+	switch req.Operation {
+	case "CREATE":
+	case "UPDATE":
+	default:
+		// We only care about creations/updates, everything else is referred to plain RBAC.
+		return result("")
+	}
+
+	ingress := networking.Ingress{}
+	err := json.Unmarshal(req.Object.Raw, &ingress)
+	if err != nil {
+		glog.Errorf("Unmarshaling Ingress failed: %v", err)
+		return result("invalid object")
+	}
+
+	// Check TLS config for hosts.
+	for j, t := range ingress.Spec.TLS {
+		for k, h := range t.Hosts {
+			if strings.Contains(h, "*") {
+				// TODO(q3k): support wildcards
+				return result("wildcard host %q (%d in TLS entry %d) is not permitted", h, k, j)
+			}
+			if !i.domainAllowed(req.Namespace, h) {
+				return result("host %q (%d) in TLS entry %d is not allowed in namespace %q", h, k, j, req.Namespace)
+			}
+		}
+	}
+
+	// Check rules for hosts.
+	for j, r := range ingress.Spec.Rules {
+		h := r.Host
+		// Per IngressRule spec:
+		//   If the host is unspecified, the Ingress routes all traffic based
+		//   on the specified IngressRuleValue. Host can be "precise" which is
+		//   a domain name without the terminating dot of a network host (e.g.
+		//   "foo.bar.com") or "wildcard", which is a domain name prefixed with
+		//   a single wildcard label (e.g. "*.foo.com").
+		//
+		// We reject everything other than precise hosts.
+		if h == "" {
+			return result("empty host %q (in rule %d) is not permitted", h, j)
+		}
+		if strings.Contains(h, "*") {
+			// TODO(q3k): support wildcards
+			return result("wildcard host %q (in rule %d) is not permitted", h, j)
+		}
+		if !i.domainAllowed(req.Namespace, h) {
+			return result("host %q (in rule %d) is not allowed in namespace %q", h, j, req.Namespace)
+		}
+	}
+
+	// Only allow a trusted subset of n-i-c annotations.
+	// TODO(q3k): allow opt-out for some namespaces
+	allowed := map[string]bool{
+		"proxy-body-size":  true,
+		"ssl-redirect":     true,
+		"backend-protocol": true,
+	}
+	prefix := "nginx.ingress.kubernetes.io/"
+	for k, _ := range ingress.Annotations {
+		if !strings.HasPrefix(k, prefix) {
+			continue
+		}
+		k = strings.TrimPrefix(k, prefix)
+		if !allowed[k] {
+			return result("forbidden annotation %q", k)
+		}
+	}
+
+	// All clear, accept this Ingress.
+	return result("")
 }