diff --git a/cmd/cashierd/main.go b/cmd/cashierd/main.go
index fb67a36685f2e8077b2d587fd3eb10b4592987bc..f63189c28c8b105ce9b7570a35394aa0ab938daa 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 0000000000000000000000000000000000000000..cee30290cab4f7f8d0575eb5a5421b5429103687
--- /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 0000000000000000000000000000000000000000..4f682e8a6cf42e363b85460996da47890e3ec138
--- /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)
+}