diff --git a/CHANGELOG.md b/CHANGELOG.md index 99cbd06cef..d157bc66b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## v1.2.0 [unreleased] ### Bug Fixes + 1. [#936](https://github.com/influxdata/chronograf/pull/936): Fix leaking sockets for InfluxQL queries ### Features diff --git a/canned/consul_agent.json b/canned/consul_agent.json new file mode 100644 index 0000000000..27bc353d0c --- /dev/null +++ b/canned/consul_agent.json @@ -0,0 +1,59 @@ +{ + "id": "f3bec493-0bc1-49d5-a40a-a09bd5cfb700", + "measurement": "consul_consul_fsm_register", + "app": "consul_telemetry", + "autoflow": true, + "cells": [ + { + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "i": "9e14639d-b8d9-4245-8c45-862ed4383701", + "name": "Consul Agent – Number of Go Routines", + "queries": [ + { + "query": "SELECT max(\"value\") AS \"Go Routines\" FROM \"consul_ip-172-31-6-247_runtime_num_goroutines\"", + "groupbys": [ + ], + "wheres": [ + ] + } + ] + }, + { + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "i": "9e14639d-b8d9-4245-8c45-862ed4383702", + "name": "Consul Agent – Runtime Alloc Bytes", + "queries": [ + { + "query": "SELECT max(\"value\") AS \"Runtime Alloc Bytes\" FROM \"consul_ip-172-31-6-247_runtime_alloc_bytes\"", + "groupbys": [ + ], + "wheres": [ + ] + } + ] + }, + { + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "i": "9e14639d-b8d9-4245-8c45-862ed4383703", + "name": "Consul Agent – Heap Objects", + "queries": [ + { + "query": "SELECT max(\"value\") AS \"Heap Objects\" FROM \"consul_ip-172-31-6-247_runtime_heap_objects\"", + "groupbys": [ + ], + "wheres": [ + ] + } + ] + } + ] +} diff --git a/canned/consul_cluster.json b/canned/consul_cluster.json new file mode 100644 index 0000000000..8c560d338f --- /dev/null +++ b/canned/consul_cluster.json @@ -0,0 +1,24 @@ +{ + "id": "350b780c-7d32-4b29-ac49-0d4e2c092943", + "measurement": "consul_memberlist_msg_alive", + "app": "consul_telemetry", + "autoflow": true, + "cells": [ + { + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "i": "bd62186a-f475-478b-bf02-8c4ab07eccd1", + "name": "Consul – Number of Agents", + "queries": [ + { + "query": "SELECT min(\"value\") AS \"num_agents\" FROM \"consul_memberlist_msg_alive\"", + "label": "count", + "groupbys": [], + "wheres": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/canned/consul_election.json b/canned/consul_election.json new file mode 100644 index 0000000000..0df30c166e --- /dev/null +++ b/canned/consul_election.json @@ -0,0 +1,24 @@ +{ + "id": "b15aaf24-701a-4d9b-920c-9a407e91da71", + "measurement": "consul_raft_state_candidate", + "app": "consul_telemetry", + "autoflow": true, + "cells": [ + { + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "i": "5b2bddce-badb-4594-91fb-0486f62266e5", + "name": "Consul – Leadership Election", + "queries": [ + { + "query": "SELECT max(\"value\") AS \"max_value\" FROM \"consul_raft_state_candidate\"", + "label": "count", + "groupbys": [], + "wheres": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/canned/consul_http.json b/canned/consul_http.json new file mode 100644 index 0000000000..84a9ea986f --- /dev/null +++ b/canned/consul_http.json @@ -0,0 +1,24 @@ +{ + "id": "26809869-8df3-49ad-b2f0-b1e1c72f67b0", + "measurement": "consul_consul_http_GET_v1_health_state__", + "app": "consul_telemetry", + "autoflow": true, + "cells": [ + { + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "i": "dfb4c50f-547e-484a-944b-d6374ba2b4c0", + "name": "Consul – HTTP Request Time (ms)", + "queries": [ + { + "query": "SELECT max(\"upper\") AS \"GET_health_state\" FROM \"consul_consul_http_GET_v1_health_state__\"", + "label": "ms", + "groupbys": [], + "wheres": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/canned/consul_leadership.json b/canned/consul_leadership.json new file mode 100644 index 0000000000..cb49c6b211 --- /dev/null +++ b/canned/consul_leadership.json @@ -0,0 +1,24 @@ +{ + "id": "34611ae0-7c3e-4697-8db0-371b16bef345", + "measurement": "consul_raft_state_leader", + "app": "consul_telemetry", + "autoflow": true, + "cells": [ + { + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "i": "ef8eeeb5-b408-46d6-8cfc-20c00c9d7239", + "name": "Consul – Leadership Change", + "queries": [ + { + "query": "SELECT max(\"value\") as \"change\" FROM \"consul_raft_state_leader\"", + "label": "count", + "groupbys": [], + "wheres": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/canned/consul_serf_events.json b/canned/consul_serf_events.json new file mode 100644 index 0000000000..5fe79cd6f0 --- /dev/null +++ b/canned/consul_serf_events.json @@ -0,0 +1,24 @@ +{ + "id": "ef4b596c-77de-41c5-bb5b-d5c9a69fa633", + "measurement": "consul_serf_events", + "app": "consul_telemetry", + "autoflow": true, + "cells": [ + { + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "i": "59df3d73-5fac-48cb-84f1-dbe9a1bb886c", + "name": "Consul – Number of serf events", + "queries": [ + { + "query": "SELECT max(\"value\") AS \"serf_events\" FROM \"consul_serf_events\"", + "label": "count", + "groupbys": [], + "wheres": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/chronograf.go b/chronograf.go index 01d6f90a44..1a8b7730d8 100644 --- a/chronograf.go +++ b/chronograf.go @@ -275,6 +275,7 @@ type User struct { Name string `json:"name"` Passwd string `json:"password"` Permissions Permissions `json:"permissions,omitempty"` + Roles []Role `json:"roles,omitempty"` } // UsersStore is the Storage and retrieval of authentication information diff --git a/enterprise/enterprise.go b/enterprise/enterprise.go index b8f2ee22e8..3a39993492 100644 --- a/enterprise/enterprise.go +++ b/enterprise/enterprise.go @@ -24,6 +24,8 @@ type Ctrl interface { ChangePassword(ctx context.Context, name, passwd string) error SetUserPerms(ctx context.Context, name string, perms Permissions) error + UserRoles(ctx context.Context) (map[string]Roles, error) + Roles(ctx context.Context, name *string) (*Roles, error) Role(ctx context.Context, name string) (*Role, error) CreateRole(ctx context.Context, name string) error diff --git a/enterprise/meta.go b/enterprise/meta.go index 1a1274369f..399b057b33 100644 --- a/enterprise/meta.go +++ b/enterprise/meta.go @@ -155,6 +155,27 @@ func (m *MetaClient) SetUserPerms(ctx context.Context, name string, perms Permis return m.Post(ctx, "/user", a, nil) } +// UserRoles returns a map of users to all of their current roles +func (m *MetaClient) UserRoles(ctx context.Context) (map[string]Roles, error) { + res, err := m.Roles(ctx, nil) + if err != nil { + return nil, err + } + + userRoles := make(map[string]Roles) + for _, role := range res.Roles { + for _, u := range role.Users { + ur, ok := userRoles[u] + if !ok { + ur = Roles{} + } + ur.Roles = append(ur.Roles, role) + userRoles[u] = ur + } + } + return userRoles, nil +} + // Roles gets all the roles. If name is not nil it filters for a single role func (m *MetaClient) Roles(ctx context.Context, name *string) (*Roles, error) { params := map[string]string{} diff --git a/enterprise/meta_test.go b/enterprise/meta_test.go index b860f2192d..2823c1256d 100644 --- a/enterprise/meta_test.go +++ b/enterprise/meta_test.go @@ -876,6 +876,110 @@ func TestMetaClient_Role(t *testing.T) { } } +func TestMetaClient_UserRoles(t *testing.T) { + type fields struct { + URL *url.URL + client interface { + Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error) + } + } + type args struct { + ctx context.Context + name *string + } + tests := []struct { + name string + fields fields + args args + want map[string]Roles + wantErr bool + }{ + { + name: "Successful Show all roles", + fields: fields{ + URL: &url.URL{ + Host: "twinpinesmall.net:8091", + Scheme: "https", + }, + client: NewMockClient( + http.StatusOK, + []byte(`{"roles":[{"name":"timetravelers","users":["marty","docbrown"],"permissions":{"":["ViewAdmin","ViewChronograf"]}},{"name":"mcfly","users":["marty","george"],"permissions":{"":["ViewAdmin","ViewChronograf"]}}]}`), + nil, + nil, + ), + }, + args: args{ + ctx: context.Background(), + name: nil, + }, + want: map[string]Roles{ + "marty": Roles{ + Roles: []Role{ + { + Name: "timetravelers", + Permissions: map[string][]string{ + "": []string{ + "ViewAdmin", "ViewChronograf", + }, + }, + Users: []string{"marty", "docbrown"}, + }, + { + Name: "mcfly", + Permissions: map[string][]string{ + "": []string{ + "ViewAdmin", "ViewChronograf", + }, + }, + Users: []string{"marty", "george"}, + }, + }, + }, + "docbrown": Roles{ + Roles: []Role{ + { + Name: "timetravelers", + Permissions: map[string][]string{ + "": []string{ + "ViewAdmin", "ViewChronograf", + }, + }, + Users: []string{"marty", "docbrown"}, + }, + }, + }, + "george": Roles{ + Roles: []Role{ + { + Name: "mcfly", + Permissions: map[string][]string{ + "": []string{ + "ViewAdmin", "ViewChronograf", + }, + }, + Users: []string{"marty", "george"}, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + m := &MetaClient{ + URL: tt.fields.URL, + client: tt.fields.client, + } + got, err := m.UserRoles(tt.args.ctx) + if (err != nil) != tt.wantErr { + t.Errorf("%q. MetaClient.UserRoles() error = %v, wantErr %v", tt.name, err, tt.wantErr) + continue + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("%q. MetaClient.UserRoles() = %v, want %v", tt.name, got, tt.want) + } + } +} + func TestMetaClient_CreateRole(t *testing.T) { type fields struct { URL *url.URL diff --git a/enterprise/mocks_test.go b/enterprise/mocks_test.go index ea467adaca..08e0253624 100644 --- a/enterprise/mocks_test.go +++ b/enterprise/mocks_test.go @@ -68,6 +68,10 @@ func (cc *ControlClient) Role(ctx context.Context, name string) (*enterprise.Rol return nil, nil } +func (ccm *ControlClient) UserRoles(ctx context.Context) (map[string]enterprise.Roles, error) { + return nil, nil +} + func (ccm *ControlClient) Roles(ctx context.Context, name *string) (*enterprise.Roles, error) { return nil, nil } diff --git a/enterprise/roles.go b/enterprise/roles.go index e95d34e8cf..de238dd556 100644 --- a/enterprise/roles.go +++ b/enterprise/roles.go @@ -85,9 +85,13 @@ func (c *RolesStore) All(ctx context.Context) ([]chronograf.Role, error) { return nil, err } - res := make([]chronograf.Role, len(all.Roles)) - for i, role := range all.Roles { + return all.ToChronograf(), nil +} +// ToChronograf converts enterprise roles to chronograf +func (r *Roles) ToChronograf() []chronograf.Role { + res := make([]chronograf.Role, len(r.Roles)) + for i, role := range r.Roles { users := make([]chronograf.User, len(role.Users)) for i, user := range role.Users { users[i] = chronograf.User{ @@ -101,5 +105,5 @@ func (c *RolesStore) All(ctx context.Context) ([]chronograf.Role, error) { Users: users, } } - return res, nil + return res } diff --git a/enterprise/roles_test.go b/enterprise/roles_test.go new file mode 100644 index 0000000000..20805e6a04 --- /dev/null +++ b/enterprise/roles_test.go @@ -0,0 +1,32 @@ +package enterprise + +import ( + "reflect" + "testing" + + "github.com/influxdata/chronograf" +) + +func TestRoles_ToChronograf(t *testing.T) { + tests := []struct { + name string + roles []Role + want []chronograf.Role + }{ + { + name: "empty roles", + roles: []Role{}, + want: []chronograf.Role{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Roles{ + Roles: tt.roles, + } + if got := r.ToChronograf(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Roles.ToChronograf() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/enterprise/users.go b/enterprise/users.go index c1e940567c..ff8059a680 100644 --- a/enterprise/users.go +++ b/enterprise/users.go @@ -35,9 +35,23 @@ func (c *UserStore) Get(ctx context.Context, name string) (*chronograf.User, err if err != nil { return nil, err } + + ur, err := c.Ctrl.UserRoles(ctx) + if err != nil { + return nil, err + } + + role := ur[name] + cr := role.ToChronograf() + // For now we are removing all users from a role being returned. + for i, r := range cr { + r.Users = []chronograf.User{} + cr[i] = r + } return &chronograf.User{ Name: u.Name, Permissions: ToChronograf(u.Permissions), + Roles: cr, }, nil } @@ -59,11 +73,25 @@ func (c *UserStore) All(ctx context.Context) ([]chronograf.User, error) { return nil, err } + ur, err := c.Ctrl.UserRoles(ctx) + if err != nil { + return nil, err + } + res := make([]chronograf.User, len(all.Users)) for i, user := range all.Users { + role := ur[user.Name] + cr := role.ToChronograf() + // For now we are removing all users from a role being returned. + for i, r := range cr { + r.Users = []chronograf.User{} + cr[i] = r + } + res[i] = chronograf.User{ Name: user.Name, Permissions: ToChronograf(user.Permissions), + Roles: cr, } } return res, nil diff --git a/enterprise/users_test.go b/enterprise/users_test.go index 2a64eb55a1..54dc7f5027 100644 --- a/enterprise/users_test.go +++ b/enterprise/users_test.go @@ -180,6 +180,9 @@ func TestClient_Get(t *testing.T) { }, }, nil }, + userRoles: func(ctx context.Context) (map[string]enterprise.Roles, error) { + return map[string]enterprise.Roles{}, nil + }, }, }, args: args{ @@ -194,6 +197,71 @@ func TestClient_Get(t *testing.T) { Allowed: chronograf.Allowances{"ViewChronograf", "ReadData", "WriteData"}, }, }, + Roles: []chronograf.Role{}, + }, + }, + { + name: "Successful Get User with roles", + fields: fields{ + Ctrl: &mockCtrl{ + 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: "timetravels", + Permissions: map[string][]string{ + "": { + "ViewChronograf", + "ReadData", + "WriteData", + }, + }, + Users: []string{"marty", "docbrown"}, + }, + }, + }, + }, nil + }, + }, + }, + args: args{ + ctx: context.Background(), + name: "marty", + }, + want: &chronograf.User{ + Name: "marty", + Permissions: chronograf.Permissions{ + { + Scope: chronograf.AllScope, + Allowed: chronograf.Allowances{"ViewChronograf", "ReadData", "WriteData"}, + }, + }, + Roles: []chronograf.Role{ + { + Name: "timetravels", + Permissions: chronograf.Permissions{ + { + Scope: chronograf.AllScope, + Allowed: chronograf.Allowances{"ViewChronograf", "ReadData", "WriteData"}, + }, + }, + Users: []chronograf.User{}, + }, + }, }, }, { @@ -372,6 +440,9 @@ func TestClient_All(t *testing.T) { }, }, nil }, + userRoles: func(ctx context.Context) (map[string]enterprise.Roles, error) { + return map[string]enterprise.Roles{}, nil + }, }, }, args: args{ @@ -386,6 +457,7 @@ func TestClient_All(t *testing.T) { Allowed: chronograf.Allowances{"ViewChronograf", "ReadData", "WriteData"}, }, }, + Roles: []chronograf.Role{}, }, }, }, @@ -499,6 +571,8 @@ type mockCtrl struct { users func(ctx context.Context, name *string) (*enterprise.Users, error) setUserPerms func(ctx context.Context, name string, perms enterprise.Permissions) error + 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 @@ -510,25 +584,35 @@ type mockCtrl struct { func (m *mockCtrl) ShowCluster(ctx context.Context) (*enterprise.Cluster, error) { return m.showCluster(ctx) } + func (m *mockCtrl) User(ctx context.Context, name string) (*enterprise.User, error) { return m.user(ctx, name) } + func (m *mockCtrl) CreateUser(ctx context.Context, name, passwd string) error { return m.createUser(ctx, name, passwd) } + func (m *mockCtrl) DeleteUser(ctx context.Context, name string) error { return m.deleteUser(ctx, name) } + func (m *mockCtrl) ChangePassword(ctx context.Context, name, passwd string) error { return m.changePassword(ctx, name, passwd) } + func (m *mockCtrl) Users(ctx context.Context, name *string) (*enterprise.Users, error) { return m.users(ctx, name) } + func (m *mockCtrl) SetUserPerms(ctx context.Context, name string, perms enterprise.Permissions) error { return m.setUserPerms(ctx, name, perms) } +func (m *mockCtrl) UserRoles(ctx context.Context) (map[string]enterprise.Roles, error) { + return m.userRoles(ctx) +} + func (m *mockCtrl) Roles(ctx context.Context, name *string) (*enterprise.Roles, error) { return m.roles(ctx, name) } diff --git a/influx/influx.go b/influx/influx.go index 60ef9f786e..02d8eebf1a 100644 --- a/influx/influx.go +++ b/influx/influx.go @@ -13,6 +13,14 @@ import ( var _ chronograf.TimeSeries = &Client{} +// Shared transports for all clients to prevent leaking connections +var ( + skipVerifyTransport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + defaultTransport = &http.Transport{} +) + // Client is a device for retrieving time series data from an InfluxDB instance type Client struct { URL *url.URL @@ -74,10 +82,9 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err hc := &http.Client{} if c.InsecureSkipVerify { - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - hc.Transport = tr + hc.Transport = skipVerifyTransport + } else { + hc.Transport = defaultTransport } resp, err := hc.Do(req) if err != nil { diff --git a/server/admin.go b/server/admin.go index 02d57f72b0..6cc84e0b91 100644 --- a/server/admin.go +++ b/server/admin.go @@ -52,6 +52,7 @@ func (r *sourceUserRequest) ValidUpdate() error { type sourceUser struct { Username string `json:"name,omitempty"` // Username for new account Permissions chronograf.Permissions `json:"permissions,omitempty"` // Account's permissions + Roles []roleResponse `json:"roles,omitempty"` // Roles if source uses them Links selfLinks `json:"links"` // Links are URI locations related to user } @@ -128,11 +129,19 @@ func (h *Service) SourceUsers(w http.ResponseWriter, r *http.Request) { su := []sourceUser{} for _, u := range users { - su = append(su, sourceUser{ + res := sourceUser{ Username: u.Name, Permissions: u.Permissions, Links: newSelfLinks(srcID, "users", u.Name), - }) + } + if len(u.Roles) > 0 { + rr := make([]roleResponse, len(u.Roles)) + for i, role := range u.Roles { + rr[i] = newRoleResponse(srcID, &role) + } + res.Roles = rr + } + su = append(su, res) } res := sourceUsers{ @@ -163,6 +172,13 @@ func (h *Service) SourceUserID(w http.ResponseWriter, r *http.Request) { Permissions: u.Permissions, Links: newSelfLinks(srcID, "users", u.Name), } + if len(u.Roles) > 0 { + rr := make([]roleResponse, len(u.Roles)) + for i, role := range u.Roles { + rr[i] = newRoleResponse(srcID, &role) + } + res.Roles = rr + } encodeJSON(w, http.StatusOK, res, h.Logger) } @@ -346,7 +362,7 @@ func (r *sourceRoleRequest) ValidUpdate() error { } type roleResponse struct { - Users []sourceUser `json:"users"` + Users []sourceUser `json:"users,omitempty"` Name string `json:"name"` Permissions chronograf.Permissions `json:"permissions"` Links selfLinks `json:"links"` diff --git a/server/swagger.json b/server/swagger.json index 84a26970cf..4b67b131cb 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -2565,6 +2565,34 @@ "$ref": "#/definitions/Role" } } + }, + "example": { + "roles": [ + { + "users": [ + { + "name": "admin", + "links": { + "self": "/chronograf/v1/sources/3/users/admin" + } + } + ], + "name": "timetravelers", + "permissions": [ + { + "scope": "database", + "name": "telegraf", + "allowed": [ + "ReadData", + "WriteData" + ] + } + ], + "links": { + "self": "/chronograf/v1/sources/3/roles/timetravelers" + } + } + ] } }, "Role": { @@ -2596,6 +2624,30 @@ } } } + }, + "example": { + "users": [ + { + "name": "admin", + "links": { + "self": "/chronograf/v1/sources/3/users/admin" + } + } + ], + "name": "timetravelers", + "permissions": [ + { + "scope": "database", + "name": "telegraf", + "allowed": [ + "ReadData", + "WriteData" + ] + } + ], + "links": { + "self": "/chronograf/v1/sources/3/roles/timetravelers" + } } }, "Users": { @@ -2616,21 +2668,43 @@ { "scope": "all", "allowed": [ + "ViewAdmin", "ViewChronograf", - "ReadData" - ] - }, - { - "scope": "database", - "name": "telegraf", - "allowed": [ - "ViewChronograf", - "ReadData" + "CreateDatabase", + "CreateUserAndRole", + "DropDatabase", + "DropData", + "ReadData", + "WriteData", + "ManageShard", + "ManageContinuousQuery", + "ManageQuery", + "ManageSubscription", + "Monitor", + "KapacitorAPI" ] } ], + "roles": [ + { + "name": "timetravelers", + "permissions": [ + { + "scope": "database", + "name": "telegraf", + "allowed": [ + "ReadData", + "WriteData" + ] + } + ], + "links": { + "self": "/chronograf/v1/sources/3/roles/timetravelers" + } + } + ], "links": { - "self": "/chronograf/v1/source/1/users/docbrown" + "self": "/chronograf/v1/sources/3/users/docbrown" } } ] @@ -2669,21 +2743,43 @@ { "scope": "all", "allowed": [ + "ViewAdmin", "ViewChronograf", - "ReadData" - ] - }, - { - "scope": "database", - "name": "telegraf", - "allowed": [ - "ViewChronograf", - "ReadData" + "CreateDatabase", + "CreateUserAndRole", + "DropDatabase", + "DropData", + "ReadData", + "WriteData", + "ManageShard", + "ManageContinuousQuery", + "ManageQuery", + "ManageSubscription", + "Monitor", + "KapacitorAPI" ] } ], + "roles": [ + { + "name": "timetravelers", + "permissions": [ + { + "scope": "database", + "name": "telegraf", + "allowed": [ + "ReadData", + "WriteData" + ] + } + ], + "links": { + "self": "/chronograf/v1/sources/3/roles/timetravelers" + } + } + ], "links": { - "self": "/chronograf/v1/source/1/users/docbrown" + "self": "/chronograf/v1/sources/3/users/docbrown" } } }, @@ -3217,4 +3313,4 @@ } } } -} +} \ No newline at end of file