Add roles implementation of sources store

Minimal test coverage of Update/Delete/Add methods was done since they
do not involve any filtering. The filtering for them should have
happened at the API level.
pull/10616/head
Michael Desa 2017-11-03 15:32:59 -04:00
parent 8ded387a81
commit 9ee3b431db
5 changed files with 817 additions and 1 deletions

31
roles/roles.go Normal file
View File

@ -0,0 +1,31 @@
package roles
import (
"context"
"fmt"
)
type contextKey string
// ContextKey is the key used to specify the
// role via context
const ContextKey = contextKey("role")
func validRole(ctx context.Context) error {
// prevents panic in case of nil context
if ctx == nil {
return fmt.Errorf("expect non nil context")
}
role, ok := ctx.Value(ContextKey).(string)
// should never happen
if !ok {
return fmt.Errorf("expected role key to be a string")
}
switch role {
// TODO(desa): make real roles
case "member", "viewer", "editor", "admin":
return nil
default:
return fmt.Errorf("expected role key to be set")
}
}

134
roles/sources.go Normal file
View File

@ -0,0 +1,134 @@
package roles
import (
"context"
"github.com/influxdata/chronograf"
)
// ensure that SourcesStore implements chronograf.SourceStore
var _ chronograf.SourcesStore = &SourcesStore{}
// SourcesStore facade on a SourceStore that filters sources
// by role.
type SourcesStore struct {
store chronograf.SourcesStore
role string
}
// NewSourcesStore creates a new SourcesStore from an existing
// chronograf.SourceStore and an role string
func NewSourcesStore(s chronograf.SourcesStore, role string) *SourcesStore {
return &SourcesStore{
store: s,
role: role,
}
}
// All retrieves all sources from the underlying SourceStore and filters them
// by role.
func (s *SourcesStore) All(ctx context.Context) ([]chronograf.Source, error) {
err := validRole(ctx)
if err != nil {
return nil, err
}
ds, err := s.store.All(ctx)
if err != nil {
return nil, err
}
// This filters sources without allocating
// https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
sources := ds[:0]
for _, d := range ds {
if hasAuthorizedRole(d.Role, s.role) {
sources = append(sources, d)
}
}
return sources, nil
}
// Add creates a new Source in the SourcesStore with source.Role set to be the
// role from the source store.
func (s *SourcesStore) Add(ctx context.Context, d chronograf.Source) (chronograf.Source, error) {
err := validRole(ctx)
if err != nil {
return chronograf.Source{}, err
}
return s.store.Add(ctx, d)
}
// Delete the source from SourcesStore
func (s *SourcesStore) Delete(ctx context.Context, d chronograf.Source) error {
err := validRole(ctx)
if err != nil {
return err
}
d, err = s.store.Get(ctx, d.ID)
if err != nil {
return err
}
return s.store.Delete(ctx, d)
}
// Get returns a Source if the id exists and belongs to the role that is set.
func (s *SourcesStore) Get(ctx context.Context, id int) (chronograf.Source, error) {
err := validRole(ctx)
if err != nil {
return chronograf.Source{}, err
}
d, err := s.store.Get(ctx, id)
if err != nil {
return chronograf.Source{}, err
}
if !hasAuthorizedRole(d.Role, s.role) {
return chronograf.Source{}, chronograf.ErrSourceNotFound
}
return d, nil
}
// Update the source in SourcesStore.
func (s *SourcesStore) Update(ctx context.Context, d chronograf.Source) error {
err := validRole(ctx)
if err != nil {
return err
}
_, err = s.store.Get(ctx, d.ID)
if err != nil {
return err
}
return s.store.Update(ctx, d)
}
func hasAuthorizedRole(sourceRole, providedRole string) bool {
// TODO(desa): make real roles
switch sourceRole {
case "viewer":
switch providedRole {
case "viewer", "editor", "admin":
return true
}
case "editor":
switch providedRole {
case "editor", "admin":
return true
}
case "admin":
switch providedRole {
case "admin":
return true
}
}
return false
}

489
roles/sources_test.go Normal file
View File

@ -0,0 +1,489 @@
package roles
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/mocks"
)
func TestSources_Get(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
}
type args struct {
role string
id int
}
type wants struct {
source chronograf.Source
err bool
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "Get viewer source as viewer",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
}, nil
},
},
},
args: args{
role: "viewer",
id: 1,
},
wants: wants{
source: chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
},
},
{
name: "Get viewer source as editor",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
}, nil
},
},
},
args: args{
role: "editor",
id: 1,
},
wants: wants{
source: chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
},
},
{
name: "Get viewer source as admin",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
}, nil
},
},
},
args: args{
role: "admin",
id: 1,
},
wants: wants{
source: chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
},
},
{
name: "Get editor source as editor",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "editor",
}, nil
},
},
},
args: args{
role: "editor",
id: 1,
},
wants: wants{
source: chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "editor",
},
},
},
{
name: "Get editor source as admin",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "editor",
}, nil
},
},
},
args: args{
role: "admin",
id: 1,
},
wants: wants{
source: chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "editor",
},
},
},
{
name: "Get editor source as viewer - want error",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "editor",
}, nil
},
},
},
args: args{
role: "viewer",
id: 1,
},
wants: wants{
err: true,
},
},
{
name: "Get admin source as admin",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "admin",
}, nil
},
},
},
args: args{
role: "admin",
id: 1,
},
wants: wants{
source: chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "admin",
},
},
},
{
name: "Get admin source as viewer - want error",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "admin",
}, nil
},
},
},
args: args{
role: "viewer",
id: 1,
},
wants: wants{
err: true,
},
},
{
name: "Get admin source as editor - want error",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "admin",
}, nil
},
},
},
args: args{
role: "editor",
id: 1,
},
wants: wants{
err: true,
},
},
{
name: "Get source bad context",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "admin",
}, nil
},
},
},
args: args{
role: "random role",
id: 1,
},
wants: wants{
err: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := NewSourcesStore(tt.fields.SourcesStore, tt.args.role)
ctx := context.Background()
if tt.args.role != "" {
ctx = context.WithValue(ctx, ContextKey, tt.args.role)
}
source, err := store.Get(ctx, tt.args.id)
if (err != nil) != tt.wants.err {
t.Errorf("%q. Store.Sources().Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
return
}
if diff := cmp.Diff(source, tt.wants.source); diff != "" {
t.Errorf("%q. Store.Sources().Get():\n-got/+want\ndiff %s", tt.name, diff)
}
})
}
}
func TestSources_All(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
}
type args struct {
role string
}
type wants struct {
sources []chronograf.Source
err bool
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "Get viewer sources as viewer",
fields: fields{
SourcesStore: &mocks.SourcesStore{
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
return []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
{
ID: 2,
Name: "my sweet name",
Organization: "0",
Role: "editor",
},
{
ID: 3,
Name: "my sweet name",
Organization: "0",
Role: "admin",
},
}, nil
},
},
},
args: args{
role: "viewer",
},
wants: wants{
sources: []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
},
},
},
{
name: "Get editor sources as editor",
fields: fields{
SourcesStore: &mocks.SourcesStore{
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
return []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
{
ID: 2,
Name: "my sweet name",
Organization: "0",
Role: "editor",
},
{
ID: 3,
Name: "my sweet name",
Organization: "0",
Role: "admin",
},
}, nil
},
},
},
args: args{
role: "editor",
},
wants: wants{
sources: []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
{
ID: 2,
Name: "my sweet name",
Organization: "0",
Role: "editor",
},
},
},
},
{
name: "Get admin sources as admin",
fields: fields{
SourcesStore: &mocks.SourcesStore{
AllF: func(ctx context.Context) ([]chronograf.Source, error) {
return []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
{
ID: 2,
Name: "my sweet name",
Organization: "0",
Role: "editor",
},
{
ID: 3,
Name: "my sweet name",
Organization: "0",
Role: "admin",
},
}, nil
},
},
},
args: args{
role: "admin",
},
wants: wants{
sources: []chronograf.Source{
{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
{
ID: 2,
Name: "my sweet name",
Organization: "0",
Role: "editor",
},
{
ID: 3,
Name: "my sweet name",
Organization: "0",
Role: "admin",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := NewSourcesStore(tt.fields.SourcesStore, tt.args.role)
ctx := context.Background()
if tt.args.role != "" {
ctx = context.WithValue(ctx, ContextKey, tt.args.role)
}
sources, err := store.All(ctx)
if (err != nil) != tt.wants.err {
t.Errorf("%q. Store.Sources().Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
return
}
if diff := cmp.Diff(sources, tt.wants.sources); diff != "" {
t.Errorf("%q. Store.Sources().Get():\n-got/+want\ndiff %s", tt.name, diff)
}
})
}
}

View File

@ -6,6 +6,7 @@ import (
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/noop"
"github.com/influxdata/chronograf/organizations"
"github.com/influxdata/chronograf/roles"
)
// hasOrganizationContext retrieves organization specified on context
@ -26,6 +27,27 @@ func hasOrganizationContext(ctx context.Context) (string, bool) {
return orgID, true
}
// hasRoleContext retrieves organization specified on context
// under the organizations.ContextKey
func hasRoleContext(ctx context.Context) (string, bool) {
// prevents panic in case of nil context
if ctx == nil {
return "", false
}
role, ok := ctx.Value(roles.ContextKey).(string)
// should never happen
if !ok {
return "", false
}
switch role {
// TODO(desa): make real roles
case "member", "viewer", "editor", "admin":
return role, true
default:
return "", false
}
}
type superAdminKey string
// SuperAdminKey is the context key for retrieving is the context
@ -75,7 +97,10 @@ type Store struct {
// and a organization.SourcesStore otherwise.
func (s *Store) Sources(ctx context.Context) chronograf.SourcesStore {
if org, ok := hasOrganizationContext(ctx); ok {
return organizations.NewSourcesStore(s.SourcesStore, org)
store := organizations.NewSourcesStore(s.SourcesStore, org)
if role, ok := hasRoleContext(ctx); ok {
return roles.NewSourcesStore(store, role)
}
}
return &noop.SourcesStore{}

137
server/stores_test.go Normal file
View File

@ -0,0 +1,137 @@
package server
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/mocks"
"github.com/influxdata/chronograf/organizations"
"github.com/influxdata/chronograf/roles"
)
func TestStore_SourcesGet(t *testing.T) {
type fields struct {
SourcesStore chronograf.SourcesStore
}
type args struct {
superAdmin bool
organization string
role string
id int
}
type wants struct {
source chronograf.Source
err bool
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "Get user as super admin",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
}, nil
},
},
},
args: args{
superAdmin: true,
organization: "0",
role: "viewer",
},
wants: wants{
source: chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
},
},
},
{
name: "Get user as super admin",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
}, nil
},
},
},
args: args{
superAdmin: true,
},
wants: wants{
err: true,
},
},
{
name: "Get user as super admin",
fields: fields{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, id int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1,
Name: "my sweet name",
Organization: "0",
Role: "viewer",
}, nil
},
},
},
args: args{
superAdmin: true,
},
wants: wants{
err: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := &Store{
SourcesStore: tt.fields.SourcesStore,
}
ctx := context.Background()
if tt.args.superAdmin {
ctx = context.WithValue(ctx, SuperAdminKey, true)
}
if tt.args.organization != "" {
ctx = context.WithValue(ctx, organizations.ContextKey, tt.args.organization)
}
if tt.args.role != "" {
ctx = context.WithValue(ctx, roles.ContextKey, tt.args.role)
}
source, err := store.Sources(ctx).Get(ctx, tt.args.id)
if (err != nil) != tt.wants.err {
t.Errorf("%q. Store.Sources().Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
return
}
if diff := cmp.Diff(source, tt.wants.source); diff != "" {
t.Errorf("%q. Store.Sources().Get():\n-got/+want\ndiff %s", tt.name, diff)
}
})
}
}