174 lines
4.5 KiB
Go
174 lines
4.5 KiB
Go
|
package oauth2
|
||
|
|
||
|
import (
|
||
|
"crypto/rand"
|
||
|
"encoding/base64"
|
||
|
"errors"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
|
||
|
"github.com/google/go-github/github"
|
||
|
"github.com/influxdata/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 {
|
||
|
Auth Authenticator
|
||
|
ClientID string
|
||
|
ClientSecret string
|
||
|
Orgs []string // Optional github organization checking
|
||
|
Logger chronograf.Logger
|
||
|
}
|
||
|
|
||
|
// 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 addres and possible organizations if
|
||
|
// we are filtering by organizations.
|
||
|
func (g *Github) Scopes() []string {
|
||
|
scopes := []string{"user:email"}
|
||
|
if len(g.Orgs) > 0 {
|
||
|
scopes = append(scopes, "read:org")
|
||
|
}
|
||
|
return scopes
|
||
|
}
|
||
|
|
||
|
// NewGithub constructs a Github with default scopes.
|
||
|
func NewGithub(clientID, clientSecret string, orgs []string, auth Authenticator, log chronograf.Logger) Github {
|
||
|
scopes := []string{"user:email"}
|
||
|
if len(orgs) > 0 {
|
||
|
scopes = append(scopes, "read:org")
|
||
|
}
|
||
|
return Github{
|
||
|
ClientID: clientID,
|
||
|
ClientSecret: clientSecret,
|
||
|
Orgs: orgs,
|
||
|
Auth: auth,
|
||
|
Logger: log,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
|
||
|
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("", 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(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 && m.Primary != nil && m.Verified != nil && m.Email != nil {
|
||
|
return *m.Email, nil
|
||
|
}
|
||
|
}
|
||
|
return "", errors.New("No primary email address")
|
||
|
}
|