Support patching for users
Allow meta data + email-confirmation flah to be patched/changed via API
This commit is contained in:
@@ -563,6 +563,16 @@ endpoints:
|
||||
name: labels
|
||||
title: Labels
|
||||
parser: label.ParseStrings
|
||||
- name: partialUpdate
|
||||
method: PATCH
|
||||
title: Patch user (experimental)
|
||||
path: "/{userID}"
|
||||
parameters:
|
||||
path:
|
||||
- type: uint64
|
||||
name: userID
|
||||
required: true
|
||||
title: User ID
|
||||
- name: read
|
||||
method: GET
|
||||
title: Read user details
|
||||
@@ -603,6 +613,7 @@ endpoints:
|
||||
name: userID
|
||||
required: true
|
||||
title: User ID
|
||||
|
||||
- name: undelete
|
||||
method: POST
|
||||
title: Undelete user
|
||||
@@ -629,6 +640,7 @@ endpoints:
|
||||
required: true
|
||||
sensitive: true
|
||||
title: New password
|
||||
|
||||
- name: membershipList
|
||||
method: GET
|
||||
title: Add member to a role
|
||||
|
||||
@@ -22,6 +22,7 @@ type (
|
||||
List(context.Context, *request.UserList) (interface{}, error)
|
||||
Create(context.Context, *request.UserCreate) (interface{}, error)
|
||||
Update(context.Context, *request.UserUpdate) (interface{}, error)
|
||||
PartialUpdate(context.Context, *request.UserPartialUpdate) (interface{}, error)
|
||||
Read(context.Context, *request.UserRead) (interface{}, error)
|
||||
Delete(context.Context, *request.UserDelete) (interface{}, error)
|
||||
Suspend(context.Context, *request.UserSuspend) (interface{}, error)
|
||||
@@ -39,6 +40,7 @@ type (
|
||||
List func(http.ResponseWriter, *http.Request)
|
||||
Create func(http.ResponseWriter, *http.Request)
|
||||
Update func(http.ResponseWriter, *http.Request)
|
||||
PartialUpdate func(http.ResponseWriter, *http.Request)
|
||||
Read func(http.ResponseWriter, *http.Request)
|
||||
Delete func(http.ResponseWriter, *http.Request)
|
||||
Suspend func(http.ResponseWriter, *http.Request)
|
||||
@@ -102,6 +104,22 @@ func NewUser(h UserAPI) *User {
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
PartialUpdate: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewUserPartialUpdate()
|
||||
if err := params.Fill(r); err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.PartialUpdate(r.Context(), params)
|
||||
if err != nil {
|
||||
api.Send(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.Send(w, r, value)
|
||||
},
|
||||
Read: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewUserRead()
|
||||
@@ -271,6 +289,7 @@ func (h User) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.H
|
||||
r.Get("/users/", h.List)
|
||||
r.Post("/users/", h.Create)
|
||||
r.Put("/users/{userID}", h.Update)
|
||||
r.Patch("/users/{userID}", h.PartialUpdate)
|
||||
r.Get("/users/{userID}", h.Read)
|
||||
r.Delete("/users/{userID}", h.Delete)
|
||||
r.Post("/users/{userID}/suspend", h.Suspend)
|
||||
|
||||
@@ -167,6 +167,13 @@ type (
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
UserPartialUpdate struct {
|
||||
// UserID PATH parameter
|
||||
//
|
||||
// User ID
|
||||
UserID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
UserRead struct {
|
||||
// UserID PATH parameter
|
||||
//
|
||||
@@ -714,6 +721,51 @@ func (r *UserUpdate) Fill(req *http.Request) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// NewUserPartialUpdate request
|
||||
func NewUserPartialUpdate() *UserPartialUpdate {
|
||||
return &UserPartialUpdate{}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r UserPartialUpdate) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"userID": r.UserID,
|
||||
}
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r UserPartialUpdate) GetUserID() uint64 {
|
||||
return r.UserID
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
func (r *UserPartialUpdate) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return fmt.Errorf("error parsing http request body: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var val string
|
||||
// path params
|
||||
|
||||
val = chi.URLParam(req, "userID")
|
||||
r.UserID, err = payload.ParseUint64(val), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NewUserRead request
|
||||
func NewUserRead() *UserRead {
|
||||
return &UserRead{}
|
||||
|
||||
+79
-3
@@ -2,7 +2,8 @@ package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza-server/pkg/api"
|
||||
"github.com/cortezaproject/corteza-server/pkg/corredor"
|
||||
"github.com/cortezaproject/corteza-server/pkg/filter"
|
||||
@@ -12,6 +13,8 @@ import (
|
||||
"github.com/cortezaproject/corteza-server/system/service/event"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cast"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var _ = errors.Wrap
|
||||
@@ -38,6 +41,7 @@ func (User) New() *User {
|
||||
func (ctrl User) List(ctx context.Context, r *request.UserList) (interface{}, error) {
|
||||
var (
|
||||
err error
|
||||
set types.UserSet
|
||||
f = types.UserFilter{
|
||||
UserID: payload.ParseUint64s(r.UserID),
|
||||
RoleID: payload.ParseUint64s(r.RoleID),
|
||||
@@ -68,8 +72,8 @@ func (ctrl User) List(ctx context.Context, r *request.UserList) (interface{}, er
|
||||
f.Deleted = filter.StateInclusive
|
||||
}
|
||||
|
||||
set, filter, err := ctrl.user.Find(ctx, f)
|
||||
return ctrl.makeFilterPayload(ctx, set, filter, err)
|
||||
set, f, err = ctrl.user.Find(ctx, f)
|
||||
return ctrl.makeFilterPayload(ctx, set, f, err)
|
||||
}
|
||||
|
||||
func (ctrl User) Create(ctx context.Context, r *request.UserCreate) (interface{}, error) {
|
||||
@@ -97,6 +101,78 @@ func (ctrl User) Update(ctx context.Context, r *request.UserUpdate) (interface{}
|
||||
return ctrl.user.Update(ctx, user)
|
||||
}
|
||||
|
||||
type (
|
||||
patchOp struct {
|
||||
Operation string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
Value json.RawMessage `json:"value"`
|
||||
}
|
||||
)
|
||||
|
||||
// PartialUpdate
|
||||
//
|
||||
// experimental resource management with partial updates (patching) using
|
||||
// JavaScript Object Notation (JSON) Patch standard (RFC 6902)
|
||||
//
|
||||
// If this proves useful, we'll use it on other resources & fields
|
||||
func (ctrl User) PartialUpdate(ctx context.Context, r *request.UserPartialUpdate) (interface{}, error) {
|
||||
u, err := ctrl.user.FindByID(ctx, r.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
err = func() (err error) {
|
||||
var (
|
||||
ops = make([]*patchOp, 0)
|
||||
)
|
||||
|
||||
if err = json.NewDecoder(r.Body).Decode(&ops); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range ops {
|
||||
if p.Operation != "replace" {
|
||||
return fmt.Errorf("unsupported operation '%s'", p.Operation)
|
||||
}
|
||||
|
||||
var aux interface{}
|
||||
err = json.Unmarshal(p.Value, &aux)
|
||||
|
||||
switch p.Path {
|
||||
case "/meta/securityPolicy/mfa/enforcedEmailOTP":
|
||||
u.Meta.SecurityPolicy.MFA.EnforcedEmailOTP, err = cast.ToBoolE(aux)
|
||||
|
||||
case "/meta/securityPolicy/mfa/enforcedTOTP":
|
||||
u.Meta.SecurityPolicy.MFA.EnforcedTOTP, err = cast.ToBoolE(aux)
|
||||
|
||||
case "/emailConfirmed":
|
||||
// unfortunately, this can not be passed to update right now
|
||||
// internal limitations
|
||||
u.EmailConfirmed, err = cast.ToBoolE(aux)
|
||||
err = ctrl.user.ToggleEmailConfirmation(ctx, u.ID, u.EmailConfirmed)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown path: %s", p.Path)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not replace falue of %s: %w", p.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
u, err = ctrl.user.Update(ctx, u)
|
||||
return err
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
api.Send(w, r, err)
|
||||
}
|
||||
|
||||
api.Send(w, r, u)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ctrl User) Read(ctx context.Context, r *request.UserRead) (interface{}, error) {
|
||||
return ctrl.user.FindByID(ctx, r.UserID)
|
||||
}
|
||||
|
||||
@@ -1351,7 +1351,7 @@ func AuthErrInvalidTOTP(mm ...*authActionProps) *errors.Error {
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("invalid TOTP", nil),
|
||||
p.Format("invalid code", nil),
|
||||
|
||||
errors.Meta("type", "invalidTOTP"),
|
||||
errors.Meta("resource", "system:auth"),
|
||||
@@ -1471,7 +1471,7 @@ func AuthErrInvalidEmailOTP(mm ...*authActionProps) *errors.Error {
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("invalid email OTP", nil),
|
||||
p.Format("invalid code", nil),
|
||||
|
||||
errors.Meta("type", "invalidEmailOTP"),
|
||||
errors.Meta("resource", "system:auth"),
|
||||
|
||||
+37
-2
@@ -69,6 +69,7 @@ type (
|
||||
|
||||
Create(ctx context.Context, input *types.User) (*types.User, error)
|
||||
Update(ctx context.Context, mod *types.User) (*types.User, error)
|
||||
ToggleEmailConfirmation(ctx context.Context, userID uint64, confirm bool) error
|
||||
|
||||
CreateWithAvatar(ctx context.Context, input *types.User, avatar io.Reader) (*types.User, error)
|
||||
UpdateWithAvatar(ctx context.Context, mod *types.User, avatar io.Reader) (*types.User, error)
|
||||
@@ -85,7 +86,7 @@ type (
|
||||
)
|
||||
|
||||
func User(ctx context.Context) UserService {
|
||||
return (&user{
|
||||
return &user{
|
||||
eventbus: eventbus.Service(),
|
||||
ac: DefaultAccessControl,
|
||||
settings: CurrentSettings,
|
||||
@@ -94,7 +95,7 @@ func User(ctx context.Context) UserService {
|
||||
store: DefaultStore,
|
||||
|
||||
actionlog: DefaultActionlog,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (svc user) FindByID(ctx context.Context, userID uint64) (u *types.User, err error) {
|
||||
@@ -428,6 +429,7 @@ func (svc user) Update(ctx context.Context, upd *types.User) (u *types.User, err
|
||||
u.Name = upd.Name
|
||||
u.Handle = upd.Handle
|
||||
u.Kind = upd.Kind
|
||||
u.Meta = upd.Meta
|
||||
u.UpdatedAt = now()
|
||||
|
||||
if err = svc.eventbus.WaitFor(ctx, event.UserBeforeUpdate(upd, u)); err != nil {
|
||||
@@ -457,6 +459,39 @@ func (svc user) Update(ctx context.Context, upd *types.User) (u *types.User, err
|
||||
return u, svc.recordAction(ctx, uaProps, UserActionUpdate, err)
|
||||
}
|
||||
|
||||
func (svc user) ToggleEmailConfirmation(ctx context.Context, userID uint64, confirmed bool) (err error) {
|
||||
var (
|
||||
u *types.User
|
||||
uaProps = &userActionProps{}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
if userID == 0 {
|
||||
return UserErrInvalidID()
|
||||
}
|
||||
if u, err = store.LookupUserByID(ctx, svc.store, userID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uaProps.setUser(u)
|
||||
|
||||
if userID != internalAuth.GetIdentityFromContext(ctx).Identity() {
|
||||
if !svc.ac.CanUpdateUser(ctx, u) {
|
||||
return UserErrNotAllowedToUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
u.EmailConfirmed = confirmed
|
||||
|
||||
if err = store.UpdateUser(ctx, svc.store, u); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}()
|
||||
|
||||
return svc.recordAction(ctx, uaProps, UserActionUpdate, err)
|
||||
}
|
||||
|
||||
func (svc user) UpdateWithAvatar(ctx context.Context, mod *types.User, avatar io.Reader) (out *types.User, err error) {
|
||||
// @todo: avatar
|
||||
return svc.Create(ctx, mod)
|
||||
|
||||
@@ -24,7 +24,7 @@ type (
|
||||
|
||||
Meta *UserMeta `json:"meta"`
|
||||
|
||||
EmailConfirmed bool `json:"-"`
|
||||
EmailConfirmed bool `json:"emailConfirmed"`
|
||||
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user