538 lines
12 KiB
Go
538 lines
12 KiB
Go
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)
|
|
}
|