diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5305e8c91..e1043217d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,14 +2,17 @@
### Bug Fixes
1. [#936](https://github.com/influxdata/chronograf/pull/936): Fix leaking sockets for InfluxQL queries
+ 2. [#967](https://github.com/influxdata/chronograf/pull/967): Fix flash of empty graph on auto-refresh when no results were previously returned from a query.
+ 3. [#968](https://github.com/influxdata/chronograf/issue/968): Fix wrong database used in dashboards
### Features
1. [#993](https://github.com/influxdata/chronograf/pull/993): Add Admin page with ability to manage Users, Roles, and Permissions for InfluxDB and Enterprise
2. [#993](https://github.com/influxdata/chronograf/pull/993): Add ability to manage Queries for InfluxDB and Enterprise
### UI Improvements
- 1. [#993](https://github.com/influxdata/chronograf/pull/993): Improve multi-select dropdown
- 2. [#993](https://github.com/influxdata/chronograf/pull/993): Provide better error information to users
+ 1. [#989](https://github.com/influxdata/chronograf/pull/989) Add a canned dashboard for mesos
+ 2. [#993](https://github.com/influxdata/chronograf/pull/993): Improve multi-select dropdown
+ 3. [#993](https://github.com/influxdata/chronograf/pull/993): Provide better error information to users
## v1.2.0-beta4 [2017-02-24]
@@ -25,6 +28,7 @@
4. [#892](https://github.com/influxdata/chronograf/issues/891): Make dashboard visualizations resizable
5. [#893](https://github.com/influxdata/chronograf/issues/893): Persist dashboard visualization position
6. [#922](https://github.com/influxdata/chronograf/issues/922): Additional OAuth2 support for [Heroku](https://github.com/influxdata/chronograf/blob/master/docs/auth.md#heroku) and [Google](https://github.com/influxdata/chronograf/blob/master/docs/auth.md#google)
+ 7. [#781](https://github.com/influxdata/chronograf/issues/781): Add global auto-refresh dropdown to all graph dashboards
### UI Improvements
1. [#905](https://github.com/influxdata/chronograf/pull/905): Make scroll bar thumb element bigger
diff --git a/LICENSE b/LICENSE
index 9ffd67f52..fa74759fc 100644
--- a/LICENSE
+++ b/LICENSE
@@ -17,12 +17,11 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
InfluxData Inc.
-150 Spear Street
-Suite 1750
-San Francisco, CA 94105
-contact@influxdata.com
-
-
+799 Market Street, Suite 400
+San Francisco, CA 94103
+contact@influxdata.com
+
+
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
diff --git a/README.md b/README.md
index 662dc3a94..f4050f062 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,7 @@ Currently, Chronograf offers dashboard templates for the following Telegraf inpu
* [InfluxDB](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/influxdb)
* [Kubernetes](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/kubernetes)
* [Memcached](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/memcached)
+* [Mesos](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/mesos)
* [MongoDB](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/mongodb)
* [MySQL](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/mysql)
* Network
diff --git a/bolt/alerts.go b/bolt/alerts.go
index d67264840..d49f4d3a1 100644
--- a/bolt/alerts.go
+++ b/bolt/alerts.go
@@ -11,8 +11,10 @@ import (
// Ensure AlertsStore implements chronograf.AlertsStore.
var _ chronograf.AlertRulesStore = &AlertsStore{}
+// AlertsBucket is the name of the bucket alert configuration is stored in
var AlertsBucket = []byte("Alerts")
+// AlertsStore represents the bolt implementation of a store for alerts
type AlertsStore struct {
client *Client
}
diff --git a/bolt/client.go b/bolt/client.go
index e34a37828..ae57cc558 100644
--- a/bolt/client.go
+++ b/bolt/client.go
@@ -23,6 +23,7 @@ type Client struct {
DashboardsStore *DashboardsStore
}
+// NewClient initializes all stores
func NewClient() *Client {
c := &Client{Now: time.Now}
c.SourcesStore = &SourcesStore{client: c}
@@ -79,6 +80,7 @@ func (c *Client) Open() error {
return nil
}
+// Close the connection to the bolt database
func (c *Client) Close() error {
if c.db != nil {
return c.db.Close()
diff --git a/bolt/dashboards.go b/bolt/dashboards.go
index 4df9ebf8e..28cf83f41 100644
--- a/bolt/dashboards.go
+++ b/bolt/dashboards.go
@@ -12,8 +12,10 @@ import (
// Ensure DashboardsStore implements chronograf.DashboardsStore.
var _ chronograf.DashboardsStore = &DashboardsStore{}
+// DashboardBucket is the bolt bucket dashboards are stored in
var DashboardBucket = []byte("Dashoard")
+// DashboardsStore is the bolt implementation of storing dashboards
type DashboardsStore struct {
client *Client
IDs chronograf.DashboardID
@@ -81,9 +83,9 @@ func (d *DashboardsStore) Get(ctx context.Context, id chronograf.DashboardID) (c
}
// Delete the dashboard from DashboardsStore
-func (s *DashboardsStore) Delete(ctx context.Context, d chronograf.Dashboard) error {
- if err := s.client.db.Update(func(tx *bolt.Tx) error {
- if err := tx.Bucket(DashboardBucket).Delete(itob(int(d.ID))); err != nil {
+func (d *DashboardsStore) Delete(ctx context.Context, dash chronograf.Dashboard) error {
+ if err := d.client.db.Update(func(tx *bolt.Tx) error {
+ if err := tx.Bucket(DashboardBucket).Delete(itob(int(dash.ID))); err != nil {
return err
}
return nil
@@ -95,16 +97,16 @@ func (s *DashboardsStore) Delete(ctx context.Context, d chronograf.Dashboard) er
}
// Update the dashboard in DashboardsStore
-func (s *DashboardsStore) Update(ctx context.Context, d chronograf.Dashboard) error {
- if err := s.client.db.Update(func(tx *bolt.Tx) error {
+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)
- strID := strconv.Itoa(int(d.ID))
+ strID := strconv.Itoa(int(dash.ID))
if v := b.Get([]byte(strID)); v == nil {
return chronograf.ErrDashboardNotFound
}
- if v, err := internal.MarshalDashboard(d); err != nil {
+ if v, err := internal.MarshalDashboard(dash); err != nil {
return err
} else if err := b.Put([]byte(strID), v); err != nil {
return err
diff --git a/bolt/layouts.go b/bolt/layouts.go
index bd6ce4a8c..e443f80b1 100644
--- a/bolt/layouts.go
+++ b/bolt/layouts.go
@@ -11,8 +11,10 @@ import (
// Ensure LayoutStore implements chronograf.LayoutStore.
var _ chronograf.LayoutStore = &LayoutStore{}
+// LayoutBucket is the bolt bucket layouts are stored in
var LayoutBucket = []byte("Layout")
+// LayoutStore is the bolt implementation to store layouts
type LayoutStore struct {
client *Client
IDs chronograf.ID
diff --git a/bolt/servers.go b/bolt/servers.go
index d9a4ebccb..63bd7c801 100644
--- a/bolt/servers.go
+++ b/bolt/servers.go
@@ -11,8 +11,11 @@ import (
// Ensure ServersStore implements chronograf.ServersStore.
var _ chronograf.ServersStore = &ServersStore{}
+// ServersBucket is the bolt bucket to store lists of servers
var ServersBucket = []byte("Servers")
+// ServersStore is the bolt implementation to store servers in a store.
+// Used store servers that are associated in some way with a source
type ServersStore struct {
client *Client
}
diff --git a/bolt/sources.go b/bolt/sources.go
index 46ced92b8..a2809ff23 100644
--- a/bolt/sources.go
+++ b/bolt/sources.go
@@ -11,8 +11,10 @@ import (
// Ensure SourcesStore implements chronograf.SourcesStore.
var _ chronograf.SourcesStore = &SourcesStore{}
+// SourcesBucket is the bolt bucket used to store source information
var SourcesBucket = []byte("Sources")
+// SourcesStore is a bolt implementation to store time-series source information.
type SourcesStore struct {
client *Client
}
diff --git a/canned/apps.go b/canned/apps.go
index 1fd8440d3..8ad626e8d 100644
--- a/canned/apps.go
+++ b/canned/apps.go
@@ -11,6 +11,7 @@ import (
"github.com/influxdata/chronograf"
)
+// AppExt is the the file extension searched for in the directory for layout files
const AppExt = ".json"
// Apps are canned JSON layouts. Implements LayoutStore.
@@ -25,6 +26,7 @@ type Apps struct {
Logger chronograf.Logger
}
+// NewApps constructs a layout store wrapping a file system directory
func NewApps(dir string, ids chronograf.ID, logger chronograf.Logger) chronograf.LayoutStore {
return &Apps{
Dir: dir,
@@ -63,14 +65,14 @@ func createLayout(file string, layout chronograf.Layout) error {
defer h.Close()
if octets, err := json.MarshalIndent(layout, " ", " "); err != nil {
return chronograf.ErrLayoutInvalid
- } else {
- if _, err := h.Write(octets); err != nil {
- return err
- }
+ } else if _, err := h.Write(octets); err != nil {
+ return err
}
+
return nil
}
+// All returns all layouts from the directory
func (a *Apps) All(ctx context.Context) ([]chronograf.Layout, error) {
files, err := a.ReadDir(a.Dir)
if err != nil {
@@ -91,6 +93,7 @@ func (a *Apps) All(ctx context.Context) ([]chronograf.Layout, error) {
return layouts, nil
}
+// Add creates a new layout within the directory
func (a *Apps) Add(ctx context.Context, layout chronograf.Layout) (chronograf.Layout, error) {
var err error
layout.ID, err = a.IDs.Generate()
@@ -118,6 +121,7 @@ func (a *Apps) Add(ctx context.Context, layout chronograf.Layout) (chronograf.La
return layout, nil
}
+// Delete removes a layout file from the directory
func (a *Apps) Delete(ctx context.Context, layout chronograf.Layout) error {
_, file, err := a.idToFile(layout.ID)
if err != nil {
@@ -134,6 +138,7 @@ func (a *Apps) Delete(ctx context.Context, layout chronograf.Layout) error {
return nil
}
+// Get returns an app file from the layout directory
func (a *Apps) Get(ctx context.Context, ID string) (chronograf.Layout, error) {
l, file, err := a.idToFile(ID)
if err != nil {
@@ -157,6 +162,7 @@ func (a *Apps) Get(ctx context.Context, ID string) (chronograf.Layout, error) {
return l, nil
}
+// Update replaces a layout from the file system directory
func (a *Apps) Update(ctx context.Context, layout chronograf.Layout) error {
l, _, err := a.idToFile(layout.ID)
if err != nil {
diff --git a/canned/bin.go b/canned/bin.go
index 2c32e9e04..1b5fd4399 100644
--- a/canned/bin.go
+++ b/canned/bin.go
@@ -10,6 +10,7 @@ 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 {
Logger chronograf.Logger
}
diff --git a/canned/mesos.json b/canned/mesos.json
new file mode 100644
index 000000000..370f67f0c
--- /dev/null
+++ b/canned/mesos.json
@@ -0,0 +1,131 @@
+{
+ "id": "0fa47984-825b-46f1-9ca5-0366e3220000",
+ "measurement": "mesos",
+ "app": "mesos",
+ "autoflow": true,
+ "cells": [
+ {
+ "x": 0,
+ "y": 0,
+ "w": 4,
+ "h": 4,
+ "i": "0fa47984-825b-46f1-9ca5-0366e3220007",
+ "name": "Mesos Active Slaves",
+ "queries": [
+ {
+ "query": "SELECT max(\"master/slaves_active\") AS \"Active Slaves\" FROM \"mesos\"",
+ "label": "count",
+ "groupbys": [],
+ "wheres": []
+ }
+ ]
+ },
+ {
+ "x": 0,
+ "y": 0,
+ "w": 4,
+ "h": 4,
+ "i": "0fa47984-825b-46f1-9ca5-0366e3220001",
+ "name": "Mesos Tasks Active",
+ "queries": [
+ {
+ "query": "SELECT max(\"master/tasks_running\") AS \"num tasks\" FROM \"mesos\"",
+ "label": "count",
+ "groupbys": [],
+ "wheres": []
+ }
+ ]
+ },
+ {
+ "x": 0,
+ "y": 0,
+ "w": 4,
+ "h": 4,
+ "i": "0fa47984-825b-46f1-9ca5-0366e3220004",
+ "name": "Mesos Tasks",
+ "queries": [
+ {
+ "query": "SELECT non_negative_derivative(max(\"master/tasks_finished\"), 60s) AS \"tasks finished\" FROM \"mesos\"",
+ "label": "count",
+ "groupbys": [],
+ "wheres": []
+ },
+ {
+ "query": "SELECT non_negative_derivative(max(\"master/tasks_failed\"), 60s) AS \"tasks failed\" FROM \"mesos\"",
+ "groupbys": [],
+ "wheres": []
+ },
+ {
+ "query": "SELECT non_negative_derivative(max(\"master/tasks_killed\"), 60s) AS \"tasks killed\" FROM \"mesos\"",
+ "groupbys": [],
+ "wheres": []
+ }
+ ]
+ },
+ {
+ "x": 0,
+ "y": 0,
+ "w": 4,
+ "h": 4,
+ "i": "0fa47984-825b-46f1-9ca5-0366e3220005",
+ "name": "Mesos Outstanding offers",
+ "queries": [
+ {
+ "query": "SELECT max(\"master/outstanding_offers\") AS \"Outstanding Offers\" FROM \"mesos\"",
+ "label": "count",
+ "groupbys": [],
+ "wheres": []
+ }
+ ]
+ },
+ {
+ "x": 0,
+ "y": 0,
+ "w": 4,
+ "h": 4,
+ "i": "0fa47984-825b-46f1-9ca5-0366e3220002",
+ "name": "Mesos Available/Used CPUs",
+ "queries": [
+ {
+ "query": "SELECT max(\"master/cpus_total\") AS \"cpu total\", max(\"master/cpus_used\") AS \"cpu used\" FROM \"mesos\"",
+ "label": "count",
+ "groupbys": [],
+ "wheres": []
+ }
+ ]
+ },
+ {
+ "x": 0,
+ "y": 0,
+ "w": 4,
+ "h": 4,
+ "i": "0fa47984-825b-46f1-9ca5-0366e3220003",
+ "name": "Mesos Available/Used Memory",
+ "queries": [
+ {
+ "query": "SELECT max(\"master/mem_total\") AS \"memory total\", max(\"master/mem_used\") AS \"memory used\" FROM \"mesos\"",
+ "label": "MB",
+ "groupbys": [],
+ "wheres": []
+ }
+ ]
+ },
+ {
+ "x": 0,
+ "y": 0,
+ "w": 4,
+ "h": 4,
+ "i": "0fa47984-825b-46f1-9ca5-0366e3220008",
+ "name": "Mesos Master Uptime",
+ "type": "single-stat",
+ "queries": [
+ {
+ "query": "SELECT max(\"master/uptime_secs\") AS \"uptime\" FROM \"mesos\"",
+ "label": "Seconds",
+ "groupbys": [],
+ "wheres": []
+ }
+ ]
+ }
+ ]
+}
diff --git a/chronograf.go b/chronograf.go
index 1a8b7730d..fc624bea4 100644
--- a/chronograf.go
+++ b/chronograf.go
@@ -2,6 +2,7 @@ package chronograf
import (
"context"
+ "io"
"net/http"
)
@@ -32,12 +33,13 @@ func (e Error) Error() string {
type Logger interface {
Debug(...interface{})
Info(...interface{})
- Warn(...interface{})
Error(...interface{})
- Fatal(...interface{})
- Panic(...interface{})
WithField(string, interface{}) Logger
+
+ // Logger can be transformed into an io.Writer.
+ // That writer is the end of an io.Pipe and it is your responsibility to close it.
+ Writer() *io.PipeWriter
}
// Assets returns a handler to serve the website.
diff --git a/dist/dir.go b/dist/dir.go
index 828ef231d..1f4ac90b9 100644
--- a/dist/dir.go
+++ b/dist/dir.go
@@ -11,6 +11,7 @@ type Dir struct {
dir http.Dir
}
+// NewDir constructs a Dir with a default file
func NewDir(dir, def string) Dir {
return Dir{
Default: def,
diff --git a/enterprise/enterprise.go b/enterprise/enterprise.go
index 3a3999349..189e5a150 100644
--- a/enterprise/enterprise.go
+++ b/enterprise/enterprise.go
@@ -32,6 +32,8 @@ type Ctrl interface {
DeleteRole(ctx context.Context, name string) error
SetRolePerms(ctx context.Context, name string, perms Permissions) error
SetRoleUsers(ctx context.Context, name string, users []string) error
+ AddRoleUsers(ctx context.Context, name string, users []string) error
+ RemoveRoleUsers(ctx context.Context, name string, users []string) error
}
// Client is a device for retrieving time series data from an Influx Enterprise
@@ -148,7 +150,7 @@ func (c *Client) Roles(ctx context.Context) (chronograf.RolesStore, error) {
return c.RolesStore, nil
}
-// Allowances returns all Influx Enterprise permission strings
+// Permissions returns all Influx Enterprise permission strings
func (c *Client) Permissions(context.Context) chronograf.Permissions {
all := chronograf.Allowances{
"NoPermissions",
diff --git a/enterprise/meta.go b/enterprise/meta.go
index 399b057b3..cd08730b7 100644
--- a/enterprise/meta.go
+++ b/enterprise/meta.go
@@ -272,32 +272,52 @@ func (m *MetaClient) SetRolePerms(ctx context.Context, name string, perms Permis
return m.Post(ctx, "/role", a, nil)
}
-// RemoveAllRoleUsers removes all users from a role
-func (m *MetaClient) RemoveAllRoleUsers(ctx context.Context, name string) error {
+// SetRoleUsers removes all users and then adds the requested users to role
+func (m *MetaClient) SetRoleUsers(ctx context.Context, name string, users []string) error {
role, err := m.Role(ctx, name)
if err != nil {
return err
}
-
- // No users to remove
- if len(role.Users) == 0 {
- return nil
- }
-
- a := &RoleAction{
- Action: "remove-users",
- Role: role,
- }
- return m.Post(ctx, "/role", a, nil)
-}
-
-// SetRoleUsers removes all users and then adds the requested users to role
-func (m *MetaClient) SetRoleUsers(ctx context.Context, name string, users []string) error {
- err := m.RemoveAllRoleUsers(ctx, name)
- if err != nil {
+ revoke, add := Difference(users, role.Users)
+ if err := m.RemoveRoleUsers(ctx, name, revoke); err != nil {
return err
}
+ return m.AddRoleUsers(ctx, name, add)
+}
+
+// Difference compares two sets and returns a set to be removed and a set to be added
+func Difference(wants []string, haves []string) (revoke []string, add []string) {
+ for _, want := range wants {
+ found := false
+ for _, got := range haves {
+ if want != got {
+ continue
+ }
+ found = true
+ }
+ if !found {
+ add = append(add, want)
+ }
+ }
+ for _, got := range haves {
+ found := false
+ for _, want := range wants {
+ if want != got {
+ continue
+ }
+ found = true
+ break
+ }
+ if !found {
+ revoke = append(revoke, got)
+ }
+ }
+ return
+}
+
+// AddRoleUsers updates a role to have additional users.
+func (m *MetaClient) AddRoleUsers(ctx context.Context, name string, users []string) error {
// No permissions to add, so, role is in the right state
if len(users) == 0 {
return nil
@@ -313,6 +333,23 @@ func (m *MetaClient) SetRoleUsers(ctx context.Context, name string, users []stri
return m.Post(ctx, "/role", a, nil)
}
+// RemoveRoleUsers updates a role to remove some users.
+func (m *MetaClient) RemoveRoleUsers(ctx context.Context, name string, users []string) error {
+ // No permissions to add, so, role is in the right state
+ if len(users) == 0 {
+ return nil
+ }
+
+ a := &RoleAction{
+ Action: "remove-users",
+ Role: &Role{
+ Name: name,
+ Users: users,
+ },
+ }
+ return m.Post(ctx, "/role", a, nil)
+}
+
// Post is a helper function to POST to Influx Enterprise
func (m *MetaClient) Post(ctx context.Context, path string, action interface{}, params map[string]string) error {
b, err := json.Marshal(action)
diff --git a/enterprise/meta_test.go b/enterprise/meta_test.go
index 2823c1256..96150d054 100644
--- a/enterprise/meta_test.go
+++ b/enterprise/meta_test.go
@@ -70,7 +70,7 @@ func TestMetaClient_ShowCluster(t *testing.T) {
http.StatusBadGateway,
nil,
nil,
- fmt.Errorf("Time circuits on. Flux Capacitor... fluxxing."),
+ fmt.Errorf("time circuits on. Flux Capacitor... fluxxing"),
),
},
wantErr: true,
@@ -214,7 +214,7 @@ func TestMetaClient_Users(t *testing.T) {
http.StatusOK,
[]byte(`{"users":[{"name":"admin","hash":"1234","permissions":{"":["ViewAdmin","ViewChronograf"]}}]}`),
nil,
- fmt.Errorf("Time circuits on. Flux Capacitor... fluxxing."),
+ fmt.Errorf("time circuits on. Flux Capacitor... fluxxing"),
),
},
args: args{
@@ -1252,12 +1252,11 @@ func TestMetaClient_SetRoleUsers(t *testing.T) {
name string
fields fields
args args
- wantRm string
- wantAdd string
+ wants []string
wantErr bool
}{
{
- name: "Successful set users role",
+ name: "Successful set users role (remove user from role)",
fields: fields{
URL: &url.URL{
Host: "twinpinesmall.net:8091",
@@ -1274,7 +1273,7 @@ func TestMetaClient_SetRoleUsers(t *testing.T) {
ctx: context.Background(),
name: "admin",
},
- wantRm: `{"action":"remove-users","role":{"name":"admin","permissions":{"":["ViewAdmin","ViewChronograf"]},"users":["marty"]}}`,
+ wants: []string{`{"action":"remove-users","role":{"name":"admin","users":["marty"]}}`},
},
{
name: "Successful set single user role",
@@ -1285,7 +1284,7 @@ func TestMetaClient_SetRoleUsers(t *testing.T) {
},
client: NewMockClient(
http.StatusOK,
- []byte(`{"roles":[{"name":"admin","users":["marty"],"permissions":{"":["ViewAdmin","ViewChronograf"]}}]}`),
+ []byte(`{"roles":[{"name":"admin","users":[],"permissions":{"":["ViewAdmin","ViewChronograf"]}}]}`),
nil,
nil,
),
@@ -1295,8 +1294,9 @@ func TestMetaClient_SetRoleUsers(t *testing.T) {
name: "admin",
users: []string{"marty"},
},
- wantRm: `{"action":"remove-users","role":{"name":"admin","permissions":{"":["ViewAdmin","ViewChronograf"]},"users":["marty"]}}`,
- wantAdd: `{"action":"add-users","role":{"name":"admin","users":["marty"]}}`,
+ wants: []string{
+ `{"action":"add-users","role":{"name":"admin","users":["marty"]}}`,
+ },
},
}
for _, tt := range tests {
@@ -1312,8 +1312,8 @@ func TestMetaClient_SetRoleUsers(t *testing.T) {
continue
}
reqs := tt.fields.client.(*MockClient).Requests
- if len(reqs) < 2 {
- t.Errorf("%q. MetaClient.SetRoleUsers() expected 2 but got %d", tt.name, len(reqs))
+ if len(reqs) != len(tt.wants)+1 {
+ t.Errorf("%q. MetaClient.SetRoleUsers() expected %d but got %d", tt.name, len(tt.wants)+1, len(reqs))
continue
}
@@ -1324,21 +1324,8 @@ func TestMetaClient_SetRoleUsers(t *testing.T) {
if usr.URL.Path != "/role" {
t.Errorf("%q. MetaClient.SetRoleUsers() expected /user path but got %s", tt.name, usr.URL.Path)
}
-
- prm := reqs[1]
- if prm.Method != "POST" {
- t.Errorf("%q. MetaClient.SetRoleUsers() expected GET method", tt.name)
- }
- if prm.URL.Path != "/role" {
- t.Errorf("%q. MetaClient.SetRoleUsers() expected /role path but got %s", tt.name, prm.URL.Path)
- }
-
- got, _ := ioutil.ReadAll(prm.Body)
- if string(got) != tt.wantRm {
- t.Errorf("%q. MetaClient.SetRoleUsers() = %v, want %v", tt.name, string(got), tt.wantRm)
- }
- if tt.wantAdd != "" {
- prm := reqs[2]
+ for i := range tt.wants {
+ prm := reqs[i+1]
if prm.Method != "POST" {
t.Errorf("%q. MetaClient.SetRoleUsers() expected GET method", tt.name)
}
@@ -1347,8 +1334,8 @@ func TestMetaClient_SetRoleUsers(t *testing.T) {
}
got, _ := ioutil.ReadAll(prm.Body)
- if string(got) != tt.wantAdd {
- t.Errorf("%q. MetaClient.SetRoleUsers() = %v, want %v", tt.name, string(got), tt.wantAdd)
+ if string(got) != tt.wants[i] {
+ t.Errorf("%q. MetaClient.SetRoleUsers() = %v, want %v", tt.name, string(got), tt.wants[i])
}
}
}
diff --git a/enterprise/mocks_test.go b/enterprise/mocks_test.go
index 08e025362..6e9cc95cd 100644
--- a/enterprise/mocks_test.go
+++ b/enterprise/mocks_test.go
@@ -88,6 +88,14 @@ func (cc *ControlClient) SetRoleUsers(ctx context.Context, name string, users []
return nil
}
+func (cc *ControlClient) AddRoleUsers(ctx context.Context, name string, users []string) error {
+ return nil
+}
+
+func (cc *ControlClient) RemoveRoleUsers(ctx context.Context, name string, users []string) error {
+ return nil
+}
+
type TimeSeries struct {
URLs []string
Response Response
diff --git a/enterprise/users.go b/enterprise/users.go
index 02605d05a..68c04d193 100644
--- a/enterprise/users.go
+++ b/enterprise/users.go
@@ -22,7 +22,13 @@ func (c *UserStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.Us
if err := c.Ctrl.SetUserPerms(ctx, u.Name, perms); err != nil {
return nil, err
}
- return u, nil
+ for _, role := range u.Roles {
+ if err := c.Ctrl.AddRoleUsers(ctx, role.Name, []string{u.Name}); err != nil {
+ return nil, err
+ }
+ }
+
+ return c.Get(ctx, u.Name)
}
// Delete the User from Influx Enterprise
@@ -63,11 +69,45 @@ func (c *UserStore) Update(ctx context.Context, u *chronograf.User) error {
if u.Passwd != "" {
return c.Ctrl.ChangePassword(ctx, u.Name, u.Passwd)
}
- if u.Permissions != nil {
- perms := ToEnterprise(u.Permissions)
- return c.Ctrl.SetUserPerms(ctx, u.Name, perms)
+
+ // Make a list of the roles we want this user to have:
+ want := make([]string, len(u.Roles))
+ for i, r := range u.Roles {
+ want[i] = r.Name
}
- return nil
+
+ // Find the list of all roles this user is currently in
+ userRoles, err := c.UserRoles(ctx)
+ if err != nil {
+ return nil
+ }
+ // Make a list of the roles the user currently has
+ roles := userRoles[u.Name]
+ have := make([]string, len(roles.Roles))
+ for i, r := range roles.Roles {
+ have[i] = r.Name
+ }
+
+ // Calculate the roles the user will be removed from and the roles the user
+ // will be added to.
+ revoke, add := Difference(want, have)
+
+ // First, add the user to the new roles
+ for _, role := range add {
+ if err := c.Ctrl.AddRoleUsers(ctx, role, []string{u.Name}); err != nil {
+ return err
+ }
+ }
+
+ // ... and now remove the user from an extra roles
+ for _, role := range revoke {
+ if err := c.Ctrl.RemoveRoleUsers(ctx, role, []string{u.Name}); err != nil {
+ return err
+ }
+ }
+
+ perms := ToEnterprise(u.Permissions)
+ return c.Ctrl.SetUserPerms(ctx, u.Name, perms)
}
// All is all users in influx
diff --git a/enterprise/users_test.go b/enterprise/users_test.go
index 54dc7f502..9cc0cddc5 100644
--- a/enterprise/users_test.go
+++ b/enterprise/users_test.go
@@ -36,6 +36,22 @@ func TestClient_Add(t *testing.T) {
setUserPerms: func(ctx context.Context, name string, perms enterprise.Permissions) error {
return nil
},
+ user: func(ctx context.Context, name string) (*enterprise.User, error) {
+ return &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{
@@ -46,8 +62,82 @@ func TestClient_Add(t *testing.T) {
},
},
want: &chronograf.User{
- Name: "marty",
- Passwd: "johnny be good",
+ Name: "marty",
+ Permissions: chronograf.Permissions{
+ {
+ Scope: chronograf.AllScope,
+ Allowed: chronograf.Allowances{"ViewChronograf", "ReadData", "WriteData"},
+ },
+ },
+ Roles: []chronograf.Role{},
+ },
+ },
+ {
+ name: "Successful Create User with roles",
+ fields: fields{
+ Ctrl: &mockCtrl{
+ createUser: func(ctx context.Context, name, passwd string) error {
+ return nil
+ },
+ setUserPerms: func(ctx context.Context, name string, perms enterprise.Permissions) error {
+ return nil
+ },
+ user: func(ctx context.Context, name string) (*enterprise.User, error) {
+ return &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{
+ "marty": enterprise.Roles{
+ Roles: []enterprise.Role{
+ {
+ Name: "admin",
+ },
+ },
+ },
+ }, nil
+ },
+ addRoleUsers: func(ctx context.Context, name string, users []string) error {
+ return nil
+ },
+ },
+ },
+ args: args{
+ ctx: context.Background(),
+ u: &chronograf.User{
+ Name: "marty",
+ Passwd: "johnny be good",
+ Roles: []chronograf.Role{
+ {
+ Name: "admin",
+ },
+ },
+ },
+ },
+ want: &chronograf.User{
+ Name: "marty",
+ Permissions: chronograf.Permissions{
+ {
+ Scope: chronograf.AllScope,
+ Allowed: chronograf.Allowances{"ViewChronograf", "ReadData", "WriteData"},
+ },
+ },
+ Roles: []chronograf.Role{
+ {
+ Name: "admin",
+ Users: []chronograf.User{},
+ Permissions: chronograf.Permissions{},
+ },
+ },
},
},
{
@@ -80,7 +170,7 @@ func TestClient_Add(t *testing.T) {
continue
}
if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("%q. Client.Add() = %v, want %v", tt.name, got, tt.want)
+ t.Errorf("%q. Client.Add() = \n%#v\n, want \n%#v\n", tt.name, got, tt.want)
}
}
}
@@ -353,6 +443,9 @@ func TestClient_Update(t *testing.T) {
setUserPerms: func(ctx context.Context, name string, perms enterprise.Permissions) error {
return nil
},
+ userRoles: func(ctx context.Context) (map[string]enterprise.Roles, error) {
+ return map[string]enterprise.Roles{}, nil
+ },
},
},
args: args{
@@ -369,6 +462,40 @@ func TestClient_Update(t *testing.T) {
},
wantErr: false,
},
+ {
+ name: "Success setting permissions and roles for user",
+ fields: fields{
+ Ctrl: &mockCtrl{
+ setUserPerms: func(ctx context.Context, name string, perms enterprise.Permissions) error {
+ return nil
+ },
+ addRoleUsers: func(ctx context.Context, name string, users []string) error {
+ return nil
+ },
+ userRoles: func(ctx context.Context) (map[string]enterprise.Roles, error) {
+ return map[string]enterprise.Roles{}, nil
+ },
+ },
+ },
+ args: args{
+ ctx: context.Background(),
+ u: &chronograf.User{
+ Name: "marty",
+ Permissions: chronograf.Permissions{
+ {
+ Scope: chronograf.AllScope,
+ Allowed: chronograf.Allowances{"ViewChronograf", "KapacitorAPI"},
+ },
+ },
+ Roles: []chronograf.Role{
+ {
+ Name: "adminrole",
+ },
+ },
+ },
+ },
+ wantErr: false,
+ },
{
name: "Failure setting permissions User",
fields: fields{
@@ -376,6 +503,9 @@ func TestClient_Update(t *testing.T) {
setUserPerms: func(ctx context.Context, name string, perms enterprise.Permissions) error {
return fmt.Errorf("They found me, I don't know how, but they found me.")
},
+ userRoles: func(ctx context.Context) (map[string]enterprise.Roles, error) {
+ return map[string]enterprise.Roles{}, nil
+ },
},
},
args: args{
@@ -573,12 +703,14 @@ type mockCtrl struct {
userRoles func(ctx context.Context) (map[string]enterprise.Roles, error)
- roles func(ctx context.Context, name *string) (*enterprise.Roles, error)
- role func(ctx context.Context, name string) (*enterprise.Role, error)
- createRole func(ctx context.Context, name string) error
- deleteRole func(ctx context.Context, name string) error
- setRolePerms func(ctx context.Context, name string, perms enterprise.Permissions) error
- setRoleUsers func(ctx context.Context, name string, users []string) error
+ roles func(ctx context.Context, name *string) (*enterprise.Roles, error)
+ role func(ctx context.Context, name string) (*enterprise.Role, error)
+ createRole func(ctx context.Context, name string) error
+ deleteRole func(ctx context.Context, name string) error
+ setRolePerms func(ctx context.Context, name string, perms enterprise.Permissions) error
+ setRoleUsers func(ctx context.Context, name string, users []string) error
+ addRoleUsers func(ctx context.Context, name string, users []string) error
+ removeRoleUsers func(ctx context.Context, name string, users []string) error
}
func (m *mockCtrl) ShowCluster(ctx context.Context) (*enterprise.Cluster, error) {
@@ -636,3 +768,11 @@ func (m *mockCtrl) SetRolePerms(ctx context.Context, name string, perms enterpri
func (m *mockCtrl) SetRoleUsers(ctx context.Context, name string, users []string) error {
return m.setRoleUsers(ctx, name, users)
}
+
+func (m *mockCtrl) AddRoleUsers(ctx context.Context, name string, users []string) error {
+ return m.addRoleUsers(ctx, name, users)
+}
+
+func (m *mockCtrl) RemoveRoleUsers(ctx context.Context, name string, users []string) error {
+ return m.removeRoleUsers(ctx, name, users)
+}
diff --git a/influx/permissions.go b/influx/permissions.go
index 1548e794f..809aff953 100644
--- a/influx/permissions.go
+++ b/influx/permissions.go
@@ -8,8 +8,10 @@ import (
)
var (
- // AllowAll means a user gets both read and write permissions
- AllowAll = chronograf.Allowances{"WRITE", "READ"}
+ // AllowAllDB means a user gets both read and write permissions for a db
+ AllowAllDB = chronograf.Allowances{"WRITE", "READ"}
+ // AllowAllAdmin means a user gets both read and write permissions for an admin
+ AllowAllAdmin = chronograf.Allowances{"ALL"}
// AllowRead means a user is only able to read the database.
AllowRead = chronograf.Allowances{"READ"}
// AllowWrite means a user is able to only write to the database
@@ -31,11 +33,11 @@ func (c *Client) Permissions(context.Context) chronograf.Permissions {
return chronograf.Permissions{
{
Scope: chronograf.AllScope,
- Allowed: AllowAll,
+ Allowed: AllowAllAdmin,
},
{
Scope: chronograf.DBScope,
- Allowed: AllowAll,
+ Allowed: AllowAllDB,
},
}
}
@@ -90,7 +92,7 @@ func (r *showResults) Permissions() chronograf.Permissions {
}
switch priv {
case AllPrivileges, All:
- c.Allowed = AllowAll
+ c.Allowed = AllowAllDB
case Read:
c.Allowed = AllowRead
case Write:
@@ -111,7 +113,7 @@ func adminPerms() chronograf.Permissions {
return []chronograf.Permission{
{
Scope: chronograf.AllScope,
- Allowed: AllowAll,
+ Allowed: AllowAllAdmin,
},
}
}
diff --git a/influx/permissions_test.go b/influx/permissions_test.go
index 956e706a8..9aca7aa74 100644
--- a/influx/permissions_test.go
+++ b/influx/permissions_test.go
@@ -318,7 +318,7 @@ func Test_showResults_Users(t *testing.T) {
Permissions: chronograf.Permissions{
{
Scope: chronograf.AllScope,
- Allowed: chronograf.Allowances{"WRITE", "READ"},
+ Allowed: chronograf.Allowances{"ALL"},
},
},
},
diff --git a/influx/users.go b/influx/users.go
index bca83fec3..a8e10bcfa 100644
--- a/influx/users.go
+++ b/influx/users.go
@@ -16,8 +16,12 @@ func (c *Client) Add(ctx context.Context, u *chronograf.User) (*chronograf.User,
if err != nil {
return nil, err
}
-
- return u, nil
+ for _, p := range u.Permissions {
+ if err := c.grantPermission(ctx, u.Name, p); err != nil {
+ return nil, err
+ }
+ }
+ return c.Get(ctx, u.Name)
}
// Delete the User from InfluxDB
diff --git a/influx/users_test.go b/influx/users_test.go
index f486e13a9..9922d4e51 100644
--- a/influx/users_test.go
+++ b/influx/users_test.go
@@ -97,12 +97,12 @@ func TestClient_Add(t *testing.T) {
u *chronograf.User
}
tests := []struct {
- name string
- args args
- status int
- want *chronograf.User
- wantQuery string
- wantErr bool
+ name string
+ args args
+ status int
+ want *chronograf.User
+ wantQueries []string
+ wantErr bool
}{
{
name: "Create User",
@@ -114,10 +114,57 @@ func TestClient_Add(t *testing.T) {
Passwd: "Dont Need Roads",
},
},
- wantQuery: `CREATE USER "docbrown" WITH PASSWORD 'Dont Need Roads'`,
+ wantQueries: []string{
+ `CREATE USER "docbrown" WITH PASSWORD 'Dont Need Roads'`,
+ `SHOW USERS`,
+ `SHOW GRANTS FOR "docbrown"`,
+ },
want: &chronograf.User{
- Name: "docbrown",
- Passwd: "Dont Need Roads",
+ Name: "docbrown",
+ Permissions: chronograf.Permissions{
+ chronograf.Permission{
+ Scope: chronograf.AllScope,
+ Allowed: chronograf.Allowances{
+ "ALL",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "Create User with permissions",
+ status: http.StatusOK,
+ args: args{
+ ctx: context.Background(),
+ u: &chronograf.User{
+ Name: "docbrown",
+ Passwd: "Dont Need Roads",
+ Permissions: chronograf.Permissions{
+ chronograf.Permission{
+ Scope: chronograf.AllScope,
+ Allowed: chronograf.Allowances{
+ "ALL",
+ },
+ },
+ },
+ },
+ },
+ wantQueries: []string{
+ `CREATE USER "docbrown" WITH PASSWORD 'Dont Need Roads'`,
+ `GRANT ALL PRIVILEGES TO "docbrown"`,
+ `SHOW USERS`,
+ `SHOW GRANTS FOR "docbrown"`,
+ },
+ want: &chronograf.User{
+ Name: "docbrown",
+ Permissions: chronograf.Permissions{
+ chronograf.Permission{
+ Scope: chronograf.AllScope,
+ Allowed: chronograf.Allowances{
+ "ALL",
+ },
+ },
+ },
},
},
{
@@ -130,19 +177,19 @@ func TestClient_Add(t *testing.T) {
Passwd: "Dont Need Roads",
},
},
- wantQuery: `CREATE USER "docbrown" WITH PASSWORD 'Dont Need Roads'`,
- wantErr: true,
+ wantQueries: []string{`CREATE USER "docbrown" WITH PASSWORD 'Dont Need Roads'`},
+ wantErr: true,
},
}
for _, tt := range tests {
- query := ""
+ queries := []string{}
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")
+ queries = append(queries, r.URL.Query().Get("q"))
rw.WriteHeader(tt.status)
- rw.Write([]byte(`{"results":[{}]}`))
+ rw.Write([]byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`))
}))
u, _ := url.Parse(ts.URL)
c := &Client{
@@ -155,9 +202,16 @@ func TestClient_Add(t *testing.T) {
t.Errorf("%q. Client.Add() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
- if tt.wantQuery != query {
- t.Errorf("%q. Client.Add() query = %v, want %v", tt.name, query, tt.wantQuery)
+ if len(tt.wantQueries) != len(queries) {
+ t.Errorf("%q. Client.Add() queries = %v, want %v", tt.name, queries, tt.wantQueries)
+ continue
}
+ for i := range tt.wantQueries {
+ if tt.wantQueries[i] != queries[i] {
+ t.Errorf("%q. Client.Add() query = %v, want %v", tt.name, queries[i], tt.wantQueries[i])
+ }
+ }
+
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. Client.Add() = %v, want %v", tt.name, got, tt.want)
}
@@ -275,7 +329,7 @@ func TestClient_Get(t *testing.T) {
Permissions: chronograf.Permissions{
chronograf.Permission{
Scope: "all",
- Allowed: []string{"WRITE", "READ"},
+ Allowed: []string{"ALL"},
},
chronograf.Permission{
Scope: "database",
@@ -548,7 +602,7 @@ func TestClient_All(t *testing.T) {
Permissions: chronograf.Permissions{
chronograf.Permission{
Scope: "all",
- Allowed: []string{"WRITE", "READ"},
+ Allowed: []string{"ALL"},
},
chronograf.Permission{
Scope: "database",
@@ -562,7 +616,7 @@ func TestClient_All(t *testing.T) {
Permissions: chronograf.Permissions{
chronograf.Permission{
Scope: "all",
- Allowed: []string{"WRITE", "READ"},
+ Allowed: []string{"ALL"},
},
chronograf.Permission{
Scope: "database",
@@ -688,7 +742,7 @@ func TestClient_Update(t *testing.T) {
Permissions: chronograf.Permissions{
{
Scope: "all",
- Allowed: []string{"WRITE", "READ"},
+ Allowed: []string{"all"},
},
{
Scope: "database",
@@ -743,7 +797,7 @@ func TestClient_Update(t *testing.T) {
Permissions: chronograf.Permissions{
{
Scope: "all",
- Allowed: []string{"WRITE", "READ"},
+ Allowed: []string{"all"},
},
{
Scope: "database",
@@ -800,6 +854,34 @@ func TestClient_Update(t *testing.T) {
`REVOKE ALL PRIVILEGES FROM "docbrown"`,
},
},
+ {
+ name: "Revoke some",
+ statusUsers: http.StatusOK,
+ showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",false],["reader",false]]}]}]}`),
+ statusGrants: http.StatusOK,
+ showGrants: []byte(`{"results":[]}`),
+ statusRevoke: http.StatusOK,
+ revoke: []byte(`{"results":[]}`),
+ statusGrant: http.StatusOK,
+ grant: []byte(`{"results":[]}`),
+ args: args{
+ ctx: context.Background(),
+ u: &chronograf.User{
+ Name: "docbrown",
+ Permissions: chronograf.Permissions{
+ {
+ Scope: "all",
+ Allowed: []string{"ALL"},
+ },
+ },
+ },
+ },
+ want: []string{
+ `SHOW USERS`,
+ `SHOW GRANTS FOR "docbrown"`,
+ `GRANT ALL PRIVILEGES TO "docbrown"`,
+ },
+ },
{
name: "Fail users",
statusUsers: http.StatusBadRequest,
diff --git a/kapacitor/http_out.go b/kapacitor/http_out.go
index 848e44a97..45bb0b36e 100644
--- a/kapacitor/http_out.go
+++ b/kapacitor/http_out.go
@@ -6,8 +6,10 @@ import (
"github.com/influxdata/chronograf"
)
+// HTTPEndpoint is the default location of the tickscript output
const HTTPEndpoint = "output"
+// HTTPOut adds a kapacitor httpOutput to a tickscript
func HTTPOut(rule chronograf.AlertRule) (string, error) {
return fmt.Sprintf(`trigger|httpOut('%s')`, HTTPEndpoint), nil
}
diff --git a/kapacitor/operators.go b/kapacitor/operators.go
index 745789fa6..869c6ec23 100644
--- a/kapacitor/operators.go
+++ b/kapacitor/operators.go
@@ -3,8 +3,8 @@ package kapacitor
import "fmt"
const (
- GreaterThan = "greater than"
- LessThan = "less than"
+ greaterThan = "greater than"
+ lessThan = "less than"
LessThanEqual = "equal to or less than"
GreaterThanEqual = "equal to or greater"
Equal = "equal to"
@@ -16,9 +16,9 @@ const (
// kapaOperator converts UI strings to kapacitor operators
func kapaOperator(operator string) (string, error) {
switch operator {
- case GreaterThan:
+ case greaterThan:
return ">", nil
- case LessThan:
+ case lessThan:
return "<", nil
case LessThanEqual:
return "<=", nil
diff --git a/kapacitor/validate.go b/kapacitor/validate.go
index ed8dca32a..b7984fc16 100644
--- a/kapacitor/validate.go
+++ b/kapacitor/validate.go
@@ -3,7 +3,6 @@ package kapacitor
import (
"bytes"
"fmt"
- "log"
"time"
"github.com/influxdata/chronograf"
@@ -25,7 +24,6 @@ func ValidateAlert(service string) error {
func formatTick(tickscript string) (chronograf.TICKScript, error) {
node, err := ast.Parse(tickscript)
if err != nil {
- log.Fatalf("parse execution: %s", err)
return "", err
}
@@ -41,6 +39,9 @@ func validateTick(script chronograf.TICKScript) error {
return err
}
+// deadman is an empty implementation of a kapacitor DeadmanService to allow CreatePipeline
+var _ pipeline.DeadmanService = &deadman{}
+
type deadman struct {
interval time.Duration
threshold float64
diff --git a/kapacitor/vars.go b/kapacitor/vars.go
index 081f349fb..038834b46 100644
--- a/kapacitor/vars.go
+++ b/kapacitor/vars.go
@@ -41,17 +41,16 @@ func Vars(rule chronograf.AlertRule) (string, error) {
var crit = %s
`
return fmt.Sprintf(vars, common, formatValue(rule.TriggerValues.Value)), nil
- } else {
- vars := `
+ }
+ vars := `
%s
var lower = %s
var upper = %s
`
- return fmt.Sprintf(vars,
- common,
- rule.TriggerValues.Value,
- rule.TriggerValues.RangeValue), nil
- }
+ return fmt.Sprintf(vars,
+ common,
+ rule.TriggerValues.Value,
+ rule.TriggerValues.RangeValue), nil
case Relative:
vars := `
%s
diff --git a/log/log.go b/log/log.go
index 2cfbb2513..1b00e3332 100644
--- a/log/log.go
+++ b/log/log.go
@@ -1,6 +1,7 @@
package log
import (
+ "io"
"os"
"github.com/Sirupsen/logrus"
@@ -81,6 +82,11 @@ func (ll *logrusLogger) WithField(key string, value interface{}) chronograf.Logg
return &logrusLogger{ll.l.WithField(key, value)}
}
+func (ll *logrusLogger) Writer() *io.PipeWriter {
+ return ll.l.Logger.WriterLevel(logrus.ErrorLevel)
+}
+
+// New wraps a logrus Logger
func New(l Level) chronograf.Logger {
logger := &logrus.Logger{
Out: os.Stderr,
diff --git a/oauth2/doc.go b/oauth2/doc.go
index db132a6ca..7fed8f3b6 100644
--- a/oauth2/doc.go
+++ b/oauth2/doc.go
@@ -1,4 +1,4 @@
-// The oauth2 package provides http.Handlers necessary for implementing Oauth2
+// Package oauth2 provides http.Handlers necessary for implementing Oauth2
// authentication with multiple Providers.
//
// This is how the pieces of this package fit together:
diff --git a/oauth2/google.go b/oauth2/google.go
index fb082ace6..ee5ff3bb1 100644
--- a/oauth2/google.go
+++ b/oauth2/google.go
@@ -10,7 +10,7 @@ import (
goauth2 "google.golang.org/api/oauth2/v2"
)
-// Endpoint is Google's OAuth 2.0 endpoint.
+// GoogleEndpoint is Google's OAuth 2.0 endpoint.
// Copied here to remove tons of package dependencies
var GoogleEndpoint = oauth2.Endpoint{
AuthURL: "https://accounts.google.com/o/oauth2/auth",
@@ -18,6 +18,7 @@ var GoogleEndpoint = oauth2.Endpoint{
}
var _ Provider = &Google{}
+// Google is an oauth2 provider supporting google.
type Google struct {
ClientID string
ClientSecret string
diff --git a/oauth2/heroku.go b/oauth2/heroku.go
index 637c57adf..831b095df 100644
--- a/oauth2/heroku.go
+++ b/oauth2/heroku.go
@@ -14,8 +14,8 @@ import (
var _ Provider = &Heroku{}
const (
- // Routes required for interacting with Heroku API
- HEROKU_ACCOUNT_ROUTE string = "https://api.heroku.com/account"
+ // HerokuAccountRoute is required for interacting with Heroku API
+ HerokuAccountRoute string = "https://api.heroku.com/account"
)
// Heroku is an OAuth2 Provider allowing users to authenticate with Heroku to
@@ -61,13 +61,14 @@ func (h *Heroku) PrincipalID(provider *http.Client) (string, error) {
DefaultOrganization DefaultOrg `json:"default_organization"`
}
- resp, err := provider.Get(HEROKU_ACCOUNT_ROUTE)
+ resp, err := provider.Get(HerokuAccountRoute)
if err != nil {
h.Logger.Error("Unable to communicate with Heroku. err:", err)
return "", err
}
defer resp.Body.Close()
d := json.NewDecoder(resp.Body)
+
var account Account
if err := d.Decode(&account); err != nil {
h.Logger.Error("Unable to decode response from Heroku. err:", err)
@@ -83,9 +84,8 @@ func (h *Heroku) PrincipalID(provider *http.Client) (string, error) {
}
h.Logger.Error(ErrOrgMembership)
return "", ErrOrgMembership
- } else {
- return account.Email, nil
}
+ return account.Email, nil
}
// Scopes for heroku is "identity" which grants access to user account
diff --git a/oauth2/mux.go b/oauth2/mux.go
index dc45a1525..9ecc11dba 100644
--- a/oauth2/mux.go
+++ b/oauth2/mux.go
@@ -24,6 +24,7 @@ type cookie struct {
// Check to ensure CookieMux is an oauth2.Mux
var _ Mux = &CookieMux{}
+// NewCookieMux constructs a Mux handler that checks a cookie against the authenticator
func NewCookieMux(p Provider, a Authenticator, l chronograf.Logger) *CookieMux {
return &CookieMux{
Provider: p,
@@ -55,7 +56,7 @@ type CookieMux struct {
Now func() time.Time // Now returns the current time
}
-// Uses a Cookie with a random string as the state validation method. JWTs are
+// Login uses a Cookie with a random string as the state validation method. JWTs are
// a good choice here for encoding because they can be validated without
// storing state.
func (j *CookieMux) Login() http.Handler {
diff --git a/oauth2/oauth2_test.go b/oauth2/oauth2_test.go
index 60c34e1eb..ed6379414 100644
--- a/oauth2/oauth2_test.go
+++ b/oauth2/oauth2_test.go
@@ -27,8 +27,8 @@ func (mp *MockProvider) Config() *goauth.Config {
ClientID: "4815162342",
ClientSecret: "8675309",
Endpoint: goauth.Endpoint{
- mp.ProviderURL + "/oauth/auth",
- mp.ProviderURL + "/oauth/token",
+ AuthURL: mp.ProviderURL + "/oauth/auth",
+ TokenURL: mp.ProviderURL + "/oauth/token",
},
}
}
diff --git a/server/admin.go b/server/admin.go
deleted file mode 100644
index 27eb3428d..000000000
--- a/server/admin.go
+++ /dev/null
@@ -1,586 +0,0 @@
-package server
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "net/url"
-
- "github.com/bouk/httprouter"
- "github.com/influxdata/chronograf"
-)
-
-func validPermissions(perms *chronograf.Permissions) error {
- if perms == nil {
- return nil
- }
- for _, perm := range *perms {
- if perm.Scope != chronograf.AllScope && perm.Scope != chronograf.DBScope {
- return fmt.Errorf("Invalid permission scope")
- }
- if perm.Scope == chronograf.DBScope && perm.Name == "" {
- return fmt.Errorf("Database scoped permission requires a name")
- }
- }
- return nil
-}
-
-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
-}
-
-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)
-}
-
-func (r *sourceUserRequest) ValidUpdate() error {
- if r.Password == "" && len(r.Permissions) == 0 {
- return fmt.Errorf("No fields to update")
- }
- return validPermissions(&r.Permissions)
-}
-
-type sourceUser struct {
- Username string `json:"name"` // Username for new account
- Permissions chronograf.Permissions `json:"permissions,omitempty"` // Account's permissions
- Links selfLinks `json:"links"` // Links are URI locations related to user
-}
-
-type enterpriseSourceUser struct {
- Username string `json:"name"` // Username for new account
- Permissions chronograf.Permissions `json:"permissions"` // Account's permissions
- Roles []userRoleResponse `json:"roles"` // Roles if source uses them
- Links selfLinks `json:"links"` // Links are URI locations related to user
-}
-
-type userRoleResponse struct {
- Name string `json:"name"`
- Permissions chronograf.Permissions `json:"permissions"`
- Links selfLinks `json:"links"`
-}
-
-func newUserRoleResponse(srcID int, res *chronograf.Role) userRoleResponse {
- if res.Permissions == nil {
- res.Permissions = make(chronograf.Permissions, 0)
- }
- return userRoleResponse{
- Name: res.Name,
- Permissions: res.Permissions,
- Links: newSelfLinks(srcID, "roles", res.Name),
- }
-}
-
-type selfLinks struct {
- Self string `json:"self"` // Self link mapping to this resource
-}
-
-func sourceUserResponse(u *chronograf.User, srcID int, hasRoles bool) interface{} {
- // Permissions should always be returned. If no permissions, then
- // return empty array
- perms := u.Permissions
- if len(perms) == 0 {
- perms = make([]chronograf.Permission, 0)
- }
-
- // If the source supports roles, we return all
- // associated with this user
- if hasRoles {
- res := enterpriseSourceUser{
- Username: u.Name,
- Permissions: perms,
- Roles: make([]userRoleResponse, 0),
- Links: newSelfLinks(srcID, "users", u.Name),
- }
-
- if len(u.Roles) > 0 {
- rr := make([]userRoleResponse, len(u.Roles))
- for i, role := range u.Roles {
- rr[i] = newUserRoleResponse(srcID, &role)
- }
- res.Roles = rr
- }
- return &res
- }
-
- res := sourceUser{
- Username: u.Name,
- Permissions: perms,
- Links: newSelfLinks(srcID, "users", u.Name),
- }
- return &res
-}
-
-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),
- }
-}
-
-// NewSourceUser adds user to source
-func (h *Service) NewSourceUser(w http.ResponseWriter, r *http.Request) {
- var req sourceUserRequest
- 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, store, err := h.sourceUsersStore(ctx, w, r)
- if err != nil {
- return
- }
-
- user := &chronograf.User{
- Name: req.Username,
- Passwd: req.Password,
- Permissions: req.Permissions,
- }
-
- res, err := store.Add(ctx, user)
- if err != nil {
- Error(w, http.StatusBadRequest, err.Error(), h.Logger)
- return
- }
-
- su := sourceUser{
- Username: res.Name,
- Permissions: req.Permissions,
- Links: newSelfLinks(srcID, "users", res.Name),
- }
- w.Header().Add("Location", su.Links.Self)
- encodeJSON(w, http.StatusCreated, su, h.Logger)
-}
-
-type sourceUsers struct {
- Users []interface{} `json:"users"`
-}
-
-// SourceUsers retrieves all users from source.
-func (h *Service) SourceUsers(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
-
- srcID, ts, err := h.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(), h.Logger)
- return
- }
- _, hasRoles := h.hasRoles(ctx, ts)
-
- su := []interface{}{}
- for _, u := range users {
- res := sourceUserResponse(&u, srcID, hasRoles)
- su = append(su, res)
- }
-
- res := sourceUsers{
- Users: su,
- }
-
- 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 {
- return
- }
-
- store := ts.Users(ctx)
- u, err := store.Get(ctx, uid)
- if err != nil {
- Error(w, http.StatusBadRequest, err.Error(), h.Logger)
- return
- }
- _, hasRoles := h.hasRoles(ctx, ts)
-
- res := sourceUserResponse(u, srcID, hasRoles)
- 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)
- 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) {
- var req sourceUserRequest
- 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()
- uid := httprouter.GetParamFromContext(ctx, "uid")
- srcID, store, err := h.sourceUsersStore(ctx, w, r)
- if err != nil {
- return
- }
-
- user := &chronograf.User{
- Name: uid,
- Passwd: req.Password,
- Permissions: req.Permissions,
- }
-
- if err := store.Update(ctx, user); err != nil {
- Error(w, http.StatusBadRequest, err.Error(), h.Logger)
- return
- }
-
- su := sourceUser{
- Username: user.Name,
- Permissions: user.Permissions,
- Links: newSelfLinks(srcID, "users", user.Name),
- }
- w.Header().Add("Location", su.Links.Self)
- encodeJSON(w, http.StatusOK, su, h.Logger)
-}
-
-func (h *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(), h.Logger)
- return 0, nil, err
- }
-
- src, err := h.SourcesStore.Get(ctx, srcID)
- if err != nil {
- notFound(w, srcID, h.Logger)
- return 0, nil, err
- }
-
- 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
-}
-
-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
- }
-
- 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
-}
-
-// Permissions returns all possible permissions for this source.
-func (h *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)
- return
- }
-
- src, err := h.SourcesStore.Get(ctx, srcID)
- if err != nil {
- notFound(w, srcID, h.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
- }
-
- 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
- }
-
- perms := ts.Permissions(ctx)
- if err != nil {
- Error(w, http.StatusBadRequest, err.Error(), h.Logger)
- return
- }
- httpAPISrcs := "/chronograf/v1/sources"
- res := struct {
- Permissions chronograf.Permissions `json:"permissions"`
- Links map[string]string `json:"links"` // Links are URI locations related to user
- }{
- Permissions: perms,
- Links: map[string]string{
- "self": fmt.Sprintf("%s/%d/permissions", httpAPISrcs, srcID),
- "source": fmt.Sprintf("%s/%d", httpAPISrcs, srcID),
- },
- }
- encodeJSON(w, http.StatusOK, res, h.Logger)
-}
-
-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 []sourceUser `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([]sourceUser, len(res.Users))
- for i := range res.Users {
- name := res.Users[i].Name
- su[i] = sourceUser{
- Username: name,
- Links: newSelfLinks(srcID, "users", 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),
- }
-}
-
-// 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
- }
-
- 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)
-}
diff --git a/server/admin_test.go b/server/admin_test.go
deleted file mode 100644
index 27a421618..000000000
--- a/server/admin_test.go
+++ /dev/null
@@ -1,1482 +0,0 @@
-package server_test
-
-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"
- "github.com/influxdata/chronograf/server"
-)
-
-func TestService_NewSourceUser(t *testing.T) {
- type fields struct {
- SourcesStore chronograf.SourcesStore
- TimeSeries server.TimeSeriesClient
- Logger chronograf.Logger
- UseAuth bool
- }
- 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: "New user for data source",
- args: args{
- w: httptest.NewRecorder(),
- r: httptest.NewRequest(
- "POST",
- "http://server.local/chronograf/v1/sources/1",
- ioutil.NopCloser(
- bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
- },
- fields: fields{
- UseAuth: true,
- 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
- },
- UsersF: func(ctx context.Context) chronograf.UsersStore {
- return &mocks.UsersStore{
- AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
- return u, nil
- },
- }
- },
- },
- },
- ID: "1",
- wantStatus: http.StatusCreated,
- wantContentType: "application/json",
- wantBody: `{"name":"marty","links":{"self":"/chronograf/v1/sources/1/users/marty"}}
-`,
- },
- {
- name: "Error adding user",
- args: args{
- w: httptest.NewRecorder(),
- r: httptest.NewRequest(
- "POST",
- "http://server.local/chronograf/v1/sources/1",
- ioutil.NopCloser(
- bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
- },
- fields: fields{
- UseAuth: true,
- 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
- },
- UsersF: func(ctx context.Context) chronograf.UsersStore {
- return &mocks.UsersStore{
- AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
- return nil, fmt.Errorf("Weight Has Nothing to Do With It")
- },
- }
- },
- },
- },
- ID: "1",
- wantStatus: http.StatusBadRequest,
- wantContentType: "application/json",
- wantBody: `{"code":400,"message":"Weight Has Nothing to Do With It"}`,
- },
- {
- name: "Failure connecting to user store",
- args: args{
- w: httptest.NewRecorder(),
- r: httptest.NewRequest(
- "POST",
- "http://server.local/chronograf/v1/sources/1",
- ioutil.NopCloser(
- bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
- },
- fields: fields{
- UseAuth: true,
- 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 fmt.Errorf("Biff just happens to be my supervisor")
- },
- },
- },
- ID: "1",
- wantStatus: http.StatusBadRequest,
- wantContentType: "application/json",
- wantBody: `{"code":400,"message":"Unable to connect to source 1: Biff just happens to be my supervisor"}`,
- },
- {
- name: "Failure getting source",
- args: args{
- w: httptest.NewRecorder(),
- r: httptest.NewRequest(
- "POST",
- "http://server.local/chronograf/v1/sources/1",
- ioutil.NopCloser(
- bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
- },
- fields: fields{
- UseAuth: true,
- Logger: log.New(log.DebugLevel),
- SourcesStore: &mocks.SourcesStore{
- GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
- return chronograf.Source{}, fmt.Errorf("No McFly ever amounted to anything in the history of Hill Valley")
- },
- },
- },
- ID: "1",
- wantStatus: http.StatusNotFound,
- wantContentType: "application/json",
- wantBody: `{"code":404,"message":"ID 1 not found"}`,
- },
- {
- name: "Bad ID",
- args: args{
- w: httptest.NewRecorder(),
- r: httptest.NewRequest(
- "POST",
- "http://server.local/chronograf/v1/sources/1",
- ioutil.NopCloser(
- bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
- },
- fields: fields{
- UseAuth: true,
- Logger: log.New(log.DebugLevel),
- },
- ID: "BAD",
- wantStatus: http.StatusUnprocessableEntity,
- wantContentType: "application/json",
- wantBody: `{"code":422,"message":"Error converting ID BAD"}`,
- },
- {
- name: "Bad name",
- args: args{
- w: httptest.NewRecorder(),
- r: httptest.NewRequest(
- "POST",
- "http://server.local/chronograf/v1/sources/1",
- ioutil.NopCloser(
- bytes.NewReader([]byte(`{"password": "the_lake"}`)))),
- },
- fields: fields{
- UseAuth: true,
- Logger: log.New(log.DebugLevel),
- },
- ID: "BAD",
- wantStatus: http.StatusUnprocessableEntity,
- wantContentType: "application/json",
- wantBody: `{"code":422,"message":"Username required"}`,
- },
- {
- name: "Bad JSON",
- args: args{
- w: httptest.NewRecorder(),
- r: httptest.NewRequest(
- "POST",
- "http://server.local/chronograf/v1/sources/1",
- ioutil.NopCloser(
- bytes.NewReader([]byte(`{password}`)))),
- },
- fields: fields{
- UseAuth: true,
- Logger: log.New(log.DebugLevel),
- },
- ID: "BAD",
- wantStatus: http.StatusBadRequest,
- wantContentType: "application/json",
- wantBody: `{"code":400,"message":"Unparsable JSON"}`,
- },
- }
- for _, tt := range tests {
- tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
- context.Background(),
- httprouter.Params{
- {
- Key: "id",
- Value: tt.ID,
- },
- }))
-
- h := &server.Service{
- SourcesStore: tt.fields.SourcesStore,
- TimeSeriesClient: tt.fields.TimeSeries,
- Logger: tt.fields.Logger,
- UseAuth: tt.fields.UseAuth,
- }
-
- h.NewSourceUser(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. NewSourceUser() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
- }
- if tt.wantContentType != "" && content != tt.wantContentType {
- t.Errorf("%q. NewSourceUser() = %v, want %v", tt.name, content, tt.wantContentType)
- }
- if tt.wantBody != "" && string(body) != tt.wantBody {
- t.Errorf("%q. NewSourceUser() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
- }
- }
-}
-
-func TestService_SourceUsers(t *testing.T) {
- type fields struct {
- SourcesStore chronograf.SourcesStore
- TimeSeries server.TimeSeriesClient
- Logger chronograf.Logger
- UseAuth bool
- }
- 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: "All users for data source",
- args: args{
- w: httptest.NewRecorder(),
- r: httptest.NewRequest(
- "GET",
- "http://server.local/chronograf/v1/sources/1",
- nil),
- },
- fields: fields{
- UseAuth: true,
- 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("No role store for this test")
- },
- UsersF: func(ctx context.Context) chronograf.UsersStore {
- return &mocks.UsersStore{
- AllF: func(ctx context.Context) ([]chronograf.User, error) {
- return []chronograf.User{
- {
- Name: "strickland",
- Passwd: "discipline",
- Permissions: chronograf.Permissions{
- {
- Scope: chronograf.AllScope,
- Allowed: chronograf.Allowances{"READ"},
- },
- },
- },
- }, nil
- },
- }
- },
- },
- },
- ID: "1",
- wantStatus: http.StatusOK,
- wantContentType: "application/json",
- wantBody: `{"users":[{"name":"strickland","permissions":[{"scope":"all","allowed":["READ"]}],"links":{"self":"/chronograf/v1/sources/1/users/strickland"}}]}
-`,
- },
- }
- for _, tt := range tests {
- tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
- context.Background(),
- httprouter.Params{
- {
- Key: "id",
- Value: tt.ID,
- },
- }))
- h := &server.Service{
- SourcesStore: tt.fields.SourcesStore,
- TimeSeriesClient: tt.fields.TimeSeries,
- Logger: tt.fields.Logger,
- UseAuth: tt.fields.UseAuth,
- }
-
- h.SourceUsers(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. SourceUsers() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
- }
- if tt.wantContentType != "" && content != tt.wantContentType {
- t.Errorf("%q. SourceUsers() = %v, want %v", tt.name, content, tt.wantContentType)
- }
- if tt.wantBody != "" && string(body) != tt.wantBody {
- t.Errorf("%q. SourceUsers() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
- }
- }
-}
-
-func TestService_SourceUserID(t *testing.T) {
- type fields struct {
- SourcesStore chronograf.SourcesStore
- TimeSeries server.TimeSeriesClient
- Logger chronograf.Logger
- UseAuth bool
- }
- type args struct {
- w *httptest.ResponseRecorder
- r *http.Request
- }
- tests := []struct {
- name string
- fields fields
- args args
- ID string
- UID string
- wantStatus int
- wantContentType string
- wantBody string
- }{
- {
- name: "Single user for data source",
- args: args{
- w: httptest.NewRecorder(),
- r: httptest.NewRequest(
- "GET",
- "http://server.local/chronograf/v1/sources/1",
- nil),
- },
- fields: fields{
- UseAuth: true,
- 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("No role store for this test")
- },
- UsersF: func(ctx context.Context) chronograf.UsersStore {
- return &mocks.UsersStore{
- GetF: func(ctx context.Context, uid string) (*chronograf.User, error) {
- return &chronograf.User{
- Name: "strickland",
- Passwd: "discipline",
- Permissions: chronograf.Permissions{
- {
- Scope: chronograf.AllScope,
- Allowed: chronograf.Allowances{"READ"},
- },
- },
- }, nil
- },
- }
- },
- },
- },
- ID: "1",
- UID: "strickland",
- wantStatus: http.StatusOK,
- wantContentType: "application/json",
- wantBody: `{"name":"strickland","permissions":[{"scope":"all","allowed":["READ"]}],"links":{"self":"/chronograf/v1/sources/1/users/strickland"}}
-`,
- },
- }
- for _, tt := range tests {
- tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
- context.Background(),
- httprouter.Params{
- {
- Key: "id",
- Value: tt.ID,
- },
- }))
- h := &server.Service{
- SourcesStore: tt.fields.SourcesStore,
- TimeSeriesClient: tt.fields.TimeSeries,
- Logger: tt.fields.Logger,
- UseAuth: tt.fields.UseAuth,
- }
-
- h.SourceUserID(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. SourceUserID() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
- }
- if tt.wantContentType != "" && content != tt.wantContentType {
- t.Errorf("%q. SourceUserID() = %v, want %v", tt.name, content, tt.wantContentType)
- }
- if tt.wantBody != "" && string(body) != tt.wantBody {
- t.Errorf("%q. SourceUserID() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
- }
- }
-}
-
-func TestService_RemoveSourceUser(t *testing.T) {
- type fields struct {
- SourcesStore chronograf.SourcesStore
- TimeSeries server.TimeSeriesClient
- Logger chronograf.Logger
- UseAuth bool
- }
- type args struct {
- w *httptest.ResponseRecorder
- r *http.Request
- }
- tests := []struct {
- name string
- fields fields
- args args
- ID string
- UID string
- wantStatus int
- wantContentType string
- wantBody string
- }{
- {
- name: "Delete user for data source",
- args: args{
- w: httptest.NewRecorder(),
- r: httptest.NewRequest(
- "GET",
- "http://server.local/chronograf/v1/sources/1",
- nil),
- },
- fields: fields{
- UseAuth: true,
- 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
- },
- UsersF: func(ctx context.Context) chronograf.UsersStore {
- return &mocks.UsersStore{
- DeleteF: func(ctx context.Context, u *chronograf.User) error {
- return nil
- },
- }
- },
- },
- },
- ID: "1",
- UID: "strickland",
- wantStatus: http.StatusNoContent,
- },
- }
- for _, tt := range tests {
- tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
- context.Background(),
- httprouter.Params{
- {
- Key: "id",
- Value: tt.ID,
- },
- }))
- h := &server.Service{
- SourcesStore: tt.fields.SourcesStore,
- TimeSeriesClient: tt.fields.TimeSeries,
- Logger: tt.fields.Logger,
- UseAuth: tt.fields.UseAuth,
- }
- h.RemoveSourceUser(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. RemoveSourceUser() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
- }
- if tt.wantContentType != "" && content != tt.wantContentType {
- t.Errorf("%q. RemoveSourceUser() = %v, want %v", tt.name, content, tt.wantContentType)
- }
- if tt.wantBody != "" && string(body) != tt.wantBody {
- t.Errorf("%q. RemoveSourceUser() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
- }
- }
-}
-
-func TestService_UpdateSourceUser(t *testing.T) {
- type fields struct {
- SourcesStore chronograf.SourcesStore
- TimeSeries server.TimeSeriesClient
- Logger chronograf.Logger
- UseAuth bool
- }
- type args struct {
- w *httptest.ResponseRecorder
- r *http.Request
- }
- tests := []struct {
- name string
- fields fields
- args args
- ID string
- UID string
- wantStatus int
- wantContentType string
- wantBody string
- }{
- {
- name: "Update user password for data source",
- args: args{
- w: httptest.NewRecorder(),
- r: httptest.NewRequest(
- "POST",
- "http://server.local/chronograf/v1/sources/1",
- ioutil.NopCloser(
- bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
- },
- fields: fields{
- UseAuth: true,
- 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
- },
- UsersF: func(ctx context.Context) chronograf.UsersStore {
- return &mocks.UsersStore{
- UpdateF: func(ctx context.Context, u *chronograf.User) error {
- return nil
- },
- }
- },
- },
- },
- ID: "1",
- UID: "marty",
- wantStatus: http.StatusOK,
- wantContentType: "application/json",
- wantBody: `{"name":"marty","links":{"self":"/chronograf/v1/sources/1/users/marty"}}
-`,
- },
- {
- name: "Invalid update JSON",
- args: args{
- w: httptest.NewRecorder(),
- r: httptest.NewRequest(
- "POST",
- "http://server.local/chronograf/v1/sources/1",
- ioutil.NopCloser(
- bytes.NewReader([]byte(`{"name": "marty"}`)))),
- },
- fields: fields{
- UseAuth: true,
- Logger: log.New(log.DebugLevel),
- },
- ID: "1",
- UID: "marty",
- wantStatus: http.StatusUnprocessableEntity,
- wantContentType: "application/json",
- wantBody: `{"code":422,"message":"No fields to update"}`,
- },
- }
- for _, tt := range tests {
- tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
- context.Background(),
- httprouter.Params{
- {
- Key: "id",
- Value: tt.ID,
- },
- {
- Key: "uid",
- Value: tt.UID,
- },
- }))
- h := &server.Service{
- SourcesStore: tt.fields.SourcesStore,
- TimeSeriesClient: tt.fields.TimeSeries,
- Logger: tt.fields.Logger,
- UseAuth: tt.fields.UseAuth,
- }
- h.UpdateSourceUser(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. UpdateSourceUser() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
- }
- if tt.wantContentType != "" && content != tt.wantContentType {
- t.Errorf("%q. UpdateSourceUser() = %v, want %v", tt.name, content, tt.wantContentType)
- }
- if tt.wantBody != "" && string(body) != tt.wantBody {
- t.Errorf("%q. UpdateSourceUser() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
- }
- }
-}
-
-func TestService_Permissions(t *testing.T) {
- type fields struct {
- SourcesStore chronograf.SourcesStore
- TimeSeries server.TimeSeriesClient
- Logger chronograf.Logger
- UseAuth bool
- }
- 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: "New user for data source",
- args: args{
- w: httptest.NewRecorder(),
- r: httptest.NewRequest(
- "POST",
- "http://server.local/chronograf/v1/sources/1",
- ioutil.NopCloser(
- bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
- },
- fields: fields{
- UseAuth: true,
- 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
- },
- PermissionsF: func(ctx context.Context) chronograf.Permissions {
- return chronograf.Permissions{
- {
- Scope: chronograf.AllScope,
- Allowed: chronograf.Allowances{"READ", "WRITE"},
- },
- }
- },
- },
- },
- ID: "1",
- wantStatus: http.StatusOK,
- wantContentType: "application/json",
- wantBody: `{"permissions":[{"scope":"all","allowed":["READ","WRITE"]}],"links":{"self":"/chronograf/v1/sources/1/permissions","source":"/chronograf/v1/sources/1"}}
-`,
- },
- }
- for _, tt := range tests {
- tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
- context.Background(),
- httprouter.Params{
- {
- Key: "id",
- Value: tt.ID,
- },
- }))
- h := &server.Service{
- SourcesStore: tt.fields.SourcesStore,
- TimeSeriesClient: tt.fields.TimeSeries,
- Logger: tt.fields.Logger,
- UseAuth: tt.fields.UseAuth,
- }
- h.Permissions(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. Permissions() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
- }
- if tt.wantContentType != "" && content != tt.wantContentType {
- t.Errorf("%q. Permissions() = %v, want %v", tt.name, content, tt.wantContentType)
- }
- if tt.wantBody != "" && string(body) != tt.wantBody {
- t.Errorf("%q. Permissions() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
- }
- }
-}
-
-func TestService_NewSourceRole(t *testing.T) {
- type fields struct {
- SourcesStore chronograf.SourcesStore
- TimeSeries server.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")
- },
- }, 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
- },
- }, nil
- },
- },
- },
- ID: "1",
- wantStatus: http.StatusCreated,
- wantContentType: "application/json",
- wantBody: `{"users":[{"name":"match","links":{"self":"/chronograf/v1/sources/1/users/match"}},{"name":"skinhead","links":{"self":"/chronograf/v1/sources/1/users/skinhead"}},{"name":"3-d","links":{"self":"/chronograf/v1/sources/1/users/3-d"}}],"name":"biffsgang","permissions":[],"links":{"self":"/chronograf/v1/sources/1/roles/biffsgang"}}
-`,
- },
- }
- for _, tt := range tests {
- h := &server.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 server.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":[{"name":"match","links":{"self":"/chronograf/v1/sources/1/users/match"}},{"name":"skinhead","links":{"self":"/chronograf/v1/sources/1/users/skinhead"}},{"name":"3-d","links":{"self":"/chronograf/v1/sources/1/users/3-d"}}],"name":"biffsgang","permissions":[],"links":{"self":"/chronograf/v1/sources/1/roles/biffsgang"}}
-`,
- },
- }
- for _, tt := range tests {
- h := &server.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 server.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":[{"name":"match","links":{"self":"/chronograf/v1/sources/1/users/match"}},{"name":"skinhead","links":{"self":"/chronograf/v1/sources/1/users/skinhead"}},{"name":"3-d","links":{"self":"/chronograf/v1/sources/1/users/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 := &server.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 server.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 := &server.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 server.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":[{"name":"match","links":{"self":"/chronograf/v1/sources/1/users/match"}},{"name":"skinhead","links":{"self":"/chronograf/v1/sources/1/users/skinhead"}},{"name":"3-d","links":{"self":"/chronograf/v1/sources/1/users/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 := &server.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)
- }
- }
-}
diff --git a/server/me.go b/server/me.go
new file mode 100644
index 000000000..48d66ed6b
--- /dev/null
+++ b/server/me.go
@@ -0,0 +1,99 @@
+package server
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "golang.org/x/net/context"
+
+ "github.com/influxdata/chronograf"
+ "github.com/influxdata/chronograf/oauth2"
+)
+
+type meLinks struct {
+ Self string `json:"self"` // Self link mapping to this resource
+}
+
+type meResponse struct {
+ *chronograf.User
+ Links meLinks `json:"links"`
+}
+
+// If new user response is nil, return an empty meResponse because it
+// indicates authentication is not needed
+func newMeResponse(usr *chronograf.User) meResponse {
+ base := "/chronograf/v1/users"
+ name := "me"
+ if usr != nil {
+ // TODO: Change to urls.PathEscape for go 1.8
+ u := &url.URL{Path: usr.Name}
+ name = u.String()
+ }
+
+ return meResponse{
+ User: usr,
+ Links: meLinks{
+ Self: fmt.Sprintf("%s/%s", base, name),
+ },
+ }
+}
+
+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
+}
+
+func getPrincipal(ctx context.Context) (oauth2.Principal, error) {
+ principal, ok := ctx.Value(oauth2.PrincipalKey).(oauth2.Principal)
+ if !ok {
+ return oauth2.Principal{}, fmt.Errorf("Token not found")
+ }
+
+ return principal, nil
+}
+
+// Me does a findOrCreate based on the email in the context
+func (h *Service) Me(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ if !h.UseAuth {
+ // If there's no authentication, return an empty user
+ res := newMeResponse(nil)
+ encodeJSON(w, http.StatusOK, res, h.Logger)
+ return
+ }
+
+ email, err := getEmail(ctx)
+ if err != nil {
+ invalidData(w, err, h.Logger)
+ return
+ }
+
+ usr, err := h.UsersStore.Get(ctx, email)
+ if err == nil {
+ res := newMeResponse(usr)
+ encodeJSON(w, http.StatusOK, res, h.Logger)
+ return
+ }
+
+ // Because we didnt find a user, making a new one
+ user := &chronograf.User{
+ Name: email,
+ }
+
+ newUser, err := h.UsersStore.Add(ctx, user)
+ if err != nil {
+ msg := fmt.Errorf("error storing user %s: %v", user.Name, err)
+ unknownErrorWithMessage(w, msg, h.Logger)
+ return
+ }
+
+ res := newMeResponse(newUser)
+ encodeJSON(w, http.StatusOK, res, h.Logger)
+}
diff --git a/server/me_test.go b/server/me_test.go
new file mode 100644
index 000000000..147bf8f3a
--- /dev/null
+++ b/server/me_test.go
@@ -0,0 +1,168 @@
+package server
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/influxdata/chronograf"
+ "github.com/influxdata/chronograf/log"
+ "github.com/influxdata/chronograf/mocks"
+ "github.com/influxdata/chronograf/oauth2"
+)
+
+type MockUsers struct{}
+
+func TestService_Me(t *testing.T) {
+ type fields struct {
+ UsersStore chronograf.UsersStore
+ Logger chronograf.Logger
+ UseAuth bool
+ }
+ type args struct {
+ w *httptest.ResponseRecorder
+ r *http.Request
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ principal oauth2.Principal
+ wantStatus int
+ wantContentType string
+ wantBody string
+ }{
+ {
+ name: "Existing user",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest("GET", "http://example.com/foo", nil),
+ },
+ fields: fields{
+ UseAuth: true,
+ UsersStore: &mocks.UsersStore{
+ GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
+ return &chronograf.User{
+ Name: "me",
+ Passwd: "hunter2",
+ }, nil
+ },
+ },
+ },
+ principal: oauth2.Principal{
+ Subject: "me",
+ },
+ wantStatus: http.StatusOK,
+ wantContentType: "application/json",
+ wantBody: `{"name":"me","password":"hunter2","links":{"self":"/chronograf/v1/users/me"}}
+`,
+ },
+ {
+ name: "New user",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest("GET", "http://example.com/foo", nil),
+ },
+ fields: fields{
+ UseAuth: true,
+ UsersStore: &mocks.UsersStore{
+ GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
+ return nil, fmt.Errorf("Unknown User")
+ },
+ AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
+ return u, nil
+ },
+ },
+ },
+ principal: oauth2.Principal{
+ Subject: "secret",
+ },
+ wantStatus: http.StatusOK,
+ wantContentType: "application/json",
+ wantBody: `{"name":"secret","password":"","links":{"self":"/chronograf/v1/users/secret"}}
+`,
+ },
+ {
+ name: "Error adding user",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest("GET", "http://example.com/foo", nil),
+ },
+ fields: fields{
+ UseAuth: true,
+ UsersStore: &mocks.UsersStore{
+ GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
+ return nil, fmt.Errorf("Unknown User")
+ },
+ AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
+ return nil, fmt.Errorf("Why Heavy?")
+ },
+ },
+ Logger: log.New(log.DebugLevel),
+ },
+ principal: oauth2.Principal{
+ Subject: "secret",
+ },
+ wantStatus: http.StatusInternalServerError,
+ wantContentType: "application/json",
+ wantBody: `{"code":500,"message":"Unknown error: error storing user secret: Why Heavy?"}`,
+ },
+ {
+ name: "No Auth",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest("GET", "http://example.com/foo", nil),
+ },
+ fields: fields{
+ UseAuth: false,
+ Logger: log.New(log.DebugLevel),
+ },
+ wantStatus: http.StatusOK,
+ wantContentType: "application/json",
+ wantBody: `{"links":{"self":"/chronograf/v1/users/me"}}
+`,
+ },
+ {
+ name: "Empty Principal",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest("GET", "http://example.com/foo", nil),
+ },
+ fields: fields{
+ UseAuth: true,
+ Logger: log.New(log.DebugLevel),
+ },
+ wantStatus: http.StatusUnprocessableEntity,
+ principal: oauth2.Principal{
+ Subject: "",
+ },
+ },
+ }
+ 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,
+ }
+
+ h.Me(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. Me() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
+ }
+ 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 {
+ t.Errorf("%q. Me() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
+ }
+ }
+}
diff --git a/server/permissions.go b/server/permissions.go
new file mode 100644
index 000000000..cd353d8b2
--- /dev/null
+++ b/server/permissions.go
@@ -0,0 +1,70 @@
+package server
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/influxdata/chronograf"
+)
+
+// Permissions returns all possible permissions for this source.
+func (h *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)
+ return
+ }
+
+ src, err := h.SourcesStore.Get(ctx, srcID)
+ if err != nil {
+ notFound(w, srcID, h.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
+ }
+
+ 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
+ }
+
+ perms := ts.Permissions(ctx)
+ if err != nil {
+ Error(w, http.StatusBadRequest, err.Error(), h.Logger)
+ return
+ }
+ httpAPISrcs := "/chronograf/v1/sources"
+ res := struct {
+ Permissions chronograf.Permissions `json:"permissions"`
+ Links map[string]string `json:"links"` // Links are URI locations related to user
+ }{
+ Permissions: perms,
+ Links: map[string]string{
+ "self": fmt.Sprintf("%s/%d/permissions", httpAPISrcs, srcID),
+ "source": fmt.Sprintf("%s/%d", httpAPISrcs, srcID),
+ },
+ }
+ encodeJSON(w, http.StatusOK, res, h.Logger)
+}
+
+func validPermissions(perms *chronograf.Permissions) error {
+ if perms == nil {
+ return nil
+ }
+ for _, perm := range *perms {
+ if perm.Scope != chronograf.AllScope && perm.Scope != chronograf.DBScope {
+ return fmt.Errorf("Invalid permission scope")
+ }
+ if perm.Scope == chronograf.DBScope && perm.Name == "" {
+ return fmt.Errorf("Database scoped permission requires a name")
+ }
+ }
+ return nil
+}
diff --git a/server/permissions_test.go b/server/permissions_test.go
new file mode 100644
index 000000000..092a89c45
--- /dev/null
+++ b/server/permissions_test.go
@@ -0,0 +1,112 @@
+package server
+
+import (
+ "bytes"
+ "context"
+ "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_Permissions(t *testing.T) {
+ type fields struct {
+ SourcesStore chronograf.SourcesStore
+ TimeSeries TimeSeriesClient
+ Logger chronograf.Logger
+ UseAuth bool
+ }
+ 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: "New user for data source",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest(
+ "POST",
+ "http://server.local/chronograf/v1/sources/1",
+ ioutil.NopCloser(
+ bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
+ },
+ fields: fields{
+ UseAuth: true,
+ 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
+ },
+ PermissionsF: func(ctx context.Context) chronograf.Permissions {
+ return chronograf.Permissions{
+ {
+ Scope: chronograf.AllScope,
+ Allowed: chronograf.Allowances{"READ", "WRITE"},
+ },
+ }
+ },
+ },
+ },
+ ID: "1",
+ wantStatus: http.StatusOK,
+ wantContentType: "application/json",
+ wantBody: `{"permissions":[{"scope":"all","allowed":["READ","WRITE"]}],"links":{"self":"/chronograf/v1/sources/1/permissions","source":"/chronograf/v1/sources/1"}}
+`,
+ },
+ }
+ for _, tt := range tests {
+ tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
+ context.Background(),
+ httprouter.Params{
+ {
+ Key: "id",
+ Value: tt.ID,
+ },
+ }))
+ h := &Service{
+ SourcesStore: tt.fields.SourcesStore,
+ TimeSeriesClient: tt.fields.TimeSeries,
+ Logger: tt.fields.Logger,
+ UseAuth: tt.fields.UseAuth,
+ }
+ h.Permissions(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. Permissions() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
+ }
+ if tt.wantContentType != "" && content != tt.wantContentType {
+ t.Errorf("%q. Permissions() = %v, want %v", tt.name, content, tt.wantContentType)
+ }
+ if tt.wantBody != "" && string(body) != tt.wantBody {
+ t.Errorf("%q. Permissions() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
+ }
+ }
+}
diff --git a/server/roles.go b/server/roles.go
new file mode 100644
index 000000000..d738cbc42
--- /dev/null
+++ b/server/roles.go
@@ -0,0 +1,224 @@
+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),
+ }
+}
diff --git a/server/roles_test.go b/server/roles_test.go
new file mode 100644
index 000000000..7f6da27c2
--- /dev/null
+++ b/server/roles_test.go
@@ -0,0 +1,697 @@
+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)
+ }
+ }
+}
diff --git a/server/server.go b/server/server.go
index f9a12cd97..3a494b9f8 100644
--- a/server/server.go
+++ b/server/server.go
@@ -2,9 +2,11 @@ package server
import (
"crypto/tls"
+ "log"
"math/rand"
"net"
"net/http"
+ "os"
"runtime"
"strconv"
"time"
@@ -57,7 +59,7 @@ type Server struct {
HerokuOrganizations []string `long:"heroku-organization" description:"Heroku Organization Memberships a user is required to have for access to Chronograf (comma separated)" env:"HEROKU_ORGS" env-delim:","`
ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"`
- LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal" choice:"panic" default:"info" description:"Set the logging level" env:"LOG_LEVEL"`
+ LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"`
Basepath string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted" env:"BASE_PATH"`
ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"`
BuildInfo BuildInfo
@@ -73,14 +75,17 @@ func provide(p oauth2.Provider, m oauth2.Mux, ok func() bool) func(func(oauth2.P
}
}
+// UseGithub validates the CLI parameters to enable github oauth support
func (s *Server) UseGithub() bool {
return s.TokenSecret != "" && s.GithubClientID != "" && s.GithubClientSecret != ""
}
+// UseGoogle validates the CLI parameters to enable google oauth support
func (s *Server) UseGoogle() bool {
return s.TokenSecret != "" && s.GoogleClientID != "" && s.GoogleClientSecret != "" && s.PublicURL != ""
}
+// UseHeroku validates the CLI parameters to enable heroku oauth support
func (s *Server) UseHeroku() bool {
return s.TokenSecret != "" && s.HerokuClientID != "" && s.HerokuSecret != ""
}
@@ -208,10 +213,21 @@ func (s *Server) Serve() error {
}
s.Listener = listener
- httpServer := &graceful.Server{Server: new(http.Server)}
+ // Using a log writer for http server logging
+ w := logger.Writer()
+ defer w.Close()
+ stdLog := log.New(w, "", 0)
+
+ // TODO: Remove graceful when changing to go 1.8
+ httpServer := &graceful.Server{
+ Server: &http.Server{
+ ErrorLog: stdLog,
+ Handler: s.handler,
+ },
+ Logger: stdLog,
+ TCPKeepAlive: 5 * time.Second,
+ }
httpServer.SetKeepAlivesEnabled(true)
- httpServer.TCPKeepAlive = 5 * time.Second
- httpServer.Handler = s.handler
if !s.ReportingDisabled {
go reportUsageStats(s.BuildInfo, logger)
@@ -244,7 +260,8 @@ func openService(boltPath, cannedPath string, logger chronograf.Logger, useAuth
if err := db.Open(); err != nil {
logger.
WithField("component", "boltstore").
- Fatal("Unable to open boltdb; is there a chronograf already running? ", err)
+ Error("Unable to open boltdb; is there a chronograf already running? ", err)
+ os.Exit(1)
}
// These apps are those handled from a directory
diff --git a/server/url_prefixer.go b/server/url_prefixer.go
index d20563fc1..10387c08e 100644
--- a/server/url_prefixer.go
+++ b/server/url_prefixer.go
@@ -62,7 +62,8 @@ func (wrw *wrapResponseWriter) Header() http.Header {
return *wrw.dupHeader
}
-const CHUNK_SIZE int = 512
+// ChunkSize is the number of bytes per chunked transfer-encoding
+const ChunkSize int = 512
// ServeHTTP implements an http.Handler that prefixes relative URLs from the
// Next handler with the configured prefix. It does this by examining the
@@ -79,9 +80,9 @@ func (up *URLPrefixer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
// extract the flusher for flushing chunks
flusher, ok := rw.(http.Flusher)
if !ok {
- up.Logger.
- WithField("component", "prefixer").
- Fatal("Expected http.ResponseWriter to be an http.Flusher, but wasn't")
+ msg := "Expected http.ResponseWriter to be an http.Flusher, but wasn't"
+ Error(rw, http.StatusInternalServerError, msg, up.Logger)
+ return
}
nextRead, nextWrite := io.Pipe()
@@ -109,7 +110,7 @@ func (up *URLPrefixer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
writtenCount++
buf.Write(src.Bytes())
- if writtenCount >= CHUNK_SIZE {
+ if writtenCount >= ChunkSize {
flusher.Flush()
writtenCount = 0
}
diff --git a/server/users.go b/server/users.go
index bbf4f61db..cacf5da01 100644
--- a/server/users.go
+++ b/server/users.go
@@ -1,99 +1,317 @@
package server
import (
+ "context"
+ "encoding/json"
"fmt"
"net/http"
"net/url"
- "golang.org/x/net/context"
-
+ "github.com/bouk/httprouter"
"github.com/influxdata/chronograf"
- "github.com/influxdata/chronograf/oauth2"
)
-type userLinks struct {
- Self string `json:"self"` // Self link mapping to this resource
-}
-
-type userResponse struct {
- *chronograf.User
- Links userLinks `json:"links"`
-}
-
-// If new user response is nil, return an empty userResponse because it
-// indicates authentication is not needed
-func newUserResponse(usr *chronograf.User) userResponse {
- base := "/chronograf/v1/users"
- name := "me"
- if usr != nil {
- // TODO: Change to usrl.PathEscape for go 1.8
- u := &url.URL{Path: usr.Name}
- name = u.String()
- }
-
- return userResponse{
- User: usr,
- Links: userLinks{
- Self: fmt.Sprintf("%s/%s", base, name),
- },
- }
-}
-
-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
-}
-
-func getPrincipal(ctx context.Context) (oauth2.Principal, error) {
- principal, ok := ctx.Value(oauth2.PrincipalKey).(oauth2.Principal)
- if !ok {
- return oauth2.Principal{}, fmt.Errorf("Token not found")
- }
-
- return principal, nil
-}
-
-// Me does a findOrCreate based on the email in the context
-func (h *Service) Me(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- if !h.UseAuth {
- // If there's no authentication, return an empty user
- res := newUserResponse(nil)
- encodeJSON(w, http.StatusOK, res, h.Logger)
+// NewSourceUser adds user to source
+func (h *Service) NewSourceUser(w http.ResponseWriter, r *http.Request) {
+ var req userRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ invalidJSON(w, h.Logger)
return
}
- email, err := getEmail(ctx)
- if err != nil {
+ if err := req.ValidCreate(); err != nil {
invalidData(w, err, h.Logger)
return
}
- usr, err := h.UsersStore.Get(ctx, email)
- if err == nil {
- res := newUserResponse(usr)
- encodeJSON(w, http.StatusOK, res, h.Logger)
- return
- }
-
- // Because we didnt find a user, making a new one
- user := &chronograf.User{
- Name: email,
- }
-
- newUser, err := h.UsersStore.Add(ctx, user)
+ ctx := r.Context()
+ srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
- msg := fmt.Errorf("error storing user %s: %v", user.Name, err)
- unknownErrorWithMessage(w, msg, h.Logger)
return
}
- res := newUserResponse(newUser)
+ 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(), h.Logger)
+ return
+ }
+
+ if err != nil {
+ Error(w, http.StatusBadRequest, err.Error(), h.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)
+}
+
+// SourceUsers retrieves all users from source.
+func (h *Service) SourceUsers(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ srcID, ts, err := h.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(), h.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 {
+ return
+ }
+ store := ts.Users(ctx)
+ u, err := store.Get(ctx, uid)
+ if err != nil {
+ Error(w, http.StatusBadRequest, err.Error(), h.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)
+ 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) {
+ 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)
+ return
+ }
+
+ ctx := r.Context()
+ uid := httprouter.GetParamFromContext(ctx, "uid")
+ srcID, ts, err := h.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(), h.Logger)
+ return
+ }
+
+ u, err := store.Get(ctx, uid)
+ if err != nil {
+ Error(w, http.StatusBadRequest, err.Error(), h.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)
+}
+
+func (h *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(), h.Logger)
+ return 0, nil, err
+ }
+
+ src, err := h.SourcesStore.Get(ctx, srcID)
+ if err != nil {
+ notFound(w, srcID, h.Logger)
+ return 0, nil, err
+ }
+
+ 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
+}
+
+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
+ }
+
+ 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),
+ }
+}
diff --git a/server/users_test.go b/server/users_test.go
index 147bf8f3a..c46106d66 100644
--- a/server/users_test.go
+++ b/server/users_test.go
@@ -1,6 +1,7 @@
-package server
+package server_test
import (
+ "bytes"
"context"
"fmt"
"io/ioutil"
@@ -8,19 +9,19 @@ import (
"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/oauth2"
+ "github.com/influxdata/chronograf/server"
)
-type MockUsers struct{}
-
-func TestService_Me(t *testing.T) {
+func TestService_NewSourceUser(t *testing.T) {
type fields struct {
- UsersStore chronograf.UsersStore
- Logger chronograf.Logger
- UseAuth bool
+ SourcesStore chronograf.SourcesStore
+ TimeSeries server.TimeSeriesClient
+ Logger chronograf.Logger
+ UseAuth bool
}
type args struct {
w *httptest.ResponseRecorder
@@ -30,139 +31,900 @@ func TestService_Me(t *testing.T) {
name string
fields fields
args args
- principal oauth2.Principal
+ ID string
wantStatus int
wantContentType string
wantBody string
}{
{
- name: "Existing user",
+ name: "New user for data source",
args: args{
w: httptest.NewRecorder(),
- r: httptest.NewRequest("GET", "http://example.com/foo", nil),
+ r: httptest.NewRequest(
+ "POST",
+ "http://server.local/chronograf/v1/sources/1",
+ ioutil.NopCloser(
+ bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
},
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),
+ 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
},
},
- },
- principal: oauth2.Principal{
- Subject: "me",
- },
- wantStatus: http.StatusOK,
- wantContentType: "application/json",
- wantBody: `{"name":"me","password":"hunter2","links":{"self":"/chronograf/v1/users/me"}}
-`,
- },
- {
- name: "New user",
- args: args{
- w: httptest.NewRecorder(),
- r: httptest.NewRequest("GET", "http://example.com/foo", nil),
- },
- fields: fields{
- UseAuth: true,
- UsersStore: &mocks.UsersStore{
- GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
- return nil, fmt.Errorf("Unknown User")
+ TimeSeries: &mocks.TimeSeries{
+ ConnectF: func(ctx context.Context, src *chronograf.Source) error {
+ return nil
},
- AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
- return u, nil
+ UsersF: func(ctx context.Context) chronograf.UsersStore {
+ return &mocks.UsersStore{
+ AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
+ return u, nil
+ },
+ }
+ },
+ RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
+ return nil, fmt.Errorf("no roles")
},
},
},
- principal: oauth2.Principal{
- Subject: "secret",
- },
- wantStatus: http.StatusOK,
+ ID: "1",
+ wantStatus: http.StatusCreated,
wantContentType: "application/json",
- wantBody: `{"name":"secret","password":"","links":{"self":"/chronograf/v1/users/secret"}}
+ wantBody: `{"links":{"self":"/chronograf/v1/sources/1/users/marty"},"name":"marty","permissions":[]}
+`,
+ },
+ {
+ name: "New user for data source with roles",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest(
+ "POST",
+ "http://server.local/chronograf/v1/sources/1",
+ ioutil.NopCloser(
+ bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
+ },
+ fields: fields{
+ UseAuth: true,
+ 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
+ },
+ UsersF: func(ctx context.Context) chronograf.UsersStore {
+ return &mocks.UsersStore{
+ AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
+ return u, nil
+ },
+ }
+ },
+ RolesF: func(ctx context.Context) (chronograf.RolesStore, error) {
+ return nil, nil
+ },
+ },
+ },
+ ID: "1",
+ wantStatus: http.StatusCreated,
+ wantContentType: "application/json",
+ wantBody: `{"links":{"self":"/chronograf/v1/sources/1/users/marty"},"name":"marty","permissions":[],"roles":[]}
`,
},
{
name: "Error adding user",
args: args{
w: httptest.NewRecorder(),
- r: httptest.NewRequest("GET", "http://example.com/foo", nil),
+ r: httptest.NewRequest(
+ "POST",
+ "http://server.local/chronograf/v1/sources/1",
+ ioutil.NopCloser(
+ bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
},
fields: fields{
UseAuth: true,
- UsersStore: &mocks.UsersStore{
- GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
- return nil, fmt.Errorf("Unknown User")
- },
- AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
- return nil, fmt.Errorf("Why Heavy?")
+ 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
+ },
+ UsersF: func(ctx context.Context) chronograf.UsersStore {
+ return &mocks.UsersStore{
+ AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
+ return nil, fmt.Errorf("Weight Has Nothing to Do With It")
+ },
+ }
},
},
- Logger: log.New(log.DebugLevel),
},
- principal: oauth2.Principal{
- Subject: "secret",
- },
- wantStatus: http.StatusInternalServerError,
+ ID: "1",
+ wantStatus: http.StatusBadRequest,
wantContentType: "application/json",
- wantBody: `{"code":500,"message":"Unknown error: error storing user secret: Why Heavy?"}`,
+ wantBody: `{"code":400,"message":"Weight Has Nothing to Do With It"}`,
},
{
- name: "No Auth",
+ name: "Failure connecting to user store",
args: args{
w: httptest.NewRecorder(),
- r: httptest.NewRequest("GET", "http://example.com/foo", nil),
+ r: httptest.NewRequest(
+ "POST",
+ "http://server.local/chronograf/v1/sources/1",
+ ioutil.NopCloser(
+ bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
},
fields: fields{
- UseAuth: false,
+ UseAuth: true,
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 fmt.Errorf("Biff just happens to be my supervisor")
+ },
+ },
},
- wantStatus: http.StatusOK,
+ ID: "1",
+ wantStatus: http.StatusBadRequest,
wantContentType: "application/json",
- wantBody: `{"links":{"self":"/chronograf/v1/users/me"}}
-`,
+ wantBody: `{"code":400,"message":"Unable to connect to source 1: Biff just happens to be my supervisor"}`,
},
{
- name: "Empty Principal",
+ name: "Failure getting source",
args: args{
w: httptest.NewRecorder(),
- r: httptest.NewRequest("GET", "http://example.com/foo", nil),
+ r: httptest.NewRequest(
+ "POST",
+ "http://server.local/chronograf/v1/sources/1",
+ ioutil.NopCloser(
+ bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
+ },
+ fields: fields{
+ UseAuth: true,
+ Logger: log.New(log.DebugLevel),
+ SourcesStore: &mocks.SourcesStore{
+ GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
+ return chronograf.Source{}, fmt.Errorf("No McFly ever amounted to anything in the history of Hill Valley")
+ },
+ },
+ },
+ ID: "1",
+ wantStatus: http.StatusNotFound,
+ wantContentType: "application/json",
+ wantBody: `{"code":404,"message":"ID 1 not found"}`,
+ },
+ {
+ name: "Bad ID",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest(
+ "POST",
+ "http://server.local/chronograf/v1/sources/1",
+ ioutil.NopCloser(
+ bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
},
fields: fields{
UseAuth: true,
Logger: log.New(log.DebugLevel),
},
- wantStatus: http.StatusUnprocessableEntity,
- principal: oauth2.Principal{
- Subject: "",
+ ID: "BAD",
+ wantStatus: http.StatusUnprocessableEntity,
+ wantContentType: "application/json",
+ wantBody: `{"code":422,"message":"Error converting ID BAD"}`,
+ },
+ {
+ name: "Bad name",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest(
+ "POST",
+ "http://server.local/chronograf/v1/sources/1",
+ ioutil.NopCloser(
+ bytes.NewReader([]byte(`{"password": "the_lake"}`)))),
},
+ fields: fields{
+ UseAuth: true,
+ Logger: log.New(log.DebugLevel),
+ },
+ ID: "BAD",
+ wantStatus: http.StatusUnprocessableEntity,
+ wantContentType: "application/json",
+ wantBody: `{"code":422,"message":"Username required"}`,
+ },
+ {
+ name: "Bad JSON",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest(
+ "POST",
+ "http://server.local/chronograf/v1/sources/1",
+ ioutil.NopCloser(
+ bytes.NewReader([]byte(`{password}`)))),
+ },
+ fields: fields{
+ UseAuth: true,
+ Logger: log.New(log.DebugLevel),
+ },
+ ID: "BAD",
+ wantStatus: http.StatusBadRequest,
+ wantContentType: "application/json",
+ wantBody: `{"code":400,"message":"Unparsable JSON"}`,
},
}
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,
+ tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
+ context.Background(),
+ httprouter.Params{
+ {
+ Key: "id",
+ Value: tt.ID,
+ },
+ }))
+
+ h := &server.Service{
+ SourcesStore: tt.fields.SourcesStore,
+ TimeSeriesClient: tt.fields.TimeSeries,
+ Logger: tt.fields.Logger,
+ UseAuth: tt.fields.UseAuth,
}
- h.Me(tt.args.w, tt.args.r)
+ h.NewSourceUser(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. Me() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
+ t.Errorf("%q. NewSourceUser() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
}
if tt.wantContentType != "" && content != tt.wantContentType {
- t.Errorf("%q. Me() = %v, want %v", tt.name, content, tt.wantContentType)
+ t.Errorf("%q. NewSourceUser() = %v, want %v", tt.name, content, tt.wantContentType)
}
if tt.wantBody != "" && string(body) != tt.wantBody {
- t.Errorf("%q. Me() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
+ t.Errorf("%q. NewSourceUser() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
+ }
+ }
+}
+
+func TestService_SourceUsers(t *testing.T) {
+ type fields struct {
+ SourcesStore chronograf.SourcesStore
+ TimeSeries server.TimeSeriesClient
+ Logger chronograf.Logger
+ UseAuth bool
+ }
+ 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: "All users for data source",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest(
+ "GET",
+ "http://server.local/chronograf/v1/sources/1",
+ nil),
+ },
+ fields: fields{
+ UseAuth: true,
+ 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("no roles")
+ },
+ UsersF: func(ctx context.Context) chronograf.UsersStore {
+ return &mocks.UsersStore{
+ AllF: func(ctx context.Context) ([]chronograf.User, error) {
+ return []chronograf.User{
+ {
+ Name: "strickland",
+ Passwd: "discipline",
+ Permissions: chronograf.Permissions{
+ {
+ Scope: chronograf.AllScope,
+ Allowed: chronograf.Allowances{"READ"},
+ },
+ },
+ },
+ }, nil
+ },
+ }
+ },
+ },
+ },
+ ID: "1",
+ wantStatus: http.StatusOK,
+ wantContentType: "application/json",
+ wantBody: `{"users":[{"links":{"self":"/chronograf/v1/sources/1/users/strickland"},"name":"strickland","permissions":[{"scope":"all","allowed":["READ"]}]}]}
+`,
+ },
+ {
+ name: "All users for data source with roles",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest(
+ "GET",
+ "http://server.local/chronograf/v1/sources/1",
+ nil),
+ },
+ fields: fields{
+ UseAuth: true,
+ 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, nil
+ },
+ UsersF: func(ctx context.Context) chronograf.UsersStore {
+ return &mocks.UsersStore{
+ AllF: func(ctx context.Context) ([]chronograf.User, error) {
+ return []chronograf.User{
+ {
+ Name: "strickland",
+ Passwd: "discipline",
+ Permissions: chronograf.Permissions{
+ {
+ Scope: chronograf.AllScope,
+ Allowed: chronograf.Allowances{"READ"},
+ },
+ },
+ },
+ }, nil
+ },
+ }
+ },
+ },
+ },
+ ID: "1",
+ wantStatus: http.StatusOK,
+ wantContentType: "application/json",
+ wantBody: `{"users":[{"links":{"self":"/chronograf/v1/sources/1/users/strickland"},"name":"strickland","permissions":[{"scope":"all","allowed":["READ"]}],"roles":[]}]}
+`,
+ },
+ }
+ for _, tt := range tests {
+ tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
+ context.Background(),
+ httprouter.Params{
+ {
+ Key: "id",
+ Value: tt.ID,
+ },
+ }))
+ h := &server.Service{
+ SourcesStore: tt.fields.SourcesStore,
+ TimeSeriesClient: tt.fields.TimeSeries,
+ Logger: tt.fields.Logger,
+ UseAuth: tt.fields.UseAuth,
+ }
+
+ h.SourceUsers(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. SourceUsers() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
+ }
+ if tt.wantContentType != "" && content != tt.wantContentType {
+ t.Errorf("%q. SourceUsers() = %v, want %v", tt.name, content, tt.wantContentType)
+ }
+ if tt.wantBody != "" && string(body) != tt.wantBody {
+ t.Errorf("%q. SourceUsers() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
+ }
+ }
+}
+
+func TestService_SourceUserID(t *testing.T) {
+ type fields struct {
+ SourcesStore chronograf.SourcesStore
+ TimeSeries server.TimeSeriesClient
+ Logger chronograf.Logger
+ UseAuth bool
+ }
+ type args struct {
+ w *httptest.ResponseRecorder
+ r *http.Request
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ ID string
+ UID string
+ wantStatus int
+ wantContentType string
+ wantBody string
+ }{
+ {
+ name: "Single user for data source",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest(
+ "GET",
+ "http://server.local/chronograf/v1/sources/1",
+ nil),
+ },
+ fields: fields{
+ UseAuth: true,
+ 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("no roles")
+ },
+ UsersF: func(ctx context.Context) chronograf.UsersStore {
+ return &mocks.UsersStore{
+ GetF: func(ctx context.Context, uid string) (*chronograf.User, error) {
+ return &chronograf.User{
+ Name: "strickland",
+ Passwd: "discipline",
+ Permissions: chronograf.Permissions{
+ {
+ Scope: chronograf.AllScope,
+ Allowed: chronograf.Allowances{"READ"},
+ },
+ },
+ }, nil
+ },
+ }
+ },
+ },
+ },
+ ID: "1",
+ UID: "strickland",
+ wantStatus: http.StatusOK,
+ wantContentType: "application/json",
+ wantBody: `{"links":{"self":"/chronograf/v1/sources/1/users/strickland"},"name":"strickland","permissions":[{"scope":"all","allowed":["READ"]}]}
+`,
+ },
+ {
+ name: "Single user for data source",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest(
+ "GET",
+ "http://server.local/chronograf/v1/sources/1",
+ nil),
+ },
+ fields: fields{
+ UseAuth: true,
+ 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, nil
+ },
+ UsersF: func(ctx context.Context) chronograf.UsersStore {
+ return &mocks.UsersStore{
+ GetF: func(ctx context.Context, uid string) (*chronograf.User, error) {
+ return &chronograf.User{
+ Name: "strickland",
+ Passwd: "discipline",
+ Permissions: chronograf.Permissions{
+ {
+ Scope: chronograf.AllScope,
+ Allowed: chronograf.Allowances{"READ"},
+ },
+ },
+ }, nil
+ },
+ }
+ },
+ },
+ },
+ ID: "1",
+ UID: "strickland",
+ wantStatus: http.StatusOK,
+ wantContentType: "application/json",
+ wantBody: `{"links":{"self":"/chronograf/v1/sources/1/users/strickland"},"name":"strickland","permissions":[{"scope":"all","allowed":["READ"]}],"roles":[]}
+`,
+ },
+ }
+ for _, tt := range tests {
+ tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
+ context.Background(),
+ httprouter.Params{
+ {
+ Key: "id",
+ Value: tt.ID,
+ },
+ }))
+ h := &server.Service{
+ SourcesStore: tt.fields.SourcesStore,
+ TimeSeriesClient: tt.fields.TimeSeries,
+ Logger: tt.fields.Logger,
+ UseAuth: tt.fields.UseAuth,
+ }
+
+ h.SourceUserID(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. SourceUserID() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
+ }
+ if tt.wantContentType != "" && content != tt.wantContentType {
+ t.Errorf("%q. SourceUserID() = %v, want %v", tt.name, content, tt.wantContentType)
+ }
+ if tt.wantBody != "" && string(body) != tt.wantBody {
+ t.Errorf("%q. SourceUserID() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
+ }
+ }
+}
+
+func TestService_RemoveSourceUser(t *testing.T) {
+ type fields struct {
+ SourcesStore chronograf.SourcesStore
+ TimeSeries server.TimeSeriesClient
+ Logger chronograf.Logger
+ UseAuth bool
+ }
+ type args struct {
+ w *httptest.ResponseRecorder
+ r *http.Request
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ ID string
+ UID string
+ wantStatus int
+ wantContentType string
+ wantBody string
+ }{
+ {
+ name: "Delete user for data source",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest(
+ "GET",
+ "http://server.local/chronograf/v1/sources/1",
+ nil),
+ },
+ fields: fields{
+ UseAuth: true,
+ 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
+ },
+ UsersF: func(ctx context.Context) chronograf.UsersStore {
+ return &mocks.UsersStore{
+ DeleteF: func(ctx context.Context, u *chronograf.User) error {
+ return nil
+ },
+ }
+ },
+ },
+ },
+ ID: "1",
+ UID: "strickland",
+ wantStatus: http.StatusNoContent,
+ },
+ }
+ for _, tt := range tests {
+ tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
+ context.Background(),
+ httprouter.Params{
+ {
+ Key: "id",
+ Value: tt.ID,
+ },
+ }))
+ h := &server.Service{
+ SourcesStore: tt.fields.SourcesStore,
+ TimeSeriesClient: tt.fields.TimeSeries,
+ Logger: tt.fields.Logger,
+ UseAuth: tt.fields.UseAuth,
+ }
+ h.RemoveSourceUser(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. RemoveSourceUser() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
+ }
+ if tt.wantContentType != "" && content != tt.wantContentType {
+ t.Errorf("%q. RemoveSourceUser() = %v, want %v", tt.name, content, tt.wantContentType)
+ }
+ if tt.wantBody != "" && string(body) != tt.wantBody {
+ t.Errorf("%q. RemoveSourceUser() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
+ }
+ }
+}
+
+func TestService_UpdateSourceUser(t *testing.T) {
+ type fields struct {
+ SourcesStore chronograf.SourcesStore
+ TimeSeries server.TimeSeriesClient
+ Logger chronograf.Logger
+ UseAuth bool
+ }
+ type args struct {
+ w *httptest.ResponseRecorder
+ r *http.Request
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ ID string
+ UID string
+ wantStatus int
+ wantContentType string
+ wantBody string
+ }{
+ {
+ name: "Update user password for data source",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest(
+ "POST",
+ "http://server.local/chronograf/v1/sources/1",
+ ioutil.NopCloser(
+ bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
+ },
+ fields: fields{
+ UseAuth: true,
+ 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("no roles")
+ },
+ UsersF: func(ctx context.Context) chronograf.UsersStore {
+ return &mocks.UsersStore{
+ UpdateF: func(ctx context.Context, u *chronograf.User) error {
+ return nil
+ },
+ GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
+ return &chronograf.User{
+ Name: "marty",
+ }, nil
+ },
+ }
+ },
+ },
+ },
+ ID: "1",
+ UID: "marty",
+ wantStatus: http.StatusOK,
+ wantContentType: "application/json",
+ wantBody: `{"links":{"self":"/chronograf/v1/sources/1/users/marty"},"name":"marty","permissions":[]}
+`,
+ },
+ {
+ name: "Update user password for data source with roles",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest(
+ "POST",
+ "http://server.local/chronograf/v1/sources/1",
+ ioutil.NopCloser(
+ bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))),
+ },
+ fields: fields{
+ UseAuth: true,
+ 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, nil
+ },
+ UsersF: func(ctx context.Context) chronograf.UsersStore {
+ return &mocks.UsersStore{
+ UpdateF: func(ctx context.Context, u *chronograf.User) error {
+ return nil
+ },
+ GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
+ return &chronograf.User{
+ Name: "marty",
+ }, nil
+ },
+ }
+ },
+ },
+ },
+ ID: "1",
+ UID: "marty",
+ wantStatus: http.StatusOK,
+ wantContentType: "application/json",
+ wantBody: `{"links":{"self":"/chronograf/v1/sources/1/users/marty"},"name":"marty","permissions":[],"roles":[]}
+`,
+ },
+ {
+ name: "Invalid update JSON",
+ args: args{
+ w: httptest.NewRecorder(),
+ r: httptest.NewRequest(
+ "POST",
+ "http://server.local/chronograf/v1/sources/1",
+ ioutil.NopCloser(
+ bytes.NewReader([]byte(`{"name": "marty"}`)))),
+ },
+ fields: fields{
+ UseAuth: true,
+ Logger: log.New(log.DebugLevel),
+ },
+ ID: "1",
+ UID: "marty",
+ wantStatus: http.StatusUnprocessableEntity,
+ wantContentType: "application/json",
+ wantBody: `{"code":422,"message":"No fields to update"}`,
+ },
+ }
+ for _, tt := range tests {
+ tt.args.r = tt.args.r.WithContext(httprouter.WithParams(
+ context.Background(),
+ httprouter.Params{
+ {
+ Key: "id",
+ Value: tt.ID,
+ },
+ {
+ Key: "uid",
+ Value: tt.UID,
+ },
+ }))
+ h := &server.Service{
+ SourcesStore: tt.fields.SourcesStore,
+ TimeSeriesClient: tt.fields.TimeSeries,
+ Logger: tt.fields.Logger,
+ UseAuth: tt.fields.UseAuth,
+ }
+ h.UpdateSourceUser(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. UpdateSourceUser() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
+ }
+ if tt.wantContentType != "" && content != tt.wantContentType {
+ t.Errorf("%q. UpdateSourceUser() = %v, want %v", tt.name, content, tt.wantContentType)
+ }
+ if tt.wantBody != "" && string(body) != tt.wantBody {
+ t.Errorf("%q. UpdateSourceUser() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
}
}
}
diff --git a/server/version.go b/server/version.go
index 6bf7dbe9d..e7fc4c901 100644
--- a/server/version.go
+++ b/server/version.go
@@ -4,6 +4,7 @@ import (
"net/http"
)
+// Version handler adds X-Chronograf-Version header to responses
func Version(version string, h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-Chronograf-Version", version)
diff --git a/ui/.eslintrc b/ui/.eslintrc
index 7e30b492e..564d38c56 100644
--- a/ui/.eslintrc
+++ b/ui/.eslintrc
@@ -230,7 +230,7 @@
'react/no-string-refs': 0, // TODO: 2
'react/no-unknown-property': 2,
'react/prop-types': 2,
- 'react/prefer-es6-class': 0,
+ 'react/prefer-es6-class': [0, 'never'],
'react/react-in-jsx-scope': 2,
'react/require-extension': 0,
'react/self-closing-comp': 0, // TODO: we can re-enable this if some brave soul wants to update the code (mostly spans acting as icons)
diff --git a/ui/spec/shared/reducers/appSpec.js b/ui/spec/shared/reducers/appSpec.js
new file mode 100644
index 000000000..e43ee650a
--- /dev/null
+++ b/ui/spec/shared/reducers/appSpec.js
@@ -0,0 +1,40 @@
+import appReducer from 'src/shared/reducers/app'
+import {
+ enablePresentationMode,
+ disablePresentationMode,
+ // delayEnablePresentationMode,
+ setAutoRefresh,
+} from 'src/shared/actions/app'
+
+describe('Shared.Reducers.appReducer', () => {
+ const initialState = {
+ ephemeral: {
+ inPresentationMode: false,
+ },
+ persisted: {
+ autoRefresh: 0
+ },
+ }
+
+ it('should handle ENABLE_PRESENTATION_MODE', () => {
+ const reducedState = appReducer(initialState, enablePresentationMode());
+
+ expect(reducedState.ephemeral.inPresentationMode).to.equal(true);
+ })
+
+ it('should handle DISABLE_PRESENTATION_MODE', () => {
+ Object.assign(initialState, {ephemeral: {inPresentationMode: true}})
+
+ const reducedState = appReducer(initialState, disablePresentationMode());
+
+ expect(reducedState.ephemeral.inPresentationMode).to.equal(false);
+ })
+
+ it('should handle SET_AUTOREFRESH', () => {
+ const expectedMs = 15000
+
+ const reducedState = appReducer(initialState, setAutoRefresh(expectedMs));
+
+ expect(reducedState.persisted.autoRefresh).to.equal(expectedMs);
+ })
+})
diff --git a/ui/src/admin/containers/AdminPage.js b/ui/src/admin/containers/AdminPage.js
index 37ff0688f..3ec74b635 100644
--- a/ui/src/admin/containers/AdminPage.js
+++ b/ui/src/admin/containers/AdminPage.js
@@ -158,7 +158,7 @@ class AdminPage extends Component {
{
- users.length ?
+ users ?
+
+
+
diff --git a/ui/src/dashboards/components/Dashboard.js b/ui/src/dashboards/components/Dashboard.js
index 46eb173b0..2f03f4079 100644
--- a/ui/src/dashboards/components/Dashboard.js
+++ b/ui/src/dashboards/components/Dashboard.js
@@ -10,6 +10,7 @@ const Dashboard = ({
inPresentationMode,
onPositionChange,
source,
+ autoRefresh,
timeRange,
}) => {
if (dashboard.id === 0) {
@@ -20,20 +21,19 @@ const Dashboard = ({
{isEditMode ? : null}
- {Dashboard.renderDashboard(dashboard, timeRange, source, onPositionChange)}
+ {Dashboard.renderDashboard(dashboard, autoRefresh, timeRange, source, onPositionChange)}
)
}
-Dashboard.renderDashboard = (dashboard, timeRange, source, onPositionChange) => {
- const autoRefreshMs = 15000
+Dashboard.renderDashboard = (dashboard, autoRefresh, timeRange, source, onPositionChange) => {
const cells = dashboard.cells.map((cell, i) => {
i = `${i}`
const dashboardCell = {...cell, i}
dashboardCell.queries.forEach((q) => {
q.text = q.query;
- q.database = source.telegraf;
+ q.database = q.db;
});
return dashboardCell;
})
@@ -42,7 +42,7 @@ Dashboard.renderDashboard = (dashboard, timeRange, source, onPositionChange) =>
@@ -54,6 +54,7 @@ const {
func,
shape,
string,
+ number,
} = PropTypes
Dashboard.propTypes = {
@@ -66,6 +67,7 @@ Dashboard.propTypes = {
proxy: string,
}).isRequired,
}).isRequired,
+ autoRefresh: number.isRequired,
timeRange: shape({}).isRequired,
}
diff --git a/ui/src/dashboards/components/DashboardHeader.js b/ui/src/dashboards/components/DashboardHeader.js
index be6fd69a0..3c9c657d2 100644
--- a/ui/src/dashboards/components/DashboardHeader.js
+++ b/ui/src/dashboards/components/DashboardHeader.js
@@ -2,7 +2,9 @@ import React, {PropTypes} from 'react'
import ReactTooltip from 'react-tooltip'
import {Link} from 'react-router';
+import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown'
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
+import SourceIndicator from '../../shared/components/SourceIndicator'
const DashboardHeader = ({
children,
@@ -10,10 +12,13 @@ const DashboardHeader = ({
dashboard,
headerText,
timeRange,
+ autoRefresh,
isHidden,
handleChooseTimeRange,
+ handleChooseAutoRefresh,
handleClickPresentationButton,
sourceID,
+ source,
}) => isHidden ? null : (
@@ -45,6 +50,8 @@ const DashboardHeader = ({
Graph Tips
+
+
@@ -55,11 +62,12 @@ const DashboardHeader = ({
)
const {
- shape,
array,
- string,
- func,
bool,
+ func,
+ number,
+ shape,
+ string,
} = PropTypes
DashboardHeader.propTypes = {
@@ -69,9 +77,12 @@ DashboardHeader.propTypes = {
dashboard: shape({}),
headerText: string,
timeRange: shape({}).isRequired,
+ autoRefresh: number.isRequired,
isHidden: bool.isRequired,
handleChooseTimeRange: func.isRequired,
+ handleChooseAutoRefresh: func.isRequired,
handleClickPresentationButton: func.isRequired,
+ source: shape({}),
}
export default DashboardHeader
diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js
index c83c92020..38aa33c5b 100644
--- a/ui/src/dashboards/containers/DashboardPage.js
+++ b/ui/src/dashboards/containers/DashboardPage.js
@@ -51,6 +51,7 @@ const DashboardPage = React.createClass({
id: number.isRequired,
cells: arrayOf(shape({})).isRequired,
}).isRequired,
+ autoRefresh: number.isRequired,
timeRange: shape({}).isRequired,
inPresentationMode: bool.isRequired,
isEditMode: bool.isRequired,
@@ -100,6 +101,7 @@ const DashboardPage = React.createClass({
isEditMode,
handleClickPresentationButton,
source,
+ autoRefresh,
timeRange,
} = this.props
@@ -110,12 +112,14 @@ const DashboardPage = React.createClass({
{}} /> :
{(dashboards).map((d, i) => {
return (
@@ -133,6 +137,7 @@ const DashboardPage = React.createClass({
isEditMode={isEditMode}
inPresentationMode={inPresentationMode}
source={source}
+ autoRefresh={autoRefresh}
timeRange={timeRange}
onPositionChange={this.handleUpdatePosition}
/>
@@ -143,7 +148,10 @@ const DashboardPage = React.createClass({
const mapStateToProps = (state) => {
const {
- appUI,
+ app: {
+ ephemeral: {inPresentationMode},
+ persisted: {autoRefresh},
+ },
dashboardUI: {
dashboards,
dashboard,
@@ -153,11 +161,12 @@ const mapStateToProps = (state) => {
} = state
return {
- inPresentationMode: appUI.presentationMode,
dashboards,
dashboard,
+ autoRefresh,
timeRange,
isEditMode,
+ inPresentationMode,
}
}
diff --git a/ui/src/dashboards/containers/DashboardsPage.js b/ui/src/dashboards/containers/DashboardsPage.js
index 6ffc66d09..cedbcae9d 100644
--- a/ui/src/dashboards/containers/DashboardsPage.js
+++ b/ui/src/dashboards/containers/DashboardsPage.js
@@ -1,5 +1,6 @@
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
+import SourceIndicator from '../../shared/components/SourceIndicator';
import {getDashboards} from '../apis';
@@ -53,6 +54,9 @@ const DashboardsPage = React.createClass({
Dashboards
+
+
+
diff --git a/ui/src/data_explorer/components/Visualization.js b/ui/src/data_explorer/components/Visualization.js
index b88972a9e..b58355da6 100644
--- a/ui/src/data_explorer/components/Visualization.js
+++ b/ui/src/data_explorer/components/Visualization.js
@@ -15,6 +15,7 @@ const {
const Visualization = React.createClass({
propTypes: {
+ autoRefresh: number.isRequired,
timeRange: shape({
upper: string,
lower: string,
@@ -45,7 +46,7 @@ const Visualization = React.createClass({
},
render() {
- const {queryConfigs, timeRange, activeQueryIndex, height, heightPixels} = this.props;
+ const {queryConfigs, autoRefresh, timeRange, activeQueryIndex, height, heightPixels} = this.props;
const {source} = this.context;
const proxyLink = source.links.proxy;
@@ -57,7 +58,6 @@ const Visualization = React.createClass({
const queries = statements.filter((s) => s.text !== null).map((s) => {
return {host: [proxyLink], text: s.text, id: s.id};
});
- const autoRefreshMs = 10000;
const isInDataExplorer = true;
return (
@@ -77,7 +77,7 @@ const Visualization = React.createClass({
{isGraphInView ? (
diff --git a/ui/src/data_explorer/containers/DataExplorer.js b/ui/src/data_explorer/containers/DataExplorer.js
index 68a7c7b2e..9468fca75 100644
--- a/ui/src/data_explorer/containers/DataExplorer.js
+++ b/ui/src/data_explorer/containers/DataExplorer.js
@@ -1,17 +1,18 @@
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
import QueryBuilder from '../components/QueryBuilder';
import Visualization from '../components/Visualization';
import Header from '../containers/Header';
import ResizeContainer from 'src/shared/components/ResizeContainer';
-import {
- setTimeRange as setTimeRangeAction,
-} from '../actions/view';
+import {setAutoRefresh} from 'shared/actions/app'
+import {setTimeRange as setTimeRangeAction} from '../actions/view';
const {
arrayOf,
func,
+ number,
shape,
string,
} = PropTypes;
@@ -25,6 +26,8 @@ const DataExplorer = React.createClass({
}).isRequired,
}).isRequired,
queryConfigs: PropTypes.shape({}),
+ autoRefresh: number.isRequired,
+ handleChooseAutoRefresh: func.isRequired,
timeRange: shape({
upper: string,
lower: string,
@@ -59,18 +62,20 @@ const DataExplorer = React.createClass({
},
render() {
- const {timeRange, setTimeRange, queryConfigs, dataExplorer} = this.props;
+ const {autoRefresh, handleChooseAutoRefresh, timeRange, setTimeRange, queryConfigs, dataExplorer} = this.props;
const {activeQueryID} = this.state;
const queries = dataExplorer.queryIDs.map((qid) => queryConfigs[qid]);
return (
@@ -45,11 +58,8 @@ const Header = React.createClass({
Explorer
-
Source:
-
-
- {this.context.source.name}
-
+
+
diff --git a/ui/src/data_explorer/reducers/index.js b/ui/src/data_explorer/reducers/index.js
index cccca5d79..6db959b19 100644
--- a/ui/src/data_explorer/reducers/index.js
+++ b/ui/src/data_explorer/reducers/index.js
@@ -2,8 +2,8 @@ import queryConfigs from './queryConfigs';
import timeRange from './timeRange';
import dataExplorer from './ui';
-export {
+export default {
queryConfigs,
timeRange,
dataExplorer,
-};
+}
diff --git a/ui/src/hosts/components/HostsTable.js b/ui/src/hosts/components/HostsTable.js
index e4a98f4d2..0801871d0 100644
--- a/ui/src/hosts/components/HostsTable.js
+++ b/ui/src/hosts/components/HostsTable.js
@@ -87,12 +87,12 @@ const HostsTable = React.createClass({
const hostCount = sortedHosts.length;
let hostsTitle;
- if (hostCount === 1) {
- hostsTitle = `${hostCount} Host`;
- } else if (hostCount > 1) {
- hostsTitle = `${hostCount} Hosts`;
- } else {
+ if (hosts.length === 0) {
hostsTitle = `Loading Hosts...`;
+ } else if (hostCount === 1) {
+ hostsTitle = `${hostCount} Host`;
+ } else {
+ hostsTitle = `${hostCount} Hosts`;
}
return (
diff --git a/ui/src/hosts/containers/HostPage.js b/ui/src/hosts/containers/HostPage.js
index 38ff3ae16..fbed5e336 100644
--- a/ui/src/hosts/containers/HostPage.js
+++ b/ui/src/hosts/containers/HostPage.js
@@ -1,6 +1,7 @@
import React, {PropTypes} from 'react'
import {Link} from 'react-router'
import {connect} from 'react-redux'
+import {bindActionCreators} from 'redux'
import _ from 'lodash'
import classnames from 'classnames';
@@ -9,6 +10,8 @@ import DashboardHeader from 'src/dashboards/components/DashboardHeader';
import timeRanges from 'hson!../../shared/data/timeRanges.hson';
import {getMappings, getAppsForHosts, getMeasurementsForHost, getAllHosts} from 'src/hosts/apis';
import {fetchLayouts} from 'shared/apis';
+
+import {setAutoRefresh} from 'shared/actions/app'
import {presentationButtonDispatcher} from 'shared/dispatchers'
const {
@@ -16,6 +19,7 @@ const {
string,
bool,
func,
+ number,
} = PropTypes
export const HostPage = React.createClass({
@@ -35,6 +39,8 @@ export const HostPage = React.createClass({
app: string,
}),
}),
+ autoRefresh: number.isRequired,
+ handleChooseAutoRefresh: func.isRequired,
inPresentationMode: bool,
handleClickPresentationButton: func,
},
@@ -87,9 +93,8 @@ export const HostPage = React.createClass({
},
renderLayouts(layouts) {
- const autoRefreshMs = 15000;
const {timeRange} = this.state;
- const {source} = this.props;
+ const {source, autoRefresh} = this.props;
const autoflowLayouts = layouts.filter((layout) => !!layout.autoflow);
@@ -137,7 +142,7 @@ export const HostPage = React.createClass({
@@ -145,7 +150,16 @@ export const HostPage = React.createClass({
},
render() {
- const {params: {hostID}, location: {query: {app}}, source: {id}, inPresentationMode, handleClickPresentationButton} = this.props
+ const {
+ params: {hostID},
+ location: {query: {app}},
+ source: {id},
+ autoRefresh,
+ handleChooseAutoRefresh,
+ inPresentationMode,
+ handleClickPresentationButton,
+ source,
+ } = this.props
const {layouts, timeRange, hosts} = this.state
const appParam = app ? `?app=${app}` : ''
@@ -153,10 +167,13 @@ export const HostPage = React.createClass({
{Object.keys(hosts).map((host, i) => {
return (
@@ -181,11 +198,13 @@ export const HostPage = React.createClass({
},
});
-const mapStateToProps = (state) => ({
- inPresentationMode: state.appUI.presentationMode,
+const mapStateToProps = ({app: {ephemeral: {inPresentationMode}, persisted: {autoRefresh}}}) => ({
+ inPresentationMode,
+ autoRefresh,
})
const mapDispatchToProps = (dispatch) => ({
+ handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch),
handleClickPresentationButton: presentationButtonDispatcher(dispatch),
})
diff --git a/ui/src/hosts/containers/HostsPage.js b/ui/src/hosts/containers/HostsPage.js
index dd3a7b846..127d0d582 100644
--- a/ui/src/hosts/containers/HostsPage.js
+++ b/ui/src/hosts/containers/HostsPage.js
@@ -1,6 +1,7 @@
import React, {PropTypes} from 'react';
import _ from 'lodash';
import HostsTable from '../components/HostsTable';
+import SourceIndicator from '../../shared/components/SourceIndicator'
import {getCpuAndLoadForHosts, getMappings, getAppsForHosts} from '../apis';
export const HostsPage = React.createClass({
@@ -44,6 +45,7 @@ export const HostsPage = React.createClass({
},
render() {
+ const {source} = this.props;
return (
@@ -53,13 +55,16 @@ export const HostsPage = React.createClass({
Host List
+
+
+
diff --git a/ui/src/index.js b/ui/src/index.js
index db8cc6663..d8eb4fde5 100644
--- a/ui/src/index.js
+++ b/ui/src/index.js
@@ -20,7 +20,7 @@ import configureStore from 'src/store/configureStore';
import {getMe, getSources} from 'shared/apis';
import {receiveMe} from 'shared/actions/me';
import {receiveAuth} from 'shared/actions/auth';
-import {disablePresentationMode} from 'shared/actions/ui';
+import {disablePresentationMode} from 'shared/actions/app';
import {loadLocalStorage} from './localStorage';
import 'src/style/chronograf.scss';
diff --git a/ui/src/kapacitor/actions/view/index.js b/ui/src/kapacitor/actions/view/index.js
index 2c89fe293..3d9f9e6d9 100644
--- a/ui/src/kapacitor/actions/view/index.js
+++ b/ui/src/kapacitor/actions/view/index.js
@@ -163,12 +163,11 @@ export function deleteRule(rule) {
};
}
-export function updateRuleStatus(rule, {status}) {
+export function updateRuleStatus(rule, status) {
return (dispatch) => {
updateRuleStatusAPI(rule, status).then(() => {
dispatch(publishNotification('success', `${rule.name} ${status} successfully`));
}).catch(() => {
- dispatch(updateRuleStatusSuccess(rule.id, status));
dispatch(publishNotification('error', `${rule.name} could not be ${status}`));
});
};
diff --git a/ui/src/kapacitor/components/AlertOutputs.js b/ui/src/kapacitor/components/AlertOutputs.js
index 303fc1c6b..04b154f5e 100644
--- a/ui/src/kapacitor/components/AlertOutputs.js
+++ b/ui/src/kapacitor/components/AlertOutputs.js
@@ -105,7 +105,7 @@ const AlertOutputs = React.createClass({
return (
-
Configure Alert Endpoints
+
Configure Alert Endpoints
diff --git a/ui/src/kapacitor/components/AlertaConfig.js b/ui/src/kapacitor/components/AlertaConfig.js
index edc394f18..cb0f9ff62 100644
--- a/ui/src/kapacitor/components/AlertaConfig.js
+++ b/ui/src/kapacitor/components/AlertaConfig.js
@@ -31,10 +31,10 @@ const AlertaConfig = React.createClass({
return (
-
Alerta Alert
+
Alerta Alert