From 6dc85c849da8f1640f09fef5d591039555e5d69f Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Tue, 2 Jul 2019 18:48:42 +0200 Subject: [PATCH] Improve role membership management through REST API --- api/system/spec.json | 62 ++++++++++- api/system/spec/user.json | 62 ++++++++++- docs/system/README.md | 51 ++++++++- system/internal/repository/role.go | 7 ++ system/internal/service/role.go | 5 + system/rest/handlers/user.go | 85 +++++++++++++-- system/rest/request/user.go | 159 +++++++++++++++++++++++++++++ system/rest/user.go | 23 +++++ 8 files changed, 442 insertions(+), 12 deletions(-) diff --git a/api/system/spec.json b/api/system/spec.json index 34837f122..d0ca75158 100644 --- a/api/system/spec.json +++ b/api/system/spec.json @@ -820,7 +820,7 @@ { "name": "read", "method": "GET", - "title": "Read user details and memberships", + "title": "Read user details", "path": "/{userID}", "parameters": { "path": [ @@ -905,6 +905,66 @@ } ] } + }, + { + "name": "membershipList", + "method": "GET", + "title": "Add member to a role", + "path": "/{userID}/membership", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "userID", + "required": true, + "title": "User ID" + } + ] + } + }, + { + "name": "membershipAdd", + "method": "POST", + "title": "Add role to a user", + "path": "/{userID}/membership/{roleID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "roleID", + "required": true, + "title": "Role ID" + }, + { + "type": "uint64", + "name": "userID", + "required": true, + "title": "User ID" + } + ] + } + }, + { + "name": "membershipRemove", + "method": "DELETE", + "title": "Remove role from a user", + "path": "/{userID}/membership/{roleID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "roleID", + "required": true, + "title": "Role ID" + }, + { + "type": "uint64", + "name": "userID", + "required": true, + "title": "User ID" + } + ] + } } ] }, diff --git a/api/system/spec/user.json b/api/system/spec/user.json index 1cd05bff8..b8b604628 100644 --- a/api/system/spec/user.json +++ b/api/system/spec/user.json @@ -159,7 +159,7 @@ { "Name": "read", "Method": "GET", - "Title": "Read user details and memberships", + "Title": "Read user details", "Path": "/{userID}", "Parameters": { "path": [ @@ -244,6 +244,66 @@ } ] } + }, + { + "Name": "membershipList", + "Method": "GET", + "Title": "Add member to a role", + "Path": "/{userID}/membership", + "Parameters": { + "path": [ + { + "name": "userID", + "required": true, + "title": "User ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "membershipAdd", + "Method": "POST", + "Title": "Add role to a user", + "Path": "/{userID}/membership/{roleID}", + "Parameters": { + "path": [ + { + "name": "roleID", + "required": true, + "title": "Role ID", + "type": "uint64" + }, + { + "name": "userID", + "required": true, + "title": "User ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "membershipRemove", + "Method": "DELETE", + "Title": "Remove role from a user", + "Path": "/{userID}/membership/{roleID}", + "Parameters": { + "path": [ + { + "name": "roleID", + "required": true, + "title": "Role ID", + "type": "uint64" + }, + { + "name": "userID", + "required": true, + "title": "User ID", + "type": "uint64" + } + ] + } } ] } \ No newline at end of file diff --git a/docs/system/README.md b/docs/system/README.md index b9f697230..210b4d59a 100644 --- a/docs/system/README.md +++ b/docs/system/README.md @@ -730,11 +730,14 @@ An organisation may have many roles. Roles may have many channels available. Acc | `GET` | `/users/` | Search users (Directory) | | `POST` | `/users/` | Create user | | `PUT` | `/users/{userID}` | Update user details | -| `GET` | `/users/{userID}` | Read user details and memberships | +| `GET` | `/users/{userID}` | Read user details | | `DELETE` | `/users/{userID}` | Remove user | | `POST` | `/users/{userID}/suspend` | Suspend user | | `POST` | `/users/{userID}/unsuspend` | Unsuspend user | | `POST` | `/users/{userID}/password` | Set's or changes user's password | +| `GET` | `/users/{userID}/membership` | Add member to a role | +| `POST` | `/users/{userID}/membership/{roleID}` | Add role to a user | +| `DELETE` | `/users/{userID}/membership/{roleID}` | Remove role from a user | ## Search users (Directory) @@ -793,7 +796,7 @@ An organisation may have many roles. Roles may have many channels available. Acc | handle | string | POST | Handle | N/A | NO | | kind | types.UserKind | POST | Kind (normal, bot) | N/A | NO | -## Read user details and memberships +## Read user details #### Method @@ -864,4 +867,48 @@ An organisation may have many roles. Roles may have many channels available. Acc | userID | uint64 | PATH | User ID | N/A | YES | | password | string | POST | New password | N/A | YES | +## Add member to a role + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/users/{userID}/membership` | HTTP/S | GET | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| userID | uint64 | PATH | User ID | N/A | YES | + +## Add role to a user + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/users/{userID}/membership/{roleID}` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| roleID | uint64 | PATH | Role ID | N/A | YES | +| userID | uint64 | PATH | User ID | N/A | YES | + +## Remove role from a user + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/users/{userID}/membership/{roleID}` | HTTP/S | DELETE | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| roleID | uint64 | PATH | Role ID | N/A | YES | +| userID | uint64 | PATH | User ID | N/A | YES | + --- \ No newline at end of file diff --git a/system/internal/repository/role.go b/system/internal/repository/role.go index 562080711..93e2474a1 100644 --- a/system/internal/repository/role.go +++ b/system/internal/repository/role.go @@ -27,6 +27,7 @@ type ( MergeByID(id, targetRoleID uint64) error MoveByID(id, targetOrganisationID uint64) error + MembershipsFindByUserID(userID uint64) ([]*types.RoleMember, error) MemberFindByRoleID(roleID uint64) ([]*types.RoleMember, error) MemberAddByID(roleID, userID uint64) error MemberRemoveByID(roleID, userID uint64) error @@ -127,6 +128,12 @@ func (r *role) MoveByID(id, targetOrganisationID uint64) error { return ErrNotImplemented } +func (r *role) MembershipsFindByUserID(roleID uint64) (mm []*types.RoleMember, err error) { + rval := make([]*types.RoleMember, 0) + sql := "SELECT * FROM " + r.members + " WHERE rel_user = ?" + return rval, r.db().Select(&rval, sql, roleID) +} + func (r *role) MemberFindByRoleID(roleID uint64) (mm []*types.RoleMember, err error) { rval := make([]*types.RoleMember, 0) sql := "SELECT * FROM " + r.members + " WHERE rel_role = ?" diff --git a/system/internal/service/role.go b/system/internal/service/role.go index 534c9abd9..d637c1f7d 100644 --- a/system/internal/service/role.go +++ b/system/internal/service/role.go @@ -45,6 +45,7 @@ type ( Unarchive(ID uint64) error Delete(ID uint64) error + Membership(userID uint64) ([]*types.RoleMember, error) MemberList(roleID uint64) ([]*types.RoleMember, error) MemberAdd(roleID, userID uint64) error MemberRemove(roleID, userID uint64) error @@ -225,6 +226,10 @@ func (svc role) Move(roleID, targetOrganisationID uint64) error { return svc.role.MoveByID(roleID, targetOrganisationID) } +func (svc role) Membership(userID uint64) ([]*types.RoleMember, error) { + return svc.role.MembershipsFindByUserID(userID) +} + func (svc role) MemberList(roleID uint64) ([]*types.RoleMember, error) { _, err := svc.findByID(roleID) if err != nil { diff --git a/system/rest/handlers/user.go b/system/rest/handlers/user.go index 68ac67745..3c198c6b4 100644 --- a/system/rest/handlers/user.go +++ b/system/rest/handlers/user.go @@ -37,18 +37,24 @@ type UserAPI interface { Suspend(context.Context, *request.UserSuspend) (interface{}, error) Unsuspend(context.Context, *request.UserUnsuspend) (interface{}, error) SetPassword(context.Context, *request.UserSetPassword) (interface{}, error) + MembershipList(context.Context, *request.UserMembershipList) (interface{}, error) + MembershipAdd(context.Context, *request.UserMembershipAdd) (interface{}, error) + MembershipRemove(context.Context, *request.UserMembershipRemove) (interface{}, error) } // HTTP API interface type User struct { - List func(http.ResponseWriter, *http.Request) - Create func(http.ResponseWriter, *http.Request) - Update func(http.ResponseWriter, *http.Request) - Read func(http.ResponseWriter, *http.Request) - Delete func(http.ResponseWriter, *http.Request) - Suspend func(http.ResponseWriter, *http.Request) - Unsuspend func(http.ResponseWriter, *http.Request) - SetPassword func(http.ResponseWriter, *http.Request) + List func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Delete func(http.ResponseWriter, *http.Request) + Suspend func(http.ResponseWriter, *http.Request) + Unsuspend func(http.ResponseWriter, *http.Request) + SetPassword func(http.ResponseWriter, *http.Request) + MembershipList func(http.ResponseWriter, *http.Request) + MembershipAdd func(http.ResponseWriter, *http.Request) + MembershipRemove func(http.ResponseWriter, *http.Request) } func NewUser(h UserAPI) *User { @@ -213,6 +219,66 @@ func NewUser(h UserAPI) *User { resputil.JSON(w, value) } }, + MembershipList: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewUserMembershipList() + if err := params.Fill(r); err != nil { + logger.LogParamError("User.MembershipList", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.MembershipList(r.Context(), params) + if err != nil { + logger.LogControllerError("User.MembershipList", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("User.MembershipList", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + MembershipAdd: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewUserMembershipAdd() + if err := params.Fill(r); err != nil { + logger.LogParamError("User.MembershipAdd", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.MembershipAdd(r.Context(), params) + if err != nil { + logger.LogControllerError("User.MembershipAdd", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("User.MembershipAdd", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + MembershipRemove: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewUserMembershipRemove() + if err := params.Fill(r); err != nil { + logger.LogParamError("User.MembershipRemove", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.MembershipRemove(r.Context(), params) + if err != nil { + logger.LogControllerError("User.MembershipRemove", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("User.MembershipRemove", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, } } @@ -227,5 +293,8 @@ func (h User) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.H r.Post("/users/{userID}/suspend", h.Suspend) r.Post("/users/{userID}/unsuspend", h.Unsuspend) r.Post("/users/{userID}/password", h.SetPassword) + r.Get("/users/{userID}/membership", h.MembershipList) + r.Post("/users/{userID}/membership/{roleID}", h.MembershipAdd) + r.Delete("/users/{userID}/membership/{roleID}", h.MembershipRemove) }) } diff --git a/system/rest/request/user.go b/system/rest/request/user.go index 014d1668d..da2fe0c7e 100644 --- a/system/rest/request/user.go +++ b/system/rest/request/user.go @@ -523,3 +523,162 @@ func (r *UserSetPassword) Fill(req *http.Request) (err error) { } var _ RequestFiller = NewUserSetPassword() + +// User membershipList request parameters +type UserMembershipList struct { + UserID uint64 `json:",string"` +} + +func NewUserMembershipList() *UserMembershipList { + return &UserMembershipList{} +} + +func (r UserMembershipList) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["userID"] = r.UserID + + return out +} + +func (r *UserMembershipList) 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 errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.UserID = parseUInt64(chi.URLParam(req, "userID")) + + return err +} + +var _ RequestFiller = NewUserMembershipList() + +// User membershipAdd request parameters +type UserMembershipAdd struct { + RoleID uint64 `json:",string"` + UserID uint64 `json:",string"` +} + +func NewUserMembershipAdd() *UserMembershipAdd { + return &UserMembershipAdd{} +} + +func (r UserMembershipAdd) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["roleID"] = r.RoleID + out["userID"] = r.UserID + + return out +} + +func (r *UserMembershipAdd) 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 errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.RoleID = parseUInt64(chi.URLParam(req, "roleID")) + r.UserID = parseUInt64(chi.URLParam(req, "userID")) + + return err +} + +var _ RequestFiller = NewUserMembershipAdd() + +// User membershipRemove request parameters +type UserMembershipRemove struct { + RoleID uint64 `json:",string"` + UserID uint64 `json:",string"` +} + +func NewUserMembershipRemove() *UserMembershipRemove { + return &UserMembershipRemove{} +} + +func (r UserMembershipRemove) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["roleID"] = r.RoleID + out["userID"] = r.UserID + + return out +} + +func (r *UserMembershipRemove) 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 errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.RoleID = parseUInt64(chi.URLParam(req, "roleID")) + r.UserID = parseUInt64(chi.URLParam(req, "userID")) + + return err +} + +var _ RequestFiller = NewUserMembershipRemove() diff --git a/system/rest/user.go b/system/rest/user.go index 6ff0f99dd..36624abd9 100644 --- a/system/rest/user.go +++ b/system/rest/user.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" "github.com/titpetric/factory/resputil" + "github.com/cortezaproject/corteza-server/internal/payload" "github.com/cortezaproject/corteza-server/system/internal/service" "github.com/cortezaproject/corteza-server/system/rest/request" "github.com/cortezaproject/corteza-server/system/types" @@ -16,6 +17,7 @@ var _ = errors.Wrap type ( User struct { user service.UserService + role service.RoleService } userSetPayload struct { @@ -27,6 +29,7 @@ type ( func (User) New() *User { ctrl := &User{} ctrl.user = service.DefaultUser + ctrl.role = service.DefaultRole return ctrl } @@ -89,6 +92,26 @@ func (ctrl User) SetPassword(ctx context.Context, r *request.UserSetPassword) (i return resputil.OK(), ctrl.user.With(ctx).SetPassword(r.UserID, r.Password) } +func (ctrl User) MembershipList(ctx context.Context, r *request.UserMembershipList) (interface{}, error) { + if mm, err := ctrl.role.With(ctx).Membership(r.UserID); err != nil { + return nil, err + } else { + rval := make([]string, len(mm)) + for i := range mm { + rval[i] = payload.Uint64toa(mm[i].RoleID) + } + return rval, nil + } +} + +func (ctrl User) MembershipAdd(ctx context.Context, r *request.UserMembershipAdd) (interface{}, error) { + return resputil.OK(), ctrl.role.With(ctx).MemberAdd(r.RoleID, r.UserID) +} + +func (ctrl User) MembershipRemove(ctx context.Context, r *request.UserMembershipRemove) (interface{}, error) { + return resputil.OK(), ctrl.role.With(ctx).MemberRemove(r.RoleID, r.UserID) +} + func (ctrl User) makeFilterPayload(ctx context.Context, uu types.UserSet, f types.UserFilter, err error) (*userSetPayload, error) { if err != nil { return nil, err