From 7ec146df9989e407b816b51069c8cf9bd4eb43cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Knecht?= Date: Thu, 15 Feb 2018 19:13:47 +0100 Subject: [PATCH] social: add GitLab authentication backend GitLab could already be used as an authentication backend by properly configuring `auth.generic_oauth`, but then there was no way to authorize users based on their GitLab group membership. This commit adds a `auth.gitlab` backend, similar to `auth.github`, with an `allowed_groups` option that can be set to a list of groups whose members should be allowed access to Grafana. --- conf/defaults.ini | 12 +++ pkg/models/models.go | 1 + pkg/social/gitlab_oauth.go | 131 +++++++++++++++++++++++++++++++++ pkg/social/social.go | 16 +++- public/app/partials/login.html | 4 + public/sass/_variables.scss | 1 + 6 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 pkg/social/gitlab_oauth.go diff --git a/conf/defaults.ini b/conf/defaults.ini index 99c1537eb95..90fc144c6e0 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -270,6 +270,18 @@ api_url = https://api.github.com/user team_ids = allowed_organizations = +#################################### GitLab Auth ######################### +[auth.gitlab] +enabled = false +allow_sign_up = true +client_id = some_id +client_secret = some_secret +scopes = api +auth_url = https://gitlab.com/oauth/authorize +token_url = https://gitlab.com/oauth/token +api_url = https://gitlab.com/api/v4 +allowed_groups = + #################################### Google Auth ######################### [auth.google] enabled = false diff --git a/pkg/models/models.go b/pkg/models/models.go index c2560021ee1..ba894ae591f 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -8,4 +8,5 @@ const ( TWITTER GENERIC GRAFANA_COM + GITLAB ) diff --git a/pkg/social/gitlab_oauth.go b/pkg/social/gitlab_oauth.go new file mode 100644 index 00000000000..22e50b9653a --- /dev/null +++ b/pkg/social/gitlab_oauth.go @@ -0,0 +1,131 @@ +package social + +import ( + "encoding/json" + "fmt" + "net/http" + "regexp" + + "github.com/grafana/grafana/pkg/models" + + "golang.org/x/oauth2" +) + +type SocialGitlab struct { + *SocialBase + allowedDomains []string + allowedGroups []string + apiUrl string + allowSignup bool +} + +var ( + ErrMissingGroupMembership = &Error{"User not a member of one of the required groups"} +) + +func (s *SocialGitlab) Type() int { + return int(models.GITLAB) +} + +func (s *SocialGitlab) IsEmailAllowed(email string) bool { + return isEmailAllowed(email, s.allowedDomains) +} + +func (s *SocialGitlab) IsSignupAllowed() bool { + return s.allowSignup +} + +func (s *SocialGitlab) IsGroupMember(client *http.Client) bool { + if len(s.allowedGroups) == 0 { + return true + } + + for groups, url := s.GetGroups(client, s.apiUrl+"/groups"); groups != nil; groups, url = s.GetGroups(client, url) { + for _, allowedGroup := range s.allowedGroups { + for _, group := range groups { + if group == allowedGroup { + return true + } + } + } + } + + return false +} + +func (s *SocialGitlab) GetGroups(client *http.Client, url string) ([]string, string) { + type Group struct { + FullPath string `json:"full_path"` + } + + var ( + groups []Group + next string + ) + + if url == "" { + return nil, next + } + + response, err := HttpGet(client, url) + if err != nil { + s.log.Error("Error getting groups from GitLab API", "err", err) + return nil, next + } + + if err := json.Unmarshal(response.Body, &groups); err != nil { + s.log.Error("Error parsing JSON from GitLab API", "err", err) + return nil, next + } + + fullPaths := make([]string, len(groups)) + for i, group := range groups { + fullPaths[i] = group.FullPath + } + + if link, ok := response.Headers["Link"]; ok { + pattern := regexp.MustCompile(`<([^>]+)>; rel="next"`) + if matches := pattern.FindStringSubmatch(link[0]); matches != nil { + next = matches[1] + } + } + + return fullPaths, next +} + +func (s *SocialGitlab) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { + + var data struct { + Id int + Username string + Email string + Name string + State string + } + + response, err := HttpGet(client, s.apiUrl+"/user") + if err != nil { + return nil, fmt.Errorf("Error getting user info: %s", err) + } + + err = json.Unmarshal(response.Body, &data) + if err != nil { + return nil, fmt.Errorf("Error getting user info: %s", err) + } + + if data.State != "active" { + return nil, fmt.Errorf("User %s is inactive", data.Username) + } + + userInfo := &BasicUserInfo{ + Name: data.Name, + Login: data.Username, + Email: data.Email, + } + + if !s.IsGroupMember(client) { + return nil, ErrMissingGroupMembership + } + + return userInfo, nil +} diff --git a/pkg/social/social.go b/pkg/social/social.go index adbe5a912d9..2be71514629 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -55,7 +55,7 @@ func NewOAuthService() { setting.OAuthService = &setting.OAuther{} setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) - allOauthes := []string{"github", "google", "generic_oauth", "grafananet", "grafana_com"} + allOauthes := []string{"github", "gitlab", "google", "generic_oauth", "grafananet", "grafana_com"} for _, name := range allOauthes { sec := setting.Raw.Section("auth." + name) @@ -115,6 +115,20 @@ func NewOAuthService() { } } + // GitLab. + if name == "gitlab" { + SocialMap["gitlab"] = &SocialGitlab{ + SocialBase: &SocialBase{ + Config: &config, + log: logger, + }, + allowedDomains: info.AllowedDomains, + apiUrl: info.ApiUrl, + allowSignup: info.AllowSignup, + allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), + } + } + // Google. if name == "google" { SocialMap["google"] = &SocialGoogle{ diff --git a/public/app/partials/login.html b/public/app/partials/login.html index 1919759334b..87b3cada7b5 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -51,6 +51,10 @@ Sign in with GitHub + + + Sign in with GitLab + diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index 636b60c65a7..fc5b08ccff9 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -195,6 +195,7 @@ $tabs-padding: 10px 15px 9px; $external-services: ( github: (bgColor: #464646, borderColor: #393939, icon: ''), + gitlab: (bgColor: #fc6d26, borderColor: #e24329, icon: ''), google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ''), grafanacom: (bgColor: inherit, borderColor: #393939, icon: ''), oauth: (bgColor: inherit, borderColor: #393939, icon: '')