prod{access,vider}: implement

Prodaccess/Prodvider allow issuing short-lived certificates for all SSO
users to access the kubernetes cluster.

Currently, all users get a personal-$username namespace in which they
have adminitrative rights. Otherwise, they get no access.

In addition, we define a static CRB to allow some admins access to
everything. In the future, this will be more granular.

We also update relevant documentation.

Change-Id: Ia18594eea8a9e5efbb3e9a25a04a28bbd6a42153
diff --git a/cluster/prodvider/kubernetes.go b/cluster/prodvider/kubernetes.go
new file mode 100644
index 0000000..3386625
--- /dev/null
+++ b/cluster/prodvider/kubernetes.go
@@ -0,0 +1,205 @@
+package main
+
+import (
+	"encoding/pem"
+	"fmt"
+	"time"
+
+	"github.com/golang/glog"
+	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+
+	pb "code.hackerspace.pl/hscloud/cluster/prodvider/proto"
+)
+
+func (p *prodvider) kubernetesCreds(username string) (*pb.KubernetesKeys, error) {
+	o := fmt.Sprintf("sso:%s", username)
+
+	csrPEM, keyPEM, err := p.makeKubernetesCSR(username+"@hackerspace.pl", o)
+	if err != nil {
+		return nil, err
+	}
+
+	certPEM, err := p.makeKubernetesCertificate(csrPEM, time.Now().Add(13*time.Hour))
+	if err != nil {
+		return nil, err
+	}
+
+	caCert, _ := p.sign.Certificate("", "")
+	caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw})
+
+	// Build certificate chain from new cert and intermediate CA.
+	chainPEM := append(certPEM, caPEM...)
+
+	glog.Infof("Generated k8s certificate for %q", username)
+	return &pb.KubernetesKeys{
+		Cluster: "k0.hswaw.net",
+		// APIServerCA
+		Ca: p.kubeCAPEM,
+		// Chain of new cert + intermediate CA
+		Cert: chainPEM,
+		Key:  keyPEM,
+	}, nil
+}
+
+func (p *prodvider) kubernetesConnect() error {
+	csrPEM, keyPEM, err := p.makeKubernetesCSR("prodvider", "system:masters")
+	if err != nil {
+		return err
+	}
+
+	certPEM, err := p.makeKubernetesCertificate(csrPEM, time.Now().Add(30*24*time.Hour))
+	if err != nil {
+		return err
+	}
+
+	caCert, _ := p.sign.Certificate("", "")
+
+	caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw})
+
+	glog.Infof("Generated k8s certificate for self (system:masters)")
+
+	// Build certificate chain from our cert and intermediate CA.
+	chainPEM := append(certPEM, caPEM...)
+
+	config := &rest.Config{
+		Host: flagKubernetesHost,
+		TLSClientConfig: rest.TLSClientConfig{
+			// Chain to authenticate ourselves (us + intermediate CA).
+			CertData: chainPEM,
+			KeyData:  keyPEM,
+			// APIServer CA for verification.
+			CAData: p.kubeCAPEM,
+		},
+	}
+
+	cs, err := kubernetes.NewForConfig(config)
+	if err != nil {
+		return err
+	}
+
+	p.k8s = cs
+
+	return nil
+}
+
+// kubernetesSetupUser ensures that for a given SSO username we:
+//  - have a personal-<username> namespace
+//  - have a sso:<username>:personal rolebinding that binds
+//    system:admin-namespace to the user within their personal namespace
+//  - have a sso:<username>:global clusterrolebinding that binds
+//    system:viewer to the user at cluster level
+func (p *prodvider) kubernetesSetupUser(username string) error {
+	namespace := "personal-" + username
+	if err := p.ensureNamespace(namespace); err != nil {
+		return err
+	}
+	if err := p.ensureRoleBindingPersonal(namespace, username); err != nil {
+		return err
+	}
+	if err := p.ensureClusterRoleBindingGlobal(username); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (p *prodvider) ensureNamespace(name string) error {
+	_, err := p.k8s.CoreV1().Namespaces().Get(name, metav1.GetOptions{})
+	switch {
+	case err == nil:
+		// Already exists, nothing to do
+		return nil
+	case errors.IsNotFound(err):
+		break
+	default:
+		// Something went wrong.
+		return err
+	}
+	ns := &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: name,
+		},
+	}
+	_, err = p.k8s.CoreV1().Namespaces().Create(ns)
+	return err
+}
+
+func (p *prodvider) ensureRoleBindingPersonal(namespace, username string) error {
+	name := "sso:" + username + ":personal"
+	rb := &rbacv1.RoleBinding{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		Subjects: []rbacv1.Subject{
+			{
+				APIGroup: "rbac.authorization.k8s.io",
+				Kind:     "User",
+				Name:     username + "@hackerspace.pl",
+			},
+		},
+		RoleRef: rbacv1.RoleRef{
+			APIGroup: "rbac.authorization.k8s.io",
+			Kind:     "ClusterRole",
+			Name:     "system:admin-namespace",
+		},
+	}
+
+	rbs := p.k8s.RbacV1().RoleBindings(namespace)
+	_, err := rbs.Get(name, metav1.GetOptions{})
+	switch {
+	case err == nil:
+		// Already exists, update.
+		_, err = rbs.Update(rb)
+		return err
+	case errors.IsNotFound(err):
+		// Create.
+		_, err = rbs.Create(rb)
+		return err
+	default:
+		// Something went wrong.
+		return err
+	}
+}
+
+func (p *prodvider) ensureClusterRoleBindingGlobal(username string) error {
+	name := "sso:" + username + ":global"
+	rb := &rbacv1.ClusterRoleBinding{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: name,
+		},
+		Subjects: []rbacv1.Subject{
+			{
+				APIGroup: "rbac.authorization.k8s.io",
+				Kind:     "User",
+				Name:     username + "@hackerspace.pl",
+			},
+		},
+		RoleRef: rbacv1.RoleRef{
+			APIGroup: "rbac.authorization.k8s.io",
+			Kind:     "ClusterRole",
+			Name:     "system:viewer",
+		},
+	}
+
+	crbs := p.k8s.RbacV1().ClusterRoleBindings()
+	_, err := crbs.Get(name, metav1.GetOptions{})
+	switch {
+	case err == nil:
+		// Already exists, update.
+		_, err = crbs.Update(rb)
+		return err
+	case errors.IsNotFound(err):
+		// Create.
+		_, err = crbs.Create(rb)
+		return err
+	default:
+		// Something went wrong.
+		return err
+	}
+}