Add roles to chronograf

pull/10616/head
Chris Goller 2017-02-23 16:02:53 -06:00
parent 5eed15b450
commit 08271f25ef
16 changed files with 1596 additions and 147 deletions

View File

@ -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"`
}

View File

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

View File

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

View File

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

View File

@ -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
}*/
}

View File

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

View File

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

View File

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

View File

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

43
mocks/roles.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": {

View File

@ -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"}}
`,
},
{