199 lines
4.9 KiB
Go
199 lines
4.9 KiB
Go
package oauth2
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/google/go-github/github"
|
|
"github.com/influxdata/influxdb/chronograf"
|
|
"golang.org/x/oauth2"
|
|
ogh "golang.org/x/oauth2/github"
|
|
)
|
|
|
|
var _ Provider = &Github{}
|
|
|
|
// Github provides OAuth Login and Callback server. Callback will set
|
|
// an authentication cookie. This cookie's value is a JWT containing
|
|
// the user's primary Github email address.
|
|
type Github struct {
|
|
ClientID string
|
|
ClientSecret string
|
|
Orgs []string // Optional github organization checking
|
|
Logger chronograf.Logger
|
|
}
|
|
|
|
// Name is the name of the provider.
|
|
func (g *Github) Name() string {
|
|
return "github"
|
|
}
|
|
|
|
// ID returns the github application client id.
|
|
func (g *Github) ID() string {
|
|
return g.ClientID
|
|
}
|
|
|
|
// Secret returns the github application client secret.
|
|
func (g *Github) Secret() string {
|
|
return g.ClientSecret
|
|
}
|
|
|
|
// Scopes for github is only the email address and possible organizations if
|
|
// we are filtering by organizations.
|
|
func (g *Github) Scopes() []string {
|
|
scopes := []string{"user:email"}
|
|
// In order to access a users orgs, we need the "read:org" scope
|
|
// even if g.Orgs == 0
|
|
scopes = append(scopes, "read:org")
|
|
return scopes
|
|
}
|
|
|
|
// Config is the Github OAuth2 exchange information and endpoints.
|
|
func (g *Github) Config() *oauth2.Config {
|
|
return &oauth2.Config{
|
|
ClientID: g.ID(),
|
|
ClientSecret: g.Secret(),
|
|
Scopes: g.Scopes(),
|
|
Endpoint: ogh.Endpoint,
|
|
}
|
|
}
|
|
|
|
// PrincipalID returns the github email address of the user.
|
|
func (g *Github) PrincipalID(provider *http.Client) (string, error) {
|
|
client := github.NewClient(provider)
|
|
// If we need to restrict to a set of organizations, we first get the org
|
|
// and filter.
|
|
if len(g.Orgs) > 0 {
|
|
orgs, err := getOrganizations(client, g.Logger)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// Not a member, so, deny permission
|
|
if ok := isMember(g.Orgs, orgs); !ok {
|
|
g.Logger.Error("Not a member of required github organization")
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
email, err := getPrimaryEmail(client, g.Logger)
|
|
if err != nil {
|
|
return "", nil
|
|
}
|
|
return email, nil
|
|
}
|
|
|
|
// Group returns a comma delimited string of Github organizations
|
|
// that a user belongs to in Github
|
|
func (g *Github) Group(provider *http.Client) (string, error) {
|
|
client := github.NewClient(provider)
|
|
orgs, err := getOrganizations(client, g.Logger)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
groups := []string{}
|
|
for _, org := range orgs {
|
|
if org.Login != nil {
|
|
groups = append(groups, *org.Login)
|
|
continue
|
|
}
|
|
}
|
|
|
|
return strings.Join(groups, ","), nil
|
|
}
|
|
|
|
func randomString(length int) string {
|
|
k := make([]byte, length)
|
|
if _, err := io.ReadFull(rand.Reader, k); err != nil {
|
|
return ""
|
|
}
|
|
return base64.StdEncoding.EncodeToString(k)
|
|
}
|
|
|
|
func logResponseError(log chronograf.Logger, resp *github.Response, err error) {
|
|
switch resp.StatusCode {
|
|
case http.StatusUnauthorized, http.StatusForbidden:
|
|
log.Error("OAuth access to email address forbidden ", err.Error())
|
|
default:
|
|
log.Error("Unable to retrieve Github email ", err.Error())
|
|
}
|
|
}
|
|
|
|
// isMember makes sure that the user is in one of the required organizations.
|
|
func isMember(requiredOrgs []string, userOrgs []*github.Organization) bool {
|
|
for _, requiredOrg := range requiredOrgs {
|
|
for _, userOrg := range userOrgs {
|
|
if userOrg.Login != nil && *userOrg.Login == requiredOrg {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// getOrganizations gets all organization for the currently authenticated user.
|
|
func getOrganizations(client *github.Client, log chronograf.Logger) ([]*github.Organization, error) {
|
|
// Get all pages of results
|
|
var allOrgs []*github.Organization
|
|
for {
|
|
opt := &github.ListOptions{
|
|
PerPage: 10,
|
|
}
|
|
// Get the organizations for the current authenticated user.
|
|
orgs, resp, err := client.Organizations.List(context.TODO(), "", opt)
|
|
if err != nil {
|
|
logResponseError(log, resp, err)
|
|
return nil, err
|
|
}
|
|
allOrgs = append(allOrgs, orgs...)
|
|
if resp.NextPage == 0 {
|
|
break
|
|
}
|
|
opt.Page = resp.NextPage
|
|
}
|
|
return allOrgs, nil
|
|
}
|
|
|
|
// getPrimaryEmail gets the primary email account for the authenticated user.
|
|
func getPrimaryEmail(client *github.Client, log chronograf.Logger) (string, error) {
|
|
emails, resp, err := client.Users.ListEmails(context.TODO(), nil)
|
|
if err != nil {
|
|
logResponseError(log, resp, err)
|
|
return "", err
|
|
}
|
|
|
|
email, err := primaryEmail(emails)
|
|
if err != nil {
|
|
log.Error("Unable to retrieve primary Github email ", err.Error())
|
|
return "", err
|
|
}
|
|
return email, nil
|
|
}
|
|
|
|
func primaryEmail(emails []*github.UserEmail) (string, error) {
|
|
for _, m := range emails {
|
|
if m != nil && getPrimary(m) && getVerified(m) && m.Email != nil {
|
|
return *m.Email, nil
|
|
}
|
|
}
|
|
return "", errors.New("no primary email address")
|
|
}
|
|
|
|
func getPrimary(m *github.UserEmail) bool {
|
|
if m == nil || m.Primary == nil {
|
|
return false
|
|
}
|
|
return *m.Primary
|
|
}
|
|
|
|
func getVerified(m *github.UserEmail) bool {
|
|
if m == nil || m.Verified == nil {
|
|
return false
|
|
}
|
|
return *m.Verified
|
|
}
|