diff --git a/CHANGELOG.md b/CHANGELOG.md index c527343e7..f6b8e9b88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ### Upcoming Bug Fixes ### 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] ### Bug Fixes diff --git a/README.md b/README.md index f8c12d642..2737b0b3c 100644 --- a/README.md +++ b/README.md @@ -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: -* 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. ## Installation diff --git a/docs/auth.md b/docs/auth.md index 77a9b755a..8644f512b 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -41,6 +41,21 @@ user authorization. If you are running multiple chronograf servers in an HA conf 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 The Chronograf authentication scheme is a standard [web application](https://developer.github.com/v3/oauth/#web-application-flow) OAuth flow. diff --git a/server/dashboards.go b/server/dashboards.go index ef62028d8..f12040f82 100644 --- a/server/dashboards.go +++ b/server/dashboards.go @@ -131,7 +131,7 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) { _, err = s.DashboardsStore.Get(ctx, id) 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 } @@ -148,7 +148,7 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) { } 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) return } diff --git a/server/github.go b/server/github.go index 27007600b..b4cdf92bb 100644 --- a/server/github.go +++ b/server/github.go @@ -44,19 +44,25 @@ type Github struct { ClientID string ClientSecret string Scopes []string - SuccessURL string // SuccessURL is redirect location after successful authorization - FailureURL string // FailureURL is redirect location after authorization failure + SuccessURL string // SuccessURL is redirect location after successful authorization + FailureURL string // FailureURL is redirect location after authorization failure + Orgs []string // Optional github organization checking Now func() time.Time Logger chronograf.Logger } // 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{ ClientID: clientID, ClientSecret: clientSecret, Cookie: NewCookie(), - Scopes: []string{"user:email"}, + Scopes: scopes, + Orgs: orgs, SuccessURL: successURL, FailureURL: failureURL, Authenticator: auth, @@ -151,24 +157,26 @@ func (g *Github) Callback() http.HandlerFunc { oauthClient := conf.Client(r.Context(), token) client := github.NewClient(oauthClient) - - emails, resp, err := client.Users.ListEmails(nil) - if err != nil { - 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()) + // 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, log) + if err != nil { + http.Redirect(w, r, g.FailureURL, http.StatusTemporaryRedirect) + return + } + // 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 { - log.Error("Unable to retrieve primary Github email ", err.Error()) 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 @@ -176,6 +184,7 @@ func (g *Github) Callback() http.HandlerFunc { if err != nil { log.Error("Unable to create cookie auth token ", err.Error()) http.Redirect(w, r, g.FailureURL, http.StatusTemporaryRedirect) + return } expireCookie := time.Now().UTC().Add(g.Cookie.Duration) @@ -200,6 +209,66 @@ func randomString(length int) string { 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 { diff --git a/server/mux.go b/server/mux.go index 4dbf6f9a8..e6dc0e4d0 100644 --- a/server/mux.go +++ b/server/mux.go @@ -20,11 +20,12 @@ const ( // MuxOpts are the options for the router. Mostly related to auth. type MuxOpts struct { Logger chronograf.Logger - Develop bool // Develop loads assets from filesystem instead of bindata - UseAuth bool // UseAuth turns on Github OAuth and JWT - TokenSecret string // TokenSecret is the JWT secret - GithubClientID string // GithubClientID is the GH OAuth id - GithubClientSecret string // GithubClientSecret is the GH OAuth secret + Develop bool // Develop loads assets from filesystem instead of bindata + UseAuth bool // UseAuth turns on Github OAuth and JWT + TokenSecret string // TokenSecret is the JWT secret + GithubClientID string // GithubClientID is the GH OAuth id + 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. @@ -136,6 +137,7 @@ func AuthAPI(opts MuxOpts, router *httprouter.Router) http.Handler { opts.GithubClientSecret, successURL, failureURL, + opts.GithubOrgs, &auth, opts.Logger, ) diff --git a/server/server.go b/server/server.go index ffcc8c494..0db88ad19 100644 --- a/server/server.go +++ b/server/server.go @@ -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"` */ - 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"` - 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"` - 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"` - 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"` - BuildInfo BuildInfo - - Listener net.Listener - handler http.Handler + 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"` + 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"` + 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"` + GithubOrgs []string `short:"o" long:"github-organization" description:"Github organization user is required to have active membership" env:"GH_ORGS" env-delim:","` + 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"` + BuildInfo BuildInfo + Listener net.Listener + handler http.Handler } // BuildInfo is sent to the usage client to track versions and commits @@ -72,6 +71,7 @@ func (s *Server) Serve() error { TokenSecret: s.TokenSecret, GithubClientID: s.GithubClientID, GithubClientSecret: s.GithubClientSecret, + GithubOrgs: s.GithubOrgs, Logger: logger, UseAuth: s.useAuth(), }, service)