diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5b95834820..5a5466af1c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,16 +1,10 @@
-## v1.4.1.0 [unreleased]
-
-### Features
-### UI Improvements
-1. [#2502](https://github.com/influxdata/chronograf/pull/2502): Fix cursor flashing between default and pointer
-
-### Bug Fixes
-
## v1.4.0.0-beta2 [unreleased]
### Features
### UI Improvements
+1. [#2502](https://github.com/influxdata/chronograf/pull/2502): Fix cursor flashing between default and pointer
### Bug Fixes
1. [#2528](https://github.com/influxdata/chronograf/pull/2528): Fix template rendering to ignore template if not in query
+1. [#2563](https://github.com/influxdata/chronograf/pull/2563): Fix graph inversion if user input y-axis min greater than max
## v1.4.0.0-beta1 [2017-12-07]
### Features
diff --git a/bolt/client.go b/bolt/client.go
index 8ae39cfd09..be1b57c8ce 100644
--- a/bolt/client.go
+++ b/bolt/client.go
@@ -23,6 +23,7 @@ type Client struct {
DashboardsStore *DashboardsStore
UsersStore *UsersStore
OrganizationsStore *OrganizationsStore
+ ConfigStore *ConfigStore
}
// NewClient initializes all stores
@@ -40,6 +41,7 @@ func NewClient() *Client {
}
c.UsersStore = &UsersStore{client: c}
c.OrganizationsStore = &OrganizationsStore{client: c}
+ c.ConfigStore = &ConfigStore{client: c}
return c
}
@@ -77,6 +79,10 @@ func (c *Client) Open(ctx context.Context) error {
if _, err := tx.CreateBucketIfNotExists(UsersBucket); err != nil {
return err
}
+ // Always create Config bucket.
+ if _, err := tx.CreateBucketIfNotExists(ConfigBucket); err != nil {
+ return err
+ }
return nil
}); err != nil {
return err
@@ -98,6 +104,9 @@ func (c *Client) Open(ctx context.Context) error {
if err := c.DashboardsStore.Migrate(ctx); err != nil {
return err
}
+ if err := c.ConfigStore.Migrate(ctx); err != nil {
+ return err
+ }
return nil
}
diff --git a/bolt/config.go b/bolt/config.go
new file mode 100644
index 0000000000..98d6eaca23
--- /dev/null
+++ b/bolt/config.go
@@ -0,0 +1,71 @@
+package bolt
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/boltdb/bolt"
+ "github.com/influxdata/chronograf"
+ "github.com/influxdata/chronograf/bolt/internal"
+)
+
+// Ensure ConfigStore implements chronograf.ConfigStore.
+var _ chronograf.ConfigStore = &ConfigStore{}
+
+// ConfigBucket is used to store chronograf application state
+var ConfigBucket = []byte("ConfigV1")
+
+// configID is the boltDB key where the configuration object is stored
+var configID = []byte("config/v1")
+
+// ConfigStore uses bolt to store and retrieve global
+// application configuration
+type ConfigStore struct {
+ client *Client
+}
+
+func (s *ConfigStore) Migrate(ctx context.Context) error {
+ if _, err := s.Get(ctx); err != nil {
+ return s.Initialize(ctx)
+ }
+ return nil
+}
+
+func (s *ConfigStore) Initialize(ctx context.Context) error {
+ cfg := chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: true,
+ },
+ }
+ return s.Update(ctx, &cfg)
+}
+
+func (s *ConfigStore) Get(ctx context.Context) (*chronograf.Config, error) {
+ var cfg chronograf.Config
+ err := s.client.db.View(func(tx *bolt.Tx) error {
+ v := tx.Bucket(ConfigBucket).Get(configID)
+ if v == nil {
+ return chronograf.ErrConfigNotFound
+ }
+ return internal.UnmarshalConfig(v, &cfg)
+ })
+
+ if err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
+
+func (s *ConfigStore) Update(ctx context.Context, cfg *chronograf.Config) error {
+ if cfg == nil {
+ return fmt.Errorf("config provided was nil")
+ }
+ return s.client.db.Update(func(tx *bolt.Tx) error {
+ if v, err := internal.MarshalConfig(cfg); err != nil {
+ return err
+ } else if err := tx.Bucket(ConfigBucket).Put(configID, v); err != nil {
+ return err
+ }
+ return nil
+ })
+}
diff --git a/bolt/config_test.go b/bolt/config_test.go
new file mode 100644
index 0000000000..8577185097
--- /dev/null
+++ b/bolt/config_test.go
@@ -0,0 +1,111 @@
+package bolt_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/influxdata/chronograf"
+)
+
+func TestConfig_Get(t *testing.T) {
+ type wants struct {
+ config *chronograf.Config
+ err error
+ }
+ tests := []struct {
+ name string
+ wants wants
+ }{
+ {
+ name: "Get config",
+ wants: wants{
+ config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: true,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ client, err := NewTestClient()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := client.Open(context.TODO()); err != nil {
+ t.Fatal(err)
+ }
+ defer client.Close()
+
+ s := client.ConfigStore
+ got, err := s.Get(context.Background())
+ if (tt.wants.err != nil) != (err != nil) {
+ t.Errorf("%q. ConfigStore.Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
+ continue
+ }
+ if diff := cmp.Diff(got, tt.wants.config); diff != "" {
+ t.Errorf("%q. ConfigStore.Get():\n-got/+want\ndiff %s", tt.name, diff)
+ }
+ }
+}
+
+func TestConfig_Update(t *testing.T) {
+ type args struct {
+ config *chronograf.Config
+ }
+ type wants struct {
+ config *chronograf.Config
+ err error
+ }
+ tests := []struct {
+ name string
+ args args
+ wants wants
+ }{
+ {
+ name: "Set config",
+ args: args{
+ config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
+ wants: wants{
+ config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ client, err := NewTestClient()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := client.Open(context.TODO()); err != nil {
+ t.Fatal(err)
+ }
+ defer client.Close()
+
+ s := client.ConfigStore
+ err = s.Update(context.Background(), tt.args.config)
+ if (tt.wants.err != nil) != (err != nil) {
+ t.Errorf("%q. ConfigStore.Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
+ continue
+ }
+
+ got, _ := s.Get(context.Background())
+ if (tt.wants.err != nil) != (err != nil) {
+ t.Errorf("%q. ConfigStore.Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
+ continue
+ }
+
+ if diff := cmp.Diff(got, tt.wants.config); diff != "" {
+ t.Errorf("%q. ConfigStore.Get():\n-got/+want\ndiff %s", tt.name, diff)
+ }
+ }
+}
diff --git a/bolt/internal/internal.go b/bolt/internal/internal.go
index c657ef6783..9c2d8b3d01 100644
--- a/bolt/internal/internal.go
+++ b/bolt/internal/internal.go
@@ -2,6 +2,7 @@ package internal
import (
"encoding/json"
+ "fmt"
"github.com/gogo/protobuf/proto"
"github.com/influxdata/chronograf"
@@ -591,3 +592,39 @@ func UnmarshalOrganizationPB(data []byte, o *Organization) error {
}
return nil
}
+
+// MarshalConfig encodes a config to binary protobuf format.
+func MarshalConfig(c *chronograf.Config) ([]byte, error) {
+ return MarshalConfigPB(&Config{
+ Auth: &AuthConfig{
+ SuperAdminNewUsers: c.Auth.SuperAdminNewUsers,
+ },
+ })
+}
+
+// MarshalConfigPB encodes a config to binary protobuf format.
+func MarshalConfigPB(c *Config) ([]byte, error) {
+ return proto.Marshal(c)
+}
+
+// UnmarshalConfig decodes a config from binary protobuf data.
+func UnmarshalConfig(data []byte, c *chronograf.Config) error {
+ var pb Config
+ if err := UnmarshalConfigPB(data, &pb); err != nil {
+ return err
+ }
+ if pb.Auth == nil {
+ return fmt.Errorf("Auth config is nil")
+ }
+ c.Auth.SuperAdminNewUsers = pb.Auth.SuperAdminNewUsers
+
+ return nil
+}
+
+// UnmarshalConfigPB decodes a config from binary protobuf data.
+func UnmarshalConfigPB(data []byte, c *Config) error {
+ if err := proto.Unmarshal(data, c); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/bolt/internal/internal.pb.go b/bolt/internal/internal.pb.go
index 91eb31bf7f..bce7ea5f60 100644
--- a/bolt/internal/internal.pb.go
+++ b/bolt/internal/internal.pb.go
@@ -27,6 +27,8 @@ It has these top-level messages:
User
Role
Organization
+ Config
+ AuthConfig
*/
package internal
@@ -389,6 +391,31 @@ func (m *Organization) String() string { return proto.CompactTextStri
func (*Organization) ProtoMessage() {}
func (*Organization) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{17} }
+type Config struct {
+ Auth *AuthConfig `protobuf:"bytes,1,opt,name=Auth" json:"Auth,omitempty"`
+}
+
+func (m *Config) Reset() { *m = Config{} }
+func (m *Config) String() string { return proto.CompactTextString(m) }
+func (*Config) ProtoMessage() {}
+func (*Config) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{18} }
+
+func (m *Config) GetAuth() *AuthConfig {
+ if m != nil {
+ return m.Auth
+ }
+ return nil
+}
+
+type AuthConfig struct {
+ SuperAdminNewUsers bool `protobuf:"varint,1,opt,name=SuperAdminNewUsers,proto3" json:"SuperAdminNewUsers,omitempty"`
+}
+
+func (m *AuthConfig) Reset() { *m = AuthConfig{} }
+func (m *AuthConfig) String() string { return proto.CompactTextString(m) }
+func (*AuthConfig) ProtoMessage() {}
+func (*AuthConfig) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{19} }
+
func init() {
proto.RegisterType((*Source)(nil), "internal.Source")
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
@@ -408,89 +435,94 @@ func init() {
proto.RegisterType((*User)(nil), "internal.User")
proto.RegisterType((*Role)(nil), "internal.Role")
proto.RegisterType((*Organization)(nil), "internal.Organization")
+ proto.RegisterType((*Config)(nil), "internal.Config")
+ proto.RegisterType((*AuthConfig)(nil), "internal.AuthConfig")
}
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
var fileDescriptorInternal = []byte{
- // 1264 bytes of a gzipped FileDescriptorProto
- 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x57, 0xdf, 0x8e, 0xdb, 0xc4,
- 0x17, 0x96, 0xe3, 0x38, 0xb1, 0x4f, 0xb6, 0xfd, 0x55, 0xf3, 0xab, 0xa8, 0x29, 0x12, 0x0a, 0x16,
- 0x88, 0x45, 0xd0, 0x05, 0xb5, 0x42, 0x42, 0x5c, 0x20, 0x65, 0x37, 0xa8, 0x2c, 0xfd, 0xb7, 0x9d,
- 0x74, 0xcb, 0x15, 0xaa, 0x26, 0xce, 0x49, 0x62, 0xd5, 0xb1, 0xcd, 0xd8, 0xde, 0x8d, 0x79, 0x18,
- 0x24, 0x24, 0x9e, 0x00, 0x71, 0xcf, 0x2d, 0xe2, 0x96, 0x77, 0xe0, 0x15, 0xb8, 0x45, 0x67, 0x66,
- 0xec, 0x38, 0x9b, 0x50, 0xf5, 0x02, 0x71, 0x37, 0xdf, 0x39, 0x93, 0x33, 0x67, 0xce, 0xf9, 0xce,
- 0x37, 0x0e, 0x5c, 0x8f, 0x92, 0x02, 0x65, 0x22, 0xe2, 0xa3, 0x4c, 0xa6, 0x45, 0xca, 0xdc, 0x1a,
- 0x07, 0x7f, 0x76, 0xa0, 0x37, 0x49, 0x4b, 0x19, 0x22, 0xbb, 0x0e, 0x9d, 0xd3, 0xb1, 0x6f, 0x0d,
- 0xad, 0x43, 0x9b, 0x77, 0x4e, 0xc7, 0x8c, 0x41, 0xf7, 0xb1, 0x58, 0xa1, 0xdf, 0x19, 0x5a, 0x87,
- 0x1e, 0x57, 0x6b, 0xb2, 0x3d, 0xab, 0x32, 0xf4, 0x6d, 0x6d, 0xa3, 0x35, 0xbb, 0x0d, 0xee, 0x79,
- 0x4e, 0xd1, 0x56, 0xe8, 0x77, 0x95, 0xbd, 0xc1, 0xe4, 0x3b, 0x13, 0x79, 0x7e, 0x99, 0xca, 0x99,
- 0xef, 0x68, 0x5f, 0x8d, 0xd9, 0x0d, 0xb0, 0xcf, 0xf9, 0x43, 0xbf, 0xa7, 0xcc, 0xb4, 0x64, 0x3e,
- 0xf4, 0xc7, 0x38, 0x17, 0x65, 0x5c, 0xf8, 0xfd, 0xa1, 0x75, 0xe8, 0xf2, 0x1a, 0x52, 0x9c, 0x67,
- 0x18, 0xe3, 0x42, 0x8a, 0xb9, 0xef, 0xea, 0x38, 0x35, 0x66, 0x47, 0xc0, 0x4e, 0x93, 0x1c, 0xc3,
- 0x52, 0xe2, 0xe4, 0x65, 0x94, 0x3d, 0x47, 0x19, 0xcd, 0x2b, 0xdf, 0x53, 0x01, 0xf6, 0x78, 0xe8,
- 0x94, 0x47, 0x58, 0x08, 0x3a, 0x1b, 0x54, 0xa8, 0x1a, 0xb2, 0x00, 0x0e, 0x26, 0x4b, 0x21, 0x71,
- 0x36, 0xc1, 0x50, 0x62, 0xe1, 0x0f, 0x94, 0x7b, 0xcb, 0x46, 0x7b, 0x9e, 0xc8, 0x85, 0x48, 0xa2,
- 0xef, 0x45, 0x11, 0xa5, 0x89, 0x7f, 0xa0, 0xf7, 0xb4, 0x6d, 0x54, 0x25, 0x9e, 0xc6, 0xe8, 0x5f,
- 0xd3, 0x55, 0xa2, 0x75, 0xf0, 0x8b, 0x05, 0xde, 0x58, 0xe4, 0xcb, 0x69, 0x2a, 0xe4, 0xec, 0xb5,
- 0x6a, 0x7d, 0x07, 0x9c, 0x10, 0xe3, 0x38, 0xf7, 0xed, 0xa1, 0x7d, 0x38, 0xb8, 0x7b, 0xeb, 0xa8,
- 0x69, 0x62, 0x13, 0xe7, 0x04, 0xe3, 0x98, 0xeb, 0x5d, 0xec, 0x13, 0xf0, 0x0a, 0x5c, 0x65, 0xb1,
- 0x28, 0x30, 0xf7, 0xbb, 0xea, 0x27, 0x6c, 0xf3, 0x93, 0x67, 0xc6, 0xc5, 0x37, 0x9b, 0x76, 0xae,
- 0xe2, 0xec, 0x5e, 0x25, 0xf8, 0xa3, 0x03, 0xd7, 0xb6, 0x8e, 0x63, 0x07, 0x60, 0xad, 0x55, 0xe6,
- 0x0e, 0xb7, 0xd6, 0x84, 0x2a, 0x95, 0xb5, 0xc3, 0xad, 0x8a, 0xd0, 0xa5, 0xe2, 0x86, 0xc3, 0xad,
- 0x4b, 0x42, 0x4b, 0xc5, 0x08, 0x87, 0x5b, 0x4b, 0xf6, 0x01, 0xf4, 0xbf, 0x2b, 0x51, 0x46, 0x98,
- 0xfb, 0x8e, 0xca, 0xee, 0x7f, 0x9b, 0xec, 0x9e, 0x96, 0x28, 0x2b, 0x5e, 0xfb, 0xa9, 0x1a, 0x8a,
- 0x4d, 0x9a, 0x1a, 0x6a, 0x4d, 0xb6, 0x82, 0x98, 0xd7, 0xd7, 0x36, 0x5a, 0x9b, 0x2a, 0x6a, 0x3e,
- 0x50, 0x15, 0x3f, 0x85, 0xae, 0x58, 0x63, 0xee, 0x7b, 0x2a, 0xfe, 0x3b, 0xff, 0x50, 0xb0, 0xa3,
- 0xd1, 0x1a, 0xf3, 0x2f, 0x93, 0x42, 0x56, 0x5c, 0x6d, 0x67, 0xef, 0x43, 0x2f, 0x4c, 0xe3, 0x54,
- 0xe6, 0x3e, 0x5c, 0x4d, 0xec, 0x84, 0xec, 0xdc, 0xb8, 0x6f, 0xdf, 0x07, 0xaf, 0xf9, 0x2d, 0xd1,
- 0xf7, 0x25, 0x56, 0xaa, 0x12, 0x1e, 0xa7, 0x25, 0x7b, 0x17, 0x9c, 0x0b, 0x11, 0x97, 0xba, 0x8b,
- 0x83, 0xbb, 0xd7, 0x37, 0x61, 0x46, 0xeb, 0x28, 0xe7, 0xda, 0xf9, 0x79, 0xe7, 0x33, 0x2b, 0x58,
- 0x80, 0xa3, 0x22, 0xb7, 0x78, 0xe0, 0xd5, 0x3c, 0x50, 0xf3, 0xd5, 0x69, 0xcd, 0xd7, 0x0d, 0xb0,
- 0xbf, 0xc2, 0xb5, 0x19, 0x39, 0x5a, 0x36, 0x6c, 0xe9, 0xb6, 0xd8, 0x72, 0x13, 0x9c, 0xe7, 0xea,
- 0x70, 0xdd, 0x45, 0x0d, 0x82, 0x9f, 0x2d, 0xe8, 0xd2, 0xe1, 0xd4, 0xeb, 0x18, 0x17, 0x22, 0xac,
- 0x8e, 0xd3, 0x32, 0x99, 0xe5, 0xbe, 0x35, 0xb4, 0x0f, 0x6d, 0xbe, 0x65, 0x63, 0x6f, 0x40, 0x6f,
- 0xaa, 0xbd, 0x9d, 0xa1, 0x7d, 0xe8, 0x71, 0x83, 0x28, 0x74, 0x2c, 0xa6, 0x18, 0x9b, 0x14, 0x34,
- 0xa0, 0xdd, 0x99, 0xc4, 0x79, 0xb4, 0x36, 0x69, 0x18, 0x44, 0xf6, 0xbc, 0x9c, 0x93, 0x5d, 0x67,
- 0x62, 0x10, 0x25, 0x3d, 0x15, 0x79, 0xd3, 0x54, 0x5a, 0x53, 0xe4, 0x3c, 0x14, 0x71, 0xdd, 0x55,
- 0x0d, 0x82, 0x5f, 0x2d, 0x9a, 0x76, 0xcd, 0xd2, 0x9d, 0x0a, 0xbd, 0x09, 0x2e, 0x31, 0xf8, 0xc5,
- 0x85, 0x90, 0xa6, 0x4a, 0x7d, 0xc2, 0xcf, 0x85, 0x64, 0x1f, 0x43, 0x4f, 0x95, 0x78, 0xcf, 0xc4,
- 0xd4, 0xe1, 0x54, 0x55, 0xb8, 0xd9, 0xd6, 0x70, 0xaa, 0xdb, 0xe2, 0x54, 0x73, 0x59, 0xa7, 0x7d,
- 0xd9, 0x3b, 0xe0, 0x10, 0x39, 0x2b, 0x95, 0xfd, 0xde, 0xc8, 0x9a, 0xc2, 0x7a, 0x57, 0x70, 0x0e,
- 0xd7, 0xb6, 0x4e, 0x6c, 0x4e, 0xb2, 0xb6, 0x4f, 0xda, 0xd0, 0xc5, 0x33, 0xf4, 0x20, 0xa5, 0xcb,
- 0x31, 0xc6, 0xb0, 0xc0, 0x99, 0xaa, 0xb7, 0xcb, 0x1b, 0x1c, 0xfc, 0x68, 0x6d, 0xe2, 0xaa, 0xf3,
- 0x48, 0xcb, 0xc2, 0x74, 0xb5, 0x12, 0xc9, 0xcc, 0x84, 0xae, 0x21, 0xd5, 0x6d, 0x36, 0x35, 0xa1,
- 0x3b, 0xb3, 0x29, 0x61, 0x99, 0x99, 0x0e, 0x76, 0x64, 0xc6, 0x86, 0x30, 0x58, 0xa1, 0xc8, 0x4b,
- 0x89, 0x2b, 0x4c, 0x0a, 0x53, 0x82, 0xb6, 0x89, 0xdd, 0x82, 0x7e, 0x21, 0x16, 0x2f, 0x88, 0xe4,
- 0xa6, 0x93, 0x85, 0x58, 0x3c, 0xc0, 0x8a, 0xbd, 0x05, 0xde, 0x3c, 0xc2, 0x78, 0xa6, 0x5c, 0xba,
- 0x9d, 0xae, 0x32, 0x3c, 0xc0, 0x2a, 0xf8, 0xcd, 0x82, 0xde, 0x04, 0xe5, 0x05, 0xca, 0xd7, 0x12,
- 0xb9, 0xf6, 0xe3, 0x61, 0xbf, 0xe2, 0xf1, 0xe8, 0xee, 0x7f, 0x3c, 0x9c, 0xcd, 0xe3, 0x71, 0x13,
- 0x9c, 0x89, 0x0c, 0x4f, 0xc7, 0x2a, 0x23, 0x9b, 0x6b, 0x40, 0x6c, 0x1c, 0x85, 0x45, 0x74, 0x81,
- 0xe6, 0x45, 0x31, 0x68, 0x47, 0xfb, 0xdc, 0x3d, 0xda, 0xf7, 0x83, 0x05, 0xbd, 0x87, 0xa2, 0x4a,
- 0xcb, 0x62, 0x87, 0x85, 0x43, 0x18, 0x8c, 0xb2, 0x2c, 0x8e, 0x42, 0xfd, 0x6b, 0x7d, 0xa3, 0xb6,
- 0x89, 0x76, 0x3c, 0x6a, 0xd5, 0x57, 0xdf, 0xad, 0x6d, 0x22, 0xb9, 0x38, 0x51, 0xfa, 0xae, 0xc5,
- 0xba, 0x25, 0x17, 0x5a, 0xd6, 0x95, 0x93, 0x8a, 0x30, 0x2a, 0x8b, 0x74, 0x1e, 0xa7, 0x97, 0xea,
- 0xb6, 0x2e, 0x6f, 0x70, 0xf0, 0x7b, 0x07, 0xba, 0xff, 0x95, 0x26, 0x1f, 0x80, 0x15, 0x99, 0x66,
- 0x5b, 0x51, 0xa3, 0xd0, 0xfd, 0x96, 0x42, 0xfb, 0xd0, 0xaf, 0xa4, 0x48, 0x16, 0x98, 0xfb, 0xae,
- 0x52, 0x97, 0x1a, 0x2a, 0x8f, 0x9a, 0x23, 0x2d, 0xcd, 0x1e, 0xaf, 0x61, 0x33, 0x17, 0xd0, 0x9a,
- 0x8b, 0x8f, 0x8c, 0x8a, 0x0f, 0x54, 0x46, 0xfe, 0x76, 0x59, 0xae, 0x8a, 0xf7, 0xbf, 0xa7, 0xc9,
- 0x7f, 0x59, 0xe0, 0x34, 0x43, 0x75, 0xb2, 0x3d, 0x54, 0x27, 0x9b, 0xa1, 0x1a, 0x1f, 0xd7, 0x43,
- 0x35, 0x3e, 0x26, 0xcc, 0xcf, 0xea, 0xa1, 0xe2, 0x67, 0xd4, 0xac, 0xfb, 0x32, 0x2d, 0xb3, 0xe3,
- 0x4a, 0x77, 0xd5, 0xe3, 0x0d, 0x26, 0x26, 0x7e, 0xb3, 0x44, 0x69, 0x4a, 0xed, 0x71, 0x83, 0x88,
- 0xb7, 0x0f, 0x95, 0xe0, 0xe8, 0xe2, 0x6a, 0xc0, 0xde, 0x03, 0x87, 0x53, 0xf1, 0x54, 0x85, 0xb7,
- 0xfa, 0xa2, 0xcc, 0x5c, 0x7b, 0x29, 0xa8, 0xfe, 0x7a, 0x33, 0x04, 0xae, 0xbf, 0xe5, 0x3e, 0x84,
- 0xde, 0x64, 0x19, 0xcd, 0x8b, 0xfa, 0x2d, 0xfc, 0x7f, 0x4b, 0xb0, 0xa2, 0x15, 0x2a, 0x1f, 0x37,
- 0x5b, 0x82, 0xa7, 0xe0, 0x35, 0xc6, 0x4d, 0x3a, 0x56, 0x3b, 0x1d, 0x06, 0xdd, 0xf3, 0x24, 0x2a,
- 0xea, 0xd1, 0xa5, 0x35, 0x5d, 0xf6, 0x69, 0x29, 0x92, 0x22, 0x2a, 0xaa, 0x7a, 0x74, 0x6b, 0x1c,
- 0xdc, 0x33, 0xe9, 0x53, 0xb8, 0xf3, 0x2c, 0x43, 0x69, 0x64, 0x40, 0x03, 0x75, 0x48, 0x7a, 0x89,
- 0x5a, 0xc1, 0x6d, 0xae, 0x41, 0xf0, 0x2d, 0x78, 0xa3, 0x18, 0x65, 0xc1, 0xcb, 0x18, 0xf7, 0xbd,
- 0x8c, 0x5f, 0x4f, 0x9e, 0x3c, 0xae, 0x33, 0xa0, 0xf5, 0x66, 0xe4, 0xed, 0x2b, 0x23, 0xff, 0x40,
- 0x64, 0xe2, 0x74, 0xac, 0x78, 0x6e, 0x73, 0x83, 0x82, 0x9f, 0x2c, 0xe8, 0x92, 0xb6, 0xb4, 0x42,
- 0x77, 0x5f, 0xa5, 0x4b, 0x67, 0x32, 0xbd, 0x88, 0x66, 0x28, 0xeb, 0xcb, 0xd5, 0x58, 0x15, 0x3d,
- 0x5c, 0x62, 0xf3, 0x00, 0x1b, 0x44, 0x5c, 0xa3, 0x4f, 0xbd, 0x7a, 0x96, 0x5a, 0x5c, 0x23, 0x33,
- 0xd7, 0x4e, 0xf6, 0x36, 0xc0, 0xa4, 0xcc, 0x50, 0x8e, 0x66, 0xab, 0x28, 0x51, 0x4d, 0x77, 0x79,
- 0xcb, 0x12, 0x7c, 0xa1, 0x3f, 0x1e, 0x77, 0x14, 0xca, 0xda, 0xff, 0xa1, 0x79, 0x35, 0xf3, 0x20,
- 0xde, 0xfe, 0xdd, 0x6b, 0xdd, 0x76, 0x08, 0x03, 0xf3, 0xa5, 0xad, 0xbe, 0x5b, 0x8d, 0x58, 0xb5,
- 0x4c, 0x74, 0xe7, 0xb3, 0x72, 0x1a, 0x47, 0xa1, 0xba, 0xb3, 0xcb, 0x0d, 0x9a, 0xf6, 0xd4, 0x1f,
- 0x8a, 0x7b, 0x7f, 0x07, 0x00, 0x00, 0xff, 0xff, 0xe0, 0xc4, 0x7a, 0x3e, 0x62, 0x0c, 0x00, 0x00,
+ // 1310 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x57, 0x5f, 0x8f, 0xdb, 0x44,
+ 0x10, 0x97, 0x63, 0x3b, 0xb1, 0x27, 0xd7, 0x52, 0x99, 0x8a, 0x9a, 0x22, 0xa1, 0x60, 0x81, 0x08,
+ 0x82, 0x1e, 0xe8, 0x2a, 0x24, 0x84, 0x10, 0x52, 0xee, 0x82, 0xca, 0xd1, 0x7f, 0xd7, 0x4d, 0xaf,
+ 0x3c, 0xa1, 0x6a, 0xe3, 0x4c, 0x12, 0xab, 0x8e, 0x6d, 0xd6, 0xf6, 0x5d, 0xcc, 0x87, 0x41, 0x42,
+ 0xe2, 0x13, 0x20, 0xde, 0x79, 0x45, 0xbc, 0xf2, 0x1d, 0xf8, 0x0a, 0xbc, 0xa2, 0xd9, 0x5d, 0x3b,
+ 0xce, 0x25, 0x54, 0x7d, 0x40, 0xbc, 0xed, 0x6f, 0x66, 0x3d, 0x3b, 0x7f, 0x7e, 0x33, 0xbb, 0x86,
+ 0xeb, 0x51, 0x52, 0xa0, 0x48, 0x78, 0x7c, 0x98, 0x89, 0xb4, 0x48, 0x3d, 0xa7, 0xc6, 0xc1, 0x5f,
+ 0x1d, 0xe8, 0x4e, 0xd2, 0x52, 0x84, 0xe8, 0x5d, 0x87, 0xce, 0xe9, 0xd8, 0x37, 0x06, 0xc6, 0xd0,
+ 0x64, 0x9d, 0xd3, 0xb1, 0xe7, 0x81, 0xf5, 0x88, 0xaf, 0xd0, 0xef, 0x0c, 0x8c, 0xa1, 0xcb, 0xe4,
+ 0x9a, 0x64, 0x4f, 0xab, 0x0c, 0x7d, 0x53, 0xc9, 0x68, 0xed, 0xdd, 0x06, 0xe7, 0x3c, 0x27, 0x6b,
+ 0x2b, 0xf4, 0x2d, 0x29, 0x6f, 0x30, 0xe9, 0xce, 0x78, 0x9e, 0x5f, 0xa6, 0x62, 0xe6, 0xdb, 0x4a,
+ 0x57, 0x63, 0xef, 0x06, 0x98, 0xe7, 0xec, 0x81, 0xdf, 0x95, 0x62, 0x5a, 0x7a, 0x3e, 0xf4, 0xc6,
+ 0x38, 0xe7, 0x65, 0x5c, 0xf8, 0xbd, 0x81, 0x31, 0x74, 0x58, 0x0d, 0xc9, 0xce, 0x53, 0x8c, 0x71,
+ 0x21, 0xf8, 0xdc, 0x77, 0x94, 0x9d, 0x1a, 0x7b, 0x87, 0xe0, 0x9d, 0x26, 0x39, 0x86, 0xa5, 0xc0,
+ 0xc9, 0x8b, 0x28, 0x7b, 0x86, 0x22, 0x9a, 0x57, 0xbe, 0x2b, 0x0d, 0xec, 0xd1, 0xd0, 0x29, 0x0f,
+ 0xb1, 0xe0, 0x74, 0x36, 0x48, 0x53, 0x35, 0xf4, 0x02, 0x38, 0x98, 0x2c, 0xb9, 0xc0, 0xd9, 0x04,
+ 0x43, 0x81, 0x85, 0xdf, 0x97, 0xea, 0x2d, 0x19, 0xed, 0x79, 0x2c, 0x16, 0x3c, 0x89, 0x7e, 0xe0,
+ 0x45, 0x94, 0x26, 0xfe, 0x81, 0xda, 0xd3, 0x96, 0x51, 0x96, 0x58, 0x1a, 0xa3, 0x7f, 0x4d, 0x65,
+ 0x89, 0xd6, 0xc1, 0xaf, 0x06, 0xb8, 0x63, 0x9e, 0x2f, 0xa7, 0x29, 0x17, 0xb3, 0x57, 0xca, 0xf5,
+ 0x1d, 0xb0, 0x43, 0x8c, 0xe3, 0xdc, 0x37, 0x07, 0xe6, 0xb0, 0x7f, 0x74, 0xeb, 0xb0, 0x29, 0x62,
+ 0x63, 0xe7, 0x04, 0xe3, 0x98, 0xa9, 0x5d, 0xde, 0x27, 0xe0, 0x16, 0xb8, 0xca, 0x62, 0x5e, 0x60,
+ 0xee, 0x5b, 0xf2, 0x13, 0x6f, 0xf3, 0xc9, 0x53, 0xad, 0x62, 0x9b, 0x4d, 0x3b, 0xa1, 0xd8, 0xbb,
+ 0xa1, 0x04, 0x7f, 0x76, 0xe0, 0xda, 0xd6, 0x71, 0xde, 0x01, 0x18, 0x6b, 0xe9, 0xb9, 0xcd, 0x8c,
+ 0x35, 0xa1, 0x4a, 0x7a, 0x6d, 0x33, 0xa3, 0x22, 0x74, 0x29, 0xb9, 0x61, 0x33, 0xe3, 0x92, 0xd0,
+ 0x52, 0x32, 0xc2, 0x66, 0xc6, 0xd2, 0xfb, 0x00, 0x7a, 0xdf, 0x97, 0x28, 0x22, 0xcc, 0x7d, 0x5b,
+ 0x7a, 0xf7, 0xda, 0xc6, 0xbb, 0x27, 0x25, 0x8a, 0x8a, 0xd5, 0x7a, 0xca, 0x86, 0x64, 0x93, 0xa2,
+ 0x86, 0x5c, 0x93, 0xac, 0x20, 0xe6, 0xf5, 0x94, 0x8c, 0xd6, 0x3a, 0x8b, 0x8a, 0x0f, 0x94, 0xc5,
+ 0x4f, 0xc1, 0xe2, 0x6b, 0xcc, 0x7d, 0x57, 0xda, 0x7f, 0xe7, 0x5f, 0x12, 0x76, 0x38, 0x5a, 0x63,
+ 0xfe, 0x55, 0x52, 0x88, 0x8a, 0xc9, 0xed, 0xde, 0xfb, 0xd0, 0x0d, 0xd3, 0x38, 0x15, 0xb9, 0x0f,
+ 0x57, 0x1d, 0x3b, 0x21, 0x39, 0xd3, 0xea, 0xdb, 0xf7, 0xc0, 0x6d, 0xbe, 0x25, 0xfa, 0xbe, 0xc0,
+ 0x4a, 0x66, 0xc2, 0x65, 0xb4, 0xf4, 0xde, 0x05, 0xfb, 0x82, 0xc7, 0xa5, 0xaa, 0x62, 0xff, 0xe8,
+ 0xfa, 0xc6, 0xcc, 0x68, 0x1d, 0xe5, 0x4c, 0x29, 0x3f, 0xef, 0x7c, 0x66, 0x04, 0x0b, 0xb0, 0xa5,
+ 0xe5, 0x16, 0x0f, 0xdc, 0x9a, 0x07, 0xb2, 0xbf, 0x3a, 0xad, 0xfe, 0xba, 0x01, 0xe6, 0xd7, 0xb8,
+ 0xd6, 0x2d, 0x47, 0xcb, 0x86, 0x2d, 0x56, 0x8b, 0x2d, 0x37, 0xc1, 0x7e, 0x26, 0x0f, 0x57, 0x55,
+ 0x54, 0x20, 0xf8, 0xc5, 0x00, 0x8b, 0x0e, 0xa7, 0x5a, 0xc7, 0xb8, 0xe0, 0x61, 0x75, 0x9c, 0x96,
+ 0xc9, 0x2c, 0xf7, 0x8d, 0x81, 0x39, 0x34, 0xd9, 0x96, 0xcc, 0x7b, 0x03, 0xba, 0x53, 0xa5, 0xed,
+ 0x0c, 0xcc, 0xa1, 0xcb, 0x34, 0x22, 0xd3, 0x31, 0x9f, 0x62, 0xac, 0x5d, 0x50, 0x80, 0x76, 0x67,
+ 0x02, 0xe7, 0xd1, 0x5a, 0xbb, 0xa1, 0x11, 0xc9, 0xf3, 0x72, 0x4e, 0x72, 0xe5, 0x89, 0x46, 0xe4,
+ 0xf4, 0x94, 0xe7, 0x4d, 0x51, 0x69, 0x4d, 0x96, 0xf3, 0x90, 0xc7, 0x75, 0x55, 0x15, 0x08, 0x7e,
+ 0x33, 0xa8, 0xdb, 0x15, 0x4b, 0x77, 0x32, 0xf4, 0x26, 0x38, 0xc4, 0xe0, 0xe7, 0x17, 0x5c, 0xe8,
+ 0x2c, 0xf5, 0x08, 0x3f, 0xe3, 0xc2, 0xfb, 0x18, 0xba, 0x32, 0xc5, 0x7b, 0x3a, 0xa6, 0x36, 0x27,
+ 0xb3, 0xc2, 0xf4, 0xb6, 0x86, 0x53, 0x56, 0x8b, 0x53, 0x4d, 0xb0, 0x76, 0x3b, 0xd8, 0x3b, 0x60,
+ 0x13, 0x39, 0x2b, 0xe9, 0xfd, 0x5e, 0xcb, 0x8a, 0xc2, 0x6a, 0x57, 0x70, 0x0e, 0xd7, 0xb6, 0x4e,
+ 0x6c, 0x4e, 0x32, 0xb6, 0x4f, 0xda, 0xd0, 0xc5, 0xd5, 0xf4, 0xa0, 0x49, 0x97, 0x63, 0x8c, 0x61,
+ 0x81, 0x33, 0x99, 0x6f, 0x87, 0x35, 0x38, 0xf8, 0xc9, 0xd8, 0xd8, 0x95, 0xe7, 0xd1, 0x2c, 0x0b,
+ 0xd3, 0xd5, 0x8a, 0x27, 0x33, 0x6d, 0xba, 0x86, 0x94, 0xb7, 0xd9, 0x54, 0x9b, 0xee, 0xcc, 0xa6,
+ 0x84, 0x45, 0xa6, 0x2b, 0xd8, 0x11, 0x99, 0x37, 0x80, 0xfe, 0x0a, 0x79, 0x5e, 0x0a, 0x5c, 0x61,
+ 0x52, 0xe8, 0x14, 0xb4, 0x45, 0xde, 0x2d, 0xe8, 0x15, 0x7c, 0xf1, 0x9c, 0x48, 0xae, 0x2b, 0x59,
+ 0xf0, 0xc5, 0x7d, 0xac, 0xbc, 0xb7, 0xc0, 0x9d, 0x47, 0x18, 0xcf, 0xa4, 0x4a, 0x95, 0xd3, 0x91,
+ 0x82, 0xfb, 0x58, 0x05, 0xbf, 0x1b, 0xd0, 0x9d, 0xa0, 0xb8, 0x40, 0xf1, 0x4a, 0x43, 0xae, 0x7d,
+ 0x79, 0x98, 0x2f, 0xb9, 0x3c, 0xac, 0xfd, 0x97, 0x87, 0xbd, 0xb9, 0x3c, 0x6e, 0x82, 0x3d, 0x11,
+ 0xe1, 0xe9, 0x58, 0x7a, 0x64, 0x32, 0x05, 0x88, 0x8d, 0xa3, 0xb0, 0x88, 0x2e, 0x50, 0xdf, 0x28,
+ 0x1a, 0xed, 0xcc, 0x3e, 0x67, 0xcf, 0xec, 0xfb, 0xd1, 0x80, 0xee, 0x03, 0x5e, 0xa5, 0x65, 0xb1,
+ 0xc3, 0xc2, 0x01, 0xf4, 0x47, 0x59, 0x16, 0x47, 0xa1, 0xfa, 0x5a, 0x45, 0xd4, 0x16, 0xd1, 0x8e,
+ 0x87, 0xad, 0xfc, 0xaa, 0xd8, 0xda, 0x22, 0x1a, 0x17, 0x27, 0x72, 0xbe, 0xab, 0x61, 0xdd, 0x1a,
+ 0x17, 0x6a, 0xac, 0x4b, 0x25, 0x25, 0x61, 0x54, 0x16, 0xe9, 0x3c, 0x4e, 0x2f, 0x65, 0xb4, 0x0e,
+ 0x6b, 0x70, 0xf0, 0x47, 0x07, 0xac, 0xff, 0x6b, 0x26, 0x1f, 0x80, 0x11, 0xe9, 0x62, 0x1b, 0x51,
+ 0x33, 0xa1, 0x7b, 0xad, 0x09, 0xed, 0x43, 0xaf, 0x12, 0x3c, 0x59, 0x60, 0xee, 0x3b, 0x72, 0xba,
+ 0xd4, 0x50, 0x6a, 0x64, 0x1f, 0xa9, 0xd1, 0xec, 0xb2, 0x1a, 0x36, 0x7d, 0x01, 0xad, 0xbe, 0xf8,
+ 0x48, 0x4f, 0xf1, 0xbe, 0xf4, 0xc8, 0xdf, 0x4e, 0xcb, 0xd5, 0xe1, 0xfd, 0xdf, 0xcd, 0xe4, 0xbf,
+ 0x0d, 0xb0, 0x9b, 0xa6, 0x3a, 0xd9, 0x6e, 0xaa, 0x93, 0x4d, 0x53, 0x8d, 0x8f, 0xeb, 0xa6, 0x1a,
+ 0x1f, 0x13, 0x66, 0x67, 0x75, 0x53, 0xb1, 0x33, 0x2a, 0xd6, 0x3d, 0x91, 0x96, 0xd9, 0x71, 0xa5,
+ 0xaa, 0xea, 0xb2, 0x06, 0x13, 0x13, 0xbf, 0x5d, 0xa2, 0xd0, 0xa9, 0x76, 0x99, 0x46, 0xc4, 0xdb,
+ 0x07, 0x72, 0xe0, 0xa8, 0xe4, 0x2a, 0xe0, 0xbd, 0x07, 0x36, 0xa3, 0xe4, 0xc9, 0x0c, 0x6f, 0xd5,
+ 0x45, 0x8a, 0x99, 0xd2, 0x92, 0x51, 0xf5, 0x7a, 0xd3, 0x04, 0xae, 0xdf, 0x72, 0x1f, 0x42, 0x77,
+ 0xb2, 0x8c, 0xe6, 0x45, 0x7d, 0x17, 0xbe, 0xde, 0x1a, 0x58, 0xd1, 0x0a, 0xa5, 0x8e, 0xe9, 0x2d,
+ 0xc1, 0x13, 0x70, 0x1b, 0xe1, 0xc6, 0x1d, 0xa3, 0xed, 0x8e, 0x07, 0xd6, 0x79, 0x12, 0x15, 0x75,
+ 0xeb, 0xd2, 0x9a, 0x82, 0x7d, 0x52, 0xf2, 0xa4, 0x88, 0x8a, 0xaa, 0x6e, 0xdd, 0x1a, 0x07, 0x77,
+ 0xb5, 0xfb, 0x64, 0xee, 0x3c, 0xcb, 0x50, 0xe8, 0x31, 0xa0, 0x80, 0x3c, 0x24, 0xbd, 0x44, 0x35,
+ 0xc1, 0x4d, 0xa6, 0x40, 0xf0, 0x1d, 0xb8, 0xa3, 0x18, 0x45, 0xc1, 0xca, 0x18, 0xf7, 0xdd, 0x8c,
+ 0xdf, 0x4c, 0x1e, 0x3f, 0xaa, 0x3d, 0xa0, 0xf5, 0xa6, 0xe5, 0xcd, 0x2b, 0x2d, 0x7f, 0x9f, 0x67,
+ 0xfc, 0x74, 0x2c, 0x79, 0x6e, 0x32, 0x8d, 0x82, 0x9f, 0x0d, 0xb0, 0x68, 0xb6, 0xb4, 0x4c, 0x5b,
+ 0x2f, 0x9b, 0x4b, 0x67, 0x22, 0xbd, 0x88, 0x66, 0x28, 0xea, 0xe0, 0x6a, 0x2c, 0x93, 0x1e, 0x2e,
+ 0xb1, 0xb9, 0x80, 0x35, 0x22, 0xae, 0xd1, 0x53, 0xaf, 0xee, 0xa5, 0x16, 0xd7, 0x48, 0xcc, 0x94,
+ 0xd2, 0x7b, 0x1b, 0x60, 0x52, 0x66, 0x28, 0x46, 0xb3, 0x55, 0x94, 0xc8, 0xa2, 0x3b, 0xac, 0x25,
+ 0x09, 0xbe, 0x54, 0x8f, 0xc7, 0x9d, 0x09, 0x65, 0xec, 0x7f, 0x68, 0x5e, 0xf5, 0x3c, 0x88, 0xb7,
+ 0xbf, 0x7b, 0xa5, 0x68, 0x07, 0xd0, 0xd7, 0x2f, 0x6d, 0xf9, 0x6e, 0xd5, 0xc3, 0xaa, 0x25, 0xa2,
+ 0x98, 0xcf, 0xca, 0x69, 0x1c, 0x85, 0x32, 0x66, 0x87, 0x69, 0x14, 0x1c, 0x41, 0xf7, 0x24, 0x4d,
+ 0xe6, 0xd1, 0xc2, 0x1b, 0x82, 0x35, 0x2a, 0x8b, 0xa5, 0x3c, 0xa9, 0x7f, 0x74, 0xb3, 0xd5, 0x68,
+ 0x65, 0xb1, 0x54, 0x7b, 0x98, 0xdc, 0x11, 0x7c, 0x01, 0xb0, 0x91, 0xd1, 0xf3, 0x7d, 0x13, 0xfd,
+ 0x23, 0xbc, 0xa4, 0x12, 0xe5, 0xd2, 0x8a, 0xc3, 0xf6, 0x68, 0xa6, 0x5d, 0xf9, 0x0b, 0x73, 0xf7,
+ 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xf9, 0xde, 0x56, 0x70, 0xd4, 0x0c, 0x00, 0x00,
}
diff --git a/bolt/internal/internal.proto b/bolt/internal/internal.proto
index 2e540a64a4..4f15e3b9a1 100644
--- a/bolt/internal/internal.proto
+++ b/bolt/internal/internal.proto
@@ -164,6 +164,14 @@ message Organization {
bool Public = 4; // Public specifies that users must be explicitly added to the organization
}
+message Config {
+ AuthConfig Auth = 1; // Auth is the configuration for options that auth related
+}
+
+message AuthConfig {
+ bool SuperAdminNewUsers = 1; // SuperAdminNewUsers configuration option that specifies which users will auto become super admin
+}
+
// The following is a vim modeline, it autoconfigures vim to have the
// appropriate tabbing and whitespace management to edit this file
//
diff --git a/chronograf.go b/chronograf.go
index 77c7f6f896..2a11531a42 100644
--- a/chronograf.go
+++ b/chronograf.go
@@ -26,6 +26,7 @@ const (
ErrOrganizationNotFound = Error("organization not found")
ErrOrganizationAlreadyExists = Error("organization already exists")
ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization")
+ ErrConfigNotFound = Error("cannot find configuration")
)
// Error is a domain error encountered while processing chronograf requests
@@ -604,3 +605,30 @@ type OrganizationsStore interface {
// DefaultOrganization returns the DefaultOrganization
DefaultOrganization(ctx context.Context) (*Organization, error)
}
+
+// AuthConfig is the global application config section for auth parameters
+
+type AuthConfig struct {
+ // SuperAdminNewUsers should be true by default to give a seamless upgrade to
+ // 1.4.0 for legacy users. It means that all new users will by default receive
+ // SuperAdmin status. If a SuperAdmin wants to change this behavior, they
+ // can toggle it off via the Chronograf UI, in which case newly authenticating
+ // users will simply receive whatever role they would otherwise receive.
+ SuperAdminNewUsers bool `json:"superAdminNewUsers"`
+}
+
+// Config is the global application Config for parameters that can be set via
+// API, with different sections, such as Auth
+type Config struct {
+ Auth AuthConfig `json:"auth"`
+}
+
+// ConfigStore is the storage and retrieval of global application Config
+type ConfigStore interface {
+ // Initialize creates the initial configuration
+ Initialize(context.Context) error
+ // Get retrieves the whole Config from the ConfigStore
+ Get(context.Context) (*Config, error)
+ // Update updates the whole Config in the ConfigStore
+ Update(context.Context, *Config) error
+}
diff --git a/mocks/config.go b/mocks/config.go
new file mode 100644
index 0000000000..f46fa6f814
--- /dev/null
+++ b/mocks/config.go
@@ -0,0 +1,28 @@
+package mocks
+
+import (
+ "context"
+
+ "github.com/influxdata/chronograf"
+)
+
+// ConfigStore stores global application configuration
+type ConfigStore struct {
+ Config *chronograf.Config
+}
+
+// Initialize is noop in mocks store
+func (c ConfigStore) Initialize(ctx context.Context) error {
+ return nil
+}
+
+// Get returns the whole global application configuration
+func (c ConfigStore) Get(ctx context.Context) (*chronograf.Config, error) {
+ return c.Config, nil
+}
+
+// Update updates the whole global application configuration
+func (c ConfigStore) Update(ctx context.Context, config *chronograf.Config) error {
+ c.Config = config
+ return nil
+}
diff --git a/mocks/store.go b/mocks/store.go
index f207b87f1d..ebc05ea495 100644
--- a/mocks/store.go
+++ b/mocks/store.go
@@ -14,6 +14,7 @@ type Store struct {
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
OrganizationsStore chronograf.OrganizationsStore
+ ConfigStore chronograf.ConfigStore
}
func (s *Store) Sources(ctx context.Context) chronograf.SourcesStore {
@@ -39,3 +40,7 @@ func (s *Store) Organizations(ctx context.Context) chronograf.OrganizationsStore
func (s *Store) Dashboards(ctx context.Context) chronograf.DashboardsStore {
return s.DashboardsStore
}
+
+func (s *Store) Config(ctx context.Context) chronograf.ConfigStore {
+ return s.ConfigStore
+}
diff --git a/noop/config.go b/noop/config.go
new file mode 100644
index 0000000000..1f3b180a51
--- /dev/null
+++ b/noop/config.go
@@ -0,0 +1,26 @@
+package noop
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/influxdata/chronograf"
+)
+
+// ensure ConfigStore implements chronograf.ConfigStore
+var _ chronograf.ConfigStore = &ConfigStore{}
+
+type ConfigStore struct{}
+
+// TODO(desa): this really should be removed
+func (s *ConfigStore) Initialize(context.Context) error {
+ return fmt.Errorf("cannot initialize")
+}
+
+func (s *ConfigStore) Get(context.Context) (*chronograf.Config, error) {
+ return nil, chronograf.ErrConfigNotFound
+}
+
+func (s *ConfigStore) Update(context.Context, *chronograf.Config) error {
+ return fmt.Errorf("cannot update conifg")
+}
diff --git a/organizations/users.go b/organizations/users.go
index 0db6e2cd62..7c6600d7d1 100644
--- a/organizations/users.go
+++ b/organizations/users.go
@@ -142,6 +142,20 @@ func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.U
// and the user that was found in the underlying store
usr.Roles = append(roles, u.Roles...)
+ // u.SuperAdmin == true is logically equivalent to u.SuperAdmin, however
+ // it is more clear on a conceptual level to check equality
+ //
+ // TODO(desa): this should go away with https://github.com/influxdata/chronograf/issues/2207
+ // I do not like checking super admin here. The organization users store should only be
+ // concerned about organizations.
+ //
+ // If the user being added already existed in a previous organization, and was already a SuperAdmin,
+ // then this ensures that they retain their SuperAdmin status. And if they weren't a SuperAdmin, and
+ // the user being added has been granted SuperAdmin status, they will be promoted
+ if u.SuperAdmin == true {
+ usr.SuperAdmin = true
+ }
+
// Update the user in the underlying store
if err := s.store.Update(ctx, usr); err != nil {
return nil, err
diff --git a/organizations/users_test.go b/organizations/users_test.go
index 0baa40a08f..4f350adea8 100644
--- a/organizations/users_test.go
+++ b/organizations/users_test.go
@@ -315,9 +315,62 @@ func TestUsersStore_Add(t *testing.T) {
Scheme: "oauth2",
Roles: []chronograf.Role{
{
- Organization: "1337",
- Name: "editor",
+ Organization: "1336",
+ Name: "admin",
},
+ },
+ },
+ },
+ {
+ name: "Add non-new user with Role. Stored user is not super admin. Provided user is super admin",
+ fields: fields{
+ UsersStore: &mocks.UsersStore{
+ AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
+ return u, nil
+ },
+ UpdateF: func(ctx context.Context, u *chronograf.User) error {
+ return nil
+ },
+ GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
+ return &chronograf.User{
+ ID: 1234,
+ Name: "docbrown",
+ Provider: "github",
+ Scheme: "oauth2",
+ SuperAdmin: false,
+ Roles: []chronograf.Role{
+ {
+ Organization: "1337",
+ Name: "editor",
+ },
+ },
+ }, nil
+ },
+ },
+ },
+ args: args{
+ ctx: context.Background(),
+ u: &chronograf.User{
+ ID: 1234,
+ Name: "docbrown",
+ Provider: "github",
+ Scheme: "oauth2",
+ SuperAdmin: true,
+ Roles: []chronograf.Role{
+ {
+ Organization: "1336",
+ Name: "admin",
+ },
+ },
+ },
+ orgID: "1336",
+ },
+ want: &chronograf.User{
+ Name: "docbrown",
+ Provider: "github",
+ Scheme: "oauth2",
+ SuperAdmin: true,
+ Roles: []chronograf.Role{
{
Organization: "1336",
Name: "admin",
@@ -503,6 +556,9 @@ func TestUsersStore_Add(t *testing.T) {
if got == nil && tt.want == nil {
continue
}
+ if diff := cmp.Diff(got, tt.want, userCmpOptions...); diff != "" {
+ t.Errorf("%q. UsersStore.Add():\n-got/+want\ndiff %s", tt.name, diff)
+ }
}
}
diff --git a/server/config.go b/server/config.go
new file mode 100644
index 0000000000..68898c61fe
--- /dev/null
+++ b/server/config.go
@@ -0,0 +1,123 @@
+package server
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "github.com/bouk/httprouter"
+ "github.com/influxdata/chronograf"
+)
+
+type configResponse struct {
+ Links selfLinks `json:"links"`
+ chronograf.Config
+}
+
+func newConfigResponse(config chronograf.Config) *configResponse {
+ return &configResponse{
+ Links: selfLinks{
+ Self: "/chronograf/v1/config",
+ },
+ Config: config,
+ }
+}
+
+type authConfigResponse struct {
+ Links selfLinks `json:"links"`
+ chronograf.AuthConfig
+}
+
+func newAuthConfigResponse(config chronograf.Config) *authConfigResponse {
+ return &authConfigResponse{
+ Links: selfLinks{
+ Self: "/chronograf/v1/config/auth",
+ },
+ AuthConfig: config.Auth,
+ }
+}
+
+// Config retrieves the global application configuration
+func (s *Service) Config(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ config, err := s.Store.Config(ctx).Get(ctx)
+ if err != nil {
+ Error(w, http.StatusBadRequest, err.Error(), s.Logger)
+ return
+ }
+
+ if config == nil {
+ Error(w, http.StatusBadRequest, "Configuration object was nil", s.Logger)
+ return
+ }
+ res := newConfigResponse(*config)
+ encodeJSON(w, http.StatusOK, res, s.Logger)
+}
+
+// ConfigSection retrieves the section of the global application configuration
+func (s *Service) ConfigSection(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ config, err := s.Store.Config(ctx).Get(ctx)
+ if err != nil {
+ Error(w, http.StatusBadRequest, err.Error(), s.Logger)
+ return
+ }
+
+ if config == nil {
+ Error(w, http.StatusBadRequest, "Configuration object was nil", s.Logger)
+ return
+ }
+
+ section := httprouter.GetParamFromContext(ctx, "section")
+ var res interface{}
+ switch section {
+ case "auth":
+ res = newAuthConfigResponse(*config)
+ default:
+ Error(w, http.StatusBadRequest, fmt.Sprintf("received unknown section %q", section), s.Logger)
+ return
+ }
+
+ encodeJSON(w, http.StatusOK, res, s.Logger)
+}
+
+// ReplaceConfigSection replaces a section of the global application configuration
+func (s *Service) ReplaceConfigSection(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ config, err := s.Store.Config(ctx).Get(ctx)
+ if err != nil {
+ Error(w, http.StatusBadRequest, err.Error(), s.Logger)
+ return
+ }
+
+ if config == nil {
+ Error(w, http.StatusBadRequest, "Configuration object was nil", s.Logger)
+ return
+ }
+
+ section := httprouter.GetParamFromContext(ctx, "section")
+ var res interface{}
+ switch section {
+ case "auth":
+ var authConfig chronograf.AuthConfig
+ if err := json.NewDecoder(r.Body).Decode(&authConfig); err != nil {
+ invalidJSON(w, s.Logger)
+ return
+ }
+ config.Auth = authConfig
+ res = newAuthConfigResponse(*config)
+ default:
+ Error(w, http.StatusBadRequest, fmt.Sprintf("received unknown section %q", section), s.Logger)
+ return
+ }
+
+ if err := s.Store.Config(ctx).Update(ctx, config); err != nil {
+ unknownErrorWithMessage(w, err, s.Logger)
+ return
+ }
+
+ encodeJSON(w, http.StatusOK, res, s.Logger)
+}
diff --git a/server/config_test.go b/server/config_test.go
new file mode 100644
index 0000000000..8901dc3b88
--- /dev/null
+++ b/server/config_test.go
@@ -0,0 +1,290 @@
+package server
+
+import (
+ "bytes"
+ "encoding/json"
+ "io/ioutil"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/bouk/httprouter"
+ "github.com/influxdata/chronograf"
+ "github.com/influxdata/chronograf/log"
+ "github.com/influxdata/chronograf/mocks"
+)
+
+func TestConfig(t *testing.T) {
+ type fields struct {
+ ConfigStore chronograf.ConfigStore
+ }
+ type wants struct {
+ statusCode int
+ contentType string
+ body string
+ }
+
+ tests := []struct {
+ name string
+ fields fields
+ wants wants
+ }{
+ {
+ name: "Get global application configuration",
+ fields: fields{
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
+ },
+ wants: wants{
+ statusCode: 200,
+ contentType: "application/json",
+ body: `{"auth": {"superAdminNewUsers": false}, "links": {"self": "/chronograf/v1/config"}}`,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ s := &Service{
+ Store: &mocks.Store{
+ ConfigStore: tt.fields.ConfigStore,
+ },
+ Logger: log.New(log.DebugLevel),
+ }
+
+ w := httptest.NewRecorder()
+ r := httptest.NewRequest("GET", "http://any.url", nil)
+
+ s.Config(w, r)
+
+ resp := w.Result()
+ content := resp.Header.Get("Content-Type")
+ body, _ := ioutil.ReadAll(resp.Body)
+
+ if resp.StatusCode != tt.wants.statusCode {
+ t.Errorf("%q. Config() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode)
+ }
+ if tt.wants.contentType != "" && content != tt.wants.contentType {
+ t.Errorf("%q. Config() = %v, want %v", tt.name, content, tt.wants.contentType)
+ }
+ if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
+ t.Errorf("%q. Config() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
+ }
+ })
+ }
+}
+
+func TestConfigSection(t *testing.T) {
+ type fields struct {
+ ConfigStore chronograf.ConfigStore
+ }
+ type args struct {
+ section string
+ }
+ type wants struct {
+ statusCode int
+ contentType string
+ body string
+ }
+
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ wants wants
+ }{
+ {
+ name: "Get auth configuration",
+ fields: fields{
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
+ },
+ args: args{
+ section: "auth",
+ },
+ wants: wants{
+ statusCode: 200,
+ contentType: "application/json",
+ body: `{"superAdminNewUsers": false, "links": {"self": "/chronograf/v1/config/auth"}}`,
+ },
+ },
+ {
+ name: "Get unknown configuration",
+ fields: fields{
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
+ },
+ args: args{
+ section: "unknown",
+ },
+ wants: wants{
+ statusCode: 400,
+ contentType: "application/json",
+ body: `{"code":400,"message":"received unknown section \"unknown\""}`,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ s := &Service{
+ Store: &mocks.Store{
+ ConfigStore: tt.fields.ConfigStore,
+ },
+ Logger: log.New(log.DebugLevel),
+ }
+
+ w := httptest.NewRecorder()
+ r := httptest.NewRequest("GET", "http://any.url", nil)
+ r = r.WithContext(httprouter.WithParams(
+ r.Context(),
+ httprouter.Params{
+ {
+ Key: "section",
+ Value: tt.args.section,
+ },
+ }))
+
+ s.ConfigSection(w, r)
+
+ resp := w.Result()
+ content := resp.Header.Get("Content-Type")
+ body, _ := ioutil.ReadAll(resp.Body)
+
+ if resp.StatusCode != tt.wants.statusCode {
+ t.Errorf("%q. Config() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode)
+ }
+ if tt.wants.contentType != "" && content != tt.wants.contentType {
+ t.Errorf("%q. Config() = %v, want %v", tt.name, content, tt.wants.contentType)
+ }
+ if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
+ t.Errorf("%q. Config() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
+ }
+ })
+ }
+}
+
+func TestReplaceConfigSection(t *testing.T) {
+ type fields struct {
+ ConfigStore chronograf.ConfigStore
+ }
+ type args struct {
+ section string
+ payload interface{} // expects JSON serializable struct
+ }
+ type wants struct {
+ statusCode int
+ contentType string
+ body string
+ }
+
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ wants wants
+ }{
+ {
+ name: "Set auth configuration",
+ fields: fields{
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
+ },
+ args: args{
+ section: "auth",
+ payload: chronograf.AuthConfig{
+ SuperAdminNewUsers: true,
+ },
+ },
+ wants: wants{
+ statusCode: 200,
+ contentType: "application/json",
+ body: `{"superAdminNewUsers": true, "links": {"self": "/chronograf/v1/config/auth"}}`,
+ },
+ },
+ {
+ name: "Set unknown configuration",
+ fields: fields{
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
+ },
+ args: args{
+ section: "unknown",
+ payload: struct {
+ Data string `json:"data"`
+ }{
+ Data: "stuff",
+ },
+ },
+ wants: wants{
+ statusCode: 400,
+ contentType: "application/json",
+ body: `{"code":400,"message":"received unknown section \"unknown\""}`,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ s := &Service{
+ Store: &mocks.Store{
+ ConfigStore: tt.fields.ConfigStore,
+ },
+ Logger: log.New(log.DebugLevel),
+ }
+
+ w := httptest.NewRecorder()
+ r := httptest.NewRequest("GET", "http://any.url", nil)
+ r = r.WithContext(httprouter.WithParams(
+ r.Context(),
+ httprouter.Params{
+ {
+ Key: "section",
+ Value: tt.args.section,
+ },
+ }))
+ buf, _ := json.Marshal(tt.args.payload)
+ r.Body = ioutil.NopCloser(bytes.NewReader(buf))
+
+ s.ReplaceConfigSection(w, r)
+
+ resp := w.Result()
+ content := resp.Header.Get("Content-Type")
+ body, _ := ioutil.ReadAll(resp.Body)
+
+ if resp.StatusCode != tt.wants.statusCode {
+ t.Errorf("%q. Config() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode)
+ }
+ if tt.wants.contentType != "" && content != tt.wants.contentType {
+ t.Errorf("%q. Config() = %v, want %v", tt.name, content, tt.wants.contentType)
+ }
+ if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
+ t.Errorf("%q. Config() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
+ }
+ })
+ }
+}
diff --git a/server/links.go b/server/links.go
index 9f4f6e38aa..3a3b3fd41d 100644
--- a/server/links.go
+++ b/server/links.go
@@ -5,6 +5,11 @@ import (
"net/url"
)
+type getConfigLinksResponse struct {
+ Self string `json:"self"` // Location of the whole global application configuration
+ Auth string `json:"auth"` // Location of the auth section of the global application configuration
+}
+
type getExternalLinksResponse struct {
StatusFeed *string `json:"statusFeed,omitempty"` // Location of the a JSON Feed for client's Status page News Feed
CustomLinks []CustomLink `json:"custom,omitempty"` // Any custom external links for client's User menu
diff --git a/server/me.go b/server/me.go
index 1521afabde..94a9cfeb3f 100644
--- a/server/me.go
+++ b/server/me.go
@@ -206,6 +206,23 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
}
if usr != nil {
+
+ if defaultOrg.Public || usr.SuperAdmin == true {
+ // If the default organization is public, or the user is a super admin
+ // they will always have a role in the default organization
+ defaultOrgID := fmt.Sprintf("%d", defaultOrg.ID)
+ if !hasRoleInDefaultOrganization(usr, defaultOrgID) {
+ usr.Roles = append(usr.Roles, chronograf.Role{
+ Organization: defaultOrgID,
+ Name: defaultOrg.DefaultRole,
+ })
+ if err := s.Store.Users(serverCtx).Update(serverCtx, usr); err != nil {
+ unknownErrorWithMessage(w, err, s.Logger)
+ return
+ }
+ }
+ }
+
// If the default org is private and the user has no roles, they should not have access
if !defaultOrg.Public && len(usr.Roles) == 0 {
Error(w, http.StatusForbidden, "This organization is private. To gain access, you must be explicitly added by an administrator.", s.Logger)
@@ -227,19 +244,6 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
return
}
- defaultOrgID := fmt.Sprintf("%d", defaultOrg.ID)
- // If a user was added via the API, they might not yet be a member of the default organization
- // Here we check to verify that they are a user in the default organization
- if !hasRoleInDefaultOrganization(usr, defaultOrgID) {
- usr.Roles = append(usr.Roles, chronograf.Role{
- Organization: defaultOrgID,
- Name: defaultOrg.DefaultRole,
- })
- if err := s.Store.Users(serverCtx).Update(serverCtx, usr); err != nil {
- unknownErrorWithMessage(w, err, s.Logger)
- return
- }
- }
orgs, err := s.usersOrganizations(serverCtx, usr)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
@@ -316,10 +320,21 @@ func (s *Service) firstUser() bool {
return numUsers == 0
}
func (s *Service) newUsersAreSuperAdmin() bool {
+ // It's not necessary to enforce that the first user is superAdmin here, since
+ // superAdminNewUsers defaults to true, but there's nothing else in the
+ // application that dictates that it must be true.
+ // So for that reason, we kept this here for now. We've discussed the
+ // future possibility of allowing users to override default values via CLI and
+ // this case could possibly happen then.
if s.firstUser() {
return true
}
- return !s.SuperAdminFirstUserOnly
+ serverCtx := serverContext(context.Background())
+ cfg, err := s.Store.Config(serverCtx).Get(serverCtx)
+ if err != nil {
+ return false
+ }
+ return cfg.Auth.SuperAdminNewUsers
}
func (s *Service) usersOrganizations(ctx context.Context, u *chronograf.User) ([]chronograf.Organization, error) {
diff --git a/server/me_test.go b/server/me_test.go
index f92dc8a622..5fe2650d89 100644
--- a/server/me_test.go
+++ b/server/me_test.go
@@ -21,11 +21,11 @@ type MockUsers struct{}
func TestService_Me(t *testing.T) {
type fields struct {
- UsersStore chronograf.UsersStore
- OrganizationsStore chronograf.OrganizationsStore
- Logger chronograf.Logger
- UseAuth bool
- SuperAdminFirstUserOnly bool
+ UsersStore chronograf.UsersStore
+ OrganizationsStore chronograf.OrganizationsStore
+ ConfigStore chronograf.ConfigStore
+ Logger chronograf.Logger
+ UseAuth bool
}
type args struct {
w *httptest.ResponseRecorder
@@ -47,9 +47,15 @@ func TestService_Me(t *testing.T) {
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
- UseAuth: true,
- SuperAdminFirstUserOnly: true,
- Logger: log.New(log.DebugLevel),
+ UseAuth: true,
+ Logger: log.New(log.DebugLevel),
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
@@ -107,7 +113,138 @@ func TestService_Me(t *testing.T) {
wantBody: `{"code":403,"message":"This organization is private. To gain access, you must be explicitly added by an administrator."}`,
},
{
- name: "Existing user",
+ name: "Existing user - private default org and user is a super admin",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest("GET", "http://example.com/foo", nil),
+ },
+ fields: fields{
+ UseAuth: true,
+ Logger: log.New(log.DebugLevel),
+ OrganizationsStore: &mocks.OrganizationsStore{
+ DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
+ return &chronograf.Organization{
+ ID: 0,
+ Name: "Default",
+ DefaultRole: roles.ViewerRoleName,
+ Public: false,
+ }, nil
+ },
+ GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
+ switch *q.ID {
+ case 0:
+ return &chronograf.Organization{
+ ID: 0,
+ Name: "Default",
+ DefaultRole: roles.ViewerRoleName,
+ Public: true,
+ }, nil
+ case 1:
+ return &chronograf.Organization{
+ ID: 1,
+ Name: "The Bad Place",
+ Public: true,
+ }, nil
+ }
+ return nil, nil
+ },
+ },
+ UsersStore: &mocks.UsersStore{
+ NumF: func(ctx context.Context) (int, error) {
+ // This function gets to verify that there is at least one first user
+ return 1, nil
+ },
+ GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
+ if q.Name == nil || q.Provider == nil || q.Scheme == nil {
+ return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme")
+ }
+ return &chronograf.User{
+ Name: "me",
+ Provider: "github",
+ Scheme: "oauth2",
+ SuperAdmin: true,
+ }, nil
+ },
+ UpdateF: func(ctx context.Context, u *chronograf.User) error {
+ return nil
+ },
+ },
+ },
+ principal: oauth2.Principal{
+ Subject: "me",
+ Issuer: "github",
+ },
+ wantStatus: http.StatusOK,
+ wantContentType: "application/json",
+ wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
+ },
+ {
+ name: "Existing user - private default org",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest("GET", "http://example.com/foo", nil),
+ },
+ fields: fields{
+ UseAuth: true,
+ Logger: log.New(log.DebugLevel),
+ OrganizationsStore: &mocks.OrganizationsStore{
+ DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
+ return &chronograf.Organization{
+ ID: 0,
+ Name: "Default",
+ DefaultRole: roles.ViewerRoleName,
+ Public: false,
+ }, nil
+ },
+ GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
+ switch *q.ID {
+ case 0:
+ return &chronograf.Organization{
+ ID: 0,
+ Name: "Default",
+ DefaultRole: roles.ViewerRoleName,
+ Public: true,
+ }, nil
+ case 1:
+ return &chronograf.Organization{
+ ID: 1,
+ Name: "The Bad Place",
+ Public: true,
+ }, nil
+ }
+ return nil, nil
+ },
+ },
+ UsersStore: &mocks.UsersStore{
+ NumF: func(ctx context.Context) (int, error) {
+ // This function gets to verify that there is at least one first user
+ return 1, nil
+ },
+ GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
+ if q.Name == nil || q.Provider == nil || q.Scheme == nil {
+ return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme")
+ }
+ return &chronograf.User{
+ Name: "me",
+ Provider: "github",
+ Scheme: "oauth2",
+ }, nil
+ },
+ UpdateF: func(ctx context.Context, u *chronograf.User) error {
+ return nil
+ },
+ },
+ },
+ principal: oauth2.Principal{
+ Subject: "me",
+ Issuer: "github",
+ },
+ wantStatus: http.StatusForbidden,
+ wantContentType: "application/json",
+ wantBody: `{"code":403,"message":"This organization is private. To gain access, you must be explicitly added by an administrator."}`,
+ },
+ {
+ name: "Existing user - default org public",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
@@ -169,8 +306,7 @@ func TestService_Me(t *testing.T) {
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
- wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","defaultRole":"viewer","name":"Default","public":true}}
-`,
+ wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
},
{
name: "Existing user - organization doesn't exist",
@@ -235,9 +371,15 @@ func TestService_Me(t *testing.T) {
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
- UseAuth: true,
- SuperAdminFirstUserOnly: false,
- Logger: log.New(log.DebugLevel),
+ UseAuth: true,
+ Logger: log.New(log.DebugLevel),
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: true,
+ },
+ },
+ },
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
@@ -291,9 +433,15 @@ func TestService_Me(t *testing.T) {
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
- UseAuth: true,
- SuperAdminFirstUserOnly: true,
- Logger: log.New(log.DebugLevel),
+ UseAuth: true,
+ Logger: log.New(log.DebugLevel),
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
@@ -347,9 +495,15 @@ func TestService_Me(t *testing.T) {
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
- UseAuth: true,
- SuperAdminFirstUserOnly: true,
- Logger: log.New(log.DebugLevel),
+ UseAuth: true,
+ Logger: log.New(log.DebugLevel),
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
@@ -403,8 +557,14 @@ func TestService_Me(t *testing.T) {
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
- UseAuth: true,
- SuperAdminFirstUserOnly: true,
+ UseAuth: true,
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
@@ -452,9 +612,15 @@ func TestService_Me(t *testing.T) {
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
- UseAuth: false,
- SuperAdminFirstUserOnly: true,
- Logger: log.New(log.DebugLevel),
+ UseAuth: false,
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
+ Logger: log.New(log.DebugLevel),
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
@@ -468,9 +634,15 @@ func TestService_Me(t *testing.T) {
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
- UseAuth: true,
- SuperAdminFirstUserOnly: true,
- Logger: log.New(log.DebugLevel),
+ UseAuth: true,
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
+ Logger: log.New(log.DebugLevel),
},
wantStatus: http.StatusUnprocessableEntity,
principal: oauth2.Principal{
@@ -531,10 +703,10 @@ func TestService_Me(t *testing.T) {
Store: &mocks.Store{
UsersStore: tt.fields.UsersStore,
OrganizationsStore: tt.fields.OrganizationsStore,
+ ConfigStore: tt.fields.ConfigStore,
},
- Logger: tt.fields.Logger,
- UseAuth: tt.fields.UseAuth,
- SuperAdminFirstUserOnly: tt.fields.SuperAdminFirstUserOnly,
+ Logger: tt.fields.Logger,
+ UseAuth: tt.fields.UseAuth,
}
s.Me(tt.args.w, tt.args.r)
@@ -652,7 +824,7 @@ func TestService_UpdateMe(t *testing.T) {
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
- wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"admin","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","public":true,"defaultRole":"admin"},{"id":"1337","name":"The ShillBillThrilliettas","public":true}],"currentOrganization":{"id":"1337","name":"The ShillBillThrilliettas","public":true}}`,
+ wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"admin","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"admin","public":true},{"id":"1337","name":"The ShillBillThrilliettas","public":true}],"currentOrganization":{"id":"1337","name":"The ShillBillThrilliettas","public":true}}`,
},
{
name: "Change the current User's organization",
@@ -727,7 +899,7 @@ func TestService_UpdateMe(t *testing.T) {
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
- wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"editor","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","public":true,"defaultRole":"editor"},{"id":"1337","name":"The ThrillShilliettos","public":false}],"currentOrganization":{"id":"1337","name":"The ThrillShilliettos","public":false}}`,
+ wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"editor","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"editor","public":true},{"id":"1337","name":"The ThrillShilliettos","public":false}],"currentOrganization":{"id":"1337","name":"The ThrillShilliettos","public":false}}`,
},
{
name: "Unable to find requested user in valid organization",
diff --git a/server/mux.go b/server/mux.go
index 2848fc5935..83b1f6fad5 100644
--- a/server/mux.go
+++ b/server/mux.go
@@ -237,6 +237,11 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.PUT("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", EnsureEditor(service.UpdateRetentionPolicy))
router.DELETE("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", EnsureEditor(service.DropRetentionPolicy))
+ // Global application config for Chronograf
+ router.GET("/chronograf/v1/config", EnsureSuperAdmin(service.Config))
+ router.GET("/chronograf/v1/config/:section", EnsureSuperAdmin(service.ConfigSection))
+ router.PUT("/chronograf/v1/config/:section", EnsureSuperAdmin(service.ReplaceConfigSection))
+
allRoutes := &AllRoutes{
Logger: opts.Logger,
StatusFeed: opts.StatusFeedURL,
diff --git a/server/routes.go b/server/routes.go
index 109601d6ec..497ed21e1d 100644
--- a/server/routes.go
+++ b/server/routes.go
@@ -36,6 +36,7 @@ type getRoutesResponse struct {
Sources string `json:"sources"` // Location of the sources endpoint
Me string `json:"me"` // Location of the me endpoint
Dashboards string `json:"dashboards"` // Location of the dashboards endpoint
+ Config getConfigLinksResponse `json:"config"` // Location of the config endpoint and its various sections
Auth []AuthRoute `json:"auth"` // Location of all auth routes.
Logout *string `json:"logout,omitempty"` // Location of the logout route for all auth routes
ExternalLinks getExternalLinksResponse `json:"external"` // All external links for the client to use
@@ -68,7 +69,11 @@ func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Me: "/chronograf/v1/me",
Mappings: "/chronograf/v1/mappings",
Dashboards: "/chronograf/v1/dashboards",
- Auth: make([]AuthRoute, len(a.AuthRoutes)), // We want to return at least an empty array, rather than null
+ Config: getConfigLinksResponse{
+ Self: "/chronograf/v1/config",
+ Auth: "/chronograf/v1/config/auth",
+ },
+ Auth: make([]AuthRoute, len(a.AuthRoutes)), // We want to return at least an empty array, rather than null
ExternalLinks: getExternalLinksResponse{
StatusFeed: &a.StatusFeed,
CustomLinks: customLinks,
diff --git a/server/routes_test.go b/server/routes_test.go
index f8aff689ff..bb4203f75f 100644
--- a/server/routes_test.go
+++ b/server/routes_test.go
@@ -29,7 +29,7 @@ func TestAllRoutes(t *testing.T) {
if err := json.Unmarshal(body, &routes); err != nil {
t.Error("TestAllRoutes not able to unmarshal JSON response")
}
- want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","auth":[],"external":{"statusFeed":""}}
+ want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""}}
`
if want != string(body) {
t.Errorf("TestAllRoutes\nwanted\n*%s*\ngot\n*%s*", want, string(body))
@@ -67,7 +67,7 @@ func TestAllRoutesWithAuth(t *testing.T) {
if err := json.Unmarshal(body, &routes); err != nil {
t.Error("TestAllRoutesWithAuth not able to unmarshal JSON response")
}
- want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""}}
+ want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""}}
`
if want != string(body) {
t.Errorf("TestAllRoutesWithAuth\nwanted\n*%s*\ngot\n*%s*", want, string(body))
@@ -100,7 +100,7 @@ func TestAllRoutesWithExternalLinks(t *testing.T) {
if err := json.Unmarshal(body, &routes); err != nil {
t.Error("TestAllRoutesWithExternalLinks not able to unmarshal JSON response")
}
- want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]}}
+ want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]}}
`
if want != string(body) {
t.Errorf("TestAllRoutesWithExternalLinks\nwanted\n*%s*\ngot\n*%s*", want, string(body))
diff --git a/server/server.go b/server/server.go
index 7687a4f872..0e4a60f50b 100644
--- a/server/server.go
+++ b/server/server.go
@@ -52,12 +52,11 @@ type Server struct {
NewSources string `long:"new-sources" description:"Config for adding a new InfluxDB source and Kapacitor server, in JSON as an array of objects, and surrounded by single quotes. E.g. --new-sources='[{\"influxdb\":{\"name\":\"Influx 1\",\"username\":\"user1\",\"password\":\"pass1\",\"url\":\"http://localhost:8086\",\"metaUrl\":\"http://metaurl.com\",\"type\":\"influx-enterprise\",\"insecureSkipVerify\":false,\"default\":true,\"telegraf\":\"telegraf\",\"sharedSecret\":\"cubeapples\"},\"kapacitor\":{\"name\":\"Kapa 1\",\"url\":\"http://localhost:9092\",\"active\":true}}]'" env:"NEW_SOURCES" hidden:"true"`
- 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 (e.g. './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"`
- AuthDuration time.Duration `long:"auth-duration" default:"720h" description:"Total duration of cookie life for authentication (in hours). 0 means authentication expires on browser close." env:"AUTH_DURATION"`
- SuperAdminFirstUserOnly bool `long:"superadmin-first-user-only" description:"All new users will not be given the SuperAdmin status" env:"SUPERADMIN_FIRST_USER_ONLY"`
+ 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 (e.g. './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"`
+ AuthDuration time.Duration `long:"auth-duration" default:"720h" description:"Total duration of cookie life for authentication (in hours). 0 means authentication expires on browser close." env:"AUTH_DURATION"`
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"`
@@ -303,7 +302,6 @@ func (s *Server) Serve(ctx context.Context) error {
return err
}
service := openService(ctx, s.BoltPath, layoutBuilder, sourcesBuilder, kapacitorBuilder, logger, s.useAuth())
- service.SuperAdminFirstUserOnly = s.SuperAdminFirstUserOnly
if err := service.HandleNewSources(ctx, s.NewSources); err != nil {
logger.
WithField("component", "server").
@@ -441,7 +439,7 @@ func openService(ctx context.Context, boltPath string, lBuilder LayoutBuilder, s
OrganizationsStore: db.OrganizationsStore,
LayoutsStore: layouts,
DashboardsStore: db.DashboardsStore,
- //OrganizationUsersStore: organizations.NewUsersStore(db.UsersStore),
+ ConfigStore: db.ConfigStore,
},
Logger: logger,
UseAuth: useAuth,
diff --git a/server/service.go b/server/service.go
index 3b127835df..afe69e6dff 100644
--- a/server/service.go
+++ b/server/service.go
@@ -11,12 +11,11 @@ import (
// Service handles REST calls to the persistence
type Service struct {
- Store DataStore
- TimeSeriesClient TimeSeriesClient
- Logger chronograf.Logger
- UseAuth bool
- SuperAdminFirstUserOnly bool
- Databases chronograf.Databases
+ Store DataStore
+ TimeSeriesClient TimeSeriesClient
+ Logger chronograf.Logger
+ UseAuth bool
+ Databases chronograf.Databases
}
// TimeSeriesClient returns the correct client for a time series database.
diff --git a/server/stores.go b/server/stores.go
index 2c9d811b7b..7f9d8ac527 100644
--- a/server/stores.go
+++ b/server/stores.go
@@ -89,6 +89,7 @@ type DataStore interface {
Users(ctx context.Context) chronograf.UsersStore
Organizations(ctx context.Context) chronograf.OrganizationsStore
Dashboards(ctx context.Context) chronograf.DashboardsStore
+ Config(ctx context.Context) chronograf.ConfigStore
}
// ensure that Store implements a DataStore
@@ -102,6 +103,7 @@ type Store struct {
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
OrganizationsStore chronograf.OrganizationsStore
+ ConfigStore chronograf.ConfigStore
}
// Sources returns a noop.SourcesStore if the context has no organization specified
@@ -178,3 +180,14 @@ func (s *Store) Organizations(ctx context.Context) chronograf.OrganizationsStore
}
return &noop.OrganizationsStore{}
}
+
+// Config returns the underlying ConfigStore.
+func (s *Store) Config(ctx context.Context) chronograf.ConfigStore {
+ if isServer := hasServerContext(ctx); isServer {
+ return s.ConfigStore
+ }
+ if isSuperAdmin := hasSuperAdminContext(ctx); isSuperAdmin {
+ return s.ConfigStore
+ }
+ return &noop.ConfigStore{}
+}
diff --git a/server/users.go b/server/users.go
index ab2c13bc87..02d2d65739 100644
--- a/server/users.go
+++ b/server/users.go
@@ -157,6 +157,13 @@ func (s *Service) NewUser(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
+
+ cfg, err := s.Store.Config(ctx).Get(ctx)
+ if err != nil {
+ Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
+ return
+ }
+
user := &chronograf.User{
Name: req.Name,
Provider: req.Provider,
@@ -164,6 +171,10 @@ func (s *Service) NewUser(w http.ResponseWriter, r *http.Request) {
Roles: req.Roles,
}
+ if cfg.Auth.SuperAdminNewUsers {
+ req.SuperAdmin = true
+ }
+
if err := setSuperAdmin(ctx, req, user); err != nil {
Error(w, http.StatusUnauthorized, err.Error(), s.Logger)
return
diff --git a/server/users_test.go b/server/users_test.go
index 17155ecbec..8f9a26970e 100644
--- a/server/users_test.go
+++ b/server/users_test.go
@@ -112,8 +112,9 @@ func TestService_UserID(t *testing.T) {
func TestService_NewUser(t *testing.T) {
type fields struct {
- UsersStore chronograf.UsersStore
- Logger chronograf.Logger
+ UsersStore chronograf.UsersStore
+ ConfigStore chronograf.ConfigStore
+ Logger chronograf.Logger
}
type args struct {
w *httptest.ResponseRecorder
@@ -146,6 +147,13 @@ func TestService_NewUser(t *testing.T) {
},
fields: fields{
Logger: log.New(log.DebugLevel),
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
UsersStore: &mocks.UsersStore{
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
return &chronograf.User{
@@ -189,6 +197,13 @@ func TestService_NewUser(t *testing.T) {
},
fields: fields{
Logger: log.New(log.DebugLevel),
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
UsersStore: &mocks.UsersStore{
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
return &chronograf.User{
@@ -241,6 +256,13 @@ func TestService_NewUser(t *testing.T) {
},
fields: fields{
Logger: log.New(log.DebugLevel),
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
UsersStore: &mocks.UsersStore{
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
return &chronograf.User{
@@ -291,6 +313,13 @@ func TestService_NewUser(t *testing.T) {
},
fields: fields{
Logger: log.New(log.DebugLevel),
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
UsersStore: &mocks.UsersStore{
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
return &chronograf.User{
@@ -332,6 +361,13 @@ func TestService_NewUser(t *testing.T) {
},
fields: fields{
Logger: log.New(log.DebugLevel),
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: false,
+ },
+ },
+ },
UsersStore: &mocks.UsersStore{
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
return &chronograf.User{
@@ -349,13 +385,56 @@ func TestService_NewUser(t *testing.T) {
wantContentType: "application/json",
wantBody: `{"id":"1338","superAdmin":true,"name":"bob","provider":"github","scheme":"oauth2","roles":[],"links":{"self":"/chronograf/v1/users/1338"}}`,
},
+ {
+ name: "Create a new User with SuperAdminNewUsers: true in ConfigStore",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest(
+ "POST",
+ "http://any.url",
+ nil,
+ ),
+ user: &userRequest{
+ Name: "bob",
+ Provider: "github",
+ Scheme: "oauth2",
+ },
+ userKeyUser: &chronograf.User{
+ ID: 0,
+ Name: "coolUser",
+ Provider: "github",
+ Scheme: "oauth2",
+ SuperAdmin: true,
+ },
+ },
+ fields: fields{
+ Logger: log.New(log.DebugLevel),
+ ConfigStore: &mocks.ConfigStore{
+ Config: &chronograf.Config{
+ Auth: chronograf.AuthConfig{
+ SuperAdminNewUsers: true,
+ },
+ },
+ },
+ UsersStore: &mocks.UsersStore{
+ AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
+ user.ID = 1338
+ return user, nil
+ },
+ },
+ },
+ wantStatus: http.StatusCreated,
+ wantContentType: "application/json",
+ wantBody: `{"id":"1338","superAdmin":true,"name":"bob","provider":"github","scheme":"oauth2","roles":[],"links":{"self":"/chronograf/v1/users/1338"}}`,
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
Store: &mocks.Store{
- UsersStore: tt.fields.UsersStore,
+ UsersStore: tt.fields.UsersStore,
+ ConfigStore: tt.fields.ConfigStore,
},
Logger: tt.fields.Logger,
}
diff --git a/ui/spec/shared/presenters/presentersSpec.js b/ui/spec/shared/presenters/presentersSpec.js
index d751e9cd42..29c837490c 100644
--- a/ui/spec/shared/presenters/presentersSpec.js
+++ b/ui/spec/shared/presenters/presentersSpec.js
@@ -258,5 +258,12 @@ describe('Presenters', () => {
expect(actual).to.equal('m1.derivative_mean_usage_system')
})
+
+ it('returns a label of empty string if the query config is empty', () => {
+ const query = defaultQueryConfig({id: 1})
+ const actual = buildDefaultYLabel(query)
+
+ expect(actual).to.equal('')
+ })
})
})
diff --git a/ui/src/admin/components/chronograf/AdminTabs.js b/ui/src/admin/components/chronograf/AdminTabs.js
index 0f592e61cb..5c7bb145b1 100644
--- a/ui/src/admin/components/chronograf/AdminTabs.js
+++ b/ui/src/admin/components/chronograf/AdminTabs.js
@@ -71,6 +71,13 @@ const AdminTabs = ({
const {arrayOf, bool, func, shape, string} = PropTypes
+AdminTabs.defaultProps = {
+ organization: {
+ name: '',
+ id: '0',
+ },
+}
+
AdminTabs.propTypes = {
meRole: string.isRequired,
meID: string.isRequired,
diff --git a/ui/src/admin/components/chronograf/OrganizationsTable.js b/ui/src/admin/components/chronograf/OrganizationsTable.js
index 997c7470c8..272d574878 100644
--- a/ui/src/admin/components/chronograf/OrganizationsTable.js
+++ b/ui/src/admin/components/chronograf/OrganizationsTable.js
@@ -2,9 +2,12 @@ import React, {Component, PropTypes} from 'react'
import uuid from 'node-uuid'
+import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
+
import OrganizationsTableRow from 'src/admin/components/chronograf/OrganizationsTableRow'
import OrganizationsTableRowNew from 'src/admin/components/chronograf/OrganizationsTableRowNew'
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
+import SlideToggle from 'shared/components/SlideToggle'
import {PUBLIC_TOOLTIP} from 'src/admin/constants/index'
@@ -39,6 +42,8 @@ class OrganizationsTable extends Component {
onChooseDefaultRole,
onTogglePublic,
currentOrganization,
+ authConfig: {superAdminNewUsers},
+ onChangeAuthConfig,
} = this.props
const {isCreatingOrganization} = this.state
@@ -89,13 +94,35 @@ class OrganizationsTable extends Component {
currentOrganization={currentOrganization}
/>
)}
+
+
+
+
+
+
+
+ Config
+
+
+
+
+
+
+ All new users are SuperAdmins
+
-
false
, users cannot