From 08271f25ef7a7e216a0c97d4d0c757684ff5ebb8 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Thu, 23 Feb 2017 16:02:53 -0600 Subject: [PATCH] Add roles to chronograf --- chronograf.go | 25 +- enterprise/enterprise.go | 51 ++- enterprise/enterprise_test.go | 12 +- enterprise/mocks_test.go | 28 ++ enterprise/roles.go | 89 ++++- enterprise/users.go | 28 +- enterprise/users_test.go | 113 ++++-- influx/influx.go | 5 + influx/influx_test.go | 10 +- mocks/roles.go | 43 ++ mocks/timeseries.go | 7 + server/admin.go | 308 +++++++++++++-- server/admin_test.go | 718 ++++++++++++++++++++++++++++++++-- server/sources.go | 18 +- server/swagger.json | 284 +++++++++++++- server/users_test.go | 4 +- 16 files changed, 1596 insertions(+), 147 deletions(-) create mode 100644 mocks/roles.go diff --git a/chronograf.go b/chronograf.go index b24509a786..5e4ee05363 100644 --- a/chronograf.go +++ b/chronograf.go @@ -56,6 +56,29 @@ type TimeSeries interface { Users(context.Context) UsersStore // Allowances returns all valid names permissions in this database Allowances(context.Context) Allowances + // Roles represents the roles associated with this TimesSeriesDatabase + Roles(context.Context) (RolesStore, error) +} + +// Role is a restricted set of permissions assigned to a set of users. +type Role struct { + Name string `json:"name"` + Permissions Permissions `json:"permissions,omitempty"` + Users []User `json:"users,omitempty"` +} + +// RolesStore is the Storage and retrieval of authentication information +type RolesStore interface { + // All lists all roles from the RolesStore + All(context.Context) ([]Role, error) + // Create a new Role in the RolesStore + Add(context.Context, *Role) (*Role, error) + // Delete the Role from the RolesStore + Delete(context.Context, *Role) error + // Get retrieves a role if name exists. + Get(ctx context.Context, name string) (*Role, error) + // Update the roles' users or permissions + Update(context.Context, *Role) error } // Range represents an upper and lower bound for data @@ -249,7 +272,7 @@ type Scope string // User represents an authenticated user. type User struct { - Name string `json:"username"` + Name string `json:"name"` Passwd string `json:"password"` Permissions Permissions `json:"permissions,omitempty"` } diff --git a/enterprise/enterprise.go b/enterprise/enterprise.go index 7926d38357..a2fe5178a9 100644 --- a/enterprise/enterprise.go +++ b/enterprise/enterprise.go @@ -13,21 +13,34 @@ import ( var _ chronograf.TimeSeries = &Client{} +// Ctrl represents administrative controls over an Influx Enterprise cluster +type Ctrl interface { + ShowCluster(ctx context.Context) (*Cluster, error) + + Users(ctx context.Context, name *string) (*Users, error) + User(ctx context.Context, name string) (*User, error) + CreateUser(ctx context.Context, name, passwd string) error + DeleteUser(ctx context.Context, name string) error + ChangePassword(ctx context.Context, name, passwd string) error + SetUserPerms(ctx context.Context, name string, perms Permissions) error + + Roles(ctx context.Context, name *string) (*Roles, error) + Role(ctx context.Context, name string) (*Role, error) + CreateRole(ctx context.Context, name string) error + 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 +} + // Client is a device for retrieving time series data from an Influx Enterprise // cluster. It is configured using the addresses of one or more meta node URLs. // Data node URLs are retrieved automatically from the meta nodes and queries // are appropriately load balanced across the cluster. type Client struct { - Ctrl interface { - ShowCluster(ctx context.Context) (*Cluster, error) - User(ctx context.Context, name string) (*User, error) - CreateUser(ctx context.Context, name, passwd string) error - DeleteUser(ctx context.Context, name string) error - ChangePassword(ctx context.Context, name, passwd string) error - Users(ctx context.Context, name *string) (*Users, error) - SetUserPerms(ctx context.Context, name string, perms Permissions) error - } - Logger chronograf.Logger + Ctrl + UsersStore chronograf.UsersStore + RolesStore chronograf.RolesStore + Logger chronograf.Logger dataNodes *ring.Ring opened bool @@ -58,8 +71,17 @@ func NewClientWithURL(mu, username, password string, tls bool, lg chronograf.Log return nil, err } metaURL.User = url.UserPassword(username, password) + ctrl := NewMetaClient(metaURL) return &Client{ - Ctrl: NewMetaClient(metaURL), + Ctrl: ctrl, + UsersStore: &UserStore{ + Ctrl: ctrl, + Logger: lg, + }, + RolesStore: &RolesStore{ + Ctrl: ctrl, + Logger: lg, + }, Logger: lg, }, nil } @@ -100,7 +122,12 @@ func (c *Client) Query(ctx context.Context, q chronograf.Query) (chronograf.Resp // Users is the interface to the users within Influx Enterprise func (c *Client) Users(context.Context) chronograf.UsersStore { - return c + return c.UsersStore +} + +// Roles provide a grouping of permissions given to a grouping of users +func (c *Client) Roles(ctx context.Context) (chronograf.RolesStore, error) { + return c.RolesStore, nil } // Allowances returns all Influx Enterprise permission strings diff --git a/enterprise/enterprise_test.go b/enterprise/enterprise_test.go index 36bc6298f3..260dca712c 100644 --- a/enterprise/enterprise_test.go +++ b/enterprise/enterprise_test.go @@ -14,10 +14,14 @@ import ( func Test_Enterprise_FetchesDataNodes(t *testing.T) { t.Parallel() - - ctrl := &ControlClient{ - Cluster: &enterprise.Cluster{}, + showClustersCalled := false + ctrl := &mockCtrl{ + showCluster: func(ctx context.Context) (*enterprise.Cluster, error) { + showClustersCalled = true + return &enterprise.Cluster{}, nil + }, } + cl := &enterprise.Client{ Ctrl: ctrl, } @@ -29,7 +33,7 @@ func Test_Enterprise_FetchesDataNodes(t *testing.T) { t.Fatal("Unexpected error while creating enterprise client. err:", err) } - if ctrl.ShowClustersCalled != true { + if showClustersCalled != true { t.Fatal("Expected request to meta node but none was issued") } } diff --git a/enterprise/mocks_test.go b/enterprise/mocks_test.go index 2b45f27790..6a88d5d0d8 100644 --- a/enterprise/mocks_test.go +++ b/enterprise/mocks_test.go @@ -60,6 +60,30 @@ func (cc *ControlClient) SetUserPerms(ctx context.Context, name string, perms en return nil } +func (cc *ControlClient) CreateRole(ctx context.Context, name string) error { + return nil +} + +func (cc *ControlClient) Role(ctx context.Context, name string) (*enterprise.Role, error) { + return nil, nil +} + +func (ccm *ControlClient) Roles(ctx context.Context, name *string) (*enterprise.Roles, error) { + return nil, nil +} + +func (cc *ControlClient) DeleteRole(ctx context.Context, name string) error { + return nil +} + +func (cc *ControlClient) SetRolePerms(ctx context.Context, name string, perms enterprise.Permissions) error { + return nil +} + +func (cc *ControlClient) SetRoleUsers(ctx context.Context, name string, users []string) error { + return nil +} + type TimeSeries struct { URLs []string Response Response @@ -86,6 +110,10 @@ func (ts *TimeSeries) Users(ctx context.Context) chronograf.UsersStore { return nil } +func (ts *TimeSeries) Roles(ctx context.Context) (chronograf.RolesStore, error) { + return nil, nil +} + func (ts *TimeSeries) Allowances(ctx context.Context) chronograf.Allowances { return chronograf.Allowances{} } diff --git a/enterprise/roles.go b/enterprise/roles.go index 523ed0d009..e95d34e8cf 100644 --- a/enterprise/roles.go +++ b/enterprise/roles.go @@ -1,50 +1,105 @@ package enterprise -/* +import ( + "context" + + "github.com/influxdata/chronograf" +) + +// RolesStore uses a control client operate on Influx Enterprise roles. Roles are +// groups of permissions applied to groups of users +type RolesStore struct { + Ctrl + Logger chronograf.Logger +} + // Add creates a new Role in Influx Enterprise -func (c *Client) Add(ctx context.Context, u *chronograf.Role) (*chronograf.Role, error) { - if err := c.Ctrl.CreateRole(ctx, u.Name, u.Passwd); err != nil { +// This must be done in three smaller steps: creating, setting permissions, setting users. +func (c *RolesStore) Add(ctx context.Context, u *chronograf.Role) (*chronograf.Role, error) { + if err := c.Ctrl.CreateRole(ctx, u.Name); err != nil { + return nil, err + } + if err := c.Ctrl.SetRolePerms(ctx, u.Name, ToEnterprise(u.Permissions)); err != nil { + return nil, err + } + + users := make([]string, len(u.Users)) + for i, u := range u.Users { + users[i] = u.Name + } + if err := c.Ctrl.SetRoleUsers(ctx, u.Name, users); err != nil { return nil, err } return u, nil } // Delete the Role from Influx Enterprise -func (c *Client) Delete(ctx context.Context, u *chronograf.Role) error { +func (c *RolesStore) Delete(ctx context.Context, u *chronograf.Role) error { return c.Ctrl.DeleteRole(ctx, u.Name) } // Get retrieves a Role if name exists. -func (c *Client) Get(ctx context.Context, name string) (*chronograf.Role, error) { - u, err := c.Ctrl.Role(ctx, name) +func (c *RolesStore) Get(ctx context.Context, name string) (*chronograf.Role, error) { + role, err := c.Ctrl.Role(ctx, name) if err != nil { return nil, err } + + // Hydrate all the users to gather their permissions and their roles. + users := make([]chronograf.User, len(role.Users)) + for i, u := range role.Users { + user, err := c.Ctrl.User(ctx, u) + if err != nil { + return nil, err + } + users[i] = chronograf.User{ + Name: user.Name, + Permissions: ToChronograf(user.Permissions), + } + } return &chronograf.Role{ - Name: u.Name, - Permissions: toChronograf(u.Permissions), + Name: role.Name, + Permissions: ToChronograf(role.Permissions), + Users: users, }, nil } -// Update the Role's permissions or roles -func (c *Client) Update(ctx context.Context, u *chronograf.Role) error { - perms := toEnterprise(u.Permissions) - return c.Ctrl.SetRolePerms(ctx, u.Name, perms) +// Update the Role's permissions and roles +func (c *RolesStore) Update(ctx context.Context, u *chronograf.Role) error { + perms := ToEnterprise(u.Permissions) + if err := c.Ctrl.SetRolePerms(ctx, u.Name, perms); err != nil { + return err + } + + users := make([]string, len(u.Users)) + for i, u := range u.Users { + users[i] = u.Name + } + return c.Ctrl.SetRoleUsers(ctx, u.Name, users) } // All is all Roles in influx -func (c *Client) All(ctx context.Context) ([]chronograf.Role, error) { +func (c *RolesStore) All(ctx context.Context) ([]chronograf.Role, error) { all, err := c.Ctrl.Roles(ctx, nil) if err != nil { return nil, err } res := make([]chronograf.Role, len(all.Roles)) - for i, Role := range all.Roles { + for i, role := range all.Roles { + + users := make([]chronograf.User, len(role.Users)) + for i, user := range role.Users { + users[i] = chronograf.User{ + Name: user, + } + } + res[i] = chronograf.Role{ - Name: Role.Name, - Permissions: toChronograf(Role.Permissions), + Name: role.Name, + Permissions: ToChronograf(role.Permissions), + Users: users, } } return res, nil -}*/ +} diff --git a/enterprise/users.go b/enterprise/users.go index e9e297d3f7..b92108bdc3 100644 --- a/enterprise/users.go +++ b/enterprise/users.go @@ -6,8 +6,14 @@ import ( "github.com/influxdata/chronograf" ) +// UserStore uses a control client operate on Influx Enterprise users +type UserStore struct { + Ctrl + Logger chronograf.Logger +} + // Add creates a new User in Influx Enterprise -func (c *Client) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { +func (c *UserStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { if err := c.Ctrl.CreateUser(ctx, u.Name, u.Passwd); err != nil { return nil, err } @@ -15,35 +21,35 @@ func (c *Client) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, } // Delete the User from Influx Enterprise -func (c *Client) Delete(ctx context.Context, u *chronograf.User) error { +func (c *UserStore) Delete(ctx context.Context, u *chronograf.User) error { return c.Ctrl.DeleteUser(ctx, u.Name) } // Get retrieves a user if name exists. -func (c *Client) Get(ctx context.Context, name string) (*chronograf.User, error) { +func (c *UserStore) Get(ctx context.Context, name string) (*chronograf.User, error) { u, err := c.Ctrl.User(ctx, name) if err != nil { return nil, err } return &chronograf.User{ Name: u.Name, - Permissions: toChronograf(u.Permissions), + Permissions: ToChronograf(u.Permissions), }, nil } // Update the user's permissions or roles -func (c *Client) Update(ctx context.Context, u *chronograf.User) error { +func (c *UserStore) Update(ctx context.Context, u *chronograf.User) error { // Only allow one type of change at a time. If it is a password // change then do it and return without any changes to permissions if u.Passwd != "" { return c.Ctrl.ChangePassword(ctx, u.Name, u.Passwd) } - perms := toEnterprise(u.Permissions) + perms := ToEnterprise(u.Permissions) return c.Ctrl.SetUserPerms(ctx, u.Name, perms) } // All is all users in influx -func (c *Client) All(ctx context.Context) ([]chronograf.User, error) { +func (c *UserStore) All(ctx context.Context) ([]chronograf.User, error) { all, err := c.Ctrl.Users(ctx, nil) if err != nil { return nil, err @@ -53,13 +59,14 @@ func (c *Client) All(ctx context.Context) ([]chronograf.User, error) { for i, user := range all.Users { res[i] = chronograf.User{ Name: user.Name, - Permissions: toChronograf(user.Permissions), + Permissions: ToChronograf(user.Permissions), } } return res, nil } -func toEnterprise(perms chronograf.Permissions) Permissions { +// ToEnterprise converts chronograf permission shape to enterprise +func ToEnterprise(perms chronograf.Permissions) Permissions { res := Permissions{} for _, perm := range perms { if perm.Scope == chronograf.AllScope { @@ -72,7 +79,8 @@ func toEnterprise(perms chronograf.Permissions) Permissions { return res } -func toChronograf(perms Permissions) chronograf.Permissions { +// ToChronograf converts enterprise permissions shape to chronograf shape +func ToChronograf(perms Permissions) chronograf.Permissions { res := chronograf.Permissions{} for db, perm := range perms { // Enterprise uses empty string as the key for all databases diff --git a/enterprise/users_test.go b/enterprise/users_test.go index ef4f431a21..fe548ffb4d 100644 --- a/enterprise/users_test.go +++ b/enterprise/users_test.go @@ -1,13 +1,13 @@ -package enterprise +package enterprise_test import ( - "container/ring" "context" "fmt" "reflect" "testing" "github.com/influxdata/chronograf" + "github.com/influxdata/chronograf/enterprise" ) func TestClient_Add(t *testing.T) { @@ -67,7 +67,7 @@ func TestClient_Add(t *testing.T) { }, } for _, tt := range tests { - c := &Client{ + c := &enterprise.UserStore{ Ctrl: tt.fields.Ctrl, Logger: tt.fields.Logger, } @@ -134,7 +134,7 @@ func TestClient_Delete(t *testing.T) { }, } for _, tt := range tests { - c := &Client{ + c := &enterprise.UserStore{ Ctrl: tt.fields.Ctrl, Logger: tt.fields.Logger, } @@ -146,10 +146,8 @@ func TestClient_Delete(t *testing.T) { func TestClient_Get(t *testing.T) { type fields struct { - Ctrl *mockCtrl - Logger chronograf.Logger - dataNodes *ring.Ring - opened bool + Ctrl *mockCtrl + Logger chronograf.Logger } type args struct { ctx context.Context @@ -166,8 +164,8 @@ func TestClient_Get(t *testing.T) { name: "Successful Get User", fields: fields{ Ctrl: &mockCtrl{ - user: func(ctx context.Context, name string) (*User, error) { - return &User{ + user: func(ctx context.Context, name string) (*enterprise.User, error) { + return &enterprise.User{ Name: "marty", Password: "johnny be good", Permissions: map[string][]string{ @@ -199,7 +197,7 @@ func TestClient_Get(t *testing.T) { name: "Failure to get User", fields: fields{ Ctrl: &mockCtrl{ - user: func(ctx context.Context, name string) (*User, error) { + user: func(ctx context.Context, name string) (*enterprise.User, error) { return nil, fmt.Errorf("1.21 Gigawatts! Tom, how could I have been so careless?") }, }, @@ -212,11 +210,9 @@ func TestClient_Get(t *testing.T) { }, } for _, tt := range tests { - c := &Client{ - Ctrl: tt.fields.Ctrl, - Logger: tt.fields.Logger, - dataNodes: tt.fields.dataNodes, - opened: tt.fields.opened, + c := &enterprise.UserStore{ + Ctrl: tt.fields.Ctrl, + Logger: tt.fields.Logger, } got, err := c.Get(tt.args.ctx, tt.args.name) if (err != nil) != tt.wantErr { @@ -283,7 +279,7 @@ func TestClient_Update(t *testing.T) { name: "Success setting permissions User", fields: fields{ Ctrl: &mockCtrl{ - setUserPerms: func(ctx context.Context, name string, perms Permissions) error { + setUserPerms: func(ctx context.Context, name string, perms enterprise.Permissions) error { return nil }, }, @@ -306,7 +302,7 @@ func TestClient_Update(t *testing.T) { name: "Failure setting permissions User", fields: fields{ Ctrl: &mockCtrl{ - setUserPerms: func(ctx context.Context, name string, perms Permissions) error { + 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.") }, }, @@ -327,7 +323,7 @@ func TestClient_Update(t *testing.T) { }, } for _, tt := range tests { - c := &Client{ + c := &enterprise.UserStore{ Ctrl: tt.fields.Ctrl, Logger: tt.fields.Logger, } @@ -356,9 +352,9 @@ func TestClient_All(t *testing.T) { name: "Successful Get User", fields: fields{ Ctrl: &mockCtrl{ - users: func(ctx context.Context, name *string) (*Users, error) { - return &Users{ - Users: []User{ + users: func(ctx context.Context, name *string) (*enterprise.Users, error) { + return &enterprise.Users{ + Users: []enterprise.User{ { Name: "marty", Password: "johnny be good", @@ -394,7 +390,7 @@ func TestClient_All(t *testing.T) { name: "Failure to get User", fields: fields{ Ctrl: &mockCtrl{ - users: func(ctx context.Context, name *string) (*Users, error) { + users: func(ctx context.Context, name *string) (*enterprise.Users, error) { return nil, fmt.Errorf("1.21 Gigawatts! Tom, how could I have been so careless?") }, }, @@ -406,7 +402,7 @@ func TestClient_All(t *testing.T) { }, } for _, tt := range tests { - c := &Client{ + c := &enterprise.UserStore{ Ctrl: tt.fields.Ctrl, Logger: tt.fields.Logger, } @@ -421,15 +417,15 @@ func TestClient_All(t *testing.T) { } } -func Test_toEnterprise(t *testing.T) { +func Test_ToEnterprise(t *testing.T) { tests := []struct { name string perms chronograf.Permissions - want Permissions + want enterprise.Permissions }{ { name: "All Scopes", - want: Permissions{"": []string{"ViewChronograf", "KapacitorAPI"}}, + want: enterprise.Permissions{"": []string{"ViewChronograf", "KapacitorAPI"}}, perms: chronograf.Permissions{ { Scope: chronograf.AllScope, @@ -439,7 +435,7 @@ func Test_toEnterprise(t *testing.T) { }, { name: "DB Scope", - want: Permissions{"telegraf": []string{"ReadData", "WriteData"}}, + want: enterprise.Permissions{"telegraf": []string{"ReadData", "WriteData"}}, perms: chronograf.Permissions{ { Scope: chronograf.DBScope, @@ -450,21 +446,21 @@ func Test_toEnterprise(t *testing.T) { }, } for _, tt := range tests { - if got := toEnterprise(tt.perms); !reflect.DeepEqual(got, tt.want) { - t.Errorf("%q. toEnterprise() = %v, want %v", tt.name, got, tt.want) + if got := enterprise.ToEnterprise(tt.perms); !reflect.DeepEqual(got, tt.want) { + t.Errorf("%q. ToEnterprise() = %v, want %v", tt.name, got, tt.want) } } } -func Test_toChronograf(t *testing.T) { +func Test_ToChronograf(t *testing.T) { tests := []struct { name string - perms Permissions + perms enterprise.Permissions want chronograf.Permissions }{ { name: "All Scopes", - perms: Permissions{"": []string{"ViewChronograf", "KapacitorAPI"}}, + perms: enterprise.Permissions{"": []string{"ViewChronograf", "KapacitorAPI"}}, want: chronograf.Permissions{ { Scope: chronograf.AllScope, @@ -474,7 +470,7 @@ func Test_toChronograf(t *testing.T) { }, { name: "DB Scope", - perms: Permissions{"telegraf": []string{"ReadData", "WriteData"}}, + perms: enterprise.Permissions{"telegraf": []string{"ReadData", "WriteData"}}, want: chronograf.Permissions{ { Scope: chronograf.DBScope, @@ -485,26 +481,33 @@ func Test_toChronograf(t *testing.T) { }, } for _, tt := range tests { - if got := toChronograf(tt.perms); !reflect.DeepEqual(got, tt.want) { + if got := enterprise.ToChronograf(tt.perms); !reflect.DeepEqual(got, tt.want) { t.Errorf("%q. toChronograf() = %v, want %v", tt.name, got, tt.want) } } } type mockCtrl struct { - showCluster func(ctx context.Context) (*Cluster, error) - user func(ctx context.Context, name string) (*User, error) + showCluster func(ctx context.Context) (*enterprise.Cluster, error) + user func(ctx context.Context, name string) (*enterprise.User, error) createUser func(ctx context.Context, name, passwd string) error deleteUser func(ctx context.Context, name string) error changePassword func(ctx context.Context, name, passwd string) error - users func(ctx context.Context, name *string) (*Users, error) - setUserPerms func(ctx context.Context, name string, perms Permissions) error + users func(ctx context.Context, name *string) (*enterprise.Users, error) + setUserPerms func(ctx context.Context, name string, perms enterprise.Permissions) 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 } -func (m *mockCtrl) ShowCluster(ctx context.Context) (*Cluster, error) { +func (m *mockCtrl) ShowCluster(ctx context.Context) (*enterprise.Cluster, error) { return m.showCluster(ctx) } -func (m *mockCtrl) User(ctx context.Context, name string) (*User, error) { +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 { @@ -516,9 +519,33 @@ func (m *mockCtrl) DeleteUser(ctx context.Context, name string) error { 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) (*Users, error) { +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 Permissions) error { +func (m *mockCtrl) SetUserPerms(ctx context.Context, name string, perms enterprise.Permissions) error { return m.setUserPerms(ctx, name, perms) } + +func (m *mockCtrl) Roles(ctx context.Context, name *string) (*enterprise.Roles, error) { + return m.roles(ctx, name) +} + +func (m *mockCtrl) Role(ctx context.Context, name string) (*enterprise.Role, error) { + return m.role(ctx, name) +} + +func (m *mockCtrl) CreateRole(ctx context.Context, name string) error { + return m.createRole(ctx, name) +} + +func (m *mockCtrl) DeleteRole(ctx context.Context, name string) error { + return m.deleteRole(ctx, name) +} + +func (m *mockCtrl) SetRolePerms(ctx context.Context, name string, perms enterprise.Permissions) error { + return m.setRolePerms(ctx, name, perms) +} + +func (m *mockCtrl) SetRoleUsers(ctx context.Context, name string, users []string) error { + return m.setRoleUsers(ctx, name, users) +} diff --git a/influx/influx.go b/influx/influx.go index ccbd4c7713..60ef9f786e 100644 --- a/influx/influx.go +++ b/influx/influx.go @@ -172,3 +172,8 @@ func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error { func (c *Client) Users(ctx context.Context) chronograf.UsersStore { return c } + +// Roles aren't support in OSS +func (c *Client) Roles(ctx context.Context) (chronograf.RolesStore, error) { + return nil, fmt.Errorf("Roles not support in open-source InfluxDB. Roles are support in Influx Enterprise") +} diff --git a/influx/influx_test.go b/influx/influx_test.go index 8fabd3b795..6fa4a859fa 100644 --- a/influx/influx_test.go +++ b/influx/influx_test.go @@ -1,6 +1,7 @@ package influx_test import ( + "context" "net/http" "net/http/httptest" "testing" @@ -9,7 +10,6 @@ import ( "github.com/influxdata/chronograf" "github.com/influxdata/chronograf/influx" "github.com/influxdata/chronograf/log" - "golang.org/x/net/context" ) func Test_Influx_MakesRequestsToQueryEndpoint(t *testing.T) { @@ -204,3 +204,11 @@ func Test_Influx_ReportsInfluxErrs(t *testing.T) { t.Fatal("Expected an error but received none") } } + +func TestClient_Roles(t *testing.T) { + c := &influx.Client{} + _, err := c.Roles(context.Background()) + if err == nil { + t.Errorf("Client.Roles() want error") + } +} diff --git a/mocks/roles.go b/mocks/roles.go new file mode 100644 index 0000000000..db09f8a4a5 --- /dev/null +++ b/mocks/roles.go @@ -0,0 +1,43 @@ +package mocks + +import ( + "context" + + "github.com/influxdata/chronograf" +) + +var _ chronograf.RolesStore = &RolesStore{} + +// RolesStore mock allows all functions to be set for testing +type RolesStore struct { + AllF func(context.Context) ([]chronograf.Role, error) + AddF func(context.Context, *chronograf.Role) (*chronograf.Role, error) + DeleteF func(context.Context, *chronograf.Role) error + GetF func(ctx context.Context, name string) (*chronograf.Role, error) + UpdateF func(context.Context, *chronograf.Role) error +} + +// All lists all Roles from the RolesStore +func (s *RolesStore) All(ctx context.Context) ([]chronograf.Role, error) { + return s.AllF(ctx) +} + +// Add a new Role in the RolesStore +func (s *RolesStore) Add(ctx context.Context, u *chronograf.Role) (*chronograf.Role, error) { + return s.AddF(ctx, u) +} + +// Delete the Role from the RolesStore +func (s *RolesStore) Delete(ctx context.Context, u *chronograf.Role) error { + return s.DeleteF(ctx, u) +} + +// Get retrieves a Role if name exists. +func (s *RolesStore) Get(ctx context.Context, name string) (*chronograf.Role, error) { + return s.GetF(ctx, name) +} + +// Update the Role's permissions or users +func (s *RolesStore) Update(ctx context.Context, u *chronograf.Role) error { + return s.UpdateF(ctx, u) +} diff --git a/mocks/timeseries.go b/mocks/timeseries.go index 5d6aaea5b0..af4e967d4d 100644 --- a/mocks/timeseries.go +++ b/mocks/timeseries.go @@ -18,6 +18,8 @@ type TimeSeries struct { UsersF func(context.Context) chronograf.UsersStore // Allowances returns all valid names permissions in this database AllowancesF func(context.Context) chronograf.Allowances + // RolesF represents the roles. Roles group permissions and Users + RolesF func(context.Context) (chronograf.RolesStore, error) } // Query retrieves time series data from the database. @@ -35,6 +37,11 @@ func (t *TimeSeries) Users(ctx context.Context) chronograf.UsersStore { return t.UsersF(ctx) } +// Roles represents the roles. Roles group permissions and Users +func (t *TimeSeries) Roles(ctx context.Context) (chronograf.RolesStore, error) { + return t.RolesF(ctx) +} + // Allowances returns all valid names permissions in this database func (t *TimeSeries) Allowances(ctx context.Context) chronograf.Allowances { return t.AllowancesF(ctx) diff --git a/server/admin.go b/server/admin.go index 5f19a214d9..202332715b 100644 --- a/server/admin.go +++ b/server/admin.go @@ -11,8 +11,23 @@ import ( "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:"username,omitempty"` // Username for new account + 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 } @@ -24,40 +39,35 @@ func (r *sourceUserRequest) ValidCreate() error { if r.Password == "" { return fmt.Errorf("Password required") } - return nil + return validPermissions(&r.Permissions) } func (r *sourceUserRequest) ValidUpdate() error { if r.Password == "" && len(r.Permissions) == 0 { return fmt.Errorf("No fields to update") } - return nil + return validPermissions(&r.Permissions) } type sourceUser struct { - Username string `json:"username,omitempty"` // Username for new account + Username string `json:"name,omitempty"` // Username for new account Permissions chronograf.Permissions `json:"permissions,omitempty"` // Account's permissions - Links sourceUserLinks `json:"links"` // Links are URI locations related to user + Links selfLinks `json:"links"` // Links are URI locations related to user } -// newSourceUser creates a new user in the InfluxDB data source -func newSourceUser(srcID int, name string, perms chronograf.Permissions) sourceUser { - u := &url.URL{Path: name} - encodedUser := u.String() - httpAPISrcs := "/chronograf/v1/sources" - return sourceUser{ - Username: name, - Permissions: perms, - Links: sourceUserLinks{ - Self: fmt.Sprintf("%s/%d/users/%s", httpAPISrcs, srcID, encodedUser), - }, - } -} - -type sourceUserLinks struct { +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), + } +} + // NewSourceUser adds user to source func (h *Service) NewSourceUser(w http.ResponseWriter, r *http.Request) { var req sourceUserRequest @@ -88,7 +98,11 @@ func (h *Service) NewSourceUser(w http.ResponseWriter, r *http.Request) { return } - su := newSourceUser(srcID, res.Name, req.Permissions) + 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) } @@ -114,7 +128,11 @@ func (h *Service) SourceUsers(w http.ResponseWriter, r *http.Request) { su := []sourceUser{} for _, u := range users { - su = append(su, newSourceUser(srcID, u.Name, u.Permissions)) + su = append(su, sourceUser{ + Username: u.Name, + Permissions: u.Permissions, + Links: newSelfLinks(srcID, "users", u.Name), + }) } res := sourceUsers{ @@ -140,7 +158,11 @@ func (h *Service) SourceUserID(w http.ResponseWriter, r *http.Request) { return } - res := newSourceUser(srcID, u.Name, u.Permissions) + res := sourceUser{ + Username: u.Name, + Permissions: u.Permissions, + Links: newSelfLinks(srcID, "users", u.Name), + } encodeJSON(w, http.StatusOK, res, h.Logger) } @@ -192,27 +214,39 @@ func (h *Service) UpdateSourceUser(w http.ResponseWriter, r *http.Request) { return } - su := newSourceUser(srcID, user.Name, user.Permissions) + 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) sourceUsersStore(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, chronograf.UsersStore, error) { +func (h *Service) sourcesSeries(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, error) { srcID, err := paramID("id", r) if err != nil { Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger) - return 0, nil, err + return 0, err } src, err := h.SourcesStore.Get(ctx, srcID) if err != nil { notFound(w, srcID, h.Logger) - return 0, nil, err + return 0, err } if err = h.TimeSeries.Connect(ctx, &src); err != nil { msg := fmt.Sprintf("Unable to connect to source %d", srcID) Error(w, http.StatusBadRequest, msg, h.Logger) + return 0, err + } + return srcID, nil +} + +func (h *Service) sourceUsersStore(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, chronograf.UsersStore, error) { + srcID, err := h.sourcesSeries(ctx, w, r) + if err != nil { return 0, nil, err } @@ -220,6 +254,15 @@ func (h *Service) sourceUsersStore(ctx context.Context, w http.ResponseWriter, r return srcID, store, nil } +// hasRoles checks if the influx source has roles or not +func (h *Service) hasRoles(ctx context.Context) (chronograf.RolesStore, bool) { + store, err := h.TimeSeries.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() @@ -259,3 +302,214 @@ func (h *Service) Permissions(w http.ResponseWriter, r *http.Request) { } 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, err := h.sourcesSeries(ctx, w, r) + if err != nil { + return + } + + roles, ok := h.hasRoles(ctx) + 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, err := h.sourcesSeries(ctx, w, r) + if err != nil { + return + } + + roles, ok := h.hasRoles(ctx) + 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, err := h.sourcesSeries(ctx, w, r) + if err != nil { + return + } + + roles, ok := h.hasRoles(ctx) + 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, err := h.sourcesSeries(ctx, w, r) + if err != nil { + return + } + + store, ok := h.hasRoles(ctx) + 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, err := h.sourcesSeries(ctx, w, r) + if err != nil { + return + } + + roles, ok := h.hasRoles(ctx) + 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 index a0d56a0feb..b544198cd1 100644 --- a/server/admin_test.go +++ b/server/admin_test.go @@ -10,7 +10,6 @@ import ( "testing" "github.com/bouk/httprouter" - "github.com/influxdata/chronograf" "github.com/influxdata/chronograf/log" "github.com/influxdata/chronograf/mocks" @@ -45,7 +44,7 @@ func TestService_NewSourceUser(t *testing.T) { "POST", "http://server.local/chronograf/v1/sources/1", ioutil.NopCloser( - bytes.NewReader([]byte(`{"username": "marty", "password": "the_lake"}`)))), + bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))), }, fields: fields{ UseAuth: true, @@ -55,7 +54,7 @@ func TestService_NewSourceUser(t *testing.T) { return chronograf.Source{ ID: 1, Name: "muh source", - Username: "username", + Username: "name", Password: "hunter2", URL: "http://localhost:8086", }, nil @@ -77,7 +76,7 @@ func TestService_NewSourceUser(t *testing.T) { ID: "1", wantStatus: http.StatusCreated, wantContentType: "application/json", - wantBody: `{"username":"marty","links":{"self":"/chronograf/v1/sources/1/users/marty"}} + wantBody: `{"name":"marty","links":{"self":"/chronograf/v1/sources/1/users/marty"}} `, }, { @@ -88,7 +87,7 @@ func TestService_NewSourceUser(t *testing.T) { "POST", "http://server.local/chronograf/v1/sources/1", ioutil.NopCloser( - bytes.NewReader([]byte(`{"username": "marty", "password": "the_lake"}`)))), + bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))), }, fields: fields{ UseAuth: true, @@ -98,7 +97,7 @@ func TestService_NewSourceUser(t *testing.T) { return chronograf.Source{ ID: 1, Name: "muh source", - Username: "username", + Username: "name", Password: "hunter2", URL: "http://localhost:8086", }, nil @@ -130,7 +129,7 @@ func TestService_NewSourceUser(t *testing.T) { "POST", "http://server.local/chronograf/v1/sources/1", ioutil.NopCloser( - bytes.NewReader([]byte(`{"username": "marty", "password": "the_lake"}`)))), + bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))), }, fields: fields{ UseAuth: true, @@ -140,7 +139,7 @@ func TestService_NewSourceUser(t *testing.T) { return chronograf.Source{ ID: 1, Name: "muh source", - Username: "username", + Username: "name", Password: "hunter2", URL: "http://localhost:8086", }, nil @@ -165,7 +164,7 @@ func TestService_NewSourceUser(t *testing.T) { "POST", "http://server.local/chronograf/v1/sources/1", ioutil.NopCloser( - bytes.NewReader([]byte(`{"username": "marty", "password": "the_lake"}`)))), + bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))), }, fields: fields{ UseAuth: true, @@ -189,7 +188,7 @@ func TestService_NewSourceUser(t *testing.T) { "POST", "http://server.local/chronograf/v1/sources/1", ioutil.NopCloser( - bytes.NewReader([]byte(`{"username": "marty", "password": "the_lake"}`)))), + bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))), }, fields: fields{ UseAuth: true, @@ -201,7 +200,7 @@ func TestService_NewSourceUser(t *testing.T) { wantBody: `{"code":422,"message":"Error converting ID BAD"}`, }, { - name: "Bad username", + name: "Bad name", args: args{ w: httptest.NewRecorder(), r: httptest.NewRequest( @@ -311,7 +310,7 @@ func TestService_SourceUsers(t *testing.T) { return chronograf.Source{ ID: 1, Name: "muh source", - Username: "username", + Username: "name", Password: "hunter2", URL: "http://localhost:8086", }, nil @@ -344,7 +343,7 @@ func TestService_SourceUsers(t *testing.T) { ID: "1", wantStatus: http.StatusOK, wantContentType: "application/json", - wantBody: `{"users":[{"username":"strickland","permissions":[{"scope":"all","allowed":["READ"]}],"links":{"self":"/chronograf/v1/sources/1/users/strickland"}}]} + wantBody: `{"users":[{"name":"strickland","permissions":[{"scope":"all","allowed":["READ"]}],"links":{"self":"/chronograf/v1/sources/1/users/strickland"}}]} `, }, } @@ -419,7 +418,7 @@ func TestService_SourceUserID(t *testing.T) { return chronograf.Source{ ID: 1, Name: "muh source", - Username: "username", + Username: "name", Password: "hunter2", URL: "http://localhost:8086", }, nil @@ -451,7 +450,7 @@ func TestService_SourceUserID(t *testing.T) { UID: "strickland", wantStatus: http.StatusOK, wantContentType: "application/json", - wantBody: `{"username":"strickland","permissions":[{"scope":"all","allowed":["READ"]}],"links":{"self":"/chronograf/v1/sources/1/users/strickland"}} + wantBody: `{"name":"strickland","permissions":[{"scope":"all","allowed":["READ"]}],"links":{"self":"/chronograf/v1/sources/1/users/strickland"}} `, }, } @@ -526,7 +525,7 @@ func TestService_RemoveSourceUser(t *testing.T) { return chronograf.Source{ ID: 1, Name: "muh source", - Username: "username", + Username: "name", Password: "hunter2", URL: "http://localhost:8086", }, nil @@ -611,7 +610,7 @@ func TestService_UpdateSourceUser(t *testing.T) { "POST", "http://server.local/chronograf/v1/sources/1", ioutil.NopCloser( - bytes.NewReader([]byte(`{"username": "marty", "password": "the_lake"}`)))), + bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))), }, fields: fields{ UseAuth: true, @@ -621,7 +620,7 @@ func TestService_UpdateSourceUser(t *testing.T) { return chronograf.Source{ ID: 1, Name: "muh source", - Username: "username", + Username: "name", Password: "hunter2", URL: "http://localhost:8086", }, nil @@ -644,7 +643,7 @@ func TestService_UpdateSourceUser(t *testing.T) { UID: "marty", wantStatus: http.StatusOK, wantContentType: "application/json", - wantBody: `{"username":"marty","links":{"self":"/chronograf/v1/sources/1/users/marty"}} + wantBody: `{"name":"marty","links":{"self":"/chronograf/v1/sources/1/users/marty"}} `, }, { @@ -655,7 +654,7 @@ func TestService_UpdateSourceUser(t *testing.T) { "POST", "http://server.local/chronograf/v1/sources/1", ioutil.NopCloser( - bytes.NewReader([]byte(`{"username": "marty"}`)))), + bytes.NewReader([]byte(`{"name": "marty"}`)))), }, fields: fields{ UseAuth: true, @@ -732,7 +731,7 @@ func TestService_Permissions(t *testing.T) { "POST", "http://server.local/chronograf/v1/sources/1", ioutil.NopCloser( - bytes.NewReader([]byte(`{"username": "marty", "password": "the_lake"}`)))), + bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))), }, fields: fields{ UseAuth: true, @@ -742,7 +741,7 @@ func TestService_Permissions(t *testing.T) { return chronograf.Source{ ID: 1, Name: "muh source", - Username: "username", + Username: "name", Password: "hunter2", URL: "http://localhost:8086", }, nil @@ -795,3 +794,678 @@ func TestService_Permissions(t *testing.T) { } } } + +func TestService_NewSourceRole(t *testing.T) { + type fields struct { + SourcesStore chronograf.SourcesStore + TimeSeries chronograf.TimeSeries + 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, + TimeSeries: 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 chronograf.TimeSeries + 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, + TimeSeries: 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 chronograf.TimeSeries + 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, + TimeSeries: 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 chronograf.TimeSeries + 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, + TimeSeries: 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 chronograf.TimeSeries + 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, + TimeSeries: 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/sources.go b/server/sources.go index f1e2b336b6..2f2e73cd21 100644 --- a/server/sources.go +++ b/server/sources.go @@ -11,11 +11,12 @@ import ( ) type sourceLinks struct { - Self string `json:"self"` // Self link mapping to this resource - Kapacitors string `json:"kapacitors"` // URL for kapacitors endpoint - Proxy string `json:"proxy"` // URL for proxy endpoint - Permissions string `json:"permissions"` // URL for all allowed permissions for this source - Users string `json:"users"` // URL for all users associated with this source + Self string `json:"self"` // Self link mapping to this resource + Kapacitors string `json:"kapacitors"` // URL for kapacitors endpoint + Proxy string `json:"proxy"` // URL for proxy endpoint + Permissions string `json:"permissions"` // URL for all allowed permissions for this source + Users string `json:"users"` // URL for all users associated with this source + Roles string `json:"roles,omitempty"` // URL for all users associated with this source } type sourceResponse struct { @@ -33,7 +34,7 @@ func newSourceResponse(src chronograf.Source) sourceResponse { src.Password = "" httpAPISrcs := "/chronograf/v1/sources" - return sourceResponse{ + res := sourceResponse{ Source: src, Links: sourceLinks{ Self: fmt.Sprintf("%s/%d", httpAPISrcs, src.ID), @@ -43,6 +44,11 @@ func newSourceResponse(src chronograf.Source) sourceResponse { Users: fmt.Sprintf("%s/%d/users", httpAPISrcs, src.ID), }, } + + if src.Type == "influx-enterprise" { + res.Links.Roles = fmt.Sprintf("%s/%d/roles", httpAPISrcs, src.ID) + } + return res } // NewSource adds a new valid source to the store diff --git a/server/swagger.json b/server/swagger.json index 3e9c0a6886..2d623cd5aa 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -537,6 +537,238 @@ } } }, + "/sources/{id}/roles": { + "get": { + "tags": [ + "sources", + "users", + "roles" + ], + "summary": "Retrieve all data sources roles. Available only in Influx Enterprise", + "parameters": [ + { + "name": "id", + "in": "path", + "type": "string", + "description": "ID of the data source", + "required": true + } + ], + "responses": { + "200": { + "description": "Listing of all roles", + "schema": { + "$ref": "#/definitions/Roles" + } + }, + "404": { + "description": "Data source id does not exist.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "A processing or an unexpected error.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "post": { + "tags": [ + "sources", + "users", + "roles" + ], + "summary": "Create new role for this data source", + "parameters": [ + { + "name": "id", + "in": "path", + "type": "string", + "description": "ID of the data source", + "required": true + }, + { + "name": "roleuser", + "in": "body", + "description": "Configuration options for new role", + "schema": { + "$ref": "#/definitions/Role" + } + } + ], + "responses": { + "201": { + "description": "Successfully created new role", + "headers": { + "Location": { + "type": "string", + "format": "url", + "description": "Location of the newly created role resource." + } + }, + "schema": { + "$ref": "#/definitions/Role" + } + }, + "404": { + "description": "Data source id does not exist.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "A processing or an unexpected error.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/sources/{id}/roles/{role_id}": { + "get": { + "tags": [ + "sources", + "users", + "roles" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "string", + "description": "ID of the data source", + "required": true + }, + { + "name": "role_id", + "in": "path", + "type": "string", + "description": "ID of the specific role", + "required": true + } + ], + "summary": "Returns information about a specific role", + "description": "Specific role within a data source", + "responses": { + "200": { + "description": "Information relating to the role", + "schema": { + "$ref": "#/definitions/Role" + } + }, + "404": { + "description": "Unknown role or unknown source", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "Unexpected internal service error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "patch": { + "tags": [ + "sources", + "users", + "roles" + ], + "summary": "Update role configuration", + "parameters": [ + { + "name": "id", + "in": "path", + "type": "string", + "description": "ID of the data source", + "required": true + }, + { + "name": "role_id", + "in": "path", + "type": "string", + "description": "ID of the specific role", + "required": true + }, + { + "name": "config", + "in": "body", + "description": "role configuration", + "schema": { + "$ref": "#/definitions/Role" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "Roles's configuration was changed", + "schema": { + "$ref": "#/definitions/Role" + } + }, + "404": { + "description": "Happens when trying to access a non-existent role or source.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "A processing or an unexpected error.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "delete": { + "tags": [ + "sources", + "users", + "roles" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "string", + "description": "ID of the data source", + "required": true + }, + { + "name": "role_id", + "in": "path", + "type": "string", + "description": "ID of the specific role", + "required": true + } + ], + "summary": "This specific role will be removed from the data source", + "responses": { + "204": { + "description": "Role has been removed" + }, + "404": { + "description": "Unknown role id or data source", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "Unexpected internal service error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, "/sources/{id}/kapacitors": { "get": { "tags": [ @@ -1590,7 +1822,7 @@ }, "put": { "tags": [ - "layouts" + "dashboards" ], "summary": "Replace dashboard information.", "parameters": [ @@ -2102,7 +2334,8 @@ "kapacitors": "/chronograf/v1/sources/4/kapacitors", "proxy": "/chronograf/v1/sources/4/proxy", "permissions": "/chronograf/v1/sources/4/permissions", - "users": "/chronograf/v1/sources/4/users" + "users": "/chronograf/v1/sources/4/users", + "roles": "/chronograf/v1/sources/4/roles" } }, "required": [ @@ -2180,6 +2413,11 @@ "type": "string", "description": "URL location of the permissions endpoint for this source", "format": "url" + }, + "roles": { + "type": "string", + "description": "Optional path to the roles endpoint IFF it is supported on this source", + "format": "url" } } } @@ -2268,6 +2506,48 @@ } } }, + "Roles": { + "type": "object", + "properties": { + "roles": { + "type": "array", + "items": { + "$ref": "#/definitions/Role" + } + } + } + }, + "Role": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "Unique name of the role", + "maxLength": 254, + "minLength": 1 + }, + "users": { + "$ref": "#/definitions/Users" + }, + "permissions": { + "$ref": "#/definitions/Permissions" + }, + "links": { + "type": "object", + "description": "URL relations of this role", + "properties": { + "self": { + "type": "string", + "format": "url", + "description": "URI of resource." + } + } + } + } + }, "Users": { "type": "object", "properties": { diff --git a/server/users_test.go b/server/users_test.go index 2a79f1bd26..42161f8f4c 100644 --- a/server/users_test.go +++ b/server/users_test.go @@ -54,7 +54,7 @@ func TestService_Me(t *testing.T) { principal: "me", wantStatus: http.StatusOK, wantContentType: "application/json", - wantBody: `{"username":"me","password":"hunter2","links":{"self":"/chronograf/v1/users/me"}} + wantBody: `{"name":"me","password":"hunter2","links":{"self":"/chronograf/v1/users/me"}} `, }, { @@ -77,7 +77,7 @@ func TestService_Me(t *testing.T) { principal: "secret", wantStatus: http.StatusOK, wantContentType: "application/json", - wantBody: `{"username":"secret","password":"","links":{"self":"/chronograf/v1/users/secret"}} + wantBody: `{"name":"secret","password":"","links":{"self":"/chronograf/v1/users/secret"}} `, }, {