Merge branch 'master' into fix-optin-cursor
commit
da5ec796d5
|
@ -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}
|
||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -1,11 +1,31 @@
|
|||
## v1.3.11.0 [unreleased]
|
||||
## v1.4.0.0-beta2 [unreleased]
|
||||
### Features
|
||||
### UI Improvements
|
||||
### Bug Fixes
|
||||
|
||||
## 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 +34,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
172
bolt/users.go
172
bolt/users.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
128
chronograf.go
128
chronograf.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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,
|
||||
}
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
173
server/auth.go
173
server/auth.go
|
@ -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
|
||||
}
|
||||
|
|
1588
server/auth_test.go
1588
server/auth_test.go
File diff suppressed because it is too large
Load Diff
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package server
|
||||
|
||||
import "net/http"
|
||||
|
||||
func location(w http.ResponseWriter, self string) {
|
||||
w.Header().Add("Location", self)
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
118
server/layout.go
118
server/layout.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
313
server/me.go
313
server/me.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
193
server/mux.go
193
server/mux.go
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
224
server/roles.go
224
server/roles.go
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
@ -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{}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
474
server/users.go
474
server/users.go
|
@ -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
|
||||
}
|
||||
|
|
1927
server/users_test.go
1927
server/users_test.go
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
import reducer from 'src/admin/reducers/chronograf'
|
||||
|
||||
import {loadUsers} from 'src/admin/actions/chronograf'
|
||||
|
||||
import {
|
||||
MEMBER_ROLE,
|
||||
VIEWER_ROLE,
|
||||
EDITOR_ROLE,
|
||||
ADMIN_ROLE,
|
||||
} from 'src/auth/Authorized'
|
||||
|
||||
let state
|
||||
|
||||
const users = [
|
||||
{
|
||||
id: '666',
|
||||
name: 'bob@billietta.com',
|
||||
roles: [
|
||||
{
|
||||
name: 'admin',
|
||||
organization: '0',
|
||||
},
|
||||
{
|
||||
name: 'member',
|
||||
organization: '667',
|
||||
},
|
||||
],
|
||||
provider: 'github',
|
||||
scheme: 'oauth2',
|
||||
superAdmin: true,
|
||||
links: {
|
||||
self: '/chronograf/v1/users/666',
|
||||
},
|
||||
organizations: [
|
||||
{
|
||||
id: '0',
|
||||
name: 'Default',
|
||||
},
|
||||
{
|
||||
id: '667',
|
||||
name: 'Engineering',
|
||||
defaultRole: 'member',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '831',
|
||||
name: 'billybob@gmail.com',
|
||||
roles: [
|
||||
{
|
||||
name: 'member',
|
||||
organization: '0',
|
||||
},
|
||||
{
|
||||
name: 'viewer',
|
||||
organization: '667',
|
||||
},
|
||||
{
|
||||
name: 'editor',
|
||||
organization: '1236',
|
||||
},
|
||||
],
|
||||
provider: 'github',
|
||||
scheme: 'oauth2',
|
||||
superAdmin: false,
|
||||
links: {
|
||||
self: '/chronograf/v1/users/831',
|
||||
},
|
||||
organizations: [
|
||||
{
|
||||
id: '0',
|
||||
name: 'Default',
|
||||
},
|
||||
{
|
||||
id: '667',
|
||||
name: 'Engineering',
|
||||
defaultRole: 'member',
|
||||
},
|
||||
{
|
||||
id: '1236',
|
||||
name: 'PsyOps',
|
||||
defaultRole: 'editor',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '720',
|
||||
name: 'shorty@gmail.com',
|
||||
roles: [
|
||||
{
|
||||
name: 'admin',
|
||||
organization: '667',
|
||||
},
|
||||
{
|
||||
name: 'viewer',
|
||||
organization: '1236',
|
||||
},
|
||||
],
|
||||
provider: 'github',
|
||||
scheme: 'oauth2',
|
||||
superAdmin: false,
|
||||
links: {
|
||||
self: '/chronograf/v1/users/720',
|
||||
},
|
||||
organizations: [
|
||||
{
|
||||
id: '667',
|
||||
name: 'Engineering',
|
||||
defaultRole: 'member',
|
||||
},
|
||||
{
|
||||
id: '1236',
|
||||
name: 'PsyOps',
|
||||
defaultRole: 'editor',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '271',
|
||||
name: 'shawn.ofthe.dead@altavista.yop',
|
||||
roles: [],
|
||||
provider: 'github',
|
||||
scheme: 'oauth2',
|
||||
superAdmin: false,
|
||||
links: {
|
||||
self: '/chronograf/v1/users/271',
|
||||
},
|
||||
organizations: [],
|
||||
},
|
||||
]
|
||||
|
||||
describe('Admin.Chronograf.Reducers', () => {
|
||||
it('it can load all users', () => {
|
||||
const actual = reducer(state, loadUsers({users}))
|
||||
const expected = {
|
||||
users,
|
||||
}
|
||||
|
||||
expect(actual.users).to.deep.equal(expected.users)
|
||||
})
|
||||
})
|
|
@ -1,4 +1,4 @@
|
|||
import reducer from 'src/admin/reducers/admin'
|
||||
import reducer from 'src/admin/reducers/influxdb'
|
||||
|
||||
import {
|
||||
addUser,
|
||||
|
@ -21,7 +21,7 @@ import {
|
|||
filterUsers,
|
||||
addDatabaseDeleteCode,
|
||||
removeDatabaseDeleteCode,
|
||||
} from 'src/admin/actions'
|
||||
} from 'src/admin/actions/influxdb'
|
||||
|
||||
import {
|
||||
NEW_DEFAULT_USER,
|
||||
|
@ -138,7 +138,7 @@ const db2 = {
|
|||
deleteCode: 'DELETE',
|
||||
}
|
||||
|
||||
describe('Admin.Reducers', () => {
|
||||
describe('Admin.InfluxDB.Reducers', () => {
|
||||
describe('Databases', () => {
|
||||
const state = {databases: [db1, db2]}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue