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 } // handleGetUser 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, } }