3
0

Add support to provision roles (groups) via SCIM

This commit is contained in:
Denis Arh
2020-11-29 09:37:22 +01:00
parent 58fb2157eb
commit 41eae955ca
4 changed files with 283 additions and 1 deletions

View File

@@ -0,0 +1,138 @@
package scim
import (
"context"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/system/service"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/go-chi/chi"
"io"
"net/http"
"strconv"
)
type (
groupsHandler struct {
svc service.RoleService
}
)
func (h groupsHandler) get(w http.ResponseWriter, r *http.Request) {
var (
id, _ = strconv.ParseUint(chi.URLParam(r, "id"), 10, 64)
ctx = auth.SetSuperUserContext(r.Context())
svc = h.svc.With(ctx)
)
if id == 0 {
http.Error(w, "invalid group id", http.StatusBadRequest)
return
}
if u, err := svc.FindByID(id); err != nil {
sendError(w, newErrorResonse(http.StatusBadRequest, err))
} else {
send(w, http.StatusOK, newGroupResourceResponse(u))
}
}
func (h groupsHandler) create(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var (
ctx = auth.SetSuperUserContext(r.Context())
)
if u, err := h.createFromJSON(ctx, r.Body); err != nil {
sendError(w, newErrorResonse(http.StatusBadRequest, err))
} else {
send(w, http.StatusCreated, newGroupResourceResponse(u))
}
}
func (h groupsHandler) createFromJSON(ctx context.Context, j io.Reader) (r *types.Role, err error) {
var (
svc = h.svc.With(ctx)
payload = &groupResourceRequest{}
)
if err = payload.decodeJSON(j); err != nil {
return
}
// do we need to upsert?
if *payload.Name != "" {
r, err = svc.FindByName(*payload.Name)
if err != nil && !errors.Is(err, service.RoleErrNotFound()) {
return
}
}
if r == nil || r.ID == 0 {
// in case when we did not find a valid group,
// start from blank
r = &types.Role{}
}
payload.applyTo(r)
if r.ID > 0 {
return svc.Update(r)
} else {
return svc.Create(r)
}
}
func (h groupsHandler) replace(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var (
ctx = auth.SetSuperUserContext(r.Context())
groupID, _ = strconv.ParseUint(chi.URLParam(r, "id"), 10, 64)
)
if u, err := h.updateFromJSON(ctx, groupID, r.Body); err != nil {
sendError(w, newErrorResonse(http.StatusBadRequest, err))
} else {
send(w, http.StatusOK, newGroupResourceResponse(u))
}
}
func (h groupsHandler) updateFromJSON(ctx context.Context, id uint64, j io.Reader) (r *types.Role, err error) {
var (
svc = h.svc.With(ctx)
payload = &groupResourceRequest{}
)
if r, err = svc.FindByID(id); err != nil {
return
}
if r == nil {
return nil, fmt.Errorf("refusing to update invalid group")
}
if err = payload.decodeJSON(j); err != nil {
return
}
payload.applyTo(r)
return h.svc.With(ctx).Update(r)
}
func (h groupsHandler) delete(w http.ResponseWriter, r *http.Request) {
var (
ctx = auth.SetSuperUserContext(r.Context())
groupID, _ = strconv.ParseUint(chi.URLParam(r, "id"), 10, 64)
svc = h.svc.With(ctx)
)
if err := svc.Delete(groupID); err != nil {
sendError(w, newErrorResonse(http.StatusBadRequest, err))
} else {
w.WriteHeader(http.StatusNoContent)
}
}

View File

@@ -0,0 +1,61 @@
package scim
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza-server/system/types"
"io"
"strconv"
)
const (
urnGroup = "urn:ietf:params:scim:schemas:core:2.0:Group"
groupLabel_SCIM_externalId = "SCIM_externalId"
)
type (
groupResourceResponse struct {
Schemas []string `json:"schemas"`
Meta *metaResponse `json:"meta,omitempty"`
ID string `json:"id,omitempty"`
ExternalId string `json:"externalId,omitempty"`
Name string `json:"displayName"`
}
groupResourceRequest struct {
Schemas []string `json:"schemas"`
Meta *metaResponse `json:"meta,omitempty"`
ExternalId *string `json:"externalId,omitempty"`
Name *string `json:"displayName"`
}
)
func newGroupResourceResponse(u *types.Role) *groupResourceResponse {
rsp := &groupResourceResponse{
Schemas: []string{urnGroup},
Meta: newGroupMetaResponse(u),
ID: strconv.FormatUint(u.ID, 10),
ExternalId: u.Labels[groupLabel_SCIM_externalId],
Name: u.Name,
}
return rsp
}
func (req *groupResourceRequest) decodeJSON(r io.Reader) error {
if err := json.NewDecoder(r).Decode(req); err != nil {
return fmt.Errorf("could not decode group payload: %w", err)
}
return nil
}
func (req *groupResourceRequest) applyTo(u *types.Role) {
if req.Name != nil {
u.Name = *req.Name
}
if req.ExternalId != nil {
u.SetLabel("SCIM_externalId", *req.ExternalId)
}
}

View File

@@ -44,11 +44,18 @@ func Guard(opt options.SCIMOpt) func(next http.Handler) http.Handler {
func Routes(r chi.Router) {
uh := &usersHandler{svc: service.DefaultUser}
r.Route("/Users", func(r chi.Router) {
r.Get("/{id}", uh.get)
r.Post("/", uh.create)
r.Put("/{id}", uh.replace)
r.Delete("/{id}", uh.delete)
})
gh := &groupsHandler{svc: service.DefaultRole}
r.Route("/Groups", func(r chi.Router) {
r.Get("/{id}", gh.get)
r.Post("/", gh.create)
r.Put("/{id}", gh.replace)
r.Delete("/{id}", gh.delete)
})
}

View File

@@ -127,3 +127,79 @@ func TestScimUserDelete(t *testing.T) {
Status(http.StatusNoContent).
End()
}
func TestScimGroupGet(t *testing.T) {
h := newHelper(t)
h.clearRoles()
u := h.repoMakeRole()
h.scimApiInit().
Get(fmt.Sprintf("/Groups/%d", u.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Contains(`$.schemas`, "urn:ietf:params:scim:schemas:core:2.0:Group")).
Assert(jsonpath.Equal(`$.id`, fmt.Sprintf("%d", u.ID))).
End()
}
func TestScimGroupCreate(t *testing.T) {
h := newHelper(t)
h.clearRoles()
h.scimApiInit().
Debug().
Post("/Groups").
JSON(`{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
],
"displayName": "foo"
}`).
Expect(t).
Status(http.StatusCreated).
Assert(helpers.AssertNoErrors).
End()
u, err := store.LookupRoleByName(context.Background(), service.DefaultStore, "foo")
h.a.NoError(err)
h.a.Equal("foo", u.Name)
}
func TestScimGroupReplace(t *testing.T) {
h := newHelper(t)
h.clearRoles()
u := h.repoMakeRole()
h.scimApiInit().
Debug().
Put(fmt.Sprintf("/Groups/%d", u.ID)).
JSON(`{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:Group"
],
"displayName": "bar"
}`).
Expect(t).
End()
u, err := store.LookupRoleByID(context.Background(), service.DefaultStore, u.ID)
h.a.NoError(err)
h.a.NotNil(u)
h.a.Equal("bar", u.Name)
}
func TestScimGroupDelete(t *testing.T) {
h := newHelper(t)
h.clearRoles()
u := h.repoMakeRole(h.randEmail())
h.scimApiInit().
Delete(fmt.Sprintf("/Groups/%d", u.ID)).
Expect(t).
Status(http.StatusNoContent).
End()
}