diff --git a/changelog/unreleased/graph-user-sort.md b/changelog/unreleased/graph-user-sort.md new file mode 100644 index 000000000..e7f8a2885 --- /dev/null +++ b/changelog/unreleased/graph-user-sort.md @@ -0,0 +1,10 @@ +Enhancement: Add sorting to GraphAPI users and groups + +The GraphAPI endpoints for users and groups support ordering now. +User can be ordered by displayName, onPremisesSamAccountName and mail. +Groups can be ordered by displayName. + +Example: +https://localhost:9200/graph/v1.0/groups?$orderby=displayName asc + +https://github.com/owncloud/ocis/issues/3360 diff --git a/graph/pkg/service/v0/groups.go b/graph/pkg/service/v0/groups.go index 76c74f212..8c76c5c18 100644 --- a/graph/pkg/service/v0/groups.go +++ b/graph/pkg/service/v0/groups.go @@ -6,8 +6,10 @@ import ( "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" @@ -19,6 +21,14 @@ 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 { @@ -29,6 +39,17 @@ func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) { 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}) } @@ -318,3 +339,22 @@ func (g Graph) parseMemberRef(ref string) (string, string, error) { 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 == "asc" { + sorter = sort.Reverse(sorter) + } + sort.Sort(sorter) + return groups, nil +} diff --git a/graph/pkg/service/v0/ordering.go b/graph/pkg/service/v0/ordering.go index 4dd0f4a54..0655af011 100644 --- a/graph/pkg/service/v0/ordering.go +++ b/graph/pkg/service/v0/ordering.go @@ -45,3 +45,59 @@ func (s spacesByLastModifiedDateTime) Less(i, j int) bool { // fallback to name if no dateTime is set on both items return strings.ToLower(*s.spacesSlice[i].Name) > strings.ToLower(*s.spacesSlice[j].Name) } + +type userSlice []*libregraph.User + +// Len is the number of elements in the collection. +func (d userSlice) Len() int { return len(d) } + +// Swap swaps the elements with indexes i and j. +func (d userSlice) Swap(i, j int) { d[i], d[j] = d[j], d[i] } + +type usersByDisplayName struct { + userSlice +} + +type usersByMail struct { + userSlice +} + +type usersByOnPremisesSamAccountName struct { + userSlice +} + +// Less reports whether the element with index i +// must sort before the element with index j. +func (u usersByDisplayName) Less(i, j int) bool { + return strings.ToLower(u.userSlice[i].GetDisplayName()) > strings.ToLower(u.userSlice[j].GetDisplayName()) +} + +// Less reports whether the element with index i +// must sort before the element with index j. +func (u usersByMail) Less(i, j int) bool { + return strings.ToLower(u.userSlice[i].GetMail()) > strings.ToLower(u.userSlice[j].GetMail()) +} + +// Less reports whether the element with index i +// must sort before the element with index j. +func (u usersByOnPremisesSamAccountName) Less(i, j int) bool { + return strings.ToLower(u.userSlice[i].GetOnPremisesSamAccountName()) > strings.ToLower(u.userSlice[j].GetOnPremisesSamAccountName()) +} + +type groupSlice []*libregraph.Group + +// Len is the number of elements in the collection. +func (d groupSlice) Len() int { return len(d) } + +// Swap swaps the elements with indexes i and j. +func (d groupSlice) Swap(i, j int) { d[i], d[j] = d[j], d[i] } + +type groupsByDisplayName struct { + groupSlice +} + +// Less reports whether the element with index i +// must sort before the element with index j. +func (g groupsByDisplayName) Less(i, j int) bool { + return strings.ToLower(g.groupSlice[i].GetDisplayName()) > strings.ToLower(g.groupSlice[j].GetDisplayName()) +} diff --git a/graph/pkg/service/v0/users.go b/graph/pkg/service/v0/users.go index 182fc5cd9..7495a2f54 100644 --- a/graph/pkg/service/v0/users.go +++ b/graph/pkg/service/v0/users.go @@ -7,7 +7,10 @@ import ( "net/http" "net/url" "regexp" + "sort" + "strings" + "github.com/CiscoM31/godata" revactx "github.com/cs3org/reva/v2/pkg/ctx" "github.com/go-chi/chi/v5" "github.com/go-chi/render" @@ -38,6 +41,13 @@ func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) { // GetUsers implements the Service interface. func (g Graph) GetUsers(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 + } users, err := g.identityBackend.GetUsers(r.Context(), r.URL.Query()) if err != nil { var errcode errorcode.Error @@ -48,6 +58,17 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) { } return } + + users, err = sortUsers(odataReq, users) + 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: users}) } @@ -234,3 +255,26 @@ func isValidEmail(e string) bool { } return emailRegex.MatchString(e) } + +func sortUsers(req *godata.GoDataRequest, users []*libregraph.User) ([]*libregraph.User, error) { + var sorter sort.Interface + if req.Query.OrderBy == nil || len(req.Query.OrderBy.OrderByItems) != 1 { + return users, nil + } + switch req.Query.OrderBy.OrderByItems[0].Field.Value { + case "displayName": + sorter = usersByDisplayName{users} + case "mail": + sorter = usersByMail{users} + case "onPremisesSamAccountName": + sorter = usersByOnPremisesSamAccountName{users} + 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 == "asc" { + sorter = sort.Reverse(sorter) + } + sort.Sort(sorter) + return users, nil +}