2015-05-21 06:23:48 +03:00
package providers
import (
"encoding/json"
2015-06-06 21:15:43 +03:00
"fmt"
2015-05-21 06:23:48 +03:00
"io/ioutil"
"net/http"
"net/url"
2016-06-20 15:12:07 +03:00
"path"
2019-10-07 16:38:57 +03:00
"regexp"
2017-07-05 00:36:21 +03:00
"strconv"
2016-06-20 14:17:39 +03:00
"strings"
2019-02-10 19:37:45 +03:00
2019-05-05 15:33:13 +03:00
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
2019-06-15 12:33:29 +03:00
"github.com/pusher/oauth2_proxy/pkg/logger"
2015-05-21 06:23:48 +03:00
)
2018-12-20 13:37:59 +03:00
// GitHubProvider represents an GitHub based Identity Provider
2015-05-21 06:23:48 +03:00
type GitHubProvider struct {
* ProviderData
Org string
Team string
}
2018-12-20 13:37:59 +03:00
// NewGitHubProvider initiates a new GitHubProvider
2015-05-21 06:23:48 +03:00
func NewGitHubProvider ( p * ProviderData ) * GitHubProvider {
p . ProviderName = "GitHub"
2015-11-09 02:47:44 +03:00
if p . LoginURL == nil || p . LoginURL . String ( ) == "" {
p . LoginURL = & url . URL {
2015-05-21 06:23:48 +03:00
Scheme : "https" ,
Host : "github.com" ,
Path : "/login/oauth/authorize" ,
}
}
2015-11-09 02:47:44 +03:00
if p . RedeemURL == nil || p . RedeemURL . String ( ) == "" {
p . RedeemURL = & url . URL {
2015-05-21 06:23:48 +03:00
Scheme : "https" ,
Host : "github.com" ,
Path : "/login/oauth/access_token" ,
}
}
2016-06-20 15:12:07 +03:00
// ValidationURL is the API Base URL
2015-11-09 02:47:44 +03:00
if p . ValidateURL == nil || p . ValidateURL . String ( ) == "" {
p . ValidateURL = & url . URL {
2015-05-21 06:23:48 +03:00
Scheme : "https" ,
Host : "api.github.com" ,
2016-06-20 15:12:07 +03:00
Path : "/" ,
2015-05-21 06:23:48 +03:00
}
}
if p . Scope == "" {
p . Scope = "user:email"
}
return & GitHubProvider { ProviderData : p }
}
2018-12-20 13:37:59 +03:00
// SetOrgTeam adds GitHub org reading parameters to the OAuth2 scope
2015-05-21 06:23:48 +03:00
func ( p * GitHubProvider ) SetOrgTeam ( org , team string ) {
p . Org = org
p . Team = team
if org != "" || team != "" {
p . Scope += " read:org"
}
}
2015-06-06 21:03:59 +03:00
func ( p * GitHubProvider ) hasOrg ( accessToken string ) ( bool , error ) {
// https://developer.github.com/v3/orgs/#list-your-organizations
var orgs [ ] struct {
Login string ` json:"login" `
}
2017-07-05 00:36:21 +03:00
type orgsPage [ ] struct {
Login string ` json:"login" `
2015-06-06 21:03:59 +03:00
}
2017-07-05 00:36:21 +03:00
pn := 1
for {
params := url . Values {
2019-03-15 18:00:20 +03:00
"per_page" : { "100" } ,
2019-03-15 06:04:45 +03:00
"page" : { strconv . Itoa ( pn ) } ,
2017-07-05 00:36:21 +03:00
}
2015-06-06 21:03:59 +03:00
2017-07-05 00:36:21 +03:00
endpoint := & url . URL {
Scheme : p . ValidateURL . Scheme ,
Host : p . ValidateURL . Host ,
Path : path . Join ( p . ValidateURL . Path , "/user/orgs" ) ,
RawQuery : params . Encode ( ) ,
}
req , _ := http . NewRequest ( "GET" , endpoint . String ( ) , nil )
req . Header . Set ( "Accept" , "application/vnd.github.v3+json" )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "token %s" , accessToken ) )
resp , err := http . DefaultClient . Do ( req )
if err != nil {
return false , err
}
2015-06-06 21:03:59 +03:00
2017-07-05 00:36:21 +03:00
body , err := ioutil . ReadAll ( resp . Body )
resp . Body . Close ( )
if err != nil {
return false , err
}
if resp . StatusCode != 200 {
return false , fmt . Errorf (
"got %d from %q %s" , resp . StatusCode , endpoint . String ( ) , body )
}
var op orgsPage
if err := json . Unmarshal ( body , & op ) ; err != nil {
return false , err
}
if len ( op ) == 0 {
break
}
orgs = append ( orgs , op ... )
2018-11-29 17:26:41 +03:00
pn ++
2015-06-06 21:03:59 +03:00
}
2015-07-24 23:10:10 +03:00
var presentOrgs [ ] string
2015-06-06 21:03:59 +03:00
for _ , org := range orgs {
if p . Org == org . Login {
2019-02-10 19:37:45 +03:00
logger . Printf ( "Found Github Organization: %q" , org . Login )
2015-06-06 21:03:59 +03:00
return true , nil
}
2015-07-24 23:10:10 +03:00
presentOrgs = append ( presentOrgs , org . Login )
2015-06-06 21:03:59 +03:00
}
2015-07-24 23:10:10 +03:00
2019-02-10 19:37:45 +03:00
logger . Printf ( "Missing Organization:%q in %v" , p . Org , presentOrgs )
2015-06-06 21:03:59 +03:00
return false , nil
}
2015-05-21 06:23:48 +03:00
func ( p * GitHubProvider ) hasOrgAndTeam ( accessToken string ) ( bool , error ) {
2015-06-06 21:03:59 +03:00
// https://developer.github.com/v3/orgs/teams/#list-user-teams
2015-05-21 06:23:48 +03:00
var teams [ ] struct {
Name string ` json:"name" `
Slug string ` json:"slug" `
Org struct {
Login string ` json:"login" `
} ` json:"organization" `
}
2019-03-13 07:24:47 +03:00
type teamsPage [ ] struct {
Name string ` json:"name" `
Slug string ` json:"slug" `
Org struct {
Login string ` json:"login" `
} ` json:"organization" `
2015-05-21 06:23:48 +03:00
}
2019-03-13 07:24:47 +03:00
pn := 1
2019-10-07 16:38:57 +03:00
last := 0
2019-03-13 07:24:47 +03:00
for {
params := url . Values {
2019-03-15 18:00:20 +03:00
"per_page" : { "100" } ,
2019-03-15 06:04:45 +03:00
"page" : { strconv . Itoa ( pn ) } ,
2019-03-13 07:24:47 +03:00
}
2015-05-21 06:23:48 +03:00
2019-03-13 07:24:47 +03:00
endpoint := & url . URL {
Scheme : p . ValidateURL . Scheme ,
Host : p . ValidateURL . Host ,
Path : path . Join ( p . ValidateURL . Path , "/user/teams" ) ,
RawQuery : params . Encode ( ) ,
}
2015-05-21 06:23:48 +03:00
2019-03-13 07:24:47 +03:00
req , _ := http . NewRequest ( "GET" , endpoint . String ( ) , nil )
req . Header . Set ( "Accept" , "application/vnd.github.v3+json" )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "token %s" , accessToken ) )
resp , err := http . DefaultClient . Do ( req )
if err != nil {
return false , err
}
2019-10-07 16:38:57 +03:00
if last == 0 {
// link header may not be obtained
// When paging is not required and all data can be retrieved with a single call
// Conditions for obtaining the link header.
// 1. When paging is required (Example: When the data size is 100 and the page size is 99 or less)
// 2. When it exceeds the paging frame (Example: When there is only 10 records but the second page is called with a page size of 100)
// link header at not last page
// <https://api.github.com/user/teams?page=1&per_page=100>; rel="prev", <https://api.github.com/user/teams?page=1&per_page=100>; rel="last", <https://api.github.com/user/teams?page=1&per_page=100>; rel="first"
// link header at last page (doesn't exist last info)
// <https://api.github.com/user/teams?page=3&per_page=10>; rel="prev", <https://api.github.com/user/teams?page=1&per_page=10>; rel="first"
link := resp . Header . Get ( "Link" )
rep1 := regexp . MustCompile ( ` (?s).*\<https://api.github.com/user/teams\?page=(.)&per_page=[0-9]+\>; rel="last".* ` )
i , converr := strconv . Atoi ( rep1 . ReplaceAllString ( link , "$1" ) )
// If the last page cannot be taken from the link in the http header, the last variable remains zero
if converr == nil {
last = i
}
}
2019-10-11 10:02:04 +03:00
body , err := ioutil . ReadAll ( resp . Body )
2019-03-13 07:24:47 +03:00
if err != nil {
2019-10-11 10:02:04 +03:00
resp . Body . Close ( )
2019-03-13 07:24:47 +03:00
return false , err
}
2019-10-11 10:02:04 +03:00
resp . Body . Close ( )
2019-03-13 07:24:47 +03:00
if resp . StatusCode != 200 {
return false , fmt . Errorf (
"got %d from %q %s" , resp . StatusCode , endpoint . String ( ) , body )
}
var tp teamsPage
if err := json . Unmarshal ( body , & tp ) ; err != nil {
return false , fmt . Errorf ( "%s unmarshaling %s" , err , body )
}
if len ( tp ) == 0 {
break
}
teams = append ( teams , tp ... )
2019-10-07 16:38:57 +03:00
if pn == last {
break
}
if last == 0 {
break
}
2019-03-13 07:24:47 +03:00
pn ++
2015-05-21 06:23:48 +03:00
}
2015-07-24 23:10:10 +03:00
var hasOrg bool
presentOrgs := make ( map [ string ] bool )
var presentTeams [ ] string
2015-05-21 06:23:48 +03:00
for _ , team := range teams {
2015-07-24 23:10:10 +03:00
presentOrgs [ team . Org . Login ] = true
2015-05-21 06:23:48 +03:00
if p . Org == team . Org . Login {
2015-07-24 23:10:10 +03:00
hasOrg = true
2016-02-17 13:21:27 +03:00
ts := strings . Split ( p . Team , "," )
for _ , t := range ts {
if t == team . Slug {
2019-02-10 19:37:45 +03:00
logger . Printf ( "Found Github Organization:%q Team:%q (Name:%q)" , team . Org . Login , team . Slug , team . Name )
2016-02-17 13:21:27 +03:00
return true , nil
}
2015-05-21 06:23:48 +03:00
}
2015-07-24 23:10:10 +03:00
presentTeams = append ( presentTeams , team . Slug )
}
}
if hasOrg {
2019-02-10 19:37:45 +03:00
logger . Printf ( "Missing Team:%q from Org:%q in teams: %v" , p . Team , p . Org , presentTeams )
2015-07-24 23:10:10 +03:00
} else {
var allOrgs [ ] string
2018-11-29 17:26:41 +03:00
for org := range presentOrgs {
2015-07-24 23:10:10 +03:00
allOrgs = append ( allOrgs , org )
2015-05-21 06:23:48 +03:00
}
2019-02-10 19:37:45 +03:00
logger . Printf ( "Missing Organization:%q in %#v" , p . Org , allOrgs )
2015-05-21 06:23:48 +03:00
}
return false , nil
}
2018-12-20 13:37:59 +03:00
// GetEmailAddress returns the Account email address
2019-05-05 15:33:13 +03:00
func ( p * GitHubProvider ) GetEmailAddress ( s * sessions . SessionState ) ( string , error ) {
2015-05-21 06:23:48 +03:00
var emails [ ] struct {
2019-03-11 20:52:08 +03:00
Email string ` json:"email" `
2019-03-20 19:52:30 +03:00
Primary bool ` json:"primary" `
2019-03-11 20:52:08 +03:00
Verified bool ` json:"verified" `
2015-05-21 06:23:48 +03:00
}
// if we require an Org or Team, check that first
2015-06-06 21:03:59 +03:00
if p . Org != "" {
if p . Team != "" {
2015-06-23 14:23:39 +03:00
if ok , err := p . hasOrgAndTeam ( s . AccessToken ) ; err != nil || ! ok {
2015-06-06 21:03:59 +03:00
return "" , err
}
} else {
2015-06-23 14:23:39 +03:00
if ok , err := p . hasOrg ( s . AccessToken ) ; err != nil || ! ok {
2015-06-06 21:03:59 +03:00
return "" , err
}
2015-05-21 06:23:48 +03:00
}
}
2016-06-20 15:12:07 +03:00
endpoint := & url . URL {
2017-05-18 06:45:34 +03:00
Scheme : p . ValidateURL . Scheme ,
Host : p . ValidateURL . Host ,
Path : path . Join ( p . ValidateURL . Path , "/user/emails" ) ,
2016-06-20 15:12:07 +03:00
}
2017-05-18 06:45:34 +03:00
req , _ := http . NewRequest ( "GET" , endpoint . String ( ) , nil )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "token %s" , s . AccessToken ) )
resp , err := http . DefaultClient . Do ( req )
2015-05-21 06:23:48 +03:00
if err != nil {
return "" , err
}
2015-06-23 14:23:39 +03:00
body , err := ioutil . ReadAll ( resp . Body )
2015-05-21 06:23:48 +03:00
resp . Body . Close ( )
if err != nil {
return "" , err
}
2015-06-23 14:23:39 +03:00
2015-06-06 21:15:43 +03:00
if resp . StatusCode != 200 {
2016-08-03 05:27:50 +03:00
return "" , fmt . Errorf ( "got %d from %q %s" ,
2017-05-18 06:45:34 +03:00
resp . StatusCode , endpoint . String ( ) , body )
2015-06-06 21:15:43 +03:00
}
2015-05-21 06:23:48 +03:00
2019-02-10 19:37:45 +03:00
logger . Printf ( "got %d from %q %s" , resp . StatusCode , endpoint . String ( ) , body )
2017-09-27 00:31:27 +03:00
2015-05-21 06:23:48 +03:00
if err := json . Unmarshal ( body , & emails ) ; err != nil {
2015-06-06 21:15:43 +03:00
return "" , fmt . Errorf ( "%s unmarshaling %s" , err , body )
2015-05-21 06:23:48 +03:00
}
2020-02-15 16:36:28 +03:00
returnEmail := ""
2015-05-21 06:23:48 +03:00
for _ , email := range emails {
2020-02-15 16:36:28 +03:00
if email . Verified {
returnEmail = email . Email
if email . Primary {
return returnEmail , nil
}
2015-05-21 06:23:48 +03:00
}
}
2020-02-15 16:36:28 +03:00
return returnEmail , nil
2015-05-21 06:23:48 +03:00
}
2017-09-27 00:31:27 +03:00
2018-12-20 13:37:59 +03:00
// GetUserName returns the Account user name
2019-05-05 15:33:13 +03:00
func ( p * GitHubProvider ) GetUserName ( s * sessions . SessionState ) ( string , error ) {
2017-09-27 00:31:27 +03:00
var user struct {
Login string ` json:"login" `
Email string ` json:"email" `
}
endpoint := & url . URL {
Scheme : p . ValidateURL . Scheme ,
Host : p . ValidateURL . Host ,
Path : path . Join ( p . ValidateURL . Path , "/user" ) ,
}
req , err := http . NewRequest ( "GET" , endpoint . String ( ) , nil )
if err != nil {
return "" , fmt . Errorf ( "could not create new GET request: %v" , err )
}
req . Header . Set ( "Authorization" , fmt . Sprintf ( "token %s" , s . AccessToken ) )
resp , err := http . DefaultClient . Do ( req )
if err != nil {
return "" , err
}
body , err := ioutil . ReadAll ( resp . Body )
defer resp . Body . Close ( )
if err != nil {
return "" , err
}
if resp . StatusCode != 200 {
return "" , fmt . Errorf ( "got %d from %q %s" ,
resp . StatusCode , endpoint . String ( ) , body )
}
2019-02-10 19:37:45 +03:00
logger . Printf ( "got %d from %q %s" , resp . StatusCode , endpoint . String ( ) , body )
2017-09-27 00:31:27 +03:00
if err := json . Unmarshal ( body , & user ) ; err != nil {
return "" , fmt . Errorf ( "%s unmarshaling %s" , err , body )
}
return user . Login , nil
}