influxdb/http/user_service.go

731 lines
16 KiB
Go

package http
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"path"
"strconv"
"github.com/influxdata/platform"
platcontext "github.com/influxdata/platform/context"
kerrors "github.com/influxdata/platform/kit/errors"
"github.com/julienschmidt/httprouter"
)
// UserHandler represents an HTTP API handler for users.
type UserHandler struct {
*httprouter.Router
UserService platform.UserService
UserOperationLogService platform.UserOperationLogService
BasicAuthService platform.BasicAuthService
}
const (
usersPath = "/api/v2/users"
mePath = "/api/v2/me"
mePasswordPath = "/api/v2/me/password"
usersIDPath = "/api/v2/users/:id"
usersPasswordPath = "/api/v2/users/:id/password"
usersLogPath = "/api/v2/users/:id/log"
)
// NewUserHandler returns a new instance of UserHandler.
func NewUserHandler() *UserHandler {
h := &UserHandler{
Router: NewRouter(),
}
h.HandlerFunc("POST", usersPath, h.handlePostUser)
h.HandlerFunc("GET", usersPath, h.handleGetUsers)
h.HandlerFunc("GET", usersIDPath, h.handleGetUser)
h.HandlerFunc("GET", usersLogPath, h.handleGetUserLog)
h.HandlerFunc("PATCH", usersIDPath, h.handlePatchUser)
h.HandlerFunc("DELETE", usersIDPath, h.handleDeleteUser)
h.HandlerFunc("PUT", usersPasswordPath, h.handlePutUserPassword)
h.HandlerFunc("GET", mePath, h.handleGetMe)
h.HandlerFunc("PUT", mePasswordPath, h.handlePutUserPassword)
return h
}
func (h *UserHandler) putPassword(ctx context.Context, w http.ResponseWriter, r *http.Request) (username string, err error) {
req, err := decodePasswordResetRequest(ctx, r)
if err != nil {
return "", err
}
err = h.BasicAuthService.CompareAndSetPassword(ctx, req.Username, req.PasswordOld, req.PasswordNew)
if err != nil {
return "", err
}
return req.Username, nil
}
// 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()
username, err := h.putPassword(ctx, w, r)
if err != nil {
EncodeError(ctx, err, w)
return
}
filter := platform.UserFilter{
Name: &username,
}
b, err := h.UserService.FindUser(ctx, filter)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, newUserResponse(b)); err != nil {
EncodeError(ctx, err, w)
return
}
}
type passwordResetRequest struct {
Username string
PasswordOld string
PasswordNew string
}
type passwordResetRequestBody struct {
Password string `json:"password"`
}
func decodePasswordResetRequest(ctx context.Context, 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, &platform.Error{
Code: platform.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 {
EncodeError(ctx, err, w)
return
}
if err := h.UserService.CreateUser(ctx, req.User); err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusCreated, newUserResponse(req.User)); err != nil {
EncodeError(ctx, err, w)
return
}
}
type postUserRequest struct {
User *platform.User
}
func decodePostUserRequest(ctx context.Context, r *http.Request) (*postUserRequest, error) {
b := &platform.User{}
if err := json.NewDecoder(r.Body).Decode(b); err != nil {
return nil, err
}
return &postUserRequest{
User: b,
}, nil
}
// handleGetMe is the HTTP handler for the GET /api/v2/me.
func (h *UserHandler) handleGetMe(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
a, err := platcontext.GetAuthorizer(ctx)
if err != nil {
EncodeError(ctx, err, w)
return
}
var id platform.ID
switch s := a.(type) {
case *platform.Session:
id = s.UserID
case *platform.Authorization:
id = s.UserID
}
b, err := h.UserService.FindUserByID(ctx, id)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, newUserResponse(b)); err != nil {
EncodeError(ctx, err, w)
return
}
}
// 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 {
EncodeError(ctx, err, w)
return
}
b, err := h.UserService.FindUserByID(ctx, req.UserID)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, newUserResponse(b)); err != nil {
EncodeError(ctx, err, w)
return
}
}
type getUserRequest struct {
UserID platform.ID
}
func decodeGetUserRequest(ctx context.Context, r *http.Request) (*getUserRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, kerrors.InvalidDataf("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 {
EncodeError(ctx, err, w)
return
}
if err := h.UserService.DeleteUser(ctx, req.UserID); err != nil {
EncodeError(ctx, err, w)
return
}
w.WriteHeader(http.StatusNoContent)
}
type deleteUserRequest struct {
UserID platform.ID
}
func decodeDeleteUserRequest(ctx context.Context, r *http.Request) (*deleteUserRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, kerrors.InvalidDataf("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 []*userResponse `json:"users"`
}
func (us usersResponse) ToPlatform() []*platform.User {
users := make([]*platform.User, len(us.Users))
for i := range us.Users {
users[i] = &us.Users[i].User
}
return users
}
func newUsersResponse(users []*platform.User) *usersResponse {
res := usersResponse{
Links: map[string]string{
"self": "/api/v2/users",
},
Users: []*userResponse{},
}
for _, user := range users {
res.Users = append(res.Users, newUserResponse(user))
}
return &res
}
type userResponse struct {
Links map[string]string `json:"links"`
platform.User
}
func newUserResponse(u *platform.User) *userResponse {
return &userResponse{
Links: map[string]string{
"self": fmt.Sprintf("/api/v2/users/%s", u.ID),
"log": fmt.Sprintf("/api/v2/users/%s/log", 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 {
EncodeError(ctx, err, w)
return
}
users, _, err := h.UserService.FindUsers(ctx, req.filter)
if err != nil {
EncodeError(ctx, err, w)
return
}
err = encodeResponse(ctx, w, http.StatusOK, newUsersResponse(users))
if err != nil {
EncodeError(ctx, err, w)
return
}
}
type getUsersRequest struct {
filter platform.UserFilter
}
func decodeGetUsersRequest(ctx context.Context, r *http.Request) (*getUsersRequest, error) {
qp := r.URL.Query()
req := &getUsersRequest{}
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 {
EncodeError(ctx, err, w)
return
}
b, err := h.UserService.UpdateUser(ctx, req.UserID, req.Update)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, newUserResponse(b)); err != nil {
EncodeError(ctx, err, w)
return
}
}
type patchUserRequest struct {
Update platform.UserUpdate
UserID platform.ID
}
func decodePatchUserRequest(ctx context.Context, r *http.Request) (*patchUserRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, kerrors.InvalidDataf("url missing id")
}
var i platform.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
var upd platform.UserUpdate
if err := json.NewDecoder(r.Body).Decode(&upd); err != nil {
return nil, err
}
return &patchUserRequest{
Update: upd,
UserID: i,
}, nil
}
// UserService connects to Influx via HTTP using tokens to manage users
type UserService struct {
Addr string
Token string
InsecureSkipVerify bool
// OpPrefix is the ops of not found error.
OpPrefix string
}
// FindMe returns user information about the owner of the token
func (s *UserService) FindMe(ctx context.Context, id platform.ID) (*platform.User, error) {
url, err := newURL(s.Addr, mePath)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return nil, err
}
SetToken(s.Token, req)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := CheckError(resp, true); err != nil {
return nil, err
}
var res userResponse
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, err
}
return &res.User, nil
}
// FindUserByID returns a single user by ID.
func (s *UserService) FindUserByID(ctx context.Context, id platform.ID) (*platform.User, error) {
url, err := newURL(s.Addr, userIDPath(id))
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return nil, err
}
SetToken(s.Token, req)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
if err := CheckError(resp, true); err != nil {
return nil, err
}
var res userResponse
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, err
}
defer resp.Body.Close()
return &res.User, nil
}
// FindUser returns the first user that matches filter.
func (s *UserService) FindUser(ctx context.Context, filter platform.UserFilter) (*platform.User, error) {
users, n, err := s.FindUsers(ctx, filter)
if err != nil {
return nil, &platform.Error{
Op: s.OpPrefix + platform.OpFindUser,
Err: err,
}
}
if n == 0 {
return nil, &platform.Error{
Code: platform.ENotFound,
Op: s.OpPrefix + platform.OpFindUser,
Msg: "no results found",
}
}
return users[0], nil
}
// FindUsers returns a list of users that match filter and the total count of matching users.
// Additional options provide pagination & sorting.
func (s *UserService) FindUsers(ctx context.Context, filter platform.UserFilter, opt ...platform.FindOptions) ([]*platform.User, int, error) {
url, err := newURL(s.Addr, usersPath)
if err != nil {
return nil, 0, err
}
query := url.Query()
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return nil, 0, err
}
if filter.ID != nil {
query.Add("id", filter.ID.String())
}
if filter.Name != nil {
query.Add("name", *filter.Name)
}
req.URL.RawQuery = query.Encode()
SetToken(s.Token, req)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, 0, err
}
if err := CheckError(resp, true); err != nil {
return nil, 0, err
}
var r usersResponse
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return nil, 0, err
}
us := r.ToPlatform()
return us, len(us), nil
}
// CreateUser creates a new user and sets u.ID with the new identifier.
func (s *UserService) CreateUser(ctx context.Context, u *platform.User) error {
url, err := newURL(s.Addr, usersPath)
if err != nil {
return err
}
octets, err := json.Marshal(u)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url.String(), bytes.NewReader(octets))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
SetToken(s.Token, req)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
// TODO(jsternberg): Should this check for a 201 explicitly?
if err := CheckError(resp, true); err != nil {
return err
}
if err := json.NewDecoder(resp.Body).Decode(u); err != nil {
return err
}
return nil
}
// UpdateUser updates a single user with changeset.
// Returns the new user state after update.
func (s *UserService) UpdateUser(ctx context.Context, id platform.ID, upd platform.UserUpdate) (*platform.User, error) {
url, err := newURL(s.Addr, userIDPath(id))
if err != nil {
return nil, err
}
octets, err := json.Marshal(upd)
if err != nil {
return nil, err
}
req, err := http.NewRequest("PATCH", url.String(), bytes.NewReader(octets))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
SetToken(s.Token, req)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
if err := CheckError(resp, true); err != nil {
return nil, err
}
var res userResponse
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, err
}
defer resp.Body.Close()
return &res.User, nil
}
// DeleteUser removes a user by ID.
func (s *UserService) DeleteUser(ctx context.Context, id platform.ID) error {
url, err := newURL(s.Addr, userIDPath(id))
if err != nil {
return err
}
req, err := http.NewRequest("DELETE", url.String(), nil)
if err != nil {
return err
}
SetToken(s.Token, req)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
return CheckErrorStatus(http.StatusNoContent, resp, true)
}
func userIDPath(id platform.ID) string {
return path.Join(usersPath, id.String())
}
// hanldeGetUserLog retrieves a user log by the users ID.
func (h *UserHandler) handleGetUserLog(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetUserLogRequest(ctx, r)
if err != nil {
EncodeError(ctx, err, w)
return
}
log, _, err := h.UserOperationLogService.GetUserOperationLog(ctx, req.UserID, req.opts)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, newUserLogResponse(req.UserID, log)); err != nil {
EncodeError(ctx, err, w)
return
}
}
type getUserLogRequest struct {
UserID platform.ID
opts platform.FindOptions
}
func decodeGetUserLogRequest(ctx context.Context, r *http.Request) (*getUserLogRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, kerrors.InvalidDataf("url missing id")
}
var i platform.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
opts := platform.DefaultOperationLogFindOptions
qp := r.URL.Query()
if v := qp.Get("desc"); v == "false" {
opts.Descending = false
}
if v := qp.Get("limit"); v != "" {
i, err := strconv.Atoi(v)
if err != nil {
return nil, err
}
opts.Limit = i
}
if v := qp.Get("offset"); v != "" {
i, err := strconv.Atoi(v)
if err != nil {
return nil, err
}
opts.Offset = i
}
return &getUserLogRequest{
UserID: i,
opts: opts,
}, nil
}
func newUserLogResponse(id platform.ID, es []*platform.OperationLogEntry) *operationLogResponse {
log := make([]*operationLogEntryResponse, 0, len(es))
for _, e := range es {
log = append(log, newOperationLogEntryResponse(e))
}
return &operationLogResponse{
Links: map[string]string{
"self": fmt.Sprintf("/api/v2/users/%s/log", id),
},
Log: log,
}
}