mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-24 16:41:35 -04:00
feat(graph/education): Add support of 'eq' filters on users
This adds support of simple OData filters on the 'education/users' endpoint. Filters of the type '$filter=<attr> eq <value>' are supported now for the following educationUser properties: "displayname", "mail", "userType", "primaryRole" and "externalId" Closes: #1599
This commit is contained in:
@@ -107,6 +107,8 @@ type EducationBackend interface {
|
||||
GetEducationUser(ctx context.Context, nameOrID string) (*libregraph.EducationUser, error)
|
||||
// GetEducationUsers lists all education users
|
||||
GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error)
|
||||
// FilterEducationUsersByAttribute list all education users where and attribute matches a value, e.g. all users with a given externalid
|
||||
FilterEducationUsersByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationUser, error)
|
||||
|
||||
// GetEducationClassTeachers returns the EducationUser teachers for an EducationClass
|
||||
GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error)
|
||||
|
||||
@@ -119,6 +119,11 @@ func (i *ErrEducationBackend) GetEducationUsers(ctx context.Context) ([]*libregr
|
||||
return nil, errNotImplemented
|
||||
}
|
||||
|
||||
// FilterEducationUsersByAttribute implements the EducationBackend interface for the ErrEducationBackend backend.
|
||||
func (i *ErrEducationBackend) FilterEducationUsersByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationUser, error) {
|
||||
return nil, errNotImplemented
|
||||
}
|
||||
|
||||
// GetEducationClassTeachers implements the EducationBackend interface for the ErrEducationBackend backend.
|
||||
func (i *ErrEducationBackend) GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error) {
|
||||
return nil, errNotImplemented
|
||||
|
||||
@@ -252,6 +252,60 @@ func (i *LDAP) GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUs
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (i *LDAP) FilterEducationUsersByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationUser, error) {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx).With().Str("func", "FilterEducationUsersByAttribute").Logger()
|
||||
logger.Debug().Str("backend", "ldap").Str("attribute", attr).Str("value", value).Msg("")
|
||||
|
||||
var ldapAttr string
|
||||
switch attr {
|
||||
case "displayname":
|
||||
ldapAttr = i.userAttributeMap.displayName
|
||||
case "mail":
|
||||
ldapAttr = i.userAttributeMap.mail
|
||||
case "userType":
|
||||
ldapAttr = i.userAttributeMap.userType
|
||||
case "primaryRole":
|
||||
ldapAttr = i.educationConfig.userAttributeMap.primaryRole
|
||||
case "externalId":
|
||||
ldapAttr = i.educationConfig.userAttributeMap.externalID
|
||||
default:
|
||||
return nil, errorcode.New(errorcode.InvalidRequest, fmt.Sprintf("filtering by attribute '%s' is not supported", attr))
|
||||
}
|
||||
filter := fmt.Sprintf("(&%s(objectClass=%s)(%s=%s))", i.userFilter, i.educationConfig.userObjectClass, ldap.EscapeFilter(ldapAttr), ldap.EscapeFilter(value))
|
||||
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
i.userBaseDN,
|
||||
i.userScope,
|
||||
ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
i.getEducationUserAttrTypes(),
|
||||
nil,
|
||||
)
|
||||
logger.Debug().Str("base", searchRequest.BaseDN).
|
||||
Str("filter", searchRequest.Filter).
|
||||
Int("scope", searchRequest.Scope).
|
||||
Int("sizelimit", searchRequest.SizeLimit).
|
||||
Interface("attributes", searchRequest.Attributes).
|
||||
Msg("LDAP Search Request")
|
||||
|
||||
res, err := i.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
|
||||
}
|
||||
|
||||
users := make([]*libregraph.EducationUser, 0, len(res.Entries))
|
||||
|
||||
for _, e := range res.Entries {
|
||||
u := i.createEducationUserModelFromLDAP(e)
|
||||
// Skip invalid LDAP users
|
||||
if u == nil {
|
||||
continue
|
||||
}
|
||||
users = append(users, u)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (i *LDAP) educationUserToUser(eduUser libregraph.EducationUser) *libregraph.User {
|
||||
user := libregraph.NewUser(*eduUser.DisplayName, *eduUser.OnPremisesSamAccountName)
|
||||
user.Surname = eduUser.Surname
|
||||
|
||||
@@ -5,12 +5,14 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks"
|
||||
libregraph "github.com/opencloud-eu/libre-graph-api-go"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
const peopleBaseDN = "ou=people,dc=test"
|
||||
|
||||
var eduUserAttrs = []string{
|
||||
"displayname",
|
||||
"entryUUID",
|
||||
@@ -69,7 +71,7 @@ var eduUserEntryWithSchool = ldap.NewEntry("uid=user,ou=people,dc=test",
|
||||
})
|
||||
|
||||
var sr1 *ldap.SearchRequest = &ldap.SearchRequest{
|
||||
BaseDN: "ou=people,dc=test",
|
||||
BaseDN: peopleBaseDN,
|
||||
Scope: 2,
|
||||
SizeLimit: 1,
|
||||
Filter: "(&(objectClass=openCloudEducationUser)(|(uid=abcd-defg)(entryUUID=abcd-defg)))",
|
||||
@@ -77,7 +79,7 @@ var sr1 *ldap.SearchRequest = &ldap.SearchRequest{
|
||||
Controls: []ldap.Control(nil),
|
||||
}
|
||||
var sr2 *ldap.SearchRequest = &ldap.SearchRequest{
|
||||
BaseDN: "ou=people,dc=test",
|
||||
BaseDN: peopleBaseDN,
|
||||
Scope: 2,
|
||||
SizeLimit: 1,
|
||||
Filter: "(&(objectClass=openCloudEducationUser)(|(uid=xxxx-xxxx)(entryUUID=xxxx-xxxx)))",
|
||||
@@ -164,7 +166,7 @@ func TestGetEducationUser(t *testing.T) {
|
||||
func TestGetEducationUsers(t *testing.T) {
|
||||
lm := &mocks.Client{}
|
||||
sr := &ldap.SearchRequest{
|
||||
BaseDN: "ou=people,dc=test",
|
||||
BaseDN: peopleBaseDN,
|
||||
Scope: 2,
|
||||
SizeLimit: 0,
|
||||
Filter: "(objectClass=openCloudEducationUser)",
|
||||
@@ -179,12 +181,30 @@ func TestGetEducationUsers(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestFilterEducationUsersByAttr(t *testing.T) {
|
||||
lm := &mocks.Client{}
|
||||
sr := &ldap.SearchRequest{
|
||||
BaseDN: peopleBaseDN,
|
||||
Scope: 2,
|
||||
SizeLimit: 0,
|
||||
Filter: "(&(objectClass=openCloudEducationUser)(openCloudEducationExternalId=id1234))",
|
||||
Attributes: eduUserAttrs,
|
||||
Controls: []ldap.Control(nil),
|
||||
}
|
||||
lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
|
||||
b, err := getMockedBackend(lm, eduConfig, &logger)
|
||||
assert.Nil(t, err)
|
||||
_, err = b.FilterEducationUsersByAttribute(context.Background(), "externalId", "id1234")
|
||||
lm.AssertNumberOfCalls(t, "Search", 1)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestUpdateEducationUser(t *testing.T) {
|
||||
lm := &mocks.Client{}
|
||||
b, err := getMockedBackend(lm, eduConfig, &logger)
|
||||
assert.Nil(t, err)
|
||||
userSearchReq := &ldap.SearchRequest{
|
||||
BaseDN: "ou=people,dc=test",
|
||||
BaseDN: peopleBaseDN,
|
||||
Scope: 2,
|
||||
SizeLimit: 1,
|
||||
Filter: "(&(objectClass=openCloudEducationUser)(|(uid=testuser)(entryUUID=testuser)))",
|
||||
|
||||
@@ -602,6 +602,80 @@ func (_c *EducationBackend_DeleteEducationUser_Call) RunAndReturn(run func(ctx c
|
||||
return _c
|
||||
}
|
||||
|
||||
// FilterEducationUsersByAttribute provides a mock function for the type EducationBackend
|
||||
func (_mock *EducationBackend) FilterEducationUsersByAttribute(ctx context.Context, attr string, value string) ([]*libregraph.EducationUser, error) {
|
||||
ret := _mock.Called(ctx, attr, value)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for FilterEducationUsersByAttribute")
|
||||
}
|
||||
|
||||
var r0 []*libregraph.EducationUser
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) ([]*libregraph.EducationUser, error)); ok {
|
||||
return returnFunc(ctx, attr, value)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) []*libregraph.EducationUser); ok {
|
||||
r0 = returnFunc(ctx, attr, value)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*libregraph.EducationUser)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = returnFunc(ctx, attr, value)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// EducationBackend_FilterEducationUsersByAttribute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FilterEducationUsersByAttribute'
|
||||
type EducationBackend_FilterEducationUsersByAttribute_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// FilterEducationUsersByAttribute is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - attr string
|
||||
// - value string
|
||||
func (_e *EducationBackend_Expecter) FilterEducationUsersByAttribute(ctx interface{}, attr interface{}, value interface{}) *EducationBackend_FilterEducationUsersByAttribute_Call {
|
||||
return &EducationBackend_FilterEducationUsersByAttribute_Call{Call: _e.mock.On("FilterEducationUsersByAttribute", ctx, attr, value)}
|
||||
}
|
||||
|
||||
func (_c *EducationBackend_FilterEducationUsersByAttribute_Call) Run(run func(ctx context.Context, attr string, value string)) *EducationBackend_FilterEducationUsersByAttribute_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *EducationBackend_FilterEducationUsersByAttribute_Call) Return(educationUsers []*libregraph.EducationUser, err error) *EducationBackend_FilterEducationUsersByAttribute_Call {
|
||||
_c.Call.Return(educationUsers, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *EducationBackend_FilterEducationUsersByAttribute_Call) RunAndReturn(run func(ctx context.Context, attr string, value string) ([]*libregraph.EducationUser, error)) *EducationBackend_FilterEducationUsersByAttribute_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetEducationClass provides a mock function for the type EducationBackend
|
||||
func (_mock *EducationBackend) GetEducationClass(ctx context.Context, namedOrID string) (*libregraph.EducationClass, error) {
|
||||
ret := _mock.Called(ctx, namedOrID)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -21,8 +23,7 @@ import (
|
||||
|
||||
// GetEducationUsers implements the Service interface.
|
||||
func (g Graph) GetEducationUsers(w http.ResponseWriter, r *http.Request) {
|
||||
logger := g.logger.SubloggerWithRequestID(r.Context())
|
||||
logger.Info().Interface("query", r.URL.Query()).Msg("calling get education users")
|
||||
logger := g.logger.SubloggerWithRequestID(r.Context()).With().Str("func", "GetEducationUsers").Logger()
|
||||
sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
|
||||
odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query())
|
||||
if err != nil {
|
||||
@@ -31,12 +32,36 @@ func (g Graph) GetEducationUsers(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug().Interface("query", r.URL.Query()).Msg("calling get education users on backend")
|
||||
users, err := g.identityEducationBackend.GetEducationUsers(r.Context())
|
||||
if err != nil {
|
||||
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get education users from backend")
|
||||
errorcode.RenderError(w, r, err)
|
||||
return
|
||||
var users []*libregraph.EducationUser
|
||||
if odataReq.Query.Filter != nil {
|
||||
attr, value, err := g.getEqualityFilter(r.Context(), odataReq)
|
||||
if err != nil {
|
||||
logger.Debug().Err(err).Str("filter", odataReq.Query.Filter.RawValue).Msg("failed to parse filter")
|
||||
var errcode errorcode.Error
|
||||
var godataerr *godata.GoDataError
|
||||
switch {
|
||||
case errors.As(err, &errcode):
|
||||
errcode.Render(w, r)
|
||||
case errors.As(err, &godataerr):
|
||||
errorcode.GeneralException.Render(w, r, godataerr.ResponseCode, err.Error())
|
||||
default:
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
users, err = g.identityEducationBackend.FilterEducationUsersByAttribute(r.Context(), attr, value)
|
||||
if err != nil {
|
||||
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get education users from backend")
|
||||
errorcode.RenderError(w, r, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
users, err = g.identityEducationBackend.GetEducationUsers(r.Context())
|
||||
if err != nil {
|
||||
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get education users from backend")
|
||||
errorcode.RenderError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
users, err = sortEducationUsers(odataReq, users)
|
||||
@@ -387,3 +412,30 @@ func sortEducationUsers(req *godata.GoDataRequest, users []*libregraph.Education
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (g Graph) getEqualityFilter(ctx context.Context, req *godata.GoDataRequest) (string, string, error) {
|
||||
logger := g.logger.SubloggerWithRequestID(ctx)
|
||||
|
||||
root := req.Query.Filter.Tree
|
||||
|
||||
if root.Token.Type != godata.ExpressionTokenLogical {
|
||||
logger.Debug().Str("filter", req.Query.Filter.RawValue).Msg(unsupportedFilter)
|
||||
return "", "", unsupportedFilterError()
|
||||
}
|
||||
if root.Token.Value != "eq" {
|
||||
logger.Debug().Str("filter", req.Query.Filter.RawValue).Msg(unsupportedFilter)
|
||||
return "", "", unsupportedFilterError()
|
||||
}
|
||||
if len(root.Children) != 2 {
|
||||
logger.Debug().Str("filter", req.Query.Filter.RawValue).Msg(unsupportedFilter)
|
||||
return "", "", unsupportedFilterError()
|
||||
}
|
||||
if root.Children[0].Token.Type != godata.ExpressionTokenLiteral || root.Children[1].Token.Type != godata.ExpressionTokenString {
|
||||
logger.Debug().Str("filter", req.Query.Filter.RawValue).Msg(unsupportedFilter)
|
||||
return "", "", unsupportedFilterError()
|
||||
}
|
||||
|
||||
// unquote
|
||||
value := strings.Trim(root.Children[1].Token.Value, "'")
|
||||
return root.Children[0].Token.Value, value, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
@@ -202,6 +203,17 @@ var _ = Describe("EducationUsers", func() {
|
||||
|
||||
Expect(rr.Code).To(Equal(http.StatusBadRequest))
|
||||
})
|
||||
When("used with a filter", func() {
|
||||
It("fails with an unsupported filter ", func() {
|
||||
svc.GetEducationUsers(rr, httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/users?$filter="+url.QueryEscape("displayName ne 'test'"), nil))
|
||||
Expect(rr.Code).To(Equal(http.StatusNotImplemented))
|
||||
})
|
||||
It("calls the backend with the filter", func() {
|
||||
identityEducationBackend.On("FilterEducationUsersByAttribute", mock.Anything, "externalId", "id1234").Return([]*libregraph.EducationUser{}, nil)
|
||||
svc.GetEducationUsers(rr, httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/users?$filter="+url.QueryEscape("externalId eq 'id1234'"), nil))
|
||||
Expect(rr.Code).To(Equal(http.StatusOK))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetEducationUser", func() {
|
||||
|
||||
@@ -5,14 +5,15 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/CiscoM31/godata"
|
||||
libregraph "github.com/opencloud-eu/libre-graph-api-go"
|
||||
settingsmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/settings/v0"
|
||||
settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0"
|
||||
libregraph "github.com/opencloud-eu/libre-graph-api-go"
|
||||
)
|
||||
|
||||
const (
|
||||
appRoleID = "appRoleId"
|
||||
appRoleAssignments = "appRoleAssignments"
|
||||
unsupportedFilter = "unsupported filter"
|
||||
)
|
||||
|
||||
func invalidFilterError() error {
|
||||
@@ -20,7 +21,7 @@ func invalidFilterError() error {
|
||||
}
|
||||
|
||||
func unsupportedFilterError() error {
|
||||
return godata.NotImplementedError("unsupported filter")
|
||||
return godata.NotImplementedError(unsupportedFilter)
|
||||
}
|
||||
|
||||
func (g Graph) applyUserFilter(ctx context.Context, req *godata.GoDataRequest, root *godata.ParseNode) (users []*libregraph.User, err error) {
|
||||
@@ -38,7 +39,7 @@ func (g Graph) applyUserFilter(ctx context.Context, req *godata.GoDataRequest, r
|
||||
case godata.ExpressionTokenFunc:
|
||||
return g.applyFilterFunction(ctx, req, root)
|
||||
}
|
||||
logger.Debug().Str("filter", req.Query.Filter.RawValue).Msg("filter is not supported")
|
||||
logger.Debug().Str("filter", req.Query.Filter.RawValue).Msg(unsupportedFilter)
|
||||
return users, unsupportedFilterError()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user