influxdb/tenant/http_handler_urm.go

280 lines
6.3 KiB
Go

package tenant
import (
"context"
"encoding/json"
"fmt"
"net/http"
"path"
"github.com/go-chi/chi"
"github.com/influxdata/influxdb/v2"
"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"
)
type urmHandler struct {
log *zap.Logger
svc influxdb.UserResourceMappingService
userSvc influxdb.UserService
api *kithttp.API
rt influxdb.ResourceType
idLookupKey string
}
// NewURMHandler generates a mountable handler for URMs. It needs to know how it will be looking up your resource id
// this system assumes you are using chi syntax for query string params `/orgs/{id}/` so it can use chi.URLParam().
func NewURMHandler(log *zap.Logger, rt influxdb.ResourceType, idLookupKey string, uSvc influxdb.UserService, urmSvc influxdb.UserResourceMappingService) http.Handler {
h := &urmHandler{
log: log,
svc: urmSvc,
userSvc: uSvc,
api: kithttp.NewAPI(kithttp.WithLog(log)),
rt: rt,
idLookupKey: idLookupKey,
}
r := chi.NewRouter()
r.Get("/", h.getURMsByType)
r.Post("/", h.postURMByType)
r.Delete("/{userID}", h.deleteURM)
return r
}
func (h *urmHandler) getURMsByType(w http.ResponseWriter, r *http.Request) {
userType := userTypeFromPath(r.URL.Path)
ctx := r.Context()
req, err := h.decodeGetRequest(ctx, r)
if err != nil {
h.api.Err(w, r, err)
return
}
filter := influxdb.UserResourceMappingFilter{
ResourceID: req.ResourceID,
ResourceType: h.rt,
UserType: userType,
}
mappings, _, err := h.svc.FindUserResourceMappings(ctx, filter)
if err != nil {
h.api.Err(w, r, err)
return
}
users := make([]*influxdb.User, 0, len(mappings))
for _, m := range mappings {
if m.MappingType == influxdb.OrgMappingType {
continue
}
user, err := h.userSvc.FindUserByID(ctx, m.UserID)
if err != nil {
h.api.Err(w, r, err)
return
}
users = append(users, user)
}
h.log.Debug("Members/owners retrieved", zap.String("users", fmt.Sprint(users)))
h.api.Respond(w, r, http.StatusOK, newResourceUsersResponse(filter, users))
}
type getRequest struct {
ResourceID platform.ID
}
func (h *urmHandler) decodeGetRequest(ctx context.Context, r *http.Request) (*getRequest, error) {
id := chi.URLParam(r, h.idLookupKey)
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 := &getRequest{
ResourceID: i,
}
return req, nil
}
func (h *urmHandler) postURMByType(w http.ResponseWriter, r *http.Request) {
userType := userTypeFromPath(r.URL.Path)
ctx := r.Context()
req, err := h.decodePostRequest(ctx, r)
if err != nil {
h.api.Err(w, r, err)
return
}
user, err := h.userSvc.FindUserByID(ctx, req.UserID)
if err != nil {
h.api.Err(w, r, err)
return
}
mapping := &influxdb.UserResourceMapping{
ResourceID: req.ResourceID,
ResourceType: h.rt,
UserID: req.UserID,
UserType: userType,
}
if err := h.svc.CreateUserResourceMapping(ctx, mapping); err != nil {
h.api.Err(w, r, err)
return
}
h.log.Debug("Member/owner created", zap.String("mapping", fmt.Sprint(mapping)))
h.api.Respond(w, r, http.StatusCreated, newResourceUserResponse(user, userType))
}
type postRequest struct {
UserID platform.ID
ResourceID platform.ID
}
func (h urmHandler) decodePostRequest(ctx context.Context, r *http.Request) (*postRequest, error) {
id := chi.URLParam(r, h.idLookupKey)
if id == "" {
return nil, &errors.Error{
Code: errors.EInvalid,
Msg: "url missing id",
}
}
var rid platform.ID
if err := rid.DecodeFromString(id); err != nil {
return nil, err
}
u := &influxdb.User{}
if err := json.NewDecoder(r.Body).Decode(u); err != nil {
return nil, err
}
if !u.ID.Valid() {
return nil, &errors.Error{
Code: errors.EInvalid,
Msg: "user id missing or invalid",
}
}
return &postRequest{
UserID: u.ID,
ResourceID: rid,
}, nil
}
func (h *urmHandler) deleteURM(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := h.decodeDeleteRequest(ctx, r)
if err != nil {
h.api.Err(w, r, err)
return
}
if err := h.svc.DeleteUserResourceMapping(ctx, req.resourceID, req.userID); err != nil {
h.api.Err(w, r, err)
return
}
h.log.Debug("Member deleted", zap.String("resourceID", req.resourceID.String()), zap.String("memberID", req.userID.String()))
w.WriteHeader(http.StatusNoContent)
}
type deleteRequest struct {
userID platform.ID
resourceID platform.ID
}
func (h *urmHandler) decodeDeleteRequest(ctx context.Context, r *http.Request) (*deleteRequest, error) {
id := chi.URLParam(r, h.idLookupKey)
if id == "" {
return nil, &errors.Error{
Code: errors.EInvalid,
Msg: "url missing id",
}
}
var rid platform.ID
if err := rid.DecodeFromString(id); err != nil {
return nil, err
}
id = chi.URLParam(r, "userID")
if id == "" {
return nil, &errors.Error{
Code: errors.EInvalid,
Msg: "url missing member id",
}
}
var uid platform.ID
if err := uid.DecodeFromString(id); err != nil {
return nil, err
}
return &deleteRequest{
userID: uid,
resourceID: rid,
}, nil
}
type resourceUserResponse struct {
Role influxdb.UserType `json:"role"`
*influxdb.UserResponse
}
func newResourceUserResponse(u *influxdb.User, userType influxdb.UserType) *resourceUserResponse {
return &resourceUserResponse{
Role: userType,
UserResponse: newUserResponse(u),
}
}
type resourceUsersResponse struct {
Links map[string]string `json:"links"`
Users []*resourceUserResponse `json:"users"`
}
func newResourceUsersResponse(f influxdb.UserResourceMappingFilter, users []*influxdb.User) *resourceUsersResponse {
rs := resourceUsersResponse{
Links: map[string]string{
"self": fmt.Sprintf("/api/v2/%s/%s/%ss", f.ResourceType, f.ResourceID, f.UserType),
},
Users: make([]*resourceUserResponse, 0, len(users)),
}
for _, user := range users {
rs.Users = append(rs.Users, newResourceUserResponse(user, f.UserType))
}
return &rs
}
// determine the type of request from the path.
func userTypeFromPath(p string) influxdb.UserType {
if p == "" {
return influxdb.Member
}
switch path.Base(p) {
case "members":
return influxdb.Member
case "owners":
return influxdb.Owner
default:
return userTypeFromPath(path.Dir(p))
}
}