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/service.go b/cluster/prodvider/service.go
new file mode 100644
index 0000000..5635ac2
--- /dev/null
+++ b/cluster/prodvider/service.go
@@ -0,0 +1,104 @@
+package main
+
+import (
+	"context"
+	"crypto/tls"
+	"fmt"
+	"regexp"
+	"strings"
+
+	"github.com/golang/glog"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+	ldap "gopkg.in/ldap.v3"
+
+	pb "code.hackerspace.pl/hscloud/cluster/prodvider/proto"
+)
+
+var (
+	reUsername = regexp.MustCompile(`^[a-zA-Z0-9_\.]+$`)
+)
+
+func (p *prodvider) Authenticate(ctx context.Context, req *pb.AuthenticateRequest) (*pb.AuthenticateResponse, error) {
+	username := strings.TrimSpace(req.Username)
+	if username == "" || !reUsername.MatchString(username) {
+		return nil, status.Error(codes.InvalidArgument, "invalid username")
+	}
+
+	password := req.Password
+	if password == "" {
+		return &pb.AuthenticateResponse{
+			Result: pb.AuthenticateResponse_RESULT_INVALID_CREDENTIALS,
+		}, nil
+	}
+
+	tlsConfig := &tls.Config{}
+	lconn, err := ldap.DialTLS("tcp", flagLDAPServer, tlsConfig)
+	if err != nil {
+		glog.Errorf("ldap.DialTLS: %v", err)
+		return nil, status.Error(codes.Unavailable, "could not context LDAP")
+	}
+
+	dn := fmt.Sprintf(flagLDAPBindDN, username)
+	err = lconn.Bind(dn, password)
+
+	if err != nil {
+		if ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials) {
+			return &pb.AuthenticateResponse{
+				Result: pb.AuthenticateResponse_RESULT_INVALID_CREDENTIALS,
+			}, nil
+		}
+
+		glog.Errorf("ldap.Bind: %v", err)
+		return nil, status.Error(codes.Unavailable, "could not query LDAP")
+	}
+
+	groups, err := p.groupMemberships(lconn, username)
+	if err != nil {
+		return nil, err
+	}
+
+	if !groups["kubernetes-users"] && !groups["staff"] {
+		return nil, status.Error(codes.PermissionDenied, "not part of staff or kubernetes-users")
+	}
+
+	err = p.kubernetesSetupUser(username)
+	if err != nil {
+		glog.Errorf("kubernetesSetupUser(%v): %v", username, err)
+		return nil, status.Error(codes.Unavailable, "could not set up objects in Kubernetes")
+	}
+
+	keys, err := p.kubernetesCreds(username)
+	if err != nil {
+		glog.Errorf("kubernetesCreds(%q): %v", username, err)
+		return nil, status.Error(codes.Unavailable, "could not generate k8s keys")
+	}
+	return &pb.AuthenticateResponse{
+		Result:         pb.AuthenticateResponse_RESULT_AUTHENTICATED,
+		KubernetesKeys: keys,
+	}, nil
+}
+
+func (p *prodvider) groupMemberships(lconn *ldap.Conn, username string) (map[string]bool, error) {
+	searchRequest := ldap.NewSearchRequest(
+		flagLDAPGroupSearchBase,
+		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
+		fmt.Sprintf("(uniqueMember=%s)", fmt.Sprintf(flagLDAPBindDN, username)),
+		[]string{"dn", "cn"},
+		nil,
+	)
+
+	sr, err := lconn.Search(searchRequest)
+	if err != nil {
+		glog.Errorf("ldap.Search: %v", err)
+		return nil, status.Error(codes.Unavailable, "could not query LDAP for group")
+	}
+
+	res := make(map[string]bool)
+	for _, entry := range sr.Entries {
+		cn := entry.GetAttributeValue("cn")
+		res[cn] = true
+	}
+
+	return res, nil
+}