Add support for adding multiple members to a group at once

Adding multiple members at once is done via PATCH on
`groups/{group-oid}` with a body containing a list of refernces to the
members.
This commit is contained in:
Ralf Haferkamp
2022-02-01 11:45:14 +01:00
parent 166bda9ac6
commit d9aa33525a
8 changed files with 143 additions and 42 deletions
+79 -9
View File
@@ -3,6 +3,7 @@ package svc
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
@@ -14,6 +15,8 @@ import (
"github.com/go-chi/render"
)
const memberRefsLimit = 20
// GetGroups implements the Service interface.
func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) {
groups, err := g.identityBackend.GetGroups(r.Context(), r.URL.Query())
@@ -61,6 +64,66 @@ func (g Graph) PostGroup(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, grp)
}
// PatchGroup implements the Service interface.
func (g Graph) PatchGroup(w http.ResponseWriter, r *http.Request) {
g.logger.Debug().Msg("Calling PatchGroup")
groupID := chi.URLParam(r, "groupID")
groupID, err := url.PathUnescape(groupID)
if err != nil {
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping group id failed")
return
}
if groupID == "" {
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id")
return
}
changes := libregraph.NewGroup()
err = json.NewDecoder(r.Body).Decode(changes)
if err != nil {
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
if memberRefs, ok := changes.GetMembersodataBindOk(); ok {
// The spec defines a limit of 20 members maxium per Request
if len(memberRefs) > memberRefsLimit {
errorcode.NotAllowed.Render(w, r, http.StatusInternalServerError,
fmt.Sprintf("Request is limited to %d members", memberRefsLimit))
return
}
memberIDs := make([]string, 0, len(memberRefs))
for _, memberRef := range memberRefs {
memberType, id, err := g.parseMemberRef(memberRef)
if err != nil {
errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Error parsing member@odata.bind values")
return
}
g.logger.Debug().Str("memberType", memberType).Str("memberid", id).Msg("Add Member")
// The MS Graph spec allows "directoryObject", "user", "group" and "organizational Contact"
// we restrict this to users for now. Might add Groups as members later
if memberType != "users" {
errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Only user are allowed as group members")
return
}
memberIDs = append(memberIDs, id)
}
err = g.identityBackend.AddMembersToGroup(r.Context(), groupID, memberIDs)
}
if err != nil {
var errcode errorcode.Error
if errors.As(err, &errcode) {
errcode.Render(w, r)
} else {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
return
}
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
}
// GetGroup implements the Service interface.
func (g Graph) GetGroup(w http.ResponseWriter, r *http.Request) {
groupID := chi.URLParam(r, "groupID")
@@ -171,18 +234,11 @@ func (g Graph) PostGroupMember(w http.ResponseWriter, r *http.Request) {
errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "@odata.id refernce is missing")
return
}
memberURL, err := url.ParseRequestURI(*memberRefURL)
memberType, id, err := g.parseMemberRef(*memberRefURL)
if err != nil {
errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Error parsing @odata.id url")
return
}
segments := strings.Split(memberURL.Path, "/")
if len(segments) < 2 {
errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Error parsing @odata.id url path")
return
}
id := segments[len(segments)-1]
memberType := segments[len(segments)-2]
// The MS Graph spec allows "directoryObject", "user", "group" and "organizational Contact"
// we restrict this to users for now. Might add Groups as members later
if memberType != "users" {
@@ -191,7 +247,7 @@ func (g Graph) PostGroupMember(w http.ResponseWriter, r *http.Request) {
}
g.logger.Debug().Str("memberType", memberType).Str("id", id).Msg("Add Member")
err = g.identityBackend.AddMemberToGroup(r.Context(), groupID, id)
err = g.identityBackend.AddMembersToGroup(r.Context(), groupID, []string{id})
if err != nil {
var errcode errorcode.Error
@@ -248,3 +304,17 @@ func (g Graph) DeleteGroupMember(w http.ResponseWriter, r *http.Request) {
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
}
func (g Graph) parseMemberRef(ref string) (string, string, error) {
memberURL, err := url.ParseRequestURI(ref)
if err != nil {
return "", "", err
}
segments := strings.Split(memberURL.Path, "/")
if len(segments) < 2 {
return "", "", errors.New("invalid member reference")
}
id := segments[len(segments)-1]
memberType := segments[len(segments)-2]
return memberType, id, nil
}
+5
View File
@@ -69,6 +69,11 @@ func (i instrument) PostGroup(w http.ResponseWriter, r *http.Request) {
i.next.PostGroup(w, r)
}
// PatchGroup implements the Service interface.
func (i instrument) PatchGroup(w http.ResponseWriter, r *http.Request) {
i.next.PatchGroup(w, r)
}
// DeleteGroup implements the Service interface.
func (i instrument) DeleteGroup(w http.ResponseWriter, r *http.Request) {
i.next.DeleteGroup(w, r)
+5
View File
@@ -69,6 +69,11 @@ func (l logging) PostGroup(w http.ResponseWriter, r *http.Request) {
l.next.PostGroup(w, r)
}
// PatchGroup implements the Service interface.
func (l logging) PatchGroup(w http.ResponseWriter, r *http.Request) {
l.next.PatchGroup(w, r)
}
// DeleteGroup implements the Service interface.
func (l logging) DeleteGroup(w http.ResponseWriter, r *http.Request) {
l.next.DeleteGroup(w, r)
+2
View File
@@ -34,6 +34,7 @@ type Service interface {
GetGroups(http.ResponseWriter, *http.Request)
GetGroup(http.ResponseWriter, *http.Request)
PostGroup(http.ResponseWriter, *http.Request)
PatchGroup(http.ResponseWriter, *http.Request)
DeleteGroup(http.ResponseWriter, *http.Request)
GetGroupMembers(http.ResponseWriter, *http.Request)
PostGroupMember(http.ResponseWriter, *http.Request)
@@ -121,6 +122,7 @@ func NewService(opts ...Option) Service {
r.Route("/{groupID}", func(r chi.Router) {
r.Get("/", svc.GetGroup)
r.Delete("/", svc.DeleteGroup)
r.Patch("/", svc.PatchGroup)
r.Route("/members", func(r chi.Router) {
r.Get("/", svc.GetGroupMembers)
r.Post("/$ref", svc.PostGroupMember)
+5
View File
@@ -65,6 +65,11 @@ func (t tracing) PostGroup(w http.ResponseWriter, r *http.Request) {
t.next.PostGroup(w, r)
}
// PatchGroup implements the Service interface.
func (t tracing) PatchGroup(w http.ResponseWriter, r *http.Request) {
t.next.PatchGroup(w, r)
}
// DeleteGroup implements the Service interface.
func (t tracing) DeleteGroup(w http.ResponseWriter, r *http.Request) {
t.next.DeleteGroup(w, r)