Merge branch 'master' into feature/934-ew-users

pull/10616/head
Jared Scheib 2017-03-01 18:01:56 -08:00
commit 278579d631
19 changed files with 610 additions and 31 deletions

View File

@ -1,6 +1,7 @@
## v1.2.0 [unreleased] ## v1.2.0 [unreleased]
### Bug Fixes ### Bug Fixes
1. [#936](https://github.com/influxdata/chronograf/pull/936): Fix leaking sockets for InfluxQL queries
### Features ### Features

59
canned/consul_agent.json Normal file
View File

@ -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": [
]
}
]
}
]
}

View File

@ -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": []
}
]
}
]
}

View File

@ -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": []
}
]
}
]
}

24
canned/consul_http.json Normal file
View File

@ -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": []
}
]
}
]
}

View File

@ -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": []
}
]
}
]
}

View File

@ -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": []
}
]
}
]
}

View File

@ -275,6 +275,7 @@ type User struct {
Name string `json:"name"` Name string `json:"name"`
Passwd string `json:"password"` Passwd string `json:"password"`
Permissions Permissions `json:"permissions,omitempty"` Permissions Permissions `json:"permissions,omitempty"`
Roles []Role `json:"roles,omitempty"`
} }
// UsersStore is the Storage and retrieval of authentication information // UsersStore is the Storage and retrieval of authentication information

View File

@ -24,6 +24,8 @@ type Ctrl interface {
ChangePassword(ctx context.Context, name, passwd string) error ChangePassword(ctx context.Context, name, passwd string) error
SetUserPerms(ctx context.Context, name string, perms Permissions) 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) Roles(ctx context.Context, name *string) (*Roles, error)
Role(ctx context.Context, name string) (*Role, error) Role(ctx context.Context, name string) (*Role, error)
CreateRole(ctx context.Context, name string) error CreateRole(ctx context.Context, name string) error

View File

@ -155,6 +155,27 @@ func (m *MetaClient) SetUserPerms(ctx context.Context, name string, perms Permis
return m.Post(ctx, "/user", a, nil) 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 // 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) { func (m *MetaClient) Roles(ctx context.Context, name *string) (*Roles, error) {
params := map[string]string{} params := map[string]string{}

View File

@ -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) { func TestMetaClient_CreateRole(t *testing.T) {
type fields struct { type fields struct {
URL *url.URL URL *url.URL

View File

@ -68,6 +68,10 @@ func (cc *ControlClient) Role(ctx context.Context, name string) (*enterprise.Rol
return nil, nil 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) { func (ccm *ControlClient) Roles(ctx context.Context, name *string) (*enterprise.Roles, error) {
return nil, nil return nil, nil
} }

View File

@ -85,9 +85,13 @@ func (c *RolesStore) All(ctx context.Context) ([]chronograf.Role, error) {
return nil, err return nil, err
} }
res := make([]chronograf.Role, len(all.Roles)) return all.ToChronograf(), nil
for i, role := range all.Roles { }
// 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)) users := make([]chronograf.User, len(role.Users))
for i, user := range role.Users { for i, user := range role.Users {
users[i] = chronograf.User{ users[i] = chronograf.User{
@ -101,5 +105,5 @@ func (c *RolesStore) All(ctx context.Context) ([]chronograf.Role, error) {
Users: users, Users: users,
} }
} }
return res, nil return res
} }

32
enterprise/roles_test.go Normal file
View File

@ -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)
}
})
}
}

View File

@ -35,9 +35,23 @@ func (c *UserStore) Get(ctx context.Context, name string) (*chronograf.User, err
if err != nil { if err != nil {
return nil, err 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{ return &chronograf.User{
Name: u.Name, Name: u.Name,
Permissions: ToChronograf(u.Permissions), Permissions: ToChronograf(u.Permissions),
Roles: cr,
}, nil }, nil
} }
@ -59,11 +73,25 @@ func (c *UserStore) All(ctx context.Context) ([]chronograf.User, error) {
return nil, err return nil, err
} }
ur, err := c.Ctrl.UserRoles(ctx)
if err != nil {
return nil, err
}
res := make([]chronograf.User, len(all.Users)) res := make([]chronograf.User, len(all.Users))
for i, user := range 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{ res[i] = chronograf.User{
Name: user.Name, Name: user.Name,
Permissions: ToChronograf(user.Permissions), Permissions: ToChronograf(user.Permissions),
Roles: cr,
} }
} }
return res, nil return res, nil

View File

@ -180,6 +180,9 @@ func TestClient_Get(t *testing.T) {
}, },
}, nil }, nil
}, },
userRoles: func(ctx context.Context) (map[string]enterprise.Roles, error) {
return map[string]enterprise.Roles{}, nil
},
}, },
}, },
args: args{ args: args{
@ -194,6 +197,71 @@ func TestClient_Get(t *testing.T) {
Allowed: chronograf.Allowances{"ViewChronograf", "ReadData", "WriteData"}, 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 }, nil
}, },
userRoles: func(ctx context.Context) (map[string]enterprise.Roles, error) {
return map[string]enterprise.Roles{}, nil
},
}, },
}, },
args: args{ args: args{
@ -386,6 +457,7 @@ func TestClient_All(t *testing.T) {
Allowed: chronograf.Allowances{"ViewChronograf", "ReadData", "WriteData"}, 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) users func(ctx context.Context, name *string) (*enterprise.Users, error)
setUserPerms func(ctx context.Context, name string, perms enterprise.Permissions) 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) roles func(ctx context.Context, name *string) (*enterprise.Roles, error)
role func(ctx context.Context, name string) (*enterprise.Role, error) role func(ctx context.Context, name string) (*enterprise.Role, error)
createRole func(ctx context.Context, name string) 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) { func (m *mockCtrl) ShowCluster(ctx context.Context) (*enterprise.Cluster, error) {
return m.showCluster(ctx) return m.showCluster(ctx)
} }
func (m *mockCtrl) User(ctx context.Context, name string) (*enterprise.User, error) { func (m *mockCtrl) User(ctx context.Context, name string) (*enterprise.User, error) {
return m.user(ctx, name) return m.user(ctx, name)
} }
func (m *mockCtrl) CreateUser(ctx context.Context, name, passwd string) error { func (m *mockCtrl) CreateUser(ctx context.Context, name, passwd string) error {
return m.createUser(ctx, name, passwd) return m.createUser(ctx, name, passwd)
} }
func (m *mockCtrl) DeleteUser(ctx context.Context, name string) error { func (m *mockCtrl) DeleteUser(ctx context.Context, name string) error {
return m.deleteUser(ctx, name) return m.deleteUser(ctx, name)
} }
func (m *mockCtrl) ChangePassword(ctx context.Context, name, passwd string) error { func (m *mockCtrl) ChangePassword(ctx context.Context, name, passwd string) error {
return m.changePassword(ctx, name, passwd) return m.changePassword(ctx, name, passwd)
} }
func (m *mockCtrl) Users(ctx context.Context, name *string) (*enterprise.Users, error) { func (m *mockCtrl) Users(ctx context.Context, name *string) (*enterprise.Users, error) {
return m.users(ctx, name) return m.users(ctx, name)
} }
func (m *mockCtrl) SetUserPerms(ctx context.Context, name string, perms enterprise.Permissions) error { func (m *mockCtrl) SetUserPerms(ctx context.Context, name string, perms enterprise.Permissions) error {
return m.setUserPerms(ctx, name, perms) 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) { func (m *mockCtrl) Roles(ctx context.Context, name *string) (*enterprise.Roles, error) {
return m.roles(ctx, name) return m.roles(ctx, name)
} }

View File

@ -13,6 +13,14 @@ import (
var _ chronograf.TimeSeries = &Client{} 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 // Client is a device for retrieving time series data from an InfluxDB instance
type Client struct { type Client struct {
URL *url.URL URL *url.URL
@ -74,10 +82,9 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err
hc := &http.Client{} hc := &http.Client{}
if c.InsecureSkipVerify { if c.InsecureSkipVerify {
tr := &http.Transport{ hc.Transport = skipVerifyTransport
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } else {
} hc.Transport = defaultTransport
hc.Transport = tr
} }
resp, err := hc.Do(req) resp, err := hc.Do(req)
if err != nil { if err != nil {

View File

@ -52,6 +52,7 @@ func (r *sourceUserRequest) ValidUpdate() error {
type sourceUser struct { type sourceUser struct {
Username string `json:"name,omitempty"` // Username for new account Username string `json:"name,omitempty"` // Username for new account
Permissions chronograf.Permissions `json:"permissions,omitempty"` // Account's permissions 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 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{} su := []sourceUser{}
for _, u := range users { for _, u := range users {
su = append(su, sourceUser{ res := sourceUser{
Username: u.Name, Username: u.Name,
Permissions: u.Permissions, Permissions: u.Permissions,
Links: newSelfLinks(srcID, "users", u.Name), 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{ res := sourceUsers{
@ -163,6 +172,13 @@ func (h *Service) SourceUserID(w http.ResponseWriter, r *http.Request) {
Permissions: u.Permissions, Permissions: u.Permissions,
Links: newSelfLinks(srcID, "users", u.Name), 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) encodeJSON(w, http.StatusOK, res, h.Logger)
} }
@ -346,7 +362,7 @@ func (r *sourceRoleRequest) ValidUpdate() error {
} }
type roleResponse struct { type roleResponse struct {
Users []sourceUser `json:"users"` Users []sourceUser `json:"users,omitempty"`
Name string `json:"name"` Name string `json:"name"`
Permissions chronograf.Permissions `json:"permissions"` Permissions chronograf.Permissions `json:"permissions"`
Links selfLinks `json:"links"` Links selfLinks `json:"links"`

View File

@ -2565,6 +2565,34 @@
"$ref": "#/definitions/Role" "$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": { "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": { "Users": {
@ -2616,21 +2668,43 @@
{ {
"scope": "all", "scope": "all",
"allowed": [ "allowed": [
"ViewAdmin",
"ViewChronograf", "ViewChronograf",
"ReadData" "CreateDatabase",
] "CreateUserAndRole",
}, "DropDatabase",
{ "DropData",
"scope": "database", "ReadData",
"name": "telegraf", "WriteData",
"allowed": [ "ManageShard",
"ViewChronograf", "ManageContinuousQuery",
"ReadData" "ManageQuery",
"ManageSubscription",
"Monitor",
"KapacitorAPI"
] ]
} }
], ],
"roles": [
{
"name": "timetravelers",
"permissions": [
{
"scope": "database",
"name": "telegraf",
"allowed": [
"ReadData",
"WriteData"
]
}
],
"links": {
"self": "/chronograf/v1/sources/3/roles/timetravelers"
}
}
],
"links": { "links": {
"self": "/chronograf/v1/source/1/users/docbrown" "self": "/chronograf/v1/sources/3/users/docbrown"
} }
} }
] ]
@ -2669,21 +2743,43 @@
{ {
"scope": "all", "scope": "all",
"allowed": [ "allowed": [
"ViewAdmin",
"ViewChronograf", "ViewChronograf",
"ReadData" "CreateDatabase",
] "CreateUserAndRole",
}, "DropDatabase",
{ "DropData",
"scope": "database", "ReadData",
"name": "telegraf", "WriteData",
"allowed": [ "ManageShard",
"ViewChronograf", "ManageContinuousQuery",
"ReadData" "ManageQuery",
"ManageSubscription",
"Monitor",
"KapacitorAPI"
] ]
} }
], ],
"roles": [
{
"name": "timetravelers",
"permissions": [
{
"scope": "database",
"name": "telegraf",
"allowed": [
"ReadData",
"WriteData"
]
}
],
"links": {
"self": "/chronograf/v1/sources/3/roles/timetravelers"
}
}
],
"links": { "links": {
"self": "/chronograf/v1/source/1/users/docbrown" "self": "/chronograf/v1/sources/3/users/docbrown"
} }
} }
}, },
@ -3217,4 +3313,4 @@
} }
} }
} }
} }