2016-11-17 23:57:46 +00:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2017-11-06 15:48:31 +00:00
|
|
|
"context"
|
2017-03-10 19:24:48 +00:00
|
|
|
"encoding/json"
|
2016-11-17 23:57:46 +00:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2017-10-09 22:19:46 +00:00
|
|
|
"sort"
|
2017-10-18 18:17:42 +00:00
|
|
|
"strconv"
|
2016-11-17 23:57:46 +00:00
|
|
|
|
2017-03-10 19:24:48 +00:00
|
|
|
"github.com/bouk/httprouter"
|
2016-11-17 23:57:46 +00:00
|
|
|
"github.com/influxdata/chronograf"
|
2017-11-03 20:32:05 +00:00
|
|
|
"github.com/influxdata/chronograf/roles"
|
2016-11-17 23:57:46 +00:00
|
|
|
)
|
|
|
|
|
2017-10-09 21:16:24 +00:00
|
|
|
type userRequest struct {
|
2017-11-01 00:58:40 +00:00
|
|
|
ID uint64 `json:"id,string"`
|
|
|
|
Name string `json:"name"`
|
|
|
|
Provider string `json:"provider"`
|
|
|
|
Scheme string `json:"scheme"`
|
|
|
|
SuperAdmin bool `json:"superAdmin"`
|
|
|
|
Roles []chronograf.Role `json:"roles"`
|
2017-02-15 22:28:17 +00:00
|
|
|
}
|
|
|
|
|
2017-10-09 21:16:24 +00:00
|
|
|
func (r *userRequest) ValidCreate() error {
|
|
|
|
if r.Name == "" {
|
2017-10-10 21:21:43 +00:00
|
|
|
return fmt.Errorf("Name required on Chronograf User request body")
|
2017-03-10 19:24:48 +00:00
|
|
|
}
|
2017-10-09 21:16:24 +00:00
|
|
|
if r.Provider == "" {
|
2017-10-10 21:21:43 +00:00
|
|
|
return fmt.Errorf("Provider required on Chronograf User request body")
|
2017-03-10 19:24:48 +00:00
|
|
|
}
|
2017-10-09 21:16:24 +00:00
|
|
|
if r.Scheme == "" {
|
2017-10-10 21:21:43 +00:00
|
|
|
return fmt.Errorf("Scheme required on Chronograf User request body")
|
2017-03-10 19:24:48 +00:00
|
|
|
}
|
2017-10-19 18:17:40 +00:00
|
|
|
|
|
|
|
// 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.
|
2017-10-24 23:17:59 +00:00
|
|
|
r.Scheme = "oauth2"
|
2017-10-13 19:34:30 +00:00
|
|
|
return r.ValidRoles()
|
2017-10-09 21:16:24 +00:00
|
|
|
}
|
2017-03-10 19:24:48 +00:00
|
|
|
|
2017-10-09 21:16:24 +00:00
|
|
|
func (r *userRequest) ValidUpdate() error {
|
2017-11-01 16:38:26 +00:00
|
|
|
if len(r.Roles) == 0 {
|
2017-10-24 21:20:24 +00:00
|
|
|
return fmt.Errorf("No Roles to update")
|
2017-02-15 22:28:17 +00:00
|
|
|
}
|
2017-10-13 19:34:30 +00:00
|
|
|
return r.ValidRoles()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *userRequest) ValidRoles() error {
|
2017-11-03 16:06:18 +00:00
|
|
|
orgs := map[string]bool{}
|
2017-10-16 18:58:16 +00:00
|
|
|
if len(r.Roles) > 0 {
|
2017-10-13 19:34:30 +00:00
|
|
|
for _, r := range r.Roles {
|
2017-11-22 19:49:44 +00:00
|
|
|
if r.Organization == "" {
|
|
|
|
return fmt.Errorf("no organization was provided")
|
|
|
|
}
|
2017-11-22 20:03:15 +00:00
|
|
|
if _, err := parseOrganizationID(r.Organization); err != nil {
|
|
|
|
return fmt.Errorf("failed to parse organization ID: %v", err)
|
|
|
|
}
|
2017-11-03 16:06:18 +00:00
|
|
|
if _, ok := orgs[r.Organization]; ok {
|
|
|
|
return fmt.Errorf("duplicate organization %q in roles", r.Organization)
|
|
|
|
}
|
|
|
|
orgs[r.Organization] = true
|
2017-10-16 23:26:08 +00:00
|
|
|
switch r.Name {
|
2017-11-03 20:32:05 +00:00
|
|
|
case roles.MemberRoleName, roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName:
|
2017-10-16 23:26:08 +00:00
|
|
|
continue
|
|
|
|
default:
|
2017-11-07 18:59:40 +00:00
|
|
|
return fmt.Errorf("Unknown role %s. Valid roles are 'member', 'viewer', 'editor', and 'admin'", r.Name)
|
2017-10-13 19:34:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-10-09 21:16:24 +00:00
|
|
|
return nil
|
2016-11-17 23:57:46 +00:00
|
|
|
}
|
|
|
|
|
2017-10-09 21:16:24 +00:00
|
|
|
type userResponse struct {
|
2017-11-01 00:58:40 +00:00
|
|
|
Links selfLinks `json:"links"`
|
|
|
|
ID uint64 `json:"id,string"`
|
|
|
|
Name string `json:"name"`
|
|
|
|
Provider string `json:"provider"`
|
|
|
|
Scheme string `json:"scheme"`
|
|
|
|
SuperAdmin bool `json:"superAdmin"`
|
|
|
|
Roles []chronograf.Role `json:"roles"`
|
2017-10-09 21:16:24 +00:00
|
|
|
}
|
2017-03-10 19:24:48 +00:00
|
|
|
|
2017-10-09 21:16:24 +00:00
|
|
|
func newUserResponse(u *chronograf.User) *userResponse {
|
2017-10-16 23:26:08 +00:00
|
|
|
// This ensures that any user response with no roles returns an empty array instead of
|
|
|
|
// null when marshaled into JSON. That way, JavaScript doesn't need any guard on the
|
|
|
|
// key existing and it can simply be iterated over.
|
|
|
|
if u.Roles == nil {
|
|
|
|
u.Roles = []chronograf.Role{}
|
2017-10-13 19:34:30 +00:00
|
|
|
}
|
2017-10-09 21:16:24 +00:00
|
|
|
return &userResponse{
|
2017-11-02 23:24:16 +00:00
|
|
|
ID: u.ID,
|
|
|
|
Name: u.Name,
|
|
|
|
Provider: u.Provider,
|
|
|
|
Scheme: u.Scheme,
|
|
|
|
Roles: u.Roles,
|
|
|
|
SuperAdmin: u.SuperAdmin,
|
2017-10-09 21:16:24 +00:00
|
|
|
Links: selfLinks{
|
2017-10-10 19:27:55 +00:00
|
|
|
Self: fmt.Sprintf("/chronograf/v1/users/%d", u.ID),
|
2017-10-09 21:16:24 +00:00
|
|
|
},
|
2017-03-10 19:24:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-09 22:03:50 +00:00
|
|
|
type usersResponse struct {
|
|
|
|
Links selfLinks `json:"links"`
|
|
|
|
Users []*userResponse `json:"users"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func newUsersResponse(users []chronograf.User) *usersResponse {
|
2017-10-10 18:30:57 +00:00
|
|
|
usersResp := make([]*userResponse, len(users))
|
2017-10-09 22:03:50 +00:00
|
|
|
for i, user := range users {
|
|
|
|
usersResp[i] = newUserResponse(&user)
|
|
|
|
}
|
2017-10-10 18:30:57 +00:00
|
|
|
sort.Slice(usersResp, func(i, j int) bool {
|
|
|
|
return usersResp[i].ID < usersResp[j].ID
|
|
|
|
})
|
2017-10-09 22:03:50 +00:00
|
|
|
return &usersResponse{
|
|
|
|
Users: usersResp,
|
|
|
|
Links: selfLinks{
|
|
|
|
Self: "/chronograf/v1/users",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-09 22:28:39 +00:00
|
|
|
// UserID retrieves a Chronograf user with ID from store
|
2017-10-09 21:16:24 +00:00
|
|
|
func (s *Service) UserID(w http.ResponseWriter, r *http.Request) {
|
2017-03-10 19:24:48 +00:00
|
|
|
ctx := r.Context()
|
2017-02-24 05:26:09 +00:00
|
|
|
|
2017-10-18 18:17:42 +00:00
|
|
|
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
|
|
|
|
}
|
2017-10-31 20:41:17 +00:00
|
|
|
user, err := s.Store.Users(ctx).Get(ctx, chronograf.UserQuery{ID: &id})
|
2016-11-17 23:57:46 +00:00
|
|
|
if err != nil {
|
2017-10-09 21:16:24 +00:00
|
|
|
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
2017-03-10 19:24:48 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-10-09 21:16:24 +00:00
|
|
|
res := newUserResponse(user)
|
|
|
|
encodeJSON(w, http.StatusOK, res, s.Logger)
|
2017-03-10 19:24:48 +00:00
|
|
|
}
|
|
|
|
|
2017-10-09 22:28:39 +00:00
|
|
|
// NewUser adds a new Chronograf user to store
|
2017-10-09 21:16:24 +00:00
|
|
|
func (s *Service) NewUser(w http.ResponseWriter, r *http.Request) {
|
2017-03-10 19:24:48 +00:00
|
|
|
var req userRequest
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
2017-10-09 21:16:24 +00:00
|
|
|
invalidJSON(w, s.Logger)
|
2016-11-17 23:57:46 +00:00
|
|
|
return
|
|
|
|
}
|
2017-02-17 19:37:00 +00:00
|
|
|
|
2017-10-09 21:16:24 +00:00
|
|
|
if err := req.ValidCreate(); err != nil {
|
|
|
|
invalidData(w, err, s.Logger)
|
2016-11-17 23:57:46 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-10-09 21:16:24 +00:00
|
|
|
ctx := r.Context()
|
2017-12-14 19:11:51 +00:00
|
|
|
|
|
|
|
cfg, err := s.Store.Config(ctx).Get(ctx)
|
|
|
|
if err != nil {
|
|
|
|
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if cfg.Auth.SuperAdminNewUsers {
|
|
|
|
req.SuperAdmin = true
|
|
|
|
}
|
|
|
|
|
2016-11-17 23:57:46 +00:00
|
|
|
user := &chronograf.User{
|
2017-10-09 21:16:24 +00:00
|
|
|
Name: req.Name,
|
|
|
|
Provider: req.Provider,
|
|
|
|
Scheme: req.Scheme,
|
2017-10-16 23:26:08 +00:00
|
|
|
Roles: req.Roles,
|
2017-03-10 19:24:48 +00:00
|
|
|
}
|
|
|
|
|
2017-11-06 15:48:31 +00:00
|
|
|
if err := setSuperAdmin(ctx, req, user); err != nil {
|
|
|
|
Error(w, http.StatusUnauthorized, err.Error(), s.Logger)
|
2017-11-03 16:51:33 +00:00
|
|
|
return
|
2017-11-01 13:12:19 +00:00
|
|
|
}
|
|
|
|
|
2017-10-31 20:41:17 +00:00
|
|
|
res, err := s.Store.Users(ctx).Add(ctx, user)
|
2016-11-17 23:57:46 +00:00
|
|
|
if err != nil {
|
2017-10-09 21:16:24 +00:00
|
|
|
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
2016-11-17 23:57:46 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-10-09 21:16:24 +00:00
|
|
|
cu := newUserResponse(res)
|
2017-10-10 21:40:45 +00:00
|
|
|
location(w, cu.Links.Self)
|
2017-10-09 21:16:24 +00:00
|
|
|
encodeJSON(w, http.StatusCreated, cu, s.Logger)
|
2016-11-17 23:57:46 +00:00
|
|
|
}
|
2017-03-10 19:24:48 +00:00
|
|
|
|
2017-10-09 22:28:39 +00:00
|
|
|
// RemoveUser deletes a Chronograf user from store
|
2017-10-09 21:16:24 +00:00
|
|
|
func (s *Service) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
2017-10-18 18:17:42 +00:00
|
|
|
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
|
|
|
|
}
|
2017-03-10 19:24:48 +00:00
|
|
|
|
2017-10-31 20:41:17 +00:00
|
|
|
u, err := s.Store.Users(ctx).Get(ctx, chronograf.UserQuery{ID: &id})
|
2017-03-10 19:24:48 +00:00
|
|
|
if err != nil {
|
2017-10-09 21:16:24 +00:00
|
|
|
Error(w, http.StatusNotFound, err.Error(), s.Logger)
|
2017-10-20 19:42:51 +00:00
|
|
|
return
|
2017-03-10 19:24:48 +00:00
|
|
|
}
|
2017-11-09 14:37:15 +00:00
|
|
|
ctxUser, ok := hasUserContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
Error(w, http.StatusBadRequest, "failed to retrieve user from context", s.Logger)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if ctxUser.ID == u.ID {
|
|
|
|
Error(w, http.StatusForbidden, "user cannot delete themselves", s.Logger)
|
|
|
|
return
|
|
|
|
}
|
2017-10-31 20:41:17 +00:00
|
|
|
if err := s.Store.Users(ctx).Delete(ctx, u); err != nil {
|
2017-10-09 21:16:24 +00:00
|
|
|
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
2017-10-20 19:42:51 +00:00
|
|
|
return
|
2017-03-10 19:24:48 +00:00
|
|
|
}
|
|
|
|
|
2017-10-09 21:16:24 +00:00
|
|
|
w.WriteHeader(http.StatusNoContent)
|
2017-03-10 19:24:48 +00:00
|
|
|
}
|
|
|
|
|
2017-10-09 22:28:39 +00:00
|
|
|
// UpdateUser updates a Chronograf user in store
|
2017-10-09 21:16:24 +00:00
|
|
|
func (s *Service) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var req userRequest
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
|
|
invalidJSON(w, s.Logger)
|
|
|
|
return
|
2017-03-10 19:24:48 +00:00
|
|
|
}
|
|
|
|
|
2017-10-09 21:16:24 +00:00
|
|
|
ctx := r.Context()
|
2017-10-18 18:17:42 +00:00
|
|
|
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
|
|
|
|
}
|
2017-03-10 19:24:48 +00:00
|
|
|
|
2017-11-22 20:20:48 +00:00
|
|
|
if err := req.ValidUpdate(); err != nil {
|
|
|
|
invalidData(w, err, s.Logger)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-10-31 20:41:17 +00:00
|
|
|
u, err := s.Store.Users(ctx).Get(ctx, chronograf.UserQuery{ID: &id})
|
2017-10-09 21:16:24 +00:00
|
|
|
if err != nil {
|
|
|
|
Error(w, http.StatusNotFound, err.Error(), s.Logger)
|
2017-10-20 19:42:51 +00:00
|
|
|
return
|
2017-03-10 19:24:48 +00:00
|
|
|
}
|
|
|
|
|
2017-10-24 21:20:24 +00:00
|
|
|
// ValidUpdate should ensure that req.Roles is not nil
|
|
|
|
u.Roles = req.Roles
|
2017-03-10 19:24:48 +00:00
|
|
|
|
2017-11-22 20:20:48 +00:00
|
|
|
// If the request contains a name, it must be the same as the
|
|
|
|
// one on the user. This is particularly useful to the front-end
|
|
|
|
// because they would like to provide the whole user object,
|
|
|
|
// including the name, provider, and scheme in update requests.
|
|
|
|
// But currently, it is not possible to change name, provider, or
|
|
|
|
// scheme via the API.
|
|
|
|
if req.Name != "" && req.Name != u.Name {
|
|
|
|
err := fmt.Errorf("Cannot update Name")
|
|
|
|
invalidData(w, err, s.Logger)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if req.Provider != "" && req.Provider != u.Provider {
|
|
|
|
err := fmt.Errorf("Cannot update Provider")
|
|
|
|
invalidData(w, err, s.Logger)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if req.Scheme != "" && req.Scheme != u.Scheme {
|
|
|
|
err := fmt.Errorf("Cannot update Scheme")
|
|
|
|
invalidData(w, err, s.Logger)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-11-06 15:48:31 +00:00
|
|
|
if err := setSuperAdmin(ctx, req, u); err != nil {
|
|
|
|
Error(w, http.StatusUnauthorized, err.Error(), s.Logger)
|
2017-11-03 16:51:33 +00:00
|
|
|
return
|
2017-11-01 14:39:09 +00:00
|
|
|
}
|
|
|
|
|
2017-10-31 20:41:17 +00:00
|
|
|
err = s.Store.Users(ctx).Update(ctx, u)
|
2017-10-09 21:16:24 +00:00
|
|
|
if err != nil {
|
|
|
|
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
|
|
|
return
|
2017-03-10 19:24:48 +00:00
|
|
|
}
|
|
|
|
|
2017-10-09 21:16:24 +00:00
|
|
|
cu := newUserResponse(u)
|
2017-10-10 21:40:45 +00:00
|
|
|
location(w, cu.Links.Self)
|
2017-10-09 21:16:24 +00:00
|
|
|
encodeJSON(w, http.StatusOK, cu, s.Logger)
|
2017-03-10 19:24:48 +00:00
|
|
|
}
|
2017-10-09 22:03:50 +00:00
|
|
|
|
2017-10-09 22:28:39 +00:00
|
|
|
// Users retrieves all Chronograf users from store
|
2017-10-09 22:03:50 +00:00
|
|
|
func (s *Service) Users(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
|
2017-10-31 20:41:17 +00:00
|
|
|
users, err := s.Store.Users(ctx).All(ctx)
|
2017-10-09 22:03:50 +00:00
|
|
|
if err != nil {
|
|
|
|
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
res := newUsersResponse(users)
|
|
|
|
encodeJSON(w, http.StatusOK, res, s.Logger)
|
|
|
|
}
|
2017-11-06 15:48:31 +00:00
|
|
|
|
|
|
|
func setSuperAdmin(ctx context.Context, req userRequest, user *chronograf.User) error {
|
2017-12-04 21:53:30 +00:00
|
|
|
// At a high level, this function checks the following
|
|
|
|
// 1. Is the user making the request a SuperAdmin.
|
|
|
|
// If they are, allow them to make whatever changes they please.
|
|
|
|
//
|
|
|
|
// 2. Is the user making the request trying to change the SuperAdmin
|
|
|
|
// status. If so, return an error.
|
|
|
|
//
|
|
|
|
// 3. If none of the above are the case, let the user make whichever
|
|
|
|
// changes were requested.
|
|
|
|
|
2017-11-06 15:48:31 +00:00
|
|
|
// Only allow users to set SuperAdmin if they have the superadmin context
|
|
|
|
// TODO(desa): Refactor this https://github.com/influxdata/chronograf/issues/2207
|
|
|
|
if isSuperAdmin := hasSuperAdminContext(ctx); isSuperAdmin {
|
|
|
|
user.SuperAdmin = req.SuperAdmin
|
2017-12-04 21:53:30 +00:00
|
|
|
} else if !isSuperAdmin && (user.SuperAdmin != req.SuperAdmin) {
|
2017-11-06 15:48:31 +00:00
|
|
|
// If req.SuperAdmin has been set, and the request was not made with the SuperAdmin
|
|
|
|
// context, return error
|
2017-12-04 21:53:30 +00:00
|
|
|
return fmt.Errorf("User does not have authorization required to set SuperAdmin status")
|
2017-11-06 15:48:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|