2015-05-21 06:23:48 +03:00
package providers
import (
2020-05-05 18:53:33 +03:00
"context"
2020-06-01 22:02:07 +03:00
"errors"
2015-06-06 21:15:43 +03:00
"fmt"
2015-05-21 06:23:48 +03:00
"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
2022-02-15 14:18:32 +03:00
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
2020-09-29 19:44:42 +03:00
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
2023-12-18 13:03:19 +03:00
"golang.org/x/exp/maps"
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
2020-05-11 20:02:40 +03:00
Org string
Team string
Repo string
Token string
2020-06-01 22:02:07 +03:00
Users [ ] string
2015-05-21 06:23:48 +03:00
}
2020-05-05 18:53:33 +03:00
var _ Provider = ( * GitHubProvider ) ( nil )
2020-05-25 15:08:04 +03:00
const (
githubProviderName = "GitHub"
2023-12-18 13:03:19 +03:00
githubDefaultScope = "user:email read:org"
orgTeamSeparator = ":"
2020-05-25 15:08:04 +03:00
)
var (
// Default Login URL for GitHub.
// Pre-parsed URL of https://github.org/login/oauth/authorize.
githubDefaultLoginURL = & url . URL {
Scheme : "https" ,
Host : "github.com" ,
Path : "/login/oauth/authorize" ,
2015-05-21 06:23:48 +03:00
}
2020-05-25 15:08:04 +03:00
// Default Redeem URL for GitHub.
// Pre-parsed URL of https://github.org/login/oauth/access_token.
githubDefaultRedeemURL = & url . URL {
Scheme : "https" ,
Host : "github.com" ,
Path : "/login/oauth/access_token" ,
2015-05-21 06:23:48 +03:00
}
2020-05-25 15:08:04 +03:00
// Default Validation URL for GitHub.
// ValidationURL is the API Base URL.
// Other API requests are based off of this (eg to fetch users/groups).
// Pre-parsed URL of https://api.github.com/.
githubDefaultValidateURL = & url . URL {
Scheme : "https" ,
Host : "api.github.com" ,
Path : "/" ,
2015-05-21 06:23:48 +03:00
}
2020-05-25 15:08:04 +03:00
)
// NewGitHubProvider initiates a new GitHubProvider
2022-02-15 14:18:32 +03:00
func NewGitHubProvider ( p * ProviderData , opts options . GitHubOptions ) * GitHubProvider {
2020-05-25 15:08:04 +03:00
p . setProviderDefaults ( providerDefaults {
name : githubProviderName ,
loginURL : githubDefaultLoginURL ,
redeemURL : githubDefaultRedeemURL ,
profileURL : nil ,
validateURL : githubDefaultValidateURL ,
scope : githubDefaultScope ,
} )
2022-02-15 14:18:32 +03:00
provider := & GitHubProvider { ProviderData : p }
provider . setOrgTeam ( opts . Org , opts . Team )
provider . setRepo ( opts . Repo , opts . Token )
provider . setUsers ( opts . Users )
return provider
2015-05-21 06:23:48 +03:00
}
2018-12-20 13:37:59 +03:00
2020-05-17 18:34:09 +03:00
func makeGitHubHeader ( accessToken string ) http . Header {
// extra headers required by the GitHub API when making authenticated requests
extraHeaders := map [ string ] string {
acceptHeader : "application/vnd.github.v3+json" ,
}
return makeAuthorizationHeader ( tokenTypeToken , accessToken , extraHeaders )
2020-02-15 18:11:14 +03:00
}
2023-09-14 12:09:57 +03:00
func ( p * GitHubProvider ) makeGitHubAPIEndpoint ( endpoint string , params * url . Values ) * url . URL {
basePath := p . ValidateURL . Path
re := regexp . MustCompile ( ` ^/api/v\d+ ` )
match := re . FindString ( p . ValidateURL . Path )
if match != "" {
basePath = match
}
if params == nil {
params = & url . Values { }
}
return & url . URL {
Scheme : p . ValidateURL . Scheme ,
Host : p . ValidateURL . Host ,
Path : path . Join ( basePath , endpoint ) ,
RawQuery : params . Encode ( ) ,
}
}
2022-02-15 14:18:32 +03:00
// setOrgTeam adds GitHub org reading parameters to the OAuth2 scope
func ( p * GitHubProvider ) setOrgTeam ( org , team string ) {
2015-05-21 06:23:48 +03:00
p . Org = org
p . Team = team
}
2022-02-15 14:18:32 +03:00
// setRepo configures the target repository and optional token to use
func ( p * GitHubProvider ) setRepo ( repo , token string ) {
2020-05-11 20:02:40 +03:00
p . Repo = repo
p . Token = token
}
2022-02-15 14:18:32 +03:00
// setUsers configures allowed usernames
func ( p * GitHubProvider ) setUsers ( users [ ] string ) {
2020-06-01 22:02:07 +03:00
p . Users = users
}
2020-11-30 01:12:48 +03:00
// EnrichSession updates the User & Email after the initial Redeem
2020-10-24 08:06:50 +03:00
func ( p * GitHubProvider ) EnrichSession ( ctx context . Context , s * sessions . SessionState ) error {
2023-12-18 13:03:19 +03:00
// Construct user info JSON from multiple GitHub API endpoints to have a more detailed session state
if err := p . getOrgAndTeam ( ctx , s ) ; err != nil {
2020-09-27 23:35:06 +03:00
return err
}
2023-12-18 13:03:19 +03:00
if err := p . checkRestrictions ( ctx , s ) ; err != nil {
return err
}
if err := p . getEmail ( ctx , s ) ; err != nil {
return err
}
2020-09-27 23:35:06 +03:00
return p . getUser ( ctx , s )
}
2020-11-30 01:12:48 +03:00
// ValidateSession validates the AccessToken
2020-10-24 08:06:50 +03:00
func ( p * GitHubProvider ) ValidateSession ( ctx context . Context , s * sessions . SessionState ) bool {
2020-09-27 23:35:06 +03:00
return validateToken ( ctx , p , s . AccessToken , makeGitHubHeader ( s . AccessToken ) )
}
2024-01-22 16:39:53 +03:00
func ( p * GitHubProvider ) hasOrg ( s * sessions . SessionState ) error {
2015-06-06 21:03:59 +03:00
// https://developer.github.com/v3/orgs/#list-your-organizations
2023-12-18 13:03:19 +03:00
var orgs [ ] string
2015-06-06 21:03:59 +03:00
2023-12-18 13:03:19 +03:00
for _ , group := range s . Groups {
if ! strings . Contains ( group , ":" ) {
orgs = append ( orgs , group )
2017-07-05 00:36:21 +03:00
}
2015-06-06 21:03:59 +03:00
}
2020-04-14 11:36:44 +03:00
presentOrgs := make ( [ ] string , 0 , len ( orgs ) )
2015-06-06 21:03:59 +03:00
for _ , org := range orgs {
2023-12-18 13:03:19 +03:00
if p . Org == org {
logger . Printf ( "Found Github Organization:%q" , org )
return nil
2015-06-06 21:03:59 +03:00
}
2023-12-18 13:03:19 +03:00
presentOrgs = append ( presentOrgs , org )
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 )
2023-12-18 13:03:19 +03:00
return errors . New ( "user is missing required organization" )
2015-06-06 21:03:59 +03:00
}
2024-01-22 16:39:53 +03:00
func ( p * GitHubProvider ) hasOrgAndTeam ( s * sessions . SessionState ) error {
2023-12-18 13:03:19 +03:00
type orgTeam struct {
Org string ` json:"org" `
Team string ` json:"team" `
2015-05-21 06:23:48 +03:00
}
2023-12-18 13:03:19 +03:00
var presentOrgTeams [ ] orgTeam
2015-05-21 06:23:48 +03:00
2023-12-18 13:03:19 +03:00
for _ , group := range s . Groups {
if strings . Contains ( group , orgTeamSeparator ) {
ot := strings . Split ( group , orgTeamSeparator )
presentOrgTeams = append ( presentOrgTeams , orgTeam { ot [ 0 ] , ot [ 1 ] } )
2019-10-07 16:38:57 +03:00
}
2015-05-21 06:23:48 +03:00
}
2015-07-24 23:10:10 +03:00
var hasOrg bool
2023-12-18 13:03:19 +03:00
2015-07-24 23:10:10 +03:00
presentOrgs := make ( map [ string ] bool )
var presentTeams [ ] string
2023-12-18 13:03:19 +03:00
for _ , ot := range presentOrgTeams {
presentOrgs [ ot . Org ] = true
if strings . EqualFold ( p . Org , ot . Org ) {
2015-07-24 23:10:10 +03:00
hasOrg = true
2023-12-18 13:03:19 +03:00
teams := strings . Split ( p . Team , "," )
for _ , team := range teams {
if strings . EqualFold ( strings . TrimSpace ( team ) , ot . Team ) {
logger . Printf ( "Found Github Organization/Team:%q/%q" , ot . Org , ot . Team )
return nil
2016-02-17 13:21:27 +03:00
}
2015-05-21 06:23:48 +03:00
}
2023-12-18 13:03:19 +03:00
presentTeams = append ( presentTeams , ot . Team )
2015-07-24 23:10:10 +03:00
}
}
2023-12-18 13:03:19 +03:00
2015-07-24 23:10:10 +03:00
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 )
2023-12-18 13:03:19 +03:00
return errors . New ( "user is missing required team" )
2015-05-21 06:23:48 +03:00
}
2023-12-18 13:03:19 +03:00
logger . Printf ( "Missing Organization:%q in %#v" , p . Org , maps . Keys ( presentOrgs ) )
return errors . New ( "user is missing required organization" )
2015-05-21 06:23:48 +03:00
}
2023-12-18 13:03:19 +03:00
func ( p * GitHubProvider ) hasRepoAccess ( ctx context . Context , accessToken string ) error {
2020-05-11 20:02:40 +03:00
// https://developer.github.com/v3/repos/#get-a-repository
type permissions struct {
Pull bool ` json:"pull" `
Push bool ` json:"push" `
}
type repository struct {
Permissions permissions ` json:"permissions" `
Private bool ` json:"private" `
}
2023-09-14 12:09:57 +03:00
endpoint := p . makeGitHubAPIEndpoint ( "/repos/" + p . Repo , nil )
2020-05-11 20:02:40 +03:00
var repo repository
2020-07-03 21:27:25 +03:00
err := requests . New ( endpoint . String ( ) ) .
WithContext ( ctx ) .
2020-05-17 18:34:09 +03:00
WithHeaders ( makeGitHubHeader ( accessToken ) ) .
2020-07-06 19:42:26 +03:00
Do ( ) .
2020-07-03 21:27:25 +03:00
UnmarshalInto ( & repo )
2023-12-18 13:03:19 +03:00
2020-07-03 21:27:25 +03:00
if err != nil {
2023-12-18 13:03:19 +03:00
return err
2020-05-11 20:02:40 +03:00
}
// Every user can implicitly pull from a public repo, so only grant access
// if they have push access or the repo is private and they can pull
2023-12-18 13:03:19 +03:00
if repo . Permissions . Push || ( repo . Private && repo . Permissions . Pull ) {
return nil
}
return errors . New ( "user doesn't have repository access" )
2020-05-11 20:02:40 +03:00
}
2020-06-01 22:02:07 +03:00
func ( p * GitHubProvider ) hasUser ( ctx context . Context , accessToken string ) ( bool , error ) {
// https://developer.github.com/v3/users/#get-the-authenticated-user
var user struct {
Login string ` json:"login" `
Email string ` json:"email" `
}
2023-09-14 12:09:57 +03:00
endpoint := p . makeGitHubAPIEndpoint ( "/user" , nil )
2020-06-01 22:02:07 +03:00
2020-07-03 21:27:25 +03:00
err := requests . New ( endpoint . String ( ) ) .
WithContext ( ctx ) .
2020-05-17 18:34:09 +03:00
WithHeaders ( makeGitHubHeader ( accessToken ) ) .
2020-07-06 19:42:26 +03:00
Do ( ) .
2020-07-03 21:27:25 +03:00
UnmarshalInto ( & user )
2020-06-01 22:02:07 +03:00
if err != nil {
return false , err
}
if p . isVerifiedUser ( user . Login ) {
return true , nil
}
return false , nil
}
2020-05-11 20:02:40 +03:00
func ( p * GitHubProvider ) isCollaborator ( ctx context . Context , username , accessToken string ) ( bool , error ) {
//https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator
2023-09-14 12:09:57 +03:00
endpoint := p . makeGitHubAPIEndpoint ( "/repos/" + p . Repo + "/collaborators/" + username , nil )
2020-07-06 19:42:26 +03:00
result := requests . New ( endpoint . String ( ) ) .
2020-07-03 21:27:25 +03:00
WithContext ( ctx ) .
2020-05-17 18:34:09 +03:00
WithHeaders ( makeGitHubHeader ( accessToken ) ) .
2020-07-03 21:27:25 +03:00
Do ( )
2020-07-06 19:42:26 +03:00
if result . Error ( ) != nil {
return false , result . Error ( )
2020-05-11 20:02:40 +03:00
}
2020-07-06 19:42:26 +03:00
if result . StatusCode ( ) != 204 {
2020-05-11 20:02:40 +03:00
return false , fmt . Errorf ( "got %d from %q %s" ,
2020-07-06 19:42:26 +03:00
result . StatusCode ( ) , endpoint . String ( ) , result . Body ( ) )
2020-05-11 20:02:40 +03:00
}
2020-07-06 19:42:26 +03:00
logger . Printf ( "got %d from %q %s" , result . StatusCode ( ) , endpoint . String ( ) , result . Body ( ) )
2020-05-11 20:02:40 +03:00
return true , nil
}
2020-09-27 23:35:06 +03:00
// getEmail updates the SessionState Email
func ( p * GitHubProvider ) getEmail ( ctx context . Context , s * sessions . SessionState ) 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
}
2023-09-14 12:09:57 +03:00
endpoint := p . makeGitHubAPIEndpoint ( "/user/emails" , nil )
2023-12-18 13:03:19 +03:00
2020-07-03 21:27:25 +03:00
err := requests . New ( endpoint . String ( ) ) .
WithContext ( ctx ) .
2020-05-17 18:34:09 +03:00
WithHeaders ( makeGitHubHeader ( s . AccessToken ) ) .
2020-07-06 19:42:26 +03:00
Do ( ) .
2020-07-03 21:27:25 +03:00
UnmarshalInto ( & emails )
2015-05-21 06:23:48 +03:00
if err != nil {
2020-09-27 23:35:06 +03:00
return err
2015-05-21 06:23:48 +03:00
}
2015-06-23 14:23:39 +03:00
2015-05-21 06:23:48 +03:00
for _ , email := range emails {
2020-02-15 16:36:28 +03:00
if email . Verified {
if email . Primary {
2020-09-27 23:35:06 +03:00
s . Email = email . Email
return nil
2020-02-15 16:36:28 +03:00
}
2015-05-21 06:23:48 +03:00
}
}
2020-09-27 23:35:06 +03:00
return nil
2015-05-21 06:23:48 +03:00
}
2017-09-27 00:31:27 +03:00
2020-09-27 23:35:06 +03:00
// getUser updates the SessionState User
func ( p * GitHubProvider ) getUser ( ctx context . Context , s * sessions . SessionState ) error {
2017-09-27 00:31:27 +03:00
var user struct {
Login string ` json:"login" `
Email string ` json:"email" `
}
2023-09-14 12:09:57 +03:00
endpoint := p . makeGitHubAPIEndpoint ( "/user" , nil )
2017-09-27 00:31:27 +03:00
2020-07-03 21:27:25 +03:00
err := requests . New ( endpoint . String ( ) ) .
WithContext ( ctx ) .
2020-05-17 18:34:09 +03:00
WithHeaders ( makeGitHubHeader ( s . AccessToken ) ) .
2020-07-06 19:42:26 +03:00
Do ( ) .
2020-07-03 21:27:25 +03:00
UnmarshalInto ( & user )
2017-09-27 00:31:27 +03:00
if err != nil {
2020-09-27 23:35:06 +03:00
return err
2017-09-27 00:31:27 +03:00
}
2020-05-11 20:02:40 +03:00
// Now that we have the username we can check collaborator status
2020-06-01 22:02:07 +03:00
if ! p . isVerifiedUser ( user . Login ) && p . Org == "" && p . Repo != "" && p . Token != "" {
2020-05-11 20:02:40 +03:00
if ok , err := p . isCollaborator ( ctx , user . Login , p . Token ) ; err != nil || ! ok {
2020-09-27 23:35:06 +03:00
return err
2020-05-11 20:02:40 +03:00
}
}
2020-09-27 23:35:06 +03:00
s . User = user . Login
return nil
2020-02-15 18:11:14 +03:00
}
2020-06-01 22:02:07 +03:00
func ( p * GitHubProvider ) isVerifiedUser ( username string ) bool {
for _ , u := range p . Users {
if username == u {
return true
}
}
return false
}
2023-12-18 13:03:19 +03:00
func ( p * GitHubProvider ) checkRestrictions ( ctx context . Context , s * sessions . SessionState ) error {
// If a user is verified by username options, skip the following restrictions
if ok , err := p . checkUserRestriction ( ctx , s ) ; err != nil || ok {
return err
}
2024-01-22 16:39:53 +03:00
if err := p . hasOrgAndTeamAccess ( s ) ; err != nil {
2023-12-18 13:03:19 +03:00
return err
}
if p . Org == "" && p . Repo != "" && p . Token == "" {
// If we have a token we'll do the collaborator check in GetUserName
return p . hasRepoAccess ( ctx , s . AccessToken )
}
return nil
}
func ( p * GitHubProvider ) checkUserRestriction ( ctx context . Context , s * sessions . SessionState ) ( bool , error ) {
if len ( p . Users ) == 0 {
return false , nil
}
verifiedUser , err := p . hasUser ( ctx , s . AccessToken )
if err != nil {
return verifiedUser , err
}
// org and repository options are not configured
if ! verifiedUser && p . Org == "" && p . Repo == "" {
return false , errors . New ( "missing github user" )
}
return verifiedUser , nil
}
2024-01-22 16:39:53 +03:00
func ( p * GitHubProvider ) hasOrgAndTeamAccess ( s * sessions . SessionState ) error {
2023-12-18 13:03:19 +03:00
if p . Org != "" && p . Team != "" {
2024-01-22 16:39:53 +03:00
return p . hasOrgAndTeam ( s )
2023-12-18 13:03:19 +03:00
}
if p . Org != "" {
2024-01-22 16:39:53 +03:00
return p . hasOrg ( s )
2023-12-18 13:03:19 +03:00
}
return nil
}
func ( p * GitHubProvider ) getOrgAndTeam ( ctx context . Context , s * sessions . SessionState ) error {
err := p . getOrgs ( ctx , s )
if err != nil {
return err
}
return p . getTeams ( ctx , s )
}
func ( p * GitHubProvider ) getOrgs ( ctx context . Context , s * sessions . SessionState ) error {
// https://docs.github.com/en/rest/orgs/orgs#list-organizations-for-the-authenticated-user
type Organization struct {
Login string ` json:"login" `
}
pn := 1
for {
params := url . Values {
"per_page" : { "100" } ,
"page" : { strconv . Itoa ( pn ) } ,
}
endpoint := p . makeGitHubAPIEndpoint ( "/user/orgs" , & params )
var orgs [ ] Organization
err := requests . New ( endpoint . String ( ) ) .
WithContext ( ctx ) .
WithHeaders ( makeGitHubHeader ( s . AccessToken ) ) .
Do ( ) .
UnmarshalInto ( & orgs )
if err != nil {
return err
}
if len ( orgs ) == 0 {
break
}
for _ , org := range orgs {
logger . Printf ( "Member of Github Organization:%q" , org . Login )
s . Groups = append ( s . Groups , org . Login )
}
pn ++
}
return nil
}
func ( p * GitHubProvider ) getTeams ( ctx context . Context , s * sessions . SessionState ) error {
// https://docs.github.com/en/rest/teams/teams?#list-user-teams
type Team struct {
Name string ` json:"name" `
Slug string ` json:"slug" `
Org struct {
Login string ` json:"login" `
} ` json:"organization" `
}
pn := 1
for {
params := url . Values {
"per_page" : { "100" } ,
"page" : { strconv . Itoa ( pn ) } ,
}
endpoint := p . makeGitHubAPIEndpoint ( "/user/teams" , & params )
var teams [ ] Team
err := requests . New ( endpoint . String ( ) ) .
WithContext ( ctx ) .
WithHeaders ( makeGitHubHeader ( s . AccessToken ) ) .
Do ( ) .
UnmarshalInto ( & teams )
if err != nil {
return err
}
if len ( teams ) == 0 {
break
}
for _ , team := range teams {
logger . Printf ( "Member of Github Organization/Team:%q/%q" , team . Org . Login , team . Slug )
s . Groups = append ( s . Groups , team . Org . Login + orgTeamSeparator + team . Slug )
}
pn ++
}
return nil
}