mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-18 03:18:52 -06:00
feat(graph): Add $filter support for lastSuccessfulSignInDateTime
It is now possible to filter users based on the lastSuccessfulSignInDateTime attribute using query filter like: '$filter=signInActivity/lastSuccessfulSignInDateTime le 2021-09-01T00:00:00Z' Note: This does only work with LDAP servers actually supporting '<=' filters. The built-in LDAP server (idm) does not support this feature.
This commit is contained in:
committed by
Ralf Haferkamp
parent
231128950f
commit
34cc7b2e56
@@ -3,4 +3,13 @@ Enhancement: allow to maintain the last sign-in timestamp of a user
|
||||
When the LDAP identity backend is configured to have write access to the database
|
||||
we're now able to maintain the ocLastSignInTimestamp attribute for the users.
|
||||
|
||||
This attribute is return in the 'signinActivity/lastSuccessfulSignInDateTime'
|
||||
properity of the user objects. It is also possible to $filter on this attribute.
|
||||
|
||||
Use e.g. '$filter=signinActivity/lastSuccessfulSignInDateTime le 2023-12-31T00:00:00Z'
|
||||
to search for users that have not signed in since 2023-12-31.
|
||||
Note: To use this type of filter the underlying LDAP server must support the
|
||||
'<=' filter. Which is currently not the case of the built-in LDAP server (idm).
|
||||
|
||||
https://github.com/owncloud/ocis/pull/9942
|
||||
https://github.com/owncloud/ocis/pull/10111
|
||||
|
||||
@@ -19,6 +19,8 @@ var (
|
||||
ErrReadOnly = errorcode.New(errorcode.NotAllowed, "server is configured read-only")
|
||||
// ErrNotFound signals that the requested resource was not found.
|
||||
ErrNotFound = errorcode.New(errorcode.ItemNotFound, "not found")
|
||||
// ErrUnsupportedFilter signals that the requested filter is not supported by the backend.
|
||||
ErrUnsupportedFilter = godata.NotImplementedError("unsupported filter")
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -37,6 +39,8 @@ type Backend interface {
|
||||
UpdateUser(ctx context.Context, nameOrID string, user libregraph.UserUpdate) (*libregraph.User, error)
|
||||
GetUser(ctx context.Context, nameOrID string, oreq *godata.GoDataRequest) (*libregraph.User, error)
|
||||
GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*libregraph.User, error)
|
||||
// FilterUsers returns a list of users that match the filter
|
||||
FilterUsers(ctx context.Context, oreq *godata.GoDataRequest, filter *godata.ParseNode) ([]*libregraph.User, error)
|
||||
UpdateLastSignInDate(ctx context.Context, userID string, timestamp time.Time) error
|
||||
|
||||
// CreateGroup creates the supplied group in the identity backend.
|
||||
|
||||
@@ -95,12 +95,12 @@ func (i *CS3) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*libr
|
||||
case err != nil:
|
||||
logger.Error().Str("backend", "cs3").Err(err).Str("search", search).Msg("error sending find users grpc request: transport error")
|
||||
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
|
||||
case res.Status.Code != cs3rpc.Code_CODE_OK:
|
||||
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, res.Status.Message)
|
||||
case res.GetStatus().GetCode() != cs3rpc.Code_CODE_OK:
|
||||
if res.GetStatus().GetCode() == cs3rpc.Code_CODE_NOT_FOUND {
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, res.GetStatus().GetMessage())
|
||||
}
|
||||
logger.Debug().Str("backend", "cs3").Err(err).Str("search", search).Msg("error sending find users grpc request")
|
||||
return nil, errorcode.New(errorcode.GeneralException, res.Status.Message)
|
||||
return nil, errorcode.New(errorcode.GeneralException, res.GetStatus().GetMessage())
|
||||
}
|
||||
|
||||
users := make([]*libregraph.User, 0, len(res.GetUsers()))
|
||||
@@ -112,6 +112,11 @@ func (i *CS3) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*libr
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// FilterUsers implements the Backend Interface. It's currently not supported for the CS3 backend
|
||||
func (i *CS3) FilterUsers(_ context.Context, _ *godata.GoDataRequest, _ *godata.ParseNode) ([]*libregraph.User, error) {
|
||||
return nil, errNotImplemented
|
||||
}
|
||||
|
||||
// UpdateLastSignInDate implements the Backend Interface. It's currently not supported for the CS3 backend
|
||||
func (i *CS3) UpdateLastSignInDate(ctx context.Context, userID string, timestamp time.Time) error {
|
||||
return errNotImplemented
|
||||
|
||||
@@ -510,7 +510,7 @@ func (i *LDAP) getLDAPUserByNameOrID(nameOrID string) (*ldap.Entry, error) {
|
||||
idString, err := filterEscapeUUID(i.userIDisOctetString, nameOrID)
|
||||
// err != nil just means that this is not an uuid, so we can skip the uuid filter part
|
||||
// and just filter by name
|
||||
filter := ""
|
||||
var filter string
|
||||
if err == nil {
|
||||
filter = fmt.Sprintf("(|(%s=%s)(%s=%s))", i.userAttributeMap.userName, ldap.EscapeFilter(nameOrID), i.userAttributeMap.id, idString)
|
||||
} else {
|
||||
@@ -564,9 +564,19 @@ func (i *LDAP) GetUser(ctx context.Context, nameOrID string, oreq *godata.GoData
|
||||
|
||||
// GetUsers implements the Backend Interface.
|
||||
func (i *LDAP) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*libregraph.User, error) {
|
||||
return i.FilterUsers(ctx, oreq, nil)
|
||||
}
|
||||
|
||||
// FilterUsers implements the Backend Interface.
|
||||
func (i *LDAP) FilterUsers(ctx context.Context, oreq *godata.GoDataRequest, filter *godata.ParseNode) ([]*libregraph.User, error) {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
logger.Debug().Str("backend", "ldap").Msg("GetUsers")
|
||||
|
||||
queryFilter, err := i.oDataFilterToLDAPFilter(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
search, err := GetSearchValues(oreq.Query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -587,7 +597,7 @@ func (i *LDAP) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*lib
|
||||
i.userAttributeMap.displayName, search,
|
||||
)
|
||||
}
|
||||
userFilter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.userFilter, i.userObjectClass, userFilter)
|
||||
userFilter = fmt.Sprintf("(&%s(objectClass=%s)%s%s)", i.userFilter, i.userObjectClass, queryFilter, userFilter)
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
i.userBaseDN, i.userScope, ldap.NeverDerefAliases, 0, 0, false,
|
||||
userFilter,
|
||||
@@ -612,13 +622,16 @@ func (i *LDAP) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*lib
|
||||
return nil, i.mapLDAPError(err, errMap)
|
||||
}
|
||||
|
||||
users := make([]*libregraph.User, 0, len(res.Entries))
|
||||
usersEnabledState, err := i.usersEnabledState(res.Entries)
|
||||
return i.usersFromLDAPEntries(res.Entries, exp)
|
||||
}
|
||||
|
||||
func (i *LDAP) usersFromLDAPEntries(entries []*ldap.Entry, exp []string) ([]*libregraph.User, error) {
|
||||
usersEnabledState, err := i.usersEnabledState(entries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, e := range res.Entries {
|
||||
users := make([]*libregraph.User, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
u := i.createUserModelFromLDAP(e)
|
||||
// Skip invalid LDAP users
|
||||
if u == nil {
|
||||
@@ -1286,6 +1299,55 @@ func (i *LDAP) getLastSignTime(e *ldap.Entry) (*time.Time, error) {
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (i *LDAP) oDataFilterToLDAPFilter(filter *godata.ParseNode) (string, error) {
|
||||
if filter == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if filter.Token.Type != godata.ExpressionTokenLogical {
|
||||
return "", ErrUnsupportedFilter
|
||||
}
|
||||
|
||||
if filter.Token.Value != "le" {
|
||||
return "", ErrUnsupportedFilter
|
||||
}
|
||||
|
||||
if !isLastSuccessFullSignInDateTimeFilter(filter.Children[0]) {
|
||||
return "", ErrUnsupportedFilter
|
||||
}
|
||||
|
||||
if filter.Children[1].Token.Type != godata.ExpressionTokenDateTime {
|
||||
return "", ErrUnsupportedFilter
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, filter.Children[1].Token.Value)
|
||||
if err != nil {
|
||||
return "", godata.BadRequestError("invalid date format")
|
||||
}
|
||||
|
||||
ldapDateTime := parsed.UTC().Format(ldapDateFormat)
|
||||
return fmt.Sprintf("(%s<=%s)", i.userAttributeMap.lastSignIn, ldap.EscapeFilter(ldapDateTime)), nil
|
||||
}
|
||||
|
||||
func isLastSuccessFullSignInDateTimeFilter(node *godata.ParseNode) bool {
|
||||
if node.Token.Type != godata.ExpressionTokenNav {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(node.Children) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
if node.Children[0].Token.Value != "signInActivity" {
|
||||
return false
|
||||
}
|
||||
|
||||
if node.Children[1].Token.Value != "lastSuccessfulSignInDateTime" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isUserEnabledUpdate(user libregraph.UserUpdate) bool {
|
||||
switch {
|
||||
case user.Id != nil, user.DisplayName != nil,
|
||||
|
||||
@@ -289,6 +289,66 @@ func (_c *Backend_DeleteUser_Call) RunAndReturn(run func(context.Context, string
|
||||
return _c
|
||||
}
|
||||
|
||||
// FilterUsers provides a mock function with given fields: ctx, oreq, filter
|
||||
func (_m *Backend) FilterUsers(ctx context.Context, oreq *godata.GoDataRequest, filter *godata.ParseNode) ([]*libregraph.User, error) {
|
||||
ret := _m.Called(ctx, oreq, filter)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for FilterUsers")
|
||||
}
|
||||
|
||||
var r0 []*libregraph.User
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *godata.GoDataRequest, *godata.ParseNode) ([]*libregraph.User, error)); ok {
|
||||
return rf(ctx, oreq, filter)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *godata.GoDataRequest, *godata.ParseNode) []*libregraph.User); ok {
|
||||
r0 = rf(ctx, oreq, filter)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*libregraph.User)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *godata.GoDataRequest, *godata.ParseNode) error); ok {
|
||||
r1 = rf(ctx, oreq, filter)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Backend_FilterUsers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FilterUsers'
|
||||
type Backend_FilterUsers_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// FilterUsers is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - oreq *godata.GoDataRequest
|
||||
// - filter *godata.ParseNode
|
||||
func (_e *Backend_Expecter) FilterUsers(ctx interface{}, oreq interface{}, filter interface{}) *Backend_FilterUsers_Call {
|
||||
return &Backend_FilterUsers_Call{Call: _e.mock.On("FilterUsers", ctx, oreq, filter)}
|
||||
}
|
||||
|
||||
func (_c *Backend_FilterUsers_Call) Run(run func(ctx context.Context, oreq *godata.GoDataRequest, filter *godata.ParseNode)) *Backend_FilterUsers_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(*godata.GoDataRequest), args[2].(*godata.ParseNode))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Backend_FilterUsers_Call) Return(_a0 []*libregraph.User, _a1 error) *Backend_FilterUsers_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Backend_FilterUsers_Call) RunAndReturn(run func(context.Context, *godata.GoDataRequest, *godata.ParseNode) ([]*libregraph.User, error)) *Backend_FilterUsers_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetGroup provides a mock function with given fields: ctx, nameOrID, queryParam
|
||||
func (_m *Backend) GetGroup(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.Group, error) {
|
||||
ret := _m.Called(ctx, nameOrID, queryParam)
|
||||
|
||||
@@ -137,6 +137,8 @@ func (g Graph) applyFilterLogical(ctx context.Context, req *godata.GoDataRequest
|
||||
return g.applyFilterLogicalOr(ctx, req, root.Children[0], root.Children[1])
|
||||
case "eq":
|
||||
return g.applyFilterEq(ctx, req, root.Children[0], root.Children[1])
|
||||
case "le":
|
||||
return g.applyFilterLessOrEqual(ctx, req, root)
|
||||
}
|
||||
logger.Debug().Str("Token", root.Token.Value).Msg("unsupported logical filter")
|
||||
return users, unsupportedFilterError()
|
||||
@@ -245,6 +247,11 @@ func (g Graph) applyFilterEq(ctx context.Context, req *godata.GoDataRequest, ope
|
||||
return users, unsupportedFilterError()
|
||||
}
|
||||
|
||||
func (g Graph) applyFilterLessOrEqual(ctx context.Context, req *godata.GoDataRequest, filterRoot *godata.ParseNode) (users []*libregraph.User, err error) {
|
||||
// Assume that our identity backend is able to apply this filter
|
||||
return g.identityBackend.FilterUsers(ctx, req, filterRoot)
|
||||
}
|
||||
|
||||
func (g Graph) applyFilterLambda(ctx context.Context, req *godata.GoDataRequest, nodes []*godata.ParseNode) (users []*libregraph.User, err error) {
|
||||
logger := g.logger.SubloggerWithRequestID(ctx)
|
||||
if len(nodes) != 2 {
|
||||
|
||||
Reference in New Issue
Block a user