aboutsummaryrefslogtreecommitdiff
path: root/server/auth/gitlab/gitlab.go
diff options
context:
space:
mode:
Diffstat (limited to 'server/auth/gitlab/gitlab.go')
-rw-r--r--server/auth/gitlab/gitlab.go123
1 files changed, 105 insertions, 18 deletions
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