diff --git a/services/graph/pkg/service/v0/users_filter.go b/services/graph/pkg/service/v0/users_filter.go index 63a6579cd..8f46d4d12 100644 --- a/services/graph/pkg/service/v0/users_filter.go +++ b/services/graph/pkg/service/v0/users_filter.go @@ -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 { diff --git a/services/graph/pkg/service/v0/users_test.go b/services/graph/pkg/service/v0/users_test.go index 870368d3d..c3ebdd5d6 100644 --- a/services/graph/pkg/service/v0/users_test.go +++ b/services/graph/pkg/service/v0/users_test.go @@ -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),