Merge pull request #2355 from influxdata/multitenancy_all_users_superadmin_toggle
UI Toggle & API for SuperAdminFirstUserOnly server configpull/2580/head
commit
456488f0ac
|
@ -23,6 +23,7 @@ type Client struct {
|
||||||
DashboardsStore *DashboardsStore
|
DashboardsStore *DashboardsStore
|
||||||
UsersStore *UsersStore
|
UsersStore *UsersStore
|
||||||
OrganizationsStore *OrganizationsStore
|
OrganizationsStore *OrganizationsStore
|
||||||
|
ConfigStore *ConfigStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient initializes all stores
|
// NewClient initializes all stores
|
||||||
|
@ -40,6 +41,7 @@ func NewClient() *Client {
|
||||||
}
|
}
|
||||||
c.UsersStore = &UsersStore{client: c}
|
c.UsersStore = &UsersStore{client: c}
|
||||||
c.OrganizationsStore = &OrganizationsStore{client: c}
|
c.OrganizationsStore = &OrganizationsStore{client: c}
|
||||||
|
c.ConfigStore = &ConfigStore{client: c}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,6 +79,10 @@ func (c *Client) Open(ctx context.Context) error {
|
||||||
if _, err := tx.CreateBucketIfNotExists(UsersBucket); err != nil {
|
if _, err := tx.CreateBucketIfNotExists(UsersBucket); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Always create Config bucket.
|
||||||
|
if _, err := tx.CreateBucketIfNotExists(ConfigBucket); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -98,6 +104,9 @@ func (c *Client) Open(ctx context.Context) error {
|
||||||
if err := c.DashboardsStore.Migrate(ctx); err != nil {
|
if err := c.DashboardsStore.Migrate(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := c.ConfigStore.Migrate(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/gogo/protobuf/proto"
|
"github.com/gogo/protobuf/proto"
|
||||||
"github.com/influxdata/chronograf"
|
"github.com/influxdata/chronograf"
|
||||||
|
@ -591,3 +592,39 @@ func UnmarshalOrganizationPB(data []byte, o *Organization) error {
|
||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,8 @@ It has these top-level messages:
|
||||||
User
|
User
|
||||||
Role
|
Role
|
||||||
Organization
|
Organization
|
||||||
|
Config
|
||||||
|
AuthConfig
|
||||||
*/
|
*/
|
||||||
package internal
|
package internal
|
||||||
|
|
||||||
|
@ -389,6 +391,31 @@ func (m *Organization) String() string { return proto.CompactTextStri
|
||||||
func (*Organization) ProtoMessage() {}
|
func (*Organization) ProtoMessage() {}
|
||||||
func (*Organization) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{17} }
|
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() {
|
func init() {
|
||||||
proto.RegisterType((*Source)(nil), "internal.Source")
|
proto.RegisterType((*Source)(nil), "internal.Source")
|
||||||
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
|
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
|
||||||
|
@ -408,89 +435,94 @@ func init() {
|
||||||
proto.RegisterType((*User)(nil), "internal.User")
|
proto.RegisterType((*User)(nil), "internal.User")
|
||||||
proto.RegisterType((*Role)(nil), "internal.Role")
|
proto.RegisterType((*Role)(nil), "internal.Role")
|
||||||
proto.RegisterType((*Organization)(nil), "internal.Organization")
|
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) }
|
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
|
||||||
|
|
||||||
var fileDescriptorInternal = []byte{
|
var fileDescriptorInternal = []byte{
|
||||||
// 1264 bytes of a gzipped FileDescriptorProto
|
// 1310 bytes of a gzipped FileDescriptorProto
|
||||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x57, 0xdf, 0x8e, 0xdb, 0xc4,
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x57, 0x5f, 0x8f, 0xdb, 0x44,
|
||||||
0x17, 0x96, 0xe3, 0x38, 0xb1, 0x4f, 0xb6, 0xfd, 0x55, 0xf3, 0xab, 0xa8, 0x29, 0x12, 0x0a, 0x16,
|
0x10, 0x97, 0x63, 0x3b, 0xb1, 0x27, 0xd7, 0x52, 0x99, 0x8a, 0x9a, 0x22, 0xa1, 0x60, 0x81, 0x08,
|
||||||
0x88, 0x45, 0xd0, 0x05, 0xb5, 0x42, 0x42, 0x5c, 0x20, 0x65, 0x37, 0xa8, 0x2c, 0xfd, 0xb7, 0x9d,
|
0x82, 0x1e, 0xe8, 0x2a, 0x24, 0x84, 0x10, 0x52, 0xee, 0x82, 0xca, 0xd1, 0x7f, 0xd7, 0x4d, 0xaf,
|
||||||
0x74, 0xcb, 0x15, 0xaa, 0x26, 0xce, 0x49, 0x62, 0xd5, 0xb1, 0xcd, 0xd8, 0xde, 0x8d, 0x79, 0x18,
|
0x3c, 0xa1, 0x6a, 0xe3, 0x4c, 0x12, 0xab, 0x8e, 0x6d, 0xd6, 0xf6, 0x5d, 0xcc, 0x87, 0x41, 0x42,
|
||||||
0x24, 0x24, 0x9e, 0x00, 0x71, 0xcf, 0x2d, 0xe2, 0x96, 0x77, 0xe0, 0x15, 0xb8, 0x45, 0x67, 0x66,
|
0xe2, 0x13, 0x20, 0xde, 0x79, 0x45, 0xbc, 0xf2, 0x1d, 0xf8, 0x0a, 0xbc, 0xa2, 0xd9, 0x5d, 0x3b,
|
||||||
0xec, 0x38, 0x9b, 0x50, 0xf5, 0x02, 0x71, 0x37, 0xdf, 0x39, 0x93, 0x33, 0x67, 0xce, 0xf9, 0xce,
|
0xce, 0x25, 0x54, 0x7d, 0x40, 0xbc, 0xed, 0x6f, 0x66, 0x3d, 0x3b, 0x7f, 0x7e, 0x33, 0xbb, 0x86,
|
||||||
0x37, 0x0e, 0x5c, 0x8f, 0x92, 0x02, 0x65, 0x22, 0xe2, 0xa3, 0x4c, 0xa6, 0x45, 0xca, 0xdc, 0x1a,
|
0xeb, 0x51, 0x52, 0xa0, 0x48, 0x78, 0x7c, 0x98, 0x89, 0xb4, 0x48, 0x3d, 0xa7, 0xc6, 0xc1, 0x5f,
|
||||||
0x07, 0x7f, 0x76, 0xa0, 0x37, 0x49, 0x4b, 0x19, 0x22, 0xbb, 0x0e, 0x9d, 0xd3, 0xb1, 0x6f, 0x0d,
|
0x1d, 0xe8, 0x4e, 0xd2, 0x52, 0x84, 0xe8, 0x5d, 0x87, 0xce, 0xe9, 0xd8, 0x37, 0x06, 0xc6, 0xd0,
|
||||||
0xad, 0x43, 0x9b, 0x77, 0x4e, 0xc7, 0x8c, 0x41, 0xf7, 0xb1, 0x58, 0xa1, 0xdf, 0x19, 0x5a, 0x87,
|
0x64, 0x9d, 0xd3, 0xb1, 0xe7, 0x81, 0xf5, 0x88, 0xaf, 0xd0, 0xef, 0x0c, 0x8c, 0xa1, 0xcb, 0xe4,
|
||||||
0x1e, 0x57, 0x6b, 0xb2, 0x3d, 0xab, 0x32, 0xf4, 0x6d, 0x6d, 0xa3, 0x35, 0xbb, 0x0d, 0xee, 0x79,
|
0x9a, 0x64, 0x4f, 0xab, 0x0c, 0x7d, 0x53, 0xc9, 0x68, 0xed, 0xdd, 0x06, 0xe7, 0x3c, 0x27, 0x6b,
|
||||||
0x4e, 0xd1, 0x56, 0xe8, 0x77, 0x95, 0xbd, 0xc1, 0xe4, 0x3b, 0x13, 0x79, 0x7e, 0x99, 0xca, 0x99,
|
0x2b, 0xf4, 0x2d, 0x29, 0x6f, 0x30, 0xe9, 0xce, 0x78, 0x9e, 0x5f, 0xa6, 0x62, 0xe6, 0xdb, 0x4a,
|
||||||
0xef, 0x68, 0x5f, 0x8d, 0xd9, 0x0d, 0xb0, 0xcf, 0xf9, 0x43, 0xbf, 0xa7, 0xcc, 0xb4, 0x64, 0x3e,
|
0x57, 0x63, 0xef, 0x06, 0x98, 0xe7, 0xec, 0x81, 0xdf, 0x95, 0x62, 0x5a, 0x7a, 0x3e, 0xf4, 0xc6,
|
||||||
0xf4, 0xc7, 0x38, 0x17, 0x65, 0x5c, 0xf8, 0xfd, 0xa1, 0x75, 0xe8, 0xf2, 0x1a, 0x52, 0x9c, 0x67,
|
0x38, 0xe7, 0x65, 0x5c, 0xf8, 0xbd, 0x81, 0x31, 0x74, 0x58, 0x0d, 0xc9, 0xce, 0x53, 0x8c, 0x71,
|
||||||
0x18, 0xe3, 0x42, 0x8a, 0xb9, 0xef, 0xea, 0x38, 0x35, 0x66, 0x47, 0xc0, 0x4e, 0x93, 0x1c, 0xc3,
|
0x21, 0xf8, 0xdc, 0x77, 0x94, 0x9d, 0x1a, 0x7b, 0x87, 0xe0, 0x9d, 0x26, 0x39, 0x86, 0xa5, 0xc0,
|
||||||
0x52, 0xe2, 0xe4, 0x65, 0x94, 0x3d, 0x47, 0x19, 0xcd, 0x2b, 0xdf, 0x53, 0x01, 0xf6, 0x78, 0xe8,
|
0xc9, 0x8b, 0x28, 0x7b, 0x86, 0x22, 0x9a, 0x57, 0xbe, 0x2b, 0x0d, 0xec, 0xd1, 0xd0, 0x29, 0x0f,
|
||||||
0x94, 0x47, 0x58, 0x08, 0x3a, 0x1b, 0x54, 0xa8, 0x1a, 0xb2, 0x00, 0x0e, 0x26, 0x4b, 0x21, 0x71,
|
0xb1, 0xe0, 0x74, 0x36, 0x48, 0x53, 0x35, 0xf4, 0x02, 0x38, 0x98, 0x2c, 0xb9, 0xc0, 0xd9, 0x04,
|
||||||
0x36, 0xc1, 0x50, 0x62, 0xe1, 0x0f, 0x94, 0x7b, 0xcb, 0x46, 0x7b, 0x9e, 0xc8, 0x85, 0x48, 0xa2,
|
0x43, 0x81, 0x85, 0xdf, 0x97, 0xea, 0x2d, 0x19, 0xed, 0x79, 0x2c, 0x16, 0x3c, 0x89, 0x7e, 0xe0,
|
||||||
0xef, 0x45, 0x11, 0xa5, 0x89, 0x7f, 0xa0, 0xf7, 0xb4, 0x6d, 0x54, 0x25, 0x9e, 0xc6, 0xe8, 0x5f,
|
0x45, 0x94, 0x26, 0xfe, 0x81, 0xda, 0xd3, 0x96, 0x51, 0x96, 0x58, 0x1a, 0xa3, 0x7f, 0x4d, 0x65,
|
||||||
0xd3, 0x55, 0xa2, 0x75, 0xf0, 0x8b, 0x05, 0xde, 0x58, 0xe4, 0xcb, 0x69, 0x2a, 0xe4, 0xec, 0xb5,
|
0x89, 0xd6, 0xc1, 0xaf, 0x06, 0xb8, 0x63, 0x9e, 0x2f, 0xa7, 0x29, 0x17, 0xb3, 0x57, 0xca, 0xf5,
|
||||||
0x6a, 0x7d, 0x07, 0x9c, 0x10, 0xe3, 0x38, 0xf7, 0xed, 0xa1, 0x7d, 0x38, 0xb8, 0x7b, 0xeb, 0xa8,
|
0x1d, 0xb0, 0x43, 0x8c, 0xe3, 0xdc, 0x37, 0x07, 0xe6, 0xb0, 0x7f, 0x74, 0xeb, 0xb0, 0x29, 0x62,
|
||||||
0x69, 0x62, 0x13, 0xe7, 0x04, 0xe3, 0x98, 0xeb, 0x5d, 0xec, 0x13, 0xf0, 0x0a, 0x5c, 0x65, 0xb1,
|
0x63, 0xe7, 0x04, 0xe3, 0x98, 0xa9, 0x5d, 0xde, 0x27, 0xe0, 0x16, 0xb8, 0xca, 0x62, 0x5e, 0x60,
|
||||||
0x28, 0x30, 0xf7, 0xbb, 0xea, 0x27, 0x6c, 0xf3, 0x93, 0x67, 0xc6, 0xc5, 0x37, 0x9b, 0x76, 0xae,
|
0xee, 0x5b, 0xf2, 0x13, 0x6f, 0xf3, 0xc9, 0x53, 0xad, 0x62, 0x9b, 0x4d, 0x3b, 0xa1, 0xd8, 0xbb,
|
||||||
0xe2, 0xec, 0x5e, 0x25, 0xf8, 0xa3, 0x03, 0xd7, 0xb6, 0x8e, 0x63, 0x07, 0x60, 0xad, 0x55, 0xe6,
|
0xa1, 0x04, 0x7f, 0x76, 0xe0, 0xda, 0xd6, 0x71, 0xde, 0x01, 0x18, 0x6b, 0xe9, 0xb9, 0xcd, 0x8c,
|
||||||
0x0e, 0xb7, 0xd6, 0x84, 0x2a, 0x95, 0xb5, 0xc3, 0xad, 0x8a, 0xd0, 0xa5, 0xe2, 0x86, 0xc3, 0xad,
|
0x35, 0xa1, 0x4a, 0x7a, 0x6d, 0x33, 0xa3, 0x22, 0x74, 0x29, 0xb9, 0x61, 0x33, 0xe3, 0x92, 0xd0,
|
||||||
0x4b, 0x42, 0x4b, 0xc5, 0x08, 0x87, 0x5b, 0x4b, 0xf6, 0x01, 0xf4, 0xbf, 0x2b, 0x51, 0x46, 0x98,
|
0x52, 0x32, 0xc2, 0x66, 0xc6, 0xd2, 0xfb, 0x00, 0x7a, 0xdf, 0x97, 0x28, 0x22, 0xcc, 0x7d, 0x5b,
|
||||||
0xfb, 0x8e, 0xca, 0xee, 0x7f, 0x9b, 0xec, 0x9e, 0x96, 0x28, 0x2b, 0x5e, 0xfb, 0xa9, 0x1a, 0x8a,
|
0x7a, 0xf7, 0xda, 0xc6, 0xbb, 0x27, 0x25, 0x8a, 0x8a, 0xd5, 0x7a, 0xca, 0x86, 0x64, 0x93, 0xa2,
|
||||||
0x4d, 0x9a, 0x1a, 0x6a, 0x4d, 0xb6, 0x82, 0x98, 0xd7, 0xd7, 0x36, 0x5a, 0x9b, 0x2a, 0x6a, 0x3e,
|
0x86, 0x5c, 0x93, 0xac, 0x20, 0xe6, 0xf5, 0x94, 0x8c, 0xd6, 0x3a, 0x8b, 0x8a, 0x0f, 0x94, 0xc5,
|
||||||
0x50, 0x15, 0x3f, 0x85, 0xae, 0x58, 0x63, 0xee, 0x7b, 0x2a, 0xfe, 0x3b, 0xff, 0x50, 0xb0, 0xa3,
|
0x4f, 0xc1, 0xe2, 0x6b, 0xcc, 0x7d, 0x57, 0xda, 0x7f, 0xe7, 0x5f, 0x12, 0x76, 0x38, 0x5a, 0x63,
|
||||||
0xd1, 0x1a, 0xf3, 0x2f, 0x93, 0x42, 0x56, 0x5c, 0x6d, 0x67, 0xef, 0x43, 0x2f, 0x4c, 0xe3, 0x54,
|
0xfe, 0x55, 0x52, 0x88, 0x8a, 0xc9, 0xed, 0xde, 0xfb, 0xd0, 0x0d, 0xd3, 0x38, 0x15, 0xb9, 0x0f,
|
||||||
0xe6, 0x3e, 0x5c, 0x4d, 0xec, 0x84, 0xec, 0xdc, 0xb8, 0x6f, 0xdf, 0x07, 0xaf, 0xf9, 0x2d, 0xd1,
|
0x57, 0x1d, 0x3b, 0x21, 0x39, 0xd3, 0xea, 0xdb, 0xf7, 0xc0, 0x6d, 0xbe, 0x25, 0xfa, 0xbe, 0xc0,
|
||||||
0xf7, 0x25, 0x56, 0xaa, 0x12, 0x1e, 0xa7, 0x25, 0x7b, 0x17, 0x9c, 0x0b, 0x11, 0x97, 0xba, 0x8b,
|
0x4a, 0x66, 0xc2, 0x65, 0xb4, 0xf4, 0xde, 0x05, 0xfb, 0x82, 0xc7, 0xa5, 0xaa, 0x62, 0xff, 0xe8,
|
||||||
0x83, 0xbb, 0xd7, 0x37, 0x61, 0x46, 0xeb, 0x28, 0xe7, 0xda, 0xf9, 0x79, 0xe7, 0x33, 0x2b, 0x58,
|
0xfa, 0xc6, 0xcc, 0x68, 0x1d, 0xe5, 0x4c, 0x29, 0x3f, 0xef, 0x7c, 0x66, 0x04, 0x0b, 0xb0, 0xa5,
|
||||||
0x80, 0xa3, 0x22, 0xb7, 0x78, 0xe0, 0xd5, 0x3c, 0x50, 0xf3, 0xd5, 0x69, 0xcd, 0xd7, 0x0d, 0xb0,
|
0xe5, 0x16, 0x0f, 0xdc, 0x9a, 0x07, 0xb2, 0xbf, 0x3a, 0xad, 0xfe, 0xba, 0x01, 0xe6, 0xd7, 0xb8,
|
||||||
0xbf, 0xc2, 0xb5, 0x19, 0x39, 0x5a, 0x36, 0x6c, 0xe9, 0xb6, 0xd8, 0x72, 0x13, 0x9c, 0xe7, 0xea,
|
0xd6, 0x2d, 0x47, 0xcb, 0x86, 0x2d, 0x56, 0x8b, 0x2d, 0x37, 0xc1, 0x7e, 0x26, 0x0f, 0x57, 0x55,
|
||||||
0x70, 0xdd, 0x45, 0x0d, 0x82, 0x9f, 0x2d, 0xe8, 0xd2, 0xe1, 0xd4, 0xeb, 0x18, 0x17, 0x22, 0xac,
|
0x54, 0x20, 0xf8, 0xc5, 0x00, 0x8b, 0x0e, 0xa7, 0x5a, 0xc7, 0xb8, 0xe0, 0x61, 0x75, 0x9c, 0x96,
|
||||||
0x8e, 0xd3, 0x32, 0x99, 0xe5, 0xbe, 0x35, 0xb4, 0x0f, 0x6d, 0xbe, 0x65, 0x63, 0x6f, 0x40, 0x6f,
|
0xc9, 0x2c, 0xf7, 0x8d, 0x81, 0x39, 0x34, 0xd9, 0x96, 0xcc, 0x7b, 0x03, 0xba, 0x53, 0xa5, 0xed,
|
||||||
0xaa, 0xbd, 0x9d, 0xa1, 0x7d, 0xe8, 0x71, 0x83, 0x28, 0x74, 0x2c, 0xa6, 0x18, 0x9b, 0x14, 0x34,
|
0x0c, 0xcc, 0xa1, 0xcb, 0x34, 0x22, 0xd3, 0x31, 0x9f, 0x62, 0xac, 0x5d, 0x50, 0x80, 0x76, 0x67,
|
||||||
0xa0, 0xdd, 0x99, 0xc4, 0x79, 0xb4, 0x36, 0x69, 0x18, 0x44, 0xf6, 0xbc, 0x9c, 0x93, 0x5d, 0x67,
|
0x02, 0xe7, 0xd1, 0x5a, 0xbb, 0xa1, 0x11, 0xc9, 0xf3, 0x72, 0x4e, 0x72, 0xe5, 0x89, 0x46, 0xe4,
|
||||||
0x62, 0x10, 0x25, 0x3d, 0x15, 0x79, 0xd3, 0x54, 0x5a, 0x53, 0xe4, 0x3c, 0x14, 0x71, 0xdd, 0x55,
|
0xf4, 0x94, 0xe7, 0x4d, 0x51, 0x69, 0x4d, 0x96, 0xf3, 0x90, 0xc7, 0x75, 0x55, 0x15, 0x08, 0x7e,
|
||||||
0x0d, 0x82, 0x5f, 0x2d, 0x9a, 0x76, 0xcd, 0xd2, 0x9d, 0x0a, 0xbd, 0x09, 0x2e, 0x31, 0xf8, 0xc5,
|
0x33, 0xa8, 0xdb, 0x15, 0x4b, 0x77, 0x32, 0xf4, 0x26, 0x38, 0xc4, 0xe0, 0xe7, 0x17, 0x5c, 0xe8,
|
||||||
0x85, 0x90, 0xa6, 0x4a, 0x7d, 0xc2, 0xcf, 0x85, 0x64, 0x1f, 0x43, 0x4f, 0x95, 0x78, 0xcf, 0xc4,
|
0x2c, 0xf5, 0x08, 0x3f, 0xe3, 0xc2, 0xfb, 0x18, 0xba, 0x32, 0xc5, 0x7b, 0x3a, 0xa6, 0x36, 0x27,
|
||||||
0xd4, 0xe1, 0x54, 0x55, 0xb8, 0xd9, 0xd6, 0x70, 0xaa, 0xdb, 0xe2, 0x54, 0x73, 0x59, 0xa7, 0x7d,
|
0xb3, 0xc2, 0xf4, 0xb6, 0x86, 0x53, 0x56, 0x8b, 0x53, 0x4d, 0xb0, 0x76, 0x3b, 0xd8, 0x3b, 0x60,
|
||||||
0xd9, 0x3b, 0xe0, 0x10, 0x39, 0x2b, 0x95, 0xfd, 0xde, 0xc8, 0x9a, 0xc2, 0x7a, 0x57, 0x70, 0x0e,
|
0x13, 0x39, 0x2b, 0xe9, 0xfd, 0x5e, 0xcb, 0x8a, 0xc2, 0x6a, 0x57, 0x70, 0x0e, 0xd7, 0xb6, 0x4e,
|
||||||
0xd7, 0xb6, 0x4e, 0x6c, 0x4e, 0xb2, 0xb6, 0x4f, 0xda, 0xd0, 0xc5, 0x33, 0xf4, 0x20, 0xa5, 0xcb,
|
0x6c, 0x4e, 0x32, 0xb6, 0x4f, 0xda, 0xd0, 0xc5, 0xd5, 0xf4, 0xa0, 0x49, 0x97, 0x63, 0x8c, 0x61,
|
||||||
0x31, 0xc6, 0xb0, 0xc0, 0x99, 0xaa, 0xb7, 0xcb, 0x1b, 0x1c, 0xfc, 0x68, 0x6d, 0xe2, 0xaa, 0xf3,
|
0x81, 0x33, 0x99, 0x6f, 0x87, 0x35, 0x38, 0xf8, 0xc9, 0xd8, 0xd8, 0x95, 0xe7, 0xd1, 0x2c, 0x0b,
|
||||||
0x48, 0xcb, 0xc2, 0x74, 0xb5, 0x12, 0xc9, 0xcc, 0x84, 0xae, 0x21, 0xd5, 0x6d, 0x36, 0x35, 0xa1,
|
0xd3, 0xd5, 0x8a, 0x27, 0x33, 0x6d, 0xba, 0x86, 0x94, 0xb7, 0xd9, 0x54, 0x9b, 0xee, 0xcc, 0xa6,
|
||||||
0x3b, 0xb3, 0x29, 0x61, 0x99, 0x99, 0x0e, 0x76, 0x64, 0xc6, 0x86, 0x30, 0x58, 0xa1, 0xc8, 0x4b,
|
0x84, 0x45, 0xa6, 0x2b, 0xd8, 0x11, 0x99, 0x37, 0x80, 0xfe, 0x0a, 0x79, 0x5e, 0x0a, 0x5c, 0x61,
|
||||||
0x89, 0x2b, 0x4c, 0x0a, 0x53, 0x82, 0xb6, 0x89, 0xdd, 0x82, 0x7e, 0x21, 0x16, 0x2f, 0x88, 0xe4,
|
0x52, 0xe8, 0x14, 0xb4, 0x45, 0xde, 0x2d, 0xe8, 0x15, 0x7c, 0xf1, 0x9c, 0x48, 0xae, 0x2b, 0x59,
|
||||||
0xa6, 0x93, 0x85, 0x58, 0x3c, 0xc0, 0x8a, 0xbd, 0x05, 0xde, 0x3c, 0xc2, 0x78, 0xa6, 0x5c, 0xba,
|
0xf0, 0xc5, 0x7d, 0xac, 0xbc, 0xb7, 0xc0, 0x9d, 0x47, 0x18, 0xcf, 0xa4, 0x4a, 0x95, 0xd3, 0x91,
|
||||||
0x9d, 0xae, 0x32, 0x3c, 0xc0, 0x2a, 0xf8, 0xcd, 0x82, 0xde, 0x04, 0xe5, 0x05, 0xca, 0xd7, 0x12,
|
0x82, 0xfb, 0x58, 0x05, 0xbf, 0x1b, 0xd0, 0x9d, 0xa0, 0xb8, 0x40, 0xf1, 0x4a, 0x43, 0xae, 0x7d,
|
||||||
0xb9, 0xf6, 0xe3, 0x61, 0xbf, 0xe2, 0xf1, 0xe8, 0xee, 0x7f, 0x3c, 0x9c, 0xcd, 0xe3, 0x71, 0x13,
|
0x79, 0x98, 0x2f, 0xb9, 0x3c, 0xac, 0xfd, 0x97, 0x87, 0xbd, 0xb9, 0x3c, 0x6e, 0x82, 0x3d, 0x11,
|
||||||
0x9c, 0x89, 0x0c, 0x4f, 0xc7, 0x2a, 0x23, 0x9b, 0x6b, 0x40, 0x6c, 0x1c, 0x85, 0x45, 0x74, 0x81,
|
0xe1, 0xe9, 0x58, 0x7a, 0x64, 0x32, 0x05, 0x88, 0x8d, 0xa3, 0xb0, 0x88, 0x2e, 0x50, 0xdf, 0x28,
|
||||||
0xe6, 0x45, 0x31, 0x68, 0x47, 0xfb, 0xdc, 0x3d, 0xda, 0xf7, 0x83, 0x05, 0xbd, 0x87, 0xa2, 0x4a,
|
0x1a, 0xed, 0xcc, 0x3e, 0x67, 0xcf, 0xec, 0xfb, 0xd1, 0x80, 0xee, 0x03, 0x5e, 0xa5, 0x65, 0xb1,
|
||||||
0xcb, 0x62, 0x87, 0x85, 0x43, 0x18, 0x8c, 0xb2, 0x2c, 0x8e, 0x42, 0xfd, 0x6b, 0x7d, 0xa3, 0xb6,
|
0xc3, 0xc2, 0x01, 0xf4, 0x47, 0x59, 0x16, 0x47, 0xa1, 0xfa, 0x5a, 0x45, 0xd4, 0x16, 0xd1, 0x8e,
|
||||||
0x89, 0x76, 0x3c, 0x6a, 0xd5, 0x57, 0xdf, 0xad, 0x6d, 0x22, 0xb9, 0x38, 0x51, 0xfa, 0xae, 0xc5,
|
0x87, 0xad, 0xfc, 0xaa, 0xd8, 0xda, 0x22, 0x1a, 0x17, 0x27, 0x72, 0xbe, 0xab, 0x61, 0xdd, 0x1a,
|
||||||
0xba, 0x25, 0x17, 0x5a, 0xd6, 0x95, 0x93, 0x8a, 0x30, 0x2a, 0x8b, 0x74, 0x1e, 0xa7, 0x97, 0xea,
|
0x17, 0x6a, 0xac, 0x4b, 0x25, 0x25, 0x61, 0x54, 0x16, 0xe9, 0x3c, 0x4e, 0x2f, 0x65, 0xb4, 0x0e,
|
||||||
0xb6, 0x2e, 0x6f, 0x70, 0xf0, 0x7b, 0x07, 0xba, 0xff, 0x95, 0x26, 0x1f, 0x80, 0x15, 0x99, 0x66,
|
0x6b, 0x70, 0xf0, 0x47, 0x07, 0xac, 0xff, 0x6b, 0x26, 0x1f, 0x80, 0x11, 0xe9, 0x62, 0x1b, 0x51,
|
||||||
0x5b, 0x51, 0xa3, 0xd0, 0xfd, 0x96, 0x42, 0xfb, 0xd0, 0xaf, 0xa4, 0x48, 0x16, 0x98, 0xfb, 0xae,
|
0x33, 0xa1, 0x7b, 0xad, 0x09, 0xed, 0x43, 0xaf, 0x12, 0x3c, 0x59, 0x60, 0xee, 0x3b, 0x72, 0xba,
|
||||||
0x52, 0x97, 0x1a, 0x2a, 0x8f, 0x9a, 0x23, 0x2d, 0xcd, 0x1e, 0xaf, 0x61, 0x33, 0x17, 0xd0, 0x9a,
|
0xd4, 0x50, 0x6a, 0x64, 0x1f, 0xa9, 0xd1, 0xec, 0xb2, 0x1a, 0x36, 0x7d, 0x01, 0xad, 0xbe, 0xf8,
|
||||||
0x8b, 0x8f, 0x8c, 0x8a, 0x0f, 0x54, 0x46, 0xfe, 0x76, 0x59, 0xae, 0x8a, 0xf7, 0xbf, 0xa7, 0xc9,
|
0x48, 0x4f, 0xf1, 0xbe, 0xf4, 0xc8, 0xdf, 0x4e, 0xcb, 0xd5, 0xe1, 0xfd, 0xdf, 0xcd, 0xe4, 0xbf,
|
||||||
0x7f, 0x59, 0xe0, 0x34, 0x43, 0x75, 0xb2, 0x3d, 0x54, 0x27, 0x9b, 0xa1, 0x1a, 0x1f, 0xd7, 0x43,
|
0x0d, 0xb0, 0x9b, 0xa6, 0x3a, 0xd9, 0x6e, 0xaa, 0x93, 0x4d, 0x53, 0x8d, 0x8f, 0xeb, 0xa6, 0x1a,
|
||||||
0x35, 0x3e, 0x26, 0xcc, 0xcf, 0xea, 0xa1, 0xe2, 0x67, 0xd4, 0xac, 0xfb, 0x32, 0x2d, 0xb3, 0xe3,
|
0x1f, 0x13, 0x66, 0x67, 0x75, 0x53, 0xb1, 0x33, 0x2a, 0xd6, 0x3d, 0x91, 0x96, 0xd9, 0x71, 0xa5,
|
||||||
0x4a, 0x77, 0xd5, 0xe3, 0x0d, 0x26, 0x26, 0x7e, 0xb3, 0x44, 0x69, 0x4a, 0xed, 0x71, 0x83, 0x88,
|
0xaa, 0xea, 0xb2, 0x06, 0x13, 0x13, 0xbf, 0x5d, 0xa2, 0xd0, 0xa9, 0x76, 0x99, 0x46, 0xc4, 0xdb,
|
||||||
0xb7, 0x0f, 0x95, 0xe0, 0xe8, 0xe2, 0x6a, 0xc0, 0xde, 0x03, 0x87, 0x53, 0xf1, 0x54, 0x85, 0xb7,
|
0x07, 0x72, 0xe0, 0xa8, 0xe4, 0x2a, 0xe0, 0xbd, 0x07, 0x36, 0xa3, 0xe4, 0xc9, 0x0c, 0x6f, 0xd5,
|
||||||
0xfa, 0xa2, 0xcc, 0x5c, 0x7b, 0x29, 0xa8, 0xfe, 0x7a, 0x33, 0x04, 0xae, 0xbf, 0xe5, 0x3e, 0x84,
|
0x45, 0x8a, 0x99, 0xd2, 0x92, 0x51, 0xf5, 0x7a, 0xd3, 0x04, 0xae, 0xdf, 0x72, 0x1f, 0x42, 0x77,
|
||||||
0xde, 0x64, 0x19, 0xcd, 0x8b, 0xfa, 0x2d, 0xfc, 0x7f, 0x4b, 0xb0, 0xa2, 0x15, 0x2a, 0x1f, 0x37,
|
0xb2, 0x8c, 0xe6, 0x45, 0x7d, 0x17, 0xbe, 0xde, 0x1a, 0x58, 0xd1, 0x0a, 0xa5, 0x8e, 0xe9, 0x2d,
|
||||||
0x5b, 0x82, 0xa7, 0xe0, 0x35, 0xc6, 0x4d, 0x3a, 0x56, 0x3b, 0x1d, 0x06, 0xdd, 0xf3, 0x24, 0x2a,
|
0xc1, 0x13, 0x70, 0x1b, 0xe1, 0xc6, 0x1d, 0xa3, 0xed, 0x8e, 0x07, 0xd6, 0x79, 0x12, 0x15, 0x75,
|
||||||
0xea, 0xd1, 0xa5, 0x35, 0x5d, 0xf6, 0x69, 0x29, 0x92, 0x22, 0x2a, 0xaa, 0x7a, 0x74, 0x6b, 0x1c,
|
0xeb, 0xd2, 0x9a, 0x82, 0x7d, 0x52, 0xf2, 0xa4, 0x88, 0x8a, 0xaa, 0x6e, 0xdd, 0x1a, 0x07, 0x77,
|
||||||
0xdc, 0x33, 0xe9, 0x53, 0xb8, 0xf3, 0x2c, 0x43, 0x69, 0x64, 0x40, 0x03, 0x75, 0x48, 0x7a, 0x89,
|
0xb5, 0xfb, 0x64, 0xee, 0x3c, 0xcb, 0x50, 0xe8, 0x31, 0xa0, 0x80, 0x3c, 0x24, 0xbd, 0x44, 0x35,
|
||||||
0x5a, 0xc1, 0x6d, 0xae, 0x41, 0xf0, 0x2d, 0x78, 0xa3, 0x18, 0x65, 0xc1, 0xcb, 0x18, 0xf7, 0xbd,
|
0xc1, 0x4d, 0xa6, 0x40, 0xf0, 0x1d, 0xb8, 0xa3, 0x18, 0x45, 0xc1, 0xca, 0x18, 0xf7, 0xdd, 0x8c,
|
||||||
0x8c, 0x5f, 0x4f, 0x9e, 0x3c, 0xae, 0x33, 0xa0, 0xf5, 0x66, 0xe4, 0xed, 0x2b, 0x23, 0xff, 0x40,
|
0xdf, 0x4c, 0x1e, 0x3f, 0xaa, 0x3d, 0xa0, 0xf5, 0xa6, 0xe5, 0xcd, 0x2b, 0x2d, 0x7f, 0x9f, 0x67,
|
||||||
0x64, 0xe2, 0x74, 0xac, 0x78, 0x6e, 0x73, 0x83, 0x82, 0x9f, 0x2c, 0xe8, 0x92, 0xb6, 0xb4, 0x42,
|
0xfc, 0x74, 0x2c, 0x79, 0x6e, 0x32, 0x8d, 0x82, 0x9f, 0x0d, 0xb0, 0x68, 0xb6, 0xb4, 0x4c, 0x5b,
|
||||||
0x77, 0x5f, 0xa5, 0x4b, 0x67, 0x32, 0xbd, 0x88, 0x66, 0x28, 0xeb, 0xcb, 0xd5, 0x58, 0x15, 0x3d,
|
0x2f, 0x9b, 0x4b, 0x67, 0x22, 0xbd, 0x88, 0x66, 0x28, 0xea, 0xe0, 0x6a, 0x2c, 0x93, 0x1e, 0x2e,
|
||||||
0x5c, 0x62, 0xf3, 0x00, 0x1b, 0x44, 0x5c, 0xa3, 0x4f, 0xbd, 0x7a, 0x96, 0x5a, 0x5c, 0x23, 0x33,
|
0xb1, 0xb9, 0x80, 0x35, 0x22, 0xae, 0xd1, 0x53, 0xaf, 0xee, 0xa5, 0x16, 0xd7, 0x48, 0xcc, 0x94,
|
||||||
0xd7, 0x4e, 0xf6, 0x36, 0xc0, 0xa4, 0xcc, 0x50, 0x8e, 0x66, 0xab, 0x28, 0x51, 0x4d, 0x77, 0x79,
|
0xd2, 0x7b, 0x1b, 0x60, 0x52, 0x66, 0x28, 0x46, 0xb3, 0x55, 0x94, 0xc8, 0xa2, 0x3b, 0xac, 0x25,
|
||||||
0xcb, 0x12, 0x7c, 0xa1, 0x3f, 0x1e, 0x77, 0x14, 0xca, 0xda, 0xff, 0xa1, 0x79, 0x35, 0xf3, 0x20,
|
0x09, 0xbe, 0x54, 0x8f, 0xc7, 0x9d, 0x09, 0x65, 0xec, 0x7f, 0x68, 0x5e, 0xf5, 0x3c, 0x88, 0xb7,
|
||||||
0xde, 0xfe, 0xdd, 0x6b, 0xdd, 0x76, 0x08, 0x03, 0xf3, 0xa5, 0xad, 0xbe, 0x5b, 0x8d, 0x58, 0xb5,
|
0xbf, 0x7b, 0xa5, 0x68, 0x07, 0xd0, 0xd7, 0x2f, 0x6d, 0xf9, 0x6e, 0xd5, 0xc3, 0xaa, 0x25, 0xa2,
|
||||||
0x4c, 0x74, 0xe7, 0xb3, 0x72, 0x1a, 0x47, 0xa1, 0xba, 0xb3, 0xcb, 0x0d, 0x9a, 0xf6, 0xd4, 0x1f,
|
0x98, 0xcf, 0xca, 0x69, 0x1c, 0x85, 0x32, 0x66, 0x87, 0x69, 0x14, 0x1c, 0x41, 0xf7, 0x24, 0x4d,
|
||||||
0x8a, 0x7b, 0x7f, 0x07, 0x00, 0x00, 0xff, 0xff, 0xe0, 0xc4, 0x7a, 0x3e, 0x62, 0x0c, 0x00, 0x00,
|
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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,6 +164,14 @@ message Organization {
|
||||||
bool Public = 4; // Public specifies that users must be explicitly added to the 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
|
// The following is a vim modeline, it autoconfigures vim to have the
|
||||||
// appropriate tabbing and whitespace management to edit this file
|
// appropriate tabbing and whitespace management to edit this file
|
||||||
//
|
//
|
||||||
|
|
|
@ -26,6 +26,7 @@ const (
|
||||||
ErrOrganizationNotFound = Error("organization not found")
|
ErrOrganizationNotFound = Error("organization not found")
|
||||||
ErrOrganizationAlreadyExists = Error("organization already exists")
|
ErrOrganizationAlreadyExists = Error("organization already exists")
|
||||||
ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization")
|
ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization")
|
||||||
|
ErrConfigNotFound = Error("cannot find configuration")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error is a domain error encountered while processing chronograf requests
|
// Error is a domain error encountered while processing chronograf requests
|
||||||
|
@ -604,3 +605,30 @@ type OrganizationsStore interface {
|
||||||
// DefaultOrganization returns the DefaultOrganization
|
// DefaultOrganization returns the DefaultOrganization
|
||||||
DefaultOrganization(ctx context.Context) (*Organization, error)
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ type Store struct {
|
||||||
UsersStore chronograf.UsersStore
|
UsersStore chronograf.UsersStore
|
||||||
DashboardsStore chronograf.DashboardsStore
|
DashboardsStore chronograf.DashboardsStore
|
||||||
OrganizationsStore chronograf.OrganizationsStore
|
OrganizationsStore chronograf.OrganizationsStore
|
||||||
|
ConfigStore chronograf.ConfigStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) Sources(ctx context.Context) chronograf.SourcesStore {
|
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 {
|
func (s *Store) Dashboards(ctx context.Context) chronograf.DashboardsStore {
|
||||||
return s.DashboardsStore
|
return s.DashboardsStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) Config(ctx context.Context) chronograf.ConfigStore {
|
||||||
|
return s.ConfigStore
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,11 @@ import (
|
||||||
"net/url"
|
"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 {
|
type getExternalLinksResponse struct {
|
||||||
StatusFeed *string `json:"statusFeed,omitempty"` // Location of the a JSON Feed for client's Status page News Feed
|
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
|
CustomLinks []CustomLink `json:"custom,omitempty"` // Any custom external links for client's User menu
|
||||||
|
|
13
server/me.go
13
server/me.go
|
@ -320,10 +320,21 @@ func (s *Service) firstUser() bool {
|
||||||
return numUsers == 0
|
return numUsers == 0
|
||||||
}
|
}
|
||||||
func (s *Service) newUsersAreSuperAdmin() bool {
|
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() {
|
if s.firstUser() {
|
||||||
return true
|
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) {
|
func (s *Service) usersOrganizations(ctx context.Context, u *chronograf.User) ([]chronograf.Organization, error) {
|
||||||
|
|
|
@ -21,11 +21,11 @@ type MockUsers struct{}
|
||||||
|
|
||||||
func TestService_Me(t *testing.T) {
|
func TestService_Me(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
UsersStore chronograf.UsersStore
|
UsersStore chronograf.UsersStore
|
||||||
OrganizationsStore chronograf.OrganizationsStore
|
OrganizationsStore chronograf.OrganizationsStore
|
||||||
Logger chronograf.Logger
|
ConfigStore chronograf.ConfigStore
|
||||||
UseAuth bool
|
Logger chronograf.Logger
|
||||||
SuperAdminFirstUserOnly bool
|
UseAuth bool
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
w *httptest.ResponseRecorder
|
w *httptest.ResponseRecorder
|
||||||
|
@ -47,9 +47,15 @@ func TestService_Me(t *testing.T) {
|
||||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||||
},
|
},
|
||||||
fields: fields{
|
fields: fields{
|
||||||
UseAuth: true,
|
UseAuth: true,
|
||||||
SuperAdminFirstUserOnly: true,
|
Logger: log.New(log.DebugLevel),
|
||||||
Logger: log.New(log.DebugLevel),
|
ConfigStore: &mocks.ConfigStore{
|
||||||
|
Config: &chronograf.Config{
|
||||||
|
Auth: chronograf.AuthConfig{
|
||||||
|
SuperAdminNewUsers: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
OrganizationsStore: &mocks.OrganizationsStore{
|
OrganizationsStore: &mocks.OrganizationsStore{
|
||||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||||
return &chronograf.Organization{
|
return &chronograf.Organization{
|
||||||
|
@ -365,9 +371,15 @@ func TestService_Me(t *testing.T) {
|
||||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||||
},
|
},
|
||||||
fields: fields{
|
fields: fields{
|
||||||
UseAuth: true,
|
UseAuth: true,
|
||||||
SuperAdminFirstUserOnly: false,
|
Logger: log.New(log.DebugLevel),
|
||||||
Logger: log.New(log.DebugLevel),
|
ConfigStore: &mocks.ConfigStore{
|
||||||
|
Config: &chronograf.Config{
|
||||||
|
Auth: chronograf.AuthConfig{
|
||||||
|
SuperAdminNewUsers: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
OrganizationsStore: &mocks.OrganizationsStore{
|
OrganizationsStore: &mocks.OrganizationsStore{
|
||||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||||
return &chronograf.Organization{
|
return &chronograf.Organization{
|
||||||
|
@ -421,9 +433,15 @@ func TestService_Me(t *testing.T) {
|
||||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||||
},
|
},
|
||||||
fields: fields{
|
fields: fields{
|
||||||
UseAuth: true,
|
UseAuth: true,
|
||||||
SuperAdminFirstUserOnly: true,
|
Logger: log.New(log.DebugLevel),
|
||||||
Logger: log.New(log.DebugLevel),
|
ConfigStore: &mocks.ConfigStore{
|
||||||
|
Config: &chronograf.Config{
|
||||||
|
Auth: chronograf.AuthConfig{
|
||||||
|
SuperAdminNewUsers: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
OrganizationsStore: &mocks.OrganizationsStore{
|
OrganizationsStore: &mocks.OrganizationsStore{
|
||||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||||
return &chronograf.Organization{
|
return &chronograf.Organization{
|
||||||
|
@ -477,9 +495,15 @@ func TestService_Me(t *testing.T) {
|
||||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||||
},
|
},
|
||||||
fields: fields{
|
fields: fields{
|
||||||
UseAuth: true,
|
UseAuth: true,
|
||||||
SuperAdminFirstUserOnly: true,
|
Logger: log.New(log.DebugLevel),
|
||||||
Logger: log.New(log.DebugLevel),
|
ConfigStore: &mocks.ConfigStore{
|
||||||
|
Config: &chronograf.Config{
|
||||||
|
Auth: chronograf.AuthConfig{
|
||||||
|
SuperAdminNewUsers: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
OrganizationsStore: &mocks.OrganizationsStore{
|
OrganizationsStore: &mocks.OrganizationsStore{
|
||||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||||
return &chronograf.Organization{
|
return &chronograf.Organization{
|
||||||
|
@ -533,8 +557,14 @@ func TestService_Me(t *testing.T) {
|
||||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||||
},
|
},
|
||||||
fields: fields{
|
fields: fields{
|
||||||
UseAuth: true,
|
UseAuth: true,
|
||||||
SuperAdminFirstUserOnly: true,
|
ConfigStore: &mocks.ConfigStore{
|
||||||
|
Config: &chronograf.Config{
|
||||||
|
Auth: chronograf.AuthConfig{
|
||||||
|
SuperAdminNewUsers: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
OrganizationsStore: &mocks.OrganizationsStore{
|
OrganizationsStore: &mocks.OrganizationsStore{
|
||||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||||
return &chronograf.Organization{
|
return &chronograf.Organization{
|
||||||
|
@ -582,9 +612,15 @@ func TestService_Me(t *testing.T) {
|
||||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||||
},
|
},
|
||||||
fields: fields{
|
fields: fields{
|
||||||
UseAuth: false,
|
UseAuth: false,
|
||||||
SuperAdminFirstUserOnly: true,
|
ConfigStore: &mocks.ConfigStore{
|
||||||
Logger: log.New(log.DebugLevel),
|
Config: &chronograf.Config{
|
||||||
|
Auth: chronograf.AuthConfig{
|
||||||
|
SuperAdminNewUsers: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Logger: log.New(log.DebugLevel),
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusOK,
|
wantStatus: http.StatusOK,
|
||||||
wantContentType: "application/json",
|
wantContentType: "application/json",
|
||||||
|
@ -598,9 +634,15 @@ func TestService_Me(t *testing.T) {
|
||||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||||
},
|
},
|
||||||
fields: fields{
|
fields: fields{
|
||||||
UseAuth: true,
|
UseAuth: true,
|
||||||
SuperAdminFirstUserOnly: true,
|
ConfigStore: &mocks.ConfigStore{
|
||||||
Logger: log.New(log.DebugLevel),
|
Config: &chronograf.Config{
|
||||||
|
Auth: chronograf.AuthConfig{
|
||||||
|
SuperAdminNewUsers: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Logger: log.New(log.DebugLevel),
|
||||||
},
|
},
|
||||||
wantStatus: http.StatusUnprocessableEntity,
|
wantStatus: http.StatusUnprocessableEntity,
|
||||||
principal: oauth2.Principal{
|
principal: oauth2.Principal{
|
||||||
|
@ -661,10 +703,10 @@ func TestService_Me(t *testing.T) {
|
||||||
Store: &mocks.Store{
|
Store: &mocks.Store{
|
||||||
UsersStore: tt.fields.UsersStore,
|
UsersStore: tt.fields.UsersStore,
|
||||||
OrganizationsStore: tt.fields.OrganizationsStore,
|
OrganizationsStore: tt.fields.OrganizationsStore,
|
||||||
|
ConfigStore: tt.fields.ConfigStore,
|
||||||
},
|
},
|
||||||
Logger: tt.fields.Logger,
|
Logger: tt.fields.Logger,
|
||||||
UseAuth: tt.fields.UseAuth,
|
UseAuth: tt.fields.UseAuth,
|
||||||
SuperAdminFirstUserOnly: tt.fields.SuperAdminFirstUserOnly,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Me(tt.args.w, tt.args.r)
|
s.Me(tt.args.w, tt.args.r)
|
||||||
|
|
|
@ -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.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))
|
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{
|
allRoutes := &AllRoutes{
|
||||||
Logger: opts.Logger,
|
Logger: opts.Logger,
|
||||||
StatusFeed: opts.StatusFeedURL,
|
StatusFeed: opts.StatusFeedURL,
|
||||||
|
|
|
@ -36,6 +36,7 @@ type getRoutesResponse struct {
|
||||||
Sources string `json:"sources"` // Location of the sources endpoint
|
Sources string `json:"sources"` // Location of the sources endpoint
|
||||||
Me string `json:"me"` // Location of the me endpoint
|
Me string `json:"me"` // Location of the me endpoint
|
||||||
Dashboards string `json:"dashboards"` // Location of the dashboards 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.
|
Auth []AuthRoute `json:"auth"` // Location of all auth routes.
|
||||||
Logout *string `json:"logout,omitempty"` // Location of the logout route for 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
|
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",
|
Me: "/chronograf/v1/me",
|
||||||
Mappings: "/chronograf/v1/mappings",
|
Mappings: "/chronograf/v1/mappings",
|
||||||
Dashboards: "/chronograf/v1/dashboards",
|
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{
|
ExternalLinks: getExternalLinksResponse{
|
||||||
StatusFeed: &a.StatusFeed,
|
StatusFeed: &a.StatusFeed,
|
||||||
CustomLinks: customLinks,
|
CustomLinks: customLinks,
|
||||||
|
|
|
@ -29,7 +29,7 @@ func TestAllRoutes(t *testing.T) {
|
||||||
if err := json.Unmarshal(body, &routes); err != nil {
|
if err := json.Unmarshal(body, &routes); err != nil {
|
||||||
t.Error("TestAllRoutes not able to unmarshal JSON response")
|
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) {
|
if want != string(body) {
|
||||||
t.Errorf("TestAllRoutes\nwanted\n*%s*\ngot\n*%s*", 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 {
|
if err := json.Unmarshal(body, &routes); err != nil {
|
||||||
t.Error("TestAllRoutesWithAuth not able to unmarshal JSON response")
|
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) {
|
if want != string(body) {
|
||||||
t.Errorf("TestAllRoutesWithAuth\nwanted\n*%s*\ngot\n*%s*", 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 {
|
if err := json.Unmarshal(body, &routes); err != nil {
|
||||||
t.Error("TestAllRoutesWithExternalLinks not able to unmarshal JSON response")
|
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) {
|
if want != string(body) {
|
||||||
t.Errorf("TestAllRoutesWithExternalLinks\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
t.Errorf("TestAllRoutesWithExternalLinks\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||||
|
|
|
@ -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"`
|
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."`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
|
||||||
|
|
||||||
GithubClientID string `short:"i" long:"github-client-id" description:"Github Client ID for OAuth 2 support" env:"GH_CLIENT_ID"`
|
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"`
|
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
|
return err
|
||||||
}
|
}
|
||||||
service := openService(ctx, s.BoltPath, layoutBuilder, sourcesBuilder, kapacitorBuilder, logger, s.useAuth())
|
service := openService(ctx, s.BoltPath, layoutBuilder, sourcesBuilder, kapacitorBuilder, logger, s.useAuth())
|
||||||
service.SuperAdminFirstUserOnly = s.SuperAdminFirstUserOnly
|
|
||||||
if err := service.HandleNewSources(ctx, s.NewSources); err != nil {
|
if err := service.HandleNewSources(ctx, s.NewSources); err != nil {
|
||||||
logger.
|
logger.
|
||||||
WithField("component", "server").
|
WithField("component", "server").
|
||||||
|
@ -441,7 +439,7 @@ func openService(ctx context.Context, boltPath string, lBuilder LayoutBuilder, s
|
||||||
OrganizationsStore: db.OrganizationsStore,
|
OrganizationsStore: db.OrganizationsStore,
|
||||||
LayoutsStore: layouts,
|
LayoutsStore: layouts,
|
||||||
DashboardsStore: db.DashboardsStore,
|
DashboardsStore: db.DashboardsStore,
|
||||||
//OrganizationUsersStore: organizations.NewUsersStore(db.UsersStore),
|
ConfigStore: db.ConfigStore,
|
||||||
},
|
},
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
UseAuth: useAuth,
|
UseAuth: useAuth,
|
||||||
|
|
|
@ -11,12 +11,11 @@ import (
|
||||||
|
|
||||||
// Service handles REST calls to the persistence
|
// Service handles REST calls to the persistence
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Store DataStore
|
Store DataStore
|
||||||
TimeSeriesClient TimeSeriesClient
|
TimeSeriesClient TimeSeriesClient
|
||||||
Logger chronograf.Logger
|
Logger chronograf.Logger
|
||||||
UseAuth bool
|
UseAuth bool
|
||||||
SuperAdminFirstUserOnly bool
|
Databases chronograf.Databases
|
||||||
Databases chronograf.Databases
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TimeSeriesClient returns the correct client for a time series database.
|
// TimeSeriesClient returns the correct client for a time series database.
|
||||||
|
|
|
@ -89,6 +89,7 @@ type DataStore interface {
|
||||||
Users(ctx context.Context) chronograf.UsersStore
|
Users(ctx context.Context) chronograf.UsersStore
|
||||||
Organizations(ctx context.Context) chronograf.OrganizationsStore
|
Organizations(ctx context.Context) chronograf.OrganizationsStore
|
||||||
Dashboards(ctx context.Context) chronograf.DashboardsStore
|
Dashboards(ctx context.Context) chronograf.DashboardsStore
|
||||||
|
Config(ctx context.Context) chronograf.ConfigStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure that Store implements a DataStore
|
// ensure that Store implements a DataStore
|
||||||
|
@ -102,6 +103,7 @@ type Store struct {
|
||||||
UsersStore chronograf.UsersStore
|
UsersStore chronograf.UsersStore
|
||||||
DashboardsStore chronograf.DashboardsStore
|
DashboardsStore chronograf.DashboardsStore
|
||||||
OrganizationsStore chronograf.OrganizationsStore
|
OrganizationsStore chronograf.OrganizationsStore
|
||||||
|
ConfigStore chronograf.ConfigStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sources returns a noop.SourcesStore if the context has no organization specified
|
// 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{}
|
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{}
|
||||||
|
}
|
||||||
|
|
|
@ -2,9 +2,12 @@ import React, {Component, PropTypes} from 'react'
|
||||||
|
|
||||||
import uuid from 'node-uuid'
|
import uuid from 'node-uuid'
|
||||||
|
|
||||||
|
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
|
||||||
|
|
||||||
import OrganizationsTableRow from 'src/admin/components/chronograf/OrganizationsTableRow'
|
import OrganizationsTableRow from 'src/admin/components/chronograf/OrganizationsTableRow'
|
||||||
import OrganizationsTableRowNew from 'src/admin/components/chronograf/OrganizationsTableRowNew'
|
import OrganizationsTableRowNew from 'src/admin/components/chronograf/OrganizationsTableRowNew'
|
||||||
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
|
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
|
||||||
|
import SlideToggle from 'shared/components/SlideToggle'
|
||||||
|
|
||||||
import {PUBLIC_TOOLTIP} from 'src/admin/constants/index'
|
import {PUBLIC_TOOLTIP} from 'src/admin/constants/index'
|
||||||
|
|
||||||
|
@ -39,6 +42,8 @@ class OrganizationsTable extends Component {
|
||||||
onChooseDefaultRole,
|
onChooseDefaultRole,
|
||||||
onTogglePublic,
|
onTogglePublic,
|
||||||
currentOrganization,
|
currentOrganization,
|
||||||
|
authConfig: {superAdminNewUsers},
|
||||||
|
onChangeAuthConfig,
|
||||||
} = this.props
|
} = this.props
|
||||||
const {isCreatingOrganization} = this.state
|
const {isCreatingOrganization} = this.state
|
||||||
|
|
||||||
|
@ -89,13 +94,35 @@ class OrganizationsTable extends Component {
|
||||||
currentOrganization={currentOrganization}
|
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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {arrayOf, func, shape, string} = PropTypes
|
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||||
|
|
||||||
OrganizationsTable.propTypes = {
|
OrganizationsTable.propTypes = {
|
||||||
organizations: arrayOf(
|
organizations: arrayOf(
|
||||||
|
@ -113,5 +140,9 @@ OrganizationsTable.propTypes = {
|
||||||
onRenameOrg: func.isRequired,
|
onRenameOrg: func.isRequired,
|
||||||
onTogglePublic: func.isRequired,
|
onTogglePublic: func.isRequired,
|
||||||
onChooseDefaultRole: func.isRequired,
|
onChooseDefaultRole: func.isRequired,
|
||||||
|
onChangeAuthConfig: func.isRequired,
|
||||||
|
authConfig: shape({
|
||||||
|
superAdminNewUsers: bool,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
export default OrganizationsTable
|
export default OrganizationsTable
|
||||||
|
|
|
@ -3,30 +3,36 @@ import {connect} from 'react-redux'
|
||||||
import {bindActionCreators} from 'redux'
|
import {bindActionCreators} from 'redux'
|
||||||
|
|
||||||
import * as adminChronografActionCreators from 'src/admin/actions/chronograf'
|
import * as adminChronografActionCreators from 'src/admin/actions/chronograf'
|
||||||
|
import * as configActionCreators from 'shared/actions/config'
|
||||||
import {getMeAsync} from 'shared/actions/auth'
|
import {getMeAsync} from 'shared/actions/auth'
|
||||||
|
|
||||||
import OrganizationsTable from 'src/admin/components/chronograf/OrganizationsTable'
|
import OrganizationsTable from 'src/admin/components/chronograf/OrganizationsTable'
|
||||||
|
|
||||||
class OrganizationsPage extends Component {
|
class OrganizationsPage extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {links, actions: {loadOrganizationsAsync}} = this.props
|
const {
|
||||||
|
links,
|
||||||
|
actionsAdmin: {loadOrganizationsAsync},
|
||||||
|
actionsConfig: {getAuthConfigAsync},
|
||||||
|
} = this.props
|
||||||
loadOrganizationsAsync(links.organizations)
|
loadOrganizationsAsync(links.organizations)
|
||||||
|
getAuthConfigAsync(links.config.auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCreateOrganization = async organization => {
|
handleCreateOrganization = async organization => {
|
||||||
const {links, actions: {createOrganizationAsync}} = this.props
|
const {links, actionsAdmin: {createOrganizationAsync}} = this.props
|
||||||
await createOrganizationAsync(links.organizations, organization)
|
await createOrganizationAsync(links.organizations, organization)
|
||||||
this.refreshMe()
|
this.refreshMe()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRenameOrganization = async (organization, name) => {
|
handleRenameOrganization = async (organization, name) => {
|
||||||
const {actions: {updateOrganizationAsync}} = this.props
|
const {actionsAdmin: {updateOrganizationAsync}} = this.props
|
||||||
await updateOrganizationAsync(organization, {...organization, name})
|
await updateOrganizationAsync(organization, {...organization, name})
|
||||||
this.refreshMe()
|
this.refreshMe()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteOrganization = organization => {
|
handleDeleteOrganization = organization => {
|
||||||
const {actions: {deleteOrganizationAsync}} = this.props
|
const {actionsAdmin: {deleteOrganizationAsync}} = this.props
|
||||||
deleteOrganizationAsync(organization)
|
deleteOrganizationAsync(organization)
|
||||||
this.refreshMe()
|
this.refreshMe()
|
||||||
}
|
}
|
||||||
|
@ -37,7 +43,7 @@ class OrganizationsPage extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTogglePublic = organization => {
|
handleTogglePublic = organization => {
|
||||||
const {actions: {updateOrganizationAsync}} = this.props
|
const {actionsAdmin: {updateOrganizationAsync}} = this.props
|
||||||
updateOrganizationAsync(organization, {
|
updateOrganizationAsync(organization, {
|
||||||
...organization,
|
...organization,
|
||||||
public: !organization.public,
|
public: !organization.public,
|
||||||
|
@ -45,34 +51,52 @@ class OrganizationsPage extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChooseDefaultRole = (organization, defaultRole) => {
|
handleChooseDefaultRole = (organization, defaultRole) => {
|
||||||
const {actions: {updateOrganizationAsync}} = this.props
|
const {actionsAdmin: {updateOrganizationAsync}} = this.props
|
||||||
updateOrganizationAsync(organization, {...organization, defaultRole})
|
updateOrganizationAsync(organization, {...organization, defaultRole})
|
||||||
// refreshMe is here to update the org's defaultRole in `me.organizations`
|
// refreshMe is here to update the org's defaultRole in `me.organizations`
|
||||||
this.refreshMe()
|
this.refreshMe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleUpdateAuthConfig = fieldName => updatedValue => {
|
||||||
|
const {
|
||||||
|
actionsConfig: {updateAuthConfigAsync},
|
||||||
|
authConfig,
|
||||||
|
links,
|
||||||
|
} = this.props
|
||||||
|
const updatedAuthConfig = {
|
||||||
|
...authConfig,
|
||||||
|
[fieldName]: updatedValue,
|
||||||
|
}
|
||||||
|
updateAuthConfigAsync(links.config.auth, authConfig, updatedAuthConfig)
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {organizations, currentOrganization} = this.props
|
const {organizations, currentOrganization, authConfig} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OrganizationsTable
|
<OrganizationsTable
|
||||||
organizations={organizations}
|
organizations={organizations}
|
||||||
|
currentOrganization={currentOrganization}
|
||||||
onCreateOrg={this.handleCreateOrganization}
|
onCreateOrg={this.handleCreateOrganization}
|
||||||
onDeleteOrg={this.handleDeleteOrganization}
|
onDeleteOrg={this.handleDeleteOrganization}
|
||||||
onRenameOrg={this.handleRenameOrganization}
|
onRenameOrg={this.handleRenameOrganization}
|
||||||
onTogglePublic={this.handleTogglePublic}
|
onTogglePublic={this.handleTogglePublic}
|
||||||
onChooseDefaultRole={this.handleChooseDefaultRole}
|
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 = {
|
OrganizationsPage.propTypes = {
|
||||||
links: shape({
|
links: shape({
|
||||||
organizations: string.isRequired,
|
organizations: string.isRequired,
|
||||||
|
config: shape({
|
||||||
|
auth: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
}),
|
}),
|
||||||
organizations: arrayOf(
|
organizations: arrayOf(
|
||||||
shape({
|
shape({
|
||||||
|
@ -81,26 +105,39 @@ OrganizationsPage.propTypes = {
|
||||||
link: string,
|
link: string,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
actions: shape({
|
actionsAdmin: shape({
|
||||||
loadOrganizationsAsync: func.isRequired,
|
loadOrganizationsAsync: func.isRequired,
|
||||||
createOrganizationAsync: func.isRequired,
|
createOrganizationAsync: func.isRequired,
|
||||||
updateOrganizationAsync: func.isRequired,
|
updateOrganizationAsync: func.isRequired,
|
||||||
deleteOrganizationAsync: func.isRequired,
|
deleteOrganizationAsync: func.isRequired,
|
||||||
}),
|
}),
|
||||||
|
actionsConfig: shape({
|
||||||
|
getAuthConfigAsync: func.isRequired,
|
||||||
|
updateAuthConfigAsync: func.isRequired,
|
||||||
|
}),
|
||||||
getMe: func.isRequired,
|
getMe: func.isRequired,
|
||||||
currentOrganization: shape({
|
currentOrganization: shape({
|
||||||
name: string.isRequired,
|
name: string.isRequired,
|
||||||
id: string.isRequired,
|
id: string.isRequired,
|
||||||
}),
|
}),
|
||||||
|
authConfig: shape({
|
||||||
|
superAdminNewUsers: bool,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = ({links, adminChronograf: {organizations}}) => ({
|
const mapStateToProps = ({
|
||||||
|
links,
|
||||||
|
adminChronograf: {organizations},
|
||||||
|
config: {auth: authConfig},
|
||||||
|
}) => ({
|
||||||
links,
|
links,
|
||||||
organizations,
|
organizations,
|
||||||
|
authConfig,
|
||||||
})
|
})
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
actions: bindActionCreators(adminChronografActionCreators, dispatch),
|
actionsAdmin: bindActionCreators(adminChronografActionCreators, dispatch),
|
||||||
|
actionsConfig: bindActionCreators(configActionCreators, dispatch),
|
||||||
getMe: bindActionCreators(getMeAsync, dispatch),
|
getMe: bindActionCreators(getMeAsync, dispatch),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,9 @@ import {isSameUser} from 'shared/reducers/helpers/auth'
|
||||||
const initialState = {
|
const initialState = {
|
||||||
users: [],
|
users: [],
|
||||||
organizations: [],
|
organizations: [],
|
||||||
|
authConfig: {
|
||||||
|
superAdminNewUsers: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminChronograf = (state = initialState, action) => {
|
const adminChronograf = (state = initialState, action) => {
|
||||||
|
|
|
@ -64,6 +64,7 @@ export const getMeAsync = ({shouldResetMe = false} = {}) => async dispatch => {
|
||||||
users,
|
users,
|
||||||
organizations,
|
organizations,
|
||||||
meLink,
|
meLink,
|
||||||
|
config,
|
||||||
} = await getMeAJAX()
|
} = await getMeAJAX()
|
||||||
dispatch(
|
dispatch(
|
||||||
meGetCompleted({
|
meGetCompleted({
|
||||||
|
@ -72,7 +73,9 @@ export const getMeAsync = ({shouldResetMe = false} = {}) => async dispatch => {
|
||||||
logoutLink,
|
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) {
|
} catch (error) {
|
||||||
dispatch(meGetFailed())
|
dispatch(meGetFailed())
|
||||||
dispatch(errorThrown(error))
|
dispatch(errorThrown(error))
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,10 +5,14 @@ class SlideToggle extends Component {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
active: this.props.active,
|
active: props.active,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.setState({active: nextProps.active})
|
||||||
|
}
|
||||||
|
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
const {onToggle} = this.props
|
const {onToggle} = this.props
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -1,5 +1,6 @@
|
||||||
import app from './app'
|
import app from './app'
|
||||||
import auth from './auth'
|
import auth from './auth'
|
||||||
|
import config from './config'
|
||||||
import errors from './errors'
|
import errors from './errors'
|
||||||
import links from './links'
|
import links from './links'
|
||||||
import {notifications, dismissedNotifications} from './notifications'
|
import {notifications, dismissedNotifications} from './notifications'
|
||||||
|
@ -8,6 +9,7 @@ import sources from './sources'
|
||||||
export default {
|
export default {
|
||||||
app,
|
app,
|
||||||
auth,
|
auth,
|
||||||
|
config,
|
||||||
errors,
|
errors,
|
||||||
links,
|
links,
|
||||||
notifications,
|
notifications,
|
||||||
|
|
|
@ -163,3 +163,9 @@ input[type="text"].form-control.orgs-table--input {
|
||||||
padding: 0 11px;
|
padding: 0 11px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Config table beneath organizations table */
|
||||||
|
.panel .panel-body table.table.superadmin-config {
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,15 @@ const addBasepath = (url, excludeBasepath) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateResponseWithLinks = (response, newLinks) => {
|
const generateResponseWithLinks = (response, newLinks) => {
|
||||||
const {auth, logout, external, users, organizations, me: meLink} = newLinks
|
const {
|
||||||
|
auth,
|
||||||
|
logout,
|
||||||
|
external,
|
||||||
|
users,
|
||||||
|
organizations,
|
||||||
|
me: meLink,
|
||||||
|
config,
|
||||||
|
} = newLinks
|
||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
auth: {links: auth},
|
auth: {links: auth},
|
||||||
|
@ -19,6 +27,7 @@ const generateResponseWithLinks = (response, newLinks) => {
|
||||||
users,
|
users,
|
||||||
organizations,
|
organizations,
|
||||||
meLink,
|
meLink,
|
||||||
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue