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
+
) } } -const {arrayOf, func, shape, string} = PropTypes +const {arrayOf, bool, func, shape, string} = PropTypes OrganizationsTable.propTypes = { organizations: arrayOf( @@ -113,5 +140,9 @@ OrganizationsTable.propTypes = { onRenameOrg: func.isRequired, onTogglePublic: func.isRequired, onChooseDefaultRole: func.isRequired, + onChangeAuthConfig: func.isRequired, + authConfig: shape({ + superAdminNewUsers: bool, + }), } export default OrganizationsTable diff --git a/ui/src/admin/components/chronograf/UsersTable.js b/ui/src/admin/components/chronograf/UsersTable.js index 60ced87ec7..18816f5ac9 100644 --- a/ui/src/admin/components/chronograf/UsersTable.js +++ b/ui/src/admin/components/chronograf/UsersTable.js @@ -135,4 +135,5 @@ UsersTable.propTypes = { meID: string.isRequired, notify: func.isRequired, } + export default UsersTable diff --git a/ui/src/admin/components/chronograf/UsersTableRowNew.js b/ui/src/admin/components/chronograf/UsersTableRowNew.js index 60aef76926..7b63c3e46c 100644 --- a/ui/src/admin/components/chronograf/UsersTableRowNew.js +++ b/ui/src/admin/components/chronograf/UsersTableRowNew.js @@ -3,11 +3,9 @@ import React, {Component, PropTypes} from 'react' import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized' import Dropdown from 'shared/components/Dropdown' -import SlideToggle from 'shared/components/SlideToggle' import {USERS_TABLE} from 'src/admin/constants/chronografTableSizing' import {USER_ROLES} from 'src/admin/constants/chronografAdmin' -import {MEMBER_ROLE} from 'src/auth/Authorized' class UsersTableRowNew extends Component { constructor(props) { @@ -17,8 +15,7 @@ class UsersTableRowNew extends Component { name: '', provider: '', scheme: 'oauth2', - role: MEMBER_ROLE, - superAdmin: false, + role: this.props.organization.defaultRole, } } @@ -55,10 +52,6 @@ class UsersTableRowNew extends Component { this.setState({role: newRole.text}) } - handleSelectSuperAdmin = superAdmin => { - this.setState({superAdmin}) - } - handleKeyDown = e => { const {name, provider} = this.state const preventCreate = !name || !provider @@ -87,7 +80,7 @@ class UsersTableRowNew extends Component { colActions, } = USERS_TABLE const {onBlur} = this.props - const {name, provider, scheme, role, superAdmin} = this.state + const {name, provider, scheme, role} = this.state const dropdownRolesItems = USER_ROLES.map(r => ({...r, text: r.name})) const preventCreate = !name || !provider @@ -117,11 +110,7 @@ class UsersTableRowNew extends Component { - + — diff --git a/ui/src/admin/constants/index.js b/ui/src/admin/constants/index.js index 244f1bac54..64cf2d4874 100644 --- a/ui/src/admin/constants/index.js +++ b/ui/src/admin/constants/index.js @@ -48,4 +48,4 @@ export const NEW_DEFAULT_DATABASE = { } export const PUBLIC_TOOLTIP = - 'If set to false, users cannot
authenticate unless an Admin explicitly
adds them to the organization.' + 'If turned off, new users cannot
authenticate unless an Admin explicitly
adds them to the organization.' diff --git a/ui/src/admin/containers/AdminChronografPage.js b/ui/src/admin/containers/AdminChronografPage.js index f5fa35be32..0ec6f51dd3 100644 --- a/ui/src/admin/containers/AdminChronografPage.js +++ b/ui/src/admin/containers/AdminChronografPage.js @@ -64,7 +64,18 @@ class AdminChronografPage extends Component { } render() { - const {users, meCurrentOrganization, meRole, meID, notify} = this.props + const { + users, + meRole, + meID, + notify, + organizations, + meCurrentOrganization, + } = this.props + + const organization = organizations.find( + o => o.id === meCurrentOrganization.id + ) return (
@@ -84,7 +95,7 @@ class AdminChronografPage extends Component { meID={meID} // UsersTable users={users} - organization={meCurrentOrganization} + organization={organization} onCreateUser={this.handleCreateUser} onUpdateUserRole={this.handleUpdateUserRole} onUpdateUserSuperAdmin={this.handleUpdateUserSuperAdmin} @@ -107,6 +118,7 @@ AdminChronografPage.propTypes = { users: string.isRequired, }), users: arrayOf(shape), + organizations: arrayOf(shape), meCurrentOrganization: shape({ id: string.isRequired, name: string.isRequired, @@ -128,18 +140,19 @@ AdminChronografPage.propTypes = { const mapStateToProps = ({ links, - adminChronograf: {users}, + adminChronograf: {users, organizations}, auth: { me, me: {currentOrganization: meCurrentOrganization, role: meRole, id: meID}, }, }) => ({ - links, - users, - meCurrentOrganization, - meRole, me, meID, + links, + users, + meRole, + organizations, + meCurrentOrganization, }) const mapDispatchToProps = dispatch => ({ diff --git a/ui/src/admin/containers/OrganizationsPage.js b/ui/src/admin/containers/OrganizationsPage.js index 9ae5c5a5dd..4c57c88fb6 100644 --- a/ui/src/admin/containers/OrganizationsPage.js +++ b/ui/src/admin/containers/OrganizationsPage.js @@ -3,30 +3,36 @@ import {connect} from 'react-redux' import {bindActionCreators} from 'redux' import * as adminChronografActionCreators from 'src/admin/actions/chronograf' +import * as configActionCreators from 'shared/actions/config' import {getMeAsync} from 'shared/actions/auth' import OrganizationsTable from 'src/admin/components/chronograf/OrganizationsTable' class OrganizationsPage extends Component { componentDidMount() { - const {links, actions: {loadOrganizationsAsync}} = this.props + const { + links, + actionsAdmin: {loadOrganizationsAsync}, + actionsConfig: {getAuthConfigAsync}, + } = this.props loadOrganizationsAsync(links.organizations) + getAuthConfigAsync(links.config.auth) } handleCreateOrganization = async organization => { - const {links, actions: {createOrganizationAsync}} = this.props + const {links, actionsAdmin: {createOrganizationAsync}} = this.props await createOrganizationAsync(links.organizations, organization) this.refreshMe() } handleRenameOrganization = async (organization, name) => { - const {actions: {updateOrganizationAsync}} = this.props + const {actionsAdmin: {updateOrganizationAsync}} = this.props await updateOrganizationAsync(organization, {...organization, name}) this.refreshMe() } handleDeleteOrganization = organization => { - const {actions: {deleteOrganizationAsync}} = this.props + const {actionsAdmin: {deleteOrganizationAsync}} = this.props deleteOrganizationAsync(organization) this.refreshMe() } @@ -37,7 +43,7 @@ class OrganizationsPage extends Component { } handleTogglePublic = organization => { - const {actions: {updateOrganizationAsync}} = this.props + const {actionsAdmin: {updateOrganizationAsync}} = this.props updateOrganizationAsync(organization, { ...organization, public: !organization.public, @@ -45,34 +51,52 @@ class OrganizationsPage extends Component { } handleChooseDefaultRole = (organization, defaultRole) => { - const {actions: {updateOrganizationAsync}} = this.props + const {actionsAdmin: {updateOrganizationAsync}} = this.props updateOrganizationAsync(organization, {...organization, defaultRole}) // refreshMe is here to update the org's defaultRole in `me.organizations` this.refreshMe() } + handleUpdateAuthConfig = fieldName => updatedValue => { + const { + actionsConfig: {updateAuthConfigAsync}, + authConfig, + links, + } = this.props + const updatedAuthConfig = { + ...authConfig, + [fieldName]: updatedValue, + } + updateAuthConfigAsync(links.config.auth, authConfig, updatedAuthConfig) + } + render() { - const {organizations, currentOrganization} = this.props + const {organizations, currentOrganization, authConfig} = this.props return ( ) } } -const {arrayOf, func, shape, string} = PropTypes +const {arrayOf, bool, func, shape, string} = PropTypes OrganizationsPage.propTypes = { links: shape({ organizations: string.isRequired, + config: shape({ + auth: string.isRequired, + }).isRequired, }), organizations: arrayOf( shape({ @@ -81,26 +105,39 @@ OrganizationsPage.propTypes = { link: string, }) ), - actions: shape({ + actionsAdmin: shape({ loadOrganizationsAsync: func.isRequired, createOrganizationAsync: func.isRequired, updateOrganizationAsync: func.isRequired, deleteOrganizationAsync: func.isRequired, }), + actionsConfig: shape({ + getAuthConfigAsync: func.isRequired, + updateAuthConfigAsync: func.isRequired, + }), getMe: func.isRequired, currentOrganization: shape({ name: string.isRequired, id: string.isRequired, }), + authConfig: shape({ + superAdminNewUsers: bool, + }), } -const mapStateToProps = ({links, adminChronograf: {organizations}}) => ({ +const mapStateToProps = ({ + links, + adminChronograf: {organizations}, + config: {auth: authConfig}, +}) => ({ links, organizations, + authConfig, }) const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(adminChronografActionCreators, dispatch), + actionsAdmin: bindActionCreators(adminChronografActionCreators, dispatch), + actionsConfig: bindActionCreators(configActionCreators, dispatch), getMe: bindActionCreators(getMeAsync, dispatch), }) diff --git a/ui/src/admin/reducers/chronograf.js b/ui/src/admin/reducers/chronograf.js index 34d46dcceb..621f7a7a4d 100644 --- a/ui/src/admin/reducers/chronograf.js +++ b/ui/src/admin/reducers/chronograf.js @@ -3,6 +3,9 @@ import {isSameUser} from 'shared/reducers/helpers/auth' const initialState = { users: [], organizations: [], + authConfig: { + superAdminNewUsers: true, + }, } const adminChronograf = (state = initialState, action) => { diff --git a/ui/src/dashboards/components/AxesOptions.js b/ui/src/dashboards/components/AxesOptions.js index 8cd5849e79..072f6d0b56 100644 --- a/ui/src/dashboards/components/AxesOptions.js +++ b/ui/src/dashboards/components/AxesOptions.js @@ -40,7 +40,7 @@ const AxesOptions = ({
async dispatch => { users, organizations, meLink, + config, } = await getMeAJAX() dispatch( meGetCompleted({ @@ -72,7 +73,9 @@ export const getMeAsync = ({shouldResetMe = false} = {}) => async dispatch => { logoutLink, }) ) - dispatch(linksReceived({external, users, organizations, me: meLink})) // TODO: put this before meGetCompleted... though for some reason it doesn't fire the first time then + dispatch( + linksReceived({external, users, organizations, me: meLink, config}) + ) // TODO: put this before meGetCompleted... though for some reason it doesn't fire the first time then } catch (error) { dispatch(meGetFailed()) dispatch(errorThrown(error)) diff --git a/ui/src/shared/actions/config.js b/ui/src/shared/actions/config.js new file mode 100644 index 0000000000..7d4bc4b13e --- /dev/null +++ b/ui/src/shared/actions/config.js @@ -0,0 +1,67 @@ +import { + getAuthConfig as getAuthConfigAJAX, + updateAuthConfig as updateAuthConfigAJAX, +} from 'shared/apis/config' + +import {errorThrown} from 'shared/actions/errors' + +export const getAuthConfigRequested = () => ({ + type: 'CHRONOGRAF_GET_AUTH_CONFIG_REQUESTED', +}) + +export const getAuthConfigCompleted = authConfig => ({ + type: 'CHRONOGRAF_GET_AUTH_CONFIG_COMPLETED', + payload: { + authConfig, + }, +}) + +export const getAuthConfigFailed = () => ({ + type: 'CHRONOGRAF_GET_AUTH_CONFIG_FAILED', +}) + +export const updateAuthConfigRequested = authConfig => ({ + type: 'CHRONOGRAF_UPDATE_AUTH_CONFIG_REQUESTED', + payload: { + authConfig, + }, +}) + +export const updateAuthConfigCompleted = () => ({ + type: 'CHRONOGRAF_UPDATE_AUTH_CONFIG_COMPLETED', +}) + +export const updateAuthConfigFailed = authConfig => ({ + type: 'CHRONOGRAF_UPDATE_AUTH_CONFIG_FAILED', + payload: { + authConfig, + }, +}) + +// async actions (thunks) +export const getAuthConfigAsync = url => async dispatch => { + dispatch(getAuthConfigRequested()) + try { + const {data} = await getAuthConfigAJAX(url) + dispatch(getAuthConfigCompleted(data)) // TODO: change authConfig in actions & reducers to reflect final shape + } catch (error) { + dispatch(errorThrown(error)) + dispatch(getAuthConfigFailed()) + } +} + +export const updateAuthConfigAsync = ( + url, + oldAuthConfig, + updatedAuthConfig +) => async dispatch => { + const newAuthConfig = {...oldAuthConfig, ...updatedAuthConfig} + dispatch(updateAuthConfigRequested(newAuthConfig)) + try { + await updateAuthConfigAJAX(url, newAuthConfig) + dispatch(updateAuthConfigCompleted()) + } catch (error) { + dispatch(errorThrown(error)) + dispatch(updateAuthConfigFailed(oldAuthConfig)) + } +} diff --git a/ui/src/shared/apis/config.js b/ui/src/shared/apis/config.js new file mode 100644 index 0000000000..1341784088 --- /dev/null +++ b/ui/src/shared/apis/config.js @@ -0,0 +1,26 @@ +import AJAX from 'src/utils/ajax' + +export const getAuthConfig = async url => { + try { + return await AJAX({ + method: 'GET', + url, + }) + } catch (error) { + console.error(error) + throw error + } +} + +export const updateAuthConfig = async (url, authConfig) => { + try { + return await AJAX({ + method: 'PUT', + url, + data: authConfig, + }) + } catch (error) { + console.error(error) + throw error + } +} diff --git a/ui/src/shared/components/SlideToggle.js b/ui/src/shared/components/SlideToggle.js index f917ec7f88..399fae2e0c 100644 --- a/ui/src/shared/components/SlideToggle.js +++ b/ui/src/shared/components/SlideToggle.js @@ -5,10 +5,14 @@ class SlideToggle extends Component { super(props) this.state = { - active: this.props.active, + active: props.active, } } + componentWillReceiveProps(nextProps) { + this.setState({active: nextProps.active}) + } + handleClick = () => { const {onToggle} = this.props diff --git a/ui/src/shared/parsing/getRangeForDygraph.js b/ui/src/shared/parsing/getRangeForDygraph.js index f3664899b5..adeab371d7 100644 --- a/ui/src/shared/parsing/getRangeForDygraph.js +++ b/ui/src/shared/parsing/getRangeForDygraph.js @@ -80,6 +80,11 @@ const getRange = ( } } + // prevents inversion of graph + if (min > max) { + return [min, min + 1] + } + return [min, max] } diff --git a/ui/src/shared/presenters/index.js b/ui/src/shared/presenters/index.js index 1c19b5e556..ccb096ba57 100644 --- a/ui/src/shared/presenters/index.js +++ b/ui/src/shared/presenters/index.js @@ -114,6 +114,11 @@ function getRolesForUser(roles, user) { export const buildDefaultYLabel = queryConfig => { const {measurement} = queryConfig const fields = _.get(queryConfig, ['fields', '0'], []) + const isEmpty = !measurement && !fields.length + + if (isEmpty) { + return '' + } const walkZerothArgs = f => { if (f.type === 'field') { diff --git a/ui/src/shared/reducers/config.js b/ui/src/shared/reducers/config.js new file mode 100644 index 0000000000..08a731a00d --- /dev/null +++ b/ui/src/shared/reducers/config.js @@ -0,0 +1,22 @@ +const initialState = { + links: {}, + auth: {}, +} + +const config = (state = initialState, action) => { + switch (action.type) { + case 'CHRONOGRAF_GET_AUTH_CONFIG_COMPLETED': + case 'CHRONOGRAF_UPDATE_AUTH_CONFIG_REQUESTED': + case 'CHRONOGRAF_UPDATE_AUTH_CONFIG_FAILED': { + const {authConfig: auth} = action.payload + return { + ...state, + auth: {...auth}, + } + } + } + + return state +} + +export default config diff --git a/ui/src/shared/reducers/index.js b/ui/src/shared/reducers/index.js index b8824ab25d..93b4126a75 100644 --- a/ui/src/shared/reducers/index.js +++ b/ui/src/shared/reducers/index.js @@ -1,5 +1,6 @@ import app from './app' import auth from './auth' +import config from './config' import errors from './errors' import links from './links' import {notifications, dismissedNotifications} from './notifications' @@ -8,6 +9,7 @@ import sources from './sources' export default { app, auth, + config, errors, links, notifications, diff --git a/ui/src/sources/components/SourceForm.js b/ui/src/sources/components/SourceForm.js index 444624bf7c..ba0c9cbaed 100644 --- a/ui/src/sources/components/SourceForm.js +++ b/ui/src/sources/components/SourceForm.js @@ -14,6 +14,7 @@ const SourceForm = ({ onInputChange, onBlurSourceURL, isUsingAuth, + gotoPurgatory, isInitialSource, me, }) => @@ -138,7 +139,7 @@ const SourceForm = ({
: null} -
+
+
+ {isUsingAuth + ? + : null}
@@ -179,6 +187,7 @@ SourceForm.propTypes = { }), isUsingAuth: bool, isInitialSource: bool, + gotoPurgatory: func, } const mapStateToProps = ({auth: {isUsingAuth, me}}) => ({isUsingAuth, me}) diff --git a/ui/src/sources/containers/SourcePage.js b/ui/src/sources/containers/SourcePage.js index 25d160abef..d6b470a073 100644 --- a/ui/src/sources/containers/SourcePage.js +++ b/ui/src/sources/containers/SourcePage.js @@ -100,6 +100,11 @@ class SourcePage extends Component { notify('error', `${bannerText}: ${error}`) } + gotoPurgatory = () => { + const {router} = this.props + router.push('/purgatory') + } + _normalizeSource({source}) { const url = source.url.trim() if (source.url.startsWith('http')) { @@ -224,6 +229,7 @@ class SourcePage extends Component { onSubmit={this.handleSubmit} onBlurSourceURL={this.handleBlurSourceURL} isInitialSource={isInitialSource} + gotoPurgatory={this.gotoPurgatory} />
diff --git a/ui/src/style/components/organizations-table.scss b/ui/src/style/components/organizations-table.scss index 80551bbc48..308bf8689c 100644 --- a/ui/src/style/components/organizations-table.scss +++ b/ui/src/style/components/organizations-table.scss @@ -163,3 +163,9 @@ input[type="text"].form-control.orgs-table--input { padding: 0 11px; } } + + +/* Config table beneath organizations table */ +.panel .panel-body table.table.superadmin-config { + margin-top: 60px; +} diff --git a/ui/src/style/components/react-tooltips.scss b/ui/src/style/components/react-tooltips.scss index 2c1e8540da..29f7eee495 100644 --- a/ui/src/style/components/react-tooltips.scss +++ b/ui/src/style/components/react-tooltips.scss @@ -26,6 +26,7 @@ $tooltip-code-color: $c-potassium; word-break: keep-all; border-radius: $tooltip-radius; text-transform: none; + text-align: left; cursor: default; &.type-dark { diff --git a/ui/src/style/layout/sidebar.scss b/ui/src/style/layout/sidebar.scss index 2286408c59..f9a3e529d7 100644 --- a/ui/src/style/layout/sidebar.scss +++ b/ui/src/style/layout/sidebar.scss @@ -293,51 +293,52 @@ span.icon.sidebar--icon.sidebar--icon__superadmin { .fancy-scroll--thumb-v { @include gradient-v($g20-white,$c-neutrino); } } -.sidebar-menu--section__custom-links { order: 0; } -.sidebar-menu--item__link-name { order: 1; } -.sidebar-menu--section__switch-orgs { order: 2; } -.sidebar-menu--scrollbar { order: 3; } -.sidebar-menu--section__account { order: 4; } -.sidebar-menu--provider { order: 5; } -.sidebar-menu--item__logout { order: 6; } -.sidebar-menu--heading { order: 7; } -.sidebar-menu--triangle { order: 8; } - -.sidebar-menu--section__custom-links:after { - display: none; - border-top-right-radius: $radius; -} .sidebar-menu--user-nav { top: initial; bottom: 0; + + .sidebar-menu--section__custom-links { order: 0; } + .sidebar-menu--item__link-name { order: 1; } + .sidebar-menu--section__switch-orgs { order: 2; } + .sidebar-menu--scrollbar { order: 3; } + .sidebar-menu--section__account { order: 4; } + .sidebar-menu--provider { order: 5; } + .sidebar-menu--item__logout { order: 6; } + .sidebar-menu--heading { order: 7; } + .sidebar-menu--triangle { order: 8; } + + .sidebar-menu--section__custom-links:after { + display: none; + border-top-right-radius: $radius; + } } @media only screen and (min-height: 800px) { - .sidebar-menu--heading { order: 0; } - .sidebar-menu--section__account { order: 1; } - .sidebar-menu--provider { order: 2; } - .sidebar-menu--item__logout { order: 3; } - .sidebar-menu--section__switch-orgs { order: 4; } - .sidebar-menu--scrollbar { order: 5; } - .sidebar-menu--section__custom-links { order: 6; } - .sidebar-menu--item__link-name { order: 7; } - .sidebar-menu--triangle { order: 8; } - - .sidebar-menu--section__custom-links:after { - display: initial; - border-top-right-radius: 0; - } - - .sidebar-menu--triangle { - width: 40px; - height: 40px; - top: $sidebar--width; - left: 0px; - transform: translate(-50%,-50%) rotate(45deg); - } - .sidebar-menu--user-nav { top: 0; bottom: initial; + + .sidebar-menu--heading { order: 0; } + .sidebar-menu--section__account { order: 1; } + .sidebar-menu--provider { order: 2; } + .sidebar-menu--item__logout { order: 3; } + .sidebar-menu--section__switch-orgs { order: 4; } + .sidebar-menu--scrollbar { order: 5; } + .sidebar-menu--section__custom-links { order: 6; } + .sidebar-menu--item__link-name { order: 7; } + .sidebar-menu--triangle { order: 8; } + + .sidebar-menu--section__custom-links:after { + display: initial; + border-top-right-radius: 0; + } + + .sidebar-menu--triangle { + width: 40px; + height: 40px; + top: $sidebar--width; + left: 0px; + transform: translate(-50%,-50%) rotate(45deg); + } } } \ No newline at end of file diff --git a/ui/src/style/pages/users.scss b/ui/src/style/pages/users.scss index 96b4800180..9878d4b7ea 100644 --- a/ui/src/style/pages/users.scss +++ b/ui/src/style/pages/users.scss @@ -104,11 +104,6 @@ table.table.chronograf-admin-table tbody tr.chronograf-admin-table--user td div. /* Styles for new user row */ table.table.chronograf-admin-table tbody tr.chronograf-admin-table--new-user { background-color: $g4-onyx; - - > td { - padding-top: 8px; - padding-bottom: 8px; - } } /* Highlight "Me" in the users table */ diff --git a/ui/src/utils/ajax.js b/ui/src/utils/ajax.js index 69ee8bf04d..232fa81255 100644 --- a/ui/src/utils/ajax.js +++ b/ui/src/utils/ajax.js @@ -10,7 +10,15 @@ const addBasepath = (url, excludeBasepath) => { } const generateResponseWithLinks = (response, newLinks) => { - const {auth, logout, external, users, organizations, me: meLink} = newLinks + const { + auth, + logout, + external, + users, + organizations, + me: meLink, + config, + } = newLinks return { ...response, auth: {links: auth}, @@ -19,6 +27,7 @@ const generateResponseWithLinks = (response, newLinks) => { users, organizations, meLink, + config, } }