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-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
|
||||
ClientSecret string
|
||||
|
||||
Organizations []string // set of organizations permitted to access the protected resource. Empty means "all"
|
||||
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
|
||||
|
@ -50,6 +52,15 @@ func (h *Heroku) Name() string {
|
|||
|
||||
// PrincipalID returns the Heroku email address of the user.
|
||||
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)
|
||||
if err != nil {
|
||||
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()
|
||||
d := json.NewDecoder(resp.Body)
|
||||
var account struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
var account Account
|
||||
if err := d.Decode(&account); err != nil {
|
||||
h.Logger.Error("Unable to decode response from Heroku. err:", 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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
/* Errors */
|
||||
ErrAuthentication = errors.New("user not authenticated")
|
||||
ErrOrgMembership = errors.New("Not a member of the required organization")
|
||||
)
|
||||
|
||||
/* Types */
|
||||
|
|
|
@ -20,20 +20,21 @@ 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
|
||||
Basepath string // URL path prefix under which all chronograf routes will be mounted
|
||||
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 may be a member of
|
||||
GoogleClientID string // GoogleClientID is the Google OAuth id
|
||||
GoogleClientSecret string // GoogleClientSecret is the Google OAuth secret
|
||||
GoogleDomains []string // GoogleDomains is the list of domains a user may be a member of
|
||||
HerokuClientID string // HerokuClientID is the Heroku OAuth id
|
||||
HerokuSecret string // HerokuSecret is the Heroku OAuth secret
|
||||
PublicURL string // PublicURL is the public facing URL for the server
|
||||
Logger chronograf.Logger
|
||||
Develop bool // Develop loads assets from filesystem instead of bindata
|
||||
Basepath string // URL path prefix under which all chronograf routes will be mounted
|
||||
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 may be a member of
|
||||
GoogleClientID string // GoogleClientID is the Google OAuth id
|
||||
GoogleClientSecret string // GoogleClientSecret is the Google OAuth secret
|
||||
GoogleDomains []string // GoogleDomains is the list of domains a user may be a member of
|
||||
HerokuClientID string // HerokuClientID is the Heroku OAuth id
|
||||
HerokuSecret string // HerokuSecret is the Heroku OAuth secret
|
||||
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 {
|
||||
|
@ -206,9 +207,10 @@ func AuthAPI(opts MuxOpts, router *httprouter.Router) http.Handler {
|
|||
|
||||
if opts.UseHeroku() {
|
||||
heroku := oauth2.Heroku{
|
||||
ClientID: opts.HerokuClientID,
|
||||
ClientSecret: opts.HerokuSecret,
|
||||
Logger: opts.Logger,
|
||||
ClientID: opts.HerokuClientID,
|
||||
ClientSecret: opts.HerokuSecret,
|
||||
Organizations: opts.HerokuOrganizations,
|
||||
Logger: 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:","`
|
||||
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"`
|
||||
HerokuSecret string `long:"heroku-secret" description:"Heroku Secret for OAuth 2 support" env:"HEROKU_SECRET"`
|
||||
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"`
|
||||
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"`
|
||||
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())
|
||||
basepath = s.Basepath
|
||||
s.handler = NewMux(MuxOpts{
|
||||
Develop: s.Develop,
|
||||
TokenSecret: s.TokenSecret,
|
||||
GithubClientID: s.GithubClientID,
|
||||
GithubClientSecret: s.GithubClientSecret,
|
||||
GithubOrgs: s.GithubOrgs,
|
||||
GoogleClientID: s.GoogleClientID,
|
||||
GoogleClientSecret: s.GoogleClientSecret,
|
||||
GoogleDomains: s.GoogleDomains,
|
||||
HerokuClientID: s.HerokuClientID,
|
||||
HerokuSecret: s.HerokuSecret,
|
||||
PublicURL: s.PublicURL,
|
||||
Logger: logger,
|
||||
UseAuth: s.useAuth(),
|
||||
Develop: s.Develop,
|
||||
TokenSecret: s.TokenSecret,
|
||||
GithubClientID: s.GithubClientID,
|
||||
GithubClientSecret: s.GithubClientSecret,
|
||||
GithubOrgs: s.GithubOrgs,
|
||||
GoogleClientID: s.GoogleClientID,
|
||||
GoogleClientSecret: s.GoogleClientSecret,
|
||||
GoogleDomains: s.GoogleDomains,
|
||||
HerokuClientID: s.HerokuClientID,
|
||||
HerokuSecret: s.HerokuSecret,
|
||||
HerokuOrganizations: s.HerokuOrganizations,
|
||||
PublicURL: s.PublicURL,
|
||||
Logger: logger,
|
||||
UseAuth: s.useAuth(),
|
||||
}, service)
|
||||
|
||||
s.handler = Version(s.BuildInfo.Version, s.handler)
|
||||
|
|
Loading…
Reference in New Issue