Merge pull request #2355 from influxdata/multitenancy_all_users_superadmin_toggle

UI Toggle & API for SuperAdminFirstUserOnly server config
pull/2580/head
Jared Scheib 2017-12-14 10:54:18 -08:00 committed by GitHub
commit 456488f0ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1199 additions and 143 deletions

View File

@ -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
}

71
bolt/config.go Normal file
View File

@ -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
})
}

111
bolt/config_test.go Normal file
View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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,
}

View File

@ -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
//

View File

@ -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
}

28
mocks/config.go Normal file
View File

@ -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
}

View File

@ -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
}

26
noop/config.go Normal file
View File

@ -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")
}

123
server/config.go Normal file
View File

@ -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)
}

290
server/config_test.go Normal file
View File

@ -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)
}
})
}
}

View File

@ -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

View File

@ -320,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) {

View File

@ -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{
@ -365,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{
@ -421,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{
@ -477,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{
@ -533,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{
@ -582,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",
@ -598,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{
@ -661,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)

View File

@ -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,

View File

@ -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,

View File

@ -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))

View File

@ -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,

View File

@ -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.

View File

@ -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{}
}

View File

@ -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}
/>
)}
<Authorized requiredRole={SUPERADMIN_ROLE}>
<table className="table v-center superadmin-config">
<thead>
<tr>
<th style={{width: 70}}>Config</th>
<th />
</tr>
</thead>
<tbody>
<tr>
<td style={{width: 70}}>
<SlideToggle
size="xs"
active={superAdminNewUsers}
onToggle={onChangeAuthConfig('superAdminNewUsers')}
/>
</td>
<td>All new users are SuperAdmins</td>
</tr>
</tbody>
</table>
</Authorized>
</div>
</div>
)
}
}
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

View File

@ -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 (
<OrganizationsTable
organizations={organizations}
currentOrganization={currentOrganization}
onCreateOrg={this.handleCreateOrganization}
onDeleteOrg={this.handleDeleteOrganization}
onRenameOrg={this.handleRenameOrganization}
onTogglePublic={this.handleTogglePublic}
onChooseDefaultRole={this.handleChooseDefaultRole}
currentOrganization={currentOrganization}
authConfig={authConfig}
onChangeAuthConfig={this.handleUpdateAuthConfig}
/>
)
}
}
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),
})

View File

@ -3,6 +3,9 @@ import {isSameUser} from 'shared/reducers/helpers/auth'
const initialState = {
users: [],
organizations: [],
authConfig: {
superAdminNewUsers: true,
},
}
const adminChronograf = (state = initialState, action) => {

View File

@ -64,6 +64,7 @@ export const getMeAsync = ({shouldResetMe = false} = {}) => 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))

View File

@ -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))
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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;
}

View File

@ -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,
}
}