331 lines
7.1 KiB
Go
331 lines
7.1 KiB
Go
package scim
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
|
|
"github.com/cortezaproject/corteza-server/pkg/errors"
|
|
"github.com/cortezaproject/corteza-server/store"
|
|
"github.com/cortezaproject/corteza-server/system/service"
|
|
"github.com/cortezaproject/corteza-server/system/types"
|
|
"github.com/go-chi/chi"
|
|
)
|
|
|
|
type (
|
|
groupsHandler struct {
|
|
externalIdAsPrimary bool
|
|
externalIdValidator *regexp.Regexp
|
|
|
|
svc service.RoleService
|
|
userSvc service.UserService
|
|
sec getSecurityContextFn
|
|
}
|
|
)
|
|
|
|
func (h groupsHandler) get(w http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
res = h.lookup(h.sec(r), chi.URLParam(r, "id"), w)
|
|
)
|
|
|
|
if res == nil {
|
|
return
|
|
}
|
|
|
|
send(w, http.StatusOK, newGroupResourceResponse(res))
|
|
}
|
|
|
|
func (h groupsHandler) create(w http.ResponseWriter, r *http.Request) {
|
|
defer r.Body.Close()
|
|
|
|
var (
|
|
ctx = h.sec(r)
|
|
svc = h.svc
|
|
payload = &groupResourceRequest{}
|
|
err error
|
|
existing *types.Role
|
|
)
|
|
|
|
if err = payload.decodeJSON(r.Body); err != nil {
|
|
sendError(w, newErrorResponse(http.StatusBadRequest, err))
|
|
return
|
|
}
|
|
|
|
{
|
|
// do we need to upsert?
|
|
if payload.ExternalId != nil {
|
|
existing, err = h.lookupByExternalId(ctx, *payload.ExternalId)
|
|
if err != nil {
|
|
sendError(w, err)
|
|
return
|
|
}
|
|
} else if *payload.Name != "" {
|
|
existing, err = svc.FindByName(ctx, *payload.Name)
|
|
if err != nil && !errors.Is(err, service.RoleErrNotFound()) {
|
|
sendError(w, err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
res, err := h.save(ctx, payload, existing)
|
|
if err != nil {
|
|
sendError(w, err)
|
|
return
|
|
}
|
|
|
|
status := http.StatusOK
|
|
if res.UpdatedAt == nil {
|
|
status = http.StatusCreated
|
|
}
|
|
|
|
send(w, status, newGroupResourceResponse(res))
|
|
}
|
|
|
|
func (h groupsHandler) replace(w http.ResponseWriter, r *http.Request) {
|
|
defer r.Body.Close()
|
|
|
|
var (
|
|
ctx = h.sec(r)
|
|
existing = h.lookup(ctx, chi.URLParam(r, "id"), w)
|
|
payload = &groupResourceRequest{}
|
|
)
|
|
|
|
if err := payload.decodeJSON(r.Body); err != nil {
|
|
sendError(w, newErrorResponse(http.StatusBadRequest, err))
|
|
return
|
|
}
|
|
|
|
res, err := h.save(ctx, payload, existing)
|
|
if err != nil {
|
|
sendError(w, err)
|
|
return
|
|
}
|
|
|
|
status := http.StatusOK
|
|
if res.UpdatedAt == nil {
|
|
status = http.StatusCreated
|
|
}
|
|
|
|
send(w, status, newGroupResourceResponse(res))
|
|
}
|
|
|
|
// patches group
|
|
//
|
|
// only supports adding and removing members
|
|
func (h groupsHandler) patch(w http.ResponseWriter, r *http.Request) {
|
|
defer r.Body.Close()
|
|
|
|
var (
|
|
ctx = h.sec(r)
|
|
svc = h.svc
|
|
res = h.lookup(ctx, chi.URLParam(r, "id"), w)
|
|
payload = &operationsRequest{}
|
|
)
|
|
|
|
if res == nil {
|
|
return
|
|
}
|
|
|
|
if err := payload.decodeJSON(r.Body); err != nil {
|
|
sendError(w, newErrorResponse(http.StatusBadRequest, err))
|
|
return
|
|
}
|
|
|
|
var (
|
|
u *types.User
|
|
ops = make([]func() error, 0, len(payload.Operations))
|
|
err error
|
|
memberships = make(map[uint64]bool)
|
|
)
|
|
|
|
{
|
|
// collect all existing memberships into a simple map
|
|
// to ensure we dont step on our feet (too much)
|
|
//
|
|
// this is not 100% bulletproof for concurrent modifications
|
|
mm, _, err := store.SearchRoleMembers(ctx, service.DefaultStore, types.RoleMemberFilter{RoleID: res.ID})
|
|
if err != nil {
|
|
sendError(w, err)
|
|
return
|
|
}
|
|
|
|
for _, m := range mm {
|
|
memberships[m.UserID] = true
|
|
}
|
|
}
|
|
|
|
// validate and collect operations
|
|
for _, op := range payload.Operations {
|
|
if op.Path != "members" {
|
|
// allow only "members" path
|
|
sendError(w, newErrorfResponse(http.StatusBadRequest, "unsupported path: %q", op.Path))
|
|
return
|
|
}
|
|
|
|
// iterate through operation's values, load user and schedule op
|
|
for _, userExternalId := range op.Value {
|
|
u, err = lookupUserByExternalId(ctx, h.userSvc, h.externalIdValidator, userExternalId.Value)
|
|
if err != nil {
|
|
sendError(w, err)
|
|
return
|
|
}
|
|
|
|
if u == nil {
|
|
sendError(w, newErrorfResponse(http.StatusBadRequest, "no such user: %q", userExternalId.Value))
|
|
return
|
|
}
|
|
|
|
// making sure u is not overwritten
|
|
// in the next iteration
|
|
memberId := u.ID
|
|
|
|
switch op.Operation {
|
|
case patchOpAdd:
|
|
// support for add operation,
|
|
// check if there members already exist
|
|
ops = append(ops, func() error {
|
|
if memberships[memberId] {
|
|
// already added
|
|
return nil
|
|
}
|
|
|
|
memberships[memberId] = true
|
|
return svc.MemberAdd(ctx, res.ID, memberId)
|
|
})
|
|
|
|
case patchOpRemove:
|
|
// support for remove operation,
|
|
// check if there members are missing
|
|
ops = append(ops, func() error {
|
|
if !memberships[memberId] {
|
|
// already removed
|
|
return nil
|
|
}
|
|
|
|
delete(memberships, memberId)
|
|
return svc.MemberRemove(ctx, res.ID, memberId)
|
|
})
|
|
|
|
default:
|
|
sendError(w, newErrorfResponse(http.StatusBadRequest, "unsupported operation: %q", op.Operation))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// run all scheduled ops
|
|
for _, op := range ops {
|
|
if err = op(); err != nil {
|
|
sendError(w, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
send(w, http.StatusNoContent, nil)
|
|
}
|
|
|
|
func (h groupsHandler) save(ctx context.Context, req *groupResourceRequest, existing *types.Role) (res *types.Role, err error) {
|
|
var (
|
|
svc = h.svc
|
|
)
|
|
|
|
if existing == nil {
|
|
// in case when we did not find a valid group,
|
|
// start from blank
|
|
existing = &types.Role{}
|
|
}
|
|
|
|
res = existing
|
|
req.applyTo(res)
|
|
|
|
if res.ID > 0 {
|
|
res, err = svc.Update(ctx, res)
|
|
} else {
|
|
res, err = svc.Create(ctx, res)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, newErrorResponse(http.StatusInternalServerError, err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (h groupsHandler) delete(w http.ResponseWriter, r *http.Request) {
|
|
var (
|
|
ctx = h.sec(r)
|
|
svc = h.svc
|
|
res = h.lookup(ctx, chi.URLParam(r, "id"), w)
|
|
)
|
|
|
|
if res == nil {
|
|
return
|
|
}
|
|
|
|
if err := svc.Delete(ctx, res.ID); err != nil {
|
|
sendError(w, newErrorResponse(http.StatusBadRequest, err))
|
|
} else {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// loads role from request path params
|
|
//
|
|
// handles errors by writing them to response
|
|
func (h groupsHandler) lookup(ctx context.Context, id string, w http.ResponseWriter) *types.Role {
|
|
var (
|
|
svc = h.svc
|
|
)
|
|
|
|
if h.externalIdAsPrimary {
|
|
res, err := h.lookupByExternalId(ctx, id)
|
|
if err != nil {
|
|
sendError(w, err)
|
|
return nil
|
|
}
|
|
|
|
if res == nil {
|
|
sendError(w, newErrorResponse(http.StatusNotFound, fmt.Errorf("group not found")))
|
|
}
|
|
|
|
return res
|
|
} else {
|
|
id, err := strconv.ParseUint(id, 10, 64)
|
|
if err != nil || id == 0 {
|
|
sendError(w, newErrorResponse(http.StatusBadRequest, err))
|
|
return nil
|
|
}
|
|
|
|
role, err := svc.FindByID(ctx, id)
|
|
if err != nil {
|
|
sendError(w, newErrorResponse(http.StatusBadRequest, err))
|
|
return nil
|
|
}
|
|
|
|
return role
|
|
}
|
|
}
|
|
|
|
func (h groupsHandler) lookupByExternalId(ctx context.Context, id string) (r *types.Role, err error) {
|
|
if h.externalIdValidator != nil && !h.externalIdValidator.MatchString(id) {
|
|
return nil, newErrorfResponse(http.StatusBadRequest, "invalid external ID")
|
|
}
|
|
|
|
rr, _, err := h.svc.Find(ctx, types.RoleFilter{Labels: map[string]string{groupLabel_SCIM_externalId: id}})
|
|
if err != nil {
|
|
return nil, newErrorResponse(http.StatusInternalServerError, err)
|
|
}
|
|
|
|
switch len(rr) {
|
|
case 0:
|
|
return nil, nil
|
|
case 1:
|
|
return rr[0], nil
|
|
default:
|
|
return nil, newErrorfResponse(http.StatusPreconditionFailed, "more than one group matches this externalId")
|
|
}
|
|
}
|