Add organization restriction on Heroku provider
This allows operators to permit access to Chronograf only to users belonging to a set of specific Heroku organizations. This is controlled using the HEROKU_ORGS env or the --heroku-organizations switch.pull/922/head
parent
c3ada06c93
commit
3af13aa490
|
@ -95,3 +95,11 @@ The equivalent command line switches are:
|
||||||
|
|
||||||
* `--heroku-client-id`
|
* `--heroku-client-id`
|
||||||
* `--heroku-secret`
|
* `--heroku-secret`
|
||||||
|
|
||||||
|
#### Optional Heroku Organizations
|
||||||
|
|
||||||
|
Like the other OAuth2 providers, access to Chronograf via Heroku can be restricted to members of specific Heroku organizations. This is controlled using the `HEROKU_ORGS` ENV or the `--heroku-organizations` switch and is comma-separated. If we wanted to permit access from the `hill-valley-preservation-society` orgization and `the-pinheads` organization, we would use the following ENV:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export HEROKU_ORGS=hill-valley-preservation-sociey,the-pinheads
|
||||||
|
```
|
||||||
|
|
|
@ -25,6 +25,8 @@ type Heroku struct {
|
||||||
ClientID string
|
ClientID string
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
|
|
||||||
|
Organizations []string // set of organizations permitted to access the protected resource. Empty means "all"
|
||||||
|
|
||||||
Logger chronograf.Logger
|
Logger chronograf.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +52,15 @@ func (h *Heroku) Name() string {
|
||||||
|
|
||||||
// PrincipalID returns the Heroku email address of the user.
|
// PrincipalID returns the Heroku email address of the user.
|
||||||
func (h *Heroku) PrincipalID(provider *http.Client) (string, error) {
|
func (h *Heroku) PrincipalID(provider *http.Client) (string, error) {
|
||||||
|
type DefaultOrg struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
type Account struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
DefaultOrganization DefaultOrg `json:"default_organization"`
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := provider.Get(HEROKU_ACCOUNT_ROUTE)
|
resp, err := provider.Get(HEROKU_ACCOUNT_ROUTE)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.Logger.Error("Unable to communicate with Heroku. err:", err)
|
h.Logger.Error("Unable to communicate with Heroku. err:", err)
|
||||||
|
@ -57,14 +68,24 @@ func (h *Heroku) PrincipalID(provider *http.Client) (string, error) {
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
d := json.NewDecoder(resp.Body)
|
d := json.NewDecoder(resp.Body)
|
||||||
var account struct {
|
var account Account
|
||||||
Email string `json:"email"`
|
|
||||||
}
|
|
||||||
if err := d.Decode(&account); err != nil {
|
if err := d.Decode(&account); err != nil {
|
||||||
h.Logger.Error("Unable to decode response from Heroku. err:", err)
|
h.Logger.Error("Unable to decode response from Heroku. err:", err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return account.Email, nil
|
|
||||||
|
// check if member of org
|
||||||
|
if len(h.Organizations) > 0 {
|
||||||
|
for _, org := range h.Organizations {
|
||||||
|
if account.DefaultOrganization.Name == org {
|
||||||
|
return account.Email, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.Logger.Error(ErrOrgMembership)
|
||||||
|
return "", ErrOrgMembership
|
||||||
|
} else {
|
||||||
|
return account.Email, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scopes for heroku is "identity" which grants access to user account
|
// Scopes for heroku is "identity" which grants access to user account
|
||||||
|
|
|
@ -53,3 +53,50 @@ func Test_Heroku_PrincipalID_ExtractsEmailAddress(t *testing.T) {
|
||||||
t.Fatal("Retrieved email was not as expected. Want:", expected.Email, "Got:", email)
|
t.Fatal("Retrieved email was not as expected. Want:", expected.Email, "Got:", email)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_Heroku_PrincipalID_RestrictsByOrganization(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
expected := struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
DefaultOrganization map[string]string `json:"default_organization"`
|
||||||
|
}{
|
||||||
|
"martymcfly@example.com",
|
||||||
|
map[string]string{
|
||||||
|
"id": "a85eac89-56cc-498e-9a89-d8f49f6aed71",
|
||||||
|
"name": "hill-valley-preservation-society",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockAPI := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/account" {
|
||||||
|
rw.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(rw)
|
||||||
|
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
_ = enc.Encode(expected)
|
||||||
|
}))
|
||||||
|
defer mockAPI.Close()
|
||||||
|
|
||||||
|
logger := clog.New(clog.ParseLevel("debug"))
|
||||||
|
prov := oauth2.Heroku{
|
||||||
|
Logger: logger,
|
||||||
|
Organizations: []string{"enchantment-under-the-sea-dance-committee"},
|
||||||
|
}
|
||||||
|
|
||||||
|
tt, err := NewTestTripper(logger, mockAPI, http.DefaultTransport)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Error initializing TestTripper: err:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tc := &http.Client{
|
||||||
|
Transport: tt,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = prov.PrincipalID(tc)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected error while authenticating user with mismatched orgs, but received none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ const (
|
||||||
var (
|
var (
|
||||||
/* Errors */
|
/* Errors */
|
||||||
ErrAuthentication = errors.New("user not authenticated")
|
ErrAuthentication = errors.New("user not authenticated")
|
||||||
|
ErrOrgMembership = errors.New("Not a member of the required organization")
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Types */
|
/* Types */
|
||||||
|
|
|
@ -20,20 +20,21 @@ 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
|
||||||
Basepath string // URL path prefix under which all chronograf routes will be mounted
|
Basepath string // URL path prefix under which all chronograf routes will be mounted
|
||||||
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 may be a member of
|
GithubOrgs []string // GithubOrgs is the list of organizations a user may be a member of
|
||||||
GoogleClientID string // GoogleClientID is the Google OAuth id
|
GoogleClientID string // GoogleClientID is the Google OAuth id
|
||||||
GoogleClientSecret string // GoogleClientSecret is the Google OAuth secret
|
GoogleClientSecret string // GoogleClientSecret is the Google OAuth secret
|
||||||
GoogleDomains []string // GoogleDomains is the list of domains a user may be a member of
|
GoogleDomains []string // GoogleDomains is the list of domains a user may be a member of
|
||||||
HerokuClientID string // HerokuClientID is the Heroku OAuth id
|
HerokuClientID string // HerokuClientID is the Heroku OAuth id
|
||||||
HerokuSecret string // HerokuSecret is the Heroku OAuth secret
|
HerokuSecret string // HerokuSecret is the Heroku OAuth secret
|
||||||
PublicURL string // PublicURL is the public facing URL for the server
|
HerokuOrganizations []string // HerokuOrganizations is the set of organizations permitted to access Chronograf
|
||||||
|
PublicURL string // PublicURL is the public facing URL for the server
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MuxOpts) UseGithub() bool {
|
func (m *MuxOpts) UseGithub() bool {
|
||||||
|
@ -206,9 +207,10 @@ func AuthAPI(opts MuxOpts, router *httprouter.Router) http.Handler {
|
||||||
|
|
||||||
if opts.UseHeroku() {
|
if opts.UseHeroku() {
|
||||||
heroku := oauth2.Heroku{
|
heroku := oauth2.Heroku{
|
||||||
ClientID: opts.HerokuClientID,
|
ClientID: opts.HerokuClientID,
|
||||||
ClientSecret: opts.HerokuSecret,
|
ClientSecret: opts.HerokuSecret,
|
||||||
Logger: opts.Logger,
|
Organizations: opts.HerokuOrganizations,
|
||||||
|
Logger: opts.Logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
hMux := oauth2.NewCookieMux(&heroku, &auth, opts.Logger)
|
hMux := oauth2.NewCookieMux(&heroku, &auth, opts.Logger)
|
||||||
|
|
|
@ -54,8 +54,9 @@ type Server struct {
|
||||||
GoogleDomains []string `long:"google-domains" description:"Google email domain user is required to have active membership" env:"GOOGLE_DOMAINS" env-delim:","`
|
GoogleDomains []string `long:"google-domains" description:"Google email domain user is required to have active membership" env:"GOOGLE_DOMAINS" env-delim:","`
|
||||||
PublicURL string `long:"public-url" description:"Full public URL used to access Chronograf from a web browser. Used for Google OAuth2 authentication. (http://localhost:8888)" env:"PUBLIC_URL"`
|
PublicURL string `long:"public-url" description:"Full public URL used to access Chronograf from a web browser. Used for Google OAuth2 authentication. (http://localhost:8888)" env:"PUBLIC_URL"`
|
||||||
|
|
||||||
HerokuClientID string `long:"heroku-client-id" description:"Heroku Client ID for OAuth 2 support" env:"HEROKU_CLIENT_ID"`
|
HerokuClientID string `long:"heroku-client-id" description:"Heroku Client ID for OAuth 2 support" env:"HEROKU_CLIENT_ID"`
|
||||||
HerokuSecret string `long:"heroku-secret" description:"Heroku Secret for OAuth 2 support" env:"HEROKU_SECRET"`
|
HerokuSecret string `long:"heroku-secret" description:"Heroku Secret for OAuth 2 support" env:"HEROKU_SECRET"`
|
||||||
|
HerokuOrganizations []string `long:"heroku-organization" description:"Heroku Organization Memberships a user is required to have for access to Chronograf (comma separated)" env:"HEROKU_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"`
|
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"`
|
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"`
|
||||||
|
@ -85,19 +86,20 @@ func (s *Server) Serve() error {
|
||||||
service := openService(s.BoltPath, s.CannedPath, logger, s.useAuth())
|
service := openService(s.BoltPath, s.CannedPath, logger, s.useAuth())
|
||||||
basepath = s.Basepath
|
basepath = s.Basepath
|
||||||
s.handler = NewMux(MuxOpts{
|
s.handler = NewMux(MuxOpts{
|
||||||
Develop: s.Develop,
|
Develop: s.Develop,
|
||||||
TokenSecret: s.TokenSecret,
|
TokenSecret: s.TokenSecret,
|
||||||
GithubClientID: s.GithubClientID,
|
GithubClientID: s.GithubClientID,
|
||||||
GithubClientSecret: s.GithubClientSecret,
|
GithubClientSecret: s.GithubClientSecret,
|
||||||
GithubOrgs: s.GithubOrgs,
|
GithubOrgs: s.GithubOrgs,
|
||||||
GoogleClientID: s.GoogleClientID,
|
GoogleClientID: s.GoogleClientID,
|
||||||
GoogleClientSecret: s.GoogleClientSecret,
|
GoogleClientSecret: s.GoogleClientSecret,
|
||||||
GoogleDomains: s.GoogleDomains,
|
GoogleDomains: s.GoogleDomains,
|
||||||
HerokuClientID: s.HerokuClientID,
|
HerokuClientID: s.HerokuClientID,
|
||||||
HerokuSecret: s.HerokuSecret,
|
HerokuSecret: s.HerokuSecret,
|
||||||
PublicURL: s.PublicURL,
|
HerokuOrganizations: s.HerokuOrganizations,
|
||||||
Logger: logger,
|
PublicURL: s.PublicURL,
|
||||||
UseAuth: s.useAuth(),
|
Logger: logger,
|
||||||
|
UseAuth: s.useAuth(),
|
||||||
}, service)
|
}, service)
|
||||||
|
|
||||||
s.handler = Version(s.BuildInfo.Version, s.handler)
|
s.handler = Version(s.BuildInfo.Version, s.handler)
|
||||||
|
|
Loading…
Reference in New Issue