2016-07-26 01:30:28 +05:30
// Package github provides authentication strategies using GitHub.
package github
import (
2017-03-09 00:03:19 +05:30
"context"
2017-04-08 03:25:22 +05:30
"crypto/tls"
"crypto/x509"
2016-07-26 01:30:28 +05:30
"encoding/json"
2016-11-19 03:10:41 +05:30
"errors"
2016-07-26 01:30:28 +05:30
"fmt"
"io/ioutil"
2017-04-08 03:25:22 +05:30
"net"
2016-07-26 01:30:28 +05:30
"net/http"
2017-01-27 03:45:01 +05:30
"regexp"
2016-07-26 01:30:28 +05:30
"strconv"
2017-04-08 03:25:22 +05:30
"strings"
"time"
2016-07-26 01:30:28 +05:30
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
2017-07-26 02:15:17 +05:30
"github.com/sirupsen/logrus"
2018-09-03 12:14:44 +05:30
"github.com/dexidp/dex/connector"
2016-07-26 01:30:28 +05:30
)
2016-11-19 03:10:41 +05:30
const (
2017-08-18 23:34:12 +05:30
apiURL = "https://api.github.com"
// GitHub requires this scope to access '/user' and '/user/emails' API endpoints.
2016-11-19 03:10:41 +05:30
scopeEmail = "user:email"
2017-08-18 23:34:12 +05:30
// GitHub requires this scope to access '/user/teams' and '/orgs' API endpoints
// which are used when a client includes the 'groups' scope.
scopeOrgs = "read:org"
2016-11-19 03:10:41 +05:30
)
2016-07-26 01:30:28 +05:30
2017-08-08 04:28:26 +05:30
// Pagination URL patterns
// https://developer.github.com/v3/#pagination
var reNext = regexp . MustCompile ( "<([^>]+)>; rel=\"next\"" )
var reLast = regexp . MustCompile ( "<([^>]+)>; rel=\"last\"" )
2016-07-26 01:30:28 +05:30
// Config holds configuration options for github logins.
type Config struct {
2018-09-13 19:05:02 +05:30
ClientID string ` json:"clientID" `
ClientSecret string ` json:"clientSecret" `
RedirectURI string ` json:"redirectURI" `
Org string ` json:"org" `
Orgs [ ] Org ` json:"orgs" `
HostName string ` json:"hostName" `
RootCA string ` json:"rootCA" `
TeamNameField string ` json:"teamNameField" `
2016-07-26 01:30:28 +05:30
}
2017-08-04 02:23:38 +05:30
// Org holds org-team filters, in which teams are optional.
type Org struct {
// Organization name in github (not slug, full name). Only users in this github
// organization can authenticate.
Name string ` json:"name" `
// Names of teams in a github organization. A user will be able to
// authenticate if they are members of at least one of these teams. Users
// in the organization can authenticate if this field is omitted from the
// config file.
Teams [ ] string ` json:"teams,omitempty" `
}
2016-07-26 01:30:28 +05:30
// Open returns a strategy for logging in through GitHub.
2017-10-21 20:24:54 +05:30
func ( c * Config ) Open ( id string , logger logrus . FieldLogger ) ( connector . Connector , error ) {
2017-08-04 02:23:38 +05:30
if c . Org != "" {
// Return error if both 'org' and 'orgs' fields are used.
if len ( c . Orgs ) > 0 {
return nil , errors . New ( "github: cannot use both 'org' and 'orgs' fields simultaneously" )
}
logger . Warnln ( "github: legacy field 'org' being used. Switch to the newer 'orgs' field structure" )
}
2017-04-08 03:25:22 +05:30
g := githubConnector {
2016-11-19 03:10:41 +05:30
redirectURI : c . RedirectURI ,
org : c . Org ,
2017-08-04 02:23:38 +05:30
orgs : c . Orgs ,
2016-11-19 03:10:41 +05:30
clientID : c . ClientID ,
clientSecret : c . ClientSecret ,
2017-04-08 03:25:22 +05:30
apiURL : apiURL ,
2016-11-23 05:05:46 +05:30
logger : logger ,
2017-04-08 03:25:22 +05:30
}
if c . HostName != "" {
// ensure this is a hostname and not a URL or path.
if strings . Contains ( c . HostName , "/" ) {
return nil , errors . New ( "invalid hostname: hostname cannot contain `/`" )
}
g . hostName = c . HostName
g . apiURL = "https://" + c . HostName + "/api/v3"
}
if c . RootCA != "" {
if c . HostName == "" {
return nil , errors . New ( "invalid connector config: Host name field required for a root certificate file" )
}
g . rootCA = c . RootCA
var err error
if g . httpClient , err = newHTTPClient ( g . rootCA ) ; err != nil {
return nil , fmt . Errorf ( "failed to create HTTP client: %v" , err )
}
}
2018-09-13 19:05:02 +05:30
switch c . TeamNameField {
case "name" , "slug" , "" :
g . teamNameField = c . TeamNameField
default :
return nil , fmt . Errorf ( "invalid connector config: unsupported team name field value `%s`" , c . TeamNameField )
}
2017-04-08 03:25:22 +05:30
return & g , nil
2016-07-26 01:30:28 +05:30
}
type connectorData struct {
// GitHub's OAuth2 tokens never expire. We don't need a refresh token.
AccessToken string ` json:"accessToken" `
}
var (
_ connector . CallbackConnector = ( * githubConnector ) ( nil )
2016-11-19 03:10:41 +05:30
_ connector . RefreshConnector = ( * githubConnector ) ( nil )
2016-07-26 01:30:28 +05:30
)
type githubConnector struct {
redirectURI string
org string
2017-08-04 02:23:38 +05:30
orgs [ ] Org
2016-11-19 03:10:41 +05:30
clientID string
clientSecret string
2016-11-23 05:05:46 +05:30
logger logrus . FieldLogger
2017-04-08 03:25:22 +05:30
// apiURL defaults to "https://api.github.com"
apiURL string
// hostName of the GitHub enterprise account.
hostName string
// Used to support untrusted/self-signed CA certs.
rootCA string
// HTTP Client that trusts the custom delcared rootCA cert.
2018-09-13 19:05:02 +05:30
httpClient * http . Client
teamNameField string
2016-07-26 01:30:28 +05:30
}
2017-08-18 23:34:12 +05:30
// groupsRequired returns whether dex requires GitHub's 'read:org' scope. Dex
// needs 'read:org' if 'orgs' or 'org' fields are populated in a config file.
// Clients can require 'groups' scope without setting 'orgs'/'org'.
func ( c * githubConnector ) groupsRequired ( groupScope bool ) bool {
return len ( c . orgs ) > 0 || c . org != "" || groupScope
}
2016-11-19 03:10:41 +05:30
func ( c * githubConnector ) oauth2Config ( scopes connector . Scopes ) * oauth2 . Config {
2017-08-18 23:34:12 +05:30
// 'read:org' scope is required by the GitHub API, and thus for dex to ensure
// a user is a member of orgs and teams provided in configs.
2017-08-17 22:43:00 +05:30
githubScopes := [ ] string { scopeEmail }
2017-08-18 23:34:12 +05:30
if c . groupsRequired ( scopes . Groups ) {
2017-08-17 22:43:00 +05:30
githubScopes = append ( githubScopes , scopeOrgs )
2016-11-19 03:10:41 +05:30
}
2017-04-08 03:25:22 +05:30
endpoint := github . Endpoint
// case when it is a GitHub Enterprise account.
if c . hostName != "" {
endpoint = oauth2 . Endpoint {
AuthURL : "https://" + c . hostName + "/login/oauth/authorize" ,
TokenURL : "https://" + c . hostName + "/login/oauth/access_token" ,
}
}
2016-11-19 03:10:41 +05:30
return & oauth2 . Config {
ClientID : c . clientID ,
ClientSecret : c . clientSecret ,
2017-04-08 03:25:22 +05:30
Endpoint : endpoint ,
2016-11-19 03:10:41 +05:30
Scopes : githubScopes ,
}
2016-07-26 01:30:28 +05:30
}
2016-11-19 03:10:41 +05:30
func ( c * githubConnector ) LoginURL ( scopes connector . Scopes , callbackURL , state string ) ( string , error ) {
2016-07-26 01:30:28 +05:30
if c . redirectURI != callbackURL {
2017-06-14 04:22:33 +05:30
return "" , fmt . Errorf ( "expected callback URL %q did not match the URL in the config %q" , callbackURL , c . redirectURI )
2016-07-26 01:30:28 +05:30
}
2017-04-08 03:25:22 +05:30
2016-11-19 03:10:41 +05:30
return c . oauth2Config ( scopes ) . AuthCodeURL ( state ) , nil
2016-07-26 01:30:28 +05:30
}
type oauth2Error struct {
error string
errorDescription string
}
func ( e * oauth2Error ) Error ( ) string {
if e . errorDescription == "" {
return e . error
}
return e . error + ": " + e . errorDescription
}
2017-04-08 03:25:22 +05:30
// newHTTPClient returns a new HTTP client that trusts the custom delcared rootCA cert.
func newHTTPClient ( rootCA string ) ( * http . Client , error ) {
tlsConfig := tls . Config { RootCAs : x509 . NewCertPool ( ) }
rootCABytes , err := ioutil . ReadFile ( rootCA )
if err != nil {
return nil , fmt . Errorf ( "failed to read root-ca: %v" , err )
}
if ! tlsConfig . RootCAs . AppendCertsFromPEM ( rootCABytes ) {
return nil , fmt . Errorf ( "no certs found in root CA file %q" , rootCA )
}
return & http . Client {
Transport : & http . Transport {
TLSClientConfig : & tlsConfig ,
Proxy : http . ProxyFromEnvironment ,
DialContext : ( & net . Dialer {
Timeout : 30 * time . Second ,
KeepAlive : 30 * time . Second ,
DualStack : true ,
} ) . DialContext ,
MaxIdleConns : 100 ,
IdleConnTimeout : 90 * time . Second ,
TLSHandshakeTimeout : 10 * time . Second ,
ExpectContinueTimeout : 1 * time . Second ,
} ,
} , nil
}
2016-11-19 03:10:41 +05:30
func ( c * githubConnector ) HandleCallback ( s connector . Scopes , r * http . Request ) ( identity connector . Identity , err error ) {
2016-07-26 01:30:28 +05:30
q := r . URL . Query ( )
if errType := q . Get ( "error" ) ; errType != "" {
2016-10-27 22:38:08 +05:30
return identity , & oauth2Error { errType , q . Get ( "error_description" ) }
2016-07-26 01:30:28 +05:30
}
2016-11-19 03:10:41 +05:30
oauth2Config := c . oauth2Config ( s )
2017-04-08 03:25:22 +05:30
2016-11-19 03:10:41 +05:30
ctx := r . Context ( )
2017-04-08 03:25:22 +05:30
// GitHub Enterprise account
if c . httpClient != nil {
ctx = context . WithValue ( r . Context ( ) , oauth2 . HTTPClient , c . httpClient )
}
2016-11-19 03:10:41 +05:30
token , err := oauth2Config . Exchange ( ctx , q . Get ( "code" ) )
2016-07-26 01:30:28 +05:30
if err != nil {
2016-10-27 22:38:08 +05:30
return identity , fmt . Errorf ( "github: failed to get token: %v" , err )
2016-07-26 01:30:28 +05:30
}
2016-11-19 03:10:41 +05:30
client := oauth2Config . Client ( ctx , token )
user , err := c . user ( ctx , client )
2016-07-26 01:30:28 +05:30
if err != nil {
2016-11-19 03:10:41 +05:30
return identity , fmt . Errorf ( "github: get user: %v" , err )
2016-07-26 01:30:28 +05:30
}
2016-11-19 03:10:41 +05:30
username := user . Name
if username == "" {
username = user . Login
}
identity = connector . Identity {
UserID : strconv . Itoa ( user . ID ) ,
Username : username ,
Email : user . Email ,
EmailVerified : true ,
}
2017-08-18 23:34:12 +05:30
// Only set identity.Groups if 'orgs', 'org', or 'groups' scope are specified.
if c . groupsRequired ( s . Groups ) {
groups , err := c . getGroups ( ctx , client , s . Groups , user . Login )
if err != nil {
return identity , err
2016-07-26 01:30:28 +05:30
}
2016-11-19 03:10:41 +05:30
identity . Groups = groups
2016-07-26 01:30:28 +05:30
}
2016-11-19 03:10:41 +05:30
if s . OfflineAccess {
data := connectorData { AccessToken : token . AccessToken }
connData , err := json . Marshal ( data )
if err != nil {
return identity , fmt . Errorf ( "marshal connector data: %v" , err )
}
identity . ConnectorData = connData
2016-07-26 01:30:28 +05:30
}
2016-11-19 03:10:41 +05:30
return identity , nil
}
2017-08-04 02:23:38 +05:30
func ( c * githubConnector ) Refresh ( ctx context . Context , s connector . Scopes , identity connector . Identity ) ( connector . Identity , error ) {
if len ( identity . ConnectorData ) == 0 {
return identity , errors . New ( "no upstream access token found" )
2016-11-19 03:10:41 +05:30
}
var data connectorData
2017-08-04 02:23:38 +05:30
if err := json . Unmarshal ( identity . ConnectorData , & data ) ; err != nil {
return identity , fmt . Errorf ( "github: unmarshal access token: %v" , err )
2016-07-26 01:30:28 +05:30
}
2016-11-19 03:10:41 +05:30
client := c . oauth2Config ( s ) . Client ( ctx , & oauth2 . Token { AccessToken : data . AccessToken } )
user , err := c . user ( ctx , client )
2016-07-26 01:30:28 +05:30
if err != nil {
2017-08-04 02:23:38 +05:30
return identity , fmt . Errorf ( "github: get user: %v" , err )
2016-07-26 01:30:28 +05:30
}
username := user . Name
if username == "" {
username = user . Login
}
2017-08-04 02:23:38 +05:30
identity . Username = username
identity . Email = user . Email
2017-08-18 23:34:12 +05:30
// Only set identity.Groups if 'orgs', 'org', or 'groups' scope are specified.
if c . groupsRequired ( s . Groups ) {
groups , err := c . getGroups ( ctx , client , s . Groups , user . Login )
if err != nil {
return identity , err
2017-08-04 02:23:38 +05:30
}
identity . Groups = groups
}
return identity , nil
}
2016-11-19 03:10:41 +05:30
2017-08-18 23:34:12 +05:30
// getGroups retrieves GitHub orgs and teams a user is in, if any.
func ( c * githubConnector ) getGroups ( ctx context . Context , client * http . Client , groupScope bool , userLogin string ) ( [ ] string , error ) {
if len ( c . orgs ) > 0 {
return c . groupsForOrgs ( ctx , client , userLogin )
} else if c . org != "" {
return c . teamsForOrg ( ctx , client , c . org )
2018-11-15 05:01:31 +05:30
} else if groupScope {
return c . userGroups ( ctx , client )
2017-08-18 23:34:12 +05:30
}
return nil , nil
}
2018-11-15 05:01:31 +05:30
// formatTeamName return unique team name: prgs might have the same team names team name should be prefixed with org name to make team names unique across orgs.
func formatTeamName ( org string , team string ) string {
return fmt . Sprintf ( "%s:%s" , org , team )
}
2017-08-18 23:34:12 +05:30
// groupsForOrgs enforces org and team constraints on user authorization
2017-08-04 02:23:38 +05:30
// Cases in which user is authorized:
// N orgs, no teams: user is member of at least 1 org
// N orgs, M teams per org: user is member of any team from at least 1 org
// N-1 orgs, M teams per org, 1 org with no teams: user is member of any team
// from at least 1 org, or member of org with no teams
2017-08-18 23:34:12 +05:30
func ( c * githubConnector ) groupsForOrgs ( ctx context . Context , client * http . Client , userName string ) ( groups [ ] string , err error ) {
2017-08-04 02:23:38 +05:30
var inOrgNoTeams bool
for _ , org := range c . orgs {
inOrg , err := c . userInOrg ( ctx , client , userName , org . Name )
2016-11-19 03:10:41 +05:30
if err != nil {
2017-08-04 02:23:38 +05:30
return groups , err
}
if ! inOrg {
continue
}
2017-08-18 23:34:12 +05:30
teams , err := c . teamsForOrg ( ctx , client , org . Name )
2017-08-04 02:23:38 +05:30
if err != nil {
return groups , err
}
// User is in at least one org. User is authorized if no teams are specified
// in config; include all teams in claim. Otherwise filter out teams not in
// 'teams' list in config.
if len ( org . Teams ) == 0 {
inOrgNoTeams = true
} else if teams = filterTeams ( teams , org . Teams ) ; len ( teams ) == 0 {
2017-08-17 23:10:22 +05:30
c . logger . Infof ( "github: user %q in org %q but no teams" , userName , org . Name )
2017-08-04 02:23:38 +05:30
}
for _ , teamName := range teams {
2018-11-15 05:01:31 +05:30
groups = append ( groups , formatTeamName ( org . Name , teamName ) )
2017-08-04 02:23:38 +05:30
}
}
if inOrgNoTeams || len ( groups ) > 0 {
return
}
return groups , fmt . Errorf ( "github: user %q not in required orgs or teams" , userName )
}
2018-11-15 05:01:31 +05:30
func ( c * githubConnector ) userGroups ( ctx context . Context , client * http . Client ) ( groups [ ] string , err error ) {
orgs , err := c . userOrgs ( ctx , client )
if err != nil {
return groups , err
}
orgTeams , err := c . userOrgTeams ( ctx , client )
if err != nil {
return groups , err
}
for _ , org := range orgs {
if teams , ok := orgTeams [ org ] ; ! ok {
groups = append ( groups , org )
} else {
for _ , team := range teams {
groups = append ( groups , formatTeamName ( org , team ) )
}
}
}
return groups , err
}
// userOrgs retrieves list of current user orgs
func ( c * githubConnector ) userOrgs ( ctx context . Context , client * http . Client ) ( [ ] string , error ) {
apiURL , groups := c . apiURL + "/user/orgs" , [ ] string { }
for {
// https://developer.github.com/v3/orgs/#list-your-organizations
var (
orgs [ ] org
err error
)
if apiURL , err = get ( ctx , client , apiURL , & orgs ) ; err != nil {
return nil , fmt . Errorf ( "github: get orgs: %v" , err )
}
for _ , org := range orgs {
groups = append ( groups , org . Login )
}
if apiURL == "" {
break
}
}
return groups , nil
}
// userOrgTeams retrieves teams which current user belongs to.
// Method returns a map where key is an org name and value list of teams under the org.
func ( c * githubConnector ) userOrgTeams ( ctx context . Context , client * http . Client ) ( map [ string ] [ ] string , error ) {
apiURL , groups := c . apiURL + "/user/teams" , map [ string ] [ ] string { }
for {
// https://developer.github.com/v3/orgs/teams/#list-user-teams
var (
teams [ ] team
err error
)
if apiURL , err = get ( ctx , client , apiURL , & teams ) ; err != nil {
return nil , fmt . Errorf ( "github: get teams: %v" , err )
}
for _ , team := range teams {
groups [ team . Org . Login ] = append ( groups [ team . Org . Login ] , team . Name )
}
if apiURL == "" {
break
}
}
return groups , nil
}
2017-08-04 02:23:38 +05:30
// Filter the users' team memberships by 'teams' from config.
func filterTeams ( userTeams , configTeams [ ] string ) ( teams [ ] string ) {
teamFilter := make ( map [ string ] struct { } )
for _ , team := range configTeams {
if _ , ok := teamFilter [ team ] ; ! ok {
teamFilter [ team ] = struct { } { }
}
}
for _ , team := range userTeams {
if _ , ok := teamFilter [ team ] ; ok {
teams = append ( teams , team )
2016-11-19 03:10:41 +05:30
}
2016-07-26 01:30:28 +05:30
}
2017-08-04 02:23:38 +05:30
return
2016-07-26 01:30:28 +05:30
}
2017-08-08 04:28:26 +05:30
// get creates a "GET `apiURL`" request with context, sends the request using
// the client, and decodes the resulting response body into v. A pagination URL
// is returned if one exists. Any errors encountered when building requests,
// sending requests, and reading and decoding response data are returned.
func get ( ctx context . Context , client * http . Client , apiURL string , v interface { } ) ( string , error ) {
req , err := http . NewRequest ( "GET" , apiURL , nil )
2016-11-19 03:10:41 +05:30
if err != nil {
2017-08-08 04:28:26 +05:30
return "" , fmt . Errorf ( "github: new req: %v" , err )
2016-07-26 01:30:28 +05:30
}
2016-11-19 03:10:41 +05:30
req = req . WithContext ( ctx )
resp , err := client . Do ( req )
if err != nil {
2017-08-08 04:28:26 +05:30
return "" , fmt . Errorf ( "github: get URL %v" , err )
2016-11-19 03:10:41 +05:30
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
body , err := ioutil . ReadAll ( resp . Body )
if err != nil {
2017-08-08 04:28:26 +05:30
return "" , fmt . Errorf ( "github: read body: %v" , err )
}
return "" , fmt . Errorf ( "%s: %s" , resp . Status , body )
}
if err := json . NewDecoder ( resp . Body ) . Decode ( v ) ; err != nil {
return "" , fmt . Errorf ( "failed to decode response: %v" , err )
}
return getPagination ( apiURL , resp ) , nil
}
// getPagination checks the "Link" header field for "next" or "last" pagination
// URLs, and returns true only if a "next" URL is found. The next pages' URL is
// returned if a "next" URL is found. apiURL is returned if apiURL equals the
// "last" URL or no "next" or "last" URL are found.
//
// https://developer.github.com/v3/#pagination
func getPagination ( apiURL string , resp * http . Response ) string {
if resp == nil {
return ""
}
links := resp . Header . Get ( "Link" )
if len ( reLast . FindStringSubmatch ( links ) ) > 1 {
lastPageURL := reLast . FindStringSubmatch ( links ) [ 1 ]
if apiURL == lastPageURL {
return ""
2016-11-19 03:10:41 +05:30
}
2017-08-08 04:28:26 +05:30
} else {
return ""
2016-11-19 03:10:41 +05:30
}
2017-08-08 04:28:26 +05:30
if len ( reNext . FindStringSubmatch ( links ) ) > 1 {
return reNext . FindStringSubmatch ( links ) [ 1 ]
}
return ""
}
// user holds GitHub user information (relevant to dex) as defined by
// https://developer.github.com/v3/users/#response-with-public-profile-information
type user struct {
Name string ` json:"name" `
Login string ` json:"login" `
ID int ` json:"id" `
Email string ` json:"email" `
}
// user queries the GitHub API for profile information using the provided client.
//
// The HTTP client is expected to be constructed by the golang.org/x/oauth2 package,
// which inserts a bearer token as part of the request.
func ( c * githubConnector ) user ( ctx context . Context , client * http . Client ) ( user , error ) {
// https://developer.github.com/v3/users/#get-the-authenticated-user
var u user
if _ , err := get ( ctx , client , c . apiURL + "/user" , & u ) ; err != nil {
return u , err
}
2018-05-10 09:25:11 +05:30
// Only public user emails are returned by 'GET /user'. u.Email will be empty
2017-08-08 04:28:26 +05:30
// if a users' email is private. We must retrieve private emails explicitly.
if u . Email == "" {
var err error
if u . Email , err = c . userEmail ( ctx , client ) ; err != nil {
return u , err
}
2016-11-19 03:10:41 +05:30
}
return u , nil
}
2017-08-08 04:28:26 +05:30
// userEmail holds GitHub user email information as defined by
// https://developer.github.com/v3/users/emails/#response
type userEmail struct {
Email string ` json:"email" `
Verified bool ` json:"verified" `
Primary bool ` json:"primary" `
Visibility string ` json:"visibility" `
}
// userEmail queries the GitHub API for a users' email information using the
// provided client. Only returns the users' verified, primary email (private or
// public).
//
// The HTTP client is expected to be constructed by the golang.org/x/oauth2 package,
// which inserts a bearer token as part of the request.
func ( c * githubConnector ) userEmail ( ctx context . Context , client * http . Client ) ( string , error ) {
apiURL := c . apiURL + "/user/emails"
for {
// https://developer.github.com/v3/users/emails/#list-email-addresses-for-a-user
var (
emails [ ] userEmail
err error
)
if apiURL , err = get ( ctx , client , apiURL , & emails ) ; err != nil {
return "" , err
}
for _ , email := range emails {
2017-08-18 21:16:05 +05:30
/ *
if GitHub Enterprise , set email . Verified to true
This change being made because GitHub Enterprise does not
support email verification . CircleCI indicated that GitHub
advised them not to check for verified emails
( https : //circleci.com/enterprise/changelog/#1-47-1).
In addition , GitHub Enterprise support replied to a support
2017-09-14 22:14:15 +05:30
ticket with " There is no way to verify an email address in
GitHub Enterprise . "
2017-08-18 21:16:05 +05:30
* /
2017-08-18 02:56:10 +05:30
if c . hostName != "" {
email . Verified = true
}
2017-09-14 22:14:15 +05:30
2017-08-08 04:28:26 +05:30
if email . Verified && email . Primary {
return email . Email , nil
}
}
if apiURL == "" {
break
}
}
return "" , errors . New ( "github: user has no verified, primary email" )
}
2017-08-04 02:23:38 +05:30
// userInOrg queries the GitHub API for a users' org membership.
//
// The HTTP passed client is expected to be constructed by the golang.org/x/oauth2 package,
// which inserts a bearer token as part of the request.
func ( c * githubConnector ) userInOrg ( ctx context . Context , client * http . Client , userName , orgName string ) ( bool , error ) {
// requester == user, so GET-ing this endpoint should return 404/302 if user
// is not a member
//
// https://developer.github.com/v3/orgs/members/#check-membership
apiURL := fmt . Sprintf ( "%s/orgs/%s/members/%s" , c . apiURL , orgName , userName )
req , err := http . NewRequest ( "GET" , apiURL , nil )
if err != nil {
return false , fmt . Errorf ( "github: new req: %v" , err )
}
req = req . WithContext ( ctx )
resp , err := client . Do ( req )
if err != nil {
return false , fmt . Errorf ( "github: get teams: %v" , err )
}
defer resp . Body . Close ( )
switch resp . StatusCode {
case http . StatusNoContent :
case http . StatusFound , http . StatusNotFound :
2017-08-17 23:10:22 +05:30
c . logger . Infof ( "github: user %q not in org %q or application not authorized to read org data" , userName , orgName )
2017-08-04 02:23:38 +05:30
default :
err = fmt . Errorf ( "github: unexpected return status: %q" , resp . Status )
}
// 204 if user is a member
return resp . StatusCode == http . StatusNoContent , err
}
2017-08-08 04:28:26 +05:30
// teams holds GitHub a users' team information as defined by
// https://developer.github.com/v3/orgs/teams/#response-12
type team struct {
Name string ` json:"name" `
2018-11-15 05:01:31 +05:30
Org org ` json:"organization" `
2018-09-13 19:05:02 +05:30
Slug string ` json:"slug" `
2017-08-08 04:28:26 +05:30
}
2018-11-15 05:01:31 +05:30
type org struct {
Login string ` json:"login" `
}
2017-08-18 23:34:12 +05:30
// teamsForOrg queries the GitHub API for team membership within a specific organization.
2016-11-19 03:10:41 +05:30
//
// The HTTP passed client is expected to be constructed by the golang.org/x/oauth2 package,
// which inserts a bearer token as part of the request.
2017-08-18 23:34:12 +05:30
func ( c * githubConnector ) teamsForOrg ( ctx context . Context , client * http . Client , orgName string ) ( [ ] string , error ) {
2017-08-08 04:28:26 +05:30
apiURL , groups := c . apiURL + "/user/teams" , [ ] string { }
2017-01-27 03:45:01 +05:30
for {
2017-08-08 04:28:26 +05:30
// https://developer.github.com/v3/orgs/teams/#list-user-teams
var (
teams [ ] team
err error
)
if apiURL , err = get ( ctx , client , apiURL , & teams ) ; err != nil {
2017-08-18 23:34:12 +05:30
return nil , fmt . Errorf ( "github: get teams: %v" , err )
2017-01-27 03:45:01 +05:30
}
for _ , team := range teams {
2017-08-04 02:23:38 +05:30
if team . Org . Login == orgName {
2018-09-13 19:05:02 +05:30
switch c . teamNameField {
case "name" , "" :
groups = append ( groups , team . Name )
case "slug" :
groups = append ( groups , team . Slug )
}
2017-01-27 03:45:01 +05:30
}
}
2017-08-08 04:28:26 +05:30
if apiURL == "" {
2017-01-27 03:45:01 +05:30
break
2016-07-26 01:30:28 +05:30
}
}
2017-08-08 04:28:26 +05:30
2016-07-26 01:30:28 +05:30
return groups , nil
}