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) +}