Add Github organization restriction to authentication

pull/733/head
Chris Goller 2017-01-05 11:29:26 -06:00
parent 3da8375e77
commit bc3a0e1b3d
7 changed files with 127 additions and 41 deletions

View File

@ -2,6 +2,8 @@
### Upcoming Bug Fixes ### Upcoming Bug Fixes
### Upcoming Features ### Upcoming Features
1. [#660](https://github.com/influxdata/chronograf/issues/660): Add option to accept any certificate from InfluxDB.
1. [#733](https://github.com/influxdata/chronograf/pull/733): Add optional Github organization membership checks to authentication
## v1.1.0-beta5 [2017-01-05] ## v1.1.0-beta5 [2017-01-05]

View File

@ -83,8 +83,6 @@ Please open [an issue](https://github.com/influxdata/chronograf/issues/new)!
The Chronograf team has identified and is working on the following issues: The Chronograf team has identified and is working on the following issues:
* Chronograf's [OAuth 2.0 Style Authentication](https://github.com/influxdata/chronograf/blob/master/docs/auth.md) allows all users to authenticate with any GitHub account.
It does not yet offer any additional security or allow administrators to whitelist users or organizations.
* Currently, Chronograf requires users to run Telegraf's [CPU](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/CPU_README.md) and [system](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/SYSTEM_README.md) plugins to ensure that all Apps appear on the [HOST LIST](https://github.com/influxdata/chronograf/blob/master/docs/GETTING_STARTED.md#host-list) page. * Currently, Chronograf requires users to run Telegraf's [CPU](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/CPU_README.md) and [system](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/system/SYSTEM_README.md) plugins to ensure that all Apps appear on the [HOST LIST](https://github.com/influxdata/chronograf/blob/master/docs/GETTING_STARTED.md#host-list) page.
## Installation ## Installation

View File

@ -41,6 +41,21 @@ user authorization. If you are running multiple chronograf servers in an HA conf
export TOKEN_SECRET=supersupersecret export TOKEN_SECRET=supersupersecret
``` ```
#### Optional Github Organizations
To require an organization membership for a user, set the `GH_ORGS` environment variables
```sh
export GH_ORGS=biffs-gang
```
If the user is not a member, then the user will not be allowed access.
To support multiple organizations use a comma delimted list like so:
```sh
export GH_ORGS=hill-valley-preservation-sociey,the-pinheads
```
### Design ### Design
The Chronograf authentication scheme is a standard [web application](https://developer.github.com/v3/oauth/#web-application-flow) OAuth flow. The Chronograf authentication scheme is a standard [web application](https://developer.github.com/v3/oauth/#web-application-flow) OAuth flow.

View File

@ -131,7 +131,7 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
_, err = s.DashboardsStore.Get(ctx, id) _, err = s.DashboardsStore.Get(ctx, id)
if err != nil { if err != nil {
Error(w, http.StatusNotFound, fmt.Sprintf("ID %s not found", id), s.Logger) Error(w, http.StatusNotFound, fmt.Sprintf("ID %d not found", id), s.Logger)
return return
} }
@ -148,7 +148,7 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
} }
if err := s.DashboardsStore.Update(ctx, req); err != nil { if err := s.DashboardsStore.Update(ctx, req); err != nil {
msg := fmt.Sprintf("Error updating dashboard ID %s: %v", id, err) msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger) Error(w, http.StatusInternalServerError, msg, s.Logger)
return return
} }

View File

@ -44,19 +44,25 @@ type Github struct {
ClientID string ClientID string
ClientSecret string ClientSecret string
Scopes []string Scopes []string
SuccessURL string // SuccessURL is redirect location after successful authorization SuccessURL string // SuccessURL is redirect location after successful authorization
FailureURL string // FailureURL is redirect location after authorization failure FailureURL string // FailureURL is redirect location after authorization failure
Orgs []string // Optional github organization checking
Now func() time.Time Now func() time.Time
Logger chronograf.Logger Logger chronograf.Logger
} }
// NewGithub constructs a Github with default cookie behavior and scopes. // NewGithub constructs a Github with default cookie behavior and scopes.
func NewGithub(clientID, clientSecret, successURL, failureURL string, auth chronograf.Authenticator, log chronograf.Logger) Github { func NewGithub(clientID, clientSecret, successURL, failureURL string, orgs []string, auth chronograf.Authenticator, log chronograf.Logger) Github {
scopes := []string{"user:email"}
if len(orgs) > 0 {
scopes = append(scopes, "read:org")
}
return Github{ return Github{
ClientID: clientID, ClientID: clientID,
ClientSecret: clientSecret, ClientSecret: clientSecret,
Cookie: NewCookie(), Cookie: NewCookie(),
Scopes: []string{"user:email"}, Scopes: scopes,
Orgs: orgs,
SuccessURL: successURL, SuccessURL: successURL,
FailureURL: failureURL, FailureURL: failureURL,
Authenticator: auth, Authenticator: auth,
@ -151,24 +157,26 @@ func (g *Github) Callback() http.HandlerFunc {
oauthClient := conf.Client(r.Context(), token) oauthClient := conf.Client(r.Context(), token)
client := github.NewClient(oauthClient) client := github.NewClient(oauthClient)
// If we need to restrict to a set of organizations, we first get the org
emails, resp, err := client.Users.ListEmails(nil) // and filter.
if err != nil { if len(g.Orgs) > 0 {
switch resp.StatusCode { orgs, err := getOrganizations(client, log)
case http.StatusUnauthorized, http.StatusForbidden: if err != nil {
log.Error("OAuth access to email address forbidden ", err.Error()) http.Redirect(w, r, g.FailureURL, http.StatusTemporaryRedirect)
default: return
log.Error("Unable to retrieve Github email ", err.Error()) }
// Not a member, so, deny permission
if ok := isMember(g.Orgs, orgs); !ok {
log.Error("Not a member of required github organization")
http.Redirect(w, r, g.FailureURL, http.StatusTemporaryRedirect)
return
} }
http.Redirect(w, r, g.FailureURL, http.StatusTemporaryRedirect)
return
} }
email, err := primaryEmail(emails) email, err := getPrimaryEmail(client, log)
if err != nil { if err != nil {
log.Error("Unable to retrieve primary Github email ", err.Error())
http.Redirect(w, r, g.FailureURL, http.StatusTemporaryRedirect) http.Redirect(w, r, g.FailureURL, http.StatusTemporaryRedirect)
return
} }
// We create an auth token that will be used by all other endpoints to validate the principal has a claim // We create an auth token that will be used by all other endpoints to validate the principal has a claim
@ -176,6 +184,7 @@ func (g *Github) Callback() http.HandlerFunc {
if err != nil { if err != nil {
log.Error("Unable to create cookie auth token ", err.Error()) log.Error("Unable to create cookie auth token ", err.Error())
http.Redirect(w, r, g.FailureURL, http.StatusTemporaryRedirect) http.Redirect(w, r, g.FailureURL, http.StatusTemporaryRedirect)
return
} }
expireCookie := time.Now().UTC().Add(g.Cookie.Duration) expireCookie := time.Now().UTC().Add(g.Cookie.Duration)
@ -200,6 +209,66 @@ func randomString(length int) string {
return base64.StdEncoding.EncodeToString(k) 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) { func primaryEmail(emails []*github.UserEmail) (string, error) {
for _, m := range emails { for _, m := range emails {
if m != nil && m.Primary != nil && m.Verified != nil && m.Email != nil { if m != nil && m.Primary != nil && m.Verified != nil && m.Email != nil {

View File

@ -20,11 +20,12 @@ const (
// MuxOpts are the options for the router. Mostly related to auth. // MuxOpts are the options for the router. Mostly related to auth.
type MuxOpts struct { type MuxOpts struct {
Logger chronograf.Logger Logger chronograf.Logger
Develop bool // Develop loads assets from filesystem instead of bindata Develop bool // Develop loads assets from filesystem instead of bindata
UseAuth bool // UseAuth turns on Github OAuth and JWT UseAuth bool // UseAuth turns on Github OAuth and JWT
TokenSecret string // TokenSecret is the JWT secret TokenSecret string // TokenSecret is the JWT secret
GithubClientID string // GithubClientID is the GH OAuth id GithubClientID string // GithubClientID is the GH OAuth id
GithubClientSecret string // GithubClientSecret is the GH OAuth secret GithubClientSecret string // GithubClientSecret is the GH OAuth secret
GithubOrgs []string // GithubOrgs is the list of organizations a user my be a member of
} }
// NewMux attaches all the route handlers; handler returned servers chronograf. // NewMux attaches all the route handlers; handler returned servers chronograf.
@ -136,6 +137,7 @@ func AuthAPI(opts MuxOpts, router *httprouter.Router) http.Handler {
opts.GithubClientSecret, opts.GithubClientSecret,
successURL, successURL,
failureURL, failureURL,
opts.GithubOrgs,
&auth, &auth,
opts.Logger, opts.Logger,
) )

View File

@ -37,20 +37,19 @@ type Server struct {
TLSCertificateKey flags.Filename `long:"tls-key" description:"the private key to use for secure conections" env:"TLS_PRIVATE_KEY"` TLSCertificateKey flags.Filename `long:"tls-key" description:"the private key to use for secure conections" env:"TLS_PRIVATE_KEY"`
*/ */
Develop bool `short:"d" long:"develop" description:"Run server in develop mode."` Develop bool `short:"d" long:"develop" description:"Run server in develop mode."`
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (/var/lib/chronograf/chronograf-v1.db)" env:"BOLT_PATH" default:"chronograf-v1.db"` BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (/var/lib/chronograf/chronograf-v1.db)" env:"BOLT_PATH" default:"chronograf-v1.db"`
CannedPath string `short:"c" long:"canned-path" description:"Path to directory of pre-canned application layouts (/usr/share/chronograf/canned)" env:"CANNED_PATH" default:"canned"` CannedPath string `short:"c" long:"canned-path" description:"Path to directory of pre-canned application layouts (/usr/share/chronograf/canned)" env:"CANNED_PATH" default:"canned"`
TokenSecret string `short:"t" long:"token-secret" description:"Secret to sign tokens" env:"TOKEN_SECRET"` TokenSecret string `short:"t" long:"token-secret" description:"Secret to sign tokens" env:"TOKEN_SECRET"`
GithubClientID string `short:"i" long:"github-client-id" description:"Github Client ID for OAuth 2 support" env:"GH_CLIENT_ID"` GithubClientID string `short:"i" long:"github-client-id" description:"Github Client ID for OAuth 2 support" env:"GH_CLIENT_ID"`
GithubClientSecret string `short:"s" long:"github-client-secret" description:"Github Client Secret for OAuth 2 support" env:"GH_CLIENT_SECRET"` GithubClientSecret string `short:"s" long:"github-client-secret" description:"Github Client Secret for OAuth 2 support" env:"GH_CLIENT_SECRET"`
ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"` GithubOrgs []string `short:"o" long:"github-organization" description:"Github organization user is required to have active membership" env:"GH_ORGS" env-delim:","`
LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal" choice:"panic" default:"info" description:"Set the logging level" env:"LOG_LEVEL"` ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"`
LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal" choice:"panic" default:"info" description:"Set the logging level" env:"LOG_LEVEL"`
ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"` ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"`
BuildInfo BuildInfo BuildInfo BuildInfo
Listener net.Listener
Listener net.Listener handler http.Handler
handler http.Handler
} }
// BuildInfo is sent to the usage client to track versions and commits // BuildInfo is sent to the usage client to track versions and commits
@ -72,6 +71,7 @@ func (s *Server) Serve() error {
TokenSecret: s.TokenSecret, TokenSecret: s.TokenSecret,
GithubClientID: s.GithubClientID, GithubClientID: s.GithubClientID,
GithubClientSecret: s.GithubClientSecret, GithubClientSecret: s.GithubClientSecret,
GithubOrgs: s.GithubOrgs,
Logger: logger, Logger: logger,
UseAuth: s.useAuth(), UseAuth: s.useAuth(),
}, service) }, service)