Auth flow #311
pull/627/head
Will Piers 2016-11-28 10:20:43 -07:00 committed by GitHub
commit 2330011e96
31 changed files with 589 additions and 796 deletions

View File

@ -19,6 +19,7 @@ type Client struct {
SourcesStore *SourcesStore
ServersStore *ServersStore
LayoutStore *LayoutStore
UsersStore *UsersStore
AlertsStore *AlertsStore
}
@ -28,6 +29,7 @@ func NewClient() *Client {
c.SourcesStore = &SourcesStore{client: c}
c.ServersStore = &ServersStore{client: c}
c.AlertsStore = &AlertsStore{client: c}
c.UsersStore = &UsersStore{client: c}
c.LayoutStore = &LayoutStore{
client: c,
IDs: &uuid.V4{},
@ -65,6 +67,10 @@ func (c *Client) Open() error {
if _, err := tx.CreateBucketIfNotExists(AlertsBucket); err != nil {
return err
}
// Always create Users bucket.
if _, err := tx.CreateBucketIfNotExists(UsersBucket); err != nil {
return err
}
return nil
}); err != nil {
return err

View File

@ -205,3 +205,23 @@ func UnmarshalAlertRule(data []byte, r *ScopedAlert) error {
r.KapaID = int(pb.KapaID)
return nil
}
// MarshalUser encodes a user to binary protobuf format.
func MarshalUser(u *chronograf.User) ([]byte, error) {
return proto.Marshal(&User{
ID: uint64(u.ID),
Email: u.Email,
})
}
// UnmarshalUser decodes a user from binary protobuf data.
func UnmarshalUser(data []byte, u *chronograf.User) error {
var pb User
if err := proto.Unmarshal(data, &pb); err != nil {
return err
}
u.ID = chronograf.UserID(pb.ID)
u.Email = pb.Email
return nil
}

View File

@ -16,6 +16,7 @@ It has these top-level messages:
Cell
Query
AlertRule
User
*/
package internal
@ -35,13 +36,13 @@ var _ = math.Inf
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
type Exploration struct {
ID int64 `protobuf:"varint,1,opt,name=ID,json=iD,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,json=name,proto3" json:"Name,omitempty"`
UserID int64 `protobuf:"varint,3,opt,name=UserID,json=userID,proto3" json:"UserID,omitempty"`
Data string `protobuf:"bytes,4,opt,name=Data,json=data,proto3" json:"Data,omitempty"`
CreatedAt int64 `protobuf:"varint,5,opt,name=CreatedAt,json=createdAt,proto3" json:"CreatedAt,omitempty"`
UpdatedAt int64 `protobuf:"varint,6,opt,name=UpdatedAt,json=updatedAt,proto3" json:"UpdatedAt,omitempty"`
Default bool `protobuf:"varint,7,opt,name=Default,json=default,proto3" json:"Default,omitempty"`
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
UserID int64 `protobuf:"varint,3,opt,name=UserID,proto3" json:"UserID,omitempty"`
Data string `protobuf:"bytes,4,opt,name=Data,proto3" json:"Data,omitempty"`
CreatedAt int64 `protobuf:"varint,5,opt,name=CreatedAt,proto3" json:"CreatedAt,omitempty"`
UpdatedAt int64 `protobuf:"varint,6,opt,name=UpdatedAt,proto3" json:"UpdatedAt,omitempty"`
Default bool `protobuf:"varint,7,opt,name=Default,proto3" json:"Default,omitempty"`
}
func (m *Exploration) Reset() { *m = Exploration{} }
@ -50,14 +51,14 @@ func (*Exploration) ProtoMessage() {}
func (*Exploration) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{0} }
type Source struct {
ID int64 `protobuf:"varint,1,opt,name=ID,json=iD,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,json=name,proto3" json:"Name,omitempty"`
Type string `protobuf:"bytes,3,opt,name=Type,json=type,proto3" json:"Type,omitempty"`
Username string `protobuf:"bytes,4,opt,name=Username,json=username,proto3" json:"Username,omitempty"`
Password string `protobuf:"bytes,5,opt,name=Password,json=password,proto3" json:"Password,omitempty"`
URL string `protobuf:"bytes,6,opt,name=URL,json=uRL,proto3" json:"URL,omitempty"`
Default bool `protobuf:"varint,7,opt,name=Default,json=default,proto3" json:"Default,omitempty"`
Telegraf string `protobuf:"bytes,8,opt,name=Telegraf,json=telegraf,proto3" json:"Telegraf,omitempty"`
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
Type string `protobuf:"bytes,3,opt,name=Type,proto3" json:"Type,omitempty"`
Username string `protobuf:"bytes,4,opt,name=Username,proto3" json:"Username,omitempty"`
Password string `protobuf:"bytes,5,opt,name=Password,proto3" json:"Password,omitempty"`
URL string `protobuf:"bytes,6,opt,name=URL,proto3" json:"URL,omitempty"`
Default bool `protobuf:"varint,7,opt,name=Default,proto3" json:"Default,omitempty"`
Telegraf string `protobuf:"bytes,8,opt,name=Telegraf,proto3" json:"Telegraf,omitempty"`
}
func (m *Source) Reset() { *m = Source{} }
@ -66,12 +67,12 @@ func (*Source) ProtoMessage() {}
func (*Source) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{1} }
type Server struct {
ID int64 `protobuf:"varint,1,opt,name=ID,json=iD,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,json=name,proto3" json:"Name,omitempty"`
Username string `protobuf:"bytes,3,opt,name=Username,json=username,proto3" json:"Username,omitempty"`
Password string `protobuf:"bytes,4,opt,name=Password,json=password,proto3" json:"Password,omitempty"`
URL string `protobuf:"bytes,5,opt,name=URL,json=uRL,proto3" json:"URL,omitempty"`
SrcID int64 `protobuf:"varint,6,opt,name=SrcID,json=srcID,proto3" json:"SrcID,omitempty"`
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
Username string `protobuf:"bytes,3,opt,name=Username,proto3" json:"Username,omitempty"`
Password string `protobuf:"bytes,4,opt,name=Password,proto3" json:"Password,omitempty"`
URL string `protobuf:"bytes,5,opt,name=URL,proto3" json:"URL,omitempty"`
SrcID int64 `protobuf:"varint,6,opt,name=SrcID,proto3" json:"SrcID,omitempty"`
}
func (m *Server) Reset() { *m = Server{} }
@ -80,10 +81,10 @@ func (*Server) ProtoMessage() {}
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{2} }
type Layout struct {
ID string `protobuf:"bytes,1,opt,name=ID,json=iD,proto3" json:"ID,omitempty"`
Application string `protobuf:"bytes,2,opt,name=Application,json=application,proto3" json:"Application,omitempty"`
Measurement string `protobuf:"bytes,3,opt,name=Measurement,json=measurement,proto3" json:"Measurement,omitempty"`
Cells []*Cell `protobuf:"bytes,4,rep,name=Cells,json=cells" json:"Cells,omitempty"`
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
Application string `protobuf:"bytes,2,opt,name=Application,proto3" json:"Application,omitempty"`
Measurement string `protobuf:"bytes,3,opt,name=Measurement,proto3" json:"Measurement,omitempty"`
Cells []*Cell `protobuf:"bytes,4,rep,name=Cells" json:"Cells,omitempty"`
}
func (m *Layout) Reset() { *m = Layout{} }
@ -121,11 +122,11 @@ func (m *Cell) GetQueries() []*Query {
}
type Query struct {
Command string `protobuf:"bytes,1,opt,name=Command,json=command,proto3" json:"Command,omitempty"`
DB string `protobuf:"bytes,2,opt,name=DB,json=dB,proto3" json:"DB,omitempty"`
RP string `protobuf:"bytes,3,opt,name=RP,json=rP,proto3" json:"RP,omitempty"`
GroupBys []string `protobuf:"bytes,4,rep,name=GroupBys,json=groupBys" json:"GroupBys,omitempty"`
Wheres []string `protobuf:"bytes,5,rep,name=Wheres,json=wheres" json:"Wheres,omitempty"`
Command string `protobuf:"bytes,1,opt,name=Command,proto3" json:"Command,omitempty"`
DB string `protobuf:"bytes,2,opt,name=DB,proto3" json:"DB,omitempty"`
RP string `protobuf:"bytes,3,opt,name=RP,proto3" json:"RP,omitempty"`
GroupBys []string `protobuf:"bytes,4,rep,name=GroupBys" json:"GroupBys,omitempty"`
Wheres []string `protobuf:"bytes,5,rep,name=Wheres" json:"Wheres,omitempty"`
}
func (m *Query) Reset() { *m = Query{} }
@ -134,10 +135,10 @@ func (*Query) ProtoMessage() {}
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
type AlertRule struct {
ID string `protobuf:"bytes,1,opt,name=ID,json=iD,proto3" json:"ID,omitempty"`
JSON string `protobuf:"bytes,2,opt,name=JSON,json=jSON,proto3" json:"JSON,omitempty"`
SrcID int64 `protobuf:"varint,3,opt,name=SrcID,json=srcID,proto3" json:"SrcID,omitempty"`
KapaID int64 `protobuf:"varint,4,opt,name=KapaID,json=kapaID,proto3" json:"KapaID,omitempty"`
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
JSON string `protobuf:"bytes,2,opt,name=JSON,proto3" json:"JSON,omitempty"`
SrcID int64 `protobuf:"varint,3,opt,name=SrcID,proto3" json:"SrcID,omitempty"`
KapaID int64 `protobuf:"varint,4,opt,name=KapaID,proto3" json:"KapaID,omitempty"`
}
func (m *AlertRule) Reset() { *m = AlertRule{} }
@ -145,6 +146,16 @@ func (m *AlertRule) String() string { return proto.CompactTextString(
func (*AlertRule) ProtoMessage() {}
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
type User struct {
ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Email string `protobuf:"bytes,2,opt,name=Email,proto3" json:"Email,omitempty"`
}
func (m *User) Reset() { *m = User{} }
func (m *User) String() string { return proto.CompactTextString(m) }
func (*User) ProtoMessage() {}
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
func init() {
proto.RegisterType((*Exploration)(nil), "internal.Exploration")
proto.RegisterType((*Source)(nil), "internal.Source")
@ -153,45 +164,45 @@ func init() {
proto.RegisterType((*Cell)(nil), "internal.Cell")
proto.RegisterType((*Query)(nil), "internal.Query")
proto.RegisterType((*AlertRule)(nil), "internal.AlertRule")
proto.RegisterType((*User)(nil), "internal.User")
}
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
var fileDescriptorInternal = []byte{
// 547 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x94, 0xcd, 0x8e, 0xd3, 0x3e,
0x10, 0xc0, 0xe5, 0x26, 0xce, 0x87, 0xfb, 0xd7, 0xfe, 0x91, 0x85, 0x50, 0x84, 0x38, 0x54, 0x11,
0x87, 0x72, 0xd9, 0x03, 0x3c, 0x41, 0xdb, 0x20, 0x54, 0x28, 0xdd, 0xe2, 0x6e, 0xc5, 0x89, 0x83,
0x49, 0x66, 0xb7, 0x81, 0x7c, 0xe1, 0xd8, 0xb4, 0xb9, 0x72, 0x85, 0xb7, 0xe1, 0x05, 0x78, 0x34,
0x64, 0xc7, 0x25, 0x95, 0xf8, 0xd0, 0x1e, 0x7f, 0x33, 0x13, 0xfb, 0x37, 0x33, 0x6e, 0xc9, 0x45,
0x5e, 0x49, 0x10, 0x15, 0x2f, 0x2e, 0x1b, 0x51, 0xcb, 0x9a, 0x06, 0x27, 0x8e, 0xbf, 0x23, 0x32,
0x7e, 0x7e, 0x6c, 0x8a, 0x5a, 0x70, 0x99, 0xd7, 0x15, 0xbd, 0x20, 0xa3, 0x65, 0x12, 0xa1, 0x09,
0x9a, 0x3a, 0x6c, 0x94, 0x27, 0x94, 0x12, 0x77, 0xcd, 0x4b, 0x88, 0x46, 0x13, 0x34, 0x0d, 0x99,
0x5b, 0xf1, 0x12, 0xe8, 0x03, 0xe2, 0xed, 0x5a, 0x10, 0xcb, 0x24, 0x72, 0x4c, 0x9d, 0xa7, 0x0c,
0xe9, 0xda, 0x84, 0x4b, 0x1e, 0xb9, 0x7d, 0x6d, 0xc6, 0x25, 0xa7, 0x8f, 0x48, 0xb8, 0x10, 0xc0,
0x25, 0x64, 0x33, 0x19, 0x61, 0x53, 0x1e, 0xa6, 0xa7, 0x80, 0xce, 0xee, 0x9a, 0xcc, 0x66, 0xbd,
0x3e, 0xab, 0x4e, 0x01, 0x1a, 0x11, 0x3f, 0x81, 0x1b, 0xae, 0x0a, 0x19, 0xf9, 0x13, 0x34, 0x0d,
0x98, 0x9f, 0xf5, 0x18, 0xff, 0x40, 0xc4, 0xdb, 0xd6, 0x4a, 0xa4, 0x70, 0x27, 0x61, 0x4a, 0xdc,
0xeb, 0xae, 0x01, 0xa3, 0x1b, 0x32, 0x57, 0x76, 0x0d, 0xd0, 0x87, 0x24, 0xd0, 0x4d, 0xe8, 0xbc,
0x15, 0x0e, 0x94, 0x65, 0x9d, 0xdb, 0xf0, 0xb6, 0x3d, 0xd4, 0x22, 0x33, 0xce, 0x21, 0x0b, 0x1a,
0xcb, 0xf4, 0x1e, 0x71, 0x76, 0x6c, 0x65, 0x64, 0x43, 0xe6, 0x28, 0xb6, 0xfa, 0xbb, 0xa6, 0x3e,
0xe7, 0x1a, 0x0a, 0xb8, 0x15, 0xfc, 0x26, 0x0a, 0xfa, 0x73, 0xa4, 0xe5, 0xf8, 0x9b, 0x6e, 0x01,
0xc4, 0x67, 0x10, 0x77, 0x6a, 0xe1, 0x5c, 0xd7, 0xf9, 0x87, 0xae, 0xfb, 0x67, 0x5d, 0x3c, 0xe8,
0xde, 0x27, 0x78, 0x2b, 0xd2, 0x65, 0x62, 0xe7, 0x8d, 0x5b, 0x0d, 0xf1, 0x17, 0x44, 0xbc, 0x15,
0xef, 0x6a, 0x25, 0xcf, 0x74, 0x42, 0xa3, 0x33, 0x21, 0xe3, 0x59, 0xd3, 0x14, 0x79, 0x6a, 0x5e,
0x88, 0xb5, 0x1a, 0xf3, 0x21, 0xa4, 0x2b, 0x5e, 0x03, 0x6f, 0x95, 0x80, 0x12, 0x2a, 0x69, 0xfd,
0xc6, 0xe5, 0x10, 0xa2, 0x8f, 0x09, 0x5e, 0x40, 0x51, 0xb4, 0x91, 0x3b, 0x71, 0xa6, 0xe3, 0xa7,
0x17, 0x97, 0xbf, 0x1e, 0xa4, 0x0e, 0x33, 0x9c, 0xea, 0x64, 0xfc, 0x15, 0x11, 0x57, 0x33, 0xfd,
0x8f, 0xa0, 0xa3, 0x31, 0xc0, 0x0c, 0x1d, 0x35, 0x75, 0xe6, 0x5a, 0xcc, 0x50, 0xa7, 0xe9, 0x60,
0xae, 0xc0, 0x0c, 0x1d, 0x34, 0xed, 0x4d, 0xd3, 0x98, 0xa1, 0x3d, 0x7d, 0x42, 0xfc, 0x4f, 0x0a,
0x44, 0x0e, 0x6d, 0x84, 0xcd, 0x45, 0xff, 0x0f, 0x17, 0xbd, 0x51, 0x20, 0x3a, 0x76, 0xca, 0xeb,
0x0f, 0x73, 0xbb, 0x45, 0x94, 0xeb, 0x91, 0x9b, 0xd1, 0xfa, 0xc3, 0xc8, 0x63, 0x45, 0xb0, 0xf9,
0x46, 0x2f, 0x78, 0x51, 0x97, 0x25, 0xaf, 0x32, 0x3b, 0x15, 0x3f, 0xed, 0x51, 0x8f, 0x2a, 0x99,
0xdb, 0x89, 0x8c, 0xb2, 0xb9, 0x66, 0xb6, 0xb1, 0xfd, 0x8f, 0xc4, 0x46, 0x6f, 0xe6, 0x85, 0xa8,
0x55, 0x33, 0xef, 0xfa, 0xce, 0x43, 0x16, 0xdc, 0x5a, 0xd6, 0xbf, 0xa2, 0xb7, 0x7b, 0x10, 0x56,
0x35, 0x64, 0xde, 0xc1, 0x50, 0xfc, 0x8e, 0x84, 0xb3, 0x02, 0x84, 0x64, 0xaa, 0x80, 0xdf, 0x76,
0x41, 0x89, 0xfb, 0x72, 0x7b, 0xb5, 0x3e, 0x3d, 0x8d, 0x0f, 0xdb, 0xab, 0xf5, 0xb0, 0x50, 0xe7,
0x6c, 0xa1, 0xfa, 0xf8, 0x57, 0xbc, 0xe1, 0xcb, 0xc4, 0x4c, 0xc7, 0x61, 0xde, 0x47, 0x43, 0xef,
0x3d, 0xf3, 0x0f, 0xf0, 0xec, 0x67, 0x00, 0x00, 0x00, 0xff, 0xff, 0xad, 0x5d, 0x44, 0xfb, 0x13,
0x04, 0x00, 0x00,
// 541 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x94, 0x4b, 0x8e, 0xd3, 0x4c,
0x10, 0xc7, 0xd5, 0xb1, 0x3b, 0x89, 0x2b, 0x9f, 0xf2, 0xa1, 0xd6, 0x08, 0x59, 0x88, 0x45, 0x64,
0xb1, 0x08, 0x12, 0x9a, 0x05, 0x9c, 0x20, 0x89, 0x47, 0x28, 0x30, 0x0c, 0xa1, 0x33, 0x11, 0x2b,
0x16, 0x4d, 0x52, 0x43, 0x2c, 0x39, 0xb6, 0x69, 0xdb, 0x24, 0xde, 0xb2, 0x85, 0xdb, 0x70, 0x01,
0x8e, 0x86, 0xaa, 0xdd, 0x76, 0x2c, 0xf1, 0xd0, 0xec, 0xea, 0x5f, 0x55, 0xae, 0xfe, 0xd5, 0x23,
0x81, 0x71, 0x94, 0x14, 0xa8, 0x13, 0x15, 0x5f, 0x66, 0x3a, 0x2d, 0x52, 0x31, 0x6c, 0x74, 0xf0,
0x83, 0xc1, 0xe8, 0xea, 0x94, 0xc5, 0xa9, 0x56, 0x45, 0x94, 0x26, 0x62, 0x0c, 0xbd, 0x65, 0xe8,
0xb3, 0x09, 0x9b, 0x3a, 0xb2, 0xb7, 0x0c, 0x85, 0x00, 0xf7, 0x46, 0x1d, 0xd0, 0xef, 0x4d, 0xd8,
0xd4, 0x93, 0xc6, 0x16, 0x0f, 0xa1, 0xbf, 0xc9, 0x51, 0x2f, 0x43, 0xdf, 0x31, 0x79, 0x56, 0x51,
0x6e, 0xa8, 0x0a, 0xe5, 0xbb, 0x75, 0x2e, 0xd9, 0xe2, 0x31, 0x78, 0x0b, 0x8d, 0xaa, 0xc0, 0xdd,
0xac, 0xf0, 0xb9, 0x49, 0x3f, 0x3b, 0x28, 0xba, 0xc9, 0x76, 0x36, 0xda, 0xaf, 0xa3, 0xad, 0x43,
0xf8, 0x30, 0x08, 0xf1, 0x4e, 0x95, 0x71, 0xe1, 0x0f, 0x26, 0x6c, 0x3a, 0x94, 0x8d, 0x0c, 0x7e,
0x32, 0xe8, 0xaf, 0xd3, 0x52, 0x6f, 0xf1, 0x5e, 0xc0, 0x02, 0xdc, 0xdb, 0x2a, 0x43, 0x83, 0xeb,
0x49, 0x63, 0x8b, 0x47, 0x30, 0x24, 0xec, 0x84, 0x72, 0x6b, 0xe0, 0x56, 0x53, 0x6c, 0xa5, 0xf2,
0xfc, 0x98, 0xea, 0x9d, 0x61, 0xf6, 0x64, 0xab, 0xc5, 0x03, 0x70, 0x36, 0xf2, 0xda, 0xc0, 0x7a,
0x92, 0xcc, 0xbf, 0x63, 0x52, 0x9d, 0x5b, 0x8c, 0xf1, 0x93, 0x56, 0x77, 0xfe, 0xb0, 0xae, 0xd3,
0xe8, 0xe0, 0x3b, 0xb5, 0x80, 0xfa, 0x0b, 0xea, 0x7b, 0xb5, 0xd0, 0xc5, 0x75, 0xfe, 0x81, 0xeb,
0xfe, 0x19, 0x97, 0x9f, 0x71, 0x2f, 0x80, 0xaf, 0xf5, 0x76, 0x19, 0xda, 0x79, 0xd7, 0x22, 0xf8,
0xca, 0xa0, 0x7f, 0xad, 0xaa, 0xb4, 0x2c, 0x3a, 0x38, 0x9e, 0xc1, 0x99, 0xc0, 0x68, 0x96, 0x65,
0x71, 0xb4, 0x35, 0x17, 0x62, 0xa9, 0xba, 0x2e, 0xca, 0x78, 0x83, 0x2a, 0x2f, 0x35, 0x1e, 0x30,
0x29, 0x2c, 0x5f, 0xd7, 0x25, 0x9e, 0x00, 0x5f, 0x60, 0x1c, 0xe7, 0xbe, 0x3b, 0x71, 0xa6, 0xa3,
0xe7, 0xe3, 0xcb, 0xf6, 0x20, 0xc9, 0x2d, 0xeb, 0x60, 0xf0, 0x8d, 0x81, 0x4b, 0x96, 0xf8, 0x0f,
0xd8, 0xc9, 0x10, 0x70, 0xc9, 0x4e, 0xa4, 0x2a, 0xf3, 0x2c, 0x97, 0xac, 0x22, 0x75, 0x34, 0x4f,
0x70, 0xc9, 0x8e, 0xa4, 0xf6, 0xa6, 0x69, 0x2e, 0xd9, 0x5e, 0x3c, 0x85, 0xc1, 0xe7, 0x12, 0x75,
0x84, 0xb9, 0xcf, 0xcd, 0x43, 0xff, 0x9f, 0x1f, 0x7a, 0x57, 0xa2, 0xae, 0x64, 0x13, 0xa7, 0x0f,
0x23, 0xbb, 0x45, 0x16, 0xd1, 0xc8, 0xcd, 0x68, 0x07, 0xf5, 0xc8, 0xc9, 0x0e, 0x4a, 0xe0, 0xe6,
0x1b, 0x5a, 0xf0, 0x22, 0x3d, 0x1c, 0x54, 0xb2, 0xb3, 0x53, 0x69, 0x24, 0x8d, 0x2a, 0x9c, 0xdb,
0x89, 0xf4, 0xc2, 0x39, 0x69, 0xb9, 0xb2, 0xfd, 0xf7, 0xe4, 0x8a, 0x36, 0xf3, 0x52, 0xa7, 0x65,
0x36, 0xaf, 0xea, 0xce, 0x3d, 0xd9, 0x6a, 0xfa, 0x15, 0xbd, 0xdf, 0xa3, 0xb6, 0xa8, 0x9e, 0xb4,
0x2a, 0xf8, 0x00, 0xde, 0x2c, 0x46, 0x5d, 0xc8, 0x32, 0xc6, 0xdf, 0x76, 0x21, 0xc0, 0x7d, 0xb5,
0x7e, 0x7b, 0xd3, 0x9c, 0x06, 0xd9, 0xe7, 0x85, 0x3a, 0x9d, 0x85, 0x52, 0xf9, 0xd7, 0x2a, 0x53,
0xcb, 0xd0, 0x4c, 0xc7, 0x91, 0x56, 0x05, 0xcf, 0xc0, 0xa5, 0xc3, 0xe9, 0x54, 0x76, 0x4d, 0xe5,
0x0b, 0xe0, 0x57, 0x07, 0x15, 0xc5, 0xb6, 0x74, 0x2d, 0x3e, 0xf6, 0xcd, 0xff, 0xc5, 0x8b, 0x5f,
0x01, 0x00, 0x00, 0xff, 0xff, 0x00, 0x2c, 0x30, 0x90, 0x41, 0x04, 0x00, 0x00,
}

View File

@ -62,3 +62,8 @@ message AlertRule {
int64 SrcID = 3; // SrcID is the id of the source this alert is associated with
int64 KapaID = 4; // KapaID is the id of the kapacitor this alert is associated with
}
message User {
uint64 ID = 1; // ID is the unique ID of this user
string Email = 2; // Email byte representation of the user
}

129
bolt/users.go Normal file
View File

@ -0,0 +1,129 @@
package bolt
import (
"context"
"github.com/boltdb/bolt"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/bolt/internal"
)
// Ensure UsersStore implements chronograf.UsersStore.
var _ chronograf.UsersStore = &UsersStore{}
var UsersBucket = []byte("Users")
type UsersStore struct {
client *Client
}
// FindByEmail searches the UsersStore for all users owned with the email
func (s *UsersStore) FindByEmail(ctx context.Context, email string) (*chronograf.User, error) {
var user chronograf.User
err := s.client.db.View(func(tx *bolt.Tx) error {
err := tx.Bucket(UsersBucket).ForEach(func(k, v []byte) error {
var u chronograf.User
if err := internal.UnmarshalUser(v, &u); err != nil {
return err
} else if u.Email != email {
return nil
}
user.Email = u.Email
user.ID = u.ID
return nil
})
if err != nil {
return err
}
if user.ID == 0 {
return chronograf.ErrUserNotFound
}
return nil
})
if err != nil {
return nil, err
}
return &user, nil
}
// Create a new Users in the UsersStore.
func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
if err := s.client.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(UsersBucket)
seq, err := b.NextSequence()
if err != nil {
return err
}
u.ID = chronograf.UserID(seq)
if v, err := internal.MarshalUser(u); err != nil {
return err
} else if err := b.Put(itob(int(u.ID)), v); err != nil {
return err
}
return nil
}); err != nil {
return nil, err
}
return u, nil
}
// Delete the users from the UsersStore
func (s *UsersStore) Delete(ctx context.Context, u *chronograf.User) error {
if err := s.client.db.Update(func(tx *bolt.Tx) error {
if err := tx.Bucket(UsersBucket).Delete(itob(int(u.ID))); err != nil {
return err
}
return nil
}); err != nil {
return err
}
return nil
}
// Get retrieves a user by id.
func (s *UsersStore) Get(ctx context.Context, id chronograf.UserID) (*chronograf.User, error) {
var u chronograf.User
if err := s.client.db.View(func(tx *bolt.Tx) error {
if v := tx.Bucket(UsersBucket).Get(itob(int(id))); v == nil {
return chronograf.ErrUserNotFound
} else if err := internal.UnmarshalUser(v, &u); err != nil {
return err
}
return nil
}); err != nil {
return nil, err
}
return &u, nil
}
// Update a user
func (s *UsersStore) Update(ctx context.Context, usr *chronograf.User) error {
if err := s.client.db.Update(func(tx *bolt.Tx) error {
// Retrieve an existing user with the same ID.
var u chronograf.User
b := tx.Bucket(UsersBucket)
if v := b.Get(itob(int(usr.ID))); v == nil {
return chronograf.ErrUserNotFound
} else if err := internal.UnmarshalUser(v, &u); err != nil {
return err
}
u.Email = usr.Email
if v, err := internal.MarshalUser(&u); err != nil {
return err
} else if err := b.Put(itob(int(u.ID)), v); err != nil {
return err
}
return nil
}); err != nil {
return err
}
return nil
}

View File

@ -13,6 +13,7 @@ const (
ErrSourceNotFound = Error("source not found")
ErrServerNotFound = Error("server not found")
ErrLayoutNotFound = Error("layout not found")
ErrUserNotFound = Error("user not found")
ErrLayoutInvalid = Error("layout is invalid")
ErrAlertNotFound = Error("alert not found")
ErrAuthentication = Error("user not authenticated")
@ -196,23 +197,22 @@ type UserID int
// User represents an authenticated user.
type User struct {
ID UserID
Name string
ID UserID `json:"id"`
Email string `json:"email"`
}
// AuthStore is the Storage and retrieval of authentication information
type AuthStore struct {
// User management for the AuthStore
Users interface {
// Create a new User in the AuthStore
Add(context.Context, User) error
// Delete the User from the AuthStore
Delete(context.Context, User) error
// Retrieve a user if `ID` exists.
Get(ctx context.Context, ID int) error
// UsersStore is the Storage and retrieval of authentication information
type UsersStore interface {
// Create a new User in the UsersStore
Add(context.Context, *User) (*User, error)
// Delete the User from the UsersStore
Delete(context.Context, *User) error
// Get retrieves a user if `ID` exists.
Get(ctx context.Context, ID UserID) (*User, error)
// Update the user's permissions or roles
Update(context.Context, User) error
}
Update(context.Context, *User) error
// FindByEmail will retrieve a user by email address.
FindByEmail(ctx context.Context, Email string) (*User, error)
}
// ExplorationID is a unique ID for an Exploration.

9
docs/proto.md Normal file
View File

@ -0,0 +1,9 @@
download a binary here https://github.com/google/protobuf/releases/tag/v3.1.0
run the following 4 commands listed here https://github.com/gogo/protobuf
```sh
go get github.com/gogo/protobuf/proto
go get github.com/gogo/protobuf/jsonpb
go get github.com/gogo/protobuf/protoc-gen-gogo
go get github.com/gogo/protobuf/gogoproto
```

View File

@ -94,14 +94,13 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.DELETE("/chronograf/v1/layouts/:id", service.RemoveLayout)
// Users
/*
router.GET("/chronograf/v1/users", Users)
router.POST("/chronograf/v1/users", NewUser)
router.GET("/chronograf/v1/me", service.Me)
router.POST("/chronograf/v1/users", service.NewUser)
router.GET("/chronograf/v1/users/:id", service.UserID)
router.PATCH("/chronograf/v1/users/:id", service.UpdateUser)
router.DELETE("/chronograf/v1/users/:id", service.RemoveUser)
router.GET("/chronograf/v1/users/:id", UsersID)
router.PATCH("/chronograf/v1/users/:id", UpdateUser)
router.DELETE("/chronograf/v1/users/:id", RemoveUser)
*/
// Explorations
router.GET("/chronograf/v1/users/:id/explorations", service.Explorations)
router.POST("/chronograf/v1/users/:id/explorations", service.NewExploration)
@ -133,7 +132,7 @@ func AuthAPI(opts MuxOpts, router *httprouter.Router) http.Handler {
opts.Logger,
)
router.GET("/oauth", gh.Login())
router.GET("/oauth/github", gh.Login())
router.GET("/oauth/logout", gh.Logout())
router.GET("/oauth/github/callback", gh.Callback())

View File

@ -11,6 +11,7 @@ type getRoutesResponse struct {
Mappings string `json:"mappings"` // Location of the application mappings endpoint
Sources string `json:"sources"` // Location of the sources endpoint
Users string `json:"users"` // Location of the users endpoint
Me string `json:"me"` // Location of the me endpoint
}
// AllRoutes returns all top level routes within chronograf
@ -19,6 +20,7 @@ func AllRoutes(logger chronograf.Logger) http.HandlerFunc {
Sources: "/chronograf/v1/sources",
Layouts: "/chronograf/v1/layouts",
Users: "/chronograf/v1/users",
Me: "/chronograf/v1/me",
Mappings: "/chronograf/v1/mappings",
}

View File

@ -60,7 +60,7 @@ func (s *Server) useAuth() bool {
// Serve starts and runs the chronograf server
func (s *Server) Serve() error {
logger := clog.New(clog.ParseLevel(s.LogLevel))
service := openService(s.BoltPath, s.CannedPath, logger)
service := openService(s.BoltPath, s.CannedPath, logger, s.useAuth())
s.handler = NewMux(MuxOpts{
Develop: s.Develop,
TokenSecret: s.TokenSecret,
@ -106,7 +106,7 @@ func (s *Server) Serve() error {
return nil
}
func openService(boltPath, cannedPath string, logger chronograf.Logger) Service {
func openService(boltPath, cannedPath string, logger chronograf.Logger, useAuth bool) Service {
db := bolt.NewClient()
db.Path = boltPath
if err := db.Open(); err != nil {
@ -137,12 +137,14 @@ func openService(boltPath, cannedPath string, logger chronograf.Logger) Service
ExplorationStore: db.ExplorationStore,
SourcesStore: db.SourcesStore,
ServersStore: db.ServersStore,
UsersStore: db.UsersStore,
TimeSeries: &influx.Client{
Logger: logger,
},
LayoutStore: layouts,
AlertRulesStore: db.AlertsStore,
Logger: logger,
UseAuth: useAuth,
}
}

View File

@ -9,8 +9,10 @@ type Service struct {
ServersStore chronograf.ServersStore
LayoutStore chronograf.LayoutStore
AlertRulesStore chronograf.AlertRulesStore
UsersStore chronograf.UsersStore
TimeSeries chronograf.TimeSeries
Logger chronograf.Logger
UseAuth bool
}
// ErrorMessage is the error response format for all service errors

180
server/users.go Normal file
View File

@ -0,0 +1,180 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"golang.org/x/net/context"
"github.com/influxdata/chronograf"
)
type userLinks struct {
Self string `json:"self"` // Self link mapping to this resource
Explorations string `json:"explorations"` // URL for explorations endpoint
}
type userResponse struct {
*chronograf.User
Links userLinks `json:"links"`
}
func newUserResponse(usr *chronograf.User) userResponse {
base := "/chronograf/v1/users"
return userResponse{
User: usr,
Links: userLinks{
Self: fmt.Sprintf("%s/%d", base, usr.ID),
Explorations: fmt.Sprintf("%s/%d/explorations", base, usr.ID),
},
}
}
// NewUser adds a new valid user to the store
func (h *Service) NewUser(w http.ResponseWriter, r *http.Request) {
var usr *chronograf.User
if err := json.NewDecoder(r.Body).Decode(usr); err != nil {
invalidJSON(w, h.Logger)
return
}
if err := ValidUserRequest(usr); err != nil {
invalidData(w, err, h.Logger)
return
}
var err error
if usr, err = h.UsersStore.Add(r.Context(), usr); err != nil {
msg := fmt.Errorf("error storing user %v: %v", *usr, err)
unknownErrorWithMessage(w, msg, h.Logger)
return
}
res := newUserResponse(usr)
w.Header().Add("Location", res.Links.Self)
encodeJSON(w, http.StatusCreated, res, h.Logger)
}
// UserID retrieves a user from the store
func (h *Service) UserID(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return
}
ctx := r.Context()
usr, err := h.UsersStore.Get(ctx, chronograf.UserID(id))
if err != nil {
notFound(w, id, h.Logger)
return
}
res := newUserResponse(usr)
encodeJSON(w, http.StatusOK, res, h.Logger)
}
// RemoveUser deletes the user from the store
func (h *Service) RemoveUser(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return
}
usr := &chronograf.User{ID: chronograf.UserID(id)}
ctx := r.Context()
if err = h.UsersStore.Delete(ctx, usr); err != nil {
unknownErrorWithMessage(w, err, h.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}
// UpdateUser handles incremental updates of a data user
func (h *Service) UpdateUser(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return
}
ctx := r.Context()
usr, err := h.UsersStore.Get(ctx, chronograf.UserID(id))
if err != nil {
notFound(w, id, h.Logger)
return
}
var req chronograf.User
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
return
}
usr.Email = req.Email
if err := ValidUserRequest(usr); err != nil {
invalidData(w, err, h.Logger)
return
}
if err := h.UsersStore.Update(ctx, usr); err != nil {
msg := fmt.Sprintf("Error updating user ID %d", id)
Error(w, http.StatusInternalServerError, msg, h.Logger)
return
}
encodeJSON(w, http.StatusOK, newUserResponse(usr), h.Logger)
}
// ValidUserRequest checks if email is nonempty
func ValidUserRequest(s *chronograf.User) error {
// email is required
if s.Email == "" {
return fmt.Errorf("Email required")
}
return nil
}
func getEmail(ctx context.Context) (string, error) {
principal := ctx.Value(chronograf.PrincipalKey).(chronograf.Principal)
if principal == "" {
return "", fmt.Errorf("Token not found")
}
return string(principal), nil
}
// Me does a findOrCreate based on the email in the context
func (h *Service) Me(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if !h.UseAuth {
Error(w, http.StatusTeapot, fmt.Sprintf("%v", "Go to line 151 users.go. Look for Arnold"), h.Logger)
_ = 42 // did you mean to learn the answer? if so go to line aslfjasdlfja; (gee willickers.... tbc)
return
}
email, err := getEmail(ctx)
if err != nil {
invalidData(w, err, h.Logger)
return
}
usr, err := h.UsersStore.FindByEmail(ctx, email)
if err == nil {
res := newUserResponse(usr)
encodeJSON(w, http.StatusOK, res, h.Logger)
return
}
// Because we didnt find a user, making a new one
user := &chronograf.User{
Email: email,
}
user, err = h.UsersStore.Add(ctx, user)
if err != nil {
msg := fmt.Errorf("error storing user %v: %v", user, err)
unknownErrorWithMessage(w, msg, h.Logger)
return
}
res := newUserResponse(user)
encodeJSON(w, http.StatusOK, res, h.Logger)
}

View File

@ -1,68 +0,0 @@
import PermissionsTable from 'src/shared/components/PermissionsTable';
import React from 'react';
import {shallow} from 'enzyme';
import sinon from 'sinon';
describe('Shared.Components.PermissionsTable', function() {
it('renders a row for each permission', function() {
const permissions = [
{name: 'ViewChronograf', displayName: 'View Chronograf', description: 'Can use Chronograf tools', resources: ['db1']},
{name: 'Read', displayName: 'Read', description: 'Can read data', resources: ['']},
];
const wrapper = shallow(
<PermissionsTable
permissions={permissions}
showAddResource={true}
onRemovePermission={sinon.spy()}
/>
);
expect(wrapper.find('tr').length).to.equal(2);
expect(wrapper.find('table').text()).to.match(/View Chronograf/);
expect(wrapper.find('table').text()).to.match(/db1/);
expect(wrapper.find('table').text()).to.match(/Read/);
expect(wrapper.find('table').text()).to.match(/All Databases/);
});
it('only renders the control to add a resource when specified', function() {
const wrapper = shallow(
<PermissionsTable
permissions={[{name: 'Read', displayName: 'Read', description: 'Can read data', resources: ['']}]}
showAddResource={false}
onRemovePermission={sinon.spy()}
/>
);
expect(wrapper.find('.pill-add').length).to.equal(0);
});
it('only renders the "Remove" control when a callback is provided', function() {
const wrapper = shallow(
<PermissionsTable
permissions={[{name: 'Read', displayName: 'Read', description: 'Can read data', resources: ['']}]}
showAddResource={true}
/>
);
expect(wrapper.find('.remove-permission').length).to.equal(0);
});
describe('when a user clicks "Remove"', function() {
it('fires a callback', function() {
const permission = {name: 'Read', displayName: 'Read', description: 'Can read data', resources: ['']};
const cb = sinon.spy();
const wrapper = shallow(
<PermissionsTable
permissions={[permission]}
showAddResource={false}
onRemovePermission={cb}
/>
);
wrapper.find('button[children="Remove"]').at(0).simulate('click');
expect(cb.calledWith(permission)).to.be.true;
});
});
});

View File

@ -53,7 +53,7 @@ const CheckSources = React.createClass({
const {isFetching, sources} = nextState;
const source = sources.find((s) => s.id === params.sourceID);
if (!isFetching && !source) {
return router.push(`/?redirectPath=${location.pathname}`);
return router.push(`/sources/new?redirectPath=${location.pathname}`);
}
if (!isFetching && !location.pathname.includes("/manage-sources")) {

12
ui/src/auth/Login.js Normal file
View File

@ -0,0 +1,12 @@
import React from 'react';
import {withRouter} from 'react-router';
const Login = React.createClass({
render() {
return (
<a className="btn btn-primary" href="/oauth/github">Click me to log in</a>
);
},
});
export default withRouter(Login);

2
ui/src/auth/index.js Normal file
View File

@ -0,0 +1,2 @@
import Login from './Login';
export {Login};

View File

@ -1,25 +1,24 @@
import React, {PropTypes} from 'react';
import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import {Router, Route, browserHistory} from 'react-router';
import {Router, Route, browserHistory, Redirect} from 'react-router';
import App from 'src/App';
import AlertsApp from 'src/alerts';
import CheckSources from 'src/CheckSources';
import {HostsPage, HostPage} from 'src/hosts';
import {KubernetesPage} from 'src/kubernetes';
import {Login} from 'src/auth';
import {KapacitorPage, KapacitorRulePage, KapacitorRulesPage, KapacitorTasksPage} from 'src/kapacitor';
import DataExplorer from 'src/chronograf';
import {CreateSource, SourceForm, ManageSources} from 'src/sources';
import NotFound from 'src/shared/components/NotFound';
import NoClusterError from 'src/shared/components/NoClusterError';
import configureStore from 'src/store/configureStore';
import {getSources} from 'shared/apis';
import {getMe, getSources} from 'shared/apis';
import {receiveMe} from 'shared/actions/me';
import 'src/style/enterprise_style/application.scss';
const {number, shape, string, bool} = PropTypes;
const defaultTimeRange = {upper: null, lower: 'now() - 15m'};
const lsTimeRange = window.localStorage.getItem('timeRange');
const parsedTimeRange = JSON.parse(lsTimeRange) || {};
@ -28,38 +27,15 @@ const timeRange = Object.assign(defaultTimeRange, parsedTimeRange);
const store = configureStore({timeRange});
const rootNode = document.getElementById('react-root');
const HTTP_SERVER_ERROR = 500;
const Root = React.createClass({
getInitialState() {
return {
me: {
id: 1,
name: 'Chronograf',
email: 'foo@example.com',
admin: true,
},
isFetching: false,
hasReadPermission: false,
clusterStatus: null,
loggedIn: null,
};
},
childContextTypes: {
me: shape({
id: number.isRequired,
name: string.isRequired,
email: string.isRequired,
admin: bool.isRequired,
}),
componentDidMount() {
this.checkAuth();
},
getChildContext() {
return {
me: this.state.me,
};
},
activeSource(sources) {
const defaultSource = sources.find((s) => s.default);
if (defaultSource && defaultSource.id) {
@ -68,29 +44,53 @@ const Root = React.createClass({
return sources[0];
},
redirectToHosts(_, replace, callback) {
redirectFromRoot(_, replace, callback) {
getSources().then(({data: {sources}}) => {
if (sources && sources.length) {
const path = `/sources/${this.activeSource(sources).id}/hosts`;
replace(path);
}
callback();
}).catch(callback);
});
},
checkAuth() {
if (store.getState().me.links) {
return this.setState({loggedIn: true});
}
getMe().then(({data: me}) => {
store.dispatch(receiveMe(me));
this.setState({loggedIn: true});
}).catch((err) => {
const ImATeapot = 418;
if (err.response.status === ImATeapot) { // This means authentication is not set up!
return this.setState({loggedIn: true});
// may be good to store this info somewhere. So that pages know whether they can use me or not
}
this.setState({loggedIn: false});
});
},
render() {
if (this.state.isFetching) {
return null;
if (this.state.loggedIn === null) {
return <div className="page-spinner"></div>;
}
if (this.state.clusterStatus === HTTP_SERVER_ERROR) {
return <NoClusterError />;
}
if (this.state.loggedIn === false) {
return (
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/" component={CreateSource} onEnter={this.redirectToHosts} />
<Route path="/login" component={Login} />
<Redirect from="*" to="/login" />
</Router>
</Provider>
);
}
return (
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/" component={CreateSource} onEnter={this.redirectFromRoot} />
<Route path="/sources/new" component={CreateSource} />
<Route path="/sources/:sourceID" component={App}>
<Route component={CheckSources}>
<Route path="manage-sources" component={ManageSources} />

View File

@ -0,0 +1,14 @@
export function receiveMe(me) {
return {
type: 'ME_RECEIVED',
payload: {
me,
},
};
}
export function logout() {
return {
type: 'LOGOUT',
};
}

View File

@ -7,6 +7,13 @@ export function fetchLayouts() {
});
}
export function getMe() {
return AJAX({
url: `/chronograf/v1/me`,
method: 'GET',
});
}
export function getSources() {
return AJAX({
url: '/chronograf/v1/sources',

View File

@ -1,79 +0,0 @@
import React, {PropTypes} from 'react';
const {arrayOf, number, shape, func, string} = PropTypes;
const AddClusterAccounts = React.createClass({
propTypes: {
clusters: arrayOf(shape({
id: number.isRequired,
cluster_users: arrayOf(shape({
name: string.isRequired,
})),
dipslay_name: string,
cluster_id: string.isRequired,
})).isRequired,
onSelectClusterAccount: func.isRequired,
headerText: string,
},
getDefaultProps() {
return {
headerText: 'Pair With Cluster Accounts',
};
},
handleSelectClusterAccount(e, clusterID) {
this.props.onSelectClusterAccount({
clusterID,
accountName: e.target.value,
});
},
render() {
return (
<div>
{
this.props.clusters.map((cluster, i) => {
return (
<div key={i} className="form-grid">
<div className="form-group col-sm-6">
{i === 0 ? <label>Cluster</label> : null}
<div className="form-control-static">
{cluster.display_name || cluster.cluster_id}
</div>
</div>
<div className="form-group col-sm-6">
{i === 0 ? <label>Account</label> : null}
{this.renderClusterUsers(cluster)}
</div>
</div>
);
})
}
</div>
);
},
renderClusterUsers(cluster) {
if (!cluster.cluster_users) {
return (
<select disabled={true} defaultValue="No cluster accounts" className="form-control" id="cluster-account">
<option>No cluster accounts</option>
</select>
);
}
return (
<select onChange={(e) => this.handleSelectClusterAccount(e, cluster.cluster_id)} className="form-control">
<option value="">No Association</option>
{
cluster.cluster_users.map((cu) => {
return <option value={cu.name} key={cu.name}>{cu.name}</option>;
})
}
</select>
);
},
});
export default AddClusterAccounts;

View File

@ -1,124 +0,0 @@
import React, {PropTypes} from 'react';
const CLUSTER_WIDE_PERMISSIONS = ["CreateDatabase", "AddRemoveNode", "ManageShard", "DropDatabase", "CopyShard", "Rebalance"];
const AddPermissionModal = React.createClass({
propTypes: {
activeCluster: PropTypes.string.isRequired,
permissions: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
})),
databases: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
onAddPermission: PropTypes.func.isRequired,
},
getInitialState() {
return {
selectedPermission: null,
selectedDatabase: '',
};
},
handlePermissionClick(permission) {
this.setState({
selectedPermission: permission,
selectedDatabase: '',
});
},
handleDatabaseChange(e) {
this.setState({selectedDatabase: e.target.value});
},
handleSubmit(e) {
e.preventDefault();
this.props.onAddPermission({
name: this.state.selectedPermission,
resources: [this.state.selectedDatabase],
});
$('#addPermissionModal').modal('hide'); // eslint-disable-line no-undef
},
render() {
const {permissions} = this.props;
return (
<div className="modal fade" id="addPermissionModal" tabIndex="-1" role="dialog">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 className="modal-title">Select a Permission to Add</h4>
</div>
<form onSubmit={this.handleSubmit}>
<div className="modal-body">
<div className="well permission-list">
<ul>
{permissions.map((perm) => {
return (
<li key={perm.name}>
<input onClick={() => this.handlePermissionClick(perm.name)} type="radio" name="permissionName" value={`${perm.name}`} id={`permission-${perm.name}`}></input>
<label htmlFor={`permission-${perm.name}`}>
{perm.displayName}
<br/>
<span className="permission-description">{perm.description}</span>
</label>
</li>
);
})}
</ul>
</div>
{this.renderOptions()}
</div>
{this.renderFooter()}
</form>
</div>
</div>
</div>
);
},
renderFooter() {
return (
<div className="modal-footer">
<button className="btn btn-default" data-dismiss="modal">Cancel</button>
<input disabled={!this.state.selectedPermission} className="btn btn-success" type="submit" value="Add Permission"></input>
</div>
);
},
renderOptions() {
return (
<div>
{this.state.selectedPermission ? this.renderDatabases() : null}
</div>
);
},
renderDatabases() {
const isClusterWide = CLUSTER_WIDE_PERMISSIONS.includes(this.state.selectedPermission);
if (!this.props.databases.length || isClusterWide) {
return null;
}
return (
<div>
<div className="form-grid">
<div className="form-group col-md-12">
<label htmlFor="#permissions-database">Limit Permission to...</label>
<select onChange={this.handleDatabaseChange} className="form-control" name="database" id="permissions-database">
<option value={''}>All Databases</option>
{this.props.databases.map((databaseName, i) => <option key={i}>{databaseName}</option>)}
</select>
</div>
</div>
</div>
);
},
});
export default AddPermissionModal;

View File

@ -1,24 +0,0 @@
import React from 'react';
const {node} = React.PropTypes;
const ClusterError = React.createClass({
propTypes: {
children: node.isRequired,
},
render() {
return (
<div className="container-fluid">
<div className="row">
<div className="col-sm-6 col-sm-offset-3">
<div className="panel panel-error panel-summer">
{this.props.children}
</div>
</div>
</div>
</div>
);
},
});
export default ClusterError;

View File

@ -1,21 +0,0 @@
import React from 'react';
import ClusterError from './ClusterError';
const InsufficientPermissions = React.createClass({
render() {
return (
<ClusterError>
<div className="panel-heading text-center">
<h2 className="deluxe">
{`Your account has insufficient permissions`}
</h2>
</div>
<div className="panel-body text-center">
<h3 className="deluxe">Talk to your admin to get additional permissions for access</h3>
</div>
</ClusterError>
);
},
});
export default InsufficientPermissions;

View File

@ -1,35 +0,0 @@
import React from 'react';
import errorCopy from 'hson!shared/copy/errors.hson';
const NoClusterError = React.createClass({
render() {
return (
<div>
<div className="container">
<div className="row">
<div className="col-sm-6 col-sm-offset-3">
<div className="panel panel-error panel-summer">
<div className="panel-heading text-center">
<h2 className="deluxe">
{errorCopy.noCluster.head}
</h2>
</div>
<div className="panel-body text-center">
<h3 className="deluxe">How to resolve:</h3>
<p>
{errorCopy.noCluster.body}
</p>
<div className="text-center">
<button className="btn btn-center btn-success" onClick={() => window.location.reload()}>My Cluster Is Back Up</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
},
});
export default NoClusterError;

View File

@ -1,27 +0,0 @@
import React from 'react';
const NoClusterLinksError = React.createClass({
render() {
return (
<div className="container-fluid">
<div className="row">
<div className="col-sm-6 col-sm-offset-3">
<div className="panel panel-error panel-summer">
<div className="panel-heading text-center">
<h2 className="deluxe">
This user is not associated with any cluster accounts!
</h2>
</div>
<div className="panel-body text-center">
<p>Many features in Chronograf require your user to be associated with a cluster account.</p>
<p>Ask an administrator to associate your user with a cluster account.</p>
</div>
</div>
</div>
</div>
</div>
);
},
});
export default NoClusterLinksError;

View File

@ -1,76 +0,0 @@
import React, {PropTypes} from 'react';
const {arrayOf, shape, string} = PropTypes;
const PermissionsTable = React.createClass({
propTypes: {
permissions: PropTypes.arrayOf(shape({
name: string.isRequired,
displayName: string.isRequired,
description: string.isRequired,
resources: arrayOf(string.isRequired).isRequired,
})).isRequired,
showAddResource: PropTypes.bool,
onRemovePermission: PropTypes.func,
},
getDefaultProps() {
return {
permissions: [],
showAddResource: false,
};
},
handleAddResourceClick() {
// TODO
},
handleRemovePermission(permission) {
this.props.onRemovePermission(permission);
},
render() {
if (!this.props.permissions.length) {
return (
<div className="generic-empty-state">
<span className="icon alert-triangle"></span>
<h4>This Role has no Permissions</h4>
</div>
);
}
return (
<div className="panel-body">
<table className="table permissions-table">
<tbody>
{this.props.permissions.map((p) => (
<tr key={p.name}>
<td>{p.displayName}</td>
<td>
{p.resources.map((resource, i) => <div key={i} className="pill">{resource === '' ? 'All Databases' : resource}</div>)}
{this.props.showAddResource ? (
<div onClick={this.handleAddResourceClick} className="pill-add" data-toggle="modal" data-target="#addPermissionModal">
<span className="icon plus"></span>
</div>
) : null}
</td>
{this.props.onRemovePermission ? (
<td className="remove-permission">
<button
onClick={() => this.handleRemovePermission(p)}
type="button"
className="btn btn-sm btn-link-danger">
Remove
</button>
</td>
) : null}
</tr>
))}
</tbody>
</table>
</div>
);
},
});
export default PermissionsTable;

View File

@ -1,86 +0,0 @@
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import PermissionsTable from 'src/shared/components/PermissionsTable';
const {arrayOf, bool, func, shape, string} = PropTypes;
const RolePanels = React.createClass({
propTypes: {
roles: arrayOf(shape({
name: string.isRequired,
users: arrayOf(string.isRequired).isRequired,
permissions: arrayOf(shape({
name: string.isRequired,
displayName: string.isRequired,
description: string.isRequired,
resources: arrayOf(string.isRequired).isRequired,
})).isRequired,
})).isRequired,
showUserCount: bool,
onRemoveAccountFromRole: func,
},
getDefaultProps() {
return {
showUserCount: false,
};
},
render() {
const {roles} = this.props;
if (!roles.length) {
return (
<div className="panel panel-default">
<div className="panel-body">
<div className="generic-empty-state">
<span className="icon alert-triangle"></span>
<h4>This user has no roles</h4>
</div>
</div>
</div>
);
}
return (
<div className="panel-group sub-page" role="tablist">
{roles.map((role) => {
const id = role.name.replace(/[^\w]/gi, '');
return (
<div key={role.name} className="panel panel-default">
<div className="panel-heading" role="tab" id={`heading${id}`}>
<h4 className="panel-title u-flex u-ai-center u-jc-space-between">
<a className="collapsed" role="button" data-toggle="collapse" href={`#collapse-role-${id}`}>
<span className="caret"></span>
{role.name}
</a>
<div>
{this.props.showUserCount ? <p>{role.users ? role.users.length : 0} Users</p> : null}
{this.props.onRemoveAccountFromRole ? (
<button
onClick={() => this.props.onRemoveAccountFromRole(role)}
data-toggle="modal"
data-target="#removeAccountFromRoleModal"
type="button"
className="btn btn-sm btn-link">
Remove
</button>
) : null}
<Link to={`/roles/${encodeURIComponent(role.name)}`} className="btn btn-xs btn-link">
Go To Role
</Link>
</div>
</h4>
</div>
<div id={`collapse-role-${id}`} className="panel-collapse collapse" role="tabpanel">
<PermissionsTable permissions={role.permissions} />
</div>
</div>
);
})}
</div>
);
},
});
export default RolePanels;

View File

@ -1,95 +0,0 @@
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import classNames from 'classnames';
const {func, shape, arrayOf, string} = PropTypes;
const UsersTable = React.createClass({
propTypes: {
users: arrayOf(shape({}).isRequired).isRequired,
activeCluster: string.isRequired,
onUserToDelete: func.isRequired,
me: shape({}).isRequired,
deleteText: string,
},
getDefaultProps() {
return {
deleteText: 'Delete',
};
},
handleSelectUserToDelete(user) {
this.props.onUserToDelete(user);
},
render() {
const {users, activeCluster, me} = this.props;
if (!users.length) {
return (
<div className="generic-empty-state">
<span className="icon user-outline"/>
<h4>No users</h4>
</div>
);
}
return (
<table className="table v-center users-table">
<tbody>
<tr>
<th></th>
<th>Name</th>
<th>Admin</th>
<th>Email</th>
<th></th>
</tr>
{
users.map((user) => {
const isMe = me.id === user.id;
return (
<tr key={user.id}>
<td></td>
<td>
<span>
<Link to={`/clusters/${activeCluster}/users/${user.id}`} title={`Go to ${user.name}'s profile`}>{user.name}</Link>
{isMe ? <em> (You) </em> : null}
</span>
</td>
<td className="admin-column">{this.renderAdminIcon(user.admin)}</td>
<td>{user.email}</td>
<td>
{this.renderDeleteButton(user)}
</td>
</tr>
);
})
}
</tbody>
</table>
);
},
renderAdminIcon(isAdmin) {
return <span className={classNames("icon", {"checkmark text-color-success": isAdmin, "remove text-color-danger": !isAdmin})}></span>;
},
renderDeleteButton(user) {
if (this.props.me.id === user.id) {
return <button type="button" className="btn btn-sm btn-link-danger disabled" title={`Cannot ${this.props.deleteText} Yourself`}>{this.props.deleteText}</button>;
}
return (
<button
onClick={() => this.handleSelectUserToDelete({id: user.id, name: user.name})}
type="button"
data-toggle="modal"
data-target="#deleteUsersModal"
className="btn btn-sm btn-link-danger"
>
{this.props.deleteText}
</button>
);
},
});
export default UsersTable;

View File

@ -0,0 +1,7 @@
import me from './me';
import notifications from './notifications';
export {
me,
notifications,
};

View File

@ -0,0 +1,17 @@
function getInitialState() {
return {};
}
const initialState = getInitialState();
export default function me(state = initialState, action) {
switch (action.type) {
case 'ME_RECEIVED': {
return action.payload.me;
}
case 'LOGOUT': {
return {};
}
}
return state;
}

View File

@ -3,11 +3,15 @@ import {combineReducers} from 'redux';
import thunkMiddleware from 'redux-thunk';
import makeQueryExecuter from 'src/shared/middleware/queryExecuter';
import * as chronografReducers from 'src/chronograf/reducers';
import * as sharedReducers from 'src/shared/reducers';
import rulesReducer from 'src/kapacitor/reducers/rules';
import notifications from 'src/shared/reducers/notifications';
import persistStateEnhancer from './persistStateEnhancer';
const rootReducer = combineReducers({notifications, ...chronografReducers, rules: rulesReducer});
const rootReducer = combineReducers({
...sharedReducers,
...chronografReducers,
rules: rulesReducer,
});
export default function configureStore(initialState) {
const createPersistentStore = compose(