From c5ec176a1cfba4f170991ca7bf6296185b10f565 Mon Sep 17 00:00:00 2001 From: Kevin Lyda Date: Sun, 21 Oct 2018 15:34:30 +0100 Subject: Gitlab auth issue (#79) * Fix the gitlab oauth issue. * Update for gitlab 11.1+ Versions beyond 11.1 (and possibly a few releases before) use a different method for delivering tokens. They also have disabled version 3 of the api. These changes address that and add a debugging mode for the server that make it easier to debug issues like this in the future. * Cleanup of PR. Updated README. Removed code duplication. --- server/auth/gitlab/gitlab.go | 123 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 105 insertions(+), 18 deletions(-) (limited to 'server/auth/gitlab/gitlab.go') diff --git a/server/auth/gitlab/gitlab.go b/server/auth/gitlab/gitlab.go index 5e1f95f..70d3d1c 100644 --- a/server/auth/gitlab/gitlab.go +++ b/server/auth/gitlab/gitlab.go @@ -1,13 +1,18 @@ package gitlab import ( + "bytes" + "encoding/json" "errors" + "fmt" + "io" + "log" + "net/http" "strconv" "github.com/nsheridan/cashier/server/config" "github.com/nsheridan/cashier/server/metrics" - gitlabapi "github.com/xanzy/go-gitlab" "golang.org/x/oauth2" ) @@ -19,14 +24,93 @@ const ( // Gitlab account. type Config struct { config *oauth2.Config - baseurl string group string whitelist map[string]bool allusers bool + apiurl string + log bool +} + +// Note on Gitlab REST API calls. We don't parse errors because it's +// kind of a pain: +// https://gitlab.com/help/api/README.md#data-validation-and-error-reporting +// The two v4 api calls used are /user and /groups/:group/members/:uid +// https://gitlab.com/help/api/users.md#for-normal-users-1 +// https://gitlab.com/help/api/members.md#get-a-member-of-a-group-or-project +type serviceUser struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` +} + +type serviceGroupMember struct { + ID int `json:"id"` + State string `json:"state"` + AccessLevel int `json:"access_level"` +} + +func (c *Config) logMsg(message error) { + if c.log { + log.Print(message) + } +} + +// A new oauth2 http client. +func (c *Config) newClient(token *oauth2.Token) *http.Client { + return c.config.Client(oauth2.NoContext, token) +} + +func (c *Config) getURL(token *oauth2.Token, url string) (*bytes.Buffer, error) { + client := c.newClient(token) + resp, err := client.Get(url) + if err != nil { + return nil, fmt.Errorf("Failed to get groups: %s", err) + } + defer resp.Body.Close() + var body bytes.Buffer + io.Copy(&body, resp.Body) + if resp.StatusCode != 200 { + return nil, fmt.Errorf("Gitlab error(http: %d) getting %s: '%s'", + resp.StatusCode, url, body.String()) + } + return &body, nil +} + +// Gets info on the current user. +func (c *Config) getUser(token *oauth2.Token) *serviceUser { + url := c.apiurl + "user" + body, err := c.getURL(token, url) + if err != nil { + c.logMsg(err) + return nil + } + var user serviceUser + if err := json.NewDecoder(body).Decode(&user); err != nil { + c.logMsg(fmt.Errorf("Failed to decode user (%s): %s", url, err)) + return nil + } + return &user +} + +// Gets current user group membership info. +func (c *Config) checkGroupMembership(token *oauth2.Token, uid int, group string) bool { + url := fmt.Sprintf("%sgroups/%s/members/%d", c.apiurl, group, uid) + body, err := c.getURL(token, url) + if err != nil { + c.logMsg(err) + return false + } + var m serviceGroupMember + if err := json.NewDecoder(body).Decode(&m); err != nil { + c.logMsg(fmt.Errorf("Failed to parse groups (%s): %s", url, err)) + return false + } + return m.ID == uid } // New creates a new Gitlab provider from a configuration. func New(c *config.Auth) (*Config, error) { + logOpt, _ := strconv.ParseBool(c.ProviderOpts["log"]) uw := make(map[string]bool) for _, u := range c.UsersWhitelist { uw[u] = true @@ -46,6 +130,8 @@ func New(c *config.Auth) (*Config, error) { return nil, errors.New("gitlab_opts if allusers is set, siteurl must be set") } } + // TODO: Should make sure siteURL is just the host bit. + oauth2.RegisterBrokenAuthHeaderProvider(siteURL) return &Config{ config: &oauth2.Config{ @@ -63,7 +149,8 @@ func New(c *config.Auth) (*Config, error) { group: c.ProviderOpts["group"], whitelist: uw, allusers: allUsers, - baseurl: siteURL + "api/v3/", + apiurl: siteURL + "api/v4/", + log: logOpt, }, nil } @@ -75,34 +162,36 @@ func (c *Config) Name() string { // Valid validates the oauth token. func (c *Config) Valid(token *oauth2.Token) bool { if !token.Valid() { + log.Printf("Auth fail (oauth2 Valid failure)") return false } if c.allusers { + log.Printf("Auth success (allusers)") metrics.M.AuthValid.WithLabelValues("gitlab").Inc() return true } + u := c.getUser(token) + if u == nil { + return false + } if len(c.whitelist) > 0 && !c.whitelist[c.Username(token)] { + c.logMsg(errors.New("Auth fail (not in whitelist)")) return false } if c.group == "" { // There's no group and token is valid. Can only reach // here if user whitelist is set and user is in whitelist. + c.logMsg(errors.New("Auth success (no groups specified in server config)")) metrics.M.AuthValid.WithLabelValues("gitlab").Inc() return true } - client := gitlabapi.NewOAuthClient(nil, token.AccessToken) - client.SetBaseURL(c.baseurl) - groups, _, err := client.Groups.SearchGroup(c.group) - if err != nil { + if !c.checkGroupMembership(token, u.ID, c.group) { + c.logMsg(errors.New("Auth failure (not in allowed group)")) return false } - for _, g := range groups { - if g.Path == c.group { - metrics.M.AuthValid.WithLabelValues("gitlab").Inc() - return true - } - } - return false + metrics.M.AuthValid.WithLabelValues("gitlab").Inc() + c.logMsg(errors.New("Auth success (in allowed group)")) + return true } // Revoke is a no-op revoke method. Gitlab doesn't allow token @@ -128,10 +217,8 @@ func (c *Config) Exchange(code string) (*oauth2.Token, error) { // Username retrieves the username of the Gitlab user. func (c *Config) Username(token *oauth2.Token) string { - client := gitlabapi.NewOAuthClient(nil, token.AccessToken) - client.SetBaseURL(c.baseurl) - u, _, err := client.Users.CurrentUser() - if err != nil { + u := c.getUser(token) + if u == nil { return "" } return u.Username -- cgit v1.2.3