Jelajahi Sumber

replace utils.Contains with slices.Contains

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 1 tahun lalu
induk
melakukan
d94f80c8da
51 mengubah file dengan 353 tambahan dan 322 penghapusan
  1. 2 1
      internal/acme/acme.go
  2. 2 3
      internal/command/command.go
  3. 5 4
      internal/common/actions.go
  4. 4 3
      internal/common/common.go
  5. 4 3
      internal/common/common_test.go
  6. 2 2
      internal/common/connection.go
  7. 2 1
      internal/common/connection_test.go
  8. 5 4
      internal/common/eventmanager.go
  9. 35 34
      internal/common/protocol_test.go
  10. 2 1
      internal/common/ratelimiter.go
  11. 3 2
      internal/common/tlsutils.go
  12. 2 1
      internal/config/config.go
  13. 11 11
      internal/config/config_test.go
  14. 3 2
      internal/dataprovider/actions.go
  15. 7 6
      internal/dataprovider/admin.go
  16. 11 10
      internal/dataprovider/bolt.go
  17. 6 5
      internal/dataprovider/configs.go
  18. 33 32
      internal/dataprovider/dataprovider.go
  19. 14 13
      internal/dataprovider/eventrule.go
  20. 2 1
      internal/dataprovider/iplist.go
  21. 9 8
      internal/dataprovider/memory.go
  22. 2 1
      internal/dataprovider/pgsql.go
  23. 28 27
      internal/dataprovider/user.go
  24. 2 1
      internal/ftpd/server.go
  25. 2 1
      internal/httpd/api_mfa.go
  26. 6 5
      internal/httpd/api_shares.go
  27. 3 2
      internal/httpd/api_utils.go
  28. 12 11
      internal/httpd/auth_utils.go
  29. 26 25
      internal/httpd/httpd_test.go
  30. 6 5
      internal/httpd/middleware.go
  31. 2 1
      internal/httpd/oidc.go
  32. 6 5
      internal/httpd/server.go
  33. 8 7
      internal/httpd/webadmin.go
  34. 4 3
      internal/httpd/webclient.go
  35. 25 25
      internal/httpdtest/httpdtest.go
  36. 3 3
      internal/plugin/kms.go
  37. 4 4
      internal/plugin/notifier.go
  38. 2 1
      internal/plugin/plugin.go
  39. 2 1
      internal/service/service_portable.go
  40. 4 3
      internal/sftpd/internal_test.go
  41. 17 16
      internal/sftpd/server.go
  42. 3 2
      internal/sftpd/sftpd_test.go
  43. 6 5
      internal/sftpd/ssh_cmd.go
  44. 2 2
      internal/smtp/oauth2.go
  45. 0 10
      internal/util/util.go
  46. 2 2
      internal/vfs/osfs.go
  47. 2 1
      internal/vfs/s3fs.go
  48. 4 3
      internal/vfs/sftpfs.go
  49. 2 1
      internal/vfs/vfs.go
  50. 2 1
      internal/webdavd/file.go
  51. 2 1
      internal/webdavd/server.go

+ 2 - 1
internal/acme/acme.go

@@ -30,6 +30,7 @@ import (
 	"net/url"
 	"os"
 	"path/filepath"
+	"slices"
 	"strconv"
 	"strings"
 	"time"
@@ -249,7 +250,7 @@ func (c *Configuration) Initialize(configDir string) error {
 	if c.RenewDays < 1 {
 		return fmt.Errorf("invalid number of days remaining before renewal: %d", c.RenewDays)
 	}
-	if !util.Contains(supportedKeyTypes, c.KeyType) {
+	if !slices.Contains(supportedKeyTypes, c.KeyType) {
 		return fmt.Errorf("invalid key type %q", c.KeyType)
 	}
 	caURL, err := url.Parse(c.CAEndpoint)

+ 2 - 3
internal/command/command.go

@@ -17,10 +17,9 @@ package command
 
 import (
 	"fmt"
+	"slices"
 	"strings"
 	"time"
-
-	"github.com/drakkan/sftpgo/v2/internal/util"
 )
 
 const (
@@ -117,7 +116,7 @@ func (c Config) Initialize() error {
 		}
 		// don't validate args, we allow to pass empty arguments
 		if cmd.Hook != "" {
-			if !util.Contains(supportedHooks, cmd.Hook) {
+			if !slices.Contains(supportedHooks, cmd.Hook) {
 				return fmt.Errorf("invalid hook name %q, supported values: %+v", cmd.Hook, supportedHooks)
 			}
 		}

+ 5 - 4
internal/common/actions.go

@@ -25,6 +25,7 @@ import (
 	"os/exec"
 	"path"
 	"path/filepath"
+	"slices"
 	"strings"
 	"sync/atomic"
 	"time"
@@ -86,7 +87,7 @@ func InitializeActionHandler(handler ActionHandler) {
 func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath string, fileSize int64, openFlags int) (int, error) {
 	var event *notifier.FsEvent
 	hasNotifiersPlugin := plugin.Handler.HasNotifiers()
-	hasHook := util.Contains(Config.Actions.ExecuteOn, operation)
+	hasHook := slices.Contains(Config.Actions.ExecuteOn, operation)
 	hasRules := eventManager.hasFsRules()
 	if !hasHook && !hasNotifiersPlugin && !hasRules {
 		return 0, nil
@@ -132,7 +133,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
 	fileSize int64, err error, elapsed int64, metadata map[string]string,
 ) error {
 	hasNotifiersPlugin := plugin.Handler.HasNotifiers()
-	hasHook := util.Contains(Config.Actions.ExecuteOn, operation)
+	hasHook := slices.Contains(Config.Actions.ExecuteOn, operation)
 	hasRules := eventManager.hasFsRules()
 	if !hasHook && !hasNotifiersPlugin && !hasRules {
 		return nil
@@ -173,7 +174,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
 		}
 	}
 	if hasHook {
-		if util.Contains(Config.Actions.ExecuteSync, operation) {
+		if slices.Contains(Config.Actions.ExecuteSync, operation) {
 			_, err := actionHandler.Handle(notification)
 			return err
 		}
@@ -247,7 +248,7 @@ func newActionNotification(
 type defaultActionHandler struct{}
 
 func (h *defaultActionHandler) Handle(event *notifier.FsEvent) (int, error) {
-	if !util.Contains(Config.Actions.ExecuteOn, event.Action) {
+	if !slices.Contains(Config.Actions.ExecuteOn, event.Action) {
 		return 0, nil
 	}
 

+ 4 - 3
internal/common/common.go

@@ -25,6 +25,7 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"slices"
 	"strconv"
 	"strings"
 	"sync"
@@ -207,7 +208,7 @@ func Initialize(c Configuration, isShared int) error {
 		Config.rateLimitersList = rateLimitersList
 	}
 	if c.DefenderConfig.Enabled {
-		if !util.Contains(supportedDefenderDrivers, c.DefenderConfig.Driver) {
+		if !slices.Contains(supportedDefenderDrivers, c.DefenderConfig.Driver) {
 			return fmt.Errorf("unsupported defender driver %q", c.DefenderConfig.Driver)
 		}
 		var defender Defender
@@ -777,7 +778,7 @@ func (c *Configuration) checkPostDisconnectHook(remoteAddr, protocol, username,
 	if c.PostDisconnectHook == "" {
 		return
 	}
-	if !util.Contains(disconnHookProtocols, protocol) {
+	if !slices.Contains(disconnHookProtocols, protocol) {
 		return
 	}
 	go c.executePostDisconnectHook(remoteAddr, protocol, username, connID, connectionTime)
@@ -1019,7 +1020,7 @@ func (conns *ActiveConnections) Remove(connectionID string) {
 		metric.UpdateActiveConnectionsSize(lastIdx)
 		logger.Debug(conn.GetProtocol(), conn.GetID(), "connection removed, local address %q, remote address %q close fs error: %v, num open connections: %d",
 			conn.GetLocalAddress(), conn.GetRemoteAddress(), err, lastIdx)
-		if conn.GetProtocol() == ProtocolFTP && conn.GetUsername() == "" && !util.Contains(ftpLoginCommands, conn.GetCommand()) {
+		if conn.GetProtocol() == ProtocolFTP && conn.GetUsername() == "" && !slices.Contains(ftpLoginCommands, conn.GetCommand()) {
 			ip := util.GetIPFromRemoteAddress(conn.GetRemoteAddress())
 			logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTried, ProtocolFTP,
 				dataprovider.ErrNoAuthTried.Error())

+ 4 - 3
internal/common/common_test.go

@@ -23,6 +23,7 @@ import (
 	"os/exec"
 	"path/filepath"
 	"runtime"
+	"slices"
 	"sync"
 	"testing"
 	"time"
@@ -1226,8 +1227,8 @@ func TestFolderCopy(t *testing.T) {
 	folder.ID = 2
 	folder.Users = []string{"user3"}
 	require.Len(t, folderCopy.Users, 2)
-	require.True(t, util.Contains(folderCopy.Users, "user1"))
-	require.True(t, util.Contains(folderCopy.Users, "user2"))
+	require.True(t, slices.Contains(folderCopy.Users, "user1"))
+	require.True(t, slices.Contains(folderCopy.Users, "user2"))
 	require.Equal(t, int64(1), folderCopy.ID)
 	require.Equal(t, folder.Name, folderCopy.Name)
 	require.Equal(t, folder.MappedPath, folderCopy.MappedPath)
@@ -1243,7 +1244,7 @@ func TestFolderCopy(t *testing.T) {
 	folderCopy = folder.GetACopy()
 	folder.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()
 	require.Len(t, folderCopy.Users, 1)
-	require.True(t, util.Contains(folderCopy.Users, "user3"))
+	require.True(t, slices.Contains(folderCopy.Users, "user3"))
 	require.Equal(t, int64(2), folderCopy.ID)
 	require.Equal(t, folder.Name, folderCopy.Name)
 	require.Equal(t, folder.MappedPath, folderCopy.MappedPath)

+ 2 - 2
internal/common/connection.go

@@ -63,7 +63,7 @@ type BaseConnection struct {
 // NewBaseConnection returns a new BaseConnection
 func NewBaseConnection(id, protocol, localAddr, remoteAddr string, user dataprovider.User) *BaseConnection {
 	connID := id
-	if util.Contains(supportedProtocols, protocol) {
+	if slices.Contains(supportedProtocols, protocol) {
 		connID = fmt.Sprintf("%s_%s", protocol, id)
 	}
 	user.UploadBandwidth, user.DownloadBandwidth = user.GetBandwidthForIP(util.GetIPFromRemoteAddress(remoteAddr), connID)
@@ -132,7 +132,7 @@ func (c *BaseConnection) GetRemoteIP() string {
 // SetProtocol sets the protocol for this connection
 func (c *BaseConnection) SetProtocol(protocol string) {
 	c.protocol = protocol
-	if util.Contains(supportedProtocols, c.protocol) {
+	if slices.Contains(supportedProtocols, c.protocol) {
 		c.ID = fmt.Sprintf("%v_%v", c.protocol, c.ID)
 	}
 }

+ 2 - 1
internal/common/connection_test.go

@@ -22,6 +22,7 @@ import (
 	"path"
 	"path/filepath"
 	"runtime"
+	"slices"
 	"strconv"
 	"testing"
 	"time"
@@ -389,7 +390,7 @@ func TestErrorsMapping(t *testing.T) {
 		err := conn.GetFsError(fs, os.ErrNotExist)
 		if protocol == ProtocolSFTP {
 			assert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)
-		} else if util.Contains(osErrorsProtocols, protocol) {
+		} else if slices.Contains(osErrorsProtocols, protocol) {
 			assert.EqualError(t, err, os.ErrNotExist.Error())
 		} else {
 			assert.EqualError(t, err, ErrNotExist.Error())

+ 5 - 4
internal/common/eventmanager.go

@@ -31,6 +31,7 @@ import (
 	"os/exec"
 	"path"
 	"path/filepath"
+	"slices"
 	"strconv"
 	"strings"
 	"sync"
@@ -307,7 +308,7 @@ func (*eventRulesContainer) checkIPDLoginEventMatch(conditions *dataprovider.Eve
 }
 
 func (*eventRulesContainer) checkProviderEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {
-	if !util.Contains(conditions.ProviderEvents, params.Event) {
+	if !slices.Contains(conditions.ProviderEvents, params.Event) {
 		return false
 	}
 	if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
@@ -316,14 +317,14 @@ func (*eventRulesContainer) checkProviderEventMatch(conditions *dataprovider.Eve
 	if !checkEventConditionPatterns(params.Role, conditions.Options.RoleNames) {
 		return false
 	}
-	if len(conditions.Options.ProviderObjects) > 0 && !util.Contains(conditions.Options.ProviderObjects, params.ObjectType) {
+	if len(conditions.Options.ProviderObjects) > 0 && !slices.Contains(conditions.Options.ProviderObjects, params.ObjectType) {
 		return false
 	}
 	return true
 }
 
 func (*eventRulesContainer) checkFsEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {
-	if !util.Contains(conditions.FsEvents, params.Event) {
+	if !slices.Contains(conditions.FsEvents, params.Event) {
 		return false
 	}
 	if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
@@ -338,7 +339,7 @@ func (*eventRulesContainer) checkFsEventMatch(conditions *dataprovider.EventCond
 	if !checkEventConditionPatterns(params.VirtualPath, conditions.Options.FsPaths) {
 		return false
 	}
-	if len(conditions.Options.Protocols) > 0 && !util.Contains(conditions.Options.Protocols, params.Protocol) {
+	if len(conditions.Options.Protocols) > 0 && !slices.Contains(conditions.Options.Protocols, params.Protocol) {
 		return false
 	}
 	if params.Event == operationUpload || params.Event == operationDownload {

+ 35 - 34
internal/common/protocol_test.go

@@ -30,6 +30,7 @@ import (
 	"path"
 	"path/filepath"
 	"runtime"
+	"slices"
 	"strings"
 	"sync"
 	"testing"
@@ -3978,9 +3979,9 @@ func TestEventRule(t *testing.T) {
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		email := lastReceivedEmail.get()
 		assert.Len(t, email.To, 3)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
-		assert.True(t, util.Contains(email.To, "[email protected]"))
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "upload" from "%s" status OK`, user.Username))
 		// test the failure action, we download a file that exceeds the transfer quota limit
 		err = writeSFTPFileNoCheck(path.Join("subdir1", testFileName), 1*1024*1024+65535, client)
@@ -3999,9 +4000,9 @@ func TestEventRule(t *testing.T) {
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		email = lastReceivedEmail.get()
 		assert.Len(t, email.To, 3)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
-		assert.True(t, util.Contains(email.To, "[email protected]"))
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "download" from "%s" status KO`, user.Username))
 		assert.Contains(t, email.Data, `"download" failed`)
 		assert.Contains(t, email.Data, common.ErrReadQuotaExceeded.Error())
@@ -4019,7 +4020,7 @@ func TestEventRule(t *testing.T) {
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		email = lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: Failed "upload" from "%s"`, user.Username))
 		assert.Contains(t, email.Data, fmt.Sprintf(`action %q failed`, action1.Name))
 		// now test the download rule
@@ -4036,9 +4037,9 @@ func TestEventRule(t *testing.T) {
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		email = lastReceivedEmail.get()
 		assert.Len(t, email.To, 3)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
-		assert.True(t, util.Contains(email.To, "[email protected]"))
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: New "download" from "%s"`, user.Username))
 	}
 	// test upload action command with arguments
@@ -4079,9 +4080,9 @@ func TestEventRule(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 3)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
-	assert.True(t, util.Contains(email.To, "[email protected]"))
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, `Subject: New "delete" from "admin"`)
 	_, err = httpdtest.RemoveEventRule(rule3, http.StatusOK)
 	assert.NoError(t, err)
@@ -4236,7 +4237,7 @@ func TestEventRuleProviderEvents(t *testing.T) {
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		email := lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, `Subject: New "update" from "admin"`)
 	}
 	// now delete the script to generate an error
@@ -4251,7 +4252,7 @@ func TestEventRuleProviderEvents(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, `Subject: Failed "update" from "admin"`)
 	assert.Contains(t, email.Data, fmt.Sprintf("Object name: %s object type: folder", folder.Name))
 	lastReceivedEmail.reset()
@@ -5306,7 +5307,7 @@ func TestBackupAsAttachment(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent))
 	assert.Contains(t, email.Data, `Domain: example.com`)
 	assert.Contains(t, email.Data, "Content-Type: application/json")
@@ -5676,7 +5677,7 @@ func TestEventActionCompressQuotaErrors(t *testing.T) {
 		}, 3*time.Second, 100*time.Millisecond)
 		email := lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, `Subject: "Compress failed"`)
 		assert.Contains(t, email.Data, common.ErrQuotaExceeded.Error())
 		// update quota size so the user is already overquota
@@ -5691,7 +5692,7 @@ func TestEventActionCompressQuotaErrors(t *testing.T) {
 		}, 3*time.Second, 100*time.Millisecond)
 		email = lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, `Subject: "Compress failed"`)
 		assert.Contains(t, email.Data, common.ErrQuotaExceeded.Error())
 		// remove the path to compress to trigger an error for size estimation
@@ -5705,7 +5706,7 @@ func TestEventActionCompressQuotaErrors(t *testing.T) {
 		}, 3*time.Second, 100*time.Millisecond)
 		email = lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, `Subject: "Compress failed"`)
 		assert.Contains(t, email.Data, "unable to estimate archive size")
 	}
@@ -6041,7 +6042,7 @@ func TestEventActionEmailAttachments(t *testing.T) {
 			}, 1500*time.Millisecond, 100*time.Millisecond)
 			email := lastReceivedEmail.get()
 			assert.Len(t, email.To, 1)
-			assert.True(t, util.Contains(email.To, "[email protected]"))
+			assert.True(t, slices.Contains(email.To, "[email protected]"))
 			assert.Contains(t, email.Data, `Subject: "upload" from`)
 			assert.Contains(t, email.Data, "Content-Disposition: attachment")
 		}
@@ -6218,7 +6219,7 @@ func TestEventActionsRetentionReports(t *testing.T) {
 
 		email := lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "upload" from "%s"`, user.Username))
 		assert.Contains(t, email.Data, "Content-Disposition: attachment")
 		_, err = client.Stat(testDir)
@@ -6391,7 +6392,7 @@ func TestEventRuleFirstUploadDownloadActions(t *testing.T) {
 		}, 1500*time.Millisecond, 100*time.Millisecond)
 		email := lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "first-upload" from "%s"`, user.Username))
 		lastReceivedEmail.reset()
 		// a new upload will not produce a new notification
@@ -6414,7 +6415,7 @@ func TestEventRuleFirstUploadDownloadActions(t *testing.T) {
 		}, 1500*time.Millisecond, 100*time.Millisecond)
 		email = lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "first-download" from "%s"`, user.Username))
 		// download again
 		lastReceivedEmail.reset()
@@ -6510,7 +6511,7 @@ func TestEventRuleRenameEvent(t *testing.T) {
 		}, 1500*time.Millisecond, 100*time.Millisecond)
 		email := lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, "[email protected]"))
+		assert.True(t, slices.Contains(email.To, "[email protected]"))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "rename" from "%s"`, user.Username))
 		assert.Contains(t, email.Data, "Content-Type: text/html")
 		assert.Contains(t, email.Data, fmt.Sprintf("Target path %q", path.Join("/subdir", testFileName)))
@@ -6644,7 +6645,7 @@ func TestEventRuleIDPLogin(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, common.IDPLoginUser))
 	assert.Contains(t, email.Data, username)
 	assert.Contains(t, email.Data, custom1)
@@ -6708,7 +6709,7 @@ func TestEventRuleIDPLogin(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email = lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, common.IDPLoginAdmin))
 	assert.Contains(t, email.Data, username)
 	assert.Contains(t, email.Data, custom1)
@@ -6900,7 +6901,7 @@ func TestEventRuleEmailField(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, user.Email))
+	assert.True(t, slices.Contains(email.To, user.Email))
 	assert.Contains(t, email.Data, `Subject: "add" from "admin"`)
 
 	// if we add a user without email the notification will fail
@@ -6914,7 +6915,7 @@ func TestEventRuleEmailField(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email = lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, `no recipient addresses set`)
 
 	conn, client, err := getSftpClient(user)
@@ -6931,7 +6932,7 @@ func TestEventRuleEmailField(t *testing.T) {
 		}, 3000*time.Millisecond, 100*time.Millisecond)
 		email := lastReceivedEmail.get()
 		assert.Len(t, email.To, 1)
-		assert.True(t, util.Contains(email.To, user.Email))
+		assert.True(t, slices.Contains(email.To, user.Email))
 		assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "mkdir" from "%s"`, user.Username))
 	}
 
@@ -7038,7 +7039,7 @@ func TestEventRuleCertificate(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent))
 	assert.Contains(t, email.Data, "Content-Type: text/plain")
 	assert.Contains(t, email.Data, `Domain: example.com Timestamp`)
@@ -7058,7 +7059,7 @@ func TestEventRuleCertificate(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email = lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s KO"`, renewalEvent))
 	assert.Contains(t, email.Data, `Domain: example.com Timestamp`)
 	assert.Contains(t, email.Data, errRenew.Error())
@@ -7184,8 +7185,8 @@ func TestEventRuleIPBlocked(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 2)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, `Subject: New "IP Blocked"`)
 
 	err = dataprovider.DeleteEventRule(rule1.Name, "", "", "")
@@ -8357,7 +8358,7 @@ func TestSFTPLoopError(t *testing.T) {
 	}, 3000*time.Millisecond, 100*time.Millisecond)
 	email := lastReceivedEmail.get()
 	assert.Len(t, email.To, 1)
-	assert.True(t, util.Contains(email.To, "[email protected]"))
+	assert.True(t, slices.Contains(email.To, "[email protected]"))
 	assert.Contains(t, email.Data, `Subject: Failed action`)
 
 	user1.VirtualFolders[0].FsConfig.SFTPConfig.Password = kms.NewPlainSecret(defaultPassword)

+ 2 - 1
internal/common/ratelimiter.go

@@ -17,6 +17,7 @@ package common
 import (
 	"errors"
 	"fmt"
+	"slices"
 	"sort"
 	"sync"
 	"sync/atomic"
@@ -94,7 +95,7 @@ func (r *RateLimiterConfig) validate() error {
 	}
 	r.Protocols = util.RemoveDuplicates(r.Protocols, true)
 	for _, protocol := range r.Protocols {
-		if !util.Contains(rateLimiterProtocolValues, protocol) {
+		if !slices.Contains(rateLimiterProtocolValues, protocol) {
 			return fmt.Errorf("invalid protocol %q", protocol)
 		}
 	}

+ 3 - 2
internal/common/tlsutils.go

@@ -25,6 +25,7 @@ import (
 	"math/rand"
 	"os"
 	"path/filepath"
+	"slices"
 	"sync"
 
 	"github.com/drakkan/sftpgo/v2/internal/logger"
@@ -96,7 +97,7 @@ func (m *CertManager) loadCertificates() error {
 		}
 		logger.Debug(m.logSender, "", "TLS certificate %q successfully loaded, id %v", keyPair.Cert, keyPair.ID)
 		certs[keyPair.ID] = &newCert
-		if !util.Contains(m.monitorList, keyPair.Cert) {
+		if !slices.Contains(m.monitorList, keyPair.Cert) {
 			m.monitorList = append(m.monitorList, keyPair.Cert)
 		}
 	}
@@ -190,7 +191,7 @@ func (m *CertManager) LoadCRLs() error {
 
 		logger.Debug(m.logSender, "", "CRL %q successfully loaded", revocationList)
 		crls = append(crls, crl)
-		if !util.Contains(m.monitorList, revocationList) {
+		if !slices.Contains(m.monitorList, revocationList) {
 			m.monitorList = append(m.monitorList, revocationList)
 		}
 	}

+ 2 - 1
internal/config/config.go

@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -716,7 +717,7 @@ func checkOverrideDefaultSettings() {
 		}
 	}
 
-	if util.Contains(viper.AllKeys(), "mfa.totp") {
+	if slices.Contains(viper.AllKeys(), "mfa.totp") {
 		globalConf.MFAConfig.TOTP = nil
 	}
 }

+ 11 - 11
internal/config/config_test.go

@@ -19,6 +19,7 @@ import (
 	"encoding/json"
 	"os"
 	"path/filepath"
+	"slices"
 	"testing"
 
 	"github.com/sftpgo/sdk/kms"
@@ -36,7 +37,6 @@ import (
 	"github.com/drakkan/sftpgo/v2/internal/plugin"
 	"github.com/drakkan/sftpgo/v2/internal/sftpd"
 	"github.com/drakkan/sftpgo/v2/internal/smtp"
-	"github.com/drakkan/sftpgo/v2/internal/util"
 	"github.com/drakkan/sftpgo/v2/internal/webdavd"
 )
 
@@ -679,8 +679,8 @@ func TestPluginsFromEnv(t *testing.T) {
 	pluginConf := pluginsConf[0]
 	require.Equal(t, "notifier", pluginConf.Type)
 	require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
-	require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
-	require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
+	require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
+	require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
 	require.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)
 	require.Equal(t, "add", pluginConf.NotifierOptions.ProviderEvents[0])
 	require.Equal(t, "update", pluginConf.NotifierOptions.ProviderEvents[1])
@@ -729,8 +729,8 @@ func TestPluginsFromEnv(t *testing.T) {
 	pluginConf = pluginsConf[0]
 	require.Equal(t, "notifier", pluginConf.Type)
 	require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
-	require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
-	require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
+	require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
+	require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
 	require.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)
 	require.Equal(t, "add", pluginConf.NotifierOptions.ProviderEvents[0])
 	require.Equal(t, "update", pluginConf.NotifierOptions.ProviderEvents[1])
@@ -787,8 +787,8 @@ func TestRateLimitersFromEnv(t *testing.T) {
 	require.Equal(t, 2, limiters[0].Type)
 	protocols := limiters[0].Protocols
 	require.Len(t, protocols, 2)
-	require.True(t, util.Contains(protocols, common.ProtocolFTP))
-	require.True(t, util.Contains(protocols, common.ProtocolSSH))
+	require.True(t, slices.Contains(protocols, common.ProtocolFTP))
+	require.True(t, slices.Contains(protocols, common.ProtocolSSH))
 	require.True(t, limiters[0].GenerateDefenderEvents)
 	require.Equal(t, 50, limiters[0].EntriesSoftLimit)
 	require.Equal(t, 100, limiters[0].EntriesHardLimit)
@@ -799,10 +799,10 @@ func TestRateLimitersFromEnv(t *testing.T) {
 	require.Equal(t, 2, limiters[1].Type)
 	protocols = limiters[1].Protocols
 	require.Len(t, protocols, 4)
-	require.True(t, util.Contains(protocols, common.ProtocolFTP))
-	require.True(t, util.Contains(protocols, common.ProtocolSSH))
-	require.True(t, util.Contains(protocols, common.ProtocolWebDAV))
-	require.True(t, util.Contains(protocols, common.ProtocolHTTP))
+	require.True(t, slices.Contains(protocols, common.ProtocolFTP))
+	require.True(t, slices.Contains(protocols, common.ProtocolSSH))
+	require.True(t, slices.Contains(protocols, common.ProtocolWebDAV))
+	require.True(t, slices.Contains(protocols, common.ProtocolHTTP))
 	require.False(t, limiters[1].GenerateDefenderEvents)
 	require.Equal(t, 100, limiters[1].EntriesSoftLimit)
 	require.Equal(t, 150, limiters[1].EntriesHardLimit)

+ 3 - 2
internal/dataprovider/actions.go

@@ -21,6 +21,7 @@ import (
 	"net/url"
 	"os/exec"
 	"path/filepath"
+	"slices"
 	"strings"
 	"time"
 
@@ -78,8 +79,8 @@ func executeAction(operation, executor, ip, objectType, objectName, role string,
 	if config.Actions.Hook == "" {
 		return
 	}
-	if !util.Contains(config.Actions.ExecuteOn, operation) ||
-		!util.Contains(config.Actions.ExecuteFor, objectType) {
+	if !slices.Contains(config.Actions.ExecuteOn, operation) ||
+		!slices.Contains(config.Actions.ExecuteFor, objectType) {
 		return
 	}
 

+ 7 - 6
internal/dataprovider/admin.go

@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"net"
 	"os"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -96,7 +97,7 @@ func (c *AdminTOTPConfig) validate(username string) error {
 	if c.ConfigName == "" {
 		return util.NewValidationError("totp: config name is mandatory")
 	}
-	if !util.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
+	if !slices.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
 		return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
 	}
 	if c.Secret.IsEmpty() {
@@ -337,15 +338,15 @@ func (a *Admin) validatePermissions() error {
 			util.I18nErrorPermissionsRequired,
 		)
 	}
-	if util.Contains(a.Permissions, PermAdminAny) {
+	if slices.Contains(a.Permissions, PermAdminAny) {
 		a.Permissions = []string{PermAdminAny}
 	}
 	for _, perm := range a.Permissions {
-		if !util.Contains(validAdminPerms, perm) {
+		if !slices.Contains(validAdminPerms, perm) {
 			return util.NewValidationError(fmt.Sprintf("invalid permission: %q", perm))
 		}
 		if a.Role != "" {
-			if util.Contains(forbiddenPermsForRoleAdmins, perm) {
+			if slices.Contains(forbiddenPermsForRoleAdmins, perm) {
 				deniedPerms := strings.Join(forbiddenPermsForRoleAdmins, ",")
 				return util.NewI18nError(
 					util.NewValidationError(fmt.Sprintf("a role admin cannot have the following permissions: %q", deniedPerms)),
@@ -559,10 +560,10 @@ func (a *Admin) SetNilSecretsIfEmpty() {
 
 // HasPermission returns true if the admin has the specified permission
 func (a *Admin) HasPermission(perm string) bool {
-	if util.Contains(a.Permissions, PermAdminAny) {
+	if slices.Contains(a.Permissions, PermAdminAny) {
 		return true
 	}
-	return util.Contains(a.Permissions, perm)
+	return slices.Contains(a.Permissions, perm)
 }
 
 // GetAllowedIPAsString returns the allowed IP as comma separated string

+ 11 - 10
internal/dataprovider/bolt.go

@@ -25,6 +25,7 @@ import (
 	"fmt"
 	"net/netip"
 	"path/filepath"
+	"slices"
 	"sort"
 	"time"
 
@@ -3320,7 +3321,7 @@ func (p *BoltProvider) addAdminToRole(username, roleName string, bucket *bolt.Bu
 	if err != nil {
 		return err
 	}
-	if !util.Contains(role.Admins, username) {
+	if !slices.Contains(role.Admins, username) {
 		role.Admins = append(role.Admins, username)
 		buf, err := json.Marshal(role)
 		if err != nil {
@@ -3345,7 +3346,7 @@ func (p *BoltProvider) removeAdminFromRole(username, roleName string, bucket *bo
 	if err != nil {
 		return err
 	}
-	if util.Contains(role.Admins, username) {
+	if slices.Contains(role.Admins, username) {
 		var admins []string
 		for _, admin := range role.Admins {
 			if admin != username {
@@ -3375,7 +3376,7 @@ func (p *BoltProvider) addUserToRole(username, roleName string, bucket *bolt.Buc
 	if err != nil {
 		return err
 	}
-	if !util.Contains(role.Users, username) {
+	if !slices.Contains(role.Users, username) {
 		role.Users = append(role.Users, username)
 		buf, err := json.Marshal(role)
 		if err != nil {
@@ -3400,7 +3401,7 @@ func (p *BoltProvider) removeUserFromRole(username, roleName string, bucket *bol
 	if err != nil {
 		return err
 	}
-	if util.Contains(role.Users, username) {
+	if slices.Contains(role.Users, username) {
 		var users []string
 		for _, user := range role.Users {
 			if user != username {
@@ -3428,7 +3429,7 @@ func (p *BoltProvider) addRuleToActionMapping(ruleName, actionName string, bucke
 	if err != nil {
 		return err
 	}
-	if !util.Contains(action.Rules, ruleName) {
+	if !slices.Contains(action.Rules, ruleName) {
 		action.Rules = append(action.Rules, ruleName)
 		buf, err := json.Marshal(action)
 		if err != nil {
@@ -3450,7 +3451,7 @@ func (p *BoltProvider) removeRuleFromActionMapping(ruleName, actionName string,
 	if err != nil {
 		return err
 	}
-	if util.Contains(action.Rules, ruleName) {
+	if slices.Contains(action.Rules, ruleName) {
 		var rules []string
 		for _, r := range action.Rules {
 			if r != ruleName {
@@ -3477,7 +3478,7 @@ func (p *BoltProvider) addUserToGroupMapping(username, groupname string, bucket
 	if err != nil {
 		return err
 	}
-	if !util.Contains(group.Users, username) {
+	if !slices.Contains(group.Users, username) {
 		group.Users = append(group.Users, username)
 		buf, err := json.Marshal(group)
 		if err != nil {
@@ -3522,7 +3523,7 @@ func (p *BoltProvider) addAdminToGroupMapping(username, groupname string, bucket
 	if err != nil {
 		return err
 	}
-	if !util.Contains(group.Admins, username) {
+	if !slices.Contains(group.Admins, username) {
 		group.Admins = append(group.Admins, username)
 		buf, err := json.Marshal(group)
 		if err != nil {
@@ -3593,11 +3594,11 @@ func (p *BoltProvider) addRelationToFolderMapping(folderName string, user *User,
 		return err
 	}
 	updated := false
-	if user != nil && !util.Contains(folder.Users, user.Username) {
+	if user != nil && !slices.Contains(folder.Users, user.Username) {
 		folder.Users = append(folder.Users, user.Username)
 		updated = true
 	}
-	if group != nil && !util.Contains(folder.Groups, group.Name) {
+	if group != nil && !slices.Contains(folder.Groups, group.Name) {
 		folder.Groups = append(folder.Groups, group.Name)
 		updated = true
 	}

+ 6 - 5
internal/dataprovider/configs.go

@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"image/png"
 	"net/url"
+	"slices"
 
 	"golang.org/x/crypto/ssh"
 
@@ -105,7 +106,7 @@ func (c *SFTPDConfigs) validate() error {
 		if algo == ssh.CertAlgoRSAv01 {
 			continue
 		}
-		if !util.Contains(supportedHostKeyAlgos, algo) {
+		if !slices.Contains(supportedHostKeyAlgos, algo) {
 			return util.NewValidationError(fmt.Sprintf("unsupported host key algorithm %q", algo))
 		}
 		hostKeyAlgos = append(hostKeyAlgos, algo)
@@ -116,24 +117,24 @@ func (c *SFTPDConfigs) validate() error {
 		if algo == "diffie-hellman-group18-sha512" || algo == ssh.KeyExchangeDHGEXSHA256 {
 			continue
 		}
-		if !util.Contains(supportedKexAlgos, algo) {
+		if !slices.Contains(supportedKexAlgos, algo) {
 			return util.NewValidationError(fmt.Sprintf("unsupported KEX algorithm %q", algo))
 		}
 		kexAlgos = append(kexAlgos, algo)
 	}
 	c.KexAlgorithms = kexAlgos
 	for _, cipher := range c.Ciphers {
-		if !util.Contains(supportedCiphers, cipher) {
+		if !slices.Contains(supportedCiphers, cipher) {
 			return util.NewValidationError(fmt.Sprintf("unsupported cipher %q", cipher))
 		}
 	}
 	for _, mac := range c.MACs {
-		if !util.Contains(supportedMACs, mac) {
+		if !slices.Contains(supportedMACs, mac) {
 			return util.NewValidationError(fmt.Sprintf("unsupported MAC algorithm %q", mac))
 		}
 	}
 	for _, algo := range c.PublicKeyAlgos {
-		if !util.Contains(supportedPublicKeyAlgos, algo) {
+		if !slices.Contains(supportedPublicKeyAlgos, algo) {
 			return util.NewValidationError(fmt.Sprintf("unsupported public key algorithm %q", algo))
 		}
 	}

+ 33 - 32
internal/dataprovider/dataprovider.go

@@ -44,6 +44,7 @@ import (
 	"path/filepath"
 	"regexp"
 	"runtime"
+	"slices"
 	"strconv"
 	"strings"
 	"sync"
@@ -519,7 +520,7 @@ type Config struct {
 // GetShared returns the provider share mode.
 // This method is called before the provider is initialized
 func (c *Config) GetShared() int {
-	if !util.Contains(sharedProviders, c.Driver) {
+	if !slices.Contains(sharedProviders, c.Driver) {
 		return 0
 	}
 	return c.IsShared
@@ -885,7 +886,7 @@ func SetTempPath(fsPath string) {
 }
 
 func checkSharedMode() {
-	if !util.Contains(sharedProviders, config.Driver) {
+	if !slices.Contains(sharedProviders, config.Driver) {
 		config.IsShared = 0
 	}
 }
@@ -1714,7 +1715,7 @@ func IPListEntryExists(ipOrNet string, listType IPListType) (IPListEntry, error)
 
 // GetIPListEntries returns the IP list entries applying the specified criteria and search limit
 func GetIPListEntries(listType IPListType, filter, from, order string, limit int) ([]IPListEntry, error) {
-	if !util.Contains(supportedIPListType, listType) {
+	if !slices.Contains(supportedIPListType, listType) {
 		return nil, util.NewValidationError(fmt.Sprintf("invalid list type %d", listType))
 	}
 	return provider.getIPListEntries(listType, filter, from, order, limit)
@@ -2373,7 +2374,7 @@ func GetFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtua
 }
 
 func dumpUsers(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeUsers) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeUsers) {
 		users, err := provider.dumpUsers()
 		if err != nil {
 			return err
@@ -2384,7 +2385,7 @@ func dumpUsers(data *BackupData, scopes []string) error {
 }
 
 func dumpFolders(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeFolders) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeFolders) {
 		folders, err := provider.dumpFolders()
 		if err != nil {
 			return err
@@ -2395,7 +2396,7 @@ func dumpFolders(data *BackupData, scopes []string) error {
 }
 
 func dumpGroups(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeGroups) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeGroups) {
 		groups, err := provider.dumpGroups()
 		if err != nil {
 			return err
@@ -2406,7 +2407,7 @@ func dumpGroups(data *BackupData, scopes []string) error {
 }
 
 func dumpAdmins(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeAdmins) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeAdmins) {
 		admins, err := provider.dumpAdmins()
 		if err != nil {
 			return err
@@ -2417,7 +2418,7 @@ func dumpAdmins(data *BackupData, scopes []string) error {
 }
 
 func dumpAPIKeys(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeAPIKeys) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeAPIKeys) {
 		apiKeys, err := provider.dumpAPIKeys()
 		if err != nil {
 			return err
@@ -2428,7 +2429,7 @@ func dumpAPIKeys(data *BackupData, scopes []string) error {
 }
 
 func dumpShares(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeShares) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeShares) {
 		shares, err := provider.dumpShares()
 		if err != nil {
 			return err
@@ -2439,7 +2440,7 @@ func dumpShares(data *BackupData, scopes []string) error {
 }
 
 func dumpActions(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeActions) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeActions) {
 		actions, err := provider.dumpEventActions()
 		if err != nil {
 			return err
@@ -2450,7 +2451,7 @@ func dumpActions(data *BackupData, scopes []string) error {
 }
 
 func dumpRules(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeRules) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeRules) {
 		rules, err := provider.dumpEventRules()
 		if err != nil {
 			return err
@@ -2461,7 +2462,7 @@ func dumpRules(data *BackupData, scopes []string) error {
 }
 
 func dumpRoles(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeRoles) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeRoles) {
 		roles, err := provider.dumpRoles()
 		if err != nil {
 			return err
@@ -2472,7 +2473,7 @@ func dumpRoles(data *BackupData, scopes []string) error {
 }
 
 func dumpIPLists(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeIPLists) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeIPLists) {
 		ipLists, err := provider.dumpIPListEntries()
 		if err != nil {
 			return err
@@ -2483,7 +2484,7 @@ func dumpIPLists(data *BackupData, scopes []string) error {
 }
 
 func dumpConfigs(data *BackupData, scopes []string) error {
-	if len(scopes) == 0 || util.Contains(scopes, DumpScopeConfigs) {
+	if len(scopes) == 0 || slices.Contains(scopes, DumpScopeConfigs) {
 		configs, err := provider.getConfigs()
 		if err != nil {
 			return err
@@ -2787,7 +2788,7 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
 	if c.ConfigName == "" {
 		return util.NewValidationError("totp: config name is mandatory")
 	}
-	if !util.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
+	if !slices.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
 		return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
 	}
 	if c.Secret.IsEmpty() {
@@ -2803,7 +2804,7 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
 		return util.NewValidationError("totp: specify at least one protocol")
 	}
 	for _, protocol := range c.Protocols {
-		if !util.Contains(MFAProtocols, protocol) {
+		if !slices.Contains(MFAProtocols, protocol) {
 			return util.NewValidationError(fmt.Sprintf("totp: invalid protocol %q", protocol))
 		}
 	}
@@ -2836,7 +2837,7 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
 			return permissions, util.NewValidationError("invalid permissions")
 		}
 		for _, p := range perms {
-			if !util.Contains(ValidPerms, p) {
+			if !slices.Contains(ValidPerms, p) {
 				return permissions, util.NewValidationError(fmt.Sprintf("invalid permission: %q", p))
 			}
 		}
@@ -2850,7 +2851,7 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
 		if dir != cleanedDir && cleanedDir == "/" {
 			return permissions, util.NewValidationError(fmt.Sprintf("cannot set permissions for invalid subdirectory: %q is an alias for \"/\"", dir))
 		}
-		if util.Contains(perms, PermAny) {
+		if slices.Contains(perms, PermAny) {
 			permissions[cleanedDir] = []string{PermAny}
 		} else {
 			permissions[cleanedDir] = util.RemoveDuplicates(perms, false)
@@ -2926,7 +2927,7 @@ func validateFiltersPatternExtensions(baseFilters *sdk.BaseUserFilters) error {
 				util.I18nErrorFilePatternPathInvalid,
 			)
 		}
-		if util.Contains(filteredPaths, cleanedPath) {
+		if slices.Contains(filteredPaths, cleanedPath) {
 			return util.NewI18nError(
 				util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %q", f.Path)),
 				util.I18nErrorFilePatternDuplicated,
@@ -3045,13 +3046,13 @@ func validateFilterProtocols(filters *sdk.BaseUserFilters) error {
 		return util.NewValidationError("invalid denied_protocols")
 	}
 	for _, p := range filters.DeniedProtocols {
-		if !util.Contains(ValidProtocols, p) {
+		if !slices.Contains(ValidProtocols, p) {
 			return util.NewValidationError(fmt.Sprintf("invalid denied protocol %q", p))
 		}
 	}
 
 	for _, p := range filters.TwoFactorAuthProtocols {
-		if !util.Contains(MFAProtocols, p) {
+		if !slices.Contains(MFAProtocols, p) {
 			return util.NewValidationError(fmt.Sprintf("invalid two factor protocol %q", p))
 		}
 	}
@@ -3107,7 +3108,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
 		return util.NewValidationError("invalid denied_login_methods")
 	}
 	for _, loginMethod := range filters.DeniedLoginMethods {
-		if !util.Contains(ValidLoginMethods, loginMethod) {
+		if !slices.Contains(ValidLoginMethods, loginMethod) {
 			return util.NewValidationError(fmt.Sprintf("invalid login method: %q", loginMethod))
 		}
 	}
@@ -3115,7 +3116,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
 		return err
 	}
 	if filters.TLSUsername != "" {
-		if !util.Contains(validTLSUsernames, string(filters.TLSUsername)) {
+		if !slices.Contains(validTLSUsernames, string(filters.TLSUsername)) {
 			return util.NewValidationError(fmt.Sprintf("invalid TLS username: %q", filters.TLSUsername))
 		}
 	}
@@ -3125,7 +3126,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
 	}
 	filters.TLSCerts = certs
 	for _, opts := range filters.WebClient {
-		if !util.Contains(sdk.WebClientOptions, opts) {
+		if !slices.Contains(sdk.WebClientOptions, opts) {
 			return util.NewValidationError(fmt.Sprintf("invalid web client options %q", opts))
 		}
 	}
@@ -3193,19 +3194,19 @@ func validateAccessTimeFilters(filters *sdk.BaseUserFilters) error {
 }
 
 func validateCombinedUserFilters(user *User) error {
-	if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
+	if user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
 		return util.NewI18nError(
 			util.NewValidationError("two-factor authentication cannot be disabled for a user with an active configuration"),
 			util.I18nErrorDisableActive2FA,
 		)
 	}
-	if user.Filters.RequirePasswordChange && util.Contains(user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) {
+	if user.Filters.RequirePasswordChange && slices.Contains(user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) {
 		return util.NewI18nError(
 			util.NewValidationError("you cannot require password change and at the same time disallow it"),
 			util.I18nErrorPwdChangeConflict,
 		)
 	}
-	if len(user.Filters.TwoFactorAuthProtocols) > 0 && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
+	if len(user.Filters.TwoFactorAuthProtocols) > 0 && slices.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
 		return util.NewI18nError(
 			util.NewValidationError("you cannot require two-factor authentication and at the same time disallow it"),
 			util.I18nError2FAConflict,
@@ -3526,7 +3527,7 @@ func checkUserPasscode(user *User, password, protocol string) (string, error) {
 	if user.Filters.TOTPConfig.Enabled {
 		switch protocol {
 		case protocolFTP:
-			if util.Contains(user.Filters.TOTPConfig.Protocols, protocol) {
+			if slices.Contains(user.Filters.TOTPConfig.Protocols, protocol) {
 				// the TOTP passcode has six digits
 				pwdLen := len(password)
 				if pwdLen < 7 {
@@ -3732,7 +3733,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
 	if err := user.LoadAndApplyGroupSettings(); err != nil {
 		return 0, err
 	}
-	hasSecondFactor := user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH)
+	hasSecondFactor := user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH)
 	if !isPartialAuth || !hasSecondFactor {
 		answers, err := client("", "", []string{"Password: "}, []bool{false})
 		if err != nil {
@@ -3750,7 +3751,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
 }
 
 func checkKeyboardInteractiveSecondFactor(user *User, client ssh.KeyboardInteractiveChallenge, protocol string) (int, error) {
-	if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
+	if !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
 		return 1, nil
 	}
 	err := user.Filters.TOTPConfig.Secret.TryDecrypt()
@@ -3874,7 +3875,7 @@ func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, resp
 	}
 	if len(answers) == 1 && response.CheckPwd > 0 {
 		if response.CheckPwd == 2 {
-			if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
+			if !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
 				providerLog(logger.LevelInfo, "keyboard interactive auth error: unable to check TOTP passcode, TOTP is not enabled for user %q",
 					user.Username)
 				return answers, errors.New("TOTP not enabled for SSH protocol")
@@ -4640,7 +4641,7 @@ func getConfigPath(name, configDir string) string {
 }
 
 func checkReservedUsernames(username string) error {
-	if util.Contains(reservedUsers, username) {
+	if slices.Contains(reservedUsers, username) {
 		return util.NewValidationError("this username is reserved")
 	}
 	return nil

+ 14 - 13
internal/dataprovider/eventrule.go

@@ -23,6 +23,7 @@ import (
 	"net/http"
 	"path"
 	"path/filepath"
+	"slices"
 	"strings"
 	"time"
 
@@ -60,7 +61,7 @@ var (
 )
 
 func isActionTypeValid(action int) bool {
-	return util.Contains(supportedEventActions, action)
+	return slices.Contains(supportedEventActions, action)
 }
 
 func getActionTypeAsString(action int) string {
@@ -115,7 +116,7 @@ var (
 )
 
 func isEventTriggerValid(trigger int) bool {
-	return util.Contains(supportedEventTriggers, trigger)
+	return slices.Contains(supportedEventTriggers, trigger)
 }
 
 func getTriggerTypeAsString(trigger int) string {
@@ -169,7 +170,7 @@ var (
 )
 
 func isFilesystemActionValid(value int) bool {
-	return util.Contains(supportedFsActions, value)
+	return slices.Contains(supportedFsActions, value)
 }
 
 func getFsActionTypeAsString(value int) string {
@@ -380,7 +381,7 @@ func (c *EventActionHTTPConfig) validate(additionalData string) error {
 			return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP password: %v", err))
 		}
 	}
-	if !util.Contains(SupportedHTTPActionMethods, c.Method) {
+	if !slices.Contains(SupportedHTTPActionMethods, c.Method) {
 		return util.NewValidationError(fmt.Sprintf("unsupported HTTP method: %s", c.Method))
 	}
 	for _, kv := range c.QueryParameters {
@@ -1280,7 +1281,7 @@ func (a *EventAction) validateAssociation(trigger int, fsEvents []string) error
 		}
 		if trigger == EventTriggerFsEvent {
 			for _, ev := range fsEvents {
-				if !util.Contains(allowedSyncFsEvents, ev) {
+				if !slices.Contains(allowedSyncFsEvents, ev) {
 					return util.NewI18nError(
 						util.NewValidationError("sync execution is only supported for upload and pre-* events"),
 						util.I18nErrorEvSyncUnsupportedFs,
@@ -1361,12 +1362,12 @@ func (f *ConditionOptions) validate() error {
 	}
 
 	for _, p := range f.Protocols {
-		if !util.Contains(SupportedRuleConditionProtocols, p) {
+		if !slices.Contains(SupportedRuleConditionProtocols, p) {
 			return util.NewValidationError(fmt.Sprintf("unsupported rule condition protocol: %q", p))
 		}
 	}
 	for _, p := range f.ProviderObjects {
-		if !util.Contains(SupporteRuleConditionProviderObjects, p) {
+		if !slices.Contains(SupporteRuleConditionProviderObjects, p) {
 			return util.NewValidationError(fmt.Sprintf("unsupported provider object: %q", p))
 		}
 	}
@@ -1468,7 +1469,7 @@ func (c *EventConditions) validate(trigger int) error {
 			)
 		}
 		for _, ev := range c.FsEvents {
-			if !util.Contains(SupportedFsEvents, ev) {
+			if !slices.Contains(SupportedFsEvents, ev) {
 				return util.NewValidationError(fmt.Sprintf("unsupported fs event: %q", ev))
 			}
 		}
@@ -1488,7 +1489,7 @@ func (c *EventConditions) validate(trigger int) error {
 			)
 		}
 		for _, ev := range c.ProviderEvents {
-			if !util.Contains(SupportedProviderEvents, ev) {
+			if !slices.Contains(SupportedProviderEvents, ev) {
 				return util.NewValidationError(fmt.Sprintf("unsupported provider event: %q", ev))
 			}
 		}
@@ -1537,7 +1538,7 @@ func (c *EventConditions) validate(trigger int) error {
 		c.Options.MinFileSize = 0
 		c.Options.MaxFileSize = 0
 		c.Schedules = nil
-		if !util.Contains(supportedIDPLoginEvents, c.IDPLoginEvent) {
+		if !slices.Contains(supportedIDPLoginEvents, c.IDPLoginEvent) {
 			return util.NewValidationError(fmt.Sprintf("invalid Identity Provider login event %d", c.IDPLoginEvent))
 		}
 	default:
@@ -1690,7 +1691,7 @@ func (r *EventRule) validateMandatorySyncActions() error {
 		return nil
 	}
 	for _, ev := range r.Conditions.FsEvents {
-		if util.Contains(mandatorySyncFsEvents, ev) {
+		if slices.Contains(mandatorySyncFsEvents, ev) {
 			return util.NewI18nError(
 				util.NewValidationError(fmt.Sprintf("event %q requires at least a sync action", ev)),
 				util.I18nErrorRuleSyncActionRequired,
@@ -1708,7 +1709,7 @@ func (r *EventRule) checkIPBlockedAndCertificateActions() error {
 		ActionTypeDataRetentionCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck,
 		ActionTypeUserExpirationCheck}
 	for _, action := range r.Actions {
-		if util.Contains(unavailableActions, action.Type) {
+		if slices.Contains(unavailableActions, action.Type) {
 			return fmt.Errorf("action %q, type %q is not supported for event trigger %q",
 				action.Name, getActionTypeAsString(action.Type), getTriggerTypeAsString(r.Trigger))
 		}
@@ -1724,7 +1725,7 @@ func (r *EventRule) checkProviderEventActions(providerObjectType string) error {
 		ActionTypeDataRetentionCheck, ActionTypeFilesystem,
 		ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck}
 	for _, action := range r.Actions {
-		if util.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
+		if slices.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
 			return fmt.Errorf("action %q, type %q is only supported for provider user events",
 				action.Name, getActionTypeAsString(action.Type))
 		}

+ 2 - 1
internal/dataprovider/iplist.go

@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"net"
 	"net/netip"
+	"slices"
 	"strings"
 	"sync"
 	"sync/atomic"
@@ -85,7 +86,7 @@ var (
 
 // CheckIPListType returns an error if the provided IP list type is not valid
 func CheckIPListType(t IPListType) error {
-	if !util.Contains(supportedIPListType, t) {
+	if !slices.Contains(supportedIPListType, t) {
 		return util.NewValidationError(fmt.Sprintf("invalid list type %d", t))
 	}
 	return nil

+ 9 - 8
internal/dataprovider/memory.go

@@ -22,6 +22,7 @@ import (
 	"net/netip"
 	"os"
 	"path/filepath"
+	"slices"
 	"sort"
 	"sync"
 	"time"
@@ -1210,7 +1211,7 @@ func (p *MemoryProvider) addRuleToActionMapping(ruleName, actionName string) err
 	if err != nil {
 		return util.NewGenericError(fmt.Sprintf("action %q does not exist", actionName))
 	}
-	if !util.Contains(a.Rules, ruleName) {
+	if !slices.Contains(a.Rules, ruleName) {
 		a.Rules = append(a.Rules, ruleName)
 		p.dbHandle.actions[actionName] = a
 	}
@@ -1223,7 +1224,7 @@ func (p *MemoryProvider) removeRuleFromActionMapping(ruleName, actionName string
 		providerLog(logger.LevelWarn, "action %q does not exist, cannot remove from mapping", actionName)
 		return
 	}
-	if util.Contains(a.Rules, ruleName) {
+	if slices.Contains(a.Rules, ruleName) {
 		var rules []string
 		for _, r := range a.Rules {
 			if r != ruleName {
@@ -1240,7 +1241,7 @@ func (p *MemoryProvider) addAdminToGroupMapping(username, groupname string) erro
 	if err != nil {
 		return err
 	}
-	if !util.Contains(g.Admins, username) {
+	if !slices.Contains(g.Admins, username) {
 		g.Admins = append(g.Admins, username)
 		p.dbHandle.groups[groupname] = g
 	}
@@ -1283,7 +1284,7 @@ func (p *MemoryProvider) addUserToGroupMapping(username, groupname string) error
 	if err != nil {
 		return err
 	}
-	if !util.Contains(g.Users, username) {
+	if !slices.Contains(g.Users, username) {
 		g.Users = append(g.Users, username)
 		p.dbHandle.groups[groupname] = g
 	}
@@ -1313,7 +1314,7 @@ func (p *MemoryProvider) addAdminToRole(username, role string) error {
 	if err != nil {
 		return fmt.Errorf("%w: role %q does not exist", ErrForeignKeyViolated, role)
 	}
-	if !util.Contains(r.Admins, username) {
+	if !slices.Contains(r.Admins, username) {
 		r.Admins = append(r.Admins, username)
 		p.dbHandle.roles[role] = r
 	}
@@ -1347,7 +1348,7 @@ func (p *MemoryProvider) addUserToRole(username, role string) error {
 	if err != nil {
 		return fmt.Errorf("%w: role %q does not exist", ErrForeignKeyViolated, role)
 	}
-	if !util.Contains(r.Users, username) {
+	if !slices.Contains(r.Users, username) {
 		r.Users = append(r.Users, username)
 		p.dbHandle.roles[role] = r
 	}
@@ -1378,7 +1379,7 @@ func (p *MemoryProvider) addUserToFolderMapping(username, foldername string) err
 	if err != nil {
 		return util.NewGenericError(fmt.Sprintf("unable to get folder %q: %v", foldername, err))
 	}
-	if !util.Contains(f.Users, username) {
+	if !slices.Contains(f.Users, username) {
 		f.Users = append(f.Users, username)
 		p.dbHandle.vfolders[foldername] = f
 	}
@@ -1390,7 +1391,7 @@ func (p *MemoryProvider) addGroupToFolderMapping(name, foldername string) error
 	if err != nil {
 		return util.NewGenericError(fmt.Sprintf("unable to get folder %q: %v", foldername, err))
 	}
-	if !util.Contains(f.Groups, name) {
+	if !slices.Contains(f.Groups, name) {
 		f.Groups = append(f.Groups, name)
 		p.dbHandle.vfolders[foldername] = f
 	}

+ 2 - 1
internal/dataprovider/pgsql.go

@@ -24,6 +24,7 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"slices"
 	"strconv"
 	"strings"
 	"time"
@@ -305,7 +306,7 @@ func getPGSQLConnectionString(redactedPwd bool) string {
 		if config.DisableSNI {
 			connectionString += " sslsni=0"
 		}
-		if util.Contains(pgSQLTargetSessionAttrs, config.TargetSessionAttrs) {
+		if slices.Contains(pgSQLTargetSessionAttrs, config.TargetSessionAttrs) {
 			connectionString += fmt.Sprintf(" target_session_attrs='%s'", config.TargetSessionAttrs)
 		}
 	} else {

+ 28 - 27
internal/dataprovider/user.go

@@ -22,6 +22,7 @@ import (
 	"net"
 	"os"
 	"path"
+	"slices"
 	"strconv"
 	"strings"
 	"time"
@@ -844,20 +845,20 @@ func (u *User) HasPermissionsInside(virtualPath string) bool {
 // HasPerm returns true if the user has the given permission or any permission
 func (u *User) HasPerm(permission, path string) bool {
 	perms := u.GetPermissionsForPath(path)
-	if util.Contains(perms, PermAny) {
+	if slices.Contains(perms, PermAny) {
 		return true
 	}
-	return util.Contains(perms, permission)
+	return slices.Contains(perms, permission)
 }
 
 // HasAnyPerm returns true if the user has at least one of the given permissions
 func (u *User) HasAnyPerm(permissions []string, path string) bool {
 	perms := u.GetPermissionsForPath(path)
-	if util.Contains(perms, PermAny) {
+	if slices.Contains(perms, PermAny) {
 		return true
 	}
 	for _, permission := range permissions {
-		if util.Contains(perms, permission) {
+		if slices.Contains(perms, permission) {
 			return true
 		}
 	}
@@ -867,11 +868,11 @@ func (u *User) HasAnyPerm(permissions []string, path string) bool {
 // HasPerms returns true if the user has all the given permissions
 func (u *User) HasPerms(permissions []string, path string) bool {
 	perms := u.GetPermissionsForPath(path)
-	if util.Contains(perms, PermAny) {
+	if slices.Contains(perms, PermAny) {
 		return true
 	}
 	for _, permission := range permissions {
-		if !util.Contains(perms, permission) {
+		if !slices.Contains(perms, permission) {
 			return false
 		}
 	}
@@ -931,11 +932,11 @@ func (u *User) IsLoginMethodAllowed(loginMethod, protocol string) bool {
 	if len(u.Filters.DeniedLoginMethods) == 0 {
 		return true
 	}
-	if util.Contains(u.Filters.DeniedLoginMethods, loginMethod) {
+	if slices.Contains(u.Filters.DeniedLoginMethods, loginMethod) {
 		return false
 	}
 	if protocol == protocolSSH && loginMethod == LoginMethodPassword {
-		if util.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
+		if slices.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
 			return false
 		}
 	}
@@ -969,10 +970,10 @@ func (u *User) IsPartialAuth() bool {
 			method == SSHLoginMethodPassword {
 			continue
 		}
-		if method == LoginMethodPassword && util.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
+		if method == LoginMethodPassword && slices.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
 			continue
 		}
-		if !util.Contains(SSHMultiStepsLoginMethods, method) {
+		if !slices.Contains(SSHMultiStepsLoginMethods, method) {
 			return false
 		}
 	}
@@ -986,7 +987,7 @@ func (u *User) GetAllowedLoginMethods() []string {
 		if method == SSHLoginMethodPassword {
 			continue
 		}
-		if !util.Contains(u.Filters.DeniedLoginMethods, method) {
+		if !slices.Contains(u.Filters.DeniedLoginMethods, method) {
 			allowedMethods = append(allowedMethods, method)
 		}
 	}
@@ -1056,7 +1057,7 @@ func (u *User) IsFileAllowed(virtualPath string) (bool, int) {
 
 // CanManageMFA returns true if the user can add a multi-factor authentication configuration
 func (u *User) CanManageMFA() bool {
-	if util.Contains(u.Filters.WebClient, sdk.WebClientMFADisabled) {
+	if slices.Contains(u.Filters.WebClient, sdk.WebClientMFADisabled) {
 		return false
 	}
 	return len(mfa.GetAvailableTOTPConfigs()) > 0
@@ -1077,39 +1078,39 @@ func (u *User) skipExternalAuth() bool {
 
 // CanManageShares returns true if the user can add, update and list shares
 func (u *User) CanManageShares() bool {
-	return !util.Contains(u.Filters.WebClient, sdk.WebClientSharesDisabled)
+	return !slices.Contains(u.Filters.WebClient, sdk.WebClientSharesDisabled)
 }
 
 // CanResetPassword returns true if this user is allowed to reset its password
 func (u *User) CanResetPassword() bool {
-	return !util.Contains(u.Filters.WebClient, sdk.WebClientPasswordResetDisabled)
+	return !slices.Contains(u.Filters.WebClient, sdk.WebClientPasswordResetDisabled)
 }
 
 // CanChangePassword returns true if this user is allowed to change its password
 func (u *User) CanChangePassword() bool {
-	return !util.Contains(u.Filters.WebClient, sdk.WebClientPasswordChangeDisabled)
+	return !slices.Contains(u.Filters.WebClient, sdk.WebClientPasswordChangeDisabled)
 }
 
 // CanChangeAPIKeyAuth returns true if this user is allowed to enable/disable API key authentication
 func (u *User) CanChangeAPIKeyAuth() bool {
-	return !util.Contains(u.Filters.WebClient, sdk.WebClientAPIKeyAuthChangeDisabled)
+	return !slices.Contains(u.Filters.WebClient, sdk.WebClientAPIKeyAuthChangeDisabled)
 }
 
 // CanChangeInfo returns true if this user is allowed to change its info such as email and description
 func (u *User) CanChangeInfo() bool {
-	return !util.Contains(u.Filters.WebClient, sdk.WebClientInfoChangeDisabled)
+	return !slices.Contains(u.Filters.WebClient, sdk.WebClientInfoChangeDisabled)
 }
 
 // CanManagePublicKeys returns true if this user is allowed to manage public keys
 // from the WebClient. Used in WebClient UI
 func (u *User) CanManagePublicKeys() bool {
-	return !util.Contains(u.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
+	return !slices.Contains(u.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
 }
 
 // CanManageTLSCerts returns true if this user is allowed to manage TLS certificates
 // from the WebClient. Used in WebClient UI
 func (u *User) CanManageTLSCerts() bool {
-	return !util.Contains(u.Filters.WebClient, sdk.WebClientTLSCertChangeDisabled)
+	return !slices.Contains(u.Filters.WebClient, sdk.WebClientTLSCertChangeDisabled)
 }
 
 // CanUpdateProfile returns true if the user is allowed to update the profile.
@@ -1121,7 +1122,7 @@ func (u *User) CanUpdateProfile() bool {
 // CanAddFilesFromWeb returns true if the client can add files from the web UI.
 // The specified target is the directory where the files must be uploaded
 func (u *User) CanAddFilesFromWeb(target string) bool {
-	if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
+	if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
 		return false
 	}
 	return u.HasPerm(PermUpload, target) || u.HasPerm(PermOverwrite, target)
@@ -1130,7 +1131,7 @@ func (u *User) CanAddFilesFromWeb(target string) bool {
 // CanAddDirsFromWeb returns true if the client can add directories from the web UI.
 // The specified target is the directory where the new directory must be created
 func (u *User) CanAddDirsFromWeb(target string) bool {
-	if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
+	if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
 		return false
 	}
 	return u.HasPerm(PermCreateDirs, target)
@@ -1139,7 +1140,7 @@ func (u *User) CanAddDirsFromWeb(target string) bool {
 // CanRenameFromWeb returns true if the client can rename objects from the web UI.
 // The specified src and dest are the source and target directories for the rename.
 func (u *User) CanRenameFromWeb(src, dest string) bool {
-	if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
+	if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
 		return false
 	}
 	return u.HasAnyPerm(permsRenameAny, src) && u.HasAnyPerm(permsRenameAny, dest)
@@ -1148,7 +1149,7 @@ func (u *User) CanRenameFromWeb(src, dest string) bool {
 // CanDeleteFromWeb returns true if the client can delete objects from the web UI.
 // The specified target is the parent directory for the object to delete
 func (u *User) CanDeleteFromWeb(target string) bool {
-	if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
+	if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
 		return false
 	}
 	return u.HasAnyPerm(permsDeleteAny, target)
@@ -1157,7 +1158,7 @@ func (u *User) CanDeleteFromWeb(target string) bool {
 // CanCopyFromWeb returns true if the client can copy objects from the web UI.
 // The specified src and dest are the source and target directories for the copy.
 func (u *User) CanCopyFromWeb(src, dest string) bool {
-	if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
+	if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
 		return false
 	}
 	if !u.HasPerm(PermListItems, src) {
@@ -1217,7 +1218,7 @@ func (u *User) MustSetSecondFactor() bool {
 			return true
 		}
 		for _, p := range u.Filters.TwoFactorAuthProtocols {
-			if !util.Contains(u.Filters.TOTPConfig.Protocols, p) {
+			if !slices.Contains(u.Filters.TOTPConfig.Protocols, p) {
 				return true
 			}
 		}
@@ -1228,11 +1229,11 @@ func (u *User) MustSetSecondFactor() bool {
 // MustSetSecondFactorForProtocol returns true if the user must set a second factor authentication
 // for the specified protocol
 func (u *User) MustSetSecondFactorForProtocol(protocol string) bool {
-	if util.Contains(u.Filters.TwoFactorAuthProtocols, protocol) {
+	if slices.Contains(u.Filters.TwoFactorAuthProtocols, protocol) {
 		if !u.Filters.TOTPConfig.Enabled {
 			return true
 		}
-		if !util.Contains(u.Filters.TOTPConfig.Protocols, protocol) {
+		if !slices.Contains(u.Filters.TOTPConfig.Protocols, protocol) {
 			return true
 		}
 	}

+ 2 - 1
internal/ftpd/server.go

@@ -22,6 +22,7 @@ import (
 	"net"
 	"os"
 	"path/filepath"
+	"slices"
 
 	ftpserver "github.com/fclairamb/ftpserverlib"
 	"github.com/sftpgo/sdk/plugin/notifier"
@@ -361,7 +362,7 @@ func (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext
 			user.Username, user.HomeDir)
 		return nil, fmt.Errorf("cannot login user with invalid home dir: %q", user.HomeDir)
 	}
-	if util.Contains(user.Filters.DeniedProtocols, common.ProtocolFTP) {
+	if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolFTP) {
 		logger.Info(logSender, connectionID, "cannot login user %q, protocol FTP is not allowed", user.Username)
 		return nil, fmt.Errorf("protocol FTP is not allowed for user %q", user.Username)
 	}

+ 2 - 1
internal/httpd/api_mfa.go

@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -275,7 +276,7 @@ func saveUserTOTPConfig(username string, r *http.Request, recoveryCodes []datapr
 		return util.NewValidationError("two-factor authentication must be enabled")
 	}
 	for _, p := range userMerged.Filters.TwoFactorAuthProtocols {
-		if !util.Contains(user.Filters.TOTPConfig.Protocols, p) {
+		if !slices.Contains(user.Filters.TOTPConfig.Protocols, p) {
 			return util.NewValidationError(fmt.Sprintf("totp: the following protocols are required: %q",
 				strings.Join(userMerged.Filters.TwoFactorAuthProtocols, ", ")))
 		}

+ 6 - 5
internal/httpd/api_shares.go

@@ -22,6 +22,7 @@ import (
 	"net/url"
 	"os"
 	"path"
+	"slices"
 	"strings"
 	"time"
 
@@ -107,7 +108,7 @@ func addShare(w http.ResponseWriter, r *http.Request) {
 		share.Name = share.ShareID
 	}
 	if share.Password == "" {
-		if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
+		if slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
 			sendAPIResponse(w, r, nil, "You are not authorized to share files/folders without a password",
 				http.StatusForbidden)
 			return
@@ -155,7 +156,7 @@ func updateShare(w http.ResponseWriter, r *http.Request) {
 		updatedShare.Password = share.Password
 	}
 	if updatedShare.Password == "" {
-		if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
+		if slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
 			sendAPIResponse(w, r, nil, "You are not authorized to share files/folders without a password",
 				http.StatusForbidden)
 			return
@@ -434,7 +435,7 @@ func (s *httpdServer) getShareClaims(r *http.Request, shareID string) (*jwtToken
 	if tokenString == "" || invalidatedJWTTokens.Get(tokenString) {
 		return nil, errInvalidToken
 	}
-	if !util.Contains(token.Audience(), tokenAudienceWebShare) {
+	if !slices.Contains(token.Audience(), tokenAudienceWebShare) {
 		logger.Debug(logSender, "", "invalid token audience for share %q", shareID)
 		return nil, errInvalidToken
 	}
@@ -486,7 +487,7 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, v
 		renderError(err, "", statusCode)
 		return share, nil, err
 	}
-	if !util.Contains(validScopes, share.Scope) {
+	if !slices.Contains(validScopes, share.Scope) {
 		err := errors.New("invalid share scope")
 		renderError(util.NewI18nError(err, util.I18nErrorShareScope), "", http.StatusForbidden)
 		return share, nil, err
@@ -543,7 +544,7 @@ func getUserForShare(share dataprovider.Share) (dataprovider.User, error) {
 	if !user.CanManageShares() {
 		return user, util.NewI18nError(util.NewRecordNotFoundError("this share does not exist"), util.I18nError404Message)
 	}
-	if share.Password == "" && util.Contains(user.Filters.WebClient, sdk.WebClientShareNoPasswordDisabled) {
+	if share.Password == "" && slices.Contains(user.Filters.WebClient, sdk.WebClientShareNoPasswordDisabled) {
 		return user, util.NewI18nError(
 			fmt.Errorf("sharing without a password was disabled: %w", os.ErrPermission),
 			util.I18nError403Message,

+ 3 - 2
internal/httpd/api_utils.go

@@ -27,6 +27,7 @@ import (
 	"net/url"
 	"os"
 	"path"
+	"slices"
 	"strconv"
 	"strings"
 	"sync"
@@ -727,7 +728,7 @@ func updateLoginMetrics(user *dataprovider.User, loginMethod, ip string, err err
 }
 
 func checkHTTPClientUser(user *dataprovider.User, r *http.Request, connectionID string, checkSessions bool) error {
-	if util.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {
+	if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {
 		logger.Info(logSender, connectionID, "cannot login user %q, protocol HTTP is not allowed", user.Username)
 		return util.NewI18nError(
 			fmt.Errorf("protocol HTTP is not allowed for user %q", user.Username),
@@ -912,7 +913,7 @@ func isUserAllowedToResetPassword(r *http.Request, user *dataprovider.User) bool
 	if !user.CanResetPassword() {
 		return false
 	}
-	if util.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {
+	if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) {
 		return false
 	}
 	if !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolHTTP) {

+ 12 - 11
internal/httpd/auth_utils.go

@@ -18,6 +18,7 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"slices"
 	"time"
 
 	"github.com/go-chi/jwtauth/v5"
@@ -227,24 +228,24 @@ func (c *jwtTokenClaims) Decode(token map[string]any) {
 }
 
 func (c *jwtTokenClaims) isCriticalPermRemoved(permissions []string) bool {
-	if util.Contains(permissions, dataprovider.PermAdminAny) {
+	if slices.Contains(permissions, dataprovider.PermAdminAny) {
 		return false
 	}
-	if (util.Contains(c.Permissions, dataprovider.PermAdminManageAdmins) ||
-		util.Contains(c.Permissions, dataprovider.PermAdminAny)) &&
-		!util.Contains(permissions, dataprovider.PermAdminManageAdmins) &&
-		!util.Contains(permissions, dataprovider.PermAdminAny) {
+	if (slices.Contains(c.Permissions, dataprovider.PermAdminManageAdmins) ||
+		slices.Contains(c.Permissions, dataprovider.PermAdminAny)) &&
+		!slices.Contains(permissions, dataprovider.PermAdminManageAdmins) &&
+		!slices.Contains(permissions, dataprovider.PermAdminAny) {
 		return true
 	}
 	return false
 }
 
 func (c *jwtTokenClaims) hasPerm(perm string) bool {
-	if util.Contains(c.Permissions, dataprovider.PermAdminAny) {
+	if slices.Contains(c.Permissions, dataprovider.PermAdminAny) {
 		return true
 	}
 
-	return util.Contains(c.Permissions, perm)
+	return slices.Contains(c.Permissions, perm)
 }
 
 func (c *jwtTokenClaims) createToken(tokenAuth *jwtauth.JWTAuth, audience tokenAudience, ip string) (jwt.Token, string, error) {
@@ -458,7 +459,7 @@ func verifyCSRFToken(r *http.Request, csrfTokenAuth *jwtauth.JWTAuth) error {
 		return fmt.Errorf("unable to verify form token: %v", err)
 	}
 
-	if !util.Contains(token.Audience(), tokenAudienceCSRF) {
+	if !slices.Contains(token.Audience(), tokenAudienceCSRF) {
 		logger.Debug(logSender, "", "error validating CSRF token audience")
 		return errors.New("the form token is not valid")
 	}
@@ -495,7 +496,7 @@ func verifyLoginCookie(r *http.Request) error {
 		logger.Debug(logSender, "", "the login token has been invalidated")
 		return errInvalidToken
 	}
-	if !util.Contains(token.Audience(), tokenAudienceWebLogin) {
+	if !slices.Contains(token.Audience(), tokenAudienceWebLogin) {
 		logger.Debug(logSender, "", "the token with id %q is not valid for audience %q", token.JwtID(), tokenAudienceWebLogin)
 		return errInvalidToken
 	}
@@ -543,7 +544,7 @@ func verifyOAuth2Token(csrfTokenAuth *jwtauth.JWTAuth, tokenString, ip string) (
 		)
 	}
 
-	if !util.Contains(token.Audience(), tokenAudienceOAuth2) {
+	if !slices.Contains(token.Audience(), tokenAudienceOAuth2) {
 		logger.Debug(logSender, "", "error validating OAuth2 token audience")
 		return "", util.NewI18nError(errors.New("invalid OAuth2 state"), util.I18nOAuth2InvalidState)
 	}
@@ -563,7 +564,7 @@ func verifyOAuth2Token(csrfTokenAuth *jwtauth.JWTAuth, tokenString, ip string) (
 
 func validateIPForToken(token jwt.Token, ip string) error {
 	if tokenValidationMode != tokenValidationNoIPMatch {
-		if !util.Contains(token.Audience(), ip) {
+		if !slices.Contains(token.Audience(), ip) {
 			return errInvalidToken
 		}
 	}

+ 26 - 25
internal/httpd/httpd_test.go

@@ -36,6 +36,7 @@ import (
 	"path/filepath"
 	"regexp"
 	"runtime"
+	"slices"
 	"strconv"
 	"strings"
 	"sync"
@@ -1340,7 +1341,7 @@ func TestGroupSettingsOverride(t *testing.T) {
 	var folderNames []string
 	if assert.Len(t, user.VirtualFolders, 4) {
 		for _, f := range user.VirtualFolders {
-			if !util.Contains(folderNames, f.Name) {
+			if !slices.Contains(folderNames, f.Name) {
 				folderNames = append(folderNames, f.Name)
 			}
 			switch f.Name {
@@ -1348,7 +1349,7 @@ func TestGroupSettingsOverride(t *testing.T) {
 				assert.Equal(t, mappedPath1, f.MappedPath)
 				assert.Equal(t, 3, f.BaseVirtualFolder.FsConfig.OSConfig.ReadBufferSize)
 				assert.Equal(t, 5, f.BaseVirtualFolder.FsConfig.OSConfig.WriteBufferSize)
-				assert.True(t, util.Contains([]string{"/vdir1", "/vdir2"}, f.VirtualPath))
+				assert.True(t, slices.Contains([]string{"/vdir1", "/vdir2"}, f.VirtualPath))
 			case folderName2:
 				assert.Equal(t, mappedPath2, f.MappedPath)
 				assert.Equal(t, "/vdir3", f.VirtualPath)
@@ -2103,16 +2104,16 @@ func TestActionRuleRelations(t *testing.T) {
 	action1, _, err = httpdtest.GetEventActionByName(action1.Name, http.StatusOK)
 	assert.NoError(t, err)
 	assert.Len(t, action1.Rules, 1)
-	assert.True(t, util.Contains(action1.Rules, rule1.Name))
+	assert.True(t, slices.Contains(action1.Rules, rule1.Name))
 	action2, _, err = httpdtest.GetEventActionByName(action2.Name, http.StatusOK)
 	assert.NoError(t, err)
 	assert.Len(t, action2.Rules, 1)
-	assert.True(t, util.Contains(action2.Rules, rule2.Name))
+	assert.True(t, slices.Contains(action2.Rules, rule2.Name))
 	action3, _, err = httpdtest.GetEventActionByName(action3.Name, http.StatusOK)
 	assert.NoError(t, err)
 	assert.Len(t, action3.Rules, 2)
-	assert.True(t, util.Contains(action3.Rules, rule1.Name))
-	assert.True(t, util.Contains(action3.Rules, rule2.Name))
+	assert.True(t, slices.Contains(action3.Rules, rule1.Name))
+	assert.True(t, slices.Contains(action3.Rules, rule2.Name))
 	// referenced actions cannot be removed
 	_, err = httpdtest.RemoveEventAction(action1, http.StatusBadRequest)
 	assert.NoError(t, err)
@@ -2140,7 +2141,7 @@ func TestActionRuleRelations(t *testing.T) {
 	action3, _, err = httpdtest.GetEventActionByName(action3.Name, http.StatusOK)
 	assert.NoError(t, err)
 	assert.Len(t, action3.Rules, 1)
-	assert.True(t, util.Contains(action3.Rules, rule1.Name))
+	assert.True(t, slices.Contains(action3.Rules, rule1.Name))
 
 	_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
 	assert.NoError(t, err)
@@ -8912,7 +8913,7 @@ func TestBasicUserHandlingMock(t *testing.T) {
 	assert.Equal(t, user.MaxSessions, updatedUser.MaxSessions)
 	assert.Equal(t, user.UploadBandwidth, updatedUser.UploadBandwidth)
 	assert.Equal(t, 1, len(updatedUser.Permissions["/"]))
-	assert.True(t, util.Contains(updatedUser.Permissions["/"], dataprovider.PermAny))
+	assert.True(t, slices.Contains(updatedUser.Permissions["/"], dataprovider.PermAny))
 	req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+user.Username, nil)
 	setBearerForReq(req, token)
 	rr = executeRequest(req)
@@ -12140,7 +12141,7 @@ func TestUserPermissionsMock(t *testing.T) {
 	err = render.DecodeJSON(rr.Body, &updatedUser)
 	assert.NoError(t, err)
 	if val, ok := updatedUser.Permissions["/otherdir"]; ok {
-		assert.True(t, util.Contains(val, dataprovider.PermListItems))
+		assert.True(t, slices.Contains(val, dataprovider.PermListItems))
 		assert.Equal(t, 1, len(val))
 	} else {
 		assert.Fail(t, "expected dir not found in permissions")
@@ -21552,10 +21553,10 @@ func TestWebUserAddMock(t *testing.T) {
 	assert.Equal(t, 60, newUser.Filters.PasswordStrength)
 	assert.Greater(t, newUser.LastPasswordChange, int64(0))
 	assert.True(t, newUser.Filters.RequirePasswordChange)
-	assert.True(t, util.Contains(newUser.PublicKeys, testPubKey))
+	assert.True(t, slices.Contains(newUser.PublicKeys, testPubKey))
 	if val, ok := newUser.Permissions["/subdir"]; ok {
-		assert.True(t, util.Contains(val, dataprovider.PermListItems))
-		assert.True(t, util.Contains(val, dataprovider.PermDownload))
+		assert.True(t, slices.Contains(val, dataprovider.PermListItems))
+		assert.True(t, slices.Contains(val, dataprovider.PermDownload))
 	} else {
 		assert.Fail(t, "user permissions must contain /somedir", "actual: %v", newUser.Permissions)
 	}
@@ -21574,20 +21575,20 @@ func TestWebUserAddMock(t *testing.T) {
 		case "/dir1":
 			assert.Len(t, filter.DeniedPatterns, 1)
 			assert.Len(t, filter.AllowedPatterns, 1)
-			assert.True(t, util.Contains(filter.AllowedPatterns, "*.png"))
-			assert.True(t, util.Contains(filter.DeniedPatterns, "*.zip"))
+			assert.True(t, slices.Contains(filter.AllowedPatterns, "*.png"))
+			assert.True(t, slices.Contains(filter.DeniedPatterns, "*.zip"))
 			assert.Equal(t, sdk.DenyPolicyDefault, filter.DenyPolicy)
 		case "/dir2":
 			assert.Len(t, filter.DeniedPatterns, 1)
 			assert.Len(t, filter.AllowedPatterns, 2)
-			assert.True(t, util.Contains(filter.AllowedPatterns, "*.jpg"))
-			assert.True(t, util.Contains(filter.AllowedPatterns, "*.png"))
-			assert.True(t, util.Contains(filter.DeniedPatterns, "*.mkv"))
+			assert.True(t, slices.Contains(filter.AllowedPatterns, "*.jpg"))
+			assert.True(t, slices.Contains(filter.AllowedPatterns, "*.png"))
+			assert.True(t, slices.Contains(filter.DeniedPatterns, "*.mkv"))
 			assert.Equal(t, sdk.DenyPolicyHide, filter.DenyPolicy)
 		case "/dir3":
 			assert.Len(t, filter.DeniedPatterns, 1)
 			assert.Len(t, filter.AllowedPatterns, 0)
-			assert.True(t, util.Contains(filter.DeniedPatterns, "*.rar"))
+			assert.True(t, slices.Contains(filter.DeniedPatterns, "*.rar"))
 			assert.Equal(t, sdk.DenyPolicyDefault, filter.DenyPolicy)
 		}
 	}
@@ -21828,16 +21829,16 @@ func TestWebUserUpdateMock(t *testing.T) {
 	assert.Equal(t, 40, updateUser.Filters.PasswordStrength)
 	assert.True(t, updateUser.Filters.RequirePasswordChange)
 	if val, ok := updateUser.Permissions["/otherdir"]; ok {
-		assert.True(t, util.Contains(val, dataprovider.PermListItems))
-		assert.True(t, util.Contains(val, dataprovider.PermUpload))
+		assert.True(t, slices.Contains(val, dataprovider.PermListItems))
+		assert.True(t, slices.Contains(val, dataprovider.PermUpload))
 	} else {
 		assert.Fail(t, "user permissions must contains /otherdir", "actual: %v", updateUser.Permissions)
 	}
-	assert.True(t, util.Contains(updateUser.Filters.AllowedIP, "192.168.1.3/32"))
-	assert.True(t, util.Contains(updateUser.Filters.DeniedIP, "10.0.0.2/32"))
-	assert.True(t, util.Contains(updateUser.Filters.DeniedLoginMethods, dataprovider.SSHLoginMethodKeyboardInteractive))
-	assert.True(t, util.Contains(updateUser.Filters.DeniedProtocols, common.ProtocolFTP))
-	assert.True(t, util.Contains(updateUser.Filters.FilePatterns[0].DeniedPatterns, "*.zip"))
+	assert.True(t, slices.Contains(updateUser.Filters.AllowedIP, "192.168.1.3/32"))
+	assert.True(t, slices.Contains(updateUser.Filters.DeniedIP, "10.0.0.2/32"))
+	assert.True(t, slices.Contains(updateUser.Filters.DeniedLoginMethods, dataprovider.SSHLoginMethodKeyboardInteractive))
+	assert.True(t, slices.Contains(updateUser.Filters.DeniedProtocols, common.ProtocolFTP))
+	assert.True(t, slices.Contains(updateUser.Filters.FilePatterns[0].DeniedPatterns, "*.zip"))
 	assert.Len(t, updateUser.Filters.BandwidthLimits, 0)
 	assert.Len(t, updateUser.Filters.TLSCerts, 1)
 	req, err = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)

+ 6 - 5
internal/httpd/middleware.go

@@ -20,6 +20,7 @@ import (
 	"io/fs"
 	"net/http"
 	"net/url"
+	"slices"
 	"strings"
 
 	"github.com/go-chi/jwtauth/v5"
@@ -83,7 +84,7 @@ func validateJWTToken(w http.ResponseWriter, r *http.Request, audience tokenAudi
 	if err := checkPartialAuth(w, r, audience, token.Audience()); err != nil {
 		return err
 	}
-	if !util.Contains(token.Audience(), audience) {
+	if !slices.Contains(token.Audience(), audience) {
 		logger.Debug(logSender, "", "the token is not valid for audience %q", audience)
 		doRedirect("Your token audience is not valid", nil)
 		return errInvalidToken
@@ -113,7 +114,7 @@ func (s *httpdServer) validateJWTPartialToken(w http.ResponseWriter, r *http.Req
 		notFoundFunc(w, r, nil)
 		return errInvalidToken
 	}
-	if !util.Contains(token.Audience(), audience) {
+	if !slices.Contains(token.Audience(), audience) {
 		logger.Debug(logSender, "", "the partial token with id %q is not valid for audience %q", token.JwtID(), audience)
 		notFoundFunc(w, r, nil)
 		return errInvalidToken
@@ -331,7 +332,7 @@ func (s *httpdServer) verifyCSRFHeader(next http.Handler) http.Handler {
 			return
 		}
 
-		if !util.Contains(token.Audience(), tokenAudienceCSRF) {
+		if !slices.Contains(token.Audience(), tokenAudienceCSRF) {
 			logger.Debug(logSender, "", "error validating CSRF header token audience")
 			sendAPIResponse(w, r, errors.New("the token is not valid"), "", http.StatusForbidden)
 			return
@@ -571,11 +572,11 @@ func authenticateUserWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAu
 }
 
 func checkPartialAuth(w http.ResponseWriter, r *http.Request, audience string, tokenAudience []string) error {
-	if audience == tokenAudienceWebAdmin && util.Contains(tokenAudience, tokenAudienceWebAdminPartial) {
+	if audience == tokenAudienceWebAdmin && slices.Contains(tokenAudience, tokenAudienceWebAdminPartial) {
 		http.Redirect(w, r, webAdminTwoFactorPath, http.StatusFound)
 		return errInvalidToken
 	}
-	if audience == tokenAudienceWebClient && util.Contains(tokenAudience, tokenAudienceWebClientPartial) {
+	if audience == tokenAudienceWebClient && slices.Contains(tokenAudience, tokenAudienceWebClientPartial) {
 		http.Redirect(w, r, webClientTwoFactorPath, http.StatusFound)
 		return errInvalidToken
 	}

+ 2 - 1
internal/httpd/oidc.go

@@ -21,6 +21,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"slices"
 	"strings"
 	"time"
 
@@ -143,7 +144,7 @@ func (o *OIDC) initialize() error {
 	if o.RedirectBaseURL == "" {
 		return errors.New("oidc: redirect base URL cannot be empty")
 	}
-	if !util.Contains(o.Scopes, oidc.ScopeOpenID) {
+	if !slices.Contains(o.Scopes, oidc.ScopeOpenID) {
 		return fmt.Errorf("oidc: required scope %q is not set", oidc.ScopeOpenID)
 	}
 	if o.ClientSecretFile != "" {

+ 6 - 5
internal/httpd/server.go

@@ -26,6 +26,7 @@ import (
 	"net/url"
 	"path"
 	"path/filepath"
+	"slices"
 	"strings"
 	"time"
 
@@ -355,7 +356,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
 			util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials))
 		return
 	}
-	if !userMerged.Filters.TOTPConfig.Enabled || !util.Contains(userMerged.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
+	if !userMerged.Filters.TOTPConfig.Enabled || !slices.Contains(userMerged.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
 		s.renderClientTwoFactorPage(w, r, util.NewI18nError(
 			util.NewValidationError("two factory authentication is not enabled"), util.I18n2FADisabled))
 		return
@@ -423,7 +424,7 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
 		s.renderClientTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials))
 		return
 	}
-	if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
+	if !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
 		updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
 		s.renderClientTwoFactorPage(w, r, util.NewI18nError(common.ErrInternalFailure, util.I18n2FADisabled))
 		return
@@ -743,7 +744,7 @@ func (s *httpdServer) loginUser(
 	}
 
 	audience := tokenAudienceWebClient
-	if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) &&
+	if user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) &&
 		user.CanManageMFA() && !isSecondFactorAuth {
 		audience = tokenAudienceWebClientPartial
 	}
@@ -863,7 +864,7 @@ func (s *httpdServer) getUserToken(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
+	if user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) {
 		passcode := r.Header.Get(otpHeaderCode)
 		if passcode == "" {
 			logger.Debug(logSender, "", "TOTP enabled for user %q and not passcode provided, authentication refused", user.Username)
@@ -1009,7 +1010,7 @@ func (s *httpdServer) checkCookieExpiration(w http.ResponseWriter, r *http.Reque
 	if time.Until(token.Expiration()) > tokenRefreshThreshold {
 		return
 	}
-	if util.Contains(token.Audience(), tokenAudienceWebClient) {
+	if slices.Contains(token.Audience(), tokenAudienceWebClient) {
 		s.refreshClientToken(w, r, &tokenClaims)
 	} else {
 		s.refreshAdminToken(w, r, &tokenClaims)

+ 8 - 7
internal/httpd/webadmin.go

@@ -25,6 +25,7 @@ import (
 	"net/url"
 	"os"
 	"path/filepath"
+	"slices"
 	"sort"
 	"strconv"
 	"strings"
@@ -1488,13 +1489,13 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
 	filters.PasswordStrength = passwordStrength
 	filters.AccessTime = getAccessTimeRestrictionsFromPostFields(r)
 	hooks := r.Form["hooks"]
-	if util.Contains(hooks, "external_auth_disabled") {
+	if slices.Contains(hooks, "external_auth_disabled") {
 		filters.Hooks.ExternalAuthDisabled = true
 	}
-	if util.Contains(hooks, "pre_login_disabled") {
+	if slices.Contains(hooks, "pre_login_disabled") {
 		filters.Hooks.PreLoginDisabled = true
 	}
-	if util.Contains(hooks, "check_password_disabled") {
+	if slices.Contains(hooks, "check_password_disabled") {
 		filters.Hooks.CheckPasswordDisabled = true
 	}
 	filters.IsAnonymous = r.Form.Get("is_anonymous") != ""
@@ -2215,7 +2216,7 @@ func getFoldersRetentionFromPostFields(r *http.Request) ([]dataprovider.FolderRe
 			res = append(res, dataprovider.FolderRetention{
 				Path:            p,
 				Retention:       retention,
-				DeleteEmptyDirs: util.Contains(opts, "1"),
+				DeleteEmptyDirs: slices.Contains(opts, "1"),
 			})
 		}
 	}
@@ -2557,9 +2558,9 @@ func getEventRuleActionsFromPostFields(r *http.Request) []dataprovider.EventActi
 					},
 					Order: order + 1,
 					Options: dataprovider.EventActionOptions{
-						IsFailureAction: util.Contains(options, "1"),
-						StopOnFailure:   util.Contains(options, "2"),
-						ExecuteSync:     util.Contains(options, "3"),
+						IsFailureAction: slices.Contains(options, "1"),
+						StopOnFailure:   slices.Contains(options, "2"),
+						ExecuteSync:     slices.Contains(options, "3"),
 					},
 				})
 			}

+ 4 - 3
internal/httpd/webclient.go

@@ -27,6 +27,7 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"slices"
 	"strconv"
 	"strings"
 	"time"
@@ -1463,7 +1464,7 @@ func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Re
 	share.LastUseAt = 0
 	share.Username = claims.Username
 	if share.Password == "" {
-		if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
+		if slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
 			s.renderAddUpdateSharePage(w, r, share,
 				util.NewI18nError(util.NewValidationError("You are not allowed to share files/folders without password"), util.I18nErrorShareNoPwd),
 				true)
@@ -1532,7 +1533,7 @@ func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http
 		updatedShare.Password = share.Password
 	}
 	if updatedShare.Password == "" {
-		if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
+		if slices.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) {
 			s.renderAddUpdateSharePage(w, r, updatedShare,
 				util.NewI18nError(util.NewValidationError("You are not allowed to share files/folders without password"), util.I18nErrorShareNoPwd),
 				false)
@@ -2015,7 +2016,7 @@ func doCheckExist(w http.ResponseWriter, r *http.Request, connection *Connection
 		}
 		existing := make([]map[string]any, 0)
 		for _, info := range contents {
-			if util.Contains(filesList.Files, info.Name()) {
+			if slices.Contains(filesList.Files, info.Name()) {
 				res := make(map[string]any)
 				res["name"] = info.Name()
 				if info.IsDir() {

+ 25 - 25
internal/httpdtest/httpdtest.go

@@ -25,6 +25,7 @@ import (
 	"net/http"
 	"net/url"
 	"path"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -36,7 +37,6 @@ import (
 	"github.com/drakkan/sftpgo/v2/internal/httpclient"
 	"github.com/drakkan/sftpgo/v2/internal/httpd"
 	"github.com/drakkan/sftpgo/v2/internal/kms"
-	"github.com/drakkan/sftpgo/v2/internal/util"
 	"github.com/drakkan/sftpgo/v2/internal/version"
 	"github.com/drakkan/sftpgo/v2/internal/vfs"
 )
@@ -1679,7 +1679,7 @@ func checkEventConditionOptions(expected, actual dataprovider.ConditionOptions)
 		return errors.New("condition protocols mismatch")
 	}
 	for _, v := range expected.Protocols {
-		if !util.Contains(actual.Protocols, v) {
+		if !slices.Contains(actual.Protocols, v) {
 			return errors.New("condition protocols content mismatch")
 		}
 	}
@@ -1687,7 +1687,7 @@ func checkEventConditionOptions(expected, actual dataprovider.ConditionOptions)
 		return errors.New("condition provider objects mismatch")
 	}
 	for _, v := range expected.ProviderObjects {
-		if !util.Contains(actual.ProviderObjects, v) {
+		if !slices.Contains(actual.ProviderObjects, v) {
 			return errors.New("condition provider objects content mismatch")
 		}
 	}
@@ -1705,7 +1705,7 @@ func checkEventConditions(expected, actual dataprovider.EventConditions) error {
 		return errors.New("fs events mismatch")
 	}
 	for _, v := range expected.FsEvents {
-		if !util.Contains(actual.FsEvents, v) {
+		if !slices.Contains(actual.FsEvents, v) {
 			return errors.New("fs events content mismatch")
 		}
 	}
@@ -1713,7 +1713,7 @@ func checkEventConditions(expected, actual dataprovider.EventConditions) error {
 		return errors.New("provider events mismatch")
 	}
 	for _, v := range expected.ProviderEvents {
-		if !util.Contains(actual.ProviderEvents, v) {
+		if !slices.Contains(actual.ProviderEvents, v) {
 			return errors.New("provider events content mismatch")
 		}
 	}
@@ -1948,7 +1948,7 @@ func checkAdmin(expected, actual *dataprovider.Admin) error {
 		return errors.New("permissions mismatch")
 	}
 	for _, p := range expected.Permissions {
-		if !util.Contains(actual.Permissions, p) {
+		if !slices.Contains(actual.Permissions, p) {
 			return errors.New("permissions content mismatch")
 		}
 	}
@@ -1966,7 +1966,7 @@ func compareAdminFilters(expected, actual dataprovider.AdminFilters) error {
 		return errors.New("allow list mismatch")
 	}
 	for _, v := range expected.AllowList {
-		if !util.Contains(actual.AllowList, v) {
+		if !slices.Contains(actual.AllowList, v) {
 			return errors.New("allow list content mismatch")
 		}
 	}
@@ -2057,7 +2057,7 @@ func compareUserPermissions(expected map[string][]string, actual map[string][]st
 	for dir, perms := range expected {
 		if actualPerms, ok := actual[dir]; ok {
 			for _, v := range actualPerms {
-				if !util.Contains(perms, v) {
+				if !slices.Contains(perms, v) {
 					return errors.New("permissions contents mismatch")
 				}
 			}
@@ -2310,7 +2310,7 @@ func compareSFTPFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error
 		return errors.New("SFTPFs fingerprints mismatch")
 	}
 	for _, value := range actual.SFTPConfig.Fingerprints {
-		if !util.Contains(expected.SFTPConfig.Fingerprints, value) {
+		if !slices.Contains(expected.SFTPConfig.Fingerprints, value) {
 			return errors.New("SFTPFs fingerprints mismatch")
 		}
 	}
@@ -2401,27 +2401,27 @@ func checkEncryptedSecret(expected, actual *kms.Secret) error {
 
 func compareUserFilterSubStructs(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
 	for _, IPMask := range expected.AllowedIP {
-		if !util.Contains(actual.AllowedIP, IPMask) {
+		if !slices.Contains(actual.AllowedIP, IPMask) {
 			return errors.New("allowed IP contents mismatch")
 		}
 	}
 	for _, IPMask := range expected.DeniedIP {
-		if !util.Contains(actual.DeniedIP, IPMask) {
+		if !slices.Contains(actual.DeniedIP, IPMask) {
 			return errors.New("denied IP contents mismatch")
 		}
 	}
 	for _, method := range expected.DeniedLoginMethods {
-		if !util.Contains(actual.DeniedLoginMethods, method) {
+		if !slices.Contains(actual.DeniedLoginMethods, method) {
 			return errors.New("denied login methods contents mismatch")
 		}
 	}
 	for _, protocol := range expected.DeniedProtocols {
-		if !util.Contains(actual.DeniedProtocols, protocol) {
+		if !slices.Contains(actual.DeniedProtocols, protocol) {
 			return errors.New("denied protocols contents mismatch")
 		}
 	}
 	for _, options := range expected.WebClient {
-		if !util.Contains(actual.WebClient, options) {
+		if !slices.Contains(actual.WebClient, options) {
 			return errors.New("web client options contents mismatch")
 		}
 	}
@@ -2430,7 +2430,7 @@ func compareUserFilterSubStructs(expected sdk.BaseUserFilters, actual sdk.BaseUs
 		return errors.New("TLS certs mismatch")
 	}
 	for _, cert := range expected.TLSCerts {
-		if !util.Contains(actual.TLSCerts, cert) {
+		if !slices.Contains(actual.TLSCerts, cert) {
 			return errors.New("TLS certs content mismatch")
 		}
 	}
@@ -2527,7 +2527,7 @@ func checkFilterMatch(expected []string, actual []string) bool {
 		return false
 	}
 	for _, e := range expected {
-		if !util.Contains(actual, strings.ToLower(e)) {
+		if !slices.Contains(actual, strings.ToLower(e)) {
 			return false
 		}
 	}
@@ -2570,7 +2570,7 @@ func compareUserBandwidthLimitFilters(expected sdk.BaseUserFilters, actual sdk.B
 			return errors.New("bandwidth filters sources mismatch")
 		}
 		for _, source := range actual.BandwidthLimits[idx].Sources {
-			if !util.Contains(l.Sources, source) {
+			if !slices.Contains(l.Sources, source) {
 				return errors.New("bandwidth filters source mismatch")
 			}
 		}
@@ -2680,7 +2680,7 @@ func compareEventActionEmailConfigFields(expected, actual dataprovider.EventActi
 		return errors.New("email recipients mismatch")
 	}
 	for _, v := range expected.Recipients {
-		if !util.Contains(actual.Recipients, v) {
+		if !slices.Contains(actual.Recipients, v) {
 			return errors.New("email recipients content mismatch")
 		}
 	}
@@ -2688,7 +2688,7 @@ func compareEventActionEmailConfigFields(expected, actual dataprovider.EventActi
 		return errors.New("email bcc mismatch")
 	}
 	for _, v := range expected.Bcc {
-		if !util.Contains(actual.Bcc, v) {
+		if !slices.Contains(actual.Bcc, v) {
 			return errors.New("email bcc content mismatch")
 		}
 	}
@@ -2705,7 +2705,7 @@ func compareEventActionEmailConfigFields(expected, actual dataprovider.EventActi
 		return errors.New("email attachments mismatch")
 	}
 	for _, v := range expected.Attachments {
-		if !util.Contains(actual.Attachments, v) {
+		if !slices.Contains(actual.Attachments, v) {
 			return errors.New("email attachments content mismatch")
 		}
 	}
@@ -2720,7 +2720,7 @@ func compareEventActionFsCompressFields(expected, actual dataprovider.EventActio
 		return errors.New("fs compress paths mismatch")
 	}
 	for _, v := range expected.Paths {
-		if !util.Contains(actual.Paths, v) {
+		if !slices.Contains(actual.Paths, v) {
 			return errors.New("fs compress paths content mismatch")
 		}
 	}
@@ -2741,7 +2741,7 @@ func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionF
 		return errors.New("fs deletes mismatch")
 	}
 	for _, v := range expected.Deletes {
-		if !util.Contains(actual.Deletes, v) {
+		if !slices.Contains(actual.Deletes, v) {
 			return errors.New("fs deletes content mismatch")
 		}
 	}
@@ -2749,7 +2749,7 @@ func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionF
 		return errors.New("fs mkdirs mismatch")
 	}
 	for _, v := range expected.MkDirs {
-		if !util.Contains(actual.MkDirs, v) {
+		if !slices.Contains(actual.MkDirs, v) {
 			return errors.New("fs mkdir content mismatch")
 		}
 	}
@@ -2757,7 +2757,7 @@ func compareEventActionFsConfigFields(expected, actual dataprovider.EventActionF
 		return errors.New("fs exist mismatch")
 	}
 	for _, v := range expected.Exist {
-		if !util.Contains(actual.Exist, v) {
+		if !slices.Contains(actual.Exist, v) {
 			return errors.New("fs exist content mismatch")
 		}
 	}
@@ -2788,7 +2788,7 @@ func compareEventActionCmdConfigFields(expected, actual dataprovider.EventAction
 		return errors.New("cmd args mismatch")
 	}
 	for _, v := range expected.Args {
-		if !util.Contains(actual.Args, v) {
+		if !slices.Contains(actual.Args, v) {
 			return errors.New("cmd args content mismatch")
 		}
 	}

+ 3 - 3
internal/plugin/kms.go

@@ -17,6 +17,7 @@ package plugin
 import (
 	"fmt"
 	"path/filepath"
+	"slices"
 
 	"github.com/hashicorp/go-hclog"
 	"github.com/hashicorp/go-plugin"
@@ -25,7 +26,6 @@ import (
 
 	"github.com/drakkan/sftpgo/v2/internal/kms"
 	"github.com/drakkan/sftpgo/v2/internal/logger"
-	"github.com/drakkan/sftpgo/v2/internal/util"
 )
 
 var (
@@ -41,10 +41,10 @@ type KMSConfig struct {
 }
 
 func (c *KMSConfig) validate() error {
-	if !util.Contains(validKMSSchemes, c.Scheme) {
+	if !slices.Contains(validKMSSchemes, c.Scheme) {
 		return fmt.Errorf("invalid kms scheme: %v", c.Scheme)
 	}
-	if !util.Contains(validKMSEncryptedStatuses, c.EncryptedStatus) {
+	if !slices.Contains(validKMSEncryptedStatuses, c.EncryptedStatus) {
 		return fmt.Errorf("invalid kms encrypted status: %v", c.EncryptedStatus)
 	}
 	return nil

+ 4 - 4
internal/plugin/notifier.go

@@ -16,6 +16,7 @@ package plugin
 
 import (
 	"fmt"
+	"slices"
 	"sync"
 	"time"
 
@@ -24,7 +25,6 @@ import (
 	"github.com/sftpgo/sdk/plugin/notifier"
 
 	"github.com/drakkan/sftpgo/v2/internal/logger"
-	"github.com/drakkan/sftpgo/v2/internal/util"
 )
 
 // NotifierConfig defines configuration parameters for notifiers plugins
@@ -220,7 +220,7 @@ func (p *notifierPlugin) canQueueEvent(timestamp int64) bool {
 }
 
 func (p *notifierPlugin) notifyFsAction(event *notifier.FsEvent) {
-	if !util.Contains(p.config.NotifierOptions.FsEvents, event.Action) {
+	if !slices.Contains(p.config.NotifierOptions.FsEvents, event.Action) {
 		return
 	}
 
@@ -233,8 +233,8 @@ func (p *notifierPlugin) notifyFsAction(event *notifier.FsEvent) {
 }
 
 func (p *notifierPlugin) notifyProviderAction(event *notifier.ProviderEvent, object Renderer) {
-	if !util.Contains(p.config.NotifierOptions.ProviderEvents, event.Action) ||
-		!util.Contains(p.config.NotifierOptions.ProviderObjects, event.ObjectType) {
+	if !slices.Contains(p.config.NotifierOptions.ProviderEvents, event.Action) ||
+		!slices.Contains(p.config.NotifierOptions.ProviderObjects, event.ObjectType) {
 		return
 	}
 

+ 2 - 1
internal/plugin/plugin.go

@@ -24,6 +24,7 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"slices"
 	"strings"
 	"sync"
 	"sync/atomic"
@@ -336,7 +337,7 @@ func (m *Manager) NotifyLogEvent(event notifier.LogEventType, protocol, username
 	var e *notifier.LogEvent
 
 	for _, n := range m.notifiers {
-		if util.Contains(n.config.NotifierOptions.LogEvents, int(event)) {
+		if slices.Contains(n.config.NotifierOptions.LogEvents, int(event)) {
 			if e == nil {
 				message := ""
 				if err != nil {

+ 2 - 1
internal/service/service_portable.go

@@ -20,6 +20,7 @@ package service
 import (
 	"fmt"
 	"math/rand"
+	"slices"
 	"strings"
 
 	"github.com/sftpgo/sdk"
@@ -211,7 +212,7 @@ func configurePortableSFTPService(port int, enabledSSHCommands []string) {
 	} else {
 		sftpdConf.Bindings[0].Port = 0
 	}
-	if util.Contains(enabledSSHCommands, "*") {
+	if slices.Contains(enabledSSHCommands, "*") {
 		sftpdConf.EnabledSSHCommands = sftpd.GetSupportedSSHCommands()
 	} else {
 		sftpdConf.EnabledSSHCommands = enabledSSHCommands

+ 4 - 3
internal/sftpd/internal_test.go

@@ -24,6 +24,7 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
+	"slices"
 	"testing"
 	"time"
 
@@ -418,7 +419,7 @@ func TestSupportedSSHCommands(t *testing.T) {
 	assert.Equal(t, len(supportedSSHCommands), len(cmds))
 
 	for _, c := range cmds {
-		assert.True(t, util.Contains(supportedSSHCommands, c))
+		assert.True(t, slices.Contains(supportedSSHCommands, c))
 	}
 }
 
@@ -842,7 +843,7 @@ func TestRsyncOptions(t *testing.T) {
 	}
 	cmd, err := sshCmd.getSystemCommand()
 	assert.NoError(t, err)
-	assert.True(t, util.Contains(cmd.cmd.Args, "--safe-links"),
+	assert.True(t, slices.Contains(cmd.cmd.Args, "--safe-links"),
 		"--safe-links must be added if the user has the create symlinks permission")
 
 	permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs,
@@ -859,7 +860,7 @@ func TestRsyncOptions(t *testing.T) {
 	}
 	cmd, err = sshCmd.getSystemCommand()
 	assert.NoError(t, err)
-	assert.True(t, util.Contains(cmd.cmd.Args, "--munge-links"),
+	assert.True(t, slices.Contains(cmd.cmd.Args, "--munge-links"),
 		"--munge-links must be added if the user has the create symlinks permission")
 
 	sshCmd.connection.User.VirtualFolders = append(sshCmd.connection.User.VirtualFolders, vfs.VirtualFolder{

+ 17 - 16
internal/sftpd/server.go

@@ -26,6 +26,7 @@ import (
 	"os"
 	"path/filepath"
 	"runtime/debug"
+	"slices"
 	"strings"
 	"sync"
 	"time"
@@ -263,13 +264,13 @@ func (c *Configuration) getServerConfig() *ssh.ServerConfig {
 func (c *Configuration) updateSupportedAuthentications() {
 	serviceStatus.Authentications = util.RemoveDuplicates(serviceStatus.Authentications, false)
 
-	if util.Contains(serviceStatus.Authentications, dataprovider.LoginMethodPassword) &&
-		util.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey) {
+	if slices.Contains(serviceStatus.Authentications, dataprovider.LoginMethodPassword) &&
+		slices.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey) {
 		serviceStatus.Authentications = append(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyAndPassword)
 	}
 
-	if util.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyboardInteractive) &&
-		util.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey) {
+	if slices.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyboardInteractive) &&
+		slices.Contains(serviceStatus.Authentications, dataprovider.SSHLoginMethodPublicKey) {
 		serviceStatus.Authentications = append(serviceStatus.Authentications, dataprovider.SSHLoginMethodKeyAndKeyboardInt)
 	}
 }
@@ -422,7 +423,7 @@ func (c *Configuration) configureKeyAlgos(serverConfig *ssh.ServerConfig) error
 		c.HostKeyAlgorithms = util.RemoveDuplicates(c.HostKeyAlgorithms, true)
 	}
 	for _, hostKeyAlgo := range c.HostKeyAlgorithms {
-		if !util.Contains(supportedHostKeyAlgos, hostKeyAlgo) {
+		if !slices.Contains(supportedHostKeyAlgos, hostKeyAlgo) {
 			return fmt.Errorf("unsupported host key algorithm %q", hostKeyAlgo)
 		}
 	}
@@ -430,7 +431,7 @@ func (c *Configuration) configureKeyAlgos(serverConfig *ssh.ServerConfig) error
 	if len(c.PublicKeyAlgorithms) > 0 {
 		c.PublicKeyAlgorithms = util.RemoveDuplicates(c.PublicKeyAlgorithms, true)
 		for _, algo := range c.PublicKeyAlgorithms {
-			if !util.Contains(supportedPublicKeyAlgos, algo) {
+			if !slices.Contains(supportedPublicKeyAlgos, algo) {
 				return fmt.Errorf("unsupported public key authentication algorithm %q", algo)
 			}
 		}
@@ -472,7 +473,7 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
 			if kex == keyExchangeCurve25519SHA256LibSSH {
 				continue
 			}
-			if !util.Contains(supportedKexAlgos, kex) {
+			if !slices.Contains(supportedKexAlgos, kex) {
 				return fmt.Errorf("unsupported key-exchange algorithm %q", kex)
 			}
 		}
@@ -486,7 +487,7 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
 	if len(c.Ciphers) > 0 {
 		c.Ciphers = util.RemoveDuplicates(c.Ciphers, true)
 		for _, cipher := range c.Ciphers {
-			if !util.Contains(supportedCiphers, cipher) {
+			if !slices.Contains(supportedCiphers, cipher) {
 				return fmt.Errorf("unsupported cipher %q", cipher)
 			}
 		}
@@ -499,7 +500,7 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
 	if len(c.MACs) > 0 {
 		c.MACs = util.RemoveDuplicates(c.MACs, true)
 		for _, mac := range c.MACs {
-			if !util.Contains(supportedMACs, mac) {
+			if !slices.Contains(supportedMACs, mac) {
 				return fmt.Errorf("unsupported MAC algorithm %q", mac)
 			}
 		}
@@ -785,7 +786,7 @@ func loginUser(user *dataprovider.User, loginMethod, publicKey string, conn ssh.
 			user.Username, user.HomeDir)
 		return nil, fmt.Errorf("cannot login user with invalid home dir: %q", user.HomeDir)
 	}
-	if util.Contains(user.Filters.DeniedProtocols, common.ProtocolSSH) {
+	if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolSSH) {
 		logger.Info(logSender, connectionID, "cannot login user %q, protocol SSH is not allowed", user.Username)
 		return nil, fmt.Errorf("protocol SSH is not allowed for user %q", user.Username)
 	}
@@ -830,14 +831,14 @@ func loginUser(user *dataprovider.User, loginMethod, publicKey string, conn ssh.
 }
 
 func (c *Configuration) checkSSHCommands() {
-	if util.Contains(c.EnabledSSHCommands, "*") {
+	if slices.Contains(c.EnabledSSHCommands, "*") {
 		c.EnabledSSHCommands = GetSupportedSSHCommands()
 		return
 	}
 	sshCommands := []string{}
 	for _, command := range c.EnabledSSHCommands {
 		command = strings.TrimSpace(command)
-		if util.Contains(supportedSSHCommands, command) {
+		if slices.Contains(supportedSSHCommands, command) {
 			sshCommands = append(sshCommands, command)
 		} else {
 			logger.Warn(logSender, "", "unsupported ssh command: %q ignored", command)
@@ -927,7 +928,7 @@ func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error {
 func (c *Configuration) getHostKeyAlgorithms(keyFormat string) []string {
 	var algos []string
 	for _, algo := range algorithmsForKeyFormat(keyFormat) {
-		if util.Contains(c.HostKeyAlgorithms, algo) {
+		if slices.Contains(c.HostKeyAlgorithms, algo) {
 			algos = append(algos, algo)
 		}
 	}
@@ -986,7 +987,7 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
 				var algos []string
 				for _, algo := range algorithmsForKeyFormat(signer.PublicKey().Type()) {
 					if underlyingAlgo, ok := certKeyAlgoNames[algo]; ok {
-						if util.Contains(mas.Algorithms(), underlyingAlgo) {
+						if slices.Contains(mas.Algorithms(), underlyingAlgo) {
 							algos = append(algos, algo)
 						}
 					}
@@ -1098,12 +1099,12 @@ func (c *Configuration) initializeCertChecker(configDir string) error {
 
 func (c *Configuration) getPartialSuccessError(nextAuthMethods []string) error {
 	err := &ssh.PartialSuccessError{}
-	if c.PasswordAuthentication && util.Contains(nextAuthMethods, dataprovider.LoginMethodPassword) {
+	if c.PasswordAuthentication && slices.Contains(nextAuthMethods, dataprovider.LoginMethodPassword) {
 		err.Next.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
 			return c.validatePasswordCredentials(conn, password, dataprovider.SSHLoginMethodKeyAndPassword)
 		}
 	}
-	if c.KeyboardInteractiveAuthentication && util.Contains(nextAuthMethods, dataprovider.SSHLoginMethodKeyboardInteractive) {
+	if c.KeyboardInteractiveAuthentication && slices.Contains(nextAuthMethods, dataprovider.SSHLoginMethodKeyboardInteractive) {
 		err.Next.KeyboardInteractiveCallback = func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
 			return c.validateKeyboardInteractiveCredentials(conn, client, dataprovider.SSHLoginMethodKeyAndKeyboardInt, true)
 		}

+ 3 - 2
internal/sftpd/sftpd_test.go

@@ -38,6 +38,7 @@ import (
 	"path"
 	"path/filepath"
 	"runtime"
+	"slices"
 	"strconv"
 	"strings"
 	"sync"
@@ -8639,8 +8640,8 @@ func TestUserAllowedLoginMethods(t *testing.T) {
 	allowedMethods = user.GetAllowedLoginMethods()
 	assert.Equal(t, 4, len(allowedMethods))
 
-	assert.True(t, util.Contains(allowedMethods, dataprovider.SSHLoginMethodKeyAndKeyboardInt))
-	assert.True(t, util.Contains(allowedMethods, dataprovider.SSHLoginMethodKeyAndPassword))
+	assert.True(t, slices.Contains(allowedMethods, dataprovider.SSHLoginMethodKeyAndKeyboardInt))
+	assert.True(t, slices.Contains(allowedMethods, dataprovider.SSHLoginMethodKeyAndPassword))
 }
 
 func TestUserPartialAuth(t *testing.T) {

+ 6 - 5
internal/sftpd/ssh_cmd.go

@@ -27,6 +27,7 @@ import (
 	"os/exec"
 	"path"
 	"runtime/debug"
+	"slices"
 	"strings"
 	"sync"
 	"time"
@@ -91,7 +92,7 @@ func processSSHCommand(payload []byte, connection *Connection, enabledSSHCommand
 		name, args, err := parseCommandPayload(msg.Command)
 		connection.Log(logger.LevelDebug, "new ssh command: %q args: %v num args: %d user: %s, error: %v",
 			name, args, len(args), connection.User.Username, err)
-		if err == nil && util.Contains(enabledSSHCommands, name) {
+		if err == nil && slices.Contains(enabledSSHCommands, name) {
 			connection.command = msg.Command
 			if name == scpCmdName && len(args) >= 2 {
 				connection.SetProtocol(common.ProtocolSCP)
@@ -139,9 +140,9 @@ func (c *sshCommand) handle() (err error) {
 	defer common.Connections.Remove(c.connection.GetID())
 
 	c.connection.UpdateLastActivity()
-	if util.Contains(sshHashCommands, c.command) {
+	if slices.Contains(sshHashCommands, c.command) {
 		return c.handleHashCommands()
-	} else if util.Contains(systemCommands, c.command) {
+	} else if slices.Contains(systemCommands, c.command) {
 		command, err := c.getSystemCommand()
 		if err != nil {
 			return c.sendErrorResponse(err)
@@ -429,11 +430,11 @@ func (c *sshCommand) getSystemCommand() (systemCommand, error) {
 		// If the user cannot create symlinks we add the option --munge-links, if it is not
 		// already set. This should make symlinks unusable (but manually recoverable)
 		if c.connection.User.HasPerm(dataprovider.PermCreateSymlinks, c.getDestPath()) {
-			if !util.Contains(args, "--safe-links") {
+			if !slices.Contains(args, "--safe-links") {
 				args = append([]string{"--safe-links"}, args...)
 			}
 		} else {
-			if !util.Contains(args, "--munge-links") {
+			if !slices.Contains(args, "--munge-links") {
 				args = append([]string{"--munge-links"}, args...)
 			}
 		}

+ 2 - 2
internal/smtp/oauth2.go

@@ -19,6 +19,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"slices"
 	"sync"
 	"time"
 
@@ -27,7 +28,6 @@ import (
 	"golang.org/x/oauth2/microsoft"
 
 	"github.com/drakkan/sftpgo/v2/internal/logger"
-	"github.com/drakkan/sftpgo/v2/internal/util"
 )
 
 // Supported OAuth2 providers
@@ -56,7 +56,7 @@ type OAuth2Config struct {
 
 // Validate validates and initializes the configuration
 func (c *OAuth2Config) Validate() error {
-	if !util.Contains(supportedOAuth2Providers, c.Provider) {
+	if !slices.Contains(supportedOAuth2Providers, c.Provider) {
 		return fmt.Errorf("smtp oauth2: unsupported provider %d", c.Provider)
 	}
 	if c.ClientID == "" {

+ 0 - 10
internal/util/util.go

@@ -128,16 +128,6 @@ var bytesSizeTable = map[string]uint64{
 	"e":  eByte,
 }
 
-// Contains reports whether v is present in elems.
-func Contains[T comparable](elems []T, v T) bool {
-	for _, s := range elems {
-		if v == s {
-			return true
-		}
-	}
-	return false
-}
-
 // Remove removes an element from a string slice and
 // returns the modified slice
 func Remove(elems []string, val string) []string {

+ 2 - 2
internal/vfs/osfs.go

@@ -24,6 +24,7 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"slices"
 	"strings"
 	"time"
 
@@ -34,7 +35,6 @@ import (
 	"github.com/sftpgo/sdk"
 
 	"github.com/drakkan/sftpgo/v2/internal/logger"
-	"github.com/drakkan/sftpgo/v2/internal/util"
 )
 
 const (
@@ -475,7 +475,7 @@ func (fs *OsFs) findNonexistentDirs(filePath string) ([]string, error) {
 	for fs.IsNotExist(err) {
 		results = append(results, parent)
 		parent = filepath.Dir(parent)
-		if util.Contains(results, parent) {
+		if slices.Contains(results, parent) {
 			break
 		}
 		_, err = os.Stat(parent)

+ 2 - 1
internal/vfs/s3fs.go

@@ -30,6 +30,7 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"slices"
 	"sort"
 	"strings"
 	"sync"
@@ -161,7 +162,7 @@ func (fs *S3Fs) Stat(name string) (os.FileInfo, error) {
 	if err == nil {
 		// Some S3 providers (like SeaweedFS) remove the trailing '/' from object keys.
 		// So we check some common content types to detect if this is a "directory".
-		isDir := util.Contains(s3DirMimeTypes, util.GetStringFromPointer(obj.ContentType))
+		isDir := slices.Contains(s3DirMimeTypes, util.GetStringFromPointer(obj.ContentType))
 		if util.GetIntFromPointer(obj.ContentLength) == 0 && !isDir {
 			_, err = fs.headObject(name + "/")
 			isDir = err == nil

+ 4 - 3
internal/vfs/sftpfs.go

@@ -28,6 +28,7 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"slices"
 	"strconv"
 	"strings"
 	"sync"
@@ -125,7 +126,7 @@ func (c *SFTPFsConfig) isEqual(other SFTPFsConfig) bool {
 		return false
 	}
 	for _, fp := range c.Fingerprints {
-		if !util.Contains(other.Fingerprints, fp) {
+		if !slices.Contains(other.Fingerprints, fp) {
 			return false
 		}
 	}
@@ -954,12 +955,12 @@ func (c *sftpConnection) openConnNoLock() error {
 		User: c.config.Username,
 		HostKeyCallback: func(_ string, _ net.Addr, key ssh.PublicKey) error {
 			fp := ssh.FingerprintSHA256(key)
-			if util.Contains(sftpFingerprints, fp) {
+			if slices.Contains(sftpFingerprints, fp) {
 				if allowSelfConnections == 0 {
 					logger.Log(logger.LevelError, c.logSender, "", "SFTP self connections not allowed")
 					return ErrSFTPLoop
 				}
-				if util.Contains(c.config.forbiddenSelfUsernames, c.config.Username) {
+				if slices.Contains(c.config.forbiddenSelfUsernames, c.config.Username) {
 					logger.Log(logger.LevelError, c.logSender, "",
 						"SFTP loop or nested local SFTP folders detected, username %q, forbidden usernames: %+v",
 						c.config.Username, c.config.forbiddenSelfUsernames)

+ 2 - 1
internal/vfs/vfs.go

@@ -24,6 +24,7 @@ import (
 	"path"
 	"path/filepath"
 	"runtime"
+	"slices"
 	"strconv"
 	"strings"
 	"sync"
@@ -764,7 +765,7 @@ func (c *AzBlobFsConfig) validate() error {
 	if err := c.checkPartSizeAndConcurrency(); err != nil {
 		return err
 	}
-	if !util.Contains(validAzAccessTier, c.AccessTier) {
+	if !slices.Contains(validAzAccessTier, c.AccessTier) {
 		return fmt.Errorf("invalid access tier %q, valid values: \"''%v\"", c.AccessTier, strings.Join(validAzAccessTier, ", "))
 	}
 	return nil

+ 2 - 1
internal/webdavd/file.go

@@ -23,6 +23,7 @@ import (
 	"net/http"
 	"os"
 	"path"
+	"slices"
 	"sync/atomic"
 	"time"
 
@@ -447,7 +448,7 @@ func (f *webDavFile) Patch(patches []webdav.Proppatch) ([]webdav.Propstat, error
 		pstat := webdav.Propstat{}
 		for _, p := range patch.Props {
 			if status == http.StatusForbidden && !hasError {
-				if !patch.Remove && util.Contains(lastModifiedProps, p.XMLName.Local) {
+				if !patch.Remove && slices.Contains(lastModifiedProps, p.XMLName.Local) {
 					parsed, err := parseTime(util.BytesToString(p.InnerXML))
 					if err != nil {
 						f.Connection.Log(logger.LevelWarn, "unsupported last modification time: %q, err: %v",

+ 2 - 1
internal/webdavd/server.go

@@ -26,6 +26,7 @@ import (
 	"path"
 	"path/filepath"
 	"runtime/debug"
+	"slices"
 	"strings"
 	"time"
 
@@ -346,7 +347,7 @@ func (s *webDavServer) validateUser(user *dataprovider.User, r *http.Request, lo
 			user.Username, user.HomeDir)
 		return connID, fmt.Errorf("cannot login user with invalid home dir: %q", user.HomeDir)
 	}
-	if util.Contains(user.Filters.DeniedProtocols, common.ProtocolWebDAV) {
+	if slices.Contains(user.Filters.DeniedProtocols, common.ProtocolWebDAV) {
 		logger.Info(logSender, connectionID, "cannot login user %q, protocol DAV is not allowed", user.Username)
 		return connID, fmt.Errorf("protocol DAV is not allowed for user %q", user.Username)
 	}