Merge branch 'master' into single-stat-colors

pull/2545/head
Alex P 2017-12-13 11:01:18 -08:00
commit 88fa28af9a
189 changed files with 20006 additions and 3655 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.3.10.0
current_version = 1.4.0.0-beta1
files = README.md server/swagger.json
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<release>\d+)
serialize = {major}.{minor}.{patch}.{release}

View File

@ -1,11 +1,40 @@
## v1.3.11.0 [unreleased]
## 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
### Bug Fixes
1. [#2528](https://github.com/influxdata/chronograf/pull/2528): Fix template rendering to ignore template if not in query
## v1.4.0.0-beta1 [2017-12-07]
### Features
1. [#2506](https://github.com/influxdata/chronograf/pull/2506): Add support for multiple organizations, multiple users with role-based access control, and private instances
1. [#2188](https://github.com/influxdata/chronograf/pull/2188): Add Kapacitor logs to the TICKscript editor
1. [#2385](https://github.com/influxdata/chronograf/pull/2385): Add time shift feature to DataExplorer and Dashboards
1. [#2426](https://github.com/influxdata/chronograf/pull/2426): Add auto group by time to Data Explorer
1. [#2479](https://github.com/influxdata/chronograf/pull/2479): Support authentication for Enterprise Meta Nodes
1. [#2456](https://github.com/influxdata/chronograf/pull/2456): Add boolean thresholds for kapacitor threshold alerts
1. [#2460](https://github.com/influxdata/chronograf/pull/2460): Update kapacitor alerts to cast to float before sending to influx
1. [#2400](https://github.com/influxdata/chronograf/pull/2400): Allow override of generic oauth2 keys for email
### UI Improvements
1. [#2410](https://github.com/influxdata/chronograf/pull/2410): Introduce customizable Gauge visualization type for dashboard cells
1. [#2427](https://github.com/influxdata/chronograf/pull/2427): Improve performance of Hosts, Alert History, and TICKscript logging pages when there are many items to display
1. [#2384](https://github.com/influxdata/chronograf/pull/2384): Add filtering by name to Dashboard index page
1. [#2477](https://github.com/influxdata/chronograf/pull/2477): Improve performance of hoverline rendering
### Bug Fixes
1. [#2449](https://github.com/influxdata/chronograf/pull/2449): Fix .jsdep step fails when LDFLAGS is exported
1. [#2157](https://github.com/influxdata/chronograf/pull/2157): Fix logscale producing console errors when only one point in graph
1. [#2157](https://github.com/influxdata/chronograf/pull/2157): Fix logscale producing console errors when only one point in graph
1. [#2158](https://github.com/influxdata/chronograf/pull/2158): Fix 'Cannot connect to source' false error flag on Dashboard page
1. [#2167](https://github.com/influxdata/chronograf/pull/2167): Add fractions of seconds to time field in csv export
1. [#1077](https://github.com/influxdata/chronograf/pull/2087): Fix Chronograf requiring Telegraf's CPU and system plugins to ensure that all Apps appear on the HOST LIST page.
1. [#2087](https://github.com/influxdata/chronograf/pull/2087): Fix Chronograf requiring Telegraf's CPU and system plugins to ensure that all Apps appear on the HOST LIST page.
1. [#2222](https://github.com/influxdata/chronograf/pull/2222): Fix template variables in dashboard query building.
1. [#2291](https://github.com/influxdata/chronograf/pull/2291): Fix several kapacitor alert creation panics.
1. [#2303](https://github.com/influxdata/chronograf/pull/2303): Add shadow-utils to RPM release packages
@ -14,7 +43,6 @@
1. [#2327](https://github.com/influxdata/chronograf/pull/2327): Visualize CREATE/DELETE queries with Table view in Data Explorer
1. [#2329](https://github.com/influxdata/chronograf/pull/2329): Include tag values alongside measurement name in Data Explorer result tabs
1. [#2410](https://github.com/influxdata/chronograf/pull/2410): Redesign cell display options panel
1. [#2410](https://github.com/influxdata/chronograf/pull/2410): Introduce customizable Gauge visualization type for dashboard cells
1. [#2386](https://github.com/influxdata/chronograf/pull/2386): Fix queries that include regex, numbers and wildcard
1. [#2398](https://github.com/influxdata/chronograf/pull/2398): Fix apps on hosts page from parsing tags with null values
1. [#2408](https://github.com/influxdata/chronograf/pull/2408): Fix updated Dashboard names not updating dashboard list
@ -28,20 +56,6 @@
1. [#2483](https://github.com/influxdata/chronograf/pull/2483): Update MySQL pre-canned dashboard to have query derivative correctly
1. [#2545](https://github.com/influxdata/chronograf/pull/2545): Allow configurable thresholds and colors of Single-Stat visualizations in dashboards
### Features
1. [#2188](https://github.com/influxdata/chronograf/pull/2188): Add Kapacitor logs to the TICKscript editor
1. [#2384](https://github.com/influxdata/chronograf/pull/2384): Add filtering by name to Dashboard index page
1. [#2385](https://github.com/influxdata/chronograf/pull/2385): Add time shift feature to DataExplorer and Dashboards
1. [#2400](https://github.com/influxdata/chronograf/pull/2400): Allow override of generic oauth2 keys for email
1. [#2426](https://github.com/influxdata/chronograf/pull/2426): Add auto group by time to Data Explorer
1. [#2456](https://github.com/influxdata/chronograf/pull/2456): Add boolean thresholds for kapacitor threshold alerts
1. [#2460](https://github.com/influxdata/chronograf/pull/2460): Update kapacitor alerts to cast to float before sending to influx
1. [#2479](https://github.com/influxdata/chronograf/pull/2479): Support authentication for Enterprise Meta Nodes
1. [#2477](https://github.com/influxdata/chronograf/pull/2477): Improve performance of hoverline rendering
### UI Improvements
1. [#2427](https://github.com/influxdata/chronograf/pull/2427): Improve performance of Hosts, Alert History, and TICKscript logging pages when there are many items to display
## v1.3.10.0 [2017-10-24]
### Bug Fixes
1. [#2095](https://github.com/influxdata/chronograf/pull/2095): Improve the copy in the retention policy edit page

View File

@ -136,7 +136,7 @@ option.
## Versions
The most recent version of Chronograf is
[v1.3.10.0](https://www.influxdata.com/downloads/).
[v1.4.0.0-beta1](https://www.influxdata.com/downloads/).
Spotted a bug or have a feature request? Please open
[an issue](https://github.com/influxdata/chronograf/issues/new)!
@ -178,7 +178,7 @@ By default, chronograf runs on port `8888`.
To get started right away with Docker, you can pull down our latest release:
```sh
docker pull chronograf:1.3.10.0
docker pull chronograf:1.4.0.0
```
### From Source

View File

@ -2,6 +2,7 @@ package bolt
import (
"context"
"path"
"time"
"github.com/boltdb/bolt"
@ -16,11 +17,12 @@ type Client struct {
Now func() time.Time
LayoutIDs chronograf.ID
SourcesStore *SourcesStore
ServersStore *ServersStore
LayoutStore *LayoutStore
UsersStore *UsersStore
DashboardsStore *DashboardsStore
SourcesStore *SourcesStore
ServersStore *ServersStore
LayoutsStore *LayoutsStore
DashboardsStore *DashboardsStore
UsersStore *UsersStore
OrganizationsStore *OrganizationsStore
}
// NewClient initializes all stores
@ -28,8 +30,7 @@ func NewClient() *Client {
c := &Client{Now: time.Now}
c.SourcesStore = &SourcesStore{client: c}
c.ServersStore = &ServersStore{client: c}
c.UsersStore = &UsersStore{client: c}
c.LayoutStore = &LayoutStore{
c.LayoutsStore = &LayoutsStore{
client: c,
IDs: &uuid.V4{},
}
@ -37,6 +38,8 @@ func NewClient() *Client {
client: c,
IDs: &uuid.V4{},
}
c.UsersStore = &UsersStore{client: c}
c.OrganizationsStore = &OrganizationsStore{client: c}
return c
}
@ -50,6 +53,10 @@ func (c *Client) Open(ctx context.Context) error {
c.db = db
if err := c.db.Update(func(tx *bolt.Tx) error {
// Always create Organizations bucket.
if _, err := tx.CreateBucketIfNotExists(OrganizationsBucket); err != nil {
return err
}
// Always create Sources bucket.
if _, err := tx.CreateBucketIfNotExists(SourcesBucket); err != nil {
return err
@ -59,11 +66,11 @@ func (c *Client) Open(ctx context.Context) error {
return err
}
// Always create Layouts bucket.
if _, err := tx.CreateBucketIfNotExists(LayoutBucket); err != nil {
if _, err := tx.CreateBucketIfNotExists(LayoutsBucket); err != nil {
return err
}
// Always create Dashboards bucket.
if _, err := tx.CreateBucketIfNotExists(DashboardBucket); err != nil {
if _, err := tx.CreateBucketIfNotExists(DashboardsBucket); err != nil {
return err
}
// Always create Users bucket.
@ -76,7 +83,23 @@ func (c *Client) Open(ctx context.Context) error {
}
// Runtime migrations
return c.DashboardsStore.Migrate(ctx)
if err := c.OrganizationsStore.Migrate(ctx); err != nil {
return err
}
if err := c.SourcesStore.Migrate(ctx); err != nil {
return err
}
if err := c.ServersStore.Migrate(ctx); err != nil {
return err
}
if err := c.LayoutsStore.Migrate(ctx); err != nil {
return err
}
if err := c.DashboardsStore.Migrate(ctx); err != nil {
return err
}
return nil
}
// Close the connection to the bolt database
@ -86,3 +109,7 @@ func (c *Client) Close() error {
}
return nil
}
func bucket(b []byte, org string) []byte {
return []byte(path.Join(string(b), org))
}

View File

@ -2,6 +2,7 @@ package bolt
import (
"context"
"fmt"
"strconv"
"github.com/boltdb/bolt"
@ -12,8 +13,8 @@ import (
// Ensure DashboardsStore implements chronograf.DashboardsStore.
var _ chronograf.DashboardsStore = &DashboardsStore{}
// DashboardBucket is the bolt bucket dashboards are stored in
var DashboardBucket = []byte("Dashoard")
// DashboardsBucket is the bolt bucket dashboards are stored in
var DashboardsBucket = []byte("Dashoard")
// DashboardsStore is the bolt implementation of storing dashboards
type DashboardsStore struct {
@ -54,14 +55,34 @@ func (d *DashboardsStore) Migrate(ctx context.Context) error {
if err != nil {
return err
}
return d.AddIDs(ctx, boards)
if err := d.AddIDs(ctx, boards); err != nil {
return nil
}
defaultOrg, err := d.client.OrganizationsStore.DefaultOrganization(ctx)
if err != nil {
return err
}
defaultOrgID := fmt.Sprintf("%d", defaultOrg.ID)
for _, board := range boards {
if board.Organization == "" {
board.Organization = defaultOrgID
if err := d.Update(ctx, board); err != nil {
return nil
}
}
}
return nil
}
// All returns all known dashboards
func (d *DashboardsStore) All(ctx context.Context) ([]chronograf.Dashboard, error) {
var srcs []chronograf.Dashboard
if err := d.client.db.View(func(tx *bolt.Tx) error {
if err := tx.Bucket(DashboardBucket).ForEach(func(k, v []byte) error {
if err := tx.Bucket(DashboardsBucket).ForEach(func(k, v []byte) error {
var src chronograf.Dashboard
if err := internal.UnmarshalDashboard(v, &src); err != nil {
return err
@ -82,7 +103,7 @@ func (d *DashboardsStore) All(ctx context.Context) ([]chronograf.Dashboard, erro
// Add creates a new Dashboard in the DashboardsStore
func (d *DashboardsStore) Add(ctx context.Context, src chronograf.Dashboard) (chronograf.Dashboard, error) {
if err := d.client.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(DashboardBucket)
b := tx.Bucket(DashboardsBucket)
id, _ := b.NextSequence()
src.ID = chronograf.DashboardID(id)
@ -113,7 +134,7 @@ func (d *DashboardsStore) Get(ctx context.Context, id chronograf.DashboardID) (c
var src chronograf.Dashboard
if err := d.client.db.View(func(tx *bolt.Tx) error {
strID := strconv.Itoa(int(id))
if v := tx.Bucket(DashboardBucket).Get([]byte(strID)); v == nil {
if v := tx.Bucket(DashboardsBucket).Get([]byte(strID)); v == nil {
return chronograf.ErrDashboardNotFound
} else if err := internal.UnmarshalDashboard(v, &src); err != nil {
return err
@ -130,7 +151,7 @@ func (d *DashboardsStore) Get(ctx context.Context, id chronograf.DashboardID) (c
func (d *DashboardsStore) Delete(ctx context.Context, dash chronograf.Dashboard) error {
if err := d.client.db.Update(func(tx *bolt.Tx) error {
strID := strconv.Itoa(int(dash.ID))
if err := tx.Bucket(DashboardBucket).Delete([]byte(strID)); err != nil {
if err := tx.Bucket(DashboardsBucket).Delete([]byte(strID)); err != nil {
return err
}
return nil
@ -145,7 +166,7 @@ func (d *DashboardsStore) Delete(ctx context.Context, dash chronograf.Dashboard)
func (d *DashboardsStore) Update(ctx context.Context, dash chronograf.Dashboard) error {
if err := d.client.db.Update(func(tx *bolt.Tx) error {
// Get an existing dashboard with the same ID.
b := tx.Bucket(DashboardBucket)
b := tx.Bucket(DashboardsBucket)
strID := strconv.Itoa(int(dash.ID))
if v := b.Get([]byte(strID)); v == nil {
return chronograf.ErrDashboardNotFound

View File

@ -23,6 +23,8 @@ func MarshalSource(s chronograf.Source) ([]byte, error) {
InsecureSkipVerify: s.InsecureSkipVerify,
Default: s.Default,
Telegraf: s.Telegraf,
Organization: s.Organization,
Role: s.Role,
})
}
@ -44,19 +46,22 @@ func UnmarshalSource(data []byte, s *chronograf.Source) error {
s.InsecureSkipVerify = pb.InsecureSkipVerify
s.Default = pb.Default
s.Telegraf = pb.Telegraf
s.Organization = pb.Organization
s.Role = pb.Role
return nil
}
// MarshalServer encodes a server to binary protobuf format.
func MarshalServer(s chronograf.Server) ([]byte, error) {
return proto.Marshal(&Server{
ID: int64(s.ID),
SrcID: int64(s.SrcID),
Name: s.Name,
Username: s.Username,
Password: s.Password,
URL: s.URL,
Active: s.Active,
ID: int64(s.ID),
SrcID: int64(s.SrcID),
Name: s.Name,
Username: s.Username,
Password: s.Password,
URL: s.URL,
Active: s.Active,
Organization: s.Organization,
})
}
@ -74,6 +79,7 @@ func UnmarshalServer(data []byte, s *chronograf.Server) error {
s.Password = pb.Password
s.URL = pb.URL
s.Active = pb.Active
s.Organization = pb.Organization
return nil
}
@ -280,10 +286,11 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
templates[i] = template
}
return proto.Marshal(&Dashboard{
ID: int64(d.ID),
Cells: cells,
Templates: templates,
Name: d.Name,
ID: int64(d.ID),
Cells: cells,
Templates: templates,
Name: d.Name,
Organization: d.Organization,
})
}
@ -418,6 +425,7 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
d.Cells = cells
d.Templates = templates
d.Name = pb.Name
d.Organization = pb.Organization
return nil
}
@ -461,8 +469,20 @@ func UnmarshalAlertRule(data []byte, r *ScopedAlert) error {
// MarshalUser encodes a user to binary protobuf format.
// We are ignoring the password for now.
func MarshalUser(u *chronograf.User) ([]byte, error) {
roles := make([]*Role, len(u.Roles))
for i, role := range u.Roles {
roles[i] = &Role{
Organization: role.Organization,
Name: role.Name,
}
}
return MarshalUserPB(&User{
Name: u.Name,
ID: u.ID,
Name: u.Name,
Provider: u.Provider,
Scheme: u.Scheme,
Roles: roles,
SuperAdmin: u.SuperAdmin,
})
}
@ -479,7 +499,20 @@ func UnmarshalUser(data []byte, u *chronograf.User) error {
if err := UnmarshalUserPB(data, &pb); err != nil {
return err
}
roles := make([]chronograf.Role, len(pb.Roles))
for i, role := range pb.Roles {
roles[i] = chronograf.Role{
Organization: role.Organization,
Name: role.Name,
}
}
u.ID = pb.ID
u.Name = pb.Name
u.Provider = pb.Provider
u.Scheme = pb.Scheme
u.SuperAdmin = pb.SuperAdmin
u.Roles = roles
return nil
}
@ -488,3 +521,73 @@ func UnmarshalUser(data []byte, u *chronograf.User) error {
func UnmarshalUserPB(data []byte, u *User) error {
return proto.Unmarshal(data, u)
}
// MarshalRole encodes a role to binary protobuf format.
func MarshalRole(r *chronograf.Role) ([]byte, error) {
return MarshalRolePB(&Role{
Organization: r.Organization,
Name: r.Name,
})
}
// MarshalRolePB encodes a role to binary protobuf format.
func MarshalRolePB(r *Role) ([]byte, error) {
return proto.Marshal(r)
}
// UnmarshalRole decodes a role from binary protobuf data.
func UnmarshalRole(data []byte, r *chronograf.Role) error {
var pb Role
if err := UnmarshalRolePB(data, &pb); err != nil {
return err
}
r.Organization = pb.Organization
r.Name = pb.Name
return nil
}
// UnmarshalRolePB decodes a role from binary protobuf data.
func UnmarshalRolePB(data []byte, r *Role) error {
if err := proto.Unmarshal(data, r); err != nil {
return err
}
return nil
}
// MarshalOrganization encodes a organization to binary protobuf format.
func MarshalOrganization(o *chronograf.Organization) ([]byte, error) {
return MarshalOrganizationPB(&Organization{
ID: o.ID,
Name: o.Name,
DefaultRole: o.DefaultRole,
Public: o.Public,
})
}
// MarshalOrganizationPB encodes a organization to binary protobuf format.
func MarshalOrganizationPB(o *Organization) ([]byte, error) {
return proto.Marshal(o)
}
// UnmarshalOrganization decodes a organization from binary protobuf data.
func UnmarshalOrganization(data []byte, o *chronograf.Organization) error {
var pb Organization
if err := UnmarshalOrganizationPB(data, &pb); err != nil {
return err
}
o.ID = pb.ID
o.Name = pb.Name
o.DefaultRole = pb.DefaultRole
o.Public = pb.Public
return nil
}
// UnmarshalOrganizationPB decodes a organization from binary protobuf data.
func UnmarshalOrganizationPB(data []byte, o *Organization) error {
if err := proto.Unmarshal(data, o); err != nil {
return err
}
return nil
}

View File

@ -25,6 +25,8 @@ It has these top-level messages:
Range
AlertRule
User
Role
Organization
*/
package internal
@ -55,6 +57,8 @@ type Source struct {
InsecureSkipVerify bool `protobuf:"varint,9,opt,name=InsecureSkipVerify,proto3" json:"InsecureSkipVerify,omitempty"`
MetaURL string `protobuf:"bytes,10,opt,name=MetaURL,proto3" json:"MetaURL,omitempty"`
SharedSecret string `protobuf:"bytes,11,opt,name=SharedSecret,proto3" json:"SharedSecret,omitempty"`
Organization string `protobuf:"bytes,12,opt,name=Organization,proto3" json:"Organization,omitempty"`
Role string `protobuf:"bytes,13,opt,name=Role,proto3" json:"Role,omitempty"`
}
func (m *Source) Reset() { *m = Source{} }
@ -63,10 +67,11 @@ func (*Source) ProtoMessage() {}
func (*Source) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{0} }
type Dashboard struct {
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
Cells []*DashboardCell `protobuf:"bytes,3,rep,name=cells" json:"cells,omitempty"`
Templates []*Template `protobuf:"bytes,4,rep,name=templates" json:"templates,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"`
Cells []*DashboardCell `protobuf:"bytes,3,rep,name=cells" json:"cells,omitempty"`
Templates []*Template `protobuf:"bytes,4,rep,name=templates" json:"templates,omitempty"`
Organization string `protobuf:"bytes,5,opt,name=Organization,proto3" json:"Organization,omitempty"`
}
func (m *Dashboard) Reset() { *m = Dashboard{} }
@ -209,13 +214,14 @@ func (*TemplateQuery) ProtoMessage() {}
func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
type Server struct {
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"`
Active bool `protobuf:"varint,7,opt,name=Active,proto3" json:"Active,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"`
Active bool `protobuf:"varint,7,opt,name=Active,proto3" json:"Active,omitempty"`
Organization string `protobuf:"bytes,8,opt,name=Organization,proto3" json:"Organization,omitempty"`
}
func (m *Server) Reset() { *m = Server{} }
@ -341,8 +347,12 @@ func (*AlertRule) ProtoMessage() {}
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{14} }
type User struct {
ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
Provider string `protobuf:"bytes,3,opt,name=Provider,proto3" json:"Provider,omitempty"`
Scheme string `protobuf:"bytes,4,opt,name=Scheme,proto3" json:"Scheme,omitempty"`
Roles []*Role `protobuf:"bytes,5,rep,name=Roles" json:"Roles,omitempty"`
SuperAdmin bool `protobuf:"varint,6,opt,name=SuperAdmin,proto3" json:"SuperAdmin,omitempty"`
}
func (m *User) Reset() { *m = User{} }
@ -350,6 +360,35 @@ func (m *User) String() string { return proto.CompactTextString(m) }
func (*User) ProtoMessage() {}
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{15} }
func (m *User) GetRoles() []*Role {
if m != nil {
return m.Roles
}
return nil
}
type Role struct {
Organization string `protobuf:"bytes,1,opt,name=Organization,proto3" json:"Organization,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
}
func (m *Role) Reset() { *m = Role{} }
func (m *Role) String() string { return proto.CompactTextString(m) }
func (*Role) ProtoMessage() {}
func (*Role) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{16} }
type Organization struct {
ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
DefaultRole string `protobuf:"bytes,3,opt,name=DefaultRole,proto3" json:"DefaultRole,omitempty"`
Public bool `protobuf:"varint,4,opt,name=Public,proto3" json:"Public,omitempty"`
}
func (m *Organization) Reset() { *m = Organization{} }
func (m *Organization) String() string { return proto.CompactTextString(m) }
func (*Organization) ProtoMessage() {}
func (*Organization) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{17} }
func init() {
proto.RegisterType((*Source)(nil), "internal.Source")
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
@ -367,81 +406,91 @@ func init() {
proto.RegisterType((*Range)(nil), "internal.Range")
proto.RegisterType((*AlertRule)(nil), "internal.AlertRule")
proto.RegisterType((*User)(nil), "internal.User")
proto.RegisterType((*Role)(nil), "internal.Role")
proto.RegisterType((*Organization)(nil), "internal.Organization")
}
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
var fileDescriptorInternal = []byte{
// 1134 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x56, 0xcf, 0x8e, 0xe3, 0xc4,
0x13, 0x96, 0x63, 0x3b, 0x89, 0x2b, 0xbb, 0xf3, 0x5b, 0xf5, 0x6f, 0xc5, 0x9a, 0xe5, 0x12, 0x2c,
0x10, 0xe1, 0xcf, 0x0e, 0x68, 0x57, 0x48, 0x88, 0x5b, 0x66, 0x82, 0x96, 0x61, 0x66, 0x97, 0x99,
0xce, 0xcc, 0x70, 0x42, 0xab, 0x8e, 0x53, 0x49, 0xac, 0x75, 0x6c, 0xd3, 0xb6, 0x67, 0xe2, 0xb7,
0xe0, 0x09, 0x90, 0x90, 0x38, 0x73, 0xe0, 0x05, 0xb8, 0x73, 0xe5, 0x61, 0xb8, 0xa2, 0xea, 0x6e,
0x3b, 0xce, 0xec, 0x2c, 0xda, 0x03, 0xe2, 0xd6, 0x5f, 0x55, 0xa7, 0xaa, 0xba, 0xea, 0xab, 0x2f,
0x86, 0xbd, 0x28, 0x29, 0x50, 0x26, 0x22, 0xde, 0xcf, 0x64, 0x5a, 0xa4, 0xac, 0x5f, 0xe3, 0xe0,
0xd7, 0x0e, 0x74, 0xa7, 0x69, 0x29, 0x43, 0x64, 0x7b, 0xd0, 0x39, 0x9a, 0xf8, 0xd6, 0xd0, 0x1a,
0xd9, 0xbc, 0x73, 0x34, 0x61, 0x0c, 0x9c, 0xe7, 0x62, 0x8d, 0x7e, 0x67, 0x68, 0x8d, 0x3c, 0xae,
0xce, 0x64, 0x3b, 0xaf, 0x32, 0xf4, 0x6d, 0x6d, 0xa3, 0x33, 0x7b, 0x08, 0xfd, 0x8b, 0x9c, 0xa2,
0xad, 0xd1, 0x77, 0x94, 0xbd, 0xc1, 0xe4, 0x3b, 0x15, 0x79, 0x7e, 0x9d, 0xca, 0xb9, 0xef, 0x6a,
0x5f, 0x8d, 0xd9, 0x3d, 0xb0, 0x2f, 0xf8, 0x89, 0xdf, 0x55, 0x66, 0x3a, 0x32, 0x1f, 0x7a, 0x13,
0x5c, 0x88, 0x32, 0x2e, 0xfc, 0xde, 0xd0, 0x1a, 0xf5, 0x79, 0x0d, 0x29, 0xce, 0x39, 0xc6, 0xb8,
0x94, 0x62, 0xe1, 0xf7, 0x75, 0x9c, 0x1a, 0xb3, 0x7d, 0x60, 0x47, 0x49, 0x8e, 0x61, 0x29, 0x71,
0xfa, 0x32, 0xca, 0x2e, 0x51, 0x46, 0x8b, 0xca, 0xf7, 0x54, 0x80, 0x5b, 0x3c, 0x94, 0xe5, 0x19,
0x16, 0x82, 0x72, 0x83, 0x0a, 0x55, 0x43, 0x16, 0xc0, 0x9d, 0xe9, 0x4a, 0x48, 0x9c, 0x4f, 0x31,
0x94, 0x58, 0xf8, 0x03, 0xe5, 0xde, 0xb1, 0x05, 0x3f, 0x5a, 0xe0, 0x4d, 0x44, 0xbe, 0x9a, 0xa5,
0x42, 0xce, 0xdf, 0xa8, 0x67, 0x8f, 0xc0, 0x0d, 0x31, 0x8e, 0x73, 0xdf, 0x1e, 0xda, 0xa3, 0xc1,
0xe3, 0x07, 0xfb, 0xcd, 0x30, 0x9a, 0x38, 0x87, 0x18, 0xc7, 0x5c, 0xdf, 0x62, 0x9f, 0x81, 0x57,
0xe0, 0x3a, 0x8b, 0x45, 0x81, 0xb9, 0xef, 0xa8, 0x9f, 0xb0, 0xed, 0x4f, 0xce, 0x8d, 0x8b, 0x6f,
0x2f, 0x05, 0x7f, 0x76, 0xe0, 0xee, 0x4e, 0x28, 0x76, 0x07, 0xac, 0x8d, 0xaa, 0xca, 0xe5, 0xd6,
0x86, 0x50, 0xa5, 0x2a, 0x72, 0xb9, 0x55, 0x11, 0xba, 0x56, 0xf3, 0x73, 0xb9, 0x75, 0x4d, 0x68,
0xa5, 0xa6, 0xe6, 0x72, 0x6b, 0xc5, 0x3e, 0x84, 0xde, 0x0f, 0x25, 0xca, 0x08, 0x73, 0xdf, 0x55,
0x99, 0xff, 0xb7, 0xcd, 0x7c, 0x56, 0xa2, 0xac, 0x78, 0xed, 0xa7, 0x97, 0xaa, 0x89, 0xeb, 0xf1,
0xa9, 0x33, 0xd9, 0x0a, 0x62, 0x47, 0x4f, 0xdb, 0xe8, 0x6c, 0x3a, 0xa4, 0x67, 0x46, 0x1d, 0xfa,
0x1c, 0x1c, 0xb1, 0xc1, 0xdc, 0xf7, 0x54, 0xfc, 0x77, 0x5f, 0xd3, 0x8c, 0xfd, 0xf1, 0x06, 0xf3,
0xaf, 0x92, 0x42, 0x56, 0x5c, 0x5d, 0x67, 0x1f, 0x40, 0x37, 0x4c, 0xe3, 0x54, 0xe6, 0x3e, 0xdc,
0x2c, 0xec, 0x90, 0xec, 0xdc, 0xb8, 0x1f, 0x3e, 0x05, 0xaf, 0xf9, 0x2d, 0x51, 0xec, 0x25, 0x56,
0xaa, 0x13, 0x1e, 0xa7, 0x23, 0x7b, 0x0f, 0xdc, 0x2b, 0x11, 0x97, 0x7a, 0x42, 0x83, 0xc7, 0x7b,
0xdb, 0x30, 0xe3, 0x4d, 0x94, 0x73, 0xed, 0xfc, 0xb2, 0xf3, 0x85, 0x15, 0x2c, 0xc1, 0x55, 0x91,
0x5b, 0x33, 0xf6, 0xea, 0x19, 0xab, 0x1d, 0xe8, 0xb4, 0x76, 0xe0, 0x1e, 0xd8, 0x5f, 0xe3, 0xc6,
0xac, 0x05, 0x1d, 0x1b, 0x26, 0x38, 0x2d, 0x26, 0xdc, 0x07, 0xf7, 0x52, 0x25, 0xd7, 0xab, 0xa0,
0x41, 0xf0, 0x9b, 0x05, 0x0e, 0x25, 0x27, 0xfa, 0xc5, 0xb8, 0x14, 0x61, 0x75, 0x90, 0x96, 0xc9,
0x3c, 0xf7, 0xad, 0xa1, 0x3d, 0xb2, 0xf9, 0x8e, 0x8d, 0xbd, 0x05, 0xdd, 0x99, 0xf6, 0x76, 0x86,
0xf6, 0xc8, 0xe3, 0x06, 0x51, 0xe8, 0x58, 0xcc, 0x30, 0x36, 0x25, 0x68, 0x40, 0xb7, 0x33, 0x89,
0x8b, 0x68, 0x63, 0xca, 0x30, 0x88, 0xec, 0x79, 0xb9, 0x20, 0xbb, 0xae, 0xc4, 0x20, 0x2a, 0x7a,
0x26, 0xf2, 0x66, 0xa8, 0x74, 0xa6, 0xc8, 0x79, 0x28, 0xe2, 0x7a, 0xaa, 0x1a, 0x04, 0xbf, 0x5b,
0xb4, 0x91, 0x9a, 0x81, 0xaf, 0x74, 0xe8, 0x6d, 0xe8, 0x13, 0x3b, 0x5f, 0x5c, 0x09, 0x69, 0xba,
0xd4, 0x23, 0x7c, 0x29, 0x24, 0xfb, 0x14, 0xba, 0xaa, 0xc5, 0xb7, 0x6c, 0x43, 0x1d, 0x4e, 0x75,
0x85, 0x9b, 0x6b, 0x0d, 0xa7, 0x9c, 0x16, 0xa7, 0x9a, 0xc7, 0xba, 0xed, 0xc7, 0x3e, 0x02, 0x97,
0xc8, 0x59, 0xa9, 0xea, 0x6f, 0x8d, 0xac, 0x29, 0xac, 0x6f, 0x05, 0x17, 0x70, 0x77, 0x27, 0x63,
0x93, 0xc9, 0xda, 0xcd, 0xb4, 0xa5, 0x8b, 0x67, 0xe8, 0x41, 0x6a, 0x94, 0x63, 0x8c, 0x61, 0x81,
0x73, 0xd5, 0xef, 0x3e, 0x6f, 0x70, 0xf0, 0xb3, 0xb5, 0x8d, 0xab, 0xf2, 0x91, 0xde, 0x84, 0xe9,
0x7a, 0x2d, 0x92, 0xb9, 0x09, 0x5d, 0x43, 0xea, 0xdb, 0x7c, 0x66, 0x42, 0x77, 0xe6, 0x33, 0xc2,
0x32, 0x33, 0x13, 0xec, 0xc8, 0x8c, 0x0d, 0x61, 0xb0, 0x46, 0x91, 0x97, 0x12, 0xd7, 0x98, 0x14,
0xa6, 0x05, 0x6d, 0x13, 0x7b, 0x00, 0xbd, 0x42, 0x2c, 0x5f, 0x10, 0xc9, 0xcd, 0x24, 0x0b, 0xb1,
0x3c, 0xc6, 0x8a, 0xbd, 0x03, 0xde, 0x22, 0xc2, 0x78, 0xae, 0x5c, 0x7a, 0x9c, 0x7d, 0x65, 0x38,
0xc6, 0x2a, 0xf8, 0xc5, 0x82, 0xee, 0x14, 0xe5, 0x15, 0xca, 0x37, 0x12, 0xb0, 0xb6, 0xc0, 0xdb,
0xff, 0x20, 0xf0, 0xce, 0xed, 0x02, 0xef, 0x6e, 0x05, 0xfe, 0x3e, 0xb8, 0x53, 0x19, 0x1e, 0x4d,
0x54, 0x45, 0x36, 0xd7, 0x80, 0xd8, 0x38, 0x0e, 0x8b, 0xe8, 0x0a, 0x8d, 0xea, 0x1b, 0x14, 0xfc,
0x64, 0x41, 0xf7, 0x44, 0x54, 0x69, 0x59, 0xbc, 0xc2, 0xb0, 0x21, 0x0c, 0xc6, 0x59, 0x16, 0x47,
0xa1, 0x28, 0xa2, 0x34, 0x31, 0xd5, 0xb6, 0x4d, 0x74, 0xe3, 0x59, 0xab, 0x77, 0xba, 0xee, 0xb6,
0x89, 0xa4, 0xe0, 0x50, 0xe9, 0xb2, 0x16, 0xd9, 0x96, 0x14, 0x68, 0x39, 0x56, 0x4e, 0x7a, 0xe0,
0xb8, 0x2c, 0xd2, 0x45, 0x9c, 0x5e, 0xab, 0x97, 0xf4, 0x79, 0x83, 0x83, 0x3f, 0x3a, 0xe0, 0xfc,
0x57, 0x7a, 0x7b, 0x07, 0xac, 0xc8, 0x0c, 0xd2, 0x8a, 0x1a, 0xf5, 0xed, 0xb5, 0xd4, 0xd7, 0x87,
0x5e, 0x25, 0x45, 0xb2, 0xc4, 0xdc, 0xef, 0x2b, 0xe5, 0xa8, 0xa1, 0xf2, 0xa8, 0x1d, 0xd1, 0xb2,
0xeb, 0xf1, 0x1a, 0x36, 0x9c, 0x87, 0x16, 0xe7, 0x3f, 0x31, 0x0a, 0x3d, 0x50, 0x15, 0xf9, 0xbb,
0x6d, 0xb9, 0x29, 0xcc, 0xff, 0x9e, 0xde, 0xfe, 0x65, 0x81, 0xdb, 0x2c, 0xcc, 0xe1, 0xee, 0xc2,
0x1c, 0x6e, 0x17, 0x66, 0x72, 0x50, 0x2f, 0xcc, 0xe4, 0x80, 0x30, 0x3f, 0xad, 0x17, 0x86, 0x9f,
0xd2, 0xb0, 0x9e, 0xca, 0xb4, 0xcc, 0x0e, 0x2a, 0x3d, 0x55, 0x8f, 0x37, 0x98, 0x58, 0xf6, 0xdd,
0x0a, 0xa5, 0x69, 0xb5, 0xc7, 0x0d, 0x22, 0x4e, 0x9e, 0x28, 0x31, 0xd1, 0xcd, 0xd5, 0x80, 0xbd,
0x0f, 0x2e, 0xa7, 0xe6, 0xa9, 0x0e, 0xef, 0xcc, 0x45, 0x99, 0xb9, 0xf6, 0x52, 0x50, 0xfd, 0xf5,
0x64, 0xfe, 0xe1, 0xea, 0x6f, 0xa9, 0x8f, 0xa1, 0x3b, 0x5d, 0x45, 0x8b, 0xa2, 0xfe, 0x9f, 0xfb,
0x7f, 0x4b, 0x8c, 0xa2, 0x35, 0x2a, 0x1f, 0x37, 0x57, 0x82, 0x33, 0xf0, 0x1a, 0xe3, 0xb6, 0x1c,
0xab, 0x5d, 0x0e, 0x03, 0xe7, 0x22, 0x89, 0x8a, 0x7a, 0x2d, 0xe9, 0x4c, 0x8f, 0x3d, 0x2b, 0x45,
0x52, 0x44, 0x45, 0x55, 0xaf, 0x65, 0x8d, 0x83, 0x27, 0xa6, 0x7c, 0x0a, 0x77, 0x91, 0x65, 0x28,
0xcd, 0x8a, 0x6b, 0xa0, 0x92, 0xa4, 0xd7, 0xa8, 0xd5, 0xd9, 0xe6, 0x1a, 0x04, 0xdf, 0x83, 0x37,
0x8e, 0x51, 0x16, 0xbc, 0x8c, 0xf1, 0xb6, 0x7f, 0xbd, 0x6f, 0xa6, 0xdf, 0x3e, 0xaf, 0x2b, 0xa0,
0xf3, 0x76, 0x9d, 0xed, 0x1b, 0xeb, 0x7c, 0x2c, 0x32, 0x71, 0x34, 0x51, 0x3c, 0xb7, 0xb9, 0x41,
0xc1, 0x47, 0xe0, 0x90, 0x6c, 0xb4, 0x22, 0x3b, 0xaf, 0x93, 0x9c, 0x59, 0x57, 0x7d, 0xa7, 0x3e,
0xf9, 0x3b, 0x00, 0x00, 0xff, 0xff, 0xe5, 0xc0, 0x79, 0x31, 0xb9, 0x0a, 0x00, 0x00,
// 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,
}

View File

@ -13,6 +13,8 @@ message Source {
bool InsecureSkipVerify = 9; // InsecureSkipVerify accepts any certificate from the influx server
string MetaURL = 10; // MetaURL is the connection URL for the meta node.
string SharedSecret = 11; // SharedSecret signs the optional InfluxDB JWT Authorization
string Organization = 12; // Organization is the organization ID that resource belongs to
string Role = 13; // Role is the name of the miniumum role that a user must possess to access the resource
}
message Dashboard {
@ -20,6 +22,7 @@ message Dashboard {
string Name = 2; // Name is the user-defined name of the dashboard
repeated DashboardCell cells = 3; // a representation of all visual data required for rendering the dashboard
repeated Template templates = 4; // Templates replace template variables within InfluxQL
string Organization = 5; // Organization is the organization ID that resource belongs to
}
message DashboardCell {
@ -85,6 +88,7 @@ message Server {
string URL = 5; // URL is the path to the server
int64 SrcID = 6; // SrcID is the ID of the data source
bool Active = 7; // is this the currently active server for the source
string Organization = 8; // Organization is the organization ID that resource belongs to
}
message Layout {
@ -140,8 +144,24 @@ message AlertRule {
}
message User {
uint64 ID = 1; // ID is the unique ID of this user
string Name = 2; // Name is the user's login name
uint64 ID = 1; // ID is the unique ID of this user
string Name = 2; // Name is the user's login name
string Provider = 3; // Provider is the provider that certifies and issues this user's authentication, e.g. GitHub
string Scheme = 4; // Scheme is the scheme used to perform this user's authentication, e.g. OAuth2 or LDAP
repeated Role Roles = 5; // Roles is set of roles a user has
bool SuperAdmin = 6; // SuperAdmin is bool that specifies whether a user is a super admin
}
message Role {
string Organization = 1; // Organization is the ID of the organization that this user has a role in
string Name = 2; // Name is the name of the role of this user in the respective organization
}
message Organization {
uint64 ID = 1; // ID is the unique ID of the organization
string Name = 2; // Name is the organization's name
string DefaultRole = 3; // DefaultRole is the name of the role that is the default for any users added to the organization
bool Public = 4; // Public specifies that users must be explicitly added to the organization
}
// The following is a vim modeline, it autoconfigures vim to have the

View File

@ -8,23 +8,27 @@ import (
"github.com/influxdata/chronograf/bolt/internal"
)
// Ensure LayoutStore implements chronograf.LayoutStore.
var _ chronograf.LayoutStore = &LayoutStore{}
// Ensure LayoutsStore implements chronograf.LayoutsStore.
var _ chronograf.LayoutsStore = &LayoutsStore{}
// LayoutBucket is the bolt bucket layouts are stored in
var LayoutBucket = []byte("Layout")
// LayoutsBucket is the bolt bucket layouts are stored in
var LayoutsBucket = []byte("Layout")
// LayoutStore is the bolt implementation to store layouts
type LayoutStore struct {
// LayoutsStore is the bolt implementation to store layouts
type LayoutsStore struct {
client *Client
IDs chronograf.ID
}
func (s *LayoutsStore) Migrate(ctx context.Context) error {
return nil
}
// All returns all known layouts
func (s *LayoutStore) All(ctx context.Context) ([]chronograf.Layout, error) {
func (s *LayoutsStore) All(ctx context.Context) ([]chronograf.Layout, error) {
var srcs []chronograf.Layout
if err := s.client.db.View(func(tx *bolt.Tx) error {
if err := tx.Bucket(LayoutBucket).ForEach(func(k, v []byte) error {
if err := tx.Bucket(LayoutsBucket).ForEach(func(k, v []byte) error {
var src chronograf.Layout
if err := internal.UnmarshalLayout(v, &src); err != nil {
return err
@ -43,10 +47,10 @@ func (s *LayoutStore) All(ctx context.Context) ([]chronograf.Layout, error) {
}
// Add creates a new Layout in the LayoutStore.
func (s *LayoutStore) Add(ctx context.Context, src chronograf.Layout) (chronograf.Layout, error) {
// Add creates a new Layout in the LayoutsStore.
func (s *LayoutsStore) Add(ctx context.Context, src chronograf.Layout) (chronograf.Layout, error) {
if err := s.client.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(LayoutBucket)
b := tx.Bucket(LayoutsBucket)
id, err := s.IDs.Generate()
if err != nil {
return err
@ -66,14 +70,14 @@ func (s *LayoutStore) Add(ctx context.Context, src chronograf.Layout) (chronogra
return src, nil
}
// Delete removes the Layout from the LayoutStore
func (s *LayoutStore) Delete(ctx context.Context, src chronograf.Layout) error {
// Delete removes the Layout from the LayoutsStore
func (s *LayoutsStore) Delete(ctx context.Context, src chronograf.Layout) error {
_, err := s.Get(ctx, src.ID)
if err != nil {
return err
}
if err := s.client.db.Update(func(tx *bolt.Tx) error {
if err := tx.Bucket(LayoutBucket).Delete([]byte(src.ID)); err != nil {
if err := tx.Bucket(LayoutsBucket).Delete([]byte(src.ID)); err != nil {
return err
}
return nil
@ -85,10 +89,10 @@ func (s *LayoutStore) Delete(ctx context.Context, src chronograf.Layout) error {
}
// Get returns a Layout if the id exists.
func (s *LayoutStore) Get(ctx context.Context, id string) (chronograf.Layout, error) {
func (s *LayoutsStore) Get(ctx context.Context, id string) (chronograf.Layout, error) {
var src chronograf.Layout
if err := s.client.db.View(func(tx *bolt.Tx) error {
if v := tx.Bucket(LayoutBucket).Get([]byte(id)); v == nil {
if v := tx.Bucket(LayoutsBucket).Get([]byte(id)); v == nil {
return chronograf.ErrLayoutNotFound
} else if err := internal.UnmarshalLayout(v, &src); err != nil {
return err
@ -102,10 +106,10 @@ func (s *LayoutStore) Get(ctx context.Context, id string) (chronograf.Layout, er
}
// Update a Layout
func (s *LayoutStore) Update(ctx context.Context, src chronograf.Layout) error {
func (s *LayoutsStore) Update(ctx context.Context, src chronograf.Layout) error {
if err := s.client.db.Update(func(tx *bolt.Tx) error {
// Get an existing layout with the same ID.
b := tx.Bucket(LayoutBucket)
b := tx.Bucket(LayoutsBucket)
if v := b.Get([]byte(src.ID)); v == nil {
return chronograf.ErrLayoutNotFound
}

278
bolt/organizations.go Normal file
View File

@ -0,0 +1,278 @@
package bolt
import (
"context"
"fmt"
"github.com/boltdb/bolt"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/bolt/internal"
"github.com/influxdata/chronograf/organizations"
)
// Ensure OrganizationsStore implements chronograf.OrganizationsStore.
var _ chronograf.OrganizationsStore = &OrganizationsStore{}
// OrganizationsBucket is the bucket where organizations are stored.
var OrganizationsBucket = []byte("OrganizationsV1")
const (
// DefaultOrganizationID is the ID of the default organization.
DefaultOrganizationID uint64 = 0
// DefaultOrganizationName is the Name of the default organization
DefaultOrganizationName string = "Default"
// DefaultOrganizationRole is the DefaultRole for the Default organization
DefaultOrganizationRole string = "member"
// DefaultOrganizationPublic is the Public setting for the Default organization.
DefaultOrganizationPublic bool = true
)
// OrganizationsStore uses bolt to store and retrieve Organizations
type OrganizationsStore struct {
client *Client
}
// Migrate sets the default organization at runtime
func (s *OrganizationsStore) Migrate(ctx context.Context) error {
return s.CreateDefault(ctx)
}
// CreateDefault does a findOrCreate on the default organization
func (s *OrganizationsStore) CreateDefault(ctx context.Context) error {
o := chronograf.Organization{
ID: DefaultOrganizationID,
Name: DefaultOrganizationName,
DefaultRole: DefaultOrganizationRole,
Public: DefaultOrganizationPublic,
}
return s.client.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(OrganizationsBucket)
v := b.Get(u64tob(o.ID))
if v != nil {
return nil
}
if v, err := internal.MarshalOrganization(&o); err != nil {
return err
} else if err := b.Put(u64tob(o.ID), v); err != nil {
return err
}
return nil
})
}
func (s *OrganizationsStore) nameIsUnique(ctx context.Context, name string) bool {
_, err := s.Get(ctx, chronograf.OrganizationQuery{Name: &name})
switch err {
case chronograf.ErrOrganizationNotFound:
return true
default:
return false
}
}
// DefaultOrganizationID returns the ID of the default organization
func (s *OrganizationsStore) DefaultOrganization(ctx context.Context) (*chronograf.Organization, error) {
var org chronograf.Organization
if err := s.client.db.View(func(tx *bolt.Tx) error {
v := tx.Bucket(OrganizationsBucket).Get(u64tob(DefaultOrganizationID))
return internal.UnmarshalOrganization(v, &org)
}); err != nil {
return nil, err
}
return &org, nil
}
// Add creates a new Organization in the OrganizationsStore
func (s *OrganizationsStore) Add(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) {
if !s.nameIsUnique(ctx, o.Name) {
return nil, chronograf.ErrOrganizationAlreadyExists
}
if err := s.client.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(OrganizationsBucket)
seq, err := b.NextSequence()
if err != nil {
return err
}
o.ID = seq
if v, err := internal.MarshalOrganization(o); err != nil {
return err
} else if err := b.Put(u64tob(seq), v); err != nil {
return err
}
return nil
}); err != nil {
return nil, err
}
return o, nil
}
// All returns all known organizations
func (s *OrganizationsStore) All(ctx context.Context) ([]chronograf.Organization, error) {
var orgs []chronograf.Organization
err := s.each(ctx, func(o *chronograf.Organization) {
orgs = append(orgs, *o)
})
if err != nil {
return nil, err
}
return orgs, nil
}
// Delete the organization from OrganizationsStore
func (s *OrganizationsStore) Delete(ctx context.Context, o *chronograf.Organization) error {
if o.ID == DefaultOrganizationID {
return chronograf.ErrCannotDeleteDefaultOrganization
}
_, err := s.get(ctx, o.ID)
if err != nil {
return err
}
if err := s.client.db.Update(func(tx *bolt.Tx) error {
return tx.Bucket(OrganizationsBucket).Delete(u64tob(o.ID))
}); err != nil {
return err
}
// Dependent Delete of all resources
org := fmt.Sprintf("%d", o.ID)
// Each of the associated organization stores expects organization to be
// set on the context.
ctx = context.WithValue(ctx, organizations.ContextKey, org)
sourcesStore := organizations.NewSourcesStore(s.client.SourcesStore, org)
sources, err := sourcesStore.All(ctx)
if err != nil {
return err
}
for _, source := range sources {
if err := sourcesStore.Delete(ctx, source); err != nil {
return err
}
}
serversStore := organizations.NewServersStore(s.client.ServersStore, org)
servers, err := serversStore.All(ctx)
if err != nil {
return err
}
for _, server := range servers {
if err := serversStore.Delete(ctx, server); err != nil {
return err
}
}
dashboardsStore := organizations.NewDashboardsStore(s.client.DashboardsStore, org)
dashboards, err := dashboardsStore.All(ctx)
if err != nil {
return err
}
for _, dashboard := range dashboards {
if err := dashboardsStore.Delete(ctx, dashboard); err != nil {
return err
}
}
usersStore := organizations.NewUsersStore(s.client.UsersStore, org)
users, err := usersStore.All(ctx)
if err != nil {
return err
}
for _, user := range users {
if err := usersStore.Delete(ctx, &user); err != nil {
return err
}
}
return nil
}
func (s *OrganizationsStore) get(ctx context.Context, id uint64) (*chronograf.Organization, error) {
var o chronograf.Organization
err := s.client.db.View(func(tx *bolt.Tx) error {
v := tx.Bucket(OrganizationsBucket).Get(u64tob(id))
if v == nil {
return chronograf.ErrOrganizationNotFound
}
return internal.UnmarshalOrganization(v, &o)
})
if err != nil {
return nil, err
}
return &o, nil
}
func (s *OrganizationsStore) each(ctx context.Context, fn func(*chronograf.Organization)) error {
return s.client.db.View(func(tx *bolt.Tx) error {
return tx.Bucket(OrganizationsBucket).ForEach(func(k, v []byte) error {
var org chronograf.Organization
if err := internal.UnmarshalOrganization(v, &org); err != nil {
return err
}
fn(&org)
return nil
})
})
return nil
}
// Get returns a Organization if the id exists.
// If an ID is provided in the query, the lookup time for an organization will be O(1).
// If Name is provided, the lookup time will be O(n).
// Get expects that only one of ID or Name will be specified, but will prefer ID over Name if both are specified.
func (s *OrganizationsStore) Get(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
if q.ID != nil {
return s.get(ctx, *q.ID)
}
if q.Name != nil {
var org *chronograf.Organization
err := s.each(ctx, func(o *chronograf.Organization) {
if org != nil {
return
}
if o.Name == *q.Name {
org = o
}
})
if err != nil {
return nil, err
}
if org == nil {
return nil, chronograf.ErrOrganizationNotFound
}
return org, nil
}
return nil, fmt.Errorf("must specify either ID, or Name in OrganizationQuery")
}
// Update the organization in OrganizationsStore
func (s *OrganizationsStore) Update(ctx context.Context, o *chronograf.Organization) error {
org, err := s.get(ctx, o.ID)
if err != nil {
return err
}
if o.Name != org.Name && !s.nameIsUnique(ctx, o.Name) {
return chronograf.ErrOrganizationAlreadyExists
}
return s.client.db.Update(func(tx *bolt.Tx) error {
if v, err := internal.MarshalOrganization(o); err != nil {
return err
} else if err := tx.Bucket(OrganizationsBucket).Put(u64tob(o.ID), v); err != nil {
return err
}
return nil
})
}

678
bolt/organizations_test.go Normal file
View File

@ -0,0 +1,678 @@
package bolt_test
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/bolt"
"github.com/influxdata/chronograf/roles"
)
var orgCmpOptions = cmp.Options{
cmpopts.IgnoreFields(chronograf.Organization{}, "ID"),
cmpopts.EquateEmpty(),
}
func TestOrganizationsStore_GetWithName(t *testing.T) {
type args struct {
ctx context.Context
org *chronograf.Organization
}
tests := []struct {
name string
args args
want *chronograf.Organization
wantErr bool
addFirst bool
}{
{
name: "Organization not found",
args: args{
ctx: context.Background(),
org: &chronograf.Organization{},
},
wantErr: true,
},
{
name: "Get Organization",
args: args{
ctx: context.Background(),
org: &chronograf.Organization{
Name: "EE - Evil Empire",
},
},
want: &chronograf.Organization{
Name: "EE - Evil Empire",
},
addFirst: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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.OrganizationsStore
if tt.addFirst {
tt.args.org, err = s.Add(tt.args.ctx, tt.args.org)
if err != nil {
t.Fatal(err)
}
}
got, err := s.Get(tt.args.ctx, chronograf.OrganizationQuery{Name: &tt.args.org.Name})
if (err != nil) != tt.wantErr {
t.Errorf("%q. OrganizationsStore.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
if tt.wantErr {
return
}
if diff := cmp.Diff(got, tt.want, orgCmpOptions...); diff != "" {
t.Errorf("%q. OrganizationsStore.Get():\n-got/+want\ndiff %s", tt.name, diff)
}
})
}
}
func TestOrganizationsStore_GetWithID(t *testing.T) {
type args struct {
ctx context.Context
org *chronograf.Organization
}
tests := []struct {
name string
args args
want *chronograf.Organization
wantErr bool
addFirst bool
}{
{
name: "Organization not found",
args: args{
ctx: context.Background(),
org: &chronograf.Organization{
ID: 1234,
},
},
wantErr: true,
},
{
name: "Get Organization",
args: args{
ctx: context.Background(),
org: &chronograf.Organization{
Name: "EE - Evil Empire",
},
},
want: &chronograf.Organization{
Name: "EE - Evil Empire",
},
addFirst: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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.OrganizationsStore
if tt.addFirst {
tt.args.org, err = s.Add(tt.args.ctx, tt.args.org)
if err != nil {
t.Fatal(err)
}
}
got, err := s.Get(tt.args.ctx, chronograf.OrganizationQuery{ID: &tt.args.org.ID})
if (err != nil) != tt.wantErr {
t.Errorf("%q. OrganizationsStore.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr)
return
}
if tt.wantErr {
return
}
if diff := cmp.Diff(got, tt.want, orgCmpOptions...); diff != "" {
t.Errorf("%q. OrganizationsStore.Get():\n-got/+want\ndiff %s", tt.name, diff)
}
})
}
}
func TestOrganizationsStore_All(t *testing.T) {
type args struct {
ctx context.Context
orgs []chronograf.Organization
}
tests := []struct {
name string
args args
want []chronograf.Organization
addFirst bool
}{
{
name: "Get Organizations",
args: args{
ctx: context.Background(),
orgs: []chronograf.Organization{
{
Name: "EE - Evil Empire",
DefaultRole: roles.MemberRoleName,
Public: true,
},
{
Name: "The Good Place",
DefaultRole: roles.EditorRoleName,
Public: true,
},
},
},
want: []chronograf.Organization{
{
Name: bolt.DefaultOrganizationName,
DefaultRole: bolt.DefaultOrganizationRole,
Public: bolt.DefaultOrganizationPublic,
},
{
Name: "EE - Evil Empire",
DefaultRole: roles.MemberRoleName,
Public: true,
},
{
Name: "The Good Place",
DefaultRole: roles.EditorRoleName,
Public: true,
},
},
addFirst: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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.OrganizationsStore
if tt.addFirst {
for _, org := range tt.args.orgs {
_, err = s.Add(tt.args.ctx, &org)
if err != nil {
t.Fatal(err)
}
}
}
got, err := s.All(tt.args.ctx)
if err != nil {
t.Fatal(err)
return
}
if diff := cmp.Diff(got, tt.want, orgCmpOptions...); diff != "" {
t.Errorf("%q. OrganizationsStore.All():\n-got/+want\ndiff %s", tt.name, diff)
}
})
}
}
func TestOrganizationsStore_Update(t *testing.T) {
type fields struct {
orgs []chronograf.Organization
}
type args struct {
ctx context.Context
initial *chronograf.Organization
updates *chronograf.Organization
}
tests := []struct {
name string
fields fields
args args
addFirst bool
want *chronograf.Organization
wantErr bool
}{
{
name: "No such organization",
fields: fields{},
args: args{
ctx: context.Background(),
initial: &chronograf.Organization{
ID: 1234,
Name: "The Okay Place",
},
updates: &chronograf.Organization{},
},
wantErr: true,
},
{
name: "Update organization name",
fields: fields{},
args: args{
ctx: context.Background(),
initial: &chronograf.Organization{
Name: "The Good Place",
},
updates: &chronograf.Organization{
Name: "The Bad Place",
},
},
want: &chronograf.Organization{
Name: "The Bad Place",
},
addFirst: true,
},
{
name: "Update organization default role",
fields: fields{},
args: args{
ctx: context.Background(),
initial: &chronograf.Organization{
Name: "The Good Place",
},
updates: &chronograf.Organization{
DefaultRole: roles.ViewerRoleName,
},
},
want: &chronograf.Organization{
Name: "The Good Place",
DefaultRole: roles.ViewerRoleName,
},
addFirst: true,
},
{
name: "Update organization name and default role",
fields: fields{},
args: args{
ctx: context.Background(),
initial: &chronograf.Organization{
Name: "The Good Place",
DefaultRole: roles.AdminRoleName,
},
updates: &chronograf.Organization{
Name: "The Bad Place",
DefaultRole: roles.ViewerRoleName,
},
},
want: &chronograf.Organization{
Name: "The Bad Place",
DefaultRole: roles.ViewerRoleName,
},
addFirst: true,
},
{
name: "Update organization name, role, public",
fields: fields{},
args: args{
ctx: context.Background(),
initial: &chronograf.Organization{
Name: "The Good Place",
DefaultRole: roles.ViewerRoleName,
Public: false,
},
updates: &chronograf.Organization{
Name: "The Bad Place",
Public: true,
DefaultRole: roles.AdminRoleName,
},
},
want: &chronograf.Organization{
Name: "The Bad Place",
Public: true,
DefaultRole: roles.AdminRoleName,
},
addFirst: true,
},
{
name: "Update organization name and public",
fields: fields{},
args: args{
ctx: context.Background(),
initial: &chronograf.Organization{
Name: "The Good Place",
DefaultRole: roles.EditorRoleName,
Public: false,
},
updates: &chronograf.Organization{
Name: "The Bad Place",
Public: true,
},
},
want: &chronograf.Organization{
Name: "The Bad Place",
DefaultRole: roles.EditorRoleName,
Public: true,
},
addFirst: true,
},
{
name: "Update organization name - organization already exists",
fields: fields{
orgs: []chronograf.Organization{
{
Name: "The Bad Place",
},
},
},
args: args{
ctx: context.Background(),
initial: &chronograf.Organization{
Name: "The Good Place",
},
updates: &chronograf.Organization{
Name: "The Bad Place",
},
},
wantErr: true,
addFirst: 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.OrganizationsStore
for _, org := range tt.fields.orgs {
_, err = s.Add(tt.args.ctx, &org)
if err != nil {
t.Fatal(err)
}
}
if tt.addFirst {
tt.args.initial, err = s.Add(tt.args.ctx, tt.args.initial)
}
if tt.args.updates.Name != "" {
tt.args.initial.Name = tt.args.updates.Name
}
if tt.args.updates.DefaultRole != "" {
tt.args.initial.DefaultRole = tt.args.updates.DefaultRole
}
if tt.args.updates.Public != tt.args.initial.Public {
tt.args.initial.Public = tt.args.updates.Public
}
if err := s.Update(tt.args.ctx, tt.args.initial); (err != nil) != tt.wantErr {
t.Errorf("%q. OrganizationsStore.Update() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
// for the empty test
if tt.want == nil {
continue
}
got, err := s.Get(tt.args.ctx, chronograf.OrganizationQuery{Name: &tt.args.initial.Name})
if err != nil {
t.Fatalf("failed to get organization: %v", err)
}
if diff := cmp.Diff(got, tt.want, orgCmpOptions...); diff != "" {
t.Errorf("%q. OrganizationsStore.Update():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
func TestOrganizationStore_Delete(t *testing.T) {
type args struct {
ctx context.Context
org *chronograf.Organization
}
tests := []struct {
name string
args args
addFirst bool
wantErr bool
}{
{
name: "No such organization",
args: args{
ctx: context.Background(),
org: &chronograf.Organization{
ID: 10,
},
},
wantErr: true,
},
{
name: "Delete new organization",
args: args{
ctx: context.Background(),
org: &chronograf.Organization{
Name: "The Deleted Place",
},
},
addFirst: 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.OrganizationsStore
if tt.addFirst {
tt.args.org, _ = s.Add(tt.args.ctx, tt.args.org)
}
if err := s.Delete(tt.args.ctx, tt.args.org); (err != nil) != tt.wantErr {
t.Errorf("%q. OrganizationsStore.Delete() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
}
}
func TestOrganizationStore_DeleteDefaultOrg(t *testing.T) {
type args struct {
ctx context.Context
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Delete the default organization",
args: args{
ctx: context.Background(),
},
wantErr: 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.OrganizationsStore
defaultOrg, err := s.DefaultOrganization(tt.args.ctx)
if err != nil {
t.Fatal(err)
}
if err := s.Delete(tt.args.ctx, defaultOrg); (err != nil) != tt.wantErr {
t.Errorf("%q. OrganizationsStore.Delete() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
}
}
func TestOrganizationsStore_Add(t *testing.T) {
type fields struct {
orgs []chronograf.Organization
}
type args struct {
ctx context.Context
org *chronograf.Organization
}
tests := []struct {
name string
fields fields
args args
want *chronograf.Organization
wantErr bool
}{
{
name: "Add organization - organization already exists",
fields: fields{
orgs: []chronograf.Organization{
{
Name: "The Good Place",
},
},
},
args: args{
ctx: context.Background(),
org: &chronograf.Organization{
Name: "The Good Place",
},
},
wantErr: 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.OrganizationsStore
for _, org := range tt.fields.orgs {
_, err = s.Add(tt.args.ctx, &org)
if err != nil {
t.Fatal(err)
}
}
_, err = s.Add(tt.args.ctx, tt.args.org)
if (err != nil) != tt.wantErr {
t.Errorf("%q. OrganizationsStore.Update() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
// for the empty test
if tt.want == nil {
continue
}
got, err := s.Get(tt.args.ctx, chronograf.OrganizationQuery{Name: &tt.args.org.Name})
if err != nil {
t.Fatalf("failed to get organization: %v", err)
}
if diff := cmp.Diff(got, tt.want, orgCmpOptions...); diff != "" {
t.Errorf("%q. OrganizationsStore.Update():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
func TestOrganizationsStore_DefaultOrganization(t *testing.T) {
type fields struct {
orgs []chronograf.Organization
}
type args struct {
ctx context.Context
}
tests := []struct {
name string
fields fields
args args
want *chronograf.Organization
wantErr bool
}{
{
name: "Get Default Organization",
fields: fields{
orgs: []chronograf.Organization{
{
Name: "The Good Place",
},
},
},
args: args{
ctx: context.Background(),
},
want: &chronograf.Organization{
ID: bolt.DefaultOrganizationID,
Name: bolt.DefaultOrganizationName,
DefaultRole: bolt.DefaultOrganizationRole,
Public: bolt.DefaultOrganizationPublic,
},
wantErr: 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.OrganizationsStore
for _, org := range tt.fields.orgs {
_, err = s.Add(tt.args.ctx, &org)
if err != nil {
t.Fatal(err)
}
}
got, err := s.DefaultOrganization(tt.args.ctx)
if (err != nil) != tt.wantErr {
t.Errorf("%q. OrganizationsStore.Update() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
if tt.want == nil {
continue
}
if diff := cmp.Diff(got, tt.want, orgCmpOptions...); diff != "" {
t.Errorf("%q. OrganizationsStore.Update():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}

View File

@ -2,6 +2,7 @@ package bolt
import (
"context"
"fmt"
"github.com/boltdb/bolt"
"github.com/influxdata/chronograf"
@ -20,6 +21,31 @@ type ServersStore struct {
client *Client
}
func (s *ServersStore) Migrate(ctx context.Context) error {
servers, err := s.All(ctx)
if err != nil {
return err
}
defaultOrg, err := s.client.OrganizationsStore.DefaultOrganization(ctx)
if err != nil {
return err
}
defaultOrgID := fmt.Sprintf("%d", defaultOrg.ID)
for _, server := range servers {
if server.Organization == "" {
server.Organization = defaultOrgID
if err := s.Update(ctx, server); err != nil {
return nil
}
}
}
return nil
}
// All returns all known servers
func (s *ServersStore) All(ctx context.Context) ([]chronograf.Server, error) {
var srcs []chronograf.Server

View File

@ -22,20 +22,22 @@ func TestServerStore(t *testing.T) {
srcs := []chronograf.Server{
chronograf.Server{
Name: "Of Truth",
SrcID: 10,
Username: "marty",
Password: "I❤ jennifer parker",
URL: "toyota-hilux.lyon-estates.local",
Active: false,
Name: "Of Truth",
SrcID: 10,
Username: "marty",
Password: "I❤ jennifer parker",
URL: "toyota-hilux.lyon-estates.local",
Active: false,
Organization: "133",
},
chronograf.Server{
Name: "HipToBeSquare",
SrcID: 12,
Username: "calvinklein",
Password: "chuck b3rry",
URL: "toyota-hilux.lyon-estates.local",
Active: false,
Name: "HipToBeSquare",
SrcID: 12,
Username: "calvinklein",
Password: "chuck b3rry",
URL: "toyota-hilux.lyon-estates.local",
Active: false,
Organization: "133",
},
}
@ -56,6 +58,7 @@ func TestServerStore(t *testing.T) {
// Update server.
srcs[0].Username = "calvinklein"
srcs[1].Name = "Enchantment Under the Sea Dance"
srcs[1].Organization = "1234"
if err := s.Update(ctx, srcs[0]); err != nil {
t.Fatal(err)
} else if err := s.Update(ctx, srcs[1]); err != nil {
@ -72,6 +75,8 @@ func TestServerStore(t *testing.T) {
t.Fatal(err)
} else if src.Name != "Enchantment Under the Sea Dance" {
t.Fatalf("server 1 update error: got %v, expected %v", src.Name, "Enchantment Under the Sea Dance")
} else if src.Organization != "1234" {
t.Fatalf("server 1 update error: got %v, expected %v", src.Organization, "1234")
}
// Attempt to make two active sources

View File

@ -2,10 +2,12 @@ package bolt
import (
"context"
"fmt"
"github.com/boltdb/bolt"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/bolt/internal"
"github.com/influxdata/chronograf/roles"
)
// Ensure SourcesStore implements chronograf.SourcesStore.
@ -19,6 +21,34 @@ type SourcesStore struct {
client *Client
}
func (s *SourcesStore) Migrate(ctx context.Context) error {
sources, err := s.All(ctx)
if err != nil {
return err
}
defaultOrg, err := s.client.OrganizationsStore.DefaultOrganization(ctx)
if err != nil {
return err
}
defaultOrgID := fmt.Sprintf("%d", defaultOrg.ID)
for _, source := range sources {
if source.Organization == "" {
source.Organization = defaultOrgID
}
if source.Role == "" {
source.Role = roles.ViewerRoleName
}
if err := s.Update(ctx, source); err != nil {
return nil
}
}
return nil
}
// All returns all known sources
func (s *SourcesStore) All(ctx context.Context) ([]chronograf.Source, error) {
var srcs []chronograf.Source

View File

@ -23,20 +23,22 @@ func TestSourceStore(t *testing.T) {
srcs := []chronograf.Source{
chronograf.Source{
Name: "Of Truth",
Type: "influx",
Username: "marty",
Password: "I❤ jennifer parker",
URL: "toyota-hilux.lyon-estates.local",
Default: true,
Name: "Of Truth",
Type: "influx",
Username: "marty",
Password: "I❤ jennifer parker",
URL: "toyota-hilux.lyon-estates.local",
Default: true,
Organization: "1337",
},
chronograf.Source{
Name: "HipToBeSquare",
Type: "influx",
Username: "calvinklein",
Password: "chuck b3rry",
URL: "toyota-hilux.lyon-estates.local",
Default: true,
Name: "HipToBeSquare",
Type: "influx",
Username: "calvinklein",
Password: "chuck b3rry",
URL: "toyota-hilux.lyon-estates.local",
Default: true,
Organization: "1337",
},
chronograf.Source{
Name: "HipToBeSquare",
@ -46,6 +48,7 @@ func TestSourceStore(t *testing.T) {
URL: "https://toyota-hilux.lyon-estates.local",
InsecureSkipVerify: true,
Default: false,
Organization: "1337",
},
}
@ -95,12 +98,13 @@ func TestSourceStore(t *testing.T) {
// Attempt to add a new default source
srcs = append(srcs, chronograf.Source{
Name: "Biff Tannen",
Type: "influx",
Username: "HELLO",
Password: "MCFLY",
URL: "anybody.in.there.local",
Default: true,
Name: "Biff Tannen",
Type: "influx",
Username: "HELLO",
Password: "MCFLY",
URL: "anybody.in.there.local",
Default: true,
Organization: "1892",
})
srcs[3] = mustAddSource(t, s, srcs[3])
@ -153,12 +157,13 @@ func TestSourceStore(t *testing.T) {
// Try to add one source as a non-default and ensure that it becomes a
// default
src := mustAddSource(t, s, chronograf.Source{
Name: "Biff Tannen",
Type: "influx",
Username: "HELLO",
Password: "MCFLY",
URL: "anybody.in.there.local",
Default: false,
Name: "Biff Tannen",
Type: "influx",
Username: "HELLO",
Password: "MCFLY",
URL: "anybody.in.there.local",
Default: false,
Organization: "1234",
})
if actual, err := s.Get(ctx, src.ID); err != nil {

View File

@ -2,6 +2,7 @@ package bolt
import (
"context"
"fmt"
"github.com/boltdb/bolt"
"github.com/influxdata/chronograf"
@ -12,65 +13,129 @@ import (
var _ chronograf.UsersStore = &UsersStore{}
// UsersBucket is used to store users local to chronograf
var UsersBucket = []byte("UsersV1")
var UsersBucket = []byte("UsersV2")
// UsersStore uses bolt to store and retrieve users
type UsersStore struct {
client *Client
}
// get searches the UsersStore for user with name and returns the bolt representation
func (s *UsersStore) get(ctx context.Context, name string) (*internal.User, error) {
found := false
var user internal.User
// get searches the UsersStore for user with id and returns the bolt representation
func (s *UsersStore) get(ctx context.Context, id uint64) (*chronograf.User, error) {
var u 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.Name != name {
return nil
}
found = true
if err := internal.UnmarshalUserPB(v, &user); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
if found == false {
v := tx.Bucket(UsersBucket).Get(u64tob(id))
if v == nil {
return chronograf.ErrUserNotFound
}
return nil
return internal.UnmarshalUser(v, &u)
})
if err != nil {
return nil, err
}
return &user, nil
return &u, nil
}
func (s *UsersStore) each(ctx context.Context, fn func(*chronograf.User)) error {
return s.client.db.View(func(tx *bolt.Tx) error {
return tx.Bucket(UsersBucket).ForEach(func(k, v []byte) error {
var user chronograf.User
if err := internal.UnmarshalUser(v, &user); err != nil {
return err
}
fn(&user)
return nil
})
})
}
// Num returns the number of users in the UsersStore
func (s *UsersStore) Num(ctx context.Context) (int, error) {
count := 0
err := s.client.db.View(func(tx *bolt.Tx) error {
return tx.Bucket(UsersBucket).ForEach(func(k, v []byte) error {
count++
return nil
})
})
if err != nil {
return 0, err
}
return count, nil
}
// Get searches the UsersStore for user with name
func (s *UsersStore) Get(ctx context.Context, name string) (*chronograf.User, error) {
u, err := s.get(ctx, name)
func (s *UsersStore) Get(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
if q.ID != nil {
return s.get(ctx, *q.ID)
}
if q.Name != nil && q.Provider != nil && q.Scheme != nil {
var user *chronograf.User
err := s.each(ctx, func(u *chronograf.User) {
if user != nil {
return
}
if u.Name == *q.Name && u.Provider == *q.Provider && u.Scheme == *q.Scheme {
user = u
}
})
if err != nil {
return nil, err
}
if user == nil {
return nil, chronograf.ErrUserNotFound
}
return user, nil
}
return nil, fmt.Errorf("must specify either ID, or Name, Provider, and Scheme in UserQuery")
}
func (s *UsersStore) userExists(ctx context.Context, u *chronograf.User) (bool, error) {
_, err := s.Get(ctx, chronograf.UserQuery{
Name: &u.Name,
Provider: &u.Provider,
Scheme: &u.Scheme,
})
if err == chronograf.ErrUserNotFound {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
// Add a new User to the UsersStore.
func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
if u == nil {
return nil, fmt.Errorf("user provided is nil")
}
userExists, err := s.userExists(ctx, u)
if err != nil {
return nil, err
}
return &chronograf.User{
Name: u.Name,
}, nil
}
// Add a new Users in the UsersStore.
func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
if userExists {
return nil, chronograf.ErrUserAlreadyExists
}
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 = seq
if v, err := internal.MarshalUser(u); err != nil {
return err
} else if err := b.Put(u64tob(seq), v); err != nil {
@ -84,60 +149,45 @@ func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.U
return u, nil
}
// Delete the users from the UsersStore
func (s *UsersStore) Delete(ctx context.Context, user *chronograf.User) error {
u, err := s.get(ctx, user.Name)
// Delete a user from the UsersStore
func (s *UsersStore) Delete(ctx context.Context, usr *chronograf.User) error {
_, err := s.get(ctx, usr.ID)
if err != nil {
return err
}
if err := s.client.db.Update(func(tx *bolt.Tx) error {
if err := tx.Bucket(UsersBucket).Delete(u64tob(u.ID)); err != nil {
return err
}
return nil
}); err != nil {
return err
}
return nil
return s.client.db.Update(func(tx *bolt.Tx) error {
return tx.Bucket(UsersBucket).Delete(u64tob(usr.ID))
})
}
// Update a user
func (s *UsersStore) Update(ctx context.Context, usr *chronograf.User) error {
u, err := s.get(ctx, usr.Name)
_, err := s.get(ctx, usr.ID)
if err != nil {
return err
}
if err := s.client.db.Update(func(tx *bolt.Tx) error {
u.Name = usr.Name
if v, err := internal.MarshalUserPB(u); err != nil {
return s.client.db.Update(func(tx *bolt.Tx) error {
if v, err := internal.MarshalUser(usr); err != nil {
return err
} else if err := tx.Bucket(UsersBucket).Put(u64tob(u.ID), v); err != nil {
} else if err := tx.Bucket(UsersBucket).Put(u64tob(usr.ID), v); err != nil {
return err
}
return nil
}); err != nil {
return err
}
return nil
})
}
// All returns all users
func (s *UsersStore) All(ctx context.Context) ([]chronograf.User, error) {
var users []chronograf.User
if err := s.client.db.View(func(tx *bolt.Tx) error {
if err := tx.Bucket(UsersBucket).ForEach(func(k, v []byte) error {
return tx.Bucket(UsersBucket).ForEach(func(k, v []byte) error {
var user chronograf.User
if err := internal.UnmarshalUser(v, &user); err != nil {
return err
}
users = append(users, user)
return nil
}); err != nil {
return err
}
return nil
})
}); err != nil {
return nil, err
}

View File

@ -2,16 +2,189 @@ package bolt_test
import (
"context"
"reflect"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/influxdata/chronograf"
)
func TestUsersStore_Get(t *testing.T) {
// IgnoreFields is used because ID is created by BoltDB and cannot be predicted reliably
// EquateEmpty is used because we want nil slices, arrays, and maps to be equal to the empty map
var cmpOptions = cmp.Options{
cmpopts.IgnoreFields(chronograf.User{}, "ID"),
cmpopts.EquateEmpty(),
}
func TestUsersStore_GetWithID(t *testing.T) {
type args struct {
ctx context.Context
name string
ctx context.Context
usr *chronograf.User
}
tests := []struct {
name string
args args
want *chronograf.User
wantErr bool
addFirst bool
}{
{
name: "User not found",
args: args{
ctx: context.Background(),
usr: &chronograf.User{
ID: 1337,
},
},
wantErr: true,
},
{
name: "Get user",
args: args{
ctx: context.Background(),
usr: &chronograf.User{
Name: "billietta",
Provider: "google",
Scheme: "oauth2",
},
},
want: &chronograf.User{
Name: "billietta",
Provider: "google",
Scheme: "oauth2",
},
addFirst: 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.UsersStore
if tt.addFirst {
tt.args.usr, err = s.Add(tt.args.ctx, tt.args.usr)
if err != nil {
t.Fatal(err)
}
}
got, err := s.Get(tt.args.ctx, chronograf.UserQuery{ID: &tt.args.usr.ID})
if (err != nil) != tt.wantErr {
t.Errorf("%q. UsersStore.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if diff := cmp.Diff(got, tt.want, cmpOptions...); diff != "" {
t.Errorf("%q. UsersStore.Get():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
func TestUsersStore_GetWithNameProviderScheme(t *testing.T) {
type args struct {
ctx context.Context
name string
provider string
usr *chronograf.User
}
tests := []struct {
name string
args args
want *chronograf.User
wantErr bool
addFirst bool
}{
{
name: "User not found",
args: args{
ctx: context.Background(),
usr: &chronograf.User{
Name: "billietta",
Provider: "google",
Scheme: "oauth2",
},
},
wantErr: true,
},
{
name: "Get user",
args: args{
ctx: context.Background(),
usr: &chronograf.User{
Name: "billietta",
Provider: "google",
Scheme: "oauth2",
},
},
want: &chronograf.User{
Name: "billietta",
Provider: "google",
Scheme: "oauth2",
},
addFirst: 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.UsersStore
if tt.addFirst {
tt.args.usr, err = s.Add(tt.args.ctx, tt.args.usr)
if err != nil {
t.Fatal(err)
}
}
got, err := s.Get(tt.args.ctx, chronograf.UserQuery{
Name: &tt.args.usr.Name,
Provider: &tt.args.usr.Provider,
Scheme: &tt.args.usr.Scheme,
})
if (err != nil) != tt.wantErr {
t.Errorf("%q. UsersStore.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if diff := cmp.Diff(got, tt.want, cmpOptions...); diff != "" {
t.Errorf("%q. UsersStore.Get():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
func TestUsersStore_GetInvalid(t *testing.T) {
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.UsersStore
_, err = s.Get(context.Background(), chronograf.UserQuery{})
if err == nil {
t.Errorf("Invalid Get. UsersStore.Get() error = %v", err)
}
}
func TestUsersStore_Add(t *testing.T) {
type args struct {
ctx context.Context
u *chronograf.User
addFirst bool
}
tests := []struct {
name string
@ -20,10 +193,46 @@ func TestUsersStore_Get(t *testing.T) {
wantErr bool
}{
{
name: "User not found",
name: "Add new user",
args: args{
ctx: context.Background(),
name: "unknown",
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: "editor",
},
},
},
},
want: &chronograf.User{
Name: "docbrown",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: "editor",
},
},
},
},
{
name: "User already exists",
args: args{
ctx: context.Background(),
addFirst: true,
u: &chronograf.User{
Name: "docbrown",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: "editor",
},
},
},
},
wantErr: true,
},
@ -37,65 +246,26 @@ func TestUsersStore_Get(t *testing.T) {
t.Fatal(err)
}
defer client.Close()
s := client.UsersStore
got, err := s.Get(tt.args.ctx, tt.args.name)
if (err != nil) != tt.wantErr {
t.Errorf("%q. UsersStore.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
if tt.args.addFirst {
_, _ = s.Add(tt.args.ctx, tt.args.u)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. UsersStore.Get() = %v, want %v", tt.name, got, tt.want)
}
}
}
func TestUsersStore_Add(t *testing.T) {
type args struct {
ctx context.Context
u *chronograf.User
}
tests := []struct {
name string
args args
want *chronograf.User
wantErr bool
}{
{
name: "Add new user",
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
},
},
want: &chronograf.User{
Name: "docbrown",
},
},
}
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.UsersStore
got, err := s.Add(tt.args.ctx, tt.args.u)
if (err != nil) != tt.wantErr {
t.Errorf("%q. UsersStore.Add() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. UsersStore.Add() = %v, want %v", tt.name, got, tt.want)
if tt.wantErr {
continue
}
got, _ = s.Get(tt.args.ctx, got.Name)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. UsersStore.Add() = %v, want %v", tt.name, got, tt.want)
got, err = s.Get(tt.args.ctx, chronograf.UserQuery{ID: &got.ID})
if err != nil {
t.Fatalf("failed to get user: %v", err)
}
if diff := cmp.Diff(got, tt.want, cmpOptions...); diff != "" {
t.Errorf("%q. UsersStore.Add():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
@ -116,7 +286,7 @@ func TestUsersStore_Delete(t *testing.T) {
args: args{
ctx: context.Background(),
user: &chronograf.User{
Name: "noone",
ID: 10,
},
},
wantErr: true,
@ -144,7 +314,9 @@ func TestUsersStore_Delete(t *testing.T) {
s := client.UsersStore
if tt.addFirst {
s.Add(tt.args.ctx, tt.args.user)
var err error
tt.args.user, err = s.Add(tt.args.ctx, tt.args.user)
fmt.Println(err)
}
if err := s.Delete(tt.args.ctx, tt.args.user); (err != nil) != tt.wantErr {
t.Errorf("%q. UsersStore.Delete() error = %v, wantErr %v", tt.name, err, tt.wantErr)
@ -154,13 +326,18 @@ func TestUsersStore_Delete(t *testing.T) {
func TestUsersStore_Update(t *testing.T) {
type args struct {
ctx context.Context
usr *chronograf.User
ctx context.Context
usr *chronograf.User
roles []chronograf.Role
provider string
scheme string
name string
}
tests := []struct {
name string
args args
addFirst bool
want *chronograf.User
wantErr bool
}{
{
@ -168,18 +345,60 @@ func TestUsersStore_Update(t *testing.T) {
args: args{
ctx: context.Background(),
usr: &chronograf.User{
Name: "noone",
ID: 10,
},
},
wantErr: true,
},
{
name: "Update new user",
name: "Update user role",
args: args{
ctx: context.Background(),
usr: &chronograf.User{
Name: "noone",
Name: "bobetta",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: "viewer",
},
},
},
roles: []chronograf.Role{
{
Name: "editor",
},
},
},
want: &chronograf.User{
Name: "bobetta",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: "editor",
},
},
},
addFirst: true,
},
{
name: "Update user provider and scheme",
args: args{
ctx: context.Background(),
usr: &chronograf.User{
Name: "bobetta",
Provider: "github",
Scheme: "oauth2",
},
provider: "google",
scheme: "oauth2",
name: "billietta",
},
want: &chronograf.User{
Name: "billietta",
Provider: "google",
Scheme: "oauth2",
},
addFirst: true,
},
@ -196,12 +415,44 @@ func TestUsersStore_Update(t *testing.T) {
s := client.UsersStore
if tt.addFirst {
s.Add(tt.args.ctx, tt.args.usr)
tt.args.usr, err = s.Add(tt.args.ctx, tt.args.usr)
if err != nil {
t.Fatal(err)
}
}
if tt.args.roles != nil {
tt.args.usr.Roles = tt.args.roles
}
if tt.args.provider != "" {
tt.args.usr.Provider = tt.args.provider
}
if tt.args.scheme != "" {
tt.args.usr.Scheme = tt.args.scheme
}
if tt.args.name != "" {
tt.args.usr.Name = tt.args.name
}
if err := s.Update(tt.args.ctx, tt.args.usr); (err != nil) != tt.wantErr {
t.Errorf("%q. UsersStore.Update() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
// for the empty test
if tt.want == nil {
continue
}
got, err := s.Get(tt.args.ctx, chronograf.UserQuery{ID: &tt.args.usr.ID})
if err != nil {
t.Fatalf("failed to get user: %v", err)
}
if diff := cmp.Diff(got, tt.want, cmpOptions...); diff != "" {
t.Errorf("%q. UsersStore.Update():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
@ -220,10 +471,24 @@ func TestUsersStore_All(t *testing.T) {
name: "Update new user",
want: []chronograf.User{
{
Name: "howdy",
Name: "howdy",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: "viewer",
},
},
},
{
Name: "doody",
Name: "doody",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: "editor",
},
},
},
},
addFirst: true,
@ -245,13 +510,79 @@ func TestUsersStore_All(t *testing.T) {
s.Add(tt.ctx, &u)
}
}
got, err := s.All(tt.ctx)
gots, err := s.All(tt.ctx)
if (err != nil) != tt.wantErr {
t.Errorf("%q. UsersStore.All() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. UsersStore.All() = %v, want %v", tt.name, got, tt.want)
for i, got := range gots {
if diff := cmp.Diff(got, tt.want[i], cmpOptions...); diff != "" {
t.Errorf("%q. UsersStore.All():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
}
func TestUsersStore_Num(t *testing.T) {
tests := []struct {
name string
ctx context.Context
users []chronograf.User
want int
wantErr bool
}{
{
name: "No users",
want: 0,
},
{
name: "Update new user",
want: 2,
users: []chronograf.User{
{
Name: "howdy",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: "viewer",
},
},
},
{
Name: "doody",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: "editor",
},
},
},
},
},
}
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.UsersStore
for _, u := range tt.users {
s.Add(tt.ctx, &u)
}
got, err := s.Num(tt.ctx)
if (err != nil) != tt.wantErr {
t.Errorf("%q. UsersStore.Num() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if got != tt.want {
t.Errorf("%q. UsersStore.Num() = %d. want %d", tt.name, got, tt.want)
}
}
}

View File

@ -14,7 +14,7 @@ import (
// AppExt is the the file extension searched for in the directory for layout files
const AppExt = ".json"
// Apps are canned JSON layouts. Implements LayoutStore.
// Apps are canned JSON layouts. Implements LayoutsStore.
type Apps struct {
Dir string // Dir is the directory contained the pre-canned applications.
Load func(string) (chronograf.Layout, error) // Load loads string name and return a Layout
@ -27,7 +27,7 @@ type Apps struct {
}
// NewApps constructs a layout store wrapping a file system directory
func NewApps(dir string, ids chronograf.ID, logger chronograf.Logger) chronograf.LayoutStore {
func NewApps(dir string, ids chronograf.ID, logger chronograf.Logger) chronograf.LayoutsStore {
return &Apps{
Dir: dir,
Load: loadFile,

View File

@ -10,13 +10,13 @@ import (
//go:generate go-bindata -o bin_gen.go -ignore README|apps|.sh|go -pkg canned .
// BinLayoutStore represents a layout store using data generated by go-bindata
type BinLayoutStore struct {
// BinLayoutsStore represents a layout store using data generated by go-bindata
type BinLayoutsStore struct {
Logger chronograf.Logger
}
// All returns the set of all layouts
func (s *BinLayoutStore) All(ctx context.Context) ([]chronograf.Layout, error) {
func (s *BinLayoutsStore) All(ctx context.Context) ([]chronograf.Layout, error) {
names := AssetNames()
layouts := make([]chronograf.Layout, len(names))
for i, name := range names {
@ -43,18 +43,18 @@ func (s *BinLayoutStore) All(ctx context.Context) ([]chronograf.Layout, error) {
return layouts, nil
}
// Add is not support by BinLayoutStore
func (s *BinLayoutStore) Add(ctx context.Context, layout chronograf.Layout) (chronograf.Layout, error) {
return chronograf.Layout{}, fmt.Errorf("Add to BinLayoutStore not supported")
// Add is not support by BinLayoutsStore
func (s *BinLayoutsStore) Add(ctx context.Context, layout chronograf.Layout) (chronograf.Layout, error) {
return chronograf.Layout{}, fmt.Errorf("Add to BinLayoutsStore not supported")
}
// Delete is not support by BinLayoutStore
func (s *BinLayoutStore) Delete(ctx context.Context, layout chronograf.Layout) error {
return fmt.Errorf("Delete to BinLayoutStore not supported")
// Delete is not support by BinLayoutsStore
func (s *BinLayoutsStore) Delete(ctx context.Context, layout chronograf.Layout) error {
return fmt.Errorf("Delete to BinLayoutsStore not supported")
}
// Get retrieves Layout if `ID` exists.
func (s *BinLayoutStore) Get(ctx context.Context, ID string) (chronograf.Layout, error) {
func (s *BinLayoutsStore) Get(ctx context.Context, ID string) (chronograf.Layout, error) {
layouts, err := s.All(ctx)
if err != nil {
s.Logger.
@ -78,6 +78,6 @@ func (s *BinLayoutStore) Get(ctx context.Context, ID string) (chronograf.Layout,
}
// Update not supported
func (s *BinLayoutStore) Update(ctx context.Context, layout chronograf.Layout) error {
return fmt.Errorf("Update to BinLayoutStore not supported")
func (s *BinLayoutsStore) Update(ctx context.Context, layout chronograf.Layout) error {
return fmt.Errorf("Update to BinLayoutsStore not supported")
}

View File

@ -9,19 +9,23 @@ import (
// General errors.
const (
ErrUpstreamTimeout = Error("request to backend timed out")
ErrSourceNotFound = Error("source not found")
ErrServerNotFound = Error("server not found")
ErrLayoutNotFound = Error("layout not found")
ErrDashboardNotFound = Error("dashboard not found")
ErrUserNotFound = Error("user not found")
ErrLayoutInvalid = Error("layout is invalid")
ErrAlertNotFound = Error("alert not found")
ErrAuthentication = Error("user not authenticated")
ErrUninitialized = Error("client uninitialized. Call Open() method")
ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'")
ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold'")
ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB")
ErrUpstreamTimeout = Error("request to backend timed out")
ErrSourceNotFound = Error("source not found")
ErrServerNotFound = Error("server not found")
ErrLayoutNotFound = Error("layout not found")
ErrDashboardNotFound = Error("dashboard not found")
ErrUserNotFound = Error("user not found")
ErrLayoutInvalid = Error("layout is invalid")
ErrAlertNotFound = Error("alert not found")
ErrAuthentication = Error("user not authenticated")
ErrUninitialized = Error("client uninitialized. Call Open() method")
ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'")
ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold'")
ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB")
ErrUserAlreadyExists = Error("user already exists")
ErrOrganizationNotFound = Error("organization not found")
ErrOrganizationAlreadyExists = Error("organization already exists")
ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization")
)
// Error is a domain error encountered while processing chronograf requests
@ -102,9 +106,10 @@ type TimeSeries interface {
// Role is a restricted set of permissions assigned to a set of users.
type Role struct {
Name string `json:"name"`
Permissions Permissions `json:"permissions,omitempty"`
Users []User `json:"users,omitempty"`
Name string `json:"name"`
Permissions Permissions `json:"permissions,omitempty"`
Users []User `json:"users,omitempty"`
Organization string `json:"organization,omitempty"`
}
// RolesStore is the Storage and retrieval of authentication information
@ -204,6 +209,8 @@ type Source struct {
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the source is accepted.
Default bool `json:"default"` // Default specifies the default source for the application
Telegraf string `json:"telegraf"` // Telegraf is the db telegraf is written to. By default it is "telegraf"
Organization string `json:"organization"` // Organization is the organization ID that resource belongs to
Role string `json:"role,omitempty"` // Not Currently Used. Role is the name of the minimum role that a user must possess to access the resource.
}
// SourcesStore stores connection information for a `TimeSeries`
@ -334,6 +341,7 @@ type Server struct {
URL string // URL are the connections to the server
InsecureSkipVerify bool // InsecureSkipVerify as true means any certificate presented by the server is accepted.
Active bool // Is this the active server for the source?
Organization string // Organization is the organization ID that resource belongs to
}
// ServersStore stores connection information for a `Server`
@ -382,13 +390,34 @@ type Scope string
// User represents an authenticated user.
type User struct {
ID uint64 `json:"id,string,omitempty"`
Name string `json:"name"`
Passwd string `json:"password"`
Passwd string `json:"password,omitempty"`
Permissions Permissions `json:"permissions,omitempty"`
Roles []Role `json:"roles,omitempty"`
Provider string `json:"provider,omitempty"`
Scheme string `json:"scheme,omitempty"`
SuperAdmin bool `json:"superAdmin,omitempty"`
}
// UserQuery represents the attributes that a user may be retrieved by.
// It is predominantly used in the UsersStore.Get method.
//
// It is expected that only one of ID or Name, Provider, and Scheme will be
// specified, but all are provided UserStores should prefer ID.
type UserQuery struct {
ID *uint64
Name *string
Provider *string
Scheme *string
}
// UsersStore is the Storage and retrieval of authentication information
//
// While not necessary for the app to function correctly, it is
// expected that Implementors of the UsersStore will take
// care to guarantee that the combinartion of a users Name, Provider,
// and Scheme are unique.
type UsersStore interface {
// All lists all users from the UsersStore
All(context.Context) ([]User, error)
@ -397,9 +426,11 @@ type UsersStore interface {
// Delete the User from the UsersStore
Delete(context.Context, *User) error
// Get retrieves a user if name exists.
Get(ctx context.Context, name string) (*User, error)
Get(ctx context.Context, q UserQuery) (*User, error)
// Update the user's permissions or roles
Update(context.Context, *User) error
// Num returns the number of users in the UsersStore
Num(context.Context) (int, error)
}
// Database represents a database in a time series source
@ -437,10 +468,11 @@ type DashboardID int
// Dashboard represents all visual and query data for a dashboard
type Dashboard struct {
ID DashboardID `json:"id"`
Cells []DashboardCell `json:"cells"`
Templates []Template `json:"templates"`
Name string `json:"name"`
ID DashboardID `json:"id"`
Cells []DashboardCell `json:"cells"`
Templates []Template `json:"templates"`
Name string `json:"name"`
Organization string `json:"organization"` // Organization is the organization ID that resource belongs to
}
// Axis represents the visible extents of a visualization
@ -513,11 +545,11 @@ type Layout struct {
Cells []Cell `json:"cells"`
}
// LayoutStore stores dashboards and associated Cells
type LayoutStore interface {
// LayoutsStore stores dashboards and associated Cells
type LayoutsStore interface {
// All returns all dashboards in the store
All(context.Context) ([]Layout, error)
// Add creates a new dashboard in the LayoutStore
// Add creates a new dashboard in the LayoutsStore
Add(context.Context, Layout) (Layout, error)
// Delete the dashboard from the store
Delete(context.Context, Layout) error
@ -526,3 +558,49 @@ type LayoutStore interface {
// Update the dashboard in the store.
Update(context.Context, Layout) error
}
// Organization is a group of resources under a common name
type Organization struct {
ID uint64 `json:"id,string"`
Name string `json:"name"`
// DefaultRole is the name of the role that is the default for any users added to the organization
DefaultRole string `json:"defaultRole,omitempty"`
// Public specifies whether users must be explicitly added to the organization.
// It is currently only used by the default organization, but that may change in the future.
Public bool `json:"public"`
}
// OrganizationQuery represents the attributes that a organization may be retrieved by.
// It is predominantly used in the OrganizationsStore.Get method.
// It is expected that only one of ID or Name will be specified, but will prefer ID over Name if both are specified.
type OrganizationQuery struct {
// If an ID is provided in the query, the lookup time for an organization will be O(1).
ID *uint64
// If Name is provided, the lookup time will be O(n).
Name *string
}
// OrganizationsStore is the storage and retrieval of Organizations
//
// While not necessary for the app to function correctly, it is
// expected that Implementors of the OrganizationsStore will take
// care to guarantee that the Organization.Name is unqiue. Allowing
// for duplicate names creates a confusing UX experience for the User.
type OrganizationsStore interface {
// Add creates a new Organization.
// The Created organization is returned back to the user with the
// ID field populated.
Add(context.Context, *Organization) (*Organization, error)
// All lists all Organizations in the OrganizationsStore
All(context.Context) ([]Organization, error)
// Delete removes an Organization from the OrganizationsStore
Delete(context.Context, *Organization) error
// Get retrieves an Organization from the OrganizationsStore
Get(context.Context, OrganizationQuery) (*Organization, error)
// Update updates an Organization in the OrganizationsStore
Update(context.Context, *Organization) error
// CreateDefault creates the default organization
CreateDefault(ctx context.Context) error
// DefaultOrganization returns the DefaultOrganization
DefaultOrganization(ctx context.Context) (*Organization, error)
}

View File

@ -2,6 +2,7 @@ package enterprise
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
@ -28,7 +29,7 @@ func (c *UserStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.Us
}
}
return c.Get(ctx, u.Name)
return c.Get(ctx, chronograf.UserQuery{Name: &u.Name})
}
// Delete the User from Influx Enterprise
@ -36,9 +37,22 @@ func (c *UserStore) Delete(ctx context.Context, u *chronograf.User) error {
return c.Ctrl.DeleteUser(ctx, u.Name)
}
// Number of users in Influx
func (c *UserStore) Num(ctx context.Context) (int, error) {
all, err := c.All(ctx)
if err != nil {
return 0, err
}
return len(all), nil
}
// Get retrieves a user if name exists.
func (c *UserStore) Get(ctx context.Context, name string) (*chronograf.User, error) {
u, err := c.Ctrl.User(ctx, name)
func (c *UserStore) Get(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
if q.Name == nil {
return nil, fmt.Errorf("query must specify name")
}
u, err := c.Ctrl.User(ctx, *q.Name)
if err != nil {
return nil, err
}
@ -48,7 +62,7 @@ func (c *UserStore) Get(ctx context.Context, name string) (*chronograf.User, err
return nil, err
}
role := ur[name]
role := ur[*q.Name]
cr := role.ToChronograf()
// For now we are removing all users from a role being returned.
for i, r := range cr {

View File

@ -375,7 +375,7 @@ func TestClient_Get(t *testing.T) {
Ctrl: tt.fields.Ctrl,
Logger: tt.fields.Logger,
}
got, err := c.Get(tt.args.ctx, tt.args.name)
got, err := c.Get(tt.args.ctx, chronograf.UserQuery{Name: &tt.args.name})
if (err != nil) != tt.wantErr {
t.Errorf("%q. Client.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
@ -534,6 +534,94 @@ func TestClient_Update(t *testing.T) {
}
}
func TestClient_Num(t *testing.T) {
type fields struct {
Ctrl *mockCtrl
Logger chronograf.Logger
}
type args struct {
ctx context.Context
}
tests := []struct {
name string
fields fields
args args
want []chronograf.User
wantErr bool
}{
{
name: "Successful Get User",
fields: fields{
Ctrl: &mockCtrl{
users: func(ctx context.Context, name *string) (*enterprise.Users, error) {
return &enterprise.Users{
Users: []enterprise.User{
{
Name: "marty",
Password: "johnny be good",
Permissions: map[string][]string{
"": {
"ViewChronograf",
"ReadData",
"WriteData",
},
},
},
},
}, nil
},
userRoles: func(ctx context.Context) (map[string]enterprise.Roles, error) {
return map[string]enterprise.Roles{}, nil
},
},
},
args: args{
ctx: context.Background(),
},
want: []chronograf.User{
{
Name: "marty",
Permissions: chronograf.Permissions{
{
Scope: chronograf.AllScope,
Allowed: chronograf.Allowances{"ViewChronograf", "ReadData", "WriteData"},
},
},
Roles: []chronograf.Role{},
},
},
},
{
name: "Failure to get User",
fields: fields{
Ctrl: &mockCtrl{
users: func(ctx context.Context, name *string) (*enterprise.Users, error) {
return nil, fmt.Errorf("1.21 Gigawatts! Tom, how could I have been so careless?")
},
},
},
args: args{
ctx: context.Background(),
},
wantErr: true,
},
}
for _, tt := range tests {
c := &enterprise.UserStore{
Ctrl: tt.fields.Ctrl,
Logger: tt.fields.Logger,
}
got, err := c.Num(tt.args.ctx)
if (err != nil) != tt.wantErr {
t.Errorf("%q. Client.Num() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if got != len(tt.want) {
t.Errorf("%q. Client.Num() = %v, want %v", tt.name, got, len(tt.want))
}
}
}
func TestClient_All(t *testing.T) {
type fields struct {
Ctrl *mockCtrl

View File

@ -38,6 +38,12 @@ func RenderTemplate(query string, t chronograf.TemplateVar, now time.Time) (stri
if len(t.Values) == 0 {
return query, nil
}
// we only need to render the template if the template exists in the query
if !strings.Contains(query, t.Var) {
return query, nil
}
switch t.Values[0].Type {
case "tagKey", "fieldKey", "measurement", "database":
return strings.Replace(query, t.Var, `"`+t.Values[0].Value+`"`, -1), nil

View File

@ -62,10 +62,10 @@ func TestTemplateReplace(t *testing.T) {
},
{
name: "select with parameters and aggregates",
query: `SELECT mean($field) FROM "cpu" WHERE $tag = $value GROUP BY $tag`,
query: `SELECT mean(:field:) FROM "cpu" WHERE :tag: = :value: GROUP BY :tag:`,
vars: []chronograf.TemplateVar{
chronograf.TemplateVar{
Var: "$value",
Var: ":value:",
Values: []chronograf.TemplateValue{
{
Type: "tagValue",
@ -74,7 +74,7 @@ func TestTemplateReplace(t *testing.T) {
},
},
chronograf.TemplateVar{
Var: "$tag",
Var: ":tag:",
Values: []chronograf.TemplateValue{
{
Type: "tagKey",
@ -83,7 +83,7 @@ func TestTemplateReplace(t *testing.T) {
},
},
chronograf.TemplateVar{
Var: "$field",
Var: ":field:",
Values: []chronograf.TemplateValue{
{
Type: "fieldKey",
@ -96,25 +96,25 @@ func TestTemplateReplace(t *testing.T) {
},
{
name: "Non-existant parameters",
query: `SELECT $field FROM "cpu"`,
want: `SELECT $field FROM "cpu"`,
query: `SELECT :field: FROM "cpu"`,
want: `SELECT :field: FROM "cpu"`,
},
{
name: "var without a value",
query: `SELECT $field FROM "cpu"`,
query: `SELECT :field: FROM "cpu"`,
vars: []chronograf.TemplateVar{
chronograf.TemplateVar{
Var: "$field",
Var: ":field:",
},
},
want: `SELECT $field FROM "cpu"`,
want: `SELECT :field: FROM "cpu"`,
},
{
name: "var with unknown type",
query: `SELECT $field FROM "cpu"`,
query: `SELECT :field: FROM "cpu"`,
vars: []chronograf.TemplateVar{
chronograf.TemplateVar{
Var: "$field",
Var: ":field:",
Values: []chronograf.TemplateValue{
{
Type: "who knows?",
@ -123,7 +123,7 @@ func TestTemplateReplace(t *testing.T) {
},
},
},
want: `SELECT $field FROM "cpu"`,
want: `SELECT :field: FROM "cpu"`,
},
{
name: "auto group by",
@ -224,6 +224,71 @@ func TestTemplateReplace(t *testing.T) {
},
want: `SELECT mean(usage_idle) FROM "cpu" WHERE time > now() - 1h GROUP BY time(93s)`,
},
{
name: "no template variables specified",
query: `SELECT mean(usage_idle) FROM "cpu" WHERE time > :dashboardTime: GROUP BY :interval:`,
want: `SELECT mean(usage_idle) FROM "cpu" WHERE time > :dashboardTime: GROUP BY :interval:`,
},
{
name: "auto group by failing condition",
query: `SELECT mean(usage_idle) FROM "cpu" WHERE time > :dashboardTime: GROUP BY :interval:`,
vars: []chronograf.TemplateVar{
{
Var: ":interval:",
Values: []chronograf.TemplateValue{
{
Value: "115",
Type: "resolution",
},
{
Value: "3",
Type: "pointsPerPixel",
},
},
},
{
Var: ":dashboardTime:",
Values: []chronograf.TemplateValue{
{
Value: "now() - 1h",
Type: "constant",
Selected: true,
},
},
},
},
want: `SELECT mean(usage_idle) FROM "cpu" WHERE time > now() - 1h GROUP BY time(93s)`,
},
{
name: "query with no template variables contained should return query",
query: `SHOW DATABASES`,
vars: []chronograf.TemplateVar{
{
Var: ":interval:",
Values: []chronograf.TemplateValue{
{
Value: "115",
Type: "resolution",
},
{
Value: "3",
Type: "pointsPerPixel",
},
},
},
{
Var: ":dashboardTime:",
Values: []chronograf.TemplateValue{
{
Value: "now() - 1h",
Type: "constant",
Selected: true,
},
},
},
},
want: `SHOW DATABASES`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -392,11 +457,16 @@ func Test_RenderTemplate(t *testing.T) {
want: "SELECT mean(usage_idle) FROM cpu WHERE time > '1985-10-25T00:01:00Z' and time < '1985-10-25T00:02:00Z' GROUP BY time(179ms)",
},
{
name: "absolute time with nano seconds and zero duraiton",
name: "absolute time with nano seconds and zero duration",
query: "SELECT mean(usage_idle) FROM cpu WHERE time > '2017-07-24T15:33:42.994Z' and time < '2017-07-24T15:33:42.994Z' GROUP BY :interval:",
resolution: 1000,
want: "SELECT mean(usage_idle) FROM cpu WHERE time > '2017-07-24T15:33:42.994Z' and time < '2017-07-24T15:33:42.994Z' GROUP BY time(1ms)",
},
{
name: "query should be returned if there are no template variables",
query: "SHOW DATABASES",
want: "SHOW DATABASES",
},
}
for _, tt := range gbvTests {
@ -426,5 +496,3 @@ func Test_RenderTemplate(t *testing.T) {
})
}
}
// SELECT mean("numSeries") AS "mean_numSeries" FROM "_internal"."monitor"."database" WHERE time > now() - 1h GROUP BY :interval: FILL(null);SELECT mean("numSeries") AS "mean_numSeries_shifted__1__h" FROM "_internal"."monitor"."database" WHERE time > now() - 1h - 1h AND time < now() - 1h GROUP BY :interval: FILL(null)

View File

@ -21,7 +21,7 @@ func (c *Client) Add(ctx context.Context, u *chronograf.User) (*chronograf.User,
return nil, err
}
}
return c.Get(ctx, u.Name)
return c.Get(ctx, chronograf.UserQuery{Name: &u.Name})
}
// Delete the User from InfluxDB
@ -54,14 +54,18 @@ func (c *Client) Delete(ctx context.Context, u *chronograf.User) error {
}
// Get retrieves a user if name exists.
func (c *Client) Get(ctx context.Context, name string) (*chronograf.User, error) {
func (c *Client) Get(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
if q.Name == nil {
return nil, fmt.Errorf("query must specify name")
}
users, err := c.showUsers(ctx)
if err != nil {
return nil, err
}
for _, user := range users {
if user.Name == name {
if user.Name == *q.Name {
perms, err := c.userPermissions(ctx, user.Name)
if err != nil {
return nil, err
@ -82,7 +86,7 @@ func (c *Client) Update(ctx context.Context, u *chronograf.User) error {
return c.updatePassword(ctx, u.Name, u.Passwd)
}
user, err := c.Get(ctx, u.Name)
user, err := c.Get(ctx, chronograf.UserQuery{Name: &u.Name})
if err != nil {
return err
}
@ -122,6 +126,16 @@ func (c *Client) All(ctx context.Context) ([]chronograf.User, error) {
return users, nil
}
// Number of users in Influx
func (c *Client) Num(ctx context.Context) (int, error) {
all, err := c.All(ctx)
if err != nil {
return 0, err
}
return len(all), nil
}
// showUsers runs SHOW USERS InfluxQL command and returns chronograf users.
func (c *Client) showUsers(ctx context.Context) ([]chronograf.User, error) {
res, err := c.Query(ctx, chronograf.Query{

View File

@ -392,7 +392,7 @@ func TestClient_Get(t *testing.T) {
Logger: log.New(log.DebugLevel),
}
defer ts.Close()
got, err := c.Get(tt.args.ctx, tt.args.name)
got, err := c.Get(tt.args.ctx, chronograf.UserQuery{Name: &tt.args.name})
if (err != nil) != tt.wantErr {
t.Errorf("%q. Client.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
@ -573,6 +573,102 @@ func TestClient_revokePermission(t *testing.T) {
}
}
func TestClient_Num(t *testing.T) {
type args struct {
ctx context.Context
}
tests := []struct {
name string
args args
statusUsers int
showUsers []byte
statusGrants int
showGrants []byte
want []chronograf.User
wantErr bool
}{
{
name: "All Users",
statusUsers: http.StatusOK,
showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`),
statusGrants: http.StatusOK,
showGrants: []byte(`{"results":[{"series":[{"columns":["database","privilege"],"values":[["mydb","ALL PRIVILEGES"]]}]}]}`),
args: args{
ctx: context.Background(),
},
want: []chronograf.User{
{
Name: "admin",
Permissions: chronograf.Permissions{
chronograf.Permission{
Scope: "all",
Allowed: []string{"ALL"},
},
chronograf.Permission{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE", "READ"},
},
},
},
{
Name: "docbrown",
Permissions: chronograf.Permissions{
chronograf.Permission{
Scope: "all",
Allowed: []string{"ALL"},
},
chronograf.Permission{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE", "READ"},
},
},
},
{
Name: "reader",
Permissions: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE", "READ"},
},
},
},
},
},
}
for _, tt := range tests {
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if path := r.URL.Path; path != "/query" {
t.Error("Expected the path to contain `/query` but was", path)
}
query := r.URL.Query().Get("q")
if strings.Contains(query, "GRANTS") {
rw.WriteHeader(tt.statusGrants)
rw.Write(tt.showGrants)
} else if strings.Contains(query, "USERS") {
rw.WriteHeader(tt.statusUsers)
rw.Write(tt.showUsers)
}
}))
u, _ := url.Parse(ts.URL)
c := &Client{
URL: u,
Logger: log.New(log.DebugLevel),
}
defer ts.Close()
got, err := c.Num(tt.args.ctx)
if (err != nil) != tt.wantErr {
t.Errorf("%q. Client.Num() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if got != len(tt.want) {
t.Errorf("%q. Client.Num() = %v, want %v", tt.name, got, len(tt.want))
}
}
}
func TestClient_All(t *testing.T) {
type args struct {
ctx context.Context

296
integrations/server_test.go Normal file
View File

@ -0,0 +1,296 @@
package integrations
// This was intentionally added under the integrations package and not the integrations test package
// so that changes in other parts of the code base that may have an effect on these test will not
// compile until they are fixed.
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"testing"
"time"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/bolt"
"github.com/influxdata/chronograf/oauth2"
"github.com/influxdata/chronograf/server"
)
func TestServer(t *testing.T) {
type fields struct {
Organizations []chronograf.Organization
Users []chronograf.User
Sources []chronograf.Source
Servers []chronograf.Server
Layouts []chronograf.Layout
Dashboards []chronograf.Dashboard
}
type args struct {
server *server.Server
method string
path string
payload interface{} // Expects this to be a json serializable struct
principal oauth2.Principal
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
subName string
fields fields
args args
wants wants
}{
{
name: "GET /users",
subName: "User Not Found in the Default Organization",
fields: fields{
Users: []chronograf.User{},
},
args: args{
server: &server.Server{
GithubClientID: "not empty",
GithubClientSecret: "not empty",
},
method: "GET",
path: "/chronograf/v1/users",
principal: oauth2.Principal{
Organization: "0",
Subject: "billibob",
Issuer: "github",
},
},
wants: wants{
statusCode: 403,
body: `{"code":403,"message":"User is not authorized"}`,
},
},
{
name: "GET /users",
subName: "Single User in the Default Organization as SuperAdmin",
fields: fields{
Users: []chronograf.User{
{
ID: 1, // This is artificial, but should be reflective of the users actual ID
Name: "billibob",
Provider: "github",
Scheme: "oauth2",
SuperAdmin: true,
Roles: []chronograf.Role{
{
Name: "admin",
Organization: "0",
},
},
},
},
},
args: args{
server: &server.Server{
GithubClientID: "not empty",
GithubClientSecret: "not empty",
},
method: "GET",
path: "/chronograf/v1/users",
principal: oauth2.Principal{
Organization: "0",
Subject: "billibob",
Issuer: "github",
},
},
wants: wants{
statusCode: 200,
body: `
{
"links": {
"self": "/chronograf/v1/users"
},
"users": [
{
"links": {
"self": "/chronograf/v1/users/1"
},
"id": "1",
"name": "billibob",
"provider": "github",
"scheme": "oauth2",
"superAdmin": true,
"roles": [
{
"name": "admin",
"organization": "0"
}
]
}
]
}`,
},
},
}
for _, tt := range tests {
testName := fmt.Sprintf("%s: %s", tt.name, tt.subName)
t.Run(testName, func(t *testing.T) {
ctx := context.TODO()
// Create Test Server
host, port := hostAndPort()
tt.args.server.Host = host
tt.args.server.Port = port
// This is so that we can use staticly generate jwts
tt.args.server.TokenSecret = "secret"
boltFile := newBoltFile()
tt.args.server.BoltPath = boltFile
// Prepopulate BoltDB Database for Server
boltdb := bolt.NewClient()
boltdb.Path = boltFile
_ = boltdb.Open(ctx)
// Populate Organizations
for i, organization := range tt.fields.Organizations {
o, err := boltdb.OrganizationsStore.Add(ctx, &organization)
if err != nil {
t.Fatalf("failed to add organization: %v", err)
return
}
tt.fields.Organizations[i] = *o
}
// Populate Users
for i, user := range tt.fields.Users {
u, err := boltdb.UsersStore.Add(ctx, &user)
if err != nil {
t.Fatalf("failed to add user: %v", err)
return
}
tt.fields.Users[i] = *u
}
// Populate Sources
for i, source := range tt.fields.Sources {
s, err := boltdb.SourcesStore.Add(ctx, source)
if err != nil {
t.Fatalf("failed to add source: %v", err)
return
}
tt.fields.Sources[i] = s
}
// Populate Servers
for i, server := range tt.fields.Servers {
s, err := boltdb.ServersStore.Add(ctx, server)
if err != nil {
t.Fatalf("failed to add server: %v", err)
return
}
tt.fields.Servers[i] = s
}
// Populate Layouts
for i, layout := range tt.fields.Layouts {
l, err := boltdb.LayoutsStore.Add(ctx, layout)
if err != nil {
t.Fatalf("failed to add layout: %v", err)
return
}
tt.fields.Layouts[i] = l
}
// Populate Dashboards
for i, dashboard := range tt.fields.Dashboards {
d, err := boltdb.DashboardsStore.Add(ctx, dashboard)
if err != nil {
t.Fatalf("failed to add dashboard: %v", err)
return
}
tt.fields.Dashboards[i] = d
}
_ = boltdb.Close()
go tt.args.server.Serve(ctx)
serverURL := fmt.Sprintf("http://%v:%v%v", host, port, tt.args.path)
// Wait for the server to come online
timeout := time.Now().Add(100 * time.Millisecond)
for {
_, err := http.Get(serverURL + "/swagger.json")
if err == nil {
break
}
if time.Now().After(timeout) {
t.Fatalf("failed to start server")
return
}
}
// Set the Expiry time on the principal
tt.args.principal.IssuedAt = time.Now()
tt.args.principal.ExpiresAt = time.Now().Add(10 * time.Second)
// Construct HTTP Request
buf, _ := json.Marshal(tt.args.payload)
reqBody := ioutil.NopCloser(bytes.NewReader(buf))
req, _ := http.NewRequest(tt.args.method, serverURL, reqBody)
token, _ := oauth2.NewJWT(tt.args.server.TokenSecret).Create(ctx, tt.args.principal)
req.AddCookie(&http.Cookie{
Name: "session",
Value: string(token),
HttpOnly: true,
Path: "/",
})
// Make actual http request
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("failed to make httprequest: %v", err)
return
}
content := resp.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != tt.wants.statusCode {
t.Errorf(
"%s %s Status Code = %v, want %v",
tt.args.method,
tt.args.path,
resp.StatusCode,
tt.wants.statusCode,
)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf(
"%s %s Content Type = %v, want %v",
tt.args.method,
tt.args.path,
content,
tt.wants.contentType,
)
}
if eq, err := jsonEqual(tt.wants.body, string(body)); err != nil || !eq {
t.Errorf(
"%s %s Body = %v, want %v",
tt.args.method,
tt.args.path,
string(body),
tt.wants.body,
)
}
tt.args.server.Listener.Close()
})
}
}

54
integrations/utils.go Normal file
View File

@ -0,0 +1,54 @@
package integrations
import (
"encoding/json"
"io/ioutil"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"github.com/google/go-cmp/cmp"
)
func hostAndPort() (string, int) {
s := httptest.NewServer(nil)
defer s.Close()
u, err := url.Parse(s.URL)
if err != nil {
panic(err)
}
xs := strings.Split(u.Host, ":")
host := xs[0]
portStr := xs[1]
port, err := strconv.Atoi(portStr)
if err != nil {
panic(err)
}
return host, port
}
func newBoltFile() string {
f, err := ioutil.TempFile("", "chronograf-bolt-")
if err != nil {
panic(err)
}
f.Close()
return f.Name()
}
func jsonEqual(s1, s2 string) (eq bool, err error) {
var o1, o2 interface{}
if err = json.Unmarshal([]byte(s1), &o1); err != nil {
return
}
if err = json.Unmarshal([]byte(s2), &o2); err != nil {
return
}
return cmp.Equal(o1, o2), nil
}

View File

@ -6,15 +6,15 @@ import (
"github.com/influxdata/chronograf"
)
// MultiLayoutStore is a Layoutstore that contains multiple LayoutStores
// MultiLayoutsStore is a LayoutsStore that contains multiple LayoutsStores
// The All method will return the set of all Layouts.
// Each method will be tried against the Stores slice serially.
type MultiLayoutStore struct {
Stores []chronograf.LayoutStore
type MultiLayoutsStore struct {
Stores []chronograf.LayoutsStore
}
// All returns the set of all layouts
func (s *MultiLayoutStore) All(ctx context.Context) ([]chronograf.Layout, error) {
func (s *MultiLayoutsStore) All(ctx context.Context) ([]chronograf.Layout, error) {
all := []chronograf.Layout{}
layoutSet := map[string]chronograf.Layout{}
ok := false
@ -42,8 +42,8 @@ func (s *MultiLayoutStore) All(ctx context.Context) ([]chronograf.Layout, error)
return all, nil
}
// Add creates a new dashboard in the LayoutStore. Tries each store sequentially until success.
func (s *MultiLayoutStore) Add(ctx context.Context, layout chronograf.Layout) (chronograf.Layout, error) {
// Add creates a new dashboard in the LayoutsStore. Tries each store sequentially until success.
func (s *MultiLayoutsStore) Add(ctx context.Context, layout chronograf.Layout) (chronograf.Layout, error) {
var err error
for _, store := range s.Stores {
var l chronograf.Layout
@ -57,7 +57,7 @@ func (s *MultiLayoutStore) Add(ctx context.Context, layout chronograf.Layout) (c
// Delete the dashboard from the store. Searches through all stores to find Layout and
// then deletes from that store.
func (s *MultiLayoutStore) Delete(ctx context.Context, layout chronograf.Layout) error {
func (s *MultiLayoutsStore) Delete(ctx context.Context, layout chronograf.Layout) error {
var err error
for _, store := range s.Stores {
err = store.Delete(ctx, layout)
@ -69,7 +69,7 @@ func (s *MultiLayoutStore) Delete(ctx context.Context, layout chronograf.Layout)
}
// Get retrieves Layout if `ID` exists. Searches through each store sequentially until success.
func (s *MultiLayoutStore) Get(ctx context.Context, ID string) (chronograf.Layout, error) {
func (s *MultiLayoutsStore) Get(ctx context.Context, ID string) (chronograf.Layout, error) {
var err error
for _, store := range s.Stores {
var l chronograf.Layout
@ -82,7 +82,7 @@ func (s *MultiLayoutStore) Get(ctx context.Context, ID string) (chronograf.Layou
}
// Update the dashboard in the store. Searches through each store sequentially until success.
func (s *MultiLayoutStore) Update(ctx context.Context, layout chronograf.Layout) error {
func (s *MultiLayoutsStore) Update(ctx context.Context, layout chronograf.Layout) error {
var err error
for _, store := range s.Stores {
err = store.Update(ctx, layout)

51
mocks/auth.go Normal file
View File

@ -0,0 +1,51 @@
package mocks
import (
"context"
"net/http"
"github.com/influxdata/chronograf/oauth2"
)
// Authenticator implements a OAuth2 authenticator
type Authenticator struct {
Principal oauth2.Principal
ValidateErr error
ExtendErr error
Serialized string
}
// Validate returns Principal associated with authenticated and authorized
// entity if successful.
func (a *Authenticator) Validate(context.Context, *http.Request) (oauth2.Principal, error) {
return a.Principal, a.ValidateErr
}
// Extend will extend the lifetime of a already validated Principal
func (a *Authenticator) Extend(ctx context.Context, w http.ResponseWriter, p oauth2.Principal) (oauth2.Principal, error) {
cookie := http.Cookie{}
http.SetCookie(w, &cookie)
return a.Principal, a.ExtendErr
}
// Authorize will grant privileges to a Principal
func (a *Authenticator) Authorize(ctx context.Context, w http.ResponseWriter, p oauth2.Principal) error {
cookie := http.Cookie{}
http.SetCookie(w, &cookie)
return nil
}
// Expire revokes privileges from a Principal
func (a *Authenticator) Expire(http.ResponseWriter) {}
// ValidAuthorization returns the Principal
func (a *Authenticator) ValidAuthorization(ctx context.Context, serializedAuthorization string) (oauth2.Principal, error) {
return oauth2.Principal{}, nil
}
// Serialize the serialized values stored on the Authenticator
func (a *Authenticator) Serialize(context.Context, oauth2.Principal) (string, error) {
return a.Serialized, nil
}

View File

@ -6,9 +6,9 @@ import (
"github.com/influxdata/chronograf"
)
var _ chronograf.LayoutStore = &LayoutStore{}
var _ chronograf.LayoutsStore = &LayoutsStore{}
type LayoutStore struct {
type LayoutsStore struct {
AddF func(ctx context.Context, layout chronograf.Layout) (chronograf.Layout, error)
AllF func(ctx context.Context) ([]chronograf.Layout, error)
DeleteF func(ctx context.Context, layout chronograf.Layout) error
@ -16,22 +16,22 @@ type LayoutStore struct {
UpdateF func(ctx context.Context, layout chronograf.Layout) error
}
func (s *LayoutStore) Add(ctx context.Context, layout chronograf.Layout) (chronograf.Layout, error) {
func (s *LayoutsStore) Add(ctx context.Context, layout chronograf.Layout) (chronograf.Layout, error) {
return s.AddF(ctx, layout)
}
func (s *LayoutStore) All(ctx context.Context) ([]chronograf.Layout, error) {
func (s *LayoutsStore) All(ctx context.Context) ([]chronograf.Layout, error) {
return s.AllF(ctx)
}
func (s *LayoutStore) Delete(ctx context.Context, layout chronograf.Layout) error {
func (s *LayoutsStore) Delete(ctx context.Context, layout chronograf.Layout) error {
return s.DeleteF(ctx, layout)
}
func (s *LayoutStore) Get(ctx context.Context, id string) (chronograf.Layout, error) {
func (s *LayoutsStore) Get(ctx context.Context, id string) (chronograf.Layout, error) {
return s.GetF(ctx, id)
}
func (s *LayoutStore) Update(ctx context.Context, layout chronograf.Layout) error {
func (s *LayoutsStore) Update(ctx context.Context, layout chronograf.Layout) error {
return s.UpdateF(ctx, layout)
}

47
mocks/organizations.go Normal file
View File

@ -0,0 +1,47 @@
package mocks
import (
"context"
"github.com/influxdata/chronograf"
)
var _ chronograf.OrganizationsStore = &OrganizationsStore{}
type OrganizationsStore struct {
AllF func(context.Context) ([]chronograf.Organization, error)
AddF func(context.Context, *chronograf.Organization) (*chronograf.Organization, error)
DeleteF func(context.Context, *chronograf.Organization) error
GetF func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error)
UpdateF func(context.Context, *chronograf.Organization) error
CreateDefaultF func(context.Context) error
DefaultOrganizationF func(context.Context) (*chronograf.Organization, error)
}
func (s *OrganizationsStore) CreateDefault(ctx context.Context) error {
return s.CreateDefaultF(ctx)
}
func (s *OrganizationsStore) DefaultOrganization(ctx context.Context) (*chronograf.Organization, error) {
return s.DefaultOrganizationF(ctx)
}
func (s *OrganizationsStore) Add(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) {
return s.AddF(ctx, o)
}
func (s *OrganizationsStore) All(ctx context.Context) ([]chronograf.Organization, error) {
return s.AllF(ctx)
}
func (s *OrganizationsStore) Delete(ctx context.Context, o *chronograf.Organization) error {
return s.DeleteF(ctx, o)
}
func (s *OrganizationsStore) Get(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return s.GetF(ctx, q)
}
func (s *OrganizationsStore) Update(ctx context.Context, o *chronograf.Organization) error {
return s.UpdateF(ctx, o)
}

41
mocks/store.go Normal file
View File

@ -0,0 +1,41 @@
package mocks
import (
"context"
"github.com/influxdata/chronograf"
)
// Store is a server.DataStore
type Store struct {
SourcesStore chronograf.SourcesStore
ServersStore chronograf.ServersStore
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
OrganizationsStore chronograf.OrganizationsStore
}
func (s *Store) Sources(ctx context.Context) chronograf.SourcesStore {
return s.SourcesStore
}
func (s *Store) Servers(ctx context.Context) chronograf.ServersStore {
return s.ServersStore
}
func (s *Store) Layouts(ctx context.Context) chronograf.LayoutsStore {
return s.LayoutsStore
}
func (s *Store) Users(ctx context.Context) chronograf.UsersStore {
return s.UsersStore
}
func (s *Store) Organizations(ctx context.Context) chronograf.OrganizationsStore {
return s.OrganizationsStore
}
func (s *Store) Dashboards(ctx context.Context) chronograf.DashboardsStore {
return s.DashboardsStore
}

View File

@ -13,8 +13,9 @@ type UsersStore struct {
AllF func(context.Context) ([]chronograf.User, error)
AddF func(context.Context, *chronograf.User) (*chronograf.User, error)
DeleteF func(context.Context, *chronograf.User) error
GetF func(ctx context.Context, name string) (*chronograf.User, error)
GetF func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error)
UpdateF func(context.Context, *chronograf.User) error
NumF func(context.Context) (int, error)
}
// All lists all users from the UsersStore
@ -22,6 +23,11 @@ func (s *UsersStore) All(ctx context.Context) ([]chronograf.User, error) {
return s.AllF(ctx)
}
// Num returns the number of users in the UsersStore
func (s *UsersStore) Num(ctx context.Context) (int, error) {
return s.NumF(ctx)
}
// Add a new User in the UsersStore
func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return s.AddF(ctx, u)
@ -33,8 +39,8 @@ func (s *UsersStore) Delete(ctx context.Context, u *chronograf.User) error {
}
// Get retrieves a user if name exists.
func (s *UsersStore) Get(ctx context.Context, name string) (*chronograf.User, error) {
return s.GetF(ctx, name)
func (s *UsersStore) Get(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
return s.GetF(ctx, q)
}
// Update the user's permissions or roles

33
noop/dashboards.go Normal file
View File

@ -0,0 +1,33 @@
package noop
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
// ensure DashboardsStore implements chronograf.DashboardsStore
var _ chronograf.DashboardsStore = &DashboardsStore{}
type DashboardsStore struct{}
func (s *DashboardsStore) All(context.Context) ([]chronograf.Dashboard, error) {
return nil, fmt.Errorf("no dashboards found")
}
func (s *DashboardsStore) Add(context.Context, chronograf.Dashboard) (chronograf.Dashboard, error) {
return chronograf.Dashboard{}, fmt.Errorf("failed to add dashboard")
}
func (s *DashboardsStore) Delete(context.Context, chronograf.Dashboard) error {
return fmt.Errorf("failed to delete dashboard")
}
func (s *DashboardsStore) Get(ctx context.Context, ID chronograf.DashboardID) (chronograf.Dashboard, error) {
return chronograf.Dashboard{}, chronograf.ErrDashboardNotFound
}
func (s *DashboardsStore) Update(context.Context, chronograf.Dashboard) error {
return fmt.Errorf("failed to update dashboard")
}

33
noop/layouts.go Normal file
View File

@ -0,0 +1,33 @@
package noop
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
// ensure LayoutsStore implements chronograf.LayoutsStore
var _ chronograf.LayoutsStore = &LayoutsStore{}
type LayoutsStore struct{}
func (s *LayoutsStore) All(context.Context) ([]chronograf.Layout, error) {
return nil, fmt.Errorf("no layouts found")
}
func (s *LayoutsStore) Add(context.Context, chronograf.Layout) (chronograf.Layout, error) {
return chronograf.Layout{}, fmt.Errorf("failed to add layout")
}
func (s *LayoutsStore) Delete(context.Context, chronograf.Layout) error {
return fmt.Errorf("failed to delete layout")
}
func (s *LayoutsStore) Get(ctx context.Context, ID string) (chronograf.Layout, error) {
return chronograf.Layout{}, chronograf.ErrLayoutNotFound
}
func (s *LayoutsStore) Update(context.Context, chronograf.Layout) error {
return fmt.Errorf("failed to update layout")
}

41
noop/organizations.go Normal file
View File

@ -0,0 +1,41 @@
package noop
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
// ensure OrganizationsStore implements chronograf.OrganizationsStore
var _ chronograf.OrganizationsStore = &OrganizationsStore{}
type OrganizationsStore struct{}
func (s *OrganizationsStore) CreateDefault(context.Context) error {
return fmt.Errorf("failed to add organization")
}
func (s *OrganizationsStore) DefaultOrganization(context.Context) (*chronograf.Organization, error) {
return nil, fmt.Errorf("failed to retrieve default organization")
}
func (s *OrganizationsStore) All(context.Context) ([]chronograf.Organization, error) {
return nil, fmt.Errorf("no organizations found")
}
func (s *OrganizationsStore) Add(context.Context, *chronograf.Organization) (*chronograf.Organization, error) {
return nil, fmt.Errorf("failed to add organization")
}
func (s *OrganizationsStore) Delete(context.Context, *chronograf.Organization) error {
return fmt.Errorf("failed to delete organization")
}
func (s *OrganizationsStore) Get(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return nil, chronograf.ErrOrganizationNotFound
}
func (s *OrganizationsStore) Update(context.Context, *chronograf.Organization) error {
return fmt.Errorf("failed to update organization")
}

33
noop/servers.go Normal file
View File

@ -0,0 +1,33 @@
package noop
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
// ensure ServersStore implements chronograf.ServersStore
var _ chronograf.ServersStore = &ServersStore{}
type ServersStore struct{}
func (s *ServersStore) All(context.Context) ([]chronograf.Server, error) {
return nil, fmt.Errorf("no servers found")
}
func (s *ServersStore) Add(context.Context, chronograf.Server) (chronograf.Server, error) {
return chronograf.Server{}, fmt.Errorf("failed to add server")
}
func (s *ServersStore) Delete(context.Context, chronograf.Server) error {
return fmt.Errorf("failed to delete server")
}
func (s *ServersStore) Get(ctx context.Context, ID int) (chronograf.Server, error) {
return chronograf.Server{}, chronograf.ErrServerNotFound
}
func (s *ServersStore) Update(context.Context, chronograf.Server) error {
return fmt.Errorf("failed to update server")
}

33
noop/sources.go Normal file
View File

@ -0,0 +1,33 @@
package noop
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
// ensure SourcesStore implements chronograf.SourcesStore
var _ chronograf.SourcesStore = &SourcesStore{}
type SourcesStore struct{}
func (s *SourcesStore) All(context.Context) ([]chronograf.Source, error) {
return nil, fmt.Errorf("no sources found")
}
func (s *SourcesStore) Add(context.Context, chronograf.Source) (chronograf.Source, error) {
return chronograf.Source{}, fmt.Errorf("failed to add source")
}
func (s *SourcesStore) Delete(context.Context, chronograf.Source) error {
return fmt.Errorf("failed to delete source")
}
func (s *SourcesStore) Get(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{}, chronograf.ErrSourceNotFound
}
func (s *SourcesStore) Update(context.Context, chronograf.Source) error {
return fmt.Errorf("failed to update source")
}

37
noop/users.go Normal file
View File

@ -0,0 +1,37 @@
package noop
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
// ensure UsersStore implements chronograf.UsersStore
var _ chronograf.UsersStore = &UsersStore{}
type UsersStore struct{}
func (s *UsersStore) All(context.Context) ([]chronograf.User, error) {
return nil, fmt.Errorf("no users found")
}
func (s *UsersStore) Add(context.Context, *chronograf.User) (*chronograf.User, error) {
return nil, fmt.Errorf("failed to add user")
}
func (s *UsersStore) Delete(context.Context, *chronograf.User) error {
return fmt.Errorf("failed to delete user")
}
func (s *UsersStore) Get(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
return nil, chronograf.ErrUserNotFound
}
func (s *UsersStore) Update(context.Context, *chronograf.User) error {
return fmt.Errorf("failed to update user")
}
func (s *UsersStore) Num(context.Context) (int, error) {
return 0, fmt.Errorf("failed to get number of users")
}

View File

@ -31,6 +31,9 @@ var _ gojwt.Claims = &Claims{}
// Claims extends jwt.StandardClaims' Valid to make sure claims has a subject.
type Claims struct {
gojwt.StandardClaims
// We were unable to find a standard claim at https://www.iana.org/assignments/jwt/jwt.xhtmldd
// that felt appropriate for Organization. As a result, we added a custom `org` field.
Organization string `json:"org,omitempty"`
}
// Valid adds an empty subject test to the StandardClaims checks.
@ -93,10 +96,11 @@ func (j *JWT) ValidClaims(jwtToken Token, lifespan time.Duration, alg gojwt.Keyf
}
return Principal{
Subject: claims.Subject,
Issuer: claims.Issuer,
ExpiresAt: exp,
IssuedAt: iat,
Subject: claims.Subject,
Issuer: claims.Issuer,
Organization: claims.Organization,
ExpiresAt: exp,
IssuedAt: iat,
}, nil
}
@ -105,13 +109,14 @@ func (j *JWT) Create(ctx context.Context, user Principal) (Token, error) {
// Create a new token object, specifying signing method and the claims
// you would like it to contain.
claims := &Claims{
gojwt.StandardClaims{
StandardClaims: gojwt.StandardClaims{
Subject: user.Subject,
Issuer: user.Issuer,
ExpiresAt: user.ExpiresAt.Unix(),
IssuedAt: user.IssuedAt.Unix(),
NotBefore: user.IssuedAt.Unix(),
},
Organization: user.Organization,
}
token := gojwt.NewWithClaims(gojwt.SigningMethodHS256, claims)
// Sign and get the complete encoded token as a string using the secret

View File

@ -13,8 +13,9 @@ import (
func TestAuthenticate(t *testing.T) {
history := time.Unix(-446774400, 0)
var tests = []struct {
Desc string
Secret string
Desc string
Secret string
// JWT tokens were generated at https://jwt.io/ using their Debugger
Token oauth2.Token
Duration time.Duration
Principal oauth2.Principal
@ -40,6 +41,18 @@ func TestAuthenticate(t *testing.T) {
IssuedAt: history,
},
},
{
Desc: "Test valid jwt token with organization",
Secret: "secret",
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsIm9yZyI6IjEzMzciLCJpYXQiOi00NDY3NzQ0MDAsImV4cCI6LTQ0Njc3NDM5OSwibmJmIjotNDQ2Nzc0NDAwfQ.b38MK5liimWsvvJr4a3GNYRDJOAN7WCrfZ0FfZftqjc",
Duration: time.Second,
Principal: oauth2.Principal{
Subject: "/chronograf/v1/users/1",
Organization: "1337",
ExpiresAt: history.Add(time.Second),
IssuedAt: history,
},
},
{
Desc: "Test expired jwt token",
Secret: "secret",

View File

@ -30,10 +30,11 @@ var (
// Principal is any entity that can be authenticated
type Principal struct {
Subject string
Issuer string
ExpiresAt time.Time
IssuedAt time.Time
Subject string
Issuer string
Organization string
ExpiresAt time.Time
IssuedAt time.Time
}
/* Interfaces */

112
organizations/dashboards.go Normal file
View File

@ -0,0 +1,112 @@
package organizations
import (
"context"
"github.com/influxdata/chronograf"
)
// ensure that DashboardsStore implements chronograf.DashboardStore
var _ chronograf.DashboardsStore = &DashboardsStore{}
// DashboardsStore facade on a DashboardStore that filters dashboards
// by organization.
type DashboardsStore struct {
store chronograf.DashboardsStore
organization string
}
// NewDashboardsStore creates a new DashboardsStore from an existing
// chronograf.DashboardStore and an organization string
func NewDashboardsStore(s chronograf.DashboardsStore, org string) *DashboardsStore {
return &DashboardsStore{
store: s,
organization: org,
}
}
// All retrieves all dashboards from the underlying DashboardStore and filters them
// by organization.
func (s *DashboardsStore) All(ctx context.Context) ([]chronograf.Dashboard, error) {
err := validOrganization(ctx)
if err != nil {
return nil, err
}
ds, err := s.store.All(ctx)
if err != nil {
return nil, err
}
// This filters dashboards without allocating
// https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
dashboards := ds[:0]
for _, d := range ds {
if d.Organization == s.organization {
dashboards = append(dashboards, d)
}
}
return dashboards, nil
}
// Add creates a new Dashboard in the DashboardsStore with dashboard.Organization set to be the
// organization from the dashboard store.
func (s *DashboardsStore) Add(ctx context.Context, d chronograf.Dashboard) (chronograf.Dashboard, error) {
err := validOrganization(ctx)
if err != nil {
return chronograf.Dashboard{}, err
}
d.Organization = s.organization
return s.store.Add(ctx, d)
}
// Delete the dashboard from DashboardsStore
func (s *DashboardsStore) Delete(ctx context.Context, d chronograf.Dashboard) error {
err := validOrganization(ctx)
if err != nil {
return err
}
d, err = s.store.Get(ctx, d.ID)
if err != nil {
return err
}
return s.store.Delete(ctx, d)
}
// Get returns a Dashboard if the id exists and belongs to the organization that is set.
func (s *DashboardsStore) Get(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) {
err := validOrganization(ctx)
if err != nil {
return chronograf.Dashboard{}, err
}
d, err := s.store.Get(ctx, id)
if err != nil {
return chronograf.Dashboard{}, err
}
if d.Organization != s.organization {
return chronograf.Dashboard{}, chronograf.ErrDashboardNotFound
}
return d, nil
}
// Update the dashboard in DashboardsStore.
func (s *DashboardsStore) Update(ctx context.Context, d chronograf.Dashboard) error {
err := validOrganization(ctx)
if err != nil {
return err
}
_, err = s.store.Get(ctx, d.ID)
if err != nil {
return err
}
return s.store.Update(ctx, d)
}

View File

@ -0,0 +1,339 @@
package organizations_test
import (
"context"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/mocks"
"github.com/influxdata/chronograf/organizations"
)
// IgnoreFields is used because ID cannot be predicted reliably
// EquateEmpty is used because we want nil slices, arrays, and maps to be equal to the empty map
var dashboardCmpOptions = cmp.Options{
cmpopts.EquateEmpty(),
cmpopts.IgnoreFields(chronograf.Dashboard{}, "ID"),
}
func TestDashboards_All(t *testing.T) {
type fields struct {
DashboardsStore chronograf.DashboardsStore
}
type args struct {
organization string
ctx context.Context
}
tests := []struct {
name string
args args
fields fields
want []chronograf.Dashboard
wantRaw []chronograf.Dashboard
wantErr bool
}{
{
name: "No Dashboards",
fields: fields{
DashboardsStore: &mocks.DashboardsStore{
AllF: func(ctx context.Context) ([]chronograf.Dashboard, error) {
return nil, fmt.Errorf("No Dashboards")
},
},
},
wantErr: true,
},
{
name: "All Dashboards",
fields: fields{
DashboardsStore: &mocks.DashboardsStore{
AllF: func(ctx context.Context) ([]chronograf.Dashboard, error) {
return []chronograf.Dashboard{
{
Name: "howdy",
Organization: "1337",
},
{
Name: "doody",
Organization: "1338",
},
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
},
want: []chronograf.Dashboard{
{
Name: "howdy",
Organization: "1337",
},
},
},
}
for _, tt := range tests {
s := organizations.NewDashboardsStore(tt.fields.DashboardsStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
gots, err := s.All(tt.args.ctx)
if (err != nil) != tt.wantErr {
t.Errorf("%q. DashboardsStore.All() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
for i, got := range gots {
if diff := cmp.Diff(got, tt.want[i], dashboardCmpOptions...); diff != "" {
t.Errorf("%q. DashboardsStore.All():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
}
func TestDashboards_Add(t *testing.T) {
type fields struct {
DashboardsStore chronograf.DashboardsStore
}
type args struct {
organization string
ctx context.Context
dashboard chronograf.Dashboard
}
tests := []struct {
name string
args args
fields fields
want chronograf.Dashboard
wantErr bool
}{
{
name: "Add Dashboard",
fields: fields{
DashboardsStore: &mocks.DashboardsStore{
AddF: func(ctx context.Context, s chronograf.Dashboard) (chronograf.Dashboard, error) {
return s, nil
},
GetF: func(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) {
return chronograf.Dashboard{
ID: 1229,
Name: "howdy",
Organization: "1337",
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
dashboard: chronograf.Dashboard{
ID: 1229,
Name: "howdy",
},
},
want: chronograf.Dashboard{
Name: "howdy",
Organization: "1337",
},
},
}
for _, tt := range tests {
s := organizations.NewDashboardsStore(tt.fields.DashboardsStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
d, err := s.Add(tt.args.ctx, tt.args.dashboard)
if (err != nil) != tt.wantErr {
t.Errorf("%q. DashboardsStore.Add() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
got, err := s.Get(tt.args.ctx, d.ID)
if diff := cmp.Diff(got, tt.want, dashboardCmpOptions...); diff != "" {
t.Errorf("%q. DashboardsStore.Add():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
func TestDashboards_Delete(t *testing.T) {
type fields struct {
DashboardsStore chronograf.DashboardsStore
}
type args struct {
organization string
ctx context.Context
dashboard chronograf.Dashboard
}
tests := []struct {
name string
fields fields
args args
want []chronograf.Dashboard
addFirst bool
wantErr bool
}{
{
name: "Delete dashboard",
fields: fields{
DashboardsStore: &mocks.DashboardsStore{
DeleteF: func(ctx context.Context, s chronograf.Dashboard) error {
return nil
},
GetF: func(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) {
return chronograf.Dashboard{
ID: 1229,
Name: "howdy",
Organization: "1337",
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
dashboard: chronograf.Dashboard{
ID: 1229,
Name: "howdy",
Organization: "1337",
},
},
addFirst: true,
},
}
for _, tt := range tests {
s := organizations.NewDashboardsStore(tt.fields.DashboardsStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
err := s.Delete(tt.args.ctx, tt.args.dashboard)
if (err != nil) != tt.wantErr {
t.Errorf("%q. DashboardsStore.All() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
}
}
func TestDashboards_Get(t *testing.T) {
type fields struct {
DashboardsStore chronograf.DashboardsStore
}
type args struct {
organization string
ctx context.Context
dashboard chronograf.Dashboard
}
tests := []struct {
name string
fields fields
args args
want chronograf.Dashboard
addFirst bool
wantErr bool
}{
{
name: "Get Dashboard",
fields: fields{
DashboardsStore: &mocks.DashboardsStore{
GetF: func(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) {
return chronograf.Dashboard{
ID: 1229,
Name: "howdy",
Organization: "1337",
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
dashboard: chronograf.Dashboard{
ID: 1229,
Name: "howdy",
Organization: "1337",
},
},
want: chronograf.Dashboard{
ID: 1229,
Name: "howdy",
Organization: "1337",
},
},
}
for _, tt := range tests {
s := organizations.NewDashboardsStore(tt.fields.DashboardsStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
got, err := s.Get(tt.args.ctx, tt.args.dashboard.ID)
if (err != nil) != tt.wantErr {
t.Errorf("%q. DashboardsStore.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if diff := cmp.Diff(got, tt.want, dashboardCmpOptions...); diff != "" {
t.Errorf("%q. DashboardsStore.Get():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
func TestDashboards_Update(t *testing.T) {
type fields struct {
DashboardsStore chronograf.DashboardsStore
}
type args struct {
organization string
ctx context.Context
dashboard chronograf.Dashboard
name string
}
tests := []struct {
name string
fields fields
args args
want chronograf.Dashboard
addFirst bool
wantErr bool
}{
{
name: "Update Dashboard Name",
fields: fields{
DashboardsStore: &mocks.DashboardsStore{
UpdateF: func(ctx context.Context, s chronograf.Dashboard) error {
return nil
},
GetF: func(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) {
return chronograf.Dashboard{
ID: 1229,
Name: "doody",
Organization: "1337",
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
dashboard: chronograf.Dashboard{
ID: 1229,
Name: "howdy",
Organization: "1337",
},
name: "doody",
},
want: chronograf.Dashboard{
Name: "doody",
Organization: "1337",
},
addFirst: true,
},
}
for _, tt := range tests {
if tt.args.name != "" {
tt.args.dashboard.Name = tt.args.name
}
s := organizations.NewDashboardsStore(tt.fields.DashboardsStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
err := s.Update(tt.args.ctx, tt.args.dashboard)
if (err != nil) != tt.wantErr {
t.Errorf("%q. DashboardsStore.Update() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
got, err := s.Get(tt.args.ctx, tt.args.dashboard.ID)
if diff := cmp.Diff(got, tt.want, dashboardCmpOptions...); diff != "" {
t.Errorf("%q. DashboardsStore.Update():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}

View File

@ -0,0 +1,158 @@
package organizations
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
type contextKey string
// ContextKey is the key used to specify the
// organization via context
const ContextKey = contextKey("organization")
func validOrganization(ctx context.Context) error {
// prevents panic in case of nil context
if ctx == nil {
return fmt.Errorf("expect non nil context")
}
orgID, ok := ctx.Value(ContextKey).(string)
// should never happen
if !ok {
return fmt.Errorf("expected organization key to be a string")
}
if orgID == "" {
return fmt.Errorf("expected organization key to be set")
}
return nil
}
// ensure that OrganizationsStore implements chronograf.OrganizationStore
var _ chronograf.OrganizationsStore = &OrganizationsStore{}
// OrganizationsStore facade on a OrganizationStore that filters organizations
// by organization.
type OrganizationsStore struct {
store chronograf.OrganizationsStore
organization string
}
// NewOrganizationsStore creates a new OrganizationsStore from an existing
// chronograf.OrganizationStore and an organization string
func NewOrganizationsStore(s chronograf.OrganizationsStore, org string) *OrganizationsStore {
return &OrganizationsStore{
store: s,
organization: org,
}
}
// All retrieves all organizations from the underlying OrganizationStore and filters them
// by organization.
func (s *OrganizationsStore) All(ctx context.Context) ([]chronograf.Organization, error) {
err := validOrganization(ctx)
if err != nil {
return nil, err
}
ds, err := s.store.All(ctx)
if err != nil {
return nil, err
}
defaultOrg, err := s.store.DefaultOrganization(ctx)
if err != nil {
return nil, err
}
defaultOrgID := fmt.Sprintf("%d", defaultOrg.ID)
// This filters organizations without allocating
// https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
organizations := ds[:0]
for _, d := range ds {
id := fmt.Sprintf("%d", d.ID)
switch id {
case s.organization, defaultOrgID:
organizations = append(organizations, d)
default:
continue
}
}
return organizations, nil
}
// Add creates a new Organization in the OrganizationsStore with organization.Organization set to be the
// organization from the organization store.
func (s *OrganizationsStore) Add(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) {
return nil, fmt.Errorf("cannot create organization")
}
// Delete the organization from OrganizationsStore
func (s *OrganizationsStore) Delete(ctx context.Context, o *chronograf.Organization) error {
err := validOrganization(ctx)
if err != nil {
return err
}
o, err = s.store.Get(ctx, chronograf.OrganizationQuery{ID: &o.ID})
if err != nil {
return err
}
return s.store.Delete(ctx, o)
}
// Get returns a Organization if the id exists and belongs to the organization that is set.
func (s *OrganizationsStore) Get(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
err := validOrganization(ctx)
if err != nil {
return nil, err
}
d, err := s.store.Get(ctx, q)
if err != nil {
return nil, err
}
if fmt.Sprintf("%d", d.ID) != s.organization {
return nil, chronograf.ErrOrganizationNotFound
}
return d, nil
}
// Update the organization in OrganizationsStore.
func (s *OrganizationsStore) Update(ctx context.Context, o *chronograf.Organization) error {
err := validOrganization(ctx)
if err != nil {
return err
}
_, err = s.store.Get(ctx, chronograf.OrganizationQuery{ID: &o.ID})
if err != nil {
return err
}
return s.store.Update(ctx, o)
}
func (s *OrganizationsStore) CreateDefault(ctx context.Context) error {
err := validOrganization(ctx)
if err != nil {
return err
}
return s.store.CreateDefault(ctx)
}
func (s *OrganizationsStore) DefaultOrganization(ctx context.Context) (*chronograf.Organization, error) {
err := validOrganization(ctx)
if err != nil {
return nil, err
}
return s.store.DefaultOrganization(ctx)
}

View File

@ -0,0 +1,345 @@
package organizations_test
import (
"context"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/mocks"
"github.com/influxdata/chronograf/organizations"
)
// IgnoreFields is used because ID cannot be predicted reliably
// EquateEmpty is used because we want nil slices, arrays, and maps to be equal to the empty map
var organizationCmpOptions = cmp.Options{
cmpopts.EquateEmpty(),
cmpopts.IgnoreFields(chronograf.Organization{}, "ID"),
}
func TestOrganizations_All(t *testing.T) {
type fields struct {
OrganizationsStore chronograf.OrganizationsStore
}
type args struct {
organization string
ctx context.Context
}
tests := []struct {
name string
args args
fields fields
want []chronograf.Organization
wantRaw []chronograf.Organization
wantErr bool
}{
{
name: "No Organizations",
fields: fields{
OrganizationsStore: &mocks.OrganizationsStore{
AllF: func(ctx context.Context) ([]chronograf.Organization, error) {
return nil, fmt.Errorf("No Organizations")
},
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
Name: "Default",
}, nil
},
},
},
wantErr: true,
},
{
name: "All Organizations",
fields: fields{
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
Name: "Default",
}, nil
},
AllF: func(ctx context.Context) ([]chronograf.Organization, error) {
return []chronograf.Organization{
{
Name: "howdy",
ID: 1337,
},
{
Name: "doody",
ID: 1447,
},
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
},
want: []chronograf.Organization{
{
Name: "howdy",
ID: 1337,
},
{
Name: "Default",
ID: 0,
},
},
},
}
for _, tt := range tests {
s := organizations.NewOrganizationsStore(tt.fields.OrganizationsStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
gots, err := s.All(tt.args.ctx)
if (err != nil) != tt.wantErr {
t.Errorf("%q. OrganizationsStore.All() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
for i, got := range gots {
if diff := cmp.Diff(got, tt.want[i], organizationCmpOptions...); diff != "" {
t.Errorf("%q. OrganizationsStore.All():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
}
func TestOrganizations_Add(t *testing.T) {
type fields struct {
OrganizationsStore chronograf.OrganizationsStore
}
type args struct {
organizationID string
ctx context.Context
organization *chronograf.Organization
}
tests := []struct {
name string
args args
fields fields
want *chronograf.Organization
wantErr bool
}{
{
name: "Add Organization",
fields: fields{
OrganizationsStore: &mocks.OrganizationsStore{
AddF: func(ctx context.Context, s *chronograf.Organization) (*chronograf.Organization, error) {
return s, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 1229,
Name: "howdy",
}, nil
},
},
},
args: args{
organizationID: "1229",
ctx: context.Background(),
organization: &chronograf.Organization{
Name: "howdy",
},
},
wantErr: true,
},
}
for _, tt := range tests {
s := organizations.NewOrganizationsStore(tt.fields.OrganizationsStore, tt.args.organizationID)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organizationID)
d, err := s.Add(tt.args.ctx, tt.args.organization)
if (err != nil) != tt.wantErr {
t.Errorf("%q. OrganizationsStore.Add() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if tt.wantErr {
continue
}
got, err := s.Get(tt.args.ctx, chronograf.OrganizationQuery{ID: &d.ID})
if diff := cmp.Diff(got, tt.want, organizationCmpOptions...); diff != "" {
t.Errorf("%q. OrganizationsStore.Add():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
func TestOrganizations_Delete(t *testing.T) {
type fields struct {
OrganizationsStore chronograf.OrganizationsStore
}
type args struct {
organizationID string
ctx context.Context
organization *chronograf.Organization
}
tests := []struct {
name string
fields fields
args args
want []chronograf.Organization
addFirst bool
wantErr bool
}{
{
name: "Delete organization",
fields: fields{
OrganizationsStore: &mocks.OrganizationsStore{
DeleteF: func(ctx context.Context, s *chronograf.Organization) error {
return nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 1229,
Name: "howdy",
}, nil
},
},
},
args: args{
organizationID: "1229",
ctx: context.Background(),
organization: &chronograf.Organization{
ID: 1229,
Name: "howdy",
},
},
addFirst: true,
},
}
for _, tt := range tests {
s := organizations.NewOrganizationsStore(tt.fields.OrganizationsStore, tt.args.organizationID)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organizationID)
err := s.Delete(tt.args.ctx, tt.args.organization)
if (err != nil) != tt.wantErr {
t.Errorf("%q. OrganizationsStore.All() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
}
}
func TestOrganizations_Get(t *testing.T) {
type fields struct {
OrganizationsStore chronograf.OrganizationsStore
}
type args struct {
organizationID string
ctx context.Context
organization *chronograf.Organization
}
tests := []struct {
name string
fields fields
args args
want *chronograf.Organization
addFirst bool
wantErr bool
}{
{
name: "Get Organization",
fields: fields{
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 1337,
Name: "howdy",
}, nil
},
},
},
args: args{
organizationID: "1337",
ctx: context.Background(),
organization: &chronograf.Organization{
ID: 1337,
Name: "howdy",
},
},
want: &chronograf.Organization{
ID: 1337,
Name: "howdy",
},
},
}
for _, tt := range tests {
s := organizations.NewOrganizationsStore(tt.fields.OrganizationsStore, tt.args.organizationID)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organizationID)
got, err := s.Get(tt.args.ctx, chronograf.OrganizationQuery{ID: &tt.args.organization.ID})
if (err != nil) != tt.wantErr {
t.Errorf("%q. OrganizationsStore.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if diff := cmp.Diff(got, tt.want, organizationCmpOptions...); diff != "" {
t.Errorf("%q. OrganizationsStore.Get():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
func TestOrganizations_Update(t *testing.T) {
type fields struct {
OrganizationsStore chronograf.OrganizationsStore
}
type args struct {
organizationID string
ctx context.Context
organization *chronograf.Organization
name string
}
tests := []struct {
name string
fields fields
args args
want *chronograf.Organization
addFirst bool
wantErr bool
}{
{
name: "Update Organization Name",
fields: fields{
OrganizationsStore: &mocks.OrganizationsStore{
UpdateF: func(ctx context.Context, s *chronograf.Organization) error {
return nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 1229,
Name: "doody",
}, nil
},
},
},
args: args{
organizationID: "1229",
ctx: context.Background(),
organization: &chronograf.Organization{
ID: 1229,
Name: "howdy",
},
name: "doody",
},
want: &chronograf.Organization{
Name: "doody",
},
addFirst: true,
},
}
for _, tt := range tests {
if tt.args.name != "" {
tt.args.organization.Name = tt.args.name
}
s := organizations.NewOrganizationsStore(tt.fields.OrganizationsStore, tt.args.organizationID)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organizationID)
err := s.Update(tt.args.ctx, tt.args.organization)
if (err != nil) != tt.wantErr {
t.Errorf("%q. OrganizationsStore.Update() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
got, err := s.Get(tt.args.ctx, chronograf.OrganizationQuery{ID: &tt.args.organization.ID})
if diff := cmp.Diff(got, tt.want, organizationCmpOptions...); diff != "" {
t.Errorf("%q. OrganizationsStore.Update():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}

111
organizations/servers.go Normal file
View File

@ -0,0 +1,111 @@
package organizations
import (
"context"
"github.com/influxdata/chronograf"
)
// ensure that ServersStore implements chronograf.ServerStore
var _ chronograf.ServersStore = &ServersStore{}
// ServersStore facade on a ServerStore that filters servers
// by organization.
type ServersStore struct {
store chronograf.ServersStore
organization string
}
// NewServersStore creates a new ServersStore from an existing
// chronograf.ServerStore and an organization string
func NewServersStore(s chronograf.ServersStore, org string) *ServersStore {
return &ServersStore{
store: s,
organization: org,
}
}
// All retrieves all servers from the underlying ServerStore and filters them
// by organization.
func (s *ServersStore) All(ctx context.Context) ([]chronograf.Server, error) {
err := validOrganization(ctx)
if err != nil {
return nil, err
}
ds, err := s.store.All(ctx)
if err != nil {
return nil, err
}
// This filters servers without allocating
// https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
servers := ds[:0]
for _, d := range ds {
if d.Organization == s.organization {
servers = append(servers, d)
}
}
return servers, nil
}
// Add creates a new Server in the ServersStore with server.Organization set to be the
// organization from the server store.
func (s *ServersStore) Add(ctx context.Context, d chronograf.Server) (chronograf.Server, error) {
err := validOrganization(ctx)
if err != nil {
return chronograf.Server{}, err
}
d.Organization = s.organization
return s.store.Add(ctx, d)
}
// Delete the server from ServersStore
func (s *ServersStore) Delete(ctx context.Context, d chronograf.Server) error {
err := validOrganization(ctx)
if err != nil {
return err
}
d, err = s.store.Get(ctx, d.ID)
if err != nil {
return err
}
return s.store.Delete(ctx, d)
}
// Get returns a Server if the id exists and belongs to the organization that is set.
func (s *ServersStore) Get(ctx context.Context, id int) (chronograf.Server, error) {
err := validOrganization(ctx)
if err != nil {
return chronograf.Server{}, err
}
d, err := s.store.Get(ctx, id)
if err != nil {
return chronograf.Server{}, err
}
if d.Organization != s.organization {
return chronograf.Server{}, chronograf.ErrServerNotFound
}
return d, nil
}
// Update the server in ServersStore.
func (s *ServersStore) Update(ctx context.Context, d chronograf.Server) error {
err := validOrganization(ctx)
if err != nil {
return err
}
_, err = s.store.Get(ctx, d.ID)
if err != nil {
return err
}
return s.store.Update(ctx, d)
}

View File

@ -0,0 +1,340 @@
package organizations_test
import (
"context"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/mocks"
"github.com/influxdata/chronograf/organizations"
)
// IgnoreFields is used because ID cannot be predicted reliably
// EquateEmpty is used because we want nil slices, arrays, and maps to be equal to the empty map
var serverCmpOptions = cmp.Options{
cmpopts.EquateEmpty(),
cmpopts.IgnoreFields(chronograf.Server{}, "ID"),
cmpopts.IgnoreFields(chronograf.Server{}, "Active"),
}
func TestServers_All(t *testing.T) {
type fields struct {
ServersStore chronograf.ServersStore
}
type args struct {
organization string
ctx context.Context
}
tests := []struct {
name string
args args
fields fields
want []chronograf.Server
wantRaw []chronograf.Server
wantErr bool
}{
{
name: "No Servers",
fields: fields{
ServersStore: &mocks.ServersStore{
AllF: func(ctx context.Context) ([]chronograf.Server, error) {
return nil, fmt.Errorf("No Servers")
},
},
},
wantErr: true,
},
{
name: "All Servers",
fields: fields{
ServersStore: &mocks.ServersStore{
AllF: func(ctx context.Context) ([]chronograf.Server, error) {
return []chronograf.Server{
{
Name: "howdy",
Organization: "1337",
},
{
Name: "doody",
Organization: "1338",
},
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
},
want: []chronograf.Server{
{
Name: "howdy",
Organization: "1337",
},
},
},
}
for _, tt := range tests {
s := organizations.NewServersStore(tt.fields.ServersStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
gots, err := s.All(tt.args.ctx)
if (err != nil) != tt.wantErr {
t.Errorf("%q. ServersStore.All() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
for i, got := range gots {
if diff := cmp.Diff(got, tt.want[i], serverCmpOptions...); diff != "" {
t.Errorf("%q. ServersStore.All():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
}
func TestServers_Add(t *testing.T) {
type fields struct {
ServersStore chronograf.ServersStore
}
type args struct {
organization string
ctx context.Context
server chronograf.Server
}
tests := []struct {
name string
args args
fields fields
want chronograf.Server
wantErr bool
}{
{
name: "Add Server",
fields: fields{
ServersStore: &mocks.ServersStore{
AddF: func(ctx context.Context, s chronograf.Server) (chronograf.Server, error) {
return s, nil
},
GetF: func(ctx context.Context, id int) (chronograf.Server, error) {
return chronograf.Server{
ID: 1229,
Name: "howdy",
Organization: "1337",
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
server: chronograf.Server{
ID: 1229,
Name: "howdy",
},
},
want: chronograf.Server{
Name: "howdy",
Organization: "1337",
},
},
}
for _, tt := range tests {
s := organizations.NewServersStore(tt.fields.ServersStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
d, err := s.Add(tt.args.ctx, tt.args.server)
if (err != nil) != tt.wantErr {
t.Errorf("%q. ServersStore.Add() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
got, err := s.Get(tt.args.ctx, d.ID)
if diff := cmp.Diff(got, tt.want, serverCmpOptions...); diff != "" {
t.Errorf("%q. ServersStore.Add():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
func TestServers_Delete(t *testing.T) {
type fields struct {
ServersStore chronograf.ServersStore
}
type args struct {
organization string
ctx context.Context
server chronograf.Server
}
tests := []struct {
name string
fields fields
args args
want []chronograf.Server
addFirst bool
wantErr bool
}{
{
name: "Delete server",
fields: fields{
ServersStore: &mocks.ServersStore{
DeleteF: func(ctx context.Context, s chronograf.Server) error {
return nil
},
GetF: func(ctx context.Context, id int) (chronograf.Server, error) {
return chronograf.Server{
ID: 1229,
Name: "howdy",
Organization: "1337",
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
server: chronograf.Server{
ID: 1229,
Name: "howdy",
Organization: "1337",
},
},
addFirst: true,
},
}
for _, tt := range tests {
s := organizations.NewServersStore(tt.fields.ServersStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
err := s.Delete(tt.args.ctx, tt.args.server)
if (err != nil) != tt.wantErr {
t.Errorf("%q. ServersStore.All() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
}
}
func TestServers_Get(t *testing.T) {
type fields struct {
ServersStore chronograf.ServersStore
}
type args struct {
organization string
ctx context.Context
server chronograf.Server
}
tests := []struct {
name string
fields fields
args args
want chronograf.Server
addFirst bool
wantErr bool
}{
{
name: "Get Server",
fields: fields{
ServersStore: &mocks.ServersStore{
GetF: func(ctx context.Context, id int) (chronograf.Server, error) {
return chronograf.Server{
ID: 1229,
Name: "howdy",
Organization: "1337",
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
server: chronograf.Server{
ID: 1229,
Name: "howdy",
Organization: "1337",
},
},
want: chronograf.Server{
ID: 1229,
Name: "howdy",
Organization: "1337",
},
},
}
for _, tt := range tests {
s := organizations.NewServersStore(tt.fields.ServersStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
got, err := s.Get(tt.args.ctx, tt.args.server.ID)
if (err != nil) != tt.wantErr {
t.Errorf("%q. ServersStore.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if diff := cmp.Diff(got, tt.want, serverCmpOptions...); diff != "" {
t.Errorf("%q. ServersStore.Get():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
func TestServers_Update(t *testing.T) {
type fields struct {
ServersStore chronograf.ServersStore
}
type args struct {
organization string
ctx context.Context
server chronograf.Server
name string
}
tests := []struct {
name string
fields fields
args args
want chronograf.Server
addFirst bool
wantErr bool
}{
{
name: "Update Server Name",
fields: fields{
ServersStore: &mocks.ServersStore{
UpdateF: func(ctx context.Context, s chronograf.Server) error {
return nil
},
GetF: func(ctx context.Context, id int) (chronograf.Server, error) {
return chronograf.Server{
ID: 1229,
Name: "doody",
Organization: "1337",
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
server: chronograf.Server{
ID: 1229,
Name: "howdy",
Organization: "1337",
},
name: "doody",
},
want: chronograf.Server{
Name: "doody",
Organization: "1337",
},
addFirst: true,
},
}
for _, tt := range tests {
if tt.args.name != "" {
tt.args.server.Name = tt.args.name
}
s := organizations.NewServersStore(tt.fields.ServersStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
err := s.Update(tt.args.ctx, tt.args.server)
if (err != nil) != tt.wantErr {
t.Errorf("%q. ServersStore.Update() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
got, err := s.Get(tt.args.ctx, tt.args.server.ID)
if diff := cmp.Diff(got, tt.want, serverCmpOptions...); diff != "" {
t.Errorf("%q. ServersStore.Update():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}

112
organizations/sources.go Normal file
View File

@ -0,0 +1,112 @@
package organizations
import (
"context"
"github.com/influxdata/chronograf"
)
// ensure that SourcesStore implements chronograf.SourceStore
var _ chronograf.SourcesStore = &SourcesStore{}
// SourcesStore facade on a SourceStore that filters sources
// by organization.
type SourcesStore struct {
store chronograf.SourcesStore
organization string
}
// NewSourcesStore creates a new SourcesStore from an existing
// chronograf.SourceStore and an organization string
func NewSourcesStore(s chronograf.SourcesStore, org string) *SourcesStore {
return &SourcesStore{
store: s,
organization: org,
}
}
// All retrieves all sources from the underlying SourceStore and filters them
// by organization.
func (s *SourcesStore) All(ctx context.Context) ([]chronograf.Source, error) {
err := validOrganization(ctx)
if err != nil {
return nil, err
}
ds, err := s.store.All(ctx)
if err != nil {
return nil, err
}
// This filters sources without allocating
// https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
sources := ds[:0]
for _, d := range ds {
if d.Organization == s.organization {
sources = append(sources, d)
}
}
return sources, nil
}
// Add creates a new Source in the SourcesStore with source.Organization set to be the
// organization from the source store.
func (s *SourcesStore) Add(ctx context.Context, d chronograf.Source) (chronograf.Source, error) {
err := validOrganization(ctx)
if err != nil {
return chronograf.Source{}, err
}
d.Organization = s.organization
return s.store.Add(ctx, d)
}
// Delete the source from SourcesStore
func (s *SourcesStore) Delete(ctx context.Context, d chronograf.Source) error {
err := validOrganization(ctx)
if err != nil {
return err
}
d, err = s.store.Get(ctx, d.ID)
if err != nil {
return err
}
return s.store.Delete(ctx, d)
}
// Get returns a Source if the id exists and belongs to the organization that is set.
func (s *SourcesStore) Get(ctx context.Context, id int) (chronograf.Source, error) {
err := validOrganization(ctx)
if err != nil {
return chronograf.Source{}, err
}
d, err := s.store.Get(ctx, id)
if err != nil {
return chronograf.Source{}, err
}
if d.Organization != s.organization {
return chronograf.Source{}, chronograf.ErrSourceNotFound
}
return d, nil
}
// Update the source in SourcesStore.
func (s *SourcesStore) Update(ctx context.Context, d chronograf.Source) error {
err := validOrganization(ctx)
if err != nil {
return err
}
_, err = s.store.Get(ctx, d.ID)
if err != nil {
return err
}
return s.store.Update(ctx, d)
}

View File

@ -0,0 +1,340 @@
package organizations_test
import (
"context"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/mocks"
"github.com/influxdata/chronograf/organizations"
)
// IgnoreFields is used because ID cannot be predicted reliably
// EquateEmpty is used because we want nil slices, arrays, and maps to be equal to the empty map
var sourceCmpOptions = cmp.Options{
cmpopts.EquateEmpty(),
cmpopts.IgnoreFields(chronograf.Source{}, "ID"),
cmpopts.IgnoreFields(chronograf.Source{}, "Default"),
}
func TestSources_All(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
}
type args struct {
organization string
ctx context.Context
}
tests := []struct {
name string
args args
fields fields
want []chronograf.Source
wantRaw []chronograf.Source
wantErr bool
}{
{
name: "No Sources",
fields: fields{
SourcesStore: &mocks.SourcesStore{
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
return nil, fmt.Errorf("No Sources")
},
},
},
wantErr: true,
},
{
name: "All Sources",
fields: fields{
SourcesStore: &mocks.SourcesStore{
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
return []chronograf.Source{
{
Name: "howdy",
Organization: "1337",
},
{
Name: "doody",
Organization: "1338",
},
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
},
want: []chronograf.Source{
{
Name: "howdy",
Organization: "1337",
},
},
},
}
for _, tt := range tests {
s := organizations.NewSourcesStore(tt.fields.SourcesStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
gots, err := s.All(tt.args.ctx)
if (err != nil) != tt.wantErr {
t.Errorf("%q. SourcesStore.All() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
for i, got := range gots {
if diff := cmp.Diff(got, tt.want[i], sourceCmpOptions...); diff != "" {
t.Errorf("%q. SourcesStore.All():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
}
func TestSources_Add(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
}
type args struct {
organization string
ctx context.Context
source chronograf.Source
}
tests := []struct {
name string
args args
fields fields
want chronograf.Source
wantErr bool
}{
{
name: "Add Source",
fields: fields{
SourcesStore: &mocks.SourcesStore{
AddF: func(ctx context.Context, s chronograf.Source) (chronograf.Source, error) {
return s, nil
},
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1229,
Name: "howdy",
Organization: "1337",
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
source: chronograf.Source{
ID: 1229,
Name: "howdy",
},
},
want: chronograf.Source{
Name: "howdy",
Organization: "1337",
},
},
}
for _, tt := range tests {
s := organizations.NewSourcesStore(tt.fields.SourcesStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
d, err := s.Add(tt.args.ctx, tt.args.source)
if (err != nil) != tt.wantErr {
t.Errorf("%q. SourcesStore.Add() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
got, err := s.Get(tt.args.ctx, d.ID)
if diff := cmp.Diff(got, tt.want, sourceCmpOptions...); diff != "" {
t.Errorf("%q. SourcesStore.Add():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
func TestSources_Delete(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
}
type args struct {
organization string
ctx context.Context
source chronograf.Source
}
tests := []struct {
name string
fields fields
args args
want []chronograf.Source
addFirst bool
wantErr bool
}{
{
name: "Delete source",
fields: fields{
SourcesStore: &mocks.SourcesStore{
DeleteF: func(ctx context.Context, s chronograf.Source) error {
return nil
},
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1229,
Name: "howdy",
Organization: "1337",
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
source: chronograf.Source{
ID: 1229,
Name: "howdy",
Organization: "1337",
},
},
addFirst: true,
},
}
for _, tt := range tests {
s := organizations.NewSourcesStore(tt.fields.SourcesStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
err := s.Delete(tt.args.ctx, tt.args.source)
if (err != nil) != tt.wantErr {
t.Errorf("%q. SourcesStore.All() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
}
}
func TestSources_Get(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
}
type args struct {
organization string
ctx context.Context
source chronograf.Source
}
tests := []struct {
name string
fields fields
args args
want chronograf.Source
addFirst bool
wantErr bool
}{
{
name: "Get Source",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1229,
Name: "howdy",
Organization: "1337",
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
source: chronograf.Source{
ID: 1229,
Name: "howdy",
Organization: "1337",
},
},
want: chronograf.Source{
ID: 1229,
Name: "howdy",
Organization: "1337",
},
},
}
for _, tt := range tests {
s := organizations.NewSourcesStore(tt.fields.SourcesStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
got, err := s.Get(tt.args.ctx, tt.args.source.ID)
if (err != nil) != tt.wantErr {
t.Errorf("%q. SourcesStore.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if diff := cmp.Diff(got, tt.want, sourceCmpOptions...); diff != "" {
t.Errorf("%q. SourcesStore.Get():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
func TestSources_Update(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
}
type args struct {
organization string
ctx context.Context
source chronograf.Source
name string
}
tests := []struct {
name string
fields fields
args args
want chronograf.Source
addFirst bool
wantErr bool
}{
{
name: "Update Source Name",
fields: fields{
SourcesStore: &mocks.SourcesStore{
UpdateF: func(ctx context.Context, s chronograf.Source) error {
return nil
},
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1229,
Name: "doody",
Organization: "1337",
}, nil
},
},
},
args: args{
organization: "1337",
ctx: context.Background(),
source: chronograf.Source{
ID: 1229,
Name: "howdy",
Organization: "1337",
},
name: "doody",
},
want: chronograf.Source{
Name: "doody",
Organization: "1337",
},
addFirst: true,
},
}
for _, tt := range tests {
if tt.args.name != "" {
tt.args.source.Name = tt.args.name
}
s := organizations.NewSourcesStore(tt.fields.SourcesStore, tt.args.organization)
tt.args.ctx = context.WithValue(tt.args.ctx, organizations.ContextKey, tt.args.organization)
err := s.Update(tt.args.ctx, tt.args.source)
if (err != nil) != tt.wantErr {
t.Errorf("%q. SourcesStore.Update() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
got, err := s.Get(tt.args.ctx, tt.args.source.ID)
if diff := cmp.Diff(got, tt.want, sourceCmpOptions...); diff != "" {
t.Errorf("%q. SourcesStore.Update():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}

270
organizations/users.go Normal file
View File

@ -0,0 +1,270 @@
package organizations
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
// Ensure UsersStore implements chronograf.UsersStore.
var _ chronograf.UsersStore = &UsersStore{}
// UsersStore facade on a UserStore that filters a users roles
// by organization.
//
// The high level idea here is to use the same underlying store for all users.
// In particular, this is done by having all the users Roles field be a set of
// all of the users roles in all organizations. Each CRUD method here takes care
// to ensure that the only roles that are modified are the roles for the organization
// that was provided on the UsersStore.
type UsersStore struct {
organization string
store chronograf.UsersStore
}
// NewUsersStore creates a new UsersStore from an existing
// chronograf.UserStore and an organization string
func NewUsersStore(s chronograf.UsersStore, org string) *UsersStore {
return &UsersStore{
store: s,
organization: org,
}
}
// validOrganizationRoles ensures that each User Role has both an associated Organization and a Name
func validOrganizationRoles(orgID string, u *chronograf.User) error {
if u == nil || u.Roles == nil {
return nil
}
for _, r := range u.Roles {
if r.Organization == "" {
return fmt.Errorf("user role must have an Organization")
}
if r.Organization != orgID {
return fmt.Errorf("organizationID %s does not match %s", r.Organization, orgID)
}
if r.Name == "" {
return fmt.Errorf("user role must have a Name")
}
}
return nil
}
// Get searches the UsersStore for using the query.
// The roles returned on the user are filtered to only contain roles that are for the organization
// specified on the organization store.
func (s *UsersStore) Get(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
err := validOrganization(ctx)
if err != nil {
return nil, err
}
usr, err := s.store.Get(ctx, q)
if err != nil {
return nil, err
}
// This filters a users roles so that the resulting struct only contains roles
// from the organization on the UsersStore.
roles := usr.Roles[:0]
for _, r := range usr.Roles {
if r.Organization == s.organization {
roles = append(roles, r)
}
}
if len(roles) == 0 {
// This means that the user does not belong to the organization
// and therefore, is not found.
return nil, chronograf.ErrUserNotFound
}
usr.Roles = roles
return usr, nil
}
// Add creates a new User in the UsersStore. It validates that the user provided only
// has roles for the organization set on the UsersStore.
// If a user is not found in the underlying, it calls the underlying UsersStore Add method.
// If a user is found, it removes any existing roles a user has for an organization and appends
// the roles specified on the provided user and calls the uderlying UsersStore Update method.
func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
err := validOrganization(ctx)
if err != nil {
return nil, err
}
// Validates that the users roles are only for the current organization.
if err := validOrganizationRoles(s.organization, u); err != nil {
return nil, err
}
// retrieve the user from the underlying store
usr, err := s.store.Get(ctx, chronograf.UserQuery{
Name: &u.Name,
Provider: &u.Provider,
Scheme: &u.Scheme,
})
switch err {
case nil:
// If there is no error continue to the rest of the code
break
case chronograf.ErrUserNotFound:
// If user is not found in the backed store, attempt to add the user
return s.store.Add(ctx, u)
default:
// return the error
return nil, err
}
// Filter the retrieved users roles so that the resulting struct only contains roles
// that are not from the organization on the UsersStore.
roles := usr.Roles[:0]
for _, r := range usr.Roles {
if r.Organization != s.organization {
roles = append(roles, r)
}
}
// If the user already has a role in the organization then the user
// cannot be "created".
// This can be thought of as:
// (total # of roles a user has) - (# of roles not in the organization) = (# of roles in organization)
// if this value is greater than 1 the user cannot be "added".
numRolesInOrganization := len(usr.Roles) - len(roles)
if numRolesInOrganization > 0 {
return nil, chronograf.ErrUserAlreadyExists
}
// Set the users roles to be the union of the roles set on the provided user
// and the user that was found in the underlying store
usr.Roles = append(roles, u.Roles...)
// Update the user in the underlying store
if err := s.store.Update(ctx, usr); err != nil {
return nil, err
}
// Return the provided user with ID set
u.ID = usr.ID
return u, nil
}
// Delete a user from the UsersStore. This is done by stripping a user of
// any roles it has in the organization speicified on the UsersStore.
func (s *UsersStore) Delete(ctx context.Context, usr *chronograf.User) error {
err := validOrganization(ctx)
if err != nil {
return err
}
// retrieve the user from the underlying store
u, err := s.store.Get(ctx, chronograf.UserQuery{ID: &usr.ID})
if err != nil {
return err
}
// Filter the retrieved users roles so that the resulting slice contains
// roles that are not scoped to the organization provided
roles := u.Roles[:0]
for _, r := range u.Roles {
if r.Organization != s.organization {
roles = append(roles, r)
}
}
u.Roles = roles
return s.store.Update(ctx, u)
}
// Update a user in the UsersStore.
func (s *UsersStore) Update(ctx context.Context, usr *chronograf.User) error {
err := validOrganization(ctx)
if err != nil {
return err
}
// Validates that the users roles are only for the current organization.
if err := validOrganizationRoles(s.organization, usr); err != nil {
return err
}
// retrieve the user from the underlying store
u, err := s.store.Get(ctx, chronograf.UserQuery{ID: &usr.ID})
if err != nil {
return err
}
// Filter the retrieved users roles so that the resulting slice contains
// roles that are not scoped to the organization provided
roles := u.Roles[:0]
for _, r := range u.Roles {
if r.Organization != s.organization {
roles = append(roles, r)
}
}
// Make a copy of the usr so that we dont modify the underlying add roles on to
// the user that was passed in
user := *usr
// Set the users roles to be the union of the roles set on the provided user
// and the user that was found in the underlying store
user.Roles = append(roles, usr.Roles...)
return s.store.Update(ctx, &user)
}
// All returns all users where roles have been filters to be exclusively for
// the organization provided on the UsersStore.
func (s *UsersStore) All(ctx context.Context) ([]chronograf.User, error) {
err := validOrganization(ctx)
if err != nil {
return nil, err
}
// retrieve all users from the underlying UsersStore
usrs, err := s.store.All(ctx)
if err != nil {
return nil, err
}
// Filter users to only contain users that have at least one role
// in the provided organization.
us := usrs[:0]
for _, usr := range usrs {
roles := usr.Roles[:0]
// This filters a users roles so that the resulting struct only contains roles
// from the organization on the UsersStore.
for _, r := range usr.Roles {
if r.Organization == s.organization {
roles = append(roles, r)
}
}
if len(roles) != 0 {
// Only add users if they have a role in the associated organization
usr.Roles = roles
us = append(us, usr)
}
}
return us, nil
}
// Num returns the number of users in the UsersStore
// This is unperformant, but should rarely be used.
func (s *UsersStore) Num(ctx context.Context) (int, error) {
err := validOrganization(ctx)
if err != nil {
return 0, err
}
// retrieve all users from the underlying UsersStore
usrs, err := s.All(ctx)
if err != nil {
return 0, err
}
return len(usrs), nil
}

1049
organizations/users_test.go Normal file

File diff suppressed because it is too large Load Diff

63
roles/roles.go Normal file
View File

@ -0,0 +1,63 @@
package roles
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
type contextKey string
// ContextKey is the key used to specify the
// role via context
const ContextKey = contextKey("role")
func validRole(ctx context.Context) error {
// prevents panic in case of nil context
if ctx == nil {
return fmt.Errorf("expect non nil context")
}
role, ok := ctx.Value(ContextKey).(string)
// should never happen
if !ok {
return fmt.Errorf("expected role key to be a string")
}
switch role {
case MemberRoleName, ViewerRoleName, EditorRoleName, AdminRoleName:
return nil
default:
return fmt.Errorf("expected role key to be set")
}
}
// Chronograf User Roles
const (
MemberRoleName = "member"
ViewerRoleName = "viewer"
EditorRoleName = "editor"
AdminRoleName = "admin"
SuperAdminStatus = "superadmin"
)
var (
// MemberRole is the role for a user who can only perform No operations.
MemberRole = chronograf.Role{
Name: MemberRoleName,
}
// ViewerRole is the role for a user who can only perform READ operations on Dashboards, Rules, Sources, and Servers,
ViewerRole = chronograf.Role{
Name: ViewerRoleName,
}
// EditorRole is the role for a user who can perform READ and WRITE operations on Dashboards, Rules, Sources, and Servers.
EditorRole = chronograf.Role{
Name: EditorRoleName,
}
// AdminRole is the role for a user who can perform READ and WRITE operations on Dashboards, Rules, Sources, Servers, and Users
AdminRole = chronograf.Role{
Name: AdminRoleName,
}
)

143
roles/sources.go Normal file
View File

@ -0,0 +1,143 @@
package roles
import (
"context"
"github.com/influxdata/chronograf"
)
// NOTE:
// This code is currently unused. however, it has been left in place because we aniticipate
// that it may be used in the future. It was originally developed as a misunderstanding of
// https://github.com/influxdata/chronograf/issues/1915
// ensure that SourcesStore implements chronograf.SourceStore
var _ chronograf.SourcesStore = &SourcesStore{}
// SourcesStore facade on a SourceStore that filters sources
// by minimum role required to access the source.
//
// The role is passed around on the context and set when the
// SourcesStore is instantiated.
type SourcesStore struct {
store chronograf.SourcesStore
role string
}
// NewSourcesStore creates a new SourcesStore from an existing
// chronograf.SourceStore and an role string
func NewSourcesStore(s chronograf.SourcesStore, role string) *SourcesStore {
return &SourcesStore{
store: s,
role: role,
}
}
// All retrieves all sources from the underlying SourceStore and filters them
// by role.
func (s *SourcesStore) All(ctx context.Context) ([]chronograf.Source, error) {
err := validRole(ctx)
if err != nil {
return nil, err
}
ds, err := s.store.All(ctx)
if err != nil {
return nil, err
}
// This filters sources without allocating
// https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
sources := ds[:0]
for _, d := range ds {
if hasAuthorizedRole(d.Role, s.role) {
sources = append(sources, d)
}
}
return sources, nil
}
// Add creates a new Source in the SourcesStore with source.Role set to be the
// role from the source store.
func (s *SourcesStore) Add(ctx context.Context, d chronograf.Source) (chronograf.Source, error) {
err := validRole(ctx)
if err != nil {
return chronograf.Source{}, err
}
return s.store.Add(ctx, d)
}
// Delete the source from SourcesStore
func (s *SourcesStore) Delete(ctx context.Context, d chronograf.Source) error {
err := validRole(ctx)
if err != nil {
return err
}
d, err = s.store.Get(ctx, d.ID)
if err != nil {
return err
}
return s.store.Delete(ctx, d)
}
// Get returns a Source if the id exists and belongs to the role that is set.
func (s *SourcesStore) Get(ctx context.Context, id int) (chronograf.Source, error) {
err := validRole(ctx)
if err != nil {
return chronograf.Source{}, err
}
d, err := s.store.Get(ctx, id)
if err != nil {
return chronograf.Source{}, err
}
if !hasAuthorizedRole(d.Role, s.role) {
return chronograf.Source{}, chronograf.ErrSourceNotFound
}
return d, nil
}
// Update the source in SourcesStore.
func (s *SourcesStore) Update(ctx context.Context, d chronograf.Source) error {
err := validRole(ctx)
if err != nil {
return err
}
_, err = s.store.Get(ctx, d.ID)
if err != nil {
return err
}
return s.store.Update(ctx, d)
}
// hasAuthorizedRole checks that the role provided has at least
// the minimum role required.
func hasAuthorizedRole(sourceRole, providedRole string) bool {
switch sourceRole {
case ViewerRoleName:
switch providedRole {
case ViewerRoleName, EditorRoleName, AdminRoleName:
return true
}
case EditorRoleName:
switch providedRole {
case EditorRoleName, AdminRoleName:
return true
}
case AdminRoleName:
switch providedRole {
case AdminRoleName:
return true
}
}
return false
}

489
roles/sources_test.go Normal file
View File

@ -0,0 +1,489 @@
package roles
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/mocks"
)
func TestSources_Get(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
}
type args struct {
role string
id int
}
type wants struct {
source chronograf.Source
err bool
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "Get viewer source as viewer",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
}, nil
},
},
},
args: args{
role: "viewer",
id: 1,
},
wants: wants{
source: chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
},
},
{
name: "Get viewer source as editor",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
}, nil
},
},
},
args: args{
role: "editor",
id: 1,
},
wants: wants{
source: chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
},
},
{
name: "Get viewer source as admin",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
}, nil
},
},
},
args: args{
role: "admin",
id: 1,
},
wants: wants{
source: chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
},
},
{
name: "Get editor source as editor",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "editor",
}, nil
},
},
},
args: args{
role: "editor",
id: 1,
},
wants: wants{
source: chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "editor",
},
},
},
{
name: "Get editor source as admin",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "editor",
}, nil
},
},
},
args: args{
role: "admin",
id: 1,
},
wants: wants{
source: chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "editor",
},
},
},
{
name: "Get editor source as viewer - want error",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "editor",
}, nil
},
},
},
args: args{
role: "viewer",
id: 1,
},
wants: wants{
err: true,
},
},
{
name: "Get admin source as admin",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "admin",
}, nil
},
},
},
args: args{
role: "admin",
id: 1,
},
wants: wants{
source: chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "admin",
},
},
},
{
name: "Get admin source as viewer - want error",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "admin",
}, nil
},
},
},
args: args{
role: "viewer",
id: 1,
},
wants: wants{
err: true,
},
},
{
name: "Get admin source as editor - want error",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "admin",
}, nil
},
},
},
args: args{
role: "editor",
id: 1,
},
wants: wants{
err: true,
},
},
{
name: "Get source bad context",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "admin",
}, nil
},
},
},
args: args{
role: "random role",
id: 1,
},
wants: wants{
err: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := NewSourcesStore(tt.fields.SourcesStore, tt.args.role)
ctx := context.Background()
if tt.args.role != "" {
ctx = context.WithValue(ctx, ContextKey, tt.args.role)
}
source, err := store.Get(ctx, tt.args.id)
if (err != nil) != tt.wants.err {
t.Errorf("%q. Store.Sources().Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
return
}
if diff := cmp.Diff(source, tt.wants.source); diff != "" {
t.Errorf("%q. Store.Sources().Get():\n-got/+want\ndiff %s", tt.name, diff)
}
})
}
}
func TestSources_All(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
}
type args struct {
role string
}
type wants struct {
sources []chronograf.Source
err bool
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "Get viewer sources as viewer",
fields: fields{
SourcesStore: &mocks.SourcesStore{
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
return []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
{
ID: 2,
Name: "my sweet name",
Organization: "0",
Role: "editor",
},
{
ID: 3,
Name: "my sweet name",
Organization: "0",
Role: "admin",
},
}, nil
},
},
},
args: args{
role: "viewer",
},
wants: wants{
sources: []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
},
},
},
{
name: "Get editor sources as editor",
fields: fields{
SourcesStore: &mocks.SourcesStore{
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
return []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
{
ID: 2,
Name: "my sweet name",
Organization: "0",
Role: "editor",
},
{
ID: 3,
Name: "my sweet name",
Organization: "0",
Role: "admin",
},
}, nil
},
},
},
args: args{
role: "editor",
},
wants: wants{
sources: []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
{
ID: 2,
Name: "my sweet name",
Organization: "0",
Role: "editor",
},
},
},
},
{
name: "Get admin sources as admin",
fields: fields{
SourcesStore: &mocks.SourcesStore{
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
return []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
{
ID: 2,
Name: "my sweet name",
Organization: "0",
Role: "editor",
},
{
ID: 3,
Name: "my sweet name",
Organization: "0",
Role: "admin",
},
}, nil
},
},
},
args: args{
role: "admin",
},
wants: wants{
sources: []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
{
ID: 2,
Name: "my sweet name",
Organization: "0",
Role: "editor",
},
{
ID: 3,
Name: "my sweet name",
Organization: "0",
Role: "admin",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := NewSourcesStore(tt.fields.SourcesStore, tt.args.role)
ctx := context.Background()
if tt.args.role != "" {
ctx = context.WithValue(ctx, ContextKey, tt.args.role)
}
sources, err := store.All(ctx)
if (err != nil) != tt.wants.err {
t.Errorf("%q. Store.Sources().Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
return
}
if diff := cmp.Diff(sources, tt.wants.sources); diff != "" {
t.Errorf("%q. Store.Sources().Get():\n-got/+want\ndiff %s", tt.name, diff)
}
})
}
}

View File

@ -2,10 +2,13 @@ package server
import (
"context"
"fmt"
"net/http"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/oauth2"
"github.com/influxdata/chronograf/organizations"
"github.com/influxdata/chronograf/roles"
)
// AuthorizedToken extracts the token and validates; if valid the next handler
@ -15,7 +18,7 @@ import (
func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := logger.
WithField("component", "auth").
WithField("component", "token_auth").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL)
@ -45,3 +48,171 @@ func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next h
return
})
}
// AuthorizedUser extracts the user name and provider from context. If the
// user and provider can be found on the context, we look up the user by their
// name and provider. If the user is found, we verify that the user has at at
// least the role supplied.
func AuthorizedUser(
store DataStore,
useAuth bool,
role string,
logger chronograf.Logger,
next http.HandlerFunc,
) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !useAuth {
ctx := r.Context()
// If there is no auth, then give the user raw access to the DataStore
r = r.WithContext(serverContext(ctx))
next(w, r)
return
}
log := logger.
WithField("component", "role_auth").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL)
ctx := r.Context()
serverCtx := serverContext(ctx)
p, err := getValidPrincipal(ctx)
if err != nil {
log.Error("Failed to retrieve principal from context")
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
scheme, err := getScheme(ctx)
if err != nil {
log.Error("Failed to retrieve scheme from context")
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
// This is as if the user was logged into the default organization
if p.Organization == "" {
defaultOrg, err := store.Organizations(serverCtx).DefaultOrganization(serverCtx)
if err != nil {
log.Error(fmt.Sprintf("Failed to retrieve the default organization: %v", err))
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
p.Organization = fmt.Sprintf("%d", defaultOrg.ID)
}
// validate that the organization exists
orgID, err := parseOrganizationID(p.Organization)
if err != nil {
log.Error("Failed to validate organization on context")
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
_, err = store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &orgID})
if err != nil {
log.Error(fmt.Sprintf("Failed to retrieve organization %d from organizations store", orgID))
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
ctx = context.WithValue(ctx, organizations.ContextKey, p.Organization)
// TODO: seems silly to look up a user twice
u, err := store.Users(serverCtx).Get(serverCtx, chronograf.UserQuery{
Name: &p.Subject,
Provider: &p.Issuer,
Scheme: &scheme,
})
if err != nil {
log.Error("Failed to retrieve user")
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
// In particular this is used by sever/users.go so that we know when and when not to
// allow users to make someone a super admin
ctx = context.WithValue(ctx, UserContextKey, u)
if u.SuperAdmin {
// To access resources (servers, sources, databases, layouts) within a DataStore,
// an organization and a role are required even if you are a super admin or are
// not using auth. Every user's current organization is set on context to filter
// the resources accessed within a DataStore, including for super admin or when
// not using auth. In this way, a DataStore can treat all requests the same,
// including those from a super admin and when not using auth.
//
// As for roles, in the case of super admin or when not using auth, the user's
// role on context (though not on their JWT or user) is set to be admin. In order
// to access all resources belonging to their current organization.
ctx = context.WithValue(ctx, roles.ContextKey, roles.AdminRoleName)
r = r.WithContext(ctx)
next(w, r)
return
}
u, err = store.Users(ctx).Get(ctx, chronograf.UserQuery{
Name: &p.Subject,
Provider: &p.Issuer,
Scheme: &scheme,
})
if err != nil {
log.Error("Failed to retrieve user")
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
if hasAuthorizedRole(u, role) {
if len(u.Roles) != 1 {
msg := `User %d has too many role in organization. User: %#v.Please report this log at https://github.com/influxdata/chronograf/issues/new"`
log.Error(fmt.Sprint(msg, u.ID, u))
unknownErrorWithMessage(w, fmt.Errorf("please have administrator check logs and report error"), logger)
return
}
// use the first role, since there should only ever be one
// for any particular organization and hasAuthorizedRole
// should ensure that at least one role for the org exists
ctx = context.WithValue(ctx, roles.ContextKey, u.Roles[0].Name)
r = r.WithContext(ctx)
next(w, r)
return
}
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
})
}
func hasAuthorizedRole(u *chronograf.User, role string) bool {
if u == nil {
return false
}
switch role {
case roles.ViewerRoleName:
for _, r := range u.Roles {
switch r.Name {
case roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName:
return true
}
}
case roles.EditorRoleName:
for _, r := range u.Roles {
switch r.Name {
case roles.EditorRoleName, roles.AdminRoleName:
return true
}
}
case roles.AdminRoleName:
for _, r := range u.Roles {
switch r.Name {
case roles.AdminRoleName:
return true
}
}
case roles.SuperAdminStatus:
// SuperAdmins should have been authorized before this.
// This is only meant to restrict access for non-superadmins.
return false
}
return false
}

File diff suppressed because it is too large Load Diff

View File

@ -9,30 +9,30 @@ import (
// LayoutBuilder is responsible for building Layouts
type LayoutBuilder interface {
Build(chronograf.LayoutStore) (*layouts.MultiLayoutStore, error)
Build(chronograf.LayoutsStore) (*layouts.MultiLayoutsStore, error)
}
// MultiLayoutBuilder implements LayoutBuilder and will return a MultiLayoutStore
// MultiLayoutBuilder implements LayoutBuilder and will return a MultiLayoutsStore
type MultiLayoutBuilder struct {
Logger chronograf.Logger
UUID chronograf.ID
CannedPath string
}
// Build will construct a MultiLayoutStore of canned and db-backed personalized
// Build will construct a MultiLayoutsStore of canned and db-backed personalized
// layouts
func (builder *MultiLayoutBuilder) Build(db chronograf.LayoutStore) (*layouts.MultiLayoutStore, error) {
func (builder *MultiLayoutBuilder) Build(db chronograf.LayoutsStore) (*layouts.MultiLayoutsStore, error) {
// These apps are those handled from a directory
apps := canned.NewApps(builder.CannedPath, builder.UUID, builder.Logger)
// These apps are statically compiled into chronograf
binApps := &canned.BinLayoutStore{
binApps := &canned.BinLayoutsStore{
Logger: builder.Logger,
}
// Acts as a front-end to both the bolt layouts, filesystem layouts and binary statically compiled layouts.
// The idea here is that these stores form a hierarchy in which each is tried sequentially until
// the operation has success. So, the database is preferred over filesystem over binary data.
layouts := &layouts.MultiLayoutStore{
Stores: []chronograf.LayoutStore{
layouts := &layouts.MultiLayoutsStore{
Stores: []chronograf.LayoutsStore{
db,
apps,
binApps,

View File

@ -10,7 +10,7 @@ func TestLayoutBuilder(t *testing.T) {
var l server.LayoutBuilder = &server.MultiLayoutBuilder{}
layout, err := l.Build(nil)
if err != nil {
t.Fatalf("MultiLayoutBuilder can't build a MultiLayoutStore: %v", err)
t.Fatalf("MultiLayoutBuilder can't build a MultiLayoutsStore: %v", err)
}
if layout == nil {

View File

@ -174,7 +174,7 @@ func (s *Service) DashboardCells(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
e, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
e, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
@ -194,7 +194,7 @@ func (s *Service) NewDashboardCell(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
dash, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
@ -220,7 +220,7 @@ func (s *Service) NewDashboardCell(w http.ResponseWriter, r *http.Request) {
cell.ID = cid
dash.Cells = append(dash.Cells, cell)
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
if err := s.Store.Dashboards(ctx).Update(ctx, dash); err != nil {
msg := fmt.Sprintf("Error adding cell %s to dashboard %d: %v", cid, id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
@ -244,7 +244,7 @@ func (s *Service) DashboardCellID(w http.ResponseWriter, r *http.Request) {
return
}
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
dash, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
@ -270,7 +270,7 @@ func (s *Service) RemoveDashboardCell(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
dash, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
@ -290,7 +290,7 @@ func (s *Service) RemoveDashboardCell(w http.ResponseWriter, r *http.Request) {
}
dash.Cells = append(dash.Cells[:cellid], dash.Cells[cellid+1:]...)
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
if err := s.Store.Dashboards(ctx).Update(ctx, dash); err != nil {
msg := fmt.Sprintf("Error removing cell %s from dashboard %d: %v", cid, id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
@ -307,7 +307,7 @@ func (s *Service) ReplaceDashboardCell(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
dash, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
@ -339,7 +339,7 @@ func (s *Service) ReplaceDashboardCell(w http.ResponseWriter, r *http.Request) {
cell.ID = cid
dash.Cells[cellid] = cell
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
if err := s.Store.Dashboards(ctx).Update(ctx, dash); err != nil {
msg := fmt.Sprintf("Error updating cell %s in dashboard %d: %v", cid, id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return

View File

@ -227,14 +227,16 @@ func Test_Service_DashboardCells(t *testing.T) {
// setup mock DashboardCells store and logger
tlog := &mocks.TestLogger{}
svc := &server.Service{
DashboardsStore: &mocks.DashboardsStore{
GetF: func(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) {
return chronograf.Dashboard{
ID: chronograf.DashboardID(1),
Cells: test.mockResponse,
Templates: []chronograf.Template{},
Name: "empty dashboard",
}, nil
Store: &mocks.Store{
DashboardsStore: &mocks.DashboardsStore{
GetF: func(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) {
return chronograf.Dashboard{
ID: chronograf.DashboardID(1),
Cells: test.mockResponse,
Templates: []chronograf.Template{},
Name: "empty dashboard",
}, nil
},
},
},
Logger: tlog,

30
server/context.go Normal file
View File

@ -0,0 +1,30 @@
package server
import (
"context"
)
type serverContextKey string
// ServerContextKey is the key used to specify that the
// server is making the requet via context
const ServerContextKey = serverContextKey("server")
// hasServerContext speficies if the context contains
// the ServerContextKey and that the value stored there is true
func hasServerContext(ctx context.Context) bool {
// prevents panic in case of nil context
if ctx == nil {
return false
}
sa, ok := ctx.Value(ServerContextKey).(bool)
// should never happen
if !ok {
return false
}
return sa
}
func serverContext(ctx context.Context) context.Context {
return context.WithValue(ctx, ServerContextKey, true)
}

View File

@ -15,11 +15,12 @@ type dashboardLinks struct {
}
type dashboardResponse struct {
ID chronograf.DashboardID `json:"id"`
Cells []dashboardCellResponse `json:"cells"`
Templates []templateResponse `json:"templates"`
Name string `json:"name"`
Links dashboardLinks `json:"links"`
ID chronograf.DashboardID `json:"id"`
Cells []dashboardCellResponse `json:"cells"`
Templates []templateResponse `json:"templates"`
Name string `json:"name"`
Organization string `json:"organization"`
Links dashboardLinks `json:"links"`
}
type getDashboardsResponse struct {
@ -33,10 +34,11 @@ func newDashboardResponse(d chronograf.Dashboard) *dashboardResponse {
templates := newTemplateResponses(dd.ID, dd.Templates)
return &dashboardResponse{
ID: dd.ID,
Name: dd.Name,
Cells: cells,
Templates: templates,
ID: dd.ID,
Name: dd.Name,
Cells: cells,
Templates: templates,
Organization: d.Organization,
Links: dashboardLinks{
Self: fmt.Sprintf("%s/%d", base, dd.ID),
Cells: fmt.Sprintf("%s/%d/cells", base, dd.ID),
@ -48,7 +50,7 @@ func newDashboardResponse(d chronograf.Dashboard) *dashboardResponse {
// Dashboards returns all dashboards within the store
func (s *Service) Dashboards(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
dashboards, err := s.DashboardsStore.All(ctx)
dashboards, err := s.Store.Dashboards(ctx).All(ctx)
if err != nil {
Error(w, http.StatusInternalServerError, "Error loading dashboards", s.Logger)
return
@ -73,7 +75,7 @@ func (s *Service) DashboardID(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
e, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
e, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
@ -86,25 +88,32 @@ func (s *Service) DashboardID(w http.ResponseWriter, r *http.Request) {
// NewDashboard creates and returns a new dashboard object
func (s *Service) NewDashboard(w http.ResponseWriter, r *http.Request) {
var dashboard chronograf.Dashboard
var err error
if err := json.NewDecoder(r.Body).Decode(&dashboard); err != nil {
invalidJSON(w, s.Logger)
return
}
if err := ValidDashboardRequest(&dashboard); err != nil {
ctx := r.Context()
defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
if err := ValidDashboardRequest(&dashboard, fmt.Sprintf("%d", defaultOrg.ID)); err != nil {
invalidData(w, err, s.Logger)
return
}
var err error
if dashboard, err = s.DashboardsStore.Add(r.Context(), dashboard); err != nil {
if dashboard, err = s.Store.Dashboards(ctx).Add(r.Context(), dashboard); err != nil {
msg := fmt.Errorf("Error storing dashboard %v: %v", dashboard, err)
unknownErrorWithMessage(w, msg, s.Logger)
return
}
res := newDashboardResponse(dashboard)
w.Header().Add("Location", res.Links.Self)
location(w, res.Links.Self)
encodeJSON(w, http.StatusCreated, res, s.Logger)
}
@ -117,13 +126,13 @@ func (s *Service) RemoveDashboard(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
e, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
e, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
}
if err := s.DashboardsStore.Delete(ctx, e); err != nil {
if err := s.Store.Dashboards(ctx).Delete(ctx, e); err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
@ -140,7 +149,7 @@ func (s *Service) ReplaceDashboard(w http.ResponseWriter, r *http.Request) {
}
id := chronograf.DashboardID(idParam)
_, err = s.DashboardsStore.Get(ctx, id)
_, err = s.Store.Dashboards(ctx).Get(ctx, id)
if err != nil {
Error(w, http.StatusNotFound, fmt.Sprintf("ID %d not found", id), s.Logger)
return
@ -153,12 +162,18 @@ func (s *Service) ReplaceDashboard(w http.ResponseWriter, r *http.Request) {
}
req.ID = id
if err := ValidDashboardRequest(&req); err != nil {
defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
if err := ValidDashboardRequest(&req, fmt.Sprintf("%d", defaultOrg.ID)); err != nil {
invalidData(w, err, s.Logger)
return
}
if err := s.DashboardsStore.Update(ctx, req); err != nil {
if err := s.Store.Dashboards(ctx).Update(ctx, req); err != nil {
msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
@ -179,7 +194,7 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
}
id := chronograf.DashboardID(idParam)
orig, err := s.DashboardsStore.Get(ctx, id)
orig, err := s.Store.Dashboards(ctx).Get(ctx, id)
if err != nil {
Error(w, http.StatusNotFound, fmt.Sprintf("ID %d not found", id), s.Logger)
return
@ -195,7 +210,12 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
if req.Name != "" {
orig.Name = req.Name
} else if len(req.Cells) > 0 {
if err := ValidDashboardRequest(&req); err != nil {
defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
if err := ValidDashboardRequest(&req, fmt.Sprintf("%d", defaultOrg.ID)); err != nil {
invalidData(w, err, s.Logger)
return
}
@ -205,7 +225,7 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
return
}
if err := s.DashboardsStore.Update(ctx, orig); err != nil {
if err := s.Store.Dashboards(ctx).Update(ctx, orig); err != nil {
msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
@ -216,7 +236,10 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
}
// ValidDashboardRequest verifies that the dashboard cells have a query
func ValidDashboardRequest(d *chronograf.Dashboard) error {
func ValidDashboardRequest(d *chronograf.Dashboard, defaultOrgID string) error {
if d.Organization == "" {
d.Organization = defaultOrgID
}
for i, c := range d.Cells {
if err := ValidDashboardCellRequest(&c); err != nil {
return err
@ -238,6 +261,7 @@ func DashboardDefaults(d chronograf.Dashboard) (newDash chronograf.Dashboard) {
newDash.ID = d.ID
newDash.Templates = d.Templates
newDash.Name = d.Name
newDash.Organization = d.Organization
newDash.Cells = make([]chronograf.DashboardCell, len(d.Cells))
for i, c := range d.Cells {

View File

@ -144,6 +144,7 @@ func TestValidDashboardRequest(t *testing.T) {
{
name: "Updates all cell widths/heights",
d: chronograf.Dashboard{
Organization: "1337",
Cells: []chronograf.DashboardCell{
{
W: 0,
@ -166,6 +167,7 @@ func TestValidDashboardRequest(t *testing.T) {
},
},
want: chronograf.Dashboard{
Organization: "1337",
Cells: []chronograf.DashboardCell{
{
W: 4,
@ -190,13 +192,14 @@ func TestValidDashboardRequest(t *testing.T) {
},
}
for _, tt := range tests {
err := ValidDashboardRequest(&tt.d)
// TODO(desa): this Okay?
err := ValidDashboardRequest(&tt.d, "0")
if (err != nil) != tt.wantErr {
t.Errorf("%q. ValidDashboardRequest() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if !reflect.DeepEqual(tt.d, tt.want) {
t.Errorf("%q. ValidDashboardRequest() = %v, want %v", tt.name, tt.d, tt.want)
if diff := cmp.Diff(tt.d, tt.want); diff != "" {
t.Errorf("%q. ValidDashboardRequest(). got/want diff:\n%s", tt.name, diff)
}
}
}
@ -210,6 +213,7 @@ func Test_newDashboardResponse(t *testing.T) {
{
name: "creates a dashboard response",
d: chronograf.Dashboard{
Organization: "0",
Cells: []chronograf.DashboardCell{
{
ID: "a",
@ -252,7 +256,8 @@ func Test_newDashboardResponse(t *testing.T) {
},
},
want: &dashboardResponse{
Templates: []templateResponse{},
Organization: "0",
Templates: []templateResponse{},
Cells: []dashboardCellResponse{
dashboardCellResponse{
Links: dashboardCellLinks{

View File

@ -76,7 +76,7 @@ func (h *Service) GetDatabases(w http.ResponseWriter, r *http.Request) {
return
}
src, err := h.SourcesStore.Get(ctx, srcID)
src, err := h.Store.Sources(ctx).Get(ctx, srcID)
if err != nil {
notFound(w, srcID, h.Logger)
return
@ -122,7 +122,7 @@ func (h *Service) NewDatabase(w http.ResponseWriter, r *http.Request) {
return
}
src, err := h.SourcesStore.Get(ctx, srcID)
src, err := h.Store.Sources(ctx).Get(ctx, srcID)
if err != nil {
notFound(w, srcID, h.Logger)
return
@ -172,7 +172,7 @@ func (h *Service) DropDatabase(w http.ResponseWriter, r *http.Request) {
return
}
src, err := h.SourcesStore.Get(ctx, srcID)
src, err := h.Store.Sources(ctx).Get(ctx, srcID)
if err != nil {
notFound(w, srcID, h.Logger)
return
@ -207,7 +207,7 @@ func (h *Service) RetentionPolicies(w http.ResponseWriter, r *http.Request) {
return
}
src, err := h.SourcesStore.Get(ctx, srcID)
src, err := h.Store.Sources(ctx).Get(ctx, srcID)
if err != nil {
notFound(w, srcID, h.Logger)
return
@ -261,7 +261,7 @@ func (h *Service) NewRetentionPolicy(w http.ResponseWriter, r *http.Request) {
return
}
src, err := h.SourcesStore.Get(ctx, srcID)
src, err := h.Store.Sources(ctx).Get(ctx, srcID)
if err != nil {
notFound(w, srcID, h.Logger)
return
@ -311,7 +311,7 @@ func (h *Service) UpdateRetentionPolicy(w http.ResponseWriter, r *http.Request)
return
}
src, err := h.SourcesStore.Get(ctx, srcID)
src, err := h.Store.Sources(ctx).Get(ctx, srcID)
if err != nil {
notFound(w, srcID, h.Logger)
return
@ -355,25 +355,25 @@ func (h *Service) UpdateRetentionPolicy(w http.ResponseWriter, r *http.Request)
}
// DropRetentionPolicy removes a retention policy from a database
func (h *Service) DropRetentionPolicy(w http.ResponseWriter, r *http.Request) {
func (s *Service) DropRetentionPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
src, err := h.SourcesStore.Get(ctx, srcID)
src, err := s.Store.Sources(ctx).Get(ctx, srcID)
if err != nil {
notFound(w, srcID, h.Logger)
notFound(w, srcID, s.Logger)
return
}
db := h.Databases
db := s.Databases
if err = db.Connect(ctx, &src); err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
Error(w, http.StatusBadRequest, msg, h.Logger)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
@ -381,7 +381,7 @@ func (h *Service) DropRetentionPolicy(w http.ResponseWriter, r *http.Request) {
rpID := httprouter.GetParamFromContext(ctx, "rpid")
dropErr := db.DropRP(ctx, dbID, rpID)
if dropErr != nil {
Error(w, http.StatusBadRequest, dropErr.Error(), h.Logger)
Error(w, http.StatusBadRequest, dropErr.Error(), s.Logger)
return
}

View File

@ -11,7 +11,7 @@ func TestService_GetDatabases(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
ServersStore chronograf.ServersStore
LayoutStore chronograf.LayoutStore
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
TimeSeriesClient TimeSeriesClient
@ -33,11 +33,13 @@ func TestService_GetDatabases(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &Service{
SourcesStore: tt.fields.SourcesStore,
ServersStore: tt.fields.ServersStore,
LayoutStore: tt.fields.LayoutStore,
UsersStore: tt.fields.UsersStore,
DashboardsStore: tt.fields.DashboardsStore,
Store: &Store{
SourcesStore: tt.fields.SourcesStore,
ServersStore: tt.fields.ServersStore,
LayoutsStore: tt.fields.LayoutsStore,
UsersStore: tt.fields.UsersStore,
DashboardsStore: tt.fields.DashboardsStore,
},
TimeSeriesClient: tt.fields.TimeSeriesClient,
Logger: tt.fields.Logger,
UseAuth: tt.fields.UseAuth,
@ -52,7 +54,7 @@ func TestService_NewDatabase(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
ServersStore chronograf.ServersStore
LayoutStore chronograf.LayoutStore
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
TimeSeriesClient TimeSeriesClient
@ -74,11 +76,13 @@ func TestService_NewDatabase(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &Service{
SourcesStore: tt.fields.SourcesStore,
ServersStore: tt.fields.ServersStore,
LayoutStore: tt.fields.LayoutStore,
UsersStore: tt.fields.UsersStore,
DashboardsStore: tt.fields.DashboardsStore,
Store: &Store{
SourcesStore: tt.fields.SourcesStore,
ServersStore: tt.fields.ServersStore,
LayoutsStore: tt.fields.LayoutsStore,
UsersStore: tt.fields.UsersStore,
DashboardsStore: tt.fields.DashboardsStore,
},
TimeSeriesClient: tt.fields.TimeSeriesClient,
Logger: tt.fields.Logger,
UseAuth: tt.fields.UseAuth,
@ -93,7 +97,7 @@ func TestService_DropDatabase(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
ServersStore chronograf.ServersStore
LayoutStore chronograf.LayoutStore
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
TimeSeriesClient TimeSeriesClient
@ -115,11 +119,13 @@ func TestService_DropDatabase(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &Service{
SourcesStore: tt.fields.SourcesStore,
ServersStore: tt.fields.ServersStore,
LayoutStore: tt.fields.LayoutStore,
UsersStore: tt.fields.UsersStore,
DashboardsStore: tt.fields.DashboardsStore,
Store: &Store{
SourcesStore: tt.fields.SourcesStore,
ServersStore: tt.fields.ServersStore,
LayoutsStore: tt.fields.LayoutsStore,
UsersStore: tt.fields.UsersStore,
DashboardsStore: tt.fields.DashboardsStore,
},
TimeSeriesClient: tt.fields.TimeSeriesClient,
Logger: tt.fields.Logger,
UseAuth: tt.fields.UseAuth,
@ -134,7 +140,7 @@ func TestService_RetentionPolicies(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
ServersStore chronograf.ServersStore
LayoutStore chronograf.LayoutStore
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
TimeSeriesClient TimeSeriesClient
@ -156,11 +162,13 @@ func TestService_RetentionPolicies(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &Service{
SourcesStore: tt.fields.SourcesStore,
ServersStore: tt.fields.ServersStore,
LayoutStore: tt.fields.LayoutStore,
UsersStore: tt.fields.UsersStore,
DashboardsStore: tt.fields.DashboardsStore,
Store: &Store{
SourcesStore: tt.fields.SourcesStore,
ServersStore: tt.fields.ServersStore,
LayoutsStore: tt.fields.LayoutsStore,
UsersStore: tt.fields.UsersStore,
DashboardsStore: tt.fields.DashboardsStore,
},
TimeSeriesClient: tt.fields.TimeSeriesClient,
Logger: tt.fields.Logger,
UseAuth: tt.fields.UseAuth,
@ -175,7 +183,7 @@ func TestService_NewRetentionPolicy(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
ServersStore chronograf.ServersStore
LayoutStore chronograf.LayoutStore
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
TimeSeriesClient TimeSeriesClient
@ -197,11 +205,13 @@ func TestService_NewRetentionPolicy(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &Service{
SourcesStore: tt.fields.SourcesStore,
ServersStore: tt.fields.ServersStore,
LayoutStore: tt.fields.LayoutStore,
UsersStore: tt.fields.UsersStore,
DashboardsStore: tt.fields.DashboardsStore,
Store: &Store{
SourcesStore: tt.fields.SourcesStore,
ServersStore: tt.fields.ServersStore,
LayoutsStore: tt.fields.LayoutsStore,
UsersStore: tt.fields.UsersStore,
DashboardsStore: tt.fields.DashboardsStore,
},
TimeSeriesClient: tt.fields.TimeSeriesClient,
Logger: tt.fields.Logger,
UseAuth: tt.fields.UseAuth,
@ -216,7 +226,7 @@ func TestService_UpdateRetentionPolicy(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
ServersStore chronograf.ServersStore
LayoutStore chronograf.LayoutStore
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
TimeSeriesClient TimeSeriesClient
@ -238,11 +248,13 @@ func TestService_UpdateRetentionPolicy(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &Service{
SourcesStore: tt.fields.SourcesStore,
ServersStore: tt.fields.ServersStore,
LayoutStore: tt.fields.LayoutStore,
UsersStore: tt.fields.UsersStore,
DashboardsStore: tt.fields.DashboardsStore,
Store: &Store{
SourcesStore: tt.fields.SourcesStore,
ServersStore: tt.fields.ServersStore,
LayoutsStore: tt.fields.LayoutsStore,
UsersStore: tt.fields.UsersStore,
DashboardsStore: tt.fields.DashboardsStore,
},
TimeSeriesClient: tt.fields.TimeSeriesClient,
Logger: tt.fields.Logger,
UseAuth: tt.fields.UseAuth,
@ -257,7 +269,7 @@ func TestService_DropRetentionPolicy(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
ServersStore chronograf.ServersStore
LayoutStore chronograf.LayoutStore
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
TimeSeriesClient TimeSeriesClient
@ -279,11 +291,13 @@ func TestService_DropRetentionPolicy(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &Service{
SourcesStore: tt.fields.SourcesStore,
ServersStore: tt.fields.ServersStore,
LayoutStore: tt.fields.LayoutStore,
UsersStore: tt.fields.UsersStore,
DashboardsStore: tt.fields.DashboardsStore,
Store: &Store{
SourcesStore: tt.fields.SourcesStore,
ServersStore: tt.fields.ServersStore,
LayoutsStore: tt.fields.LayoutsStore,
UsersStore: tt.fields.UsersStore,
DashboardsStore: tt.fields.DashboardsStore,
},
TimeSeriesClient: tt.fields.TimeSeriesClient,
Logger: tt.fields.Logger,
UseAuth: tt.fields.UseAuth,

7
server/helpers.go Normal file
View File

@ -0,0 +1,7 @@
package server
import "net/http"
func location(w http.ResponseWriter, self string) {
w.Header().Add("Location", self)
}

View File

@ -24,40 +24,40 @@ type postInfluxResponse struct {
}
// Influx proxies requests to influxdb.
func (h *Service) Influx(w http.ResponseWriter, r *http.Request) {
func (s *Service) Influx(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
var req chronograf.Query
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
invalidJSON(w, s.Logger)
return
}
if err = ValidInfluxRequest(req); err != nil {
invalidData(w, err, h.Logger)
invalidData(w, err, s.Logger)
return
}
ctx := r.Context()
src, err := h.SourcesStore.Get(ctx, id)
src, err := s.Store.Sources(ctx).Get(ctx, id)
if err != nil {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
ts, err := h.TimeSeries(src)
ts, err := s.TimeSeries(src)
if err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", id, err)
Error(w, http.StatusBadRequest, msg, h.Logger)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
if err = ts.Connect(ctx, &src); err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", id, err)
Error(w, http.StatusBadRequest, msg, h.Logger)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
@ -65,38 +65,38 @@ func (h *Service) Influx(w http.ResponseWriter, r *http.Request) {
if err != nil {
if err == chronograf.ErrUpstreamTimeout {
msg := "Timeout waiting for Influx response"
Error(w, http.StatusRequestTimeout, msg, h.Logger)
Error(w, http.StatusRequestTimeout, msg, s.Logger)
return
}
// TODO: Here I want to return the error code from influx.
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
res := postInfluxResponse{
Results: response,
}
encodeJSON(w, http.StatusOK, res, h.Logger)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
func (h *Service) Write(w http.ResponseWriter, r *http.Request) {
func (s *Service) Write(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
src, err := h.SourcesStore.Get(ctx, id)
src, err := s.Store.Sources(ctx).Get(ctx, id)
if err != nil {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
u, err := url.Parse(src.URL)
if err != nil {
msg := fmt.Sprintf("Error parsing source url: %v", err)
Error(w, http.StatusUnprocessableEntity, msg, h.Logger)
Error(w, http.StatusUnprocessableEntity, msg, s.Logger)
return
}
u.Path = "/write"

View File

@ -18,13 +18,18 @@ type postKapacitorRequest struct {
Password string `json:"password,omitempty"`
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted.
Active bool `json:"active"`
Organization string `json:"organization"` // Organization is the organization ID that resource belongs to
}
func (p *postKapacitorRequest) Valid() error {
func (p *postKapacitorRequest) Valid(defaultOrgID string) error {
if p.Name == nil || p.URL == nil {
return fmt.Errorf("name and url required")
}
if p.Organization == "" {
p.Organization = defaultOrgID
}
url, err := url.ParseRequestURI(*p.URL)
if err != nil {
return fmt.Errorf("invalid source URI: %v", err)
@ -56,27 +61,34 @@ type kapacitor struct {
}
// NewKapacitor adds valid kapacitor store store.
func (h *Service) NewKapacitor(w http.ResponseWriter, r *http.Request) {
func (s *Service) NewKapacitor(w http.ResponseWriter, r *http.Request) {
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
_, err = h.SourcesStore.Get(ctx, srcID)
_, err = s.Store.Sources(ctx).Get(ctx, srcID)
if err != nil {
notFound(w, srcID, h.Logger)
notFound(w, srcID, s.Logger)
return
}
var req postKapacitorRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
invalidJSON(w, s.Logger)
return
}
if err := req.Valid(); err != nil {
invalidData(w, err, h.Logger)
defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
if err := req.Valid(fmt.Sprintf("%d", defaultOrg.ID)); err != nil {
invalidData(w, err, s.Logger)
return
}
@ -88,17 +100,18 @@ func (h *Service) NewKapacitor(w http.ResponseWriter, r *http.Request) {
InsecureSkipVerify: req.InsecureSkipVerify,
URL: *req.URL,
Active: req.Active,
Organization: req.Organization,
}
if srv, err = h.ServersStore.Add(ctx, srv); err != nil {
if srv, err = s.Store.Servers(ctx).Add(ctx, srv); err != nil {
msg := fmt.Errorf("Error storing kapacitor %v: %v", req, err)
unknownErrorWithMessage(w, msg, h.Logger)
unknownErrorWithMessage(w, msg, s.Logger)
return
}
res := newKapacitor(srv)
w.Header().Add("Location", res.Links.Self)
encodeJSON(w, http.StatusCreated, res, h.Logger)
location(w, res.Links.Self)
encodeJSON(w, http.StatusCreated, res, s.Logger)
}
func newKapacitor(srv chronograf.Server) kapacitor {
@ -125,17 +138,17 @@ type kapacitors struct {
}
// Kapacitors retrieves all kapacitors from store.
func (h *Service) Kapacitors(w http.ResponseWriter, r *http.Request) {
func (s *Service) Kapacitors(w http.ResponseWriter, r *http.Request) {
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
mrSrvs, err := h.ServersStore.All(ctx)
mrSrvs, err := s.Store.Servers(ctx).All(ctx)
if err != nil {
Error(w, http.StatusInternalServerError, "Error loading kapacitors", h.Logger)
Error(w, http.StatusInternalServerError, "Error loading kapacitors", s.Logger)
return
}
@ -150,57 +163,57 @@ func (h *Service) Kapacitors(w http.ResponseWriter, r *http.Request) {
Kapacitors: srvs,
}
encodeJSON(w, http.StatusOK, res, h.Logger)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// KapacitorsID retrieves a kapacitor with ID from store.
func (h *Service) KapacitorsID(w http.ResponseWriter, r *http.Request) {
func (s *Service) KapacitorsID(w http.ResponseWriter, r *http.Request) {
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
srv, err := h.ServersStore.Get(ctx, id)
srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
res := newKapacitor(srv)
encodeJSON(w, http.StatusOK, res, h.Logger)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// RemoveKapacitor deletes kapacitor from store.
func (h *Service) RemoveKapacitor(w http.ResponseWriter, r *http.Request) {
func (s *Service) RemoveKapacitor(w http.ResponseWriter, r *http.Request) {
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
srv, err := h.ServersStore.Get(ctx, id)
srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
if err = h.ServersStore.Delete(ctx, srv); err != nil {
unknownErrorWithMessage(w, err, h.Logger)
if err = s.Store.Servers(ctx).Delete(ctx, srv); err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
@ -230,34 +243,34 @@ func (p *patchKapacitorRequest) Valid() error {
}
// UpdateKapacitor incrementally updates a kapacitor definition in the store
func (h *Service) UpdateKapacitor(w http.ResponseWriter, r *http.Request) {
func (s *Service) UpdateKapacitor(w http.ResponseWriter, r *http.Request) {
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
srv, err := h.ServersStore.Get(ctx, id)
srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
var req patchKapacitorRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
invalidJSON(w, s.Logger)
return
}
if err := req.Valid(); err != nil {
invalidData(w, err, h.Logger)
invalidData(w, err, s.Logger)
return
}
@ -280,34 +293,34 @@ func (h *Service) UpdateKapacitor(w http.ResponseWriter, r *http.Request) {
srv.Active = *req.Active
}
if err := h.ServersStore.Update(ctx, srv); err != nil {
if err := s.Store.Servers(ctx).Update(ctx, srv); err != nil {
msg := fmt.Sprintf("Error updating kapacitor ID %d", id)
Error(w, http.StatusInternalServerError, msg, h.Logger)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
}
res := newKapacitor(srv)
encodeJSON(w, http.StatusOK, res, h.Logger)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// KapacitorRulesPost proxies POST to kapacitor
func (h *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) {
func (s *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) {
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
srv, err := h.ServersStore.Get(ctx, id)
srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
@ -315,7 +328,7 @@ func (h *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) {
var req chronograf.AlertRule
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
invalidJSON(w, s.Logger)
return
}
// TODO: validate this data
@ -328,12 +341,12 @@ func (h *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) {
task, err := c.Create(ctx, req)
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
return
}
res := newAlertResponse(task, srv.SrcID, srv.ID)
w.Header().Add("Location", res.Links.Self)
encodeJSON(w, http.StatusCreated, res, h.Logger)
location(w, res.Links.Self)
encodeJSON(w, http.StatusCreated, res, s.Logger)
}
type alertLinks struct {
@ -420,23 +433,23 @@ func ValidRuleRequest(rule chronograf.AlertRule) error {
}
// KapacitorRulesPut proxies PATCH to kapacitor
func (h *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) {
func (s *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) {
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
srv, err := h.ServersStore.Get(ctx, id)
srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
@ -444,7 +457,7 @@ func (h *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) {
c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify)
var req chronograf.AlertRule
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
invalidJSON(w, s.Logger)
return
}
// TODO: validate this data
@ -458,10 +471,10 @@ func (h *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) {
// Check if the rule exists and is scoped correctly
if _, err = c.Get(ctx, tid); err != nil {
if err == chronograf.ErrAlertNotFound {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
return
}
@ -469,11 +482,11 @@ func (h *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) {
req.ID = tid
task, err := c.Update(ctx, c.Href(tid), req)
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
return
}
res := newAlertResponse(task, srv.SrcID, srv.ID)
encodeJSON(w, http.StatusOK, res, h.Logger)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// KapacitorStatus is the current state of a running task
@ -490,23 +503,23 @@ func (k *KapacitorStatus) Valid() error {
}
// KapacitorRulesStatus proxies PATCH to kapacitor to enable/disable tasks
func (h *Service) KapacitorRulesStatus(w http.ResponseWriter, r *http.Request) {
func (s *Service) KapacitorRulesStatus(w http.ResponseWriter, r *http.Request) {
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
srv, err := h.ServersStore.Get(ctx, id)
srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
@ -515,11 +528,11 @@ func (h *Service) KapacitorRulesStatus(w http.ResponseWriter, r *http.Request) {
var req KapacitorStatus
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
invalidJSON(w, s.Logger)
return
}
if err := req.Valid(); err != nil {
invalidData(w, err, h.Logger)
invalidData(w, err, s.Logger)
return
}
@ -527,10 +540,10 @@ func (h *Service) KapacitorRulesStatus(w http.ResponseWriter, r *http.Request) {
_, err = c.Get(ctx, tid)
if err != nil {
if err == chronograf.ErrAlertNotFound {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
return
}
@ -542,39 +555,39 @@ func (h *Service) KapacitorRulesStatus(w http.ResponseWriter, r *http.Request) {
}
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
return
}
res := newAlertResponse(task, srv.SrcID, srv.ID)
encodeJSON(w, http.StatusOK, res, h.Logger)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// KapacitorRulesGet retrieves all rules
func (h *Service) KapacitorRulesGet(w http.ResponseWriter, r *http.Request) {
func (s *Service) KapacitorRulesGet(w http.ResponseWriter, r *http.Request) {
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
srv, err := h.ServersStore.Get(ctx, id)
srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify)
tasks, err := c.All(ctx)
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
return
}
@ -585,7 +598,7 @@ func (h *Service) KapacitorRulesGet(w http.ResponseWriter, r *http.Request) {
ar := newAlertResponse(task, srv.SrcID, srv.ID)
res.Rules = append(res.Rules, ar)
}
encodeJSON(w, http.StatusOK, res, h.Logger)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
type allAlertsResponse struct {
@ -593,23 +606,23 @@ type allAlertsResponse struct {
}
// KapacitorRulesID retrieves specific task
func (h *Service) KapacitorRulesID(w http.ResponseWriter, r *http.Request) {
func (s *Service) KapacitorRulesID(w http.ResponseWriter, r *http.Request) {
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
srv, err := h.ServersStore.Get(ctx, id)
srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
tid := httprouter.GetParamFromContext(ctx, "tid")
@ -620,35 +633,35 @@ func (h *Service) KapacitorRulesID(w http.ResponseWriter, r *http.Request) {
task, err := c.Get(ctx, tid)
if err != nil {
if err == chronograf.ErrAlertNotFound {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
return
}
res := newAlertResponse(task, srv.SrcID, srv.ID)
encodeJSON(w, http.StatusOK, res, h.Logger)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// KapacitorRulesDelete proxies DELETE to kapacitor
func (h *Service) KapacitorRulesDelete(w http.ResponseWriter, r *http.Request) {
func (s *Service) KapacitorRulesDelete(w http.ResponseWriter, r *http.Request) {
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
srv, err := h.ServersStore.Get(ctx, id)
srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
@ -658,14 +671,14 @@ func (h *Service) KapacitorRulesDelete(w http.ResponseWriter, r *http.Request) {
// Check if the rule is linked to this server and kapacitor
if _, err := c.Get(ctx, tid); err != nil {
if err == chronograf.ErrAlertNotFound {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
return
}
if err := c.Delete(ctx, c.Href(tid)); err != nil {
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
return
}

View File

@ -186,20 +186,22 @@ func Test_KapacitorRulesGet(t *testing.T) {
// setup mock service and test logger
testLogger := mocks.TestLogger{}
svc := &server.Service{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: ID,
InsecureSkipVerify: true,
}, nil
Store: &mocks.Store{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: ID,
InsecureSkipVerify: true,
}, nil
},
},
},
ServersStore: &mocks.ServersStore{
GetF: func(ctx context.Context, ID int) (chronograf.Server, error) {
return chronograf.Server{
SrcID: ID,
URL: kapaSrv.URL,
}, nil
ServersStore: &mocks.ServersStore{
GetF: func(ctx context.Context, ID int) (chronograf.Server, error) {
return chronograf.Server{
SrcID: ID,
URL: kapaSrv.URL,
}, nil
},
},
},
Logger: &testLogger,

View File

@ -1,7 +1,6 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
@ -49,37 +48,12 @@ func newLayoutResponse(layout chronograf.Layout) layoutResponse {
}
}
// NewLayout adds a valid layout to store.
func (h *Service) NewLayout(w http.ResponseWriter, r *http.Request) {
var layout chronograf.Layout
if err := json.NewDecoder(r.Body).Decode(&layout); err != nil {
invalidJSON(w, h.Logger)
return
}
if err := ValidLayoutRequest(layout); err != nil {
invalidData(w, err, h.Logger)
return
}
var err error
if layout, err = h.LayoutStore.Add(r.Context(), layout); err != nil {
msg := fmt.Errorf("Error storing layout %v: %v", layout, err)
unknownErrorWithMessage(w, msg, h.Logger)
return
}
res := newLayoutResponse(layout)
w.Header().Add("Location", res.Link.Href)
encodeJSON(w, http.StatusCreated, res, h.Logger)
}
type getLayoutsResponse struct {
Layouts []layoutResponse `json:"layouts"`
}
// Layouts retrieves all layouts from store
func (h *Service) Layouts(w http.ResponseWriter, r *http.Request) {
func (s *Service) Layouts(w http.ResponseWriter, r *http.Request) {
// Construct a filter sieve for both applications and measurements
filtered := map[string]bool{}
for _, a := range r.URL.Query()["app"] {
@ -91,9 +65,9 @@ func (h *Service) Layouts(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
layouts, err := h.LayoutStore.All(ctx)
layouts, err := s.Store.Layouts(ctx).All(ctx)
if err != nil {
Error(w, http.StatusInternalServerError, "Error loading layouts", h.Logger)
Error(w, http.StatusInternalServerError, "Error loading layouts", s.Logger)
return
}
@ -110,94 +84,32 @@ func (h *Service) Layouts(w http.ResponseWriter, r *http.Request) {
res := getLayoutsResponse{
Layouts: []layoutResponse{},
}
seen := make(map[string]bool)
for _, layout := range layouts {
// remove duplicates
if seen[layout.Measurement+layout.ID] {
continue
}
// filter for data that belongs to provided application or measurement
if filter(&layout) {
res.Layouts = append(res.Layouts, newLayoutResponse(layout))
}
}
encodeJSON(w, http.StatusOK, res, h.Logger)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// LayoutsID retrieves layout with ID from store
func (h *Service) LayoutsID(w http.ResponseWriter, r *http.Request) {
func (s *Service) LayoutsID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := httprouter.GetParamFromContext(ctx, "id")
layout, err := h.LayoutStore.Get(ctx, id)
layout, err := s.Store.Layouts(ctx).Get(ctx, id)
if err != nil {
Error(w, http.StatusNotFound, fmt.Sprintf("ID %s not found", id), h.Logger)
Error(w, http.StatusNotFound, fmt.Sprintf("ID %s not found", id), s.Logger)
return
}
res := newLayoutResponse(layout)
encodeJSON(w, http.StatusOK, res, h.Logger)
}
// RemoveLayout deletes layout from store.
func (h *Service) RemoveLayout(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := httprouter.GetParamFromContext(ctx, "id")
layout := chronograf.Layout{
ID: id,
}
if err := h.LayoutStore.Delete(ctx, layout); err != nil {
unknownErrorWithMessage(w, err, h.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}
// UpdateLayout replaces the layout of ID with new valid layout.
func (h *Service) UpdateLayout(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := httprouter.GetParamFromContext(ctx, "id")
_, err := h.LayoutStore.Get(ctx, id)
if err != nil {
Error(w, http.StatusNotFound, fmt.Sprintf("ID %s not found", id), h.Logger)
return
}
var req chronograf.Layout
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
return
}
req.ID = id
if err := ValidLayoutRequest(req); err != nil {
invalidData(w, err, h.Logger)
return
}
if err := h.LayoutStore.Update(ctx, req); err != nil {
msg := fmt.Sprintf("Error updating layout ID %s: %v", id, err)
Error(w, http.StatusInternalServerError, msg, h.Logger)
return
}
res := newLayoutResponse(req)
encodeJSON(w, http.StatusOK, res, h.Logger)
}
// ValidLayoutRequest checks if the layout has valid application, measurement and cells.
func ValidLayoutRequest(l chronograf.Layout) error {
if l.Application == "" || l.Measurement == "" || len(l.Cells) == 0 {
return fmt.Errorf("app, measurement, and cells required")
}
for _, c := range l.Cells {
if c.W == 0 || c.H == 0 {
return fmt.Errorf("w, and h required")
}
for _, q := range c.Queries {
if q.Command == "" {
return fmt.Errorf("query required")
}
}
}
return nil
encodeJSON(w, http.StatusOK, res, s.Logger)
}

View File

@ -126,7 +126,7 @@ func Test_Layouts(t *testing.T) {
// setup mock chronograf.Service and mock logger
lg := &mocks.TestLogger{}
svc := server.Service{
LayoutStore: &mocks.LayoutStore{
Store: &mocks.Store{LayoutsStore: &mocks.LayoutsStore{
AllF: func(ctx context.Context) ([]chronograf.Layout, error) {
if len(test.allLayouts) == 0 {
return []chronograf.Layout{
@ -137,6 +137,7 @@ func Test_Layouts(t *testing.T) {
}
},
},
},
Logger: lg,
}

View File

@ -1,38 +0,0 @@
package server
import "net/http"
type getMappingsResponse struct {
Mappings []mapping `json:"mappings"`
}
type mapping struct {
Measurement string `json:"measurement"` // The measurement where data for this mapping is found
Name string `json:"name"` // The application name which will be assigned to the corresponding measurement
}
// GetMappings returns the known mappings of measurements to applications
func (h *Service) GetMappings(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
layouts, err := h.LayoutStore.All(ctx)
if err != nil {
Error(w, http.StatusInternalServerError, "Error loading layouts", h.Logger)
return
}
mp := getMappingsResponse{
Mappings: []mapping{},
}
seen := make(map[string]bool)
for _, layout := range layouts {
if seen[layout.Measurement+layout.ID] {
continue
}
mp.Mappings = append(mp.Mappings, mapping{layout.Measurement, layout.Application})
seen[layout.Measurement+layout.ID] = true
}
encodeJSON(w, http.StatusOK, mp, h.Logger)
}

View File

@ -1,13 +1,16 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"sort"
"golang.org/x/net/context"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/oauth2"
"github.com/influxdata/chronograf/organizations"
)
type meLinks struct {
@ -16,7 +19,9 @@ type meLinks struct {
type meResponse struct {
*chronograf.User
Links meLinks `json:"links"`
Links meLinks `json:"links"`
Organizations []chronograf.Organization `json:"organizations,omitempty"`
CurrentOrganization *chronograf.Organization `json:"currentOrganization,omitempty"`
}
// If new user response is nil, return an empty meResponse because it
@ -25,7 +30,7 @@ func newMeResponse(usr *chronograf.User) meResponse {
base := "/chronograf/v1/users"
name := "me"
if usr != nil {
name = PathEscape(usr.Name)
name = PathEscape(fmt.Sprintf("%d", usr.ID))
}
return meResponse{
@ -36,15 +41,11 @@ func newMeResponse(usr *chronograf.User) meResponse {
}
}
func getEmail(ctx context.Context) (string, error) {
principal, err := getPrincipal(ctx)
if err != nil {
return "", err
}
if principal.Subject == "" {
return "", fmt.Errorf("Token not found")
}
return principal.Subject, nil
// TODO: This Scheme value is hard-coded temporarily since we only currently
// support OAuth2. This hard-coding should be removed whenever we add
// support for other authentication schemes.
func getScheme(ctx context.Context) (string, error) {
return "oauth2", nil
}
func getPrincipal(ctx context.Context) (oauth2.Principal, error) {
@ -56,41 +57,305 @@ func getPrincipal(ctx context.Context) (oauth2.Principal, error) {
return principal, nil
}
// Me does a findOrCreate based on the email in the context
func (h *Service) Me(w http.ResponseWriter, r *http.Request) {
func getValidPrincipal(ctx context.Context) (oauth2.Principal, error) {
p, err := getPrincipal(ctx)
if err != nil {
return p, err
}
if p.Subject == "" {
return oauth2.Principal{}, fmt.Errorf("Token not found")
}
if p.Issuer == "" {
return oauth2.Principal{}, fmt.Errorf("Token not found")
}
return p, nil
}
type meRequest struct {
// Organization is the OrganizationID
Organization string `json:"organization"`
}
// UpdateMe changes the user's current organization on the JWT and responds
// with the same semantics as Me
func (s *Service) UpdateMe(auth oauth2.Authenticator) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
serverCtx := serverContext(ctx)
principal, err := auth.Validate(ctx, r)
if err != nil {
s.Logger.Error(fmt.Sprintf("Invalid principal: %v", err))
Error(w, http.StatusForbidden, "invalid principal", s.Logger)
return
}
var req meRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
return
}
// validate that the organization exists
orgID, err := parseOrganizationID(req.Organization)
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
return
}
_, err = s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &orgID})
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
// validate that user belongs to organization
ctx = context.WithValue(ctx, organizations.ContextKey, req.Organization)
p, err := getValidPrincipal(ctx)
if err != nil {
invalidData(w, err, s.Logger)
return
}
if p.Organization == "" {
defaultOrg, err := s.Store.Organizations(serverCtx).DefaultOrganization(serverCtx)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
p.Organization = fmt.Sprintf("%d", defaultOrg.ID)
}
scheme, err := getScheme(ctx)
if err != nil {
invalidData(w, err, s.Logger)
return
}
_, err = s.Store.Users(ctx).Get(ctx, chronograf.UserQuery{
Name: &p.Subject,
Provider: &p.Issuer,
Scheme: &scheme,
})
if err == chronograf.ErrUserNotFound {
// Since a user is not a part of this organization, we should tell them that they are Forbidden (403) from accessing this resource
Error(w, http.StatusForbidden, err.Error(), s.Logger)
return
}
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
// TODO: change to principal.CurrentOrganization
principal.Organization = req.Organization
if err := auth.Authorize(ctx, w, principal); err != nil {
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
return
}
ctx = context.WithValue(ctx, oauth2.PrincipalKey, principal)
s.Me(w, r.WithContext(ctx))
}
}
// Me does a findOrCreate based on the username in the context
func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if !h.UseAuth {
if !s.UseAuth {
// If there's no authentication, return an empty user
res := newMeResponse(nil)
encodeJSON(w, http.StatusOK, res, h.Logger)
encodeJSON(w, http.StatusOK, res, s.Logger)
return
}
email, err := getEmail(ctx)
p, err := getValidPrincipal(ctx)
if err != nil {
invalidData(w, err, h.Logger)
invalidData(w, err, s.Logger)
return
}
scheme, err := getScheme(ctx)
if err != nil {
invalidData(w, err, s.Logger)
return
}
usr, err := h.UsersStore.Get(ctx, email)
if err == nil {
ctx = context.WithValue(ctx, organizations.ContextKey, p.Organization)
serverCtx := serverContext(ctx)
if p.Organization == "" {
defaultOrg, err := s.Store.Organizations(serverCtx).DefaultOrganization(serverCtx)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
p.Organization = fmt.Sprintf("%d", defaultOrg.ID)
}
usr, err := s.Store.Users(serverCtx).Get(serverCtx, chronograf.UserQuery{
Name: &p.Subject,
Provider: &p.Issuer,
Scheme: &scheme,
})
if err != nil && err != chronograf.ErrUserNotFound {
unknownErrorWithMessage(w, err, s.Logger)
return
}
defaultOrg, err := s.Store.Organizations(serverCtx).DefaultOrganization(serverCtx)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
if usr != nil {
// 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)
return
}
orgID, err := parseOrganizationID(p.Organization)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
currentOrg, err := s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &orgID})
if err == chronograf.ErrOrganizationNotFound {
// The intent is to force a the user to go through another auth flow
Error(w, http.StatusForbidden, "user's current organization was not found", s.Logger)
return
}
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
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)
return
}
res := newMeResponse(usr)
encodeJSON(w, http.StatusOK, res, h.Logger)
res.Organizations = orgs
res.CurrentOrganization = currentOrg
encodeJSON(w, http.StatusOK, res, s.Logger)
return
}
// If users must be explicitly added to the default organization, respond with 403
// forbidden
if !defaultOrg.Public {
Error(w, http.StatusForbidden, "This organization is private. To gain access, you must be explicitly added by an administrator.", s.Logger)
return
}
// Because we didnt find a user, making a new one
user := &chronograf.User{
Name: email,
Name: p.Subject,
Provider: p.Issuer,
// TODO: This Scheme value is hard-coded temporarily since we only currently
// support OAuth2. This hard-coding should be removed whenever we add
// support for other authentication schemes.
Scheme: scheme,
Roles: []chronograf.Role{
{
Name: defaultOrg.DefaultRole,
// This is the ID of the default organization
Organization: fmt.Sprintf("%d", defaultOrg.ID),
},
},
// TODO(desa): this needs a better name
SuperAdmin: s.newUsersAreSuperAdmin(),
}
newUser, err := h.UsersStore.Add(ctx, user)
newUser, err := s.Store.Users(serverCtx).Add(serverCtx, user)
if err != nil {
msg := fmt.Errorf("error storing user %s: %v", user.Name, err)
unknownErrorWithMessage(w, msg, h.Logger)
unknownErrorWithMessage(w, msg, s.Logger)
return
}
orgs, err := s.usersOrganizations(serverCtx, newUser)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
orgID, err := parseOrganizationID(p.Organization)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
currentOrg, err := s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &orgID})
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
res := newMeResponse(newUser)
encodeJSON(w, http.StatusOK, res, h.Logger)
res.Organizations = orgs
res.CurrentOrganization = currentOrg
encodeJSON(w, http.StatusOK, res, s.Logger)
}
func (s *Service) firstUser() bool {
serverCtx := serverContext(context.Background())
numUsers, err := s.Store.Users(serverCtx).Num(serverCtx)
if err != nil {
return false
}
return numUsers == 0
}
func (s *Service) newUsersAreSuperAdmin() bool {
if s.firstUser() {
return true
}
return !s.SuperAdminFirstUserOnly
}
func (s *Service) usersOrganizations(ctx context.Context, u *chronograf.User) ([]chronograf.Organization, error) {
if u == nil {
// TODO(desa): better error
return nil, fmt.Errorf("user was nil")
}
orgIDs := map[string]bool{}
for _, role := range u.Roles {
orgIDs[role.Organization] = true
}
orgs := []chronograf.Organization{}
for orgID, _ := range orgIDs {
id, err := parseOrganizationID(orgID)
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
if err != nil {
return nil, err
}
orgs = append(orgs, *org)
}
sort.Slice(orgs, func(i, j int) bool {
return orgs[i].ID < orgs[j].ID
})
return orgs, nil
}
func hasRoleInDefaultOrganization(u *chronograf.User, orgID string) bool {
for _, role := range u.Roles {
if role.Organization == orgID {
return true
}
}
return false
}

View File

@ -1,7 +1,9 @@
package server
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
@ -12,15 +14,18 @@ import (
"github.com/influxdata/chronograf/log"
"github.com/influxdata/chronograf/mocks"
"github.com/influxdata/chronograf/oauth2"
"github.com/influxdata/chronograf/roles"
)
type MockUsers struct{}
func TestService_Me(t *testing.T) {
type fields struct {
UsersStore chronograf.UsersStore
Logger chronograf.Logger
UseAuth bool
UsersStore chronograf.UsersStore
OrganizationsStore chronograf.OrganizationsStore
Logger chronograf.Logger
UseAuth bool
SuperAdminFirstUserOnly bool
}
type args struct {
w *httptest.ResponseRecorder
@ -35,6 +40,72 @@ func TestService_Me(t *testing.T) {
wantContentType string
wantBody string
}{
{
name: "Existing user - not member of any organization",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
SuperAdminFirstUserOnly: 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: false,
}, nil
case 1:
return &chronograf.Organization{
ID: 1,
Name: "The Bad Place",
Public: false,
}, 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",
args: args{
@ -43,46 +114,286 @@ func TestService_Me(t *testing.T) {
},
fields: fields{
UseAuth: true,
UsersStore: &mocks.UsersStore{
GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
return &chronograf.User{
Name: "me",
Passwd: "hunter2",
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: true,
}, 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.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"me","password":"hunter2","links":{"self":"/chronograf/v1/users/me"}}
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}}
`,
},
{
name: "New user",
name: "Existing user - organization doesn't exist",
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: true,
}, 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
}
return nil, chronograf.ErrOrganizationNotFound
},
},
UsersStore: &mocks.UsersStore{
GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
return nil, fmt.Errorf("Unknown User")
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",
Organization: "1",
},
wantStatus: http.StatusForbidden,
wantContentType: "application/json",
wantBody: `{"code":403,"message":"user's current organization was not found"}`,
},
{
name: "new user - default org is public",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
SuperAdminFirstUserOnly: false,
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
Name: "The Gnarly Default",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
Name: "The Gnarly Default",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, 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 nil, chronograf.ErrUserNotFound
},
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
},
},
},
principal: oauth2.Principal{
Subject: "secret",
Issuer: "auth0",
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"secret","password":"","links":{"self":"/chronograf/v1/users/secret"}}
wantBody: `{"name":"secret","superAdmin":true,"roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"The Gnarly Default","defaultRole":"viewer","public":true}}
`,
},
{
name: "New user - New users not super admin, not first user",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
SuperAdminFirstUserOnly: true,
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
Name: "The Gnarly Default",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
Name: "The Gnarly Default",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, 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 nil, chronograf.ErrUserNotFound
},
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
},
},
},
principal: oauth2.Principal{
Subject: "secret",
Issuer: "auth0",
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"secret","roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}}
`,
},
{
name: "New user - New users not super admin, first user",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
SuperAdminFirstUserOnly: true,
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
Name: "The Gnarly Default",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
Name: "The Gnarly Default",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, 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 0, 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 nil, chronograf.ErrUserNotFound
},
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
},
},
},
principal: oauth2.Principal{
Subject: "secret",
Issuer: "auth0",
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"secret","superAdmin":true,"roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}}
`,
},
{
@ -92,19 +403,43 @@ func TestService_Me(t *testing.T) {
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
UseAuth: true,
SuperAdminFirstUserOnly: true,
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
Public: true,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
Name: "The Bad Place",
Public: true,
}, nil
},
},
UsersStore: &mocks.UsersStore{
GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
return nil, fmt.Errorf("Unknown User")
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) {
return nil, chronograf.ErrUserNotFound
},
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return nil, fmt.Errorf("Why Heavy?")
},
UpdateF: func(ctx context.Context, u *chronograf.User) error {
return nil
},
},
Logger: log.New(log.DebugLevel),
},
principal: oauth2.Principal{
Subject: "secret",
Issuer: "heroku",
},
wantStatus: http.StatusInternalServerError,
wantContentType: "application/json",
@ -117,8 +452,9 @@ func TestService_Me(t *testing.T) {
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: false,
Logger: log.New(log.DebugLevel),
UseAuth: false,
SuperAdminFirstUserOnly: true,
Logger: log.New(log.DebugLevel),
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
@ -132,24 +468,76 @@ func TestService_Me(t *testing.T) {
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
Logger: log.New(log.DebugLevel),
UseAuth: true,
SuperAdminFirstUserOnly: true,
Logger: log.New(log.DebugLevel),
},
wantStatus: http.StatusUnprocessableEntity,
principal: oauth2.Principal{
Subject: "",
Issuer: "",
},
},
{
name: "new user - default org is private",
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: "The Bad Place",
DefaultRole: roles.MemberRoleName,
Public: false,
}, 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 nil, chronograf.ErrUserNotFound
},
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
},
},
},
principal: oauth2.Principal{
Subject: "secret",
Issuer: "auth0",
},
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."}`,
},
}
for _, tt := range tests {
tt.args.r = tt.args.r.WithContext(context.WithValue(context.Background(), oauth2.PrincipalKey, tt.principal))
h := &Service{
UsersStore: tt.fields.UsersStore,
Logger: tt.fields.Logger,
UseAuth: tt.fields.UseAuth,
s := &Service{
Store: &mocks.Store{
UsersStore: tt.fields.UsersStore,
OrganizationsStore: tt.fields.OrganizationsStore,
},
Logger: tt.fields.Logger,
UseAuth: tt.fields.UseAuth,
SuperAdminFirstUserOnly: tt.fields.SuperAdminFirstUserOnly,
}
h.Me(tt.args.w, tt.args.r)
s.Me(tt.args.w, tt.args.r)
resp := tt.args.w.Result()
content := resp.Header.Get("Content-Type")
@ -161,8 +549,331 @@ func TestService_Me(t *testing.T) {
if tt.wantContentType != "" && content != tt.wantContentType {
t.Errorf("%q. Me() = %v, want %v", tt.name, content, tt.wantContentType)
}
if tt.wantBody != "" && string(body) != tt.wantBody {
if tt.wantBody == "" {
continue
}
if eq, err := jsonEqual(tt.wantBody, string(body)); err != nil || !eq {
t.Errorf("%q. Me() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
}
}
}
func TestService_UpdateMe(t *testing.T) {
type fields struct {
UsersStore chronograf.UsersStore
OrganizationsStore chronograf.OrganizationsStore
Logger chronograf.Logger
UseAuth bool
}
type args struct {
w *httptest.ResponseRecorder
r *http.Request
meRequest *meRequest
auth mocks.Authenticator
}
tests := []struct {
name string
fields fields
args args
principal oauth2.Principal
wantStatus int
wantContentType string
wantBody string
}{
{
name: "Set the current User's organization",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
meRequest: &meRequest{
Organization: "1337",
},
auth: mocks.Authenticator{},
},
fields: fields{
UseAuth: true,
Logger: log.New(log.DebugLevel),
UsersStore: &mocks.UsersStore{
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",
Roles: []chronograf.Role{
{
Name: roles.AdminRoleName,
Organization: "1337",
},
},
}, nil
},
UpdateF: func(ctx context.Context, u *chronograf.User) error {
return nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
Name: "Default",
DefaultRole: roles.AdminRoleName,
Public: true,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
if q.ID == nil {
return nil, fmt.Errorf("Invalid organization query: missing ID")
}
switch *q.ID {
case 0:
return &chronograf.Organization{
ID: 0,
Name: "Default",
DefaultRole: roles.AdminRoleName,
Public: true,
}, nil
case 1337:
return &chronograf.Organization{
ID: 1337,
Name: "The ShillBillThrilliettas",
Public: true,
}, nil
}
return nil, nil
},
},
},
principal: oauth2.Principal{
Subject: "me",
Issuer: "github",
},
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}}`,
},
{
name: "Change the current User's organization",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
meRequest: &meRequest{
Organization: "1337",
},
auth: mocks.Authenticator{},
},
fields: fields{
UseAuth: true,
Logger: log.New(log.DebugLevel),
UsersStore: &mocks.UsersStore{
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",
Roles: []chronograf.Role{
{
Name: roles.AdminRoleName,
Organization: "1337",
},
},
}, nil
},
UpdateF: func(ctx context.Context, u *chronograf.User) error {
return nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
Name: "Default",
DefaultRole: roles.EditorRoleName,
Public: true,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
if q.ID == nil {
return nil, fmt.Errorf("Invalid organization query: missing ID")
}
switch *q.ID {
case 1337:
return &chronograf.Organization{
ID: 1337,
Name: "The ThrillShilliettos",
Public: false,
}, nil
case 0:
return &chronograf.Organization{
ID: 0,
Name: "Default",
DefaultRole: roles.EditorRoleName,
Public: true,
}, nil
}
return nil, nil
},
},
},
principal: oauth2.Principal{
Subject: "me",
Issuer: "github",
Organization: "1338",
},
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}}`,
},
{
name: "Unable to find requested user in valid organization",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
meRequest: &meRequest{
Organization: "1337",
},
auth: mocks.Authenticator{},
},
fields: fields{
UseAuth: true,
Logger: log.New(log.DebugLevel),
UsersStore: &mocks.UsersStore{
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",
Roles: []chronograf.Role{
{
Name: roles.AdminRoleName,
Organization: "1338",
},
},
}, nil
},
UpdateF: func(ctx context.Context, u *chronograf.User) error {
return nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
if q.ID == nil {
return nil, fmt.Errorf("Invalid organization query: missing ID")
}
return &chronograf.Organization{
ID: 1337,
Name: "The ShillBillThrilliettas",
Public: true,
}, nil
},
},
},
principal: oauth2.Principal{
Subject: "me",
Issuer: "github",
Organization: "1338",
},
wantStatus: http.StatusForbidden,
wantContentType: "application/json",
wantBody: `{"code":403,"message":"user not found"}`,
},
{
name: "Unable to find requested organization",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
meRequest: &meRequest{
Organization: "1337",
},
auth: mocks.Authenticator{},
},
fields: fields{
UseAuth: true,
Logger: log.New(log.DebugLevel),
UsersStore: &mocks.UsersStore{
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",
Roles: []chronograf.Role{
{
Name: roles.AdminRoleName,
Organization: "1337",
},
},
}, nil
},
UpdateF: func(ctx context.Context, u *chronograf.User) error {
return nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return nil, chronograf.ErrOrganizationNotFound
},
},
},
principal: oauth2.Principal{
Subject: "me",
Issuer: "github",
Organization: "1338",
},
wantStatus: http.StatusBadRequest,
wantContentType: "application/json",
wantBody: `{"code":400,"message":"organization not found"}`,
},
}
for _, tt := range tests {
tt.args.r = tt.args.r.WithContext(context.WithValue(context.Background(), oauth2.PrincipalKey, tt.principal))
s := &Service{
Store: &Store{
UsersStore: tt.fields.UsersStore,
OrganizationsStore: tt.fields.OrganizationsStore,
},
Logger: tt.fields.Logger,
UseAuth: tt.fields.UseAuth,
}
buf, _ := json.Marshal(tt.args.meRequest)
tt.args.r.Body = ioutil.NopCloser(bytes.NewReader(buf))
tt.args.auth.Principal = tt.principal
s.UpdateMe(&tt.args.auth)(tt.args.w, tt.args.r)
resp := tt.args.w.Result()
content := resp.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != tt.wantStatus {
t.Errorf("%q. UpdateMe() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
}
if tt.wantContentType != "" && content != tt.wantContentType {
t.Errorf("%q. UpdateMe() = %v, want %v", tt.name, content, tt.wantContentType)
}
if eq, err := jsonEqual(tt.wantBody, string(body)); err != nil || !eq {
t.Errorf("%q. UpdateMe() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
}
}
}

View File

@ -12,6 +12,7 @@ import (
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf" // When julienschmidt/httprouter v2 w/ context is out, switch
"github.com/influxdata/chronograf/oauth2"
"github.com/influxdata/chronograf/roles"
)
const (
@ -67,120 +68,174 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
hr.NotFound = http.StripPrefix(opts.Basepath, hr.NotFound)
}
EnsureViewer := func(next http.HandlerFunc) http.HandlerFunc {
return AuthorizedUser(
service.Store,
opts.UseAuth,
roles.ViewerRoleName,
opts.Logger,
next,
)
}
EnsureEditor := func(next http.HandlerFunc) http.HandlerFunc {
return AuthorizedUser(
service.Store,
opts.UseAuth,
roles.EditorRoleName,
opts.Logger,
next,
)
}
EnsureAdmin := func(next http.HandlerFunc) http.HandlerFunc {
return AuthorizedUser(
service.Store,
opts.UseAuth,
roles.AdminRoleName,
opts.Logger,
next,
)
}
EnsureSuperAdmin := func(next http.HandlerFunc) http.HandlerFunc {
return AuthorizedUser(
service.Store,
opts.UseAuth,
roles.SuperAdminStatus,
opts.Logger,
next,
)
}
/* Documentation */
router.GET("/swagger.json", Spec())
router.GET("/docs", Redoc("/swagger.json"))
/* API */
// Sources
router.GET("/chronograf/v1/sources", service.Sources)
router.POST("/chronograf/v1/sources", service.NewSource)
// Organizations
router.GET("/chronograf/v1/organizations", EnsureAdmin(service.Organizations))
router.POST("/chronograf/v1/organizations", EnsureSuperAdmin(service.NewOrganization))
router.GET("/chronograf/v1/sources/:id", service.SourcesID)
router.PATCH("/chronograf/v1/sources/:id", service.UpdateSource)
router.DELETE("/chronograf/v1/sources/:id", service.RemoveSource)
router.GET("/chronograf/v1/organizations/:id", EnsureAdmin(service.OrganizationID))
router.PATCH("/chronograf/v1/organizations/:id", EnsureSuperAdmin(service.UpdateOrganization))
router.DELETE("/chronograf/v1/organizations/:id", EnsureSuperAdmin(service.RemoveOrganization))
// Sources
router.GET("/chronograf/v1/sources", EnsureViewer(service.Sources))
router.POST("/chronograf/v1/sources", EnsureEditor(service.NewSource))
router.GET("/chronograf/v1/sources/:id", EnsureViewer(service.SourcesID))
router.PATCH("/chronograf/v1/sources/:id", EnsureEditor(service.UpdateSource))
router.DELETE("/chronograf/v1/sources/:id", EnsureEditor(service.RemoveSource))
// Source Proxy to Influx; Has gzip compression around the handler
influx := gziphandler.GzipHandler(http.HandlerFunc(service.Influx))
influx := gziphandler.GzipHandler(http.HandlerFunc(EnsureViewer(service.Influx)))
router.Handler("POST", "/chronograf/v1/sources/:id/proxy", influx)
// Write proxies line protocol write requests to InfluxDB
router.POST("/chronograf/v1/sources/:id/write", service.Write)
router.POST("/chronograf/v1/sources/:id/write", EnsureViewer(service.Write))
// Queries is used to analyze a specific queries
router.POST("/chronograf/v1/sources/:id/queries", service.Queries)
// Queries is used to analyze a specific queries and does not create any
// resources. It's a POST because Queries are POSTed to InfluxDB, but this
// only modifies InfluxDB resources with certain metaqueries, e.g. DROP DATABASE.
//
// Admins should ensure that the InfluxDB source as the proper permissions
// intended for Chronograf Users with the Viewer Role type.
router.POST("/chronograf/v1/sources/:id/queries", EnsureViewer(service.Queries))
// All possible permissions for users in this source
router.GET("/chronograf/v1/sources/:id/permissions", service.Permissions)
router.GET("/chronograf/v1/sources/:id/permissions", EnsureViewer(service.Permissions))
// Users associated with the data source
router.GET("/chronograf/v1/sources/:id/users", service.SourceUsers)
router.POST("/chronograf/v1/sources/:id/users", service.NewSourceUser)
router.GET("/chronograf/v1/sources/:id/users", EnsureAdmin(service.SourceUsers))
router.POST("/chronograf/v1/sources/:id/users", EnsureAdmin(service.NewSourceUser))
router.GET("/chronograf/v1/sources/:id/users/:uid", service.SourceUserID)
router.DELETE("/chronograf/v1/sources/:id/users/:uid", service.RemoveSourceUser)
router.PATCH("/chronograf/v1/sources/:id/users/:uid", service.UpdateSourceUser)
router.GET("/chronograf/v1/sources/:id/users/:uid", EnsureAdmin(service.SourceUserID))
router.DELETE("/chronograf/v1/sources/:id/users/:uid", EnsureAdmin(service.RemoveSourceUser))
router.PATCH("/chronograf/v1/sources/:id/users/:uid", EnsureAdmin(service.UpdateSourceUser))
// Roles associated with the data source
router.GET("/chronograf/v1/sources/:id/roles", service.Roles)
router.POST("/chronograf/v1/sources/:id/roles", service.NewRole)
router.GET("/chronograf/v1/sources/:id/roles", EnsureViewer(service.SourceRoles))
router.POST("/chronograf/v1/sources/:id/roles", EnsureEditor(service.NewSourceRole))
router.GET("/chronograf/v1/sources/:id/roles/:rid", service.RoleID)
router.DELETE("/chronograf/v1/sources/:id/roles/:rid", service.RemoveRole)
router.PATCH("/chronograf/v1/sources/:id/roles/:rid", service.UpdateRole)
router.GET("/chronograf/v1/sources/:id/roles/:rid", EnsureViewer(service.SourceRoleID))
router.DELETE("/chronograf/v1/sources/:id/roles/:rid", EnsureEditor(service.RemoveSourceRole))
router.PATCH("/chronograf/v1/sources/:id/roles/:rid", EnsureEditor(service.UpdateSourceRole))
// Kapacitor
router.GET("/chronograf/v1/sources/:id/kapacitors", service.Kapacitors)
router.POST("/chronograf/v1/sources/:id/kapacitors", service.NewKapacitor)
router.GET("/chronograf/v1/sources/:id/kapacitors", EnsureViewer(service.Kapacitors))
router.POST("/chronograf/v1/sources/:id/kapacitors", EnsureEditor(service.NewKapacitor))
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid", service.KapacitorsID)
router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid", service.UpdateKapacitor)
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid", service.RemoveKapacitor)
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid", EnsureViewer(service.KapacitorsID))
router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid", EnsureEditor(service.UpdateKapacitor))
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid", EnsureEditor(service.RemoveKapacitor))
// Kapacitor rules
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/rules", service.KapacitorRulesGet)
router.POST("/chronograf/v1/sources/:id/kapacitors/:kid/rules", service.KapacitorRulesPost)
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/rules", EnsureViewer(service.KapacitorRulesGet))
router.POST("/chronograf/v1/sources/:id/kapacitors/:kid/rules", EnsureEditor(service.KapacitorRulesPost))
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesID)
router.PUT("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesPut)
router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesStatus)
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesDelete)
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", EnsureViewer(service.KapacitorRulesID))
router.PUT("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", EnsureEditor(service.KapacitorRulesPut))
router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", EnsureEditor(service.KapacitorRulesStatus))
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", EnsureEditor(service.KapacitorRulesDelete))
// Kapacitor Proxy
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyGet)
router.POST("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyPost)
router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyPatch)
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyDelete)
// Mappings
router.GET("/chronograf/v1/mappings", service.GetMappings)
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureViewer(service.KapacitorProxyGet))
router.POST("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureEditor(service.KapacitorProxyPost))
router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureEditor(service.KapacitorProxyPatch))
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureEditor(service.KapacitorProxyDelete))
// Layouts
router.GET("/chronograf/v1/layouts", service.Layouts)
router.POST("/chronograf/v1/layouts", service.NewLayout)
router.GET("/chronograf/v1/layouts", EnsureViewer(service.Layouts))
router.GET("/chronograf/v1/layouts/:id", EnsureViewer(service.LayoutsID))
router.GET("/chronograf/v1/layouts/:id", service.LayoutsID)
router.PUT("/chronograf/v1/layouts/:id", service.UpdateLayout)
router.DELETE("/chronograf/v1/layouts/:id", service.RemoveLayout)
// Users
// Users associated with Chronograf
router.GET("/chronograf/v1/me", service.Me)
// Set current chronograf organization the user is logged into
router.PUT("/chronograf/v1/me", service.UpdateMe(opts.Auth))
// TODO(desa): what to do about admin's being able to set superadmin
router.GET("/chronograf/v1/users", EnsureAdmin(service.Users))
router.POST("/chronograf/v1/users", EnsureAdmin(service.NewUser))
router.GET("/chronograf/v1/users/:id", EnsureAdmin(service.UserID))
router.DELETE("/chronograf/v1/users/:id", EnsureAdmin(service.RemoveUser))
router.PATCH("/chronograf/v1/users/:id", EnsureAdmin(service.UpdateUser))
// Dashboards
router.GET("/chronograf/v1/dashboards", service.Dashboards)
router.POST("/chronograf/v1/dashboards", service.NewDashboard)
router.GET("/chronograf/v1/dashboards", EnsureViewer(service.Dashboards))
router.POST("/chronograf/v1/dashboards", EnsureEditor(service.NewDashboard))
router.GET("/chronograf/v1/dashboards/:id", service.DashboardID)
router.DELETE("/chronograf/v1/dashboards/:id", service.RemoveDashboard)
router.PUT("/chronograf/v1/dashboards/:id", service.ReplaceDashboard)
router.PATCH("/chronograf/v1/dashboards/:id", service.UpdateDashboard)
router.GET("/chronograf/v1/dashboards/:id", EnsureViewer(service.DashboardID))
router.DELETE("/chronograf/v1/dashboards/:id", EnsureEditor(service.RemoveDashboard))
router.PUT("/chronograf/v1/dashboards/:id", EnsureEditor(service.ReplaceDashboard))
router.PATCH("/chronograf/v1/dashboards/:id", EnsureEditor(service.UpdateDashboard))
// Dashboard Cells
router.GET("/chronograf/v1/dashboards/:id/cells", service.DashboardCells)
router.POST("/chronograf/v1/dashboards/:id/cells", service.NewDashboardCell)
router.GET("/chronograf/v1/dashboards/:id/cells", EnsureViewer(service.DashboardCells))
router.POST("/chronograf/v1/dashboards/:id/cells", EnsureEditor(service.NewDashboardCell))
router.GET("/chronograf/v1/dashboards/:id/cells/:cid", service.DashboardCellID)
router.DELETE("/chronograf/v1/dashboards/:id/cells/:cid", service.RemoveDashboardCell)
router.PUT("/chronograf/v1/dashboards/:id/cells/:cid", service.ReplaceDashboardCell)
router.GET("/chronograf/v1/dashboards/:id/cells/:cid", EnsureViewer(service.DashboardCellID))
router.DELETE("/chronograf/v1/dashboards/:id/cells/:cid", EnsureEditor(service.RemoveDashboardCell))
router.PUT("/chronograf/v1/dashboards/:id/cells/:cid", EnsureEditor(service.ReplaceDashboardCell))
// Dashboard Templates
router.GET("/chronograf/v1/dashboards/:id/templates", service.Templates)
router.POST("/chronograf/v1/dashboards/:id/templates", service.NewTemplate)
router.GET("/chronograf/v1/dashboards/:id/templates", EnsureViewer(service.Templates))
router.POST("/chronograf/v1/dashboards/:id/templates", EnsureEditor(service.NewTemplate))
router.GET("/chronograf/v1/dashboards/:id/templates/:tid", service.TemplateID)
router.DELETE("/chronograf/v1/dashboards/:id/templates/:tid", service.RemoveTemplate)
router.PUT("/chronograf/v1/dashboards/:id/templates/:tid", service.ReplaceTemplate)
router.GET("/chronograf/v1/dashboards/:id/templates/:tid", EnsureViewer(service.TemplateID))
router.DELETE("/chronograf/v1/dashboards/:id/templates/:tid", EnsureEditor(service.RemoveTemplate))
router.PUT("/chronograf/v1/dashboards/:id/templates/:tid", EnsureEditor(service.ReplaceTemplate))
// Databases
router.GET("/chronograf/v1/sources/:id/dbs", service.GetDatabases)
router.POST("/chronograf/v1/sources/:id/dbs", service.NewDatabase)
router.GET("/chronograf/v1/sources/:id/dbs", EnsureViewer(service.GetDatabases))
router.POST("/chronograf/v1/sources/:id/dbs", EnsureEditor(service.NewDatabase))
router.DELETE("/chronograf/v1/sources/:id/dbs/:dbid", service.DropDatabase)
router.DELETE("/chronograf/v1/sources/:id/dbs/:dbid", EnsureEditor(service.DropDatabase))
// Retention Policies
router.GET("/chronograf/v1/sources/:id/dbs/:dbid/rps", service.RetentionPolicies)
router.POST("/chronograf/v1/sources/:id/dbs/:dbid/rps", service.NewRetentionPolicy)
router.GET("/chronograf/v1/sources/:id/dbs/:dbid/rps", EnsureViewer(service.RetentionPolicies))
router.POST("/chronograf/v1/sources/:id/dbs/:dbid/rps", EnsureEditor(service.NewRetentionPolicy))
router.PUT("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", service.UpdateRetentionPolicy)
router.DELETE("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", service.DropRetentionPolicy)
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))
allRoutes := &AllRoutes{
Logger: opts.Logger,

262
server/organizations.go Normal file
View File

@ -0,0 +1,262 @@
package server
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/organizations"
"github.com/influxdata/chronograf/roles"
)
func parseOrganizationID(id string) (uint64, error) {
return strconv.ParseUint(id, 10, 64)
}
type organizationRequest struct {
Name string `json:"name"`
DefaultRole string `json:"defaultRole"`
Public *bool `json:"public"`
}
func (r *organizationRequest) ValidCreate() error {
if r.Name == "" {
return fmt.Errorf("Name required on Chronograf Organization request body")
}
return r.ValidDefaultRole()
}
func (r *organizationRequest) ValidUpdate() error {
if r.Name == "" && r.DefaultRole == "" && r.Public == nil {
return fmt.Errorf("No fields to update")
}
if r.DefaultRole != "" {
return r.ValidDefaultRole()
}
return nil
}
func (r *organizationRequest) ValidDefaultRole() error {
if r.DefaultRole == "" {
r.DefaultRole = roles.MemberRoleName
}
switch r.DefaultRole {
case roles.MemberRoleName, roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName:
return nil
default:
return fmt.Errorf("default role must be member, viewer, editor, or admin")
}
}
type organizationResponse struct {
Links selfLinks `json:"links"`
chronograf.Organization
}
func newOrganizationResponse(o *chronograf.Organization) *organizationResponse {
if o == nil {
o = &chronograf.Organization{}
}
return &organizationResponse{
Organization: *o,
Links: selfLinks{
Self: fmt.Sprintf("/chronograf/v1/organizations/%d", o.ID),
},
}
}
type organizationsResponse struct {
Links selfLinks `json:"links"`
Organizations []*organizationResponse `json:"organizations"`
}
func newOrganizationsResponse(orgs []chronograf.Organization) *organizationsResponse {
orgsResp := make([]*organizationResponse, len(orgs))
for i, org := range orgs {
orgsResp[i] = newOrganizationResponse(&org)
}
return &organizationsResponse{
Organizations: orgsResp,
Links: selfLinks{
Self: "/chronograf/v1/organizations",
},
}
}
// Organizations retrieves all organizations from store
func (s *Service) Organizations(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
orgs, err := s.Store.Organizations(ctx).All(ctx)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
res := newOrganizationsResponse(orgs)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// NewOrganization adds a new organization to store
func (s *Service) NewOrganization(w http.ResponseWriter, r *http.Request) {
var req organizationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
return
}
if err := req.ValidCreate(); err != nil {
invalidData(w, err, s.Logger)
return
}
ctx := r.Context()
org := &chronograf.Organization{
Name: req.Name,
DefaultRole: req.DefaultRole,
}
if req.Public != nil {
org.Public = *req.Public
}
res, err := s.Store.Organizations(ctx).Add(ctx, org)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
// Now that the organization was created, add the user
// making the request to the organization
user, ok := hasUserContext(ctx)
if !ok {
// Best attempt at cleanup the organization if there were any errors
_ = s.Store.Organizations(ctx).Delete(ctx, res)
Error(w, http.StatusInternalServerError, "failed to retrieve user from context", s.Logger)
return
}
orgID := fmt.Sprintf("%d", res.ID)
user.Roles = []chronograf.Role{
{
Organization: orgID,
Name: roles.AdminRoleName,
},
}
orgCtx := context.WithValue(ctx, organizations.ContextKey, orgID)
_, err = s.Store.Users(orgCtx).Add(orgCtx, user)
if err != nil {
// Best attempt at cleanup the organization if there were any errors adding user to org
_ = s.Store.Organizations(ctx).Delete(ctx, res)
s.Logger.Error("failed to add user to organization", err.Error())
Error(w, http.StatusInternalServerError, "failed to add user to organization", s.Logger)
return
}
co := newOrganizationResponse(res)
location(w, co.Links.Self)
encodeJSON(w, http.StatusCreated, co, s.Logger)
}
// OrganizationID retrieves a organization with ID from store
func (s *Service) OrganizationID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
idStr := httprouter.GetParamFromContext(ctx, "id")
id, err := parseOrganizationID(idStr)
if err != nil {
Error(w, http.StatusBadRequest, fmt.Sprintf("invalid organization id: %s", err.Error()), s.Logger)
return
}
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
res := newOrganizationResponse(org)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// UpdateOrganization updates an organization in the organizations store
func (s *Service) UpdateOrganization(w http.ResponseWriter, r *http.Request) {
var req organizationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
return
}
if err := req.ValidUpdate(); err != nil {
invalidData(w, err, s.Logger)
return
}
ctx := r.Context()
idStr := httprouter.GetParamFromContext(ctx, "id")
id, err := parseOrganizationID(idStr)
if err != nil {
Error(w, http.StatusBadRequest, fmt.Sprintf("invalid organization id: %s", err.Error()), s.Logger)
return
}
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
if req.Name != "" {
org.Name = req.Name
}
if req.DefaultRole != "" {
org.DefaultRole = req.DefaultRole
}
if req.Public != nil {
org.Public = *req.Public
}
err = s.Store.Organizations(ctx).Update(ctx, org)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
res := newOrganizationResponse(org)
location(w, res.Links.Self)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// RemoveOrganization removes an organization in the organizations store
func (s *Service) RemoveOrganization(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
idStr := httprouter.GetParamFromContext(ctx, "id")
id, err := parseOrganizationID(idStr)
if err != nil {
Error(w, http.StatusBadRequest, fmt.Sprintf("invalid organization id: %s", err.Error()), s.Logger)
return
}
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
if err != nil {
Error(w, http.StatusNotFound, err.Error(), s.Logger)
return
}
if err := s.Store.Organizations(ctx).Delete(ctx, org); err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}

View File

@ -0,0 +1,705 @@
package server
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/log"
"github.com/influxdata/chronograf/mocks"
"github.com/influxdata/chronograf/roles"
)
func TestService_OrganizationID(t *testing.T) {
type fields struct {
OrganizationsStore chronograf.OrganizationsStore
Logger chronograf.Logger
}
type args struct {
w *httptest.ResponseRecorder
r *http.Request
}
tests := []struct {
name string
fields fields
args args
id string
wantStatus int
wantContentType string
wantBody string
}{
{
name: "Get Single Organization",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://any.url", // can be any valid URL as we are bypassing mux
nil,
),
},
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
switch *q.ID {
case 1337:
return &chronograf.Organization{
ID: 1337,
Name: "The Good Place",
Public: false,
}, nil
default:
return nil, fmt.Errorf("Organization with ID %s not found", *q.ID)
}
},
},
},
id: "1337",
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place","public":false}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
Store: &mocks.Store{
OrganizationsStore: tt.fields.OrganizationsStore,
},
Logger: tt.fields.Logger,
}
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
context.Background(),
httprouter.Params{
{
Key: "id",
Value: tt.id,
},
}))
s.OrganizationID(tt.args.w, tt.args.r)
resp := tt.args.w.Result()
content := resp.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != tt.wantStatus {
t.Errorf("%q. OrganizationID() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
}
if tt.wantContentType != "" && content != tt.wantContentType {
t.Errorf("%q. OrganizationID() = %v, want %v", tt.name, content, tt.wantContentType)
}
if eq, _ := jsonEqual(string(body), tt.wantBody); tt.wantBody != "" && !eq {
t.Errorf("%q. OrganizationID() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
}
})
}
}
func TestService_Organizations(t *testing.T) {
type fields struct {
OrganizationsStore chronograf.OrganizationsStore
Logger chronograf.Logger
}
type args struct {
w *httptest.ResponseRecorder
r *http.Request
}
tests := []struct {
name string
fields fields
args args
wantStatus int
wantContentType string
wantBody string
}{
{
name: "Get Single Organization",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://any.url", // can be any valid URL as we are bypassing mux
nil,
),
},
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
AllF: func(ctx context.Context) ([]chronograf.Organization, error) {
return []chronograf.Organization{
chronograf.Organization{
ID: 1337,
Name: "The Good Place",
Public: false,
},
chronograf.Organization{
ID: 100,
Name: "The Bad Place",
Public: false,
},
}, nil
},
},
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"links":{"self":"/chronograf/v1/organizations"},"organizations":[{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place","public":false},{"links":{"self":"/chronograf/v1/organizations/100"},"id":"100","name":"The Bad Place","public":false}]}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
Store: &mocks.Store{
OrganizationsStore: tt.fields.OrganizationsStore,
},
Logger: tt.fields.Logger,
}
s.Organizations(tt.args.w, tt.args.r)
resp := tt.args.w.Result()
content := resp.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != tt.wantStatus {
t.Errorf("%q. Organizations() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
}
if tt.wantContentType != "" && content != tt.wantContentType {
t.Errorf("%q. Organizations() = %v, want %v", tt.name, content, tt.wantContentType)
}
if eq, _ := jsonEqual(string(body), tt.wantBody); tt.wantBody != "" && !eq {
t.Errorf("%q. Organizations() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
}
})
}
}
func TestService_UpdateOrganization(t *testing.T) {
type fields struct {
OrganizationsStore chronograf.OrganizationsStore
Logger chronograf.Logger
}
type args struct {
w *httptest.ResponseRecorder
r *http.Request
org *organizationRequest
public bool
setPtr bool
}
tests := []struct {
name string
fields fields
args args
id string
wantStatus int
wantContentType string
wantBody string
}{
{
name: "Update Organization name",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://any.url", // can be any valid URL as we are bypassing mux
nil,
),
org: &organizationRequest{
Name: "The Bad Place",
},
},
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
UpdateF: func(ctx context.Context, o *chronograf.Organization) error {
return nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 1337,
Name: "The Good Place",
DefaultRole: roles.ViewerRoleName,
Public: false,
}, nil
},
},
},
id: "1337",
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"id":"1337","name":"The Bad Place","defaultRole":"viewer","links":{"self":"/chronograf/v1/organizations/1337"},"public":false}`,
},
{
name: "Update Organization public",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://any.url", // can be any valid URL as we are bypassing mux
nil,
),
org: &organizationRequest{},
public: false,
setPtr: true,
},
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
UpdateF: func(ctx context.Context, o *chronograf.Organization) error {
return nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 0,
Name: "The Good Place",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, nil
},
},
},
id: "0",
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"id":"0","name":"The Good Place","defaultRole":"viewer","public":false,"links":{"self":"/chronograf/v1/organizations/0"}}`,
},
{
name: "Update Organization - nothing to update",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://any.url", // can be any valid URL as we are bypassing mux
nil,
),
org: &organizationRequest{},
},
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
UpdateF: func(ctx context.Context, o *chronograf.Organization) error {
return nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 1337,
Name: "The Good Place",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, nil
},
},
},
id: "1337",
wantStatus: http.StatusUnprocessableEntity,
wantContentType: "application/json",
wantBody: `{"code":422,"message":"No fields to update"}`,
},
{
name: "Update Organization default role",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://any.url", // can be any valid URL as we are bypassing mux
nil,
),
org: &organizationRequest{
DefaultRole: roles.ViewerRoleName,
},
},
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
UpdateF: func(ctx context.Context, o *chronograf.Organization) error {
return nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 1337,
Name: "The Good Place",
DefaultRole: roles.MemberRoleName,
Public: false,
}, nil
},
},
},
id: "1337",
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place","defaultRole":"viewer","public":false}`,
},
{
name: "Update Organization - invalid update",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://any.url", // can be any valid URL as we are bypassing mux
nil,
),
org: &organizationRequest{},
},
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
UpdateF: func(ctx context.Context, o *chronograf.Organization) error {
return nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return nil, nil
},
},
},
id: "1337",
wantStatus: http.StatusUnprocessableEntity,
wantContentType: "application/json",
wantBody: `{"code":422,"message":"No fields to update"}`,
},
{
name: "Update Organization - invalid role",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://any.url", // can be any valid URL as we are bypassing mux
nil,
),
org: &organizationRequest{
DefaultRole: "sillyrole",
},
},
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
UpdateF: func(ctx context.Context, o *chronograf.Organization) error {
return nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return nil, nil
},
},
},
id: "1337",
wantStatus: http.StatusUnprocessableEntity,
wantContentType: "application/json",
wantBody: `{"code":422,"message":"default role must be member, viewer, editor, or admin"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
Store: &mocks.Store{
OrganizationsStore: tt.fields.OrganizationsStore,
},
Logger: tt.fields.Logger,
}
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(context.Background(),
httprouter.Params{
{
Key: "id",
Value: tt.id,
},
}))
if tt.args.setPtr {
tt.args.org.Public = &tt.args.public
}
buf, _ := json.Marshal(tt.args.org)
tt.args.r.Body = ioutil.NopCloser(bytes.NewReader(buf))
s.UpdateOrganization(tt.args.w, tt.args.r)
resp := tt.args.w.Result()
content := resp.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != tt.wantStatus {
t.Errorf("%q. NewOrganization() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
}
if tt.wantContentType != "" && content != tt.wantContentType {
t.Errorf("%q. NewOrganization() = %v, want %v", tt.name, content, tt.wantContentType)
}
if eq, _ := jsonEqual(string(body), tt.wantBody); tt.wantBody != "" && !eq {
t.Errorf("%q. NewOrganization() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
}
})
}
}
func TestService_RemoveOrganization(t *testing.T) {
type fields struct {
OrganizationsStore chronograf.OrganizationsStore
Logger chronograf.Logger
}
type args struct {
w *httptest.ResponseRecorder
r *http.Request
}
tests := []struct {
name string
fields fields
args args
id string
wantStatus int
}{
{
name: "Update Organization name",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://any.url", // can be any valid URL as we are bypassing mux
nil,
),
},
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
DeleteF: func(ctx context.Context, o *chronograf.Organization) error {
return nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
switch *q.ID {
case 1337:
return &chronograf.Organization{
ID: 1337,
Name: "The Good Place",
}, nil
default:
return nil, fmt.Errorf("Organization with ID %s not found", *q.ID)
}
},
},
},
id: "1337",
wantStatus: http.StatusNoContent,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
Store: &mocks.Store{
OrganizationsStore: tt.fields.OrganizationsStore,
},
Logger: tt.fields.Logger,
}
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(context.Background(),
httprouter.Params{
{
Key: "id",
Value: tt.id,
},
}))
s.RemoveOrganization(tt.args.w, tt.args.r)
resp := tt.args.w.Result()
if resp.StatusCode != tt.wantStatus {
t.Errorf("%q. NewOrganization() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
}
})
}
}
func TestService_NewOrganization(t *testing.T) {
type fields struct {
OrganizationsStore chronograf.OrganizationsStore
UsersStore chronograf.UsersStore
Logger chronograf.Logger
}
type args struct {
w *httptest.ResponseRecorder
r *http.Request
org *organizationRequest
user *chronograf.User
}
tests := []struct {
name string
fields fields
args args
id string
wantStatus int
wantContentType string
wantBody string
}{
{
name: "Create Organization",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://any.url", // can be any valid URL as we are bypassing mux
nil,
),
user: &chronograf.User{
ID: 1,
Name: "bobetta",
Provider: "github",
Scheme: "oauth2",
},
org: &organizationRequest{
Name: "The Good Place",
},
},
fields: fields{
Logger: log.New(log.DebugLevel),
UsersStore: &mocks.UsersStore{
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return &chronograf.User{
ID: 1,
Name: "bobetta",
Provider: "github",
Scheme: "oauth2",
}, nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
AddF: func(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 1337,
Name: "The Good Place",
Public: false,
}, nil
},
},
},
wantStatus: http.StatusCreated,
wantContentType: "application/json",
wantBody: `{"id":"1337","public":false,"name":"The Good Place","links":{"self":"/chronograf/v1/organizations/1337"}}`,
},
{
name: "Create Organization - no user on context",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://any.url", // can be any valid URL as we are bypassing mux
nil,
),
org: &organizationRequest{
Name: "The Good Place",
},
},
fields: fields{
Logger: log.New(log.DebugLevel),
UsersStore: &mocks.UsersStore{
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return &chronograf.User{
ID: 1,
Name: "bobetta",
Provider: "github",
Scheme: "oauth2",
}, nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
AddF: func(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 1337,
Name: "The Good Place",
}, nil
},
DeleteF: func(ctx context.Context, o *chronograf.Organization) error {
return nil
},
},
},
wantStatus: http.StatusInternalServerError,
wantContentType: "application/json",
wantBody: `{"code":500,"message":"failed to retrieve user from context"}`,
},
{
name: "Create Organization - failed to add user to organization",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://any.url", // can be any valid URL as we are bypassing mux
nil,
),
org: &organizationRequest{
Name: "The Good Place",
},
user: &chronograf.User{
ID: 1,
Name: "bobetta",
Provider: "github",
Scheme: "oauth2",
},
},
fields: fields{
Logger: log.New(log.DebugLevel),
UsersStore: &mocks.UsersStore{
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return nil, fmt.Errorf("failed to add user to org")
},
},
OrganizationsStore: &mocks.OrganizationsStore{
AddF: func(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 1337,
Name: "The Good Place",
}, nil
},
DeleteF: func(ctx context.Context, o *chronograf.Organization) error {
return nil
},
},
},
wantStatus: http.StatusInternalServerError,
wantContentType: "application/json",
wantBody: `{"code":500,"message":"failed to add user to organization"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
Store: &mocks.Store{
OrganizationsStore: tt.fields.OrganizationsStore,
UsersStore: tt.fields.UsersStore,
},
Logger: tt.fields.Logger,
}
ctx := tt.args.r.Context()
ctx = context.WithValue(ctx, UserContextKey, tt.args.user)
tt.args.r = tt.args.r.WithContext(ctx)
buf, _ := json.Marshal(tt.args.org)
tt.args.r.Body = ioutil.NopCloser(bytes.NewReader(buf))
s.NewOrganization(tt.args.w, tt.args.r)
resp := tt.args.w.Result()
content := resp.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != tt.wantStatus {
t.Errorf("%q. NewOrganization() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
}
if tt.wantContentType != "" && content != tt.wantContentType {
t.Errorf("%q. NewOrganization() = %v, want %v", tt.name, content, tt.wantContentType)
}
if eq, _ := jsonEqual(string(body), tt.wantBody); tt.wantBody != "" && !eq {
t.Errorf("%q. NewOrganization() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
}
})
}
}

View File

@ -8,36 +8,36 @@ import (
)
// Permissions returns all possible permissions for this source.
func (h *Service) Permissions(w http.ResponseWriter, r *http.Request) {
func (s *Service) Permissions(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
src, err := h.SourcesStore.Get(ctx, srcID)
src, err := s.Store.Sources(ctx).Get(ctx, srcID)
if err != nil {
notFound(w, srcID, h.Logger)
notFound(w, srcID, s.Logger)
return
}
ts, err := h.TimeSeries(src)
ts, err := s.TimeSeries(src)
if err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
Error(w, http.StatusBadRequest, msg, h.Logger)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
if err = ts.Connect(ctx, &src); err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
Error(w, http.StatusBadRequest, msg, h.Logger)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
perms := ts.Permissions(ctx)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
httpAPISrcs := "/chronograf/v1/sources"
@ -51,7 +51,7 @@ func (h *Service) Permissions(w http.ResponseWriter, r *http.Request) {
"source": fmt.Sprintf("%s/%d", httpAPISrcs, srcID),
},
}
encodeJSON(w, http.StatusOK, res, h.Logger)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
func validPermissions(perms *chronograf.Permissions) error {

View File

@ -89,7 +89,9 @@ func TestService_Permissions(t *testing.T) {
},
}))
h := &Service{
SourcesStore: tt.fields.SourcesStore,
Store: &mocks.Store{
SourcesStore: tt.fields.SourcesStore,
},
TimeSeriesClient: tt.fields.TimeSeries,
Logger: tt.fields.Logger,
UseAuth: tt.fields.UseAuth,

View File

@ -10,29 +10,29 @@ import (
)
// KapacitorProxy proxies requests to kapacitor using the path query parameter.
func (h *Service) KapacitorProxy(w http.ResponseWriter, r *http.Request) {
func (s *Service) KapacitorProxy(w http.ResponseWriter, r *http.Request) {
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
path := r.URL.Query().Get("path")
if path == "" {
Error(w, http.StatusUnprocessableEntity, "path query parameter required", h.Logger)
Error(w, http.StatusUnprocessableEntity, "path query parameter required", s.Logger)
return
}
ctx := r.Context()
srv, err := h.ServersStore.Get(ctx, id)
srv, err := s.Store.Servers(ctx).Get(ctx, id)
if err != nil || srv.SrcID != srcID {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
@ -42,7 +42,7 @@ func (h *Service) KapacitorProxy(w http.ResponseWriter, r *http.Request) {
u, err := url.Parse(uri)
if err != nil {
msg := fmt.Sprintf("Error parsing kapacitor url: %v", err)
Error(w, http.StatusUnprocessableEntity, msg, h.Logger)
Error(w, http.StatusUnprocessableEntity, msg, s.Logger)
return
}
@ -68,23 +68,23 @@ func (h *Service) KapacitorProxy(w http.ResponseWriter, r *http.Request) {
}
// KapacitorProxyPost proxies POST to kapacitor
func (h *Service) KapacitorProxyPost(w http.ResponseWriter, r *http.Request) {
h.KapacitorProxy(w, r)
func (s *Service) KapacitorProxyPost(w http.ResponseWriter, r *http.Request) {
s.KapacitorProxy(w, r)
}
// KapacitorProxyPatch proxies PATCH to kapacitor
func (h *Service) KapacitorProxyPatch(w http.ResponseWriter, r *http.Request) {
h.KapacitorProxy(w, r)
func (s *Service) KapacitorProxyPatch(w http.ResponseWriter, r *http.Request) {
s.KapacitorProxy(w, r)
}
// KapacitorProxyGet proxies GET to kapacitor
func (h *Service) KapacitorProxyGet(w http.ResponseWriter, r *http.Request) {
h.KapacitorProxy(w, r)
func (s *Service) KapacitorProxyGet(w http.ResponseWriter, r *http.Request) {
s.KapacitorProxy(w, r)
}
// KapacitorProxyDelete proxies DELETE to kapacitor
func (h *Service) KapacitorProxyDelete(w http.ResponseWriter, r *http.Request) {
h.KapacitorProxy(w, r)
func (s *Service) KapacitorProxyDelete(w http.ResponseWriter, r *http.Request) {
s.KapacitorProxy(w, r)
}
func singleJoiningSlash(a, b string) string {

View File

@ -51,7 +51,7 @@ func (s *Service) Queries(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
src, err := s.SourcesStore.Get(ctx, srcID)
src, err := s.Store.Sources(ctx).Get(ctx, srcID)
if err != nil {
notFound(w, srcID, s.Logger)
return

View File

@ -181,8 +181,10 @@ func TestService_Queries(t *testing.T) {
},
}))
s := &Service{
SourcesStore: tt.SourcesStore,
Logger: &mocks.TestLogger{},
Store: &mocks.Store{
SourcesStore: tt.SourcesStore,
},
Logger: &mocks.TestLogger{},
}
s.Queries(tt.w, tt.r)
got := tt.w.Body.String()

View File

@ -1,224 +0,0 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf"
)
// NewRole adds role to source
func (h *Service) NewRole(w http.ResponseWriter, r *http.Request) {
var req sourceRoleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
return
}
if err := req.ValidCreate(); err != nil {
invalidData(w, err, h.Logger)
return
}
ctx := r.Context()
srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
return
}
roles, ok := h.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
return
}
if _, err := roles.Get(ctx, req.Name); err == nil {
Error(w, http.StatusBadRequest, fmt.Sprintf("Source %d already has role %s", srcID, req.Name), h.Logger)
return
}
res, err := roles.Add(ctx, &req.Role)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
rr := newRoleResponse(srcID, res)
w.Header().Add("Location", rr.Links.Self)
encodeJSON(w, http.StatusCreated, rr, h.Logger)
}
// UpdateRole changes the permissions or users of a role
func (h *Service) UpdateRole(w http.ResponseWriter, r *http.Request) {
var req sourceRoleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
return
}
if err := req.ValidUpdate(); err != nil {
invalidData(w, err, h.Logger)
return
}
ctx := r.Context()
srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
return
}
roles, ok := h.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
return
}
rid := httprouter.GetParamFromContext(ctx, "rid")
req.Name = rid
if err := roles.Update(ctx, &req.Role); err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
role, err := roles.Get(ctx, req.Name)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
rr := newRoleResponse(srcID, role)
w.Header().Add("Location", rr.Links.Self)
encodeJSON(w, http.StatusOK, rr, h.Logger)
}
// RoleID retrieves a role with ID from store.
func (h *Service) RoleID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
return
}
roles, ok := h.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
return
}
rid := httprouter.GetParamFromContext(ctx, "rid")
role, err := roles.Get(ctx, rid)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
rr := newRoleResponse(srcID, role)
encodeJSON(w, http.StatusOK, rr, h.Logger)
}
// Roles retrieves all roles from the store
func (h *Service) Roles(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
return
}
store, ok := h.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
return
}
roles, err := store.All(ctx)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
rr := make([]roleResponse, len(roles))
for i, role := range roles {
rr[i] = newRoleResponse(srcID, &role)
}
res := struct {
Roles []roleResponse `json:"roles"`
}{rr}
encodeJSON(w, http.StatusOK, res, h.Logger)
}
// RemoveRole removes role from data source.
func (h *Service) RemoveRole(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
return
}
roles, ok := h.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
return
}
rid := httprouter.GetParamFromContext(ctx, "rid")
if err := roles.Delete(ctx, &chronograf.Role{Name: rid}); err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}
// sourceRoleRequest is the format used for both creating and updating roles
type sourceRoleRequest struct {
chronograf.Role
}
func (r *sourceRoleRequest) ValidCreate() error {
if r.Name == "" || len(r.Name) > 254 {
return fmt.Errorf("Name is required for a role")
}
for _, user := range r.Users {
if user.Name == "" {
return fmt.Errorf("Username required")
}
}
return validPermissions(&r.Permissions)
}
func (r *sourceRoleRequest) ValidUpdate() error {
if len(r.Name) > 254 {
return fmt.Errorf("Username too long; must be less than 254 characters")
}
for _, user := range r.Users {
if user.Name == "" {
return fmt.Errorf("Username required")
}
}
return validPermissions(&r.Permissions)
}
type roleResponse struct {
Users []*userResponse `json:"users"`
Name string `json:"name"`
Permissions chronograf.Permissions `json:"permissions"`
Links selfLinks `json:"links"`
}
func newRoleResponse(srcID int, res *chronograf.Role) roleResponse {
su := make([]*userResponse, len(res.Users))
for i := range res.Users {
name := res.Users[i].Name
su[i] = newUserResponse(srcID, name)
}
if res.Permissions == nil {
res.Permissions = make(chronograf.Permissions, 0)
}
return roleResponse{
Name: res.Name,
Permissions: res.Permissions,
Users: su,
Links: newSelfLinks(srcID, "roles", res.Name),
}
}

View File

@ -1,697 +0,0 @@
package server
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/log"
"github.com/influxdata/chronograf/mocks"
)
func TestService_NewSourceRole(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
TimeSeries TimeSeriesClient
Logger chronograf.Logger
}
type args struct {
w *httptest.ResponseRecorder
r *http.Request
}
tests := []struct {
name string
fields fields
args args
ID string
wantStatus int
wantContentType string
wantBody string
}{
{
name: "Bad JSON",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"POST",
"http://server.local/chronograf/v1/sources/1/roles",
ioutil.NopCloser(
bytes.NewReader([]byte(`{BAD}`)))),
},
fields: fields{
Logger: log.New(log.DebugLevel),
},
wantStatus: http.StatusBadRequest,
wantContentType: "application/json",
wantBody: `{"code":400,"message":"Unparsable JSON"}`,
},
{
name: "Invalid request",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"POST",
"http://server.local/chronograf/v1/sources/1/roles",
ioutil.NopCloser(
bytes.NewReader([]byte(`{"name": ""}`)))),
},
fields: fields{
Logger: log.New(log.DebugLevel),
},
ID: "1",
wantStatus: http.StatusUnprocessableEntity,
wantContentType: "application/json",
wantBody: `{"code":422,"message":"Name is required for a role"}`,
},
{
name: "Invalid source ID",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"POST",
"http://server.local/chronograf/v1/sources/1/roles",
ioutil.NopCloser(
bytes.NewReader([]byte(`{"name": "newrole"}`)))),
},
fields: fields{
Logger: log.New(log.DebugLevel),
},
ID: "BADROLE",
wantStatus: http.StatusUnprocessableEntity,
wantContentType: "application/json",
wantBody: `{"code":422,"message":"Error converting ID BADROLE"}`,
},
{
name: "Source doesn't support roles",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"POST",
"http://server.local/chronograf/v1/sources/1/roles",
ioutil.NopCloser(
bytes.NewReader([]byte(`{"name": "role"}`)))),
},
fields: fields{
Logger: log.New(log.DebugLevel),
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "muh source",
Username: "name",
Password: "hunter2",
URL: "http://localhost:8086",
}, nil
},
},
TimeSeries: &mocks.TimeSeries{
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
return nil
},
RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
return nil, fmt.Errorf("roles not supported")
},
},
},
ID: "1",
wantStatus: http.StatusNotFound,
wantContentType: "application/json",
wantBody: `{"code":404,"message":"Source 1 does not have role capability"}`,
},
{
name: "Unable to add role to server",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"POST",
"http://server.local/chronograf/v1/sources/1/roles",
ioutil.NopCloser(
bytes.NewReader([]byte(`{"name": "role"}`)))),
},
fields: fields{
Logger: log.New(log.DebugLevel),
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "muh source",
Username: "name",
Password: "hunter2",
URL: "http://localhost:8086",
}, nil
},
},
TimeSeries: &mocks.TimeSeries{
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
return nil
},
RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
return &mocks.RolesStore{
AddF: func(ctx context.Context, u *chronograf.Role) (*chronograf.Role, error) {
return nil, fmt.Errorf("server had and issue")
},
GetF: func(ctx context.Context, name string) (*chronograf.Role, error) {
return nil, fmt.Errorf("No such role")
},
}, nil
},
},
},
ID: "1",
wantStatus: http.StatusBadRequest,
wantContentType: "application/json",
wantBody: `{"code":400,"message":"server had and issue"}`,
},
{
name: "New role for data source",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"POST",
"http://server.local/chronograf/v1/sources/1/roles",
ioutil.NopCloser(
bytes.NewReader([]byte(`{"name": "biffsgang","users": [{"name": "match"},{"name": "skinhead"},{"name": "3-d"}]}`)))),
},
fields: fields{
Logger: log.New(log.DebugLevel),
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "muh source",
Username: "name",
Password: "hunter2",
URL: "http://localhost:8086",
}, nil
},
},
TimeSeries: &mocks.TimeSeries{
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
return nil
},
RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
return &mocks.RolesStore{
AddF: func(ctx context.Context, u *chronograf.Role) (*chronograf.Role, error) {
return u, nil
},
GetF: func(ctx context.Context, name string) (*chronograf.Role, error) {
return nil, fmt.Errorf("no such role")
},
}, nil
},
},
},
ID: "1",
wantStatus: http.StatusCreated,
wantContentType: "application/json",
wantBody: `{"users":[{"links":{"self":"/chronograf/v1/sources/1/users/match"},"name":"match"},{"links":{"self":"/chronograf/v1/sources/1/users/skinhead"},"name":"skinhead"},{"links":{"self":"/chronograf/v1/sources/1/users/3-d"},"name":"3-d"}],"name":"biffsgang","permissions":[],"links":{"self":"/chronograf/v1/sources/1/roles/biffsgang"}}
`,
},
}
for _, tt := range tests {
h := &Service{
SourcesStore: tt.fields.SourcesStore,
TimeSeriesClient: tt.fields.TimeSeries,
Logger: tt.fields.Logger,
}
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
context.Background(),
httprouter.Params{
{
Key: "id",
Value: tt.ID,
},
}))
h.NewRole(tt.args.w, tt.args.r)
resp := tt.args.w.Result()
content := resp.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != tt.wantStatus {
t.Errorf("%q. NewRole() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
}
if tt.wantContentType != "" && content != tt.wantContentType {
t.Errorf("%q. NewRole() = %v, want %v", tt.name, content, tt.wantContentType)
}
if tt.wantBody != "" && string(body) != tt.wantBody {
t.Errorf("%q. NewRole() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
}
}
}
func TestService_UpdateRole(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
TimeSeries TimeSeriesClient
Logger chronograf.Logger
}
type args struct {
w *httptest.ResponseRecorder
r *http.Request
}
tests := []struct {
name string
fields fields
args args
ID string
RoleID string
wantStatus int
wantContentType string
wantBody string
}{
{
name: "Update role for data source",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"POST",
"http://server.local/chronograf/v1/sources/1/roles",
ioutil.NopCloser(
bytes.NewReader([]byte(`{"name": "biffsgang","users": [{"name": "match"},{"name": "skinhead"},{"name": "3-d"}]}`)))),
},
fields: fields{
Logger: log.New(log.DebugLevel),
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "muh source",
Username: "name",
Password: "hunter2",
URL: "http://localhost:8086",
}, nil
},
},
TimeSeries: &mocks.TimeSeries{
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
return nil
},
RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
return &mocks.RolesStore{
UpdateF: func(ctx context.Context, u *chronograf.Role) error {
return nil
},
GetF: func(ctx context.Context, name string) (*chronograf.Role, error) {
return &chronograf.Role{
Name: "biffsgang",
Users: []chronograf.User{
{
Name: "match",
},
{
Name: "skinhead",
},
{
Name: "3-d",
},
},
}, nil
},
}, nil
},
},
},
ID: "1",
RoleID: "biffsgang",
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"users":[{"links":{"self":"/chronograf/v1/sources/1/users/match"},"name":"match"},{"links":{"self":"/chronograf/v1/sources/1/users/skinhead"},"name":"skinhead"},{"links":{"self":"/chronograf/v1/sources/1/users/3-d"},"name":"3-d"}],"name":"biffsgang","permissions":[],"links":{"self":"/chronograf/v1/sources/1/roles/biffsgang"}}
`,
},
}
for _, tt := range tests {
h := &Service{
SourcesStore: tt.fields.SourcesStore,
TimeSeriesClient: tt.fields.TimeSeries,
Logger: tt.fields.Logger,
}
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
context.Background(),
httprouter.Params{
{
Key: "id",
Value: tt.ID,
},
{
Key: "rid",
Value: tt.RoleID,
},
}))
h.UpdateRole(tt.args.w, tt.args.r)
resp := tt.args.w.Result()
content := resp.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != tt.wantStatus {
t.Errorf("%q. NewRole() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
}
if tt.wantContentType != "" && content != tt.wantContentType {
t.Errorf("%q. NewRole() = %v, want %v", tt.name, content, tt.wantContentType)
}
if tt.wantBody != "" && string(body) != tt.wantBody {
t.Errorf("%q. NewRole() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
}
}
}
func TestService_RoleID(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
TimeSeries TimeSeriesClient
Logger chronograf.Logger
}
type args struct {
w *httptest.ResponseRecorder
r *http.Request
}
tests := []struct {
name string
fields fields
args args
ID string
RoleID string
wantStatus int
wantContentType string
wantBody string
}{
{
name: "Get role for data source",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://server.local/chronograf/v1/sources/1/roles/biffsgang",
nil),
},
fields: fields{
Logger: log.New(log.DebugLevel),
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "muh source",
Username: "name",
Password: "hunter2",
URL: "http://localhost:8086",
}, nil
},
},
TimeSeries: &mocks.TimeSeries{
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
return nil
},
RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
return &mocks.RolesStore{
GetF: func(ctx context.Context, name string) (*chronograf.Role, error) {
return &chronograf.Role{
Name: "biffsgang",
Permissions: chronograf.Permissions{
{
Name: "grays_sports_almanac",
Scope: "DBScope",
Allowed: chronograf.Allowances{
"ReadData",
},
},
},
Users: []chronograf.User{
{
Name: "match",
},
{
Name: "skinhead",
},
{
Name: "3-d",
},
},
}, nil
},
}, nil
},
},
},
ID: "1",
RoleID: "biffsgang",
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"users":[{"links":{"self":"/chronograf/v1/sources/1/users/match"},"name":"match"},{"links":{"self":"/chronograf/v1/sources/1/users/skinhead"},"name":"skinhead"},{"links":{"self":"/chronograf/v1/sources/1/users/3-d"},"name":"3-d"}],"name":"biffsgang","permissions":[{"scope":"DBScope","name":"grays_sports_almanac","allowed":["ReadData"]}],"links":{"self":"/chronograf/v1/sources/1/roles/biffsgang"}}
`,
},
}
for _, tt := range tests {
h := &Service{
SourcesStore: tt.fields.SourcesStore,
TimeSeriesClient: tt.fields.TimeSeries,
Logger: tt.fields.Logger,
}
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
context.Background(),
httprouter.Params{
{
Key: "id",
Value: tt.ID,
},
{
Key: "rid",
Value: tt.RoleID,
},
}))
h.RoleID(tt.args.w, tt.args.r)
resp := tt.args.w.Result()
content := resp.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != tt.wantStatus {
t.Errorf("%q. RoleID() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
}
if tt.wantContentType != "" && content != tt.wantContentType {
t.Errorf("%q. RoleID() = %v, want %v", tt.name, content, tt.wantContentType)
}
if tt.wantBody != "" && string(body) != tt.wantBody {
t.Errorf("%q. RoleID() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
}
}
}
func TestService_RemoveRole(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
TimeSeries TimeSeriesClient
Logger chronograf.Logger
}
type args struct {
w *httptest.ResponseRecorder
r *http.Request
}
tests := []struct {
name string
fields fields
args args
ID string
RoleID string
wantStatus int
}{
{
name: "remove role for data source",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://server.local/chronograf/v1/sources/1/roles/biffsgang",
nil),
},
fields: fields{
Logger: log.New(log.DebugLevel),
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "muh source",
Username: "name",
Password: "hunter2",
URL: "http://localhost:8086",
}, nil
},
},
TimeSeries: &mocks.TimeSeries{
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
return nil
},
RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
return &mocks.RolesStore{
DeleteF: func(context.Context, *chronograf.Role) error {
return nil
},
}, nil
},
},
},
ID: "1",
RoleID: "biffsgang",
wantStatus: http.StatusNoContent,
},
}
for _, tt := range tests {
h := &Service{
SourcesStore: tt.fields.SourcesStore,
TimeSeriesClient: tt.fields.TimeSeries,
Logger: tt.fields.Logger,
}
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
context.Background(),
httprouter.Params{
{
Key: "id",
Value: tt.ID,
},
{
Key: "rid",
Value: tt.RoleID,
},
}))
h.RemoveRole(tt.args.w, tt.args.r)
resp := tt.args.w.Result()
if resp.StatusCode != tt.wantStatus {
t.Errorf("%q. RemoveRole() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
}
}
}
func TestService_Roles(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
TimeSeries TimeSeriesClient
Logger chronograf.Logger
}
type args struct {
w *httptest.ResponseRecorder
r *http.Request
}
tests := []struct {
name string
fields fields
args args
ID string
RoleID string
wantStatus int
wantContentType string
wantBody string
}{
{
name: "Get roles for data source",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"GET",
"http://server.local/chronograf/v1/sources/1/roles",
nil),
},
fields: fields{
Logger: log.New(log.DebugLevel),
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
}, nil
},
},
TimeSeries: &mocks.TimeSeries{
ConnectF: func(ctx context.Context, src *chronograf.Source) error {
return nil
},
RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
return &mocks.RolesStore{
AllF: func(ctx context.Context) ([]chronograf.Role, error) {
return []chronograf.Role{
chronograf.Role{
Name: "biffsgang",
Permissions: chronograf.Permissions{
{
Name: "grays_sports_almanac",
Scope: "DBScope",
Allowed: chronograf.Allowances{
"ReadData",
},
},
},
Users: []chronograf.User{
{
Name: "match",
},
{
Name: "skinhead",
},
{
Name: "3-d",
},
},
},
}, nil
},
}, nil
},
},
},
ID: "1",
RoleID: "biffsgang",
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"roles":[{"users":[{"links":{"self":"/chronograf/v1/sources/1/users/match"},"name":"match"},{"links":{"self":"/chronograf/v1/sources/1/users/skinhead"},"name":"skinhead"},{"links":{"self":"/chronograf/v1/sources/1/users/3-d"},"name":"3-d"}],"name":"biffsgang","permissions":[{"scope":"DBScope","name":"grays_sports_almanac","allowed":["ReadData"]}],"links":{"self":"/chronograf/v1/sources/1/roles/biffsgang"}}]}
`,
},
}
for _, tt := range tests {
h := &Service{
SourcesStore: tt.fields.SourcesStore,
TimeSeriesClient: tt.fields.TimeSeries,
Logger: tt.fields.Logger,
}
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
context.Background(),
httprouter.Params{
{
Key: "id",
Value: tt.ID,
},
{
Key: "rid",
Value: tt.RoleID,
},
}))
h.Roles(tt.args.w, tt.args.r)
resp := tt.args.w.Result()
content := resp.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != tt.wantStatus {
t.Errorf("%q. RoleID() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
}
if tt.wantContentType != "" && content != tt.wantContentType {
t.Errorf("%q. RoleID() = %v, want %v", tt.name, content, tt.wantContentType)
}
if tt.wantBody != "" && string(body) != tt.wantBody {
t.Errorf("%q. RoleID() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
}
}
}

View File

@ -30,6 +30,8 @@ func (r *AuthRoutes) Lookup(provider string) (AuthRoute, bool) {
type getRoutesResponse struct {
Layouts string `json:"layouts"` // Location of the layouts endpoint
Users string `json:"users"` // Location of the users endpoint
Organizations string `json:"organizations"` // Location of the organizations endpoint
Mappings string `json:"mappings"` // Location of the application mappings endpoint
Sources string `json:"sources"` // Location of the sources endpoint
Me string `json:"me"` // Location of the me endpoint
@ -59,12 +61,14 @@ func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
routes := getRoutesResponse{
Sources: "/chronograf/v1/sources",
Layouts: "/chronograf/v1/layouts",
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
Sources: "/chronograf/v1/sources",
Layouts: "/chronograf/v1/layouts",
Users: "/chronograf/v1/users",
Organizations: "/chronograf/v1/organizations",
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
ExternalLinks: getExternalLinksResponse{
StatusFeed: &a.StatusFeed,
CustomLinks: customLinks,

View File

@ -29,7 +29,7 @@ func TestAllRoutes(t *testing.T) {
if err := json.Unmarshal(body, &routes); err != nil {
t.Error("TestAllRoutes not able to unmarshal JSON response")
}
want := `{"layouts":"/chronograf/v1/layouts","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","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","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","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","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","auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]}}
`
if want != string(body) {
t.Errorf("TestAllRoutesWithExternalLinks\nwanted\n*%s*\ngot\n*%s*", want, string(body))

View File

@ -52,11 +52,12 @@ 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"`
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"`
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"`
@ -302,6 +303,7 @@ 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").
@ -406,11 +408,11 @@ func openService(ctx context.Context, boltPath string, lBuilder LayoutBuilder, s
os.Exit(1)
}
layouts, err := lBuilder.Build(db.LayoutStore)
layouts, err := lBuilder.Build(db.LayoutsStore)
if err != nil {
logger.
WithField("component", "LayoutStore").
Error("Unable to construct a MultiLayoutStore", err)
WithField("component", "LayoutsStore").
Error("Unable to construct a MultiLayoutsStore", err)
os.Exit(1)
}
@ -432,14 +434,18 @@ func openService(ctx context.Context, boltPath string, lBuilder LayoutBuilder, s
return Service{
TimeSeriesClient: &InfluxClient{},
SourcesStore: sources,
ServersStore: kapacitors,
UsersStore: db.UsersStore,
LayoutStore: layouts,
DashboardsStore: db.DashboardsStore,
Logger: logger,
UseAuth: useAuth,
Databases: &influx.Client{Logger: logger},
Store: &Store{
SourcesStore: sources,
ServersStore: kapacitors,
UsersStore: db.UsersStore,
OrganizationsStore: db.OrganizationsStore,
LayoutsStore: layouts,
DashboardsStore: db.DashboardsStore,
//OrganizationUsersStore: organizations.NewUsersStore(db.UsersStore),
},
Logger: logger,
UseAuth: useAuth,
Databases: &influx.Client{Logger: logger},
}
}

View File

@ -11,15 +11,12 @@ import (
// Service handles REST calls to the persistence
type Service struct {
SourcesStore chronograf.SourcesStore
ServersStore chronograf.ServersStore
LayoutStore chronograf.LayoutStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
TimeSeriesClient TimeSeriesClient
Logger chronograf.Logger
UseAuth bool
Databases chronograf.Databases
Store DataStore
TimeSeriesClient TimeSeriesClient
Logger chronograf.Logger
UseAuth bool
SuperAdminFirstUserOnly bool
Databases chronograf.Databases
}
// TimeSeriesClient returns the correct client for a time series database.

View File

@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/influx"
)
@ -63,15 +64,22 @@ func newSourceResponse(src chronograf.Source) sourceResponse {
}
// NewSource adds a new valid source to the store
func (h *Service) NewSource(w http.ResponseWriter, r *http.Request) {
func (s *Service) NewSource(w http.ResponseWriter, r *http.Request) {
var src chronograf.Source
if err := json.NewDecoder(r.Body).Decode(&src); err != nil {
invalidJSON(w, h.Logger)
invalidJSON(w, s.Logger)
return
}
if err := ValidSourceRequest(src); err != nil {
invalidData(w, err, h.Logger)
ctx := r.Context()
defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
if err := ValidSourceRequest(&src, fmt.Sprintf("%d", defaultOrg.ID)); err != nil {
invalidData(w, err, s.Logger)
return
}
@ -80,28 +88,27 @@ func (h *Service) NewSource(w http.ResponseWriter, r *http.Request) {
src.Telegraf = "telegraf"
}
ctx := r.Context()
dbType, err := h.tsdbType(ctx, &src)
dbType, err := s.tsdbType(ctx, &src)
if err != nil {
Error(w, http.StatusBadRequest, "Error contacting source", h.Logger)
Error(w, http.StatusBadRequest, "Error contacting source", s.Logger)
return
}
src.Type = dbType
if src, err = h.SourcesStore.Add(ctx, src); err != nil {
if src, err = s.Store.Sources(ctx).Add(ctx, src); err != nil {
msg := fmt.Errorf("Error storing source %v: %v", src, err)
unknownErrorWithMessage(w, msg, h.Logger)
unknownErrorWithMessage(w, msg, s.Logger)
return
}
res := newSourceResponse(src)
w.Header().Add("Location", res.Links.Self)
encodeJSON(w, http.StatusCreated, res, h.Logger)
location(w, res.Links.Self)
encodeJSON(w, http.StatusCreated, res, s.Logger)
}
func (h *Service) tsdbType(ctx context.Context, src *chronograf.Source) (string, error) {
func (s *Service) tsdbType(ctx context.Context, src *chronograf.Source) (string, error) {
cli := &influx.Client{
Logger: h.Logger,
Logger: s.Logger,
}
if err := cli.Connect(ctx, src); err != nil {
@ -115,11 +122,11 @@ type getSourcesResponse struct {
}
// Sources returns all sources from the store.
func (h *Service) Sources(w http.ResponseWriter, r *http.Request) {
func (s *Service) Sources(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcs, err := h.SourcesStore.All(ctx)
srcs, err := s.Store.Sources(ctx).All(ctx)
if err != nil {
Error(w, http.StatusInternalServerError, "Error loading sources", h.Logger)
Error(w, http.StatusInternalServerError, "Error loading sources", s.Logger)
return
}
@ -131,50 +138,50 @@ func (h *Service) Sources(w http.ResponseWriter, r *http.Request) {
res.Sources[i] = newSourceResponse(src)
}
encodeJSON(w, http.StatusOK, res, h.Logger)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// SourcesID retrieves a source from the store
func (h *Service) SourcesID(w http.ResponseWriter, r *http.Request) {
func (s *Service) SourcesID(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
src, err := h.SourcesStore.Get(ctx, id)
src, err := s.Store.Sources(ctx).Get(ctx, id)
if err != nil {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
res := newSourceResponse(src)
encodeJSON(w, http.StatusOK, res, h.Logger)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// RemoveSource deletes the source from the store
func (h *Service) RemoveSource(w http.ResponseWriter, r *http.Request) {
func (s *Service) RemoveSource(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
src := chronograf.Source{ID: id}
ctx := r.Context()
if err = h.SourcesStore.Delete(ctx, src); err != nil {
if err = s.Store.Sources(ctx).Delete(ctx, src); err != nil {
if err == chronograf.ErrSourceNotFound {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
} else {
unknownErrorWithMessage(w, err, h.Logger)
unknownErrorWithMessage(w, err, s.Logger)
}
return
}
// Remove all the associated kapacitors for this source
if err = h.removeSrcsKapa(ctx, id); err != nil {
unknownErrorWithMessage(w, err, h.Logger)
if err = s.removeSrcsKapa(ctx, id); err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
@ -183,8 +190,8 @@ func (h *Service) RemoveSource(w http.ResponseWriter, r *http.Request) {
// removeSrcsKapa will remove all kapacitors and kapacitor rules from the stores.
// However, it will not remove the kapacitor tickscript from kapacitor itself.
func (h *Service) removeSrcsKapa(ctx context.Context, srcID int) error {
kapas, err := h.ServersStore.All(ctx)
func (s *Service) removeSrcsKapa(ctx context.Context, srcID int) error {
kapas, err := s.Store.Servers(ctx).All(ctx)
if err != nil {
return err
}
@ -201,9 +208,9 @@ func (h *Service) removeSrcsKapa(ctx context.Context, srcID int) error {
kapa := chronograf.Server{
ID: kapaID,
}
h.Logger.Debug("Deleting kapacitor resource id ", kapa.ID)
s.Logger.Debug("Deleting kapacitor resource id ", kapa.ID)
if err := h.ServersStore.Delete(ctx, kapa); err != nil {
if err := s.Store.Servers(ctx).Delete(ctx, kapa); err != nil {
return err
}
}
@ -212,23 +219,23 @@ func (h *Service) removeSrcsKapa(ctx context.Context, srcID int) error {
}
// UpdateSource handles incremental updates of a data source
func (h *Service) UpdateSource(w http.ResponseWriter, r *http.Request) {
func (s *Service) UpdateSource(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
src, err := h.SourcesStore.Get(ctx, id)
src, err := s.Store.Sources(ctx).Get(ctx, id)
if err != nil {
notFound(w, id, h.Logger)
notFound(w, id, s.Logger)
return
}
var req chronograf.Source
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
invalidJSON(w, s.Logger)
return
}
@ -258,28 +265,37 @@ func (h *Service) UpdateSource(w http.ResponseWriter, r *http.Request) {
src.Telegraf = req.Telegraf
}
if err := ValidSourceRequest(src); err != nil {
invalidData(w, err, h.Logger)
defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
dbType, err := h.tsdbType(ctx, &src)
if err := ValidSourceRequest(&src, fmt.Sprintf("%d", defaultOrg.ID)); err != nil {
invalidData(w, err, s.Logger)
return
}
dbType, err := s.tsdbType(ctx, &src)
if err != nil {
Error(w, http.StatusBadRequest, "Error contacting source", h.Logger)
Error(w, http.StatusBadRequest, "Error contacting source", s.Logger)
return
}
src.Type = dbType
if err := h.SourcesStore.Update(ctx, src); err != nil {
if err := s.Store.Sources(ctx).Update(ctx, src); err != nil {
msg := fmt.Sprintf("Error updating source ID %d", id)
Error(w, http.StatusInternalServerError, msg, h.Logger)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
}
encodeJSON(w, http.StatusOK, newSourceResponse(src), h.Logger)
encodeJSON(w, http.StatusOK, newSourceResponse(src), s.Logger)
}
// ValidSourceRequest checks if name, url and type are valid
func ValidSourceRequest(s chronograf.Source) error {
// ValidSourceRequest checks if name, url, type, and role are valid
func ValidSourceRequest(s *chronograf.Source, defaultOrgID string) error {
if s == nil {
return fmt.Errorf("source must be non-nil")
}
// Name and URL areq required
if s.URL == "" {
return fmt.Errorf("url required")
@ -291,6 +307,10 @@ func ValidSourceRequest(s chronograf.Source) error {
}
}
if s.Organization == "" {
s.Organization = defaultOrgID
}
url, err := url.ParseRequestURI(s.URL)
if err != nil {
return fmt.Errorf("invalid source URI: %v", err)
@ -298,11 +318,12 @@ func ValidSourceRequest(s chronograf.Source) error {
if len(url.Scheme) == 0 {
return fmt.Errorf("Invalid URL; no URL scheme defined")
}
return nil
}
// HandleNewSources parses and persists new sources passed in via server flag
func (h *Service) HandleNewSources(ctx context.Context, input string) error {
func (s *Service) HandleNewSources(ctx context.Context, input string) error {
if input == "" {
return nil
}
@ -312,21 +333,26 @@ func (h *Service) HandleNewSources(ctx context.Context, input string) error {
Kapacitor chronograf.Server `json:"kapacitor"`
}
if err := json.Unmarshal([]byte(input), &srcsKaps); err != nil {
h.Logger.
s.Logger.
WithField("component", "server").
WithField("NewSources", "invalid").
Error(err)
return err
}
defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx)
if err != nil {
return err
}
for _, sk := range srcsKaps {
if err := ValidSourceRequest(sk.Source); err != nil {
if err := ValidSourceRequest(&sk.Source, fmt.Sprintf("%d", defaultOrg.ID)); err != nil {
return err
}
// Add any new sources and kapacitors as specified via server flag
if err := h.newSourceKapacitor(ctx, sk.Source, sk.Kapacitor); err != nil {
if err := s.newSourceKapacitor(ctx, sk.Source, sk.Kapacitor); err != nil {
// Continue with server run even if adding NewSource fails
h.Logger.
s.Logger.
WithField("component", "server").
WithField("NewSource", "invalid").
Error(err)
@ -337,32 +363,552 @@ func (h *Service) HandleNewSources(ctx context.Context, input string) error {
}
// newSourceKapacitor adds sources to BoltDB idempotently by name, as well as respective kapacitors
func (h *Service) newSourceKapacitor(ctx context.Context, src chronograf.Source, kapa chronograf.Server) error {
srcs, err := h.SourcesStore.All(ctx)
func (s *Service) newSourceKapacitor(ctx context.Context, src chronograf.Source, kapa chronograf.Server) error {
srcs, err := s.Store.Sources(ctx).All(ctx)
if err != nil {
return err
}
for _, s := range srcs {
for _, source := range srcs {
// If source already exists, do nothing
if s.Name == src.Name {
h.Logger.
if source.Name == src.Name {
s.Logger.
WithField("component", "server").
WithField("NewSource", s.Name).
WithField("NewSource", source.Name).
Info("Source already exists")
return nil
}
}
src, err = h.SourcesStore.Add(ctx, src)
src, err = s.Store.Sources(ctx).Add(ctx, src)
if err != nil {
return err
}
kapa.SrcID = src.ID
if _, err := h.ServersStore.Add(ctx, kapa); err != nil {
if _, err := s.Store.Servers(ctx).Add(ctx, kapa); err != nil {
return err
}
return nil
}
// NewSourceUser adds user to source
func (s *Service) NewSourceUser(w http.ResponseWriter, r *http.Request) {
var req sourceUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
return
}
if err := req.ValidCreate(); err != nil {
invalidData(w, err, s.Logger)
return
}
ctx := r.Context()
srcID, ts, err := s.sourcesSeries(ctx, w, r)
if err != nil {
return
}
store := ts.Users(ctx)
user := &chronograf.User{
Name: req.Username,
Passwd: req.Password,
Permissions: req.Permissions,
Roles: req.Roles,
}
res, err := store.Add(ctx, user)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
su := newSourceUserResponse(srcID, res.Name).WithPermissions(res.Permissions)
if _, hasRoles := s.hasRoles(ctx, ts); hasRoles {
su.WithRoles(srcID, res.Roles)
}
location(w, su.Links.Self)
encodeJSON(w, http.StatusCreated, su, s.Logger)
}
// SourceUsers retrieves all users from source.
func (s *Service) SourceUsers(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, ts, err := s.sourcesSeries(ctx, w, r)
if err != nil {
return
}
store := ts.Users(ctx)
users, err := store.All(ctx)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
_, hasRoles := s.hasRoles(ctx, ts)
ur := make([]sourceUserResponse, len(users))
for i, u := range users {
usr := newSourceUserResponse(srcID, u.Name).WithPermissions(u.Permissions)
if hasRoles {
usr.WithRoles(srcID, u.Roles)
}
ur[i] = *usr
}
res := sourceUsersResponse{
Users: ur,
}
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// SourceUserID retrieves a user with ID from store.
// In InfluxDB, a User's Name is their UID, hence the semantic below.
func (s *Service) SourceUserID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
uid := httprouter.GetParamFromContext(ctx, "uid")
srcID, ts, err := s.sourcesSeries(ctx, w, r)
if err != nil {
return
}
store := ts.Users(ctx)
u, err := store.Get(ctx, chronograf.UserQuery{Name: &uid})
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
res := newSourceUserResponse(srcID, u.Name).WithPermissions(u.Permissions)
if _, hasRoles := s.hasRoles(ctx, ts); hasRoles {
res.WithRoles(srcID, u.Roles)
}
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// RemoveSourceUser removes the user from the InfluxDB source
func (s *Service) RemoveSourceUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
uid := httprouter.GetParamFromContext(ctx, "uid")
_, store, err := s.sourceUsersStore(ctx, w, r)
if err != nil {
return
}
if err := store.Delete(ctx, &chronograf.User{Name: uid}); err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}
// UpdateSourceUser changes the password or permissions of a source user
func (s *Service) UpdateSourceUser(w http.ResponseWriter, r *http.Request) {
var req sourceUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
return
}
if err := req.ValidUpdate(); err != nil {
invalidData(w, err, s.Logger)
return
}
ctx := r.Context()
uid := httprouter.GetParamFromContext(ctx, "uid")
srcID, ts, err := s.sourcesSeries(ctx, w, r)
if err != nil {
return
}
user := &chronograf.User{
Name: uid,
Passwd: req.Password,
Permissions: req.Permissions,
Roles: req.Roles,
}
store := ts.Users(ctx)
if err := store.Update(ctx, user); err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
u, err := store.Get(ctx, chronograf.UserQuery{Name: &uid})
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
res := newSourceUserResponse(srcID, u.Name).WithPermissions(u.Permissions)
if _, hasRoles := s.hasRoles(ctx, ts); hasRoles {
res.WithRoles(srcID, u.Roles)
}
location(w, res.Links.Self)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
func (s *Service) sourcesSeries(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, chronograf.TimeSeries, error) {
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return 0, nil, err
}
src, err := s.Store.Sources(ctx).Get(ctx, srcID)
if err != nil {
notFound(w, srcID, s.Logger)
return 0, nil, err
}
ts, err := s.TimeSeries(src)
if err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
Error(w, http.StatusBadRequest, msg, s.Logger)
return 0, nil, err
}
if err = ts.Connect(ctx, &src); err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
Error(w, http.StatusBadRequest, msg, s.Logger)
return 0, nil, err
}
return srcID, ts, nil
}
func (s *Service) sourceUsersStore(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, chronograf.UsersStore, error) {
srcID, ts, err := s.sourcesSeries(ctx, w, r)
if err != nil {
return 0, nil, err
}
store := ts.Users(ctx)
return srcID, store, nil
}
// hasRoles checks if the influx source has roles or not
func (s *Service) hasRoles(ctx context.Context, ts chronograf.TimeSeries) (chronograf.RolesStore, bool) {
store, err := ts.Roles(ctx)
if err != nil {
return nil, false
}
return store, true
}
type sourceUserRequest struct {
Username string `json:"name,omitempty"` // Username for new account
Password string `json:"password,omitempty"` // Password for new account
Permissions chronograf.Permissions `json:"permissions,omitempty"` // Optional permissions
Roles []chronograf.Role `json:"roles,omitempty"` // Optional roles
}
func (r *sourceUserRequest) ValidCreate() error {
if r.Username == "" {
return fmt.Errorf("Username required")
}
if r.Password == "" {
return fmt.Errorf("Password required")
}
return validPermissions(&r.Permissions)
}
type sourceUsersResponse struct {
Users []sourceUserResponse `json:"users"`
}
func (r *sourceUserRequest) ValidUpdate() error {
if r.Password == "" && len(r.Permissions) == 0 && len(r.Roles) == 0 {
return fmt.Errorf("No fields to update")
}
return validPermissions(&r.Permissions)
}
type sourceUserResponse struct {
Name string // Username for new account
Permissions chronograf.Permissions // Account's permissions
Roles []sourceRoleResponse // Roles if source uses them
Links selfLinks // Links are URI locations related to user
hasPermissions bool
hasRoles bool
}
func (u *sourceUserResponse) MarshalJSON() ([]byte, error) {
res := map[string]interface{}{
"name": u.Name,
"links": u.Links,
}
if u.hasRoles {
res["roles"] = u.Roles
}
if u.hasPermissions {
res["permissions"] = u.Permissions
}
return json.Marshal(res)
}
// newSourceUserResponse creates an HTTP JSON response for a user w/o roles
func newSourceUserResponse(srcID int, name string) *sourceUserResponse {
self := newSelfLinks(srcID, "users", name)
return &sourceUserResponse{
Name: name,
Links: self,
}
}
func (u *sourceUserResponse) WithPermissions(perms chronograf.Permissions) *sourceUserResponse {
u.hasPermissions = true
if perms == nil {
perms = make(chronograf.Permissions, 0)
}
u.Permissions = perms
return u
}
// WithRoles adds roles to the HTTP JSON response for a user
func (u *sourceUserResponse) WithRoles(srcID int, roles []chronograf.Role) *sourceUserResponse {
u.hasRoles = true
rr := make([]sourceRoleResponse, len(roles))
for i, role := range roles {
rr[i] = newSourceRoleResponse(srcID, &role)
}
u.Roles = rr
return u
}
type selfLinks struct {
Self string `json:"self"` // Self link mapping to this resource
}
func newSelfLinks(id int, parent, resource string) selfLinks {
httpAPISrcs := "/chronograf/v1/sources"
u := &url.URL{Path: resource}
encodedResource := u.String()
return selfLinks{
Self: fmt.Sprintf("%s/%d/%s/%s", httpAPISrcs, id, parent, encodedResource),
}
}
// NewSourceRole adds role to source
func (s *Service) NewSourceRole(w http.ResponseWriter, r *http.Request) {
var req sourceRoleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
return
}
if err := req.ValidCreate(); err != nil {
invalidData(w, err, s.Logger)
return
}
ctx := r.Context()
srcID, ts, err := s.sourcesSeries(ctx, w, r)
if err != nil {
return
}
roles, ok := s.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), s.Logger)
return
}
if _, err := roles.Get(ctx, req.Name); err == nil {
Error(w, http.StatusBadRequest, fmt.Sprintf("Source %d already has role %s", srcID, req.Name), s.Logger)
return
}
res, err := roles.Add(ctx, &req.Role)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
rr := newSourceRoleResponse(srcID, res)
location(w, rr.Links.Self)
encodeJSON(w, http.StatusCreated, rr, s.Logger)
}
// UpdateSourceRole changes the permissions or users of a role
func (s *Service) UpdateSourceRole(w http.ResponseWriter, r *http.Request) {
var req sourceRoleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
return
}
if err := req.ValidUpdate(); err != nil {
invalidData(w, err, s.Logger)
return
}
ctx := r.Context()
srcID, ts, err := s.sourcesSeries(ctx, w, r)
if err != nil {
return
}
roles, ok := s.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), s.Logger)
return
}
rid := httprouter.GetParamFromContext(ctx, "rid")
req.Name = rid
if err := roles.Update(ctx, &req.Role); err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
role, err := roles.Get(ctx, req.Name)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
rr := newSourceRoleResponse(srcID, role)
location(w, rr.Links.Self)
encodeJSON(w, http.StatusOK, rr, s.Logger)
}
// SourceRoleID retrieves a role with ID from store.
func (s *Service) SourceRoleID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, ts, err := s.sourcesSeries(ctx, w, r)
if err != nil {
return
}
roles, ok := s.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), s.Logger)
return
}
rid := httprouter.GetParamFromContext(ctx, "rid")
role, err := roles.Get(ctx, rid)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
rr := newSourceRoleResponse(srcID, role)
encodeJSON(w, http.StatusOK, rr, s.Logger)
}
// SourceRoles retrieves all roles from the store
func (s *Service) SourceRoles(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, ts, err := s.sourcesSeries(ctx, w, r)
if err != nil {
return
}
store, ok := s.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), s.Logger)
return
}
roles, err := store.All(ctx)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
rr := make([]sourceRoleResponse, len(roles))
for i, role := range roles {
rr[i] = newSourceRoleResponse(srcID, &role)
}
res := struct {
Roles []sourceRoleResponse `json:"roles"`
}{rr}
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// RemoveSourceRole removes role from data source.
func (s *Service) RemoveSourceRole(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, ts, err := s.sourcesSeries(ctx, w, r)
if err != nil {
return
}
roles, ok := s.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), s.Logger)
return
}
rid := httprouter.GetParamFromContext(ctx, "rid")
if err := roles.Delete(ctx, &chronograf.Role{Name: rid}); err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}
// sourceRoleRequest is the format used for both creating and updating roles
type sourceRoleRequest struct {
chronograf.Role
}
func (r *sourceRoleRequest) ValidCreate() error {
if r.Name == "" || len(r.Name) > 254 {
return fmt.Errorf("Name is required for a role")
}
for _, user := range r.Users {
if user.Name == "" {
return fmt.Errorf("Username required")
}
}
return validPermissions(&r.Permissions)
}
func (r *sourceRoleRequest) ValidUpdate() error {
if len(r.Name) > 254 {
return fmt.Errorf("Username too long; must be less than 254 characters")
}
for _, user := range r.Users {
if user.Name == "" {
return fmt.Errorf("Username required")
}
}
return validPermissions(&r.Permissions)
}
type sourceRoleResponse struct {
Users []*sourceUserResponse `json:"users"`
Name string `json:"name"`
Permissions chronograf.Permissions `json:"permissions"`
Links selfLinks `json:"links"`
}
func newSourceRoleResponse(srcID int, res *chronograf.Role) sourceRoleResponse {
su := make([]*sourceUserResponse, len(res.Users))
for i := range res.Users {
name := res.Users[i].Name
su[i] = newSourceUserResponse(srcID, name)
}
if res.Permissions == nil {
res.Permissions = make(chronograf.Permissions, 0)
}
return sourceRoleResponse{
Name: res.Name,
Permissions: res.Permissions,
Users: su,
Links: newSelfLinks(srcID, "roles", res.Name),
}
}

File diff suppressed because it is too large Load Diff

180
server/stores.go Normal file
View File

@ -0,0 +1,180 @@
package server
import (
"context"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/noop"
"github.com/influxdata/chronograf/organizations"
"github.com/influxdata/chronograf/roles"
)
// hasOrganizationContext retrieves organization specified on context
// under the organizations.ContextKey
func hasOrganizationContext(ctx context.Context) (string, bool) {
// prevents panic in case of nil context
if ctx == nil {
return "", false
}
orgID, ok := ctx.Value(organizations.ContextKey).(string)
// should never happen
if !ok {
return "", false
}
if orgID == "" {
return "", false
}
return orgID, true
}
// hasRoleContext retrieves organization specified on context
// under the organizations.ContextKey
func hasRoleContext(ctx context.Context) (string, bool) {
// prevents panic in case of nil context
if ctx == nil {
return "", false
}
role, ok := ctx.Value(roles.ContextKey).(string)
// should never happen
if !ok {
return "", false
}
switch role {
case roles.MemberRoleName, roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName:
return role, true
default:
return "", false
}
}
type userContextKey string
// UserContextKey is the context key for retrieving the user off of context
const UserContextKey = userContextKey("user")
// hasUserContext speficies if the context contains
// the UserContextKey and that the value stored there is chronograf.User
func hasUserContext(ctx context.Context) (*chronograf.User, bool) {
// prevents panic in case of nil context
if ctx == nil {
return nil, false
}
u, ok := ctx.Value(UserContextKey).(*chronograf.User)
// should never happen
if !ok {
return nil, false
}
if u == nil {
return nil, false
}
return u, true
}
// hasSuperAdminContext speficies if the context contains
// the UserContextKey user is a super admin
func hasSuperAdminContext(ctx context.Context) bool {
u, ok := hasUserContext(ctx)
if !ok {
return false
}
return u.SuperAdmin
}
// DataStore is collection of resources that are used by the Service
// Abstracting this into an interface was useful for isolated testing
type DataStore interface {
Sources(ctx context.Context) chronograf.SourcesStore
Servers(ctx context.Context) chronograf.ServersStore
Layouts(ctx context.Context) chronograf.LayoutsStore
Users(ctx context.Context) chronograf.UsersStore
Organizations(ctx context.Context) chronograf.OrganizationsStore
Dashboards(ctx context.Context) chronograf.DashboardsStore
}
// ensure that Store implements a DataStore
var _ DataStore = &Store{}
// Store implements the DataStore interface
type Store struct {
SourcesStore chronograf.SourcesStore
ServersStore chronograf.ServersStore
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
OrganizationsStore chronograf.OrganizationsStore
}
// Sources returns a noop.SourcesStore if the context has no organization specified
// and a organization.SourcesStore otherwise.
func (s *Store) Sources(ctx context.Context) chronograf.SourcesStore {
if isServer := hasServerContext(ctx); isServer {
return s.SourcesStore
}
if org, ok := hasOrganizationContext(ctx); ok {
return organizations.NewSourcesStore(s.SourcesStore, org)
}
return &noop.SourcesStore{}
}
// Servers returns a noop.ServersStore if the context has no organization specified
// and a organization.ServersStore otherwise.
func (s *Store) Servers(ctx context.Context) chronograf.ServersStore {
if isServer := hasServerContext(ctx); isServer {
return s.ServersStore
}
if org, ok := hasOrganizationContext(ctx); ok {
return organizations.NewServersStore(s.ServersStore, org)
}
return &noop.ServersStore{}
}
// Layouts returns all layouts in the underlying layouts store.
func (s *Store) Layouts(ctx context.Context) chronograf.LayoutsStore {
return s.LayoutsStore
}
// Users returns a chronograf.UsersStore.
// If the context is a server context, then the underlying chronograf.UsersStore
// is returned.
// If there is an organization specified on context, then an organizations.UsersStore
// is returned.
// If niether are specified, a noop.UsersStore is returned.
func (s *Store) Users(ctx context.Context) chronograf.UsersStore {
if isServer := hasServerContext(ctx); isServer {
return s.UsersStore
}
if org, ok := hasOrganizationContext(ctx); ok {
return organizations.NewUsersStore(s.UsersStore, org)
}
return &noop.UsersStore{}
}
// Dashboards returns a noop.DashboardsStore if the context has no organization specified
// and a organization.DashboardsStore otherwise.
func (s *Store) Dashboards(ctx context.Context) chronograf.DashboardsStore {
if isServer := hasServerContext(ctx); isServer {
return s.DashboardsStore
}
if org, ok := hasOrganizationContext(ctx); ok {
return organizations.NewDashboardsStore(s.DashboardsStore, org)
}
return &noop.DashboardsStore{}
}
// Organizations returns the underlying OrganizationsStore.
func (s *Store) Organizations(ctx context.Context) chronograf.OrganizationsStore {
if isServer := hasServerContext(ctx); isServer {
return s.OrganizationsStore
}
if isSuperAdmin := hasSuperAdminContext(ctx); isSuperAdmin {
return s.OrganizationsStore
}
if org, ok := hasOrganizationContext(ctx); ok {
return organizations.NewOrganizationsStore(s.OrganizationsStore, org)
}
return &noop.OrganizationsStore{}
}

428
server/stores_test.go Normal file
View File

@ -0,0 +1,428 @@
package server
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/mocks"
"github.com/influxdata/chronograf/organizations"
)
func TestStore_SourcesGet(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
}
type args struct {
organization string
id int
}
type wants struct {
source chronograf.Source
err bool
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "Get source",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
}, nil
},
},
},
args: args{
organization: "0",
},
wants: wants{
source: chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
},
},
},
{
name: "Get source - no organization specified on context",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
}, nil
},
},
},
args: args{},
wants: wants{
err: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := &Store{
SourcesStore: tt.fields.SourcesStore,
}
ctx := context.Background()
if tt.args.organization != "" {
ctx = context.WithValue(ctx, organizations.ContextKey, tt.args.organization)
}
source, err := store.Sources(ctx).Get(ctx, tt.args.id)
if (err != nil) != tt.wants.err {
t.Errorf("%q. Store.Sources().Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
return
}
if diff := cmp.Diff(source, tt.wants.source); diff != "" {
t.Errorf("%q. Store.Sources().Get():\n-got/+want\ndiff %s", tt.name, diff)
}
})
}
}
func TestStore_SourcesAll(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
}
type args struct {
organization string
}
type wants struct {
sources []chronograf.Source
err bool
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "Get sources",
fields: fields{
SourcesStore: &mocks.SourcesStore{
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
return []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
},
}, nil
},
},
},
args: args{
organization: "0",
},
wants: wants{
sources: []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
},
},
},
},
{
name: "Get sources - multiple orgs",
fields: fields{
SourcesStore: &mocks.SourcesStore{
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
return []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
},
{
ID: 2,
Name: "A bad source",
Organization: "0",
},
{
ID: 3,
Name: "A good source",
Organization: "0",
},
{
ID: 4,
Name: "a source I can has",
Organization: "0",
},
{
ID: 5,
Name: "i'm in the wrong org",
Organization: "1",
},
}, nil
},
},
},
args: args{
organization: "0",
},
wants: wants{
sources: []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
},
{
ID: 2,
Name: "A bad source",
Organization: "0",
},
{
ID: 3,
Name: "A good source",
Organization: "0",
},
{
ID: 4,
Name: "a source I can has",
Organization: "0",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := &Store{
SourcesStore: tt.fields.SourcesStore,
}
ctx := context.Background()
if tt.args.organization != "" {
ctx = context.WithValue(ctx, organizations.ContextKey, tt.args.organization)
}
sources, err := store.Sources(ctx).All(ctx)
if (err != nil) != tt.wants.err {
t.Errorf("%q. Store.Sources().Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
return
}
if diff := cmp.Diff(sources, tt.wants.sources); diff != "" {
t.Errorf("%q. Store.Sources().Get():\n-got/+want\ndiff %s", tt.name, diff)
}
})
}
}
func TestStore_OrganizationsAdd(t *testing.T) {
type fields struct {
OrganizationsStore chronograf.OrganizationsStore
}
type args struct {
orgID uint64
serverContext bool
organization string
user *chronograf.User
}
type wants struct {
organization *chronograf.Organization
err bool
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "Get organization with server context",
fields: fields{
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 21,
Name: "my sweet name",
DefaultRole: "viewer",
}, nil
},
},
},
args: args{
serverContext: true,
orgID: 21,
},
wants: wants{
organization: &chronograf.Organization{
ID: 21,
Name: "my sweet name",
DefaultRole: "viewer",
},
},
},
{
name: "Get organization with super admin",
fields: fields{
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 21,
Name: "my sweet name",
DefaultRole: "viewer",
}, nil
},
},
},
args: args{
user: &chronograf.User{
ID: 1337,
Name: "bobbetta",
Provider: "github",
Scheme: "oauth2",
SuperAdmin: true,
},
orgID: 21,
},
wants: wants{
organization: &chronograf.Organization{
ID: 21,
Name: "my sweet name",
DefaultRole: "viewer",
},
},
},
{
name: "Get organization not as super admin no organization",
fields: fields{
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 21,
Name: "my sweet name",
DefaultRole: "viewer",
}, nil
},
},
},
args: args{
user: &chronograf.User{
ID: 1337,
Name: "bobbetta",
Provider: "github",
Scheme: "oauth2",
},
orgID: 21,
},
wants: wants{
err: true,
},
},
{
name: "Get organization not as super admin with organization",
fields: fields{
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 21,
Name: "my sweet name",
DefaultRole: "viewer",
}, nil
},
},
},
args: args{
user: &chronograf.User{
ID: 1337,
Name: "bobbetta",
Provider: "github",
Scheme: "oauth2",
},
organization: "21",
orgID: 21,
},
wants: wants{
organization: &chronograf.Organization{
ID: 21,
Name: "my sweet name",
DefaultRole: "viewer",
},
},
},
{
name: "Get different organization not as super admin with organization",
fields: fields{
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: 22,
Name: "my sweet name",
DefaultRole: "viewer",
}, nil
},
},
},
args: args{
user: &chronograf.User{
ID: 1337,
Name: "bobbetta",
Provider: "github",
Scheme: "oauth2",
},
organization: "21",
orgID: 21,
},
wants: wants{
err: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := &Store{
OrganizationsStore: tt.fields.OrganizationsStore,
}
ctx := context.Background()
if tt.args.serverContext {
ctx = serverContext(ctx)
}
if tt.args.organization != "" {
ctx = context.WithValue(ctx, organizations.ContextKey, tt.args.organization)
}
if tt.args.user != nil {
ctx = context.WithValue(ctx, UserContextKey, tt.args.user)
}
organization, err := store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &tt.args.orgID})
if (err != nil) != tt.wants.err {
t.Errorf("%q. Store.Organizations().Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
return
}
if diff := cmp.Diff(organization, tt.wants.organization); diff != "" {
t.Errorf("%q. Store.Organizations().Get():\n-got/+want\ndiff %s", tt.name, diff)
}
})
}
}

View File

@ -3,7 +3,7 @@
"info": {
"title": "Chronograf",
"description": "API endpoints for Chronograf",
"version": "1.3.10.0"
"version": "1.4.0.0-beta1"
},
"schemes": ["http"],
"basePath": "/chronograf/v1",

View File

@ -73,7 +73,7 @@ func (s *Service) Templates(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
d, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
d, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
@ -94,7 +94,7 @@ func (s *Service) NewTemplate(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
dash, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
@ -121,7 +121,7 @@ func (s *Service) NewTemplate(w http.ResponseWriter, r *http.Request) {
template.ID = chronograf.TemplateID(tid)
dash.Templates = append(dash.Templates, template)
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
if err := s.Store.Dashboards(ctx).Update(ctx, dash); err != nil {
msg := fmt.Sprintf("Error adding template %s to dashboard %d: %v", tid, id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
@ -140,7 +140,7 @@ func (s *Service) TemplateID(w http.ResponseWriter, r *http.Request) {
return
}
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
dash, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
@ -167,7 +167,7 @@ func (s *Service) RemoveTemplate(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
dash, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
@ -187,7 +187,7 @@ func (s *Service) RemoveTemplate(w http.ResponseWriter, r *http.Request) {
}
dash.Templates = append(dash.Templates[:pos], dash.Templates[pos+1:]...)
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
if err := s.Store.Dashboards(ctx).Update(ctx, dash); err != nil {
msg := fmt.Sprintf("Error removing template %s from dashboard %d: %v", tid, id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
@ -205,7 +205,7 @@ func (s *Service) ReplaceTemplate(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
dash, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
@ -237,7 +237,7 @@ func (s *Service) ReplaceTemplate(w http.ResponseWriter, r *http.Request) {
template.ID = chronograf.TemplateID(tid)
dash.Templates[pos] = template
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
if err := s.Store.Dashboards(ctx).Update(ctx, dash); err != nil {
msg := fmt.Sprintf("Error updating template %s in dashboard %d: %v", tid, id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return

20
server/test_helpers.go Normal file
View File

@ -0,0 +1,20 @@
package server
import (
"encoding/json"
"github.com/google/go-cmp/cmp"
)
func jsonEqual(s1, s2 string) (eq bool, err error) {
var o1, o2 interface{}
if err = json.Unmarshal([]byte(s1), &o1); err != nil {
return
}
if err = json.Unmarshal([]byte(s2), &o2); err != nil {
return
}
return cmp.Equal(o1, o2), nil
}

View File

@ -5,313 +5,315 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"sort"
"strconv"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/roles"
)
// NewSourceUser adds user to source
func (h *Service) NewSourceUser(w http.ResponseWriter, r *http.Request) {
type userRequest struct {
ID uint64 `json:"id,string"`
Name string `json:"name"`
Provider string `json:"provider"`
Scheme string `json:"scheme"`
SuperAdmin bool `json:"superAdmin"`
Roles []chronograf.Role `json:"roles"`
}
func (r *userRequest) ValidCreate() error {
if r.Name == "" {
return fmt.Errorf("Name required on Chronograf User request body")
}
if r.Provider == "" {
return fmt.Errorf("Provider required on Chronograf User request body")
}
if r.Scheme == "" {
return fmt.Errorf("Scheme required on Chronograf User request body")
}
// TODO: This Scheme value is hard-coded temporarily since we only currently
// support OAuth2. This hard-coding should be removed whenever we add
// support for other authentication schemes.
r.Scheme = "oauth2"
return r.ValidRoles()
}
func (r *userRequest) ValidUpdate() error {
if len(r.Roles) == 0 {
return fmt.Errorf("No Roles to update")
}
return r.ValidRoles()
}
func (r *userRequest) ValidRoles() error {
orgs := map[string]bool{}
if len(r.Roles) > 0 {
for _, r := range r.Roles {
if r.Organization == "" {
return fmt.Errorf("no organization was provided")
}
if _, err := parseOrganizationID(r.Organization); err != nil {
return fmt.Errorf("failed to parse organization ID: %v", err)
}
if _, ok := orgs[r.Organization]; ok {
return fmt.Errorf("duplicate organization %q in roles", r.Organization)
}
orgs[r.Organization] = true
switch r.Name {
case roles.MemberRoleName, roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName:
continue
default:
return fmt.Errorf("Unknown role %s. Valid roles are 'member', 'viewer', 'editor', and 'admin'", r.Name)
}
}
}
return nil
}
type userResponse struct {
Links selfLinks `json:"links"`
ID uint64 `json:"id,string"`
Name string `json:"name"`
Provider string `json:"provider"`
Scheme string `json:"scheme"`
SuperAdmin bool `json:"superAdmin"`
Roles []chronograf.Role `json:"roles"`
}
func newUserResponse(u *chronograf.User) *userResponse {
// This ensures that any user response with no roles returns an empty array instead of
// null when marshaled into JSON. That way, JavaScript doesn't need any guard on the
// key existing and it can simply be iterated over.
if u.Roles == nil {
u.Roles = []chronograf.Role{}
}
return &userResponse{
ID: u.ID,
Name: u.Name,
Provider: u.Provider,
Scheme: u.Scheme,
Roles: u.Roles,
SuperAdmin: u.SuperAdmin,
Links: selfLinks{
Self: fmt.Sprintf("/chronograf/v1/users/%d", u.ID),
},
}
}
type usersResponse struct {
Links selfLinks `json:"links"`
Users []*userResponse `json:"users"`
}
func newUsersResponse(users []chronograf.User) *usersResponse {
usersResp := make([]*userResponse, len(users))
for i, user := range users {
usersResp[i] = newUserResponse(&user)
}
sort.Slice(usersResp, func(i, j int) bool {
return usersResp[i].ID < usersResp[j].ID
})
return &usersResponse{
Users: usersResp,
Links: selfLinks{
Self: "/chronograf/v1/users",
},
}
}
// UserID retrieves a Chronograf user with ID from store
func (s *Service) UserID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
idStr := httprouter.GetParamFromContext(ctx, "id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
Error(w, http.StatusBadRequest, fmt.Sprintf("invalid user id: %s", err.Error()), s.Logger)
return
}
user, err := s.Store.Users(ctx).Get(ctx, chronograf.UserQuery{ID: &id})
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
res := newUserResponse(user)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// NewUser adds a new Chronograf user to store
func (s *Service) NewUser(w http.ResponseWriter, r *http.Request) {
var req userRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
invalidJSON(w, s.Logger)
return
}
if err := req.ValidCreate(); err != nil {
invalidData(w, err, h.Logger)
invalidData(w, err, s.Logger)
return
}
ctx := r.Context()
srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
return
}
store := ts.Users(ctx)
user := &chronograf.User{
Name: req.Username,
Passwd: req.Password,
Permissions: req.Permissions,
Roles: req.Roles,
Name: req.Name,
Provider: req.Provider,
Scheme: req.Scheme,
Roles: req.Roles,
}
res, err := store.Add(ctx, user)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
if err := setSuperAdmin(ctx, req, user); err != nil {
Error(w, http.StatusUnauthorized, err.Error(), s.Logger)
return
}
res, err := s.Store.Users(ctx).Add(ctx, user)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
su := newUserResponse(srcID, res.Name).WithPermissions(res.Permissions)
if _, hasRoles := h.hasRoles(ctx, ts); hasRoles {
su.WithRoles(srcID, res.Roles)
}
w.Header().Add("Location", su.Links.Self)
encodeJSON(w, http.StatusCreated, su, h.Logger)
cu := newUserResponse(res)
location(w, cu.Links.Self)
encodeJSON(w, http.StatusCreated, cu, s.Logger)
}
// SourceUsers retrieves all users from source.
func (h *Service) SourceUsers(w http.ResponseWriter, r *http.Request) {
// RemoveUser deletes a Chronograf user from store
func (s *Service) RemoveUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, ts, err := h.sourcesSeries(ctx, w, r)
idStr := httprouter.GetParamFromContext(ctx, "id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
Error(w, http.StatusBadRequest, fmt.Sprintf("invalid user id: %s", err.Error()), s.Logger)
return
}
store := ts.Users(ctx)
users, err := store.All(ctx)
u, err := s.Store.Users(ctx).Get(ctx, chronograf.UserQuery{ID: &id})
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
Error(w, http.StatusNotFound, err.Error(), s.Logger)
return
}
_, hasRoles := h.hasRoles(ctx, ts)
ur := make([]userResponse, len(users))
for i, u := range users {
usr := newUserResponse(srcID, u.Name).WithPermissions(u.Permissions)
if hasRoles {
usr.WithRoles(srcID, u.Roles)
}
ur[i] = *usr
}
res := usersResponse{
Users: ur,
}
encodeJSON(w, http.StatusOK, res, h.Logger)
}
// SourceUserID retrieves a user with ID from store.
func (h *Service) SourceUserID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
uid := httprouter.GetParamFromContext(ctx, "uid")
srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
ctxUser, ok := hasUserContext(ctx)
if !ok {
Error(w, http.StatusBadRequest, "failed to retrieve user from context", s.Logger)
return
}
store := ts.Users(ctx)
u, err := store.Get(ctx, uid)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
if ctxUser.ID == u.ID {
Error(w, http.StatusForbidden, "user cannot delete themselves", s.Logger)
return
}
res := newUserResponse(srcID, u.Name).WithPermissions(u.Permissions)
if _, hasRoles := h.hasRoles(ctx, ts); hasRoles {
res.WithRoles(srcID, u.Roles)
}
encodeJSON(w, http.StatusOK, res, h.Logger)
}
// RemoveSourceUser removes the user from the InfluxDB source
func (h *Service) RemoveSourceUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
uid := httprouter.GetParamFromContext(ctx, "uid")
_, store, err := h.sourceUsersStore(ctx, w, r)
if err != nil {
return
}
if err := store.Delete(ctx, &chronograf.User{Name: uid}); err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
if err := s.Store.Users(ctx).Delete(ctx, u); err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}
// UpdateSourceUser changes the password or permissions of a source user
func (h *Service) UpdateSourceUser(w http.ResponseWriter, r *http.Request) {
// UpdateUser updates a Chronograf user in store
func (s *Service) UpdateUser(w http.ResponseWriter, r *http.Request) {
var req userRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
return
}
if err := req.ValidUpdate(); err != nil {
invalidData(w, err, h.Logger)
invalidJSON(w, s.Logger)
return
}
ctx := r.Context()
uid := httprouter.GetParamFromContext(ctx, "uid")
srcID, ts, err := h.sourcesSeries(ctx, w, r)
idStr := httprouter.GetParamFromContext(ctx, "id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
Error(w, http.StatusBadRequest, fmt.Sprintf("invalid user id: %s", err.Error()), s.Logger)
return
}
user := &chronograf.User{
Name: uid,
Passwd: req.Password,
Permissions: req.Permissions,
Roles: req.Roles,
}
store := ts.Users(ctx)
if err := store.Update(ctx, user); err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
if err := req.ValidUpdate(); err != nil {
invalidData(w, err, s.Logger)
return
}
u, err := store.Get(ctx, uid)
u, err := s.Store.Users(ctx).Get(ctx, chronograf.UserQuery{ID: &id})
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
Error(w, http.StatusNotFound, err.Error(), s.Logger)
return
}
res := newUserResponse(srcID, u.Name).WithPermissions(u.Permissions)
if _, hasRoles := h.hasRoles(ctx, ts); hasRoles {
res.WithRoles(srcID, u.Roles)
}
w.Header().Add("Location", res.Links.Self)
encodeJSON(w, http.StatusOK, res, h.Logger)
}
// ValidUpdate should ensure that req.Roles is not nil
u.Roles = req.Roles
func (h *Service) sourcesSeries(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, chronograf.TimeSeries, error) {
srcID, err := paramID("id", r)
// If the request contains a name, it must be the same as the
// one on the user. This is particularly useful to the front-end
// because they would like to provide the whole user object,
// including the name, provider, and scheme in update requests.
// But currently, it is not possible to change name, provider, or
// scheme via the API.
if req.Name != "" && req.Name != u.Name {
err := fmt.Errorf("Cannot update Name")
invalidData(w, err, s.Logger)
return
}
if req.Provider != "" && req.Provider != u.Provider {
err := fmt.Errorf("Cannot update Provider")
invalidData(w, err, s.Logger)
return
}
if req.Scheme != "" && req.Scheme != u.Scheme {
err := fmt.Errorf("Cannot update Scheme")
invalidData(w, err, s.Logger)
return
}
if err := setSuperAdmin(ctx, req, u); err != nil {
Error(w, http.StatusUnauthorized, err.Error(), s.Logger)
return
}
err = s.Store.Users(ctx).Update(ctx, u)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return 0, nil, err
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
src, err := h.SourcesStore.Get(ctx, srcID)
cu := newUserResponse(u)
location(w, cu.Links.Self)
encodeJSON(w, http.StatusOK, cu, s.Logger)
}
// Users retrieves all Chronograf users from store
func (s *Service) Users(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
users, err := s.Store.Users(ctx).All(ctx)
if err != nil {
notFound(w, srcID, h.Logger)
return 0, nil, err
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
ts, err := h.TimeSeries(src)
if err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
Error(w, http.StatusBadRequest, msg, h.Logger)
return 0, nil, err
}
if err = ts.Connect(ctx, &src); err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
Error(w, http.StatusBadRequest, msg, h.Logger)
return 0, nil, err
}
return srcID, ts, nil
res := newUsersResponse(users)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
func (h *Service) sourceUsersStore(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, chronograf.UsersStore, error) {
srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
return 0, nil, err
func setSuperAdmin(ctx context.Context, req userRequest, user *chronograf.User) error {
// At a high level, this function checks the following
// 1. Is the user making the request a SuperAdmin.
// If they are, allow them to make whatever changes they please.
//
// 2. Is the user making the request trying to change the SuperAdmin
// status. If so, return an error.
//
// 3. If none of the above are the case, let the user make whichever
// changes were requested.
// Only allow users to set SuperAdmin if they have the superadmin context
// TODO(desa): Refactor this https://github.com/influxdata/chronograf/issues/2207
if isSuperAdmin := hasSuperAdminContext(ctx); isSuperAdmin {
user.SuperAdmin = req.SuperAdmin
} else if !isSuperAdmin && (user.SuperAdmin != req.SuperAdmin) {
// If req.SuperAdmin has been set, and the request was not made with the SuperAdmin
// context, return error
return fmt.Errorf("User does not have authorization required to set SuperAdmin status")
}
store := ts.Users(ctx)
return srcID, store, nil
}
// hasRoles checks if the influx source has roles or not
func (h *Service) hasRoles(ctx context.Context, ts chronograf.TimeSeries) (chronograf.RolesStore, bool) {
store, err := ts.Roles(ctx)
if err != nil {
return nil, false
}
return store, true
}
type userRequest struct {
Username string `json:"name,omitempty"` // Username for new account
Password string `json:"password,omitempty"` // Password for new account
Permissions chronograf.Permissions `json:"permissions,omitempty"` // Optional permissions
Roles []chronograf.Role `json:"roles,omitempty"` // Optional roles
}
func (r *userRequest) ValidCreate() error {
if r.Username == "" {
return fmt.Errorf("Username required")
}
if r.Password == "" {
return fmt.Errorf("Password required")
}
return validPermissions(&r.Permissions)
}
type usersResponse struct {
Users []userResponse `json:"users"`
}
func (r *userRequest) ValidUpdate() error {
if r.Password == "" && len(r.Permissions) == 0 && len(r.Roles) == 0 {
return fmt.Errorf("No fields to update")
}
return validPermissions(&r.Permissions)
}
type userResponse struct {
Name string // Username for new account
Permissions chronograf.Permissions // Account's permissions
Roles []roleResponse // Roles if source uses them
Links selfLinks // Links are URI locations related to user
hasPermissions bool
hasRoles bool
}
func (u *userResponse) MarshalJSON() ([]byte, error) {
res := map[string]interface{}{
"name": u.Name,
"links": u.Links,
}
if u.hasRoles {
res["roles"] = u.Roles
}
if u.hasPermissions {
res["permissions"] = u.Permissions
}
return json.Marshal(res)
}
// newUserResponse creates an HTTP JSON response for a user w/o roles
func newUserResponse(srcID int, name string) *userResponse {
self := newSelfLinks(srcID, "users", name)
return &userResponse{
Name: name,
Links: self,
}
}
func (u *userResponse) WithPermissions(perms chronograf.Permissions) *userResponse {
u.hasPermissions = true
if perms == nil {
perms = make(chronograf.Permissions, 0)
}
u.Permissions = perms
return u
}
// WithRoles adds roles to the HTTP JSON response for a user
func (u *userResponse) WithRoles(srcID int, roles []chronograf.Role) *userResponse {
u.hasRoles = true
rr := make([]roleResponse, len(roles))
for i, role := range roles {
rr[i] = newRoleResponse(srcID, &role)
}
u.Roles = rr
return u
}
type selfLinks struct {
Self string `json:"self"` // Self link mapping to this resource
}
func newSelfLinks(id int, parent, resource string) selfLinks {
httpAPISrcs := "/chronograf/v1/sources"
u := &url.URL{Path: resource}
encodedResource := u.String()
return selfLinks{
Self: fmt.Sprintf("%s/%d/%s/%s", httpAPISrcs, id, parent, encodedResource),
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "chronograf-ui",
"version": "1.3.10-0",
"version": "1.4.0-0beta1",
"private": false,
"license": "AGPL-3.0",
"description": "",
@ -103,7 +103,7 @@
"bootstrap": "^3.3.7",
"calculate-size": "^1.1.1",
"classnames": "^2.2.3",
"dygraphs": "influxdata/dygraphs",
"dygraphs": "2.1.0",
"eslint-plugin-babel": "^4.1.2",
"fast.js": "^0.1.1",
"fixed-data-table": "^0.6.1",

Some files were not shown because too many files have changed in this diff Show More