Repository: owncloud/ocis
Pull request #5233 (2022-12-15 16:30:38)

[full-ci] graph: Initial LDAP support for /education/users
This implements GetEducationUser, GetEducationUsers, DeleteEducationUser and  CreateEducationUser methods for the LDAP backend. It's still very basic and  no fancy filtering or expanding is there yet.:

Note: This is currently based on #5213.  Let's get that merged first.

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=5233)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&resolved=false&types=BUG) [1 Bug](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=CODE_SMELL) [44 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&resolved=false&types=CODE_SMELL)

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


ownclouders: :boom: Acceptance test [<strong>wopiValidatorTests-ocis</strong>](https://drone.owncloud.com/owncloud/ocis/17534/41/1) failed. Further test are cancelled...
<!-- id: pr-5233 -->

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=5233)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&resolved=false&types=BUG) [1 Bug](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=CODE_SMELL) [44 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&resolved=false&types=CODE_SMELL)

[![69.4%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '69.4%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list) [69.4% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list)  
[![13.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/20-16px.png '13.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_duplicated_lines_density&view=list) [13.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&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=5233)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=CODE_SMELL) [48 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&resolved=false&types=CODE_SMELL)

[![69.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '69.7%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list) [69.7% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list)  
[![12.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/20-16px.png '12.7%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_duplicated_lines_density&view=list) [12.7% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&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=5233)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=CODE_SMELL) [49 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&resolved=false&types=CODE_SMELL)

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


rhafer: @wkloucek @butonic Here's a quick update on want is supposed to work (and what not) with the current state:

creating users:
`curl -H "Authorization: Bearer a-secret-api-token" http://127.0.0.1:9120/graph/v1.0/education/users --request POST --data '{"onPremisesSamAccountName": "test", "displayName":"Test User", "mail": "mail@example.org", "primaryRole": "student", "identities": [{ "issuer": "http://idp.bla", "issuerAssignedId":"test"}]}'`

listing users
`curl -H "Authorization: Bearer a-secret-api-token" http://127.0.0.1:9120/graph/v1.0/education/users`

getting single users by name or id
`curl -H "Authorization: Bearer a-secret-api-token" http://127.0.0.1:9120/graph/v1.0/education/users/test`

deleting users:
`curl -H "Authorization: Bearer a-secret-api-token" http://127.0.0.1:9120/graph/v1.0/education/users/70394e30-a934-4313-b576-33223eb1cd89`

creating schools:
`curl  -H "Authorization: Bearer a-secret-api-token" http://127.0.0.1:9120/graph/v1.0/education/schools --request POST --data '{"schoolNumber": "01231", "displayName":"Test School"}'`

listing schools:
`curl -H "Authorization: Bearer a-secret-api-token" http://127.0.0.1:9120/graph/v1.0/education/schools`

getting a school by id:
`curl -H "Authorization: Bearer a-secret-api-token" http://127.0.0.1:9120/graph/v1.0/education/schools/02193f46-f455-4500-bef0-7e08234381de`

deleting a school:
`curl -H "Authorization: Bearer a-secret-api-token" http://127.0.0.1:9120/graph/v1.0/education/schools/02193f46-f455-4500-bef0-7e08234381de --request DELETE`

assigning a user to a school:
`curl -v -H "Authorization: Bearer a-secret-api-token" http://127.0.0.1:9120/graph/v1.0/education/schools/f2d0a3f8-3d88-4461-95aa-ad038fb6c336/users/\$ref --request POST --data '{ "@odata.id": "https://localhost/graph/v1.0/education/users/fd4bb1af-2002-45b4-8c62-735111ec99df"}'`

unassigning a user from a school:
`curl -v -H "Authorization: Bearer a-secret-api-token" http://127.0.0.1:9120/graph/v1.0/education/schools/f2d0a3f8-3d88-4461-95aa-ad038fb6c336/users/fd4bb1af-2002-45b4-8c62-735111ec99df/\$ref --request DELETE`

listing all users assigned to a school:
`curl -v -H "Authorization: Bearer a-secret-api-token" http://127.0.0.1:9120/graph/v1.0/education/schools/f2d0a3f8-3d88-4461-95aa-ad038fb6c336/users`

Currently NOT implemented
- updating users and school
- everything related to classes

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=5233)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=CODE_SMELL) [49 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&resolved=false&types=CODE_SMELL)

[![70.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '70.7%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list) [70.7% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list)  
[![13.3%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/20-16px.png '13.3%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_duplicated_lines_density&view=list) [13.3% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&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=5233)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=CODE_SMELL) [49 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&resolved=false&types=CODE_SMELL)

[![70.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '70.7%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list) [70.7% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list)  
[![13.3%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/20-16px.png '13.3%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_duplicated_lines_density&view=list) [13.3% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&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=5233)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&resolved=false&types=BUG) [1 Bug](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=CODE_SMELL) [46 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&resolved=false&types=CODE_SMELL)

[![70.7%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '70.7%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list) [70.7% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list)  
[![13.2%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/20-16px.png '13.2%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_duplicated_lines_density&view=list) [13.2% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&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=5233)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=CODE_SMELL) [15 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&resolved=false&types=CODE_SMELL)

[![76.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '76.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list) [76.0% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list)  
[![8.1%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/10-16px.png '8.1%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_duplicated_lines_density&view=list) [8.1% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&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=5233)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=CODE_SMELL) [15 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&resolved=false&types=CODE_SMELL)

[![76.2%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '76.2%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list) [76.2% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list)  
[![8.1%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/10-16px.png '8.1%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_duplicated_lines_density&view=list) [8.1% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&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=5233)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=5233&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=5233&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=5233&resolved=false&types=CODE_SMELL) [15 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=5233&resolved=false&types=CODE_SMELL)

[![76.2%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60-16px.png '76.2%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list) [76.2% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_coverage&view=list)  
[![8.1%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/10-16px.png '8.1%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&metric=new_duplicated_lines_density&view=list) [8.1% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=5233&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 1156a1153e6..eab2eef525f 100644
--- a/services/graph/pkg/identity/backend.go
+++ b/services/graph/pkg/identity/backend.go
@@ -42,12 +42,12 @@ type EducationBackend interface {
 	GetEducationSchool(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationSchool, error)
 	// GetEducationSchools lists all schools
 	GetEducationSchools(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationSchool, error)
-	// GetEducationSchoolMembers lists all members of a school
-	GetEducationSchoolMembers(ctx context.Context, id string) ([]*libregraph.EducationUser, error)
-	// AddMembersToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
-	AddMembersToEducationSchool(ctx context.Context, schoolID string, memberID []string) error
-	// RemoveMemberFromEducationSchool removes a single member (by ID) from a school
-	RemoveMemberFromEducationSchool(ctx context.Context, schoolID string, memberID string) error
+	// GetEducationSchoolUsers lists all members of a school
+	GetEducationSchoolUsers(ctx context.Context, id string) ([]*libregraph.EducationUser, error)
+	// AddUsersToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
+	AddUsersToEducationSchool(ctx context.Context, schoolID string, memberID []string) error
+	// RemoveUserFromEducationSchool removes a single member (by ID) from a school
+	RemoveUserFromEducationSchool(ctx context.Context, schoolID string, memberID string) error
 
 	// CreateEducationUser creates a given education user in the identity backend.
 	CreateEducationUser(ctx context.Context, user libregraph.EducationUser) (*libregraph.EducationUser, error)
diff --git a/services/graph/pkg/identity/err_school.go b/services/graph/pkg/identity/err_school.go
index 103ae9ecd1a..cbe42c70da6 100644
--- a/services/graph/pkg/identity/err_school.go
+++ b/services/graph/pkg/identity/err_school.go
@@ -30,18 +30,18 @@ func (i *ErrEducationBackend) GetEducationSchools(ctx context.Context, queryPara
 	return nil, errNotImplemented
 }
 
-// GetEducationSchoolMembers implements the EducationBackend interface for the ErrEducationBackend backend.
-func (i *ErrEducationBackend) GetEducationSchoolMembers(ctx context.Context, id string) ([]*libregraph.EducationUser, error) {
+// GetEducationSchoolUsers implements the EducationBackend interface for the ErrEducationBackend backend.
+func (i *ErrEducationBackend) GetEducationSchoolUsers(ctx context.Context, id string) ([]*libregraph.EducationUser, error) {
 	return nil, errNotImplemented
 }
 
-// AddMembersToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
-func (i *ErrEducationBackend) AddMembersToEducationSchool(ctx context.Context, schoolID string, memberID []string) error {
+// AddUsersToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
+func (i *ErrEducationBackend) AddUsersToEducationSchool(ctx context.Context, schoolID string, memberID []string) error {
 	return errNotImplemented
 }
 
-// RemoveMemberFromEducationSchool removes a single member (by ID) from a school
-func (i *ErrEducationBackend) RemoveMemberFromEducationSchool(ctx context.Context, schoolID string, memberID string) error {
+// RemoveUserFromEducationSchool removes a single member (by ID) from a school
+func (i *ErrEducationBackend) RemoveUserFromEducationSchool(ctx context.Context, schoolID string, memberID string) error {
 	return errNotImplemented
 }
 
diff --git a/services/graph/pkg/identity/ldap.go b/services/graph/pkg/identity/ldap.go
index 2256a46b287..cfa611b830f 100644
--- a/services/graph/pkg/identity/ldap.go
+++ b/services/graph/pkg/identity/ldap.go
@@ -60,6 +60,8 @@ type groupAttributeMap struct {
 	memberSyntax string
 }
 
+type ldapAttributeValues map[string][]string
+
 func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LDAP, error) {
 	if config.UserDisplayNameAttribute == "" || config.UserIDAttribute == "" ||
 		config.UserEmailAttribute == "" || config.UserNameAttribute == "" {
@@ -126,54 +128,13 @@ func (i *LDAP) CreateUser(ctx context.Context, user libregraph.User) (*libregrap
 	if !i.writeEnabled {
 		return nil, errReadOnly
 	}
-	ar := ldap.AddRequest{
-		DN: fmt.Sprintf("uid=%s,%s", oldap.EscapeDNAttributeValue(*user.OnPremisesSamAccountName), i.userBaseDN),
-		Attributes: []ldap.Attribute{
-			// inetOrgPerson requires "cn"
-			{
-				Type: "cn",
-				Vals: []string{*user.OnPremisesSamAccountName},
-			},
-			{
-				Type: i.userAttributeMap.mail,
-				Vals: []string{*user.Mail},
-			},
-			{
-				Type: i.userAttributeMap.userName,
-				Vals: []string{*user.OnPremisesSamAccountName},
-			},
-			{
-				Type: i.userAttributeMap.displayName,
-				Vals: []string{*user.DisplayName},
-			},
-		},
-	}
-
-	objectClasses := []string{"inetOrgPerson", "organizationalPerson", "person", "top"}
-
-	if !i.usePwModifyExOp && user.PasswordProfile != nil && user.PasswordProfile.Password != nil {
-		// Depending on the LDAP server implementation this might cause the
-		// password to be stored in cleartext in the LDAP database. Using the
-		// "Password Modify LDAP Extended Operation" is recommended.
-		ar.Attribute("userPassword", []string{*user.PasswordProfile.Password})
-	}
-	if !i.useServerUUID {
-		ar.Attribute("owncloudUUID", []string{uuid.Must(uuid.NewV4()).String()})
-		objectClasses = append(objectClasses, "owncloud")
-	}
-	ar.Attribute("objectClass", objectClasses)
 
-	// inetOrgPerson requires "sn" to be set. Set it to the Username if
-	// Surname is not set in the Request
-	var sn string
-	if user.Surname != nil && *user.Surname != "" {
-		sn = *user.Surname
-	} else {
-		sn = *user.OnPremisesSamAccountName
+	ar, err := i.userToAddRequest(user)
+	if err != nil {
+		return nil, err
 	}
-	ar.Attribute("sn", []string{sn})
 
-	if err := i.conn.Add(&ar); err != nil {
+	if err := i.conn.Add(ar); err != nil {
 		var lerr *ldap.Error
 		logger.Debug().Err(err).Msg("error adding user")
 		if errors.As(err, &lerr) {
@@ -366,49 +327,32 @@ func (i *LDAP) getEntryByDN(dn string, attrs []string, filter string) (*ldap.Ent
 	return res.Entries[0], nil
 }
 
-func (i *LDAP) getLDAPUserByID(id string) (*ldap.Entry, error) {
-	id = ldap.EscapeFilter(id)
-	filter := fmt.Sprintf("(%s=%s)", i.userAttributeMap.id, id)
-	return i.getLDAPUserByFilter(filter)
-}
-
-func (i *LDAP) getLDAPUserByNameOrID(nameOrID string) (*ldap.Entry, error) {
-	nameOrID = ldap.EscapeFilter(nameOrID)
-	filter := fmt.Sprintf("(|(%s=%s)(%s=%s))", i.userAttributeMap.userName, nameOrID, i.userAttributeMap.id, nameOrID)
-	return i.getLDAPUserByFilter(filter)
-}
+func (i *LDAP) searchLDAPEntryByFilter(basedn string, attrs []string, filter string) (*ldap.Entry, error) {
+	if filter == "" {
+		filter = "(objectclass=*)"
+	}
 
-func (i *LDAP) getLDAPUserByFilter(filter string) (*ldap.Entry, error) {
 	searchRequest := ldap.NewSearchRequest(
-		i.userBaseDN, i.userScope, ldap.NeverDerefAliases, 1, 0, false,
-		fmt.Sprintf("(&%s(objectClass=%s)%s)", i.userFilter, i.userObjectClass, filter),
-		[]string{
-			i.userAttributeMap.displayName,
-			i.userAttributeMap.id,
-			i.userAttributeMap.mail,
-			i.userAttributeMap.userName,
-		},
+		basedn,
+		ldap.ScopeWholeSubtree,
+		ldap.NeverDerefAliases, 1, 0, false,
+		filter,
+		attrs,
 		nil,
 	)
+
 	i.logger.Debug().Str("backend", "ldap").
 		Str("base", searchRequest.BaseDN).
 		Str("filter", searchRequest.Filter).
 		Int("scope", searchRequest.Scope).
 		Int("sizelimit", searchRequest.SizeLimit).
 		Interface("attributes", searchRequest.Attributes).
-		Msg("getLDAPUserByFilter")
+		Msg("getEntryByFilter")
 	res, err := i.conn.Search(searchRequest)
 
 	if err != nil {
-		var errmsg string
-		if lerr, ok := err.(*ldap.Error); ok {
-			if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded {
-				errmsg = fmt.Sprintf("too many results searching for user '%s'", filter)
-				i.logger.Debug().Str("backend", "ldap").Err(lerr).
-					Str("userfilter", filter).Msg("too many results searching for user")
-			}
-		}
-		return nil, errorcode.New(errorcode.ItemNotFound, errmsg)
+		i.logger.Error().Err(err).Str("backend", "ldap").Str("dn", basedn).Str("filter", filter).Msg("Search user by filter failed")
+		return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
 	}
 	if len(res.Entries) == 0 {
 		return nil, errNotFound
@@ -417,6 +361,29 @@ func (i *LDAP) getLDAPUserByFilter(filter string) (*ldap.Entry, error) {
 	return res.Entries[0], nil
 }
 
+func (i *LDAP) getLDAPUserByID(id string) (*ldap.Entry, error) {
+	id = ldap.EscapeFilter(id)
+	filter := fmt.Sprintf("(%s=%s)", i.userAttributeMap.id, id)
+	return i.getLDAPUserByFilter(filter)
+}
+
+func (i *LDAP) getLDAPUserByNameOrID(nameOrID string) (*ldap.Entry, error) {
+	nameOrID = ldap.EscapeFilter(nameOrID)
+	filter := fmt.Sprintf("(|(%s=%s)(%s=%s))", i.userAttributeMap.userName, nameOrID, i.userAttributeMap.id, nameOrID)
+	return i.getLDAPUserByFilter(filter)
+}
+
+func (i *LDAP) getLDAPUserByFilter(filter string) (*ldap.Entry, error) {
+	filter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.userFilter, i.userObjectClass, filter)
+	attrs := []string{
+		i.userAttributeMap.displayName,
+		i.userAttributeMap.id,
+		i.userAttributeMap.mail,
+		i.userAttributeMap.userName,
+	}
+	return i.searchLDAPEntryByFilter(i.userBaseDN, attrs, filter)
+}
+
 func (i *LDAP) GetUser(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.User, error) {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("GetUser")
@@ -1010,6 +977,56 @@ func (i *LDAP) groupsFromLDAPEntries(e []*ldap.Entry) []libregraph.Group {
 	return groups
 }
 
+func (i *LDAP) userToLDAPAttrValues(user libregraph.User) (map[string][]string, error) {
+	attrs := map[string][]string{
+		i.userAttributeMap.displayName: {user.GetDisplayName()},
+		i.userAttributeMap.userName:    {user.GetOnPremisesSamAccountName()},
+		i.userAttributeMap.mail:        {user.GetMail()},
+		"objectClass":                  {"inetOrgPerson", "organizationalPerson", "person", "top"},
+		"cn":                           {user.GetOnPremisesSamAccountName()},
+	}
+
+	if !i.useServerUUID {
+		attrs["owncloudUUID"] = []string{uuid.Must(uuid.NewV4()).String()}
+		attrs["objectClass"] = append(attrs["objectClass"], "owncloud")
+	}
+
+	// inetOrgPerson requires "sn" to be set. Set it to the Username if
+	// Surname is not set in the Request
+	var sn string
+	if user.Surname != nil && *user.Surname != "" {
+		sn = *user.Surname
+	} else {
+		sn = *user.OnPremisesSamAccountName
+	}
+	attrs["sn"] = []string{sn}
+
+	if !i.usePwModifyExOp && user.PasswordProfile != nil && user.PasswordProfile.Password != nil {
+		// Depending on the LDAP server implementation this might cause the
+		// password to be stored in cleartext in the LDAP database. Using the
+		// "Password Modify LDAP Extended Operation" is recommended.
+		attrs["userPassword"] = []string{*user.PasswordProfile.Password}
+	}
+	return attrs, nil
+}
+
+func (i *LDAP) getUserLDAPDN(user libregraph.User) string {
+	return fmt.Sprintf("uid=%s,%s", oldap.EscapeDNAttributeValue(*user.OnPremisesSamAccountName), i.userBaseDN)
+}
+
+func (i *LDAP) userToAddRequest(user libregraph.User) (*ldap.AddRequest, error) {
+	ar := ldap.NewAddRequest(i.getUserLDAPDN(user), nil)
+
+	attrMap, err := i.userToLDAPAttrValues(user)
+	if err != nil {
+		return nil, err
+	}
+	for attrType, values := range attrMap {
+		ar.Attribute(attrType, values)
+	}
+	return ar, nil
+}
+
 func pointerOrNil(val string) *string {
 	if val == "" {
 		return nil
diff --git a/services/graph/pkg/identity/ldap_education_user.go b/services/graph/pkg/identity/ldap_education_user.go
index 391584b25f7..1905f81d049 100644
--- a/services/graph/pkg/identity/ldap_education_user.go
+++ b/services/graph/pkg/identity/ldap_education_user.go
@@ -2,19 +2,78 @@ package identity
 
 import (
 	"context"
+	"errors"
+	"fmt"
 	"net/url"
+	"strings"
 
+	"github.com/go-ldap/ldap/v3"
 	libregraph "github.com/owncloud/libre-graph-api-go"
+	"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
 )
 
+type educationUserAttributeMap struct {
+	identities  string
+	primaryRole string
+}
+
+func newEducationUserAttributeMap() educationUserAttributeMap {
+	return educationUserAttributeMap{
+		identities:  "oCExternalIdentity",
+		primaryRole: "userClass",
+	}
+}
+
 // CreateEducationUser creates a given education user in the identity backend.
 func (i *LDAP) CreateEducationUser(ctx context.Context, user libregraph.EducationUser) (*libregraph.EducationUser, error) {
-	return nil, errNotImplemented
+	logger := i.logger.SubloggerWithRequestID(ctx)
+	logger.Debug().Str("backend", "ldap").Msg("CreateEducationUser")
+	if !i.writeEnabled {
+		return nil, errReadOnly
+	}
+
+	ar, err := i.educationUserToAddRequest(user)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := i.conn.Add(ar); err != nil {
+		var lerr *ldap.Error
+		logger.Debug().Err(err).Msg("error adding user")
+		if errors.As(err, &lerr) {
+			if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
+				err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error())
+			}
+		}
+		return nil, err
+	}
+
+	// Read	back user from LDAP to get the generated UUID
+	e, err := i.getEducationUserByDN(ar.DN)
+	if err != nil {
+		return nil, err
+	}
+	return i.createEducationUserModelFromLDAP(e), nil
 }
 
 // DeleteEducationUser deletes a given educationuser, identified by username or id, from the backend
 func (i *LDAP) DeleteEducationUser(ctx context.Context, nameOrID string) error {
-	return errNotImplemented
+	logger := i.logger.SubloggerWithRequestID(ctx)
+	logger.Debug().Str("backend", "ldap").Msg("DeleteEducationUser")
+	if !i.writeEnabled {
+		return errReadOnly
+	}
+	// TODO, implement a proper lookup for education Users here
+	e, err := i.getEducationUserByNameOrID(nameOrID)
+	if err != nil {
+		return err
+	}
+
+	dr := ldap.DelRequest{DN: e.DN}
+	if err = i.conn.Del(&dr); err != nil {
+		return err
+	}
+	return nil
 }
 
 // UpdateEducationUser applies changes to given education user, identified by username or id
@@ -24,10 +83,193 @@ 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) {
-	return nil, errNotImplemented
+	logger := i.logger.SubloggerWithRequestID(ctx)
+	logger.Debug().Str("backend", "ldap").Msg("GetEducationUser")
+	e, err := i.getEducationUserByNameOrID(nameOrID)
+	if err != nil {
+		return nil, err
+	}
+	u := i.createEducationUserModelFromLDAP(e)
+	if u == nil {
+		return nil, errNotFound
+	}
+	return u, nil
 }
 
 // GetEducationUsers implements the EducationBackend interface for the LDAP backend.
 func (i *LDAP) GetEducationUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationUser, error) {
-	return nil, errNotImplemented
+	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 == "" {
+		userFilter = fmt.Sprintf("(objectClass=%s)", i.educationConfig.userObjectClass)
+	} else {
+		userFilter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.userFilter, i.educationConfig.userObjectClass, userFilter)
+	}
+
+	searchRequest := ldap.NewSearchRequest(
+		i.userBaseDN,
+		i.userScope,
+		ldap.NeverDerefAliases, 0, 0, false,
+		userFilter,
+		i.getEducationUserAttrTypes(),
+		nil,
+	)
+	logger.Debug().Str("backend", "ldap").
+		Str("base", searchRequest.BaseDN).
+		Str("filter", searchRequest.Filter).
+		Int("scope", searchRequest.Scope).
+		Int("sizelimit", searchRequest.SizeLimit).
+		Interface("attributes", searchRequest.Attributes).
+		Msg("GetEducationUsers")
+	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()
+	user.OnPremisesSamAccountName = eduUser.OnPremisesSamAccountName
+	user.Surname = eduUser.Surname
+	user.AccountEnabled = eduUser.AccountEnabled
+	user.GivenName = eduUser.GivenName
+	user.DisplayName = eduUser.DisplayName
+	user.Mail = eduUser.Mail
+	return user
+}
+func (i *LDAP) userToEducationUser(user libregraph.User, e *ldap.Entry) *libregraph.EducationUser {
+	eduUser := libregraph.NewEducationUser()
+	eduUser.Id = user.Id
+	eduUser.OnPremisesSamAccountName = user.OnPremisesSamAccountName
+	eduUser.Surname = user.Surname
+	eduUser.AccountEnabled = user.AccountEnabled
+	eduUser.GivenName = user.GivenName
+	eduUser.DisplayName = user.DisplayName
+	eduUser.Mail = user.Mail
+
+	if e != nil {
+		// Set the education User specific Attributes from the supplied LDAP Entry
+		if primaryRole := e.GetEqualFoldAttributeValue(i.educationConfig.userAttributeMap.primaryRole); primaryRole != "" {
+			eduUser.SetPrimaryRole(primaryRole)
+		}
+		var identities []libregraph.ObjectIdentity
+		for _, identityStr := range e.GetEqualFoldAttributeValues(i.educationConfig.userAttributeMap.identities) {
+			parts := strings.SplitN(identityStr, "$", 3)
+			identity := libregraph.NewObjectIdentity()
+			identity.SetIssuer(strings.TrimSpace(parts[1]))
+			identity.SetIssuerAssignedId(strings.TrimSpace(parts[2]))
+			identities = append(identities, *identity)
+		}
+		if len(identities) > 0 {
+			eduUser.SetIdentities(identities)
+		}
+	}
+
+	return eduUser
+}
+
+func (i *LDAP) educationUserToLDAPAttrValues(user libregraph.EducationUser, attrs ldapAttributeValues) (ldapAttributeValues, error) {
+	if role, ok := user.GetPrimaryRoleOk(); ok {
+		attrs[i.educationConfig.userAttributeMap.primaryRole] = []string{*role}
+	}
+	if identities, ok := user.GetIdentitiesOk(); ok {
+		for _, identity := range identities {
+			// TODO add support for the "signInType" of objectIdentity
+			if identity.GetIssuer() == "" || identity.GetIssuerAssignedId() == "" {
+				return nil, fmt.Errorf("missing Attribute for objectIdentity")
+			}
+			identityStr := fmt.Sprintf(" $ %s $ %s", identity.GetIssuer(), identity.GetIssuerAssignedId())
+			attrs[i.educationConfig.userAttributeMap.identities] = append(
+				attrs[i.educationConfig.userAttributeMap.identities],
+				identityStr,
+			)
+		}
+	}
+	attrs["objectClass"] = append(attrs["objectClass"], i.educationConfig.userObjectClass)
+	return attrs, nil
+}
+
+func (i *LDAP) educationUserToAddRequest(user libregraph.EducationUser) (*ldap.AddRequest, error) {
+	plainUser := i.educationUserToUser(user)
+	ldapAttrs, err := i.userToLDAPAttrValues(*plainUser)
+	if err != nil {
+		return nil, err
+	}
+	ldapAttrs, err = i.educationUserToLDAPAttrValues(user, ldapAttrs)
+	if err != nil {
+		return nil, err
+	}
+
+	ar := ldap.NewAddRequest(i.getUserLDAPDN(*plainUser), nil)
+
+	for attrType, values := range ldapAttrs {
+		ar.Attribute(attrType, values)
+	}
+	return ar, nil
+}
+
+func (i *LDAP) createEducationUserModelFromLDAP(e *ldap.Entry) *libregraph.EducationUser {
+	user := i.createUserModelFromLDAP(e)
+	return i.userToEducationUser(*user, e)
+}
+
+func (i *LDAP) getEducationUserAttrTypes() []string {
+	return []string{
+		i.userAttributeMap.displayName,
+		i.userAttributeMap.id,
+		i.userAttributeMap.mail,
+		i.userAttributeMap.userName,
+		i.educationConfig.userAttributeMap.identities,
+		i.educationConfig.userAttributeMap.primaryRole,
+		i.educationConfig.memberOfSchoolAttribute,
+	}
+}
+
+func (i *LDAP) getEducationUserByDN(dn string) (*ldap.Entry, error) {
+	filter := fmt.Sprintf("(objectClass=%s)", i.educationConfig.userObjectClass)
+
+	if i.userFilter != "" {
+		filter = fmt.Sprintf("(&%s(%s))", filter, i.userFilter)
+	}
+
+	return i.getEntryByDN(dn, i.getEducationUserAttrTypes(), filter)
+}
+
+func (i *LDAP) getEducationUserByNameOrID(nameOrID string) (*ldap.Entry, error) {
+	nameOrID = ldap.EscapeFilter(nameOrID)
+	filter := fmt.Sprintf("(|(%s=%s)(%s=%s))", i.userAttributeMap.userName, nameOrID, i.userAttributeMap.id, nameOrID)
+	return i.getEducationUserByFilter(filter)
+}
+
+func (i *LDAP) getEducationUserByFilter(filter string) (*ldap.Entry, error) {
+	filter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.userFilter, i.educationConfig.userObjectClass, filter)
+	return i.searchLDAPEntryByFilter(i.userBaseDN, i.getEducationUserAttrTypes(), filter)
 }
diff --git a/services/graph/pkg/identity/ldap_school.go b/services/graph/pkg/identity/ldap_school.go
index ec30b1ccc1a..f344a8d0431 100644
--- a/services/graph/pkg/identity/ldap_school.go
+++ b/services/graph/pkg/identity/ldap_school.go
@@ -15,11 +15,17 @@ import (
 )
 
 type educationConfig struct {
-	schoolBaseDN       string
-	schoolFilter       string
-	schoolObjectClass  string
-	schoolScope        int
-	schoolAttributeMap schoolAttributeMap
+	schoolBaseDN      string
+	schoolFilter      string
+	schoolObjectClass string
+	schoolScope       int
+	// memberOfSchoolAttribute defines the AttributeType on the user/group objects
+	// which contains the school Id to which the user/group is assigned
+	memberOfSchoolAttribute string
+	schoolAttributeMap      schoolAttributeMap
+
+	userObjectClass  string
+	userAttributeMap educationUserAttributeMap
 }
 
 type schoolAttributeMap struct {
@@ -30,9 +36,13 @@ type schoolAttributeMap struct {
 
 func defaultEducationConfig() educationConfig {
 	return educationConfig{
-		schoolObjectClass:  "ocEducationSchool",
-		schoolScope:        ldap.ScopeWholeSubtree,
-		schoolAttributeMap: newSchoolAttributeMap(),
+		schoolObjectClass:       "ocEducationSchool",
+		schoolScope:             ldap.ScopeWholeSubtree,
+		memberOfSchoolAttribute: "ocMemberOfSchool",
+		schoolAttributeMap:      newSchoolAttributeMap(),
+
+		userObjectClass:  "ocEducationUser",
+		userAttributeMap: newEducationUserAttributeMap(),
 	}
 }
 
@@ -134,6 +144,8 @@ func (i *LDAP) DeleteEducationSchool(ctx context.Context, id string) error {
 	if err = i.conn.Del(&dr); err != nil {
 		return err
 	}
+
+	// TODO update any users that are member of this school
 	return nil
 }
 
@@ -194,19 +206,131 @@ func (i *LDAP) GetEducationSchools(ctx context.Context, queryParam url.Values) (
 	return schools, nil
 }
 
-// GetEducationSchoolMembers implements the EducationBackend interface for the LDAP backend.
-func (i *LDAP) GetEducationSchoolMembers(ctx context.Context, id string) ([]*libregraph.EducationUser, error) {
-	return nil, errNotImplemented
+// GetEducationSchoolUsers implements the EducationBackend interface for the LDAP backend.
+func (i *LDAP) GetEducationSchoolUsers(ctx context.Context, id string) ([]*libregraph.EducationUser, error) {
+	logger := i.logger.SubloggerWithRequestID(ctx)
+	logger.Debug().Str("backend", "ldap").Msg("GetEducationSchoolUsers")
+
+	schoolEntry, err := i.getSchoolByID(id)
+	if err != nil {
+		return nil, err
+	}
+
+	if schoolEntry == nil {
+		return nil, errNotFound
+	}
+	id = ldap.EscapeFilter(id)
+	idFilter := fmt.Sprintf("(%s=%s)", i.educationConfig.memberOfSchoolAttribute, id)
+	userFilter := fmt.Sprintf("(&%s(objectClass=%s)%s)", i.userFilter, i.educationConfig.userObjectClass, idFilter)
+
+	searchRequest := ldap.NewSearchRequest(
+		i.userBaseDN,
+		i.userScope,
+		ldap.NeverDerefAliases, 0, 0, false,
+		userFilter,
+		i.getEducationUserAttrTypes(),
+		nil,
+	)
+	logger.Debug().Str("backend", "ldap").
+		Str("base", searchRequest.BaseDN).
+		Str("filter", searchRequest.Filter).
+		Int("scope", searchRequest.Scope).
+		Int("sizelimit", searchRequest.SizeLimit).
+		Interface("attributes", searchRequest.Attributes).
+		Msg("GetEducationUsers")
+	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
 }
 
-// AddMembersToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
-func (i *LDAP) AddMembersToEducationSchool(ctx context.Context, schoolID string, memberID []string) error {
-	return errNotImplemented
+// AddUsersToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
+func (i *LDAP) AddUsersToEducationSchool(ctx context.Context, schoolID string, memberIDs []string) error {
+	logger := i.logger.SubloggerWithRequestID(ctx)
+	logger.Debug().Str("backend", "ldap").Msg("AddUsersToEducationSchool")
+
+	schoolEntry, err := i.getSchoolByID(schoolID)
+	if err != nil {
+		return err
+	}
+
+	if schoolEntry == nil {
+		return errNotFound
+	}
+
+	userEntries := make([]*ldap.Entry, 0, len(memberIDs))
+	for _, memberID := range memberIDs {
+		user, err := i.getEducationUserByNameOrID(memberID)
+		if err != nil {
+			i.logger.Warn().Str("userid", memberID).Msg("User does not exist")
+			return err
+		}
+		userEntries = append(userEntries, user)
+	}
+
+	for _, userEntry := range userEntries {
+		currentSchools := userEntry.GetEqualFoldAttributeValues(i.educationConfig.memberOfSchoolAttribute)
+		found := false
+		for _, currentSchool := range currentSchools {
+			if currentSchool == schoolID {
+				found = true
+				break
+			}
+		}
+		if !found {
+			mr := ldap.ModifyRequest{DN: userEntry.DN}
+			mr.Add(i.educationConfig.memberOfSchoolAttribute, []string{schoolID})
+			if err := i.conn.Modify(&mr); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
 }
 
-// RemoveMemberFromEducationSchool removes a single member (by ID) from a school
-func (i *LDAP) RemoveMemberFromEducationSchool(ctx context.Context, schoolID string, memberID string) error {
-	return errNotImplemented
+// RemoveUserFromEducationSchool removes a single member (by ID) from a school
+func (i *LDAP) RemoveUserFromEducationSchool(ctx context.Context, schoolID string, memberID string) error {
+	logger := i.logger.SubloggerWithRequestID(ctx)
+	logger.Debug().Str("backend", "ldap").Msg("RemoveUserFromEducationSchool")
+
+	schoolEntry, err := i.getSchoolByID(schoolID)
+	if err != nil {
+		return err
+	}
+
+	if schoolEntry == nil {
+		return errNotFound
+	}
+	user, err := i.getEducationUserByNameOrID(memberID)
+	if err != nil {
+		i.logger.Warn().Str("userid", memberID).Msg("User does not exist")
+		return err
+	}
+	currentSchools := user.GetEqualFoldAttributeValues(i.educationConfig.memberOfSchoolAttribute)
+	for _, currentSchool := range currentSchools {
+		if currentSchool == schoolID {
+			mr := ldap.ModifyRequest{DN: user.DN}
+			mr.Delete(i.educationConfig.memberOfSchoolAttribute, []string{schoolID})
+			if err := i.conn.Modify(&mr); err != nil {
+				return err
+			}
+			break
+		}
+	}
+	return nil
 }
 
 func (i *LDAP) getSchoolByDN(dn string) (*ldap.Entry, error) {
diff --git a/services/graph/pkg/identity/mocks/education_backend.go b/services/graph/pkg/identity/mocks/education_backend.go
index edd08944702..031510d3aec 100644
--- a/services/graph/pkg/identity/mocks/education_backend.go
+++ b/services/graph/pkg/identity/mocks/education_backend.go
@@ -17,8 +17,8 @@ type EducationBackend struct {
 	mock.Mock
 }
 
-// AddMembersToEducationSchool provides a mock function with given fields: ctx, schoolID, memberID
-func (_m *EducationBackend) AddMembersToEducationSchool(ctx context.Context, schoolID string, memberID []string) error {
+// AddUsersToEducationSchool provides a mock function with given fields: ctx, schoolID, memberID
+func (_m *EducationBackend) AddUsersToEducationSchool(ctx context.Context, schoolID string, memberID []string) error {
 	ret := _m.Called(ctx, schoolID, memberID)
 
 	var r0 error
@@ -128,8 +128,8 @@ func (_m *EducationBackend) GetEducationSchool(ctx context.Context, nameOrID str
 	return r0, r1
 }
 
-// GetEducationSchoolMembers provides a mock function with given fields: ctx, id
-func (_m *EducationBackend) GetEducationSchoolMembers(ctx context.Context, id string) ([]*libregraph.EducationUser, error) {
+// GetEducationSchoolUsers provides a mock function with given fields: ctx, id
+func (_m *EducationBackend) GetEducationSchoolUsers(ctx context.Context, id string) ([]*libregraph.EducationUser, error) {
 	ret := _m.Called(ctx, id)
 
 	var r0 []*libregraph.EducationUser
@@ -220,8 +220,8 @@ func (_m *EducationBackend) GetEducationUsers(ctx context.Context, queryParam ur
 	return r0, r1
 }
 
-// RemoveMemberFromEducationSchool provides a mock function with given fields: ctx, schoolID, memberID
-func (_m *EducationBackend) RemoveMemberFromEducationSchool(ctx context.Context, schoolID string, memberID string) error {
+// RemoveUserFromEducationSchool provides a mock function with given fields: ctx, schoolID, memberID
+func (_m *EducationBackend) RemoveUserFromEducationSchool(ctx context.Context, schoolID string, memberID string) error {
 	ret := _m.Called(ctx, schoolID, memberID)
 
 	var r0 error
diff --git a/services/graph/pkg/service/v0/educationschools.go b/services/graph/pkg/service/v0/educationschools.go
index f9942f8c4ff..08c51dbca21 100644
--- a/services/graph/pkg/service/v0/educationschools.go
+++ b/services/graph/pkg/service/v0/educationschools.go
@@ -221,28 +221,28 @@ func (g Graph) DeleteEducationSchool(w http.ResponseWriter, r *http.Request) {
 	render.NoContent(w, r)
 }
 
-// GetEducationSchoolMembers implements the Service interface.
-func (g Graph) GetEducationSchoolMembers(w http.ResponseWriter, r *http.Request) {
+// GetEducationSchoolUsers implements the Service interface.
+func (g Graph) GetEducationSchoolUsers(w http.ResponseWriter, r *http.Request) {
 	logger := g.logger.SubloggerWithRequestID(r.Context())
-	logger.Info().Msg("calling get school members")
+	logger.Info().Msg("calling get school users")
 	schoolID := chi.URLParam(r, "schoolID")
 	schoolID, err := url.PathUnescape(schoolID)
 	if err != nil {
-		logger.Debug().Str("id", schoolID).Msg("could not get school members: unescaping school id failed")
+		logger.Debug().Str("id", schoolID).Msg("could not get school users: unescaping school id failed")
 		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping school id failed")
 		return
 	}
 
 	if schoolID == "" {
-		logger.Debug().Msg("could not get school members: missing school id")
+		logger.Debug().Msg("could not get school users: missing school id")
 		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing school id")
 		return
 	}
 
-	logger.Debug().Str("id", schoolID).Msg("calling get school members on backend")
-	members, err := g.identityEducationBackend.GetEducationSchoolMembers(r.Context(), schoolID)
+	logger.Debug().Str("id", schoolID).Msg("calling get school users on backend")
+	users, err := g.identityEducationBackend.GetEducationSchoolUsers(r.Context(), schoolID)
 	if err != nil {
-		logger.Debug().Err(err).Msg("could not get school members: backend error")
+		logger.Debug().Err(err).Msg("could not get school users: backend error")
 		var errcode errorcode.Error
 		if errors.As(err, &errcode) {
 			errcode.Render(w, r)
@@ -253,13 +253,13 @@ func (g Graph) GetEducationSchoolMembers(w http.ResponseWriter, r *http.Request)
 	}
 
 	render.Status(r, http.StatusOK)
-	render.JSON(w, r, members)
+	render.JSON(w, r, users)
 }
 
-// PostEducationSchoolMember implements the Service interface.
-func (g Graph) PostEducationSchoolMember(w http.ResponseWriter, r *http.Request) {
+// PostEducationSchoolUser implements the Service interface.
+func (g Graph) PostEducationSchoolUser(w http.ResponseWriter, r *http.Request) {
 	logger := g.logger.SubloggerWithRequestID(r.Context())
-	logger.Info().Msg("Calling post school member")
+	logger.Info().Msg("Calling post school user")
 
 	schoolID := chi.URLParam(r, "schoolID")
 	schoolID, err := url.PathUnescape(schoolID)
@@ -267,13 +267,13 @@ func (g Graph) PostEducationSchoolMember(w http.ResponseWriter, r *http.Request)
 		logger.Debug().
 			Err(err).
 			Str("id", schoolID).
-			Msg("could not add member to school: unescaping school id failed")
+			Msg("could not add user to school: unescaping school id failed")
 		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping school id failed")
 		return
 	}
 
 	if schoolID == "" {
-		logger.Debug().Msg("could not add school member: missing school id")
+		logger.Debug().Msg("could not add school user: missing school id")
 		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing school id")
 		return
 	}
@@ -283,35 +283,35 @@ func (g Graph) PostEducationSchoolMember(w http.ResponseWriter, r *http.Request)
 		logger.Debug().
 			Err(err).
 			Interface("body", r.Body).
-			Msg("could not add school member: invalid request body")
+			Msg("could not add school user: invalid request body")
 		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid request body: %s", err.Error()))
 		return
 	}
 	memberRefURL, ok := memberRef.GetOdataIdOk()
 	if !ok {
-		logger.Debug().Msg("could not add school member: @odata.id reference is missing")
+		logger.Debug().Msg("could not add school user: @odata.id reference is missing")
 		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "@odata.id reference is missing")
 		return
 	}
 	memberType, id, err := g.parseMemberRef(*memberRefURL)
 	if err != nil {
-		logger.Debug().Err(err).Msg("could not add school member: error parsing @odata.id url")
+		logger.Debug().Err(err).Msg("could not add school user: error parsing @odata.id url")
 		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Error parsing @odata.id url")
 		return
 	}
 	// The MS Graph spec allows "directoryObject", "user", "school" and "organizational Contact"
 	// we restrict this to users for now. Might add Schools as members later
 	if memberType != "users" {
-		logger.Debug().Str("type", memberType).Msg("could not add school member: Only users are allowed as school members")
+		logger.Debug().Str("type", memberType).Msg("could not add school user: Only users are allowed as school members")
 		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Only users are allowed as school members")
 		return
 	}
 
-	logger.Debug().Str("memberType", memberType).Str("id", id).Msg("calling add member on backend")
-	err = g.identityEducationBackend.AddMembersToEducationSchool(r.Context(), schoolID, []string{id})
+	logger.Debug().Str("memberType", memberType).Str("id", id).Msg("calling add user on backend")
+	err = g.identityEducationBackend.AddUsersToEducationSchool(r.Context(), schoolID, []string{id})
 
 	if err != nil {
-		logger.Debug().Err(err).Msg("could not add school member: backend error")
+		logger.Debug().Err(err).Msg("could not add school user: backend error")
 		var errcode errorcode.Error
 		if errors.As(err, &errcode) {
 			errcode.Render(w, r)
@@ -333,8 +333,8 @@ func (g Graph) PostEducationSchoolMember(w http.ResponseWriter, r *http.Request)
 	render.NoContent(w, r)
 }
 
-// DeleteEducationSchoolMember implements the Service interface.
-func (g Graph) DeleteEducationSchoolMember(w http.ResponseWriter, r *http.Request) {
+// DeleteEducationSchoolUser implements the Service interface.
+func (g Graph) DeleteEducationSchoolUser(w http.ResponseWriter, r *http.Request) {
 	logger := g.logger.SubloggerWithRequestID(r.Context())
 	logger.Info().Msg("calling delete school member")
 
@@ -352,21 +352,21 @@ func (g Graph) DeleteEducationSchoolMember(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	memberID := chi.URLParam(r, "memberID")
-	memberID, err = url.PathUnescape(memberID)
+	userID := chi.URLParam(r, "userID")
+	userID, err = url.PathUnescape(userID)
 	if err != nil {
-		logger.Debug().Err(err).Str("id", memberID).Msg("could not delete school member: unescaping member id failed")
+		logger.Debug().Err(err).Str("id", userID).Msg("could not delete school member: unescaping member id failed")
 		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping member id failed")
 		return
 	}
 
-	if memberID == "" {
+	if userID == "" {
 		logger.Debug().Msg("could not delete school member: missing member id")
 		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing member id")
 		return
 	}
-	logger.Debug().Str("schoolID", schoolID).Str("memberID", memberID).Msg("calling delete member on backend")
-	err = g.identityEducationBackend.RemoveMemberFromEducationSchool(r.Context(), schoolID, memberID)
+	logger.Debug().Str("schoolID", schoolID).Str("userID", userID).Msg("calling delete member on backend")
+	err = g.identityEducationBackend.RemoveUserFromEducationSchool(r.Context(), schoolID, userID)
 
 	if err != nil {
 		logger.Debug().Err(err).Msg("could not delete school member: backend error")
@@ -380,7 +380,7 @@ func (g Graph) DeleteEducationSchoolMember(w http.ResponseWriter, r *http.Reques
 	}
 
 	/* TODO requires reva changes
-	e := events.SchoolMemberRemoved{SchoolID: schoolID, UserID: memberID}
+	e := events.SchoolMemberRemoved{SchoolID: schoolID, UserID: userID}
 	if currentUser, ok := ctxpkg.ContextGetUser(r.Context()); ok {
 		e.Executant = currentUser.GetId()
 	}
diff --git a/services/graph/pkg/service/v0/educationuser.go b/services/graph/pkg/service/v0/educationuser.go
index 7757b0e0fd4..41922ed0af3 100644
--- a/services/graph/pkg/service/v0/educationuser.go
+++ b/services/graph/pkg/service/v0/educationuser.go
@@ -259,7 +259,7 @@ func (g Graph) GetEducationUser(w http.ResponseWriter, r *http.Request) {
 			}
 			d.Quota = quota
 			if slices.Contains(sel, "drive") || slices.Contains(exp, "drive") {
-				if *d.DriveType == "personal" {
+				if *d.DriveType == _spaceTypePersonal {
 					user.Drive = d
 				}
 			} else {
@@ -330,7 +330,7 @@ func (g Graph) DeleteEducationUser(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 		for _, sp := range lspr.GetStorageSpaces() {
-			if !(sp.SpaceType == "personal" && sp.Owner.Id.OpaqueId == user.GetId()) {
+			if !(sp.SpaceType == _spaceTypePersonal && sp.Owner.Id.OpaqueId == user.GetId()) {
 				continue
 			}
 			// TODO: check if request contains a homespace and if, check if requesting user has the privilege to
diff --git a/services/graph/pkg/service/v0/instrument.go b/services/graph/pkg/service/v0/instrument.go
index 81fc6dbbf3d..c1567cd4048 100644
--- a/services/graph/pkg/service/v0/instrument.go
+++ b/services/graph/pkg/service/v0/instrument.go
@@ -124,19 +124,19 @@ func (i instrument) DeleteEducationSchool(w http.ResponseWriter, r *http.Request
 	i.next.DeleteEducationSchool(w, r)
 }
 
-// GetEducationSchoolMembers implements the Service interface.
-func (i instrument) GetEducationSchoolMembers(w http.ResponseWriter, r *http.Request) {
-	i.next.GetEducationSchoolMembers(w, r)
+// GetEducationSchoolUsers implements the Service interface.
+func (i instrument) GetEducationSchoolUsers(w http.ResponseWriter, r *http.Request) {
+	i.next.GetEducationSchoolUsers(w, r)
 }
 
-// PostEducationSchoolMember implements the Service interface.
-func (i instrument) PostEducationSchoolMember(w http.ResponseWriter, r *http.Request) {
-	i.next.PostEducationSchoolMember(w, r)
+// PostEducationSchoolUser implements the Service interface.
+func (i instrument) PostEducationSchoolUser(w http.ResponseWriter, r *http.Request) {
+	i.next.PostEducationSchoolUser(w, r)
 }
 
-// DeleteEducationSchoolMember implements the Service interface.
-func (i instrument) DeleteEducationSchoolMember(w http.ResponseWriter, r *http.Request) {
-	i.next.DeleteEducationSchoolMember(w, r)
+// DeleteEducationSchoolUser implements the Service interface.
+func (i instrument) DeleteEducationSchoolUser(w http.ResponseWriter, r *http.Request) {
+	i.next.DeleteEducationSchoolUser(w, r)
 }
 
 // GetEducationUsers implements the Service interface.
diff --git a/services/graph/pkg/service/v0/logging.go b/services/graph/pkg/service/v0/logging.go
index 69361da2cea..b5c832b7969 100644
--- a/services/graph/pkg/service/v0/logging.go
+++ b/services/graph/pkg/service/v0/logging.go
@@ -124,19 +124,19 @@ func (l logging) DeleteEducationSchool(w http.ResponseWriter, r *http.Request) {
 	l.next.DeleteEducationSchool(w, r)
 }
 
-// GetEducationSchoolMembers implements the Service interface.
-func (l logging) GetEducationSchoolMembers(w http.ResponseWriter, r *http.Request) {
-	l.next.GetEducationSchoolMembers(w, r)
+// GetEducationSchoolUsers implements the Service interface.
+func (l logging) GetEducationSchoolUsers(w http.ResponseWriter, r *http.Request) {
+	l.next.GetEducationSchoolUsers(w, r)
 }
 
-// PostEducationSchoolMember implements the Service interface.
-func (l logging) PostEducationSchoolMember(w http.ResponseWriter, r *http.Request) {
-	l.next.PostEducationSchoolMember(w, r)
+// PostEducationSchoolUser implements the Service interface.
+func (l logging) PostEducationSchoolUser(w http.ResponseWriter, r *http.Request) {
+	l.next.PostEducationSchoolUser(w, r)
 }
 
-// DeleteEducationSchoolMember implements the Service interface.
-func (l logging) DeleteEducationSchoolMember(w http.ResponseWriter, r *http.Request) {
-	l.next.DeleteEducationSchoolMember(w, r)
+// DeleteEducationSchoolUser implements the Service interface.
+func (l logging) DeleteEducationSchoolUser(w http.ResponseWriter, r *http.Request) {
+	l.next.DeleteEducationSchoolUser(w, r)
 }
 
 // GetEducationUsers implements the Service interface.
diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go
index 08c68ab908c..a4858d0f22a 100644
--- a/services/graph/pkg/service/v0/service.go
+++ b/services/graph/pkg/service/v0/service.go
@@ -52,9 +52,9 @@ type Service interface {
 	PostEducationSchool(http.ResponseWriter, *http.Request)
 	PatchEducationSchool(http.ResponseWriter, *http.Request)
 	DeleteEducationSchool(http.ResponseWriter, *http.Request)
-	GetEducationSchoolMembers(http.ResponseWriter, *http.Request)
-	PostEducationSchoolMember(http.ResponseWriter, *http.Request)
-	DeleteEducationSchoolMember(http.ResponseWriter, *http.Request)
+	GetEducationSchoolUsers(http.ResponseWriter, *http.Request)
+	PostEducationSchoolUser(http.ResponseWriter, *http.Request)
+	DeleteEducationSchoolUser(http.ResponseWriter, *http.Request)
 
 	GetEducationUsers(http.ResponseWriter, *http.Request)
 	GetEducationUser(http.ResponseWriter, *http.Request)
@@ -250,10 +250,10 @@ func NewService(opts ...Option) Service {
 						r.Get("/", svc.GetEducationSchool)
 						r.Delete("/", svc.DeleteEducationSchool)
 						r.Patch("/", svc.PatchEducationSchool)
-						r.Route("/members", func(r chi.Router) {
-							r.Get("/", svc.GetEducationSchoolMembers)
-							r.Post("/$ref", svc.PostEducationSchoolMember)
-							r.Delete("/{memberID}/$ref", svc.DeleteEducationSchoolMember)
+						r.Route("/users", func(r chi.Router) {
+							r.Get("/", svc.GetEducationSchoolUsers)
+							r.Post("/$ref", svc.PostEducationSchoolUser)
+							r.Delete("/{userID}/$ref", svc.DeleteEducationSchoolUser)
 						})
 					})
 				})
diff --git a/services/graph/pkg/service/v0/tracing.go b/services/graph/pkg/service/v0/tracing.go
index 306cf74456e..99daa0cb2ca 100644
--- a/services/graph/pkg/service/v0/tracing.go
+++ b/services/graph/pkg/service/v0/tracing.go
@@ -120,19 +120,19 @@ func (t tracing) DeleteEducationSchool(w http.ResponseWriter, r *http.Request) {
 	t.next.DeleteEducationSchool(w, r)
 }
 
-// GetEducationSchoolMembers implements the Service interface.
-func (t tracing) GetEducationSchoolMembers(w http.ResponseWriter, r *http.Request) {
-	t.next.GetEducationSchoolMembers(w, r)
+// GetEducationSchoolUsers implements the Service interface.
+func (t tracing) GetEducationSchoolUsers(w http.ResponseWriter, r *http.Request) {
+	t.next.GetEducationSchoolUsers(w, r)
 }
 
-// PostEducationSchoolMember implements the Service interface.
-func (t tracing) PostEducationSchoolMember(w http.ResponseWriter, r *http.Request) {
-	t.next.PostEducationSchoolMember(w, r)
+// PostEducationSchoolUser implements the Service interface.
+func (t tracing) PostEducationSchoolUser(w http.ResponseWriter, r *http.Request) {
+	t.next.PostEducationSchoolUser(w, r)
 }
 
-// DeleteEducationSchoolMember implements the Service interface.
-func (t tracing) DeleteEducationSchoolMember(w http.ResponseWriter, r *http.Request) {
-	t.next.DeleteEducationSchoolMember(w, r)
+// DeleteEducationSchoolUser implements the Service interface.
+func (t tracing) DeleteEducationSchoolUser(w http.ResponseWriter, r *http.Request) {
+	t.next.DeleteEducationSchoolUser(w, r)
 }
 
 // GetEducationUsers implements the Service interface.


Test patch:
diff --git a/services/graph/pkg/identity/ldap_education_user_test.go b/services/graph/pkg/identity/ldap_education_user_test.go
new file mode 100644
index 00000000000..2d1017cff74
--- /dev/null
+++ b/services/graph/pkg/identity/ldap_education_user_test.go
@@ -0,0 +1,145 @@
+package identity
+
+import (
+	"context"
+	"testing"
+
+	"github.com/go-ldap/ldap/v3"
+	libregraph "github.com/owncloud/libre-graph-api-go"
+	"github.com/owncloud/ocis/v2/services/graph/mocks"
+	"github.com/test-go/testify/assert"
+	"github.com/test-go/testify/mock"
+)
+
+var eduUserEntry = ldap.NewEntry("uid=user,ou=people,dc=test",
+	map[string][]string{
+		"uid":         {"testuser"},
+		"displayname": {"Test User"},
+		"mail":        {"user@example"},
+		"entryuuid":   {"abcd-defg"},
+		"userClass":   {"student"},
+		"oCExternalIdentity": {
+			"$ http://idp $ testuser",
+			"xxx $ http://idpnew $ xxxxx-xxxxx-xxxxx",
+		},
+	})
+var eduUserEntryWithSchool = ldap.NewEntry("uid=user,ou=people,dc=test",
+	map[string][]string{
+		"uid":              {"testuser"},
+		"displayname":      {"Test User"},
+		"mail":             {"user@example"},
+		"entryuuid":        {"abcd-defg"},
+		"userClass":        {"student"},
+		"ocMemberOfSchool": {"abcd-defg"},
+		"oCExternalIdentity": {
+			"$ http://idp $ testuser",
+			"xxx $ http://idpnew $ xxxxx-xxxxx-xxxxx",
+		},
+	})
+
+var sr1 *ldap.SearchRequest = &ldap.SearchRequest{
+	BaseDN:     "ou=people,dc=test",
+	Scope:      2,
+	SizeLimit:  1,
+	Filter:     "(&(objectClass=ocEducationUser)(|(uid=abcd-defg)(entryUUID=abcd-defg)))",
+	Attributes: []string{"displayname", "entryUUID", "mail", "uid", "oCExternalIdentity", "userClass", "ocMemberOfSchool"},
+	Controls:   []ldap.Control(nil),
+}
+var sr2 *ldap.SearchRequest = &ldap.SearchRequest{
+	BaseDN:     "ou=people,dc=test",
+	Scope:      2,
+	SizeLimit:  1,
+	Filter:     "(&(objectClass=ocEducationUser)(|(uid=xxxx-xxxx)(entryUUID=xxxx-xxxx)))",
+	Attributes: []string{"displayname", "entryUUID", "mail", "uid", "oCExternalIdentity", "userClass", "ocMemberOfSchool"},
+	Controls:   []ldap.Control(nil),
+}
+
+func TestCreateEducationUser(t *testing.T) {
+	lm := &mocks.Client{}
+	b, err := getMockedBackend(lm, eduConfig, &logger)
+	assert.Nil(t, err)
+	//assert.NotEqual(t, "", b.educationConfig.schoolObjectClass)
+	lm.On("Add", mock.Anything).Return(nil)
+
+	lm.On("Search", mock.Anything).
+		Return(
+			&ldap.SearchResult{
+				Entries: []*ldap.Entry{
+					eduUserEntry,
+				},
+			},
+			nil)
+	user := libregraph.NewEducationUser()
+	user.SetDisplayName("Test User")
+	user.SetOnPremisesSamAccountName("testuser")
+	user.SetMail("testuser@example.org")
+	user.SetPrimaryRole("student")
+	eduUser, err := b.CreateEducationUser(context.Background(), *user)
+	lm.AssertNumberOfCalls(t, "Add", 1)
+	lm.AssertNumberOfCalls(t, "Search", 1)
+	assert.NotNil(t, eduUser)
+	assert.Nil(t, err)
+	assert.Equal(t, eduUser.GetDisplayName(), user.GetDisplayName())
+	assert.Equal(t, eduUser.GetOnPremisesSamAccountName(), user.GetOnPremisesSamAccountName())
+	assert.Equal(t, "abcd-defg", eduUser.GetId())
+	assert.Equal(t, eduUser.GetPrimaryRole(), user.GetPrimaryRole())
+}
+
+func TestDeleteEducationUser(t *testing.T) {
+	lm := &mocks.Client{}
+
+	lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
+	lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
+	dr1 := &ldap.DelRequest{
+		DN: "uid=user,ou=people,dc=test",
+	}
+	lm.On("Del", dr1).Return(nil)
+	b, err := getMockedBackend(lm, eduConfig, &logger)
+	assert.Nil(t, err)
+	err = b.DeleteEducationUser(context.Background(), "abcd-defg")
+	lm.AssertNumberOfCalls(t, "Search", 1)
+	lm.AssertNumberOfCalls(t, "Del", 1)
+	assert.Nil(t, err)
+
+	err = b.DeleteEducationUser(context.Background(), "xxxx-xxxx")
+	lm.AssertNumberOfCalls(t, "Search", 2)
+	lm.AssertNumberOfCalls(t, "Del", 1)
+	assert.NotNil(t, err)
+	assert.Equal(t, "itemNotFound", err.Error())
+}
+
+func TestGetEducationUser(t *testing.T) {
+	lm := &mocks.Client{}
+	lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
+	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)
+	lm.AssertNumberOfCalls(t, "Search", 1)
+	assert.Nil(t, err)
+	assert.Equal(t, "Test User", user.GetDisplayName())
+	assert.Equal(t, "abcd-defg", user.GetId())
+
+	user, err = b.GetEducationUser(context.Background(), "xxxx-xxxx", nil)
+	lm.AssertNumberOfCalls(t, "Search", 2)
+	assert.NotNil(t, err)
+	assert.Equal(t, "itemNotFound", err.Error())
+}
+
+func TestGetEducationUsers(t *testing.T) {
+	lm := &mocks.Client{}
+	sr := &ldap.SearchRequest{
+		BaseDN:     "ou=people,dc=test",
+		Scope:      2,
+		SizeLimit:  0,
+		Filter:     "(objectClass=ocEducationUser)",
+		Attributes: []string{"displayname", "entryUUID", "mail", "uid", "oCExternalIdentity", "userClass", "ocMemberOfSchool"},
+		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.GetEducationUsers(context.Background(), nil)
+	lm.AssertNumberOfCalls(t, "Search", 1)
+	assert.Nil(t, err)
+}
diff --git a/services/graph/pkg/identity/ldap_school_test.go b/services/graph/pkg/identity/ldap_school_test.go
index c6a4e651714..0f1b980b248 100644
--- a/services/graph/pkg/identity/ldap_school_test.go
+++ b/services/graph/pkg/identity/ldap_school_test.go
@@ -167,3 +167,84 @@ func TestGetEducationSchools(t *testing.T) {
 	lm.AssertNumberOfCalls(t, "Search", 1)
 	assert.Nil(t, err)
 }
+
+var schoolByIDSearch1 *ldap.SearchRequest = &ldap.SearchRequest{
+	BaseDN:     "",
+	Scope:      2,
+	SizeLimit:  1,
+	Filter:     "(&(objectClass=ocEducationSchool)(owncloudUUID=abcd-defg))",
+	Attributes: []string{"ou", "owncloudUUID", "ocEducationSchoolNumber"},
+	Controls:   []ldap.Control(nil),
+}
+var userByIDSearch1 *ldap.SearchRequest = &ldap.SearchRequest{
+	BaseDN:     "ou=people,dc=test",
+	Scope:      2,
+	SizeLimit:  1,
+	Filter:     "(&(objectClass=ocEducationUser)(|(uid=abcd-defg)(entryUUID=abcd-defg)))",
+	Attributes: []string{"displayname", "entryUUID", "mail", "uid", "oCExternalIdentity", "userClass", "ocMemberOfSchool"},
+	Controls:   []ldap.Control(nil),
+}
+var userByIDSearch2 *ldap.SearchRequest = &ldap.SearchRequest{
+	BaseDN:     "ou=people,dc=test",
+	Scope:      2,
+	SizeLimit:  1,
+	Filter:     "(&(objectClass=ocEducationUser)(|(uid=does-not-exist)(entryUUID=does-not-exist)))",
+	Attributes: []string{"displayname", "entryUUID", "mail", "uid", "oCExternalIdentity", "userClass", "ocMemberOfSchool"},
+	Controls:   []ldap.Control(nil),
+}
+
+func TestAddUsersToEducationSchool(t *testing.T) {
+	lm := &mocks.Client{}
+	lm.On("Search", schoolByIDSearch1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry, schoolEntry1}}, nil)
+	lm.On("Search", userByIDSearch1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
+	lm.On("Search", userByIDSearch2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
+	lm.On("Modify", mock.Anything).Return(nil)
+	b, err := getMockedBackend(lm, eduConfig, &logger)
+	assert.Nil(t, err)
+	err = b.AddUsersToEducationSchool(context.Background(), "abcd-defg", []string{"does-not-exist"})
+	lm.AssertNumberOfCalls(t, "Search", 2)
+	assert.NotNil(t, err)
+	err = b.AddUsersToEducationSchool(context.Background(), "abcd-defg", []string{"abcd-defg", "does-not-exist"})
+	lm.AssertNumberOfCalls(t, "Search", 5)
+	assert.NotNil(t, err)
+	err = b.AddUsersToEducationSchool(context.Background(), "abcd-defg", []string{"abcd-defg"})
+	lm.AssertNumberOfCalls(t, "Search", 7)
+	assert.Nil(t, err)
+}
+
+func TestRemoveMemberFromEducationSchool(t *testing.T) {
+	lm := &mocks.Client{}
+	lm.On("Search", schoolByIDSearch1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry, schoolEntry1}}, nil)
+	lm.On("Search", userByIDSearch1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntryWithSchool}}, nil)
+	lm.On("Search", userByIDSearch2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
+	lm.On("Modify", mock.Anything).Return(nil)
+	b, err := getMockedBackend(lm, eduConfig, &logger)
+	assert.Nil(t, err)
+	err = b.RemoveUserFromEducationSchool(context.Background(), "abcd-defg", "does-not-exist")
+	lm.AssertNumberOfCalls(t, "Search", 2)
+	assert.NotNil(t, err)
+	assert.Equal(t, "itemNotFound", err.Error())
+	err = b.RemoveUserFromEducationSchool(context.Background(), "abcd-defg", "abcd-defg")
+	lm.AssertNumberOfCalls(t, "Search", 4)
+	lm.AssertNumberOfCalls(t, "Modify", 1)
+	assert.Nil(t, err)
+}
+
+var usersBySchoolIDSearch *ldap.SearchRequest = &ldap.SearchRequest{
+	BaseDN:     "ou=people,dc=test",
+	Scope:      2,
+	SizeLimit:  0,
+	Filter:     "(&(objectClass=ocEducationUser)(ocMemberOfSchool=abcd-defg))",
+	Attributes: []string{"displayname", "entryUUID", "mail", "uid", "oCExternalIdentity", "userClass", "ocMemberOfSchool"},
+	Controls:   []ldap.Control(nil),
+}
+
+func TestGetEducationSchoolUsers(t *testing.T) {
+	lm := &mocks.Client{}
+	lm.On("Search", schoolByIDSearch1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry, schoolEntry1}}, nil)
+	lm.On("Search", usersBySchoolIDSearch).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntryWithSchool}}, nil)
+	b, err := getMockedBackend(lm, eduConfig, &logger)
+	users, err := b.GetEducationSchoolUsers(context.Background(), "abcd-defg")
+	assert.Nil(t, err)
+	assert.Equal(t, 1, len(users))
+}
diff --git a/services/graph/pkg/service/v0/educationschools_test.go b/services/graph/pkg/service/v0/educationschools_test.go
index b47bfa69980..52c2404778a 100644
--- a/services/graph/pkg/service/v0/educationschools_test.go
+++ b/services/graph/pkg/service/v0/educationschools_test.go
@@ -347,17 +347,17 @@ var _ = Describe("Schools", func() {
 		})
 	})
 
-	Describe("GetEducationSchoolMembers", func() {
+	Describe("GetEducationSchoolUsers", func() {
 		It("gets the list of members", func() {
 			user := libregraph.NewEducationUser()
 			user.SetId("user")
-			identityEducationBackend.On("GetEducationSchoolMembers", mock.Anything, mock.Anything, mock.Anything).Return([]*libregraph.EducationUser{user}, nil)
+			identityEducationBackend.On("GetEducationSchoolUsers", mock.Anything, mock.Anything, mock.Anything).Return([]*libregraph.EducationUser{user}, nil)
 
-			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools/{schoolID}/members", nil)
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools/{schoolID}/users", nil)
 			rctx := chi.NewRouteContext()
 			rctx.URLParams.Add("schoolID", *newSchool.Id)
 			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
-			svc.GetEducationSchoolMembers(rr, r)
+			svc.GetEducationSchoolUsers(rr, r)
 			Expect(rr.Code).To(Equal(http.StatusOK))
 
 			data, err := io.ReadAll(rr.Body)
@@ -372,13 +372,13 @@ var _ = Describe("Schools", func() {
 		})
 	})
 
-	Describe("PostEducationSchoolMembers", func() {
+	Describe("PostEducationSchoolUsers", func() {
 		It("fails on invalid body", func() {
 			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/{schoolID}/members", bytes.NewBufferString("{invalid"))
 			rctx := chi.NewRouteContext()
 			rctx.URLParams.Add("schoolID", *newSchool.Id)
 			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
-			svc.PostEducationSchoolMember(rr, r)
+			svc.PostEducationSchoolUser(rr, r)
 			Expect(rr.Code).To(Equal(http.StatusBadRequest))
 		})
 
@@ -391,7 +391,7 @@ var _ = Describe("Schools", func() {
 			rctx := chi.NewRouteContext()
 			rctx.URLParams.Add("schoolID", *newSchool.Id)
 			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
-			svc.PostEducationSchoolMember(rr, r)
+			svc.PostEducationSchoolUser(rr, r)
 			Expect(rr.Code).To(Equal(http.StatusBadRequest))
 		})
 
@@ -405,7 +405,7 @@ var _ = Describe("Schools", func() {
 			rctx := chi.NewRouteContext()
 			rctx.URLParams.Add("schoolID", *newSchool.Id)
 			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
-			svc.PostEducationSchoolMember(rr, r)
+			svc.PostEducationSchoolUser(rr, r)
 			Expect(rr.Code).To(Equal(http.StatusBadRequest))
 		})
 
@@ -414,49 +414,49 @@ var _ = Describe("Schools", func() {
 			member.SetOdataId("/users/user")
 			data, err := json.Marshal(member)
 			Expect(err).ToNot(HaveOccurred())
-			identityEducationBackend.On("AddMembersToEducationSchool", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+			identityEducationBackend.On("AddUsersToEducationSchool", mock.Anything, mock.Anything, mock.Anything).Return(nil)
 
 			r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/{schoolID}/members", bytes.NewBuffer(data))
 			rctx := chi.NewRouteContext()
 			rctx.URLParams.Add("schoolID", *newSchool.Id)
 			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
-			svc.PostEducationSchoolMember(rr, r)
+			svc.PostEducationSchoolUser(rr, r)
 			Expect(rr.Code).To(Equal(http.StatusNoContent))
 
-			identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "AddMembersToEducationSchool", 1)
+			identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "AddUsersToEducationSchool", 1)
 		})
 	})
 
-	Describe("DeleteEducationSchoolMembers", func() {
+	Describe("DeleteEducationSchoolUsers", func() {
 		It("handles missing or empty member id", func() {
-			r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools/{schoolID}/members/{memberID}/$ref", nil)
+			r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools/{schoolID}/members/{userID}/$ref", nil)
 			rctx := chi.NewRouteContext()
 			rctx.URLParams.Add("schoolID", *newSchool.Id)
 			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
-			svc.DeleteEducationSchoolMember(rr, r)
+			svc.DeleteEducationSchoolUser(rr, r)
 			Expect(rr.Code).To(Equal(http.StatusBadRequest))
 		})
 		It("handles missing or empty member id", func() {
-			r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools/{schoolID}/members/{memberID}/$ref", nil)
+			r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools/{schoolID}/members/{userID}/$ref", nil)
 			rctx := chi.NewRouteContext()
-			rctx.URLParams.Add("memberID", "/users/user")
+			rctx.URLParams.Add("userID", "/users/user")
 			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
-			svc.DeleteEducationSchoolMember(rr, r)
+			svc.DeleteEducationSchoolUser(rr, r)
 			Expect(rr.Code).To(Equal(http.StatusBadRequest))
 		})
 
 		It("deletes members", func() {
-			identityEducationBackend.On("RemoveMemberFromEducationSchool", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+			identityEducationBackend.On("RemoveUserFromEducationSchool", mock.Anything, mock.Anything, mock.Anything).Return(nil)
 
-			r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools/{schoolID}/members/{memberID}/$ref", nil)
+			r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools/{schoolID}/members/{userID}/$ref", nil)
 			rctx := chi.NewRouteContext()
 			rctx.URLParams.Add("schoolID", *newSchool.Id)
-			rctx.URLParams.Add("memberID", "/users/user1")
+			rctx.URLParams.Add("userID", "/users/user1")
 			r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
-			svc.DeleteEducationSchoolMember(rr, r)
+			svc.DeleteEducationSchoolUser(rr, r)
 			Expect(rr.Code).To(Equal(http.StatusNoContent))
 
-			identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "RemoveMemberFromEducationSchool", 1)
+			identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "RemoveUserFromEducationSchool", 1)
 		})
 	})
 })