Repository: owncloud/ocis (go)

Issue:
Panic on missing group schema ID
## Describe the bug

oCIS panics if the `LDAP_GROUP_SCHEMA_ID` is set to an unknown attribute.

```
2022/07/25 08:54:31 http: panic serving 127.0.0.1:58538: runtime error: invalid memory address or nil pointer dereference
goroutine 2179 [running]:
net/http.(*conn).serve.func1()
	net/http/server.go:1825 +0xbf
panic({0x39ba680, 0x528cfc0})
	runtime/panic.go:844 +0x258
github.com/owncloud/ocis/v2/services/graph/pkg/identity.(*LDAP).GetGroups(0xc0008aaa20, {0x3dd8688, 0xc0089dea80}, 0xc0089dfa70)
	github.com/owncloud/ocis/v2/services/graph/pkg/identity/ldap.go:643 +0x84d
github.com/owncloud/ocis/v2/services/graph/pkg/service/v0.Graph.GetGroups({0xc000d68fc0, 0xc055252b40, 0xc0004495f0, {0x3de9348, 0xc0008aaa20}, {0x7fe8073207a0, 0xc0004679b8}, {0x3dd96f0, 0xc001584be0}, 0xc055217200, ...}, ...)
	github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/groups.go:34 +0x1a2
net/http.HandlerFunc.ServeHTTP(0xc00101f960?, {0x7fe8046e5970?, 0xc0089c7040?}, 0xc0089dfa10?)
	net/http/server.go:2084 +0x2f
github.com/owncloud/ocis/v2/services/graph/pkg/middleware.RequireAdmin.func1.1({0x7fe8046e5970, 0xc0089c7040}, 0xc008864e00)
	github.com/owncloud/ocis/v2/services/graph/pkg/middleware/requireadmin.go:46 +0x212
net/http.HandlerFunc.ServeHTTP(0xc000dbbf80?, {0x7fe8046e5970?, 0xc0089c7040?}, 0xc007ae03e0?)
	net/http/server.go:2084 +0x2f
github.com/go-chi/chi/v5.(*ChainHandler).ServeHTTP(0x3989dc0?, {0x7fe8046e5970?, 0xc0089c7040?}, 0x36396c0?)
	github.com/go-chi/chi/v5@v5.0.7/chain.go:31 +0x2c
github.com/go-chi/chi/v5.(*Mux).routeHTTP(0xc055252fc0, {0x7fe8046e5970, 0xc0089c7040}, 0xc008864e00)
	github.com/go-chi/chi/v5@v5.0.7/mux.go:442 +0x216
net/http.HandlerFunc.ServeHTTP(0x0?, {0x7fe8046e5970?, 0xc0089c7040?}, 0x528bb90?)
	net/http/server.go:2084 +0x2f
github.com/go-chi/chi/v5.(*Mux).ServeHTTP(0xc055252fc0, {0x7fe8046e5970, 0xc0089c7040}, 0xc008864e00)
	github.com/go-chi/chi/v5@v5.0.7/mux.go:71 +0x48d
github.com/go-chi/chi/v5.(*Mux).Mount.func1({0x7fe8046e5970, 0xc0089c7040}, 0xc008864e00)
	github.com/go-chi/chi/v5@v5.0.7/mux.go:314 +0x19c
net/http.HandlerFunc.ServeHTTP(0x3989dc0?, {0x7fe8046e5970?, 0xc0089c7040?}, 0xc008877bb0?)
	net/http/server.go:2084 +0x2f
github.com/go-chi/chi/v5.(*Mux).routeHTTP(0xc055252cc0, {0x7fe8046e5970, 0xc0089c7040}, 0xc008864e00)
	github.com/go-chi/chi/v5@v5.0.7/mux.go:442 +0x216
net/http.HandlerFunc.ServeHTTP(0x0?, {0x7fe8046e5970?, 0xc0089c7040?}, 0x457210?)
	net/http/server.go:2084 +0x2f
github.com/go-chi/chi/v5.(*Mux).ServeHTTP(0xc055252cc0, {0x7fe8046e5970, 0xc0089c7040}, 0xc008864e00)
	github.com/go-chi/chi/v5@v5.0.7/mux.go:71 +0x48d
github.com/go-chi/chi/v5.(*Mux).Mount.func1({0x7fe8046e5970, 0xc0089c7040}, 0xc008864e00)
	github.com/go-chi/chi/v5@v5.0.7/mux.go:314 +0x19c
net/http.HandlerFunc.ServeHTTP(0x3989dc0?, {0x7fe8046e5970?, 0xc0089c7040?}, 0xc008877ba4?)
	net/http/server.go:2084 +0x2f
github.com/go-chi/chi/v5.(*Mux).routeHTTP(0xc055252c60, {0x7fe8046e5970, 0xc0089c7040}, 0xc008864e00)
	github.com/go-chi/chi/v5@v5.0.7/mux.go:442 +0x216
net/http.HandlerFunc.ServeHTTP(0xc0055433a8?, {0x7fe8046e5970?, 0xc0089c7040?}, 0x393fe60?)
	net/http/server.go:2084 +0x2f
github.com/go-chi/chi/v5/middleware.StripSlashes.func1({0x7fe8046e5970, 0xc0089c7040}, 0xc008864e00)
	github.com/go-chi/chi/v5@v5.0.7/middleware/strip.go:30 +0x139
net/http.HandlerFunc.ServeHTTP(0x0?, {0x7fe8046e5970?, 0xc0089c7040?}, 0x457210?)
	net/http/server.go:2084 +0x2f
github.com/go-chi/chi/v5.(*Mux).ServeHTTP(0xc055252c60, {0x7fe8046e5970, 0xc0089c7040}, 0xc008864e00)
	github.com/go-chi/chi/v5@v5.0.7/mux.go:71 +0x48d
github.com/go-chi/chi/v5.(*Mux).Mount.func1({0x7fe8046e5970, 0xc0089c7040}, 0xc008864e00)
	github.com/go-chi/chi/v5@v5.0.7/mux.go:314 +0x19c
net/http.HandlerFunc.ServeHTTP(0x3989dc0?, {0x7fe8046e5970?, 0xc0089c7040?}, 0xc00898cc04?)
	net/http/server.go:2084 +0x2f
github.com/go-chi/chi/v5.(*Mux).routeHTTP(0xc055252b40, {0x7fe8046e5970, 0xc0089c7040}, 0xc008864e00)
	github.com/go-chi/chi/v5@v5.0.7/mux.go:442 +0x216
net/http.HandlerFunc.ServeHTTP(0x3dd8688?, {0x7fe8046e5970?, 0xc0089c7040?}, 0x2?)
	net/http/server.go:2084 +0x2f
github.com/owncloud/ocis/v2/services/graph/pkg/middleware.Auth.func1.1({0x7fe8046e5970, 0xc0089c7040}, 0xc008864d00)
	github.com/owncloud/ocis/v2/services/graph/pkg/middleware/auth.go:84 +0x562
net/http.HandlerFunc.ServeHTTP(0x3dd61b0?, {0x7fe8046e5970?, 0xc0089c7040?}, 0xc000078c00?)
	net/http/server.go:2084 +0x2f
github.com/owncloud/ocis/v2/ocis-pkg/middleware.Logger.func1.1({0x3dd61b0, 0xc007d03c00}, 0xc008864d00)
	github.com/owncloud/ocis/v2/ocis-pkg/middleware/logger.go:17 +0xc8
net/http.HandlerFunc.ServeHTTP(0x3ba9ae0?, {0x3dd61b0?, 0xc007d03c00?}, 0xf?)
	net/http/server.go:2084 +0x2f
github.com/owncloud/ocis/v2/ocis-pkg/middleware.Version.func1.1({0x3dd61b0, 0xc007d03c00}, 0x3639f01?)
	github.com/owncloud/ocis/v2/ocis-pkg/middleware/version.go:18 +0x18f
net/http.HandlerFunc.ServeHTTP(0x3dd8688?, {0x3dd61b0?, 0xc007d03c00?}, 0x3639fa0?)
	net/http/server.go:2084 +0x2f
github.com/go-chi/chi/v5/middleware.RequestID.func1({0x3dd61b0, 0xc007d03c00}, 0xc008864c00)
	github.com/go-chi/chi/v5@v5.0.7/middleware/request_id.go:76 +0x354
net/http.HandlerFunc.ServeHTTP(0x3dd85e0?, {0x3dd61b0?, 0xc007d03c00?}, 0x528bb90?)
	net/http/server.go:2084 +0x2f
github.com/go-chi/chi/v5.(*Mux).ServeHTTP(0xc055252b40, {0x3dd61b0, 0xc007d03c00}, 0xc0087bd500)
	github.com/go-chi/chi/v5@v5.0.7/mux.go:88 +0x442
github.com/owncloud/ocis/v2/services/graph/pkg/service/v0.Graph.ServeHTTP(...)
	github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/graph.go:85
github.com/owncloud/ocis/v2/services/graph/pkg/service/v0.instrument.ServeHTTP(...)
	github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/instrument.go:24
github.com/owncloud/ocis/v2/services/graph/pkg/service/v0.logging.ServeHTTP(...)
	github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/logging.go:24
github.com/owncloud/ocis/v2/services/graph/pkg/service/v0.tracing.ServeHTTP(...)
	github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/tracing.go:20
net/http.serverHandler.ServeHTTP({0xc0084b1830?}, {0x3dd61b0, 0xc007d03c00}, 0xc0087bd500)
	net/http/server.go:2916 +0x43b
net/http.(*conn).serve(0xc008365720, {0x3dd8688, 0xc0014f6780})
	net/http/server.go:1966 +0x5d7
created by net/http.(*Server).Serve
	net/http/server.go:3071 +0x4db
```

## Expected behavior

Raise an error, but don't panic on unknown attributes.

## Setup

Container (owncloud/ocis:2.0.0-beta.5) + External OpenLDAP


Pull request description:
Fix handling of invalid LDAP users and groups
## Description

We fixed an issue where ocis would exit with a panic when LDAP users or groups where missing required attributes (e.g. the id)
User and Groups missing any of the required attributes are now ignored.

## Related Issue
- Fixes #4274 

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:
<!-- Tick the checkboxes when done. -->
<!-- Raise documentation ticket in owncloud.github.io/ -->
- [x] Code changes
- [x] Unit tests added


Patch:
diff --git a/.drone.star b/.drone.star
index 47e78ca728f..12d3fd1ef83 100644
--- a/.drone.star
+++ b/.drone.star
@@ -1808,8 +1808,6 @@ def ocisServer(storage, accounts_hash_difficulty = 4, volumes = [], depends_on =
         environment = {
             "OCIS_URL": OCIS_URL,
             "OCIS_CONFIG_DIR": "/root/.ocis/config",
-            "LDAP_GROUP_SUBSTRING_FILTER_TYPE": "any",
-            "LDAP_USER_SUBSTRING_FILTER_TYPE": "any",
             "GATEWAY_GRPC_ADDR": "0.0.0.0:9142",  # cs3api-validator needs the cs3api gatway exposed
             "STORAGE_USERS_DRIVER": "%s" % (storage),
             "STORAGE_USERS_DRIVER_LOCAL_ROOT": "/srv/app/tmp/ocis/local/root",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a221b0995a..bef3911d079 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -101,6 +101,7 @@ The following sections list the changes for unreleased.
    https://github.com/cs3org/reva/pull/3096 https://github.com/cs3org/reva/pull/4315
 
    https://github.com/owncloud/ocis/pull/4294
+   https://github.com/owncloud/ocis/pull/4330
 
 * Enhancement - Update ownCloud Web to v5.7.0-rc.7: [#4314](https://github.com/owncloud/ocis/pull/4314)
 
diff --git a/changelog/unreleased/fix-ldap-invalid-users-groups.md b/changelog/unreleased/fix-ldap-invalid-users-groups.md
new file mode 100644
index 00000000000..9f0db8640a8
--- /dev/null
+++ b/changelog/unreleased/fix-ldap-invalid-users-groups.md
@@ -0,0 +1,6 @@
+Bugfix: Fix handling of invalid LDAP users and groups
+
+We fixed an issue where ocis would exit with a panic when LDAP users
+or groups where missing required attributes (e.g. the id)
+
+https://github.com/owncloud/ocis/issues/4274
diff --git a/changelog/unreleased/update-reva-beta.6.md b/changelog/unreleased/update-reva-beta.6.md
index 3077ce7b523..f11ff943db7 100644
--- a/changelog/unreleased/update-reva-beta.6.md
+++ b/changelog/unreleased/update-reva-beta.6.md
@@ -9,3 +9,4 @@ https://github.com/cs3org/reva/pull/3096
 https://github.com/cs3org/reva/pull/4315
 
 https://github.com/owncloud/ocis/pull/4294
+https://github.com/owncloud/ocis/pull/4330
diff --git a/go.mod b/go.mod
index 83b0c7b7162..6c7b7fc524e 100644
--- a/go.mod
+++ b/go.mod
@@ -5,13 +5,13 @@ go 1.18
 require (
 	github.com/CiscoM31/godata v1.0.5
 	github.com/Masterminds/semver v1.5.0
-	github.com/MicahParks/keyfunc v1.1.0
+	github.com/MicahParks/keyfunc v1.2.0
 	github.com/ReneKroon/ttlcache/v2 v2.11.0
 	github.com/blevesearch/bleve/v2 v2.3.3
 	github.com/blevesearch/bleve_index_api v1.0.2
 	github.com/coreos/go-oidc/v3 v3.2.0
 	github.com/cs3org/go-cs3apis v0.0.0-20220719130120-361e9f987d64
-	github.com/cs3org/reva/v2 v2.7.3-0.20220803070837-28724e42b3f9
+	github.com/cs3org/reva/v2 v2.7.3-0.20220804071019-29e92c20c731
 	github.com/disintegration/imaging v1.6.2
 	github.com/ggwhite/go-masker v1.0.9
 	github.com/go-chi/chi/v5 v5.0.7
@@ -65,10 +65,10 @@ require (
 	go-micro.dev/v4 v4.8.0
 	go.opencensus.io v0.23.0
 	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.33.0
-	go.opentelemetry.io/otel v1.8.0
-	go.opentelemetry.io/otel/exporters/jaeger v1.8.0
-	go.opentelemetry.io/otel/sdk v1.8.0
-	go.opentelemetry.io/otel/trace v1.8.0
+	go.opentelemetry.io/otel v1.9.0
+	go.opentelemetry.io/otel/exporters/jaeger v1.9.0
+	go.opentelemetry.io/otel/sdk v1.9.0
+	go.opentelemetry.io/otel/trace v1.9.0
 	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
 	golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf
 	golang.org/x/image v0.0.0-20220321031419-a8550c1d254a
diff --git a/go.sum b/go.sum
index fd04a1ad9ab..058ec9fbbb3 100644
--- a/go.sum
+++ b/go.sum
@@ -100,8 +100,8 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q
 github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
 github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
 github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
-github.com/MicahParks/keyfunc v1.1.0 h1:9NcnRwS0ciuVeVNi+vTdYVMTmk62OID7VlG6y9BgLK0=
-github.com/MicahParks/keyfunc v1.1.0/go.mod h1:a4yfunv77gZ0RgTNw7tOYS+bjtHk5565e+1dPz+YJI8=
+github.com/MicahParks/keyfunc v1.2.0 h1:qiUfvtvvcoS0JXcHtoqglT29WyvuL0l3tkBlzp0QmsU=
+github.com/MicahParks/keyfunc v1.2.0/go.mod h1:a4yfunv77gZ0RgTNw7tOYS+bjtHk5565e+1dPz+YJI8=
 github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
 github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
 github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
@@ -288,8 +288,8 @@ github.com/crewjam/saml v0.4.6 h1:XCUFPkQSJLvzyl4cW9OvpWUbRf0gE7VUpU8ZnilbeM4=
 github.com/crewjam/saml v0.4.6/go.mod h1:ZBOXnNPFzB3CgOkRm7Nd6IVdkG+l/wF+0ZXLqD96t1A=
 github.com/cs3org/go-cs3apis v0.0.0-20220719130120-361e9f987d64 h1:cFnankJOCWndnOns4sKRG7yzH61ammK2Am6rEGWCK40=
 github.com/cs3org/go-cs3apis v0.0.0-20220719130120-361e9f987d64/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
-github.com/cs3org/reva/v2 v2.7.3-0.20220803070837-28724e42b3f9 h1:iA067C/zdGMzpRXY0eGu21vFIPVYb0ONHUJbejSZ1B0=
-github.com/cs3org/reva/v2 v2.7.3-0.20220803070837-28724e42b3f9/go.mod h1:AKcCJX2IWLxKGA60eaM7A43nB6urQYuE9phbJabKkm0=
+github.com/cs3org/reva/v2 v2.7.3-0.20220804071019-29e92c20c731 h1:An4S1S9twbBD6XD67SADEY/5hBI7C9eYyY7Wcb0enko=
+github.com/cs3org/reva/v2 v2.7.3-0.20220804071019-29e92c20c731/go.mod h1:AKcCJX2IWLxKGA60eaM7A43nB6urQYuE9phbJabKkm0=
 github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8 h1:Z9lwXumT5ACSmJ7WGnFl+OMLLjpz5uR2fyz7dC255FI=
 github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8/go.mod h1:4abs/jPXcmJzYoYGF91JF9Uq9s/KL5n1jvFDix8KcqY=
 github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
@@ -1247,14 +1247,14 @@ go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.33.0 h1:z6rnla1Asjzn0FrhohzIbDi4bxbtc6EMmQ7f5ZPn+pA=
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.33.0/go.mod h1:y/SlJpJQPd2UzfBCj0E9Flk9FDCtTyqUmaCB41qFrWI=
-go.opentelemetry.io/otel v1.8.0 h1:zcvBFizPbpa1q7FehvFiHbQwGzmPILebO0tyqIR5Djg=
-go.opentelemetry.io/otel v1.8.0/go.mod h1:2pkj+iMj0o03Y+cW6/m8Y4WkRdYN3AvCXCnzRMp9yvM=
-go.opentelemetry.io/otel/exporters/jaeger v1.8.0 h1:TLLqD6kDhLPziEC7pgPrMvP9lAqdk3n1gf8DiFSnfW8=
-go.opentelemetry.io/otel/exporters/jaeger v1.8.0/go.mod h1:GbWg+ng88rDtx+id26C34QLqw2erqJeAjsCx9AFeHfE=
-go.opentelemetry.io/otel/sdk v1.8.0 h1:xwu69/fNuwbSHWe/0PGS888RmjWY181OmcXDQKu7ZQk=
-go.opentelemetry.io/otel/sdk v1.8.0/go.mod h1:uPSfc+yfDH2StDM/Rm35WE8gXSNdvCg023J6HeGNO0c=
-go.opentelemetry.io/otel/trace v1.8.0 h1:cSy0DF9eGI5WIfNwZ1q2iUyGj00tGzP24dE1lOlHrfY=
-go.opentelemetry.io/otel/trace v1.8.0/go.mod h1:0Bt3PXY8w+3pheS3hQUt+wow8b1ojPaTBoTCh2zIFI4=
+go.opentelemetry.io/otel v1.9.0 h1:8WZNQFIB2a71LnANS9JeyidJKKGOOremcUtb/OtHISw=
+go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
+go.opentelemetry.io/otel/exporters/jaeger v1.9.0 h1:gAEgEVGDWwFjcis9jJTOJqZNxDzoZfR12WNIxr7g9Ww=
+go.opentelemetry.io/otel/exporters/jaeger v1.9.0/go.mod h1:hquezOLVAybNW6vanIxkdLXTXvzlj2Vn3wevSP15RYs=
+go.opentelemetry.io/otel/sdk v1.9.0 h1:LNXp1vrr83fNXTHgU8eO89mhzxb/bbWAsHG6fNf3qWo=
+go.opentelemetry.io/otel/sdk v1.9.0/go.mod h1:AEZc8nt5bd2F7BC24J5R0mrjYnpEgYHyTcM/vrSple4=
+go.opentelemetry.io/otel/trace v1.9.0 h1:oZaCNJUjWcg60VXWee8lJKlqhPbXAPB51URuR47pQYc=
+go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
diff --git a/services/graph/pkg/identity/ldap.go b/services/graph/pkg/identity/ldap.go
index 398fabaca63..24f8678c647 100644
--- a/services/graph/pkg/identity/ldap.go
+++ b/services/graph/pkg/identity/ldap.go
@@ -315,7 +315,13 @@ func (i *LDAP) getEntryByDN(dn string, attrs []string) (*ldap.Entry, error) {
 		nil,
 	)
 
-	i.logger.Debug().Str("backend", "ldap").Str("dn", dn).Msg("Search user by DN")
+	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("getEntryByDN")
 	res, err := i.conn.Search(searchRequest)
 
 	if err != nil {
@@ -353,7 +359,13 @@ func (i *LDAP) getLDAPUserByFilter(filter string) (*ldap.Entry, error) {
 		},
 		nil,
 	)
-	i.logger.Debug().Str("backend", "ldap").Msgf("Search %s", i.userBaseDN)
+	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")
 	res, err := i.conn.Search(searchRequest)
 
 	if err != nil {
@@ -380,19 +392,19 @@ func (i *LDAP) GetUser(ctx context.Context, nameOrID string, queryParam url.Valu
 	if err != nil {
 		return nil, err
 	}
+	u := i.createUserModelFromLDAP(e)
+	if u == nil {
+		return nil, errNotFound
+	}
 	sel := strings.Split(queryParam.Get("$select"), ",")
 	exp := strings.Split(queryParam.Get("$expand"), ",")
-	u := i.createUserModelFromLDAP(e)
 	if slices.Contains(sel, "memberOf") || slices.Contains(exp, "memberOf") {
 		userGroups, err := i.getGroupsForUser(e.DN)
 		if err != nil {
 			return nil, err
 		}
 		if len(userGroups) > 0 {
-			groups := make([]libregraph.Group, 0, len(userGroups))
-			for _, g := range userGroups {
-				groups = append(groups, *i.createGroupModelFromLDAP(g))
-			}
+			groups := i.groupsFromLDAPEntries(userGroups)
 			u.MemberOf = groups
 		}
 	}
@@ -428,7 +440,13 @@ func (i *LDAP) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregra
 		},
 		nil,
 	)
-	i.logger.Debug().Str("backend", "ldap").Msgf("Search %s", i.userBaseDN)
+	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("GetUsers")
 	res, err := i.conn.Search(searchRequest)
 	if err != nil {
 		return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
@@ -440,6 +458,10 @@ func (i *LDAP) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregra
 		sel := strings.Split(queryParam.Get("$select"), ",")
 		exp := strings.Split(queryParam.Get("$expand"), ",")
 		u := i.createUserModelFromLDAP(e)
+		// Skip invalid LDAP users
+		if u == nil {
+			continue
+		}
 		if slices.Contains(sel, "memberOf") || slices.Contains(exp, "memberOf") {
 			userGroups, err := i.getGroupsForUser(e.DN)
 			if err != nil {
@@ -450,10 +472,7 @@ func (i *LDAP) GetUsers(ctx context.Context, queryParam url.Values) ([]*libregra
 				if expand == "" {
 					expand = "false"
 				}
-				groups := make([]libregraph.Group, 0, len(userGroups))
-				for _, g := range userGroups {
-					groups = append(groups, *i.createGroupModelFromLDAP(g))
-				}
+				groups := i.groupsFromLDAPEntries(userGroups)
 				u.MemberOf = groups
 			}
 		}
@@ -482,16 +501,21 @@ func (i *LDAP) GetGroup(ctx context.Context, nameOrID string, queryParam url.Val
 	}
 	sel := strings.Split(queryParam.Get("$select"), ",")
 	exp := strings.Split(queryParam.Get("$expand"), ",")
-	g := i.createGroupModelFromLDAP(e)
+	var g *libregraph.Group
+	if g = i.createGroupModelFromLDAP(e); g == nil {
+		return nil, errorcode.New(errorcode.ItemNotFound, "not found")
+	}
 	if slices.Contains(sel, "members") || slices.Contains(exp, "members") {
-		members, err := i.GetGroupMembers(ctx, *g.Id)
+		members, err := i.expandLDAPGroupMembers(ctx, e)
 		if err != nil {
 			return nil, err
 		}
 		if len(members) > 0 {
 			m := make([]libregraph.User, 0, len(members))
-			for _, u := range members {
-				m = append(m, *u)
+			for _, ue := range members {
+				if u := i.createUserModelFromLDAP(ue); u != nil {
+					m = append(m, *u)
+				}
 			}
 			g.Members = m
 		}
@@ -546,7 +570,13 @@ func (i *LDAP) getLDAPGroupsByFilter(filter string, requestMembers, single bool)
 		attrs,
 		nil,
 	)
-	i.logger.Debug().Str("backend", "ldap").Msgf("Search %s", i.groupBaseDN)
+	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("getLDAPGroupsByFilter")
 	res, err := i.conn.Search(searchRequest)
 
 	if err != nil {
@@ -608,6 +638,14 @@ func (i *LDAP) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregr
 	if search == "" {
 		search = queryParam.Get("$search")
 	}
+
+	var expandMembers bool
+	sel := strings.Split(queryParam.Get("$select"), ",")
+	exp := strings.Split(queryParam.Get("$expand"), ",")
+	if slices.Contains(sel, "members") || slices.Contains(exp, "members") {
+		expandMembers = true
+	}
+
 	var groupFilter string
 	if search != "" {
 		search = ldap.EscapeFilter(search)
@@ -618,16 +656,28 @@ func (i *LDAP) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregr
 		)
 	}
 	groupFilter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.groupFilter, i.groupObjectClass, groupFilter)
+
+	groupAttrs := []string{
+		i.groupAttributeMap.name,
+		i.groupAttributeMap.id,
+	}
+	if expandMembers {
+		groupAttrs = append(groupAttrs, i.groupAttributeMap.member)
+	}
+
 	searchRequest := ldap.NewSearchRequest(
 		i.groupBaseDN, i.groupScope, ldap.NeverDerefAliases, 0, 0, false,
 		groupFilter,
-		[]string{
-			i.groupAttributeMap.name,
-			i.groupAttributeMap.id,
-		},
+		groupAttrs,
 		nil,
 	)
-	i.logger.Debug().Str("backend", "ldap").Str("Base", i.groupBaseDN).Str("filter", groupFilter).Msg("ldap search")
+	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("GetGroups")
 	res, err := i.conn.Search(searchRequest)
 	if err != nil {
 		return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
@@ -635,19 +685,22 @@ func (i *LDAP) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregr
 
 	groups := make([]*libregraph.Group, 0, len(res.Entries))
 
+	var g *libregraph.Group
 	for _, e := range res.Entries {
-		sel := strings.Split(queryParam.Get("$select"), ",")
-		exp := strings.Split(queryParam.Get("$expand"), ",")
-		g := i.createGroupModelFromLDAP(e)
-		if slices.Contains(sel, "members") || slices.Contains(exp, "members") {
-			members, err := i.GetGroupMembers(ctx, *g.Id)
+		if g = i.createGroupModelFromLDAP(e); g == nil {
+			continue
+		}
+		if expandMembers {
+			members, err := i.expandLDAPGroupMembers(ctx, e)
 			if err != nil {
 				return nil, err
 			}
 			if len(members) > 0 {
 				m := make([]libregraph.User, 0, len(members))
-				for _, u := range members {
-					m = append(m, *u)
+				for _, ue := range members {
+					if u := i.createUserModelFromLDAP(ue); u != nil {
+						m = append(m, *u)
+					}
 				}
 				g.Members = m
 			}
@@ -664,7 +717,22 @@ func (i *LDAP) GetGroupMembers(ctx context.Context, groupID string) ([]*libregra
 		return nil, err
 	}
 
-	result := []*libregraph.User{}
+	memberEntries, err := i.expandLDAPGroupMembers(ctx, e)
+	result := make([]*libregraph.User, 0, len(memberEntries))
+	if err != nil {
+		return nil, err
+	}
+	for _, member := range memberEntries {
+		if u := i.createUserModelFromLDAP(member); u != nil {
+			result = append(result, u)
+		}
+	}
+
+	return result, nil
+}
+
+func (i *LDAP) expandLDAPGroupMembers(ctx context.Context, e *ldap.Entry) ([]*ldap.Entry, error) {
+	result := []*ldap.Entry{}
 
 	for _, memberDN := range e.GetEqualFoldAttributeValues(i.groupAttributeMap.member) {
 		if memberDN == "" {
@@ -677,7 +745,7 @@ func (i *LDAP) GetGroupMembers(ctx context.Context, groupID string) ([]*libregra
 			i.logger.Warn().Err(err).Str("member", memberDN).Msg("error reading group member")
 			continue
 		}
-		result = append(result, i.createUserModelFromLDAP(ue))
+		result = append(result, ue)
 	}
 
 	return result, nil
@@ -853,19 +921,44 @@ func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *libregraph.User {
 	if e == nil {
 		return nil
 	}
-	return &libregraph.User{
-		DisplayName:              pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.displayName)),
-		Mail:                     pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.mail)),
-		OnPremisesSamAccountName: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.userName)),
-		Id:                       pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.id)),
+
+	opsan := e.GetEqualFoldAttributeValue(i.userAttributeMap.userName)
+	id := e.GetEqualFoldAttributeValue(i.userAttributeMap.id)
+
+	if id != "" && opsan != "" {
+		return &libregraph.User{
+			DisplayName:              pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.displayName)),
+			Mail:                     pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.mail)),
+			OnPremisesSamAccountName: &opsan,
+			Id:                       &id,
+		}
 	}
+	i.logger.Warn().Str("dn", e.DN).Msg("Invalid User. Missing username or id attribute")
+	return nil
 }
 
 func (i *LDAP) createGroupModelFromLDAP(e *ldap.Entry) *libregraph.Group {
-	return &libregraph.Group{
-		DisplayName: pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.name)),
-		Id:          pointerOrNil(e.GetEqualFoldAttributeValue(i.groupAttributeMap.id)),
+	name := e.GetEqualFoldAttributeValue(i.groupAttributeMap.name)
+	id := e.GetEqualFoldAttributeValue(i.groupAttributeMap.id)
+
+	if id != "" && name != "" {
+		return &libregraph.Group{
+			DisplayName: &name,
+			Id:          &id,
+		}
+	}
+	i.logger.Warn().Str("dn", e.DN).Msg("Group is missing name or id")
+	return nil
+}
+
+func (i *LDAP) groupsFromLDAPEntries(e []*ldap.Entry) []libregraph.Group {
+	groups := make([]libregraph.Group, 0, len(e))
+	for _, g := range e {
+		if grp := i.createGroupModelFromLDAP(g); grp != nil {
+			groups = append(groups, *grp)
+		}
 	}
+	return groups
 }
 
 func pointerOrNil(val string) *string {
diff --git a/services/groups/pkg/config/defaults/defaultconfig.go b/services/groups/pkg/config/defaults/defaultconfig.go
index ec5cedf544e..822efa66dd4 100644
--- a/services/groups/pkg/config/defaults/defaultconfig.go
+++ b/services/groups/pkg/config/defaults/defaultconfig.go
@@ -43,7 +43,7 @@ func DefaultConfig() *config.Config {
 				GroupBaseDN:              "ou=groups,o=libregraph-idm",
 				UserScope:                "sub",
 				GroupScope:               "sub",
-				GroupSubstringFilterType: "initial",
+				GroupSubstringFilterType: "any",
 				UserFilter:               "",
 				GroupFilter:              "",
 				UserObjectClass:          "inetOrgPerson",
diff --git a/services/proxy/pkg/config/config.go b/services/proxy/pkg/config/config.go
index 20f58810312..2b9118deabc 100644
--- a/services/proxy/pkg/config/config.go
+++ b/services/proxy/pkg/config/config.go
@@ -27,9 +27,9 @@ type Config struct {
 	PreSignedURL          PreSignedURL    `yaml:"pre_signed_url"`
 	AccountBackend        string          `yaml:"account_backend" env:"PROXY_ACCOUNT_BACKEND_TYPE" desc:"Account backend the PROXY service should use. Currently only 'cs3' is possible here."`
 	UserOIDCClaim         string          `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM" desc:"The name of an OpenID Connect claim that should be used for resolving users with the account backend. Currently defaults to 'email'."`
-	UserCS3Claim          string          `yaml:"user_cs3_claim" env:"PROXY_USER_CS3_CLAIM" desc:"The name of a CS3 user attribute (claim) that should be mapped to the 'user_oidc_claim'. Currently defaults to 'mail'. Supported values are 'username' and 'displayname'."`
+	UserCS3Claim          string          `yaml:"user_cs3_claim" env:"PROXY_USER_CS3_CLAIM" desc:"The name of a CS3 user attribute (claim) that should be mapped to the 'user_oidc_claim'. Supported values are 'username', 'mail' and 'userid'."`
 	MachineAuthAPIKey     string          `mask:"password" yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;PROXY_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary to access resources from other services."`
-	AutoprovisionAccounts bool            `yaml:"auto_provision_accounts" env:"PROXY_AUTOPROVISION_ACCOUNTS" desc:"Set this to 'true' to automatically provsion users that do not yet exist in the users service on-demand upon first signin. To use this a write-enabled libregraph user backend needs to be setup an running."`
+	AutoprovisionAccounts bool            `yaml:"auto_provision_accounts" env:"PROXY_AUTOPROVISION_ACCOUNTS" desc:"Set this to 'true' to automatically provision users that do not yet exist in the users service on-demand upon first sign-in. To use this a write-enabled libregraph user backend needs to be setup an running."`
 	EnableBasicAuth       bool            `yaml:"enable_basic_auth" env:"PROXY_ENABLE_BASIC_AUTH" desc:"Set this to true to enable 'basic' (username/password) authentication."`
 	InsecureBackends      bool            `yaml:"insecure_backends" env:"PROXY_INSECURE_BACKENDS" desc:"Disable TLS certificate validation for all HTTP backend connections."`
 	AuthMiddleware        AuthMiddleware  `yaml:"auth_middleware"`
diff --git a/services/users/pkg/config/defaults/defaultconfig.go b/services/users/pkg/config/defaults/defaultconfig.go
index b6f43113d56..2f7e8717038 100644
--- a/services/users/pkg/config/defaults/defaultconfig.go
+++ b/services/users/pkg/config/defaults/defaultconfig.go
@@ -43,7 +43,7 @@ func DefaultConfig() *config.Config {
 				GroupBaseDN:             "ou=groups,o=libregraph-idm",
 				UserScope:               "sub",
 				GroupScope:              "sub",
-				UserSubstringFilterType: "initial",
+				UserSubstringFilterType: "any",
 				UserFilter:              "",
 				GroupFilter:             "",
 				UserObjectClass:         "inetOrgPerson",


Test patch:
diff --git a/services/graph/pkg/identity/ldap_test.go b/services/graph/pkg/identity/ldap_test.go
index 7db012edbf4..b442a906542 100644
--- a/services/graph/pkg/identity/ldap_test.go
+++ b/services/graph/pkg/identity/ldap_test.go
@@ -18,17 +18,19 @@ func getMockedBackend(l ldap.Client, lc config.LDAP, logger *log.Logger) (*LDAP,
 }
 
 var lconfig = config.LDAP{
-	UserBaseDN:               "dc=test",
+	UserBaseDN:               "ou=people,dc=test",
+	UserObjectClass:          "inetOrgPerson",
 	UserSearchScope:          "sub",
-	UserFilter:               "filter",
+	UserFilter:               "",
 	UserDisplayNameAttribute: "displayname",
 	UserIDAttribute:          "entryUUID",
 	UserEmailAttribute:       "mail",
 	UserNameAttribute:        "uid",
 
-	GroupBaseDN:        "dc=test",
+	GroupBaseDN:        "ou=groups,dc=test",
+	GroupObjectClass:   "groupOfNames",
 	GroupSearchScope:   "sub",
-	GroupFilter:        "filter",
+	GroupFilter:        "",
 	GroupNameAttribute: "cn",
 	GroupIDAttribute:   "entryUUID",
 }
@@ -40,10 +42,24 @@ var userEntry = ldap.NewEntry("uid=user",
 		"mail":        {"user@example"},
 		"entryuuid":   {"abcd-defg"},
 	})
+var invalidUserEntry = ldap.NewEntry("uid=user",
+	map[string][]string{
+		"uid":         {"invalid"},
+		"displayname": {"DisplayName"},
+		"mail":        {"user@example"},
+	})
 var groupEntry = ldap.NewEntry("cn=group",
 	map[string][]string{
 		"cn":        {"group"},
 		"entryuuid": {"abcd-defg"},
+		"member": {
+			"uid=user,ou=people,dc=test",
+			"uid=invalid,ou=people,dc=test",
+		},
+	})
+var invalidGroupEntry = ldap.NewEntry("cn=invalid",
+	map[string][]string{
+		"cn": {"invalid"},
 	})
 
 var logger = log.NewLogger(log.Level("debug"))
@@ -112,8 +128,7 @@ func TestCreateUserModelFromLDAP(t *testing.T) {
 func TestGetUser(t *testing.T) {
 	// Mock a Sizelimit Error
 	lm := &mocks.Client{}
-	lm.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything,
-		mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
+	lm.On("Search", mock.Anything).
 		Return(
 			nil, ldap.NewError(ldap.LDAPResultSizeLimitExceeded, errors.New("mock")))
 	b, _ := getMockedBackend(lm, lconfig, &logger)
@@ -141,8 +156,7 @@ func TestGetUser(t *testing.T) {
 
 	// Mock an empty Search Result
 	lm = &mocks.Client{}
-	lm.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything,
-		mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
+	lm.On("Search", mock.Anything).
 		Return(
 			&ldap.SearchResult{}, nil)
 	b, _ = getMockedBackend(lm, lconfig, &logger)
@@ -161,10 +175,9 @@ func TestGetUser(t *testing.T) {
 		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
 	}
 
-	// Mock a valid	Search Result
+	// Mock a valid Search Result
 	lm = &mocks.Client{}
-	lm.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything,
-		mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
+	lm.On("Search", mock.Anything).
 		Return(
 			&ldap.SearchResult{
 				Entries: []*ldap.Entry{userEntry},
@@ -192,14 +205,27 @@ func TestGetUser(t *testing.T) {
 	} else if *u.Id != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id) {
 		t.Errorf("Expected GetUser to return a valid user")
 	}
+
+	// Mock invalid Search Result
+	lm = &mocks.Client{}
+	lm.On("Search", mock.Anything).
+		Return(
+			&ldap.SearchResult{
+				Entries: []*ldap.Entry{invalidUserEntry},
+			},
+			nil)
+
+	b, _ = getMockedBackend(lm, lconfig, &logger)
+	u, err = b.GetUser(context.Background(), "invalid", nil)
+	if err == nil || err.Error() != "itemNotFound" {
+		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
+	}
 }
 
 func TestGetUsers(t *testing.T) {
 	// Mock a Sizelimit Error
 	lm := &mocks.Client{}
-	lm.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything,
-		mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
-		Return(nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock")))
+	lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock")))
 
 	b, _ := getMockedBackend(lm, lconfig, &logger)
 	_, err := b.GetUsers(context.Background(), url.Values{})
@@ -208,9 +234,7 @@ func TestGetUsers(t *testing.T) {
 	}
 
 	lm = &mocks.Client{}
-	lm.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything,
-		mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
-		Return(&ldap.SearchResult{}, nil)
+	lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil)
 	b, _ = getMockedBackend(lm, lconfig, &logger)
 	g, err := b.GetUsers(context.Background(), url.Values{})
 	if err != nil {
@@ -223,15 +247,13 @@ func TestGetUsers(t *testing.T) {
 func TestGetGroup(t *testing.T) {
 	// Mock a Sizelimit Error
 	lm := &mocks.Client{}
-	lm.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything,
-		mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
-		Return(nil, ldap.NewError(ldap.LDAPResultSizeLimitExceeded, errors.New("mock")))
+	lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultSizeLimitExceeded, errors.New("mock")))
 
 	queryParamExpand := url.Values{
-		"$expand": []string{"memberOf"},
+		"$expand": []string{"members"},
 	}
 	queryParamSelect := url.Values{
-		"$select": []string{"memberOf"},
+		"$select": []string{"members"},
 	}
 	b, _ := getMockedBackend(lm, lconfig, &logger)
 	_, err := b.GetGroup(context.Background(), "group", nil)
@@ -249,9 +271,7 @@ func TestGetGroup(t *testing.T) {
 
 	// Mock an empty Search Result
 	lm = &mocks.Client{}
-	lm.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything,
-		mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
-		Return(&ldap.SearchResult{}, nil)
+	lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil)
 	b, _ = getMockedBackend(lm, lconfig, &logger)
 	_, err = b.GetGroup(context.Background(), "group", nil)
 	if err == nil || err.Error() != "itemNotFound" {
@@ -266,39 +286,93 @@ func TestGetGroup(t *testing.T) {
 		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
 	}
 
-	// Mock a valid	Search Result
+	// Mock an invalid Search Result
 	lm = &mocks.Client{}
-	lm.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything,
-		mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
-		Return(&ldap.SearchResult{
-			Entries: []*ldap.Entry{groupEntry},
-		}, nil)
+	lm.On("Search", mock.Anything).Return(&ldap.SearchResult{
+		Entries: []*ldap.Entry{invalidGroupEntry},
+	}, nil)
 	b, _ = getMockedBackend(lm, lconfig, &logger)
 	g, err := b.GetGroup(context.Background(), "group", nil)
+	if err == nil || err.Error() != "itemNotFound" {
+		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
+	}
+	g, err = b.GetGroup(context.Background(), "group", queryParamExpand)
+	if err == nil || err.Error() != "itemNotFound" {
+		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
+	}
+	g, err = b.GetGroup(context.Background(), "group", queryParamSelect)
+	if err == nil || err.Error() != "itemNotFound" {
+		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
+	}
+
+	// Mock a valid	Search Result
+	lm = &mocks.Client{}
+	sr1 := &ldap.SearchRequest{
+		BaseDN:     "ou=groups,dc=test",
+		Scope:      2,
+		SizeLimit:  1,
+		Filter:     "(&(objectClass=groupOfNames)(|(cn=group)(entryUUID=group)))",
+		Attributes: []string{"cn", "entryUUID", "member"},
+		Controls:   []ldap.Control(nil),
+	}
+	sr2 := &ldap.SearchRequest{
+		BaseDN:     "uid=user,ou=people,dc=test",
+		SizeLimit:  1,
+		Filter:     "(objectclass=*)",
+		Attributes: []string{"displayname", "entryUUID", "mail", "uid"},
+		Controls:   []ldap.Control(nil),
+	}
+	sr3 := &ldap.SearchRequest{
+		BaseDN:     "uid=invalid,ou=people,dc=test",
+		SizeLimit:  1,
+		Filter:     "(objectclass=*)",
+		Attributes: []string{"displayname", "entryUUID", "mail", "uid"},
+		Controls:   []ldap.Control(nil),
+	}
+
+	lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil)
+	lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, nil)
+	lm.On("Search", sr3).Return(&ldap.SearchResult{Entries: []*ldap.Entry{invalidUserEntry}}, nil)
+	b, _ = getMockedBackend(lm, lconfig, &logger)
+	g, err = b.GetGroup(context.Background(), "group", nil)
 	if err != nil {
 		t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
 	} else if *g.Id != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) {
 		t.Errorf("Expected GetGroup to return a valid group")
 	}
 	g, err = b.GetGroup(context.Background(), "group", queryParamExpand)
-	if err != nil {
+	switch {
+	case err != nil:
 		t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
-	} else if *g.Id != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) {
+	case g.GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id):
 		t.Errorf("Expected GetGroup to return a valid group")
+	case len(g.Members) != 1:
+		t.Errorf("Expected GetGroup with expand to return one member")
+	case g.Members[0].GetId() != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id):
+		t.Errorf("Expected GetGroup with expand to return correct member")
 	}
 	g, err = b.GetGroup(context.Background(), "group", queryParamSelect)
-	if err != nil {
+	switch {
+	case err != nil:
 		t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
-	} else if *g.Id != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) {
+	case g.GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id):
 		t.Errorf("Expected GetGroup to return a valid group")
+	case len(g.Members) != 1:
+		t.Errorf("Expected GetGroup with expand to return one member")
+	case g.Members[0].GetId() != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id):
+		t.Errorf("Expected GetGroup with expand to return correct member")
 	}
 }
 
 func TestGetGroups(t *testing.T) {
+	queryParamExpand := url.Values{
+		"$expand": []string{"members"},
+	}
+	queryParamSelect := url.Values{
+		"$select": []string{"members"},
+	}
 	lm := &mocks.Client{}
-	lm.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything,
-		mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
-		Return(nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock")))
+	lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock")))
 	b, _ := getMockedBackend(lm, lconfig, &logger)
 	_, err := b.GetGroups(context.Background(), url.Values{})
 	if err == nil || err.Error() != "itemNotFound" {
@@ -306,9 +380,7 @@ func TestGetGroups(t *testing.T) {
 	}
 
 	lm = &mocks.Client{}
-	lm.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything,
-		mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
-		Return(&ldap.SearchResult{}, nil)
+	lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil)
 	b, _ = getMockedBackend(lm, lconfig, &logger)
 	g, err := b.GetGroups(context.Background(), url.Values{})
 	if err != nil {
@@ -316,4 +388,58 @@ func TestGetGroups(t *testing.T) {
 	} else if g == nil || len(g) != 0 {
 		t.Errorf("Expected zero length user slice")
 	}
+
+	lm = &mocks.Client{}
+	lm.On("Search", mock.Anything).Return(&ldap.SearchResult{
+		Entries: []*ldap.Entry{groupEntry},
+	}, nil)
+	b, _ = getMockedBackend(lm, lconfig, &logger)
+	g, err = b.GetGroups(context.Background(), url.Values{})
+	if err != nil {
+		t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
+	} else if *g[0].Id != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) {
+		t.Errorf("Expected GetGroup to return a valid group")
+	}
+
+	// Mock a valid	Search Result with expanded group members
+	lm = &mocks.Client{}
+	sr1 := &ldap.SearchRequest{
+		BaseDN:     "ou=groups,dc=test",
+		Scope:      2,
+		Filter:     "(&(objectClass=groupOfNames))",
+		Attributes: []string{"cn", "entryUUID", "member"},
+		Controls:   []ldap.Control(nil),
+	}
+	sr2 := &ldap.SearchRequest{
+		BaseDN:     "uid=user,ou=people,dc=test",
+		SizeLimit:  1,
+		Filter:     "(objectclass=*)",
+		Attributes: []string{"displayname", "entryUUID", "mail", "uid"},
+		Controls:   []ldap.Control(nil),
+	}
+	sr3 := &ldap.SearchRequest{
+		BaseDN:     "uid=invalid,ou=people,dc=test",
+		SizeLimit:  1,
+		Filter:     "(objectclass=*)",
+		Attributes: []string{"displayname", "entryUUID", "mail", "uid"},
+		Controls:   []ldap.Control(nil),
+	}
+
+	for _, param := range []url.Values{queryParamSelect, queryParamExpand} {
+		lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil)
+		lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, nil)
+		lm.On("Search", sr3).Return(&ldap.SearchResult{Entries: []*ldap.Entry{invalidUserEntry}}, nil)
+		b, _ = getMockedBackend(lm, lconfig, &logger)
+		g, err = b.GetGroups(context.Background(), param)
+		switch {
+		case err != nil:
+			t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
+		case g[0].GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id):
+			t.Errorf("Expected GetGroup to return a valid group")
+		case len(g[0].Members) != 1:
+			t.Errorf("Expected GetGroup to return group with one member")
+		case g[0].Members[0].GetId() != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id):
+			t.Errorf("Expected GetGroup to return group with correct member")
+		}
+	}
 }