Repository: owncloud/ocis
Pull request #5527 (2023-02-07 14:19:11)

graph: some preparation to support $filter queries on /users
This change is basically some  cleanup  in preparation for add $filter support to the graph users endpoint. It removes some uneeded code from the education backend for stuff we don't what to support for now and reworks the identity backend interface to handle parsed odata requests instead of dealing with the raw query params.

update-docs[bot]: Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would create a [changelog](https://github.com/owncloud/ocis/blob/master/changelog/README.md) item based on your changes.

sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=5527)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG) [![C](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/C-16px.png 'C')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG) [7 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL) [5 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL)

[![53.8%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/50-16px.png '53.8%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_coverage&view=list) [53.8% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_coverage&view=list)  
[![7.1%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/10-16px.png '7.1%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_duplicated_lines_density&view=list) [7.1% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_duplicated_lines_density&view=list)


ownclouders: :boom: Acceptance test [<strong>localApiTests-apiContract-ocis</strong>](https://drone.owncloud.com/owncloud/ocis/18831/43/1) failed. Further test are cancelled...
<!-- id: pr-5527 -->

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=5527)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL) [5 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL)

[![53.8%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/50-16px.png '53.8%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_coverage&view=list) [53.8% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_coverage&view=list)  
[![6.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/10-16px.png '6.7%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_duplicated_lines_density&view=list) [6.7% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: SonarCloud Quality Gate failed.&nbsp; &nbsp; [![Quality Gate failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/failed-16px.png 'Quality Gate failed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=5527)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG) [![C](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/C-16px.png 'C')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG) [3 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL) [![B](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/B-16px.png 'B')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL) [75 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL)

[![61.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '61.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_coverage&view=list) [61.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_coverage&view=list)  
[![7.4%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/10-16px.png '7.4%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_duplicated_lines_density&view=list) [7.4% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=5527)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL) [4 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL)

[![61.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '61.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_coverage&view=list) [61.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_coverage&view=list)  
[![7.3%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/10-16px.png '7.3%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_duplicated_lines_density&view=list) [7.3% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=5527)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5527&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL) [4 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5527&resolved=false&types=CODE_SMELL)

[![61.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '61.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_coverage&view=list) [61.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_coverage&view=list)  
[![7.3%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/10-16px.png '7.3%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_duplicated_lines_density&view=list) [7.3% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5527&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/services/graph/pkg/identity/backend.go b/services/graph/pkg/identity/backend.go
index d8c9d877d14..42aaea05254 100644
--- a/services/graph/pkg/identity/backend.go
+++ b/services/graph/pkg/identity/backend.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"net/url"
 
+	"github.com/CiscoM31/godata"
 	cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	libregraph "github.com/owncloud/libre-graph-api-go"
 	"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
@@ -25,8 +26,8 @@ type Backend interface {
 	DeleteUser(ctx context.Context, nameOrID string) error
 	// UpdateUser applies changes to given user, identified by username or id
 	UpdateUser(ctx context.Context, nameOrID string, user libregraph.User) (*libregraph.User, error)
-	GetUser(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.User, error)
-	GetUsers(ctx context.Context, queryParam url.Values) ([]*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)
 
 	// CreateGroup creates the supplied group in the identity backend.
 	CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error)
@@ -48,9 +49,9 @@ type EducationBackend interface {
 	// DeleteSchool deletes a given school, identified by id
 	DeleteEducationSchool(ctx context.Context, id string) error
 	// GetEducationSchool reads a given school by id
-	GetEducationSchool(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationSchool, error)
+	GetEducationSchool(ctx context.Context, nameOrID string) (*libregraph.EducationSchool, error)
 	// GetEducationSchools lists all schools
-	GetEducationSchools(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationSchool, error)
+	GetEducationSchools(ctx context.Context) ([]*libregraph.EducationSchool, error)
 	// UpdateEducationSchool updates attributes of a school
 	UpdateEducationSchool(ctx context.Context, numberOrID string, school libregraph.EducationSchool) (*libregraph.EducationSchool, error)
 	// GetEducationSchoolUsers lists all members of a school
@@ -68,9 +69,9 @@ type EducationBackend interface {
 	RemoveClassFromEducationSchool(ctx context.Context, schoolNumberOrID string, memberID string) error
 
 	// GetEducationClasses lists all classes
-	GetEducationClasses(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationClass, error)
+	GetEducationClasses(ctx context.Context) ([]*libregraph.EducationClass, error)
 	// GetEducationClasses reads a given class by id
-	GetEducationClass(ctx context.Context, namedOrID string, queryParam url.Values) (*libregraph.EducationClass, error)
+	GetEducationClass(ctx context.Context, namedOrID string) (*libregraph.EducationClass, error)
 	// CreateEducationClass creates the supplied education class in the identity backend.
 	CreateEducationClass(ctx context.Context, class libregraph.EducationClass) (*libregraph.EducationClass, error)
 	// DeleteEducationClass deletes the supplied education class in the identity backend.
@@ -86,8 +87,10 @@ type EducationBackend interface {
 	DeleteEducationUser(ctx context.Context, nameOrID string) error
 	// UpdateEducationUser applies changes to given education user, identified by username or id
 	UpdateEducationUser(ctx context.Context, nameOrID string, user libregraph.EducationUser) (*libregraph.EducationUser, error)
-	GetEducationUser(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationUser, error)
-	GetEducationUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationUser, error)
+	// GetEducationUser reads an education user by id or name
+	GetEducationUser(ctx context.Context, nameOrID string) (*libregraph.EducationUser, error)
+	// GetEducationUsers lists all education users
+	GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error)
 }
 
 func CreateUserModelFromCS3(u *cs3.User) *libregraph.User {
diff --git a/services/graph/pkg/identity/cs3.go b/services/graph/pkg/identity/cs3.go
index 2a3335d3ce6..d6eab4029d6 100644
--- a/services/graph/pkg/identity/cs3.go
+++ b/services/graph/pkg/identity/cs3.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"net/url"
 
+	"github.com/CiscoM31/godata"
 	cs3group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
 	cs3user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
@@ -39,7 +40,8 @@ func (i *CS3) UpdateUser(ctx context.Context, nameOrID string, user libregraph.U
 	return nil, errNotImplemented
 }
 
-func (i *CS3) GetUser(ctx context.Context, userID string, queryParam url.Values) (*libregraph.User, error) {
+// GetUser implements the Backend Interface.
+func (i *CS3) GetUser(ctx context.Context, userID string, _ *godata.GoDataRequest) (*libregraph.User, error) {
 	logger := i.Logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "cs3").Msg("GetUser")
 	client, err := pool.GetGatewayServiceClient(i.Config.Address, i.Config.GetRevaOptions()...)
@@ -67,7 +69,8 @@ func (i *CS3) GetUser(ctx context.Context, userID string, queryParam url.Values)
 	return CreateUserModelFromCS3(res.User), nil
 }
 
-func (i *CS3) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.User, error) {
+// GetUsers implements the Backend Interface.
+func (i *CS3) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*libregraph.User, error) {
 	logger := i.Logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "cs3").Msg("GetUsers")
 	client, err := pool.GetGatewayServiceClient(i.Config.Address, i.Config.GetRevaOptions()...)
@@ -76,9 +79,9 @@ func (i *CS3) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregrap
 		return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
 	}
 
-	search := queryParam.Get("search")
-	if search == "" {
-		search = queryParam.Get("$search")
+	search, err := GetSearchValues(oreq.Query)
+	if err != nil {
+		return nil, err
 	}
 
 	res, err := client.FindUsers(ctx, &cs3user.FindUsersRequest{
@@ -107,6 +110,7 @@ func (i *CS3) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregrap
 	return users, nil
 }
 
+// GetGroups implements the Backend Interface.
 func (i *CS3) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregraph.Group, error) {
 	logger := i.Logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "cs3").Msg("GetGroups")
@@ -153,6 +157,7 @@ func (i *CS3) CreateGroup(ctx context.Context, group libregraph.Group) (*libregr
 	return nil, errorcode.New(errorcode.NotSupported, "not implemented")
 }
 
+// GetGroup implements the Backend Interface.
 func (i *CS3) GetGroup(ctx context.Context, groupID string, queryParam url.Values) (*libregraph.Group, error) {
 	logger := i.Logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "cs3").Msg("GetGroup")
diff --git a/services/graph/pkg/identity/err_education.go b/services/graph/pkg/identity/err_education.go
index 323e01d7937..e68e2e030c0 100644
--- a/services/graph/pkg/identity/err_education.go
+++ b/services/graph/pkg/identity/err_education.go
@@ -2,7 +2,6 @@ package identity
 
 import (
 	"context"
-	"net/url"
 
 	libregraph "github.com/owncloud/libre-graph-api-go"
 )
@@ -21,12 +20,12 @@ func (i *ErrEducationBackend) DeleteEducationSchool(ctx context.Context, id stri
 }
 
 // GetEducationSchool implements the EducationBackend interface for the ErrEducationBackend backend.
-func (i *ErrEducationBackend) GetEducationSchool(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationSchool, error) {
+func (i *ErrEducationBackend) GetEducationSchool(ctx context.Context, nameOrID string) (*libregraph.EducationSchool, error) {
 	return nil, errNotImplemented
 }
 
 // GetEducationSchools implements the EducationBackend interface for the ErrEducationBackend backend.
-func (i *ErrEducationBackend) GetEducationSchools(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationSchool, error) {
+func (i *ErrEducationBackend) GetEducationSchools(ctx context.Context) ([]*libregraph.EducationSchool, error) {
 	return nil, errNotImplemented
 }
 
@@ -66,12 +65,12 @@ func (i *ErrEducationBackend) RemoveUserFromEducationSchool(ctx context.Context,
 }
 
 // GetEducationClasses implements the EducationBackend interface
-func (i *ErrEducationBackend) GetEducationClasses(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationClass, error) {
+func (i *ErrEducationBackend) GetEducationClasses(ctx context.Context) ([]*libregraph.EducationClass, error) {
 	return nil, errNotImplemented
 }
 
 // GetEducationClass implements the EducationBackend interface
-func (i *ErrEducationBackend) GetEducationClass(ctx context.Context, namedOrID string, queryParam url.Values) (*libregraph.EducationClass, error) {
+func (i *ErrEducationBackend) GetEducationClass(ctx context.Context, namedOrID string) (*libregraph.EducationClass, error) {
 	return nil, errNotImplemented
 }
 
@@ -111,11 +110,11 @@ func (i *ErrEducationBackend) UpdateEducationUser(ctx context.Context, nameOrID
 }
 
 // GetEducationUser implements the EducationBackend interface for the ErrEducationBackend backend.
-func (i *ErrEducationBackend) GetEducationUser(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationUser, error) {
+func (i *ErrEducationBackend) GetEducationUser(ctx context.Context, nameOrID string) (*libregraph.EducationUser, error) {
 	return nil, errNotImplemented
 }
 
 // GetEducationUsers implements the EducationBackend interface for the ErrEducationBackend backend.
-func (i *ErrEducationBackend) GetEducationUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationUser, error) {
+func (i *ErrEducationBackend) GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error) {
 	return nil, errNotImplemented
 }
diff --git a/services/graph/pkg/identity/ldap.go b/services/graph/pkg/identity/ldap.go
index ade4a9e2d04..f7b7c117421 100644
--- a/services/graph/pkg/identity/ldap.go
+++ b/services/graph/pkg/identity/ldap.go
@@ -4,9 +4,8 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"net/url"
-	"strings"
 
+	"github.com/CiscoM31/godata"
 	"github.com/go-ldap/ldap/v3"
 	"github.com/gofrs/uuid"
 	libregraph "github.com/owncloud/libre-graph-api-go"
@@ -369,20 +368,27 @@ func (i *LDAP) getLDAPUserByFilter(filter string) (*ldap.Entry, error) {
 	return i.searchLDAPEntryByFilter(i.userBaseDN, attrs, filter)
 }
 
-func (i *LDAP) GetUser(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.User, error) {
+// GetUser implements the Backend Interface.
+func (i *LDAP) GetUser(ctx context.Context, nameOrID string, oreq *godata.GoDataRequest) (*libregraph.User, error) {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("GetUser")
+
 	e, err := i.getLDAPUserByNameOrID(nameOrID)
 	if err != nil {
 		return nil, err
 	}
+
 	u := i.createUserModelFromLDAP(e)
 	if u == nil {
 		return nil, ErrNotFound
 	}
-	sel := strings.Split(queryParam.Get("$select"), ",")
-	exp := strings.Split(queryParam.Get("$expand"), ",")
-	if slices.Contains(sel, "memberOf") || slices.Contains(exp, "memberOf") {
+
+	exp, err := GetExpandValues(oreq.Query)
+	if err != nil {
+		return nil, err
+	}
+
+	if slices.Contains(exp, "memberOf") {
 		userGroups, err := i.getGroupsForUser(e.DN)
 		if err != nil {
 			return nil, err
@@ -392,14 +398,21 @@ func (i *LDAP) GetUser(ctx context.Context, nameOrID string, queryParam url.Valu
 	return u, nil
 }
 
-func (i *LDAP) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.User, error) {
+// GetUsers implements the Backend Interface.
+func (i *LDAP) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*libregraph.User, error) {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("GetUsers")
 
-	search := queryParam.Get("search")
-	if search == "" {
-		search = queryParam.Get("$search")
+	search, err := GetSearchValues(oreq.Query)
+	if err != nil {
+		return nil, err
 	}
+
+	exp, err := GetExpandValues(oreq.Query)
+	if err != nil {
+		return nil, err
+	}
+
 	var userFilter string
 	if search != "" {
 		search = ldap.EscapeFilter(search)
@@ -439,14 +452,12 @@ func (i *LDAP) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregra
 	users := make([]*libregraph.User, 0, len(res.Entries))
 
 	for _, e := range res.Entries {
-		sel := strings.Split(queryParam.Get("$select"), ",")
-		exp := strings.Split(queryParam.Get("$expand"), ",")
 		u := i.createUserModelFromLDAP(e)
 		// Skip invalid LDAP users
 		if u == nil {
 			continue
 		}
-		if slices.Contains(sel, "memberOf") || slices.Contains(exp, "memberOf") {
+		if slices.Contains(exp, "memberOf") {
 			userGroups, err := i.getGroupsForUser(e.DN)
 			if err != nil {
 				return nil, err
diff --git a/services/graph/pkg/identity/ldap_education_class.go b/services/graph/pkg/identity/ldap_education_class.go
index 96be43e0e07..3ea687c4620 100644
--- a/services/graph/pkg/identity/ldap_education_class.go
+++ b/services/graph/pkg/identity/ldap_education_class.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"net/url"
 
 	"github.com/go-ldap/ldap/v3"
 	libregraph "github.com/owncloud/libre-graph-api-go"
@@ -25,26 +24,11 @@ func newEducationClassAttributeMap() educationClassAttributeMap {
 }
 
 // GetEducationClasses implements the EducationBackend interface for the LDAP backend.
-func (i *LDAP) GetEducationClasses(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationClass, error) {
+func (i *LDAP) GetEducationClasses(ctx context.Context) ([]*libregraph.EducationClass, error) {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("GetEducationClasses")
 
-	search := queryParam.Get("search")
-	if search == "" {
-		search = queryParam.Get("$search")
-	}
-
-	var classFilter string
-	if search != "" {
-		search = ldap.EscapeFilter(search)
-		classFilter = fmt.Sprintf(
-			"(|(%s=%s)(%s=%s*)(%s=%s*))",
-			i.educationConfig.classAttributeMap.externalID, search,
-			i.groupAttributeMap.name, search,
-			i.groupAttributeMap.id, search,
-		)
-	}
-	classFilter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.groupFilter, i.educationConfig.classObjectClass, classFilter)
+	classFilter := fmt.Sprintf("(&%s(objectClass=%s))", i.groupFilter, i.educationConfig.classObjectClass)
 
 	classAttrs := i.getEducationClassAttrTypes(false)
 
@@ -105,7 +89,7 @@ func (i *LDAP) CreateEducationClass(ctx context.Context, class libregraph.Educat
 }
 
 // GetEducationClass implements the EducationBackend interface for the LDAP backend.
-func (i *LDAP) GetEducationClass(ctx context.Context, id string, queryParam url.Values) (*libregraph.EducationClass, error) {
+func (i *LDAP) GetEducationClass(ctx context.Context, id string) (*libregraph.EducationClass, error) {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("GetEducationClass")
 	e, err := i.getEducationClassByID(id, false)
diff --git a/services/graph/pkg/identity/ldap_education_school.go b/services/graph/pkg/identity/ldap_education_school.go
index d0a44d8a436..884366d386c 100644
--- a/services/graph/pkg/identity/ldap_education_school.go
+++ b/services/graph/pkg/identity/ldap_education_school.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"net/url"
 
 	"github.com/go-ldap/ldap/v3"
 	"github.com/gofrs/uuid"
@@ -292,7 +291,7 @@ func (i *LDAP) DeleteEducationSchool(ctx context.Context, id string) error {
 }
 
 // GetEducationSchool implements the EducationBackend interface for the LDAP backend.
-func (i *LDAP) GetEducationSchool(ctx context.Context, numberOrID string, queryParam url.Values) (*libregraph.EducationSchool, error) {
+func (i *LDAP) GetEducationSchool(ctx context.Context, numberOrID string) (*libregraph.EducationSchool, error) {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("GetEducationSchool")
 	e, err := i.getSchoolByNumberOrID(numberOrID)
@@ -304,7 +303,7 @@ func (i *LDAP) GetEducationSchool(ctx context.Context, numberOrID string, queryP
 }
 
 // GetEducationSchools implements the EducationBackend interface for the LDAP backend.
-func (i *LDAP) GetEducationSchools(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationSchool, error) {
+func (i *LDAP) GetEducationSchools(ctx context.Context) ([]*libregraph.EducationSchool, error) {
 	var filter string
 	filter = fmt.Sprintf("(objectClass=%s)", i.educationConfig.schoolObjectClass)
 
diff --git a/services/graph/pkg/identity/ldap_education_user.go b/services/graph/pkg/identity/ldap_education_user.go
index 3df8dffba72..e346bd9897f 100644
--- a/services/graph/pkg/identity/ldap_education_user.go
+++ b/services/graph/pkg/identity/ldap_education_user.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"net/url"
 	"strings"
 
 	"github.com/go-ldap/ldap/v3"
@@ -82,7 +81,7 @@ func (i *LDAP) UpdateEducationUser(ctx context.Context, nameOrID string, user li
 }
 
 // GetEducationUser implements the EducationBackend interface for the LDAP backend.
-func (i *LDAP) GetEducationUser(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationUser, error) {
+func (i *LDAP) GetEducationUser(ctx context.Context, nameOrID string) (*libregraph.EducationUser, error) {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("GetEducationUser")
 	e, err := i.getEducationUserByNameOrID(nameOrID)
@@ -97,29 +96,16 @@ func (i *LDAP) GetEducationUser(ctx context.Context, nameOrID string, queryParam
 }
 
 // GetEducationUsers implements the EducationBackend interface for the LDAP backend.
-func (i *LDAP) GetEducationUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationUser, error) {
+func (i *LDAP) GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error) {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("GetEducationUsers")
 
-	search := queryParam.Get("search")
-	if search == "" {
-		search = queryParam.Get("$search")
-	}
 	var userFilter string
-	if search != "" {
-		search = ldap.EscapeFilter(search)
-		userFilter = fmt.Sprintf(
-			"(|(%s=%s*)(%s=%s*)(%s=%s*))",
-			i.userAttributeMap.userName, search,
-			i.userAttributeMap.mail, search,
-			i.userAttributeMap.displayName, search,
-		)
-	}
 
-	if userFilter == "" && i.userFilter == "" {
+	if i.userFilter == "" {
 		userFilter = fmt.Sprintf("(objectClass=%s)", i.educationConfig.userObjectClass)
 	} else {
-		userFilter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.userFilter, i.educationConfig.userObjectClass, userFilter)
+		userFilter = fmt.Sprintf("(&%s(objectClass=%s))", i.userFilter, i.educationConfig.userObjectClass)
 	}
 
 	searchRequest := ldap.NewSearchRequest(
diff --git a/services/graph/pkg/identity/mocks/backend.go b/services/graph/pkg/identity/mocks/backend.go
index 5c192ab9210..9fcdcd8c195 100644
--- a/services/graph/pkg/identity/mocks/backend.go
+++ b/services/graph/pkg/identity/mocks/backend.go
@@ -5,6 +5,8 @@ package mocks
 import (
 	context "context"
 
+	godata "github.com/CiscoM31/godata"
+
 	libregraph "github.com/owncloud/libre-graph-api-go"
 
 	mock "github.com/stretchr/testify/mock"
@@ -174,13 +176,13 @@ func (_m *Backend) GetGroups(ctx context.Context, queryParam url.Values) ([]*lib
 	return r0, r1
 }
 
-// GetUser provides a mock function with given fields: ctx, nameOrID, queryParam
-func (_m *Backend) GetUser(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.User, error) {
-	ret := _m.Called(ctx, nameOrID, queryParam)
+// GetUser provides a mock function with given fields: ctx, nameOrID, oreq
+func (_m *Backend) GetUser(ctx context.Context, nameOrID string, oreq *godata.GoDataRequest) (*libregraph.User, error) {
+	ret := _m.Called(ctx, nameOrID, oreq)
 
 	var r0 *libregraph.User
-	if rf, ok := ret.Get(0).(func(context.Context, string, url.Values) *libregraph.User); ok {
-		r0 = rf(ctx, nameOrID, queryParam)
+	if rf, ok := ret.Get(0).(func(context.Context, string, *godata.GoDataRequest) *libregraph.User); ok {
+		r0 = rf(ctx, nameOrID, oreq)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).(*libregraph.User)
@@ -188,8 +190,8 @@ func (_m *Backend) GetUser(ctx context.Context, nameOrID string, queryParam url.
 	}
 
 	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, string, url.Values) error); ok {
-		r1 = rf(ctx, nameOrID, queryParam)
+	if rf, ok := ret.Get(1).(func(context.Context, string, *godata.GoDataRequest) error); ok {
+		r1 = rf(ctx, nameOrID, oreq)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -197,13 +199,13 @@ func (_m *Backend) GetUser(ctx context.Context, nameOrID string, queryParam url.
 	return r0, r1
 }
 
-// GetUsers provides a mock function with given fields: ctx, queryParam
-func (_m *Backend) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.User, error) {
-	ret := _m.Called(ctx, queryParam)
+// GetUsers provides a mock function with given fields: ctx, oreq
+func (_m *Backend) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*libregraph.User, error) {
+	ret := _m.Called(ctx, oreq)
 
 	var r0 []*libregraph.User
-	if rf, ok := ret.Get(0).(func(context.Context, url.Values) []*libregraph.User); ok {
-		r0 = rf(ctx, queryParam)
+	if rf, ok := ret.Get(0).(func(context.Context, *godata.GoDataRequest) []*libregraph.User); ok {
+		r0 = rf(ctx, oreq)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).([]*libregraph.User)
@@ -211,8 +213,8 @@ func (_m *Backend) GetUsers(ctx context.Context, queryParam url.Values) ([]*libr
 	}
 
 	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, url.Values) error); ok {
-		r1 = rf(ctx, queryParam)
+	if rf, ok := ret.Get(1).(func(context.Context, *godata.GoDataRequest) error); ok {
+		r1 = rf(ctx, oreq)
 	} else {
 		r1 = ret.Error(1)
 	}
diff --git a/services/graph/pkg/identity/mocks/education_backend.go b/services/graph/pkg/identity/mocks/education_backend.go
index dd6af905e85..42e7cbdbaf6 100644
--- a/services/graph/pkg/identity/mocks/education_backend.go
+++ b/services/graph/pkg/identity/mocks/education_backend.go
@@ -8,8 +8,6 @@ import (
 	libregraph "github.com/owncloud/libre-graph-api-go"
 
 	mock "github.com/stretchr/testify/mock"
-
-	url "net/url"
 )
 
 // EducationBackend is an autogenerated mock type for the EducationBackend type
@@ -156,13 +154,13 @@ func (_m *EducationBackend) DeleteEducationUser(ctx context.Context, nameOrID st
 	return r0
 }
 
-// GetEducationClass provides a mock function with given fields: ctx, namedOrID, queryParam
-func (_m *EducationBackend) GetEducationClass(ctx context.Context, namedOrID string, queryParam url.Values) (*libregraph.EducationClass, error) {
-	ret := _m.Called(ctx, namedOrID, queryParam)
+// GetEducationClass provides a mock function with given fields: ctx, namedOrID
+func (_m *EducationBackend) GetEducationClass(ctx context.Context, namedOrID string) (*libregraph.EducationClass, error) {
+	ret := _m.Called(ctx, namedOrID)
 
 	var r0 *libregraph.EducationClass
-	if rf, ok := ret.Get(0).(func(context.Context, string, url.Values) *libregraph.EducationClass); ok {
-		r0 = rf(ctx, namedOrID, queryParam)
+	if rf, ok := ret.Get(0).(func(context.Context, string) *libregraph.EducationClass); ok {
+		r0 = rf(ctx, namedOrID)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).(*libregraph.EducationClass)
@@ -170,8 +168,8 @@ func (_m *EducationBackend) GetEducationClass(ctx context.Context, namedOrID str
 	}
 
 	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, string, url.Values) error); ok {
-		r1 = rf(ctx, namedOrID, queryParam)
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, namedOrID)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -202,13 +200,13 @@ func (_m *EducationBackend) GetEducationClassMembers(ctx context.Context, nameOr
 	return r0, r1
 }
 
-// GetEducationClasses provides a mock function with given fields: ctx, queryParam
-func (_m *EducationBackend) GetEducationClasses(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationClass, error) {
-	ret := _m.Called(ctx, queryParam)
+// GetEducationClasses provides a mock function with given fields: ctx
+func (_m *EducationBackend) GetEducationClasses(ctx context.Context) ([]*libregraph.EducationClass, error) {
+	ret := _m.Called(ctx)
 
 	var r0 []*libregraph.EducationClass
-	if rf, ok := ret.Get(0).(func(context.Context, url.Values) []*libregraph.EducationClass); ok {
-		r0 = rf(ctx, queryParam)
+	if rf, ok := ret.Get(0).(func(context.Context) []*libregraph.EducationClass); ok {
+		r0 = rf(ctx)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).([]*libregraph.EducationClass)
@@ -216,8 +214,8 @@ func (_m *EducationBackend) GetEducationClasses(ctx context.Context, queryParam
 	}
 
 	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, url.Values) error); ok {
-		r1 = rf(ctx, queryParam)
+	if rf, ok := ret.Get(1).(func(context.Context) error); ok {
+		r1 = rf(ctx)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -225,13 +223,13 @@ func (_m *EducationBackend) GetEducationClasses(ctx context.Context, queryParam
 	return r0, r1
 }
 
-// GetEducationSchool provides a mock function with given fields: ctx, nameOrID, queryParam
-func (_m *EducationBackend) GetEducationSchool(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationSchool, error) {
-	ret := _m.Called(ctx, nameOrID, queryParam)
+// GetEducationSchool provides a mock function with given fields: ctx, nameOrID
+func (_m *EducationBackend) GetEducationSchool(ctx context.Context, nameOrID string) (*libregraph.EducationSchool, error) {
+	ret := _m.Called(ctx, nameOrID)
 
 	var r0 *libregraph.EducationSchool
-	if rf, ok := ret.Get(0).(func(context.Context, string, url.Values) *libregraph.EducationSchool); ok {
-		r0 = rf(ctx, nameOrID, queryParam)
+	if rf, ok := ret.Get(0).(func(context.Context, string) *libregraph.EducationSchool); ok {
+		r0 = rf(ctx, nameOrID)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).(*libregraph.EducationSchool)
@@ -239,8 +237,8 @@ func (_m *EducationBackend) GetEducationSchool(ctx context.Context, nameOrID str
 	}
 
 	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, string, url.Values) error); ok {
-		r1 = rf(ctx, nameOrID, queryParam)
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, nameOrID)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -294,13 +292,13 @@ func (_m *EducationBackend) GetEducationSchoolUsers(ctx context.Context, id stri
 	return r0, r1
 }
 
-// GetEducationSchools provides a mock function with given fields: ctx, queryParam
-func (_m *EducationBackend) GetEducationSchools(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationSchool, error) {
-	ret := _m.Called(ctx, queryParam)
+// GetEducationSchools provides a mock function with given fields: ctx
+func (_m *EducationBackend) GetEducationSchools(ctx context.Context) ([]*libregraph.EducationSchool, error) {
+	ret := _m.Called(ctx)
 
 	var r0 []*libregraph.EducationSchool
-	if rf, ok := ret.Get(0).(func(context.Context, url.Values) []*libregraph.EducationSchool); ok {
-		r0 = rf(ctx, queryParam)
+	if rf, ok := ret.Get(0).(func(context.Context) []*libregraph.EducationSchool); ok {
+		r0 = rf(ctx)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).([]*libregraph.EducationSchool)
@@ -308,8 +306,8 @@ func (_m *EducationBackend) GetEducationSchools(ctx context.Context, queryParam
 	}
 
 	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, url.Values) error); ok {
-		r1 = rf(ctx, queryParam)
+	if rf, ok := ret.Get(1).(func(context.Context) error); ok {
+		r1 = rf(ctx)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -317,13 +315,13 @@ func (_m *EducationBackend) GetEducationSchools(ctx context.Context, queryParam
 	return r0, r1
 }
 
-// GetEducationUser provides a mock function with given fields: ctx, nameOrID, queryParam
-func (_m *EducationBackend) GetEducationUser(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationUser, error) {
-	ret := _m.Called(ctx, nameOrID, queryParam)
+// GetEducationUser provides a mock function with given fields: ctx, nameOrID
+func (_m *EducationBackend) GetEducationUser(ctx context.Context, nameOrID string) (*libregraph.EducationUser, error) {
+	ret := _m.Called(ctx, nameOrID)
 
 	var r0 *libregraph.EducationUser
-	if rf, ok := ret.Get(0).(func(context.Context, string, url.Values) *libregraph.EducationUser); ok {
-		r0 = rf(ctx, nameOrID, queryParam)
+	if rf, ok := ret.Get(0).(func(context.Context, string) *libregraph.EducationUser); ok {
+		r0 = rf(ctx, nameOrID)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).(*libregraph.EducationUser)
@@ -331,8 +329,8 @@ func (_m *EducationBackend) GetEducationUser(ctx context.Context, nameOrID strin
 	}
 
 	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, string, url.Values) error); ok {
-		r1 = rf(ctx, nameOrID, queryParam)
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, nameOrID)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -340,13 +338,13 @@ func (_m *EducationBackend) GetEducationUser(ctx context.Context, nameOrID strin
 	return r0, r1
 }
 
-// GetEducationUsers provides a mock function with given fields: ctx, queryParam
-func (_m *EducationBackend) GetEducationUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationUser, error) {
-	ret := _m.Called(ctx, queryParam)
+// GetEducationUsers provides a mock function with given fields: ctx
+func (_m *EducationBackend) GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error) {
+	ret := _m.Called(ctx)
 
 	var r0 []*libregraph.EducationUser
-	if rf, ok := ret.Get(0).(func(context.Context, url.Values) []*libregraph.EducationUser); ok {
-		r0 = rf(ctx, queryParam)
+	if rf, ok := ret.Get(0).(func(context.Context) []*libregraph.EducationUser); ok {
+		r0 = rf(ctx)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).([]*libregraph.EducationUser)
@@ -354,8 +352,8 @@ func (_m *EducationBackend) GetEducationUsers(ctx context.Context, queryParam ur
 	}
 
 	var r1 error
-	if rf, ok := ret.Get(1).(func(context.Context, url.Values) error); ok {
-		r1 = rf(ctx, queryParam)
+	if rf, ok := ret.Get(1).(func(context.Context) error); ok {
+		r1 = rf(ctx)
 	} else {
 		r1 = ret.Error(1)
 	}
diff --git a/services/graph/pkg/identity/odata.go b/services/graph/pkg/identity/odata.go
new file mode 100644
index 00000000000..3b9e5a9b672
--- /dev/null
+++ b/services/graph/pkg/identity/odata.go
@@ -0,0 +1,41 @@
+package identity
+
+import "github.com/CiscoM31/godata"
+
+// GetExpandValues extracts the values of the $expand query parameter and
+// returns them in a []string, rejects any $expand value that consists of more
+// than just a single path segment
+func GetExpandValues(req *godata.GoDataQuery) ([]string, error) {
+	if req == nil || req.Expand == nil {
+		return []string{}, nil
+	}
+	expand := make([]string, 0, len(req.Expand.ExpandItems))
+	for _, item := range req.Expand.ExpandItems {
+		if item.Filter != nil || item.At != nil || item.Search != nil ||
+			item.OrderBy != nil || item.Skip != nil || item.Top != nil ||
+			item.Select != nil || item.Compute != nil || item.Expand != nil ||
+			item.Levels != 0 {
+			return []string{}, godata.NotImplementedError("options for $expand not supported")
+		}
+		if len(item.Path) > 1 {
+			return []string{}, godata.NotImplementedError("multiple segments in $expand not supported")
+		}
+		expand = append(expand, item.Path[0].Value)
+	}
+	return expand, nil
+}
+
+// GetSearchValues extracts the value of the $search query parameter and returns
+// it as a string. Rejects any search query that is more than just a simple string
+func GetSearchValues(req *godata.GoDataQuery) (string, error) {
+	if req == nil || req.Search == nil {
+		return "", nil
+	}
+
+	// Only allow simple search queries for now
+	if len(req.Search.Tree.Children) != 0 {
+		return "", godata.NotImplementedError("complex search queries are not supported")
+	}
+
+	return req.Search.Tree.Token.Value, nil
+}
diff --git a/services/graph/pkg/service/v0/drives.go b/services/graph/pkg/service/v0/drives.go
index 29a4e27ad8b..0bd90a616cc 100644
--- a/services/graph/pkg/service/v0/drives.go
+++ b/services/graph/pkg/service/v0/drives.go
@@ -322,6 +322,7 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
 	render.JSON(w, r, newDrive)
 }
 
+// UpdateDrive updates the properties of a storage drive (space).
 func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
 	logger := g.logger.SubloggerWithRequestID(r.Context())
 	logger.Info().Msg("calling update drive")
@@ -594,7 +595,7 @@ func (g Graph) cs3StorageSpaceToDrive(ctx context.Context, baseURL *url.URL, spa
 				} else {
 					var user libregraph.User
 					if item := g.usersCache.Get(id); item == nil {
-						if requestedUser, err := g.identityBackend.GetUser(ctx, id, url.Values{}); err == nil {
+						if requestedUser, err := g.identityBackend.GetUser(ctx, id, &godata.GoDataRequest{}); err == nil {
 							user = *requestedUser
 							g.usersCache.Set(id, user, ttlcache.DefaultTTL)
 						}
@@ -881,6 +882,7 @@ func listStorageSpacesTypeFilter(spaceType string) *storageprovider.ListStorageS
 	}
 }
 
+// DeleteDrive deletes a storage drive (space).
 func (g Graph) DeleteDrive(w http.ResponseWriter, r *http.Request) {
 	logger := g.logger.SubloggerWithRequestID(r.Context())
 	logger.Info().Msg("calling delete drive")
diff --git a/services/graph/pkg/service/v0/educationclasses.go b/services/graph/pkg/service/v0/educationclasses.go
index 330a8ef2700..989a8be7f67 100644
--- a/services/graph/pkg/service/v0/educationclasses.go
+++ b/services/graph/pkg/service/v0/educationclasses.go
@@ -32,7 +32,7 @@ func (g Graph) GetEducationClasses(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	classes, err := g.identityEducationBackend.GetEducationClasses(r.Context(), r.URL.Query())
+	classes, err := g.identityEducationBackend.GetEducationClasses(r.Context())
 	if err != nil {
 		logger.Debug().Err(err).Msg("could not get classes: backend error")
 		var errcode errorcode.Error
@@ -226,7 +226,7 @@ func (g Graph) GetEducationClass(w http.ResponseWriter, r *http.Request) {
 		Str("id", classID).
 		Interface("query", r.URL.Query()).
 		Msg("calling get class on backend")
-	class, err := g.identityEducationBackend.GetEducationClass(r.Context(), classID, r.URL.Query())
+	class, err := g.identityEducationBackend.GetEducationClass(r.Context(), classID)
 	if err != nil {
 		logger.Debug().Err(err).Msg("could not get class: backend error")
 		var errcode errorcode.Error
diff --git a/services/graph/pkg/service/v0/educationschools.go b/services/graph/pkg/service/v0/educationschools.go
index e54dc0bf6bb..6dc797bdeb6 100644
--- a/services/graph/pkg/service/v0/educationschools.go
+++ b/services/graph/pkg/service/v0/educationschools.go
@@ -30,7 +30,7 @@ func (g Graph) GetEducationSchools(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	schools, err := g.identityEducationBackend.GetEducationSchools(r.Context(), r.URL.Query())
+	schools, err := g.identityEducationBackend.GetEducationSchools(r.Context())
 	if err != nil {
 		logger.Debug().Err(err).Msg("could not get schools: backend error")
 		var errcode errorcode.Error
@@ -170,7 +170,7 @@ func (g Graph) GetEducationSchool(w http.ResponseWriter, r *http.Request) {
 		Str("id", schoolID).
 		Interface("query", r.URL.Query()).
 		Msg("calling get school on backend")
-	school, err := g.identityEducationBackend.GetEducationSchool(r.Context(), schoolID, r.URL.Query())
+	school, err := g.identityEducationBackend.GetEducationSchool(r.Context(), schoolID)
 	if err != nil {
 		logger.Debug().Err(err).Msg("could not get school: backend error")
 		var errcode errorcode.Error
diff --git a/services/graph/pkg/service/v0/educationuser.go b/services/graph/pkg/service/v0/educationuser.go
index 1ce9dae2a24..dab3d475403 100644
--- a/services/graph/pkg/service/v0/educationuser.go
+++ b/services/graph/pkg/service/v0/educationuser.go
@@ -10,11 +10,9 @@ import (
 	"strings"
 
 	"github.com/CiscoM31/godata"
-	cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
 	storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	revactx "github.com/cs3org/reva/v2/pkg/ctx"
 	"github.com/cs3org/reva/v2/pkg/events"
-	"github.com/cs3org/reva/v2/pkg/rgrpc/status"
 	"github.com/cs3org/reva/v2/pkg/utils"
 	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/render"
@@ -22,7 +20,6 @@ import (
 	settings "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
 	"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
 	settingssvc "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0"
-	"golang.org/x/exp/slices"
 )
 
 // GetEducationUsers implements the Service interface.
@@ -38,7 +35,7 @@ func (g Graph) GetEducationUsers(w http.ResponseWriter, r *http.Request) {
 	}
 
 	logger.Debug().Interface("query", r.URL.Query()).Msg("calling get education users on backend")
-	users, err := g.identityEducationBackend.GetEducationUsers(r.Context(), r.URL.Query())
+	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")
 		var errcode errorcode.Error
@@ -197,7 +194,7 @@ func (g Graph) GetEducationUser(w http.ResponseWriter, r *http.Request) {
 	}
 
 	logger.Debug().Str("id", userID).Msg("calling get education user from backend")
-	user, err := g.identityEducationBackend.GetEducationUser(r.Context(), userID, r.URL.Query())
+	user, err := g.identityEducationBackend.GetEducationUser(r.Context(), userID)
 
 	if err != nil {
 		logger.Debug().Err(err).Msg("could not get education user: error fetching education user from backend")
@@ -209,65 +206,6 @@ func (g Graph) GetEducationUser(w http.ResponseWriter, r *http.Request) {
 		}
 		return
 	}
-	sel := strings.Split(r.URL.Query().Get("$select"), ",")
-	exp := strings.Split(r.URL.Query().Get("$expand"), ",")
-	if slices.Contains(sel, "drive") || slices.Contains(sel, "drives") || slices.Contains(exp, "drive") || slices.Contains(exp, "drives") {
-		wdu, err := g.getWebDavBaseURL()
-		if err != nil {
-			// log error, wrong configuration
-			logger.Error().
-				Err(err).
-				Str("webdav_base", g.config.Spaces.WebDavBase).
-				Str("webdav_path", g.config.Spaces.WebDavPath).
-				Msg("error parsing webdav URL")
-			render.Status(r, http.StatusInternalServerError)
-			return
-		}
-		logger.Debug().Str("id", user.GetId()).Msg("calling list storage spaces with education user id filter")
-		f := listStorageSpacesUserFilter(user.GetId())
-		// use the unrestricted flag to get all possible spaces
-		// users with the canListAllSpaces permission should see all spaces
-		opaque := utils.AppendPlainToOpaque(nil, "unrestricted", "T")
-		lspr, err := g.gatewayClient.ListStorageSpaces(r.Context(), &storageprovider.ListStorageSpacesRequest{
-			Opaque:  opaque,
-			Filters: []*storageprovider.ListStorageSpacesRequest_Filter{f},
-		})
-		if err != nil {
-			// transport error, needs to be fixed by admin
-			logger.Error().Err(err).Interface("query", r.URL.Query()).Msg("error getting storages: transport error")
-			render.Status(r, http.StatusInternalServerError)
-			render.JSON(w, r, user)
-			return
-		}
-		if lspr.GetStatus().GetCode() != cs3rpc.Code_CODE_OK {
-			logger.Debug().Str("grpc", lspr.GetStatus().GetMessage()).Msg("could not get drive for education user")
-			// in case of NOT_OK, we can just return the user object with empty drives
-			render.Status(r, status.HTTPStatusFromCode(http.StatusOK))
-			render.JSON(w, r, user)
-			return
-		}
-		drives := []libregraph.Drive{}
-		for _, sp := range lspr.GetStorageSpaces() {
-			d, err := g.cs3StorageSpaceToDrive(r.Context(), wdu, sp)
-			if err != nil {
-				logger.Debug().Err(err).Interface("id", sp.Id).Msg("error converting space to drive")
-				continue
-			}
-			quota, err := g.getDriveQuota(r.Context(), sp)
-			if err != nil {
-				logger.Debug().Err(err).Interface("id", sp.Id).Msg("error calling get quota on drive")
-			}
-			d.Quota = &quota
-			if slices.Contains(sel, "drive") || slices.Contains(exp, "drive") {
-				if *d.DriveType == _spaceTypePersonal {
-					user.Drive = d
-				}
-			} else {
-				drives = append(drives, *d)
-				user.Drives = drives
-			}
-		}
-	}
 
 	render.Status(r, http.StatusOK)
 	render.JSON(w, r, user)
@@ -291,7 +229,7 @@ func (g Graph) DeleteEducationUser(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	logger.Debug().Str("id", userID).Msg("calling get education user on user backend")
-	user, err := g.identityEducationBackend.GetEducationUser(r.Context(), userID, r.URL.Query())
+	user, err := g.identityEducationBackend.GetEducationUser(r.Context(), userID)
 	if err != nil {
 		logger.Debug().Err(err).Str("userID", userID).Msg("failed to get education user from backend")
 		var errcode errorcode.Error
diff --git a/services/graph/pkg/service/v0/users.go b/services/graph/pkg/service/v0/users.go
index 09ea4512bbf..6320238b4fe 100644
--- a/services/graph/pkg/service/v0/users.go
+++ b/services/graph/pkg/service/v0/users.go
@@ -32,6 +32,14 @@ import (
 func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) {
 	logger := g.logger.SubloggerWithRequestID(r.Context())
 	logger.Info().Msg("calling get user in /me")
+	sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
+
+	odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query())
+	if err != nil {
+		logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users: query error")
+		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
+		return
+	}
 
 	u, ok := revactx.ContextGetUser(r.Context())
 	if !ok {
@@ -39,7 +47,14 @@ func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) {
 		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "user not in context")
 		return
 	}
-	exp := strings.Split(r.URL.Query().Get("$expand"), ",")
+
+	exp, err := identity.GetExpandValues(odataReq.Query)
+	if err != nil {
+		logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users: $expand error")
+		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
+		return
+	}
+
 	var me *libregraph.User
 	// We can just return the user from context unless we need to expand the group memberships
 	if !slices.Contains(exp, "memberOf") {
@@ -47,7 +62,7 @@ func (g Graph) GetMe(w http.ResponseWriter, r *http.Request) {
 	} else {
 		var err error
 		logger.Debug().Msg("calling get user on backend")
-		me, err = g.identityBackend.GetUser(r.Context(), u.GetId().GetOpaqueId(), r.URL.Query())
+		me, err = g.identityBackend.GetUser(r.Context(), u.GetId().GetOpaqueId(), odataReq)
 		if err != nil {
 			logger.Debug().Err(err).Interface("user", u).Msg("could not get user from backend")
 			var errcode errorcode.Error
@@ -111,7 +126,7 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) {
 	}
 
 	logger.Debug().Interface("query", r.URL.Query()).Msg("calling get users on backend")
-	users, err := g.identityBackend.GetUsers(r.Context(), r.URL.Query())
+	users, err := g.identityBackend.GetUsers(r.Context(), odataReq)
 	if err != nil {
 		logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users from backend")
 		var errcode errorcode.Error
@@ -123,8 +138,12 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// expand appRoleAssignments if requested
-	exp := strings.Split(r.URL.Query().Get("$expand"), ",")
+	exp, err := identity.GetExpandValues(odataReq.Query)
+	if err != nil {
+		logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users: $expand error")
+		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
+		return
+	}
 	expandAppRoleAssignments := slices.Contains(exp, "appRoleAssignments")
 	expandMemberOf := slices.Contains(exp, "memberOf")
 	for _, u := range users {
@@ -240,6 +259,9 @@ func (g Graph) PostUser(w http.ResponseWriter, r *http.Request) {
 func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) {
 	logger := g.logger.SubloggerWithRequestID(r.Context())
 	logger.Info().Msg("calling get user")
+
+	sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
+
 	userID := chi.URLParam(r, "userID")
 	userID, err := url.PathUnescape(userID)
 	if err != nil {
@@ -254,8 +276,22 @@ func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query())
+	if err != nil {
+		logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users: query error")
+		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
+		return
+	}
+
+	exp, err := identity.GetExpandValues(odataReq.Query)
+	if err != nil {
+		logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users: $expand error")
+		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
+		return
+	}
+
 	logger.Debug().Str("id", userID).Msg("calling get user from backend")
-	user, err := g.identityBackend.GetUser(r.Context(), userID, r.URL.Query())
+	user, err := g.identityBackend.GetUser(r.Context(), userID, odataReq)
 
 	if err != nil {
 		logger.Debug().Err(err).Msg("could not get user: error fetching user from backend")
@@ -267,10 +303,9 @@ func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) {
 		}
 		return
 	}
-	sel := strings.Split(r.URL.Query().Get("$select"), ",")
-	exp := strings.Split(r.URL.Query().Get("$expand"), ",")
-	listDrives := slices.Contains(sel, "drives") || slices.Contains(exp, "drives")
-	listDrive := slices.Contains(sel, "drive") || slices.Contains(exp, "drive")
+
+	listDrives := slices.Contains(exp, "drives")
+	listDrive := slices.Contains(exp, "drive")
 
 	// do we need to list all or only the personal drive
 	filters := []*storageprovider.ListStorageSpacesRequest_Filter{}
@@ -368,6 +403,7 @@ func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) {
 func (g Graph) DeleteUser(w http.ResponseWriter, r *http.Request) {
 	logger := g.logger.SubloggerWithRequestID(r.Context())
 	logger.Info().Msg("calling delete user")
+	sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
 	userID := chi.URLParam(r, "userID")
 	userID, err := url.PathUnescape(userID)
 	if err != nil {
@@ -381,8 +417,16 @@ func (g Graph) DeleteUser(w http.ResponseWriter, r *http.Request) {
 		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing user id")
 		return
 	}
+
+	odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query())
+	if err != nil {
+		logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get users: query error")
+		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
+		return
+	}
+
 	logger.Debug().Str("id", userID).Msg("calling get user on user backend")
-	user, err := g.identityBackend.GetUser(r.Context(), userID, r.URL.Query())
+	user, err := g.identityBackend.GetUser(r.Context(), userID, odataReq)
 	if err != nil {
 		logger.Debug().Err(err).Str("userID", userID).Msg("failed to get user from backend")
 		var errcode errorcode.Error


Test patch:
diff --git a/services/graph/pkg/identity/ldap_education_class_test.go b/services/graph/pkg/identity/ldap_education_class_test.go
index 8562133d0f5..21594310bf0 100644
--- a/services/graph/pkg/identity/ldap_education_class_test.go
+++ b/services/graph/pkg/identity/ldap_education_class_test.go
@@ -3,7 +3,6 @@ package identity
 import (
 	"context"
 	"errors"
-	"net/url"
 	"testing"
 
 	"github.com/go-ldap/ldap/v3"
@@ -72,7 +71,7 @@ func TestGetEducationClasses(t *testing.T) {
 	lm := &mocks.Client{}
 	lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock")))
 	b, _ := getMockedBackend(lm, lconfig, &logger)
-	_, err := b.GetEducationClasses(context.Background(), url.Values{})
+	_, err := b.GetEducationClasses(context.Background())
 	if err == nil || err.Error() != "itemNotFound" {
 		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
 	}
@@ -80,7 +79,7 @@ func TestGetEducationClasses(t *testing.T) {
 	lm = &mocks.Client{}
 	lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil)
 	b, _ = getMockedBackend(lm, lconfig, &logger)
-	g, err := b.GetEducationClasses(context.Background(), url.Values{})
+	g, err := b.GetEducationClasses(context.Background())
 	if err != nil {
 		t.Errorf("Expected success, got '%s'", err.Error())
 	} else if g == nil || len(g) != 0 {
@@ -92,7 +91,7 @@ func TestGetEducationClasses(t *testing.T) {
 		Entries: []*ldap.Entry{classEntry},
 	}, nil)
 	b, _ = getMockedBackend(lm, lconfig, &logger)
-	g, err = b.GetEducationClasses(context.Background(), url.Values{})
+	g, err = b.GetEducationClasses(context.Background())
 	if err != nil {
 		t.Errorf("Expected GetEducationClasses to succeed. Got %s", err.Error())
 	} else if *g[0].Id != classEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) {
@@ -152,7 +151,7 @@ func TestGetEducationClass(t *testing.T) {
 		b, err := getMockedBackend(lm, eduConfig, &logger)
 		assert.Nil(t, err)
 
-		class, err := b.GetEducationClass(context.Background(), tt.id, nil)
+		class, err := b.GetEducationClass(context.Background(), tt.id)
 		lm.AssertNumberOfCalls(t, "Search", 1)
 
 		if tt.expectedItemNotFound {
diff --git a/services/graph/pkg/identity/ldap_education_school_test.go b/services/graph/pkg/identity/ldap_education_school_test.go
index d4865d19fdf..47cc36437b1 100644
--- a/services/graph/pkg/identity/ldap_education_school_test.go
+++ b/services/graph/pkg/identity/ldap_education_school_test.go
@@ -264,7 +264,7 @@ func TestGetEducationSchool(t *testing.T) {
 		b, err := getMockedBackend(lm, eduConfig, &logger)
 		assert.Nil(t, err)
 
-		school, err := b.GetEducationSchool(context.Background(), tt.numberOrId, nil)
+		school, err := b.GetEducationSchool(context.Background(), tt.numberOrId)
 		lm.AssertNumberOfCalls(t, "Search", 1)
 
 		if tt.expectedItemNotFound {
@@ -293,7 +293,7 @@ func TestGetEducationSchools(t *testing.T) {
 	//	lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
 	b, err := getMockedBackend(lm, eduConfig, &logger)
 	assert.Nil(t, err)
-	_, err = b.GetEducationSchools(context.Background(), nil)
+	_, err = b.GetEducationSchools(context.Background())
 	lm.AssertNumberOfCalls(t, "Search", 1)
 	assert.Nil(t, err)
 }
diff --git a/services/graph/pkg/identity/ldap_education_user_test.go b/services/graph/pkg/identity/ldap_education_user_test.go
index 156ba297805..960bec73be1 100644
--- a/services/graph/pkg/identity/ldap_education_user_test.go
+++ b/services/graph/pkg/identity/ldap_education_user_test.go
@@ -114,13 +114,13 @@ func TestGetEducationUser(t *testing.T) {
 	lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
 	b, err := getMockedBackend(lm, eduConfig, &logger)
 	assert.Nil(t, err)
-	user, err := b.GetEducationUser(context.Background(), "abcd-defg", nil)
+	user, err := b.GetEducationUser(context.Background(), "abcd-defg")
 	lm.AssertNumberOfCalls(t, "Search", 1)
 	assert.Nil(t, err)
 	assert.Equal(t, "Test User", user.GetDisplayName())
 	assert.Equal(t, "abcd-defg", user.GetId())
 
-	_, err = b.GetEducationUser(context.Background(), "xxxx-xxxx", nil)
+	_, err = b.GetEducationUser(context.Background(), "xxxx-xxxx")
 	lm.AssertNumberOfCalls(t, "Search", 2)
 	assert.NotNil(t, err)
 	assert.Equal(t, "itemNotFound", err.Error())
@@ -139,7 +139,7 @@ func TestGetEducationUsers(t *testing.T) {
 	lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
 	b, err := getMockedBackend(lm, eduConfig, &logger)
 	assert.Nil(t, err)
-	_, err = b.GetEducationUsers(context.Background(), nil)
+	_, err = b.GetEducationUsers(context.Background())
 	lm.AssertNumberOfCalls(t, "Search", 1)
 	assert.Nil(t, err)
 }
diff --git a/services/graph/pkg/identity/ldap_test.go b/services/graph/pkg/identity/ldap_test.go
index bece82a5dc5..b645e5828b1 100644
--- a/services/graph/pkg/identity/ldap_test.go
+++ b/services/graph/pkg/identity/ldap_test.go
@@ -7,6 +7,7 @@ import (
 	"net/url"
 	"testing"
 
+	"github.com/CiscoM31/godata"
 	"github.com/go-ldap/ldap/v3"
 	libregraph "github.com/owncloud/libre-graph-api-go"
 	"github.com/owncloud/ocis/v2/ocis-pkg/log"
@@ -172,23 +173,24 @@ func TestGetUser(t *testing.T) {
 			nil, ldap.NewError(ldap.LDAPResultSizeLimitExceeded, errors.New("mock")))
 	b, _ := getMockedBackend(lm, lconfig, &logger)
 
-	queryParamExpand := url.Values{
-		"$expand": []string{"memberOf"},
-	}
-	queryParamSelect := url.Values{
-		"$select": []string{"memberOf"},
+	odataReqDefault, err := godata.ParseRequest(context.Background(), "",
+		url.Values{})
+	if err != nil {
+		t.Errorf("Expected success got '%s'", err.Error())
 	}
-	_, err := b.GetUser(context.Background(), "fred", nil)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
+
+	odataReqExpand, err := godata.ParseRequest(context.Background(), "",
+		url.Values{"$expand": []string{"memberOf"}})
+	if err != nil {
+		t.Errorf("Expected success got '%s'", err.Error())
 	}
 
-	_, err = b.GetUser(context.Background(), "fred", queryParamExpand)
+	_, err = b.GetUser(context.Background(), "fred", odataReqDefault)
 	if err == nil || err.Error() != "itemNotFound" {
 		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
 	}
 
-	_, err = b.GetUser(context.Background(), "fred", queryParamSelect)
+	_, err = b.GetUser(context.Background(), "fred", odataReqExpand)
 	if err == nil || err.Error() != "itemNotFound" {
 		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
 	}
@@ -199,17 +201,12 @@ func TestGetUser(t *testing.T) {
 		Return(
 			&ldap.SearchResult{}, nil)
 	b, _ = getMockedBackend(lm, lconfig, &logger)
-	_, err = b.GetUser(context.Background(), "fred", nil)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
-
-	_, err = b.GetUser(context.Background(), "fred", queryParamExpand)
+	_, err = b.GetUser(context.Background(), "fred", odataReqDefault)
 	if err == nil || err.Error() != "itemNotFound" {
 		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
 	}
 
-	_, err = b.GetUser(context.Background(), "fred", queryParamSelect)
+	_, err = b.GetUser(context.Background(), "fred", odataReqExpand)
 	if err == nil || err.Error() != "itemNotFound" {
 		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
 	}
@@ -224,21 +221,14 @@ func TestGetUser(t *testing.T) {
 			nil)
 
 	b, _ = getMockedBackend(lm, lconfig, &logger)
-	u, err := b.GetUser(context.Background(), "user", nil)
+	u, err := b.GetUser(context.Background(), "user", odataReqDefault)
 	if err != nil {
 		t.Errorf("Expected GetUser to succeed. Got %s", err.Error())
 	} else if *u.Id != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id) {
 		t.Errorf("Expected GetUser to return a valid user")
 	}
 
-	u, err = b.GetUser(context.Background(), "user", queryParamExpand)
-	if err != nil {
-		t.Errorf("Expected GetUser to succeed. Got %s", err.Error())
-	} else if *u.Id != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id) {
-		t.Errorf("Expected GetUser to return a valid user")
-	}
-
-	u, err = b.GetUser(context.Background(), "user", queryParamSelect)
+	u, err = b.GetUser(context.Background(), "user", odataReqExpand)
 	if err != nil {
 		t.Errorf("Expected GetUser to succeed. Got %s", err.Error())
 	} else if *u.Id != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id) {
@@ -266,8 +256,14 @@ func TestGetUsers(t *testing.T) {
 	lm := &mocks.Client{}
 	lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock")))
 
+	odataReqDefault, err := godata.ParseRequest(context.Background(), "",
+		url.Values{})
+	if err != nil {
+		t.Errorf("Expected success got '%s'", err.Error())
+	}
+
 	b, _ := getMockedBackend(lm, lconfig, &logger)
-	_, err := b.GetUsers(context.Background(), url.Values{})
+	_, err = b.GetUsers(context.Background(), odataReqDefault)
 	if err == nil || err.Error() != "itemNotFound" {
 		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
 	}
@@ -275,7 +271,7 @@ func TestGetUsers(t *testing.T) {
 	lm = &mocks.Client{}
 	lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil)
 	b, _ = getMockedBackend(lm, lconfig, &logger)
-	g, err := b.GetUsers(context.Background(), url.Values{})
+	g, err := b.GetUsers(context.Background(), odataReqDefault)
 	if err != nil {
 		t.Errorf("Expected success, got '%s'", err.Error())
 	} else if g == nil || len(g) != 0 {
diff --git a/services/graph/pkg/service/v0/educationuser_test.go b/services/graph/pkg/service/v0/educationuser_test.go
index 1e9e445cbb9..ed1c833e114 100644
--- a/services/graph/pkg/service/v0/educationuser_test.go
+++ b/services/graph/pkg/service/v0/educationuser_test.go
@@ -216,84 +216,8 @@ var _ = Describe("EducationUsers", func() {
 			err = json.Unmarshal(data, &responseUser)
 			Expect(err).ToNot(HaveOccurred())
 			Expect(responseUser.GetId()).To(Equal("user1"))
-			Expect(len(responseUser.GetDrives())).To(Equal(0))
 		})
 
-		It("includes the personal space if requested", func() {
-			user := &libregraph.EducationUser{}
-			user.SetId("user1")
-
-			identityEducationBackend.On("GetEducationUser", mock.Anything, mock.Anything, mock.Anything).Return(user, nil)
-			gatewayClient.On("GetQuota", mock.Anything, mock.Anything, mock.Anything).Return(&provider.GetQuotaResponse{
-				Status:     status.NewOK(ctx),
-				TotalBytes: 10,
-			}, nil)
-			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
-				Status: status.NewOK(ctx),
-				StorageSpaces: []*provider.StorageSpace{
-					{
-						Id:        &provider.StorageSpaceId{OpaqueId: "drive1"},
-						Root:      &provider.ResourceId{SpaceId: "space", OpaqueId: "space"},
-						SpaceType: "project",
-					},
-					{
-						Id:        &provider.StorageSpaceId{OpaqueId: "personal"},
-						Root:      &provider.ResourceId{SpaceId: "personal", OpaqueId: "personal"},
-						SpaceType: "personal",
-					},
-				},
-			}, nil)
-
-			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/users?$expand=drive", nil)
-			rctx := chi.NewRouteContext()
-			rctx.URLParams.Add("userID", *user.Id)
-			r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
-			svc.GetEducationUser(rr, r)
-
-			Expect(rr.Code).To(Equal(http.StatusOK))
-			data, err := io.ReadAll(rr.Body)
-			Expect(err).ToNot(HaveOccurred())
-			responseUser := &libregraph.EducationUser{}
-			err = json.Unmarshal(data, &responseUser)
-			Expect(err).ToNot(HaveOccurred())
-			Expect(responseUser.GetId()).To(Equal("user1"))
-			Expect(*responseUser.GetDrive().Id).To(Equal("personal"))
-		})
-
-		It("includes the drives if requested", func() {
-			user := &libregraph.EducationUser{}
-			user.SetId("user1")
-
-			identityEducationBackend.On("GetEducationUser", mock.Anything, mock.Anything, mock.Anything).Return(user, nil)
-			gatewayClient.On("GetQuota", mock.Anything, mock.Anything, mock.Anything).Return(&provider.GetQuotaResponse{
-				Status:     status.NewOK(ctx),
-				TotalBytes: 10,
-			}, nil)
-			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
-				Status: status.NewOK(ctx),
-				StorageSpaces: []*provider.StorageSpace{
-					{
-						Id:   &provider.StorageSpaceId{OpaqueId: "drive1"},
-						Root: &provider.ResourceId{SpaceId: "space", OpaqueId: "space"},
-					},
-				},
-			}, nil)
-
-			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/users?$expand=drives", nil)
-			rctx := chi.NewRouteContext()
-			rctx.URLParams.Add("userID", *user.Id)
-			r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
-			svc.GetEducationUser(rr, r)
-
-			Expect(rr.Code).To(Equal(http.StatusOK))
-			data, err := io.ReadAll(rr.Body)
-			Expect(err).ToNot(HaveOccurred())
-			responseUser := &libregraph.EducationUser{}
-			err = json.Unmarshal(data, &responseUser)
-			Expect(err).ToNot(HaveOccurred())
-			Expect(responseUser.GetId()).To(Equal("user1"))
-			Expect(len(responseUser.GetDrives())).To(Equal(1))
-		})
 	})
 
 	Describe("PostEducationUser", func() {