Add support to provision roles (groups) via SCIM
This commit is contained in:
138
system/scim/group_handler.go
Normal file
138
system/scim/group_handler.go
Normal 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)
|
||||
}
|
||||
}
|
||||
61
system/scim/group_payloads.go
Normal file
61
system/scim/group_payloads.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user