Browse Source

OAuth: return github teams as a part of user info (enable team sync) (#17797)

* OAuth: github team sync POC

* OAuth: minor refactor of github module

* OAuth: able to use team shorthands for github team sync

* support passing a list of groups via auth-proxy header
Alexander Zobnin 6 years ago
parent
commit
c2affdee1e

+ 1 - 1
docs/sources/auth/auth-proxy.md

@@ -34,7 +34,7 @@ ldap_sync_ttl = 60
 # Example `whitelist = 192.168.1.1, 192.168.1.0/24, 2001::23, 2001::0/120`
 whitelist =
 # Optionally define more headers to sync other user attributes
-# Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`
+# Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL Groups:X-WEBAUTH-GROUPS`
 headers =
 ```
 

+ 1 - 0
pkg/api/login_oauth.go

@@ -171,6 +171,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *m.ReqContext) {
 		Login:      userInfo.Login,
 		Email:      userInfo.Email,
 		OrgRoles:   map[int64]m.RoleType{},
+		Groups:     userInfo.Groups,
 	}
 
 	if userInfo.Role != "" {

+ 52 - 23
pkg/login/social/github_oauth.go

@@ -2,6 +2,7 @@ package social
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"net/http"
 	"regexp"
@@ -20,6 +21,15 @@ type SocialGithub struct {
 	teamIds              []int
 }
 
+type GithubTeam struct {
+	Id           int    `json:"id"`
+	Slug         string `json:"slug"`
+	URL          string `json:"html_url"`
+	Organization struct {
+		Login string `json:"login"`
+	} `json:"organization"`
+}
+
 var (
 	ErrMissingTeamMembership         = &Error{"User not a member of one of the required teams"}
 	ErrMissingOrganizationMembership = &Error{"User not a member of one of the required organizations"}
@@ -48,8 +58,8 @@ func (s *SocialGithub) IsTeamMember(client *http.Client) bool {
 	}
 
 	for _, teamId := range s.teamIds {
-		for _, membershipId := range teamMemberships {
-			if teamId == membershipId {
+		for _, membership := range teamMemberships {
+			if teamId == membership.Id {
 				return true
 			}
 		}
@@ -108,14 +118,10 @@ func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) {
 	return email, nil
 }
 
-func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) {
-	type Record struct {
-		Id int `json:"id"`
-	}
-
+func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]GithubTeam, error) {
 	url := fmt.Sprintf(s.apiUrl + "/teams?per_page=100")
 	hasMore := true
-	ids := make([]int, 0)
+	teams := make([]GithubTeam, 0)
 
 	for hasMore {
 
@@ -124,27 +130,19 @@ func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error)
 			return nil, fmt.Errorf("Error getting team memberships: %s", err)
 		}
 
-		var records []Record
+		var records []GithubTeam
 
 		err = json.Unmarshal(response.Body, &records)
 		if err != nil {
 			return nil, fmt.Errorf("Error getting team memberships: %s", err)
 		}
 
-		newRecords := len(records)
-		existingRecords := len(ids)
-		tempIds := make([]int, (newRecords + existingRecords))
-		copy(tempIds, ids)
-		ids = tempIds
-
-		for i, record := range records {
-			ids[i] = record.Id
-		}
+		teams = append(teams, records...)
 
 		url, hasMore = s.HasMoreRecords(response.Headers)
 	}
 
-	return ids, nil
+	return teams, nil
 }
 
 func (s *SocialGithub) HasMoreRecords(headers http.Header) (string, bool) {
@@ -210,11 +208,19 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
 		return nil, fmt.Errorf("Error getting user info: %s", err)
 	}
 
+	teamMemberships, err := s.FetchTeamMemberships(client)
+	if err != nil {
+		return nil, fmt.Errorf("Error getting user teams: %s", err)
+	}
+
+	teams := convertToGroupList(teamMemberships)
+
 	userInfo := &BasicUserInfo{
-		Name:  data.Login,
-		Login: data.Login,
-		Id:    fmt.Sprintf("%d", data.Id),
-		Email: data.Email,
+		Name:   data.Login,
+		Login:  data.Login,
+		Id:     fmt.Sprintf("%d", data.Id),
+		Email:  data.Email,
+		Groups: teams,
 	}
 
 	organizationsUrl := fmt.Sprintf(s.apiUrl + "/orgs")
@@ -236,3 +242,26 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
 
 	return userInfo, nil
 }
+
+func (t *GithubTeam) GetShorthand() (string, error) {
+	if t.Organization.Login == "" || t.Slug == "" {
+		return "", errors.New("Error getting team shorthand")
+	}
+	return fmt.Sprintf("@%s/%s", t.Organization.Login, t.Slug), nil
+}
+
+func convertToGroupList(t []GithubTeam) []string {
+	groups := make([]string, 0)
+	for _, team := range t {
+		// Group shouldn't be empty string, otherwise team sync will not work properly
+		if team.URL != "" {
+			groups = append(groups, team.URL)
+		}
+		teamShorthand, _ := team.GetShorthand()
+		if teamShorthand != "" {
+			groups = append(groups, teamShorthand)
+		}
+	}
+
+	return groups
+}

+ 1 - 0
pkg/login/social/social.go

@@ -20,6 +20,7 @@ type BasicUserInfo struct {
 	Login   string
 	Company string
 	Role    string
+	Groups  []string
 }
 
 type SocialConnector interface {

+ 7 - 2
pkg/middleware/auth_proxy/auth_proxy.go

@@ -14,6 +14,7 @@ import (
 	"github.com/grafana/grafana/pkg/services/ldap"
 	"github.com/grafana/grafana/pkg/services/multildap"
 	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/util"
 )
 
 const (
@@ -246,13 +247,17 @@ func (auth *AuthProxy) LoginViaHeader() (int64, error) {
 		return 0, newError("Auth proxy header property invalid", nil)
 	}
 
-	for _, field := range []string{"Name", "Email", "Login"} {
+	for _, field := range []string{"Name", "Email", "Login", "Groups"} {
 		if auth.headers[field] == "" {
 			continue
 		}
 
 		if val := auth.ctx.Req.Header.Get(auth.headers[field]); val != "" {
-			reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(val)
+			if field == "Groups" {
+				extUser.Groups = util.SplitString(val)
+			} else {
+				reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(val)
+			}
 		}
 	}