3
0

Support patching for users

Allow meta data + email-confirmation flah to be patched/changed via API
This commit is contained in:
Denis Arh
2021-03-08 19:22:44 +01:00
parent 71b96ef658
commit d25117ab62
7 changed files with 202 additions and 8 deletions
+12
View File
@@ -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
+19
View File
@@ -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)
+52
View File
@@ -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
View File
@@ -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)
}
+2 -2
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -24,7 +24,7 @@ type (
Meta *UserMeta `json:"meta"`
EmailConfirmed bool `json:"-"`
EmailConfirmed bool `json:"emailConfirmed"`
Labels map[string]string `json:"labels,omitempty"`