mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-03-16 06:27:17 -04:00
This tries to address a semantic glitch in Less() to actually do a "less than" operation on strings and timestamps. It does not change the actual behaviour of the endpoints that support "orderby". The sorted results will be the same as before.
361 lines
11 KiB
Go
361 lines
11 KiB
Go
package svc
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/CiscoM31/godata"
|
|
libregraph "github.com/owncloud/libre-graph-api-go"
|
|
"github.com/owncloud/ocis/graph/pkg/service/v0/errorcode"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/render"
|
|
)
|
|
|
|
const memberRefsLimit = 20
|
|
|
|
// GetGroups implements the Service interface.
|
|
func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) {
|
|
sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
|
|
odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query())
|
|
if err != nil {
|
|
g.logger.Err(err).Interface("query", r.URL.Query()).Msg("query error")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
groups, err := g.identityBackend.GetGroups(r.Context(), r.URL.Query())
|
|
|
|
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())
|
|
}
|
|
}
|
|
|
|
groups, err = sortGroups(odataReq, groups)
|
|
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.StatusOK)
|
|
render.JSON(w, r, &listResponse{Value: groups})
|
|
}
|
|
|
|
// PostGroup implements the Service interface.
|
|
func (g Graph) PostGroup(w http.ResponseWriter, r *http.Request) {
|
|
grp := libregraph.NewGroup()
|
|
err := json.NewDecoder(r.Body).Decode(grp)
|
|
if err != nil {
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
if _, ok := grp.GetDisplayNameOk(); !ok {
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Missing Required Attribute")
|
|
return
|
|
}
|
|
|
|
// Disallow user-supplied IDs. It's supposed to be readonly. We're either
|
|
// generating them in the backend ourselves or rely on the Backend's
|
|
// storage (e.g. LDAP) to provide a unique ID.
|
|
if _, ok := grp.GetIdOk(); ok {
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "group id is a read-only attribute")
|
|
return
|
|
}
|
|
|
|
if grp, err = g.identityBackend.CreateGroup(r.Context(), *grp); err != nil {
|
|
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
render.Status(r, http.StatusOK)
|
|
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")
|
|
groupID, err := url.PathUnescape(groupID)
|
|
if err != nil {
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping group id failed")
|
|
}
|
|
|
|
if groupID == "" {
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id")
|
|
return
|
|
}
|
|
|
|
group, err := g.identityBackend.GetGroup(r.Context(), groupID)
|
|
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())
|
|
}
|
|
}
|
|
|
|
render.Status(r, http.StatusOK)
|
|
render.JSON(w, r, group)
|
|
}
|
|
|
|
// DeleteGroup implements the Service interface.
|
|
func (g Graph) DeleteGroup(w http.ResponseWriter, r *http.Request) {
|
|
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
|
|
}
|
|
|
|
err = g.identityBackend.DeleteGroup(r.Context(), groupID)
|
|
|
|
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)
|
|
}
|
|
|
|
func (g Graph) GetGroupMembers(w http.ResponseWriter, r *http.Request) {
|
|
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
|
|
}
|
|
|
|
members, err := g.identityBackend.GetGroupMembers(r.Context(), groupID)
|
|
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.StatusOK)
|
|
render.JSON(w, r, members)
|
|
}
|
|
|
|
// PostGroupMember implements the Service interface.
|
|
func (g Graph) PostGroupMember(w http.ResponseWriter, r *http.Request) {
|
|
g.logger.Info().Msg("Calling PostGroupMember")
|
|
|
|
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
|
|
}
|
|
memberRef := libregraph.NewMemberReference()
|
|
err = json.NewDecoder(r.Body).Decode(memberRef)
|
|
if err != nil {
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
memberRefURL, ok := memberRef.GetOdataIdOk()
|
|
if !ok {
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "@odata.id refernce is missing")
|
|
return
|
|
}
|
|
memberType, id, err := g.parseMemberRef(*memberRefURL)
|
|
if err != nil {
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "Error parsing @odata.id url")
|
|
return
|
|
}
|
|
// 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
|
|
}
|
|
|
|
g.logger.Debug().Str("memberType", memberType).Str("id", id).Msg("Add Member")
|
|
err = g.identityBackend.AddMembersToGroup(r.Context(), groupID, []string{id})
|
|
|
|
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)
|
|
}
|
|
|
|
// DeleteGroupMember implements the Service interface.
|
|
func (g Graph) DeleteGroupMember(w http.ResponseWriter, r *http.Request) {
|
|
g.logger.Info().Msg("Calling DeleteGroupMember")
|
|
|
|
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
|
|
}
|
|
|
|
memberID := chi.URLParam(r, "memberID")
|
|
memberID, err = url.PathUnescape(memberID)
|
|
if err != nil {
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping group id failed")
|
|
return
|
|
}
|
|
|
|
if memberID == "" {
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing group id")
|
|
return
|
|
}
|
|
g.logger.Debug().Str("groupID", groupID).Str("memberID", memberID).Msg("DeleteGroupMember")
|
|
err = g.identityBackend.RemoveMemberFromGroup(r.Context(), groupID, memberID)
|
|
|
|
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)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func sortGroups(req *godata.GoDataRequest, groups []*libregraph.Group) ([]*libregraph.Group, error) {
|
|
var sorter sort.Interface
|
|
if req.Query.OrderBy == nil || len(req.Query.OrderBy.OrderByItems) != 1 {
|
|
return groups, nil
|
|
}
|
|
switch req.Query.OrderBy.OrderByItems[0].Field.Value {
|
|
case "displayName":
|
|
sorter = groupsByDisplayName{groups}
|
|
default:
|
|
return nil, fmt.Errorf("we do not support <%s> as a order parameter", req.Query.OrderBy.OrderByItems[0].Field.Value)
|
|
}
|
|
|
|
if req.Query.OrderBy.OrderByItems[0].Order == "desc" {
|
|
sorter = sort.Reverse(sorter)
|
|
}
|
|
sort.Sort(sorter)
|
|
return groups, nil
|
|
}
|