Add Github organization restriction to authentication
parent
3da8375e77
commit
bc3a0e1b3d
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
15
docs/auth.md
15
docs/auth.md
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
103
server/github.go
103
server/github.go
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue