From a349115eb6b2ae4da40f3a7dd85cf8942bf60c05 Mon Sep 17 00:00:00 2001
From: Kevin Lyda <kevin@ie.suberic.net>
Date: Sat, 14 Jan 2017 14:32:27 +0000
Subject: [PATCH] Initial pass at a gitlab module.

---
 cmd/cashierd/main.go              |   3 +
 server/auth/gitlab/gitlab.go      | 138 ++++++++++++++++++++++++++++++
 server/auth/gitlab/gitlab_test.go |  73 ++++++++++++++++
 3 files changed, 214 insertions(+)
 create mode 100644 server/auth/gitlab/gitlab.go
 create mode 100644 server/auth/gitlab/gitlab_test.go

diff --git a/cmd/cashierd/main.go b/cmd/cashierd/main.go
index fb67a366..f63189c2 100644
--- a/cmd/cashierd/main.go
+++ b/cmd/cashierd/main.go
@@ -28,6 +28,7 @@ import (
 	"github.com/nsheridan/cashier/lib"
 	"github.com/nsheridan/cashier/server/auth"
 	"github.com/nsheridan/cashier/server/auth/github"
+	"github.com/nsheridan/cashier/server/auth/gitlab"
 	"github.com/nsheridan/cashier/server/auth/google"
 	"github.com/nsheridan/cashier/server/config"
 	"github.com/nsheridan/cashier/server/signer"
@@ -383,6 +384,8 @@ func main() {
 		authprovider, err = google.New(conf.Auth)
 	case "github":
 		authprovider, err = github.New(conf.Auth)
+	case "gitlab":
+		authprovider, err = gitlab.New(conf.Auth)
 	default:
 		log.Fatalf("Unknown provider %s\n", conf.Auth.Provider)
 	}
diff --git a/server/auth/gitlab/gitlab.go b/server/auth/gitlab/gitlab.go
new file mode 100644
index 00000000..cee30290
--- /dev/null
+++ b/server/auth/gitlab/gitlab.go
@@ -0,0 +1,138 @@
+package gitlab
+
+import (
+	"errors"
+	"net/http"
+	"time"
+
+	"github.com/nsheridan/cashier/server/auth"
+	"github.com/nsheridan/cashier/server/config"
+
+	gitlabapi "github.com/xanzy/go-gitlab"
+	"golang.org/x/oauth2"
+)
+
+const (
+	name = "gitlab"
+)
+
+// Config is an implementation of `auth.Provider` for authenticating using a
+// Gitlab account.
+type Config struct {
+	config       *oauth2.Config
+	organisation string
+	whitelist    map[string]bool
+	allusers     bool
+}
+
+// New creates a new Github provider from a configuration.
+func New(c *config.Auth) (auth.Provider, error) {
+	uw := make(map[string]bool)
+	for _, u := range c.UsersWhitelist {
+		uw[u] = true
+	}
+	allUsers := false
+	if c.ProviderOpts["allusers"] == "true" {
+		allUsers = true
+	}
+	if !allUsers && c.ProviderOpts["organisation"] == "" && len(uw) == 0 {
+		return nil, errors.New("gitlab_opts organisation and the users whitelist must not be both empty if allusers isn't true")
+	}
+	if c.ProviderOpts["authurl"] == "" || c.ProviderOpts["tokenurl"] == "" {
+		return nil, errors.New("gitlab_opts authurl and tokenurl must be set")
+	}
+	return &Config{
+		config: &oauth2.Config{
+			ClientID:     c.OauthClientID,
+			ClientSecret: c.OauthClientSecret,
+			RedirectURL:  c.OauthCallbackURL,
+			Endpoint: oauth2.Endpoint{
+				AuthURL:  c.ProviderOpts["authurl"],
+				TokenURL: c.ProviderOpts["tokenurl"],
+			},
+			Scopes: []string{
+				"api",
+			},
+		},
+		organisation: c.ProviderOpts["organisation"],
+		whitelist:    uw,
+		allusers:     allUsers,
+	}, nil
+}
+
+// A new oauth2 http client.
+func (c *Config) newClient(token *oauth2.Token) *http.Client {
+	return c.config.Client(oauth2.NoContext, token)
+}
+
+// Name returns the name of the provider.
+func (c *Config) Name() string {
+	return name
+}
+
+// Valid validates the oauth token.
+func (c *Config) Valid(token *oauth2.Token) bool {
+	if c.allusers {
+		return true
+	}
+	if len(c.whitelist) > 0 && !c.whitelist[c.Username(token)] {
+		return false
+	}
+	if !token.Valid() {
+		return false
+	}
+	if c.organisation == "" {
+		// There's no organisation and token is valid.  Can only reach
+		// here if user whitelist is set and user is in whitelist.
+		return true
+	}
+	client := gitlabapi.NewClient(c.newClient(token), token.AccessToken)
+	groups, _, err := client.Groups.ListGroups(nil)
+	if err != nil {
+		return false
+	}
+	for _, g := range groups {
+		if g.Name == c.organisation {
+			return true
+		}
+	}
+	return false
+}
+
+// Revoke is a no-op revoke method. GitHub doesn't seem to allow token
+// revocation - tokens are indefinite and there are no refresh options etc.
+// Returns nil to satisfy the Provider interface.
+func (c *Config) Revoke(token *oauth2.Token) error {
+	return nil
+}
+
+// StartSession retrieves an authentication endpoint from Github.
+func (c *Config) StartSession(state string) *auth.Session {
+	return &auth.Session{
+		AuthURL: c.config.AuthCodeURL(state),
+	}
+}
+
+// Exchange authorizes the session and returns an access token.
+func (c *Config) Exchange(code string) (*oauth2.Token, error) {
+	t, err := c.config.Exchange(oauth2.NoContext, code)
+	if err != nil {
+		return nil, err
+	}
+	// Github tokens don't have an expiry. Set one so that the session expires
+	// after a period.
+	if t.Expiry.Unix() <= 0 {
+		t.Expiry = time.Now().Add(1 * time.Hour)
+	}
+	return t, nil
+}
+
+// Username retrieves the username portion of the user's email address.
+func (c *Config) Username(token *oauth2.Token) string {
+	client := gitlabapi.NewClient(c.newClient(token), token.AccessToken)
+	u, _, err := client.Users.CurrentUser()
+	if err != nil {
+		return ""
+	}
+	return u.Username
+}
diff --git a/server/auth/gitlab/gitlab_test.go b/server/auth/gitlab/gitlab_test.go
new file mode 100644
index 00000000..4f682e8a
--- /dev/null
+++ b/server/auth/gitlab/gitlab_test.go
@@ -0,0 +1,73 @@
+package gitlab
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/nsheridan/cashier/server/auth"
+	"github.com/nsheridan/cashier/server/config"
+	"github.com/stretchr/testify/assert"
+)
+
+var (
+	oauthClientID     = "id"
+	oauthClientSecret = "secret"
+	oauthCallbackURL  = "url"
+	authurl           = "https://exampleorg/oauth/authorize"
+	tokenurl          = "https://exampleorg/oauth/token"
+	group             = "exampleorg"
+)
+
+func TestNew(t *testing.T) {
+	a := assert.New(t)
+
+	p, _ := newGitlab()
+	g := p.(*Config)
+	a.Equal(g.config.ClientID, oauthClientID)
+	a.Equal(g.config.ClientSecret, oauthClientSecret)
+	a.Equal(g.config.RedirectURL, oauthCallbackURL)
+}
+
+func TestNewEmptyAuthURL(t *testing.T) {
+	authurl = ""
+	a := assert.New(t)
+
+	_, err := newGitlab()
+	a.EqualError(err, "gitlab_opts authurl and tokenurl must be set")
+
+	authurl = "https://exampleorg/oauth/authorize"
+}
+
+func TestNewEmptyGroupList(t *testing.T) {
+	group = ""
+	a := assert.New(t)
+
+	_, err := newGitlab()
+	a.EqualError(err, "gitlab_opts group and the users whitelist must not be both empty if allusers isn't true")
+
+	group = "exampleorg"
+}
+
+func TestStartSession(t *testing.T) {
+	a := assert.New(t)
+
+	p, _ := newGithub()
+	s := p.StartSession("test_state")
+	a.Contains(s.AuthURL, "exampleorg/oauth/authorize")
+	a.Contains(s.AuthURL, "state=test_state")
+	a.Contains(s.AuthURL, fmt.Sprintf("client_id=%s", oauthClientID))
+}
+
+func newGitlab() (auth.Provider, error) {
+	c := &config.Auth{
+		OauthClientID:     oauthClientID,
+		OauthClientSecret: oauthClientSecret,
+		OauthCallbackURL:  oauthCallbackURL,
+		ProviderOpts: map[string]string{
+			"group":    group,
+			"authurl":  authurl,
+			"tokenurl": tokenurl,
+		},
+	}
+	return New(c)
+}
-- 
GitLab