diff --git a/bolt/users.go b/bolt/users.go index 5ed08ad16..feadb60c3 100644 --- a/bolt/users.go +++ b/bolt/users.go @@ -2,7 +2,7 @@ package bolt import ( "context" - "strconv" + "fmt" "github.com/boltdb/bolt" "github.com/influxdata/chronograf" @@ -38,17 +38,48 @@ func (s *UsersStore) get(ctx context.Context, id uint64) (*chronograf.User, erro return &u, nil } +func (s *UsersStore) each(ctx context.Context, fn func(*chronograf.User)) error { + return s.client.db.View(func(tx *bolt.Tx) error { + return tx.Bucket(UsersBucket).ForEach(func(k, v []byte) error { + var user chronograf.User + if err := internal.UnmarshalUser(v, &user); err != nil { + return err + } + fn(&user) + return nil + }) + }) +} + // Get searches the UsersStore for user with name -func (s *UsersStore) Get(ctx context.Context, id string) (*chronograf.User, error) { - uid, err := strconv.ParseUint(id, 10, 64) - if err != nil { - return nil, err +func (s *UsersStore) Get(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.ID != nil { + return s.get(ctx, *q.ID) } - u, err := s.get(ctx, uid) - if err != nil { - return nil, err + + if q.Name != nil && q.Provider != nil && q.Scheme != nil { + var user *chronograf.User + err := s.each(ctx, func(u *chronograf.User) { + if user != nil { + return + } + if u.Name == *q.Name && u.Provider == *q.Provider && u.Scheme == *q.Scheme { + user = u + } + }) + + if err != nil { + return nil, err + } + + if user == nil { + return nil, chronograf.ErrUserNotFound + } + + return user, nil } - return u, nil + + return nil, fmt.Errorf("must specify either ID, or Name, Provider, and Scheme in UserQuery") } // Add a new Users in the UsersStore. diff --git a/bolt/users_test.go b/bolt/users_test.go index dd2edaebb..042aed6ee 100644 --- a/bolt/users_test.go +++ b/bolt/users_test.go @@ -2,8 +2,6 @@ package bolt_test import ( "context" - "fmt" - "reflect" "testing" "github.com/google/go-cmp/cmp" @@ -18,25 +16,45 @@ var cmpOptions = cmp.Options{ cmpopts.EquateEmpty(), } -func TestUsersStore_Get(t *testing.T) { +func TestUsersStore_GetWithID(t *testing.T) { type args struct { ctx context.Context - ID string + usr *chronograf.User } tests := []struct { - name string - args args - want *chronograf.User - wantErr bool + name string + args args + want *chronograf.User + wantErr bool + addFirst bool }{ { name: "User not found", args: args{ ctx: context.Background(), - ID: "1337", + usr: &chronograf.User{ + ID: 1337, + }, }, wantErr: true, }, + { + name: "Get user", + args: args{ + ctx: context.Background(), + usr: &chronograf.User{ + Name: "billietta", + Provider: "Google", + Scheme: "OAuth2", + }, + }, + want: &chronograf.User{ + Name: "billietta", + Provider: "Google", + Scheme: "OAuth2", + }, + addFirst: true, + }, } for _, tt := range tests { client, err := NewTestClient() @@ -49,17 +67,118 @@ func TestUsersStore_Get(t *testing.T) { defer client.Close() s := client.UsersStore - got, err := s.Get(tt.args.ctx, tt.args.ID) + if tt.addFirst { + tt.args.usr, err = s.Add(tt.args.ctx, tt.args.usr) + if err != nil { + t.Fatal(err) + } + } + got, err := s.Get(tt.args.ctx, chronograf.UserQuery{ID: &tt.args.usr.ID}) if (err != nil) != tt.wantErr { t.Errorf("%q. UsersStore.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr) continue } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("%q. UsersStore.Get() = %v, want %v", tt.name, got, tt.want) + if diff := cmp.Diff(got, tt.want, cmpOptions...); diff != "" { + t.Errorf("%q. UsersStore.Get():\n-got/+want\ndiff %s", tt.name, diff) } } } +func TestUsersStore_GetWithNameProviderScheme(t *testing.T) { + type args struct { + ctx context.Context + name string + provider string + usr *chronograf.User + } + tests := []struct { + name string + args args + want *chronograf.User + wantErr bool + addFirst bool + }{ + { + name: "User not found", + args: args{ + ctx: context.Background(), + usr: &chronograf.User{ + Name: "billietta", + Provider: "Google", + Scheme: "OAuth2", + }, + }, + wantErr: true, + }, + { + name: "Get user", + args: args{ + ctx: context.Background(), + usr: &chronograf.User{ + Name: "billietta", + Provider: "Google", + Scheme: "OAuth2", + }, + }, + want: &chronograf.User{ + Name: "billietta", + Provider: "Google", + Scheme: "OAuth2", + }, + addFirst: true, + }, + } + for _, tt := range tests { + client, err := NewTestClient() + if err != nil { + t.Fatal(err) + } + if err := client.Open(context.TODO()); err != nil { + t.Fatal(err) + } + defer client.Close() + + s := client.UsersStore + if tt.addFirst { + tt.args.usr, err = s.Add(tt.args.ctx, tt.args.usr) + if err != nil { + t.Fatal(err) + } + } + + got, err := s.Get(tt.args.ctx, chronograf.UserQuery{ + Name: &tt.args.usr.Name, + Provider: &tt.args.usr.Provider, + Scheme: &tt.args.usr.Scheme, + }) + if (err != nil) != tt.wantErr { + t.Errorf("%q. UsersStore.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr) + continue + } + if diff := cmp.Diff(got, tt.want, cmpOptions...); diff != "" { + t.Errorf("%q. UsersStore.Get():\n-got/+want\ndiff %s", tt.name, diff) + } + } +} + +func TestUsersStore_GetInvalid(t *testing.T) { + client, err := NewTestClient() + if err != nil { + t.Fatal(err) + } + if err := client.Open(context.TODO()); err != nil { + t.Fatal(err) + } + defer client.Close() + + s := client.UsersStore + + _, err = s.Get(context.Background(), chronograf.UserQuery{}) + if err == nil { + t.Errorf("Invalid Get. UsersStore.Get() error = %v", err) + } +} + func TestUsersStore_Add(t *testing.T) { type args struct { ctx context.Context @@ -114,7 +233,7 @@ func TestUsersStore_Add(t *testing.T) { continue } - got, err = s.Get(tt.args.ctx, fmt.Sprintf("%d", got.ID)) + got, err = s.Get(tt.args.ctx, chronograf.UserQuery{ID: &got.ID}) if err != nil { t.Fatalf("failed to get user: %v", err) } @@ -298,7 +417,7 @@ func TestUsersStore_Update(t *testing.T) { continue } - got, err := s.Get(tt.args.ctx, fmt.Sprintf("%d", tt.args.usr.ID)) + got, err := s.Get(tt.args.ctx, chronograf.UserQuery{ID: &tt.args.usr.ID}) if err != nil { t.Fatalf("failed to get user: %v", err) } diff --git a/chronograf.go b/chronograf.go index 753068583..16187e159 100644 --- a/chronograf.go +++ b/chronograf.go @@ -606,6 +606,15 @@ type User struct { Scheme string `json:"scheme,omitempty"` } +// UserQuery represents the attributes that a user may be retrieved by. +// It is predominantly used in the UsersStore.Get method. +type UserQuery struct { + ID *uint64 + Name *string + Provider *string + Scheme *string +} + // UsersStore is the Storage and retrieval of authentication information type UsersStore interface { // All lists all users from the UsersStore @@ -615,7 +624,7 @@ type UsersStore interface { // Delete the User from the UsersStore Delete(context.Context, *User) error // Get retrieves a user if name exists. - Get(ctx context.Context, name string) (*User, error) + Get(ctx context.Context, q UserQuery) (*User, error) // Update the user's permissions or roles Update(context.Context, *User) error } diff --git a/enterprise/users.go b/enterprise/users.go index 68c04d193..6dc342164 100644 --- a/enterprise/users.go +++ b/enterprise/users.go @@ -2,6 +2,7 @@ package enterprise import ( "context" + "fmt" "github.com/influxdata/chronograf" ) @@ -28,7 +29,7 @@ func (c *UserStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.Us } } - return c.Get(ctx, u.Name) + return c.Get(ctx, chronograf.UserQuery{Name: &u.Name}) } // Delete the User from Influx Enterprise @@ -37,8 +38,11 @@ func (c *UserStore) Delete(ctx context.Context, u *chronograf.User) error { } // Get retrieves a user if name exists. -func (c *UserStore) Get(ctx context.Context, name string) (*chronograf.User, error) { - u, err := c.Ctrl.User(ctx, name) +func (c *UserStore) Get(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil { + return nil, fmt.Errorf("query must specify name") + } + u, err := c.Ctrl.User(ctx, *q.Name) if err != nil { return nil, err } @@ -48,7 +52,7 @@ func (c *UserStore) Get(ctx context.Context, name string) (*chronograf.User, err return nil, err } - role := ur[name] + role := ur[*q.Name] cr := role.ToChronograf() // For now we are removing all users from a role being returned. for i, r := range cr { diff --git a/enterprise/users_test.go b/enterprise/users_test.go index 9cc0cddc5..bd2e66774 100644 --- a/enterprise/users_test.go +++ b/enterprise/users_test.go @@ -375,7 +375,7 @@ func TestClient_Get(t *testing.T) { Ctrl: tt.fields.Ctrl, Logger: tt.fields.Logger, } - got, err := c.Get(tt.args.ctx, tt.args.name) + got, err := c.Get(tt.args.ctx, chronograf.UserQuery{Name: &tt.args.name}) if (err != nil) != tt.wantErr { t.Errorf("%q. Client.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr) continue diff --git a/influx/users.go b/influx/users.go index a8e10bcfa..a18385e7d 100644 --- a/influx/users.go +++ b/influx/users.go @@ -21,7 +21,7 @@ func (c *Client) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, return nil, err } } - return c.Get(ctx, u.Name) + return c.Get(ctx, chronograf.UserQuery{Name: &u.Name}) } // Delete the User from InfluxDB @@ -54,14 +54,18 @@ func (c *Client) Delete(ctx context.Context, u *chronograf.User) error { } // Get retrieves a user if name exists. -func (c *Client) Get(ctx context.Context, name string) (*chronograf.User, error) { +func (c *Client) Get(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil { + return nil, fmt.Errorf("query must specify name") + } + users, err := c.showUsers(ctx) if err != nil { return nil, err } for _, user := range users { - if user.Name == name { + if user.Name == *q.Name { perms, err := c.userPermissions(ctx, user.Name) if err != nil { return nil, err @@ -82,7 +86,7 @@ func (c *Client) Update(ctx context.Context, u *chronograf.User) error { return c.updatePassword(ctx, u.Name, u.Passwd) } - user, err := c.Get(ctx, u.Name) + user, err := c.Get(ctx, chronograf.UserQuery{Name: &u.Name}) if err != nil { return err } diff --git a/influx/users_test.go b/influx/users_test.go index 9922d4e51..3b32a44ff 100644 --- a/influx/users_test.go +++ b/influx/users_test.go @@ -392,7 +392,7 @@ func TestClient_Get(t *testing.T) { Logger: log.New(log.DebugLevel), } defer ts.Close() - got, err := c.Get(tt.args.ctx, tt.args.name) + got, err := c.Get(tt.args.ctx, chronograf.UserQuery{Name: &tt.args.name}) if (err != nil) != tt.wantErr { t.Errorf("%q. Client.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr) continue diff --git a/mocks/users.go b/mocks/users.go index 78071307f..d78d7199e 100644 --- a/mocks/users.go +++ b/mocks/users.go @@ -13,7 +13,7 @@ type UsersStore struct { AllF func(context.Context) ([]chronograf.User, error) AddF func(context.Context, *chronograf.User) (*chronograf.User, error) DeleteF func(context.Context, *chronograf.User) error - GetF func(ctx context.Context, name string) (*chronograf.User, error) + GetF func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) UpdateF func(context.Context, *chronograf.User) error } @@ -33,8 +33,8 @@ func (s *UsersStore) Delete(ctx context.Context, u *chronograf.User) error { } // Get retrieves a user if name exists. -func (s *UsersStore) Get(ctx context.Context, name string) (*chronograf.User, error) { - return s.GetF(ctx, name) +func (s *UsersStore) Get(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + return s.GetF(ctx, q) } // Update the user's permissions or roles diff --git a/server/auth.go b/server/auth.go index eb5868109..f7fc04d29 100644 --- a/server/auth.go +++ b/server/auth.go @@ -15,7 +15,7 @@ import ( func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next http.Handler) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log := logger. - WithField("component", "auth"). + WithField("component", "token_auth"). WithField("remote_addr", r.RemoteAddr). WithField("method", r.Method). WithField("url", r.URL) @@ -45,3 +45,101 @@ func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next h return }) } + +// AuthorizedUser extracts the user name and provider from context. If the +// user and provider can be found on the context, we look up the user by their +// name and provider. If the user is found, we verify that the user has at at +// least the role supplied. +func AuthorizedUser( + store chronograf.UsersStore, + useAuth bool, + role string, + logger chronograf.Logger, + next http.HandlerFunc, +) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !useAuth { + next(w, r) + return + } + + log := logger. + WithField("component", "role_auth"). + WithField("remote_addr", r.RemoteAddr). + WithField("method", r.Method). + WithField("url", r.URL) + + ctx := r.Context() + + username, err := getUsername(ctx) + if err != nil { + log.Error("Failed to retrieve username from context") + Error(w, http.StatusUnauthorized, "User is not authorized", logger) + return + } + provider, err := getProvider(ctx) + if err != nil { + log.Error("Failed to retrieve provider from context") + Error(w, http.StatusUnauthorized, "User is not authorized", logger) + return + } + scheme, err := getScheme(ctx) + if err != nil { + log.Error("Failed to retrieve scheme from context") + Error(w, http.StatusUnauthorized, "User is not authorized", logger) + return + } + + u, err := store.Get(ctx, chronograf.UserQuery{ + Name: &username, + Provider: &provider, + Scheme: &scheme, + }) + if err != nil { + log.Error("Failed to retrieve user") + Error(w, http.StatusUnauthorized, "User is not authorized", logger) + return + } + + if hasAuthorizedRole(u, role) { + next(w, r) + return + } + + Error(w, http.StatusUnauthorized, "User is not authorized", logger) + return + + }) +} + +func hasAuthorizedRole(u *chronograf.User, role string) bool { + if u == nil { + return false + } + + switch role { + case ViewerRoleName: + for _, r := range u.Roles { + switch r.Name { + case ViewerRoleName, EditorRoleName, AdminRoleName: + return true + } + } + case EditorRoleName: + for _, r := range u.Roles { + switch r.Name { + case EditorRoleName, AdminRoleName: + return true + } + } + case AdminRoleName: + for _, r := range u.Roles { + switch r.Name { + case AdminRoleName: + return true + } + } + } + + return false +} diff --git a/server/auth_test.go b/server/auth_test.go index a0d482de3..b19dcb1e7 100644 --- a/server/auth_test.go +++ b/server/auth_test.go @@ -3,11 +3,14 @@ package server_test import ( "context" "errors" + "fmt" "net/http" "net/http/httptest" "testing" + "github.com/influxdata/chronograf" clog "github.com/influxdata/chronograf/log" + "github.com/influxdata/chronograf/mocks" "github.com/influxdata/chronograf/oauth2" "github.com/influxdata/chronograf/server" ) @@ -94,3 +97,496 @@ func TestAuthorizedToken(t *testing.T) { } } } + +func TestAuthorizedUser(t *testing.T) { + type fields struct { + UsersStore chronograf.UsersStore + Logger chronograf.Logger + } + type args struct { + username string + provider string + useAuth bool + role string + } + tests := []struct { + name string + fields fields + args args + authorized bool + }{ + { + name: "Not using auth", + fields: fields{ + UsersStore: &mocks.UsersStore{}, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + useAuth: false, + }, + authorized: true, + }, + { + name: "User with viewer role is viewer authorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{ + server.ViewerRole, + }, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "viewer", + useAuth: true, + }, + authorized: true, + }, + { + name: "User with editor role is viewer authorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{ + server.EditorRole, + }, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "viewer", + useAuth: true, + }, + authorized: true, + }, + { + name: "User with admin role is viewer authorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{ + server.AdminRole, + }, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "viewer", + useAuth: true, + }, + authorized: true, + }, + { + name: "User with viewer role is editor unauthorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{ + server.ViewerRole, + }, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "editor", + useAuth: true, + }, + authorized: false, + }, + { + name: "User with editor role is editor authorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{ + server.EditorRole, + }, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "editor", + useAuth: true, + }, + authorized: true, + }, + { + name: "User with admin role is editor authorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{ + server.AdminRole, + }, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "editor", + useAuth: true, + }, + authorized: true, + }, + { + name: "User with viewer role is admin unauthorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{ + server.ViewerRole, + }, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "admin", + useAuth: true, + }, + authorized: false, + }, + { + name: "User with editor role is admin unauthorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{ + server.EditorRole, + }, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "admin", + useAuth: true, + }, + authorized: false, + }, + { + name: "User with admin role is admin authorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{ + server.AdminRole, + }, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "admin", + useAuth: true, + }, + authorized: true, + }, + { + name: "User with no role is viewer unauthorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{}, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "view", + useAuth: true, + }, + authorized: false, + }, + { + name: "User with no role is editor unauthorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{}, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "editor", + useAuth: true, + }, + authorized: false, + }, + { + name: "User with no role is admin unauthorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{}, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "admin", + useAuth: true, + }, + authorized: false, + }, + { + name: "User with unknown role is viewer unauthorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{ + { + Name: "sweet_role", + }, + }, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "viewer", + useAuth: true, + }, + authorized: false, + }, + { + name: "User with unknown role is editor unauthorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{ + { + Name: "sweet_role", + }, + }, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "editor", + useAuth: true, + }, + authorized: false, + }, + { + name: "User with unknown role is admin unauthorized", + fields: fields{ + UsersStore: &mocks.UsersStore{ + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return &chronograf.User{ + ID: 1337, + Name: "billysteve", + Provider: "Google", + Scheme: "OAuth2", + Roles: []chronograf.Role{ + { + Name: "sweet_role", + }, + }, + }, nil + }, + }, + Logger: clog.New(clog.DebugLevel), + }, + args: args{ + username: "billysteve", + provider: "Google", + role: "admin", + useAuth: true, + }, + authorized: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var authorized bool + next := func(w http.ResponseWriter, r *http.Request) { + authorized = true + } + fn := server.AuthorizedUser(tt.fields.UsersStore, tt.args.useAuth, tt.args.role, tt.fields.Logger, next) + + w := httptest.NewRecorder() + r := httptest.NewRequest( + "GET", + "http://any.url", // can be any valid URL as we are bypassing mux + nil, + ) + r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, oauth2.Principal{ + Subject: tt.args.username, + Issuer: tt.args.provider, + })) + fn(w, r) + + if authorized != tt.authorized { + t.Errorf("%q. AuthorizedUser() = %v, expected %v", tt.name, authorized, tt.authorized) + } + + }) + } +} diff --git a/server/me.go b/server/me.go index 45b4dd54b..9f6ca0810 100644 --- a/server/me.go +++ b/server/me.go @@ -36,7 +36,7 @@ func newMeResponse(usr *chronograf.User) meResponse { } } -func getEmail(ctx context.Context) (string, error) { +func getUsername(ctx context.Context) (string, error) { principal, err := getPrincipal(ctx) if err != nil { return "", err @@ -47,6 +47,24 @@ func getEmail(ctx context.Context) (string, error) { return principal.Subject, nil } +func getProvider(ctx context.Context) (string, error) { + principal, err := getPrincipal(ctx) + if err != nil { + return "", err + } + if principal.Issuer == "" { + return "", fmt.Errorf("Token not found") + } + return principal.Issuer, nil +} + +// TODO: This Scheme value is hard-coded temporarily since we only currently +// support OAuth2. This hard-coding should be removed whenever we add +// support for other authentication schemes. +func getScheme(ctx context.Context) (string, error) { + return "OAuth2", nil +} + func getPrincipal(ctx context.Context) (oauth2.Principal, error) { principal, ok := ctx.Value(oauth2.PrincipalKey).(oauth2.Principal) if !ok { @@ -56,7 +74,7 @@ func getPrincipal(ctx context.Context) (oauth2.Principal, error) { return principal, nil } -// Me does a findOrCreate based on the email in the context +// Me does a findOrCreate based on the username in the context func (s *Service) Me(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if !s.UseAuth { @@ -66,14 +84,33 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) { return } - email, err := getEmail(ctx) + username, err := getUsername(ctx) + if err != nil { + invalidData(w, err, s.Logger) + return + } + provider, err := getProvider(ctx) + if err != nil { + invalidData(w, err, s.Logger) + return + } + scheme, err := getScheme(ctx) if err != nil { invalidData(w, err, s.Logger) return } - usr, err := s.UsersStore.Get(ctx, email) - if err == nil { + usr, err := s.UsersStore.Get(ctx, chronograf.UserQuery{ + Name: &username, + Provider: &provider, + Scheme: &scheme, + }) + if err != nil && err != chronograf.ErrUserNotFound { + unknownErrorWithMessage(w, err, s.Logger) + return + } + + if usr != nil { res := newMeResponse(usr) encodeJSON(w, http.StatusOK, res, s.Logger) return @@ -81,7 +118,12 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) { // Because we didnt find a user, making a new one user := &chronograf.User{ - Name: email, + Name: username, + Provider: provider, + // TODO: This Scheme value is hard-coded temporarily since we only currently + // support OAuth2. This hard-coding should be removed whenever we add + // support for other authentication schemes. + Scheme: "OAuth2", } newUser, err := s.UsersStore.Add(ctx, user) diff --git a/server/me_test.go b/server/me_test.go index cfe387bae..d5f9f7528 100644 --- a/server/me_test.go +++ b/server/me_test.go @@ -43,21 +43,27 @@ func TestService_Me(t *testing.T) { }, fields: fields{ UseAuth: true, + Logger: log.New(log.DebugLevel), UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, name string) (*chronograf.User, error) { + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } return &chronograf.User{ - Name: "me", - Passwd: "hunter2", + Name: "me", + Provider: "GitHub", + Scheme: "OAuth2", }, nil }, }, }, principal: oauth2.Principal{ Subject: "me", + Issuer: "GitHub", }, wantStatus: http.StatusOK, wantContentType: "application/json", - wantBody: `{"name":"me","password":"hunter2","links":{"self":"/chronograf/v1/users/me"}} + wantBody: `{"name":"me","provider":"GitHub","scheme":"OAuth2","links":{"self":"/chronograf/v1/users/me"}} `, }, { @@ -68,9 +74,13 @@ func TestService_Me(t *testing.T) { }, fields: fields{ UseAuth: true, + Logger: log.New(log.DebugLevel), UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, name string) (*chronograf.User, error) { - return nil, fmt.Errorf("Unknown User") + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + if q.Name == nil || q.Provider == nil || q.Scheme == nil { + return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme") + } + return nil, chronograf.ErrUserNotFound }, AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { return u, nil @@ -79,10 +89,11 @@ func TestService_Me(t *testing.T) { }, principal: oauth2.Principal{ Subject: "secret", + Issuer: "Auth0", }, wantStatus: http.StatusOK, wantContentType: "application/json", - wantBody: `{"name":"secret","links":{"self":"/chronograf/v1/users/secret"}} + wantBody: `{"name":"secret","provider":"Auth0","scheme":"OAuth2","links":{"self":"/chronograf/v1/users/secret"}} `, }, { @@ -94,8 +105,8 @@ func TestService_Me(t *testing.T) { fields: fields{ UseAuth: true, UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, name string) (*chronograf.User, error) { - return nil, fmt.Errorf("Unknown User") + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + return nil, chronograf.ErrUserNotFound }, AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { return nil, fmt.Errorf("Why Heavy?") @@ -105,6 +116,7 @@ func TestService_Me(t *testing.T) { }, principal: oauth2.Principal{ Subject: "secret", + Issuer: "Heroku", }, wantStatus: http.StatusInternalServerError, wantContentType: "application/json", @@ -138,6 +150,7 @@ func TestService_Me(t *testing.T) { wantStatus: http.StatusUnprocessableEntity, principal: oauth2.Principal{ Subject: "", + Issuer: "", }, }, } diff --git a/server/mux.go b/server/mux.go index 35856775a..a5c1a4408 100644 --- a/server/mux.go +++ b/server/mux.go @@ -67,127 +67,142 @@ func NewMux(opts MuxOpts, service Service) http.Handler { hr.NotFound = http.StripPrefix(opts.Basepath, hr.NotFound) } + EnsureViewer := func(next http.HandlerFunc) http.HandlerFunc { + return AuthorizedUser(service.UsersStore, opts.UseAuth, ViewerRoleName, opts.Logger, next) + } + EnsureEditor := func(next http.HandlerFunc) http.HandlerFunc { + return AuthorizedUser(service.UsersStore, opts.UseAuth, EditorRoleName, opts.Logger, next) + } + EnsureAdmin := func(next http.HandlerFunc) http.HandlerFunc { + return AuthorizedUser(service.UsersStore, opts.UseAuth, AdminRoleName, opts.Logger, next) + } + /* Documentation */ router.GET("/swagger.json", Spec()) router.GET("/docs", Redoc("/swagger.json")) /* API */ // Sources - router.GET("/chronograf/v1/sources", service.Sources) - router.POST("/chronograf/v1/sources", service.NewSource) + router.GET("/chronograf/v1/sources", EnsureViewer(service.Sources)) + router.POST("/chronograf/v1/sources", EnsureEditor(service.NewSource)) - router.GET("/chronograf/v1/sources/:id", service.SourcesID) - router.PATCH("/chronograf/v1/sources/:id", service.UpdateSource) - router.DELETE("/chronograf/v1/sources/:id", service.RemoveSource) + router.GET("/chronograf/v1/sources/:id", EnsureViewer(service.SourcesID)) + router.PATCH("/chronograf/v1/sources/:id", EnsureEditor(service.UpdateSource)) + router.DELETE("/chronograf/v1/sources/:id", EnsureEditor(service.RemoveSource)) // Source Proxy to Influx; Has gzip compression around the handler - influx := gziphandler.GzipHandler(http.HandlerFunc(service.Influx)) + influx := gziphandler.GzipHandler(http.HandlerFunc(EnsureViewer(service.Influx))) router.Handler("POST", "/chronograf/v1/sources/:id/proxy", influx) // Write proxies line protocol write requests to InfluxDB - router.POST("/chronograf/v1/sources/:id/write", service.Write) + router.POST("/chronograf/v1/sources/:id/write", EnsureViewer(service.Write)) - // Queries is used to analyze a specific queries - router.POST("/chronograf/v1/sources/:id/queries", service.Queries) + // Queries is used to analyze a specific queries and does not create any + // resources. It's a POST because Queries are POSTed to InfluxDB, but this + // only modifies InfluxDB resources with certain metaqueries, e.g. DROP DATABASE. + // + // Admins should ensure that the InfluxDB source as the proper permissions + // intended for Chronograf Users with the Viewer Role type. + router.POST("/chronograf/v1/sources/:id/queries", EnsureViewer(service.Queries)) // All possible permissions for users in this source - router.GET("/chronograf/v1/sources/:id/permissions", service.Permissions) + router.GET("/chronograf/v1/sources/:id/permissions", EnsureViewer(service.Permissions)) // Users associated with the data source - router.GET("/chronograf/v1/sources/:id/users", service.SourceUsers) - router.POST("/chronograf/v1/sources/:id/users", service.NewSourceUser) + router.GET("/chronograf/v1/sources/:id/users", EnsureAdmin(service.SourceUsers)) + router.POST("/chronograf/v1/sources/:id/users", EnsureAdmin(service.NewSourceUser)) - router.GET("/chronograf/v1/sources/:id/users/:uid", service.SourceUserID) - router.DELETE("/chronograf/v1/sources/:id/users/:uid", service.RemoveSourceUser) - router.PATCH("/chronograf/v1/sources/:id/users/:uid", service.UpdateSourceUser) + router.GET("/chronograf/v1/sources/:id/users/:uid", EnsureAdmin(service.SourceUserID)) + router.DELETE("/chronograf/v1/sources/:id/users/:uid", EnsureAdmin(service.RemoveSourceUser)) + router.PATCH("/chronograf/v1/sources/:id/users/:uid", EnsureAdmin(service.UpdateSourceUser)) // Roles associated with the data source - router.GET("/chronograf/v1/sources/:id/roles", service.SourceRoles) - router.POST("/chronograf/v1/sources/:id/roles", service.NewSourceRole) + router.GET("/chronograf/v1/sources/:id/roles", EnsureViewer(service.SourceRoles)) + router.POST("/chronograf/v1/sources/:id/roles", EnsureEditor(service.NewSourceRole)) - router.GET("/chronograf/v1/sources/:id/roles/:rid", service.SourceRoleID) - router.DELETE("/chronograf/v1/sources/:id/roles/:rid", service.RemoveSourceRole) - router.PATCH("/chronograf/v1/sources/:id/roles/:rid", service.UpdateSourceRole) + router.GET("/chronograf/v1/sources/:id/roles/:rid", EnsureViewer(service.SourceRoleID)) + router.DELETE("/chronograf/v1/sources/:id/roles/:rid", EnsureEditor(service.RemoveSourceRole)) + router.PATCH("/chronograf/v1/sources/:id/roles/:rid", EnsureEditor(service.UpdateSourceRole)) // Kapacitor - router.GET("/chronograf/v1/sources/:id/kapacitors", service.Kapacitors) - router.POST("/chronograf/v1/sources/:id/kapacitors", service.NewKapacitor) + router.GET("/chronograf/v1/sources/:id/kapacitors", EnsureViewer(service.Kapacitors)) + router.POST("/chronograf/v1/sources/:id/kapacitors", EnsureEditor(service.NewKapacitor)) - router.GET("/chronograf/v1/sources/:id/kapacitors/:kid", service.KapacitorsID) - router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid", service.UpdateKapacitor) - router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid", service.RemoveKapacitor) + router.GET("/chronograf/v1/sources/:id/kapacitors/:kid", EnsureViewer(service.KapacitorsID)) + router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid", EnsureEditor(service.UpdateKapacitor)) + router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid", EnsureEditor(service.RemoveKapacitor)) // Kapacitor rules - router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/rules", service.KapacitorRulesGet) - router.POST("/chronograf/v1/sources/:id/kapacitors/:kid/rules", service.KapacitorRulesPost) + router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/rules", EnsureViewer(service.KapacitorRulesGet)) + router.POST("/chronograf/v1/sources/:id/kapacitors/:kid/rules", EnsureEditor(service.KapacitorRulesPost)) - router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesID) - router.PUT("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesPut) - router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesStatus) - router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesDelete) + router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", EnsureViewer(service.KapacitorRulesID)) + router.PUT("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", EnsureEditor(service.KapacitorRulesPut)) + router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", EnsureEditor(service.KapacitorRulesStatus)) + router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", EnsureEditor(service.KapacitorRulesDelete)) // Kapacitor Proxy - router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyGet) - router.POST("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyPost) - router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyPatch) - router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyDelete) + router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureViewer(service.KapacitorProxyGet)) + router.POST("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureEditor(service.KapacitorProxyPost)) + router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureEditor(service.KapacitorProxyPatch)) + router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", EnsureEditor(service.KapacitorProxyDelete)) // Mappings - router.GET("/chronograf/v1/mappings", service.GetMappings) + router.GET("/chronograf/v1/mappings", EnsureViewer(service.GetMappings)) // Layouts - router.GET("/chronograf/v1/layouts", service.Layouts) - router.POST("/chronograf/v1/layouts", service.NewLayout) + router.GET("/chronograf/v1/layouts", EnsureViewer(service.Layouts)) + router.POST("/chronograf/v1/layouts", EnsureEditor(service.NewLayout)) - router.GET("/chronograf/v1/layouts/:id", service.LayoutsID) - router.PUT("/chronograf/v1/layouts/:id", service.UpdateLayout) - router.DELETE("/chronograf/v1/layouts/:id", service.RemoveLayout) + router.GET("/chronograf/v1/layouts/:id", EnsureViewer(service.LayoutsID)) + router.PUT("/chronograf/v1/layouts/:id", EnsureEditor(service.UpdateLayout)) + router.DELETE("/chronograf/v1/layouts/:id", EnsureEditor(service.RemoveLayout)) // Users associated with Chronograf router.GET("/chronograf/v1/me", service.Me) - router.GET("/chronograf/v1/users", service.Users) - router.POST("/chronograf/v1/users", service.NewUser) + router.GET("/chronograf/v1/users", EnsureAdmin(service.Users)) + router.POST("/chronograf/v1/users", EnsureAdmin(service.NewUser)) - router.GET("/chronograf/v1/users/:id", service.UserID) - router.DELETE("/chronograf/v1/users/:id", service.RemoveUser) - router.PATCH("/chronograf/v1/users/:id", service.UpdateUser) + router.GET("/chronograf/v1/users/:id", EnsureAdmin(service.UserID)) + router.DELETE("/chronograf/v1/users/:id", EnsureAdmin(service.RemoveUser)) + router.PATCH("/chronograf/v1/users/:id", EnsureAdmin(service.UpdateUser)) // Dashboards - router.GET("/chronograf/v1/dashboards", service.Dashboards) - router.POST("/chronograf/v1/dashboards", service.NewDashboard) + router.GET("/chronograf/v1/dashboards", EnsureViewer(service.Dashboards)) + router.POST("/chronograf/v1/dashboards", EnsureEditor(service.NewDashboard)) - router.GET("/chronograf/v1/dashboards/:id", service.DashboardID) - router.DELETE("/chronograf/v1/dashboards/:id", service.RemoveDashboard) - router.PUT("/chronograf/v1/dashboards/:id", service.ReplaceDashboard) - router.PATCH("/chronograf/v1/dashboards/:id", service.UpdateDashboard) + router.GET("/chronograf/v1/dashboards/:id", EnsureViewer(service.DashboardID)) + router.DELETE("/chronograf/v1/dashboards/:id", EnsureEditor(service.RemoveDashboard)) + router.PUT("/chronograf/v1/dashboards/:id", EnsureEditor(service.ReplaceDashboard)) + router.PATCH("/chronograf/v1/dashboards/:id", EnsureEditor(service.UpdateDashboard)) // Dashboard Cells - router.GET("/chronograf/v1/dashboards/:id/cells", service.DashboardCells) - router.POST("/chronograf/v1/dashboards/:id/cells", service.NewDashboardCell) + router.GET("/chronograf/v1/dashboards/:id/cells", EnsureViewer(service.DashboardCells)) + router.POST("/chronograf/v1/dashboards/:id/cells", EnsureEditor(service.NewDashboardCell)) - router.GET("/chronograf/v1/dashboards/:id/cells/:cid", service.DashboardCellID) - router.DELETE("/chronograf/v1/dashboards/:id/cells/:cid", service.RemoveDashboardCell) - router.PUT("/chronograf/v1/dashboards/:id/cells/:cid", service.ReplaceDashboardCell) + router.GET("/chronograf/v1/dashboards/:id/cells/:cid", EnsureViewer(service.DashboardCellID)) + router.DELETE("/chronograf/v1/dashboards/:id/cells/:cid", EnsureEditor(service.RemoveDashboardCell)) + router.PUT("/chronograf/v1/dashboards/:id/cells/:cid", EnsureEditor(service.ReplaceDashboardCell)) // Dashboard Templates - router.GET("/chronograf/v1/dashboards/:id/templates", service.Templates) - router.POST("/chronograf/v1/dashboards/:id/templates", service.NewTemplate) + router.GET("/chronograf/v1/dashboards/:id/templates", EnsureViewer(service.Templates)) + router.POST("/chronograf/v1/dashboards/:id/templates", EnsureEditor(service.NewTemplate)) - router.GET("/chronograf/v1/dashboards/:id/templates/:tid", service.TemplateID) - router.DELETE("/chronograf/v1/dashboards/:id/templates/:tid", service.RemoveTemplate) - router.PUT("/chronograf/v1/dashboards/:id/templates/:tid", service.ReplaceTemplate) + router.GET("/chronograf/v1/dashboards/:id/templates/:tid", EnsureViewer(service.TemplateID)) + router.DELETE("/chronograf/v1/dashboards/:id/templates/:tid", EnsureEditor(service.RemoveTemplate)) + router.PUT("/chronograf/v1/dashboards/:id/templates/:tid", EnsureEditor(service.ReplaceTemplate)) // Databases - router.GET("/chronograf/v1/sources/:id/dbs", service.GetDatabases) - router.POST("/chronograf/v1/sources/:id/dbs", service.NewDatabase) + router.GET("/chronograf/v1/sources/:id/dbs", EnsureViewer(service.GetDatabases)) + router.POST("/chronograf/v1/sources/:id/dbs", EnsureEditor(service.NewDatabase)) - router.DELETE("/chronograf/v1/sources/:id/dbs/:dbid", service.DropDatabase) + router.DELETE("/chronograf/v1/sources/:id/dbs/:dbid", EnsureEditor(service.DropDatabase)) // Retention Policies - router.GET("/chronograf/v1/sources/:id/dbs/:dbid/rps", service.RetentionPolicies) - router.POST("/chronograf/v1/sources/:id/dbs/:dbid/rps", service.NewRetentionPolicy) + router.GET("/chronograf/v1/sources/:id/dbs/:dbid/rps", EnsureViewer(service.RetentionPolicies)) + router.POST("/chronograf/v1/sources/:id/dbs/:dbid/rps", EnsureEditor(service.NewRetentionPolicy)) - router.PUT("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", service.UpdateRetentionPolicy) - router.DELETE("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", service.DropRetentionPolicy) + router.PUT("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", EnsureEditor(service.UpdateRetentionPolicy)) + router.DELETE("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", EnsureEditor(service.DropRetentionPolicy)) allRoutes := &AllRoutes{ Logger: opts.Logger, diff --git a/server/sources.go b/server/sources.go index a3fdcce6c..fb60e401c 100644 --- a/server/sources.go +++ b/server/sources.go @@ -442,6 +442,7 @@ func (s *Service) SourceUsers(w http.ResponseWriter, r *http.Request) { } // SourceUserID retrieves a user with ID from store. +// In InfluxDB, a User's Name is their UID, hence the semantic below. func (s *Service) SourceUserID(w http.ResponseWriter, r *http.Request) { ctx := r.Context() uid := httprouter.GetParamFromContext(ctx, "uid") @@ -451,7 +452,7 @@ func (s *Service) SourceUserID(w http.ResponseWriter, r *http.Request) { return } store := ts.Users(ctx) - u, err := store.Get(ctx, uid) + u, err := store.Get(ctx, chronograf.UserQuery{Name: &uid}) if err != nil { Error(w, http.StatusBadRequest, err.Error(), s.Logger) return @@ -514,7 +515,7 @@ func (s *Service) UpdateSourceUser(w http.ResponseWriter, r *http.Request) { return } - u, err := store.Get(ctx, uid) + u, err := store.Get(ctx, chronograf.UserQuery{Name: &uid}) if err != nil { Error(w, http.StatusBadRequest, err.Error(), s.Logger) return diff --git a/server/sources_test.go b/server/sources_test.go index a357bd91c..a100f0836 100644 --- a/server/sources_test.go +++ b/server/sources_test.go @@ -778,7 +778,7 @@ func TestService_SourceUserID(t *testing.T) { }, UsersF: func(ctx context.Context) chronograf.UsersStore { return &mocks.UsersStore{ - GetF: func(ctx context.Context, uid string) (*chronograf.User, error) { + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { return &chronograf.User{ Name: "strickland", Passwd: "discipline", @@ -833,7 +833,7 @@ func TestService_SourceUserID(t *testing.T) { }, UsersF: func(ctx context.Context) chronograf.UsersStore { return &mocks.UsersStore{ - GetF: func(ctx context.Context, uid string) (*chronograf.User, error) { + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { return &chronograf.User{ Name: "strickland", Passwd: "discipline", @@ -1041,7 +1041,7 @@ func TestService_UpdateSourceUser(t *testing.T) { UpdateF: func(ctx context.Context, u *chronograf.User) error { return nil }, - GetF: func(ctx context.Context, name string) (*chronograf.User, error) { + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { return &chronograf.User{ Name: "marty", }, nil @@ -1093,7 +1093,7 @@ func TestService_UpdateSourceUser(t *testing.T) { UpdateF: func(ctx context.Context, u *chronograf.User) error { return nil }, - GetF: func(ctx context.Context, name string) (*chronograf.User, error) { + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { return &chronograf.User{ Name: "marty", }, nil diff --git a/server/users.go b/server/users.go index 3a079b769..071b537ba 100644 --- a/server/users.go +++ b/server/users.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "sort" + "strconv" "github.com/bouk/httprouter" "github.com/influxdata/chronograf" @@ -28,6 +29,11 @@ func (r *userRequest) ValidCreate() error { if r.Scheme == "" { return fmt.Errorf("Scheme required on Chronograf User request body") } + + // TODO: This Scheme value is hard-coded temporarily since we only currently + // support OAuth2. This hard-coding should be removed whenever we add + // support for other authentication schemes. + r.Scheme = "OAuth2" return r.ValidRoles() } @@ -132,8 +138,13 @@ var ( func (s *Service) UserID(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - id := httprouter.GetParamFromContext(ctx, "id") - user, err := s.UsersStore.Get(ctx, id) + idStr := httprouter.GetParamFromContext(ctx, "id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + Error(w, http.StatusBadRequest, fmt.Sprintf("invalid user id: %s", err.Error()), s.Logger) + return + } + user, err := s.UsersStore.Get(ctx, chronograf.UserQuery{ID: &id}) if err != nil { Error(w, http.StatusBadRequest, err.Error(), s.Logger) return @@ -178,9 +189,14 @@ func (s *Service) NewUser(w http.ResponseWriter, r *http.Request) { // RemoveUser deletes a Chronograf user from store func (s *Service) RemoveUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - id := httprouter.GetParamFromContext(ctx, "id") + idStr := httprouter.GetParamFromContext(ctx, "id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + Error(w, http.StatusBadRequest, fmt.Sprintf("invalid user id: %s", err.Error()), s.Logger) + return + } - u, err := s.UsersStore.Get(ctx, id) + u, err := s.UsersStore.Get(ctx, chronograf.UserQuery{ID: &id}) if err != nil { Error(w, http.StatusNotFound, err.Error(), s.Logger) } @@ -205,9 +221,14 @@ func (s *Service) UpdateUser(w http.ResponseWriter, r *http.Request) { } ctx := r.Context() - id := httprouter.GetParamFromContext(ctx, "id") + idStr := httprouter.GetParamFromContext(ctx, "id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + Error(w, http.StatusBadRequest, fmt.Sprintf("invalid user id: %s", err.Error()), s.Logger) + return + } - u, err := s.UsersStore.Get(ctx, id) + u, err := s.UsersStore.Get(ctx, chronograf.UserQuery{ID: &id}) if err != nil { Error(w, http.StatusNotFound, err.Error(), s.Logger) } diff --git a/server/users_test.go b/server/users_test.go index 95bd49a0f..0d98149cf 100644 --- a/server/users_test.go +++ b/server/users_test.go @@ -47,9 +47,9 @@ func TestService_UserID(t *testing.T) { fields: fields{ Logger: log.New(log.DebugLevel), UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, ID string) (*chronograf.User, error) { - switch ID { - case "1337": + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + switch *q.ID { + case 1337: return &chronograf.User{ ID: 1337, Name: "billysteve", @@ -60,7 +60,7 @@ func TestService_UserID(t *testing.T) { }, }, nil default: - return nil, fmt.Errorf("User with ID %s not found", ID) + return nil, fmt.Errorf("User with ID %s not found", *q.ID) } }, }, @@ -212,9 +212,9 @@ func TestService_RemoveUser(t *testing.T) { fields: fields{ Logger: log.New(log.DebugLevel), UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, ID string) (*chronograf.User, error) { - switch ID { - case "1339": + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + switch *q.ID { + case 1339: return &chronograf.User{ ID: 1339, Name: "helena", @@ -222,7 +222,7 @@ func TestService_RemoveUser(t *testing.T) { Scheme: "LDAP", }, nil default: - return nil, fmt.Errorf("User with ID %s not found", ID) + return nil, fmt.Errorf("User with ID %s not found", *q.ID) } }, DeleteF: func(ctx context.Context, user *chronograf.User) error { @@ -303,9 +303,9 @@ func TestService_UpdateUser(t *testing.T) { UpdateF: func(ctx context.Context, user *chronograf.User) error { return nil }, - GetF: func(ctx context.Context, ID string) (*chronograf.User, error) { - switch ID { - case "1336": + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + switch *q.ID { + case 1336: return &chronograf.User{ ID: 1336, Name: "bobbetta2", @@ -316,7 +316,7 @@ func TestService_UpdateUser(t *testing.T) { }, }, nil default: - return nil, fmt.Errorf("User with ID %s not found", ID) + return nil, fmt.Errorf("User with ID %s not found", *q.ID) } }, }, @@ -351,9 +351,9 @@ func TestService_UpdateUser(t *testing.T) { UpdateF: func(ctx context.Context, user *chronograf.User) error { return nil }, - GetF: func(ctx context.Context, ID string) (*chronograf.User, error) { - switch ID { - case "1336": + GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { + switch *q.ID { + case 1336: return &chronograf.User{ ID: 1336, Name: "bobbetta2", @@ -361,7 +361,7 @@ func TestService_UpdateUser(t *testing.T) { Scheme: "OAuth2", }, nil default: - return nil, fmt.Errorf("User with ID %s not found", ID) + return nil, fmt.Errorf("User with ID %s not found", *q.ID) } }, },