Repository: owncloud/ocis
Pull request #6233 (2023-05-04 16:44:54)

Allow selected updates even when GRAPH_LDAP_SERVER_WRITE_ENABLED=false
This allows a couple of update request to complete even if `GRAPH_LDAP_SERVER_WRITE_ENABLED=false`:

* When using a group to disable users (`OCIS_LDAP_DISABLE_USER_MECHANISM=group`) updates to the `accountEnabled` property of a user will be allowed
* When a distinct base dn for new groups is configured ( `GRAPH_LDAP_GROUP_CREATE_BASE_DN` is set to a different value than `GRAPH_LDAP_GROUP_BASE_DN`), allow the creation/update of local groups.

rhafer: @butonic  @micbar This is a bit of a hacky fix for #6219. I must admit am not too happy with this. It feels like it's doing a bit too much magic.

(Apart from the first commit, for  which we should do similar changes on the /groups endpoint).
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=6233)

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

[![6.8%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '6.8%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6233&metric=new_coverage&view=list) [6.8% Coverage](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6233&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6233&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=6233&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29679b9c907..370fc0be98f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,17 +6,17 @@ The following sections list the changes for unreleased.
 
 ## Summary
 
-* Bugfix - Fix Logout Url config name: [#6227](https://github.com/owncloud/ocis/pull/6227)
+* Bugfix - Fix Logout Url config name: [#6226](https://github.com/owncloud/ocis/pull/6226)
 * Enhancement - Update Reva to version TODO: [#6232](https://github.com/owncloud/ocis/pull/6232)
 * Enhancement - Update web to v7.0.0-rc.36: [#6234](https://github.com/owncloud/ocis/pull/6234)
 
 ## Details
 
-* Bugfix - Fix Logout Url config name: [#6227](https://github.com/owncloud/ocis/pull/6227)
+* Bugfix - Fix Logout Url config name: [#6226](https://github.com/owncloud/ocis/pull/6226)
 
    We fixed the yaml and json name of the logout url option.
 
-   https://github.com/owncloud/ocis/pull/6227
+   https://github.com/owncloud/ocis/pull/6226
 
 * Enhancement - Update Reva to version TODO: [#6232](https://github.com/owncloud/ocis/pull/6232)
 
diff --git a/changelog/unreleased/fix-web-config.md b/changelog/unreleased/fix-web-config.md
index 4e66f8acc38..1e672944e0c 100644
--- a/changelog/unreleased/fix-web-config.md
+++ b/changelog/unreleased/fix-web-config.md
@@ -2,4 +2,4 @@ Bugfix: Fix Logout Url config name
 
 We fixed the yaml and json name of the logout url option.
 
-https://github.com/owncloud/ocis/pull/6227
+https://github.com/owncloud/ocis/pull/6226
diff --git a/services/graph/pkg/identity/ldap.go b/services/graph/pkg/identity/ldap.go
index 980cb21ca20..6920a43f4cf 100644
--- a/services/graph/pkg/identity/ldap.go
+++ b/services/graph/pkg/identity/ldap.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"math"
 	"strconv"
 	"strings"
 
@@ -85,6 +86,10 @@ type userAttributeMap struct {
 
 type ldapAttributeValues map[string][]string
 
+type ldapResultToErrMap map[uint16]errorcode.Error
+
+const ldapGenericErr = math.MaxUint16
+
 // ParseDisableMechanismType checks that the configuration option for how to disable users is correct.
 func ParseDisableMechanismType(disableMechanism string) (DisableUserMechanismType, error) {
 	disableMechanism = strings.ToLower(disableMechanism)
@@ -188,14 +193,15 @@ func (i *LDAP) CreateUser(ctx context.Context, user libregraph.User) (*libregrap
 	}
 
 	if err := i.conn.Add(ar); err != nil {
-		var lerr *ldap.Error
-		logger.Debug().Err(err).Msg("error adding user")
-		if errors.As(err, &lerr) {
-			if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
-				err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error())
-			}
+		msg := "failed to add user"
+		logger.Error().Err(err).Msg(msg)
+		errMap := ldapResultToErrMap{
+			ldap.LDAPResultEntryAlreadyExists:       errorcode.New(errorcode.NameAlreadyExists, "a user with that name already exists"),
+			ldap.LDAPResultUnwillingToPerform:       errorcode.New(errorcode.NotAllowed, msg),
+			ldap.LDAPResultInsufficientAccessRights: errorcode.New(errorcode.NotAllowed, msg),
+			ldapGenericErr:                          errorcode.New(errorcode.GeneralException, msg),
 		}
-		return nil, err
+		return nil, i.mapLDAPError(err, errMap)
 	}
 
 	if i.usePwModifyExOp && user.PasswordProfile != nil && user.PasswordProfile.Password != nil {
@@ -226,7 +232,15 @@ func (i *LDAP) DeleteUser(ctx context.Context, nameOrID string) error {
 	}
 	dr := ldap.DelRequest{DN: e.DN}
 	if err = i.conn.Del(&dr); err != nil {
-		return err
+		msg := "error deleting user"
+		logger.Error().Err(err).Msg(msg)
+		errMap := ldapResultToErrMap{
+			ldap.LDAPResultNoSuchObject:             errorcode.New(errorcode.ItemNotFound, "user not found"),
+			ldap.LDAPResultUnwillingToPerform:       errorcode.New(errorcode.NotAllowed, msg),
+			ldap.LDAPResultInsufficientAccessRights: errorcode.New(errorcode.NotAllowed, msg),
+			ldapGenericErr:                          errorcode.New(errorcode.GeneralException, msg),
+		}
+		return i.mapLDAPError(err, errMap)
 	}
 
 	if !i.refintEnabled {
@@ -253,7 +267,12 @@ func (i *LDAP) UpdateUser(ctx context.Context, nameOrID string, user libregraph.
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("UpdateUser")
 	if !i.writeEnabled {
-		return nil, ErrReadOnly
+		// still allow eanble/disable User when using DisableMechanismGroup
+		if i.disableUserMechanism == DisableMechanismGroup && isUserEnabledUpdate(user) {
+			logger.Error().Str("backend", "ldap").Msg("Allowing accountEnabled Update on read-only backend")
+		} else {
+			return nil, ErrReadOnly
+		}
 	}
 	e, err := i.getLDAPUserByNameOrID(nameOrID)
 	if err != nil {
@@ -303,7 +322,15 @@ func (i *LDAP) UpdateUser(ctx context.Context, nameOrID string, user libregraph.
 	if user.PasswordProfile != nil && user.PasswordProfile.GetPassword() != "" {
 		if i.usePwModifyExOp {
 			if err := i.updateUserPassowrd(ctx, e.DN, user.PasswordProfile.GetPassword()); err != nil {
-				return nil, err
+				msg := "error updating user password"
+				logger.Error().Err(err).Msg(msg)
+				errMap := ldapResultToErrMap{
+					ldap.LDAPResultNoSuchObject:             errorcode.New(errorcode.ItemNotFound, "user not found"),
+					ldap.LDAPResultUnwillingToPerform:       errorcode.New(errorcode.NotAllowed, msg),
+					ldap.LDAPResultInsufficientAccessRights: errorcode.New(errorcode.NotAllowed, msg),
+					ldapGenericErr:                          errorcode.New(errorcode.GeneralException, msg),
+				}
+				return nil, i.mapLDAPError(err, errMap)
 			}
 		} else {
 			// password are hashed server side there is no need to check if the new password
@@ -331,7 +358,15 @@ func (i *LDAP) UpdateUser(ctx context.Context, nameOrID string, user libregraph.
 
 	if updateNeeded {
 		if err := i.conn.Modify(&mr); err != nil {
-			return nil, err
+			msg := "error updating user"
+			logger.Error().Err(err).Msg(msg)
+			errMap := ldapResultToErrMap{
+				ldap.LDAPResultNoSuchObject:             errorcode.New(errorcode.ItemNotFound, "user not found"),
+				ldap.LDAPResultUnwillingToPerform:       errorcode.New(errorcode.NotAllowed, msg),
+				ldap.LDAPResultInsufficientAccessRights: errorcode.New(errorcode.NotAllowed, msg),
+				ldapGenericErr:                          errorcode.New(errorcode.GeneralException, msg),
+			}
+			return nil, i.mapLDAPError(err, errMap)
 		}
 	}
 
@@ -395,7 +430,7 @@ func (i *LDAP) getEntryByDN(dn string, attrs []string, filter string) (*ldap.Ent
 	res, err := i.conn.Search(searchRequest)
 	if err != nil {
 		i.logger.Error().Err(err).Str("backend", "ldap").Str("dn", dn).Msg("Search ldap by DN failed")
-		return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
+		return nil, errorcode.New(errorcode.ItemNotFound, "user lookup failed")
 	}
 	if len(res.Entries) == 0 {
 		return nil, ErrNotFound
@@ -428,7 +463,7 @@ func (i *LDAP) searchLDAPEntryByFilter(basedn string, attrs []string, filter str
 	res, err := i.conn.Search(searchRequest)
 	if err != nil {
 		i.logger.Error().Err(err).Str("backend", "ldap").Str("dn", basedn).Str("filter", filter).Msg("Search user by filter failed")
-		return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
+		return nil, errorcode.New(errorcode.ItemNotFound, "user search failed")
 	}
 	if len(res.Entries) == 0 {
 		return nil, ErrNotFound
@@ -580,7 +615,13 @@ func (i *LDAP) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*lib
 		Msg("GetUsers")
 	res, err := i.conn.Search(searchRequest)
 	if err != nil {
-		return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
+		msg := "error listing users"
+		logger.Error().Err(err).Msg(msg)
+		errMap := ldapResultToErrMap{
+			ldap.LDAPResultInsufficientAccessRights: errorcode.New(errorcode.AccessDenied, msg),
+			ldapGenericErr:                          errorcode.New(errorcode.GeneralException, msg),
+		}
+		return nil, i.mapLDAPError(err, errMap)
 	}
 
 	users := make([]*libregraph.User, 0, len(res.Entries))
@@ -626,14 +667,16 @@ func (i *LDAP) changeUserName(ctx context.Context, dn, originalUserName, newUser
 	mrdn := ldap.NewModifyDNRequest(dn, newDNString, true, "")
 
 	if err := i.conn.ModifyDN(mrdn); err != nil {
-		var lerr *ldap.Error
-		logger.Debug().Str("originalDN", dn).Str("newDN", newDNString).Err(err).Msg("Failed to modify DN")
-		if errors.As(err, &lerr) {
-			if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
-				err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error())
-			}
+		msg := "error renaming user"
+		logger.Error().Err(err).Msg(msg)
+		errMap := ldapResultToErrMap{
+			ldap.LDAPResultEntryAlreadyExists:       errorcode.New(errorcode.NameAlreadyExists, "a user with that name already exists"),
+			ldap.LDAPResultNoSuchObject:             errorcode.New(errorcode.ItemNotFound, msg),
+			ldap.LDAPResultUnwillingToPerform:       errorcode.New(errorcode.NotAllowed, msg),
+			ldap.LDAPResultInsufficientAccessRights: errorcode.New(errorcode.NotAllowed, msg),
+			ldapGenericErr:                          errorcode.New(errorcode.GeneralException, msg),
 		}
-		return nil, err
+		return nil, i.mapLDAPError(err, errMap)
 	}
 
 	parsed, err := ldap.ParseDN(dn)
@@ -678,17 +721,17 @@ func (i *LDAP) renameMemberInGroup(ctx context.Context, group *ldap.Entry, oldMe
 	mr.Delete(i.groupAttributeMap.member, []string{oldMember})
 	mr.Add(i.groupAttributeMap.member, []string{newMember})
 	if err := i.conn.Modify(mr); err != nil {
+		logger.Warn().Err(err).
+			Str("oldMember", oldMember).
+			Str("newMember", newMember).
+			Str("groupDN", group.DN).
+			Msg("Error renaming group members.")
 		var lerr *ldap.Error
 		if errors.As(err, &lerr) {
-			if lerr.ResultCode == ldap.LDAPResultNoSuchObject {
-				logger.Warn().Str("group", group.DN).Msg("Group no longer exists")
-				return nil
-			} else if lerr.ResultCode == ldap.LDAPResultNoSuchAttribute {
-				logger.Warn().
-					Str("oldMember", oldMember).
-					Str("newMember", newMember).
-					Str("groupDN", group.DN).
-					Msg("member attribute not found, this probably means that the server has refint enabled, please configure the OCIS to respect that.")
+			// NoSuchObject means that the group no longer exists. Some other client might have deleted it. There is
+			// not much we can do
+			// NoSuchAttribute means that the old value is no longer present. We can't do much here either
+			if lerr.ResultCode == ldap.LDAPResultNoSuchObject || lerr.ResultCode == ldap.LDAPResultNoSuchAttribute {
 				return nil
 			}
 		}
@@ -929,9 +972,17 @@ func (i *LDAP) removeEntryByDNAndAttributeFromEntry(entry *ldap.Entry, dn string
 	if err != nil {
 		i.logger.Error().Err(err).Str("entry", entry.DN).Str("attribute", attribute).Str("target value", dn).
 			Msg("Failed to remove dn attribute from entry")
+		msg := "failed to update object"
+		errMap := ldapResultToErrMap{
+			ldap.LDAPResultNoSuchObject:             errorcode.New(errorcode.ItemNotFound, "object does not exists"),
+			ldap.LDAPResultUnwillingToPerform:       errorcode.New(errorcode.NotAllowed, msg),
+			ldap.LDAPResultInsufficientAccessRights: errorcode.New(errorcode.NotAllowed, msg),
+			ldapGenericErr:                          errorcode.New(errorcode.GeneralException, msg),
+		}
+		return i.mapLDAPError(err, errMap)
 	}
 
-	return err
+	return nil
 }
 
 // expandLDAPAttributeEntries reads an attribute from an ldap entry and expands to users
@@ -1133,3 +1184,32 @@ func (i *LDAP) updateAccountEnabledState(logger log.Logger, accountEnabled bool,
 
 	return updateNeeded, err
 }
+
+func (i *LDAP) mapLDAPError(err error, errmap ldapResultToErrMap) errorcode.Error {
+	var lerr *ldap.Error
+	if errors.As(err, &lerr) {
+		if res, ok := errmap[lerr.ResultCode]; ok {
+			return res
+		}
+	}
+	if res, ok := errmap[ldapGenericErr]; ok {
+		return res
+	}
+	return errorcode.New(errorcode.GeneralException, err.Error())
+}
+
+func isUserEnabledUpdate(user libregraph.User) bool {
+	switch {
+	case user.Id != nil, user.DisplayName != nil,
+		user.Drive != nil, user.Mail != nil, user.OnPremisesSamAccountName != nil,
+		user.PasswordProfile != nil, user.Surname != nil, user.GivenName != nil,
+		user.UserType != nil:
+		return false
+	case len(user.AppRoleAssignments) != 0,
+		len(user.MemberOf) != 0,
+		len(user.Identities) != 0,
+		len(user.Drives) != 0:
+		return false
+	}
+	return user.AccountEnabled != nil
+}
diff --git a/services/graph/pkg/identity/ldap_group.go b/services/graph/pkg/identity/ldap_group.go
index d19b86e12cd..f4c8e85062d 100644
--- a/services/graph/pkg/identity/ldap_group.go
+++ b/services/graph/pkg/identity/ldap_group.go
@@ -177,7 +177,7 @@ func (i *LDAP) GetGroupMembers(ctx context.Context, groupID string, req *godata.
 func (i *LDAP) CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error) {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("create group")
-	if !i.writeEnabled {
+	if !i.writeEnabled && i.groupCreateBaseDN == i.groupBaseDN {
 		return nil, errorcode.New(errorcode.NotAllowed, "server is configured read-only")
 	}
 	ar, err := i.groupToAddRequest(group)
@@ -201,7 +201,7 @@ func (i *LDAP) CreateGroup(ctx context.Context, group libregraph.Group) (*libreg
 func (i *LDAP) DeleteGroup(ctx context.Context, id string) error {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("DeleteGroup")
-	if !i.writeEnabled {
+	if !i.writeEnabled && i.groupCreateBaseDN == i.groupBaseDN {
 		return errorcode.New(errorcode.NotAllowed, "server is configured read-only")
 	}
 
@@ -225,7 +225,7 @@ func (i *LDAP) DeleteGroup(ctx context.Context, id string) error {
 func (i *LDAP) UpdateGroupName(ctx context.Context, groupID string, groupName string) error {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("AddMembersToGroup")
-	if !i.writeEnabled {
+	if !i.writeEnabled && i.groupCreateBaseDN == i.groupBaseDN {
 		return errorcode.New(errorcode.NotAllowed, "server is configured read-only")
 	}
 
@@ -271,7 +271,7 @@ func (i *LDAP) UpdateGroupName(ctx context.Context, groupID string, groupName st
 func (i *LDAP) AddMembersToGroup(ctx context.Context, groupID string, memberIDs []string) error {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("AddMembersToGroup")
-	if !i.writeEnabled {
+	if !i.writeEnabled && i.groupCreateBaseDN == i.groupBaseDN {
 		return errorcode.New(errorcode.NotAllowed, "server is configured read-only")
 	}
 	ge, err := i.getLDAPGroupByNameOrID(groupID, true)
@@ -365,7 +365,7 @@ func (i *LDAP) AddMembersToGroup(ctx context.Context, groupID string, memberIDs
 func (i *LDAP) RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error {
 	logger := i.logger.SubloggerWithRequestID(ctx)
 	logger.Debug().Str("backend", "ldap").Msg("RemoveMemberFromGroup")
-	if !i.writeEnabled {
+	if !i.writeEnabled && i.groupCreateBaseDN == i.groupBaseDN {
 		return errorcode.New(errorcode.NotAllowed, "server is configured read-only")
 	}
 
@@ -562,10 +562,6 @@ func (i *LDAP) createGroupModelFromLDAP(e *ldap.Entry) *libregraph.Group {
 }
 
 func (i *LDAP) isLDAPGroupReadOnly(e *ldap.Entry) bool {
-	if !i.writeEnabled {
-		return true
-	}
-
 	groupDN, err := ldap.ParseDN(e.DN)
 	if err != nil {
 		i.logger.Warn().Err(err).Str("dn", e.DN).Msg("Failed to parse DN")
diff --git a/services/web/README.md b/services/web/README.md
index ae2067dd921..78363877c14 100644
--- a/services/web/README.md
+++ b/services/web/README.md
@@ -9,14 +9,6 @@ The web service also provides a minimal API for branding functionality like chan
 
 If you want to use your custom compiled web client assets instead of the embedded ones, then you can do that by setting the `WEB_ASSET_PATH` variable to point to your compiled files. See [ownCloud Web / Getting Started](https://owncloud.dev/clients/web/getting-started/) and [ownCloud Web / Setup with oCIS](https://owncloud.dev/clients/web/backend-ocis/) for more details.
 
-## Web UI Configuration
+## WebUI Options
 
-Note that single configuration settings of the embedded web UI can be defined via `WEB_OPTION_xxx` environment variables. If a json based configuration file is used via the `WEB_UI_CONFIG_FILE` environment variable, these configurations take precedence over single options set.
-
-### Web UI Options
-
-Beside theming, the behavior of the web UI can be configured via options. See the environment variables `WEB_OPTION_xxx` for more details.
-
-### Web UI Config File
-
-When defined via the `WEB_UI_CONFIG_FILE` environment variable, the configuration of the web UI can be made with a [json based](https://github.com/owncloud/web/tree/master/config) file.
+Beside theming, the behaviour of the embedded WebUI can be configured via options. See the environment variables `WEB_OPTION_xxx` for more details.
diff --git a/services/web/pkg/config/config.go b/services/web/pkg/config/config.go
index 99f9de93cf8..d85d7f98223 100644
--- a/services/web/pkg/config/config.go
+++ b/services/web/pkg/config/config.go
@@ -19,7 +19,7 @@ type Config struct {
 	HTTP HTTP `yaml:"http"`
 
 	Asset Asset  `yaml:"asset"`
-	File  string `yaml:"file" env:"WEB_UI_CONFIG_FILE;WEB_UI_CONFIG" desc:"Read the ownCloud Web json based configuration from this path/file. The config file takes precedence over WEB_OPTION_xxx environment variables. See the text description for more details." deprecationVersion:"3.0" removalVersion:"4.0.0" deprecationInfo:"WEB_UI_CONFIG name changing to make the usecase clear" deprecationReplacement:"WEB_UI_CONFIG_FILE"`
+	File  string `yaml:"file" env:"WEB_UI_CONFIG" desc:"Read the ownCloud Web configuration from this file."` // TODO: rename this to a more self explaining string
 	Web   Web    `yaml:"web"`
 
 	TokenManager *TokenManager `yaml:"token_manager"`
@@ -104,7 +104,7 @@ type ExternalAppConfig struct {
 
 // Web defines the available web configuration.
 type Web struct {
-	Path        string    `yaml:"path" env:"WEB_UI_CONFIG_FILE;WEB_UI_PATH" desc:"Read the ownCloud Web configuration from this file path." deprecationVersion:"3.0" removalVersion:"4.0.0" deprecationInfo:"WEB_UI_PATH will be superceded by WEB_UI_CONFIG_FILE" deprecationReplacement:"WEB_UI_CONFIG_FILE"`
+	Path        string    `yaml:"path" env:"WEB_UI_PATH" desc:"Read the ownCloud Web configuration from this file path."`
 	ThemeServer string    `yaml:"theme_server" env:"OCIS_URL;WEB_UI_THEME_SERVER" desc:"Base URL to load themes from. Will be prepended to the theme path."`  // used to build Theme in WebConfig
 	ThemePath   string    `yaml:"theme_path" env:"WEB_UI_THEME_PATH" desc:"Subpath/file to load the theme. Will be appended to the URL of the theme server."` // used to build Theme in WebConfig
 	Config      WebConfig `yaml:"config"`


Test patch:
diff --git a/services/graph/pkg/identity/ldap_group_test.go b/services/graph/pkg/identity/ldap_group_test.go
index f3557cb1978..e823444c488 100644
--- a/services/graph/pkg/identity/ldap_group_test.go
+++ b/services/graph/pkg/identity/ldap_group_test.go
@@ -161,6 +161,10 @@ func TestGetGroup(t *testing.T) {
 func TestGetGroupReadOnlyBackend(t *testing.T) {
 	readOnlyConfig := lconfig
 	readOnlyConfig.WriteEnabled = false
+	readOnlyConfig.GroupBaseDN = "ou=groups,dc=test"
+	readOnlyConfig.GroupCreateBaseDN = "ou=local,ou=group,dc=test"
+	localGroupEntry := groupEntry
+	localGroupEntry.DN = "cn=local,ou=local,o=base"
 
 	lm := &mocks.Client{}
 	lm.On("Search", groupLookupSearchRequest).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil)
diff --git a/services/graph/pkg/identity/ldap_test.go b/services/graph/pkg/identity/ldap_test.go
index 1364c0c3c3e..d3bcf919215 100644
--- a/services/graph/pkg/identity/ldap_test.go
+++ b/services/graph/pkg/identity/ldap_test.go
@@ -282,8 +282,8 @@ func TestGetUsers(t *testing.T) {
 
 	b, _ := getMockedBackend(lm, lconfig, &logger)
 	_, err = b.GetUsers(context.Background(), odataReqDefault)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
+	if err == nil || err.Error() != "generalException" {
+		t.Errorf("Expected 'generalException' got '%s'", err.Error())
 	}
 
 	lm = &mocks.Client{}