Skip to content

Commit

Permalink
graph: Initial support for $filter in /users
Browse files Browse the repository at this point in the history
This adds some initial support for using $filter (as defined in the
odata spec) on the /users endpoint. Currently the only supported filter
is a single filter on `id` property of the `memberOf` relation of users.
To list all users that are members of a specific group:

```
curl 'https://localhost:9200/graph/v1.0/users?$filter=memberOf/any(m:m/id eq '262982c1-2362-4afa-bfdf-8cbfef64a06e')
```

Closes: owncloud#5487
  • Loading branch information
rhafer committed Feb 8, 2023
1 parent 26f7523 commit 7325472
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 5 deletions.
3 changes: 2 additions & 1 deletion services/graph/pkg/identity/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ type Backend interface {
DeleteGroup(ctx context.Context, id string) error
GetGroup(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.Group, error)
GetGroups(ctx context.Context, queryParam url.Values) ([]*libregraph.Group, error)
GetGroupMembers(ctx context.Context, id string) ([]*libregraph.User, error)
// GetGroupMembers list all members of a group
GetGroupMembers(ctx context.Context, id string, oreq *godata.GoDataRequest) ([]*libregraph.User, error)
// AddMembersToGroup adds new members (reference by a slice of IDs) to supplied group in the identity backend.
AddMembersToGroup(ctx context.Context, groupID string, memberID []string) error
// RemoveMemberFromGroup removes a single member (by ID) from a group
Expand Down
2 changes: 1 addition & 1 deletion services/graph/pkg/identity/cs3.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func (i *CS3) DeleteGroup(ctx context.Context, id string) error {
}

// GetGroupMembers implements the Backend Interface. It's currently not supported for the CS3 backend
func (i *CS3) GetGroupMembers(ctx context.Context, groupID string) ([]*libregraph.User, error) {
func (i *CS3) GetGroupMembers(ctx context.Context, groupID string, _ *godata.GoDataRequest) ([]*libregraph.User, error) {
return nil, errorcode.New(errorcode.NotSupported, "not implemented")
}

Expand Down
16 changes: 15 additions & 1 deletion services/graph/pkg/identity/ldap_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/url"
"strings"

"github.com/CiscoM31/godata"
"github.com/go-ldap/ldap/v3"
"github.com/gofrs/uuid"
ldapdn "github.com/libregraph/idm/pkg/ldapdn"
Expand Down Expand Up @@ -134,9 +135,15 @@ func (i *LDAP) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregr
}

// GetGroupMembers implements the Backend Interface for the LDAP Backend
func (i *LDAP) GetGroupMembers(ctx context.Context, groupID string) ([]*libregraph.User, error) {
func (i *LDAP) GetGroupMembers(ctx context.Context, groupID string, req *godata.GoDataRequest) ([]*libregraph.User, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetGroupMembers")

exp, err := GetExpandValues(req.Query)
if err != nil {
return nil, err
}

e, err := i.getLDAPGroupByNameOrID(groupID, true)
if err != nil {
return nil, err
Expand All @@ -149,6 +156,13 @@ func (i *LDAP) GetGroupMembers(ctx context.Context, groupID string) ([]*libregra
}
for _, member := range memberEntries {
if u := i.createUserModelFromLDAP(member); u != nil {
if slices.Contains(exp, "memberOf") {
userGroups, err := i.getGroupsForUser(member.DN)
if err != nil {
return nil, err
}
u.MemberOf = i.groupsFromLDAPEntries(userGroups)
}
result = append(result, u)
}
}
Expand Down
10 changes: 9 additions & 1 deletion services/graph/pkg/service/v0/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ func (g Graph) DeleteGroup(w http.ResponseWriter, r *http.Request) {
func (g Graph) GetGroupMembers(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling get group members")
sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
groupID := chi.URLParam(r, "groupID")
groupID, err := url.PathUnescape(groupID)
if err != nil {
Expand All @@ -273,8 +274,15 @@ func (g Graph) GetGroupMembers(w http.ResponseWriter, r *http.Request) {
return
}

odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query())
if err != nil {
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users: query error")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}

logger.Debug().Str("id", groupID).Msg("calling get group members on backend")
members, err := g.identityBackend.GetGroupMembers(r.Context(), groupID)
members, err := g.identityBackend.GetGroupMembers(r.Context(), groupID, odataReq)
if err != nil {
logger.Debug().Err(err).Msg("could not get group members: backend error")
var errcode errorcode.Error
Expand Down
10 changes: 9 additions & 1 deletion services/graph/pkg/service/v0/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,15 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) {
}

logger.Debug().Interface("query", r.URL.Query()).Msg("calling get users on backend")
users, err := g.identityBackend.GetUsers(r.Context(), odataReq)

var users []*libregraph.User

if odataReq.Query.Filter != nil {
users, err = g.applyUserFilter(r.Context(), odataReq)
} else {
users, err = g.identityBackend.GetUsers(r.Context(), odataReq)
}

if err != nil {
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users from backend")
var errcode errorcode.Error
Expand Down
94 changes: 94 additions & 0 deletions services/graph/pkg/service/v0/users_filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package svc

import (
"context"
"fmt"
"strings"

"github.com/CiscoM31/godata"
libregraph "github.com/owncloud/libre-graph-api-go"
)

func (g Graph) applyUserFilter(ctx context.Context, req *godata.GoDataRequest) (users []*libregraph.User, err error) {
logger := g.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("filter", req.Query.Filter.RawValue).Msg("applying filter")

switch req.Query.Filter.Tree.Token.Type {
case godata.ExpressionTokenLambdaNav:
return g.applyLambdaFilter(ctx, req, req.Query.Filter.Tree.Children)
}
return users, godata.NotImplementedError(fmt.Sprintf("Filter '%s' is not supported", req.Query.Filter.Tree.Token.Value))
}

func (g Graph) applyLambdaFilter(ctx context.Context, req *godata.GoDataRequest, nodes []*godata.ParseNode) (users []*libregraph.User, err error) {
logger := g.logger.SubloggerWithRequestID(ctx)
// logger.Debug().Str("filter", nodes. ).Msg("applying filter")
if len(nodes) != 2 {
logger.Debug().Msg("unexpected filter length ")
return users, godata.BadRequestError("bad filter")
}
// We only support memberOf/any queries for now
if nodes[0].Token.Type != godata.ExpressionTokenLiteral || nodes[0].Token.Value != "memberOf" {
return users, godata.BadRequestError("bad filter")
}
if nodes[1].Token.Type != godata.ExpressionTokenLambda || nodes[1].Token.Value != "any" {
return users, godata.BadRequestError("bad filter")
}
return g.applyLambdaMemberOfAny(ctx, req, nodes[1].Children)
}

func (g Graph) applyLambdaMemberOfAny(ctx context.Context, req *godata.GoDataRequest, nodes []*godata.ParseNode) (users []*libregraph.User, err error) {
logger := g.logger.SubloggerWithRequestID(ctx)
if len(nodes) != 2 {
logger.Debug().Msg("unexpected filter length ")
return users, godata.BadRequestError("bad filter")
}

// First element is the "name" of the lambda function's parameter
if nodes[0].Token.Type != godata.ExpressionTokenLiteral {
return users, godata.BadRequestError("bad filter")
}

// We only support the 'eq' expression for now
if nodes[1].Token.Type != godata.ExpressionTokenLogical && nodes[1].Token.Value != "eq" {
return users, godata.BadRequestError("bad filter")
}
return g.applyMemberOfEq(ctx, req, nodes[1].Children)
}

func (g Graph) applyMemberOfEq(ctx context.Context, req *godata.GoDataRequest, nodes []*godata.ParseNode) (users []*libregraph.User, err error) {
logger := g.logger.SubloggerWithRequestID(ctx)
if len(nodes) != 2 {
return users, godata.BadRequestError("bad filter")
}

if nodes[0].Token.Type != godata.ExpressionTokenNav {
return users, godata.BadRequestError("bad filter")
}

if len(nodes[0].Children) != 2 {
return users, godata.BadRequestError("bad filter")
}

filterProperty := nodes[0].Children[1].Token.Value
var filterValue string
switch nodes[1].Token.Type {
case godata.ExpressionTokenGuid:
filterValue = nodes[1].Token.Value
case godata.ExpressionTokenString:
// unquote
filterValue = strings.Trim(nodes[1].Token.Value, "'")
default:
return users, godata.BadRequestError("bad filter")
}
logger.Debug().Str("property", filterProperty).Str("value", filterValue).Msg("Filtering memberOf")

switch filterProperty {
case "id":
logger.Debug().Str("property", filterProperty).Str("value", filterValue).Msg("Filtering memberOf by group id")
return g.identityBackend.GetGroupMembers(ctx, filterValue, req)
default:
return users, godata.BadRequestError("bad filter")
}

}

0 comments on commit 7325472

Please sign in to comment.