Merge pull request #2582 from influxdata/1.4.0.x
Merge 1.4.0.x hot-fixes into masterpull/10616/head
commit
008a540b90
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -1,16 +1,10 @@
|
|||
## v1.4.1.0 [unreleased]
|
||||
|
||||
### Features
|
||||
### UI Improvements
|
||||
1. [#2502](https://github.com/influxdata/chronograf/pull/2502): Fix cursor flashing between default and pointer
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
## v1.4.0.0-beta2 [unreleased]
|
||||
### Features
|
||||
### UI Improvements
|
||||
1. [#2502](https://github.com/influxdata/chronograf/pull/2502): Fix cursor flashing between default and pointer
|
||||
### Bug Fixes
|
||||
1. [#2528](https://github.com/influxdata/chronograf/pull/2528): Fix template rendering to ignore template if not in query
|
||||
1. [#2563](https://github.com/influxdata/chronograf/pull/2563): Fix graph inversion if user input y-axis min greater than max
|
||||
|
||||
## v1.4.0.0-beta1 [2017-12-07]
|
||||
### Features
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
"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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
//
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -142,6 +142,20 @@ func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.U
|
|||
// and the user that was found in the underlying store
|
||||
usr.Roles = append(roles, u.Roles...)
|
||||
|
||||
// u.SuperAdmin == true is logically equivalent to u.SuperAdmin, however
|
||||
// it is more clear on a conceptual level to check equality
|
||||
//
|
||||
// TODO(desa): this should go away with https://github.com/influxdata/chronograf/issues/2207
|
||||
// I do not like checking super admin here. The organization users store should only be
|
||||
// concerned about organizations.
|
||||
//
|
||||
// If the user being added already existed in a previous organization, and was already a SuperAdmin,
|
||||
// then this ensures that they retain their SuperAdmin status. And if they weren't a SuperAdmin, and
|
||||
// the user being added has been granted SuperAdmin status, they will be promoted
|
||||
if u.SuperAdmin == true {
|
||||
usr.SuperAdmin = true
|
||||
}
|
||||
|
||||
// Update the user in the underlying store
|
||||
if err := s.store.Update(ctx, usr); err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -315,9 +315,62 @@ func TestUsersStore_Add(t *testing.T) {
|
|||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Organization: "1337",
|
||||
Name: "editor",
|
||||
Organization: "1336",
|
||||
Name: "admin",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Add non-new user with Role. Stored user is not super admin. Provided user is super admin",
|
||||
fields: fields{
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
|
||||
return u, nil
|
||||
},
|
||||
UpdateF: func(ctx context.Context, u *chronograf.User) error {
|
||||
return nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
ID: 1234,
|
||||
Name: "docbrown",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: false,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Organization: "1337",
|
||||
Name: "editor",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
u: &chronograf.User{
|
||||
ID: 1234,
|
||||
Name: "docbrown",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Organization: "1336",
|
||||
Name: "admin",
|
||||
},
|
||||
},
|
||||
},
|
||||
orgID: "1336",
|
||||
},
|
||||
want: &chronograf.User{
|
||||
Name: "docbrown",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Organization: "1336",
|
||||
Name: "admin",
|
||||
|
@ -503,6 +556,9 @@ func TestUsersStore_Add(t *testing.T) {
|
|||
if got == nil && tt.want == nil {
|
||||
continue
|
||||
}
|
||||
if diff := cmp.Diff(got, tt.want, userCmpOptions...); diff != "" {
|
||||
t.Errorf("%q. UsersStore.Add():\n-got/+want\ndiff %s", tt.name, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
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
|
||||
|
|
43
server/me.go
43
server/me.go
|
@ -206,6 +206,23 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if usr != nil {
|
||||
|
||||
if defaultOrg.Public || usr.SuperAdmin == true {
|
||||
// If the default organization is public, or the user is a super admin
|
||||
// they will always have a role in the default organization
|
||||
defaultOrgID := fmt.Sprintf("%d", defaultOrg.ID)
|
||||
if !hasRoleInDefaultOrganization(usr, defaultOrgID) {
|
||||
usr.Roles = append(usr.Roles, chronograf.Role{
|
||||
Organization: defaultOrgID,
|
||||
Name: defaultOrg.DefaultRole,
|
||||
})
|
||||
if err := s.Store.Users(serverCtx).Update(serverCtx, usr); err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the default org is private and the user has no roles, they should not have access
|
||||
if !defaultOrg.Public && len(usr.Roles) == 0 {
|
||||
Error(w, http.StatusForbidden, "This organization is private. To gain access, you must be explicitly added by an administrator.", s.Logger)
|
||||
|
@ -227,19 +244,6 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
defaultOrgID := fmt.Sprintf("%d", defaultOrg.ID)
|
||||
// If a user was added via the API, they might not yet be a member of the default organization
|
||||
// Here we check to verify that they are a user in the default organization
|
||||
if !hasRoleInDefaultOrganization(usr, defaultOrgID) {
|
||||
usr.Roles = append(usr.Roles, chronograf.Role{
|
||||
Organization: defaultOrgID,
|
||||
Name: defaultOrg.DefaultRole,
|
||||
})
|
||||
if err := s.Store.Users(serverCtx).Update(serverCtx, usr); err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
orgs, err := s.usersOrganizations(serverCtx, usr)
|
||||
if err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
|
@ -316,10 +320,21 @@ func (s *Service) firstUser() bool {
|
|||
return numUsers == 0
|
||||
}
|
||||
func (s *Service) newUsersAreSuperAdmin() bool {
|
||||
// It's not necessary to enforce that the first user is superAdmin here, since
|
||||
// superAdminNewUsers defaults to true, but there's nothing else in the
|
||||
// application that dictates that it must be true.
|
||||
// So for that reason, we kept this here for now. We've discussed the
|
||||
// future possibility of allowing users to override default values via CLI and
|
||||
// this case could possibly happen then.
|
||||
if s.firstUser() {
|
||||
return true
|
||||
}
|
||||
return !s.SuperAdminFirstUserOnly
|
||||
serverCtx := serverContext(context.Background())
|
||||
cfg, err := s.Store.Config(serverCtx).Get(serverCtx)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return cfg.Auth.SuperAdminNewUsers
|
||||
}
|
||||
|
||||
func (s *Service) usersOrganizations(ctx context.Context, u *chronograf.User) ([]chronograf.Organization, error) {
|
||||
|
|
|
@ -21,11 +21,11 @@ type MockUsers struct{}
|
|||
|
||||
func TestService_Me(t *testing.T) {
|
||||
type fields struct {
|
||||
UsersStore chronograf.UsersStore
|
||||
OrganizationsStore chronograf.OrganizationsStore
|
||||
Logger chronograf.Logger
|
||||
UseAuth bool
|
||||
SuperAdminFirstUserOnly bool
|
||||
UsersStore chronograf.UsersStore
|
||||
OrganizationsStore chronograf.OrganizationsStore
|
||||
ConfigStore chronograf.ConfigStore
|
||||
Logger chronograf.Logger
|
||||
UseAuth bool
|
||||
}
|
||||
type args struct {
|
||||
w *httptest.ResponseRecorder
|
||||
|
@ -47,9 +47,15 @@ func TestService_Me(t *testing.T) {
|
|||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
SuperAdminFirstUserOnly: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
|
@ -107,7 +113,138 @@ func TestService_Me(t *testing.T) {
|
|||
wantBody: `{"code":403,"message":"This organization is private. To gain access, you must be explicitly added by an administrator."}`,
|
||||
},
|
||||
{
|
||||
name: "Existing user",
|
||||
name: "Existing user - private default org and user is a super admin",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: false,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case 0:
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
case 1:
|
||||
return &chronograf.Organization{
|
||||
ID: 1,
|
||||
Name: "The Bad Place",
|
||||
Public: true,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
NumF: func(ctx context.Context) (int, error) {
|
||||
// This function gets to verify that there is at least one first user
|
||||
return 1, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
|
||||
if q.Name == nil || q.Provider == nil || q.Scheme == nil {
|
||||
return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme")
|
||||
}
|
||||
return &chronograf.User{
|
||||
Name: "me",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
}, nil
|
||||
},
|
||||
UpdateF: func(ctx context.Context, u *chronograf.User) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
principal: oauth2.Principal{
|
||||
Subject: "me",
|
||||
Issuer: "github",
|
||||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
|
||||
},
|
||||
{
|
||||
name: "Existing user - private default org",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: false,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case 0:
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
case 1:
|
||||
return &chronograf.Organization{
|
||||
ID: 1,
|
||||
Name: "The Bad Place",
|
||||
Public: true,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
NumF: func(ctx context.Context) (int, error) {
|
||||
// This function gets to verify that there is at least one first user
|
||||
return 1, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
|
||||
if q.Name == nil || q.Provider == nil || q.Scheme == nil {
|
||||
return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme")
|
||||
}
|
||||
return &chronograf.User{
|
||||
Name: "me",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
}, nil
|
||||
},
|
||||
UpdateF: func(ctx context.Context, u *chronograf.User) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
principal: oauth2.Principal{
|
||||
Subject: "me",
|
||||
Issuer: "github",
|
||||
},
|
||||
wantStatus: http.StatusForbidden,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":403,"message":"This organization is private. To gain access, you must be explicitly added by an administrator."}`,
|
||||
},
|
||||
{
|
||||
name: "Existing user - default org public",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
|
@ -169,8 +306,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","defaultRole":"viewer","name":"Default","public":true}}
|
||||
`,
|
||||
wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
|
||||
},
|
||||
{
|
||||
name: "Existing user - organization doesn't exist",
|
||||
|
@ -235,9 +371,15 @@ func TestService_Me(t *testing.T) {
|
|||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
SuperAdminFirstUserOnly: false,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
|
@ -291,9 +433,15 @@ func TestService_Me(t *testing.T) {
|
|||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
SuperAdminFirstUserOnly: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
|
@ -347,9 +495,15 @@ func TestService_Me(t *testing.T) {
|
|||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
SuperAdminFirstUserOnly: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
|
@ -403,8 +557,14 @@ func TestService_Me(t *testing.T) {
|
|||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
SuperAdminFirstUserOnly: true,
|
||||
UseAuth: true,
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
|
@ -452,9 +612,15 @@ func TestService_Me(t *testing.T) {
|
|||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: false,
|
||||
SuperAdminFirstUserOnly: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UseAuth: false,
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
|
@ -468,9 +634,15 @@ func TestService_Me(t *testing.T) {
|
|||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
SuperAdminFirstUserOnly: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UseAuth: true,
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
},
|
||||
wantStatus: http.StatusUnprocessableEntity,
|
||||
principal: oauth2.Principal{
|
||||
|
@ -531,10 +703,10 @@ func TestService_Me(t *testing.T) {
|
|||
Store: &mocks.Store{
|
||||
UsersStore: tt.fields.UsersStore,
|
||||
OrganizationsStore: tt.fields.OrganizationsStore,
|
||||
ConfigStore: tt.fields.ConfigStore,
|
||||
},
|
||||
Logger: tt.fields.Logger,
|
||||
UseAuth: tt.fields.UseAuth,
|
||||
SuperAdminFirstUserOnly: tt.fields.SuperAdminFirstUserOnly,
|
||||
Logger: tt.fields.Logger,
|
||||
UseAuth: tt.fields.UseAuth,
|
||||
}
|
||||
|
||||
s.Me(tt.args.w, tt.args.r)
|
||||
|
@ -652,7 +824,7 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"admin","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","public":true,"defaultRole":"admin"},{"id":"1337","name":"The ShillBillThrilliettas","public":true}],"currentOrganization":{"id":"1337","name":"The ShillBillThrilliettas","public":true}}`,
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"admin","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"admin","public":true},{"id":"1337","name":"The ShillBillThrilliettas","public":true}],"currentOrganization":{"id":"1337","name":"The ShillBillThrilliettas","public":true}}`,
|
||||
},
|
||||
{
|
||||
name: "Change the current User's organization",
|
||||
|
@ -727,7 +899,7 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"editor","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","public":true,"defaultRole":"editor"},{"id":"1337","name":"The ThrillShilliettos","public":false}],"currentOrganization":{"id":"1337","name":"The ThrillShilliettos","public":false}}`,
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"editor","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"editor","public":true},{"id":"1337","name":"The ThrillShilliettos","public":false}],"currentOrganization":{"id":"1337","name":"The ThrillShilliettos","public":false}}`,
|
||||
},
|
||||
{
|
||||
name: "Unable to find requested user in valid organization",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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{}
|
||||
}
|
||||
|
|
|
@ -157,6 +157,13 @@ func (s *Service) NewUser(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
cfg, err := s.Store.Config(ctx).Get(ctx)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user := &chronograf.User{
|
||||
Name: req.Name,
|
||||
Provider: req.Provider,
|
||||
|
@ -164,6 +171,10 @@ func (s *Service) NewUser(w http.ResponseWriter, r *http.Request) {
|
|||
Roles: req.Roles,
|
||||
}
|
||||
|
||||
if cfg.Auth.SuperAdminNewUsers {
|
||||
req.SuperAdmin = true
|
||||
}
|
||||
|
||||
if err := setSuperAdmin(ctx, req, user); err != nil {
|
||||
Error(w, http.StatusUnauthorized, err.Error(), s.Logger)
|
||||
return
|
||||
|
|
|
@ -112,8 +112,9 @@ func TestService_UserID(t *testing.T) {
|
|||
|
||||
func TestService_NewUser(t *testing.T) {
|
||||
type fields struct {
|
||||
UsersStore chronograf.UsersStore
|
||||
Logger chronograf.Logger
|
||||
UsersStore chronograf.UsersStore
|
||||
ConfigStore chronograf.ConfigStore
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
type args struct {
|
||||
w *httptest.ResponseRecorder
|
||||
|
@ -146,6 +147,13 @@ func TestService_NewUser(t *testing.T) {
|
|||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
|
@ -189,6 +197,13 @@ func TestService_NewUser(t *testing.T) {
|
|||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
|
@ -241,6 +256,13 @@ func TestService_NewUser(t *testing.T) {
|
|||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
|
@ -291,6 +313,13 @@ func TestService_NewUser(t *testing.T) {
|
|||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
|
@ -332,6 +361,13 @@ func TestService_NewUser(t *testing.T) {
|
|||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
|
@ -349,13 +385,56 @@ func TestService_NewUser(t *testing.T) {
|
|||
wantContentType: "application/json",
|
||||
wantBody: `{"id":"1338","superAdmin":true,"name":"bob","provider":"github","scheme":"oauth2","roles":[],"links":{"self":"/chronograf/v1/users/1338"}}`,
|
||||
},
|
||||
{
|
||||
name: "Create a new User with SuperAdminNewUsers: true in ConfigStore",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"POST",
|
||||
"http://any.url",
|
||||
nil,
|
||||
),
|
||||
user: &userRequest{
|
||||
Name: "bob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
},
|
||||
userKeyUser: &chronograf.User{
|
||||
ID: 0,
|
||||
Name: "coolUser",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
},
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
|
||||
user.ID = 1338
|
||||
return user, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantStatus: http.StatusCreated,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"id":"1338","superAdmin":true,"name":"bob","provider":"github","scheme":"oauth2","roles":[],"links":{"self":"/chronograf/v1/users/1338"}}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
UsersStore: tt.fields.UsersStore,
|
||||
UsersStore: tt.fields.UsersStore,
|
||||
ConfigStore: tt.fields.ConfigStore,
|
||||
},
|
||||
Logger: tt.fields.Logger,
|
||||
}
|
||||
|
|
|
@ -258,5 +258,12 @@ describe('Presenters', () => {
|
|||
|
||||
expect(actual).to.equal('m1.derivative_mean_usage_system')
|
||||
})
|
||||
|
||||
it('returns a label of empty string if the query config is empty', () => {
|
||||
const query = defaultQueryConfig({id: 1})
|
||||
const actual = buildDefaultYLabel(query)
|
||||
|
||||
expect(actual).to.equal('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -71,6 +71,13 @@ const AdminTabs = ({
|
|||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
AdminTabs.defaultProps = {
|
||||
organization: {
|
||||
name: '',
|
||||
id: '0',
|
||||
},
|
||||
}
|
||||
|
||||
AdminTabs.propTypes = {
|
||||
meRole: string.isRequired,
|
||||
meID: string.isRequired,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -135,4 +135,5 @@ UsersTable.propTypes = {
|
|||
meID: string.isRequired,
|
||||
notify: func.isRequired,
|
||||
}
|
||||
|
||||
export default UsersTable
|
||||
|
|
|
@ -3,11 +3,9 @@ import React, {Component, PropTypes} from 'react'
|
|||
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import SlideToggle from 'shared/components/SlideToggle'
|
||||
|
||||
import {USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
|
||||
import {USER_ROLES} from 'src/admin/constants/chronografAdmin'
|
||||
import {MEMBER_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
class UsersTableRowNew extends Component {
|
||||
constructor(props) {
|
||||
|
@ -17,8 +15,7 @@ class UsersTableRowNew extends Component {
|
|||
name: '',
|
||||
provider: '',
|
||||
scheme: 'oauth2',
|
||||
role: MEMBER_ROLE,
|
||||
superAdmin: false,
|
||||
role: this.props.organization.defaultRole,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,10 +52,6 @@ class UsersTableRowNew extends Component {
|
|||
this.setState({role: newRole.text})
|
||||
}
|
||||
|
||||
handleSelectSuperAdmin = superAdmin => {
|
||||
this.setState({superAdmin})
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
const {name, provider} = this.state
|
||||
const preventCreate = !name || !provider
|
||||
|
@ -87,7 +80,7 @@ class UsersTableRowNew extends Component {
|
|||
colActions,
|
||||
} = USERS_TABLE
|
||||
const {onBlur} = this.props
|
||||
const {name, provider, scheme, role, superAdmin} = this.state
|
||||
const {name, provider, scheme, role} = this.state
|
||||
|
||||
const dropdownRolesItems = USER_ROLES.map(r => ({...r, text: r.name}))
|
||||
const preventCreate = !name || !provider
|
||||
|
@ -117,11 +110,7 @@ class UsersTableRowNew extends Component {
|
|||
</td>
|
||||
<Authorized requiredRole={SUPERADMIN_ROLE}>
|
||||
<td style={{width: colSuperAdmin}} className="text-center">
|
||||
<SlideToggle
|
||||
active={superAdmin}
|
||||
size="xs"
|
||||
onToggle={this.handleSelectSuperAdmin}
|
||||
/>
|
||||
—
|
||||
</td>
|
||||
</Authorized>
|
||||
<td style={{width: colProvider}}>
|
||||
|
|
|
@ -48,4 +48,4 @@ export const NEW_DEFAULT_DATABASE = {
|
|||
}
|
||||
|
||||
export const PUBLIC_TOOLTIP =
|
||||
'If set to <code>false</code>, users cannot<br/>authenticate unless an <strong>Admin</strong> explicitly<br/>adds them to the organization.'
|
||||
'If turned off, new users cannot<br/>authenticate unless an <strong>Admin</strong> explicitly<br/>adds them to the organization.'
|
||||
|
|
|
@ -64,7 +64,18 @@ class AdminChronografPage extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {users, meCurrentOrganization, meRole, meID, notify} = this.props
|
||||
const {
|
||||
users,
|
||||
meRole,
|
||||
meID,
|
||||
notify,
|
||||
organizations,
|
||||
meCurrentOrganization,
|
||||
} = this.props
|
||||
|
||||
const organization = organizations.find(
|
||||
o => o.id === meCurrentOrganization.id
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
|
@ -84,7 +95,7 @@ class AdminChronografPage extends Component {
|
|||
meID={meID}
|
||||
// UsersTable
|
||||
users={users}
|
||||
organization={meCurrentOrganization}
|
||||
organization={organization}
|
||||
onCreateUser={this.handleCreateUser}
|
||||
onUpdateUserRole={this.handleUpdateUserRole}
|
||||
onUpdateUserSuperAdmin={this.handleUpdateUserSuperAdmin}
|
||||
|
@ -107,6 +118,7 @@ AdminChronografPage.propTypes = {
|
|||
users: string.isRequired,
|
||||
}),
|
||||
users: arrayOf(shape),
|
||||
organizations: arrayOf(shape),
|
||||
meCurrentOrganization: shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
|
@ -128,18 +140,19 @@ AdminChronografPage.propTypes = {
|
|||
|
||||
const mapStateToProps = ({
|
||||
links,
|
||||
adminChronograf: {users},
|
||||
adminChronograf: {users, organizations},
|
||||
auth: {
|
||||
me,
|
||||
me: {currentOrganization: meCurrentOrganization, role: meRole, id: meID},
|
||||
},
|
||||
}) => ({
|
||||
links,
|
||||
users,
|
||||
meCurrentOrganization,
|
||||
meRole,
|
||||
me,
|
||||
meID,
|
||||
links,
|
||||
users,
|
||||
meRole,
|
||||
organizations,
|
||||
meCurrentOrganization,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
|
||||
|
|
|
@ -3,6 +3,9 @@ import {isSameUser} from 'shared/reducers/helpers/auth'
|
|||
const initialState = {
|
||||
users: [],
|
||||
organizations: [],
|
||||
authConfig: {
|
||||
superAdminNewUsers: true,
|
||||
},
|
||||
}
|
||||
|
||||
const adminChronograf = (state = initialState, action) => {
|
||||
|
|
|
@ -40,7 +40,7 @@ const AxesOptions = ({
|
|||
<div className="form-group col-sm-12">
|
||||
<label htmlFor="prefix">Title</label>
|
||||
<OptIn
|
||||
customPlaceholder={defaultYLabel}
|
||||
customPlaceholder={defaultYLabel || 'y-axis title'}
|
||||
customValue={label}
|
||||
onSetValue={onSetLabel}
|
||||
type="text"
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
||||
this.state = {
|
||||
active: this.props.active,
|
||||
active: props.active,
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setState({active: nextProps.active})
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
const {onToggle} = this.props
|
||||
|
||||
|
|
|
@ -80,6 +80,11 @@ const getRange = (
|
|||
}
|
||||
}
|
||||
|
||||
// prevents inversion of graph
|
||||
if (min > max) {
|
||||
return [min, min + 1]
|
||||
}
|
||||
|
||||
return [min, max]
|
||||
}
|
||||
|
||||
|
|
|
@ -114,6 +114,11 @@ function getRolesForUser(roles, user) {
|
|||
export const buildDefaultYLabel = queryConfig => {
|
||||
const {measurement} = queryConfig
|
||||
const fields = _.get(queryConfig, ['fields', '0'], [])
|
||||
const isEmpty = !measurement && !fields.length
|
||||
|
||||
if (isEmpty) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const walkZerothArgs = f => {
|
||||
if (f.type === 'field') {
|
||||
|
|
|
@ -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 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,
|
||||
|
|
|
@ -14,6 +14,7 @@ const SourceForm = ({
|
|||
onInputChange,
|
||||
onBlurSourceURL,
|
||||
isUsingAuth,
|
||||
gotoPurgatory,
|
||||
isInitialSource,
|
||||
me,
|
||||
}) =>
|
||||
|
@ -138,7 +139,7 @@ const SourceForm = ({
|
|||
</label>
|
||||
</div>
|
||||
: null}
|
||||
<div className="form-group form-group-submit col-xs-12 col-sm-6 col-sm-offset-3 text-center">
|
||||
<div className="form-group form-group-submit text-center col-xs-12 col-sm-6 col-sm-offset-3">
|
||||
<button
|
||||
className={classnames('btn btn-block', {
|
||||
'btn-primary': editMode,
|
||||
|
@ -146,9 +147,16 @@ const SourceForm = ({
|
|||
})}
|
||||
type="submit"
|
||||
>
|
||||
<span className={`icon ${editMode ? 'checkmark' : 'plus'}`} />
|
||||
{editMode ? 'Save Changes' : 'Add Source'}
|
||||
</button>
|
||||
|
||||
<br />
|
||||
{isUsingAuth
|
||||
? <button className="btn btn-link btn-sm" onClick={gotoPurgatory}>
|
||||
<span className="icon shuffle" /> Switch Orgs
|
||||
</button>
|
||||
: null}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -179,6 +187,7 @@ SourceForm.propTypes = {
|
|||
}),
|
||||
isUsingAuth: bool,
|
||||
isInitialSource: bool,
|
||||
gotoPurgatory: func,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({auth: {isUsingAuth, me}}) => ({isUsingAuth, me})
|
||||
|
|
|
@ -100,6 +100,11 @@ class SourcePage extends Component {
|
|||
notify('error', `${bannerText}: ${error}`)
|
||||
}
|
||||
|
||||
gotoPurgatory = () => {
|
||||
const {router} = this.props
|
||||
router.push('/purgatory')
|
||||
}
|
||||
|
||||
_normalizeSource({source}) {
|
||||
const url = source.url.trim()
|
||||
if (source.url.startsWith('http')) {
|
||||
|
@ -224,6 +229,7 @@ class SourcePage extends Component {
|
|||
onSubmit={this.handleSubmit}
|
||||
onBlurSourceURL={this.handleBlurSourceURL}
|
||||
isInitialSource={isInitialSource}
|
||||
gotoPurgatory={this.gotoPurgatory}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ $tooltip-code-color: $c-potassium;
|
|||
word-break: keep-all;
|
||||
border-radius: $tooltip-radius;
|
||||
text-transform: none;
|
||||
text-align: left;
|
||||
cursor: default;
|
||||
|
||||
&.type-dark {
|
||||
|
|
|
@ -293,51 +293,52 @@ span.icon.sidebar--icon.sidebar--icon__superadmin {
|
|||
.fancy-scroll--thumb-v { @include gradient-v($g20-white,$c-neutrino); }
|
||||
}
|
||||
|
||||
.sidebar-menu--section__custom-links { order: 0; }
|
||||
.sidebar-menu--item__link-name { order: 1; }
|
||||
.sidebar-menu--section__switch-orgs { order: 2; }
|
||||
.sidebar-menu--scrollbar { order: 3; }
|
||||
.sidebar-menu--section__account { order: 4; }
|
||||
.sidebar-menu--provider { order: 5; }
|
||||
.sidebar-menu--item__logout { order: 6; }
|
||||
.sidebar-menu--heading { order: 7; }
|
||||
.sidebar-menu--triangle { order: 8; }
|
||||
|
||||
.sidebar-menu--section__custom-links:after {
|
||||
display: none;
|
||||
border-top-right-radius: $radius;
|
||||
}
|
||||
.sidebar-menu--user-nav {
|
||||
top: initial;
|
||||
bottom: 0;
|
||||
|
||||
.sidebar-menu--section__custom-links { order: 0; }
|
||||
.sidebar-menu--item__link-name { order: 1; }
|
||||
.sidebar-menu--section__switch-orgs { order: 2; }
|
||||
.sidebar-menu--scrollbar { order: 3; }
|
||||
.sidebar-menu--section__account { order: 4; }
|
||||
.sidebar-menu--provider { order: 5; }
|
||||
.sidebar-menu--item__logout { order: 6; }
|
||||
.sidebar-menu--heading { order: 7; }
|
||||
.sidebar-menu--triangle { order: 8; }
|
||||
|
||||
.sidebar-menu--section__custom-links:after {
|
||||
display: none;
|
||||
border-top-right-radius: $radius;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-height: 800px) {
|
||||
.sidebar-menu--heading { order: 0; }
|
||||
.sidebar-menu--section__account { order: 1; }
|
||||
.sidebar-menu--provider { order: 2; }
|
||||
.sidebar-menu--item__logout { order: 3; }
|
||||
.sidebar-menu--section__switch-orgs { order: 4; }
|
||||
.sidebar-menu--scrollbar { order: 5; }
|
||||
.sidebar-menu--section__custom-links { order: 6; }
|
||||
.sidebar-menu--item__link-name { order: 7; }
|
||||
.sidebar-menu--triangle { order: 8; }
|
||||
|
||||
.sidebar-menu--section__custom-links:after {
|
||||
display: initial;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.sidebar-menu--triangle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
top: $sidebar--width;
|
||||
left: 0px;
|
||||
transform: translate(-50%,-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.sidebar-menu--user-nav {
|
||||
top: 0;
|
||||
bottom: initial;
|
||||
|
||||
.sidebar-menu--heading { order: 0; }
|
||||
.sidebar-menu--section__account { order: 1; }
|
||||
.sidebar-menu--provider { order: 2; }
|
||||
.sidebar-menu--item__logout { order: 3; }
|
||||
.sidebar-menu--section__switch-orgs { order: 4; }
|
||||
.sidebar-menu--scrollbar { order: 5; }
|
||||
.sidebar-menu--section__custom-links { order: 6; }
|
||||
.sidebar-menu--item__link-name { order: 7; }
|
||||
.sidebar-menu--triangle { order: 8; }
|
||||
|
||||
.sidebar-menu--section__custom-links:after {
|
||||
display: initial;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.sidebar-menu--triangle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
top: $sidebar--width;
|
||||
left: 0px;
|
||||
transform: translate(-50%,-50%) rotate(45deg);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -104,11 +104,6 @@ table.table.chronograf-admin-table tbody tr.chronograf-admin-table--user td div.
|
|||
/* Styles for new user row */
|
||||
table.table.chronograf-admin-table tbody tr.chronograf-admin-table--new-user {
|
||||
background-color: $g4-onyx;
|
||||
|
||||
> td {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Highlight "Me" in the users table */
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue