Add support for $filter on appRoleAssignment

This add support for filtering on the `appRoleAssignment` relation of
users.  E.g.

```
$filter=appRoleAssignments/any(m:m/appRoleId eq '262982c1-2362-4afa-bfdf-8cbfef64a06e')
```

combining it with a filter on groupMemberShip does also work:

```
$filter=memberOf/any(m:m/id eq '262982c1-2362-4afa-bfdf-8cbfef64a06e') and appRoleAssignments/any(m:m/appRoleId eq 'd7beeea8-8ff4-406b-8fb6-ab2dd81e6b11')
```

The filter is still very inefficient as it always needs to get the full
users list. We need to adapt it to only filter on a subset of users when
using this filter 'and' combined with other filters.

Closes: #5488
This commit is contained in:
Ralf Haferkamp
2023-02-21 12:30:59 +01:00
committed by Ralf Haferkamp
parent db6dc5f8af
commit 1552f6df5a
2 changed files with 105 additions and 8 deletions
+90 -6
View File
@@ -81,16 +81,22 @@ func (g Graph) applyFilterLambda(ctx context.Context, req *godata.GoDataRequest,
if len(nodes) != 2 {
return users, invalidFilterError()
}
// We only support memberOf/any queries for now
if nodes[0].Token.Type != godata.ExpressionTokenLiteral || nodes[0].Token.Value != "memberOf" {
logger.Debug().Str("Token", nodes[0].Token.Value).Msg("unsupported relation for lambda filter")
return users, unsupportedFilterError()
}
// We only support the "any" operator for lambda queries for now
if nodes[1].Token.Type != godata.ExpressionTokenLambda || nodes[1].Token.Value != "any" {
logger.Debug().Str("Token", nodes[1].Token.Value).Msg("unsupported lambda filter")
return users, unsupportedFilterError()
}
return g.applyLambdaMemberOfAny(ctx, req, nodes[1].Children)
if nodes[0].Token.Type != godata.ExpressionTokenLiteral {
return users, unsupportedFilterError()
}
switch nodes[0].Token.Value {
case "memberOf":
return g.applyLambdaMemberOfAny(ctx, req, nodes[1].Children)
case "appRoleAssignments":
return g.applyLambdaAppRoleAssignmentAny(ctx, req, nodes[1].Children)
}
logger.Debug().Str("Token", nodes[0].Token.Value).Msg("unsupported relation for lambda filter")
return users, unsupportedFilterError()
}
func (g Graph) applyLambdaMemberOfAny(ctx context.Context, req *godata.GoDataRequest, nodes []*godata.ParseNode) (users []*libregraph.User, err error) {
@@ -141,7 +147,85 @@ func (g Graph) applyMemberOfEq(ctx context.Context, req *godata.GoDataRequest, n
default:
return users, unsupportedFilterError()
}
}
func (g Graph) applyLambdaAppRoleAssignmentAny(ctx context.Context, req *godata.GoDataRequest, nodes []*godata.ParseNode) (users []*libregraph.User, err error) {
if len(nodes) != 2 {
return users, invalidFilterError()
}
// First element is the "name" of the lambda function's parameter
if nodes[0].Token.Type != godata.ExpressionTokenLiteral {
return users, invalidFilterError()
}
// We only support the 'eq' expression for now
if nodes[1].Token.Type != godata.ExpressionTokenLogical || nodes[1].Token.Value != "eq" {
return users, unsupportedFilterError()
}
return g.applyAppRoleAssignmentEq(ctx, req, nodes[1].Children)
}
func (g Graph) applyAppRoleAssignmentEq(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, invalidFilterError()
}
if nodes[0].Token.Type != godata.ExpressionTokenNav {
return users, invalidFilterError()
}
if len(nodes[0].Children) != 2 {
return users, invalidFilterError()
}
if nodes[0].Children[1].Token.Value == "appRoleId" {
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, unsupportedFilterError()
}
logger.Debug().Str("property", nodes[0].Children[1].Token.Value).Str("value", filterValue).Msg("Filtering appRoleAssignments by appRoleId")
if users, err = g.identityBackend.GetUsers(ctx, req); err != nil {
return users, err
}
return g.filterUsersByAppRoleId(ctx, filterValue, users)
}
return users, unsupportedFilterError()
}
func (g Graph) filterUsersByAppRoleId(ctx context.Context, appRoleId string, users []*libregraph.User) ([]*libregraph.User, error) {
// We're using a map for the results here, in order to avoid returning
// a user twice. The settings API, still has an issue that causes it to
// duplicate some assignments on restart:
// https://github.com/owncloud/ocis/issues/3432
resultUsersMap := make(map[string]*libregraph.User, len(users))
for _, user := range users {
assignments, err := g.fetchAppRoleAssignments(ctx, user.GetId())
if err != nil {
return users, err
}
for _, assignment := range assignments {
if assignment.GetAppRoleId() == appRoleId {
if _, ok := resultUsersMap[user.GetId()]; !ok {
resultUsersMap[user.GetId()] = user
}
}
}
}
resultUsers := make([]*libregraph.User, 0, len(resultUsersMap))
for _, user := range resultUsersMap {
resultUsers = append(resultUsers, user)
}
return resultUsers, nil
}
func userSliceToMap(users []*libregraph.User) map[string]*libregraph.User {
+15 -2
View File
@@ -340,16 +340,28 @@ var _ = Describe("Users", func() {
Entry("with unsupported lambda token ", "memberOf/all(n:n/id eq 1)", http.StatusNotImplemented),
Entry("with unsupported filter operation ", "memberOf/any(n:n/id ne 1)", http.StatusNotImplemented),
Entry("with unsupported filter operand type", "memberOf/any(n:n/id eq 1)", http.StatusNotImplemented),
Entry("with unsupported lambda filter property", "memberOf/any(n:n/name eq 'name')", http.StatusNotImplemented),
Entry("with unsupported memberOf lambda filter property", "memberOf/any(n:n/name eq 'name')", http.StatusNotImplemented),
Entry("with unsupported appRoleAssignments lambda filter property", "appRoleAssignments/any(n:n/id eq 'id')", http.StatusNotImplemented),
)
DescribeTable("With a valid memberOf filter",
DescribeTable("With a valid filter",
func(filter string, status int) {
user := &libregraph.User{}
user.SetId("25cb7bc0-3168-4a0c-adbe-396f478ad494")
users := []*libregraph.User{user}
identityBackend.On("GetGroupMembers", mock.Anything, "25cb7bc0-3168-4a0c-adbe-396f478ad494", mock.Anything).Return(users, nil)
identityBackend.On("GetGroupMembers", mock.Anything, "2713f1d5-6822-42bd-ad56-9f6c55a3a8fa", mock.Anything).Return([]*libregraph.User{}, nil)
identityBackend.On("GetUsers", mock.Anything, mock.Anything).Return([]*libregraph.User{user}, nil)
roleService.On("ListRoleAssignments", mock.Anything, mock.Anything, mock.Anything).
Return(func(ctx context.Context, in *settings.ListRoleAssignmentsRequest, opts ...client.CallOption) *settings.ListRoleAssignmentsResponse {
return &settings.ListRoleAssignmentsResponse{Assignments: []*settingsmsg.UserRoleAssignment{
{
Id: "some-appRoleAssignment-ID",
AccountUuid: user.GetId(),
RoleId: "some-appRole-ID",
},
}}
}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/users?$filter="+url.QueryEscape(filter), nil)
svc.GetUsers(rr, r)
@@ -357,6 +369,7 @@ var _ = Describe("Users", func() {
},
Entry("with memberOf lambda filter with UUID", "memberOf/any(n:n/id eq 25cb7bc0-3168-4a0c-adbe-396f478ad494)", http.StatusOK),
Entry("with memberOf lambda filter with UUID string", "memberOf/any(n:n/id eq '25cb7bc0-3168-4a0c-adbe-396f478ad494')", http.StatusOK),
Entry("with appRoleAssignments lambda filter with appRoleId", "appRoleAssignments/any(n:n/appRoleId eq 'some-appRole-ID')", http.StatusOK),
Entry("with two memberOf lambda filters",
"memberOf/any(n:n/id eq 25cb7bc0-3168-4a0c-adbe-396f478ad494) and memberOf/any(n:n/id eq 2713f1d5-6822-42bd-ad56-9f6c55a3a8fa)",
http.StatusOK),