Commit c2affdee by Alexander Zobnin Committed by GitHub

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
parent 4e27ba96
...@@ -34,7 +34,7 @@ ldap_sync_ttl = 60 ...@@ -34,7 +34,7 @@ ldap_sync_ttl = 60
# Example `whitelist = 192.168.1.1, 192.168.1.0/24, 2001::23, 2001::0/120` # Example `whitelist = 192.168.1.1, 192.168.1.0/24, 2001::23, 2001::0/120`
whitelist = whitelist =
# Optionally define more headers to sync other user attributes # 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 = headers =
``` ```
......
...@@ -171,6 +171,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *m.ReqContext) { ...@@ -171,6 +171,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *m.ReqContext) {
Login: userInfo.Login, Login: userInfo.Login,
Email: userInfo.Email, Email: userInfo.Email,
OrgRoles: map[int64]m.RoleType{}, OrgRoles: map[int64]m.RoleType{},
Groups: userInfo.Groups,
} }
if userInfo.Role != "" { if userInfo.Role != "" {
......
...@@ -2,6 +2,7 @@ package social ...@@ -2,6 +2,7 @@ package social
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
...@@ -20,6 +21,15 @@ type SocialGithub struct { ...@@ -20,6 +21,15 @@ type SocialGithub struct {
teamIds []int 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 ( var (
ErrMissingTeamMembership = &Error{"User not a member of one of the required teams"} ErrMissingTeamMembership = &Error{"User not a member of one of the required teams"}
ErrMissingOrganizationMembership = &Error{"User not a member of one of the required organizations"} ErrMissingOrganizationMembership = &Error{"User not a member of one of the required organizations"}
...@@ -48,8 +58,8 @@ func (s *SocialGithub) IsTeamMember(client *http.Client) bool { ...@@ -48,8 +58,8 @@ func (s *SocialGithub) IsTeamMember(client *http.Client) bool {
} }
for _, teamId := range s.teamIds { for _, teamId := range s.teamIds {
for _, membershipId := range teamMemberships { for _, membership := range teamMemberships {
if teamId == membershipId { if teamId == membership.Id {
return true return true
} }
} }
...@@ -108,14 +118,10 @@ func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) { ...@@ -108,14 +118,10 @@ func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) {
return email, nil return email, nil
} }
func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) { func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]GithubTeam, error) {
type Record struct {
Id int `json:"id"`
}
url := fmt.Sprintf(s.apiUrl + "/teams?per_page=100") url := fmt.Sprintf(s.apiUrl + "/teams?per_page=100")
hasMore := true hasMore := true
ids := make([]int, 0) teams := make([]GithubTeam, 0)
for hasMore { for hasMore {
...@@ -124,27 +130,19 @@ func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) ...@@ -124,27 +130,19 @@ func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error)
return nil, fmt.Errorf("Error getting team memberships: %s", err) return nil, fmt.Errorf("Error getting team memberships: %s", err)
} }
var records []Record var records []GithubTeam
err = json.Unmarshal(response.Body, &records) err = json.Unmarshal(response.Body, &records)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error getting team memberships: %s", err) return nil, fmt.Errorf("Error getting team memberships: %s", err)
} }
newRecords := len(records) teams = append(teams, records...)
existingRecords := len(ids)
tempIds := make([]int, (newRecords + existingRecords))
copy(tempIds, ids)
ids = tempIds
for i, record := range records {
ids[i] = record.Id
}
url, hasMore = s.HasMoreRecords(response.Headers) url, hasMore = s.HasMoreRecords(response.Headers)
} }
return ids, nil return teams, nil
} }
func (s *SocialGithub) HasMoreRecords(headers http.Header) (string, bool) { func (s *SocialGithub) HasMoreRecords(headers http.Header) (string, bool) {
...@@ -210,11 +208,19 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi ...@@ -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) 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{ userInfo := &BasicUserInfo{
Name: data.Login, Name: data.Login,
Login: data.Login, Login: data.Login,
Id: fmt.Sprintf("%d", data.Id), Id: fmt.Sprintf("%d", data.Id),
Email: data.Email, Email: data.Email,
Groups: teams,
} }
organizationsUrl := fmt.Sprintf(s.apiUrl + "/orgs") organizationsUrl := fmt.Sprintf(s.apiUrl + "/orgs")
...@@ -236,3 +242,26 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi ...@@ -236,3 +242,26 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
return userInfo, nil 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
}
...@@ -20,6 +20,7 @@ type BasicUserInfo struct { ...@@ -20,6 +20,7 @@ type BasicUserInfo struct {
Login string Login string
Company string Company string
Role string Role string
Groups []string
} }
type SocialConnector interface { type SocialConnector interface {
......
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/multildap" "github.com/grafana/grafana/pkg/services/multildap"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
) )
const ( const (
...@@ -246,15 +247,19 @@ func (auth *AuthProxy) LoginViaHeader() (int64, error) { ...@@ -246,15 +247,19 @@ func (auth *AuthProxy) LoginViaHeader() (int64, error) {
return 0, newError("Auth proxy header property invalid", nil) 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] == "" { if auth.headers[field] == "" {
continue continue
} }
if val := auth.ctx.Req.Header.Get(auth.headers[field]); val != "" { if val := auth.ctx.Req.Header.Get(auth.headers[field]); val != "" {
if field == "Groups" {
extUser.Groups = util.SplitString(val)
} else {
reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(val) reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(val)
} }
} }
}
upsert := &models.UpsertUserCommand{ upsert := &models.UpsertUserCommand{
ReqContext: auth.ctx, ReqContext: auth.ctx,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment