graph/users: small optimization for filter by role queries

When filtering by role assignment we now populate the
'appRoleAssignments' property of the returned users when the incoming
request contained and '$expand=appRoleAssignments'. This avoids having
to query the role assignments for all users in the result set a second
time.
The effect of this optimization depends a lot on the actual setup. In
single binary all-in-one installations it is rather small (this it saves
quite a few CPU cycles). In distributed setup it should be larger as it
reduces the number of network round trips to the settings service
significantly.
This commit is contained in:
Ralf Haferkamp
2024-06-05 11:30:24 +02:00
committed by Ralf Haferkamp
parent 4d29f5949c
commit 96e59598d3
3 changed files with 96 additions and 5 deletions

View File

@@ -175,6 +175,80 @@ func (_c *RoleService_ListRoleAssignments_Call) RunAndReturn(run func(context.Co
return _c
}
// ListRoleAssignmentsFiltered provides a mock function with given fields: ctx, in, opts
func (_m *RoleService) ListRoleAssignmentsFiltered(ctx context.Context, in *v0.ListRoleAssignmentsFilteredRequest, opts ...client.CallOption) (*v0.ListRoleAssignmentsResponse, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, in)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
if len(ret) == 0 {
panic("no return value specified for ListRoleAssignmentsFiltered")
}
var r0 *v0.ListRoleAssignmentsResponse
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *v0.ListRoleAssignmentsFilteredRequest, ...client.CallOption) (*v0.ListRoleAssignmentsResponse, error)); ok {
return rf(ctx, in, opts...)
}
if rf, ok := ret.Get(0).(func(context.Context, *v0.ListRoleAssignmentsFilteredRequest, ...client.CallOption) *v0.ListRoleAssignmentsResponse); ok {
r0 = rf(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0.ListRoleAssignmentsResponse)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *v0.ListRoleAssignmentsFilteredRequest, ...client.CallOption) error); ok {
r1 = rf(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RoleService_ListRoleAssignmentsFiltered_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRoleAssignmentsFiltered'
type RoleService_ListRoleAssignmentsFiltered_Call struct {
*mock.Call
}
// ListRoleAssignmentsFiltered is a helper method to define mock.On call
// - ctx context.Context
// - in *v0.ListRoleAssignmentsFilteredRequest
// - opts ...client.CallOption
func (_e *RoleService_Expecter) ListRoleAssignmentsFiltered(ctx interface{}, in interface{}, opts ...interface{}) *RoleService_ListRoleAssignmentsFiltered_Call {
return &RoleService_ListRoleAssignmentsFiltered_Call{Call: _e.mock.On("ListRoleAssignmentsFiltered",
append([]interface{}{ctx, in}, opts...)...)}
}
func (_c *RoleService_ListRoleAssignmentsFiltered_Call) Run(run func(ctx context.Context, in *v0.ListRoleAssignmentsFilteredRequest, opts ...client.CallOption)) *RoleService_ListRoleAssignmentsFiltered_Call {
_c.Call.Run(func(args mock.Arguments) {
variadicArgs := make([]client.CallOption, len(args)-2)
for i, a := range args[2:] {
if a != nil {
variadicArgs[i] = a.(client.CallOption)
}
}
run(args[0].(context.Context), args[1].(*v0.ListRoleAssignmentsFilteredRequest), variadicArgs...)
})
return _c
}
func (_c *RoleService_ListRoleAssignmentsFiltered_Call) Return(_a0 *v0.ListRoleAssignmentsResponse, _a1 error) *RoleService_ListRoleAssignmentsFiltered_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *RoleService_ListRoleAssignmentsFiltered_Call) RunAndReturn(run func(context.Context, *v0.ListRoleAssignmentsFilteredRequest, ...client.CallOption) (*v0.ListRoleAssignmentsResponse, error)) *RoleService_ListRoleAssignmentsFiltered_Call {
_c.Call.Return(run)
return _c
}
// ListRoles provides a mock function with given fields: ctx, in, opts
func (_m *RoleService) ListRoles(ctx context.Context, in *v0.ListBundlesRequest, opts ...client.CallOption) (*v0.ListBundlesResponse, error) {
_va := make([]interface{}, len(opts))

View File

@@ -298,7 +298,7 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) {
expandAppRoleAssignments := slices.Contains(exp, "appRoleAssignments")
expandMemberOf := slices.Contains(exp, "memberOf")
for _, u := range users {
if expandAppRoleAssignments {
if expandAppRoleAssignments && u.AppRoleAssignments == nil {
u.AppRoleAssignments, err = g.fetchAppRoleAssignments(r.Context(), u.GetId())
if err != nil {
// TODO I think we should not continue here, see http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionexpand

View File

@@ -156,7 +156,7 @@ func (g Graph) applyFilterLogicalAnd(ctx context.Context, req *godata.GoDataRequ
return []*libregraph.User{}, err
}
logger.Debug().Str("property", property).Str("value", value).Msg("applying approleAssignments filter on result set")
return g.filterUsersByAppRoleID(ctx, value, res2)
return g.filterUsersByAppRoleID(ctx, req, value, res2)
}
// 1st part is no appRoleAssignmentFilter, run the filter query
@@ -172,7 +172,7 @@ func (g Graph) applyFilterLogicalAnd(ctx context.Context, req *godata.GoDataRequ
return users, unsupportedFilterError()
}
logger.Debug().Str("property", property).Str("value", value).Msg("applying approleAssignments filter on result set")
return g.filterUsersByAppRoleID(ctx, value, res1)
return g.filterUsersByAppRoleID(ctx, req, value, res1)
}
// 2nd part is no appRoleAssignmentFilter either
@@ -329,22 +329,39 @@ func (g Graph) applyAppRoleAssignmentEq(ctx context.Context, req *godata.GoDataR
return users, err
}
return g.filterUsersByAppRoleID(ctx, filterValue, users)
return g.filterUsersByAppRoleID(ctx, req, filterValue, users)
}
return users, unsupportedFilterError()
}
func (g Graph) filterUsersByAppRoleID(ctx context.Context, id string, users []*libregraph.User) ([]*libregraph.User, error) {
func (g Graph) filterUsersByAppRoleID(ctx context.Context, req *godata.GoDataRequest, id 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
var expand bool
if exp := req.Query.GetExpand(); exp != nil {
for _, item := range exp.ExpandItems {
if item.Path[0].Value == "appRoleAssignments" {
expand = true
break
}
}
}
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
}
// To avoid re-fetching all assignments in the upper layer handler when
// the $expand query parameter is set, we're adding the assignments to the
// resulting users here already.
if expand {
user.AppRoleAssignments = assignments
}
for _, assignment := range assignments {
if assignment.GetAppRoleId() == id {
if _, ok := resultUsersMap[user.GetId()]; !ok {