diff --git a/conf/defaults.ini b/conf/defaults.ini index 628df5360e4..258a0198155 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -153,6 +153,7 @@ token_url = https://github.com/login/oauth/access_token api_url = https://api.github.com/user team_ids = allowed_domains = +allowed_organizations = #################################### Google Auth ########################## [auth.google] diff --git a/conf/sample.ini b/conf/sample.ini index df204a7f45d..3c2773fa674 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -146,12 +146,13 @@ ;allow_sign_up = false ;client_id = some_id ;client_secret = some_secret -;scopes = user:email +;scopes = user:email,read:org ;auth_url = https://github.com/login/oauth/authorize ;token_url = https://github.com/login/oauth/access_token ;api_url = https://api.github.com/user ;team_ids = ;allowed_domains = +;allowed_organizations = #################################### Google Auth ########################## [auth.google] diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 505c17ddde8..796599df864 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -48,6 +48,8 @@ func OAuthLogin(ctx *middleware.Context) { if err != nil { if err == social.ErrMissingTeamMembership { ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github team membership not fulfilled")) + } else if err == social.ErrMissingOrganizationMembership { + ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github organization membership not fulfilled")) } else { ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err) } diff --git a/pkg/social/social.go b/pkg/social/social.go index 355f85b54b6..49812ddd87f 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -78,12 +78,14 @@ func NewOAuthService() { if name == "github" { setting.OAuthService.GitHub = true teamIds := sec.Key("team_ids").Ints(",") + allowedOrganizations := sec.Key("allowed_organizations").Strings(" ") SocialMap["github"] = &SocialGithub{ - Config: &config, - allowedDomains: info.AllowedDomains, - apiUrl: info.ApiUrl, - allowSignup: info.AllowSignup, - teamIds: teamIds, + Config: &config, + allowedDomains: info.AllowedDomains, + apiUrl: info.ApiUrl, + allowSignup: info.AllowSignup, + teamIds: teamIds, + allowedOrganizations: allowedOrganizations, } } @@ -115,16 +117,21 @@ func isEmailAllowed(email string, allowedDomains []string) bool { type SocialGithub struct { *oauth2.Config - allowedDomains []string - apiUrl string - allowSignup bool - teamIds []int + allowedDomains []string + allowedOrganizations []string + apiUrl string + allowSignup bool + teamIds []int } var ( ErrMissingTeamMembership = errors.New("User not a member of one of the required teams") ) +var ( + ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations") +) + func (s *SocialGithub) Type() int { return int(models.GITHUB) } @@ -137,26 +144,100 @@ func (s *SocialGithub) IsSignupAllowed() bool { return s.allowSignup } -func (s *SocialGithub) IsTeamMember(client *http.Client, username string, teamId int) bool { - var data struct { - Url string `json:"url"` - State string `json:"state"` +func (s *SocialGithub) IsTeamMember(client *http.Client) bool { + if len(s.teamIds) == 0 { + return true } - membershipUrl := fmt.Sprintf("https://api.github.com/teams/%d/memberships/%s", teamId, username) - r, err := client.Get(membershipUrl) + teamMemberships, err := s.FetchTeamMemberships(client) if err != nil { return false } - defer r.Body.Close() + for _, teamId := range s.teamIds { + for _, membershipId := range teamMemberships { + if teamId == membershipId { + return true + } + } + } - if err = json.NewDecoder(r.Body).Decode(&data); err != nil { + return false +} + +func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool { + if len(s.allowedOrganizations) == 0 { + return true + } + + organizations, err := s.FetchOrganizations(client) + if err != nil { return false } - active := data.State == "active" - return active + for _, allowedOrganization := range s.allowedOrganizations { + for _, organization := range organizations { + if organization == allowedOrganization { + return true + } + } + } + + return false +} + +func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) { + type Record struct { + Id int `json:"id"` + } + + membershipUrl := fmt.Sprintf("https://api.github.com/user/teams") + r, err := client.Get(membershipUrl) + if err != nil { + return nil, err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return nil, err + } + + var ids = make([]int, len(records)) + for i, record := range records { + ids[i] = record.Id + } + + return ids, nil +} + +func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) { + type Record struct { + Login string `json:"login"` + } + + url := fmt.Sprintf("https://api.github.com/user/orgs") + r, err := client.Get(url) + if err != nil { + return nil, err + } + + defer r.Body.Close() + + var records []Record + + if err = json.NewDecoder(r.Body).Decode(&records); err != nil { + return nil, err + } + + var logins = make([]string, len(records)) + for i, record := range records { + logins[i] = record.Login + } + + return logins, nil } func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { @@ -185,17 +266,15 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { Email: data.Email, } - if len(s.teamIds) > 0 { - for _, teamId := range s.teamIds { - if s.IsTeamMember(client, data.Name, teamId) { - return userInfo, nil - } - } - + if !s.IsTeamMember(client) { return nil, ErrMissingTeamMembership - } else { - return userInfo, nil } + + if !s.IsOrganizationMember(client) { + return nil, ErrMissingOrganizationMembership + } + + return userInfo, nil } // ________ .__