Repository: owncloud/ocis
Pull request #7253 (2023-09-08 06:31:41)

Bump golang.org/x/oauth2 from 0.11.0 to 0.12.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.11.0 to 0.12.0.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/golang/oauth2/commit/07085280e48195ed918272008a23087d24c0a356"><code>0708528</code></a> go.mod: update golang.org/x dependencies</li>
<li><a href="https://github.com/golang/oauth2/commit/a835fc4358f6852f50c4c5c33fddcd1adade5b0a"><code>a835fc4</code></a> oauth2: move global auth style cache to be per-Config</li>
<li>See full diff in <a href="https://github.com/golang/oauth2/compare/v0.11.0...v0.12.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/oauth2&package-manager=go_modules&previous-version=0.11.0&new-version=0.12.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

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]: 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=7253)

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

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7253&metric=coverage&view=list) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7253&metric=duplicated_lines_density&view=list) No Duplication information



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a17d66aa0d..9e101744465 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ The following sections list the changes for unreleased.
 
 * Bugfix - Fixed cunary in the beginning: [#7247](https://github.com/owncloud/ocis/pull/7247)
 * Enhancement - Support spec violating AD FS access token issuer: [#7138](https://github.com/owncloud/ocis/pull/7138)
+* Enhancement - Add the password policies: [#7194](https://github.com/owncloud/ocis/pull/7194)
 * Enhancement - Auto-Accept Shares: [#7097](https://github.com/owncloud/ocis/pull/7097)
 * Enhancement - Bump Reva: [#7138](https://github.com/owncloud/ocis/pull/7138)
 * Enhancement - Introduce clientlog service: [#7217](https://github.com/owncloud/ocis/pull/7217)
@@ -32,6 +33,13 @@ The following sections list the changes for unreleased.
 
    https://github.com/owncloud/ocis/pull/7138
 
+* Enhancement - Add the password policies: [#7194](https://github.com/owncloud/ocis/pull/7194)
+
+   Add the password policies OCIS-3767
+
+   https://github.com/owncloud/ocis/pull/7194
+   https://github.com/cs3org/reva/pull/4147
+
 * Enhancement - Auto-Accept Shares: [#7097](https://github.com/owncloud/ocis/pull/7097)
 
    Automatically accept shares when configured by the user or admin
diff --git a/changelog/unreleased/add-passwod-policies.md b/changelog/unreleased/add-passwod-policies.md
new file mode 100644
index 00000000000..ece2306e56d
--- /dev/null
+++ b/changelog/unreleased/add-passwod-policies.md
@@ -0,0 +1,6 @@
+Enhancement: Add the password policies
+
+Add the password policies OCIS-3767
+
+https://github.com/owncloud/ocis/pull/7194
+https://github.com/cs3org/reva/pull/4147
diff --git a/go.mod b/go.mod
index 1ac01b855f2..2c7c6aa1feb 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
 	github.com/coreos/go-oidc v2.2.1+incompatible
 	github.com/coreos/go-oidc/v3 v3.6.0
 	github.com/cs3org/go-cs3apis v0.0.0-20230516150832-730ac860c71d
-	github.com/cs3org/reva/v2 v2.16.1-0.20230906142214-864d9012e37f
+	github.com/cs3org/reva/v2 v2.16.1-0.20230908122008-491b5c5bf72a
 	github.com/disintegration/imaging v1.6.2
 	github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
 	github.com/egirna/icap-client v0.1.1
@@ -55,7 +55,7 @@ require (
 	github.com/libregraph/lico v0.60.1-0.20230811070109-1d4140be554d
 	github.com/mitchellh/mapstructure v1.5.0
 	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
-	github.com/nats-io/nats-server/v2 v2.9.21
+	github.com/nats-io/nats-server/v2 v2.9.22
 	github.com/oklog/run v1.1.0
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/onsi/ginkgo v1.16.5
@@ -92,7 +92,7 @@ require (
 	golang.org/x/exp v0.0.0-20221026004748-78e5e7837ae6
 	golang.org/x/image v0.12.0
 	golang.org/x/net v0.15.0
-	golang.org/x/oauth2 v0.11.0
+	golang.org/x/oauth2 v0.12.0
 	golang.org/x/sync v0.3.0
 	golang.org/x/term v0.12.0
 	golang.org/x/text v0.13.0
@@ -265,7 +265,7 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/mschoch/smat v0.2.0 // indirect
-	github.com/nats-io/jwt/v2 v2.4.1 // indirect
+	github.com/nats-io/jwt/v2 v2.5.0 // indirect
 	github.com/nats-io/nats.go v1.28.0 // indirect
 	github.com/nats-io/nkeys v0.4.4 // indirect
 	github.com/nats-io/nuid v1.0.1 // indirect
diff --git a/go.sum b/go.sum
index 8719735e944..da085a963ab 100644
--- a/go.sum
+++ b/go.sum
@@ -1013,8 +1013,8 @@ github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo
 github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
 github.com/crewjam/saml v0.4.13 h1:TYHggH/hwP7eArqiXSJUvtOPNzQDyQ7vwmwEqlFWhMc=
 github.com/crewjam/saml v0.4.13/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA=
-github.com/cs3org/reva/v2 v2.16.1-0.20230906142214-864d9012e37f h1:0mbvh+AvpYOp29R5LFgeqddyI0uKZnO/E3MyPEIaYdg=
-github.com/cs3org/reva/v2 v2.16.1-0.20230906142214-864d9012e37f/go.mod h1:RvhuweTFqzezjUFU0SIdTXakrEx9vJlMvQ7znPXSP1g=
+github.com/cs3org/reva/v2 v2.16.1-0.20230908122008-491b5c5bf72a h1:E7XghgSsNsR57wd0z/emKRfwDmoT2q/vuIYGNtx9IkA=
+github.com/cs3org/reva/v2 v2.16.1-0.20230908122008-491b5c5bf72a/go.mod h1:RvhuweTFqzezjUFU0SIdTXakrEx9vJlMvQ7znPXSP1g=
 github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -1711,10 +1711,10 @@ github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOl
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
-github.com/nats-io/jwt/v2 v2.4.1 h1:Y35W1dgbbz2SQUYDPCaclXcuqleVmpbRa7646Jf2EX4=
-github.com/nats-io/jwt/v2 v2.4.1/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI=
-github.com/nats-io/nats-server/v2 v2.9.21 h1:2TBTh0UDE74eNXQmV4HofsmRSCiVN0TH2Wgrp6BD6fk=
-github.com/nats-io/nats-server/v2 v2.9.21/go.mod h1:ozqMZc2vTHcNcblOiXMWIXkf8+0lDGAi5wQcG+O1mHU=
+github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak=
+github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI=
+github.com/nats-io/nats-server/v2 v2.9.22 h1:rzl88pqWFFrU4G00ed+JnY+uGHSLZ+3jrxDnJxzKwGA=
+github.com/nats-io/nats-server/v2 v2.9.22/go.mod h1:wEjrEy9vnqIGE4Pqz4/c75v9Pmaq7My2IgFmnykc4C0=
 github.com/nats-io/nats.go v1.28.0 h1:Th4G6zdsz2d0OqXdfzKLClo6bOfoI/b1kInhRtFIy5c=
 github.com/nats-io/nats.go v1.28.0/go.mod h1:XpbWUlOElGwTYbMR7imivs7jJj9GtK7ypv321Wp6pjc=
 github.com/nats-io/nkeys v0.4.4 h1:xvBJ8d69TznjcQl9t6//Q5xXuVhyYiSos6RPtvQNTwA=
@@ -2307,8 +2307,9 @@ golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw
 golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
 golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
 golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
-golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
 golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
+golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
+golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
diff --git a/services/frontend/README.md b/services/frontend/README.md
index 36c9b2a7b3e..ed087d6def1 100644
--- a/services/frontend/README.md
+++ b/services/frontend/README.md
@@ -63,3 +63,24 @@ The `frontend` service contains an eventhandler for handling `ocs` related event
 ### Auto-Accept Shares
 
 When setting the `FRONTEND_AUTO_ACCEPT_SHARES` to `true`, all incoming shares will be accepted automatically. Users can overwrite this setting individually in their profile.
+
+## The password policy
+
+Note that the password policy currently impacts _only_ public link password validation.
+
+With the password policy, mandatory criteria for the password can be defined via the environment variables listed below.
+
+Generally, a password can contain any UTF-8 characters, however some characters are regarded as special since they are not used in ordinary texts. Which characters should be treated as special is defined by "The OWASP® Foundation" [password-special-characters](https://owasp.org/www-community/password-special-characters) (between double quotes): " !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"
+
+Note that a password can have a maximum length of **72 bytes**. Depending on the alphabet used, a character is encoded by 1 to 4 bytes, defining the maximum length of a password indirectly. While US-ASCII will only need one byte, Latin alphabets and also Greek or Cyrillic ones need two bytes. Three bytes are needed for characters in Chinese, Japanese and Korean etc.
+
+-   `FRONTEND_PASSWORD_POLICIES_MIN_CHARACTERS`
+Define the minimum password length.
+-   `FRONTEND_PASSWORD_POLICIES_MIN_LOWER_CASE_CHARACTERS`
+Define the minimum number of uppercase letters.
+-   `FRONTEND_PASSWORD_POLICIES_MIN_UPPER_CASE_CHARACTERS`
+Define the minimum number of lowercase letters.
+-   `FRONTEND_PASSWORD_POLICIES_MIN_DIGITS`
+Define the minimum number of digits.
+-   `FRONTEND_PASSWORD_POLICIES_MIN_SPECIAL_CHARACTERS`
+Define the minimum number of special characters.
diff --git a/services/frontend/pkg/config/config.go b/services/frontend/pkg/config/config.go
index 21acc43fc32..2e9d87afc71 100644
--- a/services/frontend/pkg/config/config.go
+++ b/services/frontend/pkg/config/config.go
@@ -58,6 +58,8 @@ type Config struct {
 	AutoAcceptShares bool                  `yaml:"auto_accept_shares" env:"FRONTEND_AUTO_ACCEPT_SHARES" desc:"Defines if shares should be auto accepted by default. Users can change this setting individually in their profile."`
 	ServiceAccount   ServiceAccount        `yaml:"service_account"`
 
+	PasswordPolicies PasswordPolicies `yaml:"password_policies"`
+
 	Supervised bool            `yaml:"-"`
 	Context    context.Context `yaml:"-"`
 }
@@ -171,3 +173,12 @@ type ServiceAccount struct {
 	ServiceAccountID     string `yaml:"service_account_id" env:"OCIS_SERVICE_ACCOUNT_ID;FRONTEND_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details."`
 	ServiceAccountSecret string `yaml:"service_account_secret" env:"OCIS_SERVICE_ACCOUNT_SECRET;FRONTEND_SERVICE_ACCOUNT_SECRET" desc:"The service account secret."`
 }
+
+// PasswordPolicies configures reva password policies
+type PasswordPolicies struct {
+	MinCharacters          int `yaml:"min_characters,omitempty" env:"FRONTEND_PASSWORD_POLICIES_MIN_CHARACTERS" desc:"Define the minimum password length. Defaults to 0 if not set."`
+	MinLowerCaseCharacters int `yaml:"min_lower_case_characters" env:"FRONTEND_PASSWORD_POLICIES_MIN_LOWER_CASE_CHARACTERS" desc:"Define the minimum number of uppercase letters. Defaults to 0 if not set."`
+	MinUpperCaseCharacters int `yaml:"min_upper_case_characters" env:"FRONTEND_PASSWORD_POLICIES_MIN_UPPER_CASE_CHARACTERS" desc:"Define the minimum number of lowercase letters. Defaults to 0 if not set."`
+	MinDigits              int `yaml:"min_digits" env:"FRONTEND_PASSWORD_POLICIES_MIN_DIGITS" desc:"Define the minimum number of digits. Defaults to 0 if not set."`
+	MinSpecialCharacters   int `yaml:"min_special_characters" env:"FRONTEND_PASSWORD_POLICIES_MIN_SPECIAL_CHARACTERS" desc:"Define the minimum number of characters from the special characters list to be present. Defaults to 0 if not set."`
+}
diff --git a/services/frontend/pkg/revaconfig/config.go b/services/frontend/pkg/revaconfig/config.go
index 3383251c1db..e800c5a4368 100644
--- a/services/frontend/pkg/revaconfig/config.go
+++ b/services/frontend/pkg/revaconfig/config.go
@@ -274,6 +274,14 @@ func FrontendConfigFromStruct(cfg *config.Config) (map[string]interface{}, error
 								"share_jail": cfg.EnableShareJail,
 								"max_quota":  cfg.MaxQuota,
 							},
+							"password_policies": map[string]interface{}{
+								"max_characters":            72,
+								"min_characters":            cfg.PasswordPolicies.MinCharacters,
+								"min_lower_case_characters": cfg.PasswordPolicies.MinLowerCaseCharacters,
+								"min_upper_case_characters": cfg.PasswordPolicies.MinUpperCaseCharacters,
+								"min_digits":                cfg.PasswordPolicies.MinDigits,
+								"min_special_characters":    cfg.PasswordPolicies.MinSpecialCharacters,
+							},
 							"notifications": map[string]interface{}{
 								"endpoints": []string{"list", "get", "delete"},
 							},
diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go
index 16387914cda..0e31bb677d0 100644
--- a/services/graph/pkg/service/v0/driveitems.go
+++ b/services/graph/pkg/service/v0/driveitems.go
@@ -12,6 +12,7 @@ import (
 	cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
 	storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
+	revactx "github.com/cs3org/reva/v2/pkg/ctx"
 	"github.com/cs3org/reva/v2/pkg/storagespace"
 	"github.com/cs3org/reva/v2/pkg/utils"
 	"github.com/go-chi/render"
@@ -32,10 +33,18 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	res, err := gatewayClient.GetHome(ctx, &storageprovider.GetHomeRequest{})
+	currentUser := revactx.ContextMustGetUser(r.Context())
+	// do we need to list all or only the personal drive
+	filters := []*storageprovider.ListStorageSpacesRequest_Filter{}
+	filters = append(filters, listStorageSpacesUserFilter(currentUser.GetId().OpaqueId))
+	filters = append(filters, listStorageSpacesTypeFilter("personal"))
+
+	res, err := gatewayClient.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{
+		Filters: filters,
+	})
 	switch {
 	case err != nil:
-		g.logger.Error().Err(err).Msg("error sending get home grpc request")
+		g.logger.Error().Err(err).Msg("error making ListStorageSpaces grpc call")
 		errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
 		return
 	case res.Status.Code != cs3rpc.Code_CODE_OK:
@@ -43,29 +52,34 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) {
 			errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message)
 			return
 		}
-		g.logger.Error().Err(err).Msg("error sending get home grpc request")
+		g.logger.Error().Err(err).Msg("error sending ListStorageSpaces grpc request")
 		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
 		return
 	}
 
+	var space *storageprovider.StorageSpace
+	for _, s := range res.StorageSpaces {
+		if utils.UserIDEqual(currentUser.GetId(), s.GetOwner().GetId()) {
+			space = s
+		}
+	}
+
 	lRes, err := gatewayClient.ListContainer(ctx, &storageprovider.ListContainerRequest{
-		Ref: &storageprovider.Reference{
-			Path: res.Path,
-		},
+		Ref: &storageprovider.Reference{ResourceId: space.Root},
 	})
 	switch {
 	case err != nil:
-		g.logger.Error().Err(err).Msg("error sending list container grpc request")
+		g.logger.Error().Err(err).Msg("error making ListContainer grpc call")
 		errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
 		return
-	case res.Status.Code != cs3rpc.Code_CODE_OK:
-		if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
-			errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message)
+	case lRes.Status.Code != cs3rpc.Code_CODE_OK:
+		if lRes.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
+			errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, lRes.Status.Message)
 			return
 		}
-		if res.Status.Code == cs3rpc.Code_CODE_PERMISSION_DENIED {
+		if lRes.Status.Code == cs3rpc.Code_CODE_PERMISSION_DENIED {
 			// TODO check if we should return 404 to not disclose existing items
-			errorcode.AccessDenied.Render(w, r, http.StatusForbidden, res.Status.Message)
+			errorcode.AccessDenied.Render(w, r, http.StatusForbidden, lRes.Status.Message)
 			return
 		}
 		g.logger.Error().Err(err).Msg("error sending list container grpc request")
@@ -84,6 +98,138 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) {
 	render.JSON(w, r, &ListResponse{Value: files})
 }
 
+// GetDriveItem returns a driveItem
+func (g Graph) GetDriveItem(w http.ResponseWriter, r *http.Request) {
+	g.logger.Info().Msg("Calling GetDriveItem")
+	ctx := r.Context()
+
+	driveID, err := parseIDParam(r, "driveID")
+	if err != nil {
+		errorcode.RenderError(w, r, err)
+		return
+	}
+	driveItemID, err := parseIDParam(r, "driveItemID")
+	if err != nil {
+		errorcode.RenderError(w, r, err)
+		return
+	}
+	if driveID.StorageId != driveItemID.StorageId || driveID.SpaceId != driveItemID.SpaceId {
+		errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Item does not exist")
+		return
+	}
+	/*
+		sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
+		// Parse the request with odata parser
+		odataReq, err := godata.ParseRequest(ctx, sanitizedPath, r.URL.Query())
+		if err != nil {
+			errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
+			return
+		}
+	*/
+
+	gatewayClient, err := g.gatewaySelector.Next()
+	if err != nil {
+		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
+		return
+	}
+	res, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &driveItemID}})
+	switch {
+	case err != nil:
+		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
+		return
+	case res.Status.Code == cs3rpc.Code_CODE_OK:
+		// ok
+	case res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND:
+		errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message)
+		return
+	case res.Status.Code == cs3rpc.Code_CODE_PERMISSION_DENIED:
+		errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message) // do not leak existence? check what graph does
+		return
+	case res.Status.Code == cs3rpc.Code_CODE_UNAUTHENTICATED:
+		errorcode.Unauthenticated.Render(w, r, http.StatusUnauthorized, res.Status.Message) // do not leak existence? check what graph does
+		return
+	default:
+		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
+		return
+	}
+	driveItem, err := cs3ResourceToDriveItem(res.Info)
+	if err != nil {
+		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	render.Status(r, http.StatusOK)
+	render.JSON(w, r, &driveItem)
+}
+
+// GetDriveItemChildren lists the children of a driveItem
+func (g Graph) GetDriveItemChildren(w http.ResponseWriter, r *http.Request) {
+	g.logger.Info().Msg("Calling GetDriveItemChildren")
+	ctx := r.Context()
+
+	driveID, err := parseIDParam(r, "driveID")
+	if err != nil {
+		errorcode.RenderError(w, r, err)
+		return
+	}
+	driveItemID, err := parseIDParam(r, "driveItemID")
+	if err != nil {
+		errorcode.RenderError(w, r, err)
+		return
+	}
+	if driveID.StorageId != driveItemID.StorageId || driveID.SpaceId != driveItemID.SpaceId {
+		errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Item does not exist")
+		return
+	}
+	/*
+		sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
+		// Parse the request with odata parser
+		odataReq, err := godata.ParseRequest(ctx, sanitizedPath, r.URL.Query())
+		if err != nil {
+			errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
+			return
+		}
+	*/
+
+	gatewayClient, err := g.gatewaySelector.Next()
+	if err != nil {
+		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	res, err := gatewayClient.ListContainer(ctx, &storageprovider.ListContainerRequest{
+		Ref: &storageprovider.Reference{ResourceId: &driveItemID},
+	})
+	switch {
+	case err != nil:
+		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
+		return
+	case res.Status.Code == cs3rpc.Code_CODE_OK:
+		// ok
+	case res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND:
+		errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message)
+		return
+	case res.Status.Code == cs3rpc.Code_CODE_PERMISSION_DENIED:
+		errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message) // do not leak existence? check what graph does
+		return
+	case res.Status.Code == cs3rpc.Code_CODE_UNAUTHENTICATED:
+		errorcode.Unauthenticated.Render(w, r, http.StatusUnauthorized, res.Status.Message) // do not leak existence? check what graph does
+		return
+	default:
+		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
+		return
+	}
+
+	files, err := formatDriveItems(res.Infos)
+	if err != nil {
+		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	render.Status(r, http.StatusOK)
+	render.JSON(w, r, &ListResponse{Value: files})
+}
+
 func (g Graph) getDriveItem(ctx context.Context, ref storageprovider.Reference) (*libregraph.DriveItem, error) {
 	gatewayClient, err := g.gatewaySelector.Next()
 	if err != nil {
diff --git a/services/graph/pkg/service/v0/drives.go b/services/graph/pkg/service/v0/drives.go
index 2e3898e678a..f3f1d5da35f 100644
--- a/services/graph/pkg/service/v0/drives.go
+++ b/services/graph/pkg/service/v0/drives.go
@@ -17,13 +17,11 @@ import (
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
 	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
-	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
 	storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
 	revactx "github.com/cs3org/reva/v2/pkg/ctx"
 	"github.com/cs3org/reva/v2/pkg/storagespace"
 	"github.com/cs3org/reva/v2/pkg/utils"
-	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/render"
 	"github.com/jellydator/ttlcache/v3"
 	libregraph "github.com/owncloud/libre-graph-api-go"
@@ -157,41 +155,37 @@ func (g Graph) getDrives(w http.ResponseWriter, r *http.Request, unrestricted bo
 
 // GetSingleDrive does a lookup of a single space by spaceId
 func (g Graph) GetSingleDrive(w http.ResponseWriter, r *http.Request) {
-	logger := g.logger.SubloggerWithRequestID(r.Context())
+	ctx := r.Context()
+	logger := g.logger.SubloggerWithRequestID(ctx)
 	logger.Info().Interface("query", r.URL.Query()).Msg("calling get drive")
-	driveID, err := url.PathUnescape(chi.URLParam(r, "driveID"))
+
+	rid, err := parseIDParam(r, "driveID")
 	if err != nil {
-		logger.Debug().Err(err).Str("driveID", chi.URLParam(r, "driveID")).Msg("could not get drive: unescaping drive id failed")
-		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping drive id failed")
+		errorcode.RenderError(w, r, err)
 		return
 	}
+	log := logger.With().Str("storage", rid.StorageId).Str("space", rid.SpaceId).Str("node", rid.OpaqueId).Logger()
 
-	if driveID == "" {
-		logger.Debug().Msg("could not get drive: missing drive id")
-		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing drive id")
-		return
-	}
+	log.Debug().Msg("calling list storage spaces with id filter")
 
-	logger.Debug().Str("driveID", driveID).Msg("calling list storage spaces with id filter")
-	ctx := r.Context()
-
-	filters := []*storageprovider.ListStorageSpacesRequest_Filter{listStorageSpacesIDFilter(driveID)}
+	filters := []*storageprovider.ListStorageSpacesRequest_Filter{
+		listStorageSpacesIDFilter(storagespace.FormatResourceID(rid)),
+	}
 	res, err := g.ListStorageSpacesWithFilters(ctx, filters, true)
 	switch {
 	case err != nil:
-		logger.Error().Err(err).Msg("could not get drive: transport error")
+		log.Error().Err(err).Msg("could not get drive: transport error")
 		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
 		return
 	case res.Status.Code != cs3rpc.Code_CODE_OK:
 		if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
 			// the client is doing a lookup for a specific space, therefore we need to return
 			// not found to the caller
-			logger.Debug().Str("driveID", driveID).Msg("could not get drive: not found")
+			log.Debug().Msg("could not get drive: not found")
 			errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "drive not found")
 			return
 		}
-		logger.Debug().
-			Str("id", driveID).
+		log.Debug().
 			Str("grpcmessage", res.GetStatus().GetMessage()).
 			Msg("could not get drive: grpc error")
 		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
@@ -200,26 +194,26 @@ func (g Graph) GetSingleDrive(w http.ResponseWriter, r *http.Request) {
 
 	webDavBaseURL, err := g.getWebDavBaseURL()
 	if err != nil {
-		logger.Error().Err(err).Str("url", webDavBaseURL.String()).Msg("could not get drive: error parsing webdav base url")
+		log.Error().Err(err).Str("url", webDavBaseURL.String()).Msg("could not get drive: error parsing webdav base url")
 		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
 		return
 	}
 	spaces, err := g.formatDrives(ctx, webDavBaseURL, res.StorageSpaces)
 	if err != nil {
-		logger.Debug().Err(err).Msg("could not get drive: error parsing grpc response")
+		log.Debug().Err(err).Msg("could not get drive: error parsing grpc response")
 		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
 		return
 	}
 	switch num := len(spaces); {
 	case num == 0:
-		logger.Debug().Str("id", driveID).Msg("could not get drive: no drive returned from storage")
+		log.Debug().Msg("could not get drive: no drive returned from storage")
 		errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "no drive returned from storage")
 		return
 	case num == 1:
 		render.Status(r, http.StatusOK)
 		render.JSON(w, r, spaces[0])
 	default:
-		logger.Debug().Int("number", num).Msg("could not get drive: expected to find a single drive but fetched more")
+		log.Debug().Int("number", num).Msg("could not get drive: expected to find a single drive but fetched more")
 		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not get drive: expected to find a single drive but fetched more")
 		return
 	}
@@ -354,16 +348,10 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
 func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
 	logger := g.logger.SubloggerWithRequestID(r.Context())
 	logger.Info().Msg("calling update drive")
-	driveID, err := url.PathUnescape(chi.URLParam(r, "driveID"))
-	if err != nil {
-		logger.Debug().Err(err).Str("id", chi.URLParam(r, "driveID")).Msg("could not update drive, unescaping drive id failed")
-		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping drive id failed")
-		return
-	}
 
-	if driveID == "" {
-		logger.Debug().Msg("Could not update drive, missing drive id")
-		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing drive id")
+	rid, err := parseIDParam(r, "driveID")
+	if err != nil {
+		errorcode.RenderError(w, r, err)
 		return
 	}
 
@@ -374,15 +362,6 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	rid, err := storagespace.ParseID(driveID)
-	if err != nil {
-		logger.Debug().Err(err).Interface("id", rid).Msg("could not update drive, invalid resource id")
-		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid resource id")
-		w.WriteHeader(http.StatusInternalServerError)
-		return
-	}
-	root := &rid
-
 	gatewayClient, err := g.gatewaySelector.Next()
 	if err != nil {
 		g.logger.Error().Err(err).Msg("could not select next gateway client")
@@ -390,6 +369,7 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	root := &rid
 	updateSpaceRequest := &storageprovider.UpdateStorageSpaceRequest{
 		// Prepare the object to apply the diff from. The properties on StorageSpace will overwrite
 		// the original storage space.
@@ -445,7 +425,7 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
 		dt := _spaceTypePersonal
 		filters := []*storageprovider.ListStorageSpacesRequest_Filter{listStorageSpacesTypeFilter(_spaceTypeProject)}
 		res, err := g.ListStorageSpacesWithFilters(r.Context(), filters, true)
-		if err == nil && res.GetStatus().GetCode() == rpc.Code_CODE_OK {
+		if err == nil && res.GetStatus().GetCode() == cs3rpc.Code_CODE_OK {
 			for _, sp := range res.StorageSpaces {
 				id, _ := storagespace.ParseID(sp.GetId().GetOpaqueId())
 				if id.GetSpaceId() == rid.GetSpaceId() {
@@ -1032,23 +1012,9 @@ func listStorageSpacesTypeFilter(spaceType string) *storageprovider.ListStorageS
 func (g Graph) DeleteDrive(w http.ResponseWriter, r *http.Request) {
 	logger := g.logger.SubloggerWithRequestID(r.Context())
 	logger.Info().Msg("calling delete drive")
-	driveID, err := url.PathUnescape(chi.URLParam(r, "driveID"))
-	if err != nil {
-		logger.Debug().Err(err).Str("id", chi.URLParam(r, "driveID")).Msg("could not delete drive: unescaping drive id failed")
-		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping drive id failed")
-		return
-	}
-
-	if driveID == "" {
-		logger.Debug().Msg("could not delete drive: missing drive id")
-		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing drive id")
-		return
-	}
-
-	rid, err := storagespace.ParseID(driveID)
+	rid, err := parseIDParam(r, "driveID")
 	if err != nil {
-		logger.Debug().Interface("id", rid).Err(err).Msg("could not delete drive: invalid resource id")
-		errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid resource id")
+		errorcode.RenderError(w, r, err)
 		return
 	}
 
diff --git a/services/graph/pkg/service/v0/errorcode/errorcode.go b/services/graph/pkg/service/v0/errorcode/errorcode.go
index e15289f327f..6ac3b0a44a3 100644
--- a/services/graph/pkg/service/v0/errorcode/errorcode.go
+++ b/services/graph/pkg/service/v0/errorcode/errorcode.go
@@ -18,6 +18,7 @@ type Error struct {
 	msg       string
 }
 
+// List taken from https://github.com/microsoft/microsoft-graph-docs-1/blob/main/concepts/errors.md#code-property
 const (
 	// AccessDenied defines the error if the caller doesn't have permission to perform the action.
 	AccessDenied ErrorCode = iota
@@ -47,6 +48,8 @@ const (
 	ResyncRequired
 	// ServiceNotAvailable defines the error if the service is not available. Try the request again after a delay. There may be a Retry-After header.
 	ServiceNotAvailable
+	// The sync state generation is not found. The delta token is expired and data must be synchronized again.
+	SyncStateNotFound
 	// QuotaLimitReached the user has reached their quota limit.
 	QuotaLimitReached
 	// Unauthenticated the caller is not authenticated.
@@ -70,6 +73,7 @@ var errorCodes = [...]string{
 	"resourceModified",
 	"resyncRequired",
 	"serviceNotAvailable",
+	"syncStateNotFound",
 	"quotaLimitReached",
 	"unauthenticated",
 	"preconditionFailed",
@@ -103,12 +107,18 @@ func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int, ms
 func (e Error) Render(w http.ResponseWriter, r *http.Request) {
 	var status int
 	switch e.errorCode {
+	case AccessDenied:
+		status = http.StatusForbidden
+	case InvalidRange:
+		status = http.StatusRequestedRangeNotSatisfiable
+	case InvalidRequest:
+		status = http.StatusBadRequest
 	case ItemNotFound:
 		status = http.StatusNotFound
 	case NameAlreadyExists:
 		status = http.StatusConflict
 	case NotAllowed:
-		status = http.StatusForbidden
+		status = http.StatusMethodNotAllowed
 	default:
 		status = http.StatusInternalServerError
 	}
diff --git a/services/graph/pkg/service/v0/graph.go b/services/graph/pkg/service/v0/graph.go
index 2d3c0995c13..29d22beccaa 100644
--- a/services/graph/pkg/service/v0/graph.go
+++ b/services/graph/pkg/service/v0/graph.go
@@ -9,8 +9,10 @@ import (
 	"strings"
 
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
+	storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	"github.com/cs3org/reva/v2/pkg/events"
 	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
+	"github.com/cs3org/reva/v2/pkg/storagespace"
 	"github.com/go-chi/chi/v5"
 	"github.com/jellydator/ttlcache/v3"
 	libregraph "github.com/owncloud/libre-graph-api-go"
@@ -21,6 +23,7 @@ import (
 	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
 	"github.com/owncloud/ocis/v2/services/graph/pkg/config"
 	"github.com/owncloud/ocis/v2/services/graph/pkg/identity"
+	"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
 	"go-micro.dev/v4/client"
 	mevents "go-micro.dev/v4/events"
 	"go.opentelemetry.io/otel/trace"
@@ -134,3 +137,16 @@ func (g Graph) parseMemberRef(ref string) (string, string, error) {
 	memberType := segments[len(segments)-2]
 	return memberType, id, nil
 }
+
+func parseIDParam(r *http.Request, param string) (storageprovider.ResourceId, error) {
+	driveID, err := url.PathUnescape(chi.URLParam(r, param))
+	if err != nil {
+		return storageprovider.ResourceId{}, errorcode.New(errorcode.InvalidRequest, err.Error())
+	}
+
+	id, err := storagespace.ParseID(driveID)
+	if err != nil {
+		return storageprovider.ResourceId{}, errorcode.New(errorcode.InvalidRequest, err.Error())
+	}
+	return id, nil
+}
diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go
index e47c562e821..93cbeebe0fd 100644
--- a/services/graph/pkg/service/v0/service.go
+++ b/services/graph/pkg/service/v0/service.go
@@ -96,11 +96,14 @@ type Service interface {
 	GetDrives(w http.ResponseWriter, r *http.Request)
 	GetSingleDrive(w http.ResponseWriter, r *http.Request)
 	GetAllDrives(w http.ResponseWriter, r *http.Request)
-	GetRootDriveChildren(w http.ResponseWriter, r *http.Request)
 	CreateDrive(w http.ResponseWriter, r *http.Request)
 	UpdateDrive(w http.ResponseWriter, r *http.Request)
 	DeleteDrive(w http.ResponseWriter, r *http.Request)
 
+	GetRootDriveChildren(w http.ResponseWriter, r *http.Request)
+	GetDriveItem(w http.ResponseWriter, r *http.Request)
+	GetDriveItemChildren(w http.ResponseWriter, r *http.Request)
+
 	GetTags(w http.ResponseWriter, r *http.Request)
 	AssignTags(w http.ResponseWriter, r *http.Request)
 	UnassignTags(w http.ResponseWriter, r *http.Request)
@@ -252,6 +255,10 @@ func NewService(opts ...Option) (Graph, error) {
 					r.Patch("/", svc.UpdateDrive)
 					r.Get("/", svc.GetSingleDrive)
 					r.Delete("/", svc.DeleteDrive)
+					r.Route("/items/{driveItemID}", func(r chi.Router) {
+						r.Get("/", svc.GetDriveItem)
+						r.Get("/children", svc.GetDriveItemChildren)
+					})
 				})
 			})
 			r.With(requireAdmin).Route("/education", func(r chi.Router) {
diff --git a/services/userlog/pkg/service/l10n/locale/bg_BG/LC_MESSAGES/userlog.po b/services/userlog/pkg/service/l10n/locale/bg_BG/LC_MESSAGES/userlog.po
index f4fa70a195a..055d4fb7def 100644
--- a/services/userlog/pkg/service/l10n/locale/bg_BG/LC_MESSAGES/userlog.po
+++ b/services/userlog/pkg/service/l10n/locale/bg_BG/LC_MESSAGES/userlog.po
@@ -11,7 +11,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: EMAIL\n"
-"POT-Creation-Date: 2023-08-21 00:20+0000\n"
+"POT-Creation-Date: 2023-09-11 00:05+0000\n"
 "PO-Revision-Date: 2023-03-15 08:28+0000\n"
 "Last-Translator: Martin Zhekov <martin03staff@gmail.com>, 2023\n"
 "Language-Team: Bulgarian (Bulgaria) (https://app.transifex.com/owncloud-org/teams/6149/bg_BG/)\n"
diff --git a/services/userlog/pkg/service/l10n/locale/ko/LC_MESSAGES/userlog.po b/services/userlog/pkg/service/l10n/locale/ko/LC_MESSAGES/userlog.po
index ee0a05d644c..566ce85d289 100644
--- a/services/userlog/pkg/service/l10n/locale/ko/LC_MESSAGES/userlog.po
+++ b/services/userlog/pkg/service/l10n/locale/ko/LC_MESSAGES/userlog.po
@@ -11,7 +11,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: EMAIL\n"
-"POT-Creation-Date: 2023-08-21 00:20+0000\n"
+"POT-Creation-Date: 2023-09-11 00:05+0000\n"
 "PO-Revision-Date: 2023-03-15 08:28+0000\n"
 "Last-Translator: Hugo Blackwood, 2023\n"
 "Language-Team: Korean (https://app.transifex.com/owncloud-org/teams/6149/ko/)\n"
diff --git a/services/userlog/pkg/service/l10n/locale/pt_BR/LC_MESSAGES/userlog.po b/services/userlog/pkg/service/l10n/locale/pt_BR/LC_MESSAGES/userlog.po
index ac753a798c6..cbd44832de7 100644
--- a/services/userlog/pkg/service/l10n/locale/pt_BR/LC_MESSAGES/userlog.po
+++ b/services/userlog/pkg/service/l10n/locale/pt_BR/LC_MESSAGES/userlog.po
@@ -11,7 +11,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: EMAIL\n"
-"POT-Creation-Date: 2023-08-22 00:43+0000\n"
+"POT-Creation-Date: 2023-09-11 04:26+0000\n"
 "PO-Revision-Date: 2023-03-15 08:28+0000\n"
 "Last-Translator: Flávio Veras <flaviove@gmail.com>, 2023\n"
 "Language-Team: Portuguese (Brazil) (https://app.transifex.com/owncloud-org/teams/6149/pt_BR/)\n"
diff --git a/services/userlog/pkg/service/l10n/locale/sq/LC_MESSAGES/userlog.po b/services/userlog/pkg/service/l10n/locale/sq/LC_MESSAGES/userlog.po
index 6d7436528b3..7b87689b57c 100644
--- a/services/userlog/pkg/service/l10n/locale/sq/LC_MESSAGES/userlog.po
+++ b/services/userlog/pkg/service/l10n/locale/sq/LC_MESSAGES/userlog.po
@@ -11,7 +11,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: EMAIL\n"
-"POT-Creation-Date: 2023-08-22 00:43+0000\n"
+"POT-Creation-Date: 2023-09-11 04:26+0000\n"
 "PO-Revision-Date: 2023-03-15 08:28+0000\n"
 "Last-Translator: Besnik Bleta <besnik@programeshqip.org>, 2023\n"
 "Language-Team: Albanian (https://app.transifex.com/owncloud-org/teams/6149/sq/)\n"
diff --git a/services/userlog/pkg/service/l10n/locale/zh_CN/LC_MESSAGES/userlog.po b/services/userlog/pkg/service/l10n/locale/zh_CN/LC_MESSAGES/userlog.po
index 8ca3d499918..2680f0897af 100644
--- a/services/userlog/pkg/service/l10n/locale/zh_CN/LC_MESSAGES/userlog.po
+++ b/services/userlog/pkg/service/l10n/locale/zh_CN/LC_MESSAGES/userlog.po
@@ -11,7 +11,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: EMAIL\n"
-"POT-Creation-Date: 2023-08-21 00:20+0000\n"
+"POT-Creation-Date: 2023-09-11 00:05+0000\n"
 "PO-Revision-Date: 2023-03-15 08:28+0000\n"
 "Last-Translator: Shouyuan, 2023\n"
 "Language-Team: Chinese (China) (https://app.transifex.com/owncloud-org/teams/6149/zh_CN/)\n"
diff --git a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go b/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go
index 562187df502..ff239b649c0 100644
--- a/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go
+++ b/vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go
@@ -126,7 +126,6 @@ type GatewayClientGetter func() (gateway.GatewayAPIClient, error)
 
 // Init initializes this and any contained handlers
 func (h *Handler) Init(c *config.Config) error {
-	var err error
 	h.gatewayAddr = c.GatewaySvc
 	h.machineAuthAPIKey = c.MachineAuthAPIKey
 	h.storageRegistryAddr = c.StorageregistrySvc
@@ -142,10 +141,7 @@ func (h *Handler) Init(c *config.Config) error {
 	h.deniable = c.EnableDenials
 	h.resharing = resharing(c)
 	h.publicPasswordEnforced = publicPwdEnforced(c)
-	h.passwordValidator, err = passwordPolicies(c)
-	if err != nil {
-		return err
-	}
+	h.passwordValidator = passwordPolicies(c)
 
 	h.statCache = cache.GetStatCache(c.StatCacheStore, c.StatCacheNodes, c.StatCacheDatabase, "stat", time.Duration(c.StatCacheTTL)*time.Second, c.StatCacheSize)
 	if c.CacheWarmupDriver != "" {
@@ -1594,28 +1590,17 @@ func publicPwdEnforced(c *config.Config) passwordEnforced {
 	return enf
 }
 
-func passwordPolicies(c *config.Config) (password.Validator, error) {
-	var pv password.Validator
-	var err error
+func passwordPolicies(c *config.Config) password.Validator {
 	if c.Capabilities.Capabilities == nil || c.Capabilities.Capabilities.PasswordPolicies == nil {
-		pv, err = password.NewPasswordPolicies(0, 0, 0, 0, 0, "")
-		if err != nil {
-			return nil, fmt.Errorf("can't init the Password Policies %w", err)
-		}
-		return pv, nil
+		return password.NewPasswordPolicies(0, 0, 0, 0, 0)
 	}
-	pv, err = password.NewPasswordPolicies(
+	return password.NewPasswordPolicies(
 		c.Capabilities.Capabilities.PasswordPolicies.MinCharacters,
 		c.Capabilities.Capabilities.PasswordPolicies.MinLowerCaseCharacters,
 		c.Capabilities.Capabilities.PasswordPolicies.MinUpperCaseCharacters,
 		c.Capabilities.Capabilities.PasswordPolicies.MinDigits,
 		c.Capabilities.Capabilities.PasswordPolicies.MinSpecialCharacters,
-		c.Capabilities.Capabilities.PasswordPolicies.SpecialCharacters,
 	)
-	if err != nil {
-		return nil, fmt.Errorf("can't init the Password Policies %w", err)
-	}
-	return pv, nil
 }
 
 // sufficientPermissions returns true if the `existing` permissions contain the `requested` permissions
diff --git a/vendor/github.com/cs3org/reva/v2/pkg/password/password_policies.go b/vendor/github.com/cs3org/reva/v2/pkg/password/password_policies.go
index 9ebba7b7839..9a499ba8ce7 100644
--- a/vendor/github.com/cs3org/reva/v2/pkg/password/password_policies.go
+++ b/vendor/github.com/cs3org/reva/v2/pkg/password/password_policies.go
@@ -8,6 +8,9 @@ import (
 	"unicode/utf8"
 )
 
+// https://owasp.org/www-community/password-special-characters
+var _defaultSpecialCharacters = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
+
 // Validator describes the interface providing a password Validate method
 type Validator interface {
 	Validate(str string) error
@@ -20,32 +23,23 @@ type Policies struct {
 	minUpperCaseCharacters  int
 	minDigits               int
 	minSpecialCharacters    int
-	specialCharacters       string
 	digitsRegexp            *regexp.Regexp
 	specialCharactersRegexp *regexp.Regexp
 }
 
 // NewPasswordPolicies returns a new NewPasswordPolicies instance
-func NewPasswordPolicies(minCharacters, minLowerCaseCharacters, minUpperCaseCharacters, minDigits, minSpecialCharacters int,
-	specialCharacters string) (Validator, error) {
+func NewPasswordPolicies(minCharacters, minLowerCaseCharacters, minUpperCaseCharacters, minDigits, minSpecialCharacters int) Validator {
 	p := &Policies{
 		minCharacters:          minCharacters,
 		minLowerCaseCharacters: minLowerCaseCharacters,
 		minUpperCaseCharacters: minUpperCaseCharacters,
 		minDigits:              minDigits,
 		minSpecialCharacters:   minSpecialCharacters,
-		specialCharacters:      specialCharacters,
 	}
 
 	p.digitsRegexp = regexp.MustCompile("[0-9]")
-	if len(specialCharacters) > 0 {
-		var err error
-		p.specialCharactersRegexp, err = regexp.Compile(specialCharactersExp(specialCharacters))
-		if err != nil {
-			return nil, err
-		}
-	}
-	return p, nil
+	p.specialCharactersRegexp = regexp.MustCompile(specialCharactersExp(_defaultSpecialCharacters))
+	return p
 }
 
 // Validate implements a password validation regarding the policy
@@ -110,7 +104,7 @@ func (s Policies) validateDigits(str string) error {
 
 func (s Policies) validateSpecialCharacters(str string) error {
 	if s.countSpecialCharacters(str) < s.minSpecialCharacters {
-		return fmt.Errorf("at least %d special characters are required. %s", s.minSpecialCharacters, s.specialCharacters)
+		return fmt.Errorf("at least %d special characters are required. %s", s.minSpecialCharacters, _defaultSpecialCharacters)
 	}
 	return nil
 }
diff --git a/vendor/github.com/nats-io/jwt/v2/account_claims.go b/vendor/github.com/nats-io/jwt/v2/account_claims.go
index 95240096ec7..a62d488534f 100644
--- a/vendor/github.com/nats-io/jwt/v2/account_claims.go
+++ b/vendor/github.com/nats-io/jwt/v2/account_claims.go
@@ -154,10 +154,6 @@ func (m *Mapping) Validate(vr *ValidationResults) {
 		total := uint8(0)
 		for _, wm := range wm {
 			wm.Subject.Validate(vr)
-			if wm.Subject.HasWildCards() {
-				vr.AddError("Subject %q in weighted mapping %q is not allowed to contains wildcard",
-					string(wm.Subject), ubFrom)
-			}
 			total += wm.GetWeight()
 		}
 		if total > 100 {
diff --git a/vendor/github.com/nats-io/nats-server/v2/conf/lex.go b/vendor/github.com/nats-io/nats-server/v2/conf/lex.go
index caee90782a7..b18447df864 100644
--- a/vendor/github.com/nats-io/nats-server/v2/conf/lex.go
+++ b/vendor/github.com/nats-io/nats-server/v2/conf/lex.go
@@ -78,6 +78,7 @@ const (
 	topOptTerm        = '}'
 	blockStart        = '('
 	blockEnd          = ')'
+	mapEndString      = string(mapEnd)
 )
 
 type stateFn func(lx *lexer) stateFn
diff --git a/vendor/github.com/nats-io/nats-server/v2/conf/parse.go b/vendor/github.com/nats-io/nats-server/v2/conf/parse.go
index 4d2a20adeed..c104c307f11 100644
--- a/vendor/github.com/nats-io/nats-server/v2/conf/parse.go
+++ b/vendor/github.com/nats-io/nats-server/v2/conf/parse.go
@@ -137,18 +137,22 @@ func parse(data, fp string, pedantic bool) (p *parser, err error) {
 	}
 	p.pushContext(p.mapping)
 
+	var prevItem item
 	for {
 		it := p.next()
 		if it.typ == itemEOF {
+			// Here we allow the final character to be a bracket '}'
+			// in order to support JSON like configurations.
+			if prevItem.typ == itemKey && prevItem.val != mapEndString {
+				return nil, fmt.Errorf("config is invalid (%s:%d:%d)", fp, it.line, it.pos)
+			}
 			break
 		}
+		prevItem = it
 		if err := p.processItem(it, fp); err != nil {
 			return nil, err
 		}
 	}
-	if len(p.mapping) == 0 {
-		return nil, fmt.Errorf("config has no values or is empty")
-	}
 	return p, nil
 }
 
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/client.go b/vendor/github.com/nats-io/nats-server/v2/server/client.go
index b6f73442d9b..e98c6054704 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/client.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/client.go
@@ -18,6 +18,7 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"math/rand"
@@ -5304,7 +5305,10 @@ func (c *client) doTLSHandshake(typ string, solicit bool, url *url.URL, tlsConfi
 		if solicit {
 			// Based on type of error, possibly clear the saved tlsName
 			// See: https://github.com/nats-io/nats-server/issues/1256
-			if _, ok := err.(x509.HostnameError); ok {
+			// NOTE: As of Go 1.20, the HostnameError is wrapped so cannot
+			// type assert to check directly.
+			var hostnameErr x509.HostnameError
+			if errors.As(err, &hostnameErr) {
 				if host == tlsName {
 					resetTLSName = true
 				}
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/const.go b/vendor/github.com/nats-io/nats-server/v2/server/const.go
index d5789cbacab..a76bcada546 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/const.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/const.go
@@ -41,7 +41,7 @@ var (
 
 const (
 	// VERSION is the current version for the server.
-	VERSION = "2.9.21"
+	VERSION = "2.9.22"
 
 	// PROTO is the currently supported protocol.
 	// 0 was the original
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/consumer.go b/vendor/github.com/nats-io/nats-server/v2/server/consumer.go
index b229b9fdbc1..a4dab3759d6 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/consumer.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/consumer.go
@@ -213,14 +213,14 @@ var (
 
 // Calculate accurate replicas for the consumer config with the parent stream config.
 func (consCfg ConsumerConfig) replicas(strCfg *StreamConfig) int {
-	if consCfg.Replicas == 0 {
-		if !isDurableConsumer(&consCfg) && strCfg.Retention == LimitsPolicy {
+	if consCfg.Replicas == 0 || consCfg.Replicas > strCfg.Replicas {
+		if !isDurableConsumer(&consCfg) && strCfg.Retention == LimitsPolicy && consCfg.Replicas == 0 {
+			// Matches old-school ephemerals only, where the replica count is 0.
 			return 1
 		}
 		return strCfg.Replicas
-	} else {
-		return consCfg.Replicas
 	}
+	return consCfg.Replicas
 }
 
 // Consumer is a jetstream consumer.
@@ -1091,7 +1091,7 @@ func (o *consumer) setLeader(isLeader bool) {
 		if o.dthresh > 0 && (o.isPullMode() || !o.active) {
 			// Pull consumer. We run the dtmr all the time for this one.
 			stopAndClearTimer(&o.dtmr)
-			o.dtmr = time.AfterFunc(o.dthresh, func() { o.deleteNotActive() })
+			o.dtmr = time.AfterFunc(o.dthresh, o.deleteNotActive)
 		}
 
 		// If we are not in ReplayInstant mode mark us as in replay state until resolved.
@@ -1121,7 +1121,6 @@ func (o *consumer) setLeader(isLeader bool) {
 		if pullMode {
 			// Now start up Go routine to process inbound next message requests.
 			go o.processInboundNextMsgReqs(qch)
-
 		}
 
 		// If we are R>1 spin up our proposal loop.
@@ -1140,7 +1139,10 @@ func (o *consumer) setLeader(isLeader bool) {
 			close(o.qch)
 			o.qch = nil
 		}
-		// Make sure to clear out any re delivery queues
+		// Stop any inactivity timers. Should only be running on leaders.
+		stopAndClearTimer(&o.dtmr)
+
+		// Make sure to clear out any re-deliver queues
 		stopAndClearTimer(&o.ptmr)
 		o.rdq, o.rdqi = nil, nil
 		o.pending = nil
@@ -1156,9 +1158,6 @@ func (o *consumer) setLeader(isLeader bool) {
 		// Reset waiting if we are in pull mode.
 		if o.isPullMode() {
 			o.waiting = newWaitQueue(o.cfg.MaxWaiting)
-			if !o.isDurable() {
-				stopAndClearTimer(&o.dtmr)
-			}
 			o.nextMsgReqs.drain()
 		} else if o.srv.gateway.enabled {
 			stopAndClearTimer(&o.gwdtmr)
@@ -1349,7 +1348,7 @@ func (o *consumer) updateDeliveryInterest(localInterest bool) bool {
 	// If we do not have interest anymore and have a delete threshold set, then set
 	// a timer to delete us. We wait for a bit in case of server reconnect.
 	if !interest && o.dthresh > 0 {
-		o.dtmr = time.AfterFunc(o.dthresh, func() { o.deleteNotActive() })
+		o.dtmr = time.AfterFunc(o.dthresh, o.deleteNotActive)
 		return true
 	}
 	return false
@@ -1376,7 +1375,7 @@ func (o *consumer) deleteNotActive() {
 			if o.dtmr != nil {
 				o.dtmr.Reset(o.dthresh - elapsed)
 			} else {
-				o.dtmr = time.AfterFunc(o.dthresh-elapsed, func() { o.deleteNotActive() })
+				o.dtmr = time.AfterFunc(o.dthresh-elapsed, o.deleteNotActive)
 			}
 			o.mu.Unlock()
 			return
@@ -1386,7 +1385,7 @@ func (o *consumer) deleteNotActive() {
 			if o.dtmr != nil {
 				o.dtmr.Reset(o.dthresh)
 			} else {
-				o.dtmr = time.AfterFunc(o.dthresh, func() { o.deleteNotActive() })
+				o.dtmr = time.AfterFunc(o.dthresh, o.deleteNotActive)
 			}
 			o.mu.Unlock()
 			return
@@ -1640,7 +1639,7 @@ func (o *consumer) updateConfig(cfg *ConsumerConfig) error {
 		stopAndClearTimer(&o.dtmr)
 		// Restart timer only if we are the leader.
 		if o.isLeader() && o.dthresh > 0 {
-			o.dtmr = time.AfterFunc(o.dthresh, func() { o.deleteNotActive() })
+			o.dtmr = time.AfterFunc(o.dthresh, o.deleteNotActive)
 		}
 	}
 
@@ -3031,6 +3030,22 @@ func (o *consumer) incDeliveryCount(sseq uint64) uint64 {
 	return o.rdc[sseq] + 1
 }
 
+// Used if we have to adjust on failed delivery or bad lookups.
+// Those failed attempts should not increase deliver count.
+// Lock should be held.
+func (o *consumer) decDeliveryCount(sseq uint64) {
+	if o.rdc == nil {
+		return
+	}
+	if dc, ok := o.rdc[sseq]; ok {
+		if dc == 1 {
+			delete(o.rdc, sseq)
+		} else {
+			o.rdc[sseq] -= 1
+		}
+	}
+}
+
 // send a delivery exceeded advisory.
 func (o *consumer) notifyDeliveryExceeded(sseq, dc uint64) {
 	e := JSConsumerDeliveryExceededAdvisory{
@@ -3093,7 +3108,10 @@ func (o *consumer) getNextMsg() (*jsPubMsg, uint64, error) {
 					o.notifyDeliveryExceeded(seq, dc-1)
 				}
 				// Make sure to remove from pending.
-				delete(o.pending, seq)
+				if p, ok := o.pending[seq]; ok && p != nil {
+					delete(o.pending, seq)
+					o.updateDelivered(p.Sequence, seq, dc, p.Timestamp)
+				}
 				continue
 			}
 			if seq > 0 {
@@ -3102,6 +3120,8 @@ func (o *consumer) getNextMsg() (*jsPubMsg, uint64, error) {
 				if sm == nil || err != nil {
 					pmsg.returnToPool()
 					pmsg, dc = nil, 0
+					// Adjust back deliver count.
+					o.decDeliveryCount(seq)
 				}
 				return pmsg, dc, err
 			}
@@ -3203,6 +3223,7 @@ func (o *consumer) processWaiting(eos bool) (int, int, int, time.Time) {
 				interest = true
 			}
 		}
+
 		// If interest, update batch pending requests counter and update fexp timer.
 		if interest {
 			brp += wr.n
@@ -3289,7 +3310,7 @@ func (o *consumer) checkAckFloor() {
 			}
 		}
 	} else if numPending > 0 {
-		// here it shorter to walk pending.
+		// here it is shorter to walk pending.
 		// toTerm is seq, dseq, rcd for each entry.
 		toTerm := make([]uint64, 0, numPending*3)
 		o.mu.RLock()
@@ -3506,10 +3527,16 @@ func (o *consumer) loopAndGatherMsgs(qch chan struct{}) {
 			if err == ErrStoreEOF {
 				o.checkNumPendingOnEOF()
 			}
-			if err == ErrStoreMsgNotFound || err == errDeletedMsg || err == ErrStoreEOF || err == errMaxAckPending || err == errPartialCache {
+			if err == ErrStoreMsgNotFound || err == errDeletedMsg || err == ErrStoreEOF || err == errMaxAckPending {
+				goto waitForMsgs
+			} else if err == errPartialCache {
+				s.Warnf("Unexpected partial cache error looking up message for consumer '%s > %s > %s'",
+					o.mset.acc, o.mset.cfg.Name, o.cfg.Name)
 				goto waitForMsgs
+
 			} else {
-				s.Errorf("Received an error looking up message for consumer: %v", err)
+				s.Errorf("Received an error looking up message for consumer '%s > %s > %s': %v",
+					o.mset.acc, o.mset.cfg.Name, o.cfg.Name, err)
 				goto waitForMsgs
 			}
 		}
@@ -3904,20 +3931,39 @@ func (o *consumer) trackPending(sseq, dseq uint64) {
 	}
 }
 
+// Credit back a failed delivery.
+// lock should be held.
+func (o *consumer) creditWaitingRequest(reply string) {
+	for i, rp := 0, o.waiting.rp; i < o.waiting.n; i++ {
+		if wr := o.waiting.reqs[rp]; wr != nil {
+			if wr.reply == reply {
+				wr.n++
+				wr.d--
+				return
+			}
+		}
+		rp = (rp + 1) % cap(o.waiting.reqs)
+	}
+}
+
 // didNotDeliver is called when a delivery for a consumer message failed.
 // Depending on our state, we will process the failure.
-func (o *consumer) didNotDeliver(seq uint64) {
+func (o *consumer) didNotDeliver(seq uint64, subj string) {
 	o.mu.Lock()
 	mset := o.mset
 	if mset == nil {
 		o.mu.Unlock()
 		return
 	}
+	// Adjust back deliver count.
+	o.decDeliveryCount(seq)
+
 	var checkDeliveryInterest bool
 	if o.isPushMode() {
 		o.active = false
 		checkDeliveryInterest = true
 	} else if o.pending != nil {
+		o.creditWaitingRequest(subj)
 		// pull mode and we have pending.
 		if _, ok := o.pending[seq]; ok {
 			// We found this messsage on pending, we need
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/events.go b/vendor/github.com/nats-io/nats-server/v2/server/events.go
index 1b58d912d76..2d8283e89e8 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/events.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/events.go
@@ -56,6 +56,7 @@ const (
 	connsRespSubj            = "$SYS._INBOX_.%s"
 	accConnsEventSubjNew     = "$SYS.ACCOUNT.%s.SERVER.CONNS"
 	accConnsEventSubjOld     = "$SYS.SERVER.ACCOUNT.%s.CONNS" // kept for backward compatibility
+	lameDuckEventSubj        = "$SYS.SERVER.%s.LAMEDUCK"
 	shutdownEventSubj        = "$SYS.SERVER.%s.SHUTDOWN"
 	authErrorEventSubj       = "$SYS.SERVER.%s.CLIENT.AUTH.ERR"
 	serverStatsSubj          = "$SYS.SERVER.%s.STATSZ"
@@ -533,6 +534,19 @@ RESET:
 	}
 }
 
+// Will send a shutdown message for lame-duck. Unlike sendShutdownEvent, this will
+// not close off the send queue or reply handler, as we may still have a workload
+// that needs migrating off.
+// Lock should be held.
+func (s *Server) sendLDMShutdownEventLocked() {
+	if s.sys == nil || s.sys.sendq == nil {
+		return
+	}
+	subj := fmt.Sprintf(lameDuckEventSubj, s.info.ID)
+	si := &ServerInfo{}
+	s.sys.sendq.push(newPubMsg(nil, subj, _EMPTY_, si, nil, si, noCompression, false, true))
+}
+
 // Will send a shutdown message.
 func (s *Server) sendShutdownEvent() {
 	s.mu.Lock()
@@ -944,6 +958,13 @@ func (s *Server) initEventTracking() {
 	if _, err := s.sysSubscribe(subject, s.noInlineCallback(s.remoteServerShutdown)); err != nil {
 		s.Errorf("Error setting up internal tracking: %v", err)
 	}
+	// Listen for servers entering lame-duck mode.
+	// NOTE: This currently is handled in the same way as a server shutdown, but has
+	// a different subject in case we need to handle differently in future.
+	subject = fmt.Sprintf(lameDuckEventSubj, "*")
+	if _, err := s.sysSubscribe(subject, s.noInlineCallback(s.remoteServerShutdown)); err != nil {
+		s.Errorf("Error setting up internal tracking: %v", err)
+	}
 	// Listen for account claims updates.
 	subscribeToUpdate := true
 	if s.accResolver != nil {
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/filestore.go b/vendor/github.com/nats-io/nats-server/v2/server/filestore.go
index 7b167aefb9d..34ef6ad82f5 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/filestore.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/filestore.go
@@ -292,7 +292,7 @@ func newFileStoreWithCreated(fcfg FileStoreConfig, cfg StreamConfig, created tim
 	}
 	// Default values.
 	if fcfg.BlockSize == 0 {
-		fcfg.BlockSize = dynBlkSize(cfg.Retention, cfg.MaxBytes, fcfg.Cipher)
+		fcfg.BlockSize = dynBlkSize(cfg.Retention, cfg.MaxBytes, prf != nil)
 	}
 	if fcfg.BlockSize > maxBlockSize {
 		return nil, fmt.Errorf("filestore max block size is %s", friendlyBytes(maxBlockSize))
@@ -451,7 +451,7 @@ func (fs *fileStore) UpdateConfig(cfg *StreamConfig) error {
 		fs.ageChk = nil
 	}
 
-	if cfg.MaxMsgsPer > 0 && cfg.MaxMsgsPer < old_cfg.MaxMsgsPer {
+	if fs.cfg.MaxMsgsPer > 0 && fs.cfg.MaxMsgsPer < old_cfg.MaxMsgsPer {
 		fs.enforceMsgPerSubjectLimit()
 	}
 	fs.mu.Unlock()
@@ -462,7 +462,7 @@ func (fs *fileStore) UpdateConfig(cfg *StreamConfig) error {
 	return nil
 }
 
-func dynBlkSize(retention RetentionPolicy, maxBytes int64, cipher StoreCipher) uint64 {
+func dynBlkSize(retention RetentionPolicy, maxBytes int64, encrypted bool) uint64 {
 	if maxBytes > 0 {
 		blkSize := (maxBytes / 4) + 1 // (25% overhead)
 		// Round up to nearest 100
@@ -476,7 +476,7 @@ func dynBlkSize(retention RetentionPolicy, maxBytes int64, cipher StoreCipher) u
 		} else {
 			blkSize = defaultMediumBlockSize
 		}
-		if cipher != NoCipher && blkSize > maximumEncryptedBlockSize {
+		if encrypted && blkSize > maximumEncryptedBlockSize {
 			// Notes on this below.
 			blkSize = maximumEncryptedBlockSize
 		}
@@ -484,7 +484,7 @@ func dynBlkSize(retention RetentionPolicy, maxBytes int64, cipher StoreCipher) u
 	}
 
 	switch {
-	case cipher != NoCipher:
+	case encrypted:
 		// In the case of encrypted stores, large blocks can result in worsened perf
 		// since many writes on disk involve re-encrypting the entire block. For now,
 		// we will enforce a cap on the block size when encryption is enabled to avoid
@@ -1032,7 +1032,7 @@ func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, error) {
 			fd = mb.mfd
 		} else {
 			fd, err = os.OpenFile(mb.mfn, os.O_RDWR, defaultFilePerms)
-			if err != nil {
+			if err == nil {
 				defer fd.Close()
 			}
 		}
@@ -1078,9 +1078,35 @@ func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, error) {
 			return gatherLost(lbuf - index), errBadMsg
 		}
 
+		// Check for checksum failures before additional processing.
+		data := buf[index+msgHdrSize : index+rl]
+		if hh := mb.hh; hh != nil {
+			hh.Reset()
+			hh.Write(hdr[4:20])
+			hh.Write(data[:slen])
+			if hasHeaders {
+				hh.Write(data[slen+4 : dlen-recordHashSize])
+			} else {
+				hh.Write(data[slen : dlen-recordHashSize])
+			}
+			checksum := hh.Sum(nil)
+			if !bytes.Equal(checksum, data[len(data)-recordHashSize:]) {
+				truncate(index)
+				return gatherLost(lbuf - index), errBadMsg
+			}
+			copy(mb.lchk[0:], checksum)
+		}
+
+		// Grab our sequence and timestamp.
 		seq := le.Uint64(hdr[4:])
 		ts := int64(le.Uint64(hdr[12:]))
 
+		// Check if this is a delete tombstone.
+		if seq&tbit != 0 {
+			index += rl
+			continue
+		}
+
 		// This is an old erased message, or a new one that we can track.
 		if seq == 0 || seq&ebit != 0 || seq < mb.first.seq {
 			seq = seq &^ ebit
@@ -1108,29 +1134,7 @@ func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, error) {
 			_, deleted = mb.dmap[seq]
 		}
 
-		// Always set last.
-		mb.last.seq = seq
-		mb.last.ts = ts
-
 		if !deleted {
-			data := buf[index+msgHdrSize : index+rl]
-			if hh := mb.hh; hh != nil {
-				hh.Reset()
-				hh.Write(hdr[4:20])
-				hh.Write(data[:slen])
-				if hasHeaders {
-					hh.Write(data[slen+4 : dlen-recordHashSize])
-				} else {
-					hh.Write(data[slen : dlen-recordHashSize])
-				}
-				checksum := hh.Sum(nil)
-				if !bytes.Equal(checksum, data[len(data)-recordHashSize:]) {
-					truncate(index)
-					return gatherLost(lbuf - index), errBadMsg
-				}
-				copy(mb.lchk[0:], checksum)
-			}
-
 			if firstNeedsSet {
 				firstNeedsSet, mb.first.seq, mb.first.ts = false, seq, ts
 			}
@@ -1156,6 +1160,11 @@ func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, error) {
 				mb.fssNeedsWrite = true
 			}
 		}
+
+		// Always set last
+		mb.last.seq = seq
+		mb.last.ts = ts
+
 		// Advance to next record.
 		index += rl
 	}
@@ -1315,6 +1324,11 @@ func (fs *fileStore) expireMsgsOnRecover() {
 			fs.psim = make(map[string]*psi)
 			return false
 		}
+		// Make sure we do subject cleanup as well.
+		mb.ensurePerSubjectInfoLoaded()
+		for subj := range mb.fss {
+			fs.removePerSubject(subj)
+		}
 		mb.dirtyCloseWithRemove(true)
 		deleted++
 		return true
@@ -2387,6 +2401,12 @@ func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts in
 					}
 				}
 			}
+		} else if mb := fs.selectMsgBlock(fseq); mb != nil {
+			// If we are here we could not remove fseq from above, so rebuild.
+			var ld *LostStreamData
+			if ld, _ = mb.rebuildState(); ld != nil {
+				fs.rebuildStateLocked(ld)
+			}
 		}
 	}
 
@@ -2514,7 +2534,14 @@ func (fs *fileStore) rebuildFirst() {
 
 	fmb.removeIndexFile()
 	ld, _ := fmb.rebuildState()
-	fmb.writeIndexInfo()
+	fmb.mu.RLock()
+	isEmpty := fmb.msgs == 0
+	fmb.mu.RUnlock()
+	if isEmpty {
+		fs.removeMsgBlock(fmb)
+	} else {
+		fmb.writeIndexInfo()
+	}
 	fs.selectNextFirst()
 	fs.rebuildStateLocked(ld)
 }
@@ -3036,7 +3063,8 @@ func (mb *msgBlock) compact() {
 		if !isDeleted(seq) {
 			// Normal message here.
 			nbuf = append(nbuf, buf[index:index+rl]...)
-			if !firstSet {
+			// Do not set based on tombstone.
+			if !firstSet && seq&tbit == 0 {
 				firstSet = true
 				mb.first.seq = seq
 			}
@@ -3799,20 +3827,25 @@ func (mb *msgBlock) writeMsgRecord(rl, seq uint64, subj string, mhdr, msg []byte
 	// Update write through cache.
 	// Write to msg record.
 	mb.cache.buf = append(mb.cache.buf, checksum...)
-	// Write index
-	mb.cache.idx = append(mb.cache.idx, uint32(index)|hbit)
 	mb.cache.lrl = uint32(rl)
-	if mb.cache.fseq == 0 {
-		mb.cache.fseq = seq
-	}
 
 	// Set cache timestamp for last store.
 	mb.lwts = ts
 	// Decide if we write index info if flushing in place.
 	writeIndex := ts-mb.lwits > wiThresh
 
-	// Accounting
-	mb.updateAccounting(seq, ts, rl)
+	// Only update index and do accounting if not a delete tombstone.
+	if seq&tbit == 0 {
+		// Accounting, do this before stripping ebit, it is ebit aware.
+		mb.updateAccounting(seq, ts, rl)
+		// Strip ebit if set.
+		seq = seq &^ ebit
+		if mb.cache.fseq == 0 {
+			mb.cache.fseq = seq
+		}
+		// Write index
+		mb.cache.idx = append(mb.cache.idx, uint32(index)|hbit)
+	}
 
 	fch, werr := mb.fch, mb.werr
 
@@ -3915,7 +3948,7 @@ func (mb *msgBlock) updateAccounting(seq uint64, ts int64, rl uint64) {
 		seq = seq &^ ebit
 	}
 
-	if mb.first.seq == 0 || mb.first.ts == 0 {
+	if (mb.first.seq == 0 || mb.first.ts == 0) && seq >= mb.first.seq {
 		mb.first.seq = seq
 		mb.first.ts = ts
 	}
@@ -4008,28 +4041,30 @@ func (fs *fileStore) selectMsgBlockWithIndex(seq uint64) (int, *msgBlock) {
 		return -1, nil
 	}
 
-	// Starting index, defaults to beginning.
-	si := 0
+	const linearThresh = 32
+	nb := len(fs.blks) - 1
 
-	// TODO(dlc) - Use new AVL and make this real for anything beyond certain size.
-	// Max threshold before we probe for a starting block to start our linear search.
-	const maxl = 256
-	if nb := len(fs.blks); nb > maxl {
-		d := nb / 8
-		for _, i := range []int{d, 2 * d, 3 * d, 4 * d, 5 * d, 6 * d, 7 * d} {
-			mb := fs.blks[i]
+	if nb < linearThresh {
+		for i, mb := range fs.blks {
 			if seq <= atomic.LoadUint64(&mb.last.seq) {
-				break
+				return i, mb
 			}
-			si = i
 		}
+		return -1, nil
 	}
 
-	// blks are sorted in ascending order.
-	for i := si; i < len(fs.blks); i++ {
-		mb := fs.blks[i]
-		if seq <= atomic.LoadUint64(&mb.last.seq) {
-			return i, mb
+	// Do traditional binary search here since we know the blocks are sorted by sequence first and last.
+	for low, high, mid := 0, nb, nb/2; low <= high; mid = (low + high) / 2 {
+		mb := fs.blks[mid]
+		// Right now these atomic loads do not factor in, so fine to leave. Was considering
+		// uplifting these to fs scope to avoid atomic load but not needed.
+		first, last := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
+		if seq > last {
+			low = mid + 1
+		} else if seq < first {
+			high = mid - 1
+		} else {
+			return mid, mb
 		}
 	}
 
@@ -4098,6 +4133,12 @@ func (mb *msgBlock) indexCacheBuf(buf []byte) error {
 			return errCorruptState
 		}
 
+		// Check for tombstones which we can skip in terms of indexing.
+		if seq&tbit != 0 {
+			index += rl
+			continue
+		}
+
 		// Clear erase bit.
 		seq = seq &^ ebit
 
@@ -4453,15 +4494,17 @@ var (
 	errNoMainKey     = errors.New("encrypted store encountered with no main key")
 )
 
-// Used for marking messages that have had their checksums checked.
-// Used to signal a message record with headers.
-const hbit = 1 << 31
-
-// Used for marking erased messages sequences.
-const ebit = 1 << 63
-
-// Used to mark a bad index as deleted.
-const dbit = 1 << 30
+const (
+	// Used for marking messages that have had their checksums checked.
+	// Used to signal a message record with headers.
+	hbit = 1 << 31
+	// Used for marking erased messages sequences.
+	ebit = 1 << 63
+	// Used for marking tombstone sequences.
+	tbit = 1 << 62
+	// Used to mark a bad index as deleted.
+	dbit = 1 << 30
+)
 
 // Will do a lookup from cache.
 // Lock should be held.
@@ -4606,7 +4649,7 @@ func (mb *msgBlock) msgFromBuf(buf []byte, sm *StoreMsg, hh hash.Hash64) (*Store
 	dlen := int(rl) - msgHdrSize
 	slen := int(le.Uint16(hdr[20:]))
 	// Simple sanity check.
-	if dlen < 0 || slen > dlen || int(rl) > len(buf) {
+	if dlen < 0 || slen > (dlen-recordHashSize) || dlen > int(rl) || int(rl) > len(buf) {
 		return nil, errBadMsg
 	}
 	data := buf[msgHdrSize : msgHdrSize+dlen]
@@ -4799,20 +4842,17 @@ func (fs *fileStore) LoadNextMsg(filter string, wc bool, start uint64, sm *Store
 		start = fs.state.FirstSeq
 	}
 
-	// TODO(dlc) - If num blocks gets large maybe use selectMsgBlock but have it return index b/c
-	// we need to keep walking if no match found in first mb.
-	for _, mb := range fs.blks {
-		// Skip blocks that are less than our starting sequence.
-		if start > atomic.LoadUint64(&mb.last.seq) {
-			continue
-		}
-		if sm, expireOk, err := mb.firstMatching(filter, wc, start, sm); err == nil {
-			if expireOk && mb != fs.lmb {
-				mb.tryForceExpireCache()
+	if bi, _ := fs.selectMsgBlockWithIndex(start); bi >= 0 {
+		for i := bi; i < len(fs.blks); i++ {
+			mb := fs.blks[i]
+			if sm, expireOk, err := mb.firstMatching(filter, wc, start, sm); err == nil {
+				if expireOk && mb != fs.lmb {
+					mb.tryForceExpireCache()
+				}
+				return sm, sm.seq, nil
+			} else if err != ErrStoreMsgNotFound {
+				return nil, 0, err
 			}
-			return sm, sm.seq, nil
-		} else if err != ErrStoreMsgNotFound {
-			return nil, 0, err
 		}
 	}
 
@@ -5205,16 +5245,7 @@ func (fs *fileStore) PurgeEx(subject string, sequence, keep uint64) (purged uint
 		}
 		if sequence > 1 {
 			return fs.Compact(sequence)
-		} else if keep > 0 {
-			fs.mu.RLock()
-			msgs, lseq := fs.state.Msgs, fs.state.LastSeq
-			fs.mu.RUnlock()
-			if keep >= msgs {
-				return 0, nil
-			}
-			return fs.Compact(lseq - keep + 1)
 		}
-		return 0, nil
 	}
 
 	eq, wc := compareFn(subject), subjectHasWildcard(subject)
@@ -5950,7 +5981,7 @@ func (mb *msgBlock) recalculateFirstForSubj(subj string, startSeq uint64, ss *Si
 		slen := int(le.Uint16(hdr[20:]))
 		if subj == string(buf[msgHdrSize:msgHdrSize+slen]) {
 			seq := le.Uint64(hdr[4:])
-			if seq&ebit != 0 {
+			if seq < mb.first.seq || seq&ebit != 0 {
 				continue
 			}
 			if len(mb.dmap) > 0 {
@@ -6946,20 +6977,28 @@ func (o *consumerFileStore) UpdateDelivered(dseq, sseq, dc uint64, ts int64) err
 		}
 
 		if dc > 1 {
+			if maxdc := uint64(o.cfg.MaxDeliver); maxdc > 0 && dc > maxdc {
+				// Make sure to remove from pending.
+				delete(o.state.Pending, sseq)
+			}
 			if o.state.Redelivered == nil {
 				o.state.Redelivered = make(map[uint64]uint64)
 			}
 			// Only update if greater then what we already have.
-			if o.state.Redelivered[sseq] < dc {
+			if o.state.Redelivered[sseq] < dc-1 {
 				o.state.Redelivered[sseq] = dc - 1
 			}
 		}
 	} else {
 		// For AckNone just update delivered and ackfloor at the same time.
-		o.state.Delivered.Consumer = dseq
-		o.state.Delivered.Stream = sseq
-		o.state.AckFloor.Consumer = dseq
-		o.state.AckFloor.Stream = sseq
+		if dseq > o.state.Delivered.Consumer {
+			o.state.Delivered.Consumer = dseq
+			o.state.AckFloor.Consumer = dseq
+		}
+		if sseq > o.state.Delivered.Stream {
+			o.state.Delivered.Stream = sseq
+			o.state.AckFloor.Stream = sseq
+		}
 	}
 	// Make sure we flush to disk.
 	o.kickFlusher()
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/jetstream.go b/vendor/github.com/nats-io/nats-server/v2/server/jetstream.go
index d4f642ed3d6..de79b9d4eae 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/jetstream.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/jetstream.go
@@ -148,6 +148,9 @@ type jsAccount struct {
 	// From server
 	sendq *ipQueue[*pubMsg]
 
+	// For limiting only running one checkAndSync at a time.
+	sync atomic.Bool
+
 	// Usage/limits related fields that will be protected by usageMu
 	usageMu    sync.RWMutex
 	limits     map[string]JetStreamAccountLimits // indexed by tierName
@@ -1811,6 +1814,12 @@ func (jsa *jsAccount) remoteUpdateUsage(sub *subscription, c *client, _ *Account
 // When we detect a skew of some sort this will verify the usage reporting is correct.
 // No locks should be held.
 func (jsa *jsAccount) checkAndSyncUsage(tierName string, storeType StorageType) {
+	// This will run in a separate go routine, so check that we are only running once.
+	if !jsa.sync.CompareAndSwap(false, true) {
+		return
+	}
+	defer jsa.sync.Store(false)
+
 	// Hold the account read lock and the usage lock while we calculate.
 	// We scope by tier and storage type, but if R3 File has 200 streams etc. could
 	// show a pause. I did test with > 100 non-active streams and was 80-200ns or so.
@@ -1916,7 +1925,10 @@ func (jsa *jsAccount) updateUsage(tierName string, storeType StorageType, delta
 	jsa.usageMu.Unlock()
 
 	if needsCheck {
-		jsa.checkAndSyncUsage(tierName, storeType)
+		// We could be holding the stream lock from up in the stack, and this
+		// will want the jsa lock, which would violate locking order.
+		// So do this in a Go routine. The function will check if it is already running.
+		go jsa.checkAndSyncUsage(tierName, storeType)
 	}
 }
 
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_cluster.go b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_cluster.go
index 6af4fa42ba4..52ccf5bddf1 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_cluster.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_cluster.go
@@ -2161,21 +2161,11 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment, sendSnaps
 
 	startMigrationMonitoring := func() {
 		if mmt == nil {
-			mmt = time.NewTicker(10 * time.Millisecond)
+			mmt = time.NewTicker(500 * time.Millisecond)
 			mmtc = mmt.C
 		}
 	}
 
-	adjustMigrationMonitoring := func() {
-		const delay = 500 * time.Millisecond
-		if mmt == nil {
-			mmt = time.NewTicker(delay)
-			mmtc = mmt.C
-		} else {
-			mmt.Reset(delay)
-		}
-	}
-
 	stopMigrationMonitoring := func() {
 		if mmt != nil {
 			mmt.Stop()
@@ -2407,9 +2397,6 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment, sendSnaps
 				continue
 			}
 
-			// Adjust to our normal time delay.
-			adjustMigrationMonitoring()
-
 			// Make sure we have correct cluster information on the other peers.
 			ci := js.clusterInfo(rg)
 			mset.checkClusterInfo(ci)
@@ -4001,7 +3988,7 @@ func (js *jetStream) processClusterCreateConsumer(ca *consumerAssignment, state
 	var didCreate, isConfigUpdate, needsLocalResponse bool
 	if o == nil {
 		// Add in the consumer if needed.
-		if o, err = mset.addConsumerWithAssignment(ca.Config, ca.Name, ca, false); err == nil {
+		if o, err = mset.addConsumerWithAssignment(ca.Config, ca.Name, ca, wasExisting); err == nil {
 			didCreate = true
 		}
 	} else {
@@ -5197,7 +5184,12 @@ func (cc *jetStreamCluster) remapStreamAssignment(sa *streamAssignment, removePe
 		return true
 	}
 
-	// If we are here let's remove the peer at least.
+	// If R1 just return to avoid bricking the stream.
+	if sa.Group.node == nil || len(sa.Group.Peers) == 1 {
+		return false
+	}
+
+	// If we are here let's remove the peer at least, as long as we are R>1
 	for i, peer := range sa.Group.Peers {
 		if peer == removePeer {
 			sa.Group.Peers[i] = sa.Group.Peers[len(sa.Group.Peers)-1]
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/leafnode.go b/vendor/github.com/nats-io/nats-server/v2/server/leafnode.go
index 39170e3c171..9b78676a79a 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/leafnode.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/leafnode.go
@@ -1643,6 +1643,10 @@ func (s *Server) initLeafNodeSmapAndSendSubs(c *client) {
 	subs := _subs[:0]
 	ims := []string{}
 
+	// Hold the client lock otherwise there can be a race and miss some subs.
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
 	acc.mu.RLock()
 	accName := acc.Name
 	accNTag := acc.nameTag
@@ -1718,7 +1722,6 @@ func (s *Server) initLeafNodeSmapAndSendSubs(c *client) {
 	}
 
 	// Now walk the results and add them to our smap
-	c.mu.Lock()
 	rc := c.leaf.remoteCluster
 	c.leaf.smap = make(map[string]int32)
 	for _, sub := range subs {
@@ -1784,7 +1787,6 @@ func (s *Server) initLeafNodeSmapAndSendSubs(c *client) {
 			c.mu.Unlock()
 		})
 	}
-	c.mu.Unlock()
 }
 
 // updateInterestForAccountOnGateway called from gateway code when processing RS+ and RS-.
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/memstore.go b/vendor/github.com/nats-io/nats-server/v2/server/memstore.go
index b35e3d294de..b2ed14aac85 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/memstore.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/memstore.go
@@ -1307,17 +1307,28 @@ func (o *consumerMemStore) UpdateDelivered(dseq, sseq, dc uint64, ts int64) erro
 		}
 
 		if dc > 1 {
+			if maxdc := uint64(o.cfg.MaxDeliver); maxdc > 0 && dc > maxdc {
+				// Make sure to remove from pending.
+				delete(o.state.Pending, sseq)
+			}
 			if o.state.Redelivered == nil {
 				o.state.Redelivered = make(map[uint64]uint64)
 			}
-			o.state.Redelivered[sseq] = dc - 1
+			// Only update if greater then what we already have.
+			if o.state.Redelivered[sseq] < dc-1 {
+				o.state.Redelivered[sseq] = dc - 1
+			}
 		}
 	} else {
 		// For AckNone just update delivered and ackfloor at the same time.
-		o.state.Delivered.Consumer = dseq
-		o.state.Delivered.Stream = sseq
-		o.state.AckFloor.Consumer = dseq
-		o.state.AckFloor.Stream = sseq
+		if dseq > o.state.Delivered.Consumer {
+			o.state.Delivered.Consumer = dseq
+			o.state.AckFloor.Consumer = dseq
+		}
+		if sseq > o.state.Delivered.Stream {
+			o.state.Delivered.Stream = sseq
+			o.state.AckFloor.Stream = sseq
+		}
 	}
 
 	return nil
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/monitor.go b/vendor/github.com/nats-io/nats-server/v2/server/monitor.go
index b7b26b48c52..d279abb77b3 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/monitor.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/monitor.go
@@ -492,7 +492,7 @@ func (s *Server) Connz(opts *ConnzOptions) (*Connz, error) {
 	case ByLast:
 		sort.Sort(sort.Reverse(byLast{pconns}))
 	case ByIdle:
-		sort.Sort(sort.Reverse(byIdle{pconns}))
+		sort.Sort(sort.Reverse(byIdle{pconns, c.Now}))
 	case ByUptime:
 		sort.Sort(byUptime{pconns, time.Now()})
 	case ByStop:
@@ -1120,14 +1120,17 @@ func (s *Server) HandleIPQueuesz(w http.ResponseWriter, r *http.Request) {
 
 	queues := map[string]monitorIPQueue{}
 
-	s.ipQueues.Range(func(k, v interface{}) bool {
+	s.ipQueues.Range(func(k, v any) bool {
+		var pending, inProgress int
 		name := k.(string)
-		queue := v.(interface {
+		queue, ok := v.(interface {
 			len() int
-			inProgress() uint64
+			inProgress() int64
 		})
-		pending := queue.len()
-		inProgress := int(queue.inProgress())
+		if ok {
+			pending = queue.len()
+			inProgress = int(queue.inProgress())
+		}
 		if !all && (pending == 0 && inProgress == 0) {
 			return true
 		} else if qfilter != _EMPTY_ && !strings.Contains(name, qfilter) {
@@ -2301,18 +2304,26 @@ func (s *Server) HandleAccountStatz(w http.ResponseWriter, r *http.Request) {
 	ResponseHandler(w, r, b)
 }
 
-// ResponseHandler handles responses for monitoring routes
+// ResponseHandler handles responses for monitoring routes.
 func ResponseHandler(w http.ResponseWriter, r *http.Request, data []byte) {
+	handleResponse(http.StatusOK, w, r, data)
+}
+
+// handleResponse handles responses for monitoring routes with a specific HTTP status code.
+func handleResponse(code int, w http.ResponseWriter, r *http.Request, data []byte) {
 	// Get callback from request
 	callback := r.URL.Query().Get("callback")
 	// If callback is not empty then
 	if callback != "" {
 		// Response for JSONP
 		w.Header().Set("Content-Type", "application/javascript")
+		w.WriteHeader(code)
 		fmt.Fprintf(w, "%s(%s)", callback, data)
 	} else {
 		// Otherwise JSON
 		w.Header().Set("Content-Type", "application/json")
+		w.Header().Set("Access-Control-Allow-Origin", "*")
+		w.WriteHeader(code)
 		w.Write(data)
 	}
 }
@@ -3047,7 +3058,7 @@ type HealthStatus struct {
 	Error  string `json:"error,omitempty"`
 }
 
-// https://tools.ietf.org/id/draft-inadarei-api-health-check-05.html
+// https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check
 func (s *Server) HandleHealthz(w http.ResponseWriter, r *http.Request) {
 	s.mu.Lock()
 	s.httpReqStats[HealthzPath]++
@@ -3074,16 +3085,19 @@ func (s *Server) HandleHealthz(w http.ResponseWriter, r *http.Request) {
 		JSEnabledOnly: jsEnabledOnly,
 		JSServerOnly:  jsServerOnly,
 	})
+
+	code := http.StatusOK
+
 	if hs.Error != _EMPTY_ {
 		s.Warnf("Healthcheck failed: %q", hs.Error)
-		w.WriteHeader(http.StatusServiceUnavailable)
+		code = http.StatusServiceUnavailable
 	}
 	b, err := json.Marshal(hs)
 	if err != nil {
 		s.Errorf("Error marshaling response to /healthz request: %v", err)
 	}
 
-	ResponseHandler(w, r, b)
+	handleResponse(code, w, r, b)
 }
 
 // Generate health status.
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/monitor_sort_opts.go b/vendor/github.com/nats-io/nats-server/v2/server/monitor_sort_opts.go
index 58cc3900bb0..2fcaf2e9f3f 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/monitor_sort_opts.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/monitor_sort_opts.go
@@ -92,12 +92,13 @@ func (l byLast) Less(i, j int) bool {
 }
 
 // Idle time
-type byIdle struct{ ConnInfos }
+type byIdle struct {
+	ConnInfos
+	now time.Time
+}
 
 func (l byIdle) Less(i, j int) bool {
-	ii := l.ConnInfos[i].LastActivity.Sub(l.ConnInfos[i].Start)
-	ij := l.ConnInfos[j].LastActivity.Sub(l.ConnInfos[j].Start)
-	return ii < ij
+	return l.now.Sub(l.ConnInfos[i].LastActivity) < l.now.Sub(l.ConnInfos[j].LastActivity)
 }
 
 // Uptime
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/ocsp_peer.go b/vendor/github.com/nats-io/nats-server/v2/server/ocsp_peer.go
index 0ddcc0c80fb..0e424a4a9cd 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/ocsp_peer.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/ocsp_peer.go
@@ -139,13 +139,13 @@ func (s *Server) plugTLSOCSPPeer(config *tlsConfigKind) (*tls.Config, bool, erro
 	if config == nil || config.tlsConfig == nil {
 		return nil, false, errors.New(certidp.ErrUnableToPlugTLSEmptyConfig)
 	}
-	s.Debugf(certidp.DbgPlugTLSForKind, config.kind)
 	kind := config.kind
 	isSpoke := config.isLeafSpoke
 	tcOpts := config.tlsOpts
 	if tcOpts == nil || tcOpts.OCSPPeerConfig == nil || !tcOpts.OCSPPeerConfig.Verify {
 		return nil, false, nil
 	}
+	s.Debugf(certidp.DbgPlugTLSForKind, config.kind)
 	// peer is a tls client
 	if kind == kindStringMap[CLIENT] || (kind == kindStringMap[LEAF] && !isSpoke) {
 		if !tcOpts.Verify {
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/opts.go b/vendor/github.com/nats-io/nats-server/v2/server/opts.go
index a2b231aa1f2..1cb531dae02 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/opts.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/opts.go
@@ -744,6 +744,9 @@ func (o *Options) ProcessConfigFile(configFile string) error {
 	// Collect all errors and warnings and report them all together.
 	errors := make([]error, 0)
 	warnings := make([]error, 0)
+	if len(m) == 0 {
+		warnings = append(warnings, fmt.Errorf("%s: config has no values or is empty", configFile))
+	}
 
 	// First check whether a system account has been defined,
 	// as that is a condition for other features to be enabled.
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/raft.go b/vendor/github.com/nats-io/nats-server/v2/server/raft.go
index e625c47b2ba..bee20a1fcd8 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/raft.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/raft.go
@@ -1814,6 +1814,7 @@ func (n *raft) runAsFollower() {
 				if n.catchupStalled() {
 					n.cancelCatchup()
 				}
+				n.resetElectionTimeout()
 				n.Unlock()
 			} else {
 				n.switchToCandidate()
@@ -2508,6 +2509,10 @@ func (n *raft) catchupFollower(ar *appendEntryResponse) {
 	}
 	if err != nil || ae == nil {
 		n.warn("Could not find a starting entry for catchup request: %v", err)
+		// If we are here we are seeing a request for an item we do not have, meaning we should stepdown.
+		// This is possible on a reset of our WAL but the other side has a snapshot already.
+		// If we do not stepdown this can cycle.
+		n.stepdown.push(noLeader)
 		n.Unlock()
 		arPool.Put(ar)
 		return
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/server.go b/vendor/github.com/nats-io/nats-server/v2/server/server.go
index 11e715b9b2f..3ecb4e87509 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/server.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/server.go
@@ -3557,6 +3557,7 @@ func (s *Server) lameDuckMode() {
 	}
 	s.Noticef("Entering lame duck mode, stop accepting new clients")
 	s.ldm = true
+	s.sendLDMShutdownEventLocked()
 	expected := 1
 	s.listener.Close()
 	s.listener = nil
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/stream.go b/vendor/github.com/nats-io/nats-server/v2/server/stream.go
index d03306d7429..fb792845e89 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/stream.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/stream.go
@@ -2983,7 +2983,7 @@ func streamAndSeq(shdr string) (string, uint64) {
 	}
 	// New version which is stream index name <SPC> sequence
 	fields := strings.Fields(shdr)
-	if len(fields) != 2 {
+	if len(fields) < 2 {
 		return _EMPTY_, 0
 	}
 	return fields[0], uint64(parseAckReplyNum(fields[1]))
@@ -4455,7 +4455,7 @@ func (mset *stream) internalLoop() {
 				// Check to see if this is a delivery for a consumer and
 				// we failed to deliver the message. If so alert the consumer.
 				if pm.o != nil && pm.seq > 0 && !didDeliver {
-					pm.o.didNotDeliver(pm.seq)
+					pm.o.didNotDeliver(pm.seq, pm.dsubj)
 				}
 				pm.returnToPool()
 			}
diff --git a/vendor/golang.org/x/oauth2/internal/token.go b/vendor/golang.org/x/oauth2/internal/token.go
index 58901bda53e..e83ddeef0fc 100644
--- a/vendor/golang.org/x/oauth2/internal/token.go
+++ b/vendor/golang.org/x/oauth2/internal/token.go
@@ -18,6 +18,7 @@ import (
 	"strconv"
 	"strings"
 	"sync"
+	"sync/atomic"
 	"time"
 )
 
@@ -115,41 +116,60 @@ const (
 	AuthStyleInHeader AuthStyle = 2
 )
 
-// authStyleCache is the set of tokenURLs we've successfully used via
+// LazyAuthStyleCache is a backwards compatibility compromise to let Configs
+// have a lazily-initialized AuthStyleCache.
+//
+// The two users of this, oauth2.Config and oauth2/clientcredentials.Config,
+// both would ideally just embed an unexported AuthStyleCache but because both
+// were historically allowed to be copied by value we can't retroactively add an
+// uncopyable Mutex to them.
+//
+// We could use an atomic.Pointer, but that was added recently enough (in Go
+// 1.18) that we'd break Go 1.17 users where the tests as of 2023-08-03
+// still pass. By using an atomic.Value, it supports both Go 1.17 and
+// copying by value, even if that's not ideal.
+type LazyAuthStyleCache struct {
+	v atomic.Value // of *AuthStyleCache
+}
+
+func (lc *LazyAuthStyleCache) Get() *AuthStyleCache {
+	if c, ok := lc.v.Load().(*AuthStyleCache); ok {
+		return c
+	}
+	c := new(AuthStyleCache)
+	if !lc.v.CompareAndSwap(nil, c) {
+		c = lc.v.Load().(*AuthStyleCache)
+	}
+	return c
+}
+
+// AuthStyleCache is the set of tokenURLs we've successfully used via
 // RetrieveToken and which style auth we ended up using.
 // It's called a cache, but it doesn't (yet?) shrink. It's expected that
 // the set of OAuth2 servers a program contacts over time is fixed and
 // small.
-var authStyleCache struct {
-	sync.Mutex
-	m map[string]AuthStyle // keyed by tokenURL
-}
-
-// ResetAuthCache resets the global authentication style cache used
-// for AuthStyleUnknown token requests.
-func ResetAuthCache() {
-	authStyleCache.Lock()
-	defer authStyleCache.Unlock()
-	authStyleCache.m = nil
+type AuthStyleCache struct {
+	mu sync.Mutex
+	m  map[string]AuthStyle // keyed by tokenURL
 }
 
 // lookupAuthStyle reports which auth style we last used with tokenURL
 // when calling RetrieveToken and whether we have ever done so.
-func lookupAuthStyle(tokenURL string) (style AuthStyle, ok bool) {
-	authStyleCache.Lock()
-	defer authStyleCache.Unlock()
-	style, ok = authStyleCache.m[tokenURL]
+func (c *AuthStyleCache) lookupAuthStyle(tokenURL string) (style AuthStyle, ok bool) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	style, ok = c.m[tokenURL]
 	return
 }
 
 // setAuthStyle adds an entry to authStyleCache, documented above.
-func setAuthStyle(tokenURL string, v AuthStyle) {
-	authStyleCache.Lock()
-	defer authStyleCache.Unlock()
-	if authStyleCache.m == nil {
-		authStyleCache.m = make(map[string]AuthStyle)
+func (c *AuthStyleCache) setAuthStyle(tokenURL string, v AuthStyle) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	if c.m == nil {
+		c.m = make(map[string]AuthStyle)
 	}
-	authStyleCache.m[tokenURL] = v
+	c.m[tokenURL] = v
 }
 
 // newTokenRequest returns a new *http.Request to retrieve a new token
@@ -189,10 +209,10 @@ func cloneURLValues(v url.Values) url.Values {
 	return v2
 }
 
-func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values, authStyle AuthStyle) (*Token, error) {
+func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values, authStyle AuthStyle, styleCache *AuthStyleCache) (*Token, error) {
 	needsAuthStyleProbe := authStyle == 0
 	if needsAuthStyleProbe {
-		if style, ok := lookupAuthStyle(tokenURL); ok {
+		if style, ok := styleCache.lookupAuthStyle(tokenURL); ok {
 			authStyle = style
 			needsAuthStyleProbe = false
 		} else {
@@ -222,7 +242,7 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string,
 		token, err = doTokenRoundTrip(ctx, req)
 	}
 	if needsAuthStyleProbe && err == nil {
-		setAuthStyle(tokenURL, authStyle)
+		styleCache.setAuthStyle(tokenURL, authStyle)
 	}
 	// Don't overwrite `RefreshToken` with an empty value
 	// if this was a token refreshing request.
diff --git a/vendor/golang.org/x/oauth2/oauth2.go b/vendor/golang.org/x/oauth2/oauth2.go
index 9085fabe34e..cc7c98c25dd 100644
--- a/vendor/golang.org/x/oauth2/oauth2.go
+++ b/vendor/golang.org/x/oauth2/oauth2.go
@@ -58,6 +58,10 @@ type Config struct {
 
 	// Scope specifies optional requested permissions.
 	Scopes []string
+
+	// authStyleCache caches which auth style to use when Endpoint.AuthStyle is
+	// the zero value (AuthStyleAutoDetect).
+	authStyleCache internal.LazyAuthStyleCache
 }
 
 // A TokenSource is anything that can return a token.
diff --git a/vendor/golang.org/x/oauth2/token.go b/vendor/golang.org/x/oauth2/token.go
index 5ffce9764be..5bbb3321748 100644
--- a/vendor/golang.org/x/oauth2/token.go
+++ b/vendor/golang.org/x/oauth2/token.go
@@ -164,7 +164,7 @@ func tokenFromInternal(t *internal.Token) *Token {
 // This token is then mapped from *internal.Token into an *oauth2.Token which is returned along
 // with an error..
 func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) {
-	tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v, internal.AuthStyle(c.Endpoint.AuthStyle))
+	tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v, internal.AuthStyle(c.Endpoint.AuthStyle), c.authStyleCache.Get())
 	if err != nil {
 		if rErr, ok := err.(*internal.RetrieveError); ok {
 			return nil, (*RetrieveError)(rErr)
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 9c67374e466..8bab8bff843 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -356,7 +356,7 @@ github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1
 github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1
 github.com/cs3org/go-cs3apis/cs3/tx/v1beta1
 github.com/cs3org/go-cs3apis/cs3/types/v1beta1
-# github.com/cs3org/reva/v2 v2.16.1-0.20230906142214-864d9012e37f
+# github.com/cs3org/reva/v2 v2.16.1-0.20230908122008-491b5c5bf72a
 ## explicit; go 1.20
 github.com/cs3org/reva/v2/cmd/revad/internal/grace
 github.com/cs3org/reva/v2/cmd/revad/runtime
@@ -1333,10 +1333,10 @@ github.com/mohae/deepcopy
 # github.com/mschoch/smat v0.2.0
 ## explicit; go 1.13
 github.com/mschoch/smat
-# github.com/nats-io/jwt/v2 v2.4.1
+# github.com/nats-io/jwt/v2 v2.5.0
 ## explicit; go 1.18
 github.com/nats-io/jwt/v2
-# github.com/nats-io/nats-server/v2 v2.9.21
+# github.com/nats-io/nats-server/v2 v2.9.22
 ## explicit; go 1.19
 github.com/nats-io/nats-server/v2/conf
 github.com/nats-io/nats-server/v2/internal/ldap
@@ -1982,7 +1982,7 @@ golang.org/x/net/netutil
 golang.org/x/net/proxy
 golang.org/x/net/publicsuffix
 golang.org/x/net/trace
-# golang.org/x/oauth2 v0.11.0
+# golang.org/x/oauth2 v0.12.0
 ## explicit; go 1.18
 golang.org/x/oauth2
 golang.org/x/oauth2/internal


Test patch:
diff --git a/services/graph/pkg/service/v0/driveitems_test.go b/services/graph/pkg/service/v0/driveitems_test.go
index 6e6b5ccd0a2..8b307d4330f 100644
--- a/services/graph/pkg/service/v0/driveitems_test.go
+++ b/services/graph/pkg/service/v0/driveitems_test.go
@@ -3,17 +3,21 @@ package svc_test
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"time"
 
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
+	userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
 	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
+	revactx "github.com/cs3org/reva/v2/pkg/ctx"
 	"github.com/cs3org/reva/v2/pkg/rgrpc/status"
 	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
 	"github.com/cs3org/reva/v2/pkg/utils"
 	cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
+	"github.com/go-chi/chi/v5"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	libregraph "github.com/owncloud/libre-graph-api-go"
@@ -44,6 +48,12 @@ var _ = Describe("Driveitems", func() {
 		rr *httptest.ResponseRecorder
 
 		newGroup *libregraph.Group
+
+		currentUser = &userpb.User{
+			Id: &userpb.UserId{
+				OpaqueId: "user",
+			},
+		}
 	)
 
 	BeforeEach(func() {
@@ -84,31 +94,78 @@ var _ = Describe("Driveitems", func() {
 	})
 
 	Describe("GetRootDriveChildren", func() {
-		It("handles failing GetHome", func() {
-			gatewayClient.On("GetHome", mock.Anything, mock.Anything).Return(&provider.GetHomeResponse{
+		It("handles ListStorageSpaces not found ", func() {
+			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
 				Status: status.NewNotFound(ctx, "not found"),
 			}, nil)
 
 			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drive/root/children", nil)
+			r = r.WithContext(revactx.ContextSetUser(ctx, currentUser))
 			svc.GetRootDriveChildren(rr, r)
 			Expect(rr.Code).To(Equal(http.StatusNotFound))
 		})
 
-		It("handles failing GetHome", func() {
-			gatewayClient.On("GetHome", mock.Anything, mock.Anything).Return(&provider.GetHomeResponse{
-				Status: status.NewInternal(ctx, "not found"),
+		It("handles ListStorageSpaces error", func() {
+			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
+				Status: status.NewInternal(ctx, "internal error"),
 			}, nil)
 
 			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drive/root/children", nil)
+			r = r.WithContext(revactx.ContextSetUser(ctx, currentUser))
+			svc.GetRootDriveChildren(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+		})
+
+		It("handles ListContainer not found", func() {
+			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
+				Status:        status.NewOK(ctx),
+				StorageSpaces: []*provider.StorageSpace{{Owner: currentUser, Root: &provider.ResourceId{}}},
+			}, nil)
+			gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
+				Status: status.NewNotFound(ctx, "not found"),
+			}, nil)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drive/root/children", nil)
+			r = r.WithContext(revactx.ContextSetUser(ctx, currentUser))
+			svc.GetRootDriveChildren(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusNotFound))
+		})
+
+		It("handles ListContainer permission denied", func() {
+			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
+				Status:        status.NewOK(ctx),
+				StorageSpaces: []*provider.StorageSpace{{Owner: currentUser, Root: &provider.ResourceId{}}},
+			}, nil)
+			gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
+				Status: status.NewPermissionDenied(ctx, errors.New("denied"), "denied"),
+			}, nil)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drive/root/children", nil)
+			r = r.WithContext(revactx.ContextSetUser(ctx, currentUser))
+			svc.GetRootDriveChildren(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusForbidden))
+		})
+
+		It("handles ListContainer error", func() {
+			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
+				Status:        status.NewOK(ctx),
+				StorageSpaces: []*provider.StorageSpace{{Owner: currentUser, Root: &provider.ResourceId{}}},
+			}, nil)
+			gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
+				Status: status.NewInternal(ctx, "internal"),
+			}, nil)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drive/root/children", nil)
+			r = r.WithContext(revactx.ContextSetUser(ctx, currentUser))
 			svc.GetRootDriveChildren(rr, r)
 			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
 		})
 
 		It("succeeds", func() {
 			mtime := time.Now()
-			gatewayClient.On("GetHome", mock.Anything, mock.Anything).Return(&provider.GetHomeResponse{
-				Status: status.NewOK(ctx),
-				Path:   "/",
+			gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
+				Status:        status.NewOK(ctx),
+				StorageSpaces: []*provider.StorageSpace{{Owner: currentUser, Root: &provider.ResourceId{}}},
 			}, nil)
 			gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
 				Status: status.NewOK(ctx),
@@ -122,6 +179,7 @@ var _ = Describe("Driveitems", func() {
 				},
 			}, nil)
 			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drive/root/children", nil)
+			r = r.WithContext(revactx.ContextSetUser(ctx, currentUser))
 			svc.GetRootDriveChildren(rr, r)
 			Expect(rr.Code).To(Equal(http.StatusOK))
 			data, err := io.ReadAll(rr.Body)
@@ -138,4 +196,82 @@ var _ = Describe("Driveitems", func() {
 			Expect(res.Value[0].GetId()).To(Equal("storageid$spaceid!opaqueid"))
 		})
 	})
+
+	Describe("GetDriveItemChildren", func() {
+		It("handles ListContainer not found", func() {
+			gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
+				Status: status.NewNotFound(ctx, "not found"),
+			}, nil)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives/storageid$spaceid/items/storageid$spaceid!nodeid/children", nil)
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("driveID", "storageid$spaceid")
+			rctx.URLParams.Add("driveItemID", "storageid$spaceid!nodeid")
+			r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.GetDriveItemChildren(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusNotFound))
+		})
+
+		It("handles ListContainer permission denied as not found", func() {
+			gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
+				Status: status.NewPermissionDenied(ctx, errors.New("denied"), "denied"),
+			}, nil)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives/storageid$spaceid/items/storageid$spaceid!nodeid/children", nil)
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("driveID", "storageid$spaceid")
+			rctx.URLParams.Add("driveItemID", "storageid$spaceid!nodeid")
+			r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.GetDriveItemChildren(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusNotFound))
+		})
+
+		It("handles ListContainer error", func() {
+			gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
+				Status: status.NewInternal(ctx, "internal"),
+			}, nil)
+
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives/storageid$spaceid/items/storageid$spaceid!nodeid/children", nil)
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("driveID", "storageid$spaceid")
+			rctx.URLParams.Add("driveItemID", "storageid$spaceid!nodeid")
+			r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.GetDriveItemChildren(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+		})
+
+		It("succeeds", func() {
+			mtime := time.Now()
+			gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
+				Status: status.NewOK(ctx),
+				Infos: []*provider.ResourceInfo{
+					{
+						Type:  provider.ResourceType_RESOURCE_TYPE_FILE,
+						Id:    &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"},
+						Etag:  "etag",
+						Mtime: utils.TimeToTS(mtime),
+					},
+				},
+			}, nil)
+			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives/storageid$spaceid/items/storageid$spaceid!nodeid/children", nil)
+			rctx := chi.NewRouteContext()
+			rctx.URLParams.Add("driveID", "storageid$spaceid")
+			rctx.URLParams.Add("driveItemID", "storageid$spaceid!nodeid")
+			r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
+			svc.GetDriveItemChildren(rr, r)
+			Expect(rr.Code).To(Equal(http.StatusOK))
+			data, err := io.ReadAll(rr.Body)
+			Expect(err).ToNot(HaveOccurred())
+
+			res := itemsList{}
+
+			err = json.Unmarshal(data, &res)
+			Expect(err).ToNot(HaveOccurred())
+
+			Expect(len(res.Value)).To(Equal(1))
+			Expect(res.Value[0].GetLastModifiedDateTime().Equal(mtime)).To(BeTrue())
+			Expect(res.Value[0].GetETag()).To(Equal("etag"))
+			Expect(res.Value[0].GetId()).To(Equal("storageid$spaceid!opaqueid"))
+		})
+	})
 })
diff --git a/services/graph/pkg/service/v0/educationclasses_test.go b/services/graph/pkg/service/v0/educationclasses_test.go
index 58681312774..24b7d5f0866 100644
--- a/services/graph/pkg/service/v0/educationclasses_test.go
+++ b/services/graph/pkg/service/v0/educationclasses_test.go
@@ -134,7 +134,7 @@ var _ = Describe("EducationClass", func() {
 			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/classes", nil)
 			svc.GetEducationClasses(rr, r)
 
-			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+			Expect(rr.Code).To(Equal(http.StatusForbidden))
 			data, err := io.ReadAll(rr.Body)
 			Expect(err).ToNot(HaveOccurred())
 
diff --git a/services/graph/pkg/service/v0/educationschools_test.go b/services/graph/pkg/service/v0/educationschools_test.go
index 798a565106c..9b9e4e57a5b 100644
--- a/services/graph/pkg/service/v0/educationschools_test.go
+++ b/services/graph/pkg/service/v0/educationschools_test.go
@@ -130,7 +130,7 @@ var _ = Describe("Schools", func() {
 			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools", nil)
 			svc.GetEducationSchools(rr, r)
 
-			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+			Expect(rr.Code).To(Equal(http.StatusForbidden))
 			data, err := io.ReadAll(rr.Body)
 			Expect(err).ToNot(HaveOccurred())
 
@@ -268,7 +268,7 @@ var _ = Describe("Schools", func() {
 
 			svc.PostEducationSchool(rr, r)
 
-			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+			Expect(rr.Code).To(Equal(http.StatusForbidden))
 		})
 
 		It("creates the school", func() {
diff --git a/services/graph/pkg/service/v0/groups_test.go b/services/graph/pkg/service/v0/groups_test.go
index 79d52c83657..7a492d110a6 100644
--- a/services/graph/pkg/service/v0/groups_test.go
+++ b/services/graph/pkg/service/v0/groups_test.go
@@ -133,7 +133,7 @@ var _ = Describe("Groups", func() {
 			r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/groups", nil)
 			svc.GetGroups(rr, r)
 
-			Expect(rr.Code).To(Equal(http.StatusInternalServerError))
+			Expect(rr.Code).To(Equal(http.StatusForbidden))
 			data, err := io.ReadAll(rr.Body)
 			Expect(err).ToNot(HaveOccurred())
 
diff --git a/tests/acceptance/features/apiSearch/apiSpaceSearch.feature b/tests/acceptance/features/apiSearch/apiSpaceSearch.feature
index fdbd277a705..137f19a04c5 100644
--- a/tests/acceptance/features/apiSearch/apiSpaceSearch.feature
+++ b/tests/acceptance/features/apiSearch/apiSpaceSearch.feature
@@ -16,10 +16,10 @@ Feature: Search
     And user "Alice" has created a space "find data" with the default quota using the GraphApi
     And user "Alice" has created a folder "folderMain/SubFolder1/subFOLDER2" in space "find data"
     And user "Alice" has uploaded a file inside space "find data" with content "some content" to "folderMain/SubFolder1/subFOLDER2/insideTheFolder.txt"
-    And using new DAV path
 
 
-  Scenario: user can find data from the project space
+  Scenario Outline: user can find data from the project space
+    Given using <dav-path-version> DAV path
     When user "Alice" searches for "*fol*" using the WebDAV API
     Then the HTTP status code should be "207"
     And the search result should contain "4" entries
@@ -28,9 +28,15 @@ Feature: Search
       | /folderMain/SubFolder1                                |
       | /folderMain/SubFolder1/subFOLDER2                     |
       | /folderMain/SubFolder1/subFOLDER2/insideTheFolder.txt |
+    Examples:
+      | dav-path-version |
+      | old              |
+      | new              |
+      | spaces           |
 
 
-  Scenario: user can only find data that they searched for from the project space
+  Scenario Outline: user can only find data that they searched for from the project space
+    Given using <dav-path-version> DAV path
     When user "Alice" searches for "*SUB*" using the WebDAV API
     Then the HTTP status code should be "207"
     And the search result should contain "2" entries
@@ -40,10 +46,16 @@ Feature: Search
     But the search result of user "Alice" should not contain these entries:
       | /folderMain                                           |
       | /folderMain/SubFolder1/subFOLDER2/insideTheFolder.txt |
+    Examples:
+      | dav-path-version |
+      | old              |
+      | new              |
+      | spaces           |
 
 
-  Scenario: user can find data from the shares
-    Given user "Alice" has created a share inside of space "find data" with settings:
+  Scenario Outline: user can find data from the shares
+    Given using <dav-path-version> DAV path
+    And user "Alice" has created a share inside of space "find data" with settings:
       | path      | folderMain |
       | shareWith | Brian      |
       | role      | viewer     |
@@ -56,19 +68,31 @@ Feature: Search
       | /SubFolder1/subFOLDER2                     |
       | /SubFolder1/subFOLDER2/insideTheFolder.txt |
     And for user "Brian" the search result should contain space "mountpoint/folderMain"
+    Examples:
+      | dav-path-version |
+      | old              |
+      | new              |
+      | spaces           |
 
 
-  Scenario: user can find hidden file
-    Given user "Alice" has created a folder ".space" in space "find data"
+  Scenario Outline: user can find hidden file
+    Given using <dav-path-version> DAV path
+    And user "Alice" has created a folder ".space" in space "find data"
     When user "Alice" searches for "*.sp*" using the WebDAV API
     Then the HTTP status code should be "207"
     And the search result should contain "1" entries
     And the search result of user "Alice" should contain these entries:
       | /.space |
+    Examples:
+      | dav-path-version |
+      | old              |
+      | new              |
+      | spaces           |
 
 
-  Scenario: user cannot find pending share
-    Given user "Alice" has created a share inside of space "find data" with settings:
+  Scenario Outline: user cannot find pending share
+    Given using <dav-path-version> DAV path
+    And user "Alice" has created a share inside of space "find data" with settings:
       | path      | folderMain |
       | shareWith | Brian      |
       | role      | viewer     |
@@ -79,10 +103,16 @@ Feature: Search
       | /SubFolder1                                |
       | /SubFolder1/subFOLDER2                     |
       | /SubFolder1/subFOLDER2/insideTheFolder.txt |
+    Examples:
+      | dav-path-version |
+      | old              |
+      | new              |
+      | spaces           |
 
 
-  Scenario: user cannot find declined share
-    Given user "Alice" has created a share inside of space "find data" with settings:
+  Scenario Outline: user cannot find declined share
+    Given using <dav-path-version> DAV path
+    And user "Alice" has created a share inside of space "find data" with settings:
       | path      | folderMain |
       | shareWith | Brian      |
       | role      | viewer     |
@@ -94,23 +124,41 @@ Feature: Search
       | /SubFolder1                                |
       | /SubFolder1/subFOLDER2                     |
       | /SubFolder1/subFOLDER2/insideTheFolder.txt |
+    Examples:
+      | dav-path-version |
+      | old              |
+      | new              |
+      | spaces           |
 
 
-  Scenario: user cannot find deleted folder
-    Given user "Alice" has removed the folder "folderMain" from space "find data"
+  Scenario Outline: user cannot find deleted folder
+    Given using <dav-path-version> DAV path
+    And user "Alice" has removed the folder "folderMain" from space "find data"
     When user "Alice" searches for "*folderMain*" using the WebDAV API
     Then the HTTP status code should be "207"
     And the search result should contain "0" entries
+    Examples:
+      | dav-path-version |
+      | old              |
+      | new              |
+      | spaces           |
 
 
-  Scenario: user can find project space by name
+  Scenario Outline: user can find project space by name
+    Given using <dav-path-version> DAV path
     When user "Alice" searches for '"*find data*"' using the WebDAV API
     Then the HTTP status code should be "207"
     And the search result should contain "1" entries
     And for user "Alice" the search result should contain space "find data"
+    Examples:
+      | dav-path-version |
+      | old              |
+      | new              |
+      | spaces           |
 
 
-  Scenario: user can search inside folder in space
+  Scenario Outline: user can search inside folder in space
+    Given using <dav-path-version> DAV path
     When user "Alice" searches for "*folder*" inside folder "/folderMain" in space "find data" using the WebDAV API
     Then the HTTP status code should be "207"
     And the search result should contain "3" entries
@@ -120,10 +168,16 @@ Feature: Search
       | /SubFolder1/subFOLDER2/insideTheFolder.txt |
     But the search result of user "Alice" should not contain these entries:
       | /folderMain |
+    Examples:
+      | dav-path-version |
+      | old              |
+      | new              |
+      | spaces           |
 
 
-  Scenario: search inside folder in shares
-    Given user "Alice" has created a share inside of space "find data" with settings:
+  Scenario Outline: search inside folder in shares
+    Given using <dav-path-version> DAV path
+    And user "Alice" has created a share inside of space "find data" with settings:
       | path      | folderMain |
       | shareWith | Brian      |
       | role      | viewer     |
@@ -136,10 +190,38 @@ Feature: Search
       | /SubFolder1/subFOLDER2/insideTheFolder.txt |
     But the search result of user "Brian" should not contain these entries:
       | /folderMain |
+    Examples:
+      | dav-path-version |
+      | old              |
+      | new              |
+      | spaces           |
 
-  @issue-enterprise-6000
-  Scenario: sharee cannot find resources that are not shared
-    Given user "Alice" has created a folder "foo/sharedToBrian" in space "Alice Hansen"
+
+  @skipOnStable3.0
+  Scenario Outline: search files inside the folder
+    Given using <dav-path-version> DAV path
+    And user "Alice" has uploaded file with content "hello world inside root" to "file1.txt"
+    And user "Alice" has created folder "/Folder"
+    And user "Alice" has uploaded file with content "hello world inside folder" to "/Folder/file2.txt"
+    And user "Alice" has created folder "/Folder/SubFolder"
+    And user "Alice" has uploaded file with content "hello world inside sub-folder" to "/Folder/SubFolder/file3.txt"
+    When user "Alice" searches for "*file*" inside folder "/Folder" using the WebDAV API
+    Then the HTTP status code should be "207"
+    And the search result of user "Alice" should contain only these entries:
+      | file2.txt |
+      | file3.txt |
+    But the search result of user "Alice" should not contain these entries:
+      | file1.txt |
+    Examples:
+      | dav-path-version |
+      | old              |
+      | new              |
+      | spaces           |
+
+  @issue-enterprise-6000 @issue-7028 @issue-7092
+  Scenario Outline: sharee cannot find resources that are not shared
+    Given using <dav-path-version> DAV path
+    And user "Alice" has created a folder "foo/sharedToBrian" in space "Alice Hansen"
     And user "Alice" has created a folder "sharedToCarol" in space "Alice Hansen"
     And user "Alice" has created a share inside of space "Alice Hansen" with settings:
       | path      | foo    |
@@ -152,3 +234,25 @@ Feature: Search
       | /sharedToBrian |
     But the search result of user "Brian" should not contain these entries:
       | /sharedToCarol |
+    Examples:
+      | dav-path-version |
+      | old              |
+      | new              |
+      | spaces           |
+
+
+  Scenario Outline: search resources using different search patterns (KQL feature)
+    Given using spaces DAV path
+    And user "Alice" has created a folder "subfolder" in space "find data"
+    When user "Alice" searches for '<pattern>' using the WebDAV API
+    Then the HTTP status code should be "207"
+    And the search result should contain "1" entries
+    And the search result of user "Alice" should contain these entries:
+      | <search-result> |
+    Examples:
+      | description                                            | pattern      | search-result                     |
+      | starts with                                            | fold*        | /folderMain                       |
+      | ends with                                              | *der1        | /folderMain/SubFolder1            |
+      | strict search                                          | subfolder    | /subfolder                        |
+      | using patern "name:"                                   | name:*der2   | /folderMain/SubFolder1/subFOLDER2 |
+      | using the pattern "name:" where the value is in quotes | name:"*der2" | /folderMain/SubFolder1/subFOLDER2 |
diff --git a/tests/acceptance/features/apiSearch/contentSearch.feature b/tests/acceptance/features/apiSearch/contentSearch.feature
index 7be1880d201..d80b92ae890 100644
--- a/tests/acceptance/features/apiSearch/contentSearch.feature
+++ b/tests/acceptance/features/apiSearch/contentSearch.feature
@@ -46,10 +46,6 @@ Feature: content search
       | dav-path-version |
       | old              |
       | new              |
-
-    @skipOnStable3.0
-    Examples:
-      | dav-path-version |
       | spaces           |
 
 
@@ -125,10 +121,6 @@ Feature: content search
       | dav-path-version |
       | old              |
       | new              |
-
-    @skipOnStable3.0
-    Examples:
-      | dav-path-version |
       | spaces           |
 
 
@@ -156,8 +148,27 @@ Feature: content search
       | dav-path-version |
       | old              |
       | new              |
+      | spaces           |
+
 
-    @skipOnStable3.0
+  Scenario Outline: search resources using different search patterns (KQL feature)
+    Given using spaces DAV path
+    And user "Alice" has uploaded file with content "hello world, let start to test" to "technical task.txt"
+    And user "Alice" has uploaded file with content "it's been hell" to "task comments.txt"
+    And user "Alice" has tagged the following files of the space "Personal":
+      | path               | tagName |
+      | technical task.txt | test    |
+    When user "Alice" searches for '<pattern>' using the WebDAV API
+    Then the HTTP status code should be "207"
+    And the search result should contain "<result-count>" entries
+    And the search result of user "Alice" should contain these entries:
+      | <search-result-1> |
+      | <search-result-2> |
     Examples:
-      | dav-path-version |
-      | spaces           |
+      | pattern                                     | result-count | search-result-1     | search-result-2    |
+      | Content:hello                               | 1            | /technical task.txt |                    |
+      | content:hello                               | 1            | /technical task.txt |                    |
+      | content:"hello"                             | 1            | /technical task.txt |                    |
+      | content:hel*                                | 2            | /technical task.txt | /task comments.txt |
+      | content:hel* AND tag:test                   | 1            | /technical task.txt |                    |
+      | (name:*task* AND content:hel*) NOT tag:test | 1            | /task comments.txt  |                    |
diff --git a/tests/acceptance/features/apiSearch/tagSearch.feature b/tests/acceptance/features/apiSearch/tagSearch.feature
index 239192a6200..0701a829a41 100644
--- a/tests/acceptance/features/apiSearch/tagSearch.feature
+++ b/tests/acceptance/features/apiSearch/tagSearch.feature
@@ -235,23 +235,43 @@ Feature: tag search
       | new              |
       | spaces           |
 
-  @skipOnStable3.0
-  Scenario Outline: search files inside the folder
-    Given using <dav-path-version> DAV path
-    And user "Alice" has uploaded file with content "hello world inside root" to "file1.txt"
-    And user "Alice" has created folder "/Folder"
-    And user "Alice" has uploaded file with content "hello world inside folder" to "/Folder/file2.txt"
-    And user "Alice" has created folder "/Folder/SubFolder"
-    And user "Alice" has uploaded file with content "hello world inside sub-folder" to "/Folder/SubFolder/file3.txt"
-    When user "Alice" searches for "*file*" inside folder "/Folder" using the WebDAV API
+
+  Scenario Outline: search resources using different search patterns (KQL feature)
+    Given using spaces DAV path
+    And user "Alice" has created the following folders
+      | path              |
+      | exercises         |
+      | answers           |
+      | verification work |
+      | withoutTagFolder  |
+    And user "Alice" has tagged the following folders of the space "Personal":
+      | path              | tagName |
+      | exercises         | klass10 |
+      | exercises         | mathe   |
+      | exercises         | physik  |
+      | answers           | klass10 |
+      | answers           | mathe   |
+      | answers           | chemi   |
+      | verification work | klass10 |
+    When user "Alice" searches for '<pattern>' using the WebDAV API
     Then the HTTP status code should be "207"
-    And the search result of user "Alice" should contain only these entries:
-      | file2.txt |
-      | file3.txt |
-    But the search result of user "Alice" should not contain these entries:
-      | file1.txt |
+    And the search result should contain "<result-count>" entries
+    And the search result of user "Alice" should contain these entries:
+      | <search-result-1> |
+      | <search-result-2> |
     Examples:
-      | dav-path-version |
-      | old              |
-      | new              |
-      | spaces           |
+      | pattern                                   | result-count | search-result-1    | search-result-2    |
+      | Tags:mathe                                | 2            | /exercises         | /answers           |
+      | tag:mathe                                 | 2            | /exercises         | /answers           |
+      | tag:(mathe klass10)                       | 2            | /exercises         | /answers           |
+      | tag:klass10  AND tag:chemi                | 1            | /answers           |                    |
+      | tag:chemi OR tag:physik                   | 2            | /exercises         | /answers           |
+      # why we need use quotes? it'll be fixed in future. Actual: search result gives all user content
+      #| (tag:klass10) NOT tag:physik              | 2      | /answers           | verification work |
+      | (tag:(mathe klass10)) NOT tag:chemi       | 1            | /exercises         |                    |
+      | (tag:mathe OR tag:klass10) NOT tag:physik | 2            | /answers           | /verification work |
+      | tag:mathe NOT name:exercises              | 1            | /answers           |                    |
+      | tag:mathe AND NOT name:exercises          | 1            | /answers           |                    |
+      # The third finding is the personal space itself
+      | NOT tag:mathe                             | 3            | /verification work | /withoutTagFolder  |
+
diff --git a/tests/acceptance/features/bootstrap/SearchContext.php b/tests/acceptance/features/bootstrap/SearchContext.php
index b0fd1e81357..a3163aa304b 100644
--- a/tests/acceptance/features/bootstrap/SearchContext.php
+++ b/tests/acceptance/features/bootstrap/SearchContext.php
@@ -169,28 +169,4 @@ class SearchContext implements Context {
 		// Get all the contexts you need in this context
 		$this->featureContext = $environment->getContext('FeatureContext');
 	}
-
-	/**
-	 * @Then the search result by tags for user :user should contain these entries:
-	 *
-	 * @param string|null $user
-	 * @param TableNode $expectedEntries
-	 *
-	 * @return void
-	 * @throws Exception
-	 */
-	public function theSearchResultByTagsForUserShouldContainTheseEntries(
-		?string $user,
-		TableNode $expectedEntries
-	):void {
-		$user = $this->featureContext->getActualUsername($user);
-		$this->featureContext->verifyTableNodeColumnsCount($expectedEntries, 1);
-		$expectedEntries = $expectedEntries->getRows();
-		$expectedEntriesArray = [];
-		$responseResourcesArray = $this->featureContext->findEntryFromReportResponse($user);
-		foreach ($expectedEntries as $item) {
-			$expectedEntriesArray[] = $item[0];
-		}
-		Assert::assertEqualsCanonicalizing($expectedEntriesArray, $responseResourcesArray);
-	}
 }