package tenant import ( "context" "encoding/json" "fmt" "net/http" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/influxdata/influxdb/v2" icontext "github.com/influxdata/influxdb/v2/context" "github.com/influxdata/influxdb/v2/kit/platform" "github.com/influxdata/influxdb/v2/kit/platform/errors" kithttp "github.com/influxdata/influxdb/v2/kit/transport/http" "go.uber.org/zap" ) // UserHandler represents an HTTP API handler for users. type UserHandler struct { chi.Router api *kithttp.API log *zap.Logger userSvc influxdb.UserService passwordSvc influxdb.PasswordsService } const ( prefixUsers = "/api/v2/users" prefixMe = "/api/v2/me" ) // NewHTTPUserHandler constructs a new http server. func NewHTTPUserHandler(log *zap.Logger, userService influxdb.UserService, passwordService influxdb.PasswordsService) *UserHandler { svr := &UserHandler{ api: kithttp.NewAPI(kithttp.WithLog(log)), log: log, userSvc: userService, passwordSvc: passwordService, } r := chi.NewRouter() r.Use( middleware.Recoverer, middleware.RequestID, middleware.RealIP, ) // RESTy routes for "articles" resource r.Route("/", func(r chi.Router) { r.Post("/", svr.handlePostUser) r.Get("/", svr.handleGetUsers) r.Put("/password", svr.handlePutUserPassword) r.Route("/{id}", func(r chi.Router) { r.Get("/", svr.handleGetUser) r.Patch("/", svr.handlePatchUser) r.Delete("/", svr.handleDeleteUser) r.Get("/permissions", svr.handleGetPermissions) r.Put("/password", svr.handlePutUserPassword) r.Post("/password", svr.handlePostUserPassword) }) }) svr.Router = r return svr } func (h *UserHandler) Prefix() string { return prefixUsers } type passwordSetRequest struct { Password string `json:"password"` } // handlePutPassword is the HTTP handler for the PUT /api/v2/users/:id/password func (h *UserHandler) handlePostUserPassword(w http.ResponseWriter, r *http.Request) { var body passwordSetRequest err := json.NewDecoder(r.Body).Decode(&body) if err != nil { h.api.Err(w, r, &errors.Error{ Code: errors.EInvalid, Err: err, }) return } param := chi.URLParam(r, "id") userID, err := platform.IDFromString(param) if err != nil { h.api.Err(w, r, &errors.Error{ Msg: "invalid user ID provided in route", }) return } err = h.passwordSvc.SetPassword(r.Context(), *userID, body.Password) if err != nil { h.api.Err(w, r, err) return } w.WriteHeader(http.StatusNoContent) } // handlePutPassword is the HTTP handler for the PUT /api/v2/users/:id/password func (h *UserHandler) handlePutUserPassword(w http.ResponseWriter, r *http.Request) { ctx := r.Context() req, err := decodePasswordResetRequest(r) if err != nil { h.api.Err(w, r, &errors.Error{ Msg: fmt.Sprintf("error decoding password reset request: %s", err), }) return } param := chi.URLParam(r, "id") userID, err := platform.IDFromString(param) if err != nil { h.api.Err(w, r, &errors.Error{ Msg: "invalid user ID provided in route", }) return } err = h.passwordSvc.CompareAndSetPassword(ctx, *userID, req.PasswordOld, req.PasswordNew) if err != nil { h.api.Err(w, r, err) return } h.log.Debug("User password updated") w.WriteHeader(http.StatusNoContent) } type passwordResetRequest struct { Username string PasswordOld string PasswordNew string } type passwordResetRequestBody struct { Password string `json:"password"` } func decodePasswordResetRequest(r *http.Request) (*passwordResetRequest, error) { u, o, ok := r.BasicAuth() if !ok { return nil, fmt.Errorf("invalid basic auth") } pr := new(passwordResetRequestBody) err := json.NewDecoder(r.Body).Decode(pr) if err != nil { return nil, &errors.Error{ Code: errors.EInvalid, Err: err, } } return &passwordResetRequest{ Username: u, PasswordOld: o, PasswordNew: pr.Password, }, nil } // handlePostUser is the HTTP handler for the POST /api/v2/users route. func (h *UserHandler) handlePostUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() req, err := decodePostUserRequest(ctx, r) if err != nil { h.api.Err(w, r, err) return } if req.User.Status == "" { req.User.Status = influxdb.Active } if err := h.userSvc.CreateUser(ctx, req.User); err != nil { h.api.Err(w, r, err) return } h.log.Debug("User created", zap.String("user", fmt.Sprint(req.User))) h.api.Respond(w, r, http.StatusCreated, newUserResponse(req.User)) } type postUserRequest struct { User *influxdb.User } func decodePostUserRequest(ctx context.Context, r *http.Request) (*postUserRequest, error) { b := &influxdb.User{} if err := json.NewDecoder(r.Body).Decode(b); err != nil { return nil, err } return &postUserRequest{ User: b, }, nil } // handleGetUser is the HTTP handler for the GET /api/v2/users/:id route. func (h *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() req, err := decodeGetUserRequest(ctx, r) if err != nil { h.api.Err(w, r, err) return } b, err := h.userSvc.FindUserByID(ctx, req.UserID) if err != nil { h.api.Err(w, r, err) return } h.log.Debug("User retrieved", zap.String("user", fmt.Sprint(b))) h.api.Respond(w, r, http.StatusOK, newUserResponse(b)) } func (h *UserHandler) handleGetPermissions(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if id == "" { err := &errors.Error{ Code: errors.EInvalid, Msg: "url missing id", } h.api.Err(w, r, err) return } var i platform.ID if err := i.DecodeFromString(id); err != nil { h.api.Err(w, r, err) return } ps, err := h.userSvc.FindPermissionForUser(r.Context(), i) if err != nil { h.api.Err(w, r, err) return } h.api.Respond(w, r, http.StatusOK, ps) } type getUserRequest struct { UserID platform.ID } func decodeGetUserRequest(ctx context.Context, r *http.Request) (*getUserRequest, error) { id := chi.URLParam(r, "id") if id == "" { return nil, &errors.Error{ Code: errors.EInvalid, Msg: "url missing id", } } var i platform.ID if err := i.DecodeFromString(id); err != nil { return nil, err } req := &getUserRequest{ UserID: i, } return req, nil } // handleDeleteUser is the HTTP handler for the DELETE /api/v2/users/:id route. func (h *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() req, err := decodeDeleteUserRequest(ctx, r) if err != nil { h.api.Err(w, r, err) return } if err := h.userSvc.DeleteUser(ctx, req.UserID); err != nil { h.api.Err(w, r, err) return } h.log.Debug("User deleted", zap.String("userID", fmt.Sprint(req.UserID))) w.WriteHeader(http.StatusNoContent) } type deleteUserRequest struct { UserID platform.ID } func decodeDeleteUserRequest(ctx context.Context, r *http.Request) (*deleteUserRequest, error) { id := chi.URLParam(r, "id") if id == "" { return nil, &errors.Error{ Code: errors.EInvalid, Msg: "url missing id", } } var i platform.ID if err := i.DecodeFromString(id); err != nil { return nil, err } return &deleteUserRequest{ UserID: i, }, nil } type usersResponse struct { Links map[string]string `json:"links"` Users []*influxdb.UserResponse `json:"users"` } func (us usersResponse) ToInfluxdb() []*influxdb.User { users := make([]*influxdb.User, len(us.Users)) for i := range us.Users { users[i] = &us.Users[i].User } return users } func newUsersResponse(users []*influxdb.User) *usersResponse { res := usersResponse{ Links: map[string]string{ "self": "/api/v2/users", }, Users: []*influxdb.UserResponse{}, } for _, user := range users { res.Users = append(res.Users, newUserResponse(user)) } return &res } func newUserResponse(u *influxdb.User) *influxdb.UserResponse { return &influxdb.UserResponse{ Links: map[string]string{ "self": fmt.Sprintf("/api/v2/users/%s", u.ID), }, User: *u, } } // handleGetUsers is the HTTP handler for the GET /api/v2/users route. func (h *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Request) { ctx := r.Context() req, err := decodeGetUsersRequest(ctx, r) if err != nil { h.api.Err(w, r, err) return } users, _, err := h.userSvc.FindUsers(ctx, req.filter, req.opts) if err != nil { h.api.Err(w, r, err) return } h.log.Debug("Users retrieved", zap.String("users", fmt.Sprint(users))) h.api.Respond(w, r, http.StatusOK, newUsersResponse(users)) } type getUsersRequest struct { filter influxdb.UserFilter opts influxdb.FindOptions } func decodeGetUsersRequest(ctx context.Context, r *http.Request) (*getUsersRequest, error) { opts, err := influxdb.DecodeFindOptions(r) if err != nil { return nil, err } qp := r.URL.Query() req := &getUsersRequest{ opts: *opts, } if userID := qp.Get("id"); userID != "" { id, err := platform.IDFromString(userID) if err != nil { return nil, err } req.filter.ID = id } if name := qp.Get("name"); name != "" { req.filter.Name = &name } return req, nil } // handlePatchUser is the HTTP handler for the PATCH /api/v2/users/:id route. func (h *UserHandler) handlePatchUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() req, err := decodePatchUserRequest(ctx, r) if err != nil { h.api.Err(w, r, err) return } b, err := h.userSvc.UpdateUser(ctx, req.UserID, req.Update) if err != nil { h.api.Err(w, r, err) return } h.log.Debug("Users updated", zap.String("user", fmt.Sprint(b))) h.api.Respond(w, r, http.StatusOK, newUserResponse(b)) } type patchUserRequest struct { Update influxdb.UserUpdate UserID platform.ID } func decodePatchUserRequest(ctx context.Context, r *http.Request) (*patchUserRequest, error) { id := chi.URLParam(r, "id") if id == "" { return nil, &errors.Error{ Code: errors.EInvalid, Msg: "url missing id", } } var i platform.ID if err := i.DecodeFromString(id); err != nil { return nil, err } var upd influxdb.UserUpdate if err := json.NewDecoder(r.Body).Decode(&upd); err != nil { return nil, err } if err := upd.Valid(); err != nil { return nil, err } return &patchUserRequest{ Update: upd, UserID: i, }, nil } // MeHandler represents an HTTP API handler for /me routes. type MeHandler struct { chi.Router api *kithttp.API log *zap.Logger userSvc influxdb.UserService passwordSvc influxdb.PasswordsService } func (h *MeHandler) Prefix() string { return prefixMe } func NewHTTPMeHandler(log *zap.Logger, userService influxdb.UserService, passwordService influxdb.PasswordsService) *MeHandler { svr := &MeHandler{ api: kithttp.NewAPI(kithttp.WithLog(log)), log: log, userSvc: userService, passwordSvc: passwordService, } r := chi.NewRouter() r.Use( middleware.Recoverer, middleware.RequestID, middleware.RealIP, ) // RESTy routes for "articles" resource r.Route("/", func(r chi.Router) { r.Get("/", svr.handleMe) r.Put("/password", svr.handlePutMePassword) }) svr.Router = r return svr } func (h *MeHandler) getUserID(ctx context.Context) (*platform.ID, error) { a, err := icontext.GetAuthorizer(ctx) if err != nil { return nil, err } id := a.GetUserID() return &id, nil } func (h *MeHandler) handleMe(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userID, err := h.getUserID(ctx) if err != nil { h.api.Err(w, r, err) return } user, err := h.userSvc.FindUserByID(ctx, *userID) if err != nil { h.api.Err(w, r, err) return } h.api.Respond(w, r, http.StatusOK, newUserResponse(user)) } func (h *MeHandler) handlePutMePassword(w http.ResponseWriter, r *http.Request) { ctx := r.Context() userID, err := h.getUserID(ctx) if err != nil { h.api.Err(w, r, err) return } req, err := decodePasswordResetRequest(r) if err != nil { h.api.Err(w, r, &errors.Error{ Msg: fmt.Sprintf("error decoding password reset request: %s", err), }) } err = h.passwordSvc.CompareAndSetPassword(ctx, *userID, req.PasswordOld, req.PasswordNew) if err != nil { h.api.Err(w, r, err) return } h.log.Debug("User password updated") w.WriteHeader(http.StatusNoContent) }